[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\nmax_line_length = 80"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [JayceV552]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\nthanks_dev: # Replace with a single thanks.dev username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n\n- OS: [e.g. iOS]\n- Browser [e.g. chrome, safari]\n- Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n\n- Device: [e.g. iPhone6]\n- OS: [e.g. iOS8.1]\n- Browser [e.g. stock browser, safari]\n- Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy to GitHub Pages\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: 'pages'\n  cancel-in-progress: false\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n\n      - name: Install deps\n        run: |\n          cd website\n          npm install\n\n      - name: Build website\n        run: |\n          cd website\n          npm run build\n        env:\n          BASE_PATH: ''\n          NEXT_PUBLIC_BASE_PATH: ''\n          NEXT_PUBLIC_SITE_URL: https://calendar.dayflow.studio\n\n      - name: Disable Jekyll and verify output\n        run: |\n          ls -la website/\n          if [ -d \"website/out\" ]; then\n            touch website/out/.nojekyll\n            echo \"calendar.dayflow.studio\" > website/out/CNAME\n            echo \"Created .nojekyll and CNAME in website/out\"\n          else\n            echo \"Error: website/out directory not found!\"\n            exit 1\n          fi\n\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          path: website/out\n\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".gitignore",
    "content": "# Dependencies\nnode_modules\n.pnp\n.pnp.js\n\n# Testing\ncoverage\n\n# Next.js\nbuild\ndist\nwebsite/.next\nwebsite/out\n\n# Production\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Misc\n.DS_Store\n*.pem\n\n# Debug\n.vscode/*\n!.vscode/settings.json\n!.vscode/extensions.json\n!.zed/settings.json\n.idea\n\n# Local env files\n.env*.local\n\n# Vercel\n.vercel\n\n# TypeScript\n*.tsbuildinfo\nnext-env.d.ts\n\n# Turbo\n.turbo/\n\n/bundle-analysis.html\ntemp/\n*.tgz\n"
  },
  {
    "path": ".nojekyll",
    "content": ""
  },
  {
    "path": ".npmignore",
    "content": "# Source files\nsrc/\nwebsite/\nexamples/\n\n# Development files\n.git/\n.gitignore\n.vscode/\n.idea/\n\n# Build artifacts\n.next/\nnode_modules/\n\n# Logs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Configuration files (keep only essential ones)\ntsconfig.json\ntsconfig.build.json\nrollup.config.js\n\n# Documentation (except README)\n*.md\n!README.md\n\n# Test files\ntests/\n__tests__/\n*.test.*\n*.spec.*\ncoverage/\n\n# Environment files\n.env*\n\n# OS files\n.DS_Store\nThumbs.db\n\n# Temporary files\n*.tmp\n*.temp\n*.tgz\n\n# Source maps (not needed in published package)\n*.map\ndist/**/*.map"
  },
  {
    "path": ".oxfmtrc.jsonc",
    "content": "// https://oxc.rs/docs/guide/usage/formatter/config-file-reference.html\n\n{\n  \"$schema\": \"./node_modules/oxfmt/configuration_schema.json\",\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"quoteProps\": \"as-needed\",\n  \"jsxSingleQuote\": true,\n  \"trailingComma\": \"es5\",\n  \"bracketSpacing\": true,\n  \"bracketSameLine\": false,\n  \"arrowParens\": \"avoid\",\n  \"endOfLine\": \"lf\",\n  \"sortPackageJson\": {\n    \"sortScripts\": true,\n  },\n  \"sortImports\": {\n    \"ignoreCase\": true,\n    \"newlinesBetween\": true,\n    \"order\": \"asc\",\n  },\n  \"sortTailwindcss\": {\n    \"stylesheet\": \"./packages/core/src/styles/tailwind.css\",\n    \"attributes\": [\"class\", \"className\"],\n    \"functions\": [\"clsx\", \"cn\", \"cva\", \"tw\"],\n  },\n  \"ignorePatterns\": [\n    \"pnpm-lock.yaml\",\n    \"package-lock.json\",\n    \"yarn.lock\",\n    \"bun.lock\",\n    \"pnpm-workspace.yaml\",\n    \".turbo\",\n    \".cache\",\n    \".output\",\n    \"dist\",\n  ],\n}\n"
  },
  {
    "path": ".oxlintrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n  \"ignorePatterns\": [\"**/*.mdx\"],\n  \"plugins\": [\n    \"eslint\",\n    \"typescript\",\n    \"unicorn\",\n    \"oxc\",\n    \"import\",\n    \"jsdoc\",\n    \"node\",\n    \"promise\",\n    \"jest\",\n    \"vitest\",\n    \"react\",\n    \"react-perf\",\n    \"jsx-a11y\",\n    \"nextjs\",\n    \"vue\"\n  ],\n  \"env\": {\n    \"browser\": true\n  },\n  \"categories\": {\n    \"correctness\": \"error\",\n    \"perf\": \"error\",\n    \"restriction\": \"error\",\n    \"suspicious\": \"error\",\n    \"pedantic\": \"error\",\n    \"style\": \"off\"\n  },\n  \"rules\": {\n    \"complexity\": \"off\",\n    \"no-await-in-loop\": \"off\",\n    \"max-lines-per-function\": \"off\",\n    \"no-inline-comments\": \"off\",\n    \"no-implicit-coercion\": \"off\",\n    \"no-magic-numbers\": \"off\",\n    \"no-console\": \"off\",\n    \"no-ternary\": \"off\",\n    \"no-undefined\": \"off\",\n    \"no-unused-vars\": \"warn\",\n    \"max-lines\": \"off\",\n    \"id-length\": \"off\",\n    \"func-style\": \"off\",\n    \"max-statements\": \"off\",\n    \"no-plusplus\": \"off\",\n    \"arrow-body-style\": [\"error\", \"as-needed\"],\n    \"max-depth\": \"off\",\n    \"max-params\": \"off\",\n    \"capitalized-comments\": \"off\",\n    \"new-cap\": \"off\",\n    \"no-continue\": \"off\",\n    \"no-barrel-file\": \"off\",\n    \"init-declarations\": \"off\",\n    // Rely on oxfmt `sortImports` instead\n    \"sort-imports\": \"off\",\n    \"sort-keys\": \"off\",\n    \"no-duplicate-imports\": [\"error\", { \"allowSeparateTypeImports\": true }],\n\n    \"import/no-default-export\": \"off\",\n    \"import/exports-last\": \"off\",\n    \"import/no-named-export\": \"off\",\n    \"import/max-dependencies\": \"off\",\n    \"import/no-unresolved\": \"off\",\n    \"import/extensions\": \"off\",\n    \"import/no-namespace\": \"off\",\n    \"import/no-anonymous-default-export\": \"off\",\n    \"import/prefer-default-export\": \"off\",\n    \"import/group-exports\": \"off\",\n    \"import/no-commonjs\": \"off\",\n    \"import/no-duplicates\": \"error\",\n    \"import/unambiguous\": \"off\",\n    \"import/consistent-type-specifier-style\": [\"error\", \"prefer-top-level\"],\n    \"import/no-dynamic-require\": \"off\",\n    \"import/no-unassigned-import\": \"off\",\n    \"import/no-relative-parent-imports\": \"error\",\n\n    \"jsdoc/require-param\": \"off\",\n    \"jsdoc/require-returns\": \"off\",\n    \"jsdoc/require-param-type\": \"off\",\n    \"jsdoc/require-returns-type\": \"off\",\n\n    \"unicorn/explicit-length-check\": \"off\",\n    \"unicorn/no-array-callback-reference\": \"off\",\n    \"unicorn/no-process-exit\": \"off\",\n    \"unicorn/prefer-global-this\": \"off\",\n    \"unicorn/no-null\": \"off\",\n    \"unicorn/prefer-top-level-await\": \"off\",\n    \"unicorn/prefer-string-raw\": \"off\",\n    \"unicorn/filename-case\": \"off\",\n    \"unicorn/no-array-for-each\": \"off\",\n\n    \"typescript/explicit-module-boundary-types\": \"off\",\n    \"typescript/explicit-function-return-type\": \"off\",\n    \"typescript/no-var-requires\": \"off\",\n    \"typescript/no-unused-vars\": \"warn\",\n    \"typescript/no-non-null-assertion\": \"off\",\n    \"typescript/no-require-imports\": \"off\",\n    \"typescript/require-await\": \"off\",\n\n    \"node/no-process-env\": \"off\",\n\n    \"oxc/no-map-spread\": \"off\",\n    \"oxc/no-async-await\": \"off\",\n    \"oxc/no-rest-spread-properties\": \"off\",\n    \"oxc/no-optional-chaining\": \"off\",\n\n    \"promise/catch-or-return\": \"off\",\n    \"promise/always-return\": \"off\",\n\n    \"vitest/prefer-called-times\": \"off\",\n\n    \"react/jsx-max-depth\": \"off\",\n    \"react/jsx-boolean-value\": \"off\",\n    \"react/jsx-filename-extension\": \"off\",\n    \"react/jsx-props-no-spreading\": \"off\",\n    \"react/jsx-no-constructed-context-values\": \"warn\",\n    \"react/no-array-index-key\": \"off\",\n    \"react/no-multi-comp\": \"off\",\n    \"react/no-unknown-property\": \"off\",\n    \"react/react-in-jsx-scope\": \"off\",\n    \"react/only-export-components\": \"off\",\n\n    \"react_perf/jsx-no-jsx-as-prop\": \"off\",\n    \"react_perf/jsx-no-new-object-as-prop\": \"off\",\n    \"react_perf/jsx-no-new-array-as-prop\": \"off\",\n    \"react_perf/jsx-no-new-function-as-prop\": \"off\",\n\n    \"react-hooks/exhaustive-deps\": \"off\",\n\n    \"jsx-a11y/alt-text\": \"warn\",\n    \"jsx-a11y/click-events-have-key-events\": \"off\",\n    \"jsx-a11y/heading-has-content\": \"warn\",\n    \"jsx-a11y/no-autofocus\": \"off\",\n    \"jsx-a11y/no-static-element-interactions\": \"off\",\n\n    \"nextjs/no-img-element\": \"warn\"\n  },\n  \"overrides\": [\n    {\n      \"files\": [\"**/next-env.d.ts\"],\n      \"rules\": {\n        \"import/no-unassigned-import\": \"off\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"oxc.oxc-vscode\",\n    \"bradlc.vscode-tailwindcss\",\n    \"fill-labs.dependi\",\n    \"svelte.svelte-vscode\",\n    \"antfu.pnpm-catalog-lens\",\n    \"vue.volar\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"files.eol\": \"\\n\",\n\n  // Suppress CSS language-server warnings for Tailwind-specific at-rules\n  // (@apply, @variant, @theme, @source) that are valid PostCSS/Tailwind\n  // directives but unknown to the standard VS Code CSS validator.\n  \"css.lint.unknownAtRules\": \"ignore\",\n\n  \"prettier.enable\": false,\n  // \"eslint.enable\": false,\n  \"oxc.enable\": true,\n\n  \"editor.tabSize\": 2,\n  \"editor.formatOnSave\": true,\n  \"editor.formatOnPaste\": true,\n  \"editor.defaultFormatter\": \"oxc.oxc-vscode\",\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.oxc\": \"explicit\"\n  },\n\n  \"files.watcherExclude\": {\n    \"**/node_modules/**\": true,\n    \"**/dist/**\": true,\n    \"**/build/**\": true,\n    \"**/coverage/**\": true\n  },\n  \"search.exclude\": {\n    \"**/node_modules/**\": true,\n    \"**/dist/**\": true,\n    \"**/build/**\": true,\n    \"**/coverage/**\": true\n  },\n\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[javascriptreact]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[json]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[jsonc]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[css]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[html]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[vue]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[vue-html]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[markdown]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[yaml]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  },\n  \"[tailwindcss]\": {\n    \"editor.defaultFormatter\": \"oxc.oxc-vscode\"\n  }\n}\n"
  },
  {
    "path": ".zed/settings.json",
    "content": "{\n  \"formatter\": \"language_server\",\n  \"format_on_save\": \"on\",\n\n  \"languages\": {\n    \"JavaScript\": {\n      \"formatter\": {\n        \"language_server\": {\n          \"name\": \"oxfmt\"\n        }\n      },\n      \"code_actions_on_format\": {\n        \"source.fixAll.oxc\": true,\n        \"source.organizeImports.oxc\": true\n      }\n    },\n    \"TypeScript\": {\n      \"formatter\": {\n        \"language_server\": {\n          \"name\": \"oxfmt\"\n        }\n      },\n      \"code_actions_on_format\": {\n        \"source.fixAll.oxc\": true,\n        \"source.organizeImports.oxc\": true\n      }\n    },\n    \"TSX\": {\n      \"formatter\": {\n        \"language_server\": {\n          \"name\": \"oxfmt\"\n        }\n      },\n      \"code_actions_on_format\": {\n        \"source.fixAll.oxc\": true,\n        \"source.organizeImports.oxc\": true\n      }\n    },\n    \"JSON\": {\n      \"formatter\": {\n        \"language_server\": {\n          \"name\": \"oxfmt\"\n        }\n      },\n      \"code_actions_on_format\": {\n        \"source.fixAll.oxc\": true,\n        \"source.organizeImports.oxc\": true\n      }\n    },\n    \"JSONC\": {\n      \"formatter\": {\n        \"language_server\": {\n          \"name\": \"oxfmt\"\n        }\n      },\n      \"code_actions_on_format\": {\n        \"source.fixAll.oxc\": true,\n        \"source.organizeImports.oxc\": true\n      }\n    },\n    \"CSS\": {\n      \"formatter\": {\n        \"language_server\": {\n          \"name\": \"oxfmt\"\n        }\n      },\n      \"code_actions_on_format\": {\n        \"source.fixAll.oxc\": true,\n        \"source.organizeImports.oxc\": true\n      }\n    },\n    \"HTML\": {\n      \"formatter\": {\n        \"language_server\": {\n          \"name\": \"oxfmt\"\n        }\n      },\n      \"code_actions_on_format\": {\n        \"source.fixAll.oxc\": true,\n        \"source.organizeImports.oxc\": true\n      }\n    },\n    \"Markdown\": {\n      \"formatter\": {\n        \"language_server\": {\n          \"name\": \"oxfmt\"\n        }\n      },\n      \"code_actions_on_format\": {\n        \"source.fixAll.oxc\": true,\n        \"source.organizeImports.oxc\": true\n      }\n    },\n    \"YAML\": {\n      \"formatter\": {\n        \"language_server\": {\n          \"name\": \"oxfmt\"\n        }\n      },\n      \"code_actions_on_format\": {\n        \"source.fixAll.oxc\": true,\n        \"source.organizeImports.oxc\": true\n      }\n    }\n  },\n\n  \"lsp\": {\n    \"typescript-language-server\": {\n      \"settings\": {\n        \"typescript\": {\n          \"preferences\": {\n            \"includePackageJsonAutoImports\": \"on\"\n          }\n        }\n      }\n    },\n    \"oxlint\": {\n      \"initialization_options\": {\n        \"settings\": {\n          \"disableNestedConfig\": false,\n          \"fixKind\": \"safe_fix\",\n          \"run\": \"onType\",\n          \"typeAware\": true,\n          \"unusedDisableDirectives\": \"deny\"\n        }\n      }\n    },\n    \"oxfmt\": {\n      \"initialization_options\": {\n        \"settings\": {\n          \"configPath\": null,\n          \"flags\": {},\n          \"fmt.configPath\": null,\n          \"fmt.experimental\": true,\n          \"run\": \"onSave\",\n          \"typeAware\": false,\n          \"unusedDisableDirectives\": false\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## [3.6.0] - 2026-05-03\n\n### New Features & Enhancements\n\n- **Agenda List View**: Added a new `createAgendaView` factory for a scrollable agenda-style list layout. Events are grouped by day with configurable options including `daysToShow`, `showEmptyDays`, and `timeFormat` (12h/24h). Supports both timed and all-day events, multi-day event continuation indicators, and full Content Slot integration.\n- **Grid Date Click Callbacks**: Added `gridDateClick` and `gridDateDoubleClick` callbacks to Month and Week views, enabling click and double-click handlers on empty date/time grid cells.\n- **Mobile Event Detail as Content Slot**: Migrated the internal `MobileEventDetailComponent` to the Content Slot system, making the mobile event detail panel fully customizable via framework render props.\n- **Keyboard Shortcuts Callbacks**: Added lifecycle callbacks to the keyboard-shortcuts plugin (`onKeyDown`, `onKeyUp`, and per-action hooks) for programmatic integration and custom shortcut handling.\n- **Sidebar `onReorder` Callback**: Added an `onReorder` callback to the sidebar plugin, fired whenever the user drags calendars to reorder them in the list.\n\n### Fixed\n\n- **Range Picker `startOfWeek` Prop**: Added `startOfWeek` configuration to the range picker component so the calendar grid respects the locale or app-level week start setting.\n- **`useEventDetailPanel` API**: Moved `useEventDetailPanel` into `useCalendarApp`, making the event detail panel controls accessible from the same unified app hook across React, Vue, Angular, and Svelte.\n- **Framework Adapter Rendering**: Updated core rendering and framework-specific adapters (Vue, Angular, Svelte) to align with the Content Slot architecture introduced in v3.5.0.\n\n### Performance\n\n- **View Rendering for Large Datasets**: Significantly optimized event layout calculations across Month, Week, Day, and Year views for calendars with large numbers of events.\n- **Plugin Bundle Size**: Reduced bundle size for the drag, keyboard-shortcuts, localization, sidebar, and UI packages by streamlining rollup configurations.\n- **Bundle Shrink & CSS Isolation**: Reduced the core bundle footprint and eliminated residual CSS conflicts by refining the build pipeline and library import boundaries.\n\n## [3.5.0] - 2026-04-20\n\n### New Features & Enhancements\n\n- **Multi-calendar Events**: Added native `calendarIds` support on events. A single event can now belong to multiple calendars, stays visible as long as any linked calendar is visible, and renders with DayFlow's built-in multi-color striped styling.\n\n### Fixed\n\n- **Style Self-Containment**: Completed the style refactor so DayFlow no longer depends on internal atomic CSS classes or host-side `@source` scanning. `styles.css` and `styles.components.css` are now self-contained distribution artifacts.\n- **Host Style Isolation**: Tightened `df-*` semantic styling contracts across core, UI packages, plugins, and dist CSS checks to reduce conflicts with UnoCSS, Tailwind, and other host styling systems.\n- **Year / Day / Week View Polish**: Refined several view-specific layout details, including year-view event rendering, day/week timezone labels, and overlay/token usage in complex UI surfaces.\n\n### Documentation\n\n- Added multi-calendar event documentation and showcase examples.\n- Refreshed the website live demo controls and interactive examples.\n- Updated setup and theme customization guidance to match the self-contained styling model and remove outdated `@source`-based integration steps.\n\n## [3.4.0] - 2026-04-06\n\n### New Features & Enhancements\n\n- **Global Calendar Time Zone**: Added `timeZone` to `useCalendarApp` / `CalendarAppConfig` as the primary display and editing timezone for all views. Day, Week, Month, and Year views now share the same calendar timezone semantics.\n- **Secondary Timeline Refinement**: Kept `secondaryTimeZone` for Day and Week views as a dedicated secondary timeline display setting, independent from the calendar's primary timezone.\n- **Mini Calendar Event Dots**: Added richer mini calendar event dot support, including calendar-color-aware dots with up to four unique indicators per day.\n- **Interactive Demo Controls**: Updated the website interactive calendar controls to expose both the global timezone and the Day/Week secondary timezone with clearer guidance.\n\n### Fixed\n\n- **Timezone Consistency Across Views**:\n  - Unified event projection in Day/Week/Month/Year so view filtering, layout, and visible dates stay aligned with the configured calendar timezone.\n- **Canonical Callback Semantics**:\n  - Editing in a different calendar timezone now behaves: switching timezone only changes display, while `onEventUpdate`, `onEventDrop`, and `onEventResize` return canonical events after the edit is applied.\n  - Aligned timed event creation flows such as quick create, context-menu paste, sidebar calendar drop, Month view create, and keyboard paste with `app.timeZone`.\n\n### Documentation\n\n- Updated timezone documentation in English, Chinese, and Japanese to describe:\n  - the new app-level `timeZone`\n  - Day/Week-only `secondaryTimeZone`\n  - canonical callback semantics after drag/resize/edit operations\n- Refreshed the website interactive calendar help text and tooltips to match the new timezone model.\n\n## [3.3.0] - 2026-03-20\n\n### New Features & Enhancements\n\n- **Grid Year View**: Added a new `grid` mode for `createYearView`, providing a compact month-grid layout with heatmap intensity colors for event density visualization.\n- **`@create-dayflow` CLI**: Introduced the `npm create dayflow@latest` scaffolding tool. Interactively configures a new project with framework selection (React, Vue, Angular, Svelte), TypeScript support, and Tailwind CSS integration.\n- **`renderSidebarHeader`**: Added a `renderSidebarHeader` render prop to the sidebar plugin, allowing full customization of the sidebar header area (e.g. user avatar, collapse toggle).\n\n### Fixed\n\n- **Style Isolation**: Fixed `tailwind-components.css` overriding host application styles. DayFlow's component CSS no longer emits Tailwind utility classes or leaks bare pseudo-class selectors (`:focus-visible`, `:checked`) to the host app.\n- **`bg-primary` Pollution**: Resolved context menu, sidebar merge menu, and calendar list items using the host application's `--color-primary` instead of DayFlow's own color variables. All interactive elements now use `var(--df-color-*)` directly.\n- **Month View Scroll**: Clicking on cross-month dates (previous month's trailing dates in the first row, next month's leading dates in the last row) no longer triggers unwanted month navigation.\n- **Portal Color Scope**: Added `df-portal` class to all `createPortal` root elements so portaled components (context menus, dialogs, drawers) correctly inherit DayFlow's color token scope.\n\n### Style\n\n- Added `df-` prefix scoping to CSS class names in `tailwind-components.css` and `tailwind.css` to prevent conflicts with host application Tailwind instances.\n- Remapped `--color-primary` and related tokens within `.df-calendar-container` and `.df-portal` to always resolve to DayFlow's own `--df-color-*` variables, regardless of host app theme.\n\n### Documentation\n\n- Migrated website from Nextra to Fumadocs.\n- Updated installation guides to feature `@create-dayflow` CLI as the primary setup method.\n- Updated theme customization guide.\n- Added `renderSidebarHeader` API documentation.\n\n## [3.2.0] - 2026-02-28\n\n### New Features & Enhancements\n\n- **Drag & Drop Improvements**:\n  - Added `onEventDrop` and `onEventResize` callbacks to the drag plugin for better event handling.\n  - Updated Month and Year View all-day event drag indicators for better visual feedback.\n- **View Enhancements**:\n  - Added `secondaryTimeZone` label support for Day and Week Views.\n  - Added `timeFormat` configuration for Day and Week Views.\n  - Updated configuration options for `monthView` and `yearView`.\n- **Developer Experience**:\n  - Introduced `oxlint` for faster linting and improved code quality.\n  - Added `pre-commit` hooks and `format:check` scripts to ensure code consistency.\n  - Migrated to `pnpm workspace catalog` for better dependency management.\n  - Added `.editorconfig` and improved VSCode settings/extensions recommendations.\n\n### Performance\n\n- **Scrolling**: Optimized `MonthView` scrolling performance by memoizing scrollbar checks.\n\n### Fixed\n\n- **Layout**: Resolved `eventLayout` stacking issues and improved mobile `WeekView` layout.\n- **Framework Support**: Corrected `ng-packagr` configuration schema path for Angular.\n- **Build & Packaging**:\n  - Fixed CSS export errors and website build issues.\n  - Removed duplicate `peerDependencies` in `package.json` files.\n  - Fixed Tailwind CSS path configurations.\n- **UI/UX**: Fixed an issue where the \"+ more\" click in the website had no reaction.\n- **Documentation**: Corrected README image paths and updated view documentation.\n\n### Style\n\n- Improved Day/Week View event resize pointer display.\n- Cleaned up Tailwind CSS class formatting.\n- Resolved various lint warnings reported by `oxlint`.\n\n## [3.1.0] - 2026-02-20\n\n### Plugin Architecture & Decoupling\n\nThis release introduces a new plugin-based architecture, further reducing the core bundle size and providing greater flexibility. Core features have been extracted into independent, optional packages.\n\n#### New Plugin Packages (v1.0.0)\n\n- **`@dayflow/plugin-drag`**: Handles all drag-and-drop interactions (move, resize, and create).\n- **`@dayflow/plugin-keyboard-shortcuts`**: Provides keyboard navigation and shortcuts support.\n- **`@dayflow/plugin-localization`**: Dedicated package for multi-language support and internationalization.\n- **`@dayflow/plugin-sidebar`**: Extracts the sidebar UI and logic into a standalone plugin.\n\n### New Features & Enhancements\n\n- **Enhanced Visibility Control**:\n  - Added `onVisibleRangeChange` callback with a `reason` parameter (scroll vs. navigation).\n  - Marked `onVisibleMonthChange` as deprecated in favor of the more flexible range change callback.\n- **Improved API**: Simplified framework wrappers by removing the `sidebarConfig` attribute (now handled via the sidebar plugin).\n- **UI Refresh**: Updated the view switching button styles for a more modern look and feel.\n\n### Fixed\n\n- **Accessibility**: Fixed an event scaling issue when using the keyboard `Tab` key for navigation.\n- **Search**: Improved search result location accuracy within the calendar views.\n- **Documentation**: Comprehensive updates to plugin documentation and multi-language guides.\n\n### Breaking Changes\n\n- **Feature Extraction**: Drag-and-drop, keyboard shortcuts, and the sidebar are no longer included in `@dayflow/core` by default. You must install and register the corresponding plugins to retain these features.\n- **Sidebar Configuration**: The `sidebarConfig` prop has been removed from framework adapters. Configuration is now passed directly to the `@dayflow/plugin-sidebar` during initialization.\n\n## [3.0.0] - 2026-02-15\n\n### Major Architectural Overhaul: Multi-Framework Support\n\nThis version marks a complete rewrite of the DayFlow internal architecture, moving from a React-only library to a **framework-agnostic monorepo structure**.\n\n#### New Package Structure\n\n- **`@dayflow/core`**: The new heart of DayFlow. Powered by **Preact**, it handles all state management, layout algorithms, and the core rendering engine (~3KB gzipped).\n- **`@dayflow/react`**: High-performance React adapter.\n- **`@dayflow/vue`**: Brand new adapter for Vue 3.\n- **`@dayflow/svelte`**: Brand new adapter for Svelte 5 (with full SSR support).\n- **`@dayflow/angular`**: Brand new adapter for Angular (v14+).\n\n### New Features\n\n- **Framework Agnostic**: Core logic and UI are now decoupled from specific frameworks.\n- **Improved Content Injection**: New **Content Slots** system allowing users to inject native framework components (React/Vue/Svelte/Angular) into the Preact-driven calendar.\n- **SSR Ready**:\n  - **Svelte**: Provided dedicated SSR bundles (`dist/index.ssr.js`) to avoid DOM reference errors during server-side rendering.\n  - **React/Vue**: Enhanced hydration safety.\n\n### Fixed & Improved\n\n- Optimized mobile responsiveness for all framework adapters.\n- Improved build process using Rollup and Turborepo for faster and smaller bundles.\n\n### Breaking Changes\n\n- **Package Names**: If you were using the old `dayflow` package, you should now migrate to framework-specific packages (e.g., `@dayflow/react`).\n- **Import Paths**:\n  - Components and hooks are now exported from `@dayflow/[framework]`.\n  - Core types and utilities are exported from `@dayflow/core`.\n- **External Dependencies**: To maintain framework-agnosticism, the built-in color picker (`react-color`) has been removed. Users should now provide their own color picker via Content Slots.\n\n---\n"
  },
  {
    "path": "CNAME",
    "content": "calendar.dayflow.studio"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution Guide\n\nThank you for your interest in contributing to **DayFlow**! We welcome contributions from the community. Please follow this guide to set up the project and ensure your contributions align with our standards.\n\n## 🚀 How to Start the Project\n\nIf you have forked the repository and want to run the examples locally, follow these steps:\n\n1.  **Clone your fork:**\n\n    ```bash\n    git clone https://github.com/YOUR_USERNAME/DayFlow.git\n    cd DayFlow\n    ```\n\n2.  **Install dependencies:**\n\n    ```bash\n    pnpm install\n    ```\n\n### 3. Start the development server\n\nNavigate to the core package and start the dev server:\n\n```bash\ncd packages/core\npnpm run dev\n```\n\nThe application will typically be available at:\nhttp://localhost:5529\n\n---\n\n## Commit Message Convention\n\nPlease follow these prefixes when writing commit messages:\n\n- `feat:` — new features\n- `fix:` — bug fixes\n- `docs:` — documentation updates\n- `style:` — formatting, missing semicolons, etc. (no logic changes)\n- `refactor:` — code changes that neither fix a bug nor add a feature\n- `test:` — adding or updating tests\n- `chore:` — maintenance tasks (build, dependencies, etc.)\n\nExample:\n\n```bash\nfeat: add drag-and-drop event support\nfix: resolve timezone offset issue in calendar view\n```\n\n---\n\n## Pull Request Guidelines\n\nTo keep the project maintainable and easy to review:\n\n- Keep each pull request focused on **one feature, bug fix, or refactor**\n- Avoid mixing unrelated changes in a single PR\n- Provide a clear description of:\n  - What changed\n  - Why it was needed\n\n- Update documentation if necessary\n- Add or update tests when applicable\n- Ensure the project builds and runs correctly before submitting\n\n---\n\n## Development Tips\n\n- Keep changes minimal and focused\n- Follow existing code style and structure\n- When in doubt, open an issue first to discuss your idea\n\n---\n\n## License and Contribution Terms\n\nBy submitting a contribution to this project, you represent that:\n\n- The contribution is your original work, or you have the legal right to submit it.\n- You grant DayFlow the right to use, modify, distribute, and relicense your contribution as part of both open source and commercial versions of the project.\n\n---\n\nThanks again for contributing to **DayFlow**! 🚀\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "README.ja.md",
    "content": "# DayFlow\n\n[English](README.md) | [中文](README.zh.md) | **日本語** | [はじめに & コントリビューション](CONTRIBUTING.md)\n\nドラッグ＆ドロップ、マルチビュー、プラグインアーキテクチャをサポートする、柔軟で機能豊富なReactカレンダーコンポーネントライブラリ。\n\n[![npm](https://img.shields.io/npm/v/@dayflow/core?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/core)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/calendar/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/calendar)](https://github.com/dayflow-js/calendar/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## 機能\n\n### 日次、週次、月次、年次、その他のビュータイプ\n\n#### 日次\n\n![日次](./assets/images/DayView.png)\n\n#### 週次\n\n![週次](./assets/images/WeekView.png)\n\n#### 月次\n\n![月次](./assets/images/MonthView.png)\n\n#### 年次 (固定週)\n\n![年次](./assets/images/Year-Fixed-Week.png)\n\n#### 年次 (キャンバス)\n\n![年次 (キャンバス)](./assets/images/Year-Canvas.png)\n\n### モバイルビューのサポート\n\n#### モバイル日次 & 年次\n\n![モバイル日次と年次](./assets/images/Mobile-Day-Year.png)\n\n#### モバイル週次 & 月次\n\n![モバイル週次と月次](./assets/images/Mobile-Week-Month.png)\n\n### デフォルトパネル（複数のイベント詳細パネルオプションが利用可能）\n\n#### 詳細ポップアップ\n\n![詳細ポップアップ](./assets/images/popup.png)\n\n#### 詳細ダイアログ\n\n![詳細ダイアログ](./assets/images/dialog.png)\n\n### ダークモードのサポート\n\n![ダークモード](./assets/images/DarkMode.png)\n\n### ドラッグ＆ドロップとリサイズも簡単\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## コントリビューション\n\nコントリビューションは大歓迎です！お気軽に Pull Request を送信してください。\n\n## バグ報告\n\nバグを見つけた場合は、[GitHub Issues](https://github.com/dayflow-js/calendar/issues) で問題を報告してください。\n\n## サポート\n\n質問やサポートについては、GitHub で Issue を開くか、Discord に参加してください。\n\n---\n"
  },
  {
    "path": "README.md",
    "content": "# DayFlow\n\n**English** | [中文](README.zh.md) | [日本語](README.ja.md) | [Getting Started & Contributing](CONTRIBUTING.md)\n\nA flexible and feature-rich calendar component library for **React, Vue, Angular, and Svelte** with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/core?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/core)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/calendar/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/calendar)](https://github.com/dayflow-js/calendar/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](./assets/images/DayView.png)\n\n#### Week View\n\n![Week View](./assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](./assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](./assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](./assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](./assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](./assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](./assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](./assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](./assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/calendar/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n\n---\n"
  },
  {
    "path": "README.zh.md",
    "content": "# DayFlow\n\n[English](README.md) | **中文** | [日本語](README.ja.md) | [快速开始 & 贡献](CONTRIBUTING.md)\n\n一个灵活且功能丰富的 React 日历组件库，支持拖拽、多视图和插件架构。\n\n[![npm](https://img.shields.io/npm/v/@dayflow/core?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/core)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/calendar/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/calendar)](https://github.com/dayflow-js/calendar/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## 功能特性\n\n### 日视图、周视图、月视图、年视图及多种视图类型\n\n#### 日视图\n\n![日视图](./assets/images/DayView.png)\n\n#### 周视图\n\n![周视图](./assets/images/WeekView.png)\n\n#### 月视图\n\n![月视图](./assets/images/MonthView.png)\n\n#### 年视图 (固定周)\n\n![年视图](./assets/images/Year-Fixed-Week.png)\n\n#### 年视图 (画布)\n\n![年视图 (画布)](./assets/images/Year-Canvas.png)\n\n### 移动端视图支持\n\n#### 移动端日视图 & 年视图\n\n![移动端日视图与年视图](./assets/images/Mobile-Day-Year.png)\n\n#### 移动端周视图 & 月视图\n\n![移动端周视图与月视图](./assets/images/Mobile-Week-Month.png)\n\n### 默认面板（提供多种事件详情面板选项）\n\n#### 详情弹窗\n\n![详情弹窗](./assets/images/popup.png)\n\n#### 详情对话框\n\n![详情对话框](./assets/images/dialog.png)\n\n### 暗色模式支持\n\n![暗色模式](./assets/images/DarkMode.png)\n\n### 轻松拖拽与缩放\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## 贡献\n\n欢迎贡献！请随意提交 Pull Request。\n\n## Bug 反馈\n\n如果您发现 Bug，请在 [GitHub Issues](https://github.com/dayflow-js/calendar/issues) 上提交 issue。\n\n## 支持\n\n如有问题和支持需求，请在 GitHub 上打开 issue 或加入 discord。\n\n---\n"
  },
  {
    "path": "examples/defaultCalendarExample/defaultCalendarExample.tsx",
    "content": "import {\n  Event,\n  CalendarType,\n  EventChange,\n  ReadOnlyConfig,\n  TimeZone,\n  ViewType,\n} from '@dayflow/core';\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport { createLocalizationPlugin, zh } from '@dayflow/plugin-localization';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  createDayView,\n  createYearView,\n  createAgendaView,\n  UseCalendarAppReturn,\n} from '@dayflow/react';\nimport { getWebsiteCalendars } from '@examples/utils/palette';\nimport { generateSampleEvents } from '@examples/utils/sampleData';\nimport { createKeyboardShortcutsPlugin } from '@keyboard-shortcuts/plugin';\nimport { createSidebarPlugin } from '@sidebar/plugin';\nimport { Sun, Moon, Globe, Clock } from 'lucide-react';\nimport React, { useState, useRef, useEffect, useMemo } from 'react';\n\nconst TZ_OPTIONS = Object.entries(TimeZone).map(([key, value]) => ({\n  label: `${key.replaceAll('_', ' ')} (${value})`,\n  value,\n}));\n\nconst hasRedo = (app: object): app is object & { redo: () => void } =>\n  'redo' in app && typeof app.redo === 'function';\n\ntype ExampleThemeMode = 'light' | 'dark';\n\nconst getInitialThemeMode = (): ExampleThemeMode => {\n  if (typeof window === 'undefined') {\n    return 'light';\n  }\n\n  if (document.documentElement.classList.contains('dark')) {\n    return 'dark';\n  }\n\n  const storedTheme = localStorage.getItem('theme');\n  if (storedTheme === 'dark' || storedTheme === 'light') {\n    return storedTheme;\n  }\n\n  return window.matchMedia('(prefers-color-scheme: dark)').matches\n    ? 'dark'\n    : 'light';\n};\n\nconst DefaultCalendarExample: React.FC<{\n  themeMode: ExampleThemeMode;\n}> = ({ themeMode }) => {\n  const [events] = useState<Event[]>(generateSampleEvents());\n  const calendarRef = useRef<UseCalendarAppReturn | null>(null);\n  const [readOnly] = useState<boolean | ReadOnlyConfig>(false);\n  // Global calendar timezone — affects all views' event bucketing and editing\n  const [appTz, setAppTz] = useState<string>('');\n  // Secondary timezone — only adds a second timeline label column in Day/Week\n  const [secondaryTz, setSecondaryTz] = useState<string>('');\n\n  const [isMobile, setIsMobile] = useState(false);\n\n  useEffect(() => {\n    const checkMobile = () => setIsMobile(window.innerWidth < 768);\n    checkMobile();\n    window.addEventListener('resize', checkMobile);\n    return () => window.removeEventListener('resize', checkMobile);\n  }, []);\n\n  const plugins = useMemo(\n    () =>\n      [\n        createDragPlugin({\n          onEventDrop: (updatedEvent, _) => {\n            console.log('onEventDrop:', updatedEvent);\n          },\n          onEventResize: (updatedEvent, _) => {\n            console.log('onEventResize:', updatedEvent);\n          },\n        }),\n        createSidebarPlugin({\n          createCalendarMode: 'modal',\n          // colorPickerMode: 'default',\n          showEventDots: true,\n        }),\n        createLocalizationPlugin({\n          locales: [zh],\n        }),\n        createKeyboardShortcutsPlugin({\n          callbacks: {\n            redo: app => {\n              console.log('Redo triggered via callback!', app);\n              // You can add custom redo logic here if app.redo() is not enough\n              if (hasRedo(app)) {\n                app.redo();\n              }\n            },\n            undo: app => {\n              console.log('Undo triggered via callback!');\n              app.undo();\n            },\n            delete: (app, event) => {\n              console.log('Delete triggered via callback!', event);\n              if (!event) return;\n              app.deleteEvent(event.id);\n              app.selectEvent(null);\n              console.log(`Deleted event without confirmation: ${event.title}`);\n            },\n          },\n        }),\n      ].filter(plugin => !(isMobile && plugin.name === 'sidebar')),\n    [isMobile]\n  );\n\n  const searchConfig = useMemo(\n    () => ({\n      onResultClick: ({\n        event,\n        defaultAction,\n      }: {\n        event: Event;\n        defaultAction: () => void;\n      }) => {\n        console.log('Search result clicked:', event);\n        defaultAction();\n      },\n    }),\n    []\n  );\n\n  const views = useMemo(\n    () => [\n      createDayView({\n        // timeFormat: '12h',\n        secondaryTimeZone: secondaryTz || undefined,\n        showEventDots: true,\n        scrollToCurrentTime: true,\n      }),\n      createWeekView({\n        // timeFormat: '12h',\n        secondaryTimeZone: secondaryTz || undefined,\n        // startOfWeek: 2,\n        // showAllDay: false,\n        showEventDots: true,\n        scrollToCurrentTime: true,\n      }),\n      createMonthView({\n        showWeekNumbers: true,\n        // showMonthIndicator: false,\n        showEventDots: true,\n      }),\n      createYearView({\n        mode: 'fixed-week',\n        showTimedEventsInYearView: true,\n        startOfWeek: 7,\n        showEventDots: true,\n      }),\n      createAgendaView({}),\n    ],\n    [secondaryTz]\n  );\n\n  const calendars = useMemo(() => getWebsiteCalendars(), []);\n\n  const callbacks = useMemo(\n    () => ({\n      onEventCreate: async (event: Event) => {\n        await new Promise(resolve => {\n          setTimeout(resolve, 500);\n        });\n        console.log('create event:', event);\n      },\n      onEventClick: (event: Event) => {\n        console.log('click event:', event);\n      },\n      onEventDoubleClick: (event: Event) => {\n        console.log('double click event:', event);\n        // You could use the event element as an anchor for a custom popover here\n        return true;\n      },\n      onEventUpdate: async (event: Event) => {\n        await new Promise(resolve => {\n          setTimeout(resolve, 1500);\n        });\n        console.log('update event:', event);\n      },\n      onEventDelete: async (eventId: string) => {\n        await new Promise(resolve => {\n          setTimeout(resolve, 1500);\n        });\n        console.log('delete event:', eventId);\n      },\n      onMoreEventsClick: (date: Date) => {\n        console.log('more events click date:', date);\n        calendarRef.current?.selectDate(date);\n        calendarRef.current?.changeView(ViewType.DAY);\n      },\n      onCalendarUpdate: async (cal: CalendarType) => {\n        await new Promise(resolve => {\n          setTimeout(resolve, 1500);\n        });\n        console.log('update calendar:', cal);\n      },\n      onCalendarDelete: async (calendarId: string) => {\n        await new Promise(resolve => {\n          setTimeout(resolve, 1500);\n        });\n        console.log('delete calendar:', calendarId);\n      },\n      onCalendarCreate: async (cal: CalendarType) => {\n        await new Promise(resolve => {\n          setTimeout(resolve, 1500);\n        });\n        console.log('create calendar: w', cal);\n      },\n      onCalendarMerge: async (sourceId: string, targetId: string) => {\n        await new Promise(resolve => {\n          setTimeout(resolve, 1500);\n        });\n        console.log('merge calendar:', sourceId, targetId);\n      },\n      onEventBatchChange: (event: EventChange[]) => {\n        console.log('batch change events:', event);\n      },\n    }),\n    []\n  );\n\n  const calendar = useCalendarApp({\n    timeZone: appTz || undefined,\n    views,\n    theme: { mode: themeMode },\n    events: events,\n    calendars,\n    defaultCalendar: 'work',\n    // useEventDetailDialog: true,\n    // switcherMode: 'select',\n    plugins,\n    // locale: zh,\n    defaultView: ViewType.MONTH,\n    // useEventDetailDialog: true,\n    // switcherMode: 'select' as const,\n    // readOnly,\n    callbacks,\n  });\n\n  calendarRef.current = calendar;\n\n  return (\n    <div>\n      <div className='mb-4 flex flex-wrap items-center gap-3 px-4'>\n        {/* Global calendar timezone */}\n        <div className='flex min-w-[18rem] items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-gray-700 shadow-sm md:min-w-88 dark:border-slate-700 dark:bg-slate-800 dark:text-gray-200'>\n          <Globe size={16} className='text-gray-400' />\n          <div className='flex min-w-0 flex-1 flex-col'>\n            <span className='text-[10px] leading-none text-gray-400 dark:text-gray-500'>\n              Calendar Timezone\n            </span>\n            <select\n              value={appTz}\n              onChange={e => setAppTz(e.target.value)}\n              className='w-full bg-transparent pr-6 text-sm font-medium outline-none'\n            >\n              <option value=''>Device local</option>\n              {TZ_OPTIONS.map(({ label, value }) => (\n                <option key={value} value={value}>\n                  {label}\n                </option>\n              ))}\n            </select>\n          </div>\n        </div>\n\n        {/* Secondary timeline for Day/Week */}\n        <div className='flex min-w-[18rem] items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-gray-700 shadow-sm md:min-w-88 dark:border-slate-700 dark:bg-slate-800 dark:text-gray-200'>\n          <Clock size={16} className='text-gray-400' />\n          <div className='flex min-w-0 flex-1 flex-col'>\n            <span className='text-[10px] leading-none text-gray-400 dark:text-gray-500'>\n              Secondary Timeline (Day/Week)\n            </span>\n            <select\n              value={secondaryTz}\n              onChange={e => setSecondaryTz(e.target.value)}\n              className='w-full bg-transparent pr-6 text-sm font-medium outline-none'\n            >\n              <option value=''>None</option>\n              {TZ_OPTIONS.map(({ label, value }) => (\n                <option key={value} value={value}>\n                  {label}\n                </option>\n              ))}\n            </select>\n          </div>\n        </div>\n      </div>\n      <DayFlowCalendar\n        calendar={calendar}\n        search={searchConfig}\n        // eventContentDay={({ event, isSelected }) => (\n        //   <div\n        //     className='flex h-full flex-col justify-start overflow-hidden border-l-4'\n        //     style={{\n        //       borderTopLeftRadius: '4px',\n        //       borderBottomLeftRadius: '4px',\n        //       borderColor: getLineColor(event.calendarId || 'blue'),\n        //     }}\n        //   >\n        //     <div className='flex items-center gap-1 px-0.5'>\n        //       <span className='shrink-0 text-[10px]'>📅</span>\n        //       <span\n        //         className={`truncate text-xs font-semibold ${isSelected ? 'text-white' : ''}`}\n        //       >\n        //         {event.title}\n        //       </span>\n        //     </div>\n        //     <div className='flex flex-col px-1'>\n        //       <span\n        //         className={`truncate text-[10px] opacity-70 ${isSelected ? 'text-white' : ''}`}\n        //       >\n        //         📍 {`${event.meta?.location || 'No location'}`}\n        //       </span>\n        //       <span\n        //         className={`text-[10px] opacity-60 ${isSelected ? 'text-white' : ''}`}\n        //       >\n        //         {event.description}\n        //       </span>\n        //     </div>\n        //   </div>\n        // )}\n        // eventContentWeek={({ event, isSelected }) => (\n        //   <div\n        //     className='flex h-full flex-col justify-start overflow-hidden border-l-4 border-black/20 pr-0.5 pl-1'\n        //     style={{\n        //       borderTopLeftRadius: '4px',\n        //       borderBottomLeftRadius: '4px',\n        //       borderColor: getLineColor(event.calendarId || 'blue'),\n        //     }}\n        //   >\n        //     <div className='flex items-center gap-1 px-0.5'>\n        //       <EventIcon calendarId={event.calendarId} defaultIcon='🗓' />\n        //       <span\n        //         className={`text-xs font-semibold ${isSelected ? 'text-white' : ''}`}\n        //       >\n        //         {event.title}\n        //       </span>\n        //     </div>\n        //     <div className='flex flex-col'>\n        //       <span\n        //         className={`text-[9px] opacity-70 ${isSelected ? 'text-white' : ''}`}\n        //       >\n        //         📍 {`${event.meta?.location || 'No location'}`}\n        //       </span>\n        //       <span\n        //         className={`text-[9px] opacity-60 ${isSelected ? 'text-white' : ''}`}\n        //       >\n        //         {event.description}\n        //       </span>\n        //     </div>\n        //   </div>\n        // )}\n        // eventContentMonth={({ event }) => (\n        //   <div className='flex items-center gap-1 overflow-hidden px-0.5'>\n        //     <EventIcon calendarId={event.calendarId} defaultIcon='🗃' />\n        //     <span className='truncate text-xs font-medium'>{event.title}</span>\n        //   </div>\n        // )}\n        // eventContentYear={({ event }) => (\n        //   <div className='flex items-center gap-1 overflow-hidden px-0.5'>\n        //     <EventIcon calendarId={event.calendarId} defaultIcon='🗃' />\n        //     <span className='truncate text-xs font-medium'>{event.title}</span>\n        //   </div>\n        // )}\n        // eventContentAllDayDay={({ event }) => (\n        //   <div className='flex h-full items-center gap-1 overflow-hidden px-0.5'>\n        //     <EventIcon calendarId={event.calendarId} defaultIcon='☀️' />\n        //     <span className='truncate text-xs font-medium'>{event.title}</span>\n        //   </div>\n        // )}\n        // eventContentAllDayWeek={({ event }) => (\n        //   <div className='flex h-full items-center gap-1 overflow-hidden px-0.5'>\n        //     <EventIcon calendarId={event.calendarId} defaultIcon='☀️' />\n        //     <span className='truncate text-xs font-medium'>{event.title}</span>\n        //   </div>\n        // )}\n        // eventContentAllDayMonth={({ event }) => (\n        //   <div className='flex h-full items-center gap-1 overflow-hidden px-0.5'>\n        //     <EventIcon calendarId={event.calendarId} defaultIcon='☀️' />\n        //     <span className='truncate text-xs font-medium'>{event.title}</span>\n        //   </div>\n        // )}\n        // eventContentAllDayYear={({ event }) => (\n        //   <div className='flex h-full items-center gap-1 overflow-hidden px-0.5'>\n        //     <EventIcon calendarId={event.calendarId} defaultIcon='☀️' />\n        //     <span className='truncate text-xs font-medium'>{event.title}</span>\n        //   </div>\n        // )}\n      />\n    </div>\n  );\n};\n\nconst ThemeToggle = ({\n  isDark,\n  onToggle,\n}: {\n  isDark: boolean;\n  onToggle: () => void;\n}) => (\n  <div className='flex shrink-0 items-center gap-4'>\n    <button\n      type='button'\n      onClick={onToggle}\n      className='flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-gray-700 shadow-sm transition-colors hover:bg-gray-50 dark:border-slate-700 dark:bg-slate-800 dark:text-gray-200 dark:hover:bg-slate-700'\n    >\n      {isDark ? <Sun size={18} /> : <Moon size={18} />}\n      <span className='text-sm font-medium'>{isDark ? 'Light' : 'Dark'}</span>\n    </button>\n  </div>\n);\n\nexport function CalendarTypesExample() {\n  const [themeMode, setThemeMode] =\n    useState<ExampleThemeMode>(getInitialThemeMode);\n\n  useEffect(() => {\n    const root = document.documentElement;\n    root.classList.toggle('dark', themeMode === 'dark');\n    root.classList.toggle('light', themeMode === 'light');\n    localStorage.theme = themeMode;\n  }, [themeMode]);\n\n  return (\n    <div className='min-h-screen bg-gray-50 p-2 text-gray-900 transition-colors duration-200 dark:bg-slate-950 dark:text-gray-100'>\n      <div className=''>\n        {/* Header */}\n        <div className='mb-4 flex items-center justify-between gap-4 px-4'>\n          <div className='space-y-4'>\n            <h1 className='text-3xl font-bold tracking-tight'>\n              Calendar Example\n            </h1>\n          </div>\n          <ThemeToggle\n            isDark={themeMode === 'dark'}\n            onToggle={() =>\n              setThemeMode(current => (current === 'dark' ? 'light' : 'dark'))\n            }\n          />\n        </div>\n\n        {/* Calendar Instance */}\n        <div>\n          <DefaultCalendarExample themeMode={themeMode} />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default CalendarTypesExample;\n"
  },
  {
    "path": "examples/main.tsx",
    "content": "// import 'preact/debug';\nimport { createRoot } from 'react-dom/client';\n\n// Local example shell utilities live in examples/styles/tailwind.css.\n// DayFlow component styles stay on the library side.\nimport '@/styles/tailwind-components.css';\nimport './styles/tailwind.css';\nimport CalendarExample from './defaultCalendarExample/defaultCalendarExample';\n\nconst container = document.querySelector('#root');\nif (container) {\n  const root = createRoot(container);\n  root.render(<CalendarExample />);\n}\n"
  },
  {
    "path": "examples/styles/tailwind.css",
    "content": "@import 'tailwindcss/preflight' layer(base);\n@import 'tailwindcss/theme' layer(theme);\n@import 'tailwindcss/utilities';\n\n@source \"../**/*.{ts,tsx}\";\n\n@variant dark (.dark &);\n"
  },
  {
    "path": "examples/utils/palette.ts",
    "content": "import { CalendarType, CalendarColors } from '@dayflow/core';\n\ninterface PaletteCalendar extends Pick<\n  CalendarType,\n  'id' | 'name' | 'icon' | 'readOnly'\n> {\n  color: string;\n  colors: CalendarColors;\n  darkColors: CalendarColors;\n  source?: string;\n}\n\nexport const CALENDAR_SIDE_PANEL: PaletteCalendar[] = [\n  {\n    id: 'team',\n    name: 'Product Team',\n    source: 'Google',\n    readOnly: true,\n    color: '#2563eb',\n    colors: {\n      eventColor: 'rgba(37, 99, 235, 0.12)',\n      eventSelectedColor: '#2563eb',\n      lineColor: '#2563eb',\n      textColor: '#1d4ed8',\n    },\n    darkColors: {\n      eventColor: 'rgba(59, 130, 246, 0.25)',\n      eventSelectedColor: '#3b82f6',\n      lineColor: '#60a5fa',\n      textColor: '#dbeafe',\n    },\n  },\n  {\n    id: 'personal',\n    name: 'Personal',\n    source: 'iCloud',\n    color: '#0ea5e9',\n    colors: {\n      eventColor: 'rgba(14, 165, 233, 0.12)',\n      eventSelectedColor: '#0ea5e9',\n      lineColor: '#0ea5e9',\n      textColor: '#0369a1',\n    },\n    darkColors: {\n      eventColor: 'rgba(14, 165, 233, 0.24)',\n      eventSelectedColor: '#38bdf8',\n      lineColor: '#7dd3fc',\n      textColor: '#e0f2fe',\n    },\n  },\n  {\n    id: 'learning',\n    name: 'Learning',\n    source: 'iCloud',\n    color: '#8b5cf6',\n    colors: {\n      eventColor: 'rgba(139, 92, 246, 0.15)',\n      eventSelectedColor: '#8b5cf6',\n      lineColor: '#8b5cf6',\n      textColor: '#5b21b6',\n    },\n    darkColors: {\n      eventColor: 'rgba(167, 139, 250, 0.28)',\n      eventSelectedColor: '#a855f7',\n      lineColor: '#c084fc',\n      textColor: '#ede9fe',\n    },\n  },\n  {\n    id: 'travel',\n    name: 'Travel',\n    source: 'iCloud',\n    color: '#f97316',\n    colors: {\n      eventColor: 'rgba(249, 115, 22, 0.15)',\n      eventSelectedColor: '#f97316',\n      lineColor: '#f97316',\n      textColor: '#7c2d12',\n    },\n    darkColors: {\n      eventColor: 'rgba(251, 146, 60, 0.3)',\n      eventSelectedColor: '#fb923c',\n      lineColor: '#fdba74',\n      textColor: '#ffedd5',\n    },\n  },\n  {\n    id: 'wellness',\n    name: 'Wellness',\n    source: 'Google',\n    color: '#10b981',\n    colors: {\n      eventColor: 'rgba(16, 185, 129, 0.15)',\n      eventSelectedColor: '#10b981',\n      lineColor: '#10b981',\n      textColor: '#065f46',\n    },\n    darkColors: {\n      eventColor: 'rgba(52, 211, 153, 0.25)',\n      eventSelectedColor: '#34d399',\n      lineColor: '#6ee7b7',\n      textColor: '#ecfdf5',\n    },\n  },\n  {\n    id: 'marketing',\n    name: 'Marketing',\n    source: 'Google',\n    color: '#ec4899',\n    colors: {\n      eventColor: 'rgba(236, 72, 153, 0.15)',\n      eventSelectedColor: '#ec4899',\n      lineColor: '#ec4899',\n      textColor: '#831843',\n    },\n    darkColors: {\n      eventColor: 'rgba(244, 114, 182, 0.28)',\n      eventSelectedColor: '#f472b6',\n      lineColor: '#f9a8d4',\n      textColor: '#fce7f3',\n    },\n  },\n  {\n    id: 'support',\n    name: 'Support',\n    source: 'Google',\n    color: '#14b8a6',\n    colors: {\n      eventColor: 'rgba(20, 184, 166, 0.15)',\n      eventSelectedColor: '#14b8a6',\n      lineColor: '#14b8a6',\n      textColor: '#115e59',\n    },\n    darkColors: {\n      eventColor: 'rgba(45, 212, 191, 0.25)',\n      eventSelectedColor: '#5eead4',\n      lineColor: '#99f6e4',\n      textColor: '#ccfbf1',\n    },\n  },\n];\n\nexport const getWebsiteCalendars = (): CalendarType[] =>\n  CALENDAR_SIDE_PANEL.map(item => ({\n    id: item.id,\n    name: item.name,\n    icon: item.icon,\n    source: item.source,\n    readOnly: item.readOnly,\n    colors: {\n      eventColor: `${item.color}30`,\n      eventSelectedColor: `${item.color}`,\n      lineColor: item.color,\n      textColor: item.colors.textColor,\n    },\n    isVisible: true,\n  }));\n"
  },
  {
    "path": "examples/utils/sampleData.ts",
    "content": "import { Event } from '@dayflow/core';\nimport { Temporal } from 'temporal-polyfill';\n\nconst calendarIds = [\n  'team',\n  'personal',\n  'learning',\n  'travel',\n  'wellness',\n  'marketing',\n  'support',\n];\n\nconst titles = [\n  'Product Sync',\n  'Design Review',\n  'Customer Call',\n  'Weekly Planning',\n  'Deep Work',\n  'Code Review',\n  'Brainstorm',\n  'Usability Test',\n  'Team Retro',\n  'Partner Demo',\n  'Lunch & Learn',\n  'Yoga Break',\n  'Travel Block',\n  'Hiring Interview',\n  'Content Planning',\n];\n\nconst locations = [\n  'Conference Room A',\n  'Meeting Room 302',\n  'Zoom Meeting',\n  'Main Office, 4th Floor',\n  'Starbucks Coffee',\n  'Community Center',\n  'Innovation Hub',\n  'Building 5, Lab 2',\n];\n\nconst eventDetails: Record<string, { description: string; location?: string }> =\n  {\n    'Product Sync': {\n      description: 'Sync up on the latest product roadmap and milestones.',\n      location: 'Room 101',\n    },\n    'Design Review': {\n      description:\n        'Review the new UI/UX designs for the upcoming mobile app release.',\n      location: 'Design Studio',\n    },\n    'Customer Call': {\n      description:\n        'Discussion with key clients regarding feature requests and feedback.',\n      location: 'Virtual',\n    },\n    'Weekly Planning': {\n      description: 'Plan tasks and priorities for the upcoming week.',\n      location: 'Main Hall',\n    },\n    'Deep Work': {\n      description: 'Focus time for intense development and problem solving.',\n      location: 'Quiet Zone',\n    },\n    'Code Review': {\n      description: 'Review pull requests and ensure code quality standards.',\n      location: 'Dev Corner',\n    },\n    Brainstorm: {\n      description: 'Ideation session for the next big feature.',\n      location: 'Whiteboard Room',\n    },\n    'Usability Test': {\n      description: 'Observe users interacting with the latest prototype.',\n      location: 'User Lab',\n    },\n    'Team Retro': {\n      description: 'Reflect on the past sprint and discuss improvements.',\n      location: 'Common Area',\n    },\n    'Partner Demo': {\n      description: 'Demonstrate our latest capabilities to potential partners.',\n      location: 'Executive Suite',\n    },\n    'Lunch & Learn': {\n      description: 'Educational session over lunch about new technologies.',\n      location: 'Cafeteria',\n    },\n    'Yoga Break': {\n      description: 'Stretch and relax with a quick yoga session.',\n      location: 'Wellness Room',\n    },\n    'Travel Block': {\n      description: 'Time allocated for travel and logistics.',\n      location: 'Airport Terminal',\n    },\n    'Hiring Interview': {\n      description: 'Interviewing candidates for the Senior Engineer position.',\n      location: 'HR Office',\n    },\n    'Content Planning': {\n      description: 'Plan the editorial calendar and upcoming blog posts.',\n      location: 'Marketing Hub',\n    },\n  };\n\n// Simple deterministic random number generator\nconst createRandom = (seed: number) => {\n  let s = seed;\n  return () => {\n    const x = Math.sin(s++) * 10000;\n    return x - Math.floor(x);\n  };\n};\n\nconst createRandomInt = (random: () => number) => (min: number, max: number) =>\n  Math.floor(random() * (max - min + 1)) + min;\n\nconst DEFAULT_TIME_ZONE = Temporal.Now.timeZoneId();\n\nconst createTimedEvent = (\n  baseDate: Temporal.PlainDate,\n  index: number,\n  randomInt: (min: number, max: number) => number\n): Event => {\n  const title = titles[index % titles.length];\n  const details = eventDetails[title] || {\n    description: 'General event details.',\n    location: locations[index % locations.length],\n  };\n\n  // Keep sample events concentrated in local working hours for easier testing.\n  const startHour = randomInt(8, 15);\n  const maxDuration = Math.max(1, 17 - startHour);\n  const duration = Math.max(1, randomInt(1, Math.min(3, maxDuration)));\n\n  const startPlain = baseDate.toPlainDateTime({\n    hour: startHour,\n    minute: randomInt(0, 1) ? 30 : 0,\n  });\n\n  const start = Temporal.ZonedDateTime.from({\n    timeZone: DEFAULT_TIME_ZONE,\n    year: startPlain.year,\n    month: startPlain.month,\n    day: startPlain.day,\n    hour: startPlain.hour,\n    minute: startPlain.minute,\n  });\n\n  const end = start.add({ hours: duration });\n\n  return {\n    id: `event-${index}`,\n    title: title,\n    description: details.description,\n    start,\n    end,\n    calendarId: calendarIds[index % calendarIds.length],\n    meta: {\n      location: details.location || locations[index % locations.length],\n    },\n  };\n};\n\nconst createAllDayEvent = (\n  start: Temporal.PlainDate,\n  span: number,\n  index: number,\n  calendarId: string,\n  title: string\n): Event => {\n  const details = eventDetails[title] || {\n    description: 'All day event details.',\n    location: 'Various',\n  };\n  return {\n    id: `all-day-${index}`,\n    title,\n    description: details.description,\n    start,\n    end: start.add({ days: span }),\n    allDay: true,\n    calendarId,\n    icon: true,\n    meta: {\n      location: details.location || 'Multiple Locations',\n    },\n  };\n};\n\nconst baseAllDayDefinitions: Array<{\n  offset: number;\n  span: number;\n  calendarId: string;\n  title: string;\n}> = [\n  { offset: -6, span: 2, calendarId: 'team', title: 'Sprint Offsite' },\n  { offset: -2, span: 0, calendarId: 'personal', title: 'Family Day' },\n  { offset: 3, span: 1, calendarId: 'travel', title: 'Client Visit' },\n  { offset: 7, span: 2, calendarId: 'marketing', title: 'Campaign Launch' },\n  { offset: 12, span: 0, calendarId: 'learning', title: 'Conference' },\n  { offset: 16, span: 3, calendarId: 'wellness', title: 'Wellness Retreat' },\n  { offset: 20, span: 1, calendarId: 'support', title: 'Support Rotation' },\n];\n\nexport interface Resource {\n  id: string;\n  title: string;\n  avatar?: string;\n  color?: string;\n  meta?: Record<string, unknown>;\n}\n\nexport const generateSampleResources = (): Resource[] => [\n  {\n    id: 'team',\n    title: 'Team Alpha',\n    avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alpha',\n    color: '#3b82f6',\n  },\n  {\n    id: 'personal',\n    title: 'John Doe',\n    avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=John',\n    color: '#ef4444',\n  },\n  {\n    id: 'learning',\n    title: 'Training Room',\n    avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Room',\n    color: '#10b981',\n  },\n  {\n    id: 'travel',\n    title: 'Flight 101',\n    avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Flight',\n    color: '#f59e0b',\n  },\n  {\n    id: 'wellness',\n    title: 'Gym',\n    avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Gym',\n    color: '#8b5cf6',\n  },\n  {\n    id: 'marketing',\n    title: 'Marketing Suite',\n    avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Marketing',\n    color: '#ec4899',\n  },\n  {\n    id: 'support',\n    title: 'Support Desk',\n    avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Support',\n    color: '#64748b',\n  },\n];\n\n/** Multi-calendar sample events that demonstrate calendarIds support. */\nexport const generateMultiCalendarEvents = (): Event[] => {\n  const today = Temporal.Now.plainDateISO();\n  const tz = Temporal.Now.timeZoneId();\n\n  const makeZoned = (\n    date: Temporal.PlainDate,\n    hour: number,\n    minute = 0\n  ): Temporal.ZonedDateTime =>\n    Temporal.ZonedDateTime.from({\n      timeZone: tz,\n      year: date.year,\n      month: date.month,\n      day: date.day,\n      hour,\n      minute,\n    });\n\n  return [\n    // Two-calendar timed event — today\n    {\n      id: 'multi-cal-1',\n      title: 'Family Dance Workshop',\n      description: 'Shared across the whole family calendar.',\n      start: makeZoned(today, 10),\n      end: makeZoned(today, 11, 30),\n      calendarIds: ['personal', 'wellness'],\n      meta: { location: 'Community Center' },\n    },\n    // Three-calendar timed event — tomorrow\n    {\n      id: 'multi-cal-2',\n      title: 'All-Hands Sprint Planning',\n      description: 'Cross-team planning session.',\n      start: makeZoned(today.add({ days: 1 }), 14),\n      end: makeZoned(today.add({ days: 1 }), 16),\n      calendarIds: ['team', 'marketing', 'support'],\n      meta: { location: 'Main Hall' },\n    },\n    // Four-calendar all-day event — spans today + 2 days\n    {\n      id: 'multi-cal-3',\n      title: 'Company Off-site',\n      description: 'Belongs to every team calendar.',\n      start: today.add({ days: 3 }),\n      end: today.add({ days: 5 }),\n      allDay: true,\n      icon: true,\n      calendarIds: ['team', 'personal', 'travel', 'learning'],\n    },\n    // Two-calendar timed event — yesterday (tests backward visibility)\n    {\n      id: 'multi-cal-4',\n      title: 'Retrospective + Wellness Check',\n      description: 'Weekly retro combined with wellness review.',\n      start: makeZoned(today.subtract({ days: 1 }), 15),\n      end: makeZoned(today.subtract({ days: 1 }), 16),\n      calendarIds: ['team', 'wellness'],\n      meta: { location: 'Zoom' },\n    },\n  ];\n};\n\nexport const generateSampleEvents = (): Event[] => {\n  const today = Temporal.Now.plainDateISO();\n  const windowStart = today.subtract({ days: 24 });\n  const events: Event[] = [];\n\n  // Initialize deterministic random generator\n  const random = createRandom(12345);\n  const randomInt = createRandomInt(random);\n\n  for (let offset = 0; offset < 56; offset += 1) {\n    const date = windowStart.add({ days: offset });\n    const dayEvents = randomInt(2, 4);\n    for (let i = 0; i < dayEvents; i += 1) {\n      events.push(createTimedEvent(date, offset * 10 + i, randomInt));\n    }\n  }\n  baseAllDayDefinitions.forEach((definition, index) => {\n    const start = today.add({ days: definition.offset });\n    const span = Math.max(0, definition.span);\n    events.push(\n      createAllDayEvent(\n        start,\n        span,\n        index,\n        definition.calendarId,\n        definition.title\n      )\n    );\n  });\n\n  // Append multi-calendar demo events\n  events.push(...generateMultiCalendarEvents());\n\n  // Annual events for Year View demonstration\n  const currentYear = today.year;\n  const annualEvents = [\n    // Jan: New Year & Kickoff\n    {\n      month: 1,\n      day: 1,\n      span: 3,\n      calendarId: 'personal',\n      title: 'New Year Holiday',\n    },\n    {\n      month: 1,\n      day: 15,\n      span: 5,\n      calendarId: 'team',\n      title: 'Annual Kickoff Week',\n    },\n    {\n      month: 1,\n      day: 25,\n      span: 3,\n      calendarId: 'learning',\n      title: 'Goal Setting Workshop',\n    },\n\n    // Feb-Mar: Work Focus\n    {\n      month: 2,\n      day: 5,\n      span: 4,\n      calendarId: 'team',\n      title: 'Q1 Strategy Offsite',\n    },\n    {\n      month: 2,\n      day: 14,\n      span: 3,\n      calendarId: 'personal',\n      title: \"Valentine's Trip\",\n    },\n    {\n      month: 2,\n      day: 26,\n      span: 4,\n      calendarId: 'learning',\n      title: 'Tech Conference',\n    },\n    {\n      month: 3,\n      day: 10,\n      span: 4,\n      calendarId: 'team',\n      title: 'Design Sprint Week',\n    },\n    {\n      month: 3,\n      day: 24,\n      span: 4,\n      calendarId: 'marketing',\n      title: 'Product Launch Week',\n    },\n\n    // Apr-May: Conferences & Holidays\n    {\n      month: 4,\n      day: 12,\n      span: 5,\n      calendarId: 'travel',\n      title: 'Spring Team Building',\n    },\n    {\n      month: 4,\n      day: 25,\n      span: 3,\n      calendarId: 'personal',\n      title: 'Anniversary Trip',\n    },\n    {\n      month: 5,\n      day: 1,\n      span: 3,\n      calendarId: 'personal',\n      title: 'Labour Day Holiday',\n    },\n    {\n      month: 5,\n      day: 15,\n      span: 4,\n      calendarId: 'learning',\n      title: 'Developer Summit',\n    },\n    {\n      month: 5,\n      day: 28,\n      span: 3,\n      calendarId: 'marketing',\n      title: 'Brand Workshop',\n    },\n\n    // Jun-Jul: Travel & Vacation\n    {\n      month: 6,\n      day: 10,\n      span: 4,\n      calendarId: 'support',\n      title: 'Quarterly Review',\n    },\n    {\n      month: 6,\n      day: 15,\n      span: 14,\n      calendarId: 'travel',\n      title: 'Summer Vacation (Europe)',\n    },\n    {\n      month: 7,\n      day: 8,\n      span: 4,\n      calendarId: 'team',\n      title: 'Mid-Year Review Week',\n    },\n    {\n      month: 7,\n      day: 20,\n      span: 5,\n      calendarId: 'wellness',\n      title: 'Hiking Trip',\n    },\n\n    // Aug-Sep: Projects & Learning\n    { month: 8, day: 12, span: 6, calendarId: 'team', title: 'Hackathon Week' },\n    {\n      month: 8,\n      day: 25,\n      span: 3,\n      calendarId: 'wellness',\n      title: 'Wellness Retreat',\n    },\n    {\n      month: 9,\n      day: 5,\n      span: 3,\n      calendarId: 'learning',\n      title: 'Leadership Training',\n    },\n    {\n      month: 9,\n      day: 18,\n      span: 4,\n      calendarId: 'travel',\n      title: 'Client Roadshow',\n    },\n\n    // Oct-Nov: Q4 Push\n    {\n      month: 10,\n      day: 10,\n      span: 5,\n      calendarId: 'team',\n      title: 'Q4 Planning Week',\n    },\n    {\n      month: 10,\n      day: 31,\n      span: 3,\n      calendarId: 'personal',\n      title: 'Halloween Weekend',\n    },\n    {\n      month: 11,\n      day: 15,\n      span: 5,\n      calendarId: 'marketing',\n      title: 'Black Friday Prep',\n    },\n    {\n      month: 11,\n      day: 24,\n      span: 3,\n      calendarId: 'personal',\n      title: 'Thanksgiving Holiday',\n    },\n\n    // Dec: Holidays\n    {\n      month: 12,\n      day: 10,\n      span: 3,\n      calendarId: 'team',\n      title: 'Year End Party Trip',\n    },\n    {\n      month: 12,\n      day: 24,\n      span: 5,\n      calendarId: 'personal',\n      title: 'Christmas Holiday',\n    },\n    {\n      month: 12,\n      day: 29,\n      span: 4,\n      calendarId: 'travel',\n      title: 'New Year Ski Trip',\n    },\n  ];\n\n  annualEvents.forEach((def, index) => {\n    try {\n      const start = Temporal.PlainDate.from({\n        year: currentYear,\n        month: def.month,\n        day: def.day,\n      });\n      events.push(\n        createAllDayEvent(\n          start,\n          def.span,\n          2000 + index, // Use a high base index to avoid collisions\n          def.calendarId,\n          def.title\n        )\n      );\n    } catch {\n      // Ignore invalid dates (e.g. leap years edge cases in simple config)\n    }\n  });\n\n  return events;\n};\n\nexport const generateMinimalSampleEvents = (): Event[] => {\n  const today = Temporal.Now.plainDateISO();\n  const windowStart = today.subtract({ days: 3 });\n  const events: Event[] = [];\n\n  const random = createRandom(54321);\n  const randomInt = createRandomInt(random);\n\n  for (let offset = 0; offset < 7; offset += 1) {\n    const date = windowStart.add({ days: offset });\n    const dayEvents = randomInt(1, 2);\n    for (let i = 0; i < dayEvents; i += 1) {\n      events.push(createTimedEvent(date, offset * 10 + i, randomInt));\n    }\n  }\n\n  // Add just a couple of all-day events\n  events.push(\n    createAllDayEvent(\n      today.subtract({ days: 1 }),\n      2,\n      999,\n      'team',\n      'Minimal Team Event'\n    )\n  );\n\n  return events;\n};\n"
  },
  {
    "path": "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>Day Flow Example</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/examples/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "lefthook.yml",
    "content": "pre-commit:\n  jobs:\n    - run: pnpm --filter @dayflow/core run check:css\n\n    - run: pnpm format\n      stage_fixed: true\n\n    - run: pnpm lint:fix\n      glob:\n        - '*.js'\n        - '*.jsx'\n        - '*.ts'\n        - '*.tsx'\n        - '*.json'\n        - '*.jsonc'\n        - '*.css'\n        - '*.md'\n      stage_fixed: true\n\n    - glob: 'website/**/*'\n      run: pnpm --dir website run build\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"dayflow-monorepo\",\n  \"version\": \"3.1.2\",\n  \"private\": true,\n  \"description\": \"Multi-framework calendar component library\",\n  \"scripts\": {\n    \"build\": \"turbo run build\",\n    \"clean\": \"turbo run clean && rimraf node_modules pnpm-lock.yaml\",\n    \"dev\": \"turbo run dev\",\n    \"format\": \"oxfmt .\",\n    \"format:check\": \"oxfmt . --check\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"prepare\": \"lefthook install\",\n    \"publish:all\": \"./scripts/publish.sh all\",\n    \"publish:cli\": \"./scripts/publish.sh cli\",\n    \"publish:dry\": \"./scripts/publish.sh all --dry-run\",\n    \"publish:main\": \"./scripts/publish.sh main\",\n    \"tag\": \"./scripts/git-tag.sh\",\n    \"test\": \"turbo run test\",\n    \"typecheck\": \"turbo run typecheck\",\n    \"version:update\": \"./scripts/update-versions.sh\"\n  },\n  \"devDependencies\": {\n    \"@dayflow/plugin-drag\": \"workspace:*\",\n    \"@dayflow/plugin-keyboard-shortcuts\": \"workspace:*\",\n    \"@dayflow/plugin-localization\": \"workspace:*\",\n    \"@dayflow/plugin-sidebar\": \"workspace:*\",\n    \"@dayflow/ui-range-picker\": \"workspace:*\",\n    \"@preact/preset-vite\": \"catalog:\",\n    \"@tailwindcss/postcss\": \"catalog:\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"autoprefixer\": \"catalog:\",\n    \"lefthook\": \"^2.1.1\",\n    \"lucide-react\": \"catalog:\",\n    \"oxfmt\": \"^0.35.0\",\n    \"oxlint\": \"^1.50.0\",\n    \"postcss-import\": \"catalog:\",\n    \"preact\": \"catalog:\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"tailwindcss\": \"catalog:\",\n    \"temporal-polyfill\": \"catalog:\",\n    \"tsc-alias\": \"^1.8.16\",\n    \"turbo\": \"^2.8.10\",\n    \"typescript\": \"catalog:\"\n  },\n  \"packageManager\": \"pnpm@9.15.0\"\n}\n"
  },
  {
    "path": "packages/angular/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/angular/README.md",
    "content": "# DayFlow Angular\n\nA flexible and feature-rich Angular calendar component library with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/angular?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/angular)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/dayflow/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/dayflow)](https://github.com/dayflow-js/dayflow/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/dayflow/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n"
  },
  {
    "path": "packages/angular/ng-packagr.json",
    "content": "{\n  \"$schema\": \"./node_modules/ng-packagr/ng-package.schema.json\",\n  \"lib\": {\n    \"entryFile\": \"src/public-api.ts\"\n  },\n  \"assets\": [\"README.md\", \"LICENSE\"],\n  \"dest\": \"dist\"\n}\n"
  },
  {
    "path": "packages/angular/package.json",
    "content": "{\n  \"name\": \"@dayflow/angular\",\n  \"version\": \"3.6.2\",\n  \"description\": \"Angular adapter for DayFlow calendar\",\n  \"files\": [\n    \"**/*.mjs\",\n    \"**/*.d.ts\",\n    \"**/*.json\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"scripts\": {\n    \"build\": \"ng-packagr -p ng-packagr.json\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@angular/common\": \"^18.2.0\",\n    \"@angular/compiler\": \"^18.2.0\",\n    \"@angular/compiler-cli\": \"^18.2.0\",\n    \"@angular/core\": \"^18.2.0\",\n    \"@angular/platform-browser\": \"^18.2.0\",\n    \"@angular/platform-browser-dynamic\": \"^18.2.0\",\n    \"@dayflow/core\": \"workspace:*\",\n    \"ng-packagr\": \"^18.2.1\",\n    \"rimraf\": \"catalog:\",\n    \"rxjs\": \"^7.8.1\",\n    \"tslib\": \"catalog:\",\n    \"typescript\": \"~5.5.0\",\n    \"zone.js\": \"~0.14.10\"\n  },\n  \"peerDependencies\": {\n    \"@angular/common\": \">=14.0.0\",\n    \"@angular/core\": \">=14.0.0\",\n    \"@dayflow/core\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/angular/src/lib/day-flow-calendar.component.ts",
    "content": "import {\n  ElementRef,\n  OnChanges,\n  OnDestroy,\n  AfterViewInit,\n  SimpleChanges,\n  TemplateRef,\n  ChangeDetectorRef,\n  Component,\n  Input,\n  ViewChild,\n  ChangeDetectionStrategy,\n} from '@angular/core';\nimport type {\n  ICalendarApp,\n  CalendarAppConfig,\n  CalendarAppConfigSyncSnapshot,\n  UseCalendarAppReturn,\n  CustomRendering,\n  EventDetailContentProps,\n  EventDetailDialogProps,\n  CreateCalendarDialogProps,\n  TitleBarSlotProps,\n  EventContentSlotArgs,\n  ColorPickerProps,\n  CreateCalendarDialogColorPickerProps,\n  CalendarHeaderProps,\n  EventContextMenuSlotArgs,\n  GridContextMenuSlotArgs,\n  CalendarSearchProps,\n  MobileEventProps,\n} from '@dayflow/core';\nimport {\n  CalendarRenderer,\n  CalendarApp,\n  createConfigSyncSnapshot,\n  createNormalizedCalendarAppConfigGetter,\n  syncCalendarAppConfig,\n} from '@dayflow/core';\n\n@Component({\n  selector: 'dayflow-calendar',\n  template: `\n    <div #container class=\"df-calendar-wrapper\"></div>\n\n    <!-- Hidden area to render Angular templates before they are portaled -->\n    <div style=\"display: none\">\n      <ng-container *ngFor=\"let rendering of customRenderings; trackBy: trackById\">\n        <div\n          *ngIf=\"getTemplate(rendering.generatorName)\"\n          [dayflowPortal]=\"rendering.containerEl\"\n        >\n          <ng-container\n            *ngTemplateOutlet=\"\n              getTemplate(rendering.generatorName)!;\n              context: { $implicit: rendering.generatorArgs }\n            \"\n          ></ng-container>\n        </div>\n      </ng-container>\n    </div>\n  `,\n  changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DayFlowCalendarComponent\n  implements AfterViewInit, OnChanges, OnDestroy\n{\n  @Input() calendar!: ICalendarApp | UseCalendarAppReturn | CalendarAppConfig;\n\n  // Templates for custom content injection\n  @Input() eventContentDay?: TemplateRef<EventContentSlotArgs>;\n  @Input() eventContentWeek?: TemplateRef<EventContentSlotArgs>;\n  @Input() eventContentMonth?: TemplateRef<EventContentSlotArgs>;\n  @Input() eventContentYear?: TemplateRef<EventContentSlotArgs>;\n  @Input() eventContentAllDayDay?: TemplateRef<EventContentSlotArgs>;\n  @Input() eventContentAllDayWeek?: TemplateRef<EventContentSlotArgs>;\n  @Input() eventContentAllDayMonth?: TemplateRef<EventContentSlotArgs>;\n  @Input() eventContentAllDayYear?: TemplateRef<EventContentSlotArgs>;\n  @Input() eventDetailContent?: TemplateRef<EventDetailContentProps>;\n  @Input() eventDetailDialog?: TemplateRef<EventDetailDialogProps>;\n  @Input() createCalendarDialog?: TemplateRef<CreateCalendarDialogProps>;\n  @Input() titleBarSlot?: TemplateRef<TitleBarSlotProps>;\n  @Input() colorPicker?: TemplateRef<ColorPickerProps>;\n  @Input()\n  createCalendarDialogColorPicker?: TemplateRef<CreateCalendarDialogColorPickerProps>;\n  @Input() calendarHeader?: TemplateRef<CalendarHeaderProps>;\n  @Input() eventContextMenu?: TemplateRef<EventContextMenuSlotArgs>;\n  @Input() gridContextMenu?: TemplateRef<GridContextMenuSlotArgs>;\n  @Input() mobileEventDetail?: TemplateRef<MobileEventProps>;\n  @Input() collapsedSafeAreaLeft?: number;\n  @Input() search?: CalendarSearchProps;\n\n  @ViewChild('container') container!: ElementRef<HTMLElement>;\n\n  customRenderings: CustomRendering[] = [];\n  private renderer?: CalendarRenderer;\n  private unsubscribe?: () => void;\n  private internalApp?: CalendarApp;\n  private getNormalizedInternalConfig?: () => CalendarAppConfig;\n  private internalConfigSyncSnapshot?: CalendarAppConfigSyncSnapshot;\n\n  constructor(private cdr: ChangeDetectorRef) {}\n\n  private get app(): ICalendarApp {\n    if (this.internalApp) {\n      return this.internalApp;\n    }\n\n    if (this.calendar instanceof CalendarApp) {\n      return this.calendar;\n    }\n\n    if ((this.calendar as { app?: ICalendarApp; views?: unknown[] }).app) {\n      return (this.calendar as { app?: ICalendarApp; views?: unknown[] }).app!;\n    }\n\n    // If it's a config object, we create an internal instance\n    if (DayFlowCalendarComponent.isCalendarConfig(this.calendar)) {\n      return this.getOrCreateInternalApp();\n    }\n\n    return this.calendar as ICalendarApp;\n  }\n\n  ngAfterViewInit() {\n    this.initCalendar();\n  }\n\n  ngOnChanges(changes: SimpleChanges) {\n    if (changes['calendar'] && !changes['calendar'].firstChange) {\n      if (this.canSyncInternalCalendarConfig(changes['calendar'])) {\n        this.syncInternalCalendarConfig();\n      } else {\n        this.resetInternalCalendarState();\n        this.destroyCalendar();\n        this.initCalendar();\n      }\n    } else if (this.renderer) {\n      if (changes['collapsedSafeAreaLeft'] || changes['search']) {\n        this.renderer.setProps(this.getRendererProps());\n      }\n      const slotKeys = [\n        'eventContentDay',\n        'eventContentWeek',\n        'eventContentMonth',\n        'eventContentYear',\n        'eventContentAllDayDay',\n        'eventContentAllDayWeek',\n        'eventContentAllDayMonth',\n        'eventContentAllDayYear',\n        'eventDetailContent',\n        'eventDetailDialog',\n        'createCalendarDialog',\n        'titleBarSlot',\n        'colorPicker',\n        'createCalendarDialogColorPicker',\n        'calendarHeader',\n        'eventContextMenu',\n        'gridContextMenu',\n        'mobileEventDetail',\n      ];\n      if (slotKeys.some(key => changes[key])) {\n        const activeOverrides = this.getActiveOverrides();\n        this.renderer.getCustomRenderingStore().setOverrides(activeOverrides);\n        this.app.setOverrides(activeOverrides);\n      }\n    }\n  }\n\n  ngOnDestroy() {\n    this.destroyCalendar();\n  }\n\n  private getRendererProps(): Record<string, unknown> {\n    return {\n      collapsedSafeAreaLeft: this.collapsedSafeAreaLeft,\n      search: this.search,\n    };\n  }\n\n  private initCalendar() {\n    if (!this.container || !this.calendar) {\n      return;\n    }\n\n    const activeOverrides = this.getActiveOverrides();\n    this.renderer = new CalendarRenderer(this.app, activeOverrides);\n    this.renderer.setProps(this.getRendererProps());\n    this.renderer.mount(this.container.nativeElement);\n    this.app.setOverrides(activeOverrides);\n\n    this.unsubscribe = this.renderer\n      .getCustomRenderingStore()\n      .subscribe(renderings => {\n        this.customRenderings = [...renderings.values()];\n        this.cdr.markForCheck();\n      });\n  }\n\n  private getActiveOverrides(): string[] {\n    const templateInputs: Record<string, TemplateRef<unknown> | undefined> = {\n      eventContentDay: this.eventContentDay,\n      eventContentWeek: this.eventContentWeek,\n      eventContentMonth: this.eventContentMonth,\n      eventContentYear: this.eventContentYear,\n      eventContentAllDayDay: this.eventContentAllDayDay,\n      eventContentAllDayWeek: this.eventContentAllDayWeek,\n      eventContentAllDayMonth: this.eventContentAllDayMonth,\n      eventContentAllDayYear: this.eventContentAllDayYear,\n      eventDetailContent: this.eventDetailContent,\n      eventDetailDialog: this.eventDetailDialog,\n      createCalendarDialog: this.createCalendarDialog,\n      titleBarSlot: this.titleBarSlot,\n      colorPicker: this.colorPicker,\n      createCalendarDialogColorPicker: this.createCalendarDialogColorPicker,\n      calendarHeader: this.calendarHeader,\n      eventContextMenu: this.eventContextMenu,\n      gridContextMenu: this.gridContextMenu,\n      mobileEventDetail: this.mobileEventDetail,\n    };\n    return Object.keys(templateInputs).filter(\n      key => templateInputs[key] !== null && templateInputs[key] !== undefined\n    );\n  }\n\n  private destroyCalendar() {\n    if (this.unsubscribe) {\n      this.unsubscribe();\n    }\n    if (this.renderer) {\n      this.renderer.unmount();\n    }\n    this.unsubscribe = undefined;\n    this.renderer = undefined;\n  }\n\n  private static isCalendarConfig(value: unknown): value is CalendarAppConfig {\n    return (\n      !!value &&\n      typeof value === 'object' &&\n      'views' in value &&\n      !('app' in value)\n    );\n  }\n\n  private getOrCreateInternalApp(): CalendarApp {\n    if (!this.internalApp) {\n      this.getNormalizedInternalConfig =\n        createNormalizedCalendarAppConfigGetter(\n          () => this.calendar as CalendarAppConfig\n        );\n      const normalizedConfig = this.getNormalizedInternalConfig();\n      this.internalApp = new CalendarApp(normalizedConfig);\n      this.internalConfigSyncSnapshot =\n        createConfigSyncSnapshot(normalizedConfig);\n    }\n\n    return this.internalApp;\n  }\n\n  private canSyncInternalCalendarConfig(\n    change: SimpleChanges['calendar']\n  ): boolean {\n    return (\n      DayFlowCalendarComponent.isCalendarConfig(change.currentValue) &&\n      DayFlowCalendarComponent.isCalendarConfig(change.previousValue) &&\n      !!this.internalApp &&\n      !!this.getNormalizedInternalConfig &&\n      !!this.internalConfigSyncSnapshot\n    );\n  }\n\n  private syncInternalCalendarConfig() {\n    if (\n      !this.internalApp ||\n      !this.getNormalizedInternalConfig ||\n      !this.internalConfigSyncSnapshot\n    ) {\n      return;\n    }\n\n    this.internalConfigSyncSnapshot = syncCalendarAppConfig(\n      this.internalApp,\n      this.internalConfigSyncSnapshot,\n      this.getNormalizedInternalConfig()\n    );\n  }\n\n  private resetInternalCalendarState() {\n    this.internalApp = undefined;\n    this.getNormalizedInternalConfig = undefined;\n    this.internalConfigSyncSnapshot = undefined;\n  }\n\n  getTemplate(name: string): TemplateRef<unknown> | null {\n    // Switch avoids allocating a new Record on every change-detection cycle.\n    switch (name) {\n      case 'eventContentDay': {\n        return this.eventContentDay ?? null;\n      }\n      case 'eventContentWeek': {\n        return this.eventContentWeek ?? null;\n      }\n      case 'eventContentMonth': {\n        return this.eventContentMonth ?? null;\n      }\n      case 'eventContentYear': {\n        return this.eventContentYear ?? null;\n      }\n      case 'eventContentAllDayDay': {\n        return this.eventContentAllDayDay ?? null;\n      }\n      case 'eventContentAllDayWeek': {\n        return this.eventContentAllDayWeek ?? null;\n      }\n      case 'eventContentAllDayMonth': {\n        return this.eventContentAllDayMonth ?? null;\n      }\n      case 'eventContentAllDayYear': {\n        return this.eventContentAllDayYear ?? null;\n      }\n      case 'eventDetailContent': {\n        return this.eventDetailContent ?? null;\n      }\n      case 'eventDetailDialog': {\n        return this.eventDetailDialog ?? null;\n      }\n      case 'createCalendarDialog': {\n        return this.createCalendarDialog ?? null;\n      }\n      case 'titleBarSlot': {\n        return this.titleBarSlot ?? null;\n      }\n      case 'colorPicker': {\n        return this.colorPicker ?? null;\n      }\n      case 'createCalendarDialogColorPicker': {\n        return this.createCalendarDialogColorPicker ?? null;\n      }\n      case 'calendarHeader': {\n        return this.calendarHeader ?? null;\n      }\n      case 'eventContextMenu': {\n        return this.eventContextMenu ?? null;\n      }\n      case 'gridContextMenu': {\n        return this.gridContextMenu ?? null;\n      }\n      case 'mobileEventDetail': {\n        return this.mobileEventDetail ?? null;\n      }\n      default: {\n        return null;\n      }\n    }\n  }\n\n  // eslint-disable-next-line class-methods-use-this\n  trackById(_index: number, item: CustomRendering) {\n    return item.id;\n  }\n}\n"
  },
  {
    "path": "packages/angular/src/lib/day-flow-calendar.module.ts",
    "content": "import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\n\nimport { DayFlowCalendarComponent } from './day-flow-calendar.component';\nimport { DayFlowPortalDirective } from './day-flow-portal.directive';\n\n@NgModule({\n  declarations: [DayFlowCalendarComponent, DayFlowPortalDirective],\n  imports: [CommonModule],\n  exports: [DayFlowCalendarComponent, DayFlowPortalDirective],\n})\n// eslint-disable-next-line @typescript-eslint/no-extraneous-class\nexport class DayFlowCalendarModule {}\n"
  },
  {
    "path": "packages/angular/src/lib/day-flow-portal.directive.ts",
    "content": "import {\n  ElementRef,\n  OnChanges,\n  SimpleChanges,\n  OnDestroy,\n  Directive,\n  Input,\n} from '@angular/core';\n\n@Directive({\n  selector: '[dayflowPortal]',\n})\nexport class DayFlowPortalDirective implements OnChanges, OnDestroy {\n  @Input('dayflowPortal') targetEl!: HTMLElement;\n\n  constructor(private el: ElementRef) {}\n\n  ngOnChanges(changes: SimpleChanges) {\n    if (changes['targetEl'] && this.targetEl) {\n      this.targetEl.append(this.el.nativeElement);\n    }\n  }\n\n  ngOnDestroy() {\n    if (this.el.nativeElement.parentNode === this.targetEl) {\n      this.el.nativeElement.remove();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/angular/src/public-api.ts",
    "content": "/*\n * Public API Surface of @dayflow/angular\n */\n\nexport * from './lib/day-flow-calendar.component';\nexport * from './lib/day-flow-calendar.module';\nexport * from './lib/day-flow-portal.directive';\n\nexport * from '@dayflow/core';\n"
  },
  {
    "path": "packages/angular/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./dist/out-tsc\",\n    \"types\": [],\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@dayflow/core\": [\"../../packages/core/dist/index.d.ts\"]\n    }\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/core/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/core/README.md",
    "content": "# DayFlow Core\n\nThe core engine of DayFlow, a flexible and feature-rich calendar library with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/core?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/core)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/dayflow/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/dayflow)](https://github.com/dayflow-js/dayflow/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/dayflow/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n"
  },
  {
    "path": "packages/core/bundle-analysis.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    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>Rollup Visualizer</title>\n    <style>\n      :root {\n        --font-family:\n          -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,\n          'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',\n          'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\n        --background-color: #2b2d42;\n        --text-color: #edf2f4;\n      }\n\n      html {\n        box-sizing: border-box;\n      }\n\n      *,\n      *:before,\n      *:after {\n        box-sizing: inherit;\n      }\n\n      html {\n        background-color: var(--background-color);\n        color: var(--text-color);\n        font-family: var(--font-family);\n      }\n\n      body {\n        padding: 0;\n        margin: 0;\n      }\n\n      html,\n      body {\n        height: 100%;\n        width: 100%;\n        overflow: hidden;\n      }\n\n      body {\n        display: flex;\n        flex-direction: column;\n      }\n\n      svg {\n        vertical-align: middle;\n        width: 100%;\n        height: 100%;\n        max-height: 100vh;\n      }\n\n      main {\n        flex-grow: 1;\n        height: 100vh;\n        padding: 20px;\n      }\n\n      .tooltip {\n        position: absolute;\n        z-index: 1070;\n        border: 2px solid;\n        border-radius: 5px;\n        padding: 5px;\n        font-size: 0.875rem;\n        background-color: var(--background-color);\n        color: var(--text-color);\n      }\n\n      .tooltip-hidden {\n        visibility: hidden;\n        opacity: 0;\n      }\n\n      .sidebar {\n        position: fixed;\n        top: 0;\n        left: 0;\n        right: 0;\n        display: flex;\n        flex-direction: row;\n        font-size: 0.7rem;\n        align-items: center;\n        margin: 0 50px;\n        height: 20px;\n      }\n\n      .size-selectors {\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n      }\n\n      .size-selector {\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n        justify-content: center;\n        margin-right: 1rem;\n      }\n      .size-selector input {\n        margin: 0 0.3rem 0 0;\n      }\n\n      .filters {\n        flex: 1;\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n      }\n\n      .module-filters {\n        display: flex;\n        flex-grow: 1;\n      }\n\n      .module-filter {\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n        justify-content: center;\n        flex: 1;\n      }\n      .module-filter input {\n        flex: 1;\n        height: 1rem;\n        padding: 0.01rem;\n        font-size: 0.7rem;\n        margin-left: 0.3rem;\n      }\n      .module-filter + .module-filter {\n        margin-left: 0.5rem;\n      }\n\n      .node {\n        cursor: pointer;\n      }\n    </style>\n  </head>\n  <body>\n    <main></main>\n    <script>\n      /*<!--*/\n      var drawChart = (function (exports) {\n        'use strict';\n\n        var n,\n          l$1,\n          u$2,\n          i$1,\n          r$1,\n          o$1,\n          e$1,\n          f$2,\n          c$1,\n          s$1,\n          a$1,\n          h$1,\n          p$1 = {},\n          v$1 = [],\n          y$1 =\n            /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,\n          w$1 = Array.isArray;\n        function d$1(n, l) {\n          for (var u in l) n[u] = l[u];\n          return n;\n        }\n        function g(n) {\n          n && n.parentNode && n.parentNode.removeChild(n);\n        }\n        function _$1(l, u, t) {\n          var i,\n            r,\n            o,\n            e = {};\n          for (o in u)\n            'key' == o ? (i = u[o]) : 'ref' == o ? (r = u[o]) : (e[o] = u[o]);\n          if (\n            (arguments.length > 2 &&\n              (e.children = arguments.length > 3 ? n.call(arguments, 2) : t),\n            'function' == typeof l && null != l.defaultProps)\n          )\n            for (o in l.defaultProps)\n              void 0 === e[o] && (e[o] = l.defaultProps[o]);\n          return m$1(l, e, i, r, null);\n        }\n        function m$1(n, t, i, r, o) {\n          var e = {\n            type: n,\n            props: t,\n            key: i,\n            ref: r,\n            __k: null,\n            __: null,\n            __b: 0,\n            __e: null,\n            __c: null,\n            constructor: void 0,\n            __v: null == o ? ++u$2 : o,\n            __i: -1,\n            __u: 0,\n          };\n          return (null == o && null != l$1.vnode && l$1.vnode(e), e);\n        }\n        function k$1(n) {\n          return n.children;\n        }\n        function x$1(n, l) {\n          ((this.props = n), (this.context = l));\n        }\n        function S(n, l) {\n          if (null == l) return n.__ ? S(n.__, n.__i + 1) : null;\n          for (var u; l < n.__k.length; l++)\n            if (null != (u = n.__k[l]) && null != u.__e) return u.__e;\n          return 'function' == typeof n.type ? S(n) : null;\n        }\n        function C$1(n) {\n          var l, u;\n          if (null != (n = n.__) && null != n.__c) {\n            for (n.__e = n.__c.base = null, l = 0; l < n.__k.length; l++)\n              if (null != (u = n.__k[l]) && null != u.__e) {\n                n.__e = n.__c.base = u.__e;\n                break;\n              }\n            return C$1(n);\n          }\n        }\n        function M(n) {\n          ((!n.__d && (n.__d = true) && i$1.push(n) && !$.__r++) ||\n            r$1 != l$1.debounceRendering) &&\n            ((r$1 = l$1.debounceRendering) || o$1)($);\n        }\n        function $() {\n          for (var n, u, t, r, o, f, c, s = 1; i$1.length; )\n            (i$1.length > s && i$1.sort(e$1),\n              (n = i$1.shift()),\n              (s = i$1.length),\n              n.__d &&\n                ((t = void 0),\n                (o = (r = (u = n).__v).__e),\n                (f = []),\n                (c = []),\n                u.__P &&\n                  (((t = d$1({}, r)).__v = r.__v + 1),\n                  l$1.vnode && l$1.vnode(t),\n                  O(\n                    u.__P,\n                    t,\n                    r,\n                    u.__n,\n                    u.__P.namespaceURI,\n                    32 & r.__u ? [o] : null,\n                    f,\n                    null == o ? S(r) : o,\n                    !!(32 & r.__u),\n                    c\n                  ),\n                  (t.__v = r.__v),\n                  (t.__.__k[t.__i] = t),\n                  z$1(f, t, c),\n                  t.__e != o && C$1(t))));\n          $.__r = 0;\n        }\n        function I(n, l, u, t, i, r, o, e, f, c, s) {\n          var a,\n            h,\n            y,\n            w,\n            d,\n            g,\n            _ = (t && t.__k) || v$1,\n            m = l.length;\n          for (f = P(u, l, _, f, m), a = 0; a < m; a++)\n            null != (y = u.__k[a]) &&\n              ((h = -1 == y.__i ? p$1 : _[y.__i] || p$1),\n              (y.__i = a),\n              (g = O(n, y, h, i, r, o, e, f, c, s)),\n              (w = y.__e),\n              y.ref &&\n                h.ref != y.ref &&\n                (h.ref && q$1(h.ref, null, y), s.push(y.ref, y.__c || w, y)),\n              null == d && null != w && (d = w),\n              4 & y.__u || h.__k === y.__k\n                ? (f = A$1(y, f, n))\n                : 'function' == typeof y.type && void 0 !== g\n                  ? (f = g)\n                  : w && (f = w.nextSibling),\n              (y.__u &= -7));\n          return ((u.__e = d), f);\n        }\n        function P(n, l, u, t, i) {\n          var r,\n            o,\n            e,\n            f,\n            c,\n            s = u.length,\n            a = s,\n            h = 0;\n          for (n.__k = new Array(i), r = 0; r < i; r++)\n            null != (o = l[r]) &&\n            'boolean' != typeof o &&\n            'function' != typeof o\n              ? ((f = r + h),\n                ((o = n.__k[r] =\n                  'string' == typeof o ||\n                  'number' == typeof o ||\n                  'bigint' == typeof o ||\n                  o.constructor == String\n                    ? m$1(null, o, null, null, null)\n                    : w$1(o)\n                      ? m$1(k$1, { children: o }, null, null, null)\n                      : null == o.constructor && o.__b > 0\n                        ? m$1(\n                            o.type,\n                            o.props,\n                            o.key,\n                            o.ref ? o.ref : null,\n                            o.__v\n                          )\n                        : o).__ = n),\n                (o.__b = n.__b + 1),\n                (e = null),\n                -1 != (c = o.__i = L(o, u, f, a)) &&\n                  (a--, (e = u[c]) && (e.__u |= 2)),\n                null == e || null == e.__v\n                  ? (-1 == c && (i > s ? h-- : i < s && h++),\n                    'function' != typeof o.type && (o.__u |= 4))\n                  : c != f &&\n                    (c == f - 1\n                      ? h--\n                      : c == f + 1\n                        ? h++\n                        : (c > f ? h-- : h++, (o.__u |= 4))))\n              : (n.__k[r] = null);\n          if (a)\n            for (r = 0; r < s; r++)\n              null != (e = u[r]) &&\n                0 == (2 & e.__u) &&\n                (e.__e == t && (t = S(e)), B$1(e, e));\n          return t;\n        }\n        function A$1(n, l, u) {\n          var t, i;\n          if ('function' == typeof n.type) {\n            for (t = n.__k, i = 0; t && i < t.length; i++)\n              t[i] && ((t[i].__ = n), (l = A$1(t[i], l, u)));\n            return l;\n          }\n          n.__e != l &&\n            (l && n.type && !u.contains(l) && (l = S(n)),\n            u.insertBefore(n.__e, l || null),\n            (l = n.__e));\n          do {\n            l = l && l.nextSibling;\n          } while (null != l && 8 == l.nodeType);\n          return l;\n        }\n        function L(n, l, u, t) {\n          var i,\n            r,\n            o = n.key,\n            e = n.type,\n            f = l[u];\n          if (\n            (null === f && null == n.key) ||\n            (f && o == f.key && e == f.type && 0 == (2 & f.__u))\n          )\n            return u;\n          if (t > (null != f && 0 == (2 & f.__u) ? 1 : 0))\n            for (i = u - 1, r = u + 1; i >= 0 || r < l.length; ) {\n              if (i >= 0) {\n                if ((f = l[i]) && 0 == (2 & f.__u) && o == f.key && e == f.type)\n                  return i;\n                i--;\n              }\n              if (r < l.length) {\n                if ((f = l[r]) && 0 == (2 & f.__u) && o == f.key && e == f.type)\n                  return r;\n                r++;\n              }\n            }\n          return -1;\n        }\n        function T$1(n, l, u) {\n          '-' == l[0]\n            ? n.setProperty(l, null == u ? '' : u)\n            : (n[l] =\n                null == u\n                  ? ''\n                  : 'number' != typeof u || y$1.test(l)\n                    ? u\n                    : u + 'px');\n        }\n        function j$1(n, l, u, t, i) {\n          var r, o;\n          n: if ('style' == l)\n            if ('string' == typeof u) n.style.cssText = u;\n            else {\n              if (('string' == typeof t && (n.style.cssText = t = ''), t))\n                for (l in t) (u && l in u) || T$1(n.style, l, '');\n              if (u) for (l in u) (t && u[l] == t[l]) || T$1(n.style, l, u[l]);\n            }\n          else if ('o' == l[0] && 'n' == l[1])\n            ((r = l != (l = l.replace(f$2, '$1'))),\n              (o = l.toLowerCase()),\n              (l =\n                o in n || 'onFocusOut' == l || 'onFocusIn' == l\n                  ? o.slice(2)\n                  : l.slice(2)),\n              n.l || (n.l = {}),\n              (n.l[l + r] = u),\n              u\n                ? t\n                  ? (u.u = t.u)\n                  : ((u.u = c$1), n.addEventListener(l, r ? a$1 : s$1, r))\n                : n.removeEventListener(l, r ? a$1 : s$1, r));\n          else {\n            if ('http://www.w3.org/2000/svg' == i)\n              l = l.replace(/xlink(H|:h)/, 'h').replace(/sName$/, 's');\n            else if (\n              'width' != l &&\n              'height' != l &&\n              'href' != l &&\n              'list' != l &&\n              'form' != l &&\n              'tabIndex' != l &&\n              'download' != l &&\n              'rowSpan' != l &&\n              'colSpan' != l &&\n              'role' != l &&\n              'popover' != l &&\n              l in n\n            )\n              try {\n                n[l] = null == u ? '' : u;\n                break n;\n              } catch (n) {}\n            'function' == typeof u ||\n              (null == u || (false === u && '-' != l[4])\n                ? n.removeAttribute(l)\n                : n.setAttribute(l, 'popover' == l && 1 == u ? '' : u));\n          }\n        }\n        function F(n) {\n          return function (u) {\n            if (this.l) {\n              var t = this.l[u.type + n];\n              if (null == u.t) u.t = c$1++;\n              else if (u.t < t.u) return;\n              return t(l$1.event ? l$1.event(u) : u);\n            }\n          };\n        }\n        function O(n, u, t, i, r, o, e, f, c, s) {\n          var a,\n            h,\n            p,\n            v,\n            y,\n            _,\n            m,\n            b,\n            S,\n            C,\n            M,\n            $,\n            P,\n            A,\n            H,\n            L,\n            T,\n            j = u.type;\n          if (null != u.constructor) return null;\n          (128 & t.__u && ((c = !!(32 & t.__u)), (o = [(f = u.__e = t.__e)])),\n            (a = l$1.__b) && a(u));\n          n: if ('function' == typeof j)\n            try {\n              if (\n                ((b = u.props),\n                (S = 'prototype' in j && j.prototype.render),\n                (C = (a = j.contextType) && i[a.__c]),\n                (M = a ? (C ? C.props.value : a.__) : i),\n                t.__c\n                  ? (m = (h = u.__c = t.__c).__ = h.__E)\n                  : (S\n                      ? (u.__c = h = new j(b, M))\n                      : ((u.__c = h = new x$1(b, M)),\n                        (h.constructor = j),\n                        (h.render = D$1)),\n                    C && C.sub(h),\n                    (h.props = b),\n                    h.state || (h.state = {}),\n                    (h.context = M),\n                    (h.__n = i),\n                    (p = h.__d = !0),\n                    (h.__h = []),\n                    (h._sb = [])),\n                S && null == h.__s && (h.__s = h.state),\n                S &&\n                  null != j.getDerivedStateFromProps &&\n                  (h.__s == h.state && (h.__s = d$1({}, h.__s)),\n                  d$1(h.__s, j.getDerivedStateFromProps(b, h.__s))),\n                (v = h.props),\n                (y = h.state),\n                (h.__v = u),\n                p)\n              )\n                (S &&\n                  null == j.getDerivedStateFromProps &&\n                  null != h.componentWillMount &&\n                  h.componentWillMount(),\n                  S &&\n                    null != h.componentDidMount &&\n                    h.__h.push(h.componentDidMount));\n              else {\n                if (\n                  (S &&\n                    null == j.getDerivedStateFromProps &&\n                    b !== v &&\n                    null != h.componentWillReceiveProps &&\n                    h.componentWillReceiveProps(b, M),\n                  (!h.__e &&\n                    null != h.shouldComponentUpdate &&\n                    !1 === h.shouldComponentUpdate(b, h.__s, M)) ||\n                    u.__v == t.__v)\n                ) {\n                  for (\n                    u.__v != t.__v &&\n                      ((h.props = b), (h.state = h.__s), (h.__d = !1)),\n                      u.__e = t.__e,\n                      u.__k = t.__k,\n                      u.__k.some(function (n) {\n                        n && (n.__ = u);\n                      }),\n                      $ = 0;\n                    $ < h._sb.length;\n                    $++\n                  )\n                    h.__h.push(h._sb[$]);\n                  ((h._sb = []), h.__h.length && e.push(h));\n                  break n;\n                }\n                (null != h.componentWillUpdate &&\n                  h.componentWillUpdate(b, h.__s, M),\n                  S &&\n                    null != h.componentDidUpdate &&\n                    h.__h.push(function () {\n                      h.componentDidUpdate(v, y, _);\n                    }));\n              }\n              if (\n                ((h.context = M),\n                (h.props = b),\n                (h.__P = n),\n                (h.__e = !1),\n                (P = l$1.__r),\n                (A = 0),\n                S)\n              ) {\n                for (\n                  h.state = h.__s,\n                    h.__d = !1,\n                    P && P(u),\n                    a = h.render(h.props, h.state, h.context),\n                    H = 0;\n                  H < h._sb.length;\n                  H++\n                )\n                  h.__h.push(h._sb[H]);\n                h._sb = [];\n              } else\n                do {\n                  ((h.__d = !1),\n                    P && P(u),\n                    (a = h.render(h.props, h.state, h.context)),\n                    (h.state = h.__s));\n                } while (h.__d && ++A < 25);\n              ((h.state = h.__s),\n                null != h.getChildContext &&\n                  (i = d$1(d$1({}, i), h.getChildContext())),\n                S &&\n                  !p &&\n                  null != h.getSnapshotBeforeUpdate &&\n                  (_ = h.getSnapshotBeforeUpdate(v, y)),\n                (L = a),\n                null != a &&\n                  a.type === k$1 &&\n                  null == a.key &&\n                  (L = N(a.props.children)),\n                (f = I(n, w$1(L) ? L : [L], u, t, i, r, o, e, f, c, s)),\n                (h.base = u.__e),\n                (u.__u &= -161),\n                h.__h.length && e.push(h),\n                m && (h.__E = h.__ = null));\n            } catch (n) {\n              if (((u.__v = null), c || null != o))\n                if (n.then) {\n                  for (\n                    u.__u |= c ? 160 : 128;\n                    f && 8 == f.nodeType && f.nextSibling;\n                  )\n                    f = f.nextSibling;\n                  ((o[o.indexOf(f)] = null), (u.__e = f));\n                } else for (T = o.length; T--; ) g(o[T]);\n              else ((u.__e = t.__e), (u.__k = t.__k));\n              l$1.__e(n, u, t);\n            }\n          else\n            null == o && u.__v == t.__v\n              ? ((u.__k = t.__k), (u.__e = t.__e))\n              : (f = u.__e = V(t.__e, u, t, i, r, o, e, c, s));\n          return ((a = l$1.diffed) && a(u), 128 & u.__u ? void 0 : f);\n        }\n        function z$1(n, u, t) {\n          for (var i = 0; i < t.length; i++) q$1(t[i], t[++i], t[++i]);\n          (l$1.__c && l$1.__c(u, n),\n            n.some(function (u) {\n              try {\n                ((n = u.__h),\n                  (u.__h = []),\n                  n.some(function (n) {\n                    n.call(u);\n                  }));\n              } catch (n) {\n                l$1.__e(n, u.__v);\n              }\n            }));\n        }\n        function N(n) {\n          return 'object' != typeof n || null == n || (n.__b && n.__b > 0)\n            ? n\n            : w$1(n)\n              ? n.map(N)\n              : d$1({}, n);\n        }\n        function V(u, t, i, r, o, e, f, c, s) {\n          var a,\n            h,\n            v,\n            y,\n            d,\n            _,\n            m,\n            b = i.props,\n            k = t.props,\n            x = t.type;\n          if (\n            ('svg' == x\n              ? (o = 'http://www.w3.org/2000/svg')\n              : 'math' == x\n                ? (o = 'http://www.w3.org/1998/Math/MathML')\n                : o || (o = 'http://www.w3.org/1999/xhtml'),\n            null != e)\n          )\n            for (a = 0; a < e.length; a++)\n              if (\n                (d = e[a]) &&\n                'setAttribute' in d == !!x &&\n                (x ? d.localName == x : 3 == d.nodeType)\n              ) {\n                ((u = d), (e[a] = null));\n                break;\n              }\n          if (null == u) {\n            if (null == x) return document.createTextNode(k);\n            ((u = document.createElementNS(o, x, k.is && k)),\n              c && (l$1.__m && l$1.__m(t, e), (c = false)),\n              (e = null));\n          }\n          if (null == x) b === k || (c && u.data == k) || (u.data = k);\n          else {\n            if (\n              ((e = e && n.call(u.childNodes)),\n              (b = i.props || p$1),\n              !c && null != e)\n            )\n              for (b = {}, a = 0; a < u.attributes.length; a++)\n                b[(d = u.attributes[a]).name] = d.value;\n            for (a in b)\n              if (((d = b[a]), 'children' == a));\n              else if ('dangerouslySetInnerHTML' == a) v = d;\n              else if (!(a in k)) {\n                if (\n                  ('value' == a && 'defaultValue' in k) ||\n                  ('checked' == a && 'defaultChecked' in k)\n                )\n                  continue;\n                j$1(u, a, null, d, o);\n              }\n            for (a in k)\n              ((d = k[a]),\n                'children' == a\n                  ? (y = d)\n                  : 'dangerouslySetInnerHTML' == a\n                    ? (h = d)\n                    : 'value' == a\n                      ? (_ = d)\n                      : 'checked' == a\n                        ? (m = d)\n                        : (c && 'function' != typeof d) ||\n                          b[a] === d ||\n                          j$1(u, a, d, b[a], o));\n            if (h)\n              (c ||\n                (v && (h.__html == v.__html || h.__html == u.innerHTML)) ||\n                (u.innerHTML = h.__html),\n                (t.__k = []));\n            else if (\n              (v && (u.innerHTML = ''),\n              I(\n                'template' == t.type ? u.content : u,\n                w$1(y) ? y : [y],\n                t,\n                i,\n                r,\n                'foreignObject' == x ? 'http://www.w3.org/1999/xhtml' : o,\n                e,\n                f,\n                e ? e[0] : i.__k && S(i, 0),\n                c,\n                s\n              ),\n              null != e)\n            )\n              for (a = e.length; a--; ) g(e[a]);\n            c ||\n              ((a = 'value'),\n              'progress' == x && null == _\n                ? u.removeAttribute('value')\n                : null != _ &&\n                  (_ !== u[a] ||\n                    ('progress' == x && !_) ||\n                    ('option' == x && _ != b[a])) &&\n                  j$1(u, a, _, b[a], o),\n              (a = 'checked'),\n              null != m && m != u[a] && j$1(u, a, m, b[a], o));\n          }\n          return u;\n        }\n        function q$1(n, u, t) {\n          try {\n            if ('function' == typeof n) {\n              var i = 'function' == typeof n.__u;\n              (i && n.__u(), (i && null == u) || (n.__u = n(u)));\n            } else n.current = u;\n          } catch (n) {\n            l$1.__e(n, t);\n          }\n        }\n        function B$1(n, u, t) {\n          var i, r;\n          if (\n            (l$1.unmount && l$1.unmount(n),\n            (i = n.ref) &&\n              ((i.current && i.current != n.__e) || q$1(i, null, u)),\n            null != (i = n.__c))\n          ) {\n            if (i.componentWillUnmount)\n              try {\n                i.componentWillUnmount();\n              } catch (n) {\n                l$1.__e(n, u);\n              }\n            i.base = i.__P = null;\n          }\n          if ((i = n.__k))\n            for (r = 0; r < i.length; r++)\n              i[r] && B$1(i[r], u, t || 'function' != typeof n.type);\n          (t || g(n.__e), (n.__c = n.__ = n.__e = void 0));\n        }\n        function D$1(n, l, u) {\n          return this.constructor(n, u);\n        }\n        function E(u, t, i) {\n          var r, o, e, f;\n          (t == document && (t = document.documentElement),\n            l$1.__ && l$1.__(u, t),\n            (o = (r = 'function' == 'undefined') ? null : t.__k),\n            (e = []),\n            (f = []),\n            O(\n              t,\n              (u = t.__k = _$1(k$1, null, [u])),\n              o || p$1,\n              p$1,\n              t.namespaceURI,\n              o ? null : t.firstChild ? n.call(t.childNodes) : null,\n              e,\n              o ? o.__e : t.firstChild,\n              r,\n              f\n            ),\n            z$1(e, u, f));\n        }\n        function K(n) {\n          function l(n) {\n            var u, t;\n            return (\n              this.getChildContext ||\n                ((u = new Set()),\n                ((t = {})[l.__c] = this),\n                (this.getChildContext = function () {\n                  return t;\n                }),\n                (this.componentWillUnmount = function () {\n                  u = null;\n                }),\n                (this.shouldComponentUpdate = function (n) {\n                  this.props.value != n.value &&\n                    u.forEach(function (n) {\n                      ((n.__e = true), M(n));\n                    });\n                }),\n                (this.sub = function (n) {\n                  u.add(n);\n                  var l = n.componentWillUnmount;\n                  n.componentWillUnmount = function () {\n                    (u && u.delete(n), l && l.call(n));\n                  };\n                })),\n              n.children\n            );\n          }\n          return (\n            (l.__c = '__cC' + h$1++),\n            (l.__ = n),\n            (l.Provider =\n              l.__l =\n              (l.Consumer = function (n, l) {\n                return n.children(l);\n              }).contextType =\n                l),\n            l\n          );\n        }\n        ((n = v$1.slice),\n          (l$1 = {\n            __e: function (n, l, u, t) {\n              for (var i, r, o; (l = l.__); )\n                if ((i = l.__c) && !i.__)\n                  try {\n                    if (\n                      ((r = i.constructor) &&\n                        null != r.getDerivedStateFromError &&\n                        (i.setState(r.getDerivedStateFromError(n)),\n                        (o = i.__d)),\n                      null != i.componentDidCatch &&\n                        (i.componentDidCatch(n, t || {}), (o = i.__d)),\n                      o)\n                    )\n                      return (i.__E = i);\n                  } catch (l) {\n                    n = l;\n                  }\n              throw n;\n            },\n          }),\n          (u$2 = 0),\n          (x$1.prototype.setState = function (n, l) {\n            var u;\n            ((u =\n              null != this.__s && this.__s != this.state\n                ? this.__s\n                : (this.__s = d$1({}, this.state))),\n              'function' == typeof n && (n = n(d$1({}, u), this.props)),\n              n && d$1(u, n),\n              null != n && this.__v && (l && this._sb.push(l), M(this)));\n          }),\n          (x$1.prototype.forceUpdate = function (n) {\n            this.__v && ((this.__e = true), n && this.__h.push(n), M(this));\n          }),\n          (x$1.prototype.render = k$1),\n          (i$1 = []),\n          (o$1 =\n            'function' == typeof Promise\n              ? Promise.prototype.then.bind(Promise.resolve())\n              : setTimeout),\n          (e$1 = function (n, l) {\n            return n.__v.__b - l.__v.__b;\n          }),\n          ($.__r = 0),\n          (f$2 = /(PointerCapture)$|Capture$/i),\n          (c$1 = 0),\n          (s$1 = F(false)),\n          (a$1 = F(true)),\n          (h$1 = 0));\n\n        var f$1 = 0;\n        function u$1(e, t, n, o, i, u) {\n          t || (t = {});\n          var a,\n            c,\n            p = t;\n          if ('ref' in p)\n            for (c in ((p = {}), t)) 'ref' == c ? (a = t[c]) : (p[c] = t[c]);\n          var l = {\n            type: e,\n            props: p,\n            key: n,\n            ref: a,\n            __k: null,\n            __: null,\n            __b: 0,\n            __e: null,\n            __c: null,\n            constructor: void 0,\n            __v: --f$1,\n            __i: -1,\n            __u: 0,\n            __source: i,\n            __self: u,\n          };\n          if ('function' == typeof e && (a = e.defaultProps))\n            for (c in a) void 0 === p[c] && (p[c] = a[c]);\n          return (l$1.vnode && l$1.vnode(l), l);\n        }\n\n        function count$1(node) {\n          var sum = 0,\n            children = node.children,\n            i = children && children.length;\n          if (!i) sum = 1;\n          else while (--i >= 0) sum += children[i].value;\n          node.value = sum;\n        }\n\n        function node_count() {\n          return this.eachAfter(count$1);\n        }\n\n        function node_each(callback, that) {\n          let index = -1;\n          for (const node of this) {\n            callback.call(that, node, ++index, this);\n          }\n          return this;\n        }\n\n        function node_eachBefore(callback, that) {\n          var node = this,\n            nodes = [node],\n            children,\n            i,\n            index = -1;\n          while ((node = nodes.pop())) {\n            callback.call(that, node, ++index, this);\n            if ((children = node.children)) {\n              for (i = children.length - 1; i >= 0; --i) {\n                nodes.push(children[i]);\n              }\n            }\n          }\n          return this;\n        }\n\n        function node_eachAfter(callback, that) {\n          var node = this,\n            nodes = [node],\n            next = [],\n            children,\n            i,\n            n,\n            index = -1;\n          while ((node = nodes.pop())) {\n            next.push(node);\n            if ((children = node.children)) {\n              for (i = 0, n = children.length; i < n; ++i) {\n                nodes.push(children[i]);\n              }\n            }\n          }\n          while ((node = next.pop())) {\n            callback.call(that, node, ++index, this);\n          }\n          return this;\n        }\n\n        function node_find(callback, that) {\n          let index = -1;\n          for (const node of this) {\n            if (callback.call(that, node, ++index, this)) {\n              return node;\n            }\n          }\n        }\n\n        function node_sum(value) {\n          return this.eachAfter(function (node) {\n            var sum = +value(node.data) || 0,\n              children = node.children,\n              i = children && children.length;\n            while (--i >= 0) sum += children[i].value;\n            node.value = sum;\n          });\n        }\n\n        function node_sort(compare) {\n          return this.eachBefore(function (node) {\n            if (node.children) {\n              node.children.sort(compare);\n            }\n          });\n        }\n\n        function node_path(end) {\n          var start = this,\n            ancestor = leastCommonAncestor(start, end),\n            nodes = [start];\n          while (start !== ancestor) {\n            start = start.parent;\n            nodes.push(start);\n          }\n          var k = nodes.length;\n          while (end !== ancestor) {\n            nodes.splice(k, 0, end);\n            end = end.parent;\n          }\n          return nodes;\n        }\n\n        function leastCommonAncestor(a, b) {\n          if (a === b) return a;\n          var aNodes = a.ancestors(),\n            bNodes = b.ancestors(),\n            c = null;\n          a = aNodes.pop();\n          b = bNodes.pop();\n          while (a === b) {\n            c = a;\n            a = aNodes.pop();\n            b = bNodes.pop();\n          }\n          return c;\n        }\n\n        function node_ancestors() {\n          var node = this,\n            nodes = [node];\n          while ((node = node.parent)) {\n            nodes.push(node);\n          }\n          return nodes;\n        }\n\n        function node_descendants() {\n          return Array.from(this);\n        }\n\n        function node_leaves() {\n          var leaves = [];\n          this.eachBefore(function (node) {\n            if (!node.children) {\n              leaves.push(node);\n            }\n          });\n          return leaves;\n        }\n\n        function node_links() {\n          var root = this,\n            links = [];\n          root.each(function (node) {\n            if (node !== root) {\n              // Don’t include the root’s parent, if any.\n              links.push({ source: node.parent, target: node });\n            }\n          });\n          return links;\n        }\n\n        function* node_iterator() {\n          var node = this,\n            current,\n            next = [node],\n            children,\n            i,\n            n;\n          do {\n            ((current = next.reverse()), (next = []));\n            while ((node = current.pop())) {\n              yield node;\n              if ((children = node.children)) {\n                for (i = 0, n = children.length; i < n; ++i) {\n                  next.push(children[i]);\n                }\n              }\n            }\n          } while (next.length);\n        }\n\n        function hierarchy(data, children) {\n          if (data instanceof Map) {\n            data = [undefined, data];\n            if (children === undefined) children = mapChildren;\n          } else if (children === undefined) {\n            children = objectChildren;\n          }\n\n          var root = new Node$1(data),\n            node,\n            nodes = [root],\n            child,\n            childs,\n            i,\n            n;\n\n          while ((node = nodes.pop())) {\n            if (\n              (childs = children(node.data)) &&\n              (n = (childs = Array.from(childs)).length)\n            ) {\n              node.children = childs;\n              for (i = n - 1; i >= 0; --i) {\n                nodes.push((child = childs[i] = new Node$1(childs[i])));\n                child.parent = node;\n                child.depth = node.depth + 1;\n              }\n            }\n          }\n\n          return root.eachBefore(computeHeight);\n        }\n\n        function node_copy() {\n          return hierarchy(this).eachBefore(copyData);\n        }\n\n        function objectChildren(d) {\n          return d.children;\n        }\n\n        function mapChildren(d) {\n          return Array.isArray(d) ? d[1] : null;\n        }\n\n        function copyData(node) {\n          if (node.data.value !== undefined) node.value = node.data.value;\n          node.data = node.data.data;\n        }\n\n        function computeHeight(node) {\n          var height = 0;\n          do node.height = height;\n          while ((node = node.parent) && node.height < ++height);\n        }\n\n        function Node$1(data) {\n          this.data = data;\n          this.depth = this.height = 0;\n          this.parent = null;\n        }\n\n        Node$1.prototype = hierarchy.prototype = {\n          constructor: Node$1,\n          count: node_count,\n          each: node_each,\n          eachAfter: node_eachAfter,\n          eachBefore: node_eachBefore,\n          find: node_find,\n          sum: node_sum,\n          sort: node_sort,\n          path: node_path,\n          ancestors: node_ancestors,\n          descendants: node_descendants,\n          leaves: node_leaves,\n          links: node_links,\n          copy: node_copy,\n          [Symbol.iterator]: node_iterator,\n        };\n\n        function required(f) {\n          if (typeof f !== 'function') throw new Error();\n          return f;\n        }\n\n        function constantZero() {\n          return 0;\n        }\n\n        function constant$1(x) {\n          return function () {\n            return x;\n          };\n        }\n\n        function roundNode(node) {\n          node.x0 = Math.round(node.x0);\n          node.y0 = Math.round(node.y0);\n          node.x1 = Math.round(node.x1);\n          node.y1 = Math.round(node.y1);\n        }\n\n        function treemapDice(parent, x0, y0, x1, y1) {\n          var nodes = parent.children,\n            node,\n            i = -1,\n            n = nodes.length,\n            k = parent.value && (x1 - x0) / parent.value;\n\n          while (++i < n) {\n            ((node = nodes[i]), (node.y0 = y0), (node.y1 = y1));\n            ((node.x0 = x0), (node.x1 = x0 += node.value * k));\n          }\n        }\n\n        function treemapSlice(parent, x0, y0, x1, y1) {\n          var nodes = parent.children,\n            node,\n            i = -1,\n            n = nodes.length,\n            k = parent.value && (y1 - y0) / parent.value;\n\n          while (++i < n) {\n            ((node = nodes[i]), (node.x0 = x0), (node.x1 = x1));\n            ((node.y0 = y0), (node.y1 = y0 += node.value * k));\n          }\n        }\n\n        var phi = (1 + Math.sqrt(5)) / 2;\n\n        function squarifyRatio(ratio, parent, x0, y0, x1, y1) {\n          var rows = [],\n            nodes = parent.children,\n            row,\n            nodeValue,\n            i0 = 0,\n            i1 = 0,\n            n = nodes.length,\n            dx,\n            dy,\n            value = parent.value,\n            sumValue,\n            minValue,\n            maxValue,\n            newRatio,\n            minRatio,\n            alpha,\n            beta;\n\n          while (i0 < n) {\n            ((dx = x1 - x0), (dy = y1 - y0));\n\n            // Find the next non-empty node.\n            do sumValue = nodes[i1++].value;\n            while (!sumValue && i1 < n);\n            minValue = maxValue = sumValue;\n            alpha = Math.max(dy / dx, dx / dy) / (value * ratio);\n            beta = sumValue * sumValue * alpha;\n            minRatio = Math.max(maxValue / beta, beta / minValue);\n\n            // Keep adding nodes while the aspect ratio maintains or improves.\n            for (; i1 < n; ++i1) {\n              sumValue += nodeValue = nodes[i1].value;\n              if (nodeValue < minValue) minValue = nodeValue;\n              if (nodeValue > maxValue) maxValue = nodeValue;\n              beta = sumValue * sumValue * alpha;\n              newRatio = Math.max(maxValue / beta, beta / minValue);\n              if (newRatio > minRatio) {\n                sumValue -= nodeValue;\n                break;\n              }\n              minRatio = newRatio;\n            }\n\n            // Position and record the row orientation.\n            rows.push(\n              (row = {\n                value: sumValue,\n                dice: dx < dy,\n                children: nodes.slice(i0, i1),\n              })\n            );\n            if (row.dice)\n              treemapDice(\n                row,\n                x0,\n                y0,\n                x1,\n                value ? (y0 += (dy * sumValue) / value) : y1\n              );\n            else\n              treemapSlice(\n                row,\n                x0,\n                y0,\n                value ? (x0 += (dx * sumValue) / value) : x1,\n                y1\n              );\n            ((value -= sumValue), (i0 = i1));\n          }\n\n          return rows;\n        }\n\n        var squarify = (function custom(ratio) {\n          function squarify(parent, x0, y0, x1, y1) {\n            squarifyRatio(ratio, parent, x0, y0, x1, y1);\n          }\n\n          squarify.ratio = function (x) {\n            return custom((x = +x) > 1 ? x : 1);\n          };\n\n          return squarify;\n        })(phi);\n\n        function treemap() {\n          var tile = squarify,\n            round = false,\n            dx = 1,\n            dy = 1,\n            paddingStack = [0],\n            paddingInner = constantZero,\n            paddingTop = constantZero,\n            paddingRight = constantZero,\n            paddingBottom = constantZero,\n            paddingLeft = constantZero;\n\n          function treemap(root) {\n            root.x0 = root.y0 = 0;\n            root.x1 = dx;\n            root.y1 = dy;\n            root.eachBefore(positionNode);\n            paddingStack = [0];\n            if (round) root.eachBefore(roundNode);\n            return root;\n          }\n\n          function positionNode(node) {\n            var p = paddingStack[node.depth],\n              x0 = node.x0 + p,\n              y0 = node.y0 + p,\n              x1 = node.x1 - p,\n              y1 = node.y1 - p;\n            if (x1 < x0) x0 = x1 = (x0 + x1) / 2;\n            if (y1 < y0) y0 = y1 = (y0 + y1) / 2;\n            node.x0 = x0;\n            node.y0 = y0;\n            node.x1 = x1;\n            node.y1 = y1;\n            if (node.children) {\n              p = paddingStack[node.depth + 1] = paddingInner(node) / 2;\n              x0 += paddingLeft(node) - p;\n              y0 += paddingTop(node) - p;\n              x1 -= paddingRight(node) - p;\n              y1 -= paddingBottom(node) - p;\n              if (x1 < x0) x0 = x1 = (x0 + x1) / 2;\n              if (y1 < y0) y0 = y1 = (y0 + y1) / 2;\n              tile(node, x0, y0, x1, y1);\n            }\n          }\n\n          treemap.round = function (x) {\n            return arguments.length ? ((round = !!x), treemap) : round;\n          };\n\n          treemap.size = function (x) {\n            return arguments.length\n              ? ((dx = +x[0]), (dy = +x[1]), treemap)\n              : [dx, dy];\n          };\n\n          treemap.tile = function (x) {\n            return arguments.length ? ((tile = required(x)), treemap) : tile;\n          };\n\n          treemap.padding = function (x) {\n            return arguments.length\n              ? treemap.paddingInner(x).paddingOuter(x)\n              : treemap.paddingInner();\n          };\n\n          treemap.paddingInner = function (x) {\n            return arguments.length\n              ? ((paddingInner = typeof x === 'function' ? x : constant$1(+x)),\n                treemap)\n              : paddingInner;\n          };\n\n          treemap.paddingOuter = function (x) {\n            return arguments.length\n              ? treemap\n                  .paddingTop(x)\n                  .paddingRight(x)\n                  .paddingBottom(x)\n                  .paddingLeft(x)\n              : treemap.paddingTop();\n          };\n\n          treemap.paddingTop = function (x) {\n            return arguments.length\n              ? ((paddingTop = typeof x === 'function' ? x : constant$1(+x)),\n                treemap)\n              : paddingTop;\n          };\n\n          treemap.paddingRight = function (x) {\n            return arguments.length\n              ? ((paddingRight = typeof x === 'function' ? x : constant$1(+x)),\n                treemap)\n              : paddingRight;\n          };\n\n          treemap.paddingBottom = function (x) {\n            return arguments.length\n              ? ((paddingBottom = typeof x === 'function' ? x : constant$1(+x)),\n                treemap)\n              : paddingBottom;\n          };\n\n          treemap.paddingLeft = function (x) {\n            return arguments.length\n              ? ((paddingLeft = typeof x === 'function' ? x : constant$1(+x)),\n                treemap)\n              : paddingLeft;\n          };\n\n          return treemap;\n        }\n\n        var treemapResquarify = (function custom(ratio) {\n          function resquarify(parent, x0, y0, x1, y1) {\n            if ((rows = parent._squarify) && rows.ratio === ratio) {\n              var rows,\n                row,\n                nodes,\n                i,\n                j = -1,\n                n,\n                m = rows.length,\n                value = parent.value;\n\n              while (++j < m) {\n                ((row = rows[j]), (nodes = row.children));\n                for (i = row.value = 0, n = nodes.length; i < n; ++i)\n                  row.value += nodes[i].value;\n                if (row.dice)\n                  treemapDice(\n                    row,\n                    x0,\n                    y0,\n                    x1,\n                    value ? (y0 += ((y1 - y0) * row.value) / value) : y1\n                  );\n                else\n                  treemapSlice(\n                    row,\n                    x0,\n                    y0,\n                    value ? (x0 += ((x1 - x0) * row.value) / value) : x1,\n                    y1\n                  );\n                value -= row.value;\n              }\n            } else {\n              parent._squarify = rows = squarifyRatio(\n                ratio,\n                parent,\n                x0,\n                y0,\n                x1,\n                y1\n              );\n              rows.ratio = ratio;\n            }\n          }\n\n          resquarify.ratio = function (x) {\n            return custom((x = +x) > 1 ? x : 1);\n          };\n\n          return resquarify;\n        })(phi);\n\n        const isModuleTree = mod => 'children' in mod;\n\n        let count = 0;\n        class Id {\n          constructor(id) {\n            this._id = id;\n            const url = new URL(window.location.href);\n            url.hash = id;\n            this._href = url.toString();\n          }\n          get id() {\n            return this._id;\n          }\n          get href() {\n            return this._href;\n          }\n          toString() {\n            return `url(${this.href})`;\n          }\n        }\n        function generateUniqueId(name) {\n          count += 1;\n          const id = ['O', name, count].filter(Boolean).join('-');\n          return new Id(id);\n        }\n\n        const LABELS = {\n          renderedLength: 'Rendered',\n          gzipLength: 'Gzip',\n          brotliLength: 'Brotli',\n        };\n        const getAvailableSizeOptions = options => {\n          const availableSizeProperties = ['renderedLength'];\n          if (options.gzip) {\n            availableSizeProperties.push('gzipLength');\n          }\n          if (options.brotli) {\n            availableSizeProperties.push('brotliLength');\n          }\n          return availableSizeProperties;\n        };\n\n        var t,\n          r,\n          u,\n          i,\n          o = 0,\n          f = [],\n          c = l$1,\n          e = c.__b,\n          a = c.__r,\n          v = c.diffed,\n          l = c.__c,\n          m = c.unmount,\n          s = c.__;\n        function p(n, t) {\n          (c.__h && c.__h(r, n, o || t), (o = 0));\n          var u = r.__H || (r.__H = { __: [], __h: [] });\n          return (n >= u.__.length && u.__.push({}), u.__[n]);\n        }\n        function d(n) {\n          return ((o = 1), h(D, n));\n        }\n        function h(n, u, i) {\n          var o = p(t++, 2);\n          if (\n            ((o.t = n),\n            !o.__c &&\n              ((o.__ = [\n                D(void 0, u),\n                function (n) {\n                  var t = o.__N ? o.__N[0] : o.__[0],\n                    r = o.t(t, n);\n                  t !== r && ((o.__N = [r, o.__[1]]), o.__c.setState({}));\n                },\n              ]),\n              (o.__c = r),\n              !r.__f))\n          ) {\n            var f = function (n, t, r) {\n              if (!o.__c.__H) return true;\n              var u = o.__c.__H.__.filter(function (n) {\n                return !!n.__c;\n              });\n              if (\n                u.every(function (n) {\n                  return !n.__N;\n                })\n              )\n                return !c || c.call(this, n, t, r);\n              var i = o.__c.props !== n;\n              return (\n                u.forEach(function (n) {\n                  if (n.__N) {\n                    var t = n.__[0];\n                    ((n.__ = n.__N),\n                      (n.__N = void 0),\n                      t !== n.__[0] && (i = true));\n                  }\n                }),\n                (c && c.call(this, n, t, r)) || i\n              );\n            };\n            r.__f = true;\n            var c = r.shouldComponentUpdate,\n              e = r.componentWillUpdate;\n            ((r.componentWillUpdate = function (n, t, r) {\n              if (this.__e) {\n                var u = c;\n                ((c = void 0), f(n, t, r), (c = u));\n              }\n              e && e.call(this, n, t, r);\n            }),\n              (r.shouldComponentUpdate = f));\n          }\n          return o.__N || o.__;\n        }\n        function y(n, u) {\n          var i = p(t++, 3);\n          !c.__s && C(i.__H, u) && ((i.__ = n), (i.u = u), r.__H.__h.push(i));\n        }\n        function _(n, u) {\n          var i = p(t++, 4);\n          !c.__s && C(i.__H, u) && ((i.__ = n), (i.u = u), r.__h.push(i));\n        }\n        function A(n) {\n          return (\n            (o = 5),\n            T(function () {\n              return { current: n };\n            }, [])\n          );\n        }\n        function T(n, r) {\n          var u = p(t++, 7);\n          return (\n            C(u.__H, r) && ((u.__ = n()), (u.__H = r), (u.__h = n)),\n            u.__\n          );\n        }\n        function q(n, t) {\n          return (\n            (o = 8),\n            T(function () {\n              return n;\n            }, t)\n          );\n        }\n        function x(n) {\n          var u = r.context[n.__c],\n            i = p(t++, 9);\n          return (\n            (i.c = n),\n            u\n              ? (null == i.__ && ((i.__ = true), u.sub(r)), u.props.value)\n              : n.__\n          );\n        }\n        function j() {\n          for (var n; (n = f.shift()); )\n            if (n.__P && n.__H)\n              try {\n                (n.__H.__h.forEach(z), n.__H.__h.forEach(B), (n.__H.__h = []));\n              } catch (t) {\n                ((n.__H.__h = []), c.__e(t, n.__v));\n              }\n        }\n        ((c.__b = function (n) {\n          ((r = null), e && e(n));\n        }),\n          (c.__ = function (n, t) {\n            (n && t.__k && t.__k.__m && (n.__m = t.__k.__m), s && s(n, t));\n          }),\n          (c.__r = function (n) {\n            (a && a(n), (t = 0));\n            var i = (r = n.__c).__H;\n            (i &&\n              (u === r\n                ? ((i.__h = []),\n                  (r.__h = []),\n                  i.__.forEach(function (n) {\n                    (n.__N && (n.__ = n.__N), (n.u = n.__N = void 0));\n                  }))\n                : (i.__h.forEach(z), i.__h.forEach(B), (i.__h = []), (t = 0))),\n              (u = r));\n          }),\n          (c.diffed = function (n) {\n            v && v(n);\n            var t = n.__c;\n            (t &&\n              t.__H &&\n              (t.__H.__h.length &&\n                ((1 !== f.push(t) && i === c.requestAnimationFrame) ||\n                  ((i = c.requestAnimationFrame) || w)(j)),\n              t.__H.__.forEach(function (n) {\n                (n.u && (n.__H = n.u), (n.u = void 0));\n              })),\n              (u = r = null));\n          }),\n          (c.__c = function (n, t) {\n            (t.some(function (n) {\n              try {\n                (n.__h.forEach(z),\n                  (n.__h = n.__h.filter(function (n) {\n                    return !n.__ || B(n);\n                  })));\n              } catch (r) {\n                (t.some(function (n) {\n                  n.__h && (n.__h = []);\n                }),\n                  (t = []),\n                  c.__e(r, n.__v));\n              }\n            }),\n              l && l(n, t));\n          }),\n          (c.unmount = function (n) {\n            m && m(n);\n            var t,\n              r = n.__c;\n            r &&\n              r.__H &&\n              (r.__H.__.forEach(function (n) {\n                try {\n                  z(n);\n                } catch (n) {\n                  t = n;\n                }\n              }),\n              (r.__H = void 0),\n              t && c.__e(t, r.__v));\n          }));\n        var k = 'function' == typeof requestAnimationFrame;\n        function w(n) {\n          var t,\n            r = function () {\n              (clearTimeout(u), k && cancelAnimationFrame(t), setTimeout(n));\n            },\n            u = setTimeout(r, 35);\n          k && (t = requestAnimationFrame(r));\n        }\n        function z(n) {\n          var t = r,\n            u = n.__c;\n          ('function' == typeof u && ((n.__c = void 0), u()), (r = t));\n        }\n        function B(n) {\n          var t = r;\n          ((n.__c = n.__()), (r = t));\n        }\n        function C(n, t) {\n          return (\n            !n ||\n            n.length !== t.length ||\n            t.some(function (t, r) {\n              return t !== n[r];\n            })\n          );\n        }\n        function D(n, t) {\n          return 'function' == typeof t ? t(n) : t;\n        }\n\n        const PLACEHOLDER = '*/**/file.js';\n        const SideBar = ({\n          availableSizeProperties,\n          sizeProperty,\n          setSizeProperty,\n          onExcludeChange,\n          onIncludeChange,\n        }) => {\n          const [includeValue, setIncludeValue] = d('');\n          const [excludeValue, setExcludeValue] = d('');\n          const handleSizePropertyChange = sizeProp => () => {\n            if (sizeProp !== sizeProperty) {\n              setSizeProperty(sizeProp);\n            }\n          };\n          const handleIncludeChange = event => {\n            const value = event.currentTarget.value;\n            setIncludeValue(value);\n            onIncludeChange(value);\n          };\n          const handleExcludeChange = event => {\n            const value = event.currentTarget.value;\n            setExcludeValue(value);\n            onExcludeChange(value);\n          };\n          return u$1('aside', {\n            className: 'sidebar',\n            children: [\n              u$1('div', {\n                className: 'size-selectors',\n                children:\n                  availableSizeProperties.length > 1 &&\n                  availableSizeProperties.map(sizeProp => {\n                    const id = `selector-${sizeProp}`;\n                    return u$1(\n                      'div',\n                      {\n                        className: 'size-selector',\n                        children: [\n                          u$1('input', {\n                            type: 'radio',\n                            id: id,\n                            checked: sizeProp === sizeProperty,\n                            onChange: handleSizePropertyChange(sizeProp),\n                          }),\n                          u$1('label', {\n                            htmlFor: id,\n                            children: LABELS[sizeProp],\n                          }),\n                        ],\n                      },\n                      sizeProp\n                    );\n                  }),\n              }),\n              u$1('div', {\n                className: 'module-filters',\n                children: [\n                  u$1('div', {\n                    className: 'module-filter',\n                    children: [\n                      u$1('label', {\n                        htmlFor: 'module-filter-exclude',\n                        children: 'Exclude',\n                      }),\n                      u$1('input', {\n                        type: 'text',\n                        id: 'module-filter-exclude',\n                        value: excludeValue,\n                        onInput: handleExcludeChange,\n                        placeholder: PLACEHOLDER,\n                      }),\n                    ],\n                  }),\n                  u$1('div', {\n                    className: 'module-filter',\n                    children: [\n                      u$1('label', {\n                        htmlFor: 'module-filter-include',\n                        children: 'Include',\n                      }),\n                      u$1('input', {\n                        type: 'text',\n                        id: 'module-filter-include',\n                        value: includeValue,\n                        onInput: handleIncludeChange,\n                        placeholder: PLACEHOLDER,\n                      }),\n                    ],\n                  }),\n                ],\n              }),\n            ],\n          });\n        };\n\n        function getDefaultExportFromCjs(x) {\n          return x &&\n            x.__esModule &&\n            Object.prototype.hasOwnProperty.call(x, 'default')\n            ? x['default']\n            : x;\n        }\n\n        var utils = {};\n\n        var constants$1;\n        var hasRequiredConstants;\n\n        function requireConstants() {\n          if (hasRequiredConstants) return constants$1;\n          hasRequiredConstants = 1;\n\n          const WIN_SLASH = '\\\\\\\\/';\n          const WIN_NO_SLASH = `[^${WIN_SLASH}]`;\n\n          /**\n           * Posix glob regex\n           */\n\n          const DOT_LITERAL = '\\\\.';\n          const PLUS_LITERAL = '\\\\+';\n          const QMARK_LITERAL = '\\\\?';\n          const SLASH_LITERAL = '\\\\/';\n          const ONE_CHAR = '(?=.)';\n          const QMARK = '[^/]';\n          const END_ANCHOR = `(?:${SLASH_LITERAL}|$)`;\n          const START_ANCHOR = `(?:^|${SLASH_LITERAL})`;\n          const DOTS_SLASH = `${DOT_LITERAL}{1,2}${END_ANCHOR}`;\n          const NO_DOT = `(?!${DOT_LITERAL})`;\n          const NO_DOTS = `(?!${START_ANCHOR}${DOTS_SLASH})`;\n          const NO_DOT_SLASH = `(?!${DOT_LITERAL}{0,1}${END_ANCHOR})`;\n          const NO_DOTS_SLASH = `(?!${DOTS_SLASH})`;\n          const QMARK_NO_DOT = `[^.${SLASH_LITERAL}]`;\n          const STAR = `${QMARK}*?`;\n          const SEP = '/';\n\n          const POSIX_CHARS = {\n            DOT_LITERAL,\n            PLUS_LITERAL,\n            QMARK_LITERAL,\n            SLASH_LITERAL,\n            ONE_CHAR,\n            QMARK,\n            END_ANCHOR,\n            DOTS_SLASH,\n            NO_DOT,\n            NO_DOTS,\n            NO_DOT_SLASH,\n            NO_DOTS_SLASH,\n            QMARK_NO_DOT,\n            STAR,\n            START_ANCHOR,\n            SEP,\n          };\n\n          /**\n           * Windows glob regex\n           */\n\n          const WINDOWS_CHARS = {\n            ...POSIX_CHARS,\n\n            SLASH_LITERAL: `[${WIN_SLASH}]`,\n            QMARK: WIN_NO_SLASH,\n            STAR: `${WIN_NO_SLASH}*?`,\n            DOTS_SLASH: `${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$)`,\n            NO_DOT: `(?!${DOT_LITERAL})`,\n            NO_DOTS: `(?!(?:^|[${WIN_SLASH}])${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$))`,\n            NO_DOT_SLASH: `(?!${DOT_LITERAL}{0,1}(?:[${WIN_SLASH}]|$))`,\n            NO_DOTS_SLASH: `(?!${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$))`,\n            QMARK_NO_DOT: `[^.${WIN_SLASH}]`,\n            START_ANCHOR: `(?:^|[${WIN_SLASH}])`,\n            END_ANCHOR: `(?:[${WIN_SLASH}]|$)`,\n            SEP: '\\\\',\n          };\n\n          /**\n           * POSIX Bracket Regex\n           */\n\n          const POSIX_REGEX_SOURCE = {\n            alnum: 'a-zA-Z0-9',\n            alpha: 'a-zA-Z',\n            ascii: '\\\\x00-\\\\x7F',\n            blank: ' \\\\t',\n            cntrl: '\\\\x00-\\\\x1F\\\\x7F',\n            digit: '0-9',\n            graph: '\\\\x21-\\\\x7E',\n            lower: 'a-z',\n            print: '\\\\x20-\\\\x7E ',\n            punct: '\\\\-!\"#$%&\\'()\\\\*+,./:;<=>?@[\\\\]^_`{|}~',\n            space: ' \\\\t\\\\r\\\\n\\\\v\\\\f',\n            upper: 'A-Z',\n            word: 'A-Za-z0-9_',\n            xdigit: 'A-Fa-f0-9',\n          };\n\n          constants$1 = {\n            MAX_LENGTH: 1024 * 64,\n            POSIX_REGEX_SOURCE,\n\n            // regular expressions\n            REGEX_BACKSLASH: /\\\\(?![*+?^${}(|)[\\]])/g,\n            REGEX_NON_SPECIAL_CHARS: /^[^@![\\].,$*+?^{}()|\\\\/]+/,\n            REGEX_SPECIAL_CHARS: /[-*+?.^${}(|)[\\]]/,\n            REGEX_SPECIAL_CHARS_BACKREF: /(\\\\?)((\\W)(\\3*))/g,\n            REGEX_SPECIAL_CHARS_GLOBAL: /([-*+?.^${}(|)[\\]])/g,\n            REGEX_REMOVE_BACKSLASH: /(?:\\[.*?[^\\\\]\\]|\\\\(?=.))/g,\n\n            // Replace globs with equivalent patterns to reduce parsing time.\n            REPLACEMENTS: {\n              '***': '*',\n              '**/**': '**',\n              '**/**/**': '**',\n            },\n\n            // Digits\n            CHAR_0: 48 /* 0 */,\n            CHAR_9: 57 /* 9 */,\n\n            // Alphabet chars.\n            CHAR_UPPERCASE_A: 65 /* A */,\n            CHAR_LOWERCASE_A: 97 /* a */,\n            CHAR_UPPERCASE_Z: 90 /* Z */,\n            CHAR_LOWERCASE_Z: 122 /* z */,\n\n            CHAR_LEFT_PARENTHESES: 40 /* ( */,\n            CHAR_RIGHT_PARENTHESES: 41 /* ) */,\n\n            CHAR_ASTERISK: 42 /* * */,\n\n            // Non-alphabetic chars.\n            CHAR_AMPERSAND: 38 /* & */,\n            CHAR_AT: 64 /* @ */,\n            CHAR_BACKWARD_SLASH: 92 /* \\ */,\n            CHAR_CARRIAGE_RETURN: 13 /* \\r */,\n            CHAR_CIRCUMFLEX_ACCENT: 94 /* ^ */,\n            CHAR_COLON: 58 /* : */,\n            CHAR_COMMA: 44 /* , */,\n            CHAR_DOT: 46 /* . */,\n            CHAR_DOUBLE_QUOTE: 34 /* \" */,\n            CHAR_EQUAL: 61 /* = */,\n            CHAR_EXCLAMATION_MARK: 33 /* ! */,\n            CHAR_FORM_FEED: 12 /* \\f */,\n            CHAR_FORWARD_SLASH: 47 /* / */,\n            CHAR_GRAVE_ACCENT: 96 /* ` */,\n            CHAR_HASH: 35 /* # */,\n            CHAR_HYPHEN_MINUS: 45 /* - */,\n            CHAR_LEFT_ANGLE_BRACKET: 60 /* < */,\n            CHAR_LEFT_CURLY_BRACE: 123 /* { */,\n            CHAR_LEFT_SQUARE_BRACKET: 91 /* [ */,\n            CHAR_LINE_FEED: 10 /* \\n */,\n            CHAR_NO_BREAK_SPACE: 160 /* \\u00A0 */,\n            CHAR_PERCENT: 37 /* % */,\n            CHAR_PLUS: 43 /* + */,\n            CHAR_QUESTION_MARK: 63 /* ? */,\n            CHAR_RIGHT_ANGLE_BRACKET: 62 /* > */,\n            CHAR_RIGHT_CURLY_BRACE: 125 /* } */,\n            CHAR_RIGHT_SQUARE_BRACKET: 93 /* ] */,\n            CHAR_SEMICOLON: 59 /* ; */,\n            CHAR_SINGLE_QUOTE: 39 /* ' */,\n            CHAR_SPACE: 32 /*   */,\n            CHAR_TAB: 9 /* \\t */,\n            CHAR_UNDERSCORE: 95 /* _ */,\n            CHAR_VERTICAL_LINE: 124 /* | */,\n            CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279 /* \\uFEFF */,\n\n            /**\n             * Create EXTGLOB_CHARS\n             */\n\n            extglobChars(chars) {\n              return {\n                '!': {\n                  type: 'negate',\n                  open: '(?:(?!(?:',\n                  close: `))${chars.STAR})`,\n                },\n                '?': { type: 'qmark', open: '(?:', close: ')?' },\n                '+': { type: 'plus', open: '(?:', close: ')+' },\n                '*': { type: 'star', open: '(?:', close: ')*' },\n                '@': { type: 'at', open: '(?:', close: ')' },\n              };\n            },\n\n            /**\n             * Create GLOB_CHARS\n             */\n\n            globChars(win32) {\n              return win32 === true ? WINDOWS_CHARS : POSIX_CHARS;\n            },\n          };\n          return constants$1;\n        }\n\n        /*global navigator*/\n\n        var hasRequiredUtils;\n\n        function requireUtils() {\n          if (hasRequiredUtils) return utils;\n          hasRequiredUtils = 1;\n          (function (exports) {\n            const {\n              REGEX_BACKSLASH,\n              REGEX_REMOVE_BACKSLASH,\n              REGEX_SPECIAL_CHARS,\n              REGEX_SPECIAL_CHARS_GLOBAL,\n            } = /*@__PURE__*/ requireConstants();\n\n            exports.isObject = val =>\n              val !== null && typeof val === 'object' && !Array.isArray(val);\n            exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str);\n            exports.isRegexChar = str =>\n              str.length === 1 && exports.hasRegexChars(str);\n            exports.escapeRegex = str =>\n              str.replace(REGEX_SPECIAL_CHARS_GLOBAL, '\\\\$1');\n            exports.toPosixSlashes = str => str.replace(REGEX_BACKSLASH, '/');\n\n            exports.isWindows = () => {\n              if (typeof navigator !== 'undefined' && navigator.platform) {\n                const platform = navigator.platform.toLowerCase();\n                return platform === 'win32' || platform === 'windows';\n              }\n\n              if (typeof process !== 'undefined' && process.platform) {\n                return process.platform === 'win32';\n              }\n\n              return false;\n            };\n\n            exports.removeBackslashes = str => {\n              return str.replace(REGEX_REMOVE_BACKSLASH, match => {\n                return match === '\\\\' ? '' : match;\n              });\n            };\n\n            exports.escapeLast = (input, char, lastIdx) => {\n              const idx = input.lastIndexOf(char, lastIdx);\n              if (idx === -1) return input;\n              if (input[idx - 1] === '\\\\')\n                return exports.escapeLast(input, char, idx - 1);\n              return `${input.slice(0, idx)}\\\\${input.slice(idx)}`;\n            };\n\n            exports.removePrefix = (input, state = {}) => {\n              let output = input;\n              if (output.startsWith('./')) {\n                output = output.slice(2);\n                state.prefix = './';\n              }\n              return output;\n            };\n\n            exports.wrapOutput = (input, state = {}, options = {}) => {\n              const prepend = options.contains ? '' : '^';\n              const append = options.contains ? '' : '$';\n\n              let output = `${prepend}(?:${input})${append}`;\n              if (state.negated === true) {\n                output = `(?:^(?!${output}).*$)`;\n              }\n              return output;\n            };\n\n            exports.basename = (path, { windows } = {}) => {\n              const segs = path.split(windows ? /[\\\\/]/ : '/');\n              const last = segs[segs.length - 1];\n\n              if (last === '') {\n                return segs[segs.length - 2];\n              }\n\n              return last;\n            };\n          })(utils);\n          return utils;\n        }\n\n        var scan_1;\n        var hasRequiredScan;\n\n        function requireScan() {\n          if (hasRequiredScan) return scan_1;\n          hasRequiredScan = 1;\n\n          const utils = /*@__PURE__*/ requireUtils();\n          const {\n            CHAR_ASTERISK /* * */,\n            CHAR_AT /* @ */,\n            CHAR_BACKWARD_SLASH /* \\ */,\n            CHAR_COMMA /* , */,\n            CHAR_DOT /* . */,\n            CHAR_EXCLAMATION_MARK /* ! */,\n            CHAR_FORWARD_SLASH /* / */,\n            CHAR_LEFT_CURLY_BRACE /* { */,\n            CHAR_LEFT_PARENTHESES /* ( */,\n            CHAR_LEFT_SQUARE_BRACKET /* [ */,\n            CHAR_PLUS /* + */,\n            CHAR_QUESTION_MARK /* ? */,\n            CHAR_RIGHT_CURLY_BRACE /* } */,\n            CHAR_RIGHT_PARENTHESES /* ) */,\n            CHAR_RIGHT_SQUARE_BRACKET /* ] */,\n          } = /*@__PURE__*/ requireConstants();\n\n          const isPathSeparator = code => {\n            return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH;\n          };\n\n          const depth = token => {\n            if (token.isPrefix !== true) {\n              token.depth = token.isGlobstar ? Infinity : 1;\n            }\n          };\n\n          /**\n           * Quickly scans a glob pattern and returns an object with a handful of\n           * useful properties, like `isGlob`, `path` (the leading non-glob, if it exists),\n           * `glob` (the actual pattern), `negated` (true if the path starts with `!` but not\n           * with `!(`) and `negatedExtglob` (true if the path starts with `!(`).\n           *\n           * ```js\n           * const pm = require('picomatch');\n           * console.log(pm.scan('foo/bar/*.js'));\n           * { isGlob: true, input: 'foo/bar/*.js', base: 'foo/bar', glob: '*.js' }\n           * ```\n           * @param {String} `str`\n           * @param {Object} `options`\n           * @return {Object} Returns an object with tokens and regex source string.\n           * @api public\n           */\n\n          const scan = (input, options) => {\n            const opts = options || {};\n\n            const length = input.length - 1;\n            const scanToEnd = opts.parts === true || opts.scanToEnd === true;\n            const slashes = [];\n            const tokens = [];\n            const parts = [];\n\n            let str = input;\n            let index = -1;\n            let start = 0;\n            let lastIndex = 0;\n            let isBrace = false;\n            let isBracket = false;\n            let isGlob = false;\n            let isExtglob = false;\n            let isGlobstar = false;\n            let braceEscaped = false;\n            let backslashes = false;\n            let negated = false;\n            let negatedExtglob = false;\n            let finished = false;\n            let braces = 0;\n            let prev;\n            let code;\n            let token = { value: '', depth: 0, isGlob: false };\n\n            const eos = () => index >= length;\n            const peek = () => str.charCodeAt(index + 1);\n            const advance = () => {\n              prev = code;\n              return str.charCodeAt(++index);\n            };\n\n            while (index < length) {\n              code = advance();\n              let next;\n\n              if (code === CHAR_BACKWARD_SLASH) {\n                backslashes = token.backslashes = true;\n                code = advance();\n\n                if (code === CHAR_LEFT_CURLY_BRACE) {\n                  braceEscaped = true;\n                }\n                continue;\n              }\n\n              if (braceEscaped === true || code === CHAR_LEFT_CURLY_BRACE) {\n                braces++;\n\n                while (eos() !== true && (code = advance())) {\n                  if (code === CHAR_BACKWARD_SLASH) {\n                    backslashes = token.backslashes = true;\n                    advance();\n                    continue;\n                  }\n\n                  if (code === CHAR_LEFT_CURLY_BRACE) {\n                    braces++;\n                    continue;\n                  }\n\n                  if (\n                    braceEscaped !== true &&\n                    code === CHAR_DOT &&\n                    (code = advance()) === CHAR_DOT\n                  ) {\n                    isBrace = token.isBrace = true;\n                    isGlob = token.isGlob = true;\n                    finished = true;\n\n                    if (scanToEnd === true) {\n                      continue;\n                    }\n\n                    break;\n                  }\n\n                  if (braceEscaped !== true && code === CHAR_COMMA) {\n                    isBrace = token.isBrace = true;\n                    isGlob = token.isGlob = true;\n                    finished = true;\n\n                    if (scanToEnd === true) {\n                      continue;\n                    }\n\n                    break;\n                  }\n\n                  if (code === CHAR_RIGHT_CURLY_BRACE) {\n                    braces--;\n\n                    if (braces === 0) {\n                      braceEscaped = false;\n                      isBrace = token.isBrace = true;\n                      finished = true;\n                      break;\n                    }\n                  }\n                }\n\n                if (scanToEnd === true) {\n                  continue;\n                }\n\n                break;\n              }\n\n              if (code === CHAR_FORWARD_SLASH) {\n                slashes.push(index);\n                tokens.push(token);\n                token = { value: '', depth: 0, isGlob: false };\n\n                if (finished === true) continue;\n                if (prev === CHAR_DOT && index === start + 1) {\n                  start += 2;\n                  continue;\n                }\n\n                lastIndex = index + 1;\n                continue;\n              }\n\n              if (opts.noext !== true) {\n                const isExtglobChar =\n                  code === CHAR_PLUS ||\n                  code === CHAR_AT ||\n                  code === CHAR_ASTERISK ||\n                  code === CHAR_QUESTION_MARK ||\n                  code === CHAR_EXCLAMATION_MARK;\n\n                if (\n                  isExtglobChar === true &&\n                  peek() === CHAR_LEFT_PARENTHESES\n                ) {\n                  isGlob = token.isGlob = true;\n                  isExtglob = token.isExtglob = true;\n                  finished = true;\n                  if (code === CHAR_EXCLAMATION_MARK && index === start) {\n                    negatedExtglob = true;\n                  }\n\n                  if (scanToEnd === true) {\n                    while (eos() !== true && (code = advance())) {\n                      if (code === CHAR_BACKWARD_SLASH) {\n                        backslashes = token.backslashes = true;\n                        code = advance();\n                        continue;\n                      }\n\n                      if (code === CHAR_RIGHT_PARENTHESES) {\n                        isGlob = token.isGlob = true;\n                        finished = true;\n                        break;\n                      }\n                    }\n                    continue;\n                  }\n                  break;\n                }\n              }\n\n              if (code === CHAR_ASTERISK) {\n                if (prev === CHAR_ASTERISK)\n                  isGlobstar = token.isGlobstar = true;\n                isGlob = token.isGlob = true;\n                finished = true;\n\n                if (scanToEnd === true) {\n                  continue;\n                }\n                break;\n              }\n\n              if (code === CHAR_QUESTION_MARK) {\n                isGlob = token.isGlob = true;\n                finished = true;\n\n                if (scanToEnd === true) {\n                  continue;\n                }\n                break;\n              }\n\n              if (code === CHAR_LEFT_SQUARE_BRACKET) {\n                while (eos() !== true && (next = advance())) {\n                  if (next === CHAR_BACKWARD_SLASH) {\n                    backslashes = token.backslashes = true;\n                    advance();\n                    continue;\n                  }\n\n                  if (next === CHAR_RIGHT_SQUARE_BRACKET) {\n                    isBracket = token.isBracket = true;\n                    isGlob = token.isGlob = true;\n                    finished = true;\n                    break;\n                  }\n                }\n\n                if (scanToEnd === true) {\n                  continue;\n                }\n\n                break;\n              }\n\n              if (\n                opts.nonegate !== true &&\n                code === CHAR_EXCLAMATION_MARK &&\n                index === start\n              ) {\n                negated = token.negated = true;\n                start++;\n                continue;\n              }\n\n              if (opts.noparen !== true && code === CHAR_LEFT_PARENTHESES) {\n                isGlob = token.isGlob = true;\n\n                if (scanToEnd === true) {\n                  while (eos() !== true && (code = advance())) {\n                    if (code === CHAR_LEFT_PARENTHESES) {\n                      backslashes = token.backslashes = true;\n                      code = advance();\n                      continue;\n                    }\n\n                    if (code === CHAR_RIGHT_PARENTHESES) {\n                      finished = true;\n                      break;\n                    }\n                  }\n                  continue;\n                }\n                break;\n              }\n\n              if (isGlob === true) {\n                finished = true;\n\n                if (scanToEnd === true) {\n                  continue;\n                }\n\n                break;\n              }\n            }\n\n            if (opts.noext === true) {\n              isExtglob = false;\n              isGlob = false;\n            }\n\n            let base = str;\n            let prefix = '';\n            let glob = '';\n\n            if (start > 0) {\n              prefix = str.slice(0, start);\n              str = str.slice(start);\n              lastIndex -= start;\n            }\n\n            if (base && isGlob === true && lastIndex > 0) {\n              base = str.slice(0, lastIndex);\n              glob = str.slice(lastIndex);\n            } else if (isGlob === true) {\n              base = '';\n              glob = str;\n            } else {\n              base = str;\n            }\n\n            if (base && base !== '' && base !== '/' && base !== str) {\n              if (isPathSeparator(base.charCodeAt(base.length - 1))) {\n                base = base.slice(0, -1);\n              }\n            }\n\n            if (opts.unescape === true) {\n              if (glob) glob = utils.removeBackslashes(glob);\n\n              if (base && backslashes === true) {\n                base = utils.removeBackslashes(base);\n              }\n            }\n\n            const state = {\n              prefix,\n              input,\n              start,\n              base,\n              glob,\n              isBrace,\n              isBracket,\n              isGlob,\n              isExtglob,\n              isGlobstar,\n              negated,\n              negatedExtglob,\n            };\n\n            if (opts.tokens === true) {\n              state.maxDepth = 0;\n              if (!isPathSeparator(code)) {\n                tokens.push(token);\n              }\n              state.tokens = tokens;\n            }\n\n            if (opts.parts === true || opts.tokens === true) {\n              let prevIndex;\n\n              for (let idx = 0; idx < slashes.length; idx++) {\n                const n = prevIndex ? prevIndex + 1 : start;\n                const i = slashes[idx];\n                const value = input.slice(n, i);\n                if (opts.tokens) {\n                  if (idx === 0 && start !== 0) {\n                    tokens[idx].isPrefix = true;\n                    tokens[idx].value = prefix;\n                  } else {\n                    tokens[idx].value = value;\n                  }\n                  depth(tokens[idx]);\n                  state.maxDepth += tokens[idx].depth;\n                }\n                if (idx !== 0 || value !== '') {\n                  parts.push(value);\n                }\n                prevIndex = i;\n              }\n\n              if (prevIndex && prevIndex + 1 < input.length) {\n                const value = input.slice(prevIndex + 1);\n                parts.push(value);\n\n                if (opts.tokens) {\n                  tokens[tokens.length - 1].value = value;\n                  depth(tokens[tokens.length - 1]);\n                  state.maxDepth += tokens[tokens.length - 1].depth;\n                }\n              }\n\n              state.slashes = slashes;\n              state.parts = parts;\n            }\n\n            return state;\n          };\n\n          scan_1 = scan;\n          return scan_1;\n        }\n\n        var parse_1;\n        var hasRequiredParse;\n\n        function requireParse() {\n          if (hasRequiredParse) return parse_1;\n          hasRequiredParse = 1;\n\n          const constants = /*@__PURE__*/ requireConstants();\n          const utils = /*@__PURE__*/ requireUtils();\n\n          /**\n           * Constants\n           */\n\n          const {\n            MAX_LENGTH,\n            POSIX_REGEX_SOURCE,\n            REGEX_NON_SPECIAL_CHARS,\n            REGEX_SPECIAL_CHARS_BACKREF,\n            REPLACEMENTS,\n          } = constants;\n\n          /**\n           * Helpers\n           */\n\n          const expandRange = (args, options) => {\n            if (typeof options.expandRange === 'function') {\n              return options.expandRange(...args, options);\n            }\n\n            args.sort();\n            const value = `[${args.join('-')}]`;\n\n            try {\n              /* eslint-disable-next-line no-new */\n              new RegExp(value);\n            } catch (ex) {\n              return args.map(v => utils.escapeRegex(v)).join('..');\n            }\n\n            return value;\n          };\n\n          /**\n           * Create the message for a syntax error\n           */\n\n          const syntaxError = (type, char) => {\n            return `Missing ${type}: \"${char}\" - use \"\\\\\\\\${char}\" to match literal characters`;\n          };\n\n          /**\n           * Parse the given input string.\n           * @param {String} input\n           * @param {Object} options\n           * @return {Object}\n           */\n\n          const parse = (input, options) => {\n            if (typeof input !== 'string') {\n              throw new TypeError('Expected a string');\n            }\n\n            input = REPLACEMENTS[input] || input;\n\n            const opts = { ...options };\n            const max =\n              typeof opts.maxLength === 'number'\n                ? Math.min(MAX_LENGTH, opts.maxLength)\n                : MAX_LENGTH;\n\n            let len = input.length;\n            if (len > max) {\n              throw new SyntaxError(\n                `Input length: ${len}, exceeds maximum allowed length: ${max}`\n              );\n            }\n\n            const bos = { type: 'bos', value: '', output: opts.prepend || '' };\n            const tokens = [bos];\n\n            const capture = opts.capture ? '' : '?:';\n\n            // create constants based on platform, for windows or posix\n            const PLATFORM_CHARS = constants.globChars(opts.windows);\n            const EXTGLOB_CHARS = constants.extglobChars(PLATFORM_CHARS);\n\n            const {\n              DOT_LITERAL,\n              PLUS_LITERAL,\n              SLASH_LITERAL,\n              ONE_CHAR,\n              DOTS_SLASH,\n              NO_DOT,\n              NO_DOT_SLASH,\n              NO_DOTS_SLASH,\n              QMARK,\n              QMARK_NO_DOT,\n              STAR,\n              START_ANCHOR,\n            } = PLATFORM_CHARS;\n\n            const globstar = opts => {\n              return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`;\n            };\n\n            const nodot = opts.dot ? '' : NO_DOT;\n            const qmarkNoDot = opts.dot ? QMARK : QMARK_NO_DOT;\n            let star = opts.bash === true ? globstar(opts) : STAR;\n\n            if (opts.capture) {\n              star = `(${star})`;\n            }\n\n            // minimatch options support\n            if (typeof opts.noext === 'boolean') {\n              opts.noextglob = opts.noext;\n            }\n\n            const state = {\n              input,\n              index: -1,\n              start: 0,\n              dot: opts.dot === true,\n              consumed: '',\n              output: '',\n              prefix: '',\n              backtrack: false,\n              negated: false,\n              brackets: 0,\n              braces: 0,\n              parens: 0,\n              quotes: 0,\n              globstar: false,\n              tokens,\n            };\n\n            input = utils.removePrefix(input, state);\n            len = input.length;\n\n            const extglobs = [];\n            const braces = [];\n            const stack = [];\n            let prev = bos;\n            let value;\n\n            /**\n             * Tokenizing helpers\n             */\n\n            const eos = () => state.index === len - 1;\n            const peek = (state.peek = (n = 1) => input[state.index + n]);\n            const advance = (state.advance = () => input[++state.index] || '');\n            const remaining = () => input.slice(state.index + 1);\n            const consume = (value = '', num = 0) => {\n              state.consumed += value;\n              state.index += num;\n            };\n\n            const append = token => {\n              state.output += token.output != null ? token.output : token.value;\n              consume(token.value);\n            };\n\n            const negate = () => {\n              let count = 1;\n\n              while (peek() === '!' && (peek(2) !== '(' || peek(3) === '?')) {\n                advance();\n                state.start++;\n                count++;\n              }\n\n              if (count % 2 === 0) {\n                return false;\n              }\n\n              state.negated = true;\n              state.start++;\n              return true;\n            };\n\n            const increment = type => {\n              state[type]++;\n              stack.push(type);\n            };\n\n            const decrement = type => {\n              state[type]--;\n              stack.pop();\n            };\n\n            /**\n             * Push tokens onto the tokens array. This helper speeds up\n             * tokenizing by 1) helping us avoid backtracking as much as possible,\n             * and 2) helping us avoid creating extra tokens when consecutive\n             * characters are plain text. This improves performance and simplifies\n             * lookbehinds.\n             */\n\n            const push = tok => {\n              if (prev.type === 'globstar') {\n                const isBrace =\n                  state.braces > 0 &&\n                  (tok.type === 'comma' || tok.type === 'brace');\n                const isExtglob =\n                  tok.extglob === true ||\n                  (extglobs.length &&\n                    (tok.type === 'pipe' || tok.type === 'paren'));\n\n                if (\n                  tok.type !== 'slash' &&\n                  tok.type !== 'paren' &&\n                  !isBrace &&\n                  !isExtglob\n                ) {\n                  state.output = state.output.slice(0, -prev.output.length);\n                  prev.type = 'star';\n                  prev.value = '*';\n                  prev.output = star;\n                  state.output += prev.output;\n                }\n              }\n\n              if (extglobs.length && tok.type !== 'paren') {\n                extglobs[extglobs.length - 1].inner += tok.value;\n              }\n\n              if (tok.value || tok.output) append(tok);\n              if (prev && prev.type === 'text' && tok.type === 'text') {\n                prev.output = (prev.output || prev.value) + tok.value;\n                prev.value += tok.value;\n                return;\n              }\n\n              tok.prev = prev;\n              tokens.push(tok);\n              prev = tok;\n            };\n\n            const extglobOpen = (type, value) => {\n              const token = {\n                ...EXTGLOB_CHARS[value],\n                conditions: 1,\n                inner: '',\n              };\n\n              token.prev = prev;\n              token.parens = state.parens;\n              token.output = state.output;\n              const output = (opts.capture ? '(' : '') + token.open;\n\n              increment('parens');\n              push({ type, value, output: state.output ? '' : ONE_CHAR });\n              push({ type: 'paren', extglob: true, value: advance(), output });\n              extglobs.push(token);\n            };\n\n            const extglobClose = token => {\n              let output = token.close + (opts.capture ? ')' : '');\n              let rest;\n\n              if (token.type === 'negate') {\n                let extglobStar = star;\n\n                if (\n                  token.inner &&\n                  token.inner.length > 1 &&\n                  token.inner.includes('/')\n                ) {\n                  extglobStar = globstar(opts);\n                }\n\n                if (\n                  extglobStar !== star ||\n                  eos() ||\n                  /^\\)+$/.test(remaining())\n                ) {\n                  output = token.close = `)$))${extglobStar}`;\n                }\n\n                if (\n                  token.inner.includes('*') &&\n                  (rest = remaining()) &&\n                  /^\\.[^\\\\/.]+$/.test(rest)\n                ) {\n                  // Any non-magical string (`.ts`) or even nested expression (`.{ts,tsx}`) can follow after the closing parenthesis.\n                  // In this case, we need to parse the string and use it in the output of the original pattern.\n                  // Suitable patterns: `/!(*.d).ts`, `/!(*.d).{ts,tsx}`, `**/!(*-dbg).@(js)`.\n                  //\n                  // Disabling the `fastpaths` option due to a problem with parsing strings as `.ts` in the pattern like `**/!(*.d).ts`.\n                  const expression = parse(rest, {\n                    ...options,\n                    fastpaths: false,\n                  }).output;\n\n                  output = token.close = `)${expression})${extglobStar})`;\n                }\n\n                if (token.prev.type === 'bos') {\n                  state.negatedExtglob = true;\n                }\n              }\n\n              push({ type: 'paren', extglob: true, value, output });\n              decrement('parens');\n            };\n\n            /**\n             * Fast paths\n             */\n\n            if (\n              opts.fastpaths !== false &&\n              !/(^[*!]|[/()[\\]{}\"])/.test(input)\n            ) {\n              let backslashes = false;\n\n              let output = input.replace(\n                REGEX_SPECIAL_CHARS_BACKREF,\n                (m, esc, chars, first, rest, index) => {\n                  if (first === '\\\\') {\n                    backslashes = true;\n                    return m;\n                  }\n\n                  if (first === '?') {\n                    if (esc) {\n                      return (\n                        esc + first + (rest ? QMARK.repeat(rest.length) : '')\n                      );\n                    }\n                    if (index === 0) {\n                      return (\n                        qmarkNoDot + (rest ? QMARK.repeat(rest.length) : '')\n                      );\n                    }\n                    return QMARK.repeat(chars.length);\n                  }\n\n                  if (first === '.') {\n                    return DOT_LITERAL.repeat(chars.length);\n                  }\n\n                  if (first === '*') {\n                    if (esc) {\n                      return esc + first + (rest ? star : '');\n                    }\n                    return star;\n                  }\n                  return esc ? m : `\\\\${m}`;\n                }\n              );\n\n              if (backslashes === true) {\n                if (opts.unescape === true) {\n                  output = output.replace(/\\\\/g, '');\n                } else {\n                  output = output.replace(/\\\\+/g, m => {\n                    return m.length % 2 === 0 ? '\\\\\\\\' : m ? '\\\\' : '';\n                  });\n                }\n              }\n\n              if (output === input && opts.contains === true) {\n                state.output = input;\n                return state;\n              }\n\n              state.output = utils.wrapOutput(output, state, options);\n              return state;\n            }\n\n            /**\n             * Tokenize input until we reach end-of-string\n             */\n\n            while (!eos()) {\n              value = advance();\n\n              if (value === '\\u0000') {\n                continue;\n              }\n\n              /**\n               * Escaped characters\n               */\n\n              if (value === '\\\\') {\n                const next = peek();\n\n                if (next === '/' && opts.bash !== true) {\n                  continue;\n                }\n\n                if (next === '.' || next === ';') {\n                  continue;\n                }\n\n                if (!next) {\n                  value += '\\\\';\n                  push({ type: 'text', value });\n                  continue;\n                }\n\n                // collapse slashes to reduce potential for exploits\n                const match = /^\\\\+/.exec(remaining());\n                let slashes = 0;\n\n                if (match && match[0].length > 2) {\n                  slashes = match[0].length;\n                  state.index += slashes;\n                  if (slashes % 2 !== 0) {\n                    value += '\\\\';\n                  }\n                }\n\n                if (opts.unescape === true) {\n                  value = advance();\n                } else {\n                  value += advance();\n                }\n\n                if (state.brackets === 0) {\n                  push({ type: 'text', value });\n                  continue;\n                }\n              }\n\n              /**\n               * If we're inside a regex character class, continue\n               * until we reach the closing bracket.\n               */\n\n              if (\n                state.brackets > 0 &&\n                (value !== ']' || prev.value === '[' || prev.value === '[^')\n              ) {\n                if (opts.posix !== false && value === ':') {\n                  const inner = prev.value.slice(1);\n                  if (inner.includes('[')) {\n                    prev.posix = true;\n\n                    if (inner.includes(':')) {\n                      const idx = prev.value.lastIndexOf('[');\n                      const pre = prev.value.slice(0, idx);\n                      const rest = prev.value.slice(idx + 2);\n                      const posix = POSIX_REGEX_SOURCE[rest];\n                      if (posix) {\n                        prev.value = pre + posix;\n                        state.backtrack = true;\n                        advance();\n\n                        if (!bos.output && tokens.indexOf(prev) === 1) {\n                          bos.output = ONE_CHAR;\n                        }\n                        continue;\n                      }\n                    }\n                  }\n                }\n\n                if (\n                  (value === '[' && peek() !== ':') ||\n                  (value === '-' && peek() === ']')\n                ) {\n                  value = `\\\\${value}`;\n                }\n\n                if (\n                  value === ']' &&\n                  (prev.value === '[' || prev.value === '[^')\n                ) {\n                  value = `\\\\${value}`;\n                }\n\n                if (\n                  opts.posix === true &&\n                  value === '!' &&\n                  prev.value === '['\n                ) {\n                  value = '^';\n                }\n\n                prev.value += value;\n                append({ value });\n                continue;\n              }\n\n              /**\n               * If we're inside a quoted string, continue\n               * until we reach the closing double quote.\n               */\n\n              if (state.quotes === 1 && value !== '\"') {\n                value = utils.escapeRegex(value);\n                prev.value += value;\n                append({ value });\n                continue;\n              }\n\n              /**\n               * Double quotes\n               */\n\n              if (value === '\"') {\n                state.quotes = state.quotes === 1 ? 0 : 1;\n                if (opts.keepQuotes === true) {\n                  push({ type: 'text', value });\n                }\n                continue;\n              }\n\n              /**\n               * Parentheses\n               */\n\n              if (value === '(') {\n                increment('parens');\n                push({ type: 'paren', value });\n                continue;\n              }\n\n              if (value === ')') {\n                if (state.parens === 0 && opts.strictBrackets === true) {\n                  throw new SyntaxError(syntaxError('opening', '('));\n                }\n\n                const extglob = extglobs[extglobs.length - 1];\n                if (extglob && state.parens === extglob.parens + 1) {\n                  extglobClose(extglobs.pop());\n                  continue;\n                }\n\n                push({\n                  type: 'paren',\n                  value,\n                  output: state.parens ? ')' : '\\\\)',\n                });\n                decrement('parens');\n                continue;\n              }\n\n              /**\n               * Square brackets\n               */\n\n              if (value === '[') {\n                if (opts.nobracket === true || !remaining().includes(']')) {\n                  if (opts.nobracket !== true && opts.strictBrackets === true) {\n                    throw new SyntaxError(syntaxError('closing', ']'));\n                  }\n\n                  value = `\\\\${value}`;\n                } else {\n                  increment('brackets');\n                }\n\n                push({ type: 'bracket', value });\n                continue;\n              }\n\n              if (value === ']') {\n                if (\n                  opts.nobracket === true ||\n                  (prev && prev.type === 'bracket' && prev.value.length === 1)\n                ) {\n                  push({ type: 'text', value, output: `\\\\${value}` });\n                  continue;\n                }\n\n                if (state.brackets === 0) {\n                  if (opts.strictBrackets === true) {\n                    throw new SyntaxError(syntaxError('opening', '['));\n                  }\n\n                  push({ type: 'text', value, output: `\\\\${value}` });\n                  continue;\n                }\n\n                decrement('brackets');\n\n                const prevValue = prev.value.slice(1);\n                if (\n                  prev.posix !== true &&\n                  prevValue[0] === '^' &&\n                  !prevValue.includes('/')\n                ) {\n                  value = `/${value}`;\n                }\n\n                prev.value += value;\n                append({ value });\n\n                // when literal brackets are explicitly disabled\n                // assume we should match with a regex character class\n                if (\n                  opts.literalBrackets === false ||\n                  utils.hasRegexChars(prevValue)\n                ) {\n                  continue;\n                }\n\n                const escaped = utils.escapeRegex(prev.value);\n                state.output = state.output.slice(0, -prev.value.length);\n\n                // when literal brackets are explicitly enabled\n                // assume we should escape the brackets to match literal characters\n                if (opts.literalBrackets === true) {\n                  state.output += escaped;\n                  prev.value = escaped;\n                  continue;\n                }\n\n                // when the user specifies nothing, try to match both\n                prev.value = `(${capture}${escaped}|${prev.value})`;\n                state.output += prev.value;\n                continue;\n              }\n\n              /**\n               * Braces\n               */\n\n              if (value === '{' && opts.nobrace !== true) {\n                increment('braces');\n\n                const open = {\n                  type: 'brace',\n                  value,\n                  output: '(',\n                  outputIndex: state.output.length,\n                  tokensIndex: state.tokens.length,\n                };\n\n                braces.push(open);\n                push(open);\n                continue;\n              }\n\n              if (value === '}') {\n                const brace = braces[braces.length - 1];\n\n                if (opts.nobrace === true || !brace) {\n                  push({ type: 'text', value, output: value });\n                  continue;\n                }\n\n                let output = ')';\n\n                if (brace.dots === true) {\n                  const arr = tokens.slice();\n                  const range = [];\n\n                  for (let i = arr.length - 1; i >= 0; i--) {\n                    tokens.pop();\n                    if (arr[i].type === 'brace') {\n                      break;\n                    }\n                    if (arr[i].type !== 'dots') {\n                      range.unshift(arr[i].value);\n                    }\n                  }\n\n                  output = expandRange(range, opts);\n                  state.backtrack = true;\n                }\n\n                if (brace.comma !== true && brace.dots !== true) {\n                  const out = state.output.slice(0, brace.outputIndex);\n                  const toks = state.tokens.slice(brace.tokensIndex);\n                  brace.value = brace.output = '\\\\{';\n                  value = output = '\\\\}';\n                  state.output = out;\n                  for (const t of toks) {\n                    state.output += t.output || t.value;\n                  }\n                }\n\n                push({ type: 'brace', value, output });\n                decrement('braces');\n                braces.pop();\n                continue;\n              }\n\n              /**\n               * Pipes\n               */\n\n              if (value === '|') {\n                if (extglobs.length > 0) {\n                  extglobs[extglobs.length - 1].conditions++;\n                }\n                push({ type: 'text', value });\n                continue;\n              }\n\n              /**\n               * Commas\n               */\n\n              if (value === ',') {\n                let output = value;\n\n                const brace = braces[braces.length - 1];\n                if (brace && stack[stack.length - 1] === 'braces') {\n                  brace.comma = true;\n                  output = '|';\n                }\n\n                push({ type: 'comma', value, output });\n                continue;\n              }\n\n              /**\n               * Slashes\n               */\n\n              if (value === '/') {\n                // if the beginning of the glob is \"./\", advance the start\n                // to the current index, and don't add the \"./\" characters\n                // to the state. This greatly simplifies lookbehinds when\n                // checking for BOS characters like \"!\" and \".\" (not \"./\")\n                if (prev.type === 'dot' && state.index === state.start + 1) {\n                  state.start = state.index + 1;\n                  state.consumed = '';\n                  state.output = '';\n                  tokens.pop();\n                  prev = bos; // reset \"prev\" to the first token\n                  continue;\n                }\n\n                push({ type: 'slash', value, output: SLASH_LITERAL });\n                continue;\n              }\n\n              /**\n               * Dots\n               */\n\n              if (value === '.') {\n                if (state.braces > 0 && prev.type === 'dot') {\n                  if (prev.value === '.') prev.output = DOT_LITERAL;\n                  const brace = braces[braces.length - 1];\n                  prev.type = 'dots';\n                  prev.output += value;\n                  prev.value += value;\n                  brace.dots = true;\n                  continue;\n                }\n\n                if (\n                  state.braces + state.parens === 0 &&\n                  prev.type !== 'bos' &&\n                  prev.type !== 'slash'\n                ) {\n                  push({ type: 'text', value, output: DOT_LITERAL });\n                  continue;\n                }\n\n                push({ type: 'dot', value, output: DOT_LITERAL });\n                continue;\n              }\n\n              /**\n               * Question marks\n               */\n\n              if (value === '?') {\n                const isGroup = prev && prev.value === '(';\n                if (\n                  !isGroup &&\n                  opts.noextglob !== true &&\n                  peek() === '(' &&\n                  peek(2) !== '?'\n                ) {\n                  extglobOpen('qmark', value);\n                  continue;\n                }\n\n                if (prev && prev.type === 'paren') {\n                  const next = peek();\n                  let output = value;\n\n                  if (\n                    (prev.value === '(' && !/[!=<:]/.test(next)) ||\n                    (next === '<' && !/<([!=]|\\w+>)/.test(remaining()))\n                  ) {\n                    output = `\\\\${value}`;\n                  }\n\n                  push({ type: 'text', value, output });\n                  continue;\n                }\n\n                if (\n                  opts.dot !== true &&\n                  (prev.type === 'slash' || prev.type === 'bos')\n                ) {\n                  push({ type: 'qmark', value, output: QMARK_NO_DOT });\n                  continue;\n                }\n\n                push({ type: 'qmark', value, output: QMARK });\n                continue;\n              }\n\n              /**\n               * Exclamation\n               */\n\n              if (value === '!') {\n                if (opts.noextglob !== true && peek() === '(') {\n                  if (peek(2) !== '?' || !/[!=<:]/.test(peek(3))) {\n                    extglobOpen('negate', value);\n                    continue;\n                  }\n                }\n\n                if (opts.nonegate !== true && state.index === 0) {\n                  negate();\n                  continue;\n                }\n              }\n\n              /**\n               * Plus\n               */\n\n              if (value === '+') {\n                if (\n                  opts.noextglob !== true &&\n                  peek() === '(' &&\n                  peek(2) !== '?'\n                ) {\n                  extglobOpen('plus', value);\n                  continue;\n                }\n\n                if ((prev && prev.value === '(') || opts.regex === false) {\n                  push({ type: 'plus', value, output: PLUS_LITERAL });\n                  continue;\n                }\n\n                if (\n                  (prev &&\n                    (prev.type === 'bracket' ||\n                      prev.type === 'paren' ||\n                      prev.type === 'brace')) ||\n                  state.parens > 0\n                ) {\n                  push({ type: 'plus', value });\n                  continue;\n                }\n\n                push({ type: 'plus', value: PLUS_LITERAL });\n                continue;\n              }\n\n              /**\n               * Plain text\n               */\n\n              if (value === '@') {\n                if (\n                  opts.noextglob !== true &&\n                  peek() === '(' &&\n                  peek(2) !== '?'\n                ) {\n                  push({ type: 'at', extglob: true, value, output: '' });\n                  continue;\n                }\n\n                push({ type: 'text', value });\n                continue;\n              }\n\n              /**\n               * Plain text\n               */\n\n              if (value !== '*') {\n                if (value === '$' || value === '^') {\n                  value = `\\\\${value}`;\n                }\n\n                const match = REGEX_NON_SPECIAL_CHARS.exec(remaining());\n                if (match) {\n                  value += match[0];\n                  state.index += match[0].length;\n                }\n\n                push({ type: 'text', value });\n                continue;\n              }\n\n              /**\n               * Stars\n               */\n\n              if (prev && (prev.type === 'globstar' || prev.star === true)) {\n                prev.type = 'star';\n                prev.star = true;\n                prev.value += value;\n                prev.output = star;\n                state.backtrack = true;\n                state.globstar = true;\n                consume(value);\n                continue;\n              }\n\n              let rest = remaining();\n              if (opts.noextglob !== true && /^\\([^?]/.test(rest)) {\n                extglobOpen('star', value);\n                continue;\n              }\n\n              if (prev.type === 'star') {\n                if (opts.noglobstar === true) {\n                  consume(value);\n                  continue;\n                }\n\n                const prior = prev.prev;\n                const before = prior.prev;\n                const isStart = prior.type === 'slash' || prior.type === 'bos';\n                const afterStar =\n                  before &&\n                  (before.type === 'star' || before.type === 'globstar');\n\n                if (\n                  opts.bash === true &&\n                  (!isStart || (rest[0] && rest[0] !== '/'))\n                ) {\n                  push({ type: 'star', value, output: '' });\n                  continue;\n                }\n\n                const isBrace =\n                  state.braces > 0 &&\n                  (prior.type === 'comma' || prior.type === 'brace');\n                const isExtglob =\n                  extglobs.length &&\n                  (prior.type === 'pipe' || prior.type === 'paren');\n                if (\n                  !isStart &&\n                  prior.type !== 'paren' &&\n                  !isBrace &&\n                  !isExtglob\n                ) {\n                  push({ type: 'star', value, output: '' });\n                  continue;\n                }\n\n                // strip consecutive `/**/`\n                while (rest.slice(0, 3) === '/**') {\n                  const after = input[state.index + 4];\n                  if (after && after !== '/') {\n                    break;\n                  }\n                  rest = rest.slice(3);\n                  consume('/**', 3);\n                }\n\n                if (prior.type === 'bos' && eos()) {\n                  prev.type = 'globstar';\n                  prev.value += value;\n                  prev.output = globstar(opts);\n                  state.output = prev.output;\n                  state.globstar = true;\n                  consume(value);\n                  continue;\n                }\n\n                if (\n                  prior.type === 'slash' &&\n                  prior.prev.type !== 'bos' &&\n                  !afterStar &&\n                  eos()\n                ) {\n                  state.output = state.output.slice(\n                    0,\n                    -(prior.output + prev.output).length\n                  );\n                  prior.output = `(?:${prior.output}`;\n\n                  prev.type = 'globstar';\n                  prev.output =\n                    globstar(opts) + (opts.strictSlashes ? ')' : '|$)');\n                  prev.value += value;\n                  state.globstar = true;\n                  state.output += prior.output + prev.output;\n                  consume(value);\n                  continue;\n                }\n\n                if (\n                  prior.type === 'slash' &&\n                  prior.prev.type !== 'bos' &&\n                  rest[0] === '/'\n                ) {\n                  const end = rest[1] !== void 0 ? '|$' : '';\n\n                  state.output = state.output.slice(\n                    0,\n                    -(prior.output + prev.output).length\n                  );\n                  prior.output = `(?:${prior.output}`;\n\n                  prev.type = 'globstar';\n                  prev.output = `${globstar(opts)}${SLASH_LITERAL}|${SLASH_LITERAL}${end})`;\n                  prev.value += value;\n\n                  state.output += prior.output + prev.output;\n                  state.globstar = true;\n\n                  consume(value + advance());\n\n                  push({ type: 'slash', value: '/', output: '' });\n                  continue;\n                }\n\n                if (prior.type === 'bos' && rest[0] === '/') {\n                  prev.type = 'globstar';\n                  prev.value += value;\n                  prev.output = `(?:^|${SLASH_LITERAL}|${globstar(opts)}${SLASH_LITERAL})`;\n                  state.output = prev.output;\n                  state.globstar = true;\n                  consume(value + advance());\n                  push({ type: 'slash', value: '/', output: '' });\n                  continue;\n                }\n\n                // remove single star from output\n                state.output = state.output.slice(0, -prev.output.length);\n\n                // reset previous token to globstar\n                prev.type = 'globstar';\n                prev.output = globstar(opts);\n                prev.value += value;\n\n                // reset output with globstar\n                state.output += prev.output;\n                state.globstar = true;\n                consume(value);\n                continue;\n              }\n\n              const token = { type: 'star', value, output: star };\n\n              if (opts.bash === true) {\n                token.output = '.*?';\n                if (prev.type === 'bos' || prev.type === 'slash') {\n                  token.output = nodot + token.output;\n                }\n                push(token);\n                continue;\n              }\n\n              if (\n                prev &&\n                (prev.type === 'bracket' || prev.type === 'paren') &&\n                opts.regex === true\n              ) {\n                token.output = value;\n                push(token);\n                continue;\n              }\n\n              if (\n                state.index === state.start ||\n                prev.type === 'slash' ||\n                prev.type === 'dot'\n              ) {\n                if (prev.type === 'dot') {\n                  state.output += NO_DOT_SLASH;\n                  prev.output += NO_DOT_SLASH;\n                } else if (opts.dot === true) {\n                  state.output += NO_DOTS_SLASH;\n                  prev.output += NO_DOTS_SLASH;\n                } else {\n                  state.output += nodot;\n                  prev.output += nodot;\n                }\n\n                if (peek() !== '*') {\n                  state.output += ONE_CHAR;\n                  prev.output += ONE_CHAR;\n                }\n              }\n\n              push(token);\n            }\n\n            while (state.brackets > 0) {\n              if (opts.strictBrackets === true)\n                throw new SyntaxError(syntaxError('closing', ']'));\n              state.output = utils.escapeLast(state.output, '[');\n              decrement('brackets');\n            }\n\n            while (state.parens > 0) {\n              if (opts.strictBrackets === true)\n                throw new SyntaxError(syntaxError('closing', ')'));\n              state.output = utils.escapeLast(state.output, '(');\n              decrement('parens');\n            }\n\n            while (state.braces > 0) {\n              if (opts.strictBrackets === true)\n                throw new SyntaxError(syntaxError('closing', '}'));\n              state.output = utils.escapeLast(state.output, '{');\n              decrement('braces');\n            }\n\n            if (\n              opts.strictSlashes !== true &&\n              (prev.type === 'star' || prev.type === 'bracket')\n            ) {\n              push({\n                type: 'maybe_slash',\n                value: '',\n                output: `${SLASH_LITERAL}?`,\n              });\n            }\n\n            // rebuild the output if we had to backtrack at any point\n            if (state.backtrack === true) {\n              state.output = '';\n\n              for (const token of state.tokens) {\n                state.output +=\n                  token.output != null ? token.output : token.value;\n\n                if (token.suffix) {\n                  state.output += token.suffix;\n                }\n              }\n            }\n\n            return state;\n          };\n\n          /**\n           * Fast paths for creating regular expressions for common glob patterns.\n           * This can significantly speed up processing and has very little downside\n           * impact when none of the fast paths match.\n           */\n\n          parse.fastpaths = (input, options) => {\n            const opts = { ...options };\n            const max =\n              typeof opts.maxLength === 'number'\n                ? Math.min(MAX_LENGTH, opts.maxLength)\n                : MAX_LENGTH;\n            const len = input.length;\n            if (len > max) {\n              throw new SyntaxError(\n                `Input length: ${len}, exceeds maximum allowed length: ${max}`\n              );\n            }\n\n            input = REPLACEMENTS[input] || input;\n\n            // create constants based on platform, for windows or posix\n            const {\n              DOT_LITERAL,\n              SLASH_LITERAL,\n              ONE_CHAR,\n              DOTS_SLASH,\n              NO_DOT,\n              NO_DOTS,\n              NO_DOTS_SLASH,\n              STAR,\n              START_ANCHOR,\n            } = constants.globChars(opts.windows);\n\n            const nodot = opts.dot ? NO_DOTS : NO_DOT;\n            const slashDot = opts.dot ? NO_DOTS_SLASH : NO_DOT;\n            const capture = opts.capture ? '' : '?:';\n            const state = { negated: false, prefix: '' };\n            let star = opts.bash === true ? '.*?' : STAR;\n\n            if (opts.capture) {\n              star = `(${star})`;\n            }\n\n            const globstar = opts => {\n              if (opts.noglobstar === true) return star;\n              return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`;\n            };\n\n            const create = str => {\n              switch (str) {\n                case '*':\n                  return `${nodot}${ONE_CHAR}${star}`;\n\n                case '.*':\n                  return `${DOT_LITERAL}${ONE_CHAR}${star}`;\n\n                case '*.*':\n                  return `${nodot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`;\n\n                case '*/*':\n                  return `${nodot}${star}${SLASH_LITERAL}${ONE_CHAR}${slashDot}${star}`;\n\n                case '**':\n                  return nodot + globstar(opts);\n\n                case '**/*':\n                  return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${ONE_CHAR}${star}`;\n\n                case '**/*.*':\n                  return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`;\n\n                case '**/.*':\n                  return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${DOT_LITERAL}${ONE_CHAR}${star}`;\n\n                default: {\n                  const match = /^(.*?)\\.(\\w+)$/.exec(str);\n                  if (!match) return;\n\n                  const source = create(match[1]);\n                  if (!source) return;\n\n                  return source + DOT_LITERAL + match[2];\n                }\n              }\n            };\n\n            const output = utils.removePrefix(input, state);\n            let source = create(output);\n\n            if (source && opts.strictSlashes !== true) {\n              source += `${SLASH_LITERAL}?`;\n            }\n\n            return source;\n          };\n\n          parse_1 = parse;\n          return parse_1;\n        }\n\n        var picomatch_1$1;\n        var hasRequiredPicomatch$1;\n\n        function requirePicomatch$1() {\n          if (hasRequiredPicomatch$1) return picomatch_1$1;\n          hasRequiredPicomatch$1 = 1;\n\n          const scan = /*@__PURE__*/ requireScan();\n          const parse = /*@__PURE__*/ requireParse();\n          const utils = /*@__PURE__*/ requireUtils();\n          const constants = /*@__PURE__*/ requireConstants();\n          const isObject = val =>\n            val && typeof val === 'object' && !Array.isArray(val);\n\n          /**\n           * Creates a matcher function from one or more glob patterns. The\n           * returned function takes a string to match as its first argument,\n           * and returns true if the string is a match. The returned matcher\n           * function also takes a boolean as the second argument that, when true,\n           * returns an object with additional information.\n           *\n           * ```js\n           * const picomatch = require('picomatch');\n           * // picomatch(glob[, options]);\n           *\n           * const isMatch = picomatch('*.!(*a)');\n           * console.log(isMatch('a.a')); //=> false\n           * console.log(isMatch('a.b')); //=> true\n           * ```\n           * @name picomatch\n           * @param {String|Array} `globs` One or more glob patterns.\n           * @param {Object=} `options`\n           * @return {Function=} Returns a matcher function.\n           * @api public\n           */\n\n          const picomatch = (glob, options, returnState = false) => {\n            if (Array.isArray(glob)) {\n              const fns = glob.map(input =>\n                picomatch(input, options, returnState)\n              );\n              const arrayMatcher = str => {\n                for (const isMatch of fns) {\n                  const state = isMatch(str);\n                  if (state) return state;\n                }\n                return false;\n              };\n              return arrayMatcher;\n            }\n\n            const isState = isObject(glob) && glob.tokens && glob.input;\n\n            if (glob === '' || (typeof glob !== 'string' && !isState)) {\n              throw new TypeError('Expected pattern to be a non-empty string');\n            }\n\n            const opts = options || {};\n            const posix = opts.windows;\n            const regex = isState\n              ? picomatch.compileRe(glob, options)\n              : picomatch.makeRe(glob, options, false, true);\n\n            const state = regex.state;\n            delete regex.state;\n\n            let isIgnored = () => false;\n            if (opts.ignore) {\n              const ignoreOpts = {\n                ...options,\n                ignore: null,\n                onMatch: null,\n                onResult: null,\n              };\n              isIgnored = picomatch(opts.ignore, ignoreOpts, returnState);\n            }\n\n            const matcher = (input, returnObject = false) => {\n              const { isMatch, match, output } = picomatch.test(\n                input,\n                regex,\n                options,\n                { glob, posix }\n              );\n              const result = {\n                glob,\n                state,\n                regex,\n                posix,\n                input,\n                output,\n                match,\n                isMatch,\n              };\n\n              if (typeof opts.onResult === 'function') {\n                opts.onResult(result);\n              }\n\n              if (isMatch === false) {\n                result.isMatch = false;\n                return returnObject ? result : false;\n              }\n\n              if (isIgnored(input)) {\n                if (typeof opts.onIgnore === 'function') {\n                  opts.onIgnore(result);\n                }\n                result.isMatch = false;\n                return returnObject ? result : false;\n              }\n\n              if (typeof opts.onMatch === 'function') {\n                opts.onMatch(result);\n              }\n              return returnObject ? result : true;\n            };\n\n            if (returnState) {\n              matcher.state = state;\n            }\n\n            return matcher;\n          };\n\n          /**\n           * Test `input` with the given `regex`. This is used by the main\n           * `picomatch()` function to test the input string.\n           *\n           * ```js\n           * const picomatch = require('picomatch');\n           * // picomatch.test(input, regex[, options]);\n           *\n           * console.log(picomatch.test('foo/bar', /^(?:([^/]*?)\\/([^/]*?))$/));\n           * // { isMatch: true, match: [ 'foo/', 'foo', 'bar' ], output: 'foo/bar' }\n           * ```\n           * @param {String} `input` String to test.\n           * @param {RegExp} `regex`\n           * @return {Object} Returns an object with matching info.\n           * @api public\n           */\n\n          picomatch.test = (input, regex, options, { glob, posix } = {}) => {\n            if (typeof input !== 'string') {\n              throw new TypeError('Expected input to be a string');\n            }\n\n            if (input === '') {\n              return { isMatch: false, output: '' };\n            }\n\n            const opts = options || {};\n            const format = opts.format || (posix ? utils.toPosixSlashes : null);\n            let match = input === glob;\n            let output = match && format ? format(input) : input;\n\n            if (match === false) {\n              output = format ? format(input) : input;\n              match = output === glob;\n            }\n\n            if (match === false || opts.capture === true) {\n              if (opts.matchBase === true || opts.basename === true) {\n                match = picomatch.matchBase(input, regex, options, posix);\n              } else {\n                match = regex.exec(output);\n              }\n            }\n\n            return { isMatch: Boolean(match), match, output };\n          };\n\n          /**\n           * Match the basename of a filepath.\n           *\n           * ```js\n           * const picomatch = require('picomatch');\n           * // picomatch.matchBase(input, glob[, options]);\n           * console.log(picomatch.matchBase('foo/bar.js', '*.js'); // true\n           * ```\n           * @param {String} `input` String to test.\n           * @param {RegExp|String} `glob` Glob pattern or regex created by [.makeRe](#makeRe).\n           * @return {Boolean}\n           * @api public\n           */\n\n          picomatch.matchBase = (input, glob, options) => {\n            const regex =\n              glob instanceof RegExp ? glob : picomatch.makeRe(glob, options);\n            return regex.test(utils.basename(input));\n          };\n\n          /**\n           * Returns true if **any** of the given glob `patterns` match the specified `string`.\n           *\n           * ```js\n           * const picomatch = require('picomatch');\n           * // picomatch.isMatch(string, patterns[, options]);\n           *\n           * console.log(picomatch.isMatch('a.a', ['b.*', '*.a'])); //=> true\n           * console.log(picomatch.isMatch('a.a', 'b.*')); //=> false\n           * ```\n           * @param {String|Array} str The string to test.\n           * @param {String|Array} patterns One or more glob patterns to use for matching.\n           * @param {Object} [options] See available [options](#options).\n           * @return {Boolean} Returns true if any patterns match `str`\n           * @api public\n           */\n\n          picomatch.isMatch = (str, patterns, options) =>\n            picomatch(patterns, options)(str);\n\n          /**\n           * Parse a glob pattern to create the source string for a regular\n           * expression.\n           *\n           * ```js\n           * const picomatch = require('picomatch');\n           * const result = picomatch.parse(pattern[, options]);\n           * ```\n           * @param {String} `pattern`\n           * @param {Object} `options`\n           * @return {Object} Returns an object with useful properties and output to be used as a regex source string.\n           * @api public\n           */\n\n          picomatch.parse = (pattern, options) => {\n            if (Array.isArray(pattern))\n              return pattern.map(p => picomatch.parse(p, options));\n            return parse(pattern, { ...options, fastpaths: false });\n          };\n\n          /**\n           * Scan a glob pattern to separate the pattern into segments.\n           *\n           * ```js\n           * const picomatch = require('picomatch');\n           * // picomatch.scan(input[, options]);\n           *\n           * const result = picomatch.scan('!./foo/*.js');\n           * console.log(result);\n           * { prefix: '!./',\n           *   input: '!./foo/*.js',\n           *   start: 3,\n           *   base: 'foo',\n           *   glob: '*.js',\n           *   isBrace: false,\n           *   isBracket: false,\n           *   isGlob: true,\n           *   isExtglob: false,\n           *   isGlobstar: false,\n           *   negated: true }\n           * ```\n           * @param {String} `input` Glob pattern to scan.\n           * @param {Object} `options`\n           * @return {Object} Returns an object with\n           * @api public\n           */\n\n          picomatch.scan = (input, options) => scan(input, options);\n\n          /**\n           * Compile a regular expression from the `state` object returned by the\n           * [parse()](#parse) method.\n           *\n           * @param {Object} `state`\n           * @param {Object} `options`\n           * @param {Boolean} `returnOutput` Intended for implementors, this argument allows you to return the raw output from the parser.\n           * @param {Boolean} `returnState` Adds the state to a `state` property on the returned regex. Useful for implementors and debugging.\n           * @return {RegExp}\n           * @api public\n           */\n\n          picomatch.compileRe = (\n            state,\n            options,\n            returnOutput = false,\n            returnState = false\n          ) => {\n            if (returnOutput === true) {\n              return state.output;\n            }\n\n            const opts = options || {};\n            const prepend = opts.contains ? '' : '^';\n            const append = opts.contains ? '' : '$';\n\n            let source = `${prepend}(?:${state.output})${append}`;\n            if (state && state.negated === true) {\n              source = `^(?!${source}).*$`;\n            }\n\n            const regex = picomatch.toRegex(source, options);\n            if (returnState === true) {\n              regex.state = state;\n            }\n\n            return regex;\n          };\n\n          /**\n           * Create a regular expression from a parsed glob pattern.\n           *\n           * ```js\n           * const picomatch = require('picomatch');\n           * const state = picomatch.parse('*.js');\n           * // picomatch.compileRe(state[, options]);\n           *\n           * console.log(picomatch.compileRe(state));\n           * //=> /^(?:(?!\\.)(?=.)[^/]*?\\.js)$/\n           * ```\n           * @param {String} `state` The object returned from the `.parse` method.\n           * @param {Object} `options`\n           * @param {Boolean} `returnOutput` Implementors may use this argument to return the compiled output, instead of a regular expression. This is not exposed on the options to prevent end-users from mutating the result.\n           * @param {Boolean} `returnState` Implementors may use this argument to return the state from the parsed glob with the returned regular expression.\n           * @return {RegExp} Returns a regex created from the given pattern.\n           * @api public\n           */\n\n          picomatch.makeRe = (\n            input,\n            options = {},\n            returnOutput = false,\n            returnState = false\n          ) => {\n            if (!input || typeof input !== 'string') {\n              throw new TypeError('Expected a non-empty string');\n            }\n\n            let parsed = { negated: false, fastpaths: true };\n\n            if (\n              options.fastpaths !== false &&\n              (input[0] === '.' || input[0] === '*')\n            ) {\n              parsed.output = parse.fastpaths(input, options);\n            }\n\n            if (!parsed.output) {\n              parsed = parse(input, options);\n            }\n\n            return picomatch.compileRe(\n              parsed,\n              options,\n              returnOutput,\n              returnState\n            );\n          };\n\n          /**\n           * Create a regular expression from the given regex source string.\n           *\n           * ```js\n           * const picomatch = require('picomatch');\n           * // picomatch.toRegex(source[, options]);\n           *\n           * const { output } = picomatch.parse('*.js');\n           * console.log(picomatch.toRegex(output));\n           * //=> /^(?:(?!\\.)(?=.)[^/]*?\\.js)$/\n           * ```\n           * @param {String} `source` Regular expression source string.\n           * @param {Object} `options`\n           * @return {RegExp}\n           * @api public\n           */\n\n          picomatch.toRegex = (source, options) => {\n            try {\n              const opts = options || {};\n              return new RegExp(source, opts.flags || (opts.nocase ? 'i' : ''));\n            } catch (err) {\n              if (options && options.debug === true) throw err;\n              return /$^/;\n            }\n          };\n\n          /**\n           * Picomatch constants.\n           * @return {Object}\n           */\n\n          picomatch.constants = constants;\n\n          /**\n           * Expose \"picomatch\"\n           */\n\n          picomatch_1$1 = picomatch;\n          return picomatch_1$1;\n        }\n\n        var picomatch_1;\n        var hasRequiredPicomatch;\n\n        function requirePicomatch() {\n          if (hasRequiredPicomatch) return picomatch_1;\n          hasRequiredPicomatch = 1;\n\n          const pico = /*@__PURE__*/ requirePicomatch$1();\n          const utils = /*@__PURE__*/ requireUtils();\n\n          function picomatch(glob, options, returnState = false) {\n            // default to os.platform()\n            if (\n              options &&\n              (options.windows === null || options.windows === undefined)\n            ) {\n              // don't mutate the original options object\n              options = { ...options, windows: utils.isWindows() };\n            }\n\n            return pico(glob, options, returnState);\n          }\n\n          Object.assign(picomatch, pico);\n          picomatch_1 = picomatch;\n          return picomatch_1;\n        }\n\n        var picomatchExports = /*@__PURE__*/ requirePicomatch();\n        var pm = /*@__PURE__*/ getDefaultExportFromCjs(picomatchExports);\n\n        function isArray(arg) {\n          return Array.isArray(arg);\n        }\n        function ensureArray(thing) {\n          if (isArray(thing)) return thing;\n          if (thing == null) return [];\n          return [thing];\n        }\n        const globToTest = glob => {\n          const pattern = glob;\n          const fn = pm(pattern, { dot: true });\n          return {\n            test: what => {\n              const result = fn(what);\n              return result;\n            },\n          };\n        };\n        const testTrue = {\n          test: () => true,\n        };\n        const getMatcher = filter => {\n          const bundleTest =\n            'bundle' in filter && filter.bundle != null\n              ? globToTest(filter.bundle)\n              : testTrue;\n          const fileTest =\n            'file' in filter && filter.file != null\n              ? globToTest(filter.file)\n              : testTrue;\n          return { bundleTest, fileTest };\n        };\n        const createFilter = (include, exclude) => {\n          const includeMatchers = ensureArray(include).map(getMatcher);\n          const excludeMatchers = ensureArray(exclude).map(getMatcher);\n          return (bundleId, id) => {\n            for (let i = 0; i < excludeMatchers.length; ++i) {\n              const { bundleTest, fileTest } = excludeMatchers[i];\n              if (bundleTest.test(bundleId) && fileTest.test(id)) return false;\n            }\n            for (let i = 0; i < includeMatchers.length; ++i) {\n              const { bundleTest, fileTest } = includeMatchers[i];\n              if (bundleTest.test(bundleId) && fileTest.test(id)) return true;\n            }\n            return !includeMatchers.length;\n          };\n        };\n\n        const throttleFilter = (callback, limit) => {\n          let waiting = false;\n          return val => {\n            if (!waiting) {\n              callback(val);\n              waiting = true;\n              setTimeout(() => {\n                waiting = false;\n              }, limit);\n            }\n          };\n        };\n        const prepareFilter = filt => {\n          if (filt === '') return [];\n          return (\n            filt\n              .split(',')\n              // remove spaces before and after\n              .map(entry => entry.trim())\n              // unquote \"\n              .map(entry =>\n                entry.startsWith('\"') && entry.endsWith('\"')\n                  ? entry.substring(1, entry.length - 1)\n                  : entry\n              )\n              // unquote '\n              .map(entry =>\n                entry.startsWith(\"'\") && entry.endsWith(\"'\")\n                  ? entry.substring(1, entry.length - 1)\n                  : entry\n              )\n              // remove empty strings\n              .filter(entry => entry)\n              // parse bundle:file\n              .map(entry => entry.split(':'))\n              // normalize entry just in case\n              .flatMap(entry => {\n                if (entry.length === 0) return [];\n                let bundle = null;\n                let file = null;\n                if (entry.length === 1 && entry[0]) {\n                  file = entry[0];\n                  return [{ file, bundle }];\n                }\n                bundle = entry[0] || null;\n                file = entry.slice(1).join(':') || null;\n                return [{ bundle, file }];\n              })\n          );\n        };\n        const useFilter = () => {\n          const [includeFilter, setIncludeFilter] = d('');\n          const [excludeFilter, setExcludeFilter] = d('');\n          const setIncludeFilterTrottled = T(\n            () => throttleFilter(setIncludeFilter, 200),\n            []\n          );\n          const setExcludeFilterTrottled = T(\n            () => throttleFilter(setExcludeFilter, 200),\n            []\n          );\n          const isIncluded = T(\n            () =>\n              createFilter(\n                prepareFilter(includeFilter),\n                prepareFilter(excludeFilter)\n              ),\n            [includeFilter, excludeFilter]\n          );\n          const getModuleFilterMultiplier = q(\n            (bundleId, data) => {\n              return isIncluded(bundleId, data.id) ? 1 : 0;\n            },\n            [isIncluded]\n          );\n          return {\n            getModuleFilterMultiplier,\n            includeFilter,\n            excludeFilter,\n            setExcludeFilter: setExcludeFilterTrottled,\n            setIncludeFilter: setIncludeFilterTrottled,\n          };\n        };\n\n        function ascending(a, b) {\n          return a == null || b == null\n            ? NaN\n            : a < b\n              ? -1\n              : a > b\n                ? 1\n                : a >= b\n                  ? 0\n                  : NaN;\n        }\n\n        function descending(a, b) {\n          return a == null || b == null\n            ? NaN\n            : b < a\n              ? -1\n              : b > a\n                ? 1\n                : b >= a\n                  ? 0\n                  : NaN;\n        }\n\n        function bisector(f) {\n          let compare1, compare2, delta;\n\n          // If an accessor is specified, promote it to a comparator. In this case we\n          // can test whether the search value is (self-) comparable. We can’t do this\n          // for a comparator (except for specific, known comparators) because we can’t\n          // tell if the comparator is symmetric, and an asymmetric comparator can’t be\n          // used to test whether a single value is comparable.\n          if (f.length !== 2) {\n            compare1 = ascending;\n            compare2 = (d, x) => ascending(f(d), x);\n            delta = (d, x) => f(d) - x;\n          } else {\n            compare1 = f === ascending || f === descending ? f : zero$1;\n            compare2 = f;\n            delta = f;\n          }\n\n          function left(a, x, lo = 0, hi = a.length) {\n            if (lo < hi) {\n              if (compare1(x, x) !== 0) return hi;\n              do {\n                const mid = (lo + hi) >>> 1;\n                if (compare2(a[mid], x) < 0) lo = mid + 1;\n                else hi = mid;\n              } while (lo < hi);\n            }\n            return lo;\n          }\n\n          function right(a, x, lo = 0, hi = a.length) {\n            if (lo < hi) {\n              if (compare1(x, x) !== 0) return hi;\n              do {\n                const mid = (lo + hi) >>> 1;\n                if (compare2(a[mid], x) <= 0) lo = mid + 1;\n                else hi = mid;\n              } while (lo < hi);\n            }\n            return lo;\n          }\n\n          function center(a, x, lo = 0, hi = a.length) {\n            const i = left(a, x, lo, hi - 1);\n            return i > lo && delta(a[i - 1], x) > -delta(a[i], x) ? i - 1 : i;\n          }\n\n          return { left, center, right };\n        }\n\n        function zero$1() {\n          return 0;\n        }\n\n        function number$1(x) {\n          return x === null ? NaN : +x;\n        }\n\n        const ascendingBisect = bisector(ascending);\n        const bisectRight = ascendingBisect.right;\n        bisector(number$1).center;\n\n        class InternMap extends Map {\n          constructor(entries, key = keyof) {\n            super();\n            Object.defineProperties(this, {\n              _intern: { value: new Map() },\n              _key: { value: key },\n            });\n            if (entries != null)\n              for (const [key, value] of entries) this.set(key, value);\n          }\n          get(key) {\n            return super.get(intern_get(this, key));\n          }\n          has(key) {\n            return super.has(intern_get(this, key));\n          }\n          set(key, value) {\n            return super.set(intern_set(this, key), value);\n          }\n          delete(key) {\n            return super.delete(intern_delete(this, key));\n          }\n        }\n\n        function intern_get({ _intern, _key }, value) {\n          const key = _key(value);\n          return _intern.has(key) ? _intern.get(key) : value;\n        }\n\n        function intern_set({ _intern, _key }, value) {\n          const key = _key(value);\n          if (_intern.has(key)) return _intern.get(key);\n          _intern.set(key, value);\n          return value;\n        }\n\n        function intern_delete({ _intern, _key }, value) {\n          const key = _key(value);\n          if (_intern.has(key)) {\n            value = _intern.get(key);\n            _intern.delete(key);\n          }\n          return value;\n        }\n\n        function keyof(value) {\n          return value !== null && typeof value === 'object'\n            ? value.valueOf()\n            : value;\n        }\n\n        function identity$2(x) {\n          return x;\n        }\n\n        function group(values, ...keys) {\n          return nest(values, identity$2, identity$2, keys);\n        }\n\n        function nest(values, map, reduce, keys) {\n          return (function regroup(values, i) {\n            if (i >= keys.length) return reduce(values);\n            const groups = new InternMap();\n            const keyof = keys[i++];\n            let index = -1;\n            for (const value of values) {\n              const key = keyof(value, ++index, values);\n              const group = groups.get(key);\n              if (group) group.push(value);\n              else groups.set(key, [value]);\n            }\n            for (const [key, values] of groups) {\n              groups.set(key, regroup(values, i));\n            }\n            return map(groups);\n          })(values, 0);\n        }\n\n        const e10 = Math.sqrt(50),\n          e5 = Math.sqrt(10),\n          e2 = Math.sqrt(2);\n\n        function tickSpec(start, stop, count) {\n          const step = (stop - start) / Math.max(0, count),\n            power = Math.floor(Math.log10(step)),\n            error = step / Math.pow(10, power),\n            factor = error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1;\n          let i1, i2, inc;\n          if (power < 0) {\n            inc = Math.pow(10, -power) / factor;\n            i1 = Math.round(start * inc);\n            i2 = Math.round(stop * inc);\n            if (i1 / inc < start) ++i1;\n            if (i2 / inc > stop) --i2;\n            inc = -inc;\n          } else {\n            inc = Math.pow(10, power) * factor;\n            i1 = Math.round(start / inc);\n            i2 = Math.round(stop / inc);\n            if (i1 * inc < start) ++i1;\n            if (i2 * inc > stop) --i2;\n          }\n          if (i2 < i1 && 0.5 <= count && count < 2)\n            return tickSpec(start, stop, count * 2);\n          return [i1, i2, inc];\n        }\n\n        function ticks(start, stop, count) {\n          ((stop = +stop), (start = +start), (count = +count));\n          if (!(count > 0)) return [];\n          if (start === stop) return [start];\n          const reverse = stop < start,\n            [i1, i2, inc] = reverse\n              ? tickSpec(stop, start, count)\n              : tickSpec(start, stop, count);\n          if (!(i2 >= i1)) return [];\n          const n = i2 - i1 + 1,\n            ticks = new Array(n);\n          if (reverse) {\n            if (inc < 0) for (let i = 0; i < n; ++i) ticks[i] = (i2 - i) / -inc;\n            else for (let i = 0; i < n; ++i) ticks[i] = (i2 - i) * inc;\n          } else {\n            if (inc < 0) for (let i = 0; i < n; ++i) ticks[i] = (i1 + i) / -inc;\n            else for (let i = 0; i < n; ++i) ticks[i] = (i1 + i) * inc;\n          }\n          return ticks;\n        }\n\n        function tickIncrement(start, stop, count) {\n          ((stop = +stop), (start = +start), (count = +count));\n          return tickSpec(start, stop, count)[2];\n        }\n\n        function tickStep(start, stop, count) {\n          ((stop = +stop), (start = +start), (count = +count));\n          const reverse = stop < start,\n            inc = reverse\n              ? tickIncrement(stop, start, count)\n              : tickIncrement(start, stop, count);\n          return (reverse ? -1 : 1) * (inc < 0 ? 1 / -inc : inc);\n        }\n\n        const TOP_PADDING = 20;\n        const PADDING = 2;\n\n        const Node = ({ node, onMouseOver, onClick, selected }) => {\n          const { getModuleColor } = x(StaticContext);\n          const { backgroundColor, fontColor } = getModuleColor(node);\n          const { x0, x1, y1, y0, data, children = null } = node;\n          const textRef = A(null);\n          const textRectRef = A();\n          const width = x1 - x0;\n          const height = y1 - y0;\n          const textProps = {\n            'font-size': '0.7em',\n            'dominant-baseline': 'middle',\n            'text-anchor': 'middle',\n            x: width / 2,\n          };\n          if (children != null) {\n            textProps.y = (TOP_PADDING + PADDING) / 2;\n          } else {\n            textProps.y = height / 2;\n          }\n          _(() => {\n            if (width == 0 || height == 0 || !textRef.current) {\n              return;\n            }\n            if (textRectRef.current == null) {\n              textRectRef.current = textRef.current.getBoundingClientRect();\n            }\n            let scale = 1;\n            if (children != null) {\n              scale = Math.min(\n                (width * 0.9) / textRectRef.current.width,\n                Math.min(height, TOP_PADDING + PADDING) /\n                  textRectRef.current.height\n              );\n              scale = Math.min(1, scale);\n              textRef.current.setAttribute(\n                'y',\n                String(Math.min(TOP_PADDING + PADDING, height) / 2 / scale)\n              );\n              textRef.current.setAttribute('x', String(width / 2 / scale));\n            } else {\n              scale = Math.min(\n                (width * 0.9) / textRectRef.current.width,\n                (height * 0.9) / textRectRef.current.height\n              );\n              scale = Math.min(1, scale);\n              textRef.current.setAttribute('y', String(height / 2 / scale));\n              textRef.current.setAttribute('x', String(width / 2 / scale));\n            }\n            textRef.current.setAttribute(\n              'transform',\n              `scale(${scale.toFixed(2)})`\n            );\n          }, [children, height, width]);\n          if (width == 0 || height == 0) {\n            return null;\n          }\n          return u$1('g', {\n            className: 'node',\n            transform: `translate(${x0},${y0})`,\n            onClick: event => {\n              event.stopPropagation();\n              onClick(node);\n            },\n            onMouseOver: event => {\n              event.stopPropagation();\n              onMouseOver(node);\n            },\n            children: [\n              u$1('rect', {\n                fill: backgroundColor,\n                rx: 2,\n                ry: 2,\n                width: x1 - x0,\n                height: y1 - y0,\n                stroke: selected ? '#fff' : undefined,\n                'stroke-width': selected ? 2 : undefined,\n              }),\n              u$1(\n                'text',\n                Object.assign(\n                  {\n                    ref: textRef,\n                    fill: fontColor,\n                    onClick: event => {\n                      var _a;\n                      if (\n                        ((_a = window.getSelection()) === null || _a === void 0\n                          ? void 0\n                          : _a.toString()) !== ''\n                      ) {\n                        event.stopPropagation();\n                      }\n                    },\n                  },\n                  textProps,\n                  { children: data.name }\n                )\n              ),\n            ],\n          });\n        };\n\n        const TreeMap = ({ root, onNodeHover, selectedNode, onNodeClick }) => {\n          const { width, height, getModuleIds } = x(StaticContext);\n          console.time('layering');\n          // this will make groups by height\n          const nestedData = T(() => {\n            const nestedDataMap = group(root.descendants(), d => d.height);\n            const nestedData = Array.from(nestedDataMap, ([key, values]) => ({\n              key,\n              values,\n            }));\n            nestedData.sort((a, b) => b.key - a.key);\n            return nestedData;\n          }, [root]);\n          console.timeEnd('layering');\n          return u$1('svg', {\n            xmlns: 'http://www.w3.org/2000/svg',\n            viewBox: `0 0 ${width} ${height}`,\n            children: nestedData.map(({ key, values }) => {\n              return u$1(\n                'g',\n                {\n                  className: 'layer',\n                  children: values.map(node => {\n                    return u$1(\n                      Node,\n                      {\n                        node: node,\n                        onMouseOver: onNodeHover,\n                        selected: selectedNode === node,\n                        onClick: onNodeClick,\n                      },\n                      getModuleIds(node.data).nodeUid.id\n                    );\n                  }),\n                },\n                key\n              );\n            }),\n          });\n        };\n\n        var bytes = { exports: {} };\n\n        /*!\n         * bytes\n         * Copyright(c) 2012-2014 TJ Holowaychuk\n         * Copyright(c) 2015 Jed Watson\n         * MIT Licensed\n         */\n\n        var hasRequiredBytes;\n\n        function requireBytes() {\n          if (hasRequiredBytes) return bytes.exports;\n          hasRequiredBytes = 1;\n\n          /**\n           * Module exports.\n           * @public\n           */\n\n          bytes.exports = bytes$1;\n          bytes.exports.format = format;\n          bytes.exports.parse = parse;\n\n          /**\n           * Module variables.\n           * @private\n           */\n\n          var formatThousandsRegExp = /\\B(?=(\\d{3})+(?!\\d))/g;\n\n          var formatDecimalsRegExp = /(?:\\.0*|(\\.[^0]+)0+)$/;\n\n          var map = {\n            b: 1,\n            kb: 1 << 10,\n            mb: 1 << 20,\n            gb: 1 << 30,\n            tb: Math.pow(1024, 4),\n            pb: Math.pow(1024, 5),\n          };\n\n          var parseRegExp = /^((-|\\+)?(\\d+(?:\\.\\d+)?)) *(kb|mb|gb|tb|pb)$/i;\n\n          /**\n           * Convert the given value in bytes into a string or parse to string to an integer in bytes.\n           *\n           * @param {string|number} value\n           * @param {{\n           *  case: [string],\n           *  decimalPlaces: [number]\n           *  fixedDecimals: [boolean]\n           *  thousandsSeparator: [string]\n           *  unitSeparator: [string]\n           *  }} [options] bytes options.\n           *\n           * @returns {string|number|null}\n           */\n\n          function bytes$1(value, options) {\n            if (typeof value === 'string') {\n              return parse(value);\n            }\n\n            if (typeof value === 'number') {\n              return format(value, options);\n            }\n\n            return null;\n          }\n\n          /**\n           * Format the given value in bytes into a string.\n           *\n           * If the value is negative, it is kept as such. If it is a float,\n           * it is rounded.\n           *\n           * @param {number} value\n           * @param {object} [options]\n           * @param {number} [options.decimalPlaces=2]\n           * @param {number} [options.fixedDecimals=false]\n           * @param {string} [options.thousandsSeparator=]\n           * @param {string} [options.unit=]\n           * @param {string} [options.unitSeparator=]\n           *\n           * @returns {string|null}\n           * @public\n           */\n\n          function format(value, options) {\n            if (!Number.isFinite(value)) {\n              return null;\n            }\n\n            var mag = Math.abs(value);\n            var thousandsSeparator =\n              (options && options.thousandsSeparator) || '';\n            var unitSeparator = (options && options.unitSeparator) || '';\n            var decimalPlaces =\n              options && options.decimalPlaces !== undefined\n                ? options.decimalPlaces\n                : 2;\n            var fixedDecimals = Boolean(options && options.fixedDecimals);\n            var unit = (options && options.unit) || '';\n\n            if (!unit || !map[unit.toLowerCase()]) {\n              if (mag >= map.pb) {\n                unit = 'PB';\n              } else if (mag >= map.tb) {\n                unit = 'TB';\n              } else if (mag >= map.gb) {\n                unit = 'GB';\n              } else if (mag >= map.mb) {\n                unit = 'MB';\n              } else if (mag >= map.kb) {\n                unit = 'KB';\n              } else {\n                unit = 'B';\n              }\n            }\n\n            var val = value / map[unit.toLowerCase()];\n            var str = val.toFixed(decimalPlaces);\n\n            if (!fixedDecimals) {\n              str = str.replace(formatDecimalsRegExp, '$1');\n            }\n\n            if (thousandsSeparator) {\n              str = str\n                .split('.')\n                .map(function (s, i) {\n                  return i === 0\n                    ? s.replace(formatThousandsRegExp, thousandsSeparator)\n                    : s;\n                })\n                .join('.');\n            }\n\n            return str + unitSeparator + unit;\n          }\n\n          /**\n           * Parse the string value into an integer in bytes.\n           *\n           * If no unit is given, it is assumed the value is in bytes.\n           *\n           * @param {number|string} val\n           *\n           * @returns {number|null}\n           * @public\n           */\n\n          function parse(val) {\n            if (typeof val === 'number' && !isNaN(val)) {\n              return val;\n            }\n\n            if (typeof val !== 'string') {\n              return null;\n            }\n\n            // Test if the string passed is valid\n            var results = parseRegExp.exec(val);\n            var floatValue;\n            var unit = 'b';\n\n            if (!results) {\n              // Nothing could be extracted from the given string\n              floatValue = parseInt(val, 10);\n              unit = 'b';\n            } else {\n              // Retrieve the value and the unit\n              floatValue = parseFloat(results[1]);\n              unit = results[4].toLowerCase();\n            }\n\n            if (isNaN(floatValue)) {\n              return null;\n            }\n\n            return Math.floor(map[unit] * floatValue);\n          }\n          return bytes.exports;\n        }\n\n        var bytesExports = requireBytes();\n\n        const Tooltip_marginX = 10;\n        const Tooltip_marginY = 30;\n        const SOURCEMAP_RENDERED = u$1('span', {\n          children: [\n            ' ',\n            u$1('b', { children: LABELS.renderedLength }),\n            ' is a number of characters in the file after individual and ',\n            u$1('br', {}),\n            ' ',\n            'whole bundle transformations according to sourcemap.',\n          ],\n        });\n        const RENDRED = u$1('span', {\n          children: [\n            u$1('b', { children: LABELS.renderedLength }),\n            ' is a byte size of individual file after transformations and treeshake.',\n          ],\n        });\n        const COMPRESSED = u$1('span', {\n          children: [\n            u$1('b', { children: LABELS.gzipLength }),\n            ' and ',\n            u$1('b', { children: LABELS.brotliLength }),\n            ' is a byte size of individual file after individual transformations,',\n            u$1('br', {}),\n            ' treeshake and compression.',\n          ],\n        });\n        const Tooltip = ({ node, visible, root, sizeProperty }) => {\n          const { availableSizeProperties, getModuleSize, data } =\n            x(StaticContext);\n          const ref = A(null);\n          const [style, setStyle] = d({});\n          const content = T(() => {\n            if (!node) return null;\n            const mainSize = getModuleSize(node.data, sizeProperty);\n            const percentageNum =\n              (100 * mainSize) / getModuleSize(root.data, sizeProperty);\n            const percentage = percentageNum.toFixed(2);\n            const percentageString = percentage + '%';\n            const path = node\n              .ancestors()\n              .reverse()\n              .map(d => d.data.name)\n              .join('/');\n            let dataNode = null;\n            if (!isModuleTree(node.data)) {\n              const mainUid = data.nodeParts[node.data.uid].metaUid;\n              dataNode = data.nodeMetas[mainUid];\n            }\n            return u$1(k$1, {\n              children: [\n                u$1('div', { children: path }),\n                availableSizeProperties.map(sizeProp => {\n                  if (sizeProp === sizeProperty) {\n                    return u$1(\n                      'div',\n                      {\n                        children: [\n                          u$1('b', {\n                            children: [\n                              LABELS[sizeProp],\n                              ': ',\n                              bytesExports.format(mainSize),\n                            ],\n                          }),\n                          ' ',\n                          '(',\n                          percentageString,\n                          ')',\n                        ],\n                      },\n                      sizeProp\n                    );\n                  } else {\n                    return u$1(\n                      'div',\n                      {\n                        children: [\n                          LABELS[sizeProp],\n                          ': ',\n                          bytesExports.format(\n                            getModuleSize(node.data, sizeProp)\n                          ),\n                        ],\n                      },\n                      sizeProp\n                    );\n                  }\n                }),\n                u$1('br', {}),\n                dataNode &&\n                  dataNode.importedBy.length > 0 &&\n                  u$1('div', {\n                    children: [\n                      u$1('div', {\n                        children: [u$1('b', { children: 'Imported By' }), ':'],\n                      }),\n                      dataNode.importedBy.map(({ uid }) => {\n                        const id = data.nodeMetas[uid].id;\n                        return u$1('div', { children: id }, id);\n                      }),\n                    ],\n                  }),\n                u$1('br', {}),\n                u$1('small', {\n                  children: data.options.sourcemap\n                    ? SOURCEMAP_RENDERED\n                    : RENDRED,\n                }),\n                (data.options.gzip || data.options.brotli) &&\n                  u$1(k$1, {\n                    children: [\n                      u$1('br', {}),\n                      u$1('small', { children: COMPRESSED }),\n                    ],\n                  }),\n              ],\n            });\n          }, [\n            availableSizeProperties,\n            data,\n            getModuleSize,\n            node,\n            root.data,\n            sizeProperty,\n          ]);\n          const updatePosition = mouseCoords => {\n            if (!ref.current) return;\n            const pos = {\n              left: mouseCoords.x + Tooltip_marginX,\n              top: mouseCoords.y + Tooltip_marginY,\n            };\n            const boundingRect = ref.current.getBoundingClientRect();\n            if (pos.left + boundingRect.width > window.innerWidth) {\n              // Shifting horizontally\n              pos.left = Math.max(0, window.innerWidth - boundingRect.width);\n            }\n            if (pos.top + boundingRect.height > window.innerHeight) {\n              // Flipping vertically\n              pos.top = Math.max(\n                0,\n                mouseCoords.y - Tooltip_marginY - boundingRect.height\n              );\n            }\n            setStyle(pos);\n          };\n          y(() => {\n            const handleMouseMove = event => {\n              updatePosition({\n                x: event.pageX,\n                y: event.pageY,\n              });\n            };\n            document.addEventListener('mousemove', handleMouseMove, true);\n            return () => {\n              document.removeEventListener('mousemove', handleMouseMove, true);\n            };\n          }, []);\n          return u$1('div', {\n            className: `tooltip ${visible ? '' : 'tooltip-hidden'}`,\n            ref: ref,\n            style: style,\n            children: content,\n          });\n        };\n\n        const Chart = ({\n          root,\n          sizeProperty,\n          selectedNode,\n          setSelectedNode,\n        }) => {\n          const [showTooltip, setShowTooltip] = d(false);\n          const [tooltipNode, setTooltipNode] = d(undefined);\n          y(() => {\n            const handleMouseOut = () => {\n              setShowTooltip(false);\n            };\n            document.addEventListener('mouseover', handleMouseOut);\n            return () => {\n              document.removeEventListener('mouseover', handleMouseOut);\n            };\n          }, []);\n          return u$1(k$1, {\n            children: [\n              u$1(TreeMap, {\n                root: root,\n                onNodeHover: node => {\n                  setTooltipNode(node);\n                  setShowTooltip(true);\n                },\n                selectedNode: selectedNode,\n                onNodeClick: node => {\n                  setSelectedNode(selectedNode === node ? undefined : node);\n                },\n              }),\n              u$1(Tooltip, {\n                visible: showTooltip,\n                node: tooltipNode,\n                root: root,\n                sizeProperty: sizeProperty,\n              }),\n            ],\n          });\n        };\n\n        const Main = () => {\n          const {\n            availableSizeProperties,\n            rawHierarchy,\n            getModuleSize,\n            layout,\n            data,\n          } = x(StaticContext);\n          const [sizeProperty, setSizeProperty] = d(availableSizeProperties[0]);\n          const [selectedNode, setSelectedNode] = d(undefined);\n          const {\n            getModuleFilterMultiplier,\n            setExcludeFilter,\n            setIncludeFilter,\n          } = useFilter();\n          console.time('getNodeSizeMultiplier');\n          const getNodeSizeMultiplier = T(() => {\n            const selectedMultiplier = 1; // selectedSize < rootSize * increaseFactor ? (rootSize * increaseFactor) / selectedSize : rootSize / selectedSize;\n            const nonSelectedMultiplier = 0; // 1 / selectedMultiplier\n            if (selectedNode === undefined) {\n              return () => 1;\n            } else if (isModuleTree(selectedNode.data)) {\n              const leaves = new Set(selectedNode.leaves().map(d => d.data));\n              return node => {\n                if (leaves.has(node)) {\n                  return selectedMultiplier;\n                }\n                return nonSelectedMultiplier;\n              };\n            } else {\n              return node => {\n                if (node === selectedNode.data) {\n                  return selectedMultiplier;\n                }\n                return nonSelectedMultiplier;\n              };\n            }\n          }, [getModuleSize, rawHierarchy.data, selectedNode, sizeProperty]);\n          console.timeEnd('getNodeSizeMultiplier');\n          console.time('root hierarchy compute');\n          // root here always be the same as rawHierarchy even after layouting\n          const root = T(() => {\n            const rootWithSizesAndSorted = rawHierarchy\n              .sum(node => {\n                var _a;\n                if (isModuleTree(node)) return 0;\n                const meta = data.nodeMetas[data.nodeParts[node.uid].metaUid];\n                /* eslint-disable typescript/no-non-null-asserted-optional-chain typescript/no-extra-non-null-assertion */\n                const bundleId =\n                  (_a = Object.entries(meta.moduleParts).find(\n                    ([, uid]) => uid == node.uid\n                  )) === null || _a === void 0\n                    ? void 0\n                    : _a[0];\n                const ownSize = getModuleSize(node, sizeProperty);\n                const zoomMultiplier = getNodeSizeMultiplier(node);\n                const filterMultiplier = getModuleFilterMultiplier(\n                  bundleId,\n                  meta\n                );\n                return ownSize * zoomMultiplier * filterMultiplier;\n              })\n              .sort(\n                (a, b) =>\n                  getModuleSize(a.data, sizeProperty) -\n                  getModuleSize(b.data, sizeProperty)\n              );\n            return layout(rootWithSizesAndSorted);\n          }, [\n            data,\n            getModuleFilterMultiplier,\n            getModuleSize,\n            getNodeSizeMultiplier,\n            layout,\n            rawHierarchy,\n            sizeProperty,\n          ]);\n          console.timeEnd('root hierarchy compute');\n          return u$1(k$1, {\n            children: [\n              u$1(SideBar, {\n                sizeProperty: sizeProperty,\n                availableSizeProperties: availableSizeProperties,\n                setSizeProperty: setSizeProperty,\n                onExcludeChange: setExcludeFilter,\n                onIncludeChange: setIncludeFilter,\n              }),\n              u$1(Chart, {\n                root: root,\n                sizeProperty: sizeProperty,\n                selectedNode: selectedNode,\n                setSelectedNode: setSelectedNode,\n              }),\n            ],\n          });\n        };\n\n        function initRange(domain, range) {\n          switch (arguments.length) {\n            case 0:\n              break;\n            case 1:\n              this.range(domain);\n              break;\n            default:\n              this.range(range).domain(domain);\n              break;\n          }\n          return this;\n        }\n\n        function initInterpolator(domain, interpolator) {\n          switch (arguments.length) {\n            case 0:\n              break;\n            case 1: {\n              if (typeof domain === 'function') this.interpolator(domain);\n              else this.range(domain);\n              break;\n            }\n            default: {\n              this.domain(domain);\n              if (typeof interpolator === 'function')\n                this.interpolator(interpolator);\n              else this.range(interpolator);\n              break;\n            }\n          }\n          return this;\n        }\n\n        function define(constructor, factory, prototype) {\n          constructor.prototype = factory.prototype = prototype;\n          prototype.constructor = constructor;\n        }\n\n        function extend(parent, definition) {\n          var prototype = Object.create(parent.prototype);\n          for (var key in definition) prototype[key] = definition[key];\n          return prototype;\n        }\n\n        function Color() {}\n\n        var darker = 0.7;\n        var brighter = 1 / darker;\n\n        var reI = '\\\\s*([+-]?\\\\d+)\\\\s*',\n          reN = '\\\\s*([+-]?(?:\\\\d*\\\\.)?\\\\d+(?:[eE][+-]?\\\\d+)?)\\\\s*',\n          reP = '\\\\s*([+-]?(?:\\\\d*\\\\.)?\\\\d+(?:[eE][+-]?\\\\d+)?)%\\\\s*',\n          reHex = /^#([0-9a-f]{3,8})$/,\n          reRgbInteger = new RegExp(`^rgb\\\\(${reI},${reI},${reI}\\\\)$`),\n          reRgbPercent = new RegExp(`^rgb\\\\(${reP},${reP},${reP}\\\\)$`),\n          reRgbaInteger = new RegExp(`^rgba\\\\(${reI},${reI},${reI},${reN}\\\\)$`),\n          reRgbaPercent = new RegExp(`^rgba\\\\(${reP},${reP},${reP},${reN}\\\\)$`),\n          reHslPercent = new RegExp(`^hsl\\\\(${reN},${reP},${reP}\\\\)$`),\n          reHslaPercent = new RegExp(`^hsla\\\\(${reN},${reP},${reP},${reN}\\\\)$`);\n\n        var named = {\n          aliceblue: 0xf0f8ff,\n          antiquewhite: 0xfaebd7,\n          aqua: 0x00ffff,\n          aquamarine: 0x7fffd4,\n          azure: 0xf0ffff,\n          beige: 0xf5f5dc,\n          bisque: 0xffe4c4,\n          black: 0x000000,\n          blanchedalmond: 0xffebcd,\n          blue: 0x0000ff,\n          blueviolet: 0x8a2be2,\n          brown: 0xa52a2a,\n          burlywood: 0xdeb887,\n          cadetblue: 0x5f9ea0,\n          chartreuse: 0x7fff00,\n          chocolate: 0xd2691e,\n          coral: 0xff7f50,\n          cornflowerblue: 0x6495ed,\n          cornsilk: 0xfff8dc,\n          crimson: 0xdc143c,\n          cyan: 0x00ffff,\n          darkblue: 0x00008b,\n          darkcyan: 0x008b8b,\n          darkgoldenrod: 0xb8860b,\n          darkgray: 0xa9a9a9,\n          darkgreen: 0x006400,\n          darkgrey: 0xa9a9a9,\n          darkkhaki: 0xbdb76b,\n          darkmagenta: 0x8b008b,\n          darkolivegreen: 0x556b2f,\n          darkorange: 0xff8c00,\n          darkorchid: 0x9932cc,\n          darkred: 0x8b0000,\n          darksalmon: 0xe9967a,\n          darkseagreen: 0x8fbc8f,\n          darkslateblue: 0x483d8b,\n          darkslategray: 0x2f4f4f,\n          darkslategrey: 0x2f4f4f,\n          darkturquoise: 0x00ced1,\n          darkviolet: 0x9400d3,\n          deeppink: 0xff1493,\n          deepskyblue: 0x00bfff,\n          dimgray: 0x696969,\n          dimgrey: 0x696969,\n          dodgerblue: 0x1e90ff,\n          firebrick: 0xb22222,\n          floralwhite: 0xfffaf0,\n          forestgreen: 0x228b22,\n          fuchsia: 0xff00ff,\n          gainsboro: 0xdcdcdc,\n          ghostwhite: 0xf8f8ff,\n          gold: 0xffd700,\n          goldenrod: 0xdaa520,\n          gray: 0x808080,\n          green: 0x008000,\n          greenyellow: 0xadff2f,\n          grey: 0x808080,\n          honeydew: 0xf0fff0,\n          hotpink: 0xff69b4,\n          indianred: 0xcd5c5c,\n          indigo: 0x4b0082,\n          ivory: 0xfffff0,\n          khaki: 0xf0e68c,\n          lavender: 0xe6e6fa,\n          lavenderblush: 0xfff0f5,\n          lawngreen: 0x7cfc00,\n          lemonchiffon: 0xfffacd,\n          lightblue: 0xadd8e6,\n          lightcoral: 0xf08080,\n          lightcyan: 0xe0ffff,\n          lightgoldenrodyellow: 0xfafad2,\n          lightgray: 0xd3d3d3,\n          lightgreen: 0x90ee90,\n          lightgrey: 0xd3d3d3,\n          lightpink: 0xffb6c1,\n          lightsalmon: 0xffa07a,\n          lightseagreen: 0x20b2aa,\n          lightskyblue: 0x87cefa,\n          lightslategray: 0x778899,\n          lightslategrey: 0x778899,\n          lightsteelblue: 0xb0c4de,\n          lightyellow: 0xffffe0,\n          lime: 0x00ff00,\n          limegreen: 0x32cd32,\n          linen: 0xfaf0e6,\n          magenta: 0xff00ff,\n          maroon: 0x800000,\n          mediumaquamarine: 0x66cdaa,\n          mediumblue: 0x0000cd,\n          mediumorchid: 0xba55d3,\n          mediumpurple: 0x9370db,\n          mediumseagreen: 0x3cb371,\n          mediumslateblue: 0x7b68ee,\n          mediumspringgreen: 0x00fa9a,\n          mediumturquoise: 0x48d1cc,\n          mediumvioletred: 0xc71585,\n          midnightblue: 0x191970,\n          mintcream: 0xf5fffa,\n          mistyrose: 0xffe4e1,\n          moccasin: 0xffe4b5,\n          navajowhite: 0xffdead,\n          navy: 0x000080,\n          oldlace: 0xfdf5e6,\n          olive: 0x808000,\n          olivedrab: 0x6b8e23,\n          orange: 0xffa500,\n          orangered: 0xff4500,\n          orchid: 0xda70d6,\n          palegoldenrod: 0xeee8aa,\n          palegreen: 0x98fb98,\n          paleturquoise: 0xafeeee,\n          palevioletred: 0xdb7093,\n          papayawhip: 0xffefd5,\n          peachpuff: 0xffdab9,\n          peru: 0xcd853f,\n          pink: 0xffc0cb,\n          plum: 0xdda0dd,\n          powderblue: 0xb0e0e6,\n          purple: 0x800080,\n          rebeccapurple: 0x663399,\n          red: 0xff0000,\n          rosybrown: 0xbc8f8f,\n          royalblue: 0x4169e1,\n          saddlebrown: 0x8b4513,\n          salmon: 0xfa8072,\n          sandybrown: 0xf4a460,\n          seagreen: 0x2e8b57,\n          seashell: 0xfff5ee,\n          sienna: 0xa0522d,\n          silver: 0xc0c0c0,\n          skyblue: 0x87ceeb,\n          slateblue: 0x6a5acd,\n          slategray: 0x708090,\n          slategrey: 0x708090,\n          snow: 0xfffafa,\n          springgreen: 0x00ff7f,\n          steelblue: 0x4682b4,\n          tan: 0xd2b48c,\n          teal: 0x008080,\n          thistle: 0xd8bfd8,\n          tomato: 0xff6347,\n          turquoise: 0x40e0d0,\n          violet: 0xee82ee,\n          wheat: 0xf5deb3,\n          white: 0xffffff,\n          whitesmoke: 0xf5f5f5,\n          yellow: 0xffff00,\n          yellowgreen: 0x9acd32,\n        };\n\n        define(Color, color, {\n          copy(channels) {\n            return Object.assign(new this.constructor(), this, channels);\n          },\n          displayable() {\n            return this.rgb().displayable();\n          },\n          hex: color_formatHex, // Deprecated! Use color.formatHex.\n          formatHex: color_formatHex,\n          formatHex8: color_formatHex8,\n          formatHsl: color_formatHsl,\n          formatRgb: color_formatRgb,\n          toString: color_formatRgb,\n        });\n\n        function color_formatHex() {\n          return this.rgb().formatHex();\n        }\n\n        function color_formatHex8() {\n          return this.rgb().formatHex8();\n        }\n\n        function color_formatHsl() {\n          return hslConvert(this).formatHsl();\n        }\n\n        function color_formatRgb() {\n          return this.rgb().formatRgb();\n        }\n\n        function color(format) {\n          var m, l;\n          format = (format + '').trim().toLowerCase();\n          return (m = reHex.exec(format))\n            ? ((l = m[1].length),\n              (m = parseInt(m[1], 16)),\n              l === 6\n                ? rgbn(m) // #ff0000\n                : l === 3\n                  ? new Rgb(\n                      ((m >> 8) & 0xf) | ((m >> 4) & 0xf0),\n                      ((m >> 4) & 0xf) | (m & 0xf0),\n                      ((m & 0xf) << 4) | (m & 0xf),\n                      1\n                    ) // #f00\n                  : l === 8\n                    ? rgba(\n                        (m >> 24) & 0xff,\n                        (m >> 16) & 0xff,\n                        (m >> 8) & 0xff,\n                        (m & 0xff) / 0xff\n                      ) // #ff000000\n                    : l === 4\n                      ? rgba(\n                          ((m >> 12) & 0xf) | ((m >> 8) & 0xf0),\n                          ((m >> 8) & 0xf) | ((m >> 4) & 0xf0),\n                          ((m >> 4) & 0xf) | (m & 0xf0),\n                          (((m & 0xf) << 4) | (m & 0xf)) / 0xff\n                        ) // #f000\n                      : null) // invalid hex\n            : (m = reRgbInteger.exec(format))\n              ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0)\n              : (m = reRgbPercent.exec(format))\n                ? new Rgb(\n                    (m[1] * 255) / 100,\n                    (m[2] * 255) / 100,\n                    (m[3] * 255) / 100,\n                    1\n                  ) // rgb(100%, 0%, 0%)\n                : (m = reRgbaInteger.exec(format))\n                  ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1)\n                  : (m = reRgbaPercent.exec(format))\n                    ? rgba(\n                        (m[1] * 255) / 100,\n                        (m[2] * 255) / 100,\n                        (m[3] * 255) / 100,\n                        m[4]\n                      ) // rgb(100%, 0%, 0%, 1)\n                    : (m = reHslPercent.exec(format))\n                      ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%)\n                      : (m = reHslaPercent.exec(format))\n                        ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1)\n                        : named.hasOwnProperty(format)\n                          ? rgbn(named[format]) // eslint-disable-line no-prototype-builtins\n                          : format === 'transparent'\n                            ? new Rgb(NaN, NaN, NaN, 0)\n                            : null;\n        }\n\n        function rgbn(n) {\n          return new Rgb((n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff, 1);\n        }\n\n        function rgba(r, g, b, a) {\n          if (a <= 0) r = g = b = NaN;\n          return new Rgb(r, g, b, a);\n        }\n\n        function rgbConvert(o) {\n          if (!(o instanceof Color)) o = color(o);\n          if (!o) return new Rgb();\n          o = o.rgb();\n          return new Rgb(o.r, o.g, o.b, o.opacity);\n        }\n\n        function rgb$1(r, g, b, opacity) {\n          return arguments.length === 1\n            ? rgbConvert(r)\n            : new Rgb(r, g, b, opacity == null ? 1 : opacity);\n        }\n\n        function Rgb(r, g, b, opacity) {\n          this.r = +r;\n          this.g = +g;\n          this.b = +b;\n          this.opacity = +opacity;\n        }\n\n        define(\n          Rgb,\n          rgb$1,\n          extend(Color, {\n            brighter(k) {\n              k = k == null ? brighter : Math.pow(brighter, k);\n              return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);\n            },\n            darker(k) {\n              k = k == null ? darker : Math.pow(darker, k);\n              return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);\n            },\n            rgb() {\n              return this;\n            },\n            clamp() {\n              return new Rgb(\n                clampi(this.r),\n                clampi(this.g),\n                clampi(this.b),\n                clampa(this.opacity)\n              );\n            },\n            displayable() {\n              return (\n                -0.5 <= this.r &&\n                this.r < 255.5 &&\n                -0.5 <= this.g &&\n                this.g < 255.5 &&\n                -0.5 <= this.b &&\n                this.b < 255.5 &&\n                0 <= this.opacity &&\n                this.opacity <= 1\n              );\n            },\n            hex: rgb_formatHex, // Deprecated! Use color.formatHex.\n            formatHex: rgb_formatHex,\n            formatHex8: rgb_formatHex8,\n            formatRgb: rgb_formatRgb,\n            toString: rgb_formatRgb,\n          })\n        );\n\n        function rgb_formatHex() {\n          return `#${hex(this.r)}${hex(this.g)}${hex(this.b)}`;\n        }\n\n        function rgb_formatHex8() {\n          return `#${hex(this.r)}${hex(this.g)}${hex(this.b)}${hex((isNaN(this.opacity) ? 1 : this.opacity) * 255)}`;\n        }\n\n        function rgb_formatRgb() {\n          const a = clampa(this.opacity);\n          return `${a === 1 ? 'rgb(' : 'rgba('}${clampi(this.r)}, ${clampi(this.g)}, ${clampi(this.b)}${a === 1 ? ')' : `, ${a})`}`;\n        }\n\n        function clampa(opacity) {\n          return isNaN(opacity) ? 1 : Math.max(0, Math.min(1, opacity));\n        }\n\n        function clampi(value) {\n          return Math.max(0, Math.min(255, Math.round(value) || 0));\n        }\n\n        function hex(value) {\n          value = clampi(value);\n          return (value < 16 ? '0' : '') + value.toString(16);\n        }\n\n        function hsla(h, s, l, a) {\n          if (a <= 0) h = s = l = NaN;\n          else if (l <= 0 || l >= 1) h = s = NaN;\n          else if (s <= 0) h = NaN;\n          return new Hsl(h, s, l, a);\n        }\n\n        function hslConvert(o) {\n          if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity);\n          if (!(o instanceof Color)) o = color(o);\n          if (!o) return new Hsl();\n          if (o instanceof Hsl) return o;\n          o = o.rgb();\n          var r = o.r / 255,\n            g = o.g / 255,\n            b = o.b / 255,\n            min = Math.min(r, g, b),\n            max = Math.max(r, g, b),\n            h = NaN,\n            s = max - min,\n            l = (max + min) / 2;\n          if (s) {\n            if (r === max) h = (g - b) / s + (g < b) * 6;\n            else if (g === max) h = (b - r) / s + 2;\n            else h = (r - g) / s + 4;\n            s /= l < 0.5 ? max + min : 2 - max - min;\n            h *= 60;\n          } else {\n            s = l > 0 && l < 1 ? 0 : h;\n          }\n          return new Hsl(h, s, l, o.opacity);\n        }\n\n        function hsl(h, s, l, opacity) {\n          return arguments.length === 1\n            ? hslConvert(h)\n            : new Hsl(h, s, l, opacity == null ? 1 : opacity);\n        }\n\n        function Hsl(h, s, l, opacity) {\n          this.h = +h;\n          this.s = +s;\n          this.l = +l;\n          this.opacity = +opacity;\n        }\n\n        define(\n          Hsl,\n          hsl,\n          extend(Color, {\n            brighter(k) {\n              k = k == null ? brighter : Math.pow(brighter, k);\n              return new Hsl(this.h, this.s, this.l * k, this.opacity);\n            },\n            darker(k) {\n              k = k == null ? darker : Math.pow(darker, k);\n              return new Hsl(this.h, this.s, this.l * k, this.opacity);\n            },\n            rgb() {\n              var h = (this.h % 360) + (this.h < 0) * 360,\n                s = isNaN(h) || isNaN(this.s) ? 0 : this.s,\n                l = this.l,\n                m2 = l + (l < 0.5 ? l : 1 - l) * s,\n                m1 = 2 * l - m2;\n              return new Rgb(\n                hsl2rgb(h >= 240 ? h - 240 : h + 120, m1, m2),\n                hsl2rgb(h, m1, m2),\n                hsl2rgb(h < 120 ? h + 240 : h - 120, m1, m2),\n                this.opacity\n              );\n            },\n            clamp() {\n              return new Hsl(\n                clamph(this.h),\n                clampt(this.s),\n                clampt(this.l),\n                clampa(this.opacity)\n              );\n            },\n            displayable() {\n              return (\n                ((0 <= this.s && this.s <= 1) || isNaN(this.s)) &&\n                0 <= this.l &&\n                this.l <= 1 &&\n                0 <= this.opacity &&\n                this.opacity <= 1\n              );\n            },\n            formatHsl() {\n              const a = clampa(this.opacity);\n              return `${a === 1 ? 'hsl(' : 'hsla('}${clamph(this.h)}, ${clampt(this.s) * 100}%, ${clampt(this.l) * 100}%${a === 1 ? ')' : `, ${a})`}`;\n            },\n          })\n        );\n\n        function clamph(value) {\n          value = (value || 0) % 360;\n          return value < 0 ? value + 360 : value;\n        }\n\n        function clampt(value) {\n          return Math.max(0, Math.min(1, value || 0));\n        }\n\n        /* From FvD 13.37, CSS Color Module Level 3 */\n        function hsl2rgb(h, m1, m2) {\n          return (\n            (h < 60\n              ? m1 + ((m2 - m1) * h) / 60\n              : h < 180\n                ? m2\n                : h < 240\n                  ? m1 + ((m2 - m1) * (240 - h)) / 60\n                  : m1) * 255\n          );\n        }\n\n        var constant = x => () => x;\n\n        function linear$1(a, d) {\n          return function (t) {\n            return a + t * d;\n          };\n        }\n\n        function exponential(a, b, y) {\n          return (\n            (a = Math.pow(a, y)),\n            (b = Math.pow(b, y) - a),\n            (y = 1 / y),\n            function (t) {\n              return Math.pow(a + t * b, y);\n            }\n          );\n        }\n\n        function gamma(y) {\n          return (y = +y) === 1\n            ? nogamma\n            : function (a, b) {\n                return b - a\n                  ? exponential(a, b, y)\n                  : constant(isNaN(a) ? b : a);\n              };\n        }\n\n        function nogamma(a, b) {\n          var d = b - a;\n          return d ? linear$1(a, d) : constant(isNaN(a) ? b : a);\n        }\n\n        var rgb = (function rgbGamma(y) {\n          var color = gamma(y);\n\n          function rgb(start, end) {\n            var r = color((start = rgb$1(start)).r, (end = rgb$1(end)).r),\n              g = color(start.g, end.g),\n              b = color(start.b, end.b),\n              opacity = nogamma(start.opacity, end.opacity);\n            return function (t) {\n              start.r = r(t);\n              start.g = g(t);\n              start.b = b(t);\n              start.opacity = opacity(t);\n              return start + '';\n            };\n          }\n\n          rgb.gamma = rgbGamma;\n\n          return rgb;\n        })(1);\n\n        function numberArray(a, b) {\n          if (!b) b = [];\n          var n = a ? Math.min(b.length, a.length) : 0,\n            c = b.slice(),\n            i;\n          return function (t) {\n            for (i = 0; i < n; ++i) c[i] = a[i] * (1 - t) + b[i] * t;\n            return c;\n          };\n        }\n\n        function isNumberArray(x) {\n          return ArrayBuffer.isView(x) && !(x instanceof DataView);\n        }\n\n        function genericArray(a, b) {\n          var nb = b ? b.length : 0,\n            na = a ? Math.min(nb, a.length) : 0,\n            x = new Array(na),\n            c = new Array(nb),\n            i;\n\n          for (i = 0; i < na; ++i) x[i] = interpolate(a[i], b[i]);\n          for (; i < nb; ++i) c[i] = b[i];\n\n          return function (t) {\n            for (i = 0; i < na; ++i) c[i] = x[i](t);\n            return c;\n          };\n        }\n\n        function date(a, b) {\n          var d = new Date();\n          return (\n            (a = +a),\n            (b = +b),\n            function (t) {\n              return (d.setTime(a * (1 - t) + b * t), d);\n            }\n          );\n        }\n\n        function interpolateNumber(a, b) {\n          return (\n            (a = +a),\n            (b = +b),\n            function (t) {\n              return a * (1 - t) + b * t;\n            }\n          );\n        }\n\n        function object(a, b) {\n          var i = {},\n            c = {},\n            k;\n\n          if (a === null || typeof a !== 'object') a = {};\n          if (b === null || typeof b !== 'object') b = {};\n\n          for (k in b) {\n            if (k in a) {\n              i[k] = interpolate(a[k], b[k]);\n            } else {\n              c[k] = b[k];\n            }\n          }\n\n          return function (t) {\n            for (k in i) c[k] = i[k](t);\n            return c;\n          };\n        }\n\n        var reA = /[-+]?(?:\\d+\\.?\\d*|\\.?\\d+)(?:[eE][-+]?\\d+)?/g,\n          reB = new RegExp(reA.source, 'g');\n\n        function zero(b) {\n          return function () {\n            return b;\n          };\n        }\n\n        function one(b) {\n          return function (t) {\n            return b(t) + '';\n          };\n        }\n\n        function string(a, b) {\n          var bi = (reA.lastIndex = reB.lastIndex = 0), // scan index for next number in b\n            am, // current match in a\n            bm, // current match in b\n            bs, // string preceding current number in b, if any\n            i = -1, // index in s\n            s = [], // string constants and placeholders\n            q = []; // number interpolators\n\n          // Coerce inputs to strings.\n          ((a = a + ''), (b = b + ''));\n\n          // Interpolate pairs of numbers in a & b.\n          while ((am = reA.exec(a)) && (bm = reB.exec(b))) {\n            if ((bs = bm.index) > bi) {\n              // a string precedes the next number in b\n              bs = b.slice(bi, bs);\n              if (s[i])\n                s[i] += bs; // coalesce with previous string\n              else s[++i] = bs;\n            }\n            if ((am = am[0]) === (bm = bm[0])) {\n              // numbers in a & b match\n              if (s[i])\n                s[i] += bm; // coalesce with previous string\n              else s[++i] = bm;\n            } else {\n              // interpolate non-matching numbers\n              s[++i] = null;\n              q.push({ i: i, x: interpolateNumber(am, bm) });\n            }\n            bi = reB.lastIndex;\n          }\n\n          // Add remains of b.\n          if (bi < b.length) {\n            bs = b.slice(bi);\n            if (s[i])\n              s[i] += bs; // coalesce with previous string\n            else s[++i] = bs;\n          }\n\n          // Special optimization for only a single match.\n          // Otherwise, interpolate each of the numbers and rejoin the string.\n          return s.length < 2\n            ? q[0]\n              ? one(q[0].x)\n              : zero(b)\n            : ((b = q.length),\n              function (t) {\n                for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t);\n                return s.join('');\n              });\n        }\n\n        function interpolate(a, b) {\n          var t = typeof b,\n            c;\n          return b == null || t === 'boolean'\n            ? constant(b)\n            : (t === 'number'\n                ? interpolateNumber\n                : t === 'string'\n                  ? (c = color(b))\n                    ? ((b = c), rgb)\n                    : string\n                  : b instanceof color\n                    ? rgb\n                    : b instanceof Date\n                      ? date\n                      : isNumberArray(b)\n                        ? numberArray\n                        : Array.isArray(b)\n                          ? genericArray\n                          : (typeof b.valueOf !== 'function' &&\n                                typeof b.toString !== 'function') ||\n                              isNaN(b)\n                            ? object\n                            : interpolateNumber)(a, b);\n        }\n\n        function interpolateRound(a, b) {\n          return (\n            (a = +a),\n            (b = +b),\n            function (t) {\n              return Math.round(a * (1 - t) + b * t);\n            }\n          );\n        }\n\n        function constants(x) {\n          return function () {\n            return x;\n          };\n        }\n\n        function number(x) {\n          return +x;\n        }\n\n        var unit = [0, 1];\n\n        function identity$1(x) {\n          return x;\n        }\n\n        function normalize(a, b) {\n          return (b -= a = +a)\n            ? function (x) {\n                return (x - a) / b;\n              }\n            : constants(isNaN(b) ? NaN : 0.5);\n        }\n\n        function clamper(a, b) {\n          var t;\n          if (a > b) ((t = a), (a = b), (b = t));\n          return function (x) {\n            return Math.max(a, Math.min(b, x));\n          };\n        }\n\n        // normalize(a, b)(x) takes a domain value x in [a,b] and returns the corresponding parameter t in [0,1].\n        // interpolate(a, b)(t) takes a parameter t in [0,1] and returns the corresponding range value x in [a,b].\n        function bimap(domain, range, interpolate) {\n          var d0 = domain[0],\n            d1 = domain[1],\n            r0 = range[0],\n            r1 = range[1];\n          if (d1 < d0) ((d0 = normalize(d1, d0)), (r0 = interpolate(r1, r0)));\n          else ((d0 = normalize(d0, d1)), (r0 = interpolate(r0, r1)));\n          return function (x) {\n            return r0(d0(x));\n          };\n        }\n\n        function polymap(domain, range, interpolate) {\n          var j = Math.min(domain.length, range.length) - 1,\n            d = new Array(j),\n            r = new Array(j),\n            i = -1;\n\n          // Reverse descending domains.\n          if (domain[j] < domain[0]) {\n            domain = domain.slice().reverse();\n            range = range.slice().reverse();\n          }\n\n          while (++i < j) {\n            d[i] = normalize(domain[i], domain[i + 1]);\n            r[i] = interpolate(range[i], range[i + 1]);\n          }\n\n          return function (x) {\n            var i = bisectRight(domain, x, 1, j) - 1;\n            return r[i](d[i](x));\n          };\n        }\n\n        function copy$1(source, target) {\n          return target\n            .domain(source.domain())\n            .range(source.range())\n            .interpolate(source.interpolate())\n            .clamp(source.clamp())\n            .unknown(source.unknown());\n        }\n\n        function transformer$1() {\n          var domain = unit,\n            range = unit,\n            interpolate$1 = interpolate,\n            transform,\n            untransform,\n            unknown,\n            clamp = identity$1,\n            piecewise,\n            output,\n            input;\n\n          function rescale() {\n            var n = Math.min(domain.length, range.length);\n            if (clamp !== identity$1) clamp = clamper(domain[0], domain[n - 1]);\n            piecewise = n > 2 ? polymap : bimap;\n            output = input = null;\n            return scale;\n          }\n\n          function scale(x) {\n            return x == null || isNaN((x = +x))\n              ? unknown\n              : (\n                  output ||\n                  (output = piecewise(\n                    domain.map(transform),\n                    range,\n                    interpolate$1\n                  ))\n                )(transform(clamp(x)));\n          }\n\n          scale.invert = function (y) {\n            return clamp(\n              untransform(\n                (\n                  input ||\n                  (input = piecewise(\n                    range,\n                    domain.map(transform),\n                    interpolateNumber\n                  ))\n                )(y)\n              )\n            );\n          };\n\n          scale.domain = function (_) {\n            return arguments.length\n              ? ((domain = Array.from(_, number)), rescale())\n              : domain.slice();\n          };\n\n          scale.range = function (_) {\n            return arguments.length\n              ? ((range = Array.from(_)), rescale())\n              : range.slice();\n          };\n\n          scale.rangeRound = function (_) {\n            return (\n              (range = Array.from(_)),\n              (interpolate$1 = interpolateRound),\n              rescale()\n            );\n          };\n\n          scale.clamp = function (_) {\n            return arguments.length\n              ? ((clamp = _ ? true : identity$1), rescale())\n              : clamp !== identity$1;\n          };\n\n          scale.interpolate = function (_) {\n            return arguments.length\n              ? ((interpolate$1 = _), rescale())\n              : interpolate$1;\n          };\n\n          scale.unknown = function (_) {\n            return arguments.length ? ((unknown = _), scale) : unknown;\n          };\n\n          return function (t, u) {\n            ((transform = t), (untransform = u));\n            return rescale();\n          };\n        }\n\n        function continuous() {\n          return transformer$1()(identity$1, identity$1);\n        }\n\n        function formatDecimal(x) {\n          return Math.abs((x = Math.round(x))) >= 1e21\n            ? x.toLocaleString('en').replace(/,/g, '')\n            : x.toString(10);\n        }\n\n        // Computes the decimal coefficient and exponent of the specified number x with\n        // significant digits p, where x is positive and p is in [1, 21] or undefined.\n        // For example, formatDecimalParts(1.23) returns [\"123\", 0].\n        function formatDecimalParts(x, p) {\n          if (\n            (i = (x = p ? x.toExponential(p - 1) : x.toExponential()).indexOf(\n              'e'\n            )) < 0\n          )\n            return null; // NaN, ±Infinity\n          var i,\n            coefficient = x.slice(0, i);\n\n          // The string returned by toExponential either has the form \\d\\.\\d+e[-+]\\d+\n          // (e.g., 1.2e+3) or the form \\de[-+]\\d+ (e.g., 1e+3).\n          return [\n            coefficient.length > 1\n              ? coefficient[0] + coefficient.slice(2)\n              : coefficient,\n            +x.slice(i + 1),\n          ];\n        }\n\n        function exponent(x) {\n          return ((x = formatDecimalParts(Math.abs(x))), x ? x[1] : NaN);\n        }\n\n        function formatGroup(grouping, thousands) {\n          return function (value, width) {\n            var i = value.length,\n              t = [],\n              j = 0,\n              g = grouping[0],\n              length = 0;\n\n            while (i > 0 && g > 0) {\n              if (length + g + 1 > width) g = Math.max(1, width - length);\n              t.push(value.substring((i -= g), i + g));\n              if ((length += g + 1) > width) break;\n              g = grouping[(j = (j + 1) % grouping.length)];\n            }\n\n            return t.reverse().join(thousands);\n          };\n        }\n\n        function formatNumerals(numerals) {\n          return function (value) {\n            return value.replace(/[0-9]/g, function (i) {\n              return numerals[+i];\n            });\n          };\n        }\n\n        // [[fill]align][sign][symbol][0][width][,][.precision][~][type]\n        var re =\n          /^(?:(.)?([<>=^]))?([+\\-( ])?([$#])?(0)?(\\d+)?(,)?(\\.\\d+)?(~)?([a-z%])?$/i;\n\n        function formatSpecifier(specifier) {\n          if (!(match = re.exec(specifier)))\n            throw new Error('invalid format: ' + specifier);\n          var match;\n          return new FormatSpecifier({\n            fill: match[1],\n            align: match[2],\n            sign: match[3],\n            symbol: match[4],\n            zero: match[5],\n            width: match[6],\n            comma: match[7],\n            precision: match[8] && match[8].slice(1),\n            trim: match[9],\n            type: match[10],\n          });\n        }\n\n        formatSpecifier.prototype = FormatSpecifier.prototype; // instanceof\n\n        function FormatSpecifier(specifier) {\n          this.fill = specifier.fill === undefined ? ' ' : specifier.fill + '';\n          this.align =\n            specifier.align === undefined ? '>' : specifier.align + '';\n          this.sign = specifier.sign === undefined ? '-' : specifier.sign + '';\n          this.symbol =\n            specifier.symbol === undefined ? '' : specifier.symbol + '';\n          this.zero = !!specifier.zero;\n          this.width =\n            specifier.width === undefined ? undefined : +specifier.width;\n          this.comma = !!specifier.comma;\n          this.precision =\n            specifier.precision === undefined\n              ? undefined\n              : +specifier.precision;\n          this.trim = !!specifier.trim;\n          this.type = specifier.type === undefined ? '' : specifier.type + '';\n        }\n\n        FormatSpecifier.prototype.toString = function () {\n          return (\n            this.fill +\n            this.align +\n            this.sign +\n            this.symbol +\n            (this.zero ? '0' : '') +\n            (this.width === undefined ? '' : Math.max(1, this.width | 0)) +\n            (this.comma ? ',' : '') +\n            (this.precision === undefined\n              ? ''\n              : '.' + Math.max(0, this.precision | 0)) +\n            (this.trim ? '~' : '') +\n            this.type\n          );\n        };\n\n        // Trims insignificant zeros, e.g., replaces 1.2000k with 1.2k.\n        function formatTrim(s) {\n          out: for (var n = s.length, i = 1, i0 = -1, i1; i < n; ++i) {\n            switch (s[i]) {\n              case '.':\n                i0 = i1 = i;\n                break;\n              case '0':\n                if (i0 === 0) i0 = i;\n                i1 = i;\n                break;\n              default:\n                if (!+s[i]) break out;\n                if (i0 > 0) i0 = 0;\n                break;\n            }\n          }\n          return i0 > 0 ? s.slice(0, i0) + s.slice(i1 + 1) : s;\n        }\n\n        var prefixExponent;\n\n        function formatPrefixAuto(x, p) {\n          var d = formatDecimalParts(x, p);\n          if (!d) return x + '';\n          var coefficient = d[0],\n            exponent = d[1],\n            i =\n              exponent -\n              (prefixExponent =\n                Math.max(-8, Math.min(8, Math.floor(exponent / 3))) * 3) +\n              1,\n            n = coefficient.length;\n          return i === n\n            ? coefficient\n            : i > n\n              ? coefficient + new Array(i - n + 1).join('0')\n              : i > 0\n                ? coefficient.slice(0, i) + '.' + coefficient.slice(i)\n                : '0.' +\n                  new Array(1 - i).join('0') +\n                  formatDecimalParts(x, Math.max(0, p + i - 1))[0]; // less than 1y!\n        }\n\n        function formatRounded(x, p) {\n          var d = formatDecimalParts(x, p);\n          if (!d) return x + '';\n          var coefficient = d[0],\n            exponent = d[1];\n          return exponent < 0\n            ? '0.' + new Array(-exponent).join('0') + coefficient\n            : coefficient.length > exponent + 1\n              ? coefficient.slice(0, exponent + 1) +\n                '.' +\n                coefficient.slice(exponent + 1)\n              : coefficient +\n                new Array(exponent - coefficient.length + 2).join('0');\n        }\n\n        var formatTypes = {\n          '%': (x, p) => (x * 100).toFixed(p),\n          b: x => Math.round(x).toString(2),\n          c: x => x + '',\n          d: formatDecimal,\n          e: (x, p) => x.toExponential(p),\n          f: (x, p) => x.toFixed(p),\n          g: (x, p) => x.toPrecision(p),\n          o: x => Math.round(x).toString(8),\n          p: (x, p) => formatRounded(x * 100, p),\n          r: formatRounded,\n          s: formatPrefixAuto,\n          X: x => Math.round(x).toString(16).toUpperCase(),\n          x: x => Math.round(x).toString(16),\n        };\n\n        function identity(x) {\n          return x;\n        }\n\n        var map = Array.prototype.map,\n          prefixes = [\n            'y',\n            'z',\n            'a',\n            'f',\n            'p',\n            'n',\n            'µ',\n            'm',\n            '',\n            'k',\n            'M',\n            'G',\n            'T',\n            'P',\n            'E',\n            'Z',\n            'Y',\n          ];\n\n        function formatLocale(locale) {\n          var group =\n              locale.grouping === undefined || locale.thousands === undefined\n                ? identity\n                : formatGroup(\n                    map.call(locale.grouping, Number),\n                    locale.thousands + ''\n                  ),\n            currencyPrefix =\n              locale.currency === undefined ? '' : locale.currency[0] + '',\n            currencySuffix =\n              locale.currency === undefined ? '' : locale.currency[1] + '',\n            decimal = locale.decimal === undefined ? '.' : locale.decimal + '',\n            numerals =\n              locale.numerals === undefined\n                ? identity\n                : formatNumerals(map.call(locale.numerals, String)),\n            percent = locale.percent === undefined ? '%' : locale.percent + '',\n            minus = locale.minus === undefined ? '−' : locale.minus + '',\n            nan = locale.nan === undefined ? 'NaN' : locale.nan + '';\n\n          function newFormat(specifier) {\n            specifier = formatSpecifier(specifier);\n\n            var fill = specifier.fill,\n              align = specifier.align,\n              sign = specifier.sign,\n              symbol = specifier.symbol,\n              zero = specifier.zero,\n              width = specifier.width,\n              comma = specifier.comma,\n              precision = specifier.precision,\n              trim = specifier.trim,\n              type = specifier.type;\n\n            // The \"n\" type is an alias for \",g\".\n            if (type === 'n') ((comma = true), (type = 'g'));\n            // The \"\" type, and any invalid type, is an alias for \".12~g\".\n            else if (!formatTypes[type])\n              (precision === undefined && (precision = 12),\n                (trim = true),\n                (type = 'g'));\n\n            // If zero fill is specified, padding goes after sign and before digits.\n            if (zero || (fill === '0' && align === '='))\n              ((zero = true), (fill = '0'), (align = '='));\n\n            // Compute the prefix and suffix.\n            // For SI-prefix, the suffix is lazily computed.\n            var prefix =\n                symbol === '$'\n                  ? currencyPrefix\n                  : symbol === '#' && /[boxX]/.test(type)\n                    ? '0' + type.toLowerCase()\n                    : '',\n              suffix =\n                symbol === '$'\n                  ? currencySuffix\n                  : /[%p]/.test(type)\n                    ? percent\n                    : '';\n\n            // What format function should we use?\n            // Is this an integer type?\n            // Can this type generate exponential notation?\n            var formatType = formatTypes[type],\n              maybeSuffix = /[defgprs%]/.test(type);\n\n            // Set the default precision if not specified,\n            // or clamp the specified precision to the supported range.\n            // For significant precision, it must be in [1, 21].\n            // For fixed precision, it must be in [0, 20].\n            precision =\n              precision === undefined\n                ? 6\n                : /[gprs]/.test(type)\n                  ? Math.max(1, Math.min(21, precision))\n                  : Math.max(0, Math.min(20, precision));\n\n            function format(value) {\n              var valuePrefix = prefix,\n                valueSuffix = suffix,\n                i,\n                n,\n                c;\n\n              if (type === 'c') {\n                valueSuffix = formatType(value) + valueSuffix;\n                value = '';\n              } else {\n                value = +value;\n\n                // Determine the sign. -0 is not less than 0, but 1 / -0 is!\n                var valueNegative = value < 0 || 1 / value < 0;\n\n                // Perform the initial formatting.\n                value = isNaN(value)\n                  ? nan\n                  : formatType(Math.abs(value), precision);\n\n                // Trim insignificant zeros.\n                if (trim) value = formatTrim(value);\n\n                // If a negative value rounds to zero after formatting, and no explicit positive sign is requested, hide the sign.\n                if (valueNegative && +value === 0 && sign !== '+')\n                  valueNegative = false;\n\n                // Compute the prefix and suffix.\n                valuePrefix =\n                  (valueNegative\n                    ? sign === '('\n                      ? sign\n                      : minus\n                    : sign === '-' || sign === '('\n                      ? ''\n                      : sign) + valuePrefix;\n                valueSuffix =\n                  (type === 's' ? prefixes[8 + prefixExponent / 3] : '') +\n                  valueSuffix +\n                  (valueNegative && sign === '(' ? ')' : '');\n\n                // Break the formatted value into the integer “value” part that can be\n                // grouped, and fractional or exponential “suffix” part that is not.\n                if (maybeSuffix) {\n                  ((i = -1), (n = value.length));\n                  while (++i < n) {\n                    if (((c = value.charCodeAt(i)), 48 > c || c > 57)) {\n                      valueSuffix =\n                        (c === 46\n                          ? decimal + value.slice(i + 1)\n                          : value.slice(i)) + valueSuffix;\n                      value = value.slice(0, i);\n                      break;\n                    }\n                  }\n                }\n              }\n\n              // If the fill character is not \"0\", grouping is applied before padding.\n              if (comma && !zero) value = group(value, Infinity);\n\n              // Compute the padding.\n              var length =\n                  valuePrefix.length + value.length + valueSuffix.length,\n                padding =\n                  length < width\n                    ? new Array(width - length + 1).join(fill)\n                    : '';\n\n              // If the fill character is \"0\", grouping is applied after padding.\n              if (comma && zero)\n                ((value = group(\n                  padding + value,\n                  padding.length ? width - valueSuffix.length : Infinity\n                )),\n                  (padding = ''));\n\n              // Reconstruct the final output based on the desired alignment.\n              switch (align) {\n                case '<':\n                  value = valuePrefix + value + valueSuffix + padding;\n                  break;\n                case '=':\n                  value = valuePrefix + padding + value + valueSuffix;\n                  break;\n                case '^':\n                  value =\n                    padding.slice(0, (length = padding.length >> 1)) +\n                    valuePrefix +\n                    value +\n                    valueSuffix +\n                    padding.slice(length);\n                  break;\n                default:\n                  value = padding + valuePrefix + value + valueSuffix;\n                  break;\n              }\n\n              return numerals(value);\n            }\n\n            format.toString = function () {\n              return specifier + '';\n            };\n\n            return format;\n          }\n\n          function formatPrefix(specifier, value) {\n            var f = newFormat(\n                ((specifier = formatSpecifier(specifier)),\n                (specifier.type = 'f'),\n                specifier)\n              ),\n              e =\n                Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3,\n              k = Math.pow(10, -e),\n              prefix = prefixes[8 + e / 3];\n            return function (value) {\n              return f(k * value) + prefix;\n            };\n          }\n\n          return {\n            format: newFormat,\n            formatPrefix: formatPrefix,\n          };\n        }\n\n        var locale;\n        var format;\n        var formatPrefix;\n\n        defaultLocale({\n          thousands: ',',\n          grouping: [3],\n          currency: ['$', ''],\n        });\n\n        function defaultLocale(definition) {\n          locale = formatLocale(definition);\n          format = locale.format;\n          formatPrefix = locale.formatPrefix;\n          return locale;\n        }\n\n        function precisionFixed(step) {\n          return Math.max(0, -exponent(Math.abs(step)));\n        }\n\n        function precisionPrefix(step, value) {\n          return Math.max(\n            0,\n            Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3 -\n              exponent(Math.abs(step))\n          );\n        }\n\n        function precisionRound(step, max) {\n          ((step = Math.abs(step)), (max = Math.abs(max) - step));\n          return Math.max(0, exponent(max) - exponent(step)) + 1;\n        }\n\n        function tickFormat(start, stop, count, specifier) {\n          var step = tickStep(start, stop, count),\n            precision;\n          specifier = formatSpecifier(specifier == null ? ',f' : specifier);\n          switch (specifier.type) {\n            case 's': {\n              var value = Math.max(Math.abs(start), Math.abs(stop));\n              if (\n                specifier.precision == null &&\n                !isNaN((precision = precisionPrefix(step, value)))\n              )\n                specifier.precision = precision;\n              return formatPrefix(specifier, value);\n            }\n            case '':\n            case 'e':\n            case 'g':\n            case 'p':\n            case 'r': {\n              if (\n                specifier.precision == null &&\n                !isNaN(\n                  (precision = precisionRound(\n                    step,\n                    Math.max(Math.abs(start), Math.abs(stop))\n                  ))\n                )\n              )\n                specifier.precision = precision - (specifier.type === 'e');\n              break;\n            }\n            case 'f':\n            case '%': {\n              if (\n                specifier.precision == null &&\n                !isNaN((precision = precisionFixed(step)))\n              )\n                specifier.precision = precision - (specifier.type === '%') * 2;\n              break;\n            }\n          }\n          return format(specifier);\n        }\n\n        function linearish(scale) {\n          var domain = scale.domain;\n\n          scale.ticks = function (count) {\n            var d = domain();\n            return ticks(d[0], d[d.length - 1], count == null ? 10 : count);\n          };\n\n          scale.tickFormat = function (count, specifier) {\n            var d = domain();\n            return tickFormat(\n              d[0],\n              d[d.length - 1],\n              count == null ? 10 : count,\n              specifier\n            );\n          };\n\n          scale.nice = function (count) {\n            if (count == null) count = 10;\n\n            var d = domain();\n            var i0 = 0;\n            var i1 = d.length - 1;\n            var start = d[i0];\n            var stop = d[i1];\n            var prestep;\n            var step;\n            var maxIter = 10;\n\n            if (stop < start) {\n              ((step = start), (start = stop), (stop = step));\n              ((step = i0), (i0 = i1), (i1 = step));\n            }\n\n            while (maxIter-- > 0) {\n              step = tickIncrement(start, stop, count);\n              if (step === prestep) {\n                d[i0] = start;\n                d[i1] = stop;\n                return domain(d);\n              } else if (step > 0) {\n                start = Math.floor(start / step) * step;\n                stop = Math.ceil(stop / step) * step;\n              } else if (step < 0) {\n                start = Math.ceil(start * step) / step;\n                stop = Math.floor(stop * step) / step;\n              } else {\n                break;\n              }\n              prestep = step;\n            }\n\n            return scale;\n          };\n\n          return scale;\n        }\n\n        function linear() {\n          var scale = continuous();\n\n          scale.copy = function () {\n            return copy$1(scale, linear());\n          };\n\n          initRange.apply(scale, arguments);\n\n          return linearish(scale);\n        }\n\n        function transformer() {\n          var x0 = 0,\n            x1 = 1,\n            t0,\n            t1,\n            k10,\n            transform,\n            interpolator = identity$1,\n            clamp = false,\n            unknown;\n\n          function scale(x) {\n            return x == null || isNaN((x = +x))\n              ? unknown\n              : interpolator(\n                  k10 === 0\n                    ? 0.5\n                    : ((x = (transform(x) - t0) * k10),\n                      clamp ? Math.max(0, Math.min(1, x)) : x)\n                );\n          }\n\n          scale.domain = function (_) {\n            return arguments.length\n              ? (([x0, x1] = _),\n                (t0 = transform((x0 = +x0))),\n                (t1 = transform((x1 = +x1))),\n                (k10 = t0 === t1 ? 0 : 1 / (t1 - t0)),\n                scale)\n              : [x0, x1];\n          };\n\n          scale.clamp = function (_) {\n            return arguments.length ? ((clamp = !!_), scale) : clamp;\n          };\n\n          scale.interpolator = function (_) {\n            return arguments.length\n              ? ((interpolator = _), scale)\n              : interpolator;\n          };\n\n          function range(interpolate) {\n            return function (_) {\n              var r0, r1;\n              return arguments.length\n                ? (([r0, r1] = _), (interpolator = interpolate(r0, r1)), scale)\n                : [interpolator(0), interpolator(1)];\n            };\n          }\n\n          scale.range = range(interpolate);\n\n          scale.rangeRound = range(interpolateRound);\n\n          scale.unknown = function (_) {\n            return arguments.length ? ((unknown = _), scale) : unknown;\n          };\n\n          return function (t) {\n            ((transform = t),\n              (t0 = t(x0)),\n              (t1 = t(x1)),\n              (k10 = t0 === t1 ? 0 : 1 / (t1 - t0)));\n            return scale;\n          };\n        }\n\n        function copy(source, target) {\n          return target\n            .domain(source.domain())\n            .interpolator(source.interpolator())\n            .clamp(source.clamp())\n            .unknown(source.unknown());\n        }\n\n        function sequential() {\n          var scale = linearish(transformer()(identity$1));\n\n          scale.copy = function () {\n            return copy(scale, sequential());\n          };\n\n          return initInterpolator.apply(scale, arguments);\n        }\n\n        const COLOR_BASE = '#cecece';\n\n        // https://www.w3.org/TR/WCAG20/#relativeluminancedef\n        const rc = 0.2126;\n        const gc = 0.7152;\n        const bc = 0.0722;\n        // low-gamma adjust coefficient\n        const lowc = 1 / 12.92;\n        function adjustGamma(p) {\n          return Math.pow((p + 0.055) / 1.055, 2.4);\n        }\n        function relativeLuminance(o) {\n          const rsrgb = o.r / 255;\n          const gsrgb = o.g / 255;\n          const bsrgb = o.b / 255;\n          const r = rsrgb <= 0.03928 ? rsrgb * lowc : adjustGamma(rsrgb);\n          const g = gsrgb <= 0.03928 ? gsrgb * lowc : adjustGamma(gsrgb);\n          const b = bsrgb <= 0.03928 ? bsrgb * lowc : adjustGamma(bsrgb);\n          return r * rc + g * gc + b * bc;\n        }\n        const createRainbowColor = root => {\n          const colorParentMap = new Map();\n          colorParentMap.set(root, COLOR_BASE);\n          if (root.children != null) {\n            const colorScale = sequential([0, root.children.length], n =>\n              hsl(360 * n, 0.3, 0.85)\n            );\n            root.children.forEach((c, id) => {\n              colorParentMap.set(c, colorScale(id).toString());\n            });\n          }\n          const colorMap = new Map();\n          const lightScale = linear()\n            .domain([0, root.height])\n            .range([0.9, 0.3]);\n          const getBackgroundColor = node => {\n            const parents = node.ancestors();\n            const colorStr =\n              parents.length === 1\n                ? colorParentMap.get(parents[0])\n                : colorParentMap.get(parents[parents.length - 2]);\n            const hslColor = hsl(colorStr);\n            hslColor.l = lightScale(node.depth);\n            return hslColor;\n          };\n          return node => {\n            if (!colorMap.has(node)) {\n              const backgroundColor = getBackgroundColor(node);\n              const l = relativeLuminance(backgroundColor.rgb());\n              const fontColor = l > 0.19 ? '#000' : '#fff';\n              colorMap.set(node, {\n                backgroundColor: backgroundColor.toString(),\n                fontColor,\n              });\n            }\n            return colorMap.get(node);\n          };\n        };\n\n        const StaticContext = K({});\n        const drawChart = (parentNode, data, width, height) => {\n          const availableSizeProperties = getAvailableSizeOptions(data.options);\n          console.time('layout create');\n          const layout = treemap()\n            .size([width, height])\n            .paddingOuter(PADDING)\n            .paddingTop(TOP_PADDING)\n            .paddingInner(PADDING)\n            .round(true)\n            .tile(treemapResquarify);\n          console.timeEnd('layout create');\n          console.time('rawHierarchy create');\n          const rawHierarchy = hierarchy(data.tree);\n          console.timeEnd('rawHierarchy create');\n          const nodeSizesCache = new Map();\n          const nodeIdsCache = new Map();\n          const getModuleSize = (node, sizeKey) => {\n            var _a, _b;\n            return (_b =\n              (_a = nodeSizesCache.get(node)) === null || _a === void 0\n                ? void 0\n                : _a[sizeKey]) !== null && _b !== void 0\n              ? _b\n              : 0;\n          };\n          console.time('rawHierarchy eachAfter cache');\n          rawHierarchy.eachAfter(node => {\n            var _a;\n            const nodeData = node.data;\n            nodeIdsCache.set(nodeData, {\n              nodeUid: generateUniqueId('node'),\n              clipUid: generateUniqueId('clip'),\n            });\n            const sizes = { renderedLength: 0, gzipLength: 0, brotliLength: 0 };\n            if (isModuleTree(nodeData)) {\n              for (const sizeKey of availableSizeProperties) {\n                sizes[sizeKey] = nodeData.children.reduce(\n                  (acc, child) => getModuleSize(child, sizeKey) + acc,\n                  0\n                );\n              }\n            } else {\n              for (const sizeKey of availableSizeProperties) {\n                sizes[sizeKey] =\n                  (_a = data.nodeParts[nodeData.uid][sizeKey]) !== null &&\n                  _a !== void 0\n                    ? _a\n                    : 0;\n              }\n            }\n            nodeSizesCache.set(nodeData, sizes);\n          });\n          console.timeEnd('rawHierarchy eachAfter cache');\n          const getModuleIds = node => nodeIdsCache.get(node);\n          console.time('color');\n          const getModuleColor = createRainbowColor(rawHierarchy);\n          console.timeEnd('color');\n          E(\n            u$1(StaticContext.Provider, {\n              value: {\n                data,\n                availableSizeProperties,\n                width,\n                height,\n                getModuleSize,\n                getModuleIds,\n                getModuleColor,\n                rawHierarchy,\n                layout,\n              },\n              children: u$1(Main, {}),\n            }),\n            parentNode\n          );\n        };\n\n        exports.StaticContext = StaticContext;\n        exports.default = drawChart;\n\n        Object.defineProperty(exports, '__esModule', { value: true });\n\n        return exports;\n      })({});\n\n      /*-->*/\n    </script>\n    <script>\n      /*<!--*/\n      const data = {\n        version: 2,\n        tree: {\n          name: 'root',\n          children: [\n            {\n              name: 'index.esm.js',\n              children: [\n                {\n                  name: 'src',\n                  children: [\n                    {\n                      name: 'locale',\n                      children: [\n                        {\n                          name: 'locales',\n                          children: [\n                            { uid: '25dea94e-1', name: 'en.ts' },\n                            { uid: '25dea94e-3', name: 'index.ts' },\n                          ],\n                        },\n                        { uid: '25dea94e-5', name: 'utils.ts' },\n                        { uid: '25dea94e-91', name: 'LocaleContext.tsx' },\n                        { uid: '25dea94e-93', name: 'useLocale.ts' },\n                        { uid: '25dea94e-99', name: 'intl.ts' },\n                        { uid: '25dea94e-101', name: 'translator.ts' },\n                        { uid: '25dea94e-103', name: 'LocaleProvider.tsx' },\n                      ],\n                    },\n                    {\n                      name: 'types',\n                      children: [\n                        { uid: '25dea94e-7', name: 'core.ts' },\n                        { uid: '25dea94e-9', name: 'layout.ts' },\n                        { uid: '25dea94e-11', name: 'monthView.ts' },\n                        { uid: '25dea94e-13', name: 'timezone.ts' },\n                      ],\n                    },\n                    {\n                      name: 'utils',\n                      children: [\n                        { uid: '25dea94e-15', name: 'temporal.ts' },\n                        { uid: '25dea94e-17', name: 'dateTimeUtils.ts' },\n                        { uid: '25dea94e-21', name: 'colorUtils.ts' },\n                        { uid: '25dea94e-23', name: 'logger.ts' },\n                        { uid: '25dea94e-25', name: 'timeZoneUtils.ts' },\n                        { uid: '25dea94e-27', name: 'temporalTypeGuards.ts' },\n                        { uid: '25dea94e-29', name: 'timeUtils.ts' },\n                        { uid: '25dea94e-31', name: 'dateConstants.ts' },\n                        { uid: '25dea94e-33', name: 'dateRangeUtils.ts' },\n                        { uid: '25dea94e-35', name: 'calendarDataUtils.ts' },\n                        { uid: '25dea94e-37', name: 'eventUtils.ts' },\n                        { uid: '25dea94e-39', name: 'testDataUtils.ts' },\n                        { uid: '25dea94e-41', name: 'utilityFunctions.ts' },\n                        { uid: '25dea94e-43', name: 'compareUtils.ts' },\n                        {\n                          name: 'calendarApp',\n                          children: [\n                            { uid: '25dea94e-45', name: 'configSync.ts' },\n                            { uid: '25dea94e-47', name: 'normalizedConfig.ts' },\n                            {\n                              uid: '25dea94e-49',\n                              name: 'viewConfigComparison.ts',\n                            },\n                          ],\n                        },\n                        { uid: '25dea94e-65', name: 'dateFormat.ts' },\n                        { uid: '25dea94e-67', name: 'styleUtils.ts' },\n                        { uid: '25dea94e-69', name: 'themeUtils.ts' },\n                        { uid: '25dea94e-71', name: 'eventHelpers.ts' },\n                        { uid: '25dea94e-73', name: 'searchUtils.ts' },\n                        { uid: '25dea94e-75', name: 'clipboardStore.ts' },\n                        {\n                          name: 'ics',\n                          children: [\n                            { uid: '25dea94e-77', name: 'utils.ts' },\n                            { uid: '25dea94e-79', name: 'icsParser.ts' },\n                            { uid: '25dea94e-81', name: 'icsGenerator.ts' },\n                            { uid: '25dea94e-83', name: 'index.ts' },\n                          ],\n                        },\n                        { uid: '25dea94e-85', name: 'allDaySort.ts' },\n                        { uid: '25dea94e-87', name: 'crossRegionDrag.ts' },\n                        { uid: '25dea94e-163', name: 'subscriptionUtils.ts' },\n                      ],\n                    },\n                    {\n                      name: 'core',\n                      children: [\n                        { uid: '25dea94e-19', name: 'calendarRegistry.ts' },\n                        { uid: '25dea94e-51', name: 'CalendarStore.ts' },\n                        { name: 'events/EventManager.ts', uid: '25dea94e-53' },\n                        {\n                          name: 'navigation/NavigationController.ts',\n                          uid: '25dea94e-55',\n                        },\n                        {\n                          name: 'permissions/CalendarPermissions.ts',\n                          uid: '25dea94e-57',\n                        },\n                        {\n                          name: 'plugins/PluginManager.ts',\n                          uid: '25dea94e-59',\n                        },\n                        { uid: '25dea94e-61', name: 'CalendarApp.ts' },\n                        { uid: '25dea94e-239', name: 'config.ts' },\n                      ],\n                    },\n                    {\n                      name: 'hooks',\n                      children: [\n                        {\n                          name: 'virtualScroll/useVirtualMonthScroll.ts',\n                          uid: '25dea94e-89',\n                        },\n                        { uid: '25dea94e-241', name: 'useCalendarDrop.ts' },\n                        { uid: '25dea94e-257', name: 'useWeekViewSwipe.ts' },\n                        { uid: '25dea94e-269', name: 'useDebouncedValue.ts' },\n                      ],\n                    },\n                    { name: 'styles/classNames.ts', uid: '25dea94e-95' },\n                    {\n                      name: 'components',\n                      children: [\n                        {\n                          name: 'common',\n                          children: [\n                            { uid: '25dea94e-97', name: 'Icons.tsx' },\n                            { uid: '25dea94e-105', name: 'ViewSwitcher.tsx' },\n                            { uid: '25dea94e-107', name: 'CalendarHeader.tsx' },\n                            {\n                              uid: '25dea94e-115',\n                              name: 'BlossomColorPicker.tsx',\n                            },\n                            {\n                              uid: '25dea94e-117',\n                              name: 'DefaultColorPicker.tsx',\n                            },\n                            { uid: '25dea94e-119', name: 'LoadingButton.tsx' },\n                            {\n                              uid: '25dea94e-121',\n                              name: 'CreateCalendarDialog.tsx',\n                            },\n                            { uid: '25dea94e-125', name: 'CalendarPicker.tsx' },\n                            {\n                              uid: '25dea94e-127',\n                              name: 'DefaultEventDetailDialog.tsx',\n                            },\n                            {\n                              uid: '25dea94e-129',\n                              name: 'QuickCreateEventPopup.tsx',\n                            },\n                            { uid: '25dea94e-131', name: 'MiniCalendar.tsx' },\n                            {\n                              uid: '25dea94e-191',\n                              name: 'DefaultEventDetailPanel.tsx',\n                            },\n                            {\n                              uid: '25dea94e-193',\n                              name: 'EventDetailPanelWithContent.tsx',\n                            },\n                            { uid: '25dea94e-211', name: 'TodayBox.tsx' },\n                            { uid: '25dea94e-213', name: 'ViewHeader.tsx' },\n                          ],\n                        },\n                        {\n                          name: 'mobileEventDrawer',\n                          children: [\n                            {\n                              name: 'components',\n                              children: [\n                                { uid: '25dea94e-133', name: 'Switch.tsx' },\n                                {\n                                  uid: '25dea94e-135',\n                                  name: 'TimePickerWheel.tsx',\n                                },\n                              ],\n                            },\n                            {\n                              uid: '25dea94e-137',\n                              name: 'DefaultMobileEventDrawer.tsx',\n                            },\n                          ],\n                        },\n                        {\n                          name: 'search',\n                          children: [\n                            {\n                              uid: '25dea94e-139',\n                              name: 'SearchResultsList.tsx',\n                            },\n                            {\n                              uid: '25dea94e-141',\n                              name: 'MobileSearchDialog.tsx',\n                            },\n                            { uid: '25dea94e-143', name: 'SearchDrawer.tsx' },\n                          ],\n                        },\n                        {\n                          name: 'contextMenu',\n                          children: [\n                            { uid: '25dea94e-167', name: 'utils.ts' },\n                            {\n                              name: 'components',\n                              children: [\n                                {\n                                  uid: '25dea94e-169',\n                                  name: 'GridContextMenu.tsx',\n                                },\n                                {\n                                  uid: '25dea94e-171',\n                                  name: 'EventContextMenu.tsx',\n                                },\n                              ],\n                            },\n                          ],\n                        },\n                        {\n                          name: 'calendarEvent',\n                          children: [\n                            { uid: '25dea94e-173', name: 'utils.ts' },\n                            {\n                              name: 'components',\n                              children: [\n                                {\n                                  uid: '25dea94e-179',\n                                  name: 'AllDayContent.tsx',\n                                },\n                                {\n                                  uid: '25dea94e-181',\n                                  name: 'MonthAllDayContent.tsx',\n                                },\n                                {\n                                  uid: '25dea94e-183',\n                                  name: 'MonthRegularContent.tsx',\n                                },\n                                {\n                                  uid: '25dea94e-185',\n                                  name: 'RegularEventContent.tsx',\n                                },\n                                {\n                                  uid: '25dea94e-187',\n                                  name: 'YearEventContent.tsx',\n                                },\n                                {\n                                  uid: '25dea94e-189',\n                                  name: 'EventContent.tsx',\n                                },\n                                {\n                                  uid: '25dea94e-195',\n                                  name: 'EventDetailPanel.tsx',\n                                },\n                              ],\n                            },\n                            {\n                              name: 'hooks',\n                              children: [\n                                {\n                                  uid: '25dea94e-197',\n                                  name: 'useClickOutside.ts',\n                                },\n                                {\n                                  uid: '25dea94e-199',\n                                  name: 'useDetailPanelPosition.ts',\n                                },\n                                {\n                                  uid: '25dea94e-201',\n                                  name: 'useEventActions.ts',\n                                },\n                                {\n                                  uid: '25dea94e-203',\n                                  name: 'useEventInteraction.ts',\n                                },\n                                {\n                                  uid: '25dea94e-205',\n                                  name: 'useEventStyles.ts',\n                                },\n                                {\n                                  uid: '25dea94e-207',\n                                  name: 'useEventVisibility.ts',\n                                },\n                              ],\n                            },\n                            { uid: '25dea94e-209', name: 'CalendarEvent.tsx' },\n                          ],\n                        },\n                        {\n                          name: 'monthView',\n                          children: [\n                            { uid: '25dea94e-175', name: 'util.tsx' },\n                            { uid: '25dea94e-177', name: 'MultiDayEvent.tsx' },\n                            { uid: '25dea94e-265', name: 'WeekDayCell.tsx' },\n                            { uid: '25dea94e-267', name: 'WeekComponent.tsx' },\n                          ],\n                        },\n                        {\n                          name: 'dayView',\n                          children: [\n                            { uid: '25dea94e-217', name: 'DayContent.tsx' },\n                            { uid: '25dea94e-219', name: 'RightPanel.tsx' },\n                            { uid: '25dea94e-235', name: 'util.ts' },\n                          ],\n                        },\n                        {\n                          name: 'eventLayout',\n                          children: [\n                            { uid: '25dea94e-221', name: 'constants.ts' },\n                            { uid: '25dea94e-223', name: 'utils.ts' },\n                            {\n                              name: 'calculate',\n                              children: [\n                                { uid: '25dea94e-225', name: 'grouping.ts' },\n                                { uid: '25dea94e-227', name: 'layout.ts' },\n                                { uid: '25dea94e-229', name: 'rebalance.ts' },\n                                { uid: '25dea94e-231', name: 'structure.ts' },\n                              ],\n                            },\n                            { uid: '25dea94e-233', name: 'index.tsx' },\n                          ],\n                        },\n                        {\n                          name: 'weekView',\n                          children: [\n                            { uid: '25dea94e-237', name: 'util.ts' },\n                            { uid: '25dea94e-251', name: 'CompactHeader.tsx' },\n                            { uid: '25dea94e-253', name: 'AllDayRow.tsx' },\n                            { uid: '25dea94e-255', name: 'TimeGrid.tsx' },\n                          ],\n                        },\n                        {\n                          name: 'yearView',\n                          children: [\n                            { uid: '25dea94e-279', name: 'utils.ts' },\n                            { uid: '25dea94e-281', name: 'YearDayCell.tsx' },\n                            {\n                              uid: '25dea94e-283',\n                              name: 'YearRowComponent.tsx',\n                            },\n                            {\n                              uid: '25dea94e-285',\n                              name: 'DefaultYearView.tsx',\n                            },\n                            {\n                              uid: '25dea94e-287',\n                              name: 'FixedWeekMonthRow.tsx',\n                            },\n                            {\n                              uid: '25dea94e-289',\n                              name: 'FixedWeekYearView.tsx',\n                            },\n                            { uid: '25dea94e-291', name: 'GridDayPopup.tsx' },\n                            { uid: '25dea94e-293', name: 'GridYearView.tsx' },\n                          ],\n                        },\n                      ],\n                    },\n                    { name: 'contexts/ThemeContext.tsx', uid: '25dea94e-109' },\n                    {\n                      name: 'renderer',\n                      children: [\n                        {\n                          uid: '25dea94e-111',\n                          name: 'CustomRenderingContext.ts',\n                        },\n                        { uid: '25dea94e-113', name: 'ContentSlot.tsx' },\n                        {\n                          name: 'hooks',\n                          children: [\n                            {\n                              uid: '25dea94e-147',\n                              name: 'useAppSubscription.ts',\n                            },\n                            {\n                              uid: '25dea94e-149',\n                              name: 'useEventDialogController.ts',\n                            },\n                            {\n                              uid: '25dea94e-151',\n                              name: 'useQuickCreateController.ts',\n                            },\n                            { uid: '25dea94e-153', name: 'useResponsive.ts' },\n                            {\n                              uid: '25dea94e-155',\n                              name: 'useSearchController.ts',\n                            },\n                          ],\n                        },\n                        { uid: '25dea94e-157', name: 'CalendarRoot.tsx' },\n                        {\n                          uid: '25dea94e-159',\n                          name: 'CustomRenderingStore.ts',\n                        },\n                        { uid: '25dea94e-161', name: 'CalendarRenderer.tsx' },\n                      ],\n                    },\n                    {\n                      name: 'plugins',\n                      children: [\n                        { uid: '25dea94e-145', name: 'sidebarBridge.ts' },\n                        { uid: '25dea94e-243', name: 'dragBridge.ts' },\n                        { uid: '25dea94e-301', name: 'eventsPlugin.ts' },\n                      ],\n                    },\n                    {\n                      name: 'views',\n                      children: [\n                        {\n                          name: 'utils',\n                          children: [\n                            { uid: '25dea94e-215', name: 'dragCreate.ts' },\n                            { uid: '25dea94e-259', name: 'weekView.ts' },\n                          ],\n                        },\n                        { uid: '25dea94e-245', name: 'DayView.tsx' },\n                        { uid: '25dea94e-261', name: 'WeekView.tsx' },\n                        { uid: '25dea94e-271', name: 'MonthView.tsx' },\n                        { uid: '25dea94e-275', name: 'AgendaView.tsx' },\n                        { uid: '25dea94e-295', name: 'YearView.tsx' },\n                      ],\n                    },\n                    {\n                      name: 'factories',\n                      children: [\n                        { uid: '25dea94e-247', name: 'ViewAdapter.tsx' },\n                        { uid: '25dea94e-249', name: 'createDayView.ts' },\n                        { uid: '25dea94e-263', name: 'createWeekView.ts' },\n                        { uid: '25dea94e-273', name: 'createMonthView.ts' },\n                        { uid: '25dea94e-277', name: 'createAgendaView.ts' },\n                        { uid: '25dea94e-297', name: 'createYearView.ts' },\n                        { uid: '25dea94e-299', name: 'index.ts' },\n                      ],\n                    },\n                    { uid: '25dea94e-303', name: 'index.ts' },\n                  ],\n                },\n                {\n                  name: 'Users/jcli/Documents/GitHub/DayFlow',\n                  children: [\n                    {\n                      name: 'node_modules/.pnpm/preact@10.28.4/node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js',\n                      uid: '25dea94e-63',\n                    },\n                    {\n                      name: 'packages/ui',\n                      children: [\n                        {\n                          name: 'range-picker/dist/index.js',\n                          uid: '25dea94e-123',\n                        },\n                        {\n                          name: 'context-menu/dist/index.js',\n                          uid: '25dea94e-165',\n                        },\n                      ],\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n          isRoot: true,\n        },\n        nodeParts: {\n          '25dea94e-1': {\n            renderedLength: 3228,\n            gzipLength: 1057,\n            brotliLength: 924,\n            metaUid: '25dea94e-0',\n          },\n          '25dea94e-3': {\n            renderedLength: 402,\n            gzipLength: 270,\n            brotliLength: 190,\n            metaUid: '25dea94e-2',\n          },\n          '25dea94e-5': {\n            renderedLength: 498,\n            gzipLength: 293,\n            brotliLength: 242,\n            metaUid: '25dea94e-4',\n          },\n          '25dea94e-7': {\n            renderedLength: 293,\n            gzipLength: 182,\n            brotliLength: 140,\n            metaUid: '25dea94e-6',\n          },\n          '25dea94e-9': {\n            renderedLength: 326,\n            gzipLength: 247,\n            brotliLength: 186,\n            metaUid: '25dea94e-8',\n          },\n          '25dea94e-11': {\n            renderedLength: 1632,\n            gzipLength: 628,\n            brotliLength: 524,\n            metaUid: '25dea94e-10',\n          },\n          '25dea94e-13': {\n            renderedLength: 5361,\n            gzipLength: 1820,\n            brotliLength: 1561,\n            metaUid: '25dea94e-12',\n          },\n          '25dea94e-15': {\n            renderedLength: 10902,\n            gzipLength: 2434,\n            brotliLength: 2111,\n            metaUid: '25dea94e-14',\n          },\n          '25dea94e-17': {\n            renderedLength: 4018,\n            gzipLength: 1237,\n            brotliLength: 1068,\n            metaUid: '25dea94e-16',\n          },\n          '25dea94e-19': {\n            renderedLength: 15464,\n            gzipLength: 3271,\n            brotliLength: 2762,\n            metaUid: '25dea94e-18',\n          },\n          '25dea94e-21': {\n            renderedLength: 4636,\n            gzipLength: 1161,\n            brotliLength: 1002,\n            metaUid: '25dea94e-20',\n          },\n          '25dea94e-23': {\n            renderedLength: 1415,\n            gzipLength: 480,\n            brotliLength: 388,\n            metaUid: '25dea94e-22',\n          },\n          '25dea94e-25': {\n            renderedLength: 2453,\n            gzipLength: 636,\n            brotliLength: 550,\n            metaUid: '25dea94e-24',\n          },\n          '25dea94e-27': {\n            renderedLength: 10251,\n            gzipLength: 2087,\n            brotliLength: 1807,\n            metaUid: '25dea94e-26',\n          },\n          '25dea94e-29': {\n            renderedLength: 9520,\n            gzipLength: 2585,\n            brotliLength: 2297,\n            metaUid: '25dea94e-28',\n          },\n          '25dea94e-31': {\n            renderedLength: 1096,\n            gzipLength: 413,\n            brotliLength: 328,\n            metaUid: '25dea94e-30',\n          },\n          '25dea94e-33': {\n            renderedLength: 1952,\n            gzipLength: 725,\n            brotliLength: 621,\n            metaUid: '25dea94e-32',\n          },\n          '25dea94e-35': {\n            renderedLength: 4472,\n            gzipLength: 1376,\n            brotliLength: 1174,\n            metaUid: '25dea94e-34',\n          },\n          '25dea94e-37': {\n            renderedLength: 13216,\n            gzipLength: 2693,\n            brotliLength: 2326,\n            metaUid: '25dea94e-36',\n          },\n          '25dea94e-39': {\n            renderedLength: 10696,\n            gzipLength: 1622,\n            brotliLength: 1337,\n            metaUid: '25dea94e-38',\n          },\n          '25dea94e-41': {\n            renderedLength: 547,\n            gzipLength: 269,\n            brotliLength: 187,\n            metaUid: '25dea94e-40',\n          },\n          '25dea94e-43': {\n            renderedLength: 898,\n            gzipLength: 392,\n            brotliLength: 299,\n            metaUid: '25dea94e-42',\n          },\n          '25dea94e-45': {\n            renderedLength: 2665,\n            gzipLength: 644,\n            brotliLength: 558,\n            metaUid: '25dea94e-44',\n          },\n          '25dea94e-47': {\n            renderedLength: 650,\n            gzipLength: 270,\n            brotliLength: 234,\n            metaUid: '25dea94e-46',\n          },\n          '25dea94e-49': {\n            renderedLength: 3481,\n            gzipLength: 744,\n            brotliLength: 617,\n            metaUid: '25dea94e-48',\n          },\n          '25dea94e-51': {\n            renderedLength: 5284,\n            gzipLength: 1411,\n            brotliLength: 1207,\n            metaUid: '25dea94e-50',\n          },\n          '25dea94e-53': {\n            renderedLength: 11700,\n            gzipLength: 2218,\n            brotliLength: 1987,\n            metaUid: '25dea94e-52',\n          },\n          '25dea94e-55': {\n            renderedLength: 7248,\n            gzipLength: 1287,\n            brotliLength: 1119,\n            metaUid: '25dea94e-54',\n          },\n          '25dea94e-57': {\n            renderedLength: 1977,\n            gzipLength: 570,\n            brotliLength: 477,\n            metaUid: '25dea94e-56',\n          },\n          '25dea94e-59': {\n            renderedLength: 1055,\n            gzipLength: 346,\n            brotliLength: 310,\n            metaUid: '25dea94e-58',\n          },\n          '25dea94e-61': {\n            renderedLength: 14721,\n            gzipLength: 2847,\n            brotliLength: 2523,\n            metaUid: '25dea94e-60',\n          },\n          '25dea94e-63': {\n            renderedLength: 375,\n            gzipLength: 263,\n            brotliLength: 228,\n            metaUid: '25dea94e-62',\n          },\n          '25dea94e-65': {\n            renderedLength: 1916,\n            gzipLength: 656,\n            brotliLength: 561,\n            metaUid: '25dea94e-64',\n          },\n          '25dea94e-67': {\n            renderedLength: 2605,\n            gzipLength: 1023,\n            brotliLength: 787,\n            metaUid: '25dea94e-66',\n          },\n          '25dea94e-69': {\n            renderedLength: 685,\n            gzipLength: 297,\n            brotliLength: 231,\n            metaUid: '25dea94e-68',\n          },\n          '25dea94e-71': {\n            renderedLength: 6395,\n            gzipLength: 1628,\n            brotliLength: 1358,\n            metaUid: '25dea94e-70',\n          },\n          '25dea94e-73': {\n            renderedLength: 3043,\n            gzipLength: 1096,\n            brotliLength: 947,\n            metaUid: '25dea94e-72',\n          },\n          '25dea94e-75': {\n            renderedLength: 384,\n            gzipLength: 161,\n            brotliLength: 132,\n            metaUid: '25dea94e-74',\n          },\n          '25dea94e-77': {\n            renderedLength: 12630,\n            gzipLength: 3231,\n            brotliLength: 2870,\n            metaUid: '25dea94e-76',\n          },\n          '25dea94e-79': {\n            renderedLength: 1822,\n            gzipLength: 738,\n            brotliLength: 607,\n            metaUid: '25dea94e-78',\n          },\n          '25dea94e-81': {\n            renderedLength: 1531,\n            gzipLength: 732,\n            brotliLength: 587,\n            metaUid: '25dea94e-80',\n          },\n          '25dea94e-83': {\n            renderedLength: 1010,\n            gzipLength: 442,\n            brotliLength: 370,\n            metaUid: '25dea94e-82',\n          },\n          '25dea94e-85': {\n            renderedLength: 3582,\n            gzipLength: 924,\n            brotliLength: 806,\n            metaUid: '25dea94e-84',\n          },\n          '25dea94e-87': {\n            renderedLength: 899,\n            gzipLength: 326,\n            brotliLength: 284,\n            metaUid: '25dea94e-86',\n          },\n          '25dea94e-89': {\n            renderedLength: 27502,\n            gzipLength: 5337,\n            brotliLength: 4602,\n            metaUid: '25dea94e-88',\n          },\n          '25dea94e-91': {\n            renderedLength: 165,\n            gzipLength: 137,\n            brotliLength: 118,\n            metaUid: '25dea94e-90',\n          },\n          '25dea94e-93': {\n            renderedLength: 130,\n            gzipLength: 116,\n            brotliLength: 91,\n            metaUid: '25dea94e-92',\n          },\n          '25dea94e-95': {\n            renderedLength: 6610,\n            gzipLength: 1723,\n            brotliLength: 1475,\n            metaUid: '25dea94e-94',\n          },\n          '25dea94e-97': {\n            renderedLength: 8134,\n            gzipLength: 1003,\n            brotliLength: 849,\n            metaUid: '25dea94e-96',\n          },\n          '25dea94e-99': {\n            renderedLength: 2177,\n            gzipLength: 796,\n            brotliLength: 683,\n            metaUid: '25dea94e-98',\n          },\n          '25dea94e-101': {\n            renderedLength: 918,\n            gzipLength: 439,\n            brotliLength: 354,\n            metaUid: '25dea94e-100',\n          },\n          '25dea94e-103': {\n            renderedLength: 1733,\n            gzipLength: 673,\n            brotliLength: 582,\n            metaUid: '25dea94e-102',\n          },\n          '25dea94e-105': {\n            renderedLength: 2963,\n            gzipLength: 937,\n            brotliLength: 788,\n            metaUid: '25dea94e-104',\n          },\n          '25dea94e-107': {\n            renderedLength: 2942,\n            gzipLength: 1028,\n            brotliLength: 879,\n            metaUid: '25dea94e-106',\n          },\n          '25dea94e-109': {\n            renderedLength: 4448,\n            gzipLength: 1423,\n            brotliLength: 1187,\n            metaUid: '25dea94e-108',\n          },\n          '25dea94e-111': {\n            renderedLength: 51,\n            gzipLength: 65,\n            brotliLength: 53,\n            metaUid: '25dea94e-110',\n          },\n          '25dea94e-113': {\n            renderedLength: 2757,\n            gzipLength: 1039,\n            brotliLength: 866,\n            metaUid: '25dea94e-112',\n          },\n          '25dea94e-115': {\n            renderedLength: 893,\n            gzipLength: 318,\n            brotliLength: 261,\n            metaUid: '25dea94e-114',\n          },\n          '25dea94e-117': {\n            renderedLength: 658,\n            gzipLength: 365,\n            brotliLength: 321,\n            metaUid: '25dea94e-116',\n          },\n          '25dea94e-119': {\n            renderedLength: 2134,\n            gzipLength: 826,\n            brotliLength: 711,\n            metaUid: '25dea94e-118',\n          },\n          '25dea94e-121': {\n            renderedLength: 8354,\n            gzipLength: 2089,\n            brotliLength: 1816,\n            metaUid: '25dea94e-120',\n          },\n          '25dea94e-123': {\n            renderedLength: 42549,\n            gzipLength: 8245,\n            brotliLength: 7301,\n            metaUid: '25dea94e-122',\n          },\n          '25dea94e-125': {\n            renderedLength: 5364,\n            gzipLength: 1300,\n            brotliLength: 1101,\n            metaUid: '25dea94e-124',\n          },\n          '25dea94e-127': {\n            renderedLength: 9640,\n            gzipLength: 2315,\n            brotliLength: 2038,\n            metaUid: '25dea94e-126',\n          },\n          '25dea94e-129': {\n            renderedLength: 9321,\n            gzipLength: 2484,\n            brotliLength: 2151,\n            metaUid: '25dea94e-128',\n          },\n          '25dea94e-131': {\n            renderedLength: 5381,\n            gzipLength: 1626,\n            brotliLength: 1460,\n            metaUid: '25dea94e-130',\n          },\n          '25dea94e-133': {\n            renderedLength: 362,\n            gzipLength: 213,\n            brotliLength: 168,\n            metaUid: '25dea94e-132',\n          },\n          '25dea94e-135': {\n            renderedLength: 6625,\n            gzipLength: 1581,\n            brotliLength: 1379,\n            metaUid: '25dea94e-134',\n          },\n          '25dea94e-137': {\n            renderedLength: 17559,\n            gzipLength: 3474,\n            brotliLength: 3001,\n            metaUid: '25dea94e-136',\n          },\n          '25dea94e-139': {\n            renderedLength: 3455,\n            gzipLength: 1039,\n            brotliLength: 907,\n            metaUid: '25dea94e-138',\n          },\n          '25dea94e-141': {\n            renderedLength: 2116,\n            gzipLength: 698,\n            brotliLength: 564,\n            metaUid: '25dea94e-140',\n          },\n          '25dea94e-143': {\n            renderedLength: 388,\n            gzipLength: 218,\n            brotliLength: 163,\n            metaUid: '25dea94e-142',\n          },\n          '25dea94e-145': {\n            renderedLength: 433,\n            gzipLength: 262,\n            brotliLength: 231,\n            metaUid: '25dea94e-144',\n          },\n          '25dea94e-147': {\n            renderedLength: 1000,\n            gzipLength: 416,\n            brotliLength: 362,\n            metaUid: '25dea94e-146',\n          },\n          '25dea94e-149': {\n            renderedLength: 2813,\n            gzipLength: 950,\n            brotliLength: 819,\n            metaUid: '25dea94e-148',\n          },\n          '25dea94e-151': {\n            renderedLength: 2292,\n            gzipLength: 734,\n            brotliLength: 639,\n            metaUid: '25dea94e-150',\n          },\n          '25dea94e-153': {\n            renderedLength: 562,\n            gzipLength: 322,\n            brotliLength: 242,\n            metaUid: '25dea94e-152',\n          },\n          '25dea94e-155': {\n            renderedLength: 6650,\n            gzipLength: 1493,\n            brotliLength: 1291,\n            metaUid: '25dea94e-154',\n          },\n          '25dea94e-157': {\n            renderedLength: 10982,\n            gzipLength: 2879,\n            brotliLength: 2522,\n            metaUid: '25dea94e-156',\n          },\n          '25dea94e-159': {\n            renderedLength: 2043,\n            gzipLength: 601,\n            brotliLength: 498,\n            metaUid: '25dea94e-158',\n          },\n          '25dea94e-161': {\n            renderedLength: 1919,\n            gzipLength: 602,\n            brotliLength: 506,\n            metaUid: '25dea94e-160',\n          },\n          '25dea94e-163': {\n            renderedLength: 1475,\n            gzipLength: 639,\n            brotliLength: 552,\n            metaUid: '25dea94e-162',\n          },\n          '25dea94e-165': {\n            renderedLength: 7198,\n            gzipLength: 2137,\n            brotliLength: 1864,\n            metaUid: '25dea94e-164',\n          },\n          '25dea94e-167': {\n            renderedLength: 3130,\n            gzipLength: 1090,\n            brotliLength: 932,\n            metaUid: '25dea94e-166',\n          },\n          '25dea94e-169': {\n            renderedLength: 969,\n            gzipLength: 459,\n            brotliLength: 410,\n            metaUid: '25dea94e-168',\n          },\n          '25dea94e-171': {\n            renderedLength: 2707,\n            gzipLength: 822,\n            brotliLength: 718,\n            metaUid: '25dea94e-170',\n          },\n          '25dea94e-173': {\n            renderedLength: 5500,\n            gzipLength: 1377,\n            brotliLength: 1197,\n            metaUid: '25dea94e-172',\n          },\n          '25dea94e-175': {\n            renderedLength: 18498,\n            gzipLength: 4342,\n            brotliLength: 3766,\n            metaUid: '25dea94e-174',\n          },\n          '25dea94e-177': {\n            renderedLength: 12226,\n            gzipLength: 3205,\n            brotliLength: 2792,\n            metaUid: '25dea94e-176',\n          },\n          '25dea94e-179': {\n            renderedLength: 2709,\n            gzipLength: 875,\n            brotliLength: 736,\n            metaUid: '25dea94e-178',\n          },\n          '25dea94e-181': {\n            renderedLength: 1187,\n            gzipLength: 529,\n            brotliLength: 421,\n            metaUid: '25dea94e-180',\n          },\n          '25dea94e-183': {\n            renderedLength: 1541,\n            gzipLength: 666,\n            brotliLength: 564,\n            metaUid: '25dea94e-182',\n          },\n          '25dea94e-185': {\n            renderedLength: 5181,\n            gzipLength: 1145,\n            brotliLength: 976,\n            metaUid: '25dea94e-184',\n          },\n          '25dea94e-187': {\n            renderedLength: 3506,\n            gzipLength: 923,\n            brotliLength: 800,\n            metaUid: '25dea94e-186',\n          },\n          '25dea94e-189': {\n            renderedLength: 4305,\n            gzipLength: 1198,\n            brotliLength: 1029,\n            metaUid: '25dea94e-188',\n          },\n          '25dea94e-191': {\n            renderedLength: 16555,\n            gzipLength: 3548,\n            brotliLength: 3114,\n            metaUid: '25dea94e-190',\n          },\n          '25dea94e-193': {\n            renderedLength: 6367,\n            gzipLength: 1429,\n            brotliLength: 1210,\n            metaUid: '25dea94e-192',\n          },\n          '25dea94e-195': {\n            renderedLength: 1015,\n            gzipLength: 399,\n            brotliLength: 336,\n            metaUid: '25dea94e-194',\n          },\n          '25dea94e-197': {\n            renderedLength: 2585,\n            gzipLength: 624,\n            brotliLength: 531,\n            metaUid: '25dea94e-196',\n          },\n          '25dea94e-199': {\n            renderedLength: 6541,\n            gzipLength: 1555,\n            brotliLength: 1380,\n            metaUid: '25dea94e-198',\n          },\n          '25dea94e-201': {\n            renderedLength: 13008,\n            gzipLength: 2636,\n            brotliLength: 2344,\n            metaUid: '25dea94e-200',\n          },\n          '25dea94e-203': {\n            renderedLength: 6610,\n            gzipLength: 1302,\n            brotliLength: 1160,\n            metaUid: '25dea94e-202',\n          },\n          '25dea94e-205': {\n            renderedLength: 14782,\n            gzipLength: 3020,\n            brotliLength: 2659,\n            metaUid: '25dea94e-204',\n          },\n          '25dea94e-207': {\n            renderedLength: 7263,\n            gzipLength: 1658,\n            brotliLength: 1414,\n            metaUid: '25dea94e-206',\n          },\n          '25dea94e-209': {\n            renderedLength: 17342,\n            gzipLength: 3980,\n            brotliLength: 3505,\n            metaUid: '25dea94e-208',\n          },\n          '25dea94e-211': {\n            renderedLength: 607,\n            gzipLength: 281,\n            brotliLength: 230,\n            metaUid: '25dea94e-210',\n          },\n          '25dea94e-213': {\n            renderedLength: 2615,\n            gzipLength: 738,\n            brotliLength: 638,\n            metaUid: '25dea94e-212',\n          },\n          '25dea94e-215': {\n            renderedLength: 3317,\n            gzipLength: 1281,\n            brotliLength: 1024,\n            metaUid: '25dea94e-214',\n          },\n          '25dea94e-217': {\n            renderedLength: 22029,\n            gzipLength: 3745,\n            brotliLength: 3333,\n            metaUid: '25dea94e-216',\n          },\n          '25dea94e-219': {\n            renderedLength: 3314,\n            gzipLength: 1029,\n            brotliLength: 899,\n            metaUid: '25dea94e-218',\n          },\n          '25dea94e-221': {\n            renderedLength: 449,\n            gzipLength: 285,\n            brotliLength: 221,\n            metaUid: '25dea94e-220',\n          },\n          '25dea94e-223': {\n            renderedLength: 3817,\n            gzipLength: 1082,\n            brotliLength: 930,\n            metaUid: '25dea94e-222',\n          },\n          '25dea94e-225': {\n            renderedLength: 2480,\n            gzipLength: 698,\n            brotliLength: 605,\n            metaUid: '25dea94e-224',\n          },\n          '25dea94e-227': {\n            renderedLength: 5826,\n            gzipLength: 1532,\n            brotliLength: 1314,\n            metaUid: '25dea94e-226',\n          },\n          '25dea94e-229': {\n            renderedLength: 3186,\n            gzipLength: 975,\n            brotliLength: 859,\n            metaUid: '25dea94e-228',\n          },\n          '25dea94e-231': {\n            renderedLength: 10216,\n            gzipLength: 2286,\n            brotliLength: 2016,\n            metaUid: '25dea94e-230',\n          },\n          '25dea94e-233': {\n            renderedLength: 3134,\n            gzipLength: 1068,\n            brotliLength: 901,\n            metaUid: '25dea94e-232',\n          },\n          '25dea94e-235': {\n            renderedLength: 6362,\n            gzipLength: 1643,\n            brotliLength: 1471,\n            metaUid: '25dea94e-234',\n          },\n          '25dea94e-237': {\n            renderedLength: 10062,\n            gzipLength: 2755,\n            brotliLength: 2429,\n            metaUid: '25dea94e-236',\n          },\n          '25dea94e-239': {\n            renderedLength: 111,\n            gzipLength: 103,\n            brotliLength: 109,\n            metaUid: '25dea94e-238',\n          },\n          '25dea94e-241': {\n            renderedLength: 3968,\n            gzipLength: 1175,\n            brotliLength: 989,\n            metaUid: '25dea94e-240',\n          },\n          '25dea94e-243': {\n            renderedLength: 599,\n            gzipLength: 280,\n            brotliLength: 230,\n            metaUid: '25dea94e-242',\n          },\n          '25dea94e-245': {\n            renderedLength: 16617,\n            gzipLength: 4136,\n            brotliLength: 3654,\n            metaUid: '25dea94e-244',\n          },\n          '25dea94e-247': {\n            renderedLength: 5374,\n            gzipLength: 1153,\n            brotliLength: 1005,\n            metaUid: '25dea94e-246',\n          },\n          '25dea94e-249': {\n            renderedLength: 700,\n            gzipLength: 335,\n            brotliLength: 295,\n            metaUid: '25dea94e-248',\n          },\n          '25dea94e-251': {\n            renderedLength: 2611,\n            gzipLength: 804,\n            brotliLength: 695,\n            metaUid: '25dea94e-250',\n          },\n          '25dea94e-253': {\n            renderedLength: 12327,\n            gzipLength: 2579,\n            brotliLength: 2285,\n            metaUid: '25dea94e-252',\n          },\n          '25dea94e-255': {\n            renderedLength: 21119,\n            gzipLength: 4031,\n            brotliLength: 3614,\n            metaUid: '25dea94e-254',\n          },\n          '25dea94e-257': {\n            renderedLength: 11298,\n            gzipLength: 1883,\n            brotliLength: 1689,\n            metaUid: '25dea94e-256',\n          },\n          '25dea94e-259': {\n            renderedLength: 2790,\n            gzipLength: 748,\n            brotliLength: 672,\n            metaUid: '25dea94e-258',\n          },\n          '25dea94e-261': {\n            renderedLength: 21822,\n            gzipLength: 5068,\n            brotliLength: 4466,\n            metaUid: '25dea94e-260',\n          },\n          '25dea94e-263': {\n            renderedLength: 749,\n            gzipLength: 354,\n            brotliLength: 283,\n            metaUid: '25dea94e-262',\n          },\n          '25dea94e-265': {\n            renderedLength: 9770,\n            gzipLength: 2483,\n            brotliLength: 2200,\n            metaUid: '25dea94e-264',\n          },\n          '25dea94e-267': {\n            renderedLength: 18331,\n            gzipLength: 4418,\n            brotliLength: 3874,\n            metaUid: '25dea94e-266',\n          },\n          '25dea94e-269': {\n            renderedLength: 351,\n            gzipLength: 175,\n            brotliLength: 152,\n            metaUid: '25dea94e-268',\n          },\n          '25dea94e-271': {\n            renderedLength: 32393,\n            gzipLength: 7472,\n            brotliLength: 6594,\n            metaUid: '25dea94e-270',\n          },\n          '25dea94e-273': {\n            renderedLength: 822,\n            gzipLength: 386,\n            brotliLength: 315,\n            metaUid: '25dea94e-272',\n          },\n          '25dea94e-275': {\n            renderedLength: 21152,\n            gzipLength: 4189,\n            brotliLength: 3717,\n            metaUid: '25dea94e-274',\n          },\n          '25dea94e-277': {\n            renderedLength: 676,\n            gzipLength: 343,\n            brotliLength: 291,\n            metaUid: '25dea94e-276',\n          },\n          '25dea94e-279': {\n            renderedLength: 17568,\n            gzipLength: 3944,\n            brotliLength: 3479,\n            metaUid: '25dea94e-278',\n          },\n          '25dea94e-281': {\n            renderedLength: 1695,\n            gzipLength: 643,\n            brotliLength: 551,\n            metaUid: '25dea94e-280',\n          },\n          '25dea94e-283': {\n            renderedLength: 11203,\n            gzipLength: 2803,\n            brotliLength: 2469,\n            metaUid: '25dea94e-282',\n          },\n          '25dea94e-285': {\n            renderedLength: 17420,\n            gzipLength: 4480,\n            brotliLength: 3924,\n            metaUid: '25dea94e-284',\n          },\n          '25dea94e-287': {\n            renderedLength: 6969,\n            gzipLength: 1890,\n            brotliLength: 1663,\n            metaUid: '25dea94e-286',\n          },\n          '25dea94e-289': {\n            renderedLength: 17766,\n            gzipLength: 4328,\n            brotliLength: 3813,\n            metaUid: '25dea94e-288',\n          },\n          '25dea94e-291': {\n            renderedLength: 3562,\n            gzipLength: 1091,\n            brotliLength: 941,\n            metaUid: '25dea94e-290',\n          },\n          '25dea94e-293': {\n            renderedLength: 14144,\n            gzipLength: 3708,\n            brotliLength: 3266,\n            metaUid: '25dea94e-292',\n          },\n          '25dea94e-295': {\n            renderedLength: 418,\n            gzipLength: 241,\n            brotliLength: 195,\n            metaUid: '25dea94e-294',\n          },\n          '25dea94e-297': {\n            renderedLength: 715,\n            gzipLength: 337,\n            brotliLength: 265,\n            metaUid: '25dea94e-296',\n          },\n          '25dea94e-299': {\n            renderedLength: 544,\n            gzipLength: 236,\n            brotliLength: 184,\n            metaUid: '25dea94e-298',\n          },\n          '25dea94e-301': {\n            renderedLength: 5441,\n            gzipLength: 1425,\n            brotliLength: 1261,\n            metaUid: '25dea94e-300',\n          },\n          '25dea94e-303': {\n            renderedLength: 0,\n            gzipLength: 0,\n            brotliLength: 0,\n            metaUid: '25dea94e-302',\n          },\n        },\n        nodeMetas: {\n          '25dea94e-0': {\n            id: '/src/locale/locales/en.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-1' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-2' }],\n          },\n          '25dea94e-2': {\n            id: '/src/locale/locales/index.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-3' },\n            imported: [{ uid: '25dea94e-0' }],\n            importedBy: [\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-4' },\n              { uid: '25dea94e-100' },\n            ],\n          },\n          '25dea94e-4': {\n            id: '/src/locale/utils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-5' },\n            imported: [{ uid: '25dea94e-2' }],\n            importedBy: [\n              { uid: '25dea94e-60' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-100' },\n              { uid: '25dea94e-102' },\n            ],\n          },\n          '25dea94e-6': {\n            id: '/src/types/core.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-7' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-8': {\n            id: '/src/types/layout.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-9' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-10': {\n            id: '/src/types/monthView.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-11' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }, { uid: '25dea94e-88' }],\n          },\n          '25dea94e-12': {\n            id: '/src/types/timezone.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-13' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }, { uid: '25dea94e-24' }],\n          },\n          '25dea94e-14': {\n            id: '/src/utils/temporal.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-15' },\n            imported: [{ uid: '25dea94e-311' }],\n            importedBy: [\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-300' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-16' },\n              { uid: '25dea94e-64' },\n              { uid: '25dea94e-28' },\n              { uid: '25dea94e-36' },\n              { uid: '25dea94e-72' },\n              { uid: '25dea94e-84' },\n              { uid: '25dea94e-148' },\n              { uid: '25dea94e-154' },\n              { uid: '25dea94e-136' },\n            ],\n          },\n          '25dea94e-16': {\n            id: '/src/utils/dateTimeUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-17' },\n            imported: [{ uid: '25dea94e-311' }, { uid: '25dea94e-14' }],\n            importedBy: [\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-28' },\n            ],\n          },\n          '25dea94e-18': {\n            id: '/src/core/calendarRegistry.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-19' },\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-60' },\n              { uid: '25dea94e-162' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-124' },\n              { uid: '25dea94e-20' },\n            ],\n          },\n          '25dea94e-20': {\n            id: '/src/utils/colorUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-21' },\n            imported: [{ uid: '25dea94e-18' }],\n            importedBy: [{ uid: '25dea94e-313' }, { uid: '25dea94e-238' }],\n          },\n          '25dea94e-22': {\n            id: '/src/utils/logger.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-23' },\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-52' },\n              { uid: '25dea94e-58' },\n              { uid: '25dea94e-26' },\n              { uid: '25dea94e-200' },\n            ],\n          },\n          '25dea94e-24': {\n            id: '/src/utils/timeZoneUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-25' },\n            imported: [{ uid: '25dea94e-12' }],\n            importedBy: [\n              { uid: '25dea94e-60' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-26' },\n              { uid: '25dea94e-28' },\n            ],\n          },\n          '25dea94e-26': {\n            id: '/src/utils/temporalTypeGuards.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-27' },\n            imported: [\n              { uid: '25dea94e-311' },\n              { uid: '25dea94e-22' },\n              { uid: '25dea94e-24' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-28' },\n              { uid: '25dea94e-70' },\n              { uid: '25dea94e-128' },\n              { uid: '25dea94e-150' },\n              { uid: '25dea94e-76' },\n              { uid: '25dea94e-136' },\n              { uid: '25dea94e-218' },\n              { uid: '25dea94e-234' },\n              { uid: '25dea94e-236' },\n            ],\n          },\n          '25dea94e-28': {\n            id: '/src/utils/timeUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-29' },\n            imported: [\n              { uid: '25dea94e-311' },\n              { uid: '25dea94e-16' },\n              { uid: '25dea94e-14' },\n              { uid: '25dea94e-26' },\n              { uid: '25dea94e-24' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-128' },\n              { uid: '25dea94e-150' },\n            ],\n          },\n          '25dea94e-30': {\n            id: '/src/utils/dateConstants.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-31' },\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-32' },\n              { uid: '25dea94e-34' },\n            ],\n          },\n          '25dea94e-32': {\n            id: '/src/utils/dateRangeUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-33' },\n            imported: [{ uid: '25dea94e-30' }],\n            importedBy: [\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-54' },\n              { uid: '25dea94e-34' },\n            ],\n          },\n          '25dea94e-34': {\n            id: '/src/utils/calendarDataUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-35' },\n            imported: [{ uid: '25dea94e-30' }, { uid: '25dea94e-32' }],\n            importedBy: [{ uid: '25dea94e-313' }],\n          },\n          '25dea94e-36': {\n            id: '/src/utils/eventUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-37' },\n            imported: [{ uid: '25dea94e-311' }, { uid: '25dea94e-14' }],\n            importedBy: [\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-313' },\n            ],\n          },\n          '25dea94e-38': {\n            id: '/src/utils/testDataUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-39' },\n            imported: [{ uid: '25dea94e-311' }],\n            importedBy: [{ uid: '25dea94e-313' }],\n          },\n          '25dea94e-40': {\n            id: '/src/utils/utilityFunctions.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-41' },\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-162' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-76' },\n            ],\n          },\n          '25dea94e-42': {\n            id: '/src/utils/compareUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-43' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-160' }, { uid: '25dea94e-313' }],\n          },\n          '25dea94e-44': {\n            id: '/src/utils/calendarApp/configSync.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-45' },\n            imported: [{ uid: '25dea94e-313' }],\n            importedBy: [{ uid: '25dea94e-312' }],\n          },\n          '25dea94e-46': {\n            id: '/src/utils/calendarApp/normalizedConfig.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-47' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-312' }],\n          },\n          '25dea94e-48': {\n            id: '/src/utils/calendarApp/viewConfigComparison.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-49' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-312' }],\n          },\n          '25dea94e-50': {\n            id: '/src/core/CalendarStore.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-51' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-52' }],\n          },\n          '25dea94e-52': {\n            id: '/src/core/events/EventManager.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-53' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-50' },\n              { uid: '25dea94e-22' },\n            ],\n            importedBy: [{ uid: '25dea94e-60' }],\n          },\n          '25dea94e-54': {\n            id: '/src/core/navigation/NavigationController.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-55' },\n            imported: [\n              { uid: '25dea94e-311' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-32' },\n            ],\n            importedBy: [{ uid: '25dea94e-60' }],\n          },\n          '25dea94e-56': {\n            id: '/src/core/permissions/CalendarPermissions.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-57' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-60' }],\n          },\n          '25dea94e-58': {\n            id: '/src/core/plugins/PluginManager.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-59' },\n            imported: [{ uid: '25dea94e-22' }],\n            importedBy: [{ uid: '25dea94e-60' }],\n          },\n          '25dea94e-60': {\n            id: '/src/core/CalendarApp.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-61' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-311' },\n              { uid: '25dea94e-4' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-312' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-24' },\n              { uid: '25dea94e-18' },\n              { uid: '25dea94e-52' },\n              { uid: '25dea94e-54' },\n              { uid: '25dea94e-56' },\n              { uid: '25dea94e-58' },\n            ],\n            importedBy: [{ uid: '25dea94e-302' }],\n          },\n          '25dea94e-62': {\n            id: '/Users/jcli/Documents/GitHub/DayFlow/node_modules/.pnpm/preact@10.28.4/node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js',\n            moduleParts: { 'index.esm.js': '25dea94e-63' },\n            imported: [{ uid: '25dea94e-314' }],\n            importedBy: [\n              { uid: '25dea94e-118' },\n              { uid: '25dea94e-114' },\n              { uid: '25dea94e-116' },\n              { uid: '25dea94e-130' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-96' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-156' },\n              { uid: '25dea94e-102' },\n              { uid: '25dea94e-168' },\n              { uid: '25dea94e-170' },\n              { uid: '25dea94e-108' },\n              { uid: '25dea94e-124' },\n              { uid: '25dea94e-208' },\n              { uid: '25dea94e-106' },\n              { uid: '25dea94e-128' },\n              { uid: '25dea94e-140' },\n              { uid: '25dea94e-142' },\n              { uid: '25dea94e-244' },\n              { uid: '25dea94e-246' },\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-270' },\n              { uid: '25dea94e-274' },\n              { uid: '25dea94e-294' },\n              { uid: '25dea94e-188' },\n              { uid: '25dea94e-194' },\n              { uid: '25dea94e-104' },\n              { uid: '25dea94e-136' },\n              { uid: '25dea94e-132' },\n              { uid: '25dea94e-134' },\n              { uid: '25dea94e-138' },\n              { uid: '25dea94e-216' },\n              { uid: '25dea94e-218' },\n              { uid: '25dea94e-212' },\n              { uid: '25dea94e-252' },\n              { uid: '25dea94e-254' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-288' },\n              { uid: '25dea94e-292' },\n              { uid: '25dea94e-176' },\n              { uid: '25dea94e-178' },\n              { uid: '25dea94e-180' },\n              { uid: '25dea94e-182' },\n              { uid: '25dea94e-184' },\n              { uid: '25dea94e-186' },\n              { uid: '25dea94e-192' },\n              { uid: '25dea94e-210' },\n              { uid: '25dea94e-250' },\n              { uid: '25dea94e-264' },\n              { uid: '25dea94e-282' },\n              { uid: '25dea94e-286' },\n              { uid: '25dea94e-290' },\n              { uid: '25dea94e-280' },\n            ],\n          },\n          '25dea94e-64': {\n            id: '/src/utils/dateFormat.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-65' },\n            imported: [{ uid: '25dea94e-14' }],\n            importedBy: [{ uid: '25dea94e-305' }],\n          },\n          '25dea94e-66': {\n            id: '/src/utils/styleUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-67' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-305' }],\n          },\n          '25dea94e-68': {\n            id: '/src/utils/themeUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-69' },\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-108' },\n              { uid: '25dea94e-192' },\n            ],\n          },\n          '25dea94e-70': {\n            id: '/src/utils/eventHelpers.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-71' },\n            imported: [{ uid: '25dea94e-311' }, { uid: '25dea94e-26' }],\n            importedBy: [{ uid: '25dea94e-305' }],\n          },\n          '25dea94e-72': {\n            id: '/src/utils/searchUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-73' },\n            imported: [{ uid: '25dea94e-14' }],\n            importedBy: [{ uid: '25dea94e-305' }, { uid: '25dea94e-138' }],\n          },\n          '25dea94e-74': {\n            id: '/src/utils/clipboardStore.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-75' },\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-168' },\n              { uid: '25dea94e-170' },\n              { uid: '25dea94e-166' },\n            ],\n          },\n          '25dea94e-76': {\n            id: '/src/utils/ics/utils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-77' },\n            imported: [\n              { uid: '25dea94e-311' },\n              { uid: '25dea94e-26' },\n              { uid: '25dea94e-40' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-82' },\n              { uid: '25dea94e-78' },\n              { uid: '25dea94e-80' },\n            ],\n          },\n          '25dea94e-78': {\n            id: '/src/utils/ics/icsParser.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-79' },\n            imported: [{ uid: '25dea94e-76' }],\n            importedBy: [{ uid: '25dea94e-162' }, { uid: '25dea94e-82' }],\n          },\n          '25dea94e-80': {\n            id: '/src/utils/ics/icsGenerator.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-81' },\n            imported: [{ uid: '25dea94e-76' }],\n            importedBy: [{ uid: '25dea94e-82' }],\n          },\n          '25dea94e-82': {\n            id: '/src/utils/ics/index.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-83' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-78' },\n              { uid: '25dea94e-330' },\n              { uid: '25dea94e-76' },\n              { uid: '25dea94e-80' },\n            ],\n            importedBy: [{ uid: '25dea94e-305' }],\n          },\n          '25dea94e-84': {\n            id: '/src/utils/allDaySort.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-85' },\n            imported: [{ uid: '25dea94e-14' }],\n            importedBy: [\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-278' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-234' },\n              { uid: '25dea94e-236' },\n            ],\n          },\n          '25dea94e-86': {\n            id: '/src/utils/crossRegionDrag.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-87' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-305' }],\n          },\n          '25dea94e-88': {\n            id: '/src/hooks/virtualScroll/useVirtualMonthScroll.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-89' },\n            imported: [\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-10' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-331' }],\n          },\n          '25dea94e-90': {\n            id: '/src/locale/LocaleContext.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-91' },\n            imported: [{ uid: '25dea94e-314' }],\n            importedBy: [\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-92' },\n              { uid: '25dea94e-102' },\n            ],\n          },\n          '25dea94e-92': {\n            id: '/src/locale/useLocale.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-93' },\n            imported: [{ uid: '25dea94e-327' }, { uid: '25dea94e-90' }],\n            importedBy: [\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-156' },\n              { uid: '25dea94e-106' },\n              { uid: '25dea94e-138' },\n            ],\n          },\n          '25dea94e-94': {\n            id: '/src/styles/classNames.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-95' },\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-130' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-124' },\n              { uid: '25dea94e-106' },\n              { uid: '25dea94e-140' },\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-270' },\n              { uid: '25dea94e-216' },\n              { uid: '25dea94e-218' },\n              { uid: '25dea94e-212' },\n              { uid: '25dea94e-252' },\n              { uid: '25dea94e-254' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-292' },\n              { uid: '25dea94e-176' },\n              { uid: '25dea94e-178' },\n              { uid: '25dea94e-180' },\n              { uid: '25dea94e-182' },\n              { uid: '25dea94e-184' },\n              { uid: '25dea94e-186' },\n              { uid: '25dea94e-192' },\n              { uid: '25dea94e-210' },\n              { uid: '25dea94e-264' },\n            ],\n          },\n          '25dea94e-96': {\n            id: '/src/components/common/Icons.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-97' },\n            imported: [{ uid: '25dea94e-62' }],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-130' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-170' },\n              { uid: '25dea94e-124' },\n              { uid: '25dea94e-106' },\n              { uid: '25dea94e-140' },\n              { uid: '25dea94e-104' },\n              { uid: '25dea94e-138' },\n              { uid: '25dea94e-178' },\n              { uid: '25dea94e-180' },\n              { uid: '25dea94e-210' },\n            ],\n          },\n          '25dea94e-98': {\n            id: '/src/locale/intl.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-99' },\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-100' },\n              { uid: '25dea94e-102' },\n            ],\n          },\n          '25dea94e-100': {\n            id: '/src/locale/translator.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-101' },\n            imported: [\n              { uid: '25dea94e-98' },\n              { uid: '25dea94e-2' },\n              { uid: '25dea94e-4' },\n            ],\n            importedBy: [{ uid: '25dea94e-306' }, { uid: '25dea94e-102' }],\n          },\n          '25dea94e-102': {\n            id: '/src/locale/LocaleProvider.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-103' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-98' },\n              { uid: '25dea94e-90' },\n              { uid: '25dea94e-100' },\n              { uid: '25dea94e-4' },\n            ],\n            importedBy: [{ uid: '25dea94e-306' }, { uid: '25dea94e-156' }],\n          },\n          '25dea94e-104': {\n            id: '/src/components/common/ViewSwitcher.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-105' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-96' },\n            ],\n            importedBy: [{ uid: '25dea94e-106' }],\n          },\n          '25dea94e-106': {\n            id: '/src/components/common/CalendarHeader.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-107' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-331' },\n              { uid: '25dea94e-92' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-96' },\n              { uid: '25dea94e-104' },\n            ],\n            importedBy: [{ uid: '25dea94e-156' }],\n          },\n          '25dea94e-108': {\n            id: '/src/contexts/ThemeContext.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-109' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-314' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-68' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-156' },\n              { uid: '25dea94e-192' },\n            ],\n          },\n          '25dea94e-110': {\n            id: '/src/renderer/CustomRenderingContext.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-111' },\n            imported: [{ uid: '25dea94e-314' }],\n            importedBy: [\n              { uid: '25dea94e-160' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-156' },\n              { uid: '25dea94e-208' },\n              { uid: '25dea94e-274' },\n            ],\n          },\n          '25dea94e-112': {\n            id: '/src/renderer/ContentSlot.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-113' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-110' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-156' },\n              { uid: '25dea94e-168' },\n              { uid: '25dea94e-170' },\n              { uid: '25dea94e-208' },\n              { uid: '25dea94e-274' },\n              { uid: '25dea94e-188' },\n            ],\n          },\n          '25dea94e-114': {\n            id: '/src/components/common/BlossomColorPicker.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-115' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-328' },\n              { uid: '25dea94e-327' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-116' },\n              { uid: '25dea94e-120' },\n            ],\n          },\n          '25dea94e-116': {\n            id: '/src/components/common/DefaultColorPicker.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-117' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-328' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-114' },\n            ],\n            importedBy: [{ uid: '25dea94e-302' }, { uid: '25dea94e-120' }],\n          },\n          '25dea94e-118': {\n            id: '/src/components/common/LoadingButton.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-119' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n            ],\n          },\n          '25dea94e-120': {\n            id: '/src/components/common/CreateCalendarDialog.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-121' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-328' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-108' },\n              { uid: '25dea94e-18' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-114' },\n              { uid: '25dea94e-116' },\n              { uid: '25dea94e-118' },\n            ],\n            importedBy: [{ uid: '25dea94e-302' }, { uid: '25dea94e-156' }],\n          },\n          '25dea94e-122': {\n            id: '/Users/jcli/Documents/GitHub/DayFlow/packages/ui/range-picker/dist/index.js',\n            moduleParts: { 'index.esm.js': '25dea94e-123' },\n            imported: [\n              { uid: '25dea94e-314' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-311' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n            ],\n          },\n          '25dea94e-124': {\n            id: '/src/components/common/CalendarPicker.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-125' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-18' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-96' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-136' },\n            ],\n          },\n          '25dea94e-126': {\n            id: '/src/components/common/DefaultEventDetailDialog.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-127' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-122' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-311' },\n              { uid: '25dea94e-18' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-36' },\n              { uid: '25dea94e-14' },\n              { uid: '25dea94e-28' },\n              { uid: '25dea94e-124' },\n              { uid: '25dea94e-118' },\n            ],\n            importedBy: [{ uid: '25dea94e-302' }, { uid: '25dea94e-156' }],\n          },\n          '25dea94e-128': {\n            id: '/src/components/common/QuickCreateEventPopup.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-129' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-26' },\n              { uid: '25dea94e-28' },\n            ],\n            importedBy: [{ uid: '25dea94e-156' }],\n          },\n          '25dea94e-130': {\n            id: '/src/components/common/MiniCalendar.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-131' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-311' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-96' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-136' },\n              { uid: '25dea94e-218' },\n            ],\n          },\n          '25dea94e-132': {\n            id: '/src/components/mobileEventDrawer/components/Switch.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-133' },\n            imported: [{ uid: '25dea94e-62' }],\n            importedBy: [{ uid: '25dea94e-329' }, { uid: '25dea94e-136' }],\n          },\n          '25dea94e-134': {\n            id: '/src/components/mobileEventDrawer/components/TimePickerWheel.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-135' },\n            imported: [{ uid: '25dea94e-62' }, { uid: '25dea94e-327' }],\n            importedBy: [{ uid: '25dea94e-329' }, { uid: '25dea94e-136' }],\n          },\n          '25dea94e-136': {\n            id: '/src/components/mobileEventDrawer/DefaultMobileEventDrawer.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-137' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-124' },\n              { uid: '25dea94e-130' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-14' },\n              { uid: '25dea94e-26' },\n              { uid: '25dea94e-132' },\n              { uid: '25dea94e-134' },\n            ],\n            importedBy: [{ uid: '25dea94e-329' }],\n          },\n          '25dea94e-138': {\n            id: '/src/components/search/SearchResultsList.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-139' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-96' },\n              { uid: '25dea94e-92' },\n              { uid: '25dea94e-72' },\n            ],\n            importedBy: [{ uid: '25dea94e-140' }, { uid: '25dea94e-142' }],\n          },\n          '25dea94e-140': {\n            id: '/src/components/search/MobileSearchDialog.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-141' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-96' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-138' },\n            ],\n            importedBy: [{ uid: '25dea94e-156' }],\n          },\n          '25dea94e-142': {\n            id: '/src/components/search/SearchDrawer.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-143' },\n            imported: [{ uid: '25dea94e-62' }, { uid: '25dea94e-138' }],\n            importedBy: [{ uid: '25dea94e-156' }],\n          },\n          '25dea94e-144': {\n            id: '/src/plugins/sidebarBridge.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-145' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-302' }, { uid: '25dea94e-156' }],\n          },\n          '25dea94e-146': {\n            id: '/src/renderer/hooks/useAppSubscription.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-147' },\n            imported: [{ uid: '25dea94e-327' }],\n            importedBy: [{ uid: '25dea94e-156' }],\n          },\n          '25dea94e-148': {\n            id: '/src/renderer/hooks/useEventDialogController.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-149' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-14' },\n            ],\n            importedBy: [{ uid: '25dea94e-156' }],\n          },\n          '25dea94e-150': {\n            id: '/src/renderer/hooks/useQuickCreateController.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-151' },\n            imported: [\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-26' },\n              { uid: '25dea94e-28' },\n            ],\n            importedBy: [{ uid: '25dea94e-156' }],\n          },\n          '25dea94e-152': {\n            id: '/src/renderer/hooks/useResponsive.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-153' },\n            imported: [{ uid: '25dea94e-327' }],\n            importedBy: [{ uid: '25dea94e-156' }],\n          },\n          '25dea94e-154': {\n            id: '/src/renderer/hooks/useSearchController.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-155' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-14' },\n            ],\n            importedBy: [{ uid: '25dea94e-156' }],\n          },\n          '25dea94e-156': {\n            id: '/src/renderer/CalendarRoot.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-157' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-314' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-106' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-128' },\n              { uid: '25dea94e-329' },\n              { uid: '25dea94e-140' },\n              { uid: '25dea94e-142' },\n              { uid: '25dea94e-108' },\n              { uid: '25dea94e-102' },\n              { uid: '25dea94e-92' },\n              { uid: '25dea94e-144' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-110' },\n              { uid: '25dea94e-146' },\n              { uid: '25dea94e-148' },\n              { uid: '25dea94e-150' },\n              { uid: '25dea94e-152' },\n              { uid: '25dea94e-154' },\n            ],\n            importedBy: [{ uid: '25dea94e-160' }],\n          },\n          '25dea94e-158': {\n            id: '/src/renderer/CustomRenderingStore.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-159' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-302' }, { uid: '25dea94e-160' }],\n          },\n          '25dea94e-160': {\n            id: '/src/renderer/CalendarRenderer.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-161' },\n            imported: [\n              { uid: '25dea94e-314' },\n              { uid: '25dea94e-42' },\n              { uid: '25dea94e-156' },\n              { uid: '25dea94e-110' },\n              { uid: '25dea94e-158' },\n            ],\n            importedBy: [{ uid: '25dea94e-302' }],\n          },\n          '25dea94e-162': {\n            id: '/src/utils/subscriptionUtils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-163' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-18' },\n              { uid: '25dea94e-78' },\n              { uid: '25dea94e-40' },\n            ],\n            importedBy: [{ uid: '25dea94e-302' }],\n          },\n          '25dea94e-164': {\n            id: '/Users/jcli/Documents/GitHub/DayFlow/packages/ui/context-menu/dist/index.js',\n            moduleParts: { 'index.esm.js': '25dea94e-165' },\n            imported: [\n              { uid: '25dea94e-314' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-307' },\n              { uid: '25dea94e-168' },\n              { uid: '25dea94e-170' },\n            ],\n          },\n          '25dea94e-166': {\n            id: '/src/components/contextMenu/utils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-167' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-74' },\n            ],\n            importedBy: [{ uid: '25dea94e-307' }, { uid: '25dea94e-168' }],\n          },\n          '25dea94e-168': {\n            id: '/src/components/contextMenu/components/GridContextMenu.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-169' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-164' },\n              { uid: '25dea94e-166' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-74' },\n            ],\n            importedBy: [{ uid: '25dea94e-307' }],\n          },\n          '25dea94e-170': {\n            id: '/src/components/contextMenu/components/EventContextMenu.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-171' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-164' },\n              { uid: '25dea94e-96' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-74' },\n            ],\n            importedBy: [{ uid: '25dea94e-307' }],\n          },\n          '25dea94e-172': {\n            id: '/src/components/calendarEvent/utils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-173' },\n            imported: [{ uid: '25dea94e-304' }],\n            importedBy: [\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-208' },\n              { uid: '25dea94e-198' },\n              { uid: '25dea94e-200' },\n              { uid: '25dea94e-204' },\n              { uid: '25dea94e-206' },\n              { uid: '25dea94e-176' },\n              { uid: '25dea94e-192' },\n            ],\n          },\n          '25dea94e-174': {\n            id: '/src/components/monthView/util.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-175' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-311' },\n              { uid: '25dea94e-96' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-84' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-22' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-236' },\n              { uid: '25dea94e-254' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-176' },\n              { uid: '25dea94e-186' },\n              { uid: '25dea94e-264' },\n            ],\n          },\n          '25dea94e-176': {\n            id: '/src/components/monthView/MultiDayEvent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-177' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-172' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-188' }],\n          },\n          '25dea94e-178': {\n            id: '/src/components/calendarEvent/components/AllDayContent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-179' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-96' },\n              { uid: '25dea94e-94' },\n            ],\n            importedBy: [{ uid: '25dea94e-188' }],\n          },\n          '25dea94e-180': {\n            id: '/src/components/calendarEvent/components/MonthAllDayContent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-181' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-96' },\n              { uid: '25dea94e-94' },\n            ],\n            importedBy: [{ uid: '25dea94e-188' }],\n          },\n          '25dea94e-182': {\n            id: '/src/components/calendarEvent/components/MonthRegularContent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-183' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-188' }],\n          },\n          '25dea94e-184': {\n            id: '/src/components/calendarEvent/components/RegularEventContent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-185' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-188' }],\n          },\n          '25dea94e-186': {\n            id: '/src/components/calendarEvent/components/YearEventContent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-187' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-188' }],\n          },\n          '25dea94e-188': {\n            id: '/src/components/calendarEvent/components/EventContent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-189' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-176' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-178' },\n              { uid: '25dea94e-180' },\n              { uid: '25dea94e-182' },\n              { uid: '25dea94e-184' },\n              { uid: '25dea94e-186' },\n            ],\n            importedBy: [{ uid: '25dea94e-208' }],\n          },\n          '25dea94e-190': {\n            id: '/src/components/common/DefaultEventDetailPanel.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-191' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-122' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-311' },\n              { uid: '25dea94e-172' },\n              { uid: '25dea94e-108' },\n              { uid: '25dea94e-18' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-36' },\n              { uid: '25dea94e-22' },\n              { uid: '25dea94e-14' },\n              { uid: '25dea94e-68' },\n              { uid: '25dea94e-28' },\n              { uid: '25dea94e-124' },\n              { uid: '25dea94e-118' },\n            ],\n            importedBy: [{ uid: '25dea94e-302' }, { uid: '25dea94e-194' }],\n          },\n          '25dea94e-192': {\n            id: '/src/components/common/EventDetailPanelWithContent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-193' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-172' },\n              { uid: '25dea94e-108' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-68' },\n            ],\n            importedBy: [{ uid: '25dea94e-194' }],\n          },\n          '25dea94e-194': {\n            id: '/src/components/calendarEvent/components/EventDetailPanel.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-195' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-192' },\n            ],\n            importedBy: [{ uid: '25dea94e-208' }, { uid: '25dea94e-274' }],\n          },\n          '25dea94e-196': {\n            id: '/src/components/calendarEvent/hooks/useClickOutside.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-197' },\n            imported: [{ uid: '25dea94e-327' }],\n            importedBy: [{ uid: '25dea94e-208' }],\n          },\n          '25dea94e-198': {\n            id: '/src/components/calendarEvent/hooks/useDetailPanelPosition.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-199' },\n            imported: [\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-172' },\n              { uid: '25dea94e-304' },\n            ],\n            importedBy: [{ uid: '25dea94e-208' }],\n          },\n          '25dea94e-200': {\n            id: '/src/components/calendarEvent/hooks/useEventActions.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-201' },\n            imported: [\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-172' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-22' },\n            ],\n            importedBy: [{ uid: '25dea94e-208' }],\n          },\n          '25dea94e-202': {\n            id: '/src/components/calendarEvent/hooks/useEventInteraction.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-203' },\n            imported: [{ uid: '25dea94e-327' }],\n            importedBy: [{ uid: '25dea94e-208' }],\n          },\n          '25dea94e-204': {\n            id: '/src/components/calendarEvent/hooks/useEventStyles.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-205' },\n            imported: [\n              { uid: '25dea94e-172' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-208' }],\n          },\n          '25dea94e-206': {\n            id: '/src/components/calendarEvent/hooks/useEventVisibility.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-207' },\n            imported: [\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-172' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-208' }],\n          },\n          '25dea94e-208': {\n            id: '/src/components/calendarEvent/CalendarEvent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-209' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-307' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-110' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-188' },\n              { uid: '25dea94e-194' },\n              { uid: '25dea94e-196' },\n              { uid: '25dea94e-198' },\n              { uid: '25dea94e-200' },\n              { uid: '25dea94e-202' },\n              { uid: '25dea94e-204' },\n              { uid: '25dea94e-206' },\n              { uid: '25dea94e-172' },\n            ],\n            importedBy: [{ uid: '25dea94e-308' }],\n          },\n          '25dea94e-210': {\n            id: '/src/components/common/TodayBox.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-211' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-96' },\n            ],\n            importedBy: [{ uid: '25dea94e-218' }, { uid: '25dea94e-212' }],\n          },\n          '25dea94e-212': {\n            id: '/src/components/common/ViewHeader.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-213' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-210' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-270' },\n              { uid: '25dea94e-274' },\n              { uid: '25dea94e-216' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-288' },\n              { uid: '25dea94e-292' },\n            ],\n          },\n          '25dea94e-214': {\n            id: '/src/views/utils/dragCreate.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-215' },\n            imported: [],\n            importedBy: [{ uid: '25dea94e-216' }, { uid: '25dea94e-254' }],\n          },\n          '25dea94e-216': {\n            id: '/src/components/dayView/DayContent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-217' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-308' },\n              { uid: '25dea94e-212' },\n              { uid: '25dea94e-307' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-214' },\n            ],\n            importedBy: [{ uid: '25dea94e-244' }],\n          },\n          '25dea94e-218': {\n            id: '/src/components/dayView/RightPanel.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-219' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-308' },\n              { uid: '25dea94e-130' },\n              { uid: '25dea94e-210' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-26' },\n            ],\n            importedBy: [{ uid: '25dea94e-244' }],\n          },\n          '25dea94e-220': {\n            id: '/src/components/eventLayout/constants.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-221' },\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-232' },\n              { uid: '25dea94e-224' },\n              { uid: '25dea94e-226' },\n              { uid: '25dea94e-230' },\n              { uid: '25dea94e-222' },\n            ],\n          },\n          '25dea94e-222': {\n            id: '/src/components/eventLayout/utils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-223' },\n            imported: [{ uid: '25dea94e-313' }, { uid: '25dea94e-220' }],\n            importedBy: [\n              { uid: '25dea94e-232' },\n              { uid: '25dea94e-224' },\n              { uid: '25dea94e-226' },\n              { uid: '25dea94e-230' },\n              { uid: '25dea94e-228' },\n            ],\n          },\n          '25dea94e-224': {\n            id: '/src/components/eventLayout/calculate/grouping.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-225' },\n            imported: [{ uid: '25dea94e-220' }, { uid: '25dea94e-222' }],\n            importedBy: [{ uid: '25dea94e-232' }],\n          },\n          '25dea94e-226': {\n            id: '/src/components/eventLayout/calculate/layout.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-227' },\n            imported: [{ uid: '25dea94e-220' }, { uid: '25dea94e-222' }],\n            importedBy: [{ uid: '25dea94e-232' }],\n          },\n          '25dea94e-228': {\n            id: '/src/components/eventLayout/calculate/rebalance.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-229' },\n            imported: [{ uid: '25dea94e-222' }],\n            importedBy: [{ uid: '25dea94e-230' }],\n          },\n          '25dea94e-230': {\n            id: '/src/components/eventLayout/calculate/structure.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-231' },\n            imported: [\n              { uid: '25dea94e-220' },\n              { uid: '25dea94e-222' },\n              { uid: '25dea94e-228' },\n            ],\n            importedBy: [{ uid: '25dea94e-232' }],\n          },\n          '25dea94e-232': {\n            id: '/src/components/eventLayout/index.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-233' },\n            imported: [\n              { uid: '25dea94e-224' },\n              { uid: '25dea94e-226' },\n              { uid: '25dea94e-230' },\n              { uid: '25dea94e-220' },\n              { uid: '25dea94e-222' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-244' },\n              { uid: '25dea94e-234' },\n              { uid: '25dea94e-236' },\n            ],\n          },\n          '25dea94e-234': {\n            id: '/src/components/dayView/util.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-235' },\n            imported: [\n              { uid: '25dea94e-232' },\n              { uid: '25dea94e-84' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-26' },\n            ],\n            importedBy: [{ uid: '25dea94e-244' }],\n          },\n          '25dea94e-236': {\n            id: '/src/components/weekView/util.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-237' },\n            imported: [\n              { uid: '25dea94e-232' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-84' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-26' },\n            ],\n            importedBy: [{ uid: '25dea94e-244' }, { uid: '25dea94e-260' }],\n          },\n          '25dea94e-238': {\n            id: '/src/core/config.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-239' },\n            imported: [{ uid: '25dea94e-20' }],\n            importedBy: [{ uid: '25dea94e-244' }, { uid: '25dea94e-260' }],\n          },\n          '25dea94e-240': {\n            id: '/src/hooks/useCalendarDrop.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-241' },\n            imported: [\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-311' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-244' },\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-270' },\n            ],\n          },\n          '25dea94e-242': {\n            id: '/src/plugins/dragBridge.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-243' },\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-244' },\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-270' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-288' },\n            ],\n          },\n          '25dea94e-244': {\n            id: '/src/views/DayView.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-245' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-216' },\n              { uid: '25dea94e-218' },\n              { uid: '25dea94e-234' },\n              { uid: '25dea94e-232' },\n              { uid: '25dea94e-236' },\n              { uid: '25dea94e-238' },\n              { uid: '25dea94e-240' },\n              { uid: '25dea94e-331' },\n              { uid: '25dea94e-242' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-248' }],\n          },\n          '25dea94e-246': {\n            id: '/src/factories/ViewAdapter.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-247' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-304' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-248' },\n              { uid: '25dea94e-262' },\n              { uid: '25dea94e-272' },\n              { uid: '25dea94e-276' },\n              { uid: '25dea94e-296' },\n            ],\n          },\n          '25dea94e-248': {\n            id: '/src/factories/createDayView.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-249' },\n            imported: [\n              { uid: '25dea94e-314' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-244' },\n              { uid: '25dea94e-246' },\n            ],\n            importedBy: [{ uid: '25dea94e-298' }],\n          },\n          '25dea94e-250': {\n            id: '/src/components/weekView/CompactHeader.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-251' },\n            imported: [{ uid: '25dea94e-62' }],\n            importedBy: [{ uid: '25dea94e-252' }],\n          },\n          '25dea94e-252': {\n            id: '/src/components/weekView/AllDayRow.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-253' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-308' },\n              { uid: '25dea94e-307' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-250' },\n            ],\n            importedBy: [{ uid: '25dea94e-260' }],\n          },\n          '25dea94e-254': {\n            id: '/src/components/weekView/TimeGrid.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-255' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-308' },\n              { uid: '25dea94e-307' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-214' },\n            ],\n            importedBy: [{ uid: '25dea94e-260' }],\n          },\n          '25dea94e-256': {\n            id: '/src/hooks/useWeekViewSwipe.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-257' },\n            imported: [{ uid: '25dea94e-327' }],\n            importedBy: [{ uid: '25dea94e-260' }],\n          },\n          '25dea94e-258': {\n            id: '/src/views/utils/weekView.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-259' },\n            imported: [{ uid: '25dea94e-311' }],\n            importedBy: [{ uid: '25dea94e-260' }],\n          },\n          '25dea94e-260': {\n            id: '/src/views/WeekView.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-261' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-212' },\n              { uid: '25dea94e-252' },\n              { uid: '25dea94e-254' },\n              { uid: '25dea94e-236' },\n              { uid: '25dea94e-238' },\n              { uid: '25dea94e-240' },\n              { uid: '25dea94e-256' },\n              { uid: '25dea94e-331' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-242' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-258' },\n            ],\n            importedBy: [{ uid: '25dea94e-262' }],\n          },\n          '25dea94e-262': {\n            id: '/src/factories/createWeekView.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-263' },\n            imported: [\n              { uid: '25dea94e-314' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-246' },\n            ],\n            importedBy: [{ uid: '25dea94e-298' }],\n          },\n          '25dea94e-264': {\n            id: '/src/components/monthView/WeekDayCell.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-265' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-308' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-174' },\n            ],\n            importedBy: [{ uid: '25dea94e-266' }],\n          },\n          '25dea94e-266': {\n            id: '/src/components/monthView/WeekComponent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-267' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-308' },\n              { uid: '25dea94e-307' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-264' },\n            ],\n            importedBy: [{ uid: '25dea94e-270' }],\n          },\n          '25dea94e-268': {\n            id: '/src/hooks/useDebouncedValue.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-269' },\n            imported: [{ uid: '25dea94e-327' }],\n            importedBy: [{ uid: '25dea94e-270' }],\n          },\n          '25dea94e-270': {\n            id: '/src/views/MonthView.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-271' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-212' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-240' },\n              { uid: '25dea94e-268' },\n              { uid: '25dea94e-331' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-242' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-272' }],\n          },\n          '25dea94e-272': {\n            id: '/src/factories/createMonthView.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-273' },\n            imported: [\n              { uid: '25dea94e-314' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-270' },\n              { uid: '25dea94e-246' },\n            ],\n            importedBy: [{ uid: '25dea94e-298' }],\n          },\n          '25dea94e-274': {\n            id: '/src/views/AgendaView.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-275' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-194' },\n              { uid: '25dea94e-212' },\n              { uid: '25dea94e-331' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-110' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-276' }],\n          },\n          '25dea94e-276': {\n            id: '/src/factories/createAgendaView.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-277' },\n            imported: [\n              { uid: '25dea94e-314' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-274' },\n              { uid: '25dea94e-246' },\n            ],\n            importedBy: [{ uid: '25dea94e-298' }],\n          },\n          '25dea94e-278': {\n            id: '/src/components/yearView/utils.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-279' },\n            imported: [{ uid: '25dea94e-305' }, { uid: '25dea94e-84' }],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-288' },\n              { uid: '25dea94e-282' },\n              { uid: '25dea94e-286' },\n            ],\n          },\n          '25dea94e-280': {\n            id: '/src/components/yearView/YearDayCell.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-281' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-306' },\n            ],\n            importedBy: [{ uid: '25dea94e-282' }],\n          },\n          '25dea94e-282': {\n            id: '/src/components/yearView/YearRowComponent.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-283' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-308' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-278' },\n              { uid: '25dea94e-280' },\n            ],\n            importedBy: [{ uid: '25dea94e-284' }],\n          },\n          '25dea94e-284': {\n            id: '/src/components/yearView/DefaultYearView.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-285' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-212' },\n              { uid: '25dea94e-307' },\n              { uid: '25dea94e-278' },\n              { uid: '25dea94e-282' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-242' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-294' }],\n          },\n          '25dea94e-286': {\n            id: '/src/components/yearView/FixedWeekMonthRow.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-287' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-308' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-278' },\n            ],\n            importedBy: [{ uid: '25dea94e-288' }],\n          },\n          '25dea94e-288': {\n            id: '/src/components/yearView/FixedWeekYearView.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-289' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-212' },\n              { uid: '25dea94e-307' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-242' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-286' },\n              { uid: '25dea94e-278' },\n            ],\n            importedBy: [{ uid: '25dea94e-294' }],\n          },\n          '25dea94e-290': {\n            id: '/src/components/yearView/GridDayPopup.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-291' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-309' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-305' },\n            ],\n            importedBy: [{ uid: '25dea94e-292' }],\n          },\n          '25dea94e-292': {\n            id: '/src/components/yearView/GridYearView.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-293' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-327' },\n              { uid: '25dea94e-212' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-290' },\n            ],\n            importedBy: [{ uid: '25dea94e-294' }],\n          },\n          '25dea94e-294': {\n            id: '/src/views/YearView.tsx',\n            moduleParts: { 'index.esm.js': '25dea94e-295' },\n            imported: [\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-288' },\n              { uid: '25dea94e-292' },\n            ],\n            importedBy: [{ uid: '25dea94e-296' }],\n          },\n          '25dea94e-296': {\n            id: '/src/factories/createYearView.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-297' },\n            imported: [\n              { uid: '25dea94e-314' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-294' },\n              { uid: '25dea94e-246' },\n            ],\n            importedBy: [{ uid: '25dea94e-298' }],\n          },\n          '25dea94e-298': {\n            id: '/src/factories/index.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-299' },\n            imported: [\n              { uid: '25dea94e-248' },\n              { uid: '25dea94e-262' },\n              { uid: '25dea94e-272' },\n              { uid: '25dea94e-276' },\n              { uid: '25dea94e-296' },\n            ],\n            importedBy: [{ uid: '25dea94e-302' }],\n          },\n          '25dea94e-300': {\n            id: '/src/plugins/eventsPlugin.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-301' },\n            imported: [\n              { uid: '25dea94e-310' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-14' },\n            ],\n            importedBy: [{ uid: '25dea94e-302' }],\n          },\n          '25dea94e-302': {\n            id: '/src/index.ts',\n            moduleParts: { 'index.esm.js': '25dea94e-303' },\n            imported: [\n              { uid: '25dea94e-60' },\n              { uid: '25dea94e-18' },\n              { uid: '25dea94e-160' },\n              { uid: '25dea94e-158' },\n              { uid: '25dea94e-304' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-162' },\n              { uid: '25dea94e-306' },\n              { uid: '25dea94e-298' },\n              { uid: '25dea94e-300' },\n              { uid: '25dea94e-242' },\n              { uid: '25dea94e-144' },\n              { uid: '25dea94e-307' },\n              { uid: '25dea94e-118' },\n              { uid: '25dea94e-114' },\n              { uid: '25dea94e-116' },\n              { uid: '25dea94e-122' },\n              { uid: '25dea94e-130' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-308' },\n              { uid: '25dea94e-232' },\n              { uid: '25dea94e-96' },\n              { uid: '25dea94e-94' },\n              { uid: '25dea94e-278' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-309' },\n            ],\n            importedBy: [],\n            isEntry: true,\n          },\n          '25dea94e-304': {\n            id: '/src/types/index.ts',\n            moduleParts: {},\n            imported: [\n              { uid: '25dea94e-6' },\n              { uid: '25dea94e-315' },\n              { uid: '25dea94e-316' },\n              { uid: '25dea94e-8' },\n              { uid: '25dea94e-317' },\n              { uid: '25dea94e-10' },\n              { uid: '25dea94e-318' },\n              { uid: '25dea94e-319' },\n              { uid: '25dea94e-320' },\n              { uid: '25dea94e-321' },\n              { uid: '25dea94e-322' },\n              { uid: '25dea94e-323' },\n              { uid: '25dea94e-12' },\n              { uid: '25dea94e-324' },\n              { uid: '25dea94e-325' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-60' },\n              { uid: '25dea94e-54' },\n              { uid: '25dea94e-248' },\n              { uid: '25dea94e-262' },\n              { uid: '25dea94e-272' },\n              { uid: '25dea94e-276' },\n              { uid: '25dea94e-296' },\n              { uid: '25dea94e-166' },\n              { uid: '25dea94e-172' },\n              { uid: '25dea94e-208' },\n              { uid: '25dea94e-106' },\n              { uid: '25dea94e-244' },\n              { uid: '25dea94e-246' },\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-270' },\n              { uid: '25dea94e-188' },\n              { uid: '25dea94e-198' },\n              { uid: '25dea94e-200' },\n              { uid: '25dea94e-204' },\n              { uid: '25dea94e-206' },\n              { uid: '25dea94e-216' },\n              { uid: '25dea94e-218' },\n              { uid: '25dea94e-252' },\n              { uid: '25dea94e-254' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-288' },\n              { uid: '25dea94e-292' },\n              { uid: '25dea94e-264' },\n              { uid: '25dea94e-282' },\n              { uid: '25dea94e-286' },\n            ],\n          },\n          '25dea94e-305': {\n            id: '/src/utils/index.ts',\n            moduleParts: {},\n            imported: [\n              { uid: '25dea94e-313' },\n              { uid: '25dea94e-16' },\n              { uid: '25dea94e-64' },\n              { uid: '25dea94e-26' },\n              { uid: '25dea94e-14' },\n              { uid: '25dea94e-66' },\n              { uid: '25dea94e-28' },\n              { uid: '25dea94e-68' },\n              { uid: '25dea94e-70' },\n              { uid: '25dea94e-36' },\n              { uid: '25dea94e-72' },\n              { uid: '25dea94e-74' },\n              { uid: '25dea94e-82' },\n              { uid: '25dea94e-84' },\n              { uid: '25dea94e-86' },\n              { uid: '25dea94e-24' },\n              { uid: '25dea94e-312' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-300' },\n              { uid: '25dea94e-130' },\n              { uid: '25dea94e-278' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-166' },\n              { uid: '25dea94e-208' },\n              { uid: '25dea94e-244' },\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-270' },\n              { uid: '25dea94e-274' },\n              { uid: '25dea94e-200' },\n              { uid: '25dea94e-204' },\n              { uid: '25dea94e-206' },\n              { uid: '25dea94e-136' },\n              { uid: '25dea94e-216' },\n              { uid: '25dea94e-236' },\n              { uid: '25dea94e-240' },\n              { uid: '25dea94e-252' },\n              { uid: '25dea94e-254' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-288' },\n              { uid: '25dea94e-292' },\n              { uid: '25dea94e-176' },\n              { uid: '25dea94e-182' },\n              { uid: '25dea94e-184' },\n              { uid: '25dea94e-186' },\n              { uid: '25dea94e-88' },\n              { uid: '25dea94e-264' },\n              { uid: '25dea94e-282' },\n              { uid: '25dea94e-286' },\n              { uid: '25dea94e-290' },\n            ],\n          },\n          '25dea94e-306': {\n            id: '/src/locale/index.ts',\n            moduleParts: {},\n            imported: [\n              { uid: '25dea94e-326' },\n              { uid: '25dea94e-100' },\n              { uid: '25dea94e-98' },\n              { uid: '25dea94e-2' },\n              { uid: '25dea94e-4' },\n              { uid: '25dea94e-90' },\n              { uid: '25dea94e-92' },\n              { uid: '25dea94e-102' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-130' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-168' },\n              { uid: '25dea94e-170' },\n              { uid: '25dea94e-128' },\n              { uid: '25dea94e-140' },\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-270' },\n              { uid: '25dea94e-274' },\n              { uid: '25dea94e-104' },\n              { uid: '25dea94e-136' },\n              { uid: '25dea94e-216' },\n              { uid: '25dea94e-218' },\n              { uid: '25dea94e-240' },\n              { uid: '25dea94e-212' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-288' },\n              { uid: '25dea94e-292' },\n              { uid: '25dea94e-210' },\n              { uid: '25dea94e-290' },\n              { uid: '25dea94e-280' },\n            ],\n          },\n          '25dea94e-307': {\n            id: '/src/components/contextMenu/index.tsx',\n            moduleParts: {},\n            imported: [\n              { uid: '25dea94e-164' },\n              { uid: '25dea94e-168' },\n              { uid: '25dea94e-170' },\n              { uid: '25dea94e-166' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-208' },\n              { uid: '25dea94e-216' },\n              { uid: '25dea94e-252' },\n              { uid: '25dea94e-254' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-288' },\n            ],\n          },\n          '25dea94e-308': {\n            id: '/src/components/calendarEvent/index.tsx',\n            moduleParts: {},\n            imported: [{ uid: '25dea94e-208' }],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-216' },\n              { uid: '25dea94e-218' },\n              { uid: '25dea94e-252' },\n              { uid: '25dea94e-254' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-264' },\n              { uid: '25dea94e-282' },\n              { uid: '25dea94e-286' },\n            ],\n          },\n          '25dea94e-309': {\n            id: 'preact/compat',\n            moduleParts: {},\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-302' },\n              { uid: '25dea94e-122' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-156' },\n              { uid: '25dea94e-164' },\n              { uid: '25dea94e-124' },\n              { uid: '25dea94e-208' },\n              { uid: '25dea94e-128' },\n              { uid: '25dea94e-140' },\n              { uid: '25dea94e-136' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-176' },\n              { uid: '25dea94e-192' },\n              { uid: '25dea94e-282' },\n              { uid: '25dea94e-286' },\n              { uid: '25dea94e-290' },\n              { uid: '25dea94e-280' },\n            ],\n            isExternal: true,\n          },\n          '25dea94e-310': {\n            id: 'tslib',\n            moduleParts: {},\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-60' },\n              { uid: '25dea94e-162' },\n              { uid: '25dea94e-300' },\n              { uid: '25dea94e-118' },\n              { uid: '25dea94e-114' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-52' },\n              { uid: '25dea94e-156' },\n              { uid: '25dea94e-82' },\n              { uid: '25dea94e-168' },\n              { uid: '25dea94e-170' },\n              { uid: '25dea94e-166' },\n              { uid: '25dea94e-148' },\n              { uid: '25dea94e-154' },\n            ],\n            isExternal: true,\n          },\n          '25dea94e-311': {\n            id: 'temporal-polyfill',\n            moduleParts: {},\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-60' },\n              { uid: '25dea94e-122' },\n              { uid: '25dea94e-130' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-54' },\n              { uid: '25dea94e-16' },\n              { uid: '25dea94e-26' },\n              { uid: '25dea94e-14' },\n              { uid: '25dea94e-28' },\n              { uid: '25dea94e-70' },\n              { uid: '25dea94e-36' },\n              { uid: '25dea94e-38' },\n              { uid: '25dea94e-76' },\n              { uid: '25dea94e-240' },\n              { uid: '25dea94e-258' },\n            ],\n            isExternal: true,\n          },\n          '25dea94e-312': {\n            id: '/src/utils/calendarApp/index.ts',\n            moduleParts: {},\n            imported: [\n              { uid: '25dea94e-44' },\n              { uid: '25dea94e-46' },\n              { uid: '25dea94e-48' },\n            ],\n            importedBy: [{ uid: '25dea94e-60' }, { uid: '25dea94e-305' }],\n          },\n          '25dea94e-313': {\n            id: '/src/utils/helpers.ts',\n            moduleParts: {},\n            imported: [\n              { uid: '25dea94e-16' },\n              { uid: '25dea94e-20' },\n              { uid: '25dea94e-28' },\n              { uid: '25dea94e-30' },\n              { uid: '25dea94e-32' },\n              { uid: '25dea94e-34' },\n              { uid: '25dea94e-36' },\n              { uid: '25dea94e-38' },\n              { uid: '25dea94e-40' },\n              { uid: '25dea94e-42' },\n            ],\n            importedBy: [\n              { uid: '25dea94e-60' },\n              { uid: '25dea94e-305' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-174' },\n              { uid: '25dea94e-222' },\n              { uid: '25dea94e-44' },\n              { uid: '25dea94e-128' },\n              { uid: '25dea94e-150' },\n              { uid: '25dea94e-234' },\n              { uid: '25dea94e-236' },\n              { uid: '25dea94e-264' },\n            ],\n          },\n          '25dea94e-314': {\n            id: 'preact',\n            moduleParts: {},\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-160' },\n              { uid: '25dea94e-122' },\n              { uid: '25dea94e-156' },\n              { uid: '25dea94e-110' },\n              { uid: '25dea94e-90' },\n              { uid: '25dea94e-248' },\n              { uid: '25dea94e-262' },\n              { uid: '25dea94e-272' },\n              { uid: '25dea94e-276' },\n              { uid: '25dea94e-296' },\n              { uid: '25dea94e-164' },\n              { uid: '25dea94e-62' },\n              { uid: '25dea94e-108' },\n            ],\n            isExternal: true,\n          },\n          '25dea94e-315': {\n            id: '/src/types/calendar.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-316': {\n            id: '/src/types/event.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-317': {\n            id: '/src/types/dragIndicator.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-318': {\n            id: '/src/types/factory.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-319': {\n            id: '/src/types/plugin.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-320': {\n            id: '/src/types/config.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-321': {\n            id: '/src/types/hook.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-322': {\n            id: '/src/types/eventDetail.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-323': {\n            id: '/src/types/mobileEvent.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-324': {\n            id: '/src/types/calendarTypes.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-325': {\n            id: '/src/types/search.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-304' }],\n          },\n          '25dea94e-326': {\n            id: '/src/locale/types.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-306' }],\n          },\n          '25dea94e-327': {\n            id: 'preact/hooks',\n            moduleParts: {},\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-118' },\n              { uid: '25dea94e-114' },\n              { uid: '25dea94e-116' },\n              { uid: '25dea94e-122' },\n              { uid: '25dea94e-130' },\n              { uid: '25dea94e-120' },\n              { uid: '25dea94e-190' },\n              { uid: '25dea94e-126' },\n              { uid: '25dea94e-112' },\n              { uid: '25dea94e-156' },\n              { uid: '25dea94e-92' },\n              { uid: '25dea94e-102' },\n              { uid: '25dea94e-164' },\n              { uid: '25dea94e-108' },\n              { uid: '25dea94e-124' },\n              { uid: '25dea94e-208' },\n              { uid: '25dea94e-106' },\n              { uid: '25dea94e-128' },\n              { uid: '25dea94e-140' },\n              { uid: '25dea94e-146' },\n              { uid: '25dea94e-148' },\n              { uid: '25dea94e-150' },\n              { uid: '25dea94e-152' },\n              { uid: '25dea94e-154' },\n              { uid: '25dea94e-244' },\n              { uid: '25dea94e-246' },\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-270' },\n              { uid: '25dea94e-274' },\n              { uid: '25dea94e-196' },\n              { uid: '25dea94e-198' },\n              { uid: '25dea94e-200' },\n              { uid: '25dea94e-202' },\n              { uid: '25dea94e-206' },\n              { uid: '25dea94e-104' },\n              { uid: '25dea94e-136' },\n              { uid: '25dea94e-134' },\n              { uid: '25dea94e-138' },\n              { uid: '25dea94e-216' },\n              { uid: '25dea94e-240' },\n              { uid: '25dea94e-252' },\n              { uid: '25dea94e-254' },\n              { uid: '25dea94e-256' },\n              { uid: '25dea94e-266' },\n              { uid: '25dea94e-268' },\n              { uid: '25dea94e-284' },\n              { uid: '25dea94e-288' },\n              { uid: '25dea94e-292' },\n              { uid: '25dea94e-176' },\n              { uid: '25dea94e-332' },\n              { uid: '25dea94e-88' },\n              { uid: '25dea94e-264' },\n              { uid: '25dea94e-282' },\n              { uid: '25dea94e-286' },\n              { uid: '25dea94e-290' },\n            ],\n            isExternal: true,\n          },\n          '25dea94e-328': {\n            id: '@dayflow/blossom-color-picker',\n            moduleParts: {},\n            imported: [],\n            importedBy: [\n              { uid: '25dea94e-114' },\n              { uid: '25dea94e-116' },\n              { uid: '25dea94e-120' },\n            ],\n            isExternal: true,\n          },\n          '25dea94e-329': {\n            id: '/src/components/mobileEventDrawer/index.ts',\n            moduleParts: {},\n            imported: [\n              { uid: '25dea94e-136' },\n              { uid: '25dea94e-132' },\n              { uid: '25dea94e-134' },\n            ],\n            importedBy: [{ uid: '25dea94e-156' }],\n          },\n          '25dea94e-330': {\n            id: '/src/utils/ics/types.ts',\n            moduleParts: {},\n            imported: [],\n            importedBy: [{ uid: '25dea94e-82' }],\n          },\n          '25dea94e-331': {\n            id: '/src/hooks/virtualScroll/index.ts',\n            moduleParts: {},\n            imported: [{ uid: '25dea94e-332' }, { uid: '25dea94e-88' }],\n            importedBy: [\n              { uid: '25dea94e-106' },\n              { uid: '25dea94e-244' },\n              { uid: '25dea94e-260' },\n              { uid: '25dea94e-270' },\n              { uid: '25dea94e-274' },\n            ],\n          },\n          '25dea94e-332': {\n            id: '/src/hooks/virtualScroll/useVirtualScroll.ts',\n            moduleParts: {},\n            imported: [{ uid: '25dea94e-327' }],\n            importedBy: [{ uid: '25dea94e-331' }],\n          },\n        },\n        env: { rollup: '4.57.1' },\n        options: { gzip: true, brotli: true, sourcemap: false },\n      };\n\n      const run = () => {\n        const width = window.innerWidth;\n        const height = window.innerHeight;\n\n        const chartNode = document.querySelector('main');\n        drawChart.default(chartNode, data, width, height);\n      };\n\n      window.addEventListener('resize', run);\n\n      document.addEventListener('DOMContentLoaded', run);\n      /*-->*/\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/core/jest.config.mjs",
    "content": "export default {\n  preset: 'ts-jest',\n  testEnvironment: 'jsdom',\n  roots: ['<rootDir>/src'],\n  testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],\n  moduleNameMapper: {\n    '^@/(.*)$': '<rootDir>/src/$1',\n    '\\\\.(css|less|scss|sass)$': 'identity-obj-proxy',\n    '^preact$': '<rootDir>/node_modules/preact/dist/preact.js',\n    '^preact/hooks$': '<rootDir>/node_modules/preact/hooks/dist/hooks.js',\n    '^preact/jsx-runtime$':\n      '<rootDir>/node_modules/preact/jsx-runtime/dist/jsxRuntime.js',\n    '^preact/compat$': '<rootDir>/node_modules/preact/compat/dist/compat.js',\n    '^preact/test-utils$':\n      '<rootDir>/node_modules/preact/test-utils/dist/testUtils.js',\n    '^@testing-library/preact$':\n      '<rootDir>/node_modules/@testing-library/preact/dist/cjs/index.js',\n    '^@dayflow/ui-context-menu$': '<rootDir>/../ui/context-menu/src/index.ts',\n    '^@dayflow/ui-range-picker$': '<rootDir>/../ui/range-picker/src/index.ts',\n    '^@ui-range-picker/(.*)$': '<rootDir>/../ui/range-picker/src/$1',\n  },\n  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],\n  collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/index.ts'],\n  coverageThreshold: {\n    global: {\n      lines: 50,\n      branches: 50,\n      functions: 50,\n      statements: 50,\n    },\n  },\n};\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n  \"name\": \"@dayflow/core\",\n  \"version\": \"3.6.2\",\n  \"description\": \"A flexible and feature-rich calendar engine powered by Preact with drag-and-drop support, multiple views (Day, Week, Month, Year), and plugin architecture\",\n  \"keywords\": [\n    \"calendar\",\n    \"day-view\",\n    \"drag-drop\",\n    \"event-calendar\",\n    \"events\",\n    \"month-view\",\n    \"plugin-architecture\",\n    \"preact\",\n    \"schedule\",\n    \"typescript\",\n    \"virtual-scroll\",\n    \"week-view\",\n    \"year-view\"\n  ],\n  \"homepage\": \"https://github.com/dayflow-js/dayflow#readme\",\n  \"bugs\": {\n    \"url\": \"https://github.com/dayflow-js/dayflow/issues\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"Jayce Li\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/dayflow-js/dayflow.git\"\n  },\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"sideEffects\": [\n    \"*.css\",\n    \"**/*.css\"\n  ],\n  \"main\": \"dist/index.esm.js\",\n  \"module\": \"dist/index.esm.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.esm.js\"\n    },\n    \"./dist/styles.css\": \"./dist/styles.css\",\n    \"./dist/styles.components.css\": \"./dist/styles.components.css\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.build.json && rollup -c --bundleConfigAsCjs && pnpm run build:css && pnpm run check:css\",\n    \"build:css\": \"node ./scripts/build-css.mjs\",\n    \"check:css\": \"pnpm run check:css:source && pnpm run check:css:dist\",\n    \"check:css:dist\": \"node ./scripts/check-dist-styling.mjs --package-root .\",\n    \"check:css:source\": \"node ./scripts/check-semantic-css.mjs\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"dev\": \"vite --port 5529 --host\",\n    \"postbuild\": \"rimraf dist/types\",\n    \"prebuild\": \"rimraf dist\",\n    \"prepublishOnly\": \"npm run typecheck && npm run check:css && npm run test && npm run build\",\n    \"test\": \"jest\",\n    \"test:coverage\": \"jest --coverage\",\n    \"test:watch\": \"jest --watch\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@dayflow/blossom-color-picker\": \"catalog:\",\n    \"@dayflow/ui-context-menu\": \"workspace:*\",\n    \"@dayflow/ui-range-picker\": \"workspace:*\",\n    \"preact\": \"catalog:\",\n    \"temporal-polyfill\": \"catalog:\",\n    \"tslib\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@preact/preset-vite\": \"catalog:\",\n    \"@rollup/plugin-commonjs\": \"catalog:\",\n    \"@rollup/plugin-node-resolve\": \"catalog:\",\n    \"@rollup/plugin-terser\": \"catalog:\",\n    \"@rollup/plugin-typescript\": \"catalog:\",\n    \"@tailwindcss/postcss\": \"catalog:\",\n    \"@testing-library/jest-dom\": \"catalog:\",\n    \"@testing-library/preact\": \"catalog:\",\n    \"@testing-library/user-event\": \"catalog:\",\n    \"@types/jest\": \"catalog:\",\n    \"@types/lodash\": \"catalog:\",\n    \"@types/node\": \"catalog:\",\n    \"autoprefixer\": \"catalog:\",\n    \"cssnano\": \"catalog:\",\n    \"identity-obj-proxy\": \"^3.0.0\",\n    \"jest\": \"catalog:\",\n    \"jest-environment-jsdom\": \"catalog:\",\n    \"postcss\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-dts\": \"catalog:\",\n    \"rollup-plugin-peer-deps-external\": \"catalog:\",\n    \"rollup-plugin-postcss\": \"catalog:\",\n    \"rollup-plugin-visualizer\": \"catalog:\",\n    \"tailwindcss\": \"catalog:\",\n    \"ts-jest\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vite\": \"catalog:\"\n  }\n}\n"
  },
  {
    "path": "packages/core/postcss.build.mjs",
    "content": "export default {\n  plugins: {\n    '@tailwindcss/postcss': {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "packages/core/rollup.config.js",
    "content": "import path from 'node:path';\n\nimport resolve from '@rollup/plugin-node-resolve';\nimport terser from '@rollup/plugin-terser';\nimport typescript from '@rollup/plugin-typescript';\nimport { dts } from 'rollup-plugin-dts';\nimport peerDepsExternal from 'rollup-plugin-peer-deps-external';\nimport { visualizer } from 'rollup-plugin-visualizer';\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: [\n      {\n        file: 'dist/index.esm.js',\n        format: 'esm',\n        sourcemap: false,\n        exports: 'named',\n      },\n    ],\n    plugins: [\n      peerDepsExternal(),\n      resolve({\n        browser: true,\n        extensions: ['.js', '.jsx', '.ts', '.tsx'],\n        alias: {\n          '@': path.resolve('./src'),\n        },\n      }),\n      typescript({\n        tsconfig: './tsconfig.build.json',\n        declaration: false,\n        exclude: [\n          'src/app/**',\n          '**/*.test.ts',\n          '**/*.test.tsx',\n          '**/*.spec.ts',\n          '**/*.spec.tsx',\n        ],\n      }),\n      terser({ compress: { passes: 2, drop_console: true } }),\n      visualizer({\n        filename: 'bundle-analysis.html',\n        open: false,\n        gzipSize: true,\n        brotliSize: true,\n        template: 'treemap',\n      }),\n    ],\n    external: [\n      'preact',\n      'preact/hooks',\n      'preact/compat',\n      'temporal-polyfill',\n      'tslib',\n      '@dayflow/blossom-color-picker',\n    ],\n  },\n  {\n    input: 'dist/types/index.d.ts',\n    output: [{ file: 'dist/index.d.ts', format: 'es' }],\n    plugins: [\n      dts({\n        compilerOptions: {\n          baseUrl: '.',\n          paths: {\n            '@/*': ['./dist/types/*'],\n          },\n        },\n      }),\n    ],\n    external: [/\\.css$/],\n  },\n];\n"
  },
  {
    "path": "packages/core/scripts/atomic-css-baseline.json",
    "content": "{\n  \"source\": {},\n  \"distJs\": {}\n}\n"
  },
  {
    "path": "packages/core/scripts/atomic-css-guard-utils.mjs",
    "content": "import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport const coreRoot = path.resolve(__dirname, '..');\nexport const workspaceRoot = path.resolve(coreRoot, '..', '..');\nexport const baselineFile = path.join(__dirname, 'atomic-css-baseline.json');\n\nexport const SOURCE_SCAN_ROOTS = [\n  { root: 'packages/core/src' },\n  { root: 'packages/plugins', requireSegment: `${path.sep}src${path.sep}` },\n  { root: 'packages/resource-grid/src' },\n  { root: 'packages/ui', requireSegment: `${path.sep}src${path.sep}` },\n];\n\nexport const FORBIDDEN_TOP_LEVEL_UTILITY_SELECTORS = [\n  String.raw`^\\.flex-col\\s*\\{`,\n  String.raw`^\\.flex-row\\s*\\{`,\n  String.raw`^\\.grid\\s*\\{`,\n  String.raw`^\\.md\\\\:flex-row\\s*\\{`,\n  String.raw`^\\.bg-background\\s*\\{`,\n  String.raw`^\\.bg-primary\\s*\\{`,\n  String.raw`^\\.text-primary\\s*\\{`,\n  String.raw`^\\.bg-secondary\\s*\\{`,\n  String.raw`^\\.border-border\\s*\\{`,\n  String.raw`^\\.ring-primary\\s*\\{`,\n];\n\nconst SOURCE_FILE_PATTERN = /\\.(ts|tsx|js|jsx|mjs|cjs)$/;\nconst DIST_JS_PATTERN = /\\.(js|mjs|cjs)$/;\nconst EXCLUDED_SOURCE_PATTERNS = [\n  /(?:^|\\/)__tests__(?:\\/|$)/,\n  /\\.test\\.[jt]sx?$/,\n  /\\.spec\\.[jt]sx?$/,\n  /\\.d\\.ts$/,\n];\n\nconst RESPONSIVE_PREFIXES = new Set(['sm', 'md', 'lg', 'xl', '2xl']);\nconst STATE_PREFIXES = new Set([\n  'dark',\n  'hover',\n  'focus',\n  'focus-visible',\n  'focus-within',\n  'active',\n  'disabled',\n  'group-hover',\n  'group-focus',\n  'motion-safe',\n  'motion-reduce',\n  'aria-selected',\n  'aria-expanded',\n  'data-[state=open]',\n]);\n\nconst EXACT_FORBIDDEN_TOKENS = new Set([\n  'aspect-square',\n  'contents',\n  'cursor-pointer',\n  'cursor-default',\n  'overflow-auto',\n  'overflow-hidden',\n  'overflow-visible',\n  'select-none',\n  'shrink-0',\n  'snap-center',\n  'snap-mandatory',\n  'snap-y',\n  'uppercase',\n  'w-full',\n  'whitespace-nowrap',\n]);\n\nconst FORBIDDEN_PREFIXES = [\n  'animate-',\n  'aspect-',\n  'backdrop-',\n  'bg-',\n  'border-',\n  'bottom-',\n  'break-',\n  'content-',\n  'cursor-',\n  'duration-',\n  'ease-',\n  'fill-',\n  'flex-',\n  'font-',\n  'gap-',\n  'grid-',\n  'h-',\n  'inset-',\n  'items-',\n  'justify-',\n  'leading-',\n  'left-',\n  'line-clamp-',\n  'm-',\n  'mb-',\n  'ml-',\n  'mr-',\n  'mt-',\n  'mx-',\n  'my-',\n  'max-h-',\n  'max-w-',\n  'min-h-',\n  'min-w-',\n  'object-',\n  'opacity-',\n  'origin-',\n  'outline-',\n  'overflow-',\n  'overscroll-',\n  'p-',\n  'pb-',\n  'pl-',\n  'pr-',\n  'pt-',\n  'px-',\n  'py-',\n  'right-',\n  'ring-',\n  'rotate-',\n  'rounded',\n  'scale-',\n  'shadow',\n  'shrink-',\n  'size-',\n  'snap-',\n  'space-x-',\n  'space-y-',\n  'stroke-',\n  'text-',\n  'top-',\n  'tracking-',\n  'transition',\n  'translate-',\n  'w-',\n  'will-change-',\n  'z-',\n];\n\nfunction collectFiles(dirPath, filterPattern) {\n  if (!fs.existsSync(dirPath)) {\n    return [];\n  }\n\n  return fs.readdirSync(dirPath, { withFileTypes: true }).flatMap(entry => {\n    const entryPath = path.join(dirPath, entry.name);\n    if (entry.isDirectory()) {\n      return collectFiles(entryPath, filterPattern);\n    }\n\n    if (!filterPattern.test(entry.name)) {\n      return [];\n    }\n\n    return [entryPath];\n  });\n}\n\nexport function isDayFlowSelector(selector) {\n  return (\n    selector.startsWith('.df-') ||\n    selector.startsWith('.bcp-') ||\n    selector.startsWith('.dark ')\n  );\n}\n\nfunction stripComments(source) {\n  return source\n    .replaceAll(/\\/\\*[\\s\\S]*?\\*\\//g, ' ')\n    .replaceAll(/(^|[^:])\\/\\/.*$/gm, '$1');\n}\n\nfunction shouldSkipSourceFile(filePath) {\n  return EXCLUDED_SOURCE_PATTERNS.some(pattern => pattern.test(filePath));\n}\n\nfunction normalizeToken(token) {\n  return token\n    .trim()\n    .replaceAll(/^[,;()[\\]{}]+/g, '')\n    .replaceAll(/[,;()[\\]{}]+$/g, '');\n}\n\nfunction isAllowedToken(token) {\n  return token.startsWith('df-') || token.startsWith('bcp-');\n}\n\nfunction hasForbiddenVariant(token) {\n  if (!token.includes(':')) {\n    return false;\n  }\n  const [prefix, suffix] = token.split(/:(.*)/s, 2);\n  if (!suffix) {\n    return false;\n  }\n  return RESPONSIVE_PREFIXES.has(prefix) || STATE_PREFIXES.has(prefix);\n}\n\nconst ALLOWED_CSS_VALUES = new Set([\n  'ease-in',\n  'ease-out',\n  'ease-in-out',\n  'select-all',\n  'pointer-events-auto',\n  'pointer-events-none',\n  'stroke-width',\n  'stroke-linecap',\n  'stroke-linejoin',\n]);\n\nfunction hasForbiddenPrefix(token) {\n  if (ALLOWED_CSS_VALUES.has(token)) {\n    return false;\n  }\n  return FORBIDDEN_PREFIXES.some(\n    prefix => token.startsWith(prefix) && token.length > prefix.length\n  );\n}\n\nfunction isArbitraryUtility(token) {\n  // Only flag if it looks like a Tailwind arbitrary value/variant\n  // e.g., bg-[#fff], w-[100px], [&_p]:mt-4\n  if (!/[[()].*[\\])]/.test(token)) {\n    return false;\n  }\n\n  // Must have a prefix followed by [ or (\n  // OR start with [&\n  return (\n    /^[a-z0-9-]+[[()]/.test(token) ||\n    token.startsWith('[&') ||\n    token.startsWith('@')\n  );\n}\n\nfunction looksLikeForbiddenClassToken(token) {\n  if (!token || isAllowedToken(token)) {\n    return false;\n  }\n\n  if (hasForbiddenVariant(token)) {\n    return true;\n  }\n\n  if (EXACT_FORBIDDEN_TOKENS.has(token)) {\n    return true;\n  }\n\n  if (hasForbiddenPrefix(token)) {\n    return true;\n  }\n\n  return isArbitraryUtility(token);\n}\n\nfunction extractTokensFromText(text) {\n  return text\n    .replaceAll(/\\$\\{[\\s\\S]*?\\}/g, ' ')\n    .split(/\\s+/)\n    .map(normalizeToken)\n    .filter(Boolean)\n    .filter(looksLikeForbiddenClassToken);\n}\n\nfunction lineForIndex(source, index) {\n  return source.slice(0, index).split('\\n').length;\n}\n\nfunction collectForbiddenTokens({\n  filePath,\n  source,\n  literalContent,\n  snippet,\n  index,\n}) {\n  const tokens = extractTokensFromText(literalContent);\n  if (tokens.length === 0) {\n    return [];\n  }\n\n  const line = lineForIndex(source, index);\n  return tokens.map(token => ({\n    file: path.relative(workspaceRoot, filePath),\n    line,\n    token,\n    snippet: snippet.slice(0, 120),\n  }));\n}\n\nfunction scanStringLiterals(filePath, content) {\n  const stripped = stripComments(content);\n  const matches = [];\n  const stringLiteralPattern = /([\"'`])((?:\\\\.|(?!\\1)[\\s\\S])*?)\\1/g;\n\n  for (const match of stripped.matchAll(stringLiteralPattern)) {\n    const [fullMatch, quote, inner] = match;\n    if (!inner) continue;\n\n    matches.push(\n      ...collectForbiddenTokens({\n        filePath,\n        source: stripped,\n        literalContent: inner,\n        snippet: fullMatch,\n        index: match.index ?? 0,\n      })\n    );\n\n    if (quote === '`') {\n      const expressionPattern = /\\$\\{([\\s\\S]*?)\\}/g;\n      for (const expressionMatch of inner.matchAll(expressionPattern)) {\n        const expression = expressionMatch[1];\n        if (!expression) continue;\n\n        for (const nestedMatch of expression.matchAll(stringLiteralPattern)) {\n          const [nestedFullMatch, , nestedInner] = nestedMatch;\n          if (!nestedInner) continue;\n\n          const nestedIndex =\n            (match.index ?? 0) +\n            (expressionMatch.index ?? 0) +\n            2 +\n            (nestedMatch.index ?? 0);\n\n          matches.push(\n            ...collectForbiddenTokens({\n              filePath,\n              source: stripped,\n              literalContent: nestedInner,\n              snippet: nestedFullMatch,\n              index: nestedIndex,\n            })\n          );\n        }\n      }\n    }\n  }\n\n  return matches;\n}\n\nexport function scanSourceViolations() {\n  const files = SOURCE_SCAN_ROOTS.flatMap(target =>\n    collectFiles(\n      path.join(workspaceRoot, target.root),\n      SOURCE_FILE_PATTERN\n    ).filter(filePath =>\n      target.requireSegment ? filePath.includes(target.requireSegment) : true\n    )\n  ).filter(filePath => !shouldSkipSourceFile(filePath));\n\n  return files.flatMap(filePath =>\n    scanStringLiterals(filePath, fs.readFileSync(filePath, 'utf8'))\n  );\n}\n\nexport function scanDistJsViolations(packageRoot) {\n  const root = path.resolve(packageRoot);\n  const distRoot = path.join(root, 'dist');\n  const files = collectFiles(distRoot, DIST_JS_PATTERN).filter(\n    filePath =>\n      !filePath.endsWith('.d.ts') &&\n      !filePath.includes(`${path.sep}dist${path.sep}build${path.sep}`)\n  );\n\n  return files.flatMap(filePath =>\n    scanStringLiterals(filePath, fs.readFileSync(filePath, 'utf8')).map(\n      violation => ({\n        ...violation,\n        file: path.relative(workspaceRoot, filePath),\n      })\n    )\n  );\n}\n\nexport function summarizeViolationsByFile(violations) {\n  const summary = {};\n  for (const violation of violations) {\n    summary[violation.file] = (summary[violation.file] ?? 0) + 1;\n  }\n  return summary;\n}\n\nexport function loadBaseline() {\n  if (!fs.existsSync(baselineFile)) {\n    return { source: {}, distJs: {} };\n  }\n\n  return JSON.parse(fs.readFileSync(baselineFile, 'utf8'));\n}\n\nexport function writeBaseline(baseline) {\n  fs.writeFileSync(baselineFile, `${JSON.stringify(baseline, null, 2)}\\n`);\n}\n\nexport function diffAgainstBaseline(current, baselineSection) {\n  return Object.entries(current)\n    .map(([file, count]) => ({\n      file,\n      count,\n      baseline: baselineSection[file] ?? 0,\n    }))\n    .filter(entry => entry.count > entry.baseline)\n    .toSorted((left, right) => left.file.localeCompare(right.file));\n}\n\nexport function parseArgs(argv) {\n  const args = new Map();\n\n  for (let i = 0; i < argv.length; i += 1) {\n    const arg = argv[i];\n    if (!arg.startsWith('--')) continue;\n\n    const [key, inlineValue] = arg.split('=');\n    if (inlineValue !== undefined) {\n      args.set(key, inlineValue);\n      continue;\n    }\n\n    const next = argv[i + 1];\n    if (!next || next.startsWith('--')) {\n      args.set(key, true);\n      continue;\n    }\n\n    args.set(key, next);\n    i += 1;\n  }\n\n  return args;\n}\n"
  },
  {
    "path": "packages/core/scripts/build-css.mjs",
    "content": "import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport tailwindcss from '@tailwindcss/postcss';\nimport autoprefixer from 'autoprefixer';\nimport cssnano from 'cssnano';\nimport postcss, { parse } from 'postcss';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst root = path.resolve(__dirname, '..');\n\nfunction stripCascadeLayers(css) {\n  const rootNode = parse(css);\n\n  rootNode.walkAtRules('layer', atRule => {\n    if (!atRule.nodes?.length) {\n      atRule.remove();\n      return;\n    }\n\n    atRule.replaceWith(...atRule.nodes);\n  });\n\n  return rootNode.toString();\n}\n\nasync function buildCss(inputFile, outputFile) {\n  const input = path.join(root, inputFile);\n  const output = path.join(root, outputFile);\n\n  const css = await fs.readFile(input, 'utf8');\n\n  const result = await postcss([\n    tailwindcss,\n    autoprefixer,\n    cssnano({ preset: ['default', { uniqueSelectors: false }] }),\n  ]).process(css, {\n    from: input,\n    to: output,\n  });\n\n  const builtCss =\n    outputFile === 'dist/styles.css'\n      ? stripCascadeLayers(result.css)\n      : result.css;\n\n  await fs.writeFile(output, builtCss);\n\n  if (result.map) {\n    await fs.writeFile(`${output}.map`, result.map.toString());\n  }\n\n  console.log('CSS built successfully →', path.relative(root, output));\n}\n\nawait buildCss('src/styles/tailwind.css', 'dist/styles.css');\nawait buildCss(\n  'src/styles/tailwind-components.css',\n  'dist/styles.components.css'\n);\n"
  },
  {
    "path": "packages/core/scripts/check-dist-styling.mjs",
    "content": "import fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n  FORBIDDEN_TOP_LEVEL_UTILITY_SELECTORS,\n  diffAgainstBaseline,\n  isDayFlowSelector,\n  loadBaseline,\n  parseArgs,\n  scanDistJsViolations,\n  summarizeViolationsByFile,\n  writeBaseline,\n  workspaceRoot,\n} from './atomic-css-guard-utils.mjs';\n\nconst args = parseArgs(process.argv.slice(2));\nconst packageRoot = path.resolve(\n  String(args.get('--package-root') ?? process.cwd())\n);\nconst shouldWriteBaseline = Boolean(args.get('--write-baseline'));\n\nconst packageName = path.relative(workspaceRoot, packageRoot) || '.';\nconst stylesComponentsPath = path.join(\n  packageRoot,\n  'dist',\n  'styles.components.css'\n);\n\nfunction readCss(filePath) {\n  if (!fs.existsSync(filePath)) {\n    throw new Error(`Dist CSS file not found: ${filePath}`);\n  }\n\n  return fs.readFileSync(filePath, 'utf8');\n}\n\nfunction checkComponentsCss(css) {\n  const failures = [];\n\n  for (const pattern of FORBIDDEN_TOP_LEVEL_UTILITY_SELECTORS) {\n    if (new RegExp(pattern, 'm').test(css)) {\n      failures.push(`Top-level utility selector detected: ${pattern}`);\n    }\n  }\n\n  const topLevelSelectors = css\n    .split('\\n')\n    .filter(line => /^\\.[a-z]/.test(line.trim()))\n    .map(line => line.trim());\n\n  const violations = topLevelSelectors.filter(\n    selector => !isDayFlowSelector(selector)\n  );\n\n  if (violations.length > 0) {\n    failures.push(\n      `Found non-namespaced top-level selectors: ${violations.slice(0, 10).join(', ')}`\n    );\n  }\n\n  return failures;\n}\n\nconst cssFailures = checkComponentsCss(readCss(stylesComponentsPath));\n\nif (cssFailures.length > 0) {\n  console.error(\n    `[check-dist-styling] CSS contract violations detected for ${packageName}.`\n  );\n  for (const failure of cssFailures) {\n    console.error(`  - ${failure}`);\n  }\n  process.exit(1);\n}\n\nconst jsViolations = scanDistJsViolations(packageRoot);\nconst jsSummary = summarizeViolationsByFile(jsViolations);\nconst baseline = loadBaseline();\n\nif (shouldWriteBaseline) {\n  writeBaseline({\n    ...baseline,\n    distJs: {\n      ...baseline.distJs,\n      ...jsSummary,\n    },\n  });\n  console.log(\n    `[check-dist-styling] Updated dist JS baseline for ${packageName} (${Object.keys(jsSummary).length} files).`\n  );\n  process.exit(0);\n}\n\nconst jsRegressions = diffAgainstBaseline(jsSummary, baseline.distJs ?? {});\n\nif (jsRegressions.length > 0) {\n  console.error(\n    `[check-dist-styling] Dist JS atomic CSS regressions detected for ${packageName}.`\n  );\n  console.error(\n    'The dist guard blocks new atomic utility debt in published JS while allowing the current migration baseline.'\n  );\n\n  for (const regression of jsRegressions) {\n    console.error(\n      `  - ${regression.file}: ${regression.count} violations (baseline ${regression.baseline})`\n    );\n  }\n\n  process.exit(1);\n}\n\nconsole.log(\n  `[check-dist-styling] Dist CSS/JS contract respected for ${packageName}.`\n);\n"
  },
  {
    "path": "packages/core/scripts/check-semantic-css.mjs",
    "content": "import path from 'node:path';\n\nimport {\n  diffAgainstBaseline,\n  loadBaseline,\n  parseArgs,\n  scanSourceViolations,\n  summarizeViolationsByFile,\n  writeBaseline,\n} from './atomic-css-guard-utils.mjs';\n\nconst args = parseArgs(process.argv.slice(2));\nconst shouldWriteBaseline = Boolean(args.get('--write-baseline'));\n\nconst violations = scanSourceViolations();\nconst summary = summarizeViolationsByFile(violations);\nconst baseline = loadBaseline();\n\nif (shouldWriteBaseline) {\n  writeBaseline({\n    ...baseline,\n    source: summary,\n  });\n  console.log(\n    `[check-semantic-css] Updated source baseline with ${Object.keys(summary).length} files.`\n  );\n  process.exit(0);\n}\n\nconst regressions = diffAgainstBaseline(summary, baseline.source ?? {});\n\nif (regressions.length > 0) {\n  console.error(\n    '[check-semantic-css] Atomic CSS regressions detected in source files.'\n  );\n  console.error(\n    'The guard allows existing baseline debt, but blocks any new internal atomic utility usage.'\n  );\n  console.error(\n    'Allowed pass-through values such as `className={className}` are ignored; only literal internal class strings are counted.'\n  );\n\n  for (const regression of regressions) {\n    console.error(\n      `  - ${regression.file}: ${regression.count} violations (baseline ${regression.baseline})`\n    );\n\n    const details = violations\n      .filter(violation => violation.file === regression.file)\n      .slice(0, regression.count - regression.baseline + 3);\n\n    for (const detail of details) {\n      console.error(\n        `      line ${detail.line}: ${detail.token}  <- ${detail.snippet}`\n      );\n    }\n  }\n\n  console.error();\n  console.error(\n    `If the increase is intentional during migration, update ${path.relative(process.cwd(), new URL('./atomic-css-baseline.json', import.meta.url).pathname)} after review.`\n  );\n  process.exit(1);\n}\n\nconsole.log(\n  `[check-semantic-css] Source atomic CSS baseline respected (${violations.length} tracked matches across ${Object.keys(summary).length} files).`\n);\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/CalendarEvent.tsx",
    "content": "import { memo } from 'preact/compat';\nimport {\n  useRef,\n  useState,\n  useEffect,\n  useCallback,\n  useMemo,\n  useContext,\n} from 'preact/hooks';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { EventContextMenu } from '@/components/contextMenu';\nimport { ContentSlot } from '@/renderer/ContentSlot';\nimport { CustomRenderingContext } from '@/renderer/CustomRenderingContext';\nimport {\n  Event,\n  ViewType,\n  ReadOnlyConfig,\n  EventDetailContentProps,\n} from '@/types';\nimport {\n  getSelectedBgColor,\n  getEventBgColor,\n  getEventTextColor,\n  getPrimaryCalendarId,\n  getCalendarEventBgColors,\n  buildDiagonalPatternBackground,\n  temporalToVisualTemporal,\n} from '@/utils';\n\nimport { EventContent } from './components/EventContent';\nimport { EventDetailPanel } from './components/EventDetailPanel';\nimport { useClickOutside } from './hooks/useClickOutside';\nimport { useDetailPanelPosition } from './hooks/useDetailPanelPosition';\nimport { useEventActions } from './hooks/useEventActions';\nimport { useEventInteraction } from './hooks/useEventInteraction';\nimport { useEventStyles } from './hooks/useEventStyles';\nimport { useEventVisibility } from './hooks/useEventVisibility';\nimport { CalendarEventProps } from './types';\n// Import extracted utils and hooks\nimport {\n  getDayMetrics,\n  getActiveDayIndex,\n  getClickedDayIndex,\n  getEventClasses,\n  getEventSegmentShape,\n} from './utils';\n\nconst HIGHLIGHT_POP_DURATION_MS = 650;\n\nconst CalendarEvent = ({\n  event,\n  layout,\n  isAllDay = false,\n  allDayHeight = 28,\n  calendarRef,\n  isBeingDragged = false,\n  isBeingResized = false,\n  viewType,\n  isMultiDay = false,\n  segment,\n  yearSegment,\n  columnsPerRow,\n  segmentIndex = 0,\n  hourHeight,\n  firstHour,\n  selectedEventId,\n  detailPanelEventId,\n  onMoveStart,\n  onResizeStart,\n  onEventUpdate,\n  onEventDelete,\n  newlyCreatedEventId,\n  onDetailPanelOpen,\n  onEventSelect,\n  onEventLongPress,\n  onDetailPanelToggle,\n  useEventDetailPanel,\n  multiDaySegmentInfo,\n  app,\n  isMobile = false,\n  isSlidingView = false,\n  enableTouch,\n  hideTime,\n  timeFormat = '24h',\n  styleOverride,\n  className,\n  disableDefaultStyle = false,\n  renderVisualContent,\n  resizeHandleOrientation,\n  appTimeZone,\n  monthEventHeight,\n}: CalendarEventProps) => {\n  const customRenderingStore = useContext(CustomRenderingContext);\n  const isTouchEnabled = enableTouch ?? isMobile;\n  const isYearView = viewType === ViewType.YEAR;\n\n  // Visual event for display (shifted walls)\n  const visualEvent = useMemo(() => {\n    if (!appTimeZone || event.allDay) return event;\n    const start = temporalToVisualTemporal(\n      event.start as Temporal.PlainDate,\n      appTimeZone\n    );\n    const end = event.end\n      ? temporalToVisualTemporal(event.end as Temporal.PlainDate, appTimeZone)\n      : undefined;\n    return { ...event, start, end } as Event;\n  }, [event, appTimeZone]);\n  const [contextMenuPosition, setContextMenuPosition] = useState<{\n    x: number;\n    y: number;\n  } | null>(null);\n  const [isPopping, setIsPopping] = useState(false);\n\n  const eventRef = useRef<HTMLDivElement>(null);\n  const detailPanelRef = useRef<HTMLDivElement>(null);\n  const selectedEventElementRef = useRef<HTMLDivElement | null>(null);\n  const selectedDayIndexRef = useRef<number | null>(null);\n  // Suppress click after a mouse-initiated resize (prevents detail drawer from\n  // opening when mouseup triggers a click on the CalendarEvent element)\n  const mouseResizeActiveRef = useRef(false);\n  const mouseResizeClearTimerRef = useRef<ReturnType<typeof setTimeout> | null>(\n    null\n  );\n\n  const detailPanelKey =\n    isMultiDay && segment\n      ? `${event.id}::${segment.id}`\n      : multiDaySegmentInfo?.dayIndex === undefined\n        ? isYearView && yearSegment\n          ? yearSegment.id\n          : event.id\n        : `${event.id}::day-${multiDaySegmentInfo.dayIndex}`;\n\n  const showDetailPanel = detailPanelEventId === detailPanelKey;\n  const panelEnabled = useEventDetailPanel !== false;\n  const showDetailPanelForClickOutside = showDetailPanel && panelEnabled;\n\n  const readOnlyConfig = app?.getReadOnlyConfig(event.id) as ReadOnlyConfig;\n  const isEditable = app?.canMutateFromUI(event.id) ?? false;\n  const canOpenDetail = readOnlyConfig?.viewable !== false;\n  const isDraggable = readOnlyConfig?.draggable !== false;\n\n  // Interaction Hook\n  const {\n    isSelected,\n    setIsSelected,\n    isPressed,\n    setIsPressed,\n    handleTouchStart,\n    handleTouchMove,\n    handleTouchEnd,\n    handleTouchCancel,\n    shouldSuppressClick,\n  } = useEventInteraction({\n    event,\n    isTouchEnabled,\n    onMoveStart: isDraggable ? onMoveStart : undefined,\n    onEventLongPress,\n    onEventSelect,\n    onDetailPanelToggle,\n    canOpenDetail,\n    useEventDetailPanel,\n    app,\n    multiDaySegmentInfo,\n    isMultiDay,\n    segment,\n    detailPanelKey,\n  });\n\n  const [eventVisibility, setEventVisibility] = useState<\n    'standard' | 'sticky-top' | 'sticky-bottom' | 'sticky-left' | 'sticky-right'\n  >('standard');\n\n  // Suppress the click that fires after a mouse-based resize (mousedown → drag → mouseup)\n  // so the detail drawer doesn't open when the user resizes via mouse on small-width desktops.\n  const wrappedOnResizeStart = useCallback(\n    (e: MouseEvent | TouchEvent, ev: Event, direction: string) => {\n      if (!('touches' in e)) {\n        // Mouse resize: the browser will fire a click after mouseup, suppress it.\n        mouseResizeActiveRef.current = true;\n        if (mouseResizeClearTimerRef.current)\n          clearTimeout(mouseResizeClearTimerRef.current);\n        // Clear AFTER the click event has fired (setTimeout 0 defers past click).\n        document.addEventListener(\n          'mouseup',\n          () => {\n            mouseResizeClearTimerRef.current = setTimeout(() => {\n              mouseResizeActiveRef.current = false;\n            }, 0);\n          },\n          { once: true }\n        );\n      }\n      onResizeStart?.(e, ev, direction);\n    },\n    [onResizeStart]\n  );\n\n  // Utility Wrappers\n  const setActiveDayIndex = (dayIndex: number | null) => {\n    selectedDayIndexRef.current = dayIndex;\n  };\n\n  const getActiveDayIdx = useCallback(\n    () =>\n      getActiveDayIndex(\n        event,\n        detailPanelEventId || undefined,\n        detailPanelKey,\n        selectedDayIndexRef.current,\n        multiDaySegmentInfo,\n        segment\n      ),\n    [event, detailPanelEventId, detailPanelKey, multiDaySegmentInfo, segment]\n  );\n\n  const getClickedDayIdx = useCallback(\n    (clientX: number) =>\n      getClickedDayIndex(clientX, calendarRef, viewType, isMobile),\n    [calendarRef, viewType, isMobile]\n  );\n\n  const getDayMetricsWrapper = useCallback(\n    (dayIndex: number) =>\n      getDayMetrics(dayIndex, calendarRef, viewType, isMobile),\n    [calendarRef, viewType, isMobile]\n  );\n\n  // Positioning Hook\n  const { detailPanelPosition, setDetailPanelPosition, updatePanelPosition } =\n    useDetailPanelPosition({\n      event,\n      viewType,\n      isMultiDay,\n      segment,\n      yearSegment,\n      multiDaySegmentInfo,\n      calendarRef,\n      eventRef,\n      detailPanelRef,\n      selectedEventElementRef,\n      isMobile,\n      eventVisibility,\n      firstHour,\n      hourHeight,\n      columnsPerRow,\n      showDetailPanel,\n      detailPanelEventId,\n      detailPanelKey,\n      getActiveDayIdx,\n      getDayMetricsWrapper,\n    });\n\n  // Actions Hook\n  const {\n    handleClick,\n    handleDoubleClick,\n    handleContextMenu,\n    hasPendingSelection,\n  } = useEventActions({\n    event,\n    timingEvent: visualEvent,\n    viewType,\n    isAllDay,\n    isMultiDay,\n    segment,\n    multiDaySegmentInfo,\n    calendarRef,\n    firstHour,\n    hourHeight,\n    isMobile,\n    canOpenDetail,\n    useEventDetailPanel,\n    detailPanelKey,\n    app,\n    onEventSelect,\n    onDetailPanelToggle,\n    setIsSelected,\n    setDetailPanelPosition,\n    setContextMenuPosition,\n    setActiveDayIndex,\n    getClickedDayIdx,\n    updatePanelPosition,\n    selectedEventElementRef,\n  });\n\n  const isEventSelected =\n    (selectedEventId === undefined\n      ? isSelected\n      : selectedEventId === event.id) ||\n    hasPendingSelection ||\n    (!isTouchEnabled && isPressed) ||\n    isBeingDragged;\n  const hasTouchResizeHandles = isTouchEnabled && isEventSelected && isEditable;\n\n  // Styles Hook\n  const { calculateEventStyle } = useEventStyles({\n    event,\n    timingEvent: visualEvent,\n    layout,\n    isBeingDragged,\n    isAllDay,\n    allDayHeight,\n    viewType,\n    isMultiDay,\n    segment,\n    yearSegment,\n    columnsPerRow,\n    segmentIndex,\n    hourHeight,\n    firstHour,\n    isEventSelected,\n    showDetailPanel,\n    isPopping,\n    isDraggable,\n    canOpenDetail,\n    eventVisibility,\n    calendarRef,\n    isMobile,\n    eventRef,\n    getActiveDayIdx,\n    getDayMetricsWrapper,\n    multiDaySegmentInfo,\n    monthEventHeight,\n  });\n\n  // Visibility Hook\n  useEventVisibility({\n    event,\n    timingEvent: visualEvent,\n    isEventSelected,\n    showDetailPanel,\n    eventRef,\n    calendarRef,\n    isAllDay,\n    viewType,\n    isMobile,\n    multiDaySegmentInfo,\n    firstHour,\n    hourHeight,\n    updatePanelPosition,\n    eventVisibility,\n    setEventVisibility,\n  });\n\n  // Click Outside Hook\n  useClickOutside({\n    eventRef,\n    detailPanelRef,\n    eventId: event.id,\n    isEventSelected: isEventSelected,\n    showDetailPanel: showDetailPanelForClickOutside,\n    onEventSelect,\n    onDetailPanelToggle,\n    setIsSelected,\n    setActiveDayIndex,\n  });\n\n  // Stable panel close handler\n  const handlePanelClose = useCallback(() => {\n    if (onEventSelect) onEventSelect(null);\n    selectedDayIndexRef.current = null;\n    setIsSelected(false);\n    onDetailPanelToggle?.(null);\n  }, [onEventSelect, onDetailPanelToggle, setIsSelected]);\n\n  // Memoized args for the eventContent ContentSlot\n  const eventContentSlotArgs = useMemo(\n    () => ({\n      event,\n      viewType,\n      isAllDay,\n      isMobile,\n      isSelected: isEventSelected,\n      isDragging: isBeingDragged,\n      segment,\n      layout,\n    }),\n    [\n      event,\n      viewType,\n      isAllDay,\n      isMobile,\n      isEventSelected,\n      isBeingDragged,\n      segment,\n      layout,\n    ]\n  );\n\n  const contentSlotRenderer = useCallback(\n    (contentProps: EventDetailContentProps) => (\n      <ContentSlot\n        store={customRenderingStore}\n        generatorName='eventDetailContent'\n        generatorArgs={contentProps}\n      />\n    ),\n    [customRenderingStore]\n  );\n\n  // Highlight effect\n  useEffect(() => {\n    if (app?.state.highlightedEventId === event.id) {\n      setIsPopping(true);\n      const timer = setTimeout(() => {\n        setIsPopping(false);\n      }, HIGHLIGHT_POP_DURATION_MS);\n      return () => {\n        clearTimeout(timer);\n        setIsPopping(false);\n      };\n    }\n  }, [app?.state.highlightedEventId, event.id]);\n\n  useEffect(() => {\n    if (isEditable) return;\n    setContextMenuPosition(null);\n  }, [isEditable]);\n\n  // Auto-open detail panel for newly created events\n  useEffect(() => {\n    const isFirst =\n      (isMultiDay && segment?.isFirstSegment) ||\n      (isYearView && yearSegment?.isFirstSegment) ||\n      (!isMultiDay && !isYearView);\n\n    if (\n      newlyCreatedEventId === event.id &&\n      !showDetailPanel &&\n      isFirst &&\n      useEventDetailPanel !== false\n    ) {\n      setTimeout(() => {\n        onDetailPanelToggle?.(detailPanelKey);\n        onDetailPanelOpen?.();\n      }, 50);\n    }\n  }, [\n    newlyCreatedEventId,\n    event.id,\n    showDetailPanel,\n    isMultiDay,\n    segment,\n    isYearView,\n    yearSegment,\n    onDetailPanelToggle,\n    onDetailPanelOpen,\n    detailPanelKey,\n  ]);\n\n  // Final Render\n  const calendarId = getPrimaryCalendarId(event);\n  const calendarRegistry = app?.getCalendarRegistry();\n  const multiCalendarBgColors =\n    event.calendarIds && event.calendarIds.length > 1\n      ? getCalendarEventBgColors(event, calendarRegistry)\n      : null;\n  const eventSegmentShape = getEventSegmentShape(\n    viewType,\n    isAllDay,\n    segment,\n    yearSegment\n  );\n\n  return (\n    <>\n      <div\n        ref={eventRef}\n        data-event-id={event.id}\n        data-view={viewType}\n        data-all-day={String(isAllDay)}\n        data-selected={String(isEventSelected)}\n        data-dragging={String(isBeingDragged)}\n        data-resizing={String(isBeingResized)}\n        data-popping={String(isPopping)}\n        data-multi-day={String(isMultiDay)}\n        data-editable={String(isEditable)}\n        data-draggable={String(isDraggable)}\n        data-viewable={String(canOpenDetail)}\n        data-segment-shape={eventSegmentShape}\n        data-month-stack={String(viewType === ViewType.MONTH && !isMultiDay)}\n        data-touch-handles={String(hasTouchResizeHandles)}\n        className={`${getEventClasses(\n          viewType,\n          isAllDay,\n          isMultiDay\n        )} ${isAllDay && newlyCreatedEventId === event.id ? 'df-all-day-event-animate' : ''} ${className ?? ''}`}\n        style={{\n          ...(disableDefaultStyle ? {} : calculateEventStyle()),\n          ...(isEventSelected\n            ? {\n                background: getSelectedBgColor(calendarId, calendarRegistry),\n                color: '#fff',\n              }\n            : event.calendarIds && event.calendarIds.length > 1\n              ? {\n                  background: buildDiagonalPatternBackground(\n                    multiCalendarBgColors!\n                  ),\n                  color: getEventTextColor(calendarId, calendarRegistry),\n                }\n              : {\n                  backgroundColor: getEventBgColor(\n                    calendarId,\n                    calendarRegistry\n                  ),\n                  color: getEventTextColor(calendarId, calendarRegistry),\n                }),\n          ...styleOverride,\n          // Prevent the browser from handling this touch as a scroll or zoom\n          // gesture. CSS touch-action is evaluated before any JS runs, so it\n          // is far more reliable than e.preventDefault() in a touchstart\n          // handler for stopping the scroll container from claiming the touch.\n          // Only applied when the event is draggable on a touch-enabled screen.\n          ...(isTouchEnabled && isDraggable ? { touchAction: 'none' } : {}),\n        }}\n        onClick={e => {\n          // Suppress click that fires after a mouse-based resize\n          if (mouseResizeActiveRef.current) {\n            mouseResizeActiveRef.current = false;\n            e.preventDefault();\n            e.stopPropagation();\n            return;\n          }\n          if (isTouchEnabled && shouldSuppressClick()) {\n            e.preventDefault();\n            e.stopPropagation();\n            return;\n          }\n          handleClick(e as MouseEvent);\n        }}\n        onContextMenu={handleContextMenu}\n        onDblClick={handleDoubleClick}\n        onMouseDown={e => {\n          if (!isTouchEnabled) setIsPressed(true);\n          if (onMoveStart && isDraggable) {\n            const mouseEvent = e as MouseEvent;\n            if (multiDaySegmentInfo) {\n              onMoveStart(mouseEvent, {\n                ...event,\n                day: multiDaySegmentInfo.dayIndex ?? event.day,\n                _segmentInfo: multiDaySegmentInfo,\n              } as Event);\n            } else if (isMultiDay && segment) {\n              onMoveStart(mouseEvent, {\n                ...event,\n                day: segment.startDayIndex,\n                _segmentInfo: {\n                  dayIndex: segment.startDayIndex,\n                  isFirst: segment.isFirstSegment,\n                  isLast: segment.isLastSegment,\n                },\n              } as Event);\n            } else {\n              onMoveStart(mouseEvent, event);\n            }\n          }\n        }}\n        onMouseUp={() => !isTouchEnabled && setIsPressed(false)}\n        onMouseLeave={() => !isTouchEnabled && setIsPressed(false)}\n        onTouchStart={handleTouchStart}\n        onTouchMove={handleTouchMove}\n        onTouchEnd={handleTouchEnd}\n        onTouchCancel={handleTouchCancel}\n      >\n        <EventContent\n          event={visualEvent}\n          viewType={viewType}\n          isAllDay={isAllDay}\n          isMultiDay={isMultiDay}\n          segment={segment}\n          yearSegment={yearSegment}\n          segmentIndex={segmentIndex}\n          isBeingDragged={isBeingDragged}\n          isBeingResized={isBeingResized}\n          isEventSelected={isEventSelected}\n          isPopping={isPopping}\n          isEditable={isEditable}\n          isDraggable={isDraggable}\n          canOpenDetail={canOpenDetail}\n          isTouchEnabled={isTouchEnabled}\n          hideTime={hideTime}\n          isMobile={isMobile}\n          isSlidingView={isSlidingView}\n          app={app}\n          onMoveStart={onMoveStart}\n          onResizeStart={wrappedOnResizeStart}\n          multiDaySegmentInfo={multiDaySegmentInfo}\n          customRenderingStore={customRenderingStore}\n          eventContentSlotArgs={eventContentSlotArgs}\n          timeFormat={timeFormat}\n          appTimeZone={appTimeZone}\n          renderVisualContent={renderVisualContent}\n          resizeHandleOrientation={resizeHandleOrientation}\n          monthEventHeight={monthEventHeight}\n        />\n      </div>\n\n      {showDetailPanel && panelEnabled && (\n        <div\n          style={{\n            position: 'fixed',\n            top: 0,\n            left: 0,\n            right: 0,\n            bottom: 0,\n            zIndex: 9998,\n            pointerEvents: 'none',\n          }}\n        />\n      )}\n\n      <EventDetailPanel\n        showDetailPanel={showDetailPanel && panelEnabled}\n        detailPanelPosition={detailPanelPosition}\n        event={event}\n        detailPanelRef={detailPanelRef}\n        isAllDay={isAllDay}\n        eventVisibility={eventVisibility}\n        calendarRef={calendarRef}\n        selectedEventElementRef={selectedEventElementRef}\n        onEventUpdate={onEventUpdate}\n        onEventDelete={onEventDelete}\n        handlePanelClose={handlePanelClose}\n        customRenderingStore={customRenderingStore}\n        contentSlotRenderer={contentSlotRenderer}\n        app={app}\n      />\n\n      {contextMenuPosition && app && isEditable && (\n        <EventContextMenu\n          event={event}\n          x={contextMenuPosition.x}\n          y={contextMenuPosition.y}\n          onClose={() => setContextMenuPosition(null)}\n          app={app}\n          onDetailPanelToggle={onDetailPanelToggle}\n          detailPanelKey={detailPanelKey}\n        />\n      )}\n    </>\n  );\n};\n\nexport default memo(CalendarEvent);\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/__tests__/CalendarEvent.contract.test.tsx",
    "content": "import { render } from '@testing-library/preact';\nimport { Temporal } from 'temporal-polyfill';\n\nimport CalendarEvent from '@/components/calendarEvent/CalendarEvent';\nimport { ViewType, Event } from '@/types';\n\nconst baseEvent: Event = {\n  id: 'event-1',\n  title: 'Contract Event',\n  calendarId: 'default',\n  allDay: true,\n  start: Temporal.ZonedDateTime.from('2026-04-09T00:00:00+00:00[UTC]'),\n  end: Temporal.ZonedDateTime.from('2026-04-11T00:00:00+00:00[UTC]'),\n};\n\ndescribe('CalendarEvent style contract', () => {\n  it('exposes semantic data attributes for stateful all-day segments', () => {\n    const calendarElement = document.createElement('div');\n    const calendarRef = { current: calendarElement };\n\n    const { container } = render(\n      <CalendarEvent\n        event={baseEvent}\n        viewType={ViewType.MONTH}\n        isAllDay\n        isMultiDay\n        segment={{\n          id: 'segment-1',\n          originalEventId: baseEvent.id,\n          event: baseEvent,\n          startDayIndex: 0,\n          endDayIndex: 2,\n          segmentType: 'start',\n          totalDays: 3,\n          segmentIndex: 0,\n          isFirstSegment: true,\n          isLastSegment: false,\n        }}\n        calendarRef={calendarRef}\n        hourHeight={60}\n        firstHour={0}\n        onEventUpdate={jest.fn()}\n        onEventDelete={jest.fn()}\n      />\n    );\n\n    const eventElement = container.querySelector(\n      '[data-event-id=\"event-1\"]'\n    ) as HTMLDivElement | null;\n\n    expect(eventElement?.className).toContain('df-event');\n    expect(eventElement?.dataset.view).toBe(ViewType.MONTH);\n    expect(eventElement?.dataset.allDay).toBe('true');\n    expect(eventElement?.dataset.multiDay).toBe('true');\n    expect(eventElement?.dataset.segmentShape).toBe('start');\n    expect(eventElement?.dataset.monthStack).toBe('false');\n  });\n\n  it('suppresses the synthetic click after a touch tap so mobile opens only one path', () => {\n    const calendarElement = document.createElement('div');\n    const calendarRef = { current: calendarElement };\n    const onEventSelect = jest.fn();\n    const onDetailPanelToggle = jest.fn();\n\n    const timedEvent: Event = {\n      ...baseEvent,\n      allDay: false,\n      start: Temporal.ZonedDateTime.from('2026-04-09T09:00:00+00:00[UTC]'),\n      end: Temporal.ZonedDateTime.from('2026-04-09T10:00:00+00:00[UTC]'),\n    };\n\n    const { container } = render(\n      <CalendarEvent\n        event={timedEvent}\n        viewType={ViewType.DAY}\n        calendarRef={calendarRef}\n        hourHeight={60}\n        firstHour={0}\n        onMoveStart={jest.fn()}\n        onEventUpdate={jest.fn()}\n        onEventDelete={jest.fn()}\n        onEventSelect={onEventSelect}\n        onDetailPanelToggle={onDetailPanelToggle}\n        isMobile\n        enableTouch\n      />\n    );\n\n    const eventElement = container.querySelector(\n      '[data-event-id=\"event-1\"]'\n    ) as HTMLDivElement | null;\n\n    eventElement?.dispatchEvent(\n      new TouchEvent('touchstart', {\n        bubbles: true,\n        cancelable: true,\n        touches: [\n          {\n            identifier: 1,\n            target: eventElement!,\n            clientX: 24,\n            clientY: 24,\n            pageX: 24,\n            pageY: 24,\n            screenX: 24,\n            screenY: 24,\n            radiusX: 1,\n            radiusY: 1,\n            rotationAngle: 0,\n            force: 1,\n          } as unknown as Touch,\n        ],\n      })\n    );\n    eventElement?.dispatchEvent(\n      new TouchEvent('touchend', {\n        bubbles: true,\n        cancelable: true,\n        changedTouches: [\n          {\n            identifier: 1,\n            target: eventElement!,\n            clientX: 24,\n            clientY: 24,\n            pageX: 24,\n            pageY: 24,\n            screenX: 24,\n            screenY: 24,\n            radiusX: 1,\n            radiusY: 1,\n            rotationAngle: 0,\n            force: 1,\n          } as unknown as Touch,\n        ],\n      })\n    );\n    eventElement?.click();\n\n    expect(onEventSelect).toHaveBeenCalledTimes(1);\n    expect(onEventSelect).toHaveBeenCalledWith('event-1');\n    expect(onDetailPanelToggle).toHaveBeenCalledWith(null);\n    expect(onDetailPanelToggle).not.toHaveBeenCalledWith('event-1');\n  });\n\n  it('keeps long-press drag active through small finger drift and blocks follow-up scrolling', () => {\n    jest.useFakeTimers();\n    try {\n      const calendarElement = document.createElement('div');\n      const calendarRef = { current: calendarElement };\n      const onMoveStart = jest.fn();\n\n      const timedEvent: Event = {\n        ...baseEvent,\n        allDay: false,\n        start: Temporal.ZonedDateTime.from('2026-04-09T09:00:00+00:00[UTC]'),\n        end: Temporal.ZonedDateTime.from('2026-04-09T10:00:00+00:00[UTC]'),\n      };\n\n      const { container } = render(\n        <CalendarEvent\n          event={timedEvent}\n          viewType={ViewType.YEAR}\n          calendarRef={calendarRef}\n          hourHeight={60}\n          firstHour={0}\n          onMoveStart={onMoveStart}\n          onEventUpdate={jest.fn()}\n          onEventDelete={jest.fn()}\n          isMobile\n          enableTouch\n        />\n      );\n\n      const eventElement = container.querySelector(\n        '[data-event-id=\"event-1\"]'\n      ) as HTMLDivElement | null;\n\n      const touchStartEvent = new TouchEvent('touchstart', {\n        bubbles: true,\n        cancelable: true,\n        touches: [\n          {\n            identifier: 1,\n            target: eventElement!,\n            clientX: 24,\n            clientY: 24,\n            pageX: 24,\n            pageY: 24,\n            screenX: 24,\n            screenY: 24,\n            radiusX: 1,\n            radiusY: 1,\n            rotationAngle: 0,\n            force: 1,\n          } as unknown as Touch,\n        ],\n      });\n      eventElement?.dispatchEvent(touchStartEvent);\n\n      const driftMoveEvent = new TouchEvent('touchmove', {\n        bubbles: true,\n        cancelable: true,\n        touches: [\n          {\n            identifier: 1,\n            target: eventElement!,\n            clientX: 35,\n            clientY: 35,\n            pageX: 35,\n            pageY: 35,\n            screenX: 35,\n            screenY: 35,\n            radiusX: 1,\n            radiusY: 1,\n            rotationAngle: 0,\n            force: 1,\n          } as unknown as Touch,\n        ],\n      });\n      eventElement?.dispatchEvent(driftMoveEvent);\n\n      jest.advanceTimersByTime(500);\n\n      expect(onMoveStart).toHaveBeenCalledTimes(1);\n\n      const dragMoveEvent = new TouchEvent('touchmove', {\n        bubbles: true,\n        cancelable: true,\n        touches: [\n          {\n            identifier: 1,\n            target: eventElement!,\n            clientX: 60,\n            clientY: 60,\n            pageX: 60,\n            pageY: 60,\n            screenX: 60,\n            screenY: 60,\n            radiusX: 1,\n            radiusY: 1,\n            rotationAngle: 0,\n            force: 1,\n          } as unknown as Touch,\n        ],\n      });\n      eventElement?.dispatchEvent(dragMoveEvent);\n\n      expect(dragMoveEvent.defaultPrevented).toBe(true);\n    } finally {\n      jest.useRealTimers();\n    }\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/__tests__/CalendarEvent.timezone.test.tsx",
    "content": "import { render } from '@testing-library/preact';\nimport { Temporal } from 'temporal-polyfill';\n\nimport CalendarEvent from '@/components/calendarEvent/CalendarEvent';\nimport { Event, ViewType } from '@/types';\n\nconst baseEvent: Event = {\n  id: 'event-1',\n  title: 'Shifted Event',\n  calendarId: 'default',\n  allDay: false,\n  start: Temporal.ZonedDateTime.from('2026-04-07T18:00:00+00:00[UTC]'),\n  end: Temporal.ZonedDateTime.from('2026-04-07T19:30:00+00:00[UTC]'),\n};\n\ndescribe('CalendarEvent timezone rendering', () => {\n  it('recomputes timed event positioning when the secondary timezone changes', () => {\n    const calendarElement = document.createElement('div');\n    const calendarRef = { current: calendarElement };\n\n    const { container, rerender } = render(\n      <CalendarEvent\n        event={baseEvent}\n        viewType={ViewType.DAY}\n        calendarRef={calendarRef}\n        hourHeight={10}\n        firstHour={0}\n        onEventUpdate={jest.fn()}\n        onEventDelete={jest.fn()}\n        appTimeZone='Pacific/Kiritimati'\n      />\n    );\n\n    const eventElement = container.querySelector(\n      '[data-event-id=\"event-1\"]'\n    ) as HTMLDivElement | null;\n\n    expect(eventElement?.style.top).toBe('83px');\n    expect(eventElement?.style.height).toBe('11px');\n\n    rerender(\n      <CalendarEvent\n        event={baseEvent}\n        viewType={ViewType.DAY}\n        calendarRef={calendarRef}\n        hourHeight={10}\n        firstHour={0}\n        onEventUpdate={jest.fn()}\n        onEventDelete={jest.fn()}\n        appTimeZone='America/New_York'\n      />\n    );\n\n    const updatedEventElement = container.querySelector(\n      '[data-event-id=\"event-1\"]'\n    ) as HTMLDivElement | null;\n\n    expect(updatedEventElement?.style.top).toBe('143px');\n    expect(updatedEventElement?.style.height).toBe('11px');\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/components/AllDayContent.tsx",
    "content": "import { ComponentChildren } from 'preact';\n\nimport { CalendarDays } from '@/components/common/Icons';\nimport { MultiDayEventSegment } from '@/components/monthView/util';\nimport {\n  eventIcon,\n  eventTitleSmall,\n  resizeHandleLeft,\n  resizeHandleRight,\n} from '@/styles/classNames';\nimport { Event } from '@/types';\n\ninterface AllDayContentProps {\n  event: Event;\n  isEditable: boolean;\n  onResizeStart?: (e: MouseEvent, event: Event, direction: string) => void;\n  isMultiDay?: boolean;\n  segment?: MultiDayEventSegment;\n  isSlidingView?: boolean;\n  /** Optional slot renderer — receives the default inner content and wraps it in a ContentSlot */\n  renderSlot?: (defaultContent: ComponentChildren) => ComponentChildren;\n}\n\nconst AllDayContent = ({\n  event,\n  isEditable,\n  onResizeStart,\n  isMultiDay,\n  segment,\n  isSlidingView,\n  renderSlot,\n}: AllDayContentProps) => {\n  const showIcon = event.icon !== false;\n  const customIcon = typeof event.icon === 'boolean' ? null : event.icon;\n\n  // Calculate title offset for mobile sliding mode\n  const titleOffsetStyle = (() => {\n    if (!isSlidingView || !isMultiDay || !segment) return {};\n\n    // The current visible window starts at index 2 of the 3-page range\n    const visibleStartIndex = 2;\n    // If the event starts before the visible window but ends within or after it\n    if (\n      segment.startDayIndex < visibleStartIndex &&\n      segment.endDayIndex >= visibleStartIndex\n    ) {\n      const offsetDays = visibleStartIndex - segment.startDayIndex;\n      const spanDays = segment.endDayIndex - segment.startDayIndex + 1;\n\n      // Calculate offset as a percentage of the event bar's width\n      const offsetPercent = (offsetDays / spanDays) * 100;\n\n      return {\n        paddingLeft: `calc(${offsetPercent}% + 0.75rem)`,\n        // Ensure the transition matches the swipe transition for a smooth effect\n        // transition: 'padding-left 0.3s ease-out',\n      };\n    }\n    return {};\n  })();\n\n  const innerContent = (\n    <div className='df-event-content-row'>\n      {showIcon &&\n        (customIcon ? (\n          <div className='df-event-icon-slot'>{customIcon}</div>\n        ) : (\n          <span className='df-event-icon-slot'>\n            <CalendarDays className={eventIcon} />\n          </span>\n        ))}\n      <div className={`${eventTitleSmall} df-event-title-tight`}>\n        {event.title}\n      </div>\n    </div>\n  );\n\n  return (\n    <div className='df-event-all-day-shell' style={titleOffsetStyle}>\n      {renderSlot ? renderSlot(innerContent) : innerContent}\n\n      {/* Left/Right resize handles — absolute positioned, always rendered outside the slot */}\n      {onResizeStart && isEditable && (\n        <>\n          <div\n            className={resizeHandleLeft}\n            onMouseDown={e => {\n              e.preventDefault();\n              e.stopPropagation();\n              onResizeStart(e, event, 'left');\n            }}\n            onClick={e => {\n              e.preventDefault();\n              e.stopPropagation();\n            }}\n          />\n          <div\n            className={resizeHandleRight}\n            onMouseDown={e => {\n              e.preventDefault();\n              e.stopPropagation();\n              onResizeStart(e, event, 'right');\n            }}\n            onClick={e => {\n              e.preventDefault();\n              e.stopPropagation();\n            }}\n          />\n        </>\n      )}\n    </div>\n  );\n};\n\nexport default AllDayContent;\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/components/EventContent.tsx",
    "content": "import { ComponentChildren } from 'preact';\n\nimport MultiDayEvent from '@/components/monthView/MultiDayEvent';\nimport { MultiDayEventSegment } from '@/components/monthView/util';\nimport { YearMultiDaySegment } from '@/components/yearView/utils';\nimport { ContentSlot } from '@/renderer/ContentSlot';\nimport { CustomRenderingStore } from '@/renderer/CustomRenderingStore';\nimport { ViewType, Event, ICalendarApp, EventLayout } from '@/types';\n\nimport AllDayContent from './AllDayContent';\nimport MonthAllDayContent from './MonthAllDayContent';\nimport MonthRegularContent from './MonthRegularContent';\nimport RegularEventContent from './RegularEventContent';\nimport YearEventContent from './YearEventContent';\n\n/** Resolve the most specific overridden generator name. Returns null if not overridden. */\nfunction resolveGeneratorName(\n  store: CustomRenderingStore | null,\n  viewType: ViewType,\n  isAllDay: boolean\n): string | null {\n  const viewKey =\n    (viewType as string).charAt(0).toUpperCase() +\n    (viewType as string).slice(1);\n  const specificName = isAllDay\n    ? `eventContentAllDay${viewKey}` // e.g. 'eventContentAllDayDay'\n    : `eventContent${viewKey}`; // e.g. 'eventContentDay'\n\n  if (store?.isOverridden(specificName)) return specificName;\n  return null;\n}\n\ninterface EventContentProps {\n  event: Event;\n  viewType: ViewType;\n  isAllDay: boolean;\n  isMultiDay: boolean;\n  segment?: MultiDayEventSegment;\n  yearSegment?: YearMultiDaySegment;\n  segmentIndex: number;\n  isBeingDragged: boolean;\n  isBeingResized: boolean;\n  isEventSelected: boolean;\n  isPopping: boolean;\n  isEditable: boolean;\n  isDraggable: boolean;\n  canOpenDetail: boolean;\n  isTouchEnabled: boolean;\n  hideTime?: boolean;\n  isMobile: boolean;\n  isSlidingView?: boolean;\n  app?: ICalendarApp;\n  onMoveStart?: (e: MouseEvent | TouchEvent, event: Event) => void;\n  onResizeStart?: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  multiDaySegmentInfo?: {\n    startHour: number;\n    endHour: number;\n    isFirst: boolean;\n    isLast: boolean;\n    dayIndex?: number;\n  };\n  customRenderingStore: CustomRenderingStore | null;\n  // oxlint-disable-next-line typescript/no-explicit-any\n  eventContentSlotArgs: any;\n  layout?: EventLayout;\n  timeFormat?: '12h' | '24h';\n  appTimeZone?: string;\n  renderVisualContent?: (\n    defaultContent: ComponentChildren\n  ) => ComponentChildren;\n  resizeHandleOrientation?: 'vertical' | 'horizontal';\n  monthEventHeight?: number;\n}\n\nexport const EventContent = ({\n  event,\n  viewType,\n  isAllDay,\n  isMultiDay,\n  segment,\n  yearSegment,\n  segmentIndex,\n  isBeingDragged,\n  isBeingResized,\n  isEventSelected,\n  isPopping,\n  isEditable,\n  isDraggable,\n  canOpenDetail,\n  isTouchEnabled,\n  hideTime,\n  isMobile,\n  isSlidingView,\n  app,\n  onMoveStart,\n  onResizeStart,\n  multiDaySegmentInfo,\n  customRenderingStore,\n  eventContentSlotArgs,\n  timeFormat = '24h',\n  appTimeZone,\n  renderVisualContent,\n  resizeHandleOrientation,\n  monthEventHeight,\n}: EventContentProps) => {\n  const isMonthView = viewType === ViewType.MONTH;\n  const isYearView = viewType === ViewType.YEAR;\n\n  const generatorName = resolveGeneratorName(\n    customRenderingStore,\n    viewType,\n    isAllDay\n  );\n\n  const applyVisualWrapper = (defaultContent: ComponentChildren) =>\n    renderVisualContent ? renderVisualContent(defaultContent) : defaultContent;\n\n  // Month multi-day: MultiDayEvent owns absolute positioning and resize handles.\n  // Year view: YearEventContent owns resize handles (CalendarEvent shell handles positioning).\n  // In both cases the ContentSlot is injected via renderSlot so those are preserved\n  // even when the user provides a custom event content renderer.\n  if (isMonthView && isMultiDay && segment) {\n    return (\n      <MultiDayEvent\n        segment={segment}\n        segmentIndex={segmentIndex ?? 0}\n        eventHeight={monthEventHeight}\n        isDragging={isBeingDragged}\n        isSelected={isEventSelected}\n        isResizing={isBeingResized}\n        onMoveStart={onMoveStart}\n        onResizeStart={onResizeStart}\n        isMobile={isMobile}\n        isDraggable={isDraggable}\n        isEditable={isEditable}\n        viewable={canOpenDetail}\n        isPopping={isPopping}\n        appTimeZone={appTimeZone}\n        renderSlot={defaultContent => (\n          <ContentSlot\n            store={customRenderingStore}\n            generatorName={generatorName}\n            generatorArgs={eventContentSlotArgs}\n            defaultContent={applyVisualWrapper(defaultContent)}\n          />\n        )}\n      />\n    );\n  }\n\n  if (isYearView && yearSegment) {\n    return (\n      <YearEventContent\n        event={event}\n        segment={yearSegment}\n        isEditable={isEditable}\n        onMoveStart={onMoveStart}\n        onResizeStart={onResizeStart}\n        renderSlot={defaultContent => (\n          <ContentSlot\n            store={customRenderingStore}\n            generatorName={generatorName}\n            generatorArgs={eventContentSlotArgs}\n            defaultContent={applyVisualWrapper(defaultContent)}\n          />\n        )}\n      />\n    );\n  }\n\n  if (isMonthView) {\n    const defaultContent = isAllDay ? (\n      <MonthAllDayContent\n        event={event}\n        isEventSelected={isEventSelected}\n        isMobile={isMobile}\n      />\n    ) : (\n      <MonthRegularContent\n        event={event}\n        app={app}\n        isEventSelected={isEventSelected}\n        hideTime={hideTime}\n        isMobile={isMobile}\n      />\n    );\n    return (\n      <ContentSlot\n        store={customRenderingStore}\n        generatorName={generatorName}\n        generatorArgs={eventContentSlotArgs}\n        defaultContent={applyVisualWrapper(defaultContent)}\n      />\n    );\n  }\n\n  // Day/Week view: resize handles live inside AllDayContent/RegularEventContent.\n  // Use renderSlot so resize handles are always rendered even when a custom slot overrides\n  // the visual content.\n  const slotRenderer = (defaultContent: ComponentChildren) => (\n    <ContentSlot\n      store={customRenderingStore}\n      generatorName={generatorName}\n      generatorArgs={eventContentSlotArgs}\n      defaultContent={applyVisualWrapper(defaultContent)}\n    />\n  );\n\n  if (isAllDay) {\n    return (\n      <AllDayContent\n        event={event}\n        isEditable={isEditable}\n        onResizeStart={onResizeStart}\n        isMultiDay={isMultiDay}\n        segment={segment}\n        isSlidingView={isSlidingView}\n        renderSlot={slotRenderer}\n      />\n    );\n  }\n\n  return (\n    <RegularEventContent\n      event={event}\n      app={app}\n      multiDaySegmentInfo={multiDaySegmentInfo}\n      isEditable={isEditable}\n      isTouchEnabled={isTouchEnabled}\n      isEventSelected={isEventSelected}\n      isBeingDragged={isBeingDragged}\n      isBeingResized={isBeingResized}\n      onResizeStart={onResizeStart}\n      timeFormat={timeFormat}\n      resizeHandleOrientation={resizeHandleOrientation}\n      renderSlot={slotRenderer}\n    />\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/components/EventDetailPanel.tsx",
    "content": "import { RefObject } from 'preact';\n\nimport DefaultEventDetailPanel from '@/components/common/DefaultEventDetailPanel';\nimport { EventDetailPanelWithContent } from '@/components/common/EventDetailPanelWithContent';\nimport { CustomRenderingStore } from '@/renderer/CustomRenderingStore';\nimport {\n  Event,\n  ICalendarApp,\n  EventDetailPosition,\n  EventDetailContentRenderer,\n} from '@/types';\n\ninterface EventDetailPanelProps {\n  showDetailPanel: boolean;\n  detailPanelPosition: EventDetailPosition | null;\n  event: Event;\n  detailPanelRef: RefObject<HTMLDivElement>;\n  isAllDay: boolean;\n  eventVisibility:\n    | 'standard'\n    | 'sticky-top'\n    | 'sticky-bottom'\n    | 'sticky-left'\n    | 'sticky-right';\n  calendarRef: RefObject<HTMLDivElement>;\n  selectedEventElementRef: RefObject<HTMLElement | null>;\n  onEventUpdate: (event: Event) => void;\n  onEventDelete: (id: string) => void;\n  handlePanelClose: () => void;\n  customRenderingStore: CustomRenderingStore | null;\n  contentSlotRenderer: EventDetailContentRenderer;\n  app?: ICalendarApp;\n}\n\nexport const EventDetailPanel = ({\n  showDetailPanel,\n  detailPanelPosition,\n  event,\n  detailPanelRef,\n  isAllDay,\n  eventVisibility,\n  calendarRef,\n  selectedEventElementRef,\n  onEventUpdate,\n  onEventDelete,\n  handlePanelClose,\n  customRenderingStore,\n  contentSlotRenderer,\n  app,\n}: EventDetailPanelProps) => {\n  if (!showDetailPanel) return null;\n\n  if (!detailPanelPosition) return null;\n\n  const panelProps = {\n    event,\n    position: detailPanelPosition,\n    panelRef: detailPanelRef,\n    isAllDay,\n    eventVisibility,\n    calendarRef,\n    selectedEventElementRef,\n    onEventUpdate,\n    onEventDelete,\n    onClose: handlePanelClose,\n  };\n\n  if (customRenderingStore?.isOverridden('eventDetailContent')) {\n    return (\n      <EventDetailPanelWithContent\n        {...panelProps}\n        contentRenderer={contentSlotRenderer}\n      />\n    );\n  }\n\n  return <DefaultEventDetailPanel {...panelProps} app={app} />;\n};\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/components/MonthAllDayContent.tsx",
    "content": "import { CalendarDays } from '@/components/common/Icons';\nimport { monthAllDayContent, eventIcon } from '@/styles/classNames';\nimport { Event } from '@/types';\n\nconst mobileFadeStyle = {\n  overflow: 'hidden',\n  whiteSpace: 'nowrap',\n  textOverflow: 'clip',\n  WebkitMaskImage: 'linear-gradient(to right, black 70%, transparent 100%)',\n  maskImage: 'linear-gradient(to right, black 70%, transparent 100%)',\n  WebkitMaskRepeat: 'no-repeat',\n  maskRepeat: 'no-repeat',\n} as const;\n\ninterface MonthAllDayContentProps {\n  event: Event;\n  isEventSelected: boolean;\n  isMobile: boolean;\n}\n\nconst MonthAllDayContent = ({\n  event,\n  isEventSelected: _isEventSelected,\n  isMobile,\n}: MonthAllDayContentProps) => {\n  const showIcon = event.icon !== false;\n  const customIcon = typeof event.icon === 'boolean' ? null : event.icon;\n\n  return (\n    <div className={monthAllDayContent}>\n      {showIcon &&\n        (customIcon ? (\n          <div className='df-event-icon-slot'>{customIcon}</div>\n        ) : event.title.toLowerCase().includes('easter') ||\n          event.title.toLowerCase().includes('holiday') ? (\n          <span className='df-event-holiday-icon'>⭐</span>\n        ) : (\n          <span className='df-event-icon-slot'>\n            <CalendarDays className={eventIcon} />\n          </span>\n        ))}\n      <span\n        className={`df-event-month-title ${isMobile ? 'df-mobile-mask-fade' : ''}`}\n        style={isMobile ? mobileFadeStyle : undefined}\n      >\n        {event.title}\n      </span>\n    </div>\n  );\n};\n\nexport default MonthAllDayContent;\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/components/MonthRegularContent.tsx",
    "content": "import { monthRegularContent, monthEventColorBar } from '@/styles/classNames';\nimport { Event, ICalendarApp } from '@/types';\nimport {\n  getCalendarLineColors,\n  buildColorBarGradient,\n  extractHourFromDate,\n} from '@/utils';\n\nconst mobileFadeStyle = {\n  overflow: 'hidden',\n  whiteSpace: 'nowrap',\n  textOverflow: 'clip',\n  WebkitMaskImage: 'linear-gradient(to right, black 70%, transparent 100%)',\n  maskImage: 'linear-gradient(to right, black 70%, transparent 100%)',\n  WebkitMaskRepeat: 'no-repeat',\n  maskRepeat: 'no-repeat',\n} as const;\n\ninterface MonthRegularContentProps {\n  event: Event;\n  app?: ICalendarApp;\n  isEventSelected: boolean;\n  hideTime?: boolean;\n  isMobile?: boolean;\n}\n\nconst MonthRegularContent = ({\n  event,\n  app,\n  isEventSelected: _isEventSelected,\n  hideTime,\n  isMobile,\n}: MonthRegularContentProps) => {\n  const startTime = `${Math.floor(extractHourFromDate(event.start)).toString().padStart(2, '0')}:${Math.round(\n    (extractHourFromDate(event.start) % 1) * 60\n  )\n    .toString()\n    .padStart(2, '0')}`;\n\n  const lineColors = getCalendarLineColors(event, app?.getCalendarRegistry());\n  const colorBarValue = buildColorBarGradient(lineColors);\n  const colorBarStyle =\n    lineColors.length > 1\n      ? { background: colorBarValue }\n      : { backgroundColor: colorBarValue };\n  const hideColorBar = _isEventSelected && lineColors.length > 1;\n\n  return (\n    <div className={monthRegularContent} data-mobile={String(!!isMobile)}>\n      <div className='df-event-month-main'>\n        {!hideColorBar && (\n          <div style={colorBarStyle} className={monthEventColorBar} />\n        )}\n        <span\n          className={`df-event-month-title ${isMobile ? 'df-mobile-mask-fade' : ''}`}\n          style={isMobile ? mobileFadeStyle : undefined}\n        >\n          {event.title}\n        </span>\n      </div>\n      {!hideTime && !isMobile && (\n        <span className='df-event-month-time'>{startTime}</span>\n      )}\n    </div>\n  );\n};\n\nexport default MonthRegularContent;\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/components/RegularEventContent.tsx",
    "content": "import { ComponentChildren } from 'preact';\n\nimport {\n  eventColorBar,\n  eventTitleSmall,\n  eventTime,\n  resizeHandleLeft,\n  resizeHandleTop,\n  resizeHandleBottom,\n  resizeHandleRight,\n} from '@/styles/classNames';\nimport { Event, ICalendarApp } from '@/types';\nimport {\n  formatEventTimeRange,\n  getLineColor,\n  getCalendarLineColors,\n  buildDiagonalColorBarGradient,\n  getPrimaryCalendarId,\n  extractHourFromDate,\n  getEventEndHour,\n  formatTime,\n} from '@/utils';\n\ninterface RegularEventContentProps {\n  event: Event;\n  app?: ICalendarApp;\n  multiDaySegmentInfo?: {\n    startHour: number;\n    endHour: number;\n    isFirst: boolean;\n    isLast: boolean;\n    dayIndex?: number;\n  };\n  isEditable: boolean;\n  isTouchEnabled: boolean;\n  isEventSelected: boolean;\n  isBeingDragged?: boolean;\n  isBeingResized?: boolean;\n  onResizeStart?: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  timeFormat?: '12h' | '24h';\n  resizeHandleOrientation?: 'vertical' | 'horizontal';\n  /** Optional slot renderer — receives the default visual content and wraps it in a ContentSlot */\n  renderSlot?: (defaultContent: ComponentChildren) => ComponentChildren;\n}\n\nconst colorBarClipPath =\n  'inset(0.25rem calc(100% - 0.25rem - 3px) 0.25rem 0.25rem round 9999px)';\n\nconst RegularEventContent = ({\n  event,\n  app,\n  multiDaySegmentInfo,\n  isEditable,\n  isTouchEnabled,\n  isEventSelected,\n  isBeingDragged,\n  isBeingResized,\n  onResizeStart,\n  timeFormat = '24h',\n  resizeHandleOrientation = 'vertical',\n  renderSlot,\n}: RegularEventContentProps) => {\n  const startHour = multiDaySegmentInfo\n    ? multiDaySegmentInfo.startHour\n    : extractHourFromDate(event.start);\n  const endHour = multiDaySegmentInfo\n    ? multiDaySegmentInfo.endHour\n    : getEventEndHour(event);\n  const duration = endHour - startHour;\n  const isFirstSegment = multiDaySegmentInfo\n    ? multiDaySegmentInfo.isFirst\n    : true;\n  const isLastSegment = multiDaySegmentInfo ? multiDaySegmentInfo.isLast : true;\n  const calendarId = getPrimaryCalendarId(event);\n  const contentPaddingClass =\n    !multiDaySegmentInfo && duration <= 0.25 ? 'compact' : 'default';\n\n  const lineColors = getCalendarLineColors(event, app?.getCalendarRegistry());\n  const colorBarValue = buildDiagonalColorBarGradient(lineColors);\n  const hideColorBar = isEventSelected && lineColors.length > 1;\n  const colorBarContent = hideColorBar ? null : lineColors.length > 1 ? (\n    <div\n      className='df-event-color-bar-overlay'\n      style={{\n        background: colorBarValue,\n        clipPath: colorBarClipPath,\n      }}\n    />\n  ) : (\n    <div className={eventColorBar} style={{ backgroundColor: colorBarValue }} />\n  );\n\n  const visualContent = (\n    <>\n      {colorBarContent}\n      <div\n        className='df-event-timed-content'\n        data-density={contentPaddingClass}\n      >\n        <div\n          className={`${eventTitleSmall} ${duration <= 0.25 ? 'df-event-title-tight' : ''}`}\n        >\n          {event.title}\n        </div>\n        {duration > 0.5 && (\n          <div className={eventTime}>\n            {multiDaySegmentInfo\n              ? `${formatTime(startHour, 0, timeFormat)} - ${formatTime(endHour, 0, timeFormat)}`\n              : formatEventTimeRange(event, timeFormat)}\n          </div>\n        )}\n      </div>\n    </>\n  );\n\n  return (\n    <>\n      {renderSlot ? renderSlot(visualContent) : visualContent}\n\n      {onResizeStart &&\n        isEditable &&\n        resizeHandleOrientation === 'vertical' && (\n          <>\n            {/* Only show top resize handle on the first segment */}\n            {isFirstSegment && (\n              <div\n                className={resizeHandleTop}\n                onMouseDown={e => onResizeStart(e, event, 'top')}\n                onClick={e => e.stopPropagation()}\n              />\n            )}\n            {/* Only show bottom resize handle on the last segment */}\n            {isLastSegment && (\n              <div\n                className={resizeHandleBottom}\n                onMouseDown={e => onResizeStart(e, event, 'bottom')}\n                onClick={e => e.stopPropagation()}\n              />\n            )}\n            {/* Right resize handle for multi-day events (only on the last segment) */}\n            {!isFirstSegment && isLastSegment && multiDaySegmentInfo && (\n              <div\n                className={resizeHandleRight}\n                onMouseDown={e => {\n                  e.preventDefault();\n                  e.stopPropagation();\n                  onResizeStart(e, event, 'right');\n                }}\n                onClick={e => {\n                  e.preventDefault();\n                  e.stopPropagation();\n                }}\n              />\n            )}\n          </>\n        )}\n\n      {isTouchEnabled &&\n        isEventSelected &&\n        !isBeingDragged &&\n        !isBeingResized &&\n        onResizeStart &&\n        isEditable &&\n        resizeHandleOrientation === 'vertical' && (\n          <>\n            {/* Top-Right Indicator (Start Time) */}\n            <div\n              className='df-event-touch-resize-indicator'\n              data-axis='vertical'\n              data-position='top'\n              style={{\n                borderColor: getLineColor(\n                  calendarId,\n                  app?.getCalendarRegistry()\n                ),\n              }}\n              onTouchStart={e => {\n                e.stopPropagation();\n                onResizeStart(e, event, 'top');\n              }}\n            />\n            {/* Bottom-Left Indicator (End Time) */}\n            <div\n              className='df-event-touch-resize-indicator'\n              data-axis='vertical'\n              data-position='bottom'\n              style={{\n                borderColor: getLineColor(\n                  calendarId,\n                  app?.getCalendarRegistry()\n                ),\n              }}\n              onTouchStart={e => {\n                e.stopPropagation();\n                onResizeStart(e, event, 'bottom');\n              }}\n            />\n          </>\n        )}\n\n      {onResizeStart &&\n        isEditable &&\n        resizeHandleOrientation === 'horizontal' && (\n          <>\n            <div\n              className={resizeHandleLeft}\n              onMouseDown={e => {\n                e.preventDefault();\n                e.stopPropagation();\n                onResizeStart(e, event, 'left');\n              }}\n              onClick={e => {\n                e.preventDefault();\n                e.stopPropagation();\n              }}\n            />\n            <div\n              className={resizeHandleRight}\n              onMouseDown={e => {\n                e.preventDefault();\n                e.stopPropagation();\n                onResizeStart(e, event, 'right');\n              }}\n              onClick={e => {\n                e.preventDefault();\n                e.stopPropagation();\n              }}\n            />\n          </>\n        )}\n    </>\n  );\n};\n\nexport default RegularEventContent;\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/components/YearEventContent.tsx",
    "content": "import { ComponentChildren } from 'preact';\n\nimport { getEventIcon } from '@/components/monthView/util';\nimport { YearMultiDaySegment } from '@/components/yearView/utils';\nimport { resizeHandleLeft, resizeHandleRight } from '@/styles/classNames';\nimport { Event } from '@/types';\nimport {\n  getLineColor,\n  getPrimaryCalendarId,\n  getCalendarLineColors,\n  buildColorBarGradient,\n} from '@/utils';\n\ninterface YearEventContentProps {\n  event: Event;\n  segment: YearMultiDaySegment;\n  isEditable: boolean;\n  onMoveStart?: (e: MouseEvent | TouchEvent, event: Event) => void;\n  onResizeStart?: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  /** Optional slot renderer — receives the default visual content and wraps it in a ContentSlot */\n  renderSlot?: (defaultContent: ComponentChildren) => ComponentChildren;\n}\n\nconst YearEventContent = ({\n  event,\n  segment,\n  isEditable,\n  onMoveStart,\n  onResizeStart,\n  renderSlot,\n}: YearEventContentProps) => {\n  const isAllDay = !!event.allDay;\n  const calendarId = getPrimaryCalendarId(event);\n  const lineColors = getCalendarLineColors(event);\n  const indicatorColorBarValue = buildColorBarGradient(lineColors);\n  const indicatorColorBarStyle =\n    lineColors.length > 1\n      ? { background: indicatorColorBarValue }\n      : { backgroundColor: indicatorColorBarValue };\n  const { isFirstSegment, isLastSegment } = segment;\n\n  const renderResizeHandle = (position: 'left' | 'right') => {\n    const isLeft = position === 'left';\n    const shouldShow = isLeft ? isFirstSegment : isLastSegment;\n\n    // Only allow resizing for all-day events in Year View\n    if (!event.allDay || !shouldShow || !onResizeStart || !isEditable)\n      return null;\n\n    return (\n      <div\n        className={isLeft ? resizeHandleLeft : resizeHandleRight}\n        onMouseDown={e => {\n          e.preventDefault();\n          e.stopPropagation();\n          onResizeStart(e as MouseEvent, event, isLeft ? 'left' : 'right');\n        }}\n        onTouchStart={e => {\n          e.stopPropagation();\n          onResizeStart(e as TouchEvent, event, isLeft ? 'left' : 'right');\n        }}\n        onClick={e => {\n          e.preventDefault();\n          e.stopPropagation();\n        }}\n      />\n    );\n  };\n\n  const renderContent = () => {\n    if (isAllDay) {\n      const getDisplayText = () => {\n        if (segment.isFirstSegment) return event.title;\n        return '···';\n      };\n\n      return (\n        <div\n          className='df-event-year-content'\n          onMouseDown={e => {\n            if (onMoveStart) {\n              e.stopPropagation();\n              onMoveStart(e as MouseEvent, event);\n            }\n          }}\n        >\n          {segment.isFirstSegment && getEventIcon(event) && (\n            <div className='df-event-icon-slot'>\n              <div\n                className='df-event-year-icon-badge'\n                style={{\n                  backgroundColor: getLineColor(calendarId),\n                }}\n              >\n                {getEventIcon(event)}\n              </div>\n            </div>\n          )}\n\n          <div className='df-event-year-main'>\n            <div className='df-event-year-title df-event-year-title-fade'>\n              {getDisplayText()}\n            </div>\n          </div>\n\n          {/* Add small indicator for continuation if needed, similar to MultiDayEvent */}\n          {segment.isLastSegment && !segment.isFirstSegment && (\n            <div className='df-event-year-tail'>\n              <div className='df-event-year-tail-dot'></div>\n            </div>\n          )}\n        </div>\n      );\n    }\n\n    // For non-all-day events treated as bars in Year View\n    const titleText = segment.isFirstSegment ? event.title : '';\n\n    return (\n      <div\n        className='df-event-year-content df-event-year-content-timed'\n        onMouseDown={e => {\n          if (onMoveStart) {\n            e.stopPropagation();\n            onMoveStart(e as MouseEvent, event);\n          }\n        }}\n      >\n        {!isAllDay && (\n          <span\n            style={indicatorColorBarStyle}\n            className='df-event-year-indicator'\n          ></span>\n        )}\n        <span className='df-event-year-title df-event-year-title-strong df-event-year-title-fade'>\n          {titleText}\n        </span>\n\n        {segment.isLastSegment && !segment.isFirstSegment && (\n          <div className='df-event-year-tail'>\n            <div className='df-event-year-tail-dot'></div>\n          </div>\n        )}\n      </div>\n    );\n  };\n\n  return (\n    <>\n      {renderResizeHandle('left')}\n      {renderSlot ? renderSlot(renderContent()) : renderContent()}\n      {renderResizeHandle('right')}\n    </>\n  );\n};\n\nexport default YearEventContent;\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/components/__tests__/EventContent.test.tsx",
    "content": "import { render, screen } from '@testing-library/preact';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { EventContent } from '@/components/calendarEvent/components/EventContent';\nimport { ViewType, Event } from '@/types';\n\nconst baseEvent: Event = {\n  id: 'event-1',\n  title: 'All Day in Month',\n  calendarId: 'default',\n  allDay: false,\n  start: Temporal.ZonedDateTime.from('2026-04-07T09:00:00+00:00[UTC]'),\n  end: Temporal.ZonedDateTime.from('2026-04-07T10:00:00+00:00[UTC]'),\n};\n\ndescribe('EventContent', () => {\n  it('prefers the isAllDay prop for month view rendering', () => {\n    render(\n      <EventContent\n        event={baseEvent}\n        viewType={ViewType.MONTH}\n        isAllDay\n        isMultiDay={false}\n        segmentIndex={0}\n        isBeingDragged={false}\n        isBeingResized={false}\n        isEventSelected={false}\n        isPopping={false}\n        isEditable={false}\n        isDraggable={false}\n        canOpenDetail={true}\n        isTouchEnabled={false}\n        isMobile={false}\n        customRenderingStore={null}\n        eventContentSlotArgs={{}}\n      />\n    );\n\n    const title = screen.getByText('All Day in Month');\n    expect(title.className).toContain('df-event-month-title');\n  });\n\n  it('uses mask fade for month all-day titles on mobile', () => {\n    render(\n      <EventContent\n        event={baseEvent}\n        viewType={ViewType.MONTH}\n        isAllDay\n        isMultiDay={false}\n        segmentIndex={0}\n        isBeingDragged={false}\n        isBeingResized={false}\n        isEventSelected={false}\n        isPopping={false}\n        isEditable={false}\n        isDraggable={false}\n        canOpenDetail={true}\n        isTouchEnabled={false}\n        isMobile\n        customRenderingStore={null}\n        eventContentSlotArgs={{}}\n      />\n    );\n\n    const title = screen.getByText('All Day in Month');\n    expect(title.className).toContain('df-mobile-mask-fade');\n  });\n\n  it('uses mask fade for multi-day timed event titles on desktop', () => {\n    const multiDayTimedEvent: Event = {\n      ...baseEvent,\n      title: 'Multi-day Timed Event',\n      allDay: false,\n    };\n\n    const multiDaySegment = {\n      id: 'segment-1',\n      originalEventId: multiDayTimedEvent.id,\n      event: multiDayTimedEvent,\n      startDayIndex: 0,\n      endDayIndex: 2, // Spans 3 days in this row\n      isFirstSegment: true,\n      isLastSegment: false, // Not the last segment of the entire event to ensure it's multi-day\n      totalDays: 5,\n      segmentType: 'start' as const,\n      segmentIndex: 0,\n    };\n\n    render(\n      <EventContent\n        event={multiDayTimedEvent}\n        viewType={ViewType.MONTH}\n        isAllDay={false}\n        isMultiDay={true}\n        segment={multiDaySegment}\n        segmentIndex={0}\n        isBeingDragged={false}\n        isBeingResized={false}\n        isEventSelected={false}\n        isPopping={false}\n        isEditable={false}\n        isDraggable={false}\n        canOpenDetail={true}\n        isTouchEnabled={false}\n        isMobile={false}\n        customRenderingStore={null}\n        eventContentSlotArgs={{}}\n      />\n    );\n\n    const title = screen.getByText('Multi-day Timed Event');\n    // In MultiDayEvent, the title wrapper is a div with df-event-month-main class\n    const titleWrapper = title.closest('.df-event-month-main') as HTMLElement;\n\n    // The mask is now on this wrapper\n    const style = titleWrapper.style;\n    const maskValue = `${style.maskImage}${style.webkitMaskImage}`;\n    expect(maskValue).toContain('linear-gradient');\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/components/__tests__/RegularEventContent.test.tsx",
    "content": "import { render } from '@testing-library/preact';\nimport { Temporal } from 'temporal-polyfill';\n\nimport RegularEventContent from '@/components/calendarEvent/components/RegularEventContent';\n\ndescribe('RegularEventContent', () => {\n  it('keeps default density for multi-day timed segments', () => {\n    const event = {\n      id: 'event-1',\n      title: 'Cross-day Event',\n      start: Temporal.ZonedDateTime.from(\n        '2026-04-05T17:00:00+10:00[Australia/Sydney]'\n      ),\n      end: Temporal.ZonedDateTime.from(\n        '2026-04-06T05:00:00+10:00[Australia/Sydney]'\n      ),\n      calendarId: 'blue',\n      allDay: false,\n    };\n\n    const { container } = render(\n      <RegularEventContent\n        event={event}\n        isEditable={false}\n        isTouchEnabled={false}\n        isEventSelected={false}\n        multiDaySegmentInfo={{\n          startHour: 0,\n          endHour: 0.25,\n          isFirst: false,\n          isLast: true,\n          dayIndex: 1,\n        }}\n      />\n    );\n\n    const content = container.querySelector(\n      '.df-event-timed-content'\n    ) as HTMLElement | null;\n\n    expect(content).not.toBeNull();\n    expect(content!.dataset.density).toBe('default');\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/hooks/__tests__/useEventActions.test.tsx",
    "content": "import { render, fireEvent, act } from '@testing-library/preact';\nimport { useRef } from 'preact/hooks';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { useEventActions } from '@/components/calendarEvent/hooks/useEventActions';\nimport { Event, ViewType } from '@/types';\n\nconst baseEvent: Event = {\n  id: 'event-1',\n  title: 'Year event',\n  allDay: true,\n  calendarId: 'default',\n  start: Temporal.PlainDate.from('2026-03-24'),\n  end: Temporal.PlainDate.from('2026-03-24'),\n};\n\ninterface HarnessProps {\n  onEventSelect?: (eventId: string | null) => void;\n  onDetailPanelToggle?: (key: string | null) => void;\n}\n\nconst Harness = ({ onEventSelect, onDetailPanelToggle }: HarnessProps) => {\n  const selectedEventElementRef = useRef<HTMLElement | null>(null);\n  const handlers = useEventActions({\n    event: baseEvent,\n    viewType: ViewType.YEAR,\n    isAllDay: true,\n    isMultiDay: false,\n    calendarRef: { current: document.createElement('div') },\n    firstHour: 0,\n    hourHeight: 56,\n    isMobile: false,\n    canOpenDetail: true,\n    detailPanelKey: 'event-1::year-segment',\n    onEventSelect,\n    onDetailPanelToggle,\n    setIsSelected: jest.fn(),\n    setDetailPanelPosition: jest.fn(),\n    setContextMenuPosition: jest.fn(),\n    setActiveDayIndex: jest.fn(),\n    getClickedDayIdx: jest.fn(),\n    updatePanelPosition: jest.fn(),\n    selectedEventElementRef,\n  });\n\n  return (\n    <button\n      type='button'\n      data-testid='event'\n      onClick={handlers.handleClick}\n      onDblClick={handlers.handleDoubleClick}\n    >\n      Event\n    </button>\n  );\n};\n\ndescribe('useEventActions', () => {\n  beforeEach(() => {\n    jest.useFakeTimers();\n  });\n\n  afterEach(() => {\n    jest.runOnlyPendingTimers();\n    jest.useRealTimers();\n  });\n\n  it('delays year-view single click selection until the double-click window passes', () => {\n    const onEventSelect = jest.fn();\n    const onDetailPanelToggle = jest.fn();\n    const { getByTestId } = render(\n      <Harness\n        onEventSelect={onEventSelect}\n        onDetailPanelToggle={onDetailPanelToggle}\n      />\n    );\n\n    fireEvent.click(getByTestId('event'), { clientX: 24 });\n\n    expect(onEventSelect).not.toHaveBeenCalled();\n\n    act(() => {\n      jest.advanceTimersByTime(179);\n    });\n    expect(onEventSelect).not.toHaveBeenCalled();\n\n    act(() => {\n      jest.advanceTimersByTime(1);\n    });\n    expect(onEventSelect).toHaveBeenCalledWith('event-1');\n    expect(onDetailPanelToggle).toHaveBeenCalledWith(null);\n  });\n\n  it('cancels the pending year-view single click when the event is double-clicked', async () => {\n    const onEventSelect = jest.fn();\n    const onDetailPanelToggle = jest.fn();\n    const { getByTestId } = render(\n      <Harness\n        onEventSelect={onEventSelect}\n        onDetailPanelToggle={onDetailPanelToggle}\n      />\n    );\n\n    const eventButton = getByTestId('event');\n\n    fireEvent.click(eventButton, { clientX: 24 });\n\n    await act(async () => {\n      fireEvent.dblClick(eventButton, { clientX: 24 });\n      await Promise.resolve();\n    });\n\n    act(() => {\n      jest.advanceTimersByTime(180);\n    });\n\n    expect(onEventSelect).toHaveBeenCalledWith('event-1');\n    expect(onEventSelect).toHaveBeenCalledTimes(1);\n    expect(onDetailPanelToggle).toHaveBeenCalledWith('event-1::year-segment');\n    expect(onDetailPanelToggle).not.toHaveBeenCalledWith(null);\n  });\n\n  it('selects non-year events on double click before opening the detail panel', async () => {\n    const onEventSelect = jest.fn();\n    const onDetailPanelToggle = jest.fn();\n    const selectedEventElementRef = { current: null as HTMLElement | null };\n    const setIsSelected = jest.fn();\n\n    const DayHarness = () => {\n      const handlers = useEventActions({\n        event: baseEvent,\n        viewType: ViewType.DAY,\n        isAllDay: true,\n        isMultiDay: false,\n        calendarRef: { current: document.createElement('div') },\n        firstHour: 0,\n        hourHeight: 56,\n        isMobile: false,\n        canOpenDetail: true,\n        detailPanelKey: 'event-1',\n        onEventSelect,\n        onDetailPanelToggle,\n        setIsSelected,\n        setDetailPanelPosition: jest.fn(),\n        setContextMenuPosition: jest.fn(),\n        setActiveDayIndex: jest.fn(),\n        getClickedDayIdx: jest.fn(),\n        updatePanelPosition: jest.fn(),\n        selectedEventElementRef,\n      });\n\n      return (\n        <button\n          type='button'\n          data-testid='day-event'\n          onDblClick={handlers.handleDoubleClick}\n        >\n          Event\n        </button>\n      );\n    };\n\n    const { getByTestId } = render(<DayHarness />);\n\n    await act(async () => {\n      fireEvent.dblClick(getByTestId('day-event'), { clientX: 24 });\n      await Promise.resolve();\n    });\n\n    expect(onEventSelect).toHaveBeenCalledWith('event-1');\n    expect(onDetailPanelToggle).toHaveBeenCalledWith('event-1');\n    expect(setIsSelected).toHaveBeenCalledWith(true);\n  });\n\n  it('defers non-year click callbacks until the double-click window passes', () => {\n    const app = {\n      onEventClick: jest.fn(),\n    } as unknown as import('@/types').ICalendarApp;\n    const onEventSelect = jest.fn();\n    const onDetailPanelToggle = jest.fn();\n    const selectedEventElementRef = { current: null as HTMLElement | null };\n    const setIsSelected = jest.fn();\n\n    const DayHarness = () => {\n      const handlers = useEventActions({\n        event: baseEvent,\n        viewType: ViewType.DAY,\n        isAllDay: true,\n        isMultiDay: false,\n        app,\n        calendarRef: { current: document.createElement('div') },\n        firstHour: 0,\n        hourHeight: 56,\n        isMobile: false,\n        canOpenDetail: true,\n        detailPanelKey: 'event-1',\n        onEventSelect,\n        onDetailPanelToggle,\n        setIsSelected,\n        setDetailPanelPosition: jest.fn(),\n        setContextMenuPosition: jest.fn(),\n        setActiveDayIndex: jest.fn(),\n        getClickedDayIdx: jest.fn(),\n        updatePanelPosition: jest.fn(),\n        selectedEventElementRef,\n      });\n\n      return (\n        <button\n          type='button'\n          data-testid='day-click-event'\n          onClick={handlers.handleClick}\n        >\n          Event\n        </button>\n      );\n    };\n\n    const { getByTestId } = render(<DayHarness />);\n\n    fireEvent.click(getByTestId('day-click-event'), { clientX: 24 });\n\n    expect(onEventSelect).toHaveBeenCalledWith('event-1');\n    expect(onDetailPanelToggle).toHaveBeenCalledWith(null);\n    expect(app.onEventClick).not.toHaveBeenCalled();\n\n    act(() => {\n      jest.advanceTimersByTime(179);\n    });\n    expect(app.onEventClick).not.toHaveBeenCalled();\n\n    act(() => {\n      jest.advanceTimersByTime(1);\n    });\n    expect(app.onEventClick).toHaveBeenCalledWith(baseEvent);\n  });\n\n  it('suppresses click callbacks when a non-year event becomes a double click', async () => {\n    const app = {\n      onEventClick: jest.fn(),\n      onEventDoubleClick: jest.fn(),\n    } as unknown as import('@/types').ICalendarApp;\n    const onEventSelect = jest.fn();\n    const onDetailPanelToggle = jest.fn();\n    const selectedEventElementRef = { current: null as HTMLElement | null };\n    const setIsSelected = jest.fn();\n\n    const DayHarness = () => {\n      const handlers = useEventActions({\n        event: baseEvent,\n        viewType: ViewType.DAY,\n        isAllDay: true,\n        isMultiDay: false,\n        app,\n        calendarRef: { current: document.createElement('div') },\n        firstHour: 0,\n        hourHeight: 56,\n        isMobile: false,\n        canOpenDetail: true,\n        detailPanelKey: 'event-1',\n        onEventSelect,\n        onDetailPanelToggle,\n        setIsSelected,\n        setDetailPanelPosition: jest.fn(),\n        setContextMenuPosition: jest.fn(),\n        setActiveDayIndex: jest.fn(),\n        getClickedDayIdx: jest.fn(),\n        updatePanelPosition: jest.fn(),\n        selectedEventElementRef,\n      });\n\n      return (\n        <button\n          type='button'\n          data-testid='day-double-event'\n          onClick={handlers.handleClick}\n          onDblClick={handlers.handleDoubleClick}\n        >\n          Event\n        </button>\n      );\n    };\n\n    const { getByTestId } = render(<DayHarness />);\n    const eventButton = getByTestId('day-double-event');\n\n    fireEvent.click(eventButton, { clientX: 24 });\n    fireEvent.click(eventButton, { clientX: 24 });\n\n    await act(async () => {\n      fireEvent.dblClick(eventButton, { clientX: 24 });\n      await Promise.resolve();\n    });\n\n    act(() => {\n      jest.advanceTimersByTime(180);\n    });\n\n    expect(app.onEventClick).not.toHaveBeenCalled();\n    expect(app.onEventDoubleClick).toHaveBeenCalledWith(\n      baseEvent,\n      expect.any(MouseEvent)\n    );\n    expect(onEventSelect).toHaveBeenCalledWith('event-1');\n    expect(onDetailPanelToggle).toHaveBeenCalledWith('event-1');\n  });\n\n  it('allows onEventDoubleClick to suppress the default detail panel', async () => {\n    const app = {\n      onEventClick: jest.fn(),\n      onEventDoubleClick: jest.fn(() => false),\n    } as unknown as import('@/types').ICalendarApp;\n    const onEventSelect = jest.fn();\n    const onDetailPanelToggle = jest.fn();\n    const selectedEventElementRef = { current: null as HTMLElement | null };\n    const setIsSelected = jest.fn();\n\n    const DayHarness = () => {\n      const handlers = useEventActions({\n        event: baseEvent,\n        viewType: ViewType.DAY,\n        isAllDay: true,\n        isMultiDay: false,\n        app,\n        calendarRef: { current: document.createElement('div') },\n        firstHour: 0,\n        hourHeight: 56,\n        isMobile: false,\n        canOpenDetail: true,\n        detailPanelKey: 'event-1',\n        onEventSelect,\n        onDetailPanelToggle,\n        setIsSelected,\n        setDetailPanelPosition: jest.fn(),\n        setContextMenuPosition: jest.fn(),\n        setActiveDayIndex: jest.fn(),\n        getClickedDayIdx: jest.fn(),\n        updatePanelPosition: jest.fn(),\n        selectedEventElementRef,\n      });\n\n      return (\n        <button\n          type='button'\n          data-testid='day-event-suppress'\n          onDblClick={handlers.handleDoubleClick}\n        >\n          Event\n        </button>\n      );\n    };\n\n    const { getByTestId } = render(<DayHarness />);\n\n    await act(async () => {\n      fireEvent.dblClick(getByTestId('day-event-suppress'), { clientX: 24 });\n      await Promise.resolve();\n    });\n\n    expect(app.onEventClick).not.toHaveBeenCalled();\n    expect(app.onEventDoubleClick).toHaveBeenCalledWith(\n      baseEvent,\n      expect.any(MouseEvent)\n    );\n    expect(onEventSelect).toHaveBeenCalledWith('event-1');\n    expect(setIsSelected).toHaveBeenCalledWith(true);\n    expect(onDetailPanelToggle).not.toHaveBeenCalled();\n  });\n\n  it('waits for resource-view scrolling to settle before opening the detail panel', async () => {\n    const app = {\n      onEventClick: jest.fn(),\n    } as unknown as import('@/types').ICalendarApp;\n    const onEventSelect = jest.fn();\n    const onDetailPanelToggle = jest.fn();\n    const setIsSelected = jest.fn();\n    const setDetailPanelPosition = jest.fn();\n    const selectedEventElementRef = { current: null as HTMLElement | null };\n    const calendarContent = document.createElement('div');\n    calendarContent.className = 'df-calendar-content';\n    calendarContent.scrollLeft = 0;\n    calendarContent.scrollTop = 0;\n    Object.defineProperty(calendarContent, 'clientWidth', {\n      value: 320,\n      configurable: true,\n    });\n    Object.defineProperty(calendarContent, 'clientHeight', {\n      value: 240,\n      configurable: true,\n    });\n    Object.defineProperty(calendarContent, 'scrollWidth', {\n      value: 1200,\n      configurable: true,\n    });\n    Object.defineProperty(calendarContent, 'scrollHeight', {\n      value: 1400,\n      configurable: true,\n    });\n    calendarContent.getBoundingClientRect = jest.fn(() => ({\n      left: 0,\n      top: 0,\n      right: 320,\n      bottom: 240,\n      width: 320,\n      height: 240,\n      x: 0,\n      y: 0,\n      toJSON: () => ({}),\n    })) as unknown as typeof calendarContent.getBoundingClientRect;\n    const scrollToMock = jest.fn((left: number, top: number) => {\n      calendarContent.scrollLeft = left;\n      calendarContent.scrollTop = top;\n    });\n    calendarContent.scrollTo =\n      scrollToMock as unknown as typeof calendarContent.scrollTo;\n\n    const timedEvent: Event = {\n      ...baseEvent,\n      allDay: false,\n      start: Temporal.ZonedDateTime.from(\n        '2026-03-24T10:00:00+11:00[Australia/Sydney]'\n      ),\n      end: Temporal.ZonedDateTime.from(\n        '2026-03-24T11:00:00+11:00[Australia/Sydney]'\n      ),\n    };\n\n    const ResourceHarness = () => {\n      const handlers = useEventActions({\n        event: timedEvent,\n        viewType: ViewType.RESOURCE,\n        isAllDay: false,\n        isMultiDay: false,\n        app,\n        calendarRef: { current: calendarContent },\n        firstHour: 0,\n        hourHeight: 56,\n        isMobile: false,\n        canOpenDetail: true,\n        detailPanelKey: 'event-1::resource',\n        onEventSelect,\n        onDetailPanelToggle,\n        setIsSelected,\n        setDetailPanelPosition,\n        setContextMenuPosition: jest.fn(),\n        setActiveDayIndex: jest.fn(),\n        getClickedDayIdx: jest.fn(),\n        updatePanelPosition: jest.fn(),\n        selectedEventElementRef,\n      });\n\n      return (\n        <button\n          type='button'\n          data-testid='resource-event'\n          onDblClick={handlers.handleDoubleClick}\n          ref={element => {\n            // eslint-disable-next-line @typescript-eslint/no-unused-expressions, jest/no-conditional-in-test\n            element &&\n              (element.getBoundingClientRect = jest.fn(() => ({\n                left: 680,\n                top: 420,\n                right: 780,\n                bottom: 480,\n                width: 100,\n                height: 60,\n                x: 680,\n                y: 420,\n                toJSON: () => ({}),\n              })) as unknown as typeof element.getBoundingClientRect);\n          }}\n        >\n          Event\n        </button>\n      );\n    };\n\n    const { getByTestId } = render(<ResourceHarness />);\n\n    await act(async () => {\n      fireEvent.dblClick(getByTestId('resource-event'), { clientX: 720 });\n      await Promise.resolve();\n    });\n\n    expect(scrollToMock).toHaveBeenCalled();\n    expect(app.onEventClick).not.toHaveBeenCalled();\n    expect(onEventSelect).toHaveBeenCalledWith('event-1');\n    expect(setIsSelected).toHaveBeenCalledWith(true);\n    expect(onDetailPanelToggle).not.toHaveBeenCalled();\n\n    act(() => {\n      jest.advanceTimersByTime(159);\n    });\n    expect(onDetailPanelToggle).not.toHaveBeenCalled();\n\n    await act(async () => {\n      jest.advanceTimersByTime(1);\n      await Promise.resolve();\n    });\n\n    expect(onDetailPanelToggle).toHaveBeenCalledWith('event-1::resource');\n    expect(setDetailPanelPosition).toHaveBeenCalledWith(\n      expect.objectContaining({ left: -9999, top: -9999 })\n    );\n  });\n\n  it('does not auto-scroll resource-view events that are already fully visible', async () => {\n    const app = {\n      onEventClick: jest.fn(),\n    } as unknown as import('@/types').ICalendarApp;\n    const onEventSelect = jest.fn();\n    const onDetailPanelToggle = jest.fn();\n    const setIsSelected = jest.fn();\n    const setDetailPanelPosition = jest.fn();\n    const selectedEventElementRef = { current: null as HTMLElement | null };\n    const calendarContent = document.createElement('div');\n    calendarContent.className = 'df-calendar-content';\n    Object.defineProperty(calendarContent, 'clientWidth', {\n      value: 320,\n      configurable: true,\n    });\n    Object.defineProperty(calendarContent, 'clientHeight', {\n      value: 240,\n      configurable: true,\n    });\n    calendarContent.getBoundingClientRect = jest.fn(() => ({\n      left: 0,\n      top: 0,\n      right: 320,\n      bottom: 240,\n      width: 320,\n      height: 240,\n      x: 0,\n      y: 0,\n      toJSON: () => ({}),\n    })) as unknown as typeof calendarContent.getBoundingClientRect;\n    const scrollToMock = jest.fn();\n    calendarContent.scrollTo =\n      scrollToMock as unknown as typeof calendarContent.scrollTo;\n\n    const timedEvent: Event = {\n      ...baseEvent,\n      allDay: false,\n      start: Temporal.ZonedDateTime.from(\n        '2026-03-24T10:00:00+11:00[Australia/Sydney]'\n      ),\n      end: Temporal.ZonedDateTime.from(\n        '2026-03-24T11:00:00+11:00[Australia/Sydney]'\n      ),\n    };\n\n    const ResourceHarness = () => {\n      const handlers = useEventActions({\n        event: timedEvent,\n        viewType: ViewType.RESOURCE,\n        isAllDay: false,\n        isMultiDay: false,\n        app,\n        calendarRef: { current: calendarContent },\n        firstHour: 0,\n        hourHeight: 56,\n        isMobile: false,\n        canOpenDetail: true,\n        detailPanelKey: 'event-1::resource',\n        onEventSelect,\n        onDetailPanelToggle,\n        setIsSelected,\n        setDetailPanelPosition,\n        setContextMenuPosition: jest.fn(),\n        setActiveDayIndex: jest.fn(),\n        getClickedDayIdx: jest.fn(),\n        updatePanelPosition: jest.fn(),\n        selectedEventElementRef,\n      });\n\n      return (\n        <button\n          type='button'\n          data-testid='resource-visible-event'\n          onDblClick={handlers.handleDoubleClick}\n          ref={element => {\n            // eslint-disable-next-line @typescript-eslint/no-unused-expressions, jest/no-conditional-in-test\n            element &&\n              (element.getBoundingClientRect = jest.fn(() => ({\n                left: 80,\n                top: 60,\n                right: 180,\n                bottom: 120,\n                width: 100,\n                height: 60,\n                x: 80,\n                y: 60,\n                toJSON: () => ({}),\n              })) as unknown as typeof element.getBoundingClientRect);\n          }}\n        >\n          Event\n        </button>\n      );\n    };\n\n    const { getByTestId } = render(<ResourceHarness />);\n\n    await act(async () => {\n      fireEvent.dblClick(getByTestId('resource-visible-event'), {\n        clientX: 120,\n      });\n      await Promise.resolve();\n    });\n\n    expect(scrollToMock).not.toHaveBeenCalled();\n    expect(app.onEventClick).not.toHaveBeenCalled();\n    expect(onEventSelect).toHaveBeenCalledWith('event-1');\n    expect(setIsSelected).toHaveBeenCalledWith(true);\n    expect(onDetailPanelToggle).toHaveBeenCalledWith('event-1::resource');\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/hooks/useClickOutside.ts",
    "content": "import { RefObject } from 'preact';\nimport { useEffect } from 'preact/hooks';\n\ninterface UseClickOutsideProps {\n  eventRef: RefObject<HTMLElement>;\n  detailPanelRef: RefObject<HTMLElement>;\n  eventId: string;\n  isEventSelected: boolean;\n  showDetailPanel: boolean;\n  onEventSelect?: (id: string | null) => void;\n  onDetailPanelToggle?: (key: string | null) => void;\n  setIsSelected: (selected: boolean) => void;\n  setActiveDayIndex: (index: number | null) => void;\n}\n\nexport const useClickOutside = ({\n  eventRef,\n  detailPanelRef,\n  eventId,\n  isEventSelected,\n  showDetailPanel,\n  onEventSelect,\n  onDetailPanelToggle,\n  setIsSelected,\n  setActiveDayIndex,\n}: UseClickOutsideProps) => {\n  useEffect(() => {\n    if (!isEventSelected && !showDetailPanel) return;\n\n    const handleClickOutside = (e: MouseEvent) => {\n      const target = e.target as HTMLElement;\n      const clickedInsideEvent = eventRef.current?.contains(target);\n      const clickedOnSameEvent =\n        target.closest(`[data-event-id=\"${eventId}\"]`) !== null;\n      const clickedInsidePanel = detailPanelRef.current?.contains(target);\n      const clickedInsideDetailDialog = target.closest(\n        '[data-event-detail-dialog]'\n      );\n      const clickedInsideRangePickerPopup = target.closest(\n        '[data-range-picker-popup]'\n      );\n      const clickedInsideCalendarPickerDropdown = target.closest(\n        '[data-calendar-picker-dropdown]'\n      );\n\n      if (showDetailPanel) {\n        if (\n          !clickedInsideEvent &&\n          !clickedOnSameEvent &&\n          !clickedInsidePanel &&\n          !clickedInsideDetailDialog &&\n          !clickedInsideRangePickerPopup &&\n          !clickedInsideCalendarPickerDropdown\n        ) {\n          onEventSelect?.(null);\n          setActiveDayIndex(null);\n          setIsSelected(false);\n          onDetailPanelToggle?.(null);\n        }\n      } else if (\n        isEventSelected &&\n        !clickedInsideEvent &&\n        !clickedOnSameEvent &&\n        !clickedInsideDetailDialog &&\n        !clickedInsideRangePickerPopup &&\n        !clickedInsideCalendarPickerDropdown\n      ) {\n        onEventSelect?.(null);\n        setActiveDayIndex(null);\n        setIsSelected(false);\n        onDetailPanelToggle?.(null);\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => {\n      document.removeEventListener('mousedown', handleClickOutside);\n    };\n  }, [\n    isEventSelected,\n    showDetailPanel,\n    onEventSelect,\n    onDetailPanelToggle,\n    eventId,\n  ]);\n};\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/hooks/useDetailPanelPosition.ts",
    "content": "import { RefObject } from 'preact';\nimport { useState, useCallback, useEffect } from 'preact/hooks';\n\nimport { getTimeColumnWidth } from '@/components/calendarEvent/utils';\nimport { MultiDayEventSegment } from '@/components/monthView/util';\nimport { YearMultiDaySegment } from '@/components/yearView/utils';\nimport { Event, ViewType, EventDetailPosition } from '@/types';\n\ninterface UseDetailPanelPositionProps {\n  event: Event;\n  viewType: ViewType;\n  isMultiDay: boolean;\n  segment?: MultiDayEventSegment;\n  yearSegment?: YearMultiDaySegment;\n  multiDaySegmentInfo?: {\n    startHour: number;\n    endHour: number;\n    isFirst: boolean;\n    isLast: boolean;\n    dayIndex?: number;\n  };\n  calendarRef: RefObject<HTMLElement>;\n  eventRef: RefObject<HTMLElement>;\n  detailPanelRef: RefObject<HTMLElement>;\n  selectedEventElementRef: RefObject<HTMLElement>;\n  isMobile: boolean;\n  eventVisibility:\n    | 'standard'\n    | 'sticky-top'\n    | 'sticky-bottom'\n    | 'sticky-left'\n    | 'sticky-right';\n  firstHour: number;\n  hourHeight: number;\n  columnsPerRow?: number;\n  showDetailPanel: boolean;\n  detailPanelEventId?: string | null;\n  detailPanelKey: string;\n  getActiveDayIdx: () => number;\n  getDayMetricsWrapper: (\n    dayIndex: number\n  ) => { left: number; width: number } | null;\n}\n\nexport const useDetailPanelPosition = ({\n  event,\n  viewType,\n  isMultiDay,\n  segment,\n  yearSegment,\n  multiDaySegmentInfo,\n  calendarRef,\n  eventRef,\n  detailPanelRef,\n  selectedEventElementRef,\n  isMobile,\n  eventVisibility,\n  firstHour,\n  hourHeight,\n  columnsPerRow,\n  showDetailPanel,\n  detailPanelEventId,\n  detailPanelKey,\n  getActiveDayIdx,\n  getDayMetricsWrapper,\n}: UseDetailPanelPositionProps) => {\n  const [detailPanelPosition, setDetailPanelPosition] =\n    useState<EventDetailPosition | null>(null);\n\n  const isDayView = viewType === ViewType.DAY;\n  const isMonthView = viewType === ViewType.MONTH;\n  const isYearView = viewType === ViewType.YEAR;\n  const isResourceView = viewType === ViewType.RESOURCE;\n\n  const updatePanelPosition = useCallback(() => {\n    if (\n      (!selectedEventElementRef.current && !eventRef.current) ||\n      !calendarRef.current ||\n      !detailPanelRef.current\n    )\n      return;\n\n    const calendarRect = calendarRef.current.getBoundingClientRect();\n    const positionDayIndex = getActiveDayIdx();\n    const metricsForPosition = getDayMetricsWrapper(positionDayIndex);\n\n    let dayStartX: number;\n    let dayColumnWidth: number;\n\n    if (metricsForPosition) {\n      dayStartX = metricsForPosition.left;\n      dayColumnWidth = metricsForPosition.width;\n    } else if (isMonthView) {\n      dayColumnWidth = calendarRect.width / 7;\n      dayStartX = calendarRect.left + positionDayIndex * dayColumnWidth;\n    } else {\n      const timeColumnWidth = getTimeColumnWidth(calendarRef, isMobile);\n      dayColumnWidth = (calendarRect.width - timeColumnWidth) / 7;\n      dayStartX =\n        calendarRect.left + timeColumnWidth + positionDayIndex * dayColumnWidth;\n    }\n\n    const boundaryWidth = Math.min(window.innerWidth, calendarRect.right);\n    const boundaryHeight = Math.min(window.innerHeight, calendarRect.bottom);\n\n    requestAnimationFrame(() => {\n      if (!detailPanelRef.current) return;\n      const eventElement = selectedEventElementRef.current || eventRef.current;\n      if (!eventElement) return;\n\n      const panelRect = detailPanelRef.current.getBoundingClientRect();\n      const panelWidth = panelRect.width;\n      const panelHeight = panelRect.height;\n\n      let left: number, top: number;\n      let eventRect: DOMRect;\n\n      if (\n        eventVisibility === 'sticky-top' ||\n        eventVisibility === 'sticky-bottom' ||\n        eventVisibility === 'sticky-left' ||\n        eventVisibility === 'sticky-right'\n      ) {\n        const actualEventRect = eventRef.current?.getBoundingClientRect();\n        if (!actualEventRect) return;\n\n        eventRect = actualEventRect;\n      } else {\n        eventRect = eventElement.getBoundingClientRect();\n      }\n\n      if (isMonthView && isMultiDay && segment) {\n        const metrics = getDayMetricsWrapper(positionDayIndex);\n        const currentDayColumnWidth = metrics?.width ?? calendarRect.width / 7;\n        const selectedDayLeft =\n          metrics?.left ??\n          calendarRect.left + positionDayIndex * currentDayColumnWidth;\n        const selectedDayRight = selectedDayLeft + currentDayColumnWidth;\n        eventRect = {\n          top: eventRect.top,\n          bottom: eventRect.bottom,\n          left: selectedDayLeft,\n          right: selectedDayRight,\n          width: selectedDayRight - selectedDayLeft,\n          height: eventRect.height,\n          x: selectedDayLeft,\n          y: eventRect.top,\n          toJSON: () => ({}),\n        } as DOMRect;\n      }\n\n      const spaceOnRight = boundaryWidth - eventRect.right;\n      const spaceOnLeft = eventRect.left - calendarRect.left;\n\n      if (spaceOnRight >= panelWidth + 20) {\n        left = eventRect.right + 10;\n      } else if (spaceOnLeft >= panelWidth + 20) {\n        left = eventRect.left - panelWidth - 10;\n      } else {\n        left =\n          spaceOnRight > spaceOnLeft\n            ? Math.max(calendarRect.left + 10, boundaryWidth - panelWidth - 10)\n            : calendarRect.left + 10;\n      }\n\n      const idealTop = eventRect.top - panelHeight / 2 + eventRect.height / 2;\n      const topBoundary = Math.max(10, calendarRect.top + 10);\n      const bottomBoundary = boundaryHeight - 10;\n      top =\n        idealTop < topBoundary\n          ? topBoundary\n          : idealTop + panelHeight > bottomBoundary\n            ? bottomBoundary - panelHeight\n            : idealTop;\n\n      setDetailPanelPosition(prev => {\n        if (!prev) return null;\n        let isSunday = left < dayStartX;\n        if (isYearView || isResourceView) {\n          isSunday = left < eventRect.left;\n        }\n        return { ...prev, top, left, isSunday };\n      });\n    });\n  }, [\n    calendarRef,\n    event.day,\n    event.start,\n    event.end,\n    eventVisibility,\n    isMonthView,\n    firstHour,\n    hourHeight,\n    isMultiDay,\n    segment,\n    multiDaySegmentInfo,\n    detailPanelEventId,\n    detailPanelKey,\n    isMobile,\n    isDayView,\n    isYearView,\n    yearSegment,\n    columnsPerRow,\n    getActiveDayIdx,\n    getDayMetricsWrapper,\n    selectedEventElementRef,\n    detailPanelRef,\n    eventRef,\n  ]);\n\n  useEffect(() => {\n    if (showDetailPanel && !detailPanelPosition && !isMobile) {\n      setDetailPanelPosition({\n        top: -9999,\n        left: -9999,\n        eventHeight: 0,\n        eventMiddleY: 0,\n        isSunday: false,\n      });\n      requestAnimationFrame(() => updatePanelPosition());\n    }\n  }, [showDetailPanel, detailPanelPosition, updatePanelPosition, isMobile]);\n\n  return {\n    detailPanelPosition,\n    setDetailPanelPosition,\n    updatePanelPosition,\n  };\n};\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/hooks/useEventActions.ts",
    "content": "import { RefObject } from 'preact';\nimport { useCallback, useEffect, useRef, useState } from 'preact/hooks';\n\nimport { getCalendarContentElement } from '@/components/calendarEvent/utils';\nimport { MultiDayEventSegment } from '@/components/monthView/util';\nimport { Event, ViewType, ICalendarApp, EventDetailPosition } from '@/types';\nimport { extractHourFromDate, getEventEndHour } from '@/utils';\nimport { logger } from '@/utils/logger';\n\nconst SINGLE_CLICK_DELAY_MS = 180;\n\ninterface UseEventActionsProps {\n  event: Event;\n  timingEvent?: Event;\n  viewType: ViewType;\n  isAllDay: boolean;\n  isMultiDay: boolean;\n  segment?: MultiDayEventSegment;\n  multiDaySegmentInfo?: {\n    startHour: number;\n    endHour: number;\n    isFirst: boolean;\n    isLast: boolean;\n    dayIndex?: number;\n  };\n  calendarRef: RefObject<HTMLElement>;\n  firstHour: number;\n  hourHeight: number;\n  isMobile: boolean;\n  canOpenDetail: boolean;\n  useEventDetailPanel?: boolean;\n  detailPanelKey: string;\n  app?: ICalendarApp;\n  onEventSelect?: (eventId: string | null) => void;\n  onDetailPanelToggle?: (key: string | null) => void;\n  setIsSelected: (selected: boolean) => void;\n  setDetailPanelPosition: (pos: EventDetailPosition | null) => void;\n  setContextMenuPosition: (pos: { x: number; y: number } | null) => void;\n  setActiveDayIndex: (index: number | null) => void;\n  getClickedDayIdx: (clientX: number) => number | null;\n  updatePanelPosition: () => void;\n  selectedEventElementRef: RefObject<HTMLElement | null>;\n}\n\nexport const useEventActions = ({\n  event,\n  timingEvent,\n  viewType,\n  isAllDay,\n  isMultiDay,\n  segment,\n  multiDaySegmentInfo,\n  calendarRef,\n  firstHour,\n  hourHeight,\n  isMobile,\n  canOpenDetail,\n  useEventDetailPanel,\n  detailPanelKey,\n  app,\n  onEventSelect,\n  onDetailPanelToggle,\n  setIsSelected,\n  setDetailPanelPosition,\n  setContextMenuPosition,\n  setActiveDayIndex,\n  getClickedDayIdx,\n  updatePanelPosition,\n  selectedEventElementRef,\n}: UseEventActionsProps) => {\n  const isMonthView = viewType === ViewType.MONTH;\n  const isYearView = viewType === ViewType.YEAR;\n  const isResourceView = viewType === ViewType.RESOURCE;\n  const eventForTiming = timingEvent ?? event;\n  const clickTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const [hasPendingSelection, setHasPendingSelection] = useState(false);\n\n  const clearPendingClick = useCallback(() => {\n    if (!clickTimeoutRef.current) return;\n    clearTimeout(clickTimeoutRef.current);\n    clickTimeoutRef.current = null;\n    setHasPendingSelection(false);\n  }, []);\n\n  useEffect(() => () => clearPendingClick(), [clearPendingClick]);\n\n  const waitForScrollSettled = useCallback(\n    (\n      scrollContainer: HTMLElement,\n      initialScrollLeft: number,\n      initialScrollTop: number\n    ): Promise<void> =>\n      new Promise(resolve => {\n        const sampleIntervalMs = 40;\n        const quietWindowMs = 120;\n        const timeoutMs = 600;\n        let quietForMs = 0;\n        let elapsedMs = 0;\n        let lastScrollLeft = initialScrollLeft;\n        let lastScrollTop = initialScrollTop;\n\n        const checkScrollState = () => {\n          const nextScrollLeft = scrollContainer.scrollLeft;\n          const nextScrollTop = scrollContainer.scrollTop;\n          const didMove =\n            Math.abs(nextScrollLeft - lastScrollLeft) > 1 ||\n            Math.abs(nextScrollTop - lastScrollTop) > 1;\n\n          quietForMs = didMove ? 0 : quietForMs + sampleIntervalMs;\n          elapsedMs += sampleIntervalMs;\n          lastScrollLeft = nextScrollLeft;\n          lastScrollTop = nextScrollTop;\n\n          if (quietForMs >= quietWindowMs || elapsedMs >= timeoutMs) {\n            resolve();\n            return;\n          }\n\n          setTimeout(checkScrollState, sampleIntervalMs);\n        };\n\n        setTimeout(checkScrollState, sampleIntervalMs);\n      }),\n    []\n  );\n\n  const scrollEventToCenter = useCallback(\n    (): Promise<void> =>\n      new Promise(resolve => {\n        if (\n          !calendarRef.current ||\n          isAllDay ||\n          isMonthView ||\n          isYearView ||\n          hourHeight <= 0\n        ) {\n          resolve();\n          return;\n        }\n        const calendarContent = getCalendarContentElement(calendarRef);\n        if (!calendarContent) {\n          resolve();\n          return;\n        }\n\n        if (isResourceView && selectedEventElementRef.current) {\n          const eventRect =\n            selectedEventElementRef.current.getBoundingClientRect();\n          const contentRect = calendarContent.getBoundingClientRect();\n          const isFullyVisibleInViewport =\n            eventRect.left >= contentRect.left &&\n            eventRect.right <= contentRect.right &&\n            eventRect.top >= contentRect.top &&\n            eventRect.bottom <= contentRect.bottom;\n\n          if (isFullyVisibleInViewport) {\n            resolve();\n            return;\n          }\n\n          const initialScrollLeft = calendarContent.scrollLeft;\n          const initialScrollTop = calendarContent.scrollTop;\n          const targetScrollLeft =\n            initialScrollLeft +\n            (eventRect.left - contentRect.left) -\n            (calendarContent.clientWidth - eventRect.width) / 2;\n          const targetScrollTop =\n            initialScrollTop +\n            (eventRect.top - contentRect.top) -\n            (calendarContent.clientHeight - eventRect.height) / 2;\n          const maxScrollLeft = Math.max(\n            0,\n            calendarContent.scrollWidth - calendarContent.clientWidth\n          );\n          const maxScrollTop = Math.max(\n            0,\n            calendarContent.scrollHeight - calendarContent.clientHeight\n          );\n          const nextScrollLeft = Math.max(\n            0,\n            Math.min(maxScrollLeft, targetScrollLeft)\n          );\n          const nextScrollTop = Math.max(\n            0,\n            Math.min(maxScrollTop, targetScrollTop)\n          );\n          const needsScroll =\n            Math.abs(nextScrollLeft - initialScrollLeft) > 1 ||\n            Math.abs(nextScrollTop - initialScrollTop) > 1;\n\n          if (!needsScroll) {\n            resolve();\n            return;\n          }\n\n          calendarContent.scrollTo({\n            left: nextScrollLeft,\n            top: nextScrollTop,\n            behavior: 'smooth',\n          });\n\n          waitForScrollSettled(\n            calendarContent,\n            initialScrollLeft,\n            initialScrollTop\n          ).then(resolve);\n          return;\n        }\n\n        const segmentStartHour = multiDaySegmentInfo\n          ? multiDaySegmentInfo.startHour\n          : extractHourFromDate(eventForTiming.start);\n        const segmentEndHour = multiDaySegmentInfo\n          ? multiDaySegmentInfo.endHour\n          : getEventEndHour(eventForTiming);\n\n        const eventTop = (segmentStartHour - firstHour) * hourHeight;\n        const eventHeight = Math.max(\n          (segmentEndHour - segmentStartHour) * hourHeight,\n          hourHeight / 4\n        );\n        const eventBottom = eventTop + eventHeight;\n\n        const scrollTop = calendarContent.scrollTop;\n        const viewportHeight = calendarContent.clientHeight;\n        const scrollBottom = scrollTop + viewportHeight;\n\n        if (eventTop >= scrollTop && eventBottom <= scrollBottom) {\n          resolve();\n          return;\n        }\n\n        const eventMiddleHour = (segmentStartHour + segmentEndHour) / 2;\n        const targetScrollTop =\n          (eventMiddleHour - firstHour) * hourHeight - viewportHeight / 2;\n        const maxScrollTop = calendarContent.scrollHeight - viewportHeight;\n        const nextScrollTop = Math.max(\n          0,\n          Math.min(maxScrollTop, targetScrollTop)\n        );\n\n        if (Math.abs(nextScrollTop - scrollTop) <= 1) {\n          resolve();\n          return;\n        }\n\n        calendarContent.scrollTo({\n          top: nextScrollTop,\n          behavior: 'smooth',\n        });\n\n        waitForScrollSettled(\n          calendarContent,\n          calendarContent.scrollLeft,\n          scrollTop\n        ).then(resolve);\n      }),\n    [\n      calendarRef,\n      isAllDay,\n      isMonthView,\n      isYearView,\n      isResourceView,\n      multiDaySegmentInfo,\n      eventForTiming.start,\n      eventForTiming.end,\n      firstHour,\n      hourHeight,\n      selectedEventElementRef,\n      waitForScrollSettled,\n    ]\n  );\n\n  const handleContextMenu = useCallback(\n    (e: MouseEvent) => {\n      clearPendingClick();\n      e.preventDefault();\n      e.stopPropagation();\n      if (app && !app.canMutateFromUI(event.id)) return;\n      if (onEventSelect) onEventSelect(event.id);\n      setContextMenuPosition({ x: e.clientX, y: e.clientY });\n    },\n    [app, clearPendingClick, event.id, onEventSelect, setContextMenuPosition]\n  );\n\n  const applySingleClickSelection = useCallback(\n    (clientX: number) => {\n      if (isMultiDay) {\n        const clickedDay = getClickedDayIdx(clientX);\n        setActiveDayIndex(\n          clickedDay === null\n            ? (multiDaySegmentInfo?.dayIndex ?? event.day ?? null)\n            : segment\n              ? Math.min(\n                  Math.max(clickedDay, segment.startDayIndex),\n                  segment.endDayIndex\n                )\n              : clickedDay\n        );\n      } else {\n        setActiveDayIndex(event.day ?? null);\n      }\n\n      if (onEventSelect) {\n        onEventSelect(event.id);\n      } else if (canOpenDetail) {\n        setIsSelected(true);\n      }\n\n      if (useEventDetailPanel !== false) {\n        onDetailPanelToggle?.(null);\n        setDetailPanelPosition(null);\n      }\n    },\n    [\n      isMultiDay,\n      getClickedDayIdx,\n      setActiveDayIndex,\n      multiDaySegmentInfo?.dayIndex,\n      event,\n      segment,\n      onEventSelect,\n      canOpenDetail,\n      setIsSelected,\n      onDetailPanelToggle,\n      setDetailPanelPosition,\n    ]\n  );\n\n  const emitSingleClick = useCallback(() => {\n    app?.onEventClick(event);\n  }, [app, event]);\n\n  const performSingleClick = useCallback(\n    (clientX: number) => {\n      applySingleClickSelection(clientX);\n      emitSingleClick();\n    },\n    [applySingleClickSelection, emitSingleClick]\n  );\n\n  const handleDoubleClick = useCallback(\n    (e: MouseEvent) => {\n      clearPendingClick();\n      e.preventDefault();\n      e.stopPropagation();\n      if (!canOpenDetail) return;\n\n      let targetElement = e.currentTarget as HTMLDivElement;\n      if (isMultiDay) {\n        const multiDayElement = targetElement.querySelector(\n          'div'\n        ) as HTMLDivElement;\n        if (multiDayElement) targetElement = multiDayElement;\n      }\n\n      selectedEventElementRef.current = targetElement;\n\n      if (isMultiDay) {\n        const clickedDay = getClickedDayIdx(e.clientX);\n        setActiveDayIndex(\n          clickedDay === null\n            ? (segment?.startDayIndex ?? event.day ?? 0)\n            : Math.min(\n                Math.max(clickedDay, segment?.startDayIndex ?? 0),\n                segment?.endDayIndex ?? 6\n              )\n        );\n      } else {\n        setActiveDayIndex(event.day ?? null);\n      }\n\n      setIsSelected(true);\n      onEventSelect?.(event.id);\n\n      const openDetailPanel = () => {\n        scrollEventToCenter().then(() => {\n          if (useEventDetailPanel === false) return;\n\n          if (!isMobile) {\n            onDetailPanelToggle?.(detailPanelKey);\n            setDetailPanelPosition({\n              top: -9999,\n              left: -9999,\n              eventHeight: 0,\n              eventMiddleY: 0,\n              isSunday: false,\n            });\n            requestAnimationFrame(() => updatePanelPosition());\n          }\n        });\n      };\n\n      if (!app) {\n        openDetailPanel();\n        return;\n      }\n\n      Promise.resolve(app.onEventDoubleClick?.(event, e))\n        .then(result => {\n          if (result === false) return;\n          openDetailPanel();\n        })\n        .catch(error => {\n          logger.error('Failed to handle event double click callback', error);\n          openDetailPanel();\n        });\n    },\n    [\n      clearPendingClick,\n      canOpenDetail,\n      isMultiDay,\n      selectedEventElementRef,\n      getClickedDayIdx,\n      setActiveDayIndex,\n      segment?.startDayIndex,\n      segment?.endDayIndex,\n      event.day,\n      app,\n      event,\n      scrollEventToCenter,\n      setIsSelected,\n      isYearView,\n      isMobile,\n      onEventSelect,\n      onDetailPanelToggle,\n      detailPanelKey,\n      setDetailPanelPosition,\n      updatePanelPosition,\n    ]\n  );\n\n  const handleClick = useCallback(\n    (e: MouseEvent) => {\n      e.preventDefault();\n      e.stopPropagation();\n      const clientX = e.clientX;\n      if (!isMobile && canOpenDetail) {\n        clearPendingClick();\n        if (!isYearView && !isResourceView) {\n          applySingleClickSelection(clientX);\n        }\n        setHasPendingSelection(true);\n        clickTimeoutRef.current = setTimeout(() => {\n          if (isYearView || isResourceView) {\n            applySingleClickSelection(clientX);\n          }\n          emitSingleClick();\n          clickTimeoutRef.current = null;\n          setHasPendingSelection(false);\n        }, SINGLE_CLICK_DELAY_MS);\n        return;\n      }\n\n      performSingleClick(clientX);\n    },\n    [\n      clearPendingClick,\n      canOpenDetail,\n      applySingleClickSelection,\n      emitSingleClick,\n      isYearView,\n      isResourceView,\n      isMobile,\n      performSingleClick,\n    ]\n  );\n\n  return {\n    handleClick,\n    handleDoubleClick,\n    handleContextMenu,\n    hasPendingSelection,\n    scrollEventToCenter,\n  };\n};\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/hooks/useEventInteraction.ts",
    "content": "import { useRef, useState } from 'preact/hooks';\n\nimport { MultiDayEventSegment } from '@/components/monthView/util';\nimport { Event, ICalendarApp } from '@/types';\n\ninterface UseEventInteractionProps {\n  event: Event;\n  isTouchEnabled: boolean;\n  onMoveStart?: (e: MouseEvent | TouchEvent, event: Event) => void;\n  onEventLongPress?: (eventId: string) => void;\n  onEventSelect?: (eventId: string | null) => void;\n  onDetailPanelToggle?: (key: string | null) => void;\n  canOpenDetail: boolean;\n  useEventDetailPanel?: boolean;\n  app?: ICalendarApp;\n  multiDaySegmentInfo?: {\n    startHour?: number;\n    endHour?: number;\n    isFirst: boolean;\n    isLast: boolean;\n    dayIndex?: number;\n  };\n  isMultiDay?: boolean;\n  segment?: MultiDayEventSegment;\n  detailPanelKey: string;\n}\n\nexport const useEventInteraction = ({\n  event,\n  isTouchEnabled,\n  onMoveStart,\n  onEventLongPress,\n  onEventSelect,\n  onDetailPanelToggle,\n  canOpenDetail,\n  useEventDetailPanel,\n  app,\n  multiDaySegmentInfo,\n  isMultiDay,\n  segment,\n}: UseEventInteractionProps) => {\n  const [isSelected, setIsSelected] = useState(false);\n  const [isPressed, setIsPressed] = useState(false);\n  const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const touchStartPosRef = useRef<{ x: number; y: number } | null>(null);\n  const latestTouchPosRef = useRef<{ x: number; y: number } | null>(null);\n  const longPressTriggeredRef = useRef(false);\n  const suppressClickUntilRef = useRef(0);\n  const LONG_PRESS_DELAY_MS = 500;\n  const LONG_PRESS_MOVE_TOLERANCE_PX = 14;\n\n  const handleTouchStart = (e: TouchEvent) => {\n    if (!onMoveStart || !isTouchEnabled) return;\n    e.stopPropagation();\n    e.preventDefault();\n    setIsPressed(true);\n\n    const touch = e.touches[0];\n    const clientX = touch.clientX;\n    const clientY = touch.clientY;\n    const currentTarget = e.currentTarget as HTMLElement;\n\n    touchStartPosRef.current = { x: clientX, y: clientY };\n    latestTouchPosRef.current = { x: clientX, y: clientY };\n    longPressTriggeredRef.current = false;\n\n    longPressTimerRef.current = setTimeout(() => {\n      const latestTouch = latestTouchPosRef.current ?? {\n        x: clientX,\n        y: clientY,\n      };\n\n      if (onEventLongPress) {\n        onEventLongPress(event.id);\n      } else {\n        setIsSelected(true);\n      }\n\n      const syntheticEvent = {\n        preventDefault: () => {\n          /* noop */\n        },\n        stopPropagation: () => {\n          /* noop */\n        },\n        currentTarget: currentTarget,\n        touches: [{ clientX: latestTouch.x, clientY: latestTouch.y }],\n        cancelable: false,\n      } as unknown as MouseEvent | TouchEvent;\n\n      longPressTriggeredRef.current = true;\n\n      if (multiDaySegmentInfo) {\n        const adjustedEvent = {\n          ...event,\n          day: multiDaySegmentInfo.dayIndex ?? event.day,\n          _segmentInfo: multiDaySegmentInfo,\n        };\n        onMoveStart(syntheticEvent, adjustedEvent as Event);\n      } else if (isMultiDay && segment) {\n        const adjustedEvent = {\n          ...event,\n          day: segment.startDayIndex,\n          _segmentInfo: {\n            dayIndex: segment.startDayIndex,\n            isFirst: segment.isFirstSegment,\n            isLast: segment.isLastSegment,\n          },\n        };\n        onMoveStart(syntheticEvent, adjustedEvent as Event);\n      } else {\n        onMoveStart(syntheticEvent, event);\n      }\n      longPressTimerRef.current = null;\n      touchStartPosRef.current = null;\n\n      if (navigator.vibrate) {\n        navigator.vibrate(50);\n      }\n\n      suppressClickUntilRef.current = Date.now() + 400;\n    }, LONG_PRESS_DELAY_MS);\n  };\n\n  const handleTouchMove = (e: TouchEvent) => {\n    const touch = e.touches[0];\n    if (touch) {\n      latestTouchPosRef.current = {\n        x: touch.clientX,\n        y: touch.clientY,\n      };\n    }\n\n    if (longPressTriggeredRef.current) {\n      if (e.cancelable) {\n        e.preventDefault();\n      }\n      e.stopPropagation();\n      return;\n    }\n\n    if (isTouchEnabled) {\n      e.stopPropagation();\n    }\n    if (longPressTimerRef.current && touchStartPosRef.current) {\n      const dx = Math.abs((touch?.clientX ?? 0) - touchStartPosRef.current.x);\n      const dy = Math.abs((touch?.clientY ?? 0) - touchStartPosRef.current.y);\n      if (\n        dx > LONG_PRESS_MOVE_TOLERANCE_PX ||\n        dy > LONG_PRESS_MOVE_TOLERANCE_PX\n      ) {\n        clearTimeout(longPressTimerRef.current);\n        longPressTimerRef.current = null;\n        touchStartPosRef.current = null;\n        latestTouchPosRef.current = null;\n        setIsPressed(false);\n      }\n    }\n  };\n\n  const handleTouchEnd = (e: TouchEvent) => {\n    setIsPressed(false);\n    if (longPressTimerRef.current) {\n      clearTimeout(longPressTimerRef.current);\n      longPressTimerRef.current = null;\n    }\n\n    if (longPressTriggeredRef.current) {\n      longPressTriggeredRef.current = false;\n      touchStartPosRef.current = null;\n      latestTouchPosRef.current = null;\n      return;\n    }\n\n    if (isTouchEnabled && touchStartPosRef.current) {\n      e.preventDefault();\n      e.stopPropagation();\n      suppressClickUntilRef.current = Date.now() + 400;\n\n      if (app) {\n        app.onEventClick(event);\n      }\n\n      if (canOpenDetail) {\n        if (onEventSelect) {\n          onEventSelect(event.id);\n        } else {\n          setIsSelected(true);\n        }\n\n        if (useEventDetailPanel !== false) {\n          onDetailPanelToggle?.(null);\n        }\n      } else {\n        onEventSelect?.(null);\n        if (useEventDetailPanel !== false) {\n          onDetailPanelToggle?.(null);\n        }\n      }\n    }\n\n    touchStartPosRef.current = null;\n    latestTouchPosRef.current = null;\n  };\n\n  const handleTouchCancel = () => {\n    setIsPressed(false);\n    if (longPressTimerRef.current) {\n      clearTimeout(longPressTimerRef.current);\n      longPressTimerRef.current = null;\n    }\n    touchStartPosRef.current = null;\n    latestTouchPosRef.current = null;\n    longPressTriggeredRef.current = false;\n  };\n\n  return {\n    isSelected,\n    setIsSelected,\n    isPressed,\n    setIsPressed,\n    handleTouchStart,\n    handleTouchMove,\n    handleTouchEnd,\n    handleTouchCancel,\n    shouldSuppressClick: () => Date.now() < suppressClickUntilRef.current,\n  };\n};\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/hooks/useEventStyles.ts",
    "content": "import { RefObject } from 'preact';\n\nimport type { EventVisibility } from '@/components/calendarEvent/hooks/useEventVisibility';\nimport {\n  getCalendarContentElement,\n  getTimeColumnWidth,\n} from '@/components/calendarEvent/utils';\nimport { MultiDayEventSegment } from '@/components/monthView/util';\nimport { YearMultiDaySegment } from '@/components/yearView/utils';\nimport { ViewType, Event, EventLayout } from '@/types';\nimport { extractHourFromDate, getEventEndHour } from '@/utils';\n\nconst POP_TRANSITION = 'transform 0.5s cubic-bezier(0.22, 1, 0.36, 1)';\n\ninterface UseEventStylesProps {\n  event: Event;\n  timingEvent?: Event;\n  layout?: EventLayout;\n  isBeingDragged: boolean;\n  isAllDay: boolean;\n  allDayHeight: number;\n  viewType: ViewType;\n  isMultiDay: boolean;\n  segment?: MultiDayEventSegment;\n  yearSegment?: YearMultiDaySegment;\n  columnsPerRow?: number;\n  segmentIndex: number;\n  hourHeight: number;\n  firstHour: number;\n  isEventSelected: boolean;\n  showDetailPanel: boolean;\n  isPopping: boolean;\n  isDraggable: boolean;\n  canOpenDetail: boolean;\n  eventVisibility: EventVisibility;\n  calendarRef: RefObject<HTMLElement>;\n  isMobile: boolean;\n  eventRef: RefObject<HTMLElement>;\n  getActiveDayIdx: () => number;\n  getDayMetricsWrapper: (\n    dayIndex: number\n  ) => { left: number; width: number } | null;\n  multiDaySegmentInfo?: {\n    startHour: number;\n    endHour: number;\n    isFirst: boolean;\n    isLast: boolean;\n    dayIndex?: number;\n  };\n  monthEventHeight?: number;\n}\n\nexport const useEventStyles = ({\n  event,\n  timingEvent,\n  layout,\n  isBeingDragged,\n  isAllDay,\n  allDayHeight,\n  viewType,\n  isMultiDay,\n  segment,\n  yearSegment,\n  columnsPerRow,\n  segmentIndex,\n  hourHeight,\n  firstHour,\n  isEventSelected,\n  showDetailPanel,\n  isPopping,\n  isDraggable,\n  canOpenDetail,\n  eventVisibility,\n  calendarRef,\n  isMobile,\n  eventRef,\n  getActiveDayIdx,\n  getDayMetricsWrapper,\n  multiDaySegmentInfo,\n  monthEventHeight,\n}: UseEventStylesProps) => {\n  const isDayView = viewType === ViewType.DAY;\n  const isMonthView = viewType === ViewType.MONTH;\n  const isYearView = viewType === ViewType.YEAR;\n  const eventForTiming = timingEvent ?? event;\n\n  const calculateEventStyle = () => {\n    if (isYearView && yearSegment && columnsPerRow) {\n      const { startCellIndex, endCellIndex, visualRowIndex } = yearSegment;\n      const startPercent = (startCellIndex / columnsPerRow) * 100;\n      const widthPercent =\n        ((endCellIndex - startCellIndex + 1) / columnsPerRow) * 100;\n      const TOP_OFFSET = visualRowIndex * 18; // ROW_SPACING\n      const HORIZONTAL_MARGIN = 2;\n      const EVENT_HEIGHT = 16;\n\n      return {\n        position: 'absolute',\n        left: `calc(${startPercent}% + ${HORIZONTAL_MARGIN}px)`,\n        top: `${TOP_OFFSET}px`,\n        width: `calc(${widthPercent}% - ${HORIZONTAL_MARGIN * 2}px)`,\n        height: `${EVENT_HEIGHT}px`,\n        opacity: 1,\n        zIndex: isEventSelected || showDetailPanel ? 1000 : 1,\n        transform: isPopping ? 'scale(1.05)' : 'scale(1)',\n        transition: isBeingDragged\n          ? [\n              'left 90ms cubic-bezier(0.22, 1, 0.36, 1)',\n              'top 90ms cubic-bezier(0.22, 1, 0.36, 1)',\n              'width 90ms cubic-bezier(0.22, 1, 0.36, 1)',\n              POP_TRANSITION,\n            ].join(', ')\n          : POP_TRANSITION,\n        willChange: isBeingDragged\n          ? ('left, top, width, transform' as const)\n          : ('transform' as const),\n        cursor: isDraggable ? 'pointer' : canOpenDetail ? 'pointer' : 'default',\n      };\n    }\n\n    if (isMonthView) {\n      if (isMultiDay && segment) {\n        return {\n          opacity: 1,\n          zIndex: isEventSelected || showDetailPanel ? 1000 : 1,\n          cursor: isDraggable\n            ? 'pointer'\n            : canOpenDetail\n              ? 'pointer'\n              : 'default',\n        };\n      }\n      return {\n        ...(monthEventHeight !== undefined && {\n          height: `${monthEventHeight}px`,\n          '--df-month-event-height': `${monthEventHeight}px`,\n        }),\n        opacity: 1,\n        zIndex: isEventSelected || showDetailPanel ? 1000 : 1,\n        transform: isPopping ? 'scale(1.05)' : 'scale(1)',\n        transition: POP_TRANSITION,\n        cursor: isDraggable ? 'pointer' : canOpenDetail ? 'pointer' : 'default',\n      };\n    }\n\n    if (isAllDay) {\n      const styles: Record<string, unknown> = {\n        height: `${allDayHeight - 4}px`,\n        opacity: 1,\n        zIndex: isEventSelected || showDetailPanel ? 1000 : 1,\n        transform: isPopping ? 'scale(1.05)' : 'scale(1)',\n        transition: POP_TRANSITION,\n        cursor: isDraggable ? 'pointer' : canOpenDetail ? 'pointer' : 'default',\n      };\n\n      const topOffset = segmentIndex * allDayHeight;\n      Object.assign(styles, { top: `${topOffset}px` });\n      if (isDayView) {\n        Object.assign(styles, {\n          width: '100%',\n          left: '0px',\n          right: '2px',\n          position: 'absolute',\n        });\n      } else if (isMultiDay && segment) {\n        const cols = columnsPerRow || 7;\n        const spanDays = segment.endDayIndex - segment.startDayIndex + 1;\n        const widthPercent = (spanDays / cols) * 100;\n        const leftPercent = (segment.startDayIndex / cols) * 100;\n        const HORIZONTAL_MARGIN = 2;\n        const marginLeft = segment.isFirstSegment ? HORIZONTAL_MARGIN : 0;\n        const marginRight = segment.isLastSegment ? HORIZONTAL_MARGIN : 0;\n        const totalMargin = marginLeft + marginRight;\n\n        Object.assign(styles, {\n          width:\n            totalMargin > 0\n              ? `calc(${widthPercent}% - ${totalMargin}px)`\n              : `${widthPercent}%`,\n          left:\n            marginLeft > 0\n              ? `calc(${leftPercent}% + ${marginLeft}px)`\n              : `${leftPercent}%`,\n          position: 'absolute',\n          pointerEvents: 'auto',\n        });\n      } else {\n        Object.assign(styles, {\n          width: '100%',\n          left: '0px',\n          position: 'relative',\n        });\n      }\n      return styles;\n    }\n\n    const startHour = multiDaySegmentInfo\n      ? multiDaySegmentInfo.startHour\n      : extractHourFromDate(eventForTiming.start);\n    const endHour = multiDaySegmentInfo\n      ? multiDaySegmentInfo.endHour\n      : getEventEndHour(eventForTiming);\n\n    const top = (startHour - firstHour) * hourHeight;\n    const height = Math.max((endHour - startHour) * hourHeight, hourHeight / 4);\n\n    const baseStyle = {\n      top: `${top + 3}px`,\n      height: `${height - 4}px`,\n      position: 'absolute',\n      opacity: 1,\n      zIndex: isEventSelected || showDetailPanel ? 1000 : (layout?.zIndex ?? 1),\n      transform: isPopping ? 'scale(1.05)' : 'scale(1)',\n      transition: POP_TRANSITION,\n      cursor: isDraggable ? 'pointer' : canOpenDetail ? 'pointer' : 'default',\n    };\n\n    // Sticky-left/right: event's column has scrolled out of the visible horizontal area\n    if (\n      isEventSelected &&\n      showDetailPanel &&\n      (eventVisibility === 'sticky-left' || eventVisibility === 'sticky-right')\n    ) {\n      const calendarContent = getCalendarContentElement(calendarRef);\n      const contentRect = calendarContent?.getBoundingClientRect();\n      if (contentRect) {\n        const timeColumnWidth = getTimeColumnWidth(calendarRef, isMobile);\n        const gridLeft = contentRect.left + timeColumnWidth;\n        const gridRight = contentRect.right;\n\n        const parentRect =\n          eventRef.current?.parentElement?.getBoundingClientRect();\n\n        // Compute event's natural vertical viewport position using the column container's top\n        const gridRefTop = parentRect?.top ?? contentRect.top;\n        const eventNaturalTop =\n          gridRefTop + (startHour - firstHour) * hourHeight + 3;\n        const clampedTop = Math.max(eventNaturalTop, contentRect.top);\n        const clampedBottom = Math.min(\n          eventNaturalTop + height - 4,\n          contentRect.bottom\n        );\n\n        if (clampedBottom <= clampedTop) {\n          return { display: 'none' as const };\n        }\n\n        const stickyStyle = {\n          position: 'fixed',\n          top: `${clampedTop}px`,\n          height: `${clampedBottom - clampedTop}px`,\n          width: '6px',\n          zIndex: 1000,\n          overflow: 'hidden',\n          borderRadius: '0.25rem',\n        };\n\n        if (eventVisibility === 'sticky-left') {\n          return { ...stickyStyle, left: `${gridLeft}px` };\n        }\n        return { ...stickyStyle, left: `${gridRight - 6}px` };\n      }\n    }\n\n    if (\n      isEventSelected &&\n      showDetailPanel &&\n      (eventVisibility === 'sticky-top' || eventVisibility === 'sticky-bottom')\n    ) {\n      const calendarRect = calendarRef.current?.getBoundingClientRect();\n      if (calendarRect) {\n        const activeDayIndex = getActiveDayIdx();\n        const timeColumnWidth = getTimeColumnWidth(calendarRef, isMobile);\n        const columnCount = isDayView ? 1 : columnsPerRow || 7;\n        let dayColumnWidth =\n          (calendarRect.width - timeColumnWidth) / columnCount;\n        let dayStartX =\n          calendarRect.left +\n          timeColumnWidth +\n          (isDayView ? 0 : activeDayIndex * dayColumnWidth);\n\n        if (isMonthView) {\n          dayColumnWidth = calendarRect.width / 7;\n          dayStartX = calendarRect.left + activeDayIndex * dayColumnWidth;\n        }\n\n        const overrideMetrics = getDayMetricsWrapper(activeDayIndex);\n        if (overrideMetrics) {\n          dayStartX = overrideMetrics.left;\n          dayColumnWidth = overrideMetrics.width;\n        }\n\n        let scrollContainer = getCalendarContentElement(calendarRef);\n        if (!scrollContainer) {\n          scrollContainer =\n            (calendarRef.current?.querySelector(\n              '.df-calendar-renderer'\n            ) as HTMLElement | null) ?? null;\n        }\n        const contentRect = scrollContainer?.getBoundingClientRect();\n        const parentRect =\n          eventRef.current?.parentElement?.getBoundingClientRect();\n        let stickyLeft: number;\n        let stickyWidth: number;\n\n        if (parentRect && parentRect.width > 0) {\n          if (layout) {\n            stickyLeft =\n              parentRect.left + (layout.left / 100) * parentRect.width;\n            stickyWidth = isDayView\n              ? (layout.width / 100) * parentRect.width\n              : ((layout.width - 1) / 100) * parentRect.width;\n          } else {\n            stickyLeft = parentRect.left;\n            stickyWidth = isDayView ? parentRect.width : parentRect.width - 3;\n          }\n        } else {\n          const metrics = getDayMetricsWrapper(activeDayIndex);\n          const currentDayStartX = metrics?.left ?? dayStartX;\n          const currentDayColumnWidth = metrics?.width ?? dayColumnWidth;\n\n          stickyLeft = currentDayStartX;\n          stickyWidth = currentDayColumnWidth - 3;\n\n          if (layout) {\n            stickyLeft =\n              currentDayStartX + (layout.left / 100) * currentDayColumnWidth;\n            stickyWidth = isDayView\n              ? (layout.width / 100) * currentDayColumnWidth\n              : ((layout.width - 1) / 100) * currentDayColumnWidth;\n          }\n        }\n\n        // Clamp the sticky bar to stay within the grid content area (right of time column)\n        const gridContentLeft = calendarRect.left + timeColumnWidth;\n        if (stickyLeft < gridContentLeft) {\n          stickyWidth = Math.max(\n            0,\n            stickyWidth - (gridContentLeft - stickyLeft)\n          );\n          stickyLeft = gridContentLeft;\n        }\n        // Also clamp right edge\n        if (stickyLeft + stickyWidth > calendarRect.right) {\n          stickyWidth = Math.max(0, calendarRect.right - stickyLeft);\n        }\n\n        if (eventVisibility === 'sticky-top') {\n          let topPosition = contentRect ? contentRect.top : calendarRect.top;\n          topPosition = Math.max(topPosition, 0);\n          topPosition = Math.max(topPosition, calendarRect.top);\n          topPosition = Math.min(topPosition, calendarRect.bottom - 6);\n          topPosition = Math.min(topPosition, window.innerHeight - 6);\n\n          return {\n            position: 'fixed',\n            top: `${topPosition}px`,\n            left: `${stickyLeft}px`,\n            width: `${stickyWidth}px`,\n            height: '6px',\n            zIndex: 1000,\n            overflow: 'hidden',\n            borderRadius: '0.25rem',\n          };\n        }\n\n        let bottomPosition = contentRect\n          ? contentRect.bottom\n          : calendarRect.bottom;\n        bottomPosition = Math.min(bottomPosition, window.innerHeight);\n        bottomPosition = Math.min(bottomPosition, calendarRect.bottom);\n        bottomPosition = Math.max(bottomPosition, calendarRect.top + 6);\n        bottomPosition = Math.max(bottomPosition, 6);\n\n        return {\n          position: 'fixed',\n          top: `${bottomPosition - 6}px`,\n          left: `${stickyLeft}px`,\n          width: `${stickyWidth}px`,\n          height: '6px',\n          zIndex: 1000,\n          overflow: 'hidden',\n          borderRadius: '0.25rem',\n        };\n      }\n    }\n\n    if (layout && !isAllDay) {\n      const widthStyle = isDayView\n        ? `${layout.width}%`\n        : `${layout.width - 1}%`;\n\n      return {\n        ...baseStyle,\n        left: `${layout.left}%`,\n        width: widthStyle,\n        right: 'auto',\n      };\n    }\n\n    return {\n      ...baseStyle,\n      left: '0px',\n      right: isDayView ? '0px' : '3px',\n    };\n  };\n\n  return { calculateEventStyle };\n};\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/hooks/useEventVisibility.ts",
    "content": "import { RefObject } from 'preact';\nimport { useEffect, useCallback } from 'preact/hooks';\n\nimport {\n  getCalendarContentElement,\n  getTimeColumnWidth,\n} from '@/components/calendarEvent/utils';\nimport { Event, ViewType } from '@/types';\nimport { extractHourFromDate, getEventEndHour } from '@/utils';\n\nexport type EventVisibility =\n  | 'standard'\n  | 'sticky-top'\n  | 'sticky-bottom'\n  | 'sticky-left'\n  | 'sticky-right';\n\ninterface UseEventVisibilityProps {\n  event: Event;\n  timingEvent?: Event;\n  isEventSelected: boolean;\n  showDetailPanel: boolean;\n  eventRef: RefObject<HTMLElement>;\n  calendarRef: RefObject<HTMLElement>;\n  isAllDay: boolean;\n  viewType: ViewType;\n  isMobile?: boolean;\n  multiDaySegmentInfo?: {\n    startHour: number;\n    endHour: number;\n    isFirst: boolean;\n    isLast: boolean;\n    dayIndex?: number;\n  };\n  firstHour: number;\n  hourHeight: number;\n  updatePanelPosition: () => void;\n  eventVisibility: EventVisibility;\n  setEventVisibility: (visibility: EventVisibility) => void;\n}\n\nexport const useEventVisibility = ({\n  event,\n  timingEvent,\n  isEventSelected,\n  showDetailPanel,\n  eventRef,\n  calendarRef,\n  isAllDay,\n  viewType,\n  isMobile = false,\n  multiDaySegmentInfo,\n  firstHour,\n  hourHeight,\n  updatePanelPosition,\n  eventVisibility,\n  setEventVisibility,\n}: UseEventVisibilityProps) => {\n  const isMonthView = viewType === ViewType.MONTH;\n  const isYearView = viewType === ViewType.YEAR;\n  const isResourceView = viewType === ViewType.RESOURCE;\n  const eventForTiming = timingEvent ?? event;\n\n  const checkEventVisibility = useCallback(() => {\n    if (\n      !isEventSelected ||\n      !showDetailPanel ||\n      !eventRef.current ||\n      !calendarRef.current ||\n      isAllDay ||\n      isMonthView ||\n      isYearView\n    )\n      return;\n\n    const calendarContent = getCalendarContentElement(calendarRef);\n    if (!calendarContent) return;\n\n    const segmentStartHour = multiDaySegmentInfo\n      ? multiDaySegmentInfo.startHour\n      : extractHourFromDate(eventForTiming.start);\n    const segmentEndHour = multiDaySegmentInfo\n      ? multiDaySegmentInfo.endHour\n      : getEventEndHour(eventForTiming);\n\n    const originalTop = (segmentStartHour - firstHour) * hourHeight;\n    const originalHeight = Math.max(\n      (segmentEndHour - segmentStartHour) * hourHeight,\n      hourHeight / 4\n    );\n    const originalBottom = originalTop + originalHeight;\n\n    const contentRect = calendarContent.getBoundingClientRect();\n    const scrollTop = calendarContent.scrollTop;\n    const viewportHeight = contentRect.height;\n    const scrollBottom = scrollTop + viewportHeight;\n\n    const isContentAboveViewport = contentRect.bottom < 0;\n    const isContentBelowViewport = contentRect.top > window.innerHeight;\n\n    const STICKY_THRESHOLD = 20;\n\n    let nextVisibility: EventVisibility = eventVisibility;\n\n    if (isContentAboveViewport) {\n      nextVisibility = 'sticky-top';\n    } else if (isContentBelowViewport) {\n      nextVisibility = 'sticky-bottom';\n    } else {\n      // Determine vertical state, treating horizontal sticky as 'standard' for transition purposes\n      const isCurrentlyHorizontallySticky =\n        eventVisibility === 'sticky-left' || eventVisibility === 'sticky-right';\n      const currentVerticalState: EventVisibility =\n        isCurrentlyHorizontallySticky ? 'standard' : eventVisibility;\n\n      let newVerticalState: EventVisibility;\n      if (currentVerticalState === 'standard') {\n        if (originalBottom < scrollTop) newVerticalState = 'sticky-top';\n        else if (originalTop > scrollBottom - STICKY_THRESHOLD)\n          newVerticalState = 'sticky-bottom';\n        else newVerticalState = 'standard';\n      } else if (currentVerticalState === 'sticky-top') {\n        newVerticalState =\n          originalBottom >= scrollTop ? 'standard' : 'sticky-top';\n      } else {\n        // sticky-bottom\n        newVerticalState =\n          originalTop <= scrollBottom - STICKY_THRESHOLD\n            ? 'standard'\n            : 'sticky-bottom';\n      }\n\n      if (newVerticalState !== 'standard') {\n        nextVisibility = newVerticalState;\n      } else if (isResourceView) {\n        // Vertically standard — check horizontal visibility\n        const parentRect =\n          eventRef.current?.parentElement?.getBoundingClientRect();\n        if (parentRect) {\n          const timeColumnWidth = getTimeColumnWidth(calendarRef, isMobile);\n          const gridLeft = contentRect.left + timeColumnWidth;\n          const gridRight = contentRect.right;\n\n          if (parentRect.right <= gridLeft) {\n            nextVisibility = 'sticky-left';\n          } else if (parentRect.left >= gridRight) {\n            nextVisibility = 'sticky-right';\n          } else {\n            nextVisibility = 'standard';\n          }\n        } else {\n          nextVisibility = 'standard';\n        }\n      } else {\n        nextVisibility = 'standard';\n      }\n    }\n\n    if (nextVisibility === eventVisibility) {\n      updatePanelPosition();\n    } else {\n      setEventVisibility(nextVisibility);\n      // Defer panel update until after the re-render commits the new sticky styles\n      setTimeout(updatePanelPosition, 0);\n    }\n  }, [\n    isEventSelected,\n    showDetailPanel,\n    calendarRef,\n    isAllDay,\n    isMonthView,\n    isResourceView,\n    isMobile,\n    eventForTiming.start,\n    eventForTiming.end,\n    firstHour,\n    hourHeight,\n    updatePanelPosition,\n    multiDaySegmentInfo,\n    eventVisibility,\n    setEventVisibility,\n  ]);\n\n  useEffect(() => {\n    if (!isEventSelected || !showDetailPanel || isAllDay) return;\n\n    const calendarContent = getCalendarContentElement(calendarRef);\n    if (!calendarContent) return;\n\n    const handleScroll = () => checkEventVisibility();\n    const handleResize = () => {\n      checkEventVisibility();\n      updatePanelPosition();\n    };\n\n    const scrollContainers: Element[] = [calendarContent];\n    let parent = calendarRef.current?.parentElement;\n    while (parent) {\n      const style = window.getComputedStyle(parent);\n      if (\n        style.overflowY === 'auto' ||\n        style.overflowY === 'scroll' ||\n        style.overflowX === 'auto' ||\n        style.overflowX === 'scroll'\n      ) {\n        scrollContainers.push(parent);\n      }\n      parent = parent.parentElement;\n    }\n\n    scrollContainers.forEach(container => {\n      container.addEventListener('scroll', handleScroll);\n    });\n\n    window.addEventListener('scroll', handleScroll, true);\n    window.addEventListener('resize', handleResize);\n\n    let resizeObserver: ResizeObserver | null = null;\n    if (calendarRef.current) {\n      resizeObserver = new ResizeObserver(() => {\n        handleResize();\n      });\n      resizeObserver.observe(calendarRef.current);\n    }\n\n    checkEventVisibility();\n\n    return () => {\n      scrollContainers.forEach(container => {\n        container.removeEventListener('scroll', handleScroll);\n      });\n      window.removeEventListener('scroll', handleScroll, true);\n      window.removeEventListener('resize', handleResize);\n      if (resizeObserver) resizeObserver.disconnect();\n    };\n  }, [\n    isEventSelected,\n    showDetailPanel,\n    isAllDay,\n    checkEventVisibility,\n    updatePanelPosition,\n    calendarRef,\n  ]);\n};\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/index.tsx",
    "content": "import CalendarEvent from './CalendarEvent';\n\nexport { CalendarEvent };\nexport default CalendarEvent;\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/types.ts",
    "content": "import { ComponentChildren, RefObject } from 'preact';\n\nimport { MultiDayEventSegment } from '@/components/monthView/util';\nimport { YearMultiDaySegment } from '@/components/yearView/utils';\nimport { Event, EventLayout, ICalendarApp, ViewType } from '@/types';\n\nexport interface CalendarEventProps {\n  event: Event;\n  layout?: EventLayout;\n  isAllDay?: boolean;\n  allDayHeight?: number;\n  calendarRef: RefObject<HTMLDivElement>;\n  isBeingDragged?: boolean;\n  isBeingResized?: boolean;\n  viewType: ViewType;\n  isMultiDay?: boolean;\n  segment?: MultiDayEventSegment;\n  yearSegment?: YearMultiDaySegment;\n  columnsPerRow?: number;\n  segmentIndex?: number;\n  hourHeight: number;\n  firstHour: number;\n  newlyCreatedEventId?: string | null;\n  selectedEventId?: string | null;\n  detailPanelEventId?: string | null;\n  onMoveStart?: (e: MouseEvent | TouchEvent, event: Event) => void;\n  onResizeStart?: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  onEventUpdate: (updatedEvent: Event) => void;\n  onEventDelete: (eventId: string) => void;\n  onDetailPanelOpen?: () => void;\n  onEventSelect?: (eventId: string | null) => void;\n  onEventLongPress?: (eventId: string) => void;\n  onDetailPanelToggle?: (eventId: string | null) => void;\n  /** When false, suppresses the floating event detail panel entirely */\n  useEventDetailPanel?: boolean;\n  /** Multi-day regular event segment information */\n  multiDaySegmentInfo?: {\n    startHour: number;\n    endHour: number;\n    isFirst: boolean;\n    isLast: boolean;\n    dayIndex?: number;\n  };\n  app?: ICalendarApp;\n  /** Whether the current view is in mobile mode */\n  isMobile?: boolean;\n  /** Whether the current view is in mobile sliding mode */\n  isSlidingView?: boolean;\n  /** Force enable touch interactions regardless of isMobile */\n  enableTouch?: boolean;\n  /** Whether to hide the time in the event display (Month view regular events only) */\n  hideTime?: boolean;\n  /** Time format for event display */\n  timeFormat?: '12h' | '24h';\n  /** Optional style override for custom view layouts */\n  styleOverride?: Record<string, string | number>;\n  /** Optional additional class names */\n  className?: string;\n  /** Disable built-in layout calculation and rely on styleOverride */\n  disableDefaultStyle?: boolean;\n  /**\n   * Override the visual content rendered inside the event shell.\n   * The function receives the built-in default content as its argument,\n   * which can be wrapped, replaced, or ignored entirely.\n   */\n  renderVisualContent?: (\n    defaultContent: ComponentChildren\n  ) => ComponentChildren;\n  /** Override resize handle orientation for custom views */\n  resizeHandleOrientation?: 'vertical' | 'horizontal';\n  /** App-level timezone used to project event times for display (Month/Year view). */\n  appTimeZone?: string;\n  /** Height in pixels of each event row in the month grid (month multi-day events only). */\n  monthEventHeight?: number;\n}\n"
  },
  {
    "path": "packages/core/src/components/calendarEvent/utils.ts",
    "content": "import { RefObject } from 'preact';\n\nimport { Event, ViewType } from '@/types';\n\nexport type EventSegmentShape = 'full' | 'start' | 'end' | 'middle';\n/**\n * Gets the actual width of the time column from the DOM\n */\nexport const getTimeColumnWidth = (\n  calendarRef: RefObject<HTMLElement>,\n  isMobile: boolean\n): number => {\n  if (!calendarRef.current) return isMobile ? 48 : 80;\n  const timeColumn = calendarRef.current.querySelector('.df-time-column');\n  return timeColumn\n    ? timeColumn.getBoundingClientRect().width\n    : isMobile\n      ? 48\n      : 80;\n};\n\nexport const getCalendarContentElement = (\n  calendarRef: RefObject<HTMLElement>\n): HTMLElement | null => {\n  const element = calendarRef.current;\n  if (!element) return null;\n\n  const ownMatch = element.matches('.df-calendar-content') ? element : null;\n  const descendantMatch = element.querySelector(\n    '.df-calendar-content'\n  ) as HTMLElement | null;\n  const ancestorMatch = element.closest(\n    '.df-calendar-content'\n  ) as HTMLElement | null;\n\n  return ownMatch ?? descendantMatch ?? ancestorMatch;\n};\n\n/**\n * Calculates the horizontal metrics (left and width) for a day column\n */\nexport const getDayMetrics = (\n  dayIndex: number,\n  calendarRef: RefObject<HTMLElement>,\n  viewType: ViewType,\n  isMobile: boolean\n): { left: number; width: number } | null => {\n  if (!calendarRef.current) return null;\n\n  const calendarRect = calendarRef.current.getBoundingClientRect();\n\n  if (viewType === ViewType.MONTH) {\n    const dayColumnWidth = calendarRect.width / 7;\n    return {\n      left: calendarRect.left + dayIndex * dayColumnWidth,\n      width: dayColumnWidth,\n    };\n  }\n\n  const timeColumnWidth = getTimeColumnWidth(calendarRef, isMobile);\n  if (viewType === ViewType.DAY) {\n    const dayColumnWidth = calendarRect.width - timeColumnWidth;\n    return {\n      left: calendarRect.left + timeColumnWidth,\n      width: dayColumnWidth,\n    };\n  }\n\n  const dayColumnWidth = (calendarRect.width - timeColumnWidth) / 7;\n  return {\n    left: calendarRect.left + timeColumnWidth + dayIndex * dayColumnWidth,\n    width: dayColumnWidth,\n  };\n};\n\n/**\n * Gets the active day index for multi-day events\n */\nexport const getActiveDayIndex = (\n  event: Event,\n  detailPanelEventId: string | undefined,\n  detailPanelKey: string,\n  selectedDayIndex: number | null,\n  multiDaySegmentInfo?: { dayIndex?: number },\n  segment?: { startDayIndex: number }\n): number => {\n  if (selectedDayIndex !== null) {\n    return selectedDayIndex;\n  }\n\n  if (detailPanelEventId === detailPanelKey) {\n    const keyParts = detailPanelKey.split('::');\n    const suffix = keyParts.at(-1);\n    if (suffix && suffix.startsWith('day-')) {\n      const parsed = Number(suffix.replace('day-', ''));\n      if (!Number.isNaN(parsed)) {\n        return parsed;\n      }\n    }\n  }\n\n  if (multiDaySegmentInfo?.dayIndex !== undefined) {\n    return multiDaySegmentInfo.dayIndex;\n  }\n  if (segment) {\n    return segment.startDayIndex;\n  }\n  return event.day ?? 0;\n};\n\n/**\n * Gets the clicked day index based on mouse position\n */\nexport const getClickedDayIndex = (\n  clientX: number,\n  calendarRef: RefObject<HTMLElement>,\n  viewType: ViewType,\n  isMobile: boolean\n): number | null => {\n  if (!calendarRef.current) return null;\n\n  const calendarRect = calendarRef.current.getBoundingClientRect();\n  if (viewType === ViewType.MONTH) {\n    const dayColumnWidth = calendarRect.width / 7;\n    const relativeX = clientX - calendarRect.left;\n    const index = Math.floor(relativeX / dayColumnWidth);\n    return Number.isFinite(index) ? Math.max(0, Math.min(6, index)) : null;\n  }\n\n  const timeColumnWidth = getTimeColumnWidth(calendarRef, isMobile);\n  const columnCount = viewType === ViewType.DAY ? 1 : 7;\n  const dayColumnWidth = (calendarRect.width - timeColumnWidth) / columnCount;\n  const relativeX = clientX - calendarRect.left - timeColumnWidth;\n  const index = Math.floor(relativeX / dayColumnWidth);\n  return Number.isFinite(index)\n    ? Math.max(0, Math.min(columnCount - 1, index))\n    : null;\n};\n\n/**\n * Gets the CSS classes for the event container\n */\nexport const getEventClasses = (\n  viewType: ViewType,\n  isAllDay: boolean,\n  isMultiDay: boolean\n): string => {\n  const classes = ['df-event'];\n\n  if (viewType === ViewType.DAY) {\n    classes.push('df-day-event');\n  } else if (viewType === ViewType.WEEK) {\n    classes.push('df-week-event');\n  } else if (viewType === ViewType.MONTH) {\n    classes.push('df-month-event');\n    if (!isMultiDay) {\n      classes.push('df-month-event-stacked');\n    }\n  } else if (viewType === ViewType.YEAR) {\n    classes.push('df-year-event');\n  }\n\n  classes.push(isAllDay ? 'df-event-all-day' : 'df-event-timed');\n\n  return classes.join(' ');\n};\n\nexport const getAllDaySegmentShape = (segment?: {\n  segmentType: string;\n}): EventSegmentShape => {\n  if (!segment) return 'full';\n\n  switch (segment.segmentType) {\n    case 'start':\n    case 'start-week-end':\n      return 'start';\n    case 'end':\n    case 'end-week-start':\n      return 'end';\n    case 'middle':\n      return 'middle';\n    default:\n      return 'full';\n  }\n};\n\nexport const getYearSegmentShape = (yearSegment?: {\n  isFirstSegment: boolean;\n  isLastSegment: boolean;\n}): EventSegmentShape => {\n  if (!yearSegment) return 'full';\n  if (yearSegment.isFirstSegment && yearSegment.isLastSegment) return 'full';\n  if (yearSegment.isFirstSegment) return 'start';\n  if (yearSegment.isLastSegment) return 'end';\n  return 'middle';\n};\n\nexport const getEventSegmentShape = (\n  viewType: ViewType,\n  isAllDay: boolean,\n  segment?: { segmentType: string },\n  yearSegment?: { isFirstSegment: boolean; isLastSegment: boolean }\n): EventSegmentShape => {\n  if (viewType === ViewType.YEAR) {\n    return getYearSegmentShape(yearSegment);\n  }\n\n  if (isAllDay) {\n    return getAllDaySegmentShape(segment);\n  }\n\n  return 'full';\n};\n"
  },
  {
    "path": "packages/core/src/components/common/BlossomColorPicker.tsx",
    "content": "import {\n  BlossomColorPicker as VanillaBlossomColorPicker,\n  BlossomColorPickerOptions,\n} from '@dayflow/blossom-color-picker';\nimport { useEffect, useRef } from 'preact/hooks';\n\ninterface BlossomColorPickerProps extends Partial<BlossomColorPickerOptions> {\n  className?: string;\n}\n\nexport const BlossomColorPicker = (props: BlossomColorPickerProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const pickerRef = useRef<VanillaBlossomColorPicker | null>(null);\n\n  useEffect(() => {\n    if (containerRef.current) {\n      const { ...options } = props;\n      pickerRef.current = new VanillaBlossomColorPicker(\n        containerRef.current,\n        options\n      );\n\n      if (options.initialExpanded) {\n        pickerRef.current.expand();\n      }\n    }\n\n    return () => {\n      if (pickerRef.current) {\n        pickerRef.current.destroy();\n        pickerRef.current = null;\n      }\n    };\n  }, []);\n\n  // Handle updates to options\n  useEffect(() => {\n    if (pickerRef.current) {\n      const { ...options } = props;\n      pickerRef.current.setOptions(options);\n    }\n  }, [props]);\n\n  return <div ref={containerRef} className={props.className} />;\n};\n"
  },
  {
    "path": "packages/core/src/components/common/CalendarHeader.tsx",
    "content": "import { JSX } from 'preact';\nimport { useCallback } from 'preact/hooks';\n\nimport { useResponsiveMonthConfig } from '@/hooks/virtualScroll';\nimport { useLocale } from '@/locale/useLocale';\nimport { iconButton } from '@/styles/classNames';\nimport { ViewType, CalendarHeaderProps } from '@/types';\n\nimport { Plus, Search } from './Icons';\nimport ViewSwitcher from './ViewSwitcher';\n\nconst CalendarHeader = ({\n  calendar,\n  switcherMode = 'buttons',\n  onAddCalendar,\n  onSearchChange,\n  onSearchClick,\n  searchValue = '',\n  isSearchOpen = false,\n  isEditable = true,\n  safeAreaLeft,\n}: CalendarHeaderProps) => {\n  const isSwitcherCentered = switcherMode === 'buttons';\n  const isDayView = calendar.state.currentView === ViewType.DAY;\n  const { screenSize } = useResponsiveMonthConfig();\n  const isMobile = screenSize === 'mobile';\n  const { t } = useLocale();\n\n  const handleSearchChange = useCallback(\n    (e: JSX.TargetedEvent<HTMLInputElement, Event>) => {\n      const newValue = e.currentTarget.value;\n      if (newValue !== searchValue) {\n        onSearchChange?.(newValue);\n      }\n    },\n    [onSearchChange, searchValue]\n  );\n\n  const handleClearSearch = () => {\n    onSearchChange?.('');\n  };\n\n  const isBordered = isDayView || isSearchOpen;\n\n  return (\n    <div\n      className='df-header'\n      data-bordered={isBordered ? 'true' : 'false'}\n      style={{\n        paddingLeft: safeAreaLeft || 8,\n        transition: 'padding-left 160ms ease-in-out',\n      }}\n      onContextMenu={e => e.preventDefault()}\n    >\n      {/* Left Section: Add Calendar Button Only */}\n      <div className='df-header-left'>\n        {onAddCalendar && isEditable && (\n          <button\n            type='button'\n            id='dayflow-add-event-btn'\n            onClick={onAddCalendar}\n            className={iconButton}\n            title={\n              isMobile\n                ? t('newEvent') || 'New Event'\n                : t('createCalendar') || 'Add Calendar'\n            }\n          >\n            <Plus />\n          </button>\n        )}\n      </div>\n\n      {/* Middle Section: ViewSwitcher (if mode is buttons) */}\n      <div className='df-header-mid'>\n        {isSwitcherCentered && (\n          <ViewSwitcher mode={switcherMode} calendar={calendar} />\n        )}\n      </div>\n\n      {/* Right Section: Search, ViewSwitcher (if select) */}\n      <div className='df-header-right'>\n        {!isSwitcherCentered && (\n          <ViewSwitcher mode={switcherMode} calendar={calendar} />\n        )}\n\n        {isMobile ? (\n          /* Mobile: search icon only */\n          <button\n            type='button'\n            onClick={onSearchClick}\n            className={iconButton}\n            title={t('search') || 'Search'}\n          >\n            <Search />\n          </button>\n        ) : (\n          /* Desktop: inline search bar */\n          <div className='df-search-group'>\n            <div className='df-search-group-icon'>\n              <Search width={16} height={16} />\n            </div>\n            <input\n              id='dayflow-search-input'\n              type='text'\n              placeholder={t('search') || 'Search'}\n              value={searchValue}\n              onChange={handleSearchChange}\n              className='df-search-group-input'\n              style={{ width: '12rem' }}\n            />\n            {searchValue && (\n              <button\n                type='button'\n                onClick={handleClearSearch}\n                className='df-search-group-clear'\n              >\n                <svg\n                  width='14'\n                  height='14'\n                  viewBox='0 0 24 24'\n                  fill='none'\n                  stroke='currentColor'\n                  strokeWidth='2'\n                  strokeLinecap='round'\n                  strokeLinejoin='round'\n                >\n                  <line x1='18' y1='6' x2='6' y2='18'></line>\n                  <line x1='6' y1='6' x2='18' y2='18'></line>\n                </svg>\n              </button>\n            )}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default CalendarHeader;\n"
  },
  {
    "path": "packages/core/src/components/common/CalendarPicker.tsx",
    "content": "import { JSX } from 'preact';\nimport { createPortal } from 'preact/compat';\nimport { useState, useRef, useEffect } from 'preact/hooks';\n\nimport {\n  getDefaultCalendarRegistry,\n  CalendarRegistry,\n} from '@/core/calendarRegistry';\nimport { calendarPickerDropdown } from '@/styles/classNames';\n\nimport { ChevronsUpDown, Check } from './Icons';\n\nexport interface CalendarOption {\n  label: string;\n  value: string; // calendar ID\n}\n\nexport interface CalendarPickerProps {\n  options: CalendarOption[];\n  value: string;\n  onChange: (value: string) => void;\n  registry?: CalendarRegistry;\n  variant?: 'desktop' | 'mobile';\n  disabled?: boolean;\n}\n\n/**\n * CalendarPicker Component\n * Used to select which calendar an event belongs to\n */\nexport const CalendarPicker = ({\n  options,\n  value,\n  onChange,\n  registry,\n  variant = 'desktop',\n  disabled = false,\n}: CalendarPickerProps) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const [dropdownStyle, setDropdownStyle] = useState<JSX.CSSProperties>({});\n  const pickerRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n\n  const updatePosition = () => {\n    if (triggerRef.current) {\n      const rect = triggerRef.current.getBoundingClientRect();\n      const isMobile = variant === 'mobile';\n\n      const style: JSX.CSSProperties = {\n        position: 'fixed',\n        zIndex: 10001,\n        minWidth: isMobile ? '12rem' : `${rect.width}px`,\n        top: `${rect.bottom + 4}px`,\n      };\n\n      if (isMobile) {\n        style.right = `${window.innerWidth - rect.right}px`;\n      } else {\n        style.left = `${rect.left}px`;\n      }\n\n      setDropdownStyle(style);\n    }\n  };\n\n  useEffect(() => {\n    const handleClickOutside = (e: MouseEvent) => {\n      if (\n        pickerRef.current &&\n        !pickerRef.current.contains(e.target as Node) &&\n        !(e.target as HTMLElement).closest('[data-calendar-picker-dropdown]')\n      ) {\n        setIsOpen(false);\n      }\n    };\n\n    if (isOpen) {\n      updatePosition();\n      window.addEventListener('mousedown', handleClickOutside);\n      window.addEventListener('scroll', updatePosition, true);\n      window.addEventListener('resize', updatePosition);\n    }\n\n    return () => {\n      window.removeEventListener('mousedown', handleClickOutside);\n      window.removeEventListener('scroll', updatePosition, true);\n      window.removeEventListener('resize', updatePosition);\n    };\n  }, [isOpen]);\n\n  const getColorForCalendarId = (calendarId: string): string => {\n    const reg = registry || getDefaultCalendarRegistry();\n    const colors = reg.resolveColors(calendarId);\n    return colors.lineColor;\n  };\n\n  const handleSelect = (\n    e: JSX.TargetedMouseEvent<HTMLElement>,\n    optionValue: string\n  ) => {\n    e.stopPropagation();\n    onChange(optionValue);\n    setIsOpen(false);\n  };\n\n  const currentOption = options.find(o => o.value === value);\n\n  const renderDropdown = () => {\n    if (!isOpen || typeof window === 'undefined') return null;\n\n    if (variant === 'mobile') {\n      return createPortal(\n        <div\n          data-calendar-picker-dropdown='true'\n          style={dropdownStyle}\n          className={calendarPickerDropdown}\n        >\n          {options.map(opt => (\n            <div\n              key={opt.value}\n              className='df-calendar-picker-option df-calendar-picker-option-mobile'\n              data-selected={opt.value === value ? 'true' : 'false'}\n              onClick={e => handleSelect(e, opt.value)}\n            >\n              <div className='df-calendar-picker-option-inner'>\n                <div className='df-calendar-picker-check-area'>\n                  {opt.value === value && <Check className='df-text-primary' />}\n                </div>\n                <span className='df-calendar-picker-option-label'>\n                  {opt.label}\n                </span>\n              </div>\n              <span\n                className='df-calendar-picker-color-swatch df-calendar-picker-color-swatch-sm'\n                style={{ backgroundColor: getColorForCalendarId(opt.value) }}\n              />\n            </div>\n          ))}\n        </div>,\n        document.body\n      );\n    }\n\n    return createPortal(\n      <ul\n        data-calendar-picker-dropdown='true'\n        style={dropdownStyle}\n        className={calendarPickerDropdown}\n      >\n        {options.map(opt => (\n          <li\n            key={opt.value}\n            className='df-calendar-picker-option'\n            data-selected={value === opt.value ? 'true' : 'false'}\n            onClick={e => handleSelect(e, opt.value)}\n          >\n            <div className='df-calendar-picker-check-area'>\n              {opt.value === value && <Check className='df-text-primary' />}\n            </div>\n            <span\n              className='df-calendar-picker-color-swatch-sm'\n              style={{ backgroundColor: getColorForCalendarId(opt.value) }}\n            />\n            <span className='df-calendar-picker-option-label'>{opt.label}</span>\n          </li>\n        ))}\n      </ul>,\n      document.body\n    );\n  };\n\n  if (variant === 'mobile') {\n    return (\n      <div className='df-calendar-picker' ref={pickerRef}>\n        <button\n          type='button'\n          ref={triggerRef}\n          disabled={disabled}\n          onClick={e => {\n            e.stopPropagation();\n            if (!disabled) setIsOpen(!isOpen);\n          }}\n          className='df-calendar-picker-trigger df-calendar-picker-trigger-mobile'\n        >\n          <span\n            className='df-calendar-picker-color-swatch df-calendar-picker-color-swatch-sm'\n            style={{ backgroundColor: getColorForCalendarId(value) }}\n          />\n          <span className='df-calendar-picker-label'>\n            {currentOption?.label || value}\n          </span>\n          <ChevronsUpDown className='df-text-muted' />\n        </button>\n        {renderDropdown()}\n      </div>\n    );\n  }\n\n  return (\n    <div className='df-calendar-picker' ref={pickerRef}>\n      <button\n        ref={triggerRef}\n        type='button'\n        onClick={e => {\n          e.stopPropagation();\n          setIsOpen(!isOpen);\n        }}\n        className='df-calendar-picker-trigger'\n      >\n        <span\n          className='df-calendar-picker-color-swatch'\n          style={{ backgroundColor: getColorForCalendarId(value) }}\n        />\n        <ChevronsUpDown className='df-text-muted' />\n      </button>\n      {renderDropdown()}\n    </div>\n  );\n};\n\nexport default CalendarPicker;\n"
  },
  {
    "path": "packages/core/src/components/common/CreateCalendarDialog.tsx",
    "content": "import {\n  DEFAULT_COLORS,\n  hslToHex,\n  lightnessToSliderValue,\n} from '@dayflow/blossom-color-picker';\nimport { createPortal } from 'preact/compat';\nimport { useState, useMemo } from 'preact/hooks';\n\nimport { useTheme } from '@/contexts/ThemeContext';\nimport { getCalendarColorsForHex } from '@/core/calendarRegistry';\nimport { useLocale } from '@/locale';\nimport { ContentSlot } from '@/renderer/ContentSlot';\nimport { CalendarType, CreateCalendarDialogProps } from '@/types';\nimport { generateUniKey } from '@/utils/helpers';\n\nimport { BlossomColorPicker } from './BlossomColorPicker';\nimport { DefaultColorPicker } from './DefaultColorPicker';\nimport { LoadingButton } from './LoadingButton';\n\nconst PICKER_DEFAULT_COLORS = [\n  '#ea426b',\n  '#f19a38',\n  '#f7cf46',\n  '#83d754',\n  '#51aaf2',\n  '#b672d0',\n  '#957e5e',\n];\n\nexport const CreateCalendarDialog = ({\n  onClose,\n  onCreate,\n  app,\n}: CreateCalendarDialogProps) => {\n  const { t } = useLocale();\n  const { effectiveTheme } = useTheme();\n  const [name, setName] = useState('');\n  const [isLoading, setIsLoading] = useState(false);\n\n  const hasCustomPicker = app.state.overrides.includes(\n    'createCalendarDialogColorPicker'\n  );\n\n  const [customSelectedColor, setCustomSelectedColor] = useState(\n    PICKER_DEFAULT_COLORS[\n      Math.floor(Math.random() * PICKER_DEFAULT_COLORS.length)\n    ]\n  );\n  const [showPicker, setShowPicker] = useState(false);\n  const [previousColor, setPreviousColor] = useState('');\n\n  const initialColorData = useMemo(() => {\n    const randomColor =\n      DEFAULT_COLORS[Math.floor(Math.random() * DEFAULT_COLORS.length)];\n    const layer = (randomColor as { layer?: string }).layer || 'outer';\n    const sliderValue = lightnessToSliderValue(randomColor.l);\n    return {\n      hue: randomColor.h,\n      saturation: sliderValue,\n      lightness: randomColor.l,\n      alpha: 100,\n      layer: layer as 'inner' | 'outer',\n    };\n  }, []);\n\n  const [blossomSelectedColor, setBlossomSelectedColor] = useState<{\n    hex: string;\n    hue: number;\n    saturation: number;\n    lightness?: number;\n    alpha: number;\n    layer: 'inner' | 'outer';\n  } | null>(null);\n\n  const handleSubmit = async (e: React.FormEvent) => {\n    e.preventDefault();\n    if (!name.trim() || isLoading) return;\n\n    setIsLoading(true);\n    try {\n      let hex: string;\n      if (hasCustomPicker) {\n        hex = customSelectedColor;\n      } else {\n        hex =\n          blossomSelectedColor?.hex ??\n          hslToHex(\n            initialColorData.hue,\n            initialColorData.saturation,\n            initialColorData.lightness\n          );\n      }\n\n      const { colors, darkColors } = getCalendarColorsForHex(hex);\n\n      const newCalendar: CalendarType = {\n        id: generateUniKey(),\n        name: name.trim(),\n        colors,\n        darkColors,\n        isVisible: true,\n        isDefault: false,\n      };\n\n      await onCreate(newCalendar);\n      onClose();\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleColorChange = (color: { hex: string }) => {\n    setCustomSelectedColor(color.hex);\n  };\n\n  const handleOpenPicker = () => {\n    setPreviousColor(customSelectedColor);\n    setShowPicker(true);\n  };\n\n  const handleAccept = () => {\n    setShowPicker(false);\n  };\n\n  const handleCancel = () => {\n    setCustomSelectedColor(previousColor);\n    setShowPicker(false);\n  };\n\n  const isDark = effectiveTheme === 'dark';\n  const pickerStyles = {\n    default: {\n      picker: {\n        background: isDark ? '#1e293b' : '#ffffff',\n        boxShadow:\n          '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',\n        borderRadius: '0.5rem',\n        border: isDark ? '1px solid #4b5563' : '1px solid #e5e7eb',\n      },\n      head: {\n        background: isDark ? '#1e293b' : '#ffffff',\n        borderBottom: isDark ? '1px solid #4b5563' : '1px solid #e5e7eb',\n        boxShadow: 'none',\n      },\n      body: { background: isDark ? '#1e293b' : '#ffffff' },\n      controls: {\n        border: isDark ? '1px solid #4b5563' : '1px solid #e5e7eb',\n      },\n      input: {\n        background: isDark ? '#374151' : '#ffffff',\n        color: isDark ? '#f3f4f6' : '#1f2937',\n        border: isDark ? '1px solid #4b5563' : '1px solid #e5e7eb',\n        boxShadow: 'none',\n      },\n      previews: {\n        border: isDark ? '1px solid #4b5563' : '1px solid #e5e7eb',\n      },\n      actions: {\n        borderTop: isDark ? '1px solid #4b5563' : '1px solid #e5e7eb',\n      },\n    },\n  };\n\n  if (typeof window === 'undefined') return null;\n\n  return createPortal(\n    <div className='df-portal df-create-calendar-dialog-backdrop'>\n      <div\n        className='df-animate-in df-fade-in df-zoom-in-95 df-create-calendar-dialog-panel'\n        onClick={e => e.stopPropagation()}\n      >\n        <h2\n          className={`df-create-calendar-dialog-title ${\n            hasCustomPicker ? 'df-create-calendar-dialog-title-compact' : ''\n          }`}\n        >\n          {t('createCalendar')}\n        </h2>\n\n        <form onSubmit={handleSubmit}>\n          {hasCustomPicker ? (\n            <>\n              <div className='df-create-calendar-dialog-color-row'>\n                <div\n                  className='df-create-calendar-dialog-color-preview'\n                  style={{ backgroundColor: customSelectedColor }}\n                />\n                <input\n                  id='custom-calendar-name'\n                  name='calendar-name'\n                  type='text'\n                  value={name}\n                  onChange={e => setName((e.target as HTMLInputElement).value)}\n                  className='df-form-input'\n                  style={{ flex: 1 }}\n                  placeholder={t('calendarNamePlaceholder')}\n                  autoFocus\n                />\n              </div>\n\n              <div className='df-create-calendar-dialog-color-section'>\n                <div className='df-create-calendar-dialog-color-grid'>\n                  {PICKER_DEFAULT_COLORS.map(color => (\n                    <button\n                      key={color}\n                      type='button'\n                      className='df-create-calendar-dialog-color-btn'\n                      data-selected={\n                        customSelectedColor === color ? 'true' : 'false'\n                      }\n                      style={{ backgroundColor: color }}\n                      onClick={() => setCustomSelectedColor(color)}\n                    />\n                  ))}\n                </div>\n\n                <div style={{ position: 'relative' }}>\n                  <button\n                    type='button'\n                    onClick={handleOpenPicker}\n                    className='df-create-calendar-dialog-custom-color-btn'\n                  >\n                    {t('customColor')}\n                  </button>\n\n                  {showPicker && (\n                    <div\n                      className='df-create-calendar-dialog-picker-overlay'\n                      onClick={handleCancel}\n                    >\n                      <div\n                        className='df-animate-in df-fade-in df-zoom-in-95 df-create-calendar-dialog-picker-inner'\n                        onClick={e => e.stopPropagation()}\n                      >\n                        <ContentSlot\n                          generatorName='createCalendarDialogColorPicker'\n                          generatorArgs={{\n                            color: customSelectedColor,\n                            onChange: handleColorChange,\n                            onAccept: handleAccept,\n                            onCancel: handleCancel,\n                            styles: pickerStyles,\n                          }}\n                          defaultContent={\n                            <div className='df-create-calendar-dialog-picker-card'>\n                              <DefaultColorPicker\n                                color={customSelectedColor}\n                                onChange={handleColorChange}\n                              />\n                              <div className='df-create-calendar-dialog-picker-actions'>\n                                <button\n                                  type='button'\n                                  onClick={handleCancel}\n                                  className='df-btn-sm df-btn-sm-ghost'\n                                >\n                                  {t('cancel')}\n                                </button>\n                                <button\n                                  type='button'\n                                  onClick={handleAccept}\n                                  className='df-fill-primary df-btn-sm'\n                                >\n                                  OK\n                                </button>\n                              </div>\n                            </div>\n                          }\n                        />\n                      </div>\n                    </div>\n                  )}\n                </div>\n              </div>\n            </>\n          ) : (\n            <div className='df-create-calendar-dialog-blossom-row'>\n              <div className='df-create-calendar-dialog-blossom-input-area'>\n                <input\n                  id='blossom-calendar-name'\n                  name='calendar-name'\n                  type='text'\n                  value={name}\n                  onChange={e => setName((e.target as HTMLInputElement).value)}\n                  className='df-form-input'\n                  placeholder={t('calendarNamePlaceholder')}\n                  autoFocus\n                />\n              </div>\n\n              <div className='df-create-calendar-dialog-blossom-picker-wrap'>\n                <div className='df-create-calendar-dialog-blossom-picker-inner'>\n                  <BlossomColorPicker\n                    defaultValue={initialColorData}\n                    coreSize={36}\n                    petalSize={32}\n                    openOnHover={false}\n                    onChange={color => setBlossomSelectedColor(color)}\n                    onCollapse={color => setBlossomSelectedColor(color)}\n                    className='df-create-calendar-dialog-blossom-picker'\n                  />\n                </div>\n              </div>\n            </div>\n          )}\n\n          <div className='df-create-calendar-dialog-actions'>\n            <button\n              type='button'\n              onClick={onClose}\n              disabled={isLoading}\n              className='df-btn-sm df-btn-sm-ghost'\n            >\n              {t('cancel')}\n            </button>\n            <LoadingButton\n              type='submit'\n              disabled={!name.trim()}\n              loading={isLoading}\n              className='df-fill-primary df-hover-primary-solid df-btn-sm'\n            >\n              {t('create')}\n            </LoadingButton>\n          </div>\n        </form>\n      </div>\n    </div>,\n    document.body\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/common/DefaultColorPicker.tsx",
    "content": "import {\n  hexToHsl,\n  lightnessToSliderValue,\n} from '@dayflow/blossom-color-picker';\nimport { useMemo } from 'preact/hooks';\n\nimport { BlossomColorPicker } from './BlossomColorPicker';\n\ninterface DefaultColorPickerProps {\n  color: string;\n  onChange: (color: { hex: string }, isPending?: boolean) => void;\n  onClose?: () => void;\n  [key: string]: unknown;\n}\n\nexport const DefaultColorPicker = ({\n  color,\n  onChange,\n  onClose,\n}: DefaultColorPickerProps) => {\n  const blossomValue = useMemo(() => {\n    const { h, l } = hexToHsl(color);\n    const sliderValue = lightnessToSliderValue(l);\n\n    return {\n      hue: h,\n      saturation: sliderValue,\n      lightness: l,\n      alpha: 100,\n      layer: 'outer' as const,\n    };\n  }, [color]);\n\n  return (\n    <div className='df-default-color-picker'>\n      <BlossomColorPicker\n        defaultValue={blossomValue}\n        coreSize={36}\n        petalSize={32}\n        initialExpanded={true}\n        openOnHover={false}\n        onChange={c => onChange({ hex: c.hex }, true)}\n        onCollapse={onClose}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/common/DefaultEventDetailDialog.tsx",
    "content": "import { RangePicker } from '@dayflow/ui-range-picker';\nimport { createPortal } from 'preact/compat';\nimport { useMemo, useState, useEffect, useRef } from 'preact/hooks';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { getDefaultCalendarRegistry } from '@/core/calendarRegistry';\nimport { useLocale } from '@/locale';\nimport { dialogContainer } from '@/styles/classNames';\nimport { ICalendarApp } from '@/types';\nimport { EventDetailDialogProps } from '@/types/eventDetail';\nimport { isEventDeepEqual } from '@/utils/eventUtils';\nimport { isPlainDate } from '@/utils/temporal';\nimport { restoreVisualEventToCanonical } from '@/utils/timeUtils';\n\nimport { CalendarPicker, CalendarOption } from './CalendarPicker';\nimport { LoadingButton } from './LoadingButton';\n\ninterface DefaultEventDetailDialogProps extends EventDetailDialogProps {\n  app?: ICalendarApp;\n}\n\n/**\n * Default event detail dialog component (Dialog mode)\n */\nconst DefaultEventDetailDialog = ({\n  event,\n  isOpen,\n  onEventUpdate,\n  onEventDelete,\n  onClose,\n  app,\n}: DefaultEventDetailDialogProps) => {\n  const [editedEvent, setEditedEvent] = useState(event);\n  const [isSaving, setIsSaving] = useState(false);\n  const [isDeleting, setIsDeleting] = useState(false);\n  const previousEventIdRef = useRef(event.id);\n  const { t } = useLocale();\n\n  useEffect(() => {\n    setEditedEvent(event);\n    if (previousEventIdRef.current !== event.id) {\n      setIsSaving(false);\n      setIsDeleting(false);\n      previousEventIdRef.current = event.id;\n    }\n  }, [event]);\n\n  const colorOptions: CalendarOption[] = useMemo(() => {\n    const registry = app\n      ? app.getCalendarRegistry()\n      : getDefaultCalendarRegistry();\n    return registry.getVisible().map(cal => ({\n      label: cal.name,\n      value: cal.id,\n    }));\n  }, [app, app?.getCalendars()]);\n\n  const handleSave = async () => {\n    if (isSaving || isDeleting) return;\n    setIsSaving(true);\n    try {\n      await onEventUpdate(\n        restoreVisualEventToCanonical(event, editedEvent, app?.timeZone)\n      );\n      onClose();\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  const handleDelete = async () => {\n    if (isSaving || isDeleting) return;\n    setIsDeleting(true);\n    try {\n      await onEventDelete(event.id);\n      onClose();\n    } finally {\n      setIsDeleting(false);\n    }\n  };\n\n  const hasChanges = useMemo(\n    () => !isEventDeepEqual(event, editedEvent),\n    [event, editedEvent]\n  );\n\n  const convertToAllDay = () => {\n    const plainDate = isPlainDate(editedEvent.start)\n      ? editedEvent.start\n      : editedEvent.start.toPlainDate();\n    setEditedEvent({\n      ...editedEvent,\n      allDay: true,\n      start: plainDate,\n      end: plainDate,\n    });\n  };\n\n  const convertToRegular = () => {\n    const plainDate = isPlainDate(editedEvent.start)\n      ? editedEvent.start\n      : editedEvent.start.toPlainDate();\n    const tz = app?.timeZone ?? Temporal.Now.timeZoneId();\n    const start = Temporal.ZonedDateTime.from({\n      year: plainDate.year,\n      month: plainDate.month,\n      day: plainDate.day,\n      hour: 9,\n      minute: 0,\n      timeZone: tz,\n    });\n    const end = Temporal.ZonedDateTime.from({\n      year: plainDate.year,\n      month: plainDate.month,\n      day: plainDate.day,\n      hour: 10,\n      minute: 0,\n      timeZone: tz,\n    });\n    setEditedEvent({ ...editedEvent, allDay: false, start, end });\n  };\n\n  const eventTimeZone = useMemo(\n    () => app?.timeZone ?? Temporal.Now.timeZoneId(),\n    [app]\n  );\n\n  const startOfWeek = useMemo(\n    () => (app?.getViewConfig('week')?.startOfWeek as number) ?? 1,\n    [app]\n  );\n\n  const handleAllDayRangeChange = (\n    nextRange: [Temporal.ZonedDateTime, Temporal.ZonedDateTime]\n  ) => {\n    const [start, end] = nextRange;\n    setEditedEvent({\n      ...editedEvent,\n      start: start.toPlainDate(),\n      end: end.toPlainDate(),\n    });\n  };\n\n  const isEditable = app?.canMutateFromUI(event.id) ?? false;\n  const readOnlyConfig = app?.getReadOnlyConfig(event.id) as {\n    draggable: boolean;\n    viewable: boolean;\n  };\n  const isViewable = readOnlyConfig?.viewable !== false;\n  const isPending = isSaving || isDeleting;\n\n  const isSubscribed = useMemo(() => {\n    if (!event.calendarId) return false;\n    const calendar = app?.getCalendarRegistry().get(event.calendarId);\n    return !!calendar?.subscription;\n  }, [app, event.calendarId]);\n\n  const shouldShowNotes =\n    !isSubscribed || (editedEvent.description || '').trim() !== '';\n\n  if (!isOpen || !isViewable) return null;\n\n  if (typeof window === 'undefined' || typeof document === 'undefined') {\n    return null;\n  }\n\n  const handleBackdropClick = (e: MouseEvent) => {\n    const target = e.target as HTMLElement;\n    if (target.closest('[data-range-picker-popup]')) return;\n    if (target === e.currentTarget) onClose();\n  };\n\n  const dialogContent = (\n    <div\n      className='df-portal df-event-dialog-overlay'\n      style={{\n        zIndex: 9998,\n      }}\n      data-event-detail-dialog='true'\n    >\n      {/* Backdrop */}\n      <div\n        className='df-event-dialog-backdrop'\n        style={{ position: 'absolute', inset: 0 }}\n        onClick={handleBackdropClick}\n      />\n\n      {/* Dialog */}\n      <div className={dialogContainer}>\n        <button\n          type='button'\n          onClick={onClose}\n          disabled={isPending}\n          className='df-event-dialog-close'\n          aria-label='Close'\n        >\n          <svg fill='none' stroke='currentColor' viewBox='0 0 24 24'>\n            <path\n              strokeLinecap='round'\n              strokeLinejoin='round'\n              strokeWidth={2}\n              d='M6 18L18 6M6 6l12 12'\n            />\n          </svg>\n        </button>\n\n        <div>\n          <span className='df-form-label'>{t('eventTitle')}</span>\n          <div className='df-form-row'>\n            <div className='df-form-field'>\n              <input\n                id={`event-dialog-title-${editedEvent.id}`}\n                name='title'\n                type='text'\n                value={editedEvent.title}\n                readOnly={!isEditable || isPending}\n                disabled={!isEditable || isPending}\n                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {\n                  setEditedEvent({\n                    ...editedEvent,\n                    title: (e.target as HTMLInputElement).value,\n                  });\n                }}\n                className='df-form-input'\n              />\n            </div>\n            {isEditable && (\n              <CalendarPicker\n                options={colorOptions}\n                value={editedEvent.calendarId || 'blue'}\n                disabled={isPending}\n                onChange={value =>\n                  setEditedEvent({ ...editedEvent, calendarId: value })\n                }\n                registry={app?.getCalendarRegistry()}\n              />\n            )}\n          </div>\n\n          {editedEvent.allDay ? (\n            <div className='df-event-dialog-time-row'>\n              <div className='df-form-label'>{t('dateRange')}</div>\n              <RangePicker\n                value={[editedEvent.start, editedEvent.end]}\n                format='YYYY-MM-DD'\n                showTime={false}\n                timeZone={eventTimeZone}\n                startOfWeek={startOfWeek}\n                matchTriggerWidth\n                disabled={!isEditable || isPending}\n                onChange={handleAllDayRangeChange}\n                onOk={handleAllDayRangeChange}\n                locale={app?.state.locale}\n              />\n            </div>\n          ) : (\n            <div className='df-event-dialog-time-row'>\n              <div className='df-form-label'>{t('timeRange')}</div>\n              <RangePicker\n                value={[editedEvent.start, editedEvent.end]}\n                timeZone={eventTimeZone}\n                startOfWeek={startOfWeek}\n                disabled={!isEditable || isPending}\n                onChange={(\n                  nextRange: [Temporal.ZonedDateTime, Temporal.ZonedDateTime]\n                ) => {\n                  const [start, end] = nextRange;\n                  setEditedEvent({ ...editedEvent, start, end });\n                }}\n                onOk={(\n                  nextRange: [Temporal.ZonedDateTime, Temporal.ZonedDateTime]\n                ) => {\n                  const [start, end] = nextRange;\n                  setEditedEvent({ ...editedEvent, start, end });\n                }}\n                locale={app?.state.locale}\n              />\n            </div>\n          )}\n\n          {shouldShowNotes && (\n            <div className='df-event-dialog-notes-row'>\n              <span className='df-form-label'>{t('note')}</span>\n              <textarea\n                id={`event-dialog-note-${editedEvent.id}`}\n                name='note'\n                value={editedEvent.description ?? ''}\n                readOnly={!isEditable || isPending}\n                disabled={!isEditable || isPending}\n                onChange={e =>\n                  setEditedEvent({\n                    ...editedEvent,\n                    description: (e.target as HTMLTextAreaElement).value,\n                  })\n                }\n                rows={4}\n                className='df-form-textarea'\n                placeholder={t('addNotePlaceholder')}\n              />\n            </div>\n          )}\n\n          {isEditable && (\n            <div className='df-form-actions'>\n              {editedEvent.allDay ? (\n                <button\n                  type='button'\n                  disabled={isPending}\n                  className='df-tint-primary df-hover-primary-md df-btn-sm'\n                  onClick={convertToRegular}\n                >\n                  {t('setAsTimed')}\n                </button>\n              ) : (\n                <button\n                  type='button'\n                  disabled={isPending}\n                  className='df-tint-primary df-hover-primary-md df-btn-sm'\n                  onClick={convertToAllDay}\n                >\n                  {t('setAsAllDay')}\n                </button>\n              )}\n\n              <LoadingButton\n                type='button'\n                disabled={isPending}\n                className='df-fill-destructive df-hover-destructive df-btn-sm'\n                onClick={handleDelete}\n                loading={isDeleting}\n              >\n                {t('delete')}\n              </LoadingButton>\n\n              <LoadingButton\n                type='button'\n                className={`df-fill-primary df-btn-sm ${\n                  hasChanges ? 'df-shadow-primary df-hover-primary-solid' : ''\n                }`}\n                style={{ marginLeft: 'auto' }}\n                onClick={handleSave}\n                disabled={!hasChanges || isPending}\n                loading={isSaving}\n              >\n                {t('save')}\n              </LoadingButton>\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n\n  const portalTarget = document.body;\n  if (!portalTarget) return null;\n\n  return createPortal(dialogContent, portalTarget);\n};\n\nexport default DefaultEventDetailDialog;\n"
  },
  {
    "path": "packages/core/src/components/common/DefaultEventDetailPanel.tsx",
    "content": "import { RangePicker } from '@dayflow/ui-range-picker';\nimport { JSX } from 'preact';\nimport { createPortal } from 'preact/compat';\nimport {\n  useMemo,\n  useState,\n  useEffect,\n  useRef,\n  useCallback,\n} from 'preact/hooks';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { getCalendarContentElement } from '@/components/calendarEvent/utils';\nimport { useTheme } from '@/contexts/ThemeContext';\nimport { getDefaultCalendarRegistry } from '@/core/calendarRegistry';\nimport { useLocale } from '@/locale';\nimport { eventDetailPanel } from '@/styles/classNames';\nimport {\n  Event,\n  EventDetailPanelProps,\n  CalendarType,\n  ICalendarApp,\n} from '@/types';\nimport { isEventDeepEqual } from '@/utils/eventUtils';\nimport { logger } from '@/utils/logger';\nimport { isPlainDate } from '@/utils/temporal';\nimport { resolveAppliedTheme } from '@/utils/themeUtils';\nimport { restoreVisualEventToCanonical } from '@/utils/timeUtils';\n\nimport { CalendarOption, CalendarPicker } from './CalendarPicker';\nimport { LoadingButton } from './LoadingButton';\n\ninterface DefaultEventDetailPanelProps extends EventDetailPanelProps {\n  app?: ICalendarApp;\n}\n\n/**\n * Default event detail panel component\n */\nconst DefaultEventDetailPanel = ({\n  event,\n  position,\n  panelRef,\n  isAllDay: _isAllDay,\n  eventVisibility,\n  calendarRef,\n  selectedEventElementRef,\n  onEventUpdate,\n  onEventDelete,\n  onClose: _onClose,\n  app,\n}: DefaultEventDetailPanelProps) => {\n  const { effectiveTheme } = useTheme();\n  const appliedTheme = resolveAppliedTheme(effectiveTheme);\n  const { t } = useLocale();\n\n  const [draftEvent, setDraftEvent] = useState(event);\n  const [isLoading, setIsLoading] = useState(false);\n  const committedEventRef = useRef(event);\n  const draftEventRef = useRef(event);\n  const skipCommitRef = useRef(false);\n\n  const commitDraftChanges = useCallback(() => {\n    if (skipCommitRef.current) return;\n\n    const committedEvent = committedEventRef.current;\n    const latestDraftEvent = draftEventRef.current;\n\n    if (isEventDeepEqual(committedEvent, latestDraftEvent)) return;\n\n    const canonicalDraftEvent = restoreVisualEventToCanonical(\n      committedEvent,\n      latestDraftEvent,\n      app?.timeZone\n    );\n\n    committedEventRef.current = canonicalDraftEvent;\n    draftEventRef.current = canonicalDraftEvent;\n    const updateResult = onEventUpdate(canonicalDraftEvent);\n    if (updateResult) {\n      Promise.resolve(updateResult).catch(error => {\n        logger.error(\n          'Failed to commit deferred event detail panel update',\n          error\n        );\n      });\n    }\n  }, [app?.timeZone, onEventUpdate]);\n\n  const applyDraftEventUpdate = useCallback(\n    (nextDraftEvent: Event) => {\n      if (isEventDeepEqual(draftEventRef.current, nextDraftEvent)) return;\n\n      draftEventRef.current = nextDraftEvent;\n      setDraftEvent(nextDraftEvent);\n\n      if (app) {\n        const updateResult = app.updateEvent(\n          nextDraftEvent.id,\n          nextDraftEvent,\n          true\n        );\n        if (updateResult) {\n          Promise.resolve(updateResult).catch(error => {\n            logger.error(\n              'Failed to apply pending event detail panel update',\n              error\n            );\n          });\n        }\n      }\n    },\n    [app]\n  );\n\n  useEffect(() => {\n    const sameEvent = committedEventRef.current.id === event.id;\n\n    if (!sameEvent) {\n      commitDraftChanges();\n      skipCommitRef.current = false;\n      committedEventRef.current = event;\n      draftEventRef.current = event;\n      setDraftEvent(event);\n      return;\n    }\n\n    if (isEventDeepEqual(committedEventRef.current, draftEventRef.current)) {\n      committedEventRef.current = event;\n      draftEventRef.current = event;\n      setDraftEvent(event);\n    }\n  }, [event, commitDraftChanges]);\n\n  useEffect(\n    () => () => {\n      commitDraftChanges();\n    },\n    [commitDraftChanges]\n  );\n\n  const eventTimeZone = useMemo(\n    () => app?.timeZone ?? Temporal.Now.timeZoneId(),\n    [app]\n  );\n\n  const startOfWeek = useMemo(\n    () => (app?.getViewConfig('week')?.startOfWeek as number) ?? 1,\n    [app]\n  );\n\n  // Get visible calendar type options\n  const colorOptions: CalendarOption[] = useMemo(() => {\n    const registry = app\n      ? app.getCalendarRegistry()\n      : getDefaultCalendarRegistry();\n    return registry.getVisible().map((cal: CalendarType) => ({\n      label: cal.name,\n      value: cal.id,\n    }));\n  }, [app, app?.getCalendars()]); // Depend on app.getCalendars() to update when calendars change\n\n  // Check if dark mode is active (either via theme context or DOM class)\n  const isDark =\n    appliedTheme === 'dark' ||\n    (typeof document !== 'undefined' &&\n      document.documentElement.classList.contains('dark'));\n  const isEditable = app?.canMutateFromUI(event.id) ?? false;\n  const readOnlyConfig = app?.getReadOnlyConfig(event.id) as {\n    draggable: boolean;\n    viewable: boolean;\n  };\n  const isViewable = readOnlyConfig?.viewable !== false;\n  const isDraftAllDay = !!draftEvent.allDay;\n\n  // Check if it's a subscribed calendar\n  const isSubscribed = useMemo(() => {\n    if (!event.calendarId) return false;\n    const calendar = app?.getCalendarRegistry().get(event.calendarId);\n    return !!calendar?.subscription;\n  }, [app, event.calendarId]);\n\n  // If subscribed calendar and no notes, hide notes field\n  const shouldShowNotes =\n    !isSubscribed || (draftEvent.description || '').trim() !== '';\n\n  if (!isViewable) return null;\n\n  const arrowBgColor = isDark ? '#1f2937' : 'white';\n  const arrowBorderColor = isDark ? 'rgb(55, 65, 81)' : 'rgb(229, 231, 235)';\n\n  const convertToAllDay = () => {\n    if (isLoading) return;\n    const plainDate = isPlainDate(draftEvent.start)\n      ? draftEvent.start\n      : draftEvent.start.toPlainDate();\n    const plainEndDate = isPlainDate(draftEvent.end)\n      ? draftEvent.end\n      : draftEvent.end.toPlainDate();\n\n    applyDraftEventUpdate({\n      ...draftEvent,\n      allDay: true,\n      start: plainDate,\n      end: plainEndDate,\n    });\n  };\n\n  const convertToRegular = () => {\n    if (isLoading) return;\n    const plainDate = isPlainDate(draftEvent.start)\n      ? draftEvent.start\n      : draftEvent.start.toPlainDate();\n    const tz = app?.timeZone ?? Temporal.Now.timeZoneId();\n    const start = Temporal.ZonedDateTime.from({\n      year: plainDate.year,\n      month: plainDate.month,\n      day: plainDate.day,\n      hour: 9,\n      minute: 0,\n      timeZone: tz,\n    });\n    const end = Temporal.ZonedDateTime.from({\n      year: plainDate.year,\n      month: plainDate.month,\n      day: plainDate.day,\n      hour: 10,\n      minute: 0,\n      timeZone: tz,\n    });\n\n    applyDraftEventUpdate({\n      ...draftEvent,\n      allDay: false,\n      start,\n      end,\n    });\n  };\n\n  const handleAllDayRangeChange = (\n    nextRange: [Temporal.ZonedDateTime, Temporal.ZonedDateTime]\n  ) => {\n    if (isLoading) return;\n    const [start, end] = nextRange;\n\n    applyDraftEventUpdate({\n      ...draftEvent,\n      start: start.toPlainDate(),\n      end: end.toPlainDate(),\n    });\n  };\n\n  const handleEventDelete = async () => {\n    if (isLoading) return;\n    skipCommitRef.current = true;\n    setIsLoading(true);\n    try {\n      await onEventDelete(draftEvent.id);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  // Calculate arrow style\n  const calculateArrowStyle = (): JSX.CSSProperties => {\n    let arrowStyle: JSX.CSSProperties = {};\n\n    if (eventVisibility === 'sticky-top') {\n      const calendarContent = getCalendarContentElement(calendarRef);\n      if (calendarContent) {\n        const contentRect = calendarContent.getBoundingClientRect();\n        const stickyEventCenterY = contentRect.top + 3;\n        const arrowRelativeY = stickyEventCenterY - position.top;\n\n        arrowStyle = {\n          position: 'absolute',\n          width: '12px',\n          height: '12px',\n          backgroundColor: arrowBgColor,\n          transform: 'rotate(45deg)',\n          transformOrigin: 'center',\n          top: `${arrowRelativeY - 6}px`,\n          borderRight: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n          borderTop: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n          borderLeft: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n          borderBottom: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n          ...(position.isSunday ? { right: '-6px' } : { left: '-6px' }),\n        };\n      }\n    } else if (eventVisibility === 'sticky-bottom') {\n      const panelElement = panelRef.current;\n      let arrowTop = 200;\n\n      if (panelElement) {\n        const panelRect = panelElement.getBoundingClientRect();\n        const computedStyle = window.getComputedStyle(panelElement);\n        const paddingBottom =\n          Number.parseInt(computedStyle.paddingBottom, 10) || 0;\n        const borderBottom =\n          Number.parseInt(computedStyle.borderBottomWidth, 10) || 0;\n\n        arrowTop = panelRect.height - paddingBottom - borderBottom - 6 + 11;\n      }\n\n      arrowStyle = {\n        position: 'absolute',\n        width: '12px',\n        height: '12px',\n        backgroundColor: arrowBgColor,\n        transform: 'rotate(45deg)',\n        transformOrigin: 'center',\n        top: `${arrowTop}px`,\n        left: position.isSunday ? undefined : '-6px',\n        right: position.isSunday ? '-6px' : undefined,\n        borderRight: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n        borderTop: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n        borderLeft: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n        borderBottom: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n      };\n    } else {\n      if (position && selectedEventElementRef.current && calendarRef.current) {\n        const eventRect =\n          selectedEventElementRef.current.getBoundingClientRect();\n        const calendarContent = getCalendarContentElement(calendarRef);\n\n        if (calendarContent) {\n          const viewportRect = calendarContent.getBoundingClientRect();\n\n          const visibleTop = Math.max(eventRect.top, viewportRect.top);\n          const visibleBottom = Math.min(eventRect.bottom, viewportRect.bottom);\n          const visibleHeight = Math.max(0, visibleBottom - visibleTop);\n\n          let targetY;\n          if (visibleHeight === eventRect.height) {\n            targetY = eventRect.top + eventRect.height / 2;\n          } else if (visibleHeight > 0) {\n            targetY = visibleTop + visibleHeight / 2;\n          } else {\n            targetY = eventRect.top + eventRect.height / 2;\n          }\n\n          const arrowRelativeY = targetY - position.top;\n\n          const panelElement = panelRef.current;\n          let maxArrowY = 240 - 12;\n\n          if (panelElement) {\n            const panelRect = panelElement.getBoundingClientRect();\n            const computedStyle = window.getComputedStyle(panelElement);\n            const paddingBottom =\n              Number.parseInt(computedStyle.paddingBottom, 10) || 0;\n            const borderBottom =\n              Number.parseInt(computedStyle.borderBottomWidth, 10) || 0;\n\n            maxArrowY = panelRect.height - paddingBottom - borderBottom + 11;\n          }\n\n          const minArrowY = 12;\n          const finalArrowY = Math.max(\n            minArrowY,\n            Math.min(maxArrowY, arrowRelativeY)\n          );\n\n          arrowStyle = {\n            position: 'absolute',\n            width: '12px',\n            height: '12px',\n            backgroundColor: arrowBgColor,\n            transform: 'rotate(45deg)',\n            transformOrigin: 'center',\n            top: `${finalArrowY - 6}px`,\n            borderRight: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n            borderTop: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n            borderLeft: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n            borderBottom: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n            ...(position.isSunday ? { right: '-6px' } : { left: '-6px' }),\n          };\n        }\n      }\n    }\n\n    return arrowStyle;\n  };\n\n  const arrowStyle = calculateArrowStyle();\n\n  const panelContent = (\n    <div\n      ref={panelRef}\n      className={`${eventDetailPanel} df-event-panel-content`}\n      data-event-detail-panel='true'\n      data-event-id={event.id}\n      style={{\n        top: `${position.top}px`,\n        left: `${position.left}px`,\n        zIndex: 9999,\n        pointerEvents: 'auto',\n      }}\n    >\n      <div style={arrowStyle}></div>\n      <span className='df-form-label'>{t('eventTitle')}</span>\n      <div className='df-form-row' style={{ marginBottom: '0.75rem' }}>\n        <div className='df-form-field'>\n          <input\n            id={`event-title-${draftEvent.id}`}\n            name='title'\n            type='text'\n            value={draftEvent.title}\n            readOnly={!isEditable || isLoading}\n            disabled={!isEditable || isLoading}\n            onChange={(e: React.ChangeEvent<HTMLInputElement>) =>\n              applyDraftEventUpdate({\n                ...draftEvent,\n                title: (e.target as HTMLInputElement).value,\n              })\n            }\n            onInput={(e: React.FormEvent<HTMLInputElement>) =>\n              applyDraftEventUpdate({\n                ...draftEvent,\n                title: (e.target as HTMLInputElement).value,\n              })\n            }\n            className='df-form-input'\n          />\n        </div>\n        {isEditable && (\n          <CalendarPicker\n            options={colorOptions}\n            value={draftEvent.calendarId || 'blue'}\n            disabled={isLoading}\n            onChange={value => {\n              applyDraftEventUpdate({\n                ...draftEvent,\n                calendarId: value,\n              });\n            }}\n            registry={app?.getCalendarRegistry()}\n          />\n        )}\n      </div>\n\n      {isDraftAllDay ? (\n        <div\n          className='df-event-dialog-time-row'\n          style={{ marginBottom: '0.75rem' }}\n        >\n          <div className='df-form-label'>{t('dateRange')}</div>\n          <RangePicker\n            value={[draftEvent.start, draftEvent.end]}\n            format='YYYY-MM-DD'\n            showTime={false}\n            timeZone={eventTimeZone}\n            startOfWeek={startOfWeek}\n            matchTriggerWidth\n            disabled={!isEditable || isLoading}\n            onChange={handleAllDayRangeChange}\n            locale={app?.state.locale}\n          />\n        </div>\n      ) : (\n        <div\n          className='df-event-dialog-time-row'\n          style={{ marginBottom: '0.75rem' }}\n        >\n          <div className='df-form-label'>{t('timeRange')}</div>\n          <RangePicker\n            value={[draftEvent.start, draftEvent.end]}\n            timeZone={eventTimeZone}\n            startOfWeek={startOfWeek}\n            disabled={!isEditable || isLoading}\n            onChange={(\n              nextRange: [Temporal.ZonedDateTime, Temporal.ZonedDateTime]\n            ) => {\n              if (isLoading) return;\n              const [start, end] = nextRange;\n              applyDraftEventUpdate({ ...draftEvent, start, end });\n            }}\n            locale={app?.state.locale}\n          />\n        </div>\n      )}\n\n      {shouldShowNotes && (\n        <div\n          className='df-event-dialog-notes-row'\n          style={{ marginBottom: '0.75rem' }}\n        >\n          <span className='df-form-label'>{t('note')}</span>\n          <textarea\n            id={`event-note-${draftEvent.id}`}\n            name='note'\n            value={draftEvent.description ?? ''}\n            readOnly={!isEditable || isLoading}\n            disabled={!isEditable || isLoading}\n            onChange={e =>\n              applyDraftEventUpdate({\n                ...draftEvent,\n                description: (e.target as HTMLTextAreaElement).value,\n              })\n            }\n            onInput={e =>\n              applyDraftEventUpdate({\n                ...draftEvent,\n                description: (e.target as HTMLTextAreaElement).value,\n              })\n            }\n            rows={3}\n            className='df-form-textarea'\n            placeholder={t('addNotePlaceholder')}\n          />\n        </div>\n      )}\n\n      {isEditable && (\n        <div className='df-form-actions'>\n          {isDraftAllDay ? (\n            <LoadingButton\n              type='button'\n              className='df-fill-primary df-hover-primary-solid df-btn-sm'\n              onClick={convertToRegular}\n              loading={isLoading}\n            >\n              {t('setAsTimed')}\n            </LoadingButton>\n          ) : (\n            <LoadingButton\n              type='button'\n              className='df-fill-primary df-hover-primary-solid df-btn-sm'\n              onClick={convertToAllDay}\n              loading={isLoading}\n            >\n              {t('setAsAllDay')}\n            </LoadingButton>\n          )}\n\n          <LoadingButton\n            type='button'\n            className='df-fill-destructive df-hover-destructive df-btn-sm'\n            onClick={handleEventDelete}\n            loading={isLoading}\n          >\n            {t('delete')}\n          </LoadingButton>\n        </div>\n      )}\n    </div>\n  );\n\n  if (typeof window === 'undefined' || typeof document === 'undefined') {\n    return null;\n  }\n\n  const portalTarget = document.body;\n  if (!portalTarget) return null;\n\n  return createPortal(panelContent, portalTarget);\n};\n\nexport default DefaultEventDetailPanel;\n"
  },
  {
    "path": "packages/core/src/components/common/EventDetailPanelWithContent.tsx",
    "content": "import { JSX } from 'preact';\nimport { createPortal } from 'preact/compat';\n\nimport { getCalendarContentElement } from '@/components/calendarEvent/utils';\nimport { useTheme } from '@/contexts/ThemeContext';\nimport { eventDetailPanel } from '@/styles/classNames';\nimport {\n  EventDetailPanelProps,\n  EventDetailContentRenderer,\n} from '@/types/eventDetail';\nimport { resolveAppliedTheme } from '@/utils/themeUtils';\n\n/**\n * Event detail panel wrapper for rendering custom content in the default panel\n */\ninterface EventDetailPanelWithContentProps extends EventDetailPanelProps {\n  /** Custom content renderer */\n  contentRenderer: EventDetailContentRenderer;\n}\n\nexport const EventDetailPanelWithContent = ({\n  event,\n  position,\n  panelRef,\n  isAllDay,\n  eventVisibility,\n  calendarRef,\n  selectedEventElementRef,\n  onEventUpdate,\n  onEventDelete,\n  onClose,\n  contentRenderer: ContentComponent,\n}: EventDetailPanelWithContentProps) => {\n  const { effectiveTheme } = useTheme();\n  const appliedTheme = resolveAppliedTheme(effectiveTheme);\n  const arrowBgColor = appliedTheme === 'dark' ? '#1f2937' : 'white';\n  const arrowBorderColor =\n    appliedTheme === 'dark' ? 'rgb(55, 65, 81)' : 'rgb(229, 231, 235)';\n\n  // Calculate arrow style (same logic as DefaultEventDetailPanel)\n  const calculateArrowStyle = (): JSX.CSSProperties => {\n    let arrowStyle: JSX.CSSProperties = {};\n\n    if (eventVisibility === 'sticky-top') {\n      const calendarContent = getCalendarContentElement(calendarRef);\n      if (calendarContent) {\n        const contentRect = calendarContent.getBoundingClientRect();\n        const stickyEventCenterY = contentRect.top + 3;\n        const arrowRelativeY = stickyEventCenterY - position.top;\n\n        arrowStyle = {\n          position: 'absolute',\n          width: '12px',\n          height: '12px',\n          backgroundColor: arrowBgColor,\n          transform: 'rotate(45deg)',\n          transformOrigin: 'center',\n          top: `${arrowRelativeY - 6}px`,\n          borderRight: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n          borderTop: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n          borderLeft: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n          borderBottom: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n          ...(position.isSunday ? { right: '-6px' } : { left: '-6px' }),\n        };\n      }\n    } else if (eventVisibility === 'sticky-bottom') {\n      const panelElement = panelRef.current;\n      let arrowTop = 200;\n\n      if (panelElement) {\n        const panelRect = panelElement.getBoundingClientRect();\n        const computedStyle = window.getComputedStyle(panelElement);\n        const paddingBottom =\n          Number.parseInt(computedStyle.paddingBottom, 10) || 0;\n        const borderBottom =\n          Number.parseInt(computedStyle.borderBottomWidth, 10) || 0;\n\n        arrowTop = panelRect.height - paddingBottom - borderBottom - 6 + 11;\n      }\n\n      arrowStyle = {\n        position: 'absolute',\n        width: '12px',\n        height: '12px',\n        backgroundColor: arrowBgColor,\n        transform: 'rotate(45deg)',\n        transformOrigin: 'center',\n        top: `${arrowTop}px`,\n        left: position.isSunday ? undefined : '-6px',\n        right: position.isSunday ? '-6px' : undefined,\n        borderRight: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n        borderTop: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n        borderLeft: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n        borderBottom: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n      };\n    } else {\n      if (position && selectedEventElementRef.current && calendarRef.current) {\n        const eventRect =\n          selectedEventElementRef.current.getBoundingClientRect();\n        const calendarContent = getCalendarContentElement(calendarRef);\n\n        if (calendarContent) {\n          const viewportRect = calendarContent.getBoundingClientRect();\n\n          const visibleTop = Math.max(eventRect.top, viewportRect.top);\n          const visibleBottom = Math.min(eventRect.bottom, viewportRect.bottom);\n          const visibleHeight = Math.max(0, visibleBottom - visibleTop);\n\n          let targetY;\n          if (visibleHeight === eventRect.height) {\n            targetY = eventRect.top + eventRect.height / 2;\n          } else if (visibleHeight > 0) {\n            targetY = visibleTop + visibleHeight / 2;\n          } else {\n            targetY = eventRect.top + eventRect.height / 2;\n          }\n\n          const arrowRelativeY = targetY - position.top;\n\n          const panelElement = panelRef.current;\n          let maxArrowY = 240 - 12;\n\n          if (panelElement) {\n            const panelRect = panelElement.getBoundingClientRect();\n            const computedStyle = window.getComputedStyle(panelElement);\n            const paddingBottom =\n              Number.parseInt(computedStyle.paddingBottom, 10) || 0;\n            const borderBottom =\n              Number.parseInt(computedStyle.borderBottomWidth, 10) || 0;\n\n            maxArrowY = panelRect.height - paddingBottom - borderBottom + 11;\n          }\n\n          const minArrowY = 12;\n          const finalArrowY = Math.max(\n            minArrowY,\n            Math.min(maxArrowY, arrowRelativeY)\n          );\n\n          arrowStyle = {\n            position: 'absolute',\n            width: '12px',\n            height: '12px',\n            backgroundColor: arrowBgColor,\n            transform: 'rotate(45deg)',\n            transformOrigin: 'center',\n            top: `${finalArrowY - 6}px`,\n            borderRight: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n            borderTop: `${position.isSunday ? `1px solid ${arrowBorderColor}` : 'none'}`,\n            borderLeft: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n            borderBottom: `${position.isSunday ? 'none' : `1px solid ${arrowBorderColor}`}`,\n            ...(position.isSunday ? { right: '-6px' } : { left: '-6px' }),\n          };\n        }\n      }\n    }\n\n    return arrowStyle;\n  };\n\n  const arrowStyle = calculateArrowStyle();\n\n  const panelContent = (\n    <div\n      ref={panelRef}\n      className={`${eventDetailPanel} df-event-panel-content`}\n      data-event-detail-panel='true'\n      data-event-id={event.id}\n      style={{\n        top: `${position.top}px`,\n        left: `${position.left}px`,\n        zIndex: 9999,\n        pointerEvents: 'auto',\n        backgroundColor: appliedTheme === 'dark' ? '#1f2937' : '#ffffff',\n      }}\n    >\n      <div style={arrowStyle}></div>\n      <ContentComponent\n        event={event}\n        isAllDay={isAllDay}\n        onEventUpdate={onEventUpdate}\n        onEventDelete={onEventDelete}\n        onClose={onClose}\n      />\n    </div>\n  );\n\n  return createPortal(panelContent, document.body);\n};\n\nexport default EventDetailPanelWithContent;\n"
  },
  {
    "path": "packages/core/src/components/common/Icons.tsx",
    "content": "interface IconProps {\n  className?: string;\n  width?: number;\n  height?: number;\n  title?: string;\n}\n\nexport const ChevronLeft = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m15 18-6-6 6-6' />\n  </svg>\n);\n\nexport const ChevronRight = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m9 18 6-6-6-6' />\n  </svg>\n);\n\nexport const ChevronsLeft = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m11 17-5-5 5-5' />\n    <path d='m18 17-5-5 5-5' />\n  </svg>\n);\n\nexport const ChevronsRight = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m6 17 5-5-5-5' />\n    <path d='m13 17 5-5-5-5' />\n  </svg>\n);\n\nexport const ChevronDown = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m6 9 6 6 6-6' />\n  </svg>\n);\n\nexport const Plus = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M5 12h14' />\n    <path d='M12 5v14' />\n  </svg>\n);\n\nexport const Search = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <circle cx='11' cy='11' r='8' />\n    <path d='m21 21-4.3-4.3' />\n  </svg>\n);\n\nexport const Check = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M20 6 9 17l-5-5' />\n  </svg>\n);\n\nexport const ChevronsUpDown = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m7 15 5 5 5-5' />\n    <path d='m7 9 5-5 5 5' />\n  </svg>\n);\n\nexport const X = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M18 6 6 18' />\n    <path d='m6 6 12 12' />\n  </svg>\n);\n\nexport const CalendarDays = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M8 2v4' />\n    <path d='M16 2v4' />\n    <rect width='18' height='18' x='3' y='4' rx='2' />\n    <path d='M3 10h18' />\n    <path d='M8 14h.01' />\n    <path d='M12 14h.01' />\n    <path d='M16 14h.01' />\n    <path d='M8 18h.01' />\n    <path d='M12 18h.01' />\n    <path d='M16 18h.01' />\n  </svg>\n);\n\nexport const MoveRight = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M18 8L22 12L18 16' />\n    <path d='M2 12H22' />\n  </svg>\n);\n\nexport const Loader2 = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M21 12a9 9 0 1 1-6.219-8.56' />\n  </svg>\n);\n\nexport const Gift = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <rect width='18' height='14' x='3' y='8' rx='2' />\n    <path d='M12 5a3 3 0 1 0-3 3' />\n    <path d='M12 5a3 3 0 1 1 3 3' />\n    <path d='M3 12h18' />\n    <path d='M12 22V8' />\n  </svg>\n);\n\nexport const Heart = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z' />\n  </svg>\n);\n\nexport const MapPin = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z' />\n    <circle cx='12' cy='10' r='3' />\n  </svg>\n);\n\nexport const Star = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <polygon points='12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2' />\n  </svg>\n);\n\nexport const PanelRightClose = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <rect width='18' height='18' x='3' y='3' rx='2' />\n    <path d='M15 3v18' />\n    <path d='m8 9 3 3-3 3' />\n  </svg>\n);\n\nexport const PanelRightOpen = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <rect width='18' height='18' x='3' y='3' rx='2' />\n    <path d='M15 3v18' />\n    <path d='m10 15-3-3 3-3' />\n  </svg>\n);\n\nexport const Calendar = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <rect width='18' height='18' x='3' y='4' rx='2' />\n    <path d='M3 10h18' />\n    <path d='M8 2v4' />\n    <path d='M16 2v4' />\n  </svg>\n);\n\nexport const ArrowLeft = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m12 19-7-7 7-7' />\n    <path d='M19 12H5' />\n  </svg>\n);\n\nexport const AudioLines = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M4 11a9 9 0 0 1 9 9' />\n    <path d='M4 4a16 16 0 0 1 16 16' />\n    <circle cx='5' cy='19' r='1' />\n  </svg>\n);\n\nexport const AlertCircle = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <circle cx='12' cy='12' r='10' />\n    <line x1='12' x2='12' y1='8' y2='12' />\n    <line x1='12' x2='12.01' y1='16' y2='16' />\n  </svg>\n);\n"
  },
  {
    "path": "packages/core/src/components/common/LoadingButton.tsx",
    "content": "import { JSX, ComponentChildren } from 'preact';\nimport { useState } from 'preact/hooks';\n\ninterface ButtonProps extends Omit<\n  JSX.HTMLAttributes<HTMLButtonElement>,\n  'onClick'\n> {\n  loading?: boolean;\n  loadingText?: string;\n  onClick?: (\n    e: JSX.TargetedMouseEvent<HTMLButtonElement>\n  ) => void | Promise<void>;\n  children?: ComponentChildren;\n  // Explicitly add missing attributes if Omit or JSX.HTMLAttributes is failing\n  disabled?: boolean;\n  type?: 'button' | 'submit' | 'reset';\n  className?: string;\n}\n\nexport const LoadingButton = ({\n  children,\n  onClick,\n  loading: propLoading,\n  loadingText,\n  disabled,\n  className,\n  type = 'button',\n  ...props\n}: ButtonProps) => {\n  const [internalLoading, setInternalLoading] = useState(false);\n  const isLoading = propLoading || internalLoading;\n\n  const handleClick = async (e: JSX.TargetedMouseEvent<HTMLButtonElement>) => {\n    if (isLoading) return;\n\n    if (onClick) {\n      const result = onClick(e);\n      if (result && typeof result === 'object' && 'then' in result) {\n        setInternalLoading(true);\n        try {\n          await result;\n        } finally {\n          setInternalLoading(false);\n        }\n      }\n    }\n  };\n\n  const buttonContent = (\n    <>\n      {isLoading && (\n        <svg\n          className='df-loading-btn-spinner'\n          xmlns='http://www.w3.org/2000/svg'\n          fill='none'\n          viewBox='0 0 24 24'\n        >\n          <circle\n            className='df-loading-btn-track'\n            cx='12'\n            cy='12'\n            r='10'\n            stroke='currentColor'\n            strokeWidth='4'\n          />\n          <path\n            className='df-loading-btn-fill'\n            fill='currentColor'\n            d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'\n          />\n        </svg>\n      )}\n      <span className='df-loading-btn-label'>\n        {isLoading && loadingText ? loadingText : children}\n      </span>\n    </>\n  );\n\n  const commonProps = {\n    ...props,\n    disabled: disabled || isLoading,\n    onClick: handleClick,\n    className: `df-loading-btn${className ? ` ${className}` : ''}`,\n    'data-loading': isLoading ? 'true' : 'false',\n  };\n\n  if (type === 'submit') {\n    return (\n      <button {...commonProps} type='submit'>\n        {buttonContent}\n      </button>\n    );\n  }\n\n  if (type === 'reset') {\n    return (\n      <button {...commonProps} type='reset'>\n        {buttonContent}\n      </button>\n    );\n  }\n\n  return (\n    <button {...commonProps} type='button'>\n      {buttonContent}\n    </button>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/common/MiniCalendar.tsx",
    "content": "import { useMemo } from 'preact/hooks';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { CalendarRegistry } from '@/core/calendarRegistry';\nimport { useLocale, getWeekDaysLabels } from '@/locale';\nimport {\n  miniCalendarDay,\n  miniCalendarDayHeader,\n  miniCalendarGrid,\n} from '@/styles/classNames';\nimport type { Event } from '@/types/event';\nimport { getLineColor, temporalToVisualDate } from '@/utils';\n\nimport { ChevronLeft, ChevronRight } from './Icons';\n\nconst MAX_EVENT_DOTS = 4;\n\ninterface MiniCalendarProps {\n  visibleMonth: Date;\n  currentDate: Date;\n  showHeader?: boolean;\n  onMonthChange: (offset: number) => void;\n  onDateSelect: (date: Date) => void;\n  locale?: string;\n  events?: Event[];\n  showEventDots?: boolean;\n  calendarRegistry?: CalendarRegistry;\n  timeZone?: string;\n}\n\nexport const MiniCalendar = ({\n  visibleMonth,\n  currentDate,\n  showHeader = false,\n  onMonthChange,\n  onDateSelect,\n  events = [],\n  showEventDots = false,\n  calendarRegistry,\n  timeZone,\n}: MiniCalendarProps) => {\n  const { locale } = useLocale();\n  const todayKey = useMemo(() => {\n    const todayInTz = timeZone\n      ? Temporal.Now.plainDateISO(timeZone)\n      : Temporal.Now.plainDateISO();\n    const todayLocal = new Date(\n      todayInTz.year,\n      todayInTz.month - 1,\n      todayInTz.day\n    );\n    return todayLocal.toDateString();\n  }, [timeZone]);\n  const currentDateKey = currentDate.toDateString();\n\n  const weekdayLabels = useMemo(\n    () => getWeekDaysLabels(locale, 'narrow'),\n    [locale]\n  );\n\n  const monthLabel = useMemo(\n    () =>\n      visibleMonth.toLocaleDateString(locale, {\n        month: 'long',\n        year: 'numeric',\n      }),\n    [visibleMonth, locale]\n  );\n\n  const eventDotsByDate = useMemo(() => {\n    if (!showEventDots || !events?.length) return null;\n    const map = new Map<string, string[]>();\n\n    events.forEach(event => {\n      const startFull = temporalToVisualDate(event.start, timeZone);\n      const endFull = event.end\n        ? temporalToVisualDate(event.end, timeZone)\n        : startFull;\n\n      const startDate = new Date(startFull);\n      startDate.setHours(0, 0, 0, 0);\n\n      const endDate = new Date(endFull);\n      endDate.setHours(0, 0, 0, 0);\n\n      let adjustedEnd = new Date(endDate);\n\n      if (!event.allDay) {\n        const hasTimeComponent =\n          endFull.getHours() !== 0 ||\n          endFull.getMinutes() !== 0 ||\n          endFull.getSeconds() !== 0 ||\n          endFull.getMilliseconds() !== 0;\n\n        if (!hasTimeComponent) {\n          adjustedEnd.setDate(adjustedEnd.getDate() - 1);\n        }\n      }\n\n      if (adjustedEnd < startDate) {\n        adjustedEnd = new Date(startDate);\n      }\n\n      const color = getLineColor(\n        event.calendarId || 'default',\n        calendarRegistry\n      ).toLowerCase();\n\n      for (\n        let current = new Date(startDate);\n        current <= adjustedEnd;\n        current = new Date(current.getTime() + 86400000)\n      ) {\n        const key = current.toDateString();\n        const existing = map.get(key) ?? [];\n        if (!existing.includes(color) && existing.length < MAX_EVENT_DOTS) {\n          map.set(key, [...existing, color]);\n        }\n      }\n    });\n    return map;\n  }, [showEventDots, events, timeZone, calendarRegistry]);\n\n  const miniCalendarDays = useMemo(() => {\n    const year = visibleMonth.getFullYear();\n    const month = visibleMonth.getMonth();\n    const firstDay = new Date(year, month, 1);\n    const startOffset = (firstDay.getDay() + 6) % 7;\n    const totalCells = 42;\n    const days: Array<{\n      date: number;\n      fullDate: Date;\n      isCurrentMonth: boolean;\n      isToday: boolean;\n      isSelected: boolean;\n    }> = [];\n\n    for (let cell = 0; cell < totalCells; cell++) {\n      const cellDate = new Date(year, month, cell - startOffset + 1);\n      const cellDateString = cellDate.toDateString();\n      days.push({\n        date: cellDate.getDate(),\n        fullDate: cellDate,\n        isCurrentMonth: cellDate.getMonth() === month,\n        isToday: cellDateString === todayKey,\n        isSelected: cellDateString === currentDateKey,\n      });\n    }\n\n    return days;\n  }, [visibleMonth, currentDateKey, todayKey]);\n\n  return (\n    <div className='df-mini-calendar-body'>\n      {showHeader ? (\n        <div className='df-mini-calendar-header-nav'>\n          <button\n            type='button'\n            className='df-mini-calendar-nav-btn'\n            onClick={() => onMonthChange(-1)}\n            aria-label='Previous month'\n          >\n            <ChevronLeft />\n          </button>\n          <span className='df-mini-calendar-month-label'>{monthLabel}</span>\n          <button\n            type='button'\n            className='df-mini-calendar-nav-btn'\n            onClick={() => onMonthChange(1)}\n            aria-label='Next month'\n          >\n            <ChevronRight />\n          </button>\n        </div>\n      ) : null}\n      <div className={miniCalendarGrid}>\n        {weekdayLabels.map((label, index) => (\n          <div key={`weekday-${index}`} className={miniCalendarDayHeader}>\n            {label}\n          </div>\n        ))}\n        {miniCalendarDays.map(day => {\n          const dots = eventDotsByDate?.get(day.fullDate.toDateString()) ?? [];\n          return (\n            <button\n              type='button'\n              key={day.fullDate.getTime()}\n              className={`${miniCalendarDay} df-mini-calendar-day-cell`}\n              data-today={day.isToday ? 'true' : undefined}\n              data-selected={\n                !day.isToday && day.isSelected ? 'true' : undefined\n              }\n              data-other-month={day.isCurrentMonth ? undefined : 'true'}\n              onClick={() => onDateSelect(day.fullDate)}\n            >\n              <span className='df-mini-calendar-day-number'>{day.date}</span>\n              {showEventDots && dots.length > 0 && (\n                <div className='df-mini-calendar-dots'>\n                  {dots.slice(0, MAX_EVENT_DOTS).map((color, index) => (\n                    <div\n                      key={`${color}-${index}`}\n                      data-mini-calendar-dot='true'\n                      className='df-mini-calendar-dot'\n                      style={{ backgroundColor: color }}\n                    />\n                  ))}\n                </div>\n              )}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/common/QuickCreateEventPopup.tsx",
    "content": "import { RefObject } from 'preact';\nimport { createPortal } from 'preact/compat';\nimport {\n  useState,\n  useEffect,\n  useRef,\n  useMemo,\n  useLayoutEffect,\n} from 'preact/hooks';\n\nimport { useLocale } from '@/locale';\nimport { ICalendarApp, Event } from '@/types';\nimport { generateUniKey } from '@/utils/helpers';\nimport { dateToZonedDateTime } from '@/utils/temporalTypeGuards';\nimport { getNextHourRangeInTimeZone } from '@/utils/timeUtils';\n\ninterface QuickCreateEventPopupProps {\n  app: ICalendarApp;\n  anchorRef: RefObject<HTMLElement>;\n  onClose: () => void;\n  isOpen: boolean;\n}\n\ninterface SuggestionItem {\n  type: 'new' | 'history';\n  title: string;\n  calendarId: string;\n  color: string;\n  start: Date;\n  end: Date;\n}\n\nconst formatTime = (d: Date) =>\n  d.toLocaleTimeString([], {\n    hour: '2-digit',\n    minute: '2-digit',\n    hour12: false,\n  });\n\nconst formatTimeRange = (start: Date, end: Date) =>\n  `${formatTime(start)} - ${formatTime(end)}`;\n\nexport const QuickCreateEventPopup = ({\n  app,\n  anchorRef,\n  onClose,\n  isOpen,\n}: QuickCreateEventPopupProps) => {\n  const { t } = useLocale();\n  const [inputValue, setInputValue] = useState('');\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [isReady, setIsReady] = useState(false);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const popupRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!isOpen) setIsReady(false);\n  }, [isOpen]);\n\n  useEffect(() => {\n    if (isOpen) {\n      const focusTimer = window.setTimeout(() => inputRef.current?.focus(), 50);\n      setInputValue('');\n      setSelectedIndex(0);\n      return () => window.clearTimeout(focusTimer);\n    }\n  }, [isOpen]);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        isOpen &&\n        popupRef.current &&\n        !popupRef.current.contains(event.target as Node) &&\n        anchorRef.current &&\n        !anchorRef.current.contains(event.target as Node)\n      ) {\n        onClose();\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => document.removeEventListener('mousedown', handleClickOutside);\n  }, [isOpen, onClose, anchorRef]);\n\n  const [position, setPosition] = useState({ top: 0, left: 0 });\n  const [placement, setPlacement] = useState<'top' | 'bottom'>('top');\n  const [arrowLeft, setArrowLeft] = useState(0);\n\n  const nextHourRange = useMemo(\n    () => getNextHourRangeInTimeZone(app.timeZone),\n    [app.timeZone, isOpen]\n  );\n\n  const suggestions: SuggestionItem[] = useMemo(() => {\n    if (!inputValue.trim()) return [];\n\n    const results: SuggestionItem[] = [];\n    const calendars = app.getCalendars();\n    const allEvents = app.getAllEvents();\n    const lowerInput = inputValue.toLowerCase();\n\n    const historyEvent = allEvents.find(\n      e => e.title.toLowerCase() === lowerInput\n    );\n    let targetCalendarId = historyEvent?.calendarId;\n\n    if (!targetCalendarId) {\n      targetCalendarId = app\n        .getCalendarRegistry()\n        .getDefaultWritableCalendar()?.id;\n    }\n\n    if (targetCalendarId) {\n      const resolved = app.getCalendarRegistry().get(targetCalendarId);\n      if (resolved?.readOnly || resolved?.subscription) {\n        targetCalendarId = app\n          .getCalendarRegistry()\n          .getDefaultWritableCalendar()?.id;\n      }\n    }\n\n    const targetCalendar = calendars.find(c => c.id === targetCalendarId);\n    const color = targetCalendar?.colors.lineColor || '#3b82f6';\n\n    results.push({\n      type: 'new',\n      title: inputValue,\n      calendarId: targetCalendarId || '',\n      color,\n      start: nextHourRange.start,\n      end: nextHourRange.end,\n    });\n\n    const seenTitles = new Set<string>([inputValue.toLowerCase()]);\n\n    const matchedEvents = allEvents.filter(e => {\n      if (!e.title.toLowerCase().includes(lowerInput)) return false;\n      if (seenTitles.has(e.title.toLowerCase())) return false;\n      const cal = app.getCalendarRegistry().get(e.calendarId ?? '');\n      return !(cal?.readOnly || cal?.subscription);\n    });\n\n    matchedEvents.slice(0, 5).forEach(e => {\n      seenTitles.add(e.title.toLowerCase());\n      const cal = calendars.find(c => c.id === e.calendarId);\n      results.push({\n        type: 'history',\n        title: e.title,\n        calendarId: e.calendarId || '',\n        color: cal?.colors.lineColor || '#9ca3af',\n        start: nextHourRange.start,\n        end: nextHourRange.end,\n      });\n    });\n\n    return results;\n  }, [inputValue, app, nextHourRange]);\n\n  useLayoutEffect(() => {\n    if (isOpen && anchorRef.current && popupRef.current) {\n      const rect = anchorRef.current.getBoundingClientRect();\n      const popupHeight = popupRef.current.offsetHeight;\n      const popupWidth = 340;\n\n      let left = rect.left + rect.width / 2 - popupWidth / 2;\n      const padding = 12;\n      const windowWidth = window.innerWidth;\n\n      if (left < padding) left = padding;\n      else if (left + popupWidth > windowWidth - padding)\n        left = windowWidth - popupWidth - padding;\n\n      const buttonCenterX = rect.left + rect.width / 2;\n      setArrowLeft(buttonCenterX - left);\n\n      const spaceAbove = rect.top;\n      const requiredSpace = popupHeight + 20;\n\n      let newTop = 0;\n      let newPlacement: 'top' | 'bottom' = 'top';\n\n      if (spaceAbove < requiredSpace) {\n        newPlacement = 'bottom';\n        newTop = rect.bottom + 12;\n      } else {\n        newPlacement = 'top';\n        newTop = rect.top - 12 - popupHeight;\n      }\n\n      setPlacement(newPlacement);\n      setPosition({ top: newTop, left });\n      setIsReady(true);\n    }\n  }, [isOpen, anchorRef, suggestions]);\n\n  const handleCreate = (item: SuggestionItem) => {\n    if (!item.calendarId) return;\n\n    const newId = generateUniKey();\n    const newEvent: Event = {\n      id: newId,\n      title: item.title,\n      start: dateToZonedDateTime(item.start, app.timeZone),\n      end: dateToZonedDateTime(item.end, app.timeZone),\n      calendarId: item.calendarId,\n      allDay: false,\n    };\n\n    app.addEvent(newEvent);\n    app.setCurrentDate(item.start);\n    app.highlightEvent(newId);\n    onClose();\n  };\n\n  useEffect(() => {\n    if (!isOpen) return;\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (suggestions.length === 0) {\n        return;\n      }\n\n      if (e.key === 'ArrowDown') {\n        e.preventDefault();\n        setSelectedIndex(prev => (prev + 1) % suggestions.length);\n      } else if (e.key === 'ArrowUp') {\n        e.preventDefault();\n        setSelectedIndex(\n          prev => (prev - 1 + suggestions.length) % suggestions.length\n        );\n      } else if (e.key === 'Enter') {\n        e.preventDefault();\n        if (suggestions[selectedIndex]) {\n          handleCreate(suggestions[selectedIndex]);\n        }\n      }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n    return () => window.removeEventListener('keydown', handleKeyDown);\n  }, [isOpen, suggestions, selectedIndex]);\n\n  if (!isOpen) return null;\n\n  return createPortal(\n    <div\n      ref={popupRef}\n      className='df-portal df-quick-create'\n      data-ready={isReady ? 'true' : 'false'}\n      style={{\n        top: position.top,\n        left: position.left,\n        visibility: isReady ? 'visible' : 'hidden',\n      }}\n    >\n      <div className='df-quick-create-header'>\n        <div className='df-quick-create-title'>\n          {t('quickCreateEvent') || 'Quick Create Event'}\n        </div>\n        <div className='df-quick-create-input-wrap'>\n          <input\n            ref={inputRef}\n            type='text'\n            className='df-form-input'\n            placeholder={\n              t('quickCreatePlaceholder') || 'Enter title (e.g. Code review)'\n            }\n            value={inputValue}\n            onChange={e => setInputValue((e.target as HTMLInputElement).value)}\n          />\n        </div>\n      </div>\n\n      <div className='df-quick-create-list'>\n        {suggestions.length === 0 && inputValue && (\n          <div className='df-quick-create-empty'>\n            {t('noSuggestions') || 'Type to create'}\n          </div>\n        )}\n\n        {suggestions.map((item, index) => (\n          <div\n            key={`${item.type}-${index}`}\n            className='df-quick-create-item'\n            data-selected={index === selectedIndex ? 'true' : 'false'}\n            onClick={() => handleCreate(item)}\n            onMouseEnter={() => setSelectedIndex(index)}\n          >\n            <div\n              className='df-quick-create-color-bar'\n              style={{ backgroundColor: item.color }}\n            />\n            <div className='df-quick-create-item-content'>\n              <div className='df-quick-create-item-title'>{item.title}</div>\n              <div>\n                <span className='df-quick-create-item-badge'>\n                  {item.type === 'new' ? t('today') : t('tomorrow')}\n                </span>\n              </div>\n              <div className='df-quick-create-item-time'>\n                {formatTimeRange(item.start, item.end)}\n              </div>\n            </div>\n          </div>\n        ))}\n      </div>\n\n      {/* Triangle Arrow */}\n      <div\n        className='df-quick-create-arrow'\n        data-placement={placement === 'top' ? 'bottom' : 'top'}\n        style={{ left: arrowLeft }}\n      />\n    </div>,\n    document.body\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/common/TodayBox.tsx",
    "content": "import { useLocale } from '@/locale';\nimport { calendarNavButton, calendarTodayButton } from '@/styles/classNames';\n\nimport { ChevronLeft, ChevronRight } from './Icons';\n\ninterface Props {\n  handlePreviousMonth: () => void;\n  handleToday: () => void;\n  handleNextMonth: () => void;\n}\n\nconst TodayBox = ({\n  handlePreviousMonth,\n  handleToday,\n  handleNextMonth,\n}: Props) => {\n  const { t } = useLocale();\n  return (\n    <div className='df-navigation'>\n      <button\n        type='button'\n        className={calendarNavButton}\n        onClick={handlePreviousMonth}\n        aria-label='Previous month'\n      >\n        <ChevronLeft />\n      </button>\n      <button\n        type='button'\n        className={calendarTodayButton}\n        onClick={handleToday}\n      >\n        {t('today')}\n      </button>\n      <button\n        type='button'\n        className={calendarNavButton}\n        onClick={handleNextMonth}\n        aria-label='Next month'\n      >\n        <ChevronRight />\n      </button>\n    </div>\n  );\n};\n\nexport default TodayBox;\n"
  },
  {
    "path": "packages/core/src/components/common/ViewHeader.tsx",
    "content": "import type { ComponentChild } from 'preact';\n\nimport { useLocale } from '@/locale';\nimport { headerTitle, headerSubtitle } from '@/styles/classNames';\nimport { ICalendarApp } from '@/types';\n\nimport TodayBox from './TodayBox';\n\nexport type ViewHeaderType = 'day' | 'week' | 'month' | 'year';\nexport type ViewSwitcherMode = 'buttons' | 'select';\n\ninterface ViewHeaderProps {\n  calendar: ICalendarApp;\n  /** View type */\n  viewType: ViewHeaderType;\n  /** Current date */\n  currentDate: Date;\n  /** Previous period */\n  onPrevious?: () => void;\n  /** Next period */\n  onNext?: () => void;\n  /** Go to today */\n  onToday?: () => void;\n  /** Custom title (optional, takes priority over default title) */\n  customTitle?: string;\n  /** Custom subtitle (optional, only for Day view) */\n  customSubtitle?: string;\n  /** Extra content rendered beside the subtitle row */\n  subtitleMeta?: ComponentChild;\n  /** Whether to show TodayBox (default determined by viewType: day=false, week/month=true) */\n  showTodayBox?: boolean;\n  /** Sticky year for Year view (optional, only for Year view) */\n  stickyYear?: number | null;\n  /** Push-away offset for sticky year (in pixels) */\n  stickyYearOffset?: number;\n  /** Next year that's pushing the sticky year (optional, only for Year view) */\n  nextYear?: number | null;\n  /** Offset for the next year coming from below (in pixels) */\n  nextYearOffset?: number;\n}\n\nconst ViewHeader = ({\n  viewType,\n  currentDate,\n  onPrevious,\n  onNext,\n  onToday,\n  customTitle,\n  customSubtitle,\n  subtitleMeta,\n  showTodayBox,\n  stickyYear,\n  stickyYearOffset = 0,\n  nextYear,\n  nextYearOffset = 0,\n}: ViewHeaderProps) => {\n  const { locale } = useLocale();\n  const shouldShowTodayBox = showTodayBox === undefined ? true : showTodayBox;\n\n  const getDefaultTitle = (): string => {\n    switch (viewType) {\n      case 'day':\n        return currentDate.toLocaleDateString(locale, {\n          day: 'numeric',\n          month: 'long',\n          year: 'numeric',\n        });\n      case 'week':\n      case 'month':\n        return currentDate.toLocaleDateString(locale, {\n          month: 'long',\n          year: 'numeric',\n        });\n      case 'year':\n        return currentDate.getFullYear().toString();\n      default:\n        return '';\n    }\n  };\n\n  const getDefaultSubtitle = (): string | null => {\n    if (viewType === 'day') {\n      return currentDate.toLocaleDateString(locale, {\n        weekday: 'long',\n      });\n    }\n    return null;\n  };\n\n  const title = customTitle || getDefaultTitle();\n  const subtitle =\n    viewType === 'day' ? customSubtitle || getDefaultSubtitle() : null;\n\n  return (\n    <div\n      className='df-view-header-container'\n      onContextMenu={e => e.preventDefault()}\n    >\n      <div className='df-view-header-title-area'>\n        {viewType === 'year' && stickyYear ? (\n          <div className='df-view-header-year-stack'>\n            <h1\n              className={`${headerTitle} df-view-header-year-title`}\n              style={{\n                transform: `translateY(-${stickyYearOffset}px)`,\n              }}\n            >\n              {stickyYear}\n            </h1>\n            {nextYear && (\n              <h1\n                className={`${headerTitle} df-view-header-year-title`}\n                style={{\n                  transform: `translateY(${nextYearOffset}px)`,\n                }}\n              >\n                {nextYear}\n              </h1>\n            )}\n          </div>\n        ) : (\n          <div>\n            <div className={headerTitle}>{title}</div>\n\n            {subtitle && <div className={headerSubtitle}>{subtitle}</div>}\n            {subtitleMeta && (\n              <div className='df-view-header-subtitle-row'>\n                {subtitleMeta && (\n                  <div className='df-view-header-subtitle-meta'>\n                    {subtitleMeta}\n                  </div>\n                )}\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n\n      {shouldShowTodayBox && onPrevious && onNext && onToday && (\n        <div className='df-view-header-nav'>\n          <TodayBox\n            handlePreviousMonth={onPrevious}\n            handleNextMonth={onNext}\n            handleToday={onToday}\n          />\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default ViewHeader;\n"
  },
  {
    "path": "packages/core/src/components/common/ViewSwitcher.tsx",
    "content": "import { useState, useRef, useEffect } from 'preact/hooks';\n\nimport { useLocale } from '@/locale';\nimport { TranslationKey } from '@/locale/types';\nimport { CalendarViewType, ICalendarApp } from '@/types';\n\nimport { ChevronDown } from './Icons';\n\ninterface ViewSwitcherProps {\n  calendar: ICalendarApp;\n  mode?: 'buttons' | 'select';\n}\n\nconst getViewLabel = (\n  viewType: CalendarViewType,\n  calendar: ICalendarApp,\n  t: (key: TranslationKey) => string\n): string => {\n  const label = calendar.state.views.get(viewType)?.label;\n  if (label) {\n    return label;\n  }\n\n  const translated = t(viewType as TranslationKey);\n  if (translated !== viewType) {\n    return translated;\n  }\n\n  return viewType\n    .split(/[-_]/g)\n    .map(segment =>\n      segment ? segment.charAt(0).toUpperCase() + segment.slice(1) : segment\n    )\n    .join(' ');\n};\n\nconst ViewSwitcher = ({ calendar, mode = 'buttons' }: ViewSwitcherProps) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n  const { t } = useLocale();\n\n  const registeredViews = Array.from(calendar.state.views.keys());\n  const currentView = calendar.state.currentView;\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        dropdownRef.current &&\n        !dropdownRef.current.contains(event.target as Node)\n      ) {\n        setIsOpen(false);\n      }\n    };\n\n    if (isOpen) {\n      document.addEventListener('mousedown', handleClickOutside);\n      return () =>\n        document.removeEventListener('mousedown', handleClickOutside);\n    }\n  }, [isOpen]);\n\n  if (registeredViews.length <= 1) {\n    return null;\n  }\n\n  if (mode === 'select') {\n    return (\n      <div className='df-view-switcher-select' ref={dropdownRef}>\n        <button\n          type='button'\n          onClick={() => setIsOpen(!isOpen)}\n          className='df-view-switcher-select-trigger'\n          aria-expanded={isOpen}\n          aria-haspopup='listbox'\n        >\n          <span>{getViewLabel(currentView, calendar, t)}</span>\n          <span\n            className='df-view-switcher-select-chevron'\n            data-open={isOpen ? 'true' : 'false'}\n          >\n            <ChevronDown width={16} height={16} />\n          </span>\n        </button>\n\n        {isOpen && (\n          <div className='df-view-switcher-select-dropdown df-animate-in df-fade-in df-zoom-in-95'>\n            <div className='df-view-switcher-select-list' role='listbox'>\n              {registeredViews.map(viewType => (\n                <button\n                  type='button'\n                  key={viewType}\n                  onClick={() => {\n                    calendar.changeView(viewType);\n                    setIsOpen(false);\n                    calendar.triggerRender();\n                  }}\n                  className='df-view-switcher-select-option'\n                  data-active={currentView === viewType ? 'true' : 'false'}\n                  role='option'\n                  aria-selected={currentView === viewType}\n                >\n                  {getViewLabel(viewType, calendar, t)}\n                </button>\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n    );\n  }\n\n  return (\n    <div className='df-view-switcher'>\n      {registeredViews.map(viewType => (\n        <button\n          type='button'\n          key={viewType}\n          className='df-view-switcher-btn'\n          data-active={currentView === viewType ? 'true' : 'false'}\n          onClick={() => {\n            calendar.changeView(viewType);\n            calendar.triggerRender();\n          }}\n        >\n          {getViewLabel(viewType, calendar, t)}\n        </button>\n      ))}\n    </div>\n  );\n};\n\nexport default ViewSwitcher;\n"
  },
  {
    "path": "packages/core/src/components/common/__tests__/MiniCalendar.test.tsx",
    "content": "import { render } from '@testing-library/preact';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { MiniCalendar } from '@/components/common/MiniCalendar';\nimport { CalendarRegistry } from '@/core/calendarRegistry';\nimport { CalendarType } from '@/types/calendarTypes';\n\nconst makeCalendar = (id: string, lineColor: string): CalendarType => ({\n  id,\n  name: id,\n  colors: {\n    eventColor: '#ffffff',\n    eventSelectedColor: '#000000',\n    lineColor,\n    textColor: '#111111',\n  },\n});\n\ndescribe('MiniCalendar', () => {\n  it('shows up to four unique event dots per day using calendar line colors', () => {\n    const calendarRegistry = new CalendarRegistry([\n      makeCalendar('a', '#111111'),\n      makeCalendar('b', '#222222'),\n      makeCalendar('c', '#333333'),\n      makeCalendar('d', '#444444'),\n      makeCalendar('e', '#555555'),\n      makeCalendar('dup', '#222222'),\n    ]);\n\n    const day = Temporal.PlainDate.from('2026-04-10');\n    const events = ['a', 'b', 'c', 'd', 'e', 'dup'].map(\n      (calendarId, index) => ({\n        id: `event-${index}`,\n        title: `Event ${index}`,\n        calendarId,\n        start: day,\n        end: day,\n        allDay: true,\n      })\n    );\n\n    const { container } = render(\n      <MiniCalendar\n        visibleMonth={new Date(2026, 3, 1)}\n        currentDate={new Date(2026, 3, 10)}\n        onMonthChange={() => {\n          /* noop */\n        }}\n        onDateSelect={() => {\n          /* noop */\n        }}\n        events={events}\n        showEventDots\n        calendarRegistry={calendarRegistry}\n      />\n    );\n\n    const dots = Array.from(\n      container.querySelectorAll('[data-mini-calendar-dot=\"true\"]')\n    ) as HTMLDivElement[];\n\n    expect(dots).toHaveLength(4);\n    expect(dots.map(dot => dot.style.backgroundColor)).toEqual([\n      'rgb(17, 17, 17)',\n      'rgb(34, 34, 34)',\n      'rgb(51, 51, 51)',\n      'rgb(68, 68, 68)',\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/common/__tests__/QuickCreateEventPopup.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/preact';\n\nimport { QuickCreateEventPopup } from '@/components/common/QuickCreateEventPopup';\nimport { CalendarApp } from '@/core/CalendarApp';\nimport { ViewType } from '@/types';\n\nconst createApp = () =>\n  new CalendarApp({\n    views: [],\n    plugins: [],\n    defaultView: ViewType.MONTH,\n    events: [],\n    calendars: [\n      {\n        id: 'work',\n        name: 'Work',\n        colors: {\n          lineColor: '#2563eb',\n          eventColor: '#dbeafe',\n          eventSelectedColor: '#bfdbfe',\n          textColor: '#1e3a8a',\n        },\n      },\n    ],\n    defaultCalendar: 'work',\n    timeZone: 'Australia/Sydney',\n  });\n\ndescribe('QuickCreateEventPopup', () => {\n  it('creates an event from the keyboard suggestion flow', () => {\n    const app = createApp();\n    const onClose = jest.fn();\n    const anchor = document.createElement('div');\n\n    Object.defineProperty(anchor, 'getBoundingClientRect', {\n      value: () => ({\n        top: 120,\n        left: 160,\n        right: 200,\n        bottom: 152,\n        width: 40,\n        height: 32,\n        x: 160,\n        y: 120,\n        toJSON: () => ({}),\n      }),\n    });\n\n    document.body.append(anchor);\n\n    render(\n      <QuickCreateEventPopup\n        app={app}\n        anchorRef={{ current: anchor }}\n        onClose={onClose}\n        isOpen\n      />\n    );\n\n    const input = screen.getByRole('textbox');\n    fireEvent.input(input, { target: { value: 'Plan review' } });\n    fireEvent.keyDown(window, { key: 'Enter' });\n\n    const createdEvent = app\n      .getEvents()\n      .find(event => event.title === 'Plan review');\n\n    expect(createdEvent).toBeDefined();\n    expect(createdEvent?.calendarId).toBe('work');\n    expect(onClose).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/contextMenu/__tests__/utils.test.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { handlePasteEvent } from '@/components/contextMenu/utils';\nimport { ViewType, ICalendarApp, Event } from '@/types';\nimport { clipboardStore } from '@/utils/clipboardStore';\n\ndescribe('handlePasteEvent', () => {\n  afterEach(() => {\n    clipboardStore.clear();\n    jest.restoreAllMocks();\n  });\n\n  it('creates timed pasted events in app.timeZone', async () => {\n    const app = {\n      timeZone: 'Asia/Shanghai',\n      addEvent: jest.fn(),\n      getCalendarRegistry: jest.fn(() => ({\n        has: jest.fn(() => true),\n        getDefaultCalendarId: jest.fn(() => 'work'),\n      })),\n    } as unknown as ICalendarApp;\n\n    const copiedEvent: Event = {\n      id: 'event-1',\n      title: 'Copied Event',\n      start: Temporal.PlainDateTime.from('2026-04-02T15:30'),\n      end: Temporal.PlainDateTime.from('2026-04-02T16:30'),\n      calendarId: 'work',\n      allDay: false,\n    };\n\n    clipboardStore.setEvent(copiedEvent);\n\n    await handlePasteEvent(app, new Date(2026, 3, 10), ViewType.DAY);\n\n    expect(app.addEvent).toHaveBeenCalledTimes(1);\n    const createdEvent = (app.addEvent as jest.Mock).mock.calls[0][0] as Event;\n    expect(createdEvent.start).toBeInstanceOf(Temporal.ZonedDateTime);\n    expect(createdEvent.end).toBeInstanceOf(Temporal.ZonedDateTime);\n    expect(String(createdEvent.start)).toContain('[Asia/Shanghai]');\n    expect(String(createdEvent.end)).toContain('[Asia/Shanghai]');\n    expect((createdEvent.start as Temporal.ZonedDateTime).hour).toBe(15);\n    expect((createdEvent.start as Temporal.ZonedDateTime).minute).toBe(30);\n    expect((createdEvent.end as Temporal.ZonedDateTime).hour).toBe(16);\n    expect((createdEvent.end as Temporal.ZonedDateTime).minute).toBe(30);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/contextMenu/components/EventContextMenu.tsx",
    "content": "import {\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n} from '@dayflow/ui-context-menu';\n\nimport { Check } from '@/components/common/Icons';\nimport { useLocale } from '@/locale';\nimport { ContentSlot } from '@/renderer/ContentSlot';\nimport { Event, ICalendarApp } from '@/types';\nimport { clipboardStore } from '@/utils/clipboardStore';\n\ninterface EventContextMenuProps {\n  event: Event;\n  x: number;\n  y: number;\n  onClose: () => void;\n  app: ICalendarApp;\n  onDetailPanelToggle?: (id: string | null) => void;\n  detailPanelKey: string;\n}\n\nconst EventContextMenu = ({\n  event,\n  x,\n  y,\n  onClose,\n  app,\n}: EventContextMenuProps) => {\n  const { t } = useLocale();\n  if (!app.canMutateFromUI(event.id)) return null;\n\n  const calendars = app.getCalendars();\n\n  const handleMoveToCalendar = (calendarId: string) => {\n    app.updateEvent(event.id, { calendarId });\n    onClose();\n  };\n\n  const handleDelete = () => {\n    app.deleteEvent(event.id);\n    onClose();\n  };\n\n  const handleCopy = async () => {\n    try {\n      const eventData = JSON.stringify(event, null, 2);\n      await navigator.clipboard.writeText(eventData);\n      clipboardStore.setEvent(event);\n    } catch (err) {\n      console.error('Failed to copy event: ', err);\n    }\n    onClose();\n  };\n\n  const handleCut = async () => {\n    try {\n      const eventData = JSON.stringify(event, null, 2);\n      await navigator.clipboard.writeText(eventData);\n      clipboardStore.setEvent(event);\n      app.deleteEvent(event.id);\n    } catch (err) {\n      console.error('Failed to cut event: ', err);\n    }\n    onClose();\n  };\n\n  const defaultContent = (\n    <>\n      {/* Group 1: Calendar Submenu */}\n      <ContextMenuSub>\n        <ContextMenuSubTrigger>\n          {t('calendars') || 'Calendars'}\n        </ContextMenuSubTrigger>\n        <ContextMenuSubContent>\n          {calendars.map(cal => {\n            const isSelected = cal.id === event.calendarId;\n            return (\n              <ContextMenuItem\n                key={cal.id}\n                onClick={() => handleMoveToCalendar(cal.id)}\n              >\n                <div className='df-context-menu-calendar-item'>\n                  <div className='df-context-menu-calendar-check-wrap'>\n                    {isSelected && (\n                      <Check className='df-text-primary df-context-menu-calendar-check' />\n                    )}\n                  </div>\n                  <div className='df-context-menu-calendar-info'>\n                    <div\n                      className='df-context-menu-calendar-dot'\n                      style={{ backgroundColor: cal.colors.lineColor }}\n                    />\n                    <span\n                      className='df-context-menu-calendar-label'\n                      data-selected={isSelected}\n                    >\n                      {cal.name}\n                    </span>\n                  </div>\n                </div>\n              </ContextMenuItem>\n            );\n          })}\n        </ContextMenuSubContent>\n      </ContextMenuSub>\n\n      <ContextMenuSeparator />\n\n      {/* Group 2: Delete, Cut, Copy */}\n      <ContextMenuItem onClick={handleDelete} danger>\n        {t('delete') || 'Delete'}\n      </ContextMenuItem>\n      <ContextMenuItem onClick={handleCut}>{t('cut') || 'Cut'}</ContextMenuItem>\n      <ContextMenuItem onClick={handleCopy}>\n        {t('copy') || 'Copy'}\n      </ContextMenuItem>\n    </>\n  );\n\n  return (\n    <ContextMenu x={x} y={y} onClose={onClose}>\n      <ContentSlot\n        generatorName='eventContextMenu'\n        generatorArgs={{ event, onClose }}\n        defaultContent={defaultContent}\n      />\n    </ContextMenu>\n  );\n};\n\nexport default EventContextMenu;\n"
  },
  {
    "path": "packages/core/src/components/contextMenu/components/GridContextMenu.tsx",
    "content": "import { ContextMenu, ContextMenuItem } from '@dayflow/ui-context-menu';\n\nimport { handlePasteEvent } from '@/components/contextMenu/utils';\nimport { useLocale } from '@/locale';\nimport { ContentSlot } from '@/renderer/ContentSlot';\nimport { ICalendarApp, ViewType } from '@/types';\nimport { clipboardStore } from '@/utils/clipboardStore';\n\ninterface GridContextMenuProps {\n  x: number;\n  y: number;\n  date: Date;\n  onClose: () => void;\n  app: ICalendarApp;\n  onCreateEvent: () => void;\n  viewType?: ViewType;\n}\n\nconst GridContextMenu = ({\n  x,\n  y,\n  date,\n  onClose,\n  app,\n  onCreateEvent,\n  viewType,\n}: GridContextMenuProps) => {\n  const { t } = useLocale();\n  if (!app.canMutateFromUI()) return null;\n\n  const hasCopiedEvent = clipboardStore.hasEvent();\n\n  const handlePaste = async () => {\n    await handlePasteEvent(app, date, viewType);\n    onClose();\n  };\n\n  const defaultContent = (\n    <>\n      <ContextMenuItem\n        onClick={() => {\n          onCreateEvent();\n          onClose();\n        }}\n      >\n        {t('newEvent') || 'New Event'}\n      </ContextMenuItem>\n      <ContextMenuItem onClick={handlePaste} disabled={!hasCopiedEvent}>\n        {t('pasteHere') || 'Paste Here'}\n      </ContextMenuItem>\n    </>\n  );\n\n  return (\n    <ContextMenu x={x} y={y} onClose={onClose} className='df-context-menu'>\n      <ContentSlot\n        generatorName='gridContextMenu'\n        generatorArgs={{ date, viewType, onClose }}\n        defaultContent={defaultContent}\n      />\n    </ContextMenu>\n  );\n};\n\nexport default GridContextMenu;\n"
  },
  {
    "path": "packages/core/src/components/contextMenu/components/__tests__/readOnlyContextMenus.test.tsx",
    "content": "import { render, screen } from '@testing-library/preact';\nimport { Temporal } from 'temporal-polyfill';\n\nimport EventContextMenu from '@/components/contextMenu/components/EventContextMenu';\nimport GridContextMenu from '@/components/contextMenu/components/GridContextMenu';\nimport { CalendarApp } from '@/core/CalendarApp';\n\nconst createApp = (readOnly: boolean) =>\n  new CalendarApp({\n    views: [],\n    plugins: [],\n    events: [],\n    readOnly,\n    calendars: [\n      {\n        id: 'work',\n        name: 'Work',\n        colors: {\n          lineColor: '#2563eb',\n          eventColor: '#dbeafe',\n          eventSelectedColor: '#bfdbfe',\n          textColor: '#1e3a8a',\n        },\n      },\n    ],\n  });\n\ndescribe('read-only context menus', () => {\n  it('does not render the event context menu in read-only mode', () => {\n    const app = createApp(true);\n\n    render(\n      <EventContextMenu\n        event={{\n          id: 'event-1',\n          title: 'Test Event',\n          calendarId: 'work',\n          start: Temporal.Now.plainDateISO(),\n          end: Temporal.Now.plainDateISO(),\n        }}\n        x={10}\n        y={10}\n        onClose={jest.fn()}\n        app={app}\n        detailPanelKey='event-1'\n      />\n    );\n\n    expect(screen.queryByText('delete')).toBeNull();\n    expect(screen.queryByText('calendars')).toBeNull();\n  });\n\n  it('does not render the grid context menu in read-only mode', () => {\n    const app = createApp(true);\n\n    render(\n      <GridContextMenu\n        x={10}\n        y={10}\n        date={new Date(2026, 2, 27)}\n        onClose={jest.fn()}\n        app={app}\n        onCreateEvent={jest.fn()}\n      />\n    );\n\n    expect(screen.queryByText('newEvent')).toBeNull();\n    expect(screen.queryByText('pasteHere')).toBeNull();\n  });\n\n  it('still renders mutation menus when the calendar is editable', () => {\n    const app = createApp(false);\n\n    render(\n      <GridContextMenu\n        x={10}\n        y={10}\n        date={new Date(2026, 2, 27)}\n        onClose={jest.fn()}\n        app={app}\n        onCreateEvent={jest.fn()}\n      />\n    );\n\n    expect(screen.getByText('newEvent')).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/contextMenu/index.tsx",
    "content": "export {\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuLabel,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n  ContextMenuColorPicker,\n} from '@dayflow/ui-context-menu';\nexport { default as GridContextMenu } from './components/GridContextMenu';\nexport { default as EventContextMenu } from './components/EventContextMenu';\nexport * from './utils';\n"
  },
  {
    "path": "packages/core/src/components/contextMenu/utils.ts",
    "content": "import { Event, ViewType, ICalendarApp } from '@/types';\nimport {\n  generateUniKey,\n  temporalToDate,\n  dateToZonedDateTime,\n  dateToPlainDate,\n} from '@/utils';\nimport { clipboardStore } from '@/utils/clipboardStore';\n\n/**\n * Handle pasting an event from the clipboard store or system clipboard\n */\nexport const handlePasteEvent = async (\n  app: ICalendarApp,\n  date: Date,\n  viewType?: ViewType\n): Promise<void> => {\n  if (!clipboardStore.hasEvent()) return;\n\n  try {\n    // Prefer the internal store for consistency and speed\n    let eventData = clipboardStore.getEvent();\n\n    if (!eventData) {\n      // Fallback to system clipboard if internal store is somehow empty but text is there\n      const text = await navigator.clipboard.readText();\n      if (text) {\n        eventData = JSON.parse(text);\n      }\n    }\n\n    if (eventData && typeof eventData === 'object' && 'title' in eventData) {\n      const typedEvent = eventData as Event;\n      // Calculate duration of original event using utility to handle Temporal objects\n      const originalStart = temporalToDate(typedEvent.start as unknown as Date);\n      const originalEnd = temporalToDate(typedEvent.end as unknown as Date);\n      const duration = originalEnd.getTime() - originalStart.getTime();\n\n      // Clean up internal fields that shouldn't be copied\n      const cleanEventData = eventData;\n\n      // Target dates\n      const targetStartDate = new Date(date);\n\n      // Preserve time logic:\n      // If pasting into Month/Year view, or if the click was exactly at midnight (00:00)\n      // and the original event had a non-midnight time, preserve the original time.\n      const isMonthOrYear =\n        viewType === ViewType.MONTH || viewType === ViewType.YEAR;\n      const isClickedAtMidnight =\n        targetStartDate.getHours() === 0 && targetStartDate.getMinutes() === 0;\n      const originalHadTime =\n        originalStart.getHours() !== 0 || originalStart.getMinutes() !== 0;\n\n      if (\n        !typedEvent.allDay &&\n        (isMonthOrYear || (isClickedAtMidnight && originalHadTime))\n      ) {\n        targetStartDate.setHours(\n          originalStart.getHours(),\n          originalStart.getMinutes(),\n          originalStart.getSeconds(),\n          originalStart.getMilliseconds()\n        );\n      }\n\n      const targetEndDate = new Date(\n        targetStartDate.getTime() + (duration > 0 ? duration : 3600000)\n      );\n\n      const newEvent: Event = {\n        ...cleanEventData,\n        title: typedEvent.title,\n        id: generateUniKey(),\n        // Use Temporal objects consistently\n        start: typedEvent.allDay\n          ? dateToPlainDate(targetStartDate)\n          : dateToZonedDateTime(targetStartDate, app.timeZone),\n        end: typedEvent.allDay\n          ? dateToPlainDate(targetEndDate)\n          : dateToZonedDateTime(targetEndDate, app.timeZone),\n        // Ensure it belongs to a valid calendar\n        calendarId:\n          typedEvent.calendarId &&\n          app.getCalendarRegistry().has(typedEvent.calendarId)\n            ? typedEvent.calendarId\n            : app.getCalendarRegistry().getDefaultCalendarId() || 'default',\n      };\n\n      app.addEvent(newEvent);\n    }\n  } catch (err) {\n    console.error('Failed to paste event:', err);\n  }\n};\n"
  },
  {
    "path": "packages/core/src/components/dayView/DayContent.tsx",
    "content": "import { RefObject } from 'preact';\nimport { useEffect, useRef, useState, useMemo } from 'preact/hooks';\n\nimport CalendarEventComponent from '@/components/calendarEvent';\nimport ViewHeader from '@/components/common/ViewHeader';\nimport { GridContextMenu } from '@/components/contextMenu';\nimport { useLocale } from '@/locale';\nimport {\n  allDayRow,\n  allDayLabel,\n  calendarContent,\n  timeColumn,\n  timeSlot,\n  timeLabel,\n  timeGridRow,\n  currentTimeLine,\n  currentTimeLabel,\n  currentTimeLineBar,\n  timeGridBoundary,\n  midnightLabel,\n  cn,\n} from '@/styles/classNames';\nimport {\n  Event,\n  EventLayout,\n  WeekDayDragState,\n  ICalendarApp,\n  ViewType,\n} from '@/types';\nimport { formatTime, scrollbarTakesSpace } from '@/utils';\nimport {\n  startPendingCreate,\n  finalizeCreateOnDblClick,\n} from '@/views/utils/dragCreate';\n\ninterface DayContentProps {\n  app: ICalendarApp;\n  currentDate: Date;\n  currentWeekStart: Date;\n  events: Event[];\n  currentDayEvents: Event[];\n  organizedAllDayEvents: Array<Event & { row: number }>;\n  allDayAreaHeight: number;\n  timeSlots: Array<{ hour: number; label: string }>;\n  eventLayouts: Map<string, EventLayout>;\n  isToday: boolean;\n  currentTime: Date | null;\n  selectedEventId: string | null;\n  setSelectedEventId: (id: string | null) => void;\n  newlyCreatedEventId: string | null;\n  setNewlyCreatedEventId: (id: string | null) => void;\n  detailPanelEventId: string | null;\n  setDetailPanelEventId: (id: string | null) => void;\n  dragState: WeekDayDragState | null;\n  isDragging: boolean;\n  handleMoveStart: (e: MouseEvent | TouchEvent, event: Event) => void;\n  handleResizeStart: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  handleCreateStart: (\n    e: MouseEvent | TouchEvent,\n    dayIndex: number,\n    hour: number\n  ) => void;\n  handleCreateAllDayEvent: (\n    e: MouseEvent | TouchEvent,\n    dayIndex: number\n  ) => void;\n  handleTouchStart: (e: TouchEvent, dayIndex: number) => void;\n  handleTouchEnd: (e: TouchEvent) => void;\n  handleTouchMove: (e: TouchEvent) => void;\n  handleDragOver: (e: DragEvent) => void;\n  handleDrop: (\n    e: DragEvent,\n    date: Date,\n    hour?: number,\n    allDay?: boolean\n  ) => void;\n  handleEventUpdate: (event: Event) => void;\n  handleEventDelete: (id: string) => void;\n  onDateChange?: (date: Date) => void;\n  useEventDetailPanel?: boolean;\n  calendarRef: RefObject<HTMLDivElement>;\n  allDayRowRef: RefObject<HTMLDivElement>;\n  timeGridRef: RefObject<HTMLDivElement>;\n  switcherMode: string;\n  isMobile: boolean;\n  isTouch: boolean;\n  setDraftEvent: (event: Event | null) => void;\n  setIsDrawerOpen: (isOpen: boolean) => void;\n  ALL_DAY_HEIGHT: number;\n  HOUR_HEIGHT: number;\n  FIRST_HOUR: number;\n  LAST_HOUR: number;\n  showAllDay: boolean;\n  showStartOfDayLabel: boolean;\n  timeFormat?: '12h' | '24h';\n  secondaryTimeSlots?: string[];\n  primaryTzLabel?: string;\n  secondaryTzLabel?: string;\n  appTimeZone?: string;\n}\n\nexport const DayContent = ({\n  app,\n  currentDate,\n  currentWeekStart,\n  events,\n  currentDayEvents,\n  organizedAllDayEvents,\n  allDayAreaHeight,\n  timeSlots,\n  eventLayouts,\n  isToday,\n  currentTime,\n  selectedEventId,\n  setSelectedEventId,\n  newlyCreatedEventId,\n  setNewlyCreatedEventId,\n  detailPanelEventId,\n  setDetailPanelEventId,\n  dragState,\n  isDragging,\n  handleMoveStart,\n  handleResizeStart,\n  handleCreateStart,\n  handleCreateAllDayEvent,\n  handleTouchStart,\n  handleTouchEnd,\n  handleTouchMove,\n  handleDragOver,\n  handleDrop,\n  handleEventUpdate,\n  handleEventDelete,\n  onDateChange,\n  useEventDetailPanel,\n  calendarRef,\n  allDayRowRef,\n  timeGridRef,\n  switcherMode,\n  isMobile,\n  isTouch,\n  setDraftEvent,\n  setIsDrawerOpen,\n  ALL_DAY_HEIGHT,\n  HOUR_HEIGHT,\n  FIRST_HOUR,\n  LAST_HOUR,\n  showAllDay,\n  showStartOfDayLabel,\n  timeFormat = '24h',\n  secondaryTimeSlots,\n  primaryTzLabel,\n  secondaryTzLabel,\n  appTimeZone,\n}: DayContentProps) => {\n  const hasSecondaryTz = !!secondaryTimeSlots && secondaryTimeSlots.length > 0;\n  // On mobile the time column is too narrow for dual labels — hide secondary TZ display\n  const showSecondaryTz = hasSecondaryTz && !isMobile;\n  const { t } = useLocale();\n  const prevHighlightedEventId = useRef(app.state.highlightedEventId);\n  const [contextMenu, setContextMenu] = useState<{\n    x: number;\n    y: number;\n    date: Date;\n  } | null>(null);\n  const hasScrollbarSpace = useMemo(() => scrollbarTakesSpace(), []);\n  const headerSubtitleMeta = useMemo(() => {\n    if (!showSecondaryTz || !secondaryTzLabel || !primaryTzLabel) {\n      return null;\n    }\n\n    return (\n      <>\n        <span className='df-time-column-tz-label'>{secondaryTzLabel}</span>\n        <span className='df-time-column-tz-label'>{primaryTzLabel}</span>\n      </>\n    );\n  }, [showSecondaryTz, secondaryTzLabel, primaryTzLabel]);\n\n  // Measure offset from .df-calendar-content top to the first time grid row,\n  // accounting for boundary elements above the grid\n  const getGridOffset = () => {\n    const content = calendarRef.current?.querySelector('.df-calendar-content');\n    if (!content) return 0;\n    const firstRow = content.querySelector('.df-time-grid-row');\n    if (!firstRow) return 0;\n    return (\n      firstRow.getBoundingClientRect().top -\n      content.getBoundingClientRect().top +\n      content.scrollTop\n    );\n  };\n\n  /** Returns the fractional hour at the given clientY, or null if the ref is unavailable. */\n  const getClickedHour = (clientY: number): number | null => {\n    const content = calendarRef.current?.querySelector('.df-calendar-content');\n    if (!content) return null;\n    const rect = content.getBoundingClientRect();\n    const scrollTop = (content as HTMLElement).scrollTop || 0;\n    return (\n      FIRST_HOUR +\n      (clientY - rect.top + scrollTop - getGridOffset()) / HOUR_HEIGHT\n    );\n  };\n  const isEditable = app.canMutateFromUI();\n\n  useEffect(() => {\n    if (isEditable) return;\n    setContextMenu(null);\n  }, [isEditable]);\n\n  const handleContextMenu = (e: MouseEvent, isAllDay: boolean) => {\n    e.preventDefault();\n    if (isMobile || !isEditable) return;\n\n    const date = new Date(currentDate);\n\n    if (isAllDay) {\n      date.setHours(0, 0, 0, 0);\n    } else {\n      const rect = calendarRef.current\n        ?.querySelector('.df-calendar-content')\n        ?.getBoundingClientRect();\n      if (rect) {\n        const scrollTop =\n          (\n            calendarRef.current?.querySelector(\n              '.df-calendar-content'\n            ) as HTMLElement\n          )?.scrollTop || 0;\n        const gridOffset = getGridOffset();\n        const relativeY = e.clientY - rect.top + scrollTop - gridOffset;\n        const floatHour = relativeY / HOUR_HEIGHT + FIRST_HOUR;\n        const h = Math.floor(floatHour);\n        const m = Math.floor((floatHour - h) * 60);\n\n        const snappedMinutes = Math.round(m / 15) * 15;\n        const finalHour = snappedMinutes === 60 ? h + 1 : h;\n        const finalMinutes = snappedMinutes === 60 ? 0 : snappedMinutes;\n\n        date.setHours(finalHour, finalMinutes, 0, 0);\n      }\n    }\n\n    setContextMenu({ x: e.clientX, y: e.clientY, date });\n  };\n\n  return (\n    <div\n      className='df-day-content'\n      data-switcher-mode={switcherMode}\n      onContextMenu={e => e.preventDefault()}\n    >\n      <div className='df-day-content-layout'>\n        {/* Fixed navigation bar */}\n        <div\n          className='df-day-content-header-wrap'\n          onContextMenu={e => e.preventDefault()}\n          style={{\n            paddingRight: isMobile || !hasScrollbarSpace ? '0px' : '15px',\n          }}\n        >\n          <ViewHeader\n            calendar={app}\n            viewType={ViewType.DAY}\n            currentDate={currentDate}\n            subtitleMeta={headerSubtitleMeta}\n          />\n        </div>\n        {/* All-day event area */}\n        {showAllDay ? (\n          <div\n            className={cn(allDayRow, 'df-day-content-all-day-row')}\n            ref={allDayRowRef}\n            data-scrollbar-space={\n              !isMobile && hasScrollbarSpace ? 'true' : 'false'\n            }\n            style={{\n              paddingRight:\n                isMobile || !hasScrollbarSpace ? '0px' : '0.6875rem',\n            }}\n            onContextMenu={e => handleContextMenu(e, true)}\n          >\n            <div\n              className={cn(allDayLabel, 'df-day-content-all-day-label')}\n              onContextMenu={e => e.preventDefault()}\n            >\n              {t('allDay')}\n            </div>\n            <div\n              className='df-day-content-all-day-grid'\n              data-scrollbar-space={\n                !isMobile && hasScrollbarSpace ? 'true' : 'false'\n              }\n            >\n              <div\n                className='df-day-content-all-day-lane'\n                style={{ minHeight: `${allDayAreaHeight}px` }}\n                onClick={() => onDateChange?.(currentDate)}\n                onMouseDown={e => {\n                  const currentDayIndex = Math.floor(\n                    (currentDate.getTime() - currentWeekStart.getTime()) /\n                      (24 * 60 * 60 * 1000)\n                  );\n                  handleCreateAllDayEvent?.(e, currentDayIndex);\n                }}\n                onDblClick={e => {\n                  const currentDayIndex = Math.floor(\n                    (currentDate.getTime() - currentWeekStart.getTime()) /\n                      (24 * 60 * 60 * 1000)\n                  );\n                  handleCreateAllDayEvent?.(e, currentDayIndex);\n                }}\n                onDragOver={handleDragOver}\n                onDrop={e => {\n                  handleDrop(e, currentDate, undefined, true);\n                }}\n              >\n                {organizedAllDayEvents.map(event => (\n                  <CalendarEventComponent\n                    key={event.id}\n                    event={event}\n                    isAllDay={true}\n                    viewType={ViewType.DAY}\n                    segmentIndex={event.row}\n                    allDayHeight={ALL_DAY_HEIGHT}\n                    calendarRef={calendarRef}\n                    isBeingDragged={\n                      isDragging &&\n                      (dragState as WeekDayDragState)?.eventId === event.id &&\n                      (dragState as WeekDayDragState)?.mode === 'move'\n                    }\n                    hourHeight={HOUR_HEIGHT}\n                    firstHour={FIRST_HOUR}\n                    onMoveStart={handleMoveStart}\n                    onEventUpdate={handleEventUpdate}\n                    onEventDelete={handleEventDelete}\n                    newlyCreatedEventId={newlyCreatedEventId}\n                    onDetailPanelOpen={() => setNewlyCreatedEventId(null)}\n                    detailPanelEventId={detailPanelEventId}\n                    onDetailPanelToggle={(eventId: string | null) =>\n                      setDetailPanelEventId(eventId)\n                    }\n                    selectedEventId={selectedEventId}\n                    onEventSelect={(eventId: string | null) => {\n                      const isViewable = app.getReadOnlyConfig().viewable;\n                      const canMutateFromUI = app.canMutateFromUI();\n                      const evt = events.find(e => e.id === eventId);\n                      if (\n                        (isMobile || isTouch) &&\n                        evt &&\n                        isViewable &&\n                        canMutateFromUI\n                      ) {\n                        setDraftEvent(evt);\n                        setIsDrawerOpen(true);\n                      } else {\n                        setSelectedEventId(eventId);\n                        if (app.state.highlightedEventId) {\n                          app.highlightEvent(null);\n                          prevHighlightedEventId.current = null;\n                        }\n                      }\n                    }}\n                    onEventLongPress={(eventId: string) => {\n                      if (isMobile || isTouch) {\n                        setSelectedEventId(eventId);\n                      }\n                    }}\n                    useEventDetailPanel={useEventDetailPanel}\n                    app={app}\n                    isMobile={isMobile}\n                    enableTouch={isTouch}\n                    appTimeZone={appTimeZone}\n                  />\n                ))}\n              </div>\n            </div>\n          </div>\n        ) : (\n          <div\n            className='df-day-content-all-day-spacer'\n            data-scrollbar-space={\n              !isMobile && hasScrollbarSpace ? 'true' : 'false'\n            }\n          />\n        )}\n\n        {/* Time grid and event area */}\n        <div\n          className={cn(calendarContent, 'df-day-content-grid')}\n          style={{ scrollbarGutter: 'stable' }}\n        >\n          <div className='df-day-content-grid-inner'>\n            {/* Current time line */}\n            {isToday &&\n              currentTime &&\n              (() => {\n                const now = currentTime;\n                const hours = now.getHours() + now.getMinutes() / 60;\n                if (hours < FIRST_HOUR || hours > LAST_HOUR) return null;\n\n                const topPx = (hours - FIRST_HOUR) * HOUR_HEIGHT;\n\n                return (\n                  <div\n                    className={currentTimeLine}\n                    data-secondary-tz={showSecondaryTz ? 'true' : 'false'}\n                    style={{\n                      top: `${topPx}px`,\n                      height: 0,\n                      zIndex: 20,\n                      marginTop: '0.75rem',\n                    }}\n                  >\n                    <div className='df-day-content-current-time-side'>\n                      <div className='df-day-content-current-time-side-inner' />\n                      <div className={currentTimeLabel}>\n                        {formatTime(hours, 0, timeFormat, false)}\n                      </div>\n                    </div>\n\n                    <div className='df-day-content-current-time-rail'>\n                      <div className={currentTimeLineBar} />\n                    </div>\n                  </div>\n                );\n              })()}\n\n            {/* Time column */}\n            <div\n              className={timeColumn}\n              data-secondary-tz={showSecondaryTz ? 'true' : 'false'}\n              onContextMenu={e => e.preventDefault()}\n            >\n              {/* Top boundary spacer — expands to include timezone header when active */}\n              <div\n                className='df-time-column-spacer df-time-day-column-spacer'\n                data-secondary-tz={showSecondaryTz ? 'true' : 'false'}\n              />\n              {timeSlots.map((slot, slotIndex) => (\n                <div key={slotIndex} className={timeSlot}>\n                  {showSecondaryTz ? (\n                    <div className='df-time-column-tz-row'>\n                      <span className='df-time-column-tz-value'>\n                        {showStartOfDayLabel && slotIndex === 0\n                          ? ''\n                          : (secondaryTimeSlots?.[slotIndex] ?? '')}\n                      </span>\n                      <span className='df-time-column-tz-value'>\n                        {showStartOfDayLabel && slotIndex === 0\n                          ? ''\n                          : slot.label}\n                      </span>\n                    </div>\n                  ) : (\n                    <div className={timeLabel}>\n                      {showStartOfDayLabel && slotIndex === 0 ? '' : slot.label}\n                    </div>\n                  )}\n                </div>\n              ))}\n            </div>\n\n            {/* Time grid */}\n            <div className='df-day-content-grid-column'>\n              {/* Top boundary — height must match time column spacer */}\n              <div\n                className={cn(\n                  timeGridBoundary,\n                  'df-time-grid-boundary-top',\n                  'df-day-content-grid-boundary'\n                )}\n                data-scrollbar-space={\n                  !isMobile && hasScrollbarSpace ? 'true' : 'false'\n                }\n              >\n                <div\n                  className={cn(midnightLabel, 'df-midnight-label-offset')}\n                  style={{ top: 'auto', bottom: '-0.625rem' }}\n                >\n                  {showStartOfDayLabel\n                    ? formatTime(FIRST_HOUR, 0, timeFormat)\n                    : ''}\n                </div>\n              </div>\n              <div\n                className='df-day-content-grid-rows'\n                style={{ WebkitTouchCallout: 'none' }}\n                ref={timeGridRef}\n                data-scrollbar-space={\n                  !isMobile && hasScrollbarSpace ? 'true' : 'false'\n                }\n                onDragOver={handleDragOver}\n                onDrop={e => {\n                  const rect = timeGridRef.current?.getBoundingClientRect();\n                  if (!rect) return;\n\n                  const relativeY = e.clientY - rect.top;\n                  const dropHour = Math.floor(\n                    FIRST_HOUR + relativeY / HOUR_HEIGHT\n                  );\n                  handleDrop(e, currentDate, dropHour);\n                }}\n              >\n                {timeSlots.map((_slot, slotIndex) => (\n                  <div\n                    key={slotIndex}\n                    className={timeGridRow}\n                    data-scrollbar-space={\n                      !isMobile && hasScrollbarSpace ? 'true' : 'false'\n                    }\n                    onClick={() => onDateChange?.(currentDate)}\n                    onMouseDown={e => {\n                      const hour = getClickedHour(e.clientY);\n                      if (hour === null) return;\n                      const dayIndex = Math.floor(\n                        (currentDate.getTime() - currentWeekStart.getTime()) /\n                          (24 * 60 * 60 * 1000)\n                      );\n                      startPendingCreate(\n                        e,\n                        dayIndex,\n                        hour,\n                        isTouch,\n                        handleCreateStart\n                      );\n                    }}\n                    onDblClick={e => {\n                      const hour = getClickedHour(e.clientY);\n                      if (hour === null) return;\n                      const dayIndex = Math.floor(\n                        (currentDate.getTime() - currentWeekStart.getTime()) /\n                          (24 * 60 * 60 * 1000)\n                      );\n                      handleCreateStart?.(e, dayIndex, hour);\n                      finalizeCreateOnDblClick();\n                    }}\n                    onTouchStart={e => {\n                      const currentDayIndex = Math.floor(\n                        (currentDate.getTime() - currentWeekStart.getTime()) /\n                          (24 * 60 * 60 * 1000)\n                      );\n                      handleTouchStart(e, currentDayIndex);\n                    }}\n                    onTouchEnd={handleTouchEnd}\n                    onTouchMove={handleTouchMove}\n                    onContextMenu={e => handleContextMenu(e, false)}\n                  />\n                ))}\n\n                {/* Bottom boundary */}\n                <div\n                  className={cn(\n                    timeGridBoundary,\n                    'df-day-content-grid-boundary-bottom'\n                  )}\n                  data-scrollbar-space={\n                    !isMobile && hasScrollbarSpace ? 'true' : 'false'\n                  }\n                >\n                  {showSecondaryTz ? (\n                    <div\n                      className='df-time-column-tz-row df-day-content-bottom-tz-row'\n                      style={{\n                        left: isMobile ? '-5rem' : '-5.5rem',\n                        width: isMobile ? '5rem' : '5.5rem',\n                      }}\n                    >\n                      <span>{secondaryTimeSlots?.[0] ?? ''}</span>\n                      <span>{formatTime(0, 0, timeFormat)}</span>\n                    </div>\n                  ) : (\n                    <div\n                      className={cn(midnightLabel, 'df-midnight-label-offset')}\n                    >\n                      {formatTime(0, 0, timeFormat)}\n                    </div>\n                  )}\n                </div>\n\n                {/* Event layer */}\n                <div className='df-day-content-event-layer'>\n                  {currentDayEvents\n                    .filter(event => !event.allDay)\n                    .map(event => {\n                      const eventLayout = eventLayouts.get(event.id);\n                      return (\n                        <CalendarEventComponent\n                          key={event.id}\n                          event={event}\n                          layout={eventLayout}\n                          viewType={ViewType.DAY}\n                          calendarRef={calendarRef}\n                          isBeingDragged={\n                            isDragging &&\n                            (dragState as WeekDayDragState)?.eventId ===\n                              event.id &&\n                            (dragState as WeekDayDragState)?.mode === 'move'\n                          }\n                          hourHeight={HOUR_HEIGHT}\n                          firstHour={FIRST_HOUR}\n                          onMoveStart={handleMoveStart}\n                          onResizeStart={handleResizeStart}\n                          onEventUpdate={handleEventUpdate}\n                          onEventDelete={handleEventDelete}\n                          newlyCreatedEventId={newlyCreatedEventId}\n                          onDetailPanelOpen={() => setNewlyCreatedEventId(null)}\n                          detailPanelEventId={detailPanelEventId}\n                          onDetailPanelToggle={(eventId: string | null) =>\n                            setDetailPanelEventId(eventId)\n                          }\n                          selectedEventId={selectedEventId}\n                          onEventSelect={(eventId: string | null) => {\n                            const isViewable = app.getReadOnlyConfig().viewable;\n                            const evt = events.find(e => e.id === eventId);\n                            if ((isMobile || isTouch) && evt && isViewable) {\n                              setDraftEvent(evt);\n                              setIsDrawerOpen(true);\n                            } else {\n                              setSelectedEventId(eventId);\n                              if (app.state.highlightedEventId) {\n                                app.highlightEvent(null);\n                                prevHighlightedEventId.current = null;\n                              }\n                            }\n                          }}\n                          onEventLongPress={(eventId: string) => {\n                            if (isMobile || isTouch) {\n                              setSelectedEventId(eventId);\n                            }\n                          }}\n                          useEventDetailPanel={useEventDetailPanel}\n                          app={app}\n                          isMobile={isMobile}\n                          enableTouch={isTouch}\n                          appTimeZone={appTimeZone}\n                        />\n                      );\n                    })}\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      {isEditable && contextMenu && (\n        <GridContextMenu\n          x={contextMenu.x}\n          y={contextMenu.y}\n          date={contextMenu.date}\n          viewType={ViewType.DAY}\n          onClose={() => setContextMenu(null)}\n          app={app}\n          onCreateEvent={() => {\n            if (handleCreateStart) {\n              const currentDayIndex = Math.floor(\n                (currentDate.getTime() - currentWeekStart.getTime()) /\n                  (24 * 60 * 60 * 1000)\n              );\n              const isAllDay =\n                contextMenu.date.getHours() === 0 &&\n                contextMenu.date.getMinutes() === 0;\n\n              if (isAllDay) {\n                handleCreateAllDayEvent?.(\n                  {\n                    clientX: contextMenu.x,\n                    clientY: contextMenu.y,\n                  } as MouseEvent,\n                  currentDayIndex\n                );\n              } else {\n                const preciseHour =\n                  contextMenu.date.getHours() +\n                  contextMenu.date.getMinutes() / 60;\n                const syntheticEvent = {\n                  preventDefault: () => {\n                    /* noop */\n                  },\n                  stopPropagation: () => {\n                    /* noop */\n                  },\n                  clientX: contextMenu.x,\n                  clientY: contextMenu.y,\n                } as MouseEvent;\n                handleCreateStart(syntheticEvent, currentDayIndex, preciseHour);\n              }\n            }\n          }}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/dayView/RightPanel.tsx",
    "content": "import { RefObject } from 'preact';\n\nimport { CalendarEvent } from '@/components/calendarEvent';\nimport { MiniCalendar } from '@/components/common/MiniCalendar';\nimport TodayBox from '@/components/common/TodayBox';\nimport { useLocale } from '@/locale';\nimport { miniCalendarContainer } from '@/styles/classNames';\nimport { ICalendarApp, Event, ViewType } from '@/types';\nimport { temporalToVisualDate } from '@/utils/temporalTypeGuards';\n\ninterface RightPanelProps {\n  app: ICalendarApp;\n  currentDate: Date;\n  visibleMonth: Date;\n  currentDayEvents: Event[];\n  selectedEvent: Event | null;\n  setSelectedEvent: (event: Event | null) => void;\n  handleMonthChange: (offset: number) => void;\n  handleDateSelect: (date: Date) => void;\n  switcherMode: string;\n  timeFormat?: '12h' | '24h';\n  showEventDots?: boolean;\n  appTimeZone?: string;\n  calendarRef: RefObject<HTMLDivElement>;\n}\n\nexport const RightPanel = ({\n  app,\n  currentDate,\n  visibleMonth,\n  currentDayEvents,\n  selectedEvent,\n  setSelectedEvent,\n  handleMonthChange,\n  handleDateSelect,\n  switcherMode,\n  timeFormat = '24h',\n  showEventDots = true,\n  appTimeZone,\n  calendarRef,\n}: RightPanelProps) => {\n  const { t, locale } = useLocale();\n\n  const sortedEvents = [...currentDayEvents].toSorted((a, b) => {\n    if (a.allDay && !b.allDay) return -1;\n    if (!a.allDay && b.allDay) return 1;\n    if (!a.allDay && !b.allDay) {\n      const timeA = temporalToVisualDate(a.start, appTimeZone).getTime();\n      const timeB = temporalToVisualDate(b.start, appTimeZone).getTime();\n      return timeA - timeB;\n    }\n    return 0;\n  });\n\n  return (\n    <div\n      className='df-right-panel'\n      data-switcher-mode={switcherMode}\n      onContextMenu={e => e.preventDefault()}\n    >\n      <div className='df-right-panel-layout'>\n        {/* Mini calendar */}\n        <div className={miniCalendarContainer}>\n          <div className='df-right-panel-calendar-shell'>\n            <div className='df-right-panel-calendar-header'>\n              <div className='df-right-panel-header-spacer' aria-hidden='true'>\n                &nbsp;\n              </div>\n              <TodayBox\n                handlePreviousMonth={() => app.goToPrevious()}\n                handleNextMonth={() => app.goToNext()}\n                handleToday={() => app.goToToday()}\n              />\n            </div>\n            <MiniCalendar\n              visibleMonth={visibleMonth}\n              currentDate={currentDate}\n              showHeader={true}\n              onMonthChange={handleMonthChange}\n              onDateSelect={handleDateSelect}\n              events={app.getEvents()}\n              showEventDots={showEventDots}\n              calendarRegistry={app.getCalendarRegistry()}\n              timeZone={app.timeZone}\n            />\n          </div>\n        </div>\n\n        {/* Event details area */}\n        <div className='df-right-panel-events'>\n          <div className='df-right-panel-events-inner'>\n            <h3 className='df-right-panel-date-heading'>\n              {currentDate.toLocaleDateString(locale, {\n                weekday: 'long',\n                month: 'long',\n                day: 'numeric',\n              })}\n            </h3>\n\n            {sortedEvents.length === 0 ? (\n              <p className='df-right-panel-empty'>{t('noEvents')}</p>\n            ) : (\n              <div className='df-right-panel-list'>\n                {sortedEvents.map((event: Event) => (\n                  <div key={event.id} className='df-right-panel-event-item'>\n                    <CalendarEvent\n                      event={event}\n                      isAllDay={event.allDay}\n                      viewType={ViewType.DAY}\n                      calendarRef={calendarRef}\n                      onEventUpdate={updated =>\n                        app.updateEvent(updated.id, updated)\n                      }\n                      onEventDelete={id => app.deleteEvent(id)}\n                      onEventSelect={id => {\n                        if (!id) {\n                          setSelectedEvent(null);\n                          return;\n                        }\n                        const found = app.getEvents().find(e => e.id === id);\n                        setSelectedEvent(found || null);\n                      }}\n                      selectedEventId={selectedEvent?.id}\n                      app={app}\n                      timeFormat={timeFormat}\n                      appTimeZone={appTimeZone}\n                      hourHeight={0}\n                      firstHour={0}\n                      disableDefaultStyle={true}\n                      className='df-right-panel-event-card'\n                    />\n                  </div>\n                ))}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/dayView/__tests__/util.timezone.test.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport {\n  filterDayEvents,\n  normalizeLayoutEvents,\n} from '@/components/dayView/util';\nimport { Event } from '@/types';\nimport { extractHourFromDate } from '@/utils';\n\ndescribe('filterDayEvents', () => {\n  it('filters events using their local/original day in Day view', () => {\n    const currentDate = new Date(2026, 3, 8);\n    const currentWeekStart = new Date(2026, 3, 6);\n\n    const events: Event[] = [\n      {\n        id: 'same-day',\n        title: 'Same Day',\n        start: Temporal.PlainDateTime.from('2026-04-08T09:00:00'),\n        end: Temporal.PlainDateTime.from('2026-04-08T10:00:00'),\n      },\n      {\n        id: 'previous-day',\n        title: 'Previous Day',\n        start: Temporal.PlainDateTime.from('2026-04-07T18:00:00'),\n        end: Temporal.PlainDateTime.from('2026-04-07T19:00:00'),\n      },\n      {\n        id: 'all-day',\n        title: 'All Day',\n        start: Temporal.PlainDate.from('2026-04-08'),\n        end: Temporal.PlainDate.from('2026-04-08'),\n        allDay: true,\n      },\n    ];\n\n    const results = filterDayEvents(events, currentDate, currentWeekStart);\n\n    expect(results.map(event => event.id)).toEqual(['same-day', 'all-day']);\n  });\n\n  it('normalizes timed layout events without shifting their wall time', () => {\n    const currentDate = new Date(2026, 3, 8);\n\n    const timedEvent: Event = {\n      id: 'timed-event',\n      title: 'Timed Event',\n      start: Temporal.PlainDateTime.from('2026-04-07T23:00:00'),\n      end: Temporal.PlainDateTime.from('2026-04-08T02:30:00'),\n    };\n\n    const layoutEvents = normalizeLayoutEvents([timedEvent], currentDate);\n\n    expect(extractHourFromDate(layoutEvents[0].start)).toBe(0);\n    expect(extractHourFromDate(layoutEvents[0].end)).toBe(2.5);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/dayView/util.ts",
    "content": "import { EventLayoutCalculator } from '@/components/eventLayout';\nimport { Event, EventLayout } from '@/types';\nimport { createAllDayDisplayComparator } from '@/utils/allDaySort';\nimport { extractHourFromDate, getEventEndHour } from '@/utils/helpers';\nimport {\n  dateToZonedDateTime,\n  temporalToDate,\n  temporalToVisualDate,\n} from '@/utils/temporalTypeGuards';\n\n// Filter events for the current day\nexport const filterDayEvents = (\n  events: Event[],\n  currentDate: Date,\n  currentWeekStart: Date,\n  appTimeZone?: string\n): Event[] => {\n  const dayStart = new Date(currentDate);\n  dayStart.setHours(0, 0, 0, 0);\n\n  const dayEnd = new Date(currentDate);\n  dayEnd.setHours(23, 59, 59, 999);\n\n  const toDate = (temporal: Event['start']) =>\n    appTimeZone\n      ? temporalToVisualDate(temporal, appTimeZone)\n      : temporalToDate(temporal);\n\n  const filtered = events.filter(event => {\n    const eventStart = toDate(event.start);\n    const eventEnd = toDate(event.end ?? event.start);\n\n    if (event.allDay) {\n      const s = new Date(eventStart);\n      s.setHours(0, 0, 0, 0);\n      const e = new Date(eventEnd);\n      e.setHours(0, 0, 0, 0);\n      return s <= dayEnd && e >= dayStart;\n    }\n\n    return eventStart < dayEnd && eventEnd > dayStart;\n  });\n\n  return filtered.map(event => {\n    const eventDate = toDate(event.start);\n    const dayDiff = Math.floor(\n      (eventDate.getTime() - currentWeekStart.getTime()) / (24 * 60 * 60 * 1000)\n    );\n    const correctDay = Math.max(0, Math.min(6, dayDiff));\n\n    return {\n      ...event,\n      day: correctDay,\n    };\n  });\n};\n\n// Normalize events for layout calculation (clamping to current day)\nexport const normalizeLayoutEvents = (\n  currentDayEvents: Event[],\n  currentDate: Date,\n  appTimeZone?: string\n): Event[] => {\n  const dayStart = new Date(currentDate);\n  dayStart.setHours(0, 0, 0, 0);\n  const nextDay = new Date(dayStart);\n  nextDay.setDate(nextDay.getDate() + 1);\n\n  const toVisual = (t: Event['start']) =>\n    appTimeZone ? temporalToVisualDate(t, appTimeZone) : temporalToDate(t);\n\n  return currentDayEvents\n    .filter(e => !e.allDay)\n    .map(event => {\n      const eventStart = toVisual(event.start);\n      const eventEnd = toVisual(event.end ?? event.start);\n      let newStart = dateToZonedDateTime(eventStart, appTimeZone);\n      let newEnd = dateToZonedDateTime(eventEnd, appTimeZone);\n\n      if (eventStart < dayStart) {\n        newStart = dateToZonedDateTime(dayStart, appTimeZone);\n      }\n\n      if (eventEnd > nextDay) {\n        newEnd = dateToZonedDateTime(nextDay, appTimeZone);\n      }\n\n      return {\n        ...event,\n        start: newStart,\n        end: newEnd,\n        day: 0, // Force all events to same day index for collision detection\n        _originalStartHour: extractHourFromDate(event.start),\n        _originalEndHour: getEventEndHour(event),\n      };\n    });\n};\n\n// Organize all-day events into rows\nexport const organizeAllDayEvents = (\n  currentDayEvents: Event[],\n  comparator?: (a: Event, b: Event) => number\n) => {\n  const allDayEvents = currentDayEvents.filter(e => e.allDay);\n\n  if (comparator) {\n    allDayEvents.sort(comparator);\n  } else {\n    // Default: group by calendar (first-seen order), then preserve load order\n    const calendarOrder = new Map<string | undefined, number>();\n    allDayEvents.forEach(e => {\n      if (!calendarOrder.has(e.calendarId))\n        calendarOrder.set(e.calendarId, calendarOrder.size);\n    });\n    allDayEvents.sort(\n      createAllDayDisplayComparator(\n        allDayEvents,\n        (left, right) =>\n          (calendarOrder.get(left.calendarId) ?? 0) -\n          (calendarOrder.get(right.calendarId) ?? 0)\n      )\n    );\n  }\n\n  const rows: Event[][] = [];\n  const eventsWithRow: Array<Event & { row: number }> = [];\n\n  allDayEvents.forEach(event => {\n    let rowIndex = 0;\n    let placed = false;\n\n    while (!placed) {\n      if (rows[rowIndex]) {\n        const hasCollision = rows[rowIndex].some(existing => {\n          const aStart = temporalToDate(event.start);\n          const aEnd = temporalToDate(event.end);\n          const bStart = temporalToDate(existing.start);\n          const bEnd = temporalToDate(existing.end);\n          return aStart <= bEnd && bStart <= aEnd;\n        });\n\n        if (hasCollision) {\n          rowIndex++;\n        } else {\n          rows[rowIndex].push(event);\n          eventsWithRow.push({ ...event, row: rowIndex });\n          placed = true;\n        }\n      } else {\n        rows[rowIndex] = [event];\n        eventsWithRow.push({ ...event, row: rowIndex });\n        placed = true;\n      }\n    }\n  });\n\n  return eventsWithRow;\n};\n\n// Calculate layout for newly created events\nexport const calculateNewEventLayout = (\n  targetDay: number,\n  startHour: number,\n  endHour: number,\n  currentDate: Date,\n  layoutEvents: Event[],\n  appTimeZone?: string\n): EventLayout | null => {\n  const startDate = new Date(currentDate);\n  const endDate = new Date(currentDate);\n  startDate.setHours(Math.floor(startHour), (startHour % 1) * 60, 0, 0);\n  endDate.setHours(Math.floor(endHour), (endHour % 1) * 60, 0, 0);\n\n  const tempEvent: Event = {\n    id: '-1',\n    title: 'Temp',\n    day: 0,\n    start: dateToZonedDateTime(startDate, appTimeZone),\n    end: dateToZonedDateTime(endDate, appTimeZone),\n    calendarId: 'blue',\n    allDay: false,\n  };\n\n  const dayEvents = [...layoutEvents, tempEvent];\n  const tempLayouts = EventLayoutCalculator.calculateDayEventLayouts(\n    dayEvents,\n    { viewType: 'day' }\n  );\n  return tempLayouts.get('-1') || null;\n};\n\n// Calculate drag layout\nexport const calculateDragLayout = (\n  draggedEvent: Event,\n  targetDay: number,\n  targetStartHour: number,\n  targetEndHour: number,\n  currentDate: Date,\n  layoutEvents: Event[],\n  appTimeZone?: string\n): EventLayout | null => {\n  const otherEvents = layoutEvents.filter(e => e.id !== draggedEvent.id);\n\n  const viewDate = new Date(currentDate);\n  const startD = new Date(viewDate);\n  startD.setHours(\n    Math.floor(targetStartHour),\n    (targetStartHour % 1) * 60,\n    0,\n    0\n  );\n  const endD = new Date(viewDate);\n  endD.setHours(Math.floor(targetEndHour), (targetEndHour % 1) * 60, 0, 0);\n\n  const modifiedDraggedEvent = {\n    ...draggedEvent,\n    start: dateToZonedDateTime(startD, appTimeZone),\n    end: dateToZonedDateTime(endD, appTimeZone),\n    day: 0,\n  };\n\n  const dayEvents = [...otherEvents, modifiedDraggedEvent];\n\n  if (dayEvents.length === 0) return null;\n\n  const tempLayouts = EventLayoutCalculator.calculateDayEventLayouts(\n    dayEvents,\n    { viewType: 'day' }\n  );\n  return tempLayouts.get(draggedEvent.id) || null;\n};\n"
  },
  {
    "path": "packages/core/src/components/eventLayout/calculate/grouping.ts",
    "content": "import { LAYOUT_CONFIG } from '@/components/eventLayout/constants';\nimport { LayoutWeekEvent, ParallelGroup } from '@/components/eventLayout/types';\nimport {\n  eventsOverlap,\n  getStartHour,\n  getEndHour,\n  getOriginalStartHour,\n  getOriginalEndHour,\n} from '@/components/eventLayout/utils';\n\n/**\n * Group overlapping events using BFS\n */\nexport function groupOverlappingEvents(\n  events: LayoutWeekEvent[]\n): LayoutWeekEvent[][] {\n  const groups: LayoutWeekEvent[][] = [];\n  const processed = new Set<string>();\n\n  for (const event of events) {\n    if (processed.has(event.id)) continue;\n\n    const group = [event];\n    const queue = [event];\n    processed.add(event.id);\n\n    while (queue.length > 0) {\n      const current = queue.shift()!;\n\n      for (const otherEvent of events) {\n        if (processed.has(otherEvent.id)) continue;\n\n        if (eventsOverlap(current, otherEvent)) {\n          group.push(otherEvent);\n          queue.push(otherEvent);\n          processed.add(otherEvent.id);\n        }\n      }\n    }\n\n    groups.push(group);\n  }\n\n  return groups;\n}\n\n/**\n * Analyze parallel groups - group by start time\n */\nexport function analyzeParallelGroups(\n  sortedEvents: LayoutWeekEvent[]\n): ParallelGroup[] {\n  const groups: ParallelGroup[] = [];\n  const processed = new Set<string>();\n\n  for (const event of sortedEvents) {\n    if (processed.has(event.id)) continue;\n\n    // Create new parallel group\n    const groupEvents: LayoutWeekEvent[] = [event];\n    processed.add(event.id);\n\n    // Find events with similar start times (within threshold)\n    for (const otherEvent of sortedEvents) {\n      if (processed.has(otherEvent.id)) continue;\n\n      const timeDiff = Math.abs(\n        getOriginalStartHour(event) - getOriginalStartHour(otherEvent)\n      );\n      if (timeDiff <= LAYOUT_CONFIG.PARALLEL_THRESHOLD) {\n        groupEvents.push(otherEvent);\n        processed.add(otherEvent.id);\n      }\n    }\n\n    groupEvents.sort(\n      (a, b) => getOriginalStartHour(a) - getOriginalStartHour(b)\n    );\n\n    const group: ParallelGroup = {\n      events: groupEvents,\n      startHour: Math.min(...groupEvents.map(e => getStartHour(e))),\n      endHour: Math.max(...groupEvents.map(e => getEndHour(e))),\n      originalStartHour: Math.min(\n        ...groupEvents.map(e => getOriginalStartHour(e))\n      ),\n      originalEndHour: Math.max(...groupEvents.map(e => getOriginalEndHour(e))),\n    };\n\n    groups.push(group);\n  }\n  groups.sort((a, b) => {\n    const startA = a.originalStartHour ?? a.startHour;\n    const startB = b.originalStartHour ?? b.startHour;\n    return startA - startB;\n  });\n\n  return groups;\n}\n"
  },
  {
    "path": "packages/core/src/components/eventLayout/calculate/layout.ts",
    "content": "import { LAYOUT_CONFIG } from '@/components/eventLayout/constants';\nimport {\n  LayoutNode,\n  LayoutCalculationParams,\n  LayoutWeekEvent,\n} from '@/components/eventLayout/types';\nimport {\n  getStartHour,\n  getEndHour,\n  shouldBeParallel,\n} from '@/components/eventLayout/utils';\nimport { EventLayout } from '@/types';\n\nfunction getIndentStepPercent(viewType?: 'week' | 'day'): number {\n  return viewType === 'day' ? 0.5 : 2.5;\n}\n\nfunction calculateEventImportance(event: LayoutWeekEvent): number {\n  const duration = getEndHour(event) - getStartHour(event);\n  return Math.max(0.1, Math.min(1.0, duration / 4));\n}\n\nfunction findBranchRootIndent(\n  node: LayoutNode,\n  viewType?: 'week' | 'day'\n): number | null {\n  let current = node;\n  while (current.parent && current.parent.depth > 0) current = current.parent;\n  return current.depth === 1\n    ? current.depth * getIndentStepPercent(viewType)\n    : null;\n}\n\nfunction shouldChildrenBeParallel(childEvents: LayoutWeekEvent[]): boolean {\n  if (childEvents.length < 2) return false;\n  for (let i = 0; i < childEvents.length; i++) {\n    for (let j = i + 1; j < childEvents.length; j++) {\n      if (shouldBeParallel(childEvents[i], childEvents[j], LAYOUT_CONFIG))\n        return true;\n    }\n  }\n  return false;\n}\n\nfunction calculateNodeLayoutWithVirtualParallel(\n  node: LayoutNode,\n  baseLeft: number,\n  availableWidth: number,\n  layoutMap: Map<string, EventLayout>,\n  params: LayoutCalculationParams = {}\n): void {\n  const indentStep = getIndentStepPercent(params.viewType);\n  let finalIndentOffset = node.depth * indentStep;\n\n  if (node.isProcessed) {\n    const branchRootIndent = findBranchRootIndent(node, params.viewType);\n    if (branchRootIndent !== null) finalIndentOffset = branchRootIndent;\n  }\n\n  // Standard indentation for nested events, but remove the \"magic\" offsets\n  // that cause inconsistent alignment across days.\n  const nodeLeft = baseLeft + finalIndentOffset;\n  const usedLeftSpace = finalIndentOffset;\n  let nodeWidth = availableWidth - usedLeftSpace;\n\n  if (nodeLeft + nodeWidth > baseLeft + availableWidth) {\n    nodeWidth = baseLeft + availableWidth - nodeLeft;\n  }\n\n  layoutMap.set(node.event.id, {\n    id: node.event.id,\n    left: nodeLeft,\n    width: nodeWidth,\n    zIndex: node.depth,\n    level: node.depth,\n    isPrimary: node.depth === 0,\n    indentOffset: (finalIndentOffset * (params.containerWidth || 320)) / 100,\n    importance: calculateEventImportance(node.event),\n  });\n\n  if (node.children.length === 0) return;\n\n  const sortedChildren = [...node.children].toSorted(\n    (a, b) =>\n      getEndHour(b.event) -\n      getStartHour(b.event) -\n      (getEndHour(a.event) - getStartHour(a.event))\n  );\n\n  if (sortedChildren.length === 1) {\n    calculateNodeLayoutWithVirtualParallel(\n      sortedChildren[0],\n      nodeLeft,\n      nodeWidth,\n      layoutMap,\n      params\n    );\n  } else if (shouldChildrenBeParallel(sortedChildren.map(c => c.event))) {\n    // eslint-disable-next-line @typescript-eslint/no-use-before-define\n    calculateParallelChildrenLayout(\n      sortedChildren,\n      nodeLeft,\n      nodeWidth,\n      layoutMap,\n      params\n    );\n  } else {\n    sortedChildren.forEach(child =>\n      calculateNodeLayoutWithVirtualParallel(\n        child,\n        nodeLeft,\n        nodeWidth,\n        layoutMap,\n        params\n      )\n    );\n  }\n}\n\nfunction calculateParallelChildrenLayout(\n  children: LayoutNode[],\n  parentLeft: number,\n  parentWidth: number,\n  layoutMap: Map<string, EventLayout>,\n  params: LayoutCalculationParams = {}\n): void {\n  const childCount = children.length;\n  const firstChildDepth = children[0].depth;\n  const indentStep = getIndentStepPercent(params.viewType);\n  const childIndentOffset = firstChildDepth * indentStep;\n\n  const childrenStartLeft = parentLeft + childIndentOffset;\n  const usedLeftSpace = childIndentOffset;\n  const childrenAvailableWidth = parentWidth - usedLeftSpace;\n\n  if (childrenAvailableWidth <= 0) {\n    children.forEach(child =>\n      calculateNodeLayoutWithVirtualParallel(\n        child,\n        parentLeft,\n        parentWidth,\n        layoutMap,\n        params\n      )\n    );\n    return;\n  }\n\n  let adjustedMargin = LAYOUT_CONFIG.MARGIN_BETWEEN;\n  const childWidth =\n    (childrenAvailableWidth - adjustedMargin * (childCount - 1)) / childCount;\n\n  children.forEach((child, index) => {\n    const childLeft = childrenStartLeft + index * (childWidth + adjustedMargin);\n    layoutMap.set(child.event.id, {\n      id: child.event.id,\n      left: childLeft,\n      width: childWidth,\n      zIndex: child.depth,\n      level: child.depth,\n      isPrimary: child.depth === 0,\n      indentOffset: (childIndentOffset * (params.containerWidth || 320)) / 100,\n      importance: calculateEventImportance(child.event),\n    });\n\n    if (child.children.length > 0) {\n      const sorted = [...child.children].toSorted(\n        (a, b) =>\n          getEndHour(b.event) -\n          getStartHour(b.event) -\n          (getEndHour(a.event) - getStartHour(a.event))\n      );\n      if (sorted.length === 1) {\n        calculateNodeLayoutWithVirtualParallel(\n          sorted[0],\n          childLeft,\n          childWidth,\n          layoutMap,\n          params\n        );\n      } else if (shouldChildrenBeParallel(sorted.map(c => c.event))) {\n        calculateParallelChildrenLayout(\n          sorted,\n          childLeft,\n          childWidth,\n          layoutMap,\n          params\n        );\n      } else {\n        sorted.forEach(gc =>\n          calculateNodeLayoutWithVirtualParallel(\n            gc,\n            childLeft,\n            childWidth,\n            layoutMap,\n            params\n          )\n        );\n      }\n    }\n  });\n}\n\n/**\n * Calculate layout from nested structure\n */\nexport function calculateLayoutFromStructure(\n  rootNodes: LayoutNode[],\n  layoutMap: Map<string, EventLayout>,\n  params: LayoutCalculationParams = {}\n): void {\n  const edgeMargin =\n    params.viewType === 'day' ? 0 : LAYOUT_CONFIG.EDGE_MARGIN_PERCENT;\n  const totalWidth = 100 - edgeMargin;\n\n  if (rootNodes.length === 1) {\n    calculateNodeLayoutWithVirtualParallel(\n      rootNodes[0],\n      0,\n      totalWidth,\n      layoutMap,\n      params\n    );\n  } else if (rootNodes.length > 1) {\n    const nodeCount = rootNodes.length;\n    const totalMargin = LAYOUT_CONFIG.MARGIN_BETWEEN * (nodeCount - 1);\n    const nodeWidth = (totalWidth - totalMargin) / nodeCount;\n\n    rootNodes.forEach((node, index) => {\n      const left = index * (nodeWidth + LAYOUT_CONFIG.MARGIN_BETWEEN);\n      calculateNodeLayoutWithVirtualParallel(\n        node,\n        left,\n        Math.max(nodeWidth, LAYOUT_CONFIG.MIN_WIDTH),\n        layoutMap,\n        params\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "packages/core/src/components/eventLayout/calculate/rebalance.ts",
    "content": "import {\n  LayoutWeekEvent,\n  ParallelGroup,\n  LayoutNode,\n} from '@/components/eventLayout/types';\nimport { canEventContain } from '@/components/eventLayout/utils';\n\nfunction countDescendants(node: LayoutNode): number {\n  let count = 0;\n  for (const child of node.children) {\n    count += 1 + countDescendants(child);\n  }\n  return count;\n}\n\nfunction calculateParentLoads(\n  groupNodes: LayoutNode[],\n  allNodes: LayoutNode[]\n): Array<{ node: LayoutNode; load: number }> {\n  const parentLevel = groupNodes[0]?.parent?.depth;\n  if (parentLevel === undefined) return [];\n\n  return allNodes\n    .filter(node => node.depth === parentLevel)\n    .map(node => ({ node, load: countDescendants(node) }))\n    .toSorted((a, b) => b.load - a.load);\n}\n\nfunction needsRebalancing(\n  parentLoads: Array<{ node: LayoutNode; load: number }>\n): boolean {\n  if (parentLoads.length < 2) return false;\n  return parentLoads[0].load - parentLoads.at(-1)!.load >= 2;\n}\n\nfunction setParentChildRelation(\n  parent: LayoutWeekEvent,\n  child: LayoutWeekEvent\n): void {\n  child.parentId = parent.id;\n  if (!parent.children.includes(child.id)) {\n    parent.children.push(child.id);\n  }\n}\n\nfunction transferNode(leafNode: LayoutNode, newParent: LayoutNode): void {\n  if (leafNode.parent) {\n    leafNode.parent.children = leafNode.parent.children.filter(\n      c => c !== leafNode\n    );\n  }\n\n  const shouldNestUnder = newParent.children.find(c =>\n    canEventContain(c.event, leafNode.event)\n  );\n\n  if (shouldNestUnder) {\n    leafNode.parent = shouldNestUnder;\n    leafNode.depth = shouldNestUnder.depth + 1;\n    shouldNestUnder.children.push(leafNode);\n    setParentChildRelation(shouldNestUnder.event, leafNode.event);\n  } else {\n    leafNode.parent = newParent;\n    leafNode.depth = newParent.depth + 1;\n    newParent.children.push(leafNode);\n    setParentChildRelation(newParent.event, leafNode.event);\n  }\n}\n\nfunction collectLeaves(node: LayoutNode, leaves: LayoutNode[]): void {\n  if (node.children.length === 0) {\n    leaves.push(node);\n  } else {\n    node.children.forEach(child => collectLeaves(child, leaves));\n  }\n}\n\nfunction findTransferableLeaf(\n  heavyRoot: LayoutNode,\n  lightRoot: LayoutNode\n): LayoutNode | null {\n  const leaves: LayoutNode[] = [];\n  collectLeaves(heavyRoot, leaves);\n  return (\n    leaves.find(leaf => canEventContain(lightRoot.event, leaf.event)) ||\n    leaves[0] ||\n    null\n  );\n}\n\nfunction rebalanceGroupLoad(\n  parentLoads: Array<{ node: LayoutNode; load: number }>\n): void {\n  const maxIterations = 5;\n  let iteration = 0;\n\n  while (iteration < maxIterations) {\n    parentLoads.sort((a, b) => b.load - a.load);\n    const heaviest = parentLoads[0];\n    const lightest = parentLoads.at(-1)!;\n\n    if (heaviest.load - lightest.load < 2) break;\n\n    const transferableLeaf = findTransferableLeaf(heaviest.node, lightest.node);\n    if (transferableLeaf) {\n      transferNode(transferableLeaf, lightest.node);\n      heaviest.load--;\n      lightest.load++;\n      iteration++;\n    } else {\n      break;\n    }\n  }\n}\n\nexport function rebalanceLoadByGroups(\n  parallelGroups: ParallelGroup[],\n  allNodes: LayoutNode[]\n): void {\n  for (let i = parallelGroups.length - 1; i >= 1; i--) {\n    const groupNodes = parallelGroups[i].events.map(\n      e => allNodes.find(node => node.event.id === e.id)!\n    );\n\n    const parentLoads = calculateParentLoads(groupNodes, allNodes);\n    if (needsRebalancing(parentLoads)) {\n      rebalanceGroupLoad(parentLoads);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/components/eventLayout/calculate/structure.ts",
    "content": "import { LAYOUT_CONFIG } from '@/components/eventLayout/constants';\nimport {\n  LayoutWeekEvent,\n  ParallelGroup,\n  LayoutNode,\n} from '@/components/eventLayout/types';\nimport {\n  canEventContain,\n  eventsOverlap,\n  getOriginalStartHour,\n  getOriginalEndHour,\n  shouldBeParallel,\n} from '@/components/eventLayout/utils';\n\nimport { rebalanceLoadByGroups } from './rebalance';\n\nfunction checkLoadBalanceParallel(\n  parentGroup: ParallelGroup,\n  childGroup: ParallelGroup\n): boolean {\n  for (const parentEvent of parentGroup.events) {\n    for (const childEvent of childGroup.events) {\n      if (!eventsOverlap(parentEvent, childEvent)) continue;\n\n      const timeDiff = Math.abs(\n        getOriginalStartHour(childEvent) - getOriginalStartHour(parentEvent)\n      );\n      if (timeDiff < LAYOUT_CONFIG.NESTED_THRESHOLD) return true;\n    }\n  }\n  return false;\n}\n\nexport function canGroupContain(\n  parentGroup: ParallelGroup,\n  childGroup: ParallelGroup\n): boolean {\n  const timeDiff =\n    (childGroup.originalStartHour ?? childGroup.startHour) -\n    (parentGroup.originalStartHour ?? parentGroup.startHour);\n\n  // Check load balance parallel\n  if (checkLoadBalanceParallel(parentGroup, childGroup)) {\n    return false;\n  }\n\n  if (timeDiff < LAYOUT_CONFIG.NESTED_THRESHOLD) {\n    return false;\n  }\n\n  for (const parentEvent of parentGroup.events) {\n    for (const childEvent of childGroup.events) {\n      if (canEventContain(parentEvent, childEvent)) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n}\n\nexport function findBestParentInGroup(\n  childEvent: LayoutWeekEvent,\n  parentGroup: ParallelGroup,\n  allEvents: LayoutWeekEvent[]\n): LayoutWeekEvent | null {\n  const validParents = parentGroup.events.filter(p =>\n    canEventContain(p, childEvent)\n  );\n  if (validParents.length === 0) return null;\n  if (validParents.length === 1) return validParents[0];\n\n  const parentLoads = validParents.map(parent => ({\n    parent,\n    load: parent.children.length,\n    hasParallelSibling: parent.children.some(id => {\n      const sibling = allEvents.find(e => e.id === id);\n      return sibling && shouldBeParallel(childEvent, sibling, LAYOUT_CONFIG);\n    }),\n  }));\n\n  parentLoads.sort((a, b) => {\n    if (a.load !== b.load) return a.load - b.load;\n    if (a.hasParallelSibling !== b.hasParallelSibling)\n      return a.hasParallelSibling ? -1 : 1;\n    return (\n      Math.abs(\n        getOriginalStartHour(childEvent) - getOriginalStartHour(a.parent)\n      ) -\n      Math.abs(\n        getOriginalStartHour(childEvent) - getOriginalStartHour(b.parent)\n      )\n    );\n  });\n\n  return parentLoads[0].parent;\n}\n\nfunction findParentWithMinLoad(\n  currentChild: LayoutWeekEvent,\n  validParents: LayoutWeekEvent[],\n  children: LayoutWeekEvent[]\n): LayoutWeekEvent | null {\n  if (validParents.length === 0) return null;\n\n  let minLoad = Infinity;\n  let candidates: LayoutWeekEvent[] = [];\n  for (const parent of validParents) {\n    const load = parent.children.length;\n    if (load < minLoad) {\n      minLoad = load;\n      candidates = [parent];\n    } else if (load === minLoad) {\n      candidates.push(parent);\n    }\n  }\n\n  const currentDuration =\n    getOriginalEndHour(currentChild) - getOriginalStartHour(currentChild);\n  const loadedChildrenIds = candidates.flatMap(p => p.children);\n\n  const isLongest =\n    currentDuration >\n    Math.max(\n      ...loadedChildrenIds.map(id => {\n        const child = children.find(e => e.id === id);\n        return child\n          ? getOriginalEndHour(child) - getOriginalStartHour(child)\n          : 0;\n      }),\n      0\n    );\n\n  return isLongest ? candidates[0] : candidates.at(-1) || null;\n}\n\nfunction setRelation(parent: LayoutWeekEvent, child: LayoutWeekEvent) {\n  child.parentId = parent.id;\n  if (!parent.children.includes(child.id)) {\n    parent.children.push(child.id);\n  }\n}\n\nfunction countDescendants(node: LayoutNode): number {\n  let count = 0;\n  for (const child of node.children) {\n    count += 1 + countDescendants(child);\n  }\n  return count;\n}\n\nfunction buildTempNodeMap(\n  allEvents: LayoutWeekEvent[]\n): Map<string, LayoutNode> {\n  const nodeMap = new Map<string, LayoutNode>();\n\n  for (const event of allEvents) {\n    const node: LayoutNode = {\n      event,\n      children: [],\n      parent: null,\n      depth: 0,\n      isProcessed: false,\n    };\n    nodeMap.set(event.id, node);\n  }\n\n  for (const event of allEvents) {\n    if (event.parentId) {\n      const childNode = nodeMap.get(event.id);\n      const parentNode = nodeMap.get(event.parentId);\n      if (childNode && parentNode) {\n        childNode.parent = parentNode;\n        childNode.depth = parentNode.depth + 1;\n        parentNode.children.push(childNode);\n      }\n    }\n  }\n\n  return nodeMap;\n}\n\nfunction findAlternateBranchRoot(\n  event: LayoutWeekEvent,\n  nodeMap: Map<string, LayoutNode>\n): LayoutWeekEvent | null {\n  const eventNode = nodeMap.get(event.id);\n  if (!eventNode) return null;\n\n  let currentBranchRoot = eventNode;\n  while (currentBranchRoot.parent && currentBranchRoot.depth > 1) {\n    currentBranchRoot = currentBranchRoot.parent;\n  }\n\n  if (currentBranchRoot.depth !== 1) return null;\n\n  const rootNode = currentBranchRoot.parent;\n  if (!rootNode) return null;\n\n  const alternateBranches = rootNode.children.filter(\n    child => child.depth === 1 && child.event.id !== currentBranchRoot.event.id\n  );\n\n  let minLoad = Infinity;\n  let selectedBranch: LayoutNode | null = null;\n\n  for (const branch of alternateBranches) {\n    const load = countDescendants(branch);\n    if (load < minLoad) {\n      minLoad = load;\n      selectedBranch = branch;\n    }\n  }\n\n  return selectedBranch ? selectedBranch.event : null;\n}\n\nexport function optimizeChildAssignments(\n  childEvents: LayoutWeekEvent[],\n  parentGroup: ParallelGroup,\n  allEvents: LayoutWeekEvent[]\n): Array<{ child: LayoutWeekEvent; parent: LayoutWeekEvent }> {\n  const assignments: Array<{\n    child: LayoutWeekEvent;\n    parent: LayoutWeekEvent;\n  }> = [];\n\n  if (childEvents.length === 1) {\n    const parent = findBestParentInGroup(\n      childEvents[0],\n      parentGroup,\n      allEvents\n    );\n    if (parent) {\n      assignments.push({ child: childEvents[0], parent });\n      setRelation(parent, childEvents[0]);\n    }\n    return assignments;\n  }\n\n  const validParents = parentGroup.events.filter(parent =>\n    childEvents.every(child => canEventContain(parent, child))\n  );\n\n  if (validParents.length === 0) {\n    for (const child of childEvents) {\n      const parent = findBestParentInGroup(child, parentGroup, allEvents);\n      if (parent) {\n        assignments.push({ child, parent });\n        setRelation(parent, child);\n      } else {\n        // Find sibling events that overlap with current event\n        const siblingEvent = childEvents.find(\n          e => e.id !== child.id && eventsOverlap(e, child)\n        );\n        if (siblingEvent) {\n          const tempNodeMap = buildTempNodeMap(allEvents);\n          const alternateBranchRoot = findAlternateBranchRoot(\n            siblingEvent,\n            tempNodeMap\n          );\n\n          if (alternateBranchRoot) {\n            assignments.push({ child, parent: alternateBranchRoot });\n            setRelation(alternateBranchRoot, child);\n          }\n        }\n      }\n    }\n    return assignments;\n  }\n\n  const sortedChildren = [...childEvents].toSorted(\n    (a, b) =>\n      getOriginalEndHour(b) -\n      getOriginalStartHour(b) -\n      (getOriginalEndHour(a) - getOriginalStartHour(a))\n  );\n\n  if (sortedChildren.length % validParents.length === 0) {\n    const childrenPerParent = sortedChildren.length / validParents.length;\n    for (let i = 0; i < validParents.length; i++) {\n      const parent = validParents[i];\n      const childrenForParent = sortedChildren.slice(\n        i * childrenPerParent,\n        (i + 1) * childrenPerParent\n      );\n      for (const child of childrenForParent) {\n        assignments.push({ child, parent });\n        setRelation(parent, child);\n      }\n    }\n  } else {\n    for (const child of sortedChildren) {\n      const parent = findParentWithMinLoad(child, validParents, sortedChildren);\n      if (parent) {\n        assignments.push({ child, parent });\n        setRelation(parent, child);\n      }\n    }\n  }\n\n  return assignments;\n}\n\n/**\n * Build nested structure for overlapping events\n */\nexport function buildNestedStructure(\n  parallelGroups: ParallelGroup[],\n  allEvents: LayoutWeekEvent[]\n): LayoutNode[] {\n  const allNodes: LayoutNode[] = [];\n  const nodeMap = new Map<string, LayoutNode>();\n\n  const eventMap = new Map<string, LayoutWeekEvent>();\n  allEvents.forEach(event => eventMap.set(event.id, event));\n\n  // Create nodes\n  for (const group of parallelGroups) {\n    for (const event of group.events) {\n      const node: LayoutNode = {\n        event: eventMap.get(event.id)!,\n        children: [],\n        parent: null,\n        depth: 0,\n        isProcessed: false,\n      };\n      allNodes.push(node);\n      nodeMap.set(event.id, node);\n    }\n  }\n\n  // Establish parent-child relationships\n  for (let i = 0; i < parallelGroups.length; i++) {\n    const currentGroup = parallelGroups[i];\n    const currentGroupEvents = currentGroup.events.map(\n      e => eventMap.get(e.id)!\n    );\n\n    let foundParent = false;\n    for (let j = i - 1; j >= 0 && !foundParent; j--) {\n      const potentialParentGroup = parallelGroups[j];\n      const potentialParentGroupEvents = potentialParentGroup.events.map(\n        e => eventMap.get(e.id)!\n      );\n      const potentialParentGroupMapped: ParallelGroup = {\n        events: potentialParentGroupEvents,\n        startHour: potentialParentGroup.startHour,\n        endHour: potentialParentGroup.endHour,\n      };\n\n      if (\n        canGroupContain(potentialParentGroupMapped, {\n          events: currentGroupEvents,\n          startHour: currentGroup.startHour,\n          endHour: currentGroup.endHour,\n        })\n      ) {\n        const childAssignments = optimizeChildAssignments(\n          currentGroupEvents,\n          potentialParentGroupMapped,\n          allEvents\n        );\n\n        for (const assignment of childAssignments) {\n          const childNode = nodeMap.get(assignment.child.id)!;\n          const parentNode = nodeMap.get(assignment.parent.id)!;\n\n          childNode.parent = parentNode;\n          childNode.depth = parentNode.depth + 1;\n          parentNode.children.push(childNode);\n        }\n        foundParent = true;\n      }\n    }\n  }\n\n  const rootNodes = allNodes.filter(node => node.parent === null);\n  rootNodes.forEach(rootNode => {\n    rootNode.depth = 0;\n  });\n\n  // Load balancing\n  rebalanceLoadByGroups(parallelGroups, allNodes);\n\n  return rootNodes;\n}\n"
  },
  {
    "path": "packages/core/src/components/eventLayout/constants.ts",
    "content": "export const LAYOUT_CONFIG = {\n  PARALLEL_THRESHOLD: 0.25, // 15 minutes, parallel layout threshold\n  NESTED_THRESHOLD: 0.5, // 30 minutes, nested layout threshold\n  INDENT_STEP_PERCENT: 2.5, // Indent step percentage (replaces pixel values)\n  MIN_WIDTH: 25, // Minimum width percentage\n  MARGIN_BETWEEN: 1, // Margin between parallel events percentage\n  EDGE_MARGIN_PERCENT: 0.9, // Edge margin percentage (replaces pixel value calculation)\n} as const;\n"
  },
  {
    "path": "packages/core/src/components/eventLayout/index.tsx",
    "content": "import { EventLayout, Event } from '@/types';\n\nimport {\n  groupOverlappingEvents,\n  analyzeParallelGroups,\n} from './calculate/grouping';\nimport { calculateLayoutFromStructure } from './calculate/layout';\nimport { buildNestedStructure } from './calculate/structure';\nimport { LAYOUT_CONFIG } from './constants';\nimport { LayoutCalculationParams } from './types';\nimport { toLayoutEvent } from './utils';\n\nexport const EventLayoutCalculator = {\n  /**\n   * Calculate layout for all events in a day\n   * @param dayEvents Array of events for the day\n   * @param params Layout calculation parameters\n   */\n  calculateDayEventLayouts(\n    dayEvents: Event[],\n    params: LayoutCalculationParams = {}\n  ): Map<string, EventLayout> {\n    // 1. Convert to layout events\n    const layoutEvents = dayEvents.map(toLayoutEvent);\n\n    // Clear parent-child relationships (ensure clean start)\n    for (const event of layoutEvents) {\n      event.parentId = undefined;\n      event.children = [];\n    }\n\n    const layoutMap = new Map<string, EventLayout>();\n    const regularEvents = layoutEvents.filter(e => !e.allDay);\n\n    if (regularEvents.length === 0) {\n      return layoutMap;\n    }\n\n    // 2. Group overlapping events\n    const overlappingGroups = groupOverlappingEvents(regularEvents);\n\n    // 3. Calculate layout for each group\n    for (const group of overlappingGroups) {\n      if (group.length === 1) {\n        const edgeMargin =\n          params.viewType === 'day' ? 0 : LAYOUT_CONFIG.EDGE_MARGIN_PERCENT;\n        layoutMap.set(group[0].id, {\n          id: group[0].id,\n          left: 0,\n          width: 100 - edgeMargin,\n          zIndex: 0,\n          level: 0,\n          isPrimary: true,\n          indentOffset: 0,\n          importance: Math.max(\n            0.1,\n            Math.min(1.0, (group[0]._endHour! - group[0]._startHour!) / 4)\n          ),\n        });\n      } else {\n        // Complex layout\n        const sortedEvents = [...group].toSorted((a, b) => {\n          // Use original start hour if available for cross-day stability,\n          // otherwise fall back to the segment's start hour.\n          const startA = a._originalStartHour ?? a._startHour!;\n          const startB = b._originalStartHour ?? b._startHour!;\n\n          if (startA !== startB) return startA - startB;\n\n          // Tie-breaker: use original duration if available\n          const durA = (a._originalEndHour ?? a._endHour!) - startA;\n          const durB = (b._originalEndHour ?? b._endHour!) - startB;\n\n          if (durA !== durB) return durB - durA;\n\n          // Final fallback to segment start/end if still tied (e.g. same original times)\n          if (a._startHour! !== b._startHour!)\n            return a._startHour! - b._startHour!;\n          return b._endHour! - b._startHour! - (a._endHour! - a._startHour!);\n        });\n\n        const parallelGroups = analyzeParallelGroups(sortedEvents);\n        const rootNodes = buildNestedStructure(parallelGroups, group);\n        calculateLayoutFromStructure(rootNodes, layoutMap, params);\n      }\n    }\n\n    return layoutMap;\n  },\n};\n"
  },
  {
    "path": "packages/core/src/components/eventLayout/types.ts",
    "content": "import { Event } from '@/types';\n\nexport interface LayoutWeekEvent extends Event {\n  parentId?: string;\n  children: string[];\n  // Cached hour values to avoid repeated calculations\n  _startHour?: number;\n  _endHour?: number;\n}\n\nexport interface LayoutNode {\n  event: LayoutWeekEvent;\n  children: LayoutNode[];\n  parent: LayoutNode | null;\n  depth: number;\n  isProcessed: boolean; // Mark cross-branch parallel nodes\n}\n\nexport interface ParallelGroup {\n  events: LayoutWeekEvent[];\n  startHour: number;\n  endHour: number;\n  originalStartHour?: number;\n  originalEndHour?: number;\n}\n\nexport interface LayoutCalculationParams {\n  containerWidth?: number; // Optional container width for scenarios requiring pixel-precise calculations\n  viewType?: 'week' | 'day'; // View type for adjusting indent step size\n}\n"
  },
  {
    "path": "packages/core/src/components/eventLayout/utils.ts",
    "content": "import { Event } from '@/types';\nimport { extractHourFromDate, getEventEndHour } from '@/utils/helpers';\n\nimport { LayoutWeekEvent } from './types';\n\nexport function toLayoutEvent(event: Event): LayoutWeekEvent {\n  return {\n    ...event,\n    parentId: undefined,\n    children: [],\n    // Only calculate hour values for non-all-day events\n    _startHour: event.allDay ? 0 : extractHourFromDate(event.start),\n    _endHour: event.allDay ? 0 : getEventEndHour(event),\n  };\n}\n\nexport function getStartHour(event: LayoutWeekEvent): number {\n  return event._startHour ?? extractHourFromDate(event.start);\n}\n\nexport function getEndHour(event: LayoutWeekEvent): number {\n  return event._endHour ?? getEventEndHour(event);\n}\n\nexport function getOriginalStartHour(event: LayoutWeekEvent): number {\n  return event._originalStartHour ?? getStartHour(event);\n}\n\nexport function getOriginalEndHour(event: LayoutWeekEvent): number {\n  return event._originalEndHour ?? getEndHour(event);\n}\n\n/**\n * Check if two events overlap\n */\nexport function eventsOverlap(\n  event1: LayoutWeekEvent,\n  event2: LayoutWeekEvent\n): boolean {\n  if (event1.day !== event2.day || event1.allDay || event2.allDay) return false;\n  return (\n    getStartHour(event1) < getEndHour(event2) &&\n    getStartHour(event2) < getEndHour(event1)\n  );\n}\n\n/**\n * Check one-way parallel relationship of extended events\n * @param extendedEvent Potential extended event\n * @param otherEvent Other event\n */\nexport function isExtendedEventParallel(\n  extendedEvent: LayoutWeekEvent,\n  otherEvent: LayoutWeekEvent\n): boolean {\n  const duration = getEndHour(extendedEvent) - getStartHour(extendedEvent);\n\n  // Only consider as extended event if duration exceeds 1.25 hours\n  if (duration < 1.25) return false;\n\n  // Calculate the start time of extended event's \"latter half\" (latter half starts at 40% position)\n  const lateStartThreshold = getStartHour(extendedEvent) + duration * 0.4;\n\n  // Other event must start in the latter half and overlap with extended event\n  const isInLateStart = getStartHour(otherEvent) >= lateStartThreshold;\n  const hasOverlap = eventsOverlap(extendedEvent, otherEvent);\n\n  return isInLateStart && hasOverlap;\n}\n\n/**\n * Check parallel relationship of extended events\n */\nexport function checkExtendedEventParallel(\n  event1: LayoutWeekEvent,\n  event2: LayoutWeekEvent\n): boolean {\n  // Ensure events overlap\n  if (!eventsOverlap(event1, event2)) return false;\n\n  // Check if event1 is an extended event, event2 starts in its latter half\n  if (isExtendedEventParallel(event1, event2)) {\n    return true;\n  }\n\n  // Check if event2 is an extended event, event1 starts in its latter half\n  if (isExtendedEventParallel(event2, event1)) {\n    return true;\n  }\n\n  return false;\n}\n\nimport { LAYOUT_CONFIG } from './constants';\n\n/**\n * Check if two events should be displayed in parallel\n */\nexport function shouldBeParallel(\n  event1: LayoutWeekEvent,\n  event2: LayoutWeekEvent,\n  config: typeof LAYOUT_CONFIG = LAYOUT_CONFIG\n): boolean {\n  if (!eventsOverlap(event1, event2)) return false;\n\n  const startTimeDiff = Math.abs(\n    getOriginalStartHour(event1) - getOriginalStartHour(event2)\n  );\n\n  // Strictly within threshold, directly parallel\n  if (startTimeDiff <= config.PARALLEL_THRESHOLD) {\n    return true;\n  }\n\n  // Between threshold and nested threshold, consider load balancing\n  if (\n    startTimeDiff > config.PARALLEL_THRESHOLD &&\n    startTimeDiff < config.NESTED_THRESHOLD\n  ) {\n    return true; // For load balancing, also consider as parallel\n  }\n\n  // Check if one is an extended event\n  return checkExtendedEventParallel(event1, event2);\n}\n\n/**\n * Check if parent event can contain child event\n */\nexport function canEventContain(\n  parent: LayoutWeekEvent,\n  child: LayoutWeekEvent\n): boolean {\n  const strictContain =\n    getOriginalStartHour(parent) <= getOriginalStartHour(child) &&\n    getOriginalEndHour(parent) >= getOriginalEndHour(child);\n  const overlapNesting =\n    getOriginalStartHour(parent) <= getOriginalStartHour(child) &&\n    getOriginalStartHour(child) < getOriginalEndHour(parent) &&\n    eventsOverlap(parent, child);\n\n  return strictContain || overlapNesting;\n}\n"
  },
  {
    "path": "packages/core/src/components/mobileEventDrawer/DefaultMobileEventDrawer.tsx",
    "content": "import { JSX } from 'preact';\nimport { createPortal } from 'preact/compat';\nimport { useState, useEffect, useMemo } from 'preact/hooks';\n\nimport {\n  CalendarPicker,\n  CalendarOption,\n} from '@/components/common/CalendarPicker';\nimport { MiniCalendar } from '@/components/common/MiniCalendar';\nimport { useLocale } from '@/locale';\nimport {\n  Event as CalendarEvent,\n  MobileEventProps,\n  CalendarType,\n} from '@/types';\nimport {\n  formatTime,\n  isEventDeepEqual,\n  restoreVisualEventToCanonical,\n} from '@/utils';\nimport { temporalToDate, dateToZonedDateTime } from '@/utils/temporal';\nimport { dateToPlainDate } from '@/utils/temporalTypeGuards';\n\nimport { Switch } from './components/Switch';\nimport { TimePickerWheel } from './components/TimePickerWheel';\n\nexport const MobileEventDrawer = ({\n  isOpen,\n  onClose,\n  onSave,\n  onEventDelete,\n  draftEvent,\n  app,\n  timeFormat = '24h',\n}: MobileEventProps) => {\n  const { locale, t } = useLocale();\n  const readOnlyConfig = app.getReadOnlyConfig(draftEvent?.id) as {\n    draggable: boolean;\n    viewable: boolean;\n  };\n  const isEditable = app.canMutateFromUI(draftEvent?.id);\n  const isViewable = readOnlyConfig.viewable !== false;\n\n  const [notes, setNotes] = useState('');\n\n  // Check if it's a subscribed calendar\n  const isSubscribed = useMemo(() => {\n    if (!draftEvent?.calendarId) return false;\n    const calendar = app.getCalendarRegistry().get(draftEvent.calendarId);\n    return !!calendar?.subscription;\n  }, [app, draftEvent?.calendarId]);\n\n  // If subscribed calendar and no notes, hide notes field\n  const shouldShowNotes = !isSubscribed || notes.trim() !== '';\n\n  const [title, setTitle] = useState('');\n  const [calendarId, setCalendarId] = useState('');\n  const [isAllDay, setIsAllDay] = useState(false);\n  const [startDate, setStartDate] = useState(new Date());\n  const [endDate, setEndDate] = useState(new Date());\n\n  // Independent visible month states for date pickers\n  const [startVisibleMonth, setStartVisibleMonth] = useState(new Date());\n  const [endVisibleMonth, setEndVisibleMonth] = useState(new Date());\n\n  // Expand states\n  const [expandedPicker, setExpandedPicker] = useState<\n    'start-date' | 'start-time' | 'end-date' | 'end-time' | null\n  >(null);\n\n  // Animation states\n  const [isVisible, setIsVisible] = useState(isOpen);\n  const [isClosing, setIsClosing] = useState(false);\n\n  // Persist isEditing state to avoid flickering during exit animation\n  const [isEditing, setIsEditing] = useState(false);\n\n  useEffect(() => {\n    if (isOpen) {\n      setIsVisible(true);\n      setIsClosing(false);\n    } else {\n      setIsClosing(true);\n      const timer = setTimeout(() => {\n        setIsVisible(false);\n        setIsClosing(false);\n      }, 300); // Match CSS animation duration\n      return () => clearTimeout(timer);\n    }\n  }, [isOpen]);\n\n  useEffect(() => {\n    if (isOpen && draftEvent) {\n      const editing = app.getEvents().some(e => e.id === draftEvent.id);\n      setIsEditing(editing);\n    }\n  }, [isOpen, draftEvent, app]);\n\n  const calendars = app.getCalendars();\n  const calendarOptions: CalendarOption[] = calendars.map(\n    (cal: CalendarType) => ({\n      label: cal.name,\n      value: cal.id,\n    })\n  );\n\n  // Ensure calendarId is valid when calendars change or drawer opens\n  useEffect(() => {\n    if (isOpen && calendars.length > 0) {\n      const isCurrentIdValid = calendars.some(c => c.id === calendarId);\n      // If current ID is 'blue' (fallback) or empty, or simply not in the list (and not a draft event specific ID that might be hidden?),\n      // strictly speaking if it's the fallback 'blue', we should switch.\n      // We also check for '' just in case.\n      if (!isCurrentIdValid && (calendarId === 'blue' || calendarId === '')) {\n        setCalendarId(calendars[0].id);\n      }\n    }\n  }, [calendars, isOpen, calendarId]);\n\n  // Prevent background scroll\n  useEffect(() => {\n    if (isOpen) {\n      const scrollY = window.scrollY;\n      // Robust lock for iOS & preserve scroll position\n      document.body.style.top = `-${scrollY}px`;\n      document.body.style.position = 'fixed';\n      document.body.style.width = '100%';\n      document.body.style.overflow = 'hidden';\n      document.documentElement.style.overflow = 'hidden';\n    } else {\n      const scrollY = document.body.style.top;\n      document.body.style.position = '';\n      document.body.style.top = '';\n      document.body.style.width = '';\n      document.body.style.overflow = '';\n      document.documentElement.style.overflow = '';\n\n      if (scrollY) {\n        window.scrollTo(0, Number.parseInt(scrollY || '0', 10) * -1);\n      }\n    }\n    return () => {\n      const scrollY = document.body.style.top;\n      document.body.style.position = '';\n      document.body.style.top = '';\n      document.body.style.width = '';\n      document.body.style.overflow = '';\n      document.documentElement.style.overflow = '';\n      if (scrollY) {\n        window.scrollTo(0, Number.parseInt(scrollY || '0', 10) * -1);\n      }\n    };\n  }, [isOpen]);\n\n  useEffect(() => {\n    if (isOpen && draftEvent) {\n      setTitle(draftEvent.title || '');\n      setCalendarId(draftEvent.calendarId || calendars[0]?.id || 'blue');\n      setIsAllDay(draftEvent.allDay || false);\n      setNotes(draftEvent.description || '');\n\n      if (draftEvent.start) {\n        try {\n          const start = temporalToDate(draftEvent.start);\n          setStartDate(start);\n          setStartVisibleMonth(start);\n        } catch (e) {\n          console.error('Failed to parse start date', e);\n          const now = new Date();\n          setStartDate(now);\n          setStartVisibleMonth(now);\n        }\n      }\n\n      if (draftEvent.end) {\n        try {\n          const end = temporalToDate(draftEvent.end);\n          setEndDate(end);\n          setEndVisibleMonth(end);\n        } catch (e) {\n          console.error('Failed to parse end date', e);\n          const now = new Date();\n          setEndDate(now);\n          setEndVisibleMonth(now);\n        }\n      }\n    } else if (isOpen && !draftEvent) {\n      // Default init if no draft event (fallback)\n      setCalendarId(calendars[0]?.id || 'blue');\n      setNotes('');\n      const now = new Date();\n      now.setMinutes(0, 0, 0);\n      setStartDate(now);\n      setStartVisibleMonth(now);\n      setEndDate(new Date(now.getTime() + 60 * 60 * 1000));\n      setEndVisibleMonth(now);\n    }\n  }, [isOpen, draftEvent]);\n\n  const hasChanges = useMemo(() => {\n    if (!isOpen || !draftEvent) return false;\n\n    const finalStart = new Date(startDate);\n    const finalEnd = new Date(endDate);\n\n    const currentEvent: CalendarEvent = {\n      ...draftEvent,\n      title,\n      calendarId,\n      allDay: isAllDay,\n      description: notes,\n      start: isAllDay\n        ? dateToPlainDate(finalStart)\n        : dateToZonedDateTime(finalStart, app.timeZone),\n      end: isAllDay\n        ? dateToPlainDate(finalEnd)\n        : dateToZonedDateTime(finalEnd, app.timeZone),\n    };\n\n    return !isEventDeepEqual(\n      draftEvent,\n      restoreVisualEventToCanonical(draftEvent, currentEvent, app.timeZone)\n    );\n  }, [\n    isOpen,\n    draftEvent,\n    title,\n    calendarId,\n    isAllDay,\n    startDate,\n    endDate,\n    notes,\n  ]);\n\n  if (!isVisible || !isViewable) return null;\n\n  const handleSave = () => {\n    if (!draftEvent) return;\n\n    const finalStart = new Date(startDate);\n    const finalEnd = new Date(endDate);\n\n    const updated = {\n      ...draftEvent,\n      title,\n      calendarId,\n      allDay: isAllDay,\n      start: isAllDay\n        ? dateToPlainDate(finalStart)\n        : dateToZonedDateTime(finalStart, app.timeZone),\n      end: isAllDay\n        ? dateToPlainDate(finalEnd)\n        : dateToZonedDateTime(finalEnd, app.timeZone),\n    };\n    onSave(\n      restoreVisualEventToCanonical(\n        draftEvent,\n        updated as CalendarEvent,\n        app.timeZone\n      )\n    );\n  };\n\n  const toggleExpand = (\n    key: 'start-date' | 'start-time' | 'end-date' | 'end-time'\n  ) => {\n    setExpandedPicker(prev => (prev === key ? null : key));\n  };\n\n  const formatDate = (date: Date) =>\n    date.toLocaleDateString(locale, {\n      day: 'numeric',\n      month: 'short',\n      year: 'numeric',\n    });\n\n  const handleDateChange = (type: 'start' | 'end', d: Date) => {\n    if (type === 'start') {\n      const newDate = new Date(d);\n      newDate.setHours(startDate.getHours(), startDate.getMinutes());\n\n      const timeDiff = newDate.getTime() - startDate.getTime();\n      setStartDate(newDate);\n\n      // Sync End Date (maintain duration)\n      const newEndDate = new Date(endDate.getTime() + timeDiff);\n      setEndDate(newEndDate);\n    } else {\n      const newDate = new Date(d);\n      newDate.setHours(endDate.getHours(), endDate.getMinutes());\n      setEndDate(newDate);\n    }\n  };\n\n  const handleStartTimeChange = (newStart: Date) => {\n    const duration = endDate.getTime() - startDate.getTime();\n    setStartDate(newStart);\n    setEndDate(new Date(newStart.getTime() + duration));\n  };\n\n  const handleEndTimeChange = (newEnd: Date) => {\n    // If new end time is before start time, shift start time back to maintain duration\n    if (newEnd < startDate) {\n      const duration = endDate.getTime() - startDate.getTime();\n      setEndDate(newEnd);\n      setStartDate(new Date(newEnd.getTime() - duration));\n    } else {\n      setEndDate(newEnd);\n    }\n  };\n\n  const handleMonthChange = (type: 'start' | 'end', offset: number) => {\n    if (type === 'start') {\n      setStartVisibleMonth(prev => {\n        const next = new Date(prev);\n        next.setMonth(prev.getMonth() + offset);\n        return next;\n      });\n    } else {\n      setEndVisibleMonth(prev => {\n        const next = new Date(prev);\n        next.setMonth(prev.getMonth() + offset);\n        return next;\n      });\n    }\n  };\n\n  return createPortal(\n    <div className='df-portal df-mobile-event-drawer'>\n      <div\n        className='df-mobile-event-drawer-backdrop'\n        data-closing={String(isClosing)}\n        onClick={onClose}\n      />\n\n      <div\n        className={`df-mobile-event-drawer-panel ${isClosing ? 'df-animate-slide-down' : 'df-animate-slide-up'}`}\n        onClick={e => e.stopPropagation()}\n      >\n        <div className='df-mobile-event-drawer-header'>\n          <button\n            type='button'\n            onClick={onClose}\n            className='df-mobile-event-drawer-header-action'\n          >\n            {t('cancel')}\n          </button>\n          <span className='df-mobile-event-drawer-title'>\n            {!isEditable && isEditing\n              ? t('viewEvent')\n              : isEditing\n                ? t('editEvent')\n                : t('newEvent')}\n          </span>\n          {isEditable && (\n            <button\n              type='button'\n              onClick={handleSave}\n              disabled={!hasChanges}\n              className={`df-mobile-event-drawer-header-action df-mobile-event-drawer-header-action-primary ${hasChanges ? '' : 'df-mobile-event-drawer-header-action-disabled'}`}\n            >\n              {isEditing ? t('done') : t('create')}\n            </button>\n          )}\n          {!isEditable && (\n            <span className='df-mobile-event-drawer-header-spacer' />\n          )}\n        </div>\n\n        <div className='df-mobile-event-drawer-body'>\n          <div className='df-mobile-event-drawer-section df-mobile-event-drawer-section-framed'>\n            <input\n              type='text'\n              placeholder={t('titlePlaceholder')}\n              value={title}\n              onChange={(\n                e: JSX.TargetedEvent<HTMLInputElement, globalThis.Event>\n              ) => isEditable && setTitle(e.currentTarget.value)}\n              readOnly={!isEditable}\n              className='df-mobile-event-drawer-title-input'\n              autoFocus={isEditable}\n            />\n          </div>\n\n          {calendars.length > 0 && (\n            <div className='df-mobile-event-drawer-section df-mobile-event-drawer-section-framed'>\n              <div className='df-mobile-event-drawer-row'>\n                <span className='df-mobile-event-drawer-label'>\n                  {t('calendar')}\n                </span>\n                <CalendarPicker\n                  options={calendarOptions}\n                  value={calendarId}\n                  onChange={\n                    isEditable\n                      ? setCalendarId\n                      : () => {\n                          /* noop */\n                        }\n                  }\n                  registry={app.getCalendarRegistry()}\n                  variant='mobile'\n                  disabled={!isEditable}\n                />\n              </div>\n            </div>\n          )}\n\n          <div className='df-mobile-event-drawer-section df-mobile-event-drawer-section-framed'>\n            <div className='df-mobile-event-drawer-row'>\n              <span className='df-mobile-event-drawer-label'>\n                {t('allDay')}\n              </span>\n              <Switch\n                checked={isAllDay}\n                onChange={\n                  isEditable\n                    ? setIsAllDay\n                    : () => {\n                        /* noop */\n                      }\n                }\n                disabled={!isEditable}\n              />\n            </div>\n          </div>\n\n          <div className='df-mobile-event-drawer-section'>\n            <div className='df-mobile-event-drawer-row df-mobile-event-drawer-row-padded'>\n              <span className='df-mobile-event-drawer-label'>\n                {t('starts')}\n              </span>\n              <div className='df-mobile-event-drawer-controls'>\n                <button\n                  type='button'\n                  className='df-mobile-event-drawer-picker-trigger'\n                  data-active={String(expandedPicker === 'start-date')}\n                  onClick={() => isEditable && toggleExpand('start-date')}\n                  disabled={!isEditable}\n                  aria-expanded={expandedPicker === 'start-date'}\n                >\n                  {formatDate(startDate)}\n                </button>\n                {!isAllDay && (\n                  <button\n                    type='button'\n                    className='df-mobile-event-drawer-picker-trigger'\n                    data-active={String(expandedPicker === 'start-time')}\n                    onClick={() => isEditable && toggleExpand('start-time')}\n                    disabled={!isEditable}\n                    aria-expanded={expandedPicker === 'start-time'}\n                  >\n                    {formatTime(\n                      startDate.getHours() + startDate.getMinutes() / 60,\n                      0,\n                      timeFormat\n                    )}\n                  </button>\n                )}\n              </div>\n            </div>\n\n            <div\n              className='df-mobile-event-drawer-expander'\n              data-kind='calendar'\n              data-expanded={String(expandedPicker === 'start-date')}\n            >\n              <div className='df-mobile-event-drawer-expander-content'>\n                <MiniCalendar\n                  currentDate={startDate}\n                  visibleMonth={startVisibleMonth}\n                  onDateSelect={d => handleDateChange('start', d)}\n                  onMonthChange={offset => handleMonthChange('start', offset)}\n                  showHeader\n                  events={app.getEvents()}\n                  calendarRegistry={app.getCalendarRegistry()}\n                  timeZone={app.timeZone}\n                />\n              </div>\n            </div>\n            <div\n              className='df-mobile-event-drawer-expander'\n              data-kind='time'\n              data-expanded={String(expandedPicker === 'start-time')}\n            >\n              <div className='df-mobile-event-drawer-expander-content'>\n                <TimePickerWheel\n                  date={startDate}\n                  onChange={handleStartTimeChange}\n                  timeFormat={timeFormat}\n                />\n              </div>\n            </div>\n          </div>\n\n          <div className='df-mobile-event-drawer-section'>\n            <div className='df-mobile-event-drawer-row df-mobile-event-drawer-row-padded'>\n              <span className='df-mobile-event-drawer-label'>{t('ends')}</span>\n              <div className='df-mobile-event-drawer-controls'>\n                <button\n                  type='button'\n                  className='df-mobile-event-drawer-picker-trigger'\n                  data-active={String(expandedPicker === 'end-date')}\n                  onClick={() => isEditable && toggleExpand('end-date')}\n                  disabled={!isEditable}\n                  aria-expanded={expandedPicker === 'end-date'}\n                >\n                  {formatDate(endDate)}\n                </button>\n                {!isAllDay && (\n                  <button\n                    type='button'\n                    className='df-mobile-event-drawer-picker-trigger'\n                    data-active={String(expandedPicker === 'end-time')}\n                    onClick={() => isEditable && toggleExpand('end-time')}\n                    disabled={!isEditable}\n                    aria-expanded={expandedPicker === 'end-time'}\n                  >\n                    {formatTime(\n                      endDate.getHours() + endDate.getMinutes() / 60,\n                      0,\n                      timeFormat\n                    )}\n                  </button>\n                )}\n              </div>\n            </div>\n\n            <div\n              className='df-mobile-event-drawer-expander'\n              data-kind='calendar'\n              data-expanded={String(expandedPicker === 'end-date')}\n            >\n              <div className='df-mobile-event-drawer-expander-content'>\n                <MiniCalendar\n                  currentDate={endDate}\n                  visibleMonth={endVisibleMonth}\n                  onDateSelect={d => handleDateChange('end', d)}\n                  onMonthChange={offset => handleMonthChange('end', offset)}\n                  showHeader\n                  events={app.getEvents()}\n                  calendarRegistry={app.getCalendarRegistry()}\n                  timeZone={app.timeZone}\n                />\n              </div>\n            </div>\n            <div\n              className='df-mobile-event-drawer-expander'\n              data-kind='time'\n              data-expanded={String(expandedPicker === 'end-time')}\n            >\n              <div className='df-mobile-event-drawer-expander-content'>\n                <TimePickerWheel\n                  date={endDate}\n                  onChange={handleEndTimeChange}\n                  timeFormat={timeFormat}\n                />\n              </div>\n            </div>\n          </div>\n\n          {shouldShowNotes && (\n            <div className='df-mobile-event-drawer-section df-mobile-event-drawer-section-framed'>\n              <textarea\n                placeholder={t('notesPlaceholder')}\n                value={notes}\n                onChange={(\n                  e: JSX.TargetedEvent<HTMLTextAreaElement, globalThis.Event>\n                ) => isEditable && setNotes(e.currentTarget.value)}\n                readOnly={!isEditable}\n                className='df-mobile-event-drawer-notes'\n              />\n            </div>\n          )}\n\n          {isEditable && isEditing && onEventDelete && draftEvent && (\n            <div className='df-mobile-event-drawer-section df-mobile-event-drawer-section-danger'>\n              <button\n                type='button'\n                onClick={() => onEventDelete(draftEvent.id)}\n                className='df-mobile-event-drawer-delete-button'\n              >\n                {t('delete')}\n              </button>\n            </div>\n          )}\n        </div>\n      </div>\n    </div>,\n    document.body\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/mobileEventDrawer/__tests__/DefaultMobileEventDrawer.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/preact';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { MobileEventDrawer } from '@/components/mobileEventDrawer/DefaultMobileEventDrawer';\nimport { CalendarApp } from '@/core/CalendarApp';\nimport { ViewType } from '@/types';\n\nconst createApp = () =>\n  new CalendarApp({\n    views: [],\n    plugins: [],\n    defaultView: ViewType.MONTH,\n    events: [\n      {\n        id: 'event-1',\n        title: 'Existing event',\n        description: 'Saved notes',\n        calendarId: 'work',\n        allDay: false,\n        start: Temporal.ZonedDateTime.from(\n          '2026-04-17T09:00:00+10:00[Australia/Sydney]'\n        ),\n        end: Temporal.ZonedDateTime.from(\n          '2026-04-17T10:00:00+10:00[Australia/Sydney]'\n        ),\n      },\n    ],\n    calendars: [\n      {\n        id: 'work',\n        name: 'Work',\n        colors: {\n          lineColor: '#2563eb',\n          eventColor: '#dbeafe',\n          eventSelectedColor: '#bfdbfe',\n          textColor: '#1e3a8a',\n        },\n      },\n    ],\n    timeZone: 'Australia/Sydney',\n  });\n\ndescribe('MobileEventDrawer', () => {\n  beforeAll(() => {\n    window.scrollTo = jest.fn();\n  });\n\n  it('hydrates notes and toggles start date expander state', () => {\n    const app = createApp();\n    const draftEvent = {\n      ...app.getEvents()[0],\n      description: 'Saved notes',\n    };\n\n    render(\n      <MobileEventDrawer\n        isOpen\n        onClose={jest.fn()}\n        onSave={jest.fn()}\n        onEventDelete={jest.fn()}\n        draftEvent={draftEvent}\n        app={app}\n      />\n    );\n\n    expect(screen.getByDisplayValue('Saved notes')).toBeInTheDocument();\n\n    const [startDateButton] = screen.getAllByRole('button', {\n      name: /Apr 17, 2026/i,\n    });\n    fireEvent.click(startDateButton);\n\n    const expandedCalendar = document.querySelector(\n      '.df-mobile-event-drawer-expander[data-kind=\"calendar\"][data-expanded=\"true\"]'\n    );\n\n    expect(expandedCalendar).not.toBeNull();\n  });\n\n  it('locks background scroll when open and restores it when closed', () => {\n    const app = createApp();\n    const draftEvent = app.getEvents()[0];\n\n    const { unmount } = render(\n      <MobileEventDrawer\n        isOpen\n        onClose={jest.fn()}\n        onSave={jest.fn()}\n        onEventDelete={jest.fn()}\n        draftEvent={draftEvent}\n        app={app}\n      />\n    );\n\n    expect(document.body.style.position).toBe('fixed');\n    expect(document.body.style.overflow).toBe('hidden');\n\n    unmount();\n\n    expect(document.body.style.position).toBe('');\n    expect(document.body.style.overflow).toBe('');\n    expect(window.scrollTo).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/mobileEventDrawer/components/Switch.tsx",
    "content": "interface SwitchProps {\n  checked: boolean;\n  onChange: (v: boolean) => void;\n  disabled?: boolean;\n}\n\nexport const Switch = ({ checked, onChange, disabled }: SwitchProps) => (\n  <button\n    type='button'\n    role='switch'\n    aria-checked={checked}\n    disabled={disabled}\n    data-checked={String(checked)}\n    data-disabled={String(!!disabled)}\n    className='df-mobile-switch'\n    onClick={() => !disabled && onChange(!checked)}\n  >\n    <span className='df-mobile-switch-thumb' />\n  </button>\n);\n"
  },
  {
    "path": "packages/core/src/components/mobileEventDrawer/components/TimePickerWheel.tsx",
    "content": "import { useState, useEffect, useRef } from 'preact/hooks';\n\ninterface TimePickerWheelProps {\n  date: Date;\n  onChange: (d: Date) => void;\n  timeFormat?: '12h' | '24h';\n}\n\nexport const TimePickerWheel = ({ date, onChange }: TimePickerWheelProps) => {\n  const originalHours = Array.from({ length: 24 }, (_, i) => i);\n  const originalMinutes = Array.from({ length: 12 }, (_, i) => i * 5);\n  // Triple the arrays for infinite scroll simulation\n  const hours = [...originalHours, ...originalHours, ...originalHours];\n  const minutes = [...originalMinutes, ...originalMinutes, ...originalMinutes];\n\n  const itemHeight = 32;\n  const containerHeight = 224; // 7 items * 32px\n  const spacerHeight = (containerHeight - itemHeight) / 2; // 96px\n\n  const currentHour = date.getHours();\n  const currentMinute = Math.round(date.getMinutes() / 5) * 5;\n\n  // Initial position in the middle set\n  const initialHourScroll = (24 + currentHour) * itemHeight;\n  const initialMinuteScroll = (12 + currentMinute / 5) * itemHeight;\n\n  // State to track scroll position for visual effects\n  const [scrollTopHour, setScrollTopHour] = useState(initialHourScroll);\n  const [scrollTopMin, setScrollTopMin] = useState(initialMinuteScroll);\n\n  // Refs for scrolling to initial position and handling scroll stop\n  const hourRef = useRef<HTMLDivElement>(null);\n  const minRef = useRef<HTMLDivElement>(null);\n  const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(() => {\n    if (hourRef.current) {\n      hourRef.current.scrollTop = initialHourScroll;\n      setScrollTopHour(initialHourScroll);\n    }\n    if (minRef.current) {\n      minRef.current.scrollTop = initialMinuteScroll;\n      setScrollTopMin(initialMinuteScroll);\n    }\n  }, [initialHourScroll, initialMinuteScroll]);\n\n  const handleScrollStop = (type: 'hour' | 'minute', scrollTop: number) => {\n    const index = Math.round(scrollTop / itemHeight);\n\n    if (type === 'hour') {\n      const h = hours[index]; // Use extended array\n      if (h !== undefined && h !== date.getHours()) {\n        const newDate = new Date(date);\n        newDate.setHours(h);\n        onChange(newDate);\n      }\n    } else {\n      const m = minutes[index]; // Use extended array\n      if (m !== undefined && Math.round(date.getMinutes() / 5) * 5 !== m) {\n        const newDate = new Date(date);\n        newDate.setMinutes(m);\n        onChange(newDate);\n      }\n    }\n  };\n\n  const onScroll = (\n    e: { currentTarget: HTMLDivElement },\n    type: 'hour' | 'minute'\n  ) => {\n    const target = e.currentTarget;\n    let scrollTop = target.scrollTop;\n\n    // Infinite scroll logic: jump when reaching ends\n    if (type === 'hour') {\n      const totalHeight = 24 * itemHeight; // Height of one set\n      if (scrollTop < 10 * itemHeight) {\n        scrollTop += totalHeight;\n        target.scrollTop = scrollTop;\n      } else if (scrollTop > 50 * itemHeight) {\n        // Near bottom of 2nd set\n        scrollTop -= totalHeight;\n        target.scrollTop = scrollTop;\n      }\n      setScrollTopHour(scrollTop);\n    } else {\n      const totalHeight = 12 * itemHeight; // Height of one set\n      if (scrollTop < 5 * itemHeight) {\n        scrollTop += totalHeight;\n        target.scrollTop = scrollTop;\n      } else if (scrollTop > 25 * itemHeight) {\n        scrollTop -= totalHeight;\n        target.scrollTop = scrollTop;\n      }\n      setScrollTopMin(scrollTop);\n    }\n\n    if (scrollTimerRef.current) {\n      clearTimeout(scrollTimerRef.current);\n    }\n    scrollTimerRef.current = setTimeout(() => {\n      handleScrollStop(type, scrollTop);\n    }, 150);\n  };\n\n  const getItemStyle = (index: number, scrollTop: number) => {\n    const itemCenterY = spacerHeight + index * itemHeight + itemHeight / 2;\n    const scrollCenterY = scrollTop + containerHeight / 2;\n    const distance = itemCenterY - scrollCenterY;\n    const maxDistance = containerHeight / 2;\n\n    const ratio = Math.min(Math.abs(distance) / maxDistance, 1);\n\n    // Visual tweaks - curvature\n    const scale = 1 - ratio * 0.4;\n    const opacity = 1 - ratio * 0.7;\n    const rotateX = (distance / maxDistance) * 80;\n\n    return {\n      transform: `perspective(500px) rotateX(${-rotateX}deg) scale(${scale})`,\n      opacity,\n      transition: 'transform 0.1s ease-out, opacity 0.1s ease-out',\n    };\n  };\n\n  return (\n    <div className='df-time-wheel'>\n      <div\n        ref={hourRef}\n        className='df-time-wheel-column'\n        onScroll={e => onScroll(e, 'hour')}\n      >\n        <div\n          className='df-time-wheel-spacer'\n          style={{ height: spacerHeight }}\n        ></div>\n        {hours.map((h, i) => (\n          <div\n            key={`h-${i}-${h}`}\n            className='df-time-wheel-option df-time-wheel-option-hour'\n            onClick={() => {\n              const newDate = new Date(date);\n              newDate.setHours(h);\n              onChange(newDate);\n              hourRef.current?.scrollTo({\n                top: i * itemHeight,\n                behavior: 'smooth',\n              });\n            }}\n          >\n            <div\n              className='df-time-wheel-value'\n              data-selected={String(h === currentHour)}\n              style={getItemStyle(i, scrollTopHour)}\n            >\n              {h.toString().padStart(2, '0')}\n            </div>\n          </div>\n        ))}\n        <div\n          className='df-time-wheel-spacer'\n          style={{ height: spacerHeight }}\n        ></div>\n      </div>\n      <div\n        ref={minRef}\n        className='df-time-wheel-column'\n        onScroll={e => onScroll(e, 'minute')}\n      >\n        <div\n          className='df-time-wheel-spacer'\n          style={{ height: spacerHeight }}\n        ></div>\n        {minutes.map((m, i) => (\n          <div\n            key={`m-${i}-${m}`}\n            className='df-time-wheel-option df-time-wheel-option-minute'\n            onClick={() => {\n              const newDate = new Date(date);\n              newDate.setMinutes(m);\n              onChange(newDate);\n              minRef.current?.scrollTo({\n                top: i * itemHeight,\n                behavior: 'smooth',\n              });\n            }}\n          >\n            <div\n              className='df-time-wheel-value'\n              data-selected={String(m === currentMinute)}\n              style={getItemStyle(i, scrollTopMin)}\n            >\n              {m.toString().padStart(2, '0')}\n            </div>\n          </div>\n        ))}\n        <div\n          className='df-time-wheel-spacer'\n          style={{ height: spacerHeight }}\n        ></div>\n      </div>\n      <div className='df-time-wheel-selection'></div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/mobileEventDrawer/components/__tests__/Switch.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/preact';\n\nimport { Switch } from '@/components/mobileEventDrawer/components/Switch';\n\ndescribe('mobile drawer Switch', () => {\n  it('toggles checked state when enabled', () => {\n    const onChange = jest.fn();\n\n    render(<Switch checked={false} onChange={onChange} />);\n\n    fireEvent.click(screen.getByRole('switch'));\n\n    expect(onChange).toHaveBeenCalledWith(true);\n  });\n\n  it('does not toggle when disabled', () => {\n    const onChange = jest.fn();\n\n    render(<Switch checked={false} onChange={onChange} disabled />);\n\n    fireEvent.click(screen.getByRole('switch'));\n\n    expect(onChange).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/mobileEventDrawer/index.ts",
    "content": "export * from './DefaultMobileEventDrawer';\nexport * from './components/Switch';\nexport * from './components/TimePickerWheel';\n"
  },
  {
    "path": "packages/core/src/components/monthView/MultiDayEvent.tsx",
    "content": "import { ComponentChildren } from 'preact';\nimport { memo } from 'preact/compat';\nimport { useState, useRef, useMemo } from 'preact/hooks';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { getAllDaySegmentShape } from '@/components/calendarEvent/utils';\nimport {\n  MultiDayEventSegment,\n  getEventIcon,\n} from '@/components/monthView/util';\nimport {\n  monthEventColorBar,\n  resizeHandleLeft,\n  resizeHandleRight,\n} from '@/styles/classNames';\nimport { Event } from '@/types';\nimport {\n  getLineColor,\n  getSelectedBgColor,\n  formatDateConsistent,\n  getEventBgColor,\n  getEventTextColor,\n  getPrimaryCalendarId,\n  getCalendarLineColors,\n  buildColorBarGradient,\n  getCalendarEventBgColors,\n  buildDiagonalPatternBackground,\n  formatTime,\n  extractHourFromDate,\n  getEventEndHour,\n  temporalToVisualTemporal,\n} from '@/utils';\n\ninterface MultiDayEventProps {\n  segment: MultiDayEventSegment;\n  segmentIndex: number;\n  eventHeight?: number;\n  isDragging: boolean;\n  isResizing?: boolean;\n  isSelected?: boolean;\n  onMoveStart?: (e: MouseEvent | TouchEvent, event: Event) => void;\n  onResizeStart?: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  onEventLongPress?: (eventId: string) => void;\n  isMobile?: boolean;\n  isDraggable?: boolean;\n  isEditable?: boolean;\n  viewable?: boolean;\n  isPopping?: boolean;\n  /** Optional slot renderer — receives the default visual content and wraps it in a ContentSlot */\n  renderSlot?: (defaultContent: ComponentChildren) => ComponentChildren;\n  appTimeZone?: string;\n}\n\nconst DEFAULT_EVENT_HEIGHT = 16;\nconst POP_TRANSITION = 'transform 0.5s cubic-bezier(0.22, 1, 0.36, 1)';\nconst mobileFadeStyle = {\n  whiteSpace: 'nowrap',\n  textOverflow: 'clip',\n  WebkitMaskImage: 'linear-gradient(to right, black 70%, transparent 100%)',\n  maskImage: 'linear-gradient(to right, black 70%, transparent 100%)',\n  WebkitMaskRepeat: 'no-repeat',\n  maskRepeat: 'no-repeat',\n} as const;\n\n// Render multi-day event component\nexport const MultiDayEvent = memo(\n  ({\n    segment,\n    segmentIndex,\n    eventHeight = DEFAULT_EVENT_HEIGHT,\n    isDragging,\n    isResizing = false,\n    isSelected = false,\n    onMoveStart,\n    onResizeStart,\n    onEventLongPress,\n    isMobile = false,\n    isDraggable = true,\n    isEditable = true,\n    viewable = true,\n    isPopping,\n    renderSlot,\n    appTimeZone,\n  }: MultiDayEventProps) => {\n    const [isPressed, setIsPressed] = useState(false);\n    const HORIZONTAL_MARGIN = 2; // 2px spacing on left and right\n    const rowSpacing = eventHeight + 1;\n\n    const visualEvent = useMemo(() => {\n      if (!appTimeZone || segment.event.allDay) return segment.event;\n      const start = temporalToVisualTemporal(\n        segment.event.start as Temporal.PlainDate,\n        appTimeZone\n      );\n      const end = segment.event.end\n        ? temporalToVisualTemporal(\n            segment.event.end as Temporal.PlainDate,\n            appTimeZone\n          )\n        : undefined;\n      return { ...segment.event, start, end } as Event;\n    }, [segment.event, appTimeZone]);\n\n    const startPercent = (segment.startDayIndex / 7) * 100;\n    const widthPercent =\n      ((segment.endDayIndex - segment.startDayIndex + 1) / 7) * 100;\n    const topOffset = segmentIndex * rowSpacing;\n\n    // Calculate actual position and width with spacing\n    const adjustedLeft = `calc(${startPercent}% + ${HORIZONTAL_MARGIN}px)`;\n    const adjustedWidth = `calc(${widthPercent}% - ${HORIZONTAL_MARGIN * 2}px)`;\n\n    const handleMouseDown = (e: MouseEvent) => {\n      if (!isDraggable && !viewable) return;\n      e.preventDefault();\n      e.stopPropagation();\n      setIsPressed(true);\n\n      const target = e.target as HTMLElement;\n      const isResizeHandle = target.closest('.df-resize-handle');\n\n      if (!isResizeHandle && isDraggable) {\n        onMoveStart?.(e, segment.event);\n      }\n    };\n\n    const handleMouseUp = () => {\n      setIsPressed(false);\n    };\n\n    const handleMouseLeave = () => {\n      setIsPressed(false);\n    };\n\n    // Long press handling\n    const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(\n      null\n    );\n    const touchStartPosRef = useRef<{ x: number; y: number } | null>(null);\n\n    const handleTouchStart = (e: TouchEvent) => {\n      if (!onMoveStart || !isMobile || (!isDraggable && !viewable)) return;\n      e.stopPropagation();\n      // Prevent browser scroll/pan gesture so touchmove stays cancelable during drag\n      if (isDraggable) e.preventDefault();\n      setIsPressed(true);\n\n      const touch = e.touches[0];\n      const clientX = touch.clientX;\n      const clientY = touch.clientY;\n      const currentTarget = e.currentTarget as HTMLElement;\n\n      touchStartPosRef.current = { x: clientX, y: clientY };\n\n      longPressTimerRef.current = setTimeout(() => {\n        if (onEventLongPress) {\n          onEventLongPress(segment.event.id);\n        }\n\n        const syntheticEvent = {\n          preventDefault: () => {\n            /* noop */\n          },\n          stopPropagation: () => {\n            /* noop */\n          },\n          currentTarget,\n          touches: [{ clientX, clientY }],\n          cancelable: false,\n        } as unknown as MouseEvent | TouchEvent;\n\n        if (isDraggable) {\n          onMoveStart(syntheticEvent, segment.event);\n        }\n        longPressTimerRef.current = null;\n\n        if (navigator.vibrate) navigator.vibrate(50);\n      }, 500);\n    };\n\n    const handleTouchMove = (e: TouchEvent) => {\n      if (longPressTimerRef.current && touchStartPosRef.current) {\n        const dx = Math.abs(e.touches[0].clientX - touchStartPosRef.current.x);\n        const dy = Math.abs(e.touches[0].clientY - touchStartPosRef.current.y);\n        if (dx > 10 || dy > 10) {\n          clearTimeout(longPressTimerRef.current);\n          longPressTimerRef.current = null;\n          touchStartPosRef.current = null;\n          setIsPressed(false);\n        }\n      }\n    };\n\n    const handleTouchEnd = () => {\n      setIsPressed(false);\n      if (longPressTimerRef.current) {\n        clearTimeout(longPressTimerRef.current);\n        longPressTimerRef.current = null;\n      }\n      touchStartPosRef.current = null;\n    };\n\n    const renderResizeHandle = (position: 'left' | 'right') => {\n      const isLeft = position === 'left';\n      const shouldShow = isLeft\n        ? segment.isFirstSegment\n        : segment.isLastSegment;\n\n      if (!shouldShow || !onResizeStart || !isEditable) return null;\n\n      return (\n        <div\n          className={isLeft ? resizeHandleLeft : resizeHandleRight}\n          onMouseDown={e => {\n            e.preventDefault();\n            e.stopPropagation();\n            onResizeStart(e, segment.event, isLeft ? 'left' : 'right');\n          }}\n          onClick={e => {\n            e.preventDefault();\n            e.stopPropagation();\n          }}\n        />\n      );\n    };\n\n    const calendarId = getPrimaryCalendarId(segment.event);\n    const isMultiCalendarEvent =\n      !!segment.event.calendarIds && segment.event.calendarIds.length > 1;\n    const multiCalendarBgColors = isMultiCalendarEvent\n      ? getCalendarEventBgColors(segment.event)\n      : null;\n    const isActive = isSelected || isDragging || isPressed;\n\n    const renderEventContent = () => {\n      const isAllDayEvent = visualEvent.allDay;\n      const visualCalendarId = getPrimaryCalendarId(visualEvent);\n      const startHour = extractHourFromDate(visualEvent.start);\n      const endHour = getEventEndHour(visualEvent);\n      const startTimeText = formatTime(startHour);\n      const endTimeText = formatTime(endHour);\n      const lineColors = getCalendarLineColors(segment.event);\n      const hideColorBar =\n        (isActive && isMultiCalendarEvent) ||\n        (!isAllDayEvent &&\n          segment.segmentType !== 'start' &&\n          segment.segmentType !== 'start-week-end' &&\n          segment.segmentType !== 'single');\n\n      if (isAllDayEvent) {\n        const getDisplayText = () => {\n          if (segment.isFirstSegment) return visualEvent.title;\n          if (segment.segmentType === 'middle') return '···';\n          if (segment.isLastSegment && segment.totalDays > 1) return '···';\n          return visualEvent.title;\n        };\n\n        return (\n          <div className='df-month-segment-event-all-day'>\n            {segment.isFirstSegment && getEventIcon(visualEvent) && (\n              <div className='df-event-icon-slot'>\n                <div\n                  className='df-event-year-icon-badge'\n                  style={{\n                    backgroundColor: getLineColor(visualCalendarId),\n                  }}\n                >\n                  {getEventIcon(visualEvent)}\n                </div>\n              </div>\n            )}\n\n            <div className='df-month-segment-event-all-day-main'>\n              <div\n                className={`df-month-segment-event-all-day-title ${isMobile ? 'df-mobile-mask-fade' : ''}`}\n                style={isMobile ? mobileFadeStyle : undefined}\n              >\n                {getDisplayText()}\n              </div>\n            </div>\n\n            {segment.isLastSegment && segment.segmentType !== 'single' && (\n              <div className='df-month-segment-event-tail'>\n                <div className='df-month-segment-event-tail-dot'></div>\n              </div>\n            )}\n          </div>\n        );\n      }\n\n      const titleText =\n        segment.isFirstSegment || segment.isLastSegment\n          ? visualEvent.title\n          : '···';\n\n      const segmentDays = segment.endDayIndex - segment.startDayIndex + 1;\n      const remainingPercent =\n        segmentDays > 1 ? ((segmentDays - 1) / segmentDays) * 100 : 0;\n      const isMultiDayTimedStart =\n        !isAllDayEvent && segment.isFirstSegment && segmentDays > 1;\n\n      // For multi-day timed start, we want to limit the title to the first day's cell width minus the time display space.\n      // 100 / segmentDays is the width of exactly one day relative to the full segment width.\n      const firstDayPercent = 100 / segmentDays;\n\n      const startTimeStyle =\n        segmentDays > 1\n          ? {\n              right: `calc(${remainingPercent}% + ${HORIZONTAL_MARGIN}px)`,\n              top: '50%',\n              transform: 'translateY(-50%)',\n            }\n          : undefined;\n\n      return (\n        <div className='df-event-month-main'>\n          {!hideColorBar && (\n            <div\n              className={monthEventColorBar}\n              style={\n                lineColors.length > 1\n                  ? { background: buildColorBarGradient(lineColors) }\n                  : { backgroundColor: lineColors[0] }\n              }\n            />\n          )}\n          <div\n            className='df-event-month-main'\n            style={\n              isMultiDayTimedStart && !isMobile\n                ? {\n                    maxWidth: `calc(${firstDayPercent}% - 45px)`,\n                    overflow: 'hidden',\n                    WebkitMaskImage:\n                      'linear-gradient(to right, black 70%, transparent 100%)',\n                    maskImage:\n                      'linear-gradient(to right, black 70%, transparent 100%)',\n                    WebkitMaskRepeat: 'no-repeat',\n                    maskRepeat: 'no-repeat',\n                  }\n                : undefined\n            }\n          >\n            <span\n              className={`df-event-month-title ${isMobile || isMultiDayTimedStart ? 'df-mobile-mask-fade' : ''}`}\n              style={isMobile ? mobileFadeStyle : undefined}\n            >\n              {titleText}\n            </span>\n          </div>\n          {segment.isFirstSegment && !isMobile && (\n            <span\n              className={`df-month-segment-event-time ${segmentDays === 1 ? 'df-month-segment-event-time-spaced' : 'df-month-segment-event-time-overlay'}`}\n              style={startTimeStyle}\n            >\n              {startTimeText}\n            </span>\n          )}\n          {segment.isLastSegment &&\n            !visualEvent.allDay &&\n            endHour !== 24 &&\n            !isMobile && (\n              <span className='df-month-segment-event-tail-time'>\n                {`ends ${endTimeText}`}\n              </span>\n            )}\n        </div>\n      );\n    };\n\n    // Calculate the number of days occupied by the current segment\n    const segmentDays = segment.endDayIndex - segment.startDayIndex + 1;\n\n    return (\n      <div\n        className='df-month-segment-event'\n        style={{\n          left: adjustedLeft,\n          width: adjustedWidth,\n          top: `${topOffset}px`,\n          height: `${eventHeight}px`,\n          pointerEvents: 'auto',\n          zIndex: 10,\n          transform: isPopping ? 'scale(1.02)' : 'scale(1)',\n          transition: POP_TRANSITION,\n          willChange: 'transform',\n          ...(isActive\n            ? {\n                backgroundColor: getSelectedBgColor(calendarId),\n                color: '#fff',\n              }\n            : isMultiCalendarEvent\n              ? {\n                  background: buildDiagonalPatternBackground(\n                    multiCalendarBgColors!\n                  ),\n                  color: getEventTextColor(calendarId),\n                }\n              : {\n                  backgroundColor: getEventBgColor(calendarId),\n                  color: getEventTextColor(calendarId),\n                }),\n          cursor: isDraggable ? 'pointer' : viewable ? 'pointer' : 'default',\n          // Prevent browser scroll/zoom gestures on draggable multi-day events\n          // on touch screens — CSS touch-action is resolved before JS runs.\n          ...(isMobile && isDraggable ? { touchAction: 'none' } : {}),\n        }}\n        data-all-day={String(!!visualEvent.allDay)}\n        data-selected={String(isSelected)}\n        data-dragging={String(isDragging)}\n        data-resizing={String(isResizing)}\n        data-popping={String(!!isPopping)}\n        data-segment-shape={getAllDaySegmentShape(segment)}\n        data-segment-days={segmentDays}\n        onMouseDown={handleMouseDown}\n        onMouseUp={handleMouseUp}\n        onMouseLeave={handleMouseLeave}\n        onTouchStart={handleTouchStart}\n        onTouchMove={handleTouchMove}\n        onTouchEnd={handleTouchEnd}\n        title={`${visualEvent.title} (${formatDateConsistent(visualEvent.start)} - ${formatDateConsistent(visualEvent.end)})`}\n      >\n        {renderResizeHandle('left')}\n        <div\n          className='df-month-segment-event-body'\n          style={{\n            cursor: isResizing ? 'ew-resize' : 'pointer',\n          }}\n        >\n          {renderSlot ? renderSlot(renderEventContent()) : renderEventContent()}\n        </div>\n        {renderResizeHandle('right')}\n      </div>\n    );\n  }\n);\n\n(MultiDayEvent as { displayName?: string }).displayName = 'MultiDayEvent';\n\nexport default MultiDayEvent;\n"
  },
  {
    "path": "packages/core/src/components/monthView/WeekComponent.tsx",
    "content": "import { RefObject } from 'preact';\nimport { memo } from 'preact/compat';\nimport { useEffect, useMemo, useRef, useState } from 'preact/hooks';\n\nimport CalendarEvent from '@/components/calendarEvent';\nimport { GridContextMenu } from '@/components/contextMenu';\nimport { useLocale } from '@/locale';\nimport { monthTitle } from '@/styles/classNames';\nimport { MonthEventDragState, Event, ViewType, ICalendarApp } from '@/types';\nimport { VirtualWeekItem } from '@/types/monthView';\nimport { scrollbarTakesSpace, temporalToVisualDate } from '@/utils';\n\nimport {\n  analyzeMultiDayEventsForWeek,\n  constructRenderEvents,\n  MonthDayLayoutData,\n  organizeMultiDaySegments,\n  sortDayEvents,\n} from './util';\nimport WeekDayCell from './WeekDayCell';\n\ninterface WeekComponentProps {\n  currentMonth: string;\n  currentYear: number;\n  newlyCreatedEventId: string | null;\n  screenSize: 'mobile' | 'tablet' | 'desktop';\n  isScrolling: boolean;\n  isDragging: boolean;\n  showWeekNumbers?: boolean;\n  showMonthIndicator?: boolean;\n  item: VirtualWeekItem;\n  weekHeight: number; // Use this instead of item.height to avoid sync issues\n  eventHeight?: number;\n  events: Event[];\n  dragState: MonthEventDragState;\n  calendarRef: RefObject<HTMLDivElement>;\n  onEventUpdate: (updatedEvent: Event) => void;\n  onEventDelete: (eventId: string) => void;\n  onMoveStart?: (e: MouseEvent | TouchEvent, event: Event) => void;\n  onCreateStart?: (e: MouseEvent | TouchEvent, targetDate: Date) => void;\n  onResizeStart?: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  onDetailPanelOpen: () => void;\n  onMoreEventsClick?: (date: Date) => void;\n  onChangeView?: (view: ViewType) => void;\n  onSelectDate?: (date: Date) => void;\n  onGridDateClick?: (date: Date, events: Event[]) => void;\n  onGridDateDoubleClick?: (\n    e: MouseEvent | TouchEvent,\n    date: Date,\n    events: Event[]\n  ) => void;\n  selectedEventId?: string | null;\n  onEventSelect?: (eventId: string | null) => void;\n  onEventLongPress?: (eventId: string) => void;\n  detailPanelEventId: string | null;\n  onDetailPanelToggle: (eventId: string | null) => void;\n  useEventDetailPanel?: boolean;\n  onCalendarDrop: (\n    e: DragEvent,\n    dropDate: Date,\n    dropHour?: number,\n    isAllDay?: boolean\n  ) => Event | null;\n  onCalendarDragOver?: (e: DragEvent) => void;\n  calendarSignature?: string;\n  app: ICalendarApp;\n  enableTouch?: boolean;\n  appTimeZone?: string;\n}\n\nconst MULTI_DAY_TOP_OFFSET = 33;\nconst MORE_TEXT_HEIGHT = 20; // Height reserved for the \"+ x more\" indicator\n\nconst WeekComponent = memo(\n  ({\n    currentMonth,\n    currentYear,\n    newlyCreatedEventId,\n    screenSize,\n    isScrolling,\n    isDragging,\n    showWeekNumbers,\n    showMonthIndicator = true,\n    item,\n    weekHeight,\n    eventHeight = 16,\n    events,\n    dragState,\n    calendarRef,\n    onEventUpdate,\n    onEventDelete,\n    onMoveStart,\n    onCreateStart,\n    onResizeStart,\n    onDetailPanelOpen,\n    onMoreEventsClick,\n    onChangeView,\n    onSelectDate,\n    onGridDateClick,\n    onGridDateDoubleClick,\n    selectedEventId,\n    onEventSelect,\n    onEventLongPress,\n    detailPanelEventId,\n    onDetailPanelToggle,\n    useEventDetailPanel,\n    onCalendarDrop,\n    onCalendarDragOver,\n    app,\n    enableTouch,\n    appTimeZone,\n  }: WeekComponentProps) => {\n    const { t, locale } = useLocale();\n    const [shouldShowMonthTitle, setShouldShowMonthTitle] = useState(false);\n    const hideTitleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n      null\n    );\n\n    const [contextMenu, setContextMenu] = useState<{\n      x: number;\n      y: number;\n      date: Date;\n    } | null>(null);\n    const isEditable = app.canMutateFromUI();\n\n    useEffect(() => {\n      if (isEditable) return;\n      setContextMenu(null);\n    }, [isEditable]);\n\n    const handleContextMenu = (e: MouseEvent, date: Date) => {\n      e.preventDefault();\n      if (screenSize === 'mobile' || !isEditable) return;\n      setContextMenu({ x: e.clientX, y: e.clientY, date });\n    };\n\n    const rowSpacing = eventHeight + 1;\n\n    // Calculate layout parameters once per week render\n    const layoutParams = useMemo(() => {\n      const availableHeight = weekHeight - MULTI_DAY_TOP_OFFSET;\n      if (availableHeight <= 0) return { maxSlots: 0, maxSlotsWithMore: 0 };\n\n      const maxSlots = Math.floor(availableHeight / rowSpacing);\n\n      const spaceForMore = availableHeight - MORE_TEXT_HEIGHT;\n      const maxSlotsWithMoreRaw = Math.max(\n        0,\n        Math.floor(spaceForMore / rowSpacing)\n      );\n\n      // Ensure maxSlotsWithMore is always at most maxSlots - 1 to leave room for the \"+ x more\" indicator\n      const maxSlotsWithMore =\n        maxSlots > 0\n          ? Math.min(maxSlotsWithMoreRaw, maxSlots - 1)\n          : maxSlotsWithMoreRaw;\n\n      return { maxSlots, maxSlotsWithMore };\n    }, [weekHeight, rowSpacing]);\n\n    useEffect(() => {\n      if (isScrolling) {\n        setShouldShowMonthTitle(true);\n\n        if (hideTitleTimeoutRef.current) {\n          clearTimeout(hideTitleTimeoutRef.current);\n          hideTitleTimeoutRef.current = null;\n        }\n\n        return () => {\n          if (hideTitleTimeoutRef.current) {\n            clearTimeout(hideTitleTimeoutRef.current);\n            hideTitleTimeoutRef.current = null;\n          }\n        };\n      }\n\n      if (!shouldShowMonthTitle) {\n        return;\n      }\n\n      hideTitleTimeoutRef.current = setTimeout(() => {\n        setShouldShowMonthTitle(false);\n        hideTitleTimeoutRef.current = null;\n      }, 100);\n\n      return () => {\n        if (hideTitleTimeoutRef.current) {\n          clearTimeout(hideTitleTimeoutRef.current);\n          hideTitleTimeoutRef.current = null;\n        }\n      };\n    }, [isScrolling, shouldShowMonthTitle]);\n\n    const { weekData } = item;\n    const firstDayOfMonth = weekData.days.find(day => day.day === 1);\n\n    // Use the weekHeight prop instead of item.height to avoid jumps from virtual scroll sync delays\n    const weekHeightPx = `${weekHeight}px`;\n\n    const hasScrollbarSpace = useMemo(() => scrollbarTakesSpace(), []);\n\n    // Analyze multi-day events for the current week\n    const multiDaySegments = useMemo(\n      () =>\n        analyzeMultiDayEventsForWeek(\n          events,\n          weekData.startDate,\n          7,\n          appTimeZone\n        ),\n      [events, weekData.startDate, appTimeZone]\n    );\n\n    // Build render events\n    const constructedRenderEvents = useMemo(\n      () => constructRenderEvents(events, weekData.startDate, appTimeZone),\n      [events, weekData.startDate, appTimeZone]\n    );\n\n    // Pre-compute events grouped by day to replace 7× O(n) filter calls on every render\n    const eventsByDayDate = useMemo(() => {\n      const map = new Map<string, Event[]>();\n      weekData.days.forEach(day => {\n        const dayDateStr = day.date.toDateString();\n        map.set(\n          dayDateStr,\n          constructedRenderEvents.filter(event => {\n            if (!event.start || !event.end) {\n              return (\n                temporalToVisualDate(\n                  event.start,\n                  appTimeZone\n                ).toDateString() === dayDateStr\n              );\n            }\n            const startDate = temporalToVisualDate(event.start, appTimeZone);\n            const endDate = temporalToVisualDate(event.end, appTimeZone);\n            if (!event.allDay) {\n              const endHasTime =\n                endDate.getHours() !== 0 ||\n                endDate.getMinutes() !== 0 ||\n                endDate.getSeconds() !== 0;\n              if (!endHasTime) {\n                const durationMs = endDate.getTime() - startDate.getTime();\n                const ONE_DAY_MS = 24 * 60 * 60 * 1000;\n                if (durationMs > 0 && durationMs < ONE_DAY_MS) {\n                  return startDate.toDateString() === dayDateStr;\n                }\n              }\n            }\n            return (\n              startDate.toDateString() === dayDateStr ||\n              endDate.toDateString() === dayDateStr\n            );\n          })\n        );\n      });\n      return map;\n    }, [constructedRenderEvents, weekData.days, appTimeZone]);\n\n    // Organize multi-day event segments\n    const organizedMultiDaySegments = useMemo(\n      () =>\n        organizeMultiDaySegments(\n          multiDaySegments,\n          app.state.allDaySortComparator\n        ),\n      [multiDaySegments, app.state.allDaySortComparator]\n    );\n\n    // Memoize flat segment list to avoid 7× flat() calls inside renderDayCell\n    const allSegments = useMemo(\n      () => organizedMultiDaySegments.flat(),\n      [organizedMultiDaySegments]\n    );\n\n    const dayLayerCounts = useMemo(() => {\n      const counts: number[] = Array.from({ length: 7 }).fill(0) as number[];\n\n      organizedMultiDaySegments.forEach((layer, layerIndex) => {\n        layer.forEach(segment => {\n          for (\n            let dayIndex = segment.startDayIndex;\n            dayIndex <= segment.endDayIndex;\n            dayIndex++\n          ) {\n            counts[dayIndex] = Math.max(\n              counts[dayIndex] as number,\n              layerIndex + 1\n            );\n          }\n        });\n      });\n\n      return counts;\n    }, [organizedMultiDaySegments]);\n\n    // Track which specific layers are occupied by multi-day events for each day\n    // This allows single-day events to fill \"gaps\" in the multi-day event layers\n    const dayOccupiedLayers = useMemo(() => {\n      const occupied: Set<number>[] = Array.from(\n        { length: 7 },\n        () => new Set()\n      );\n\n      organizedMultiDaySegments.forEach((layer, layerIndex) => {\n        layer.forEach(segment => {\n          for (\n            let dayIndex = segment.startDayIndex;\n            dayIndex <= segment.endDayIndex;\n            dayIndex++\n          ) {\n            if (dayIndex >= 0 && dayIndex < 7) {\n              occupied[dayIndex].add(layerIndex);\n            }\n          }\n        });\n      });\n\n      return occupied;\n    }, [organizedMultiDaySegments]);\n\n    const dayLayoutData = useMemo<MonthDayLayoutData[]>(() => {\n      const { maxSlots, maxSlotsWithMore } = layoutParams;\n\n      // 1. Initial pass: calculate total slots and basic \"more\" check for each day\n      const initialResults = weekData.days.map((day, dayIndex) => {\n        const dayEvents = eventsByDayDate.get(day.date.toDateString()) ?? [];\n        const sortedEvents = sortDayEvents(dayEvents);\n\n        // Filter out all-day events that are rendered as multi-day segments\n        const timedEventsOnly = sortedEvents.filter(event => {\n          if (!event.allDay) return true;\n          const hasSegment = allSegments.some(\n            seg => seg.originalEventId === event.id\n          );\n          return !hasSegment;\n        });\n\n        const maxOccupiedLayer = (dayLayerCounts[dayIndex] ?? 0) - 1;\n        const occupiedLayers = dayOccupiedLayers[dayIndex];\n        const gapLayers: number[] = [];\n        for (let i = 0; i <= maxOccupiedLayer; i++) {\n          if (!occupiedLayers.has(i)) {\n            gapLayers.push(i);\n          }\n        }\n\n        const totalTimedEvents = timedEventsOnly.length;\n        const eventsAfterMultiDay = Math.max(\n          0,\n          totalTimedEvents - gapLayers.length\n        );\n        const totalSlotsNeeded =\n          Math.max(maxOccupiedLayer + 1, 0) + eventsAfterMultiDay;\n\n        const hasMore = totalSlotsNeeded > maxSlots;\n        const limit = hasMore ? maxSlotsWithMore : maxSlots;\n\n        return {\n          totalSlotsNeeded,\n          hasMore,\n          limit,\n          timedEventsOnly,\n          gapLayers,\n          occupiedLayers,\n          maxOccupiedLayer,\n        };\n      });\n\n      // 2. Multi-day segment visibility check\n      // A segment in Layer L is hidden if it spans any day where L >= limit\n      const segmentIsHidden = new Set<string>(); // segment.id\n      organizedMultiDaySegments.forEach((layer, layerIndex) => {\n        layer.forEach(segment => {\n          for (let d = segment.startDayIndex; d <= segment.endDayIndex; d++) {\n            if (d >= 0 && d < 7 && layerIndex >= initialResults[d].limit) {\n              segmentIsHidden.add(segment.id);\n              break;\n            }\n          }\n        });\n      });\n\n      // 3. Final pass: update hasMore and limit if a day contains a segment that was forced to hide\n      return initialResults.map((res, dayIndex) => {\n        let hasHiddenSegment = false;\n        allSegments.forEach(segment => {\n          if (\n            segment.startDayIndex <= dayIndex &&\n            segment.endDayIndex >= dayIndex &&\n            segmentIsHidden.has(segment.id)\n          ) {\n            hasHiddenSegment = true;\n          }\n        });\n\n        const finalHasMore = res.hasMore || hasHiddenSegment;\n        const finalLimit = finalHasMore ? maxSlotsWithMore : maxSlots;\n\n        return {\n          ...res,\n          hasMore: finalHasMore,\n          limit: finalLimit,\n          segmentIsHidden,\n        };\n      });\n    }, [\n      layoutParams,\n      weekData.days,\n      eventsByDayDate,\n      allSegments,\n      dayLayerCounts,\n      dayOccupiedLayers,\n      organizedMultiDaySegments,\n    ]);\n\n    const overlayVisibleLayerCount = useMemo(\n      () => Math.min(organizedMultiDaySegments.length, layoutParams.maxSlots),\n      [organizedMultiDaySegments.length, layoutParams.maxSlots]\n    );\n\n    // Calculate the height of the multi-day event area\n    const multiDayAreaHeight = useMemo(\n      () => Math.max(0, overlayVisibleLayerCount * rowSpacing),\n      [overlayVisibleLayerCount, rowSpacing]\n    );\n\n    const localizedMonthYear = useMemo(() => {\n      if (!firstDayOfMonth) return '';\n      return firstDayOfMonth.date.toLocaleDateString(locale, {\n        month: 'long',\n        year: 'numeric',\n      });\n    }, [firstDayOfMonth, locale]);\n\n    return (\n      <div className='df-month-week' style={{ height: weekHeightPx }}>\n        {/* Month title: displayed when scrolling, hidden after scrolling stops */}\n        {showMonthIndicator && firstDayOfMonth && (\n          <div\n            className={monthTitle}\n            data-visible={shouldShowMonthTitle ? 'true' : 'false'}\n            style={{\n              transition: 'opacity 0.5s ease',\n              maxWidth: 'fit-content',\n            }}\n            onContextMenu={e => e.preventDefault()}\n          >\n            <span className='df-month-title-label'>{localizedMonthYear}</span>\n          </div>\n        )}\n\n        <div className='df-month-week-inner'>\n          <div\n            className='df-month-week-grid-shell'\n            onDragOver={onCalendarDragOver}\n            onDrop={e => {\n              const rect = e.currentTarget.getBoundingClientRect();\n              const relativeX = e.clientX - rect.left;\n              const dayIndex = Math.floor(relativeX / (rect.width / 7));\n\n              const dropDate = new Date(weekData.startDate);\n              dropDate.setDate(weekData.startDate.getDate() + dayIndex);\n\n              onCalendarDrop(e, dropDate);\n            }}\n          >\n            {/* Date grid */}\n            <div className='df-month-week-grid'>\n              {weekData.days.map((day, index) => (\n                <WeekDayCell\n                  key={`day-${day.date.getTime()}`}\n                  app={app}\n                  appTimeZone={appTimeZone}\n                  calendarRef={calendarRef}\n                  currentMonth={currentMonth}\n                  currentYear={currentYear}\n                  useEventDetailPanel={useEventDetailPanel}\n                  day={day}\n                  dayIndex={index}\n                  dayLayout={dayLayoutData[index]}\n                  detailPanelEventId={detailPanelEventId}\n                  dragState={dragState}\n                  enableTouch={enableTouch}\n                  hasScrollbarSpace={hasScrollbarSpace}\n                  isDragging={isDragging}\n                  locale={locale}\n                  moreLabel={t('more')}\n                  newlyCreatedEventId={newlyCreatedEventId}\n                  onChangeView={onChangeView}\n                  onContextMenu={handleContextMenu}\n                  onCreateStart={onCreateStart}\n                  onDetailPanelOpen={onDetailPanelOpen}\n                  onDetailPanelToggle={onDetailPanelToggle}\n                  onEventDelete={onEventDelete}\n                  onEventLongPress={onEventLongPress}\n                  onEventSelect={onEventSelect}\n                  onEventUpdate={onEventUpdate}\n                  onMoreEventsClick={onMoreEventsClick}\n                  onMoveStart={onMoveStart}\n                  onResizeStart={onResizeStart}\n                  onSelectDate={onSelectDate}\n                  onGridDateClick={onGridDateClick}\n                  onGridDateDoubleClick={onGridDateDoubleClick}\n                  organizedMultiDaySegments={organizedMultiDaySegments}\n                  overlayVisibleLayerCount={overlayVisibleLayerCount}\n                  screenSize={screenSize}\n                  selectedEventId={selectedEventId}\n                  showWeekNumbers={showWeekNumbers}\n                  totalSlotsNeeded={dayLayoutData[index].totalSlotsNeeded}\n                  weekHeightPx={weekHeightPx}\n                  eventHeight={eventHeight}\n                />\n              ))}\n            </div>\n\n            {/* Multi-day event overlay layer */}\n            {organizedMultiDaySegments.length > 0 && (\n              <div\n                className='df-month-week-event-layer'\n                style={{\n                  top: `${MULTI_DAY_TOP_OFFSET}px`,\n                  height: `${multiDayAreaHeight}px`,\n                }}\n              >\n                {organizedMultiDaySegments\n                  .slice(0, overlayVisibleLayerCount)\n                  .map((layer, layerIndex) => (\n                    <div\n                      key={`layer-${layerIndex}`}\n                      className='df-month-week-event-layer-row'\n                    >\n                      {layer\n                        .filter(\n                          segment =>\n                            !dayLayoutData[0].segmentIsHidden.has(segment.id)\n                        )\n                        .map(segment => (\n                          <CalendarEvent\n                            key={segment.id}\n                            event={segment.event}\n                            isAllDay={true}\n                            segment={segment}\n                            segmentIndex={layerIndex}\n                            viewType={ViewType.MONTH}\n                            isMultiDay={true}\n                            calendarRef={calendarRef}\n                            hourHeight={72}\n                            firstHour={0}\n                            onEventUpdate={onEventUpdate}\n                            onEventDelete={onEventDelete}\n                            onMoveStart={onMoveStart}\n                            onResizeStart={onResizeStart}\n                            isBeingDragged={\n                              isDragging &&\n                              dragState.eventId === segment.event.id &&\n                              dragState.mode === 'move'\n                            }\n                            isBeingResized={\n                              isDragging &&\n                              dragState.eventId === segment.event.id &&\n                              dragState.mode === 'resize'\n                            }\n                            newlyCreatedEventId={newlyCreatedEventId}\n                            onDetailPanelOpen={onDetailPanelOpen}\n                            selectedEventId={selectedEventId}\n                            onEventSelect={onEventSelect}\n                            onEventLongPress={onEventLongPress}\n                            detailPanelEventId={detailPanelEventId}\n                            onDetailPanelToggle={onDetailPanelToggle}\n                            useEventDetailPanel={useEventDetailPanel}\n                            app={app}\n                            isMobile={screenSize !== 'desktop'}\n                            enableTouch={enableTouch}\n                            appTimeZone={appTimeZone}\n                            monthEventHeight={eventHeight}\n                          />\n                        ))}\n                    </div>\n                  ))}\n              </div>\n            )}\n          </div>\n        </div>\n        {isEditable && contextMenu && (\n          <GridContextMenu\n            x={contextMenu.x}\n            y={contextMenu.y}\n            date={contextMenu.date}\n            viewType={ViewType.MONTH}\n            onClose={() => setContextMenu(null)}\n            app={app}\n            onCreateEvent={() => {\n              if (onCreateStart) {\n                const syntheticEvent = {\n                  preventDefault: () => {\n                    /* noop */\n                  },\n                  stopPropagation: () => {\n                    /* noop */\n                  },\n                  clientX: contextMenu.x,\n                  clientY: contextMenu.y,\n                } as MouseEvent;\n                onCreateStart(syntheticEvent, contextMenu.date);\n              }\n            }}\n          />\n        )}\n      </div>\n    );\n  },\n  (prevProps, nextProps) =>\n    prevProps.currentMonth === nextProps.currentMonth &&\n    prevProps.currentYear === nextProps.currentYear &&\n    prevProps.newlyCreatedEventId === nextProps.newlyCreatedEventId &&\n    prevProps.screenSize === nextProps.screenSize &&\n    prevProps.isScrolling === nextProps.isScrolling &&\n    prevProps.showWeekNumbers === nextProps.showWeekNumbers &&\n    prevProps.showMonthIndicator === nextProps.showMonthIndicator &&\n    prevProps.item.weekData === nextProps.item.weekData &&\n    prevProps.weekHeight === nextProps.weekHeight &&\n    prevProps.eventHeight === nextProps.eventHeight &&\n    prevProps.events === nextProps.events &&\n    prevProps.calendarRef === nextProps.calendarRef &&\n    prevProps.onEventUpdate === nextProps.onEventUpdate &&\n    prevProps.onEventDelete === nextProps.onEventDelete &&\n    prevProps.onMoveStart === nextProps.onMoveStart &&\n    prevProps.onCreateStart === nextProps.onCreateStart &&\n    prevProps.onResizeStart === nextProps.onResizeStart &&\n    prevProps.onDetailPanelOpen === nextProps.onDetailPanelOpen &&\n    prevProps.onMoreEventsClick === nextProps.onMoreEventsClick &&\n    prevProps.onChangeView === nextProps.onChangeView &&\n    prevProps.onSelectDate === nextProps.onSelectDate &&\n    prevProps.onGridDateClick === nextProps.onGridDateClick &&\n    prevProps.onGridDateDoubleClick === nextProps.onGridDateDoubleClick &&\n    prevProps.selectedEventId === nextProps.selectedEventId &&\n    prevProps.onEventSelect === nextProps.onEventSelect &&\n    prevProps.onEventLongPress === nextProps.onEventLongPress &&\n    prevProps.detailPanelEventId === nextProps.detailPanelEventId &&\n    prevProps.onDetailPanelToggle === nextProps.onDetailPanelToggle &&\n    prevProps.useEventDetailPanel === nextProps.useEventDetailPanel &&\n    prevProps.onCalendarDrop === nextProps.onCalendarDrop &&\n    prevProps.onCalendarDragOver === nextProps.onCalendarDragOver &&\n    prevProps.app === nextProps.app &&\n    prevProps.enableTouch === nextProps.enableTouch &&\n    prevProps.appTimeZone === nextProps.appTimeZone\n);\n\n(WeekComponent as { displayName?: string }).displayName = 'WeekComponent';\n\nexport default WeekComponent;\n"
  },
  {
    "path": "packages/core/src/components/monthView/WeekDayCell.tsx",
    "content": "import { ComponentChild, RefObject } from 'preact';\nimport { useRef } from 'preact/hooks';\n\nimport CalendarEvent from '@/components/calendarEvent';\nimport {\n  monthDayCell,\n  monthDateNumberContainer,\n  monthDateNumber,\n  monthMoreEvents,\n  cn,\n} from '@/styles/classNames';\nimport { Event, ICalendarApp, MonthEventDragState, ViewType } from '@/types';\nimport { DayData } from '@/types/calendar';\nimport { getWeekNumber } from '@/utils';\nimport { extractHourFromDate } from '@/utils/helpers';\n\nimport {\n  MonthDayLayoutData,\n  MultiDayEventSegment,\n  createDateString,\n} from './util';\n\ninterface WeekDayCellProps {\n  app: ICalendarApp;\n  appTimeZone?: string;\n  calendarRef: RefObject<HTMLDivElement>;\n  currentMonth: string;\n  currentYear: number;\n  useEventDetailPanel?: boolean;\n  day: DayData;\n  dayIndex: number;\n  dayLayout: MonthDayLayoutData;\n  detailPanelEventId?: string | null;\n  dragState: MonthEventDragState;\n  enableTouch?: boolean;\n  hasScrollbarSpace: boolean;\n  isDragging: boolean;\n  locale: string;\n  moreLabel: string;\n  newlyCreatedEventId: string | null;\n  onChangeView?: (view: ViewType) => void;\n  onContextMenu: (e: MouseEvent, date: Date) => void;\n  onCreateStart?: (e: MouseEvent | TouchEvent, targetDate: Date) => void;\n  onDetailPanelOpen: () => void;\n  onDetailPanelToggle?: (eventId: string | null) => void;\n  onEventDelete: (eventId: string) => void;\n  onEventLongPress?: (eventId: string) => void;\n  onEventSelect?: (eventId: string | null) => void;\n  onEventUpdate: (updatedEvent: Event) => void;\n  onMoreEventsClick?: (date: Date) => void;\n  onMoveStart?: (e: MouseEvent | TouchEvent, event: Event) => void;\n  onResizeStart?: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  onSelectDate?: (date: Date) => void;\n  onGridDateClick?: (date: Date, dayEvents: Event[]) => void;\n  onGridDateDoubleClick?: (\n    e: MouseEvent | TouchEvent,\n    date: Date,\n    dayEvents: Event[]\n  ) => void;\n  organizedMultiDaySegments: MultiDayEventSegment[][];\n  overlayVisibleLayerCount: number;\n  screenSize: 'mobile' | 'tablet' | 'desktop';\n  selectedEventId?: string | null;\n  showWeekNumbers?: boolean;\n  totalSlotsNeeded: number;\n  weekHeightPx: string;\n  eventHeight?: number;\n}\n\nconst WeekDayCell = ({\n  app,\n  appTimeZone,\n  calendarRef,\n  currentMonth,\n  currentYear,\n  useEventDetailPanel,\n  day,\n  dayIndex,\n  dayLayout,\n  detailPanelEventId,\n  dragState,\n  enableTouch,\n  hasScrollbarSpace,\n  isDragging,\n  locale,\n  moreLabel,\n  newlyCreatedEventId,\n  onChangeView,\n  onContextMenu,\n  onCreateStart,\n  onDetailPanelOpen,\n  onDetailPanelToggle,\n  onEventDelete,\n  onEventLongPress,\n  onEventSelect,\n  onEventUpdate,\n  onMoreEventsClick,\n  onMoveStart,\n  onResizeStart,\n  onSelectDate,\n  onGridDateClick,\n  onGridDateDoubleClick,\n  organizedMultiDaySegments,\n  overlayVisibleLayerCount,\n  screenSize,\n  selectedEventId,\n  showWeekNumbers,\n  totalSlotsNeeded,\n  weekHeightPx,\n  eventHeight = 16,\n}: WeekDayCellProps) => {\n  const rowSpacing = eventHeight + 1;\n  const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const touchStartPosRef = useRef<{ x: number; y: number } | null>(null);\n\n  const dayMonthName = day.date.toLocaleDateString(locale, {\n    month:\n      locale.startsWith('zh') || locale.startsWith('ja') ? 'short' : 'long',\n  });\n\n  const belongsToCurrentMonth =\n    dayMonthName === currentMonth && day.year === currentYear;\n\n  const {\n    hasMore: hasMoreEvents,\n    limit: displaySlotLimit,\n    timedEventsOnly,\n    gapLayers,\n    occupiedLayers,\n    maxOccupiedLayer,\n    segmentIsHidden,\n  } = dayLayout;\n\n  let hiddenSegmentCount = 0;\n  organizedMultiDaySegments.slice(displaySlotLimit).forEach(layer => {\n    layer.forEach(segment => {\n      if (\n        segment.startDayIndex <= dayIndex &&\n        segment.endDayIndex >= dayIndex\n      ) {\n        hiddenSegmentCount++;\n      }\n    });\n  });\n\n  organizedMultiDaySegments.slice(0, displaySlotLimit).forEach(layer => {\n    layer.forEach(segment => {\n      if (\n        segment.startDayIndex <= dayIndex &&\n        segment.endDayIndex >= dayIndex &&\n        segmentIsHidden.has(segment.id)\n      ) {\n        hiddenSegmentCount++;\n      }\n    });\n  });\n\n  const gapsWithinLimit = gapLayers.filter(\n    layer => layer < displaySlotLimit\n  ).length;\n  const hiddenSegmentsInVisibleLayers = organizedMultiDaySegments\n    .slice(0, displaySlotLimit)\n    .filter(layer =>\n      layer.some(\n        segment =>\n          segment.startDayIndex <= dayIndex &&\n          segment.endDayIndex >= dayIndex &&\n          segmentIsHidden.has(segment.id)\n      )\n    ).length;\n\n  const slotsAfterMultiDayWithinLimit = Math.max(\n    0,\n    displaySlotLimit - Math.max(maxOccupiedLayer + 1, 0)\n  );\n\n  const displayCount = Math.min(\n    timedEventsOnly.length,\n    gapsWithinLimit +\n      slotsAfterMultiDayWithinLimit +\n      hiddenSegmentsInVisibleLayers\n  );\n\n  const displayEvents = timedEventsOnly.slice(0, displayCount);\n  const hiddenEventsCount =\n    hiddenSegmentCount + (timedEventsOnly.length - displayCount);\n  const maskHiddenOverlayRows =\n    hasMoreEvents && overlayVisibleLayerCount > displaySlotLimit;\n  const hiddenOverlayHeight =\n    (overlayVisibleLayerCount - displaySlotLimit) * rowSpacing;\n\n  const dragEventId =\n    isDragging && dragState.eventId ? dragState.eventId : null;\n  const dragEventInDisplay = dragEventId\n    ? displayEvents.find(event => event.id === dragEventId)\n    : null;\n  const dragEventInTimedOnly =\n    dragEventId && !dragEventInDisplay\n      ? timedEventsOnly.find(event => event.id === dragEventId)\n      : null;\n  const orderedDisplayEvents = dragEventInDisplay\n    ? [\n        dragEventInDisplay,\n        ...displayEvents.filter(event => event.id !== dragEventId),\n      ]\n    : dragEventInTimedOnly && displayEvents.length > 0\n      ? [dragEventInTimedOnly, ...displayEvents.slice(0, -1)]\n      : dragEventInTimedOnly\n        ? [dragEventInTimedOnly]\n        : displayEvents;\n\n  const renderElements: ComponentChild[] = [];\n  let timedEventIndex = 0;\n  const slotsToRender = Math.min(displaySlotLimit, totalSlotsNeeded);\n\n  for (let slot = 0; slot < slotsToRender; slot++) {\n    if (\n      occupiedLayers.has(slot) &&\n      !organizedMultiDaySegments[slot].some(\n        segment =>\n          segment.startDayIndex <= dayIndex &&\n          segment.endDayIndex >= dayIndex &&\n          segmentIsHidden.has(segment.id)\n      )\n    ) {\n      renderElements.push(\n        <div\n          key={`placeholder-layer-${slot}-${day.date.getTime()}`}\n          className='df-shrink-0'\n          style={{\n            height: `${rowSpacing}px`,\n            minHeight: `${rowSpacing}px`,\n          }}\n        />\n      );\n    } else if (timedEventIndex < orderedDisplayEvents.length) {\n      const event = orderedDisplayEvents[timedEventIndex];\n\n      renderElements.push(\n        <CalendarEvent\n          key={`${event.id}-${event.day}-${extractHourFromDate(event.start)}-${timedEventIndex}`}\n          event={event}\n          isAllDay={!!event.allDay}\n          viewType={ViewType.MONTH}\n          isBeingDragged={\n            isDragging &&\n            dragState.eventId === event.id &&\n            dragState.mode === 'move'\n          }\n          calendarRef={calendarRef}\n          hourHeight={72}\n          firstHour={0}\n          onEventUpdate={onEventUpdate}\n          onEventDelete={onEventDelete}\n          onMoveStart={onMoveStart}\n          onResizeStart={onResizeStart}\n          onDetailPanelOpen={onDetailPanelOpen}\n          onEventSelect={onEventSelect}\n          onEventLongPress={onEventLongPress}\n          newlyCreatedEventId={newlyCreatedEventId}\n          selectedEventId={selectedEventId}\n          detailPanelEventId={detailPanelEventId}\n          onDetailPanelToggle={onDetailPanelToggle}\n          useEventDetailPanel={useEventDetailPanel}\n          app={app}\n          isMobile={screenSize !== 'desktop'}\n          enableTouch={enableTouch}\n          appTimeZone={appTimeZone}\n          monthEventHeight={eventHeight}\n        />\n      );\n      timedEventIndex++;\n    }\n  }\n\n  const dayEvents = [\n    ...timedEventsOnly,\n    ...organizedMultiDaySegments\n      .flat()\n      .filter(\n        segment =>\n          segment.startDayIndex <= dayIndex && segment.endDayIndex >= dayIndex\n      )\n      .map(segment => segment.event),\n  ];\n\n  return (\n    <div\n      key={`day-${day.date.getTime()}`}\n      className={cn(monthDayCell, 'df-month-day-cell-surface')}\n      style={{ height: weekHeightPx }}\n      data-other-month={belongsToCurrentMonth ? 'false' : 'true'}\n      data-trailing-border={\n        dayIndex === 6 && !hasScrollbarSpace ? 'false' : 'true'\n      }\n      data-date={createDateString(day.date)}\n      onClick={() => {\n        if (!belongsToCurrentMonth) return;\n        if (onGridDateClick) {\n          onGridDateClick(day.date, dayEvents);\n        } else {\n          onSelectDate?.(day.date);\n        }\n      }}\n      onDblClick={event => {\n        if (onGridDateDoubleClick) {\n          onGridDateDoubleClick(event, day.date, dayEvents);\n        } else {\n          onCreateStart?.(event, day.date);\n        }\n      }}\n      onTouchStart={event => {\n        if (screenSize !== 'mobile' && !enableTouch) return;\n        const touch = event.touches[0];\n        touchStartPosRef.current = { x: touch.clientX, y: touch.clientY };\n\n        longPressTimerRef.current = setTimeout(() => {\n          onCreateStart?.(event as unknown as TouchEvent, day.date);\n          longPressTimerRef.current = null;\n          if (navigator.vibrate) navigator.vibrate(50);\n        }, 500);\n      }}\n      onTouchMove={event => {\n        if (longPressTimerRef.current && touchStartPosRef.current) {\n          const dx = Math.abs(\n            event.touches[0].clientX - touchStartPosRef.current.x\n          );\n          const dy = Math.abs(\n            event.touches[0].clientY - touchStartPosRef.current.y\n          );\n          if (dx > 10 || dy > 10) {\n            clearTimeout(longPressTimerRef.current);\n            longPressTimerRef.current = null;\n          }\n        }\n      }}\n      onTouchEnd={() => {\n        if (longPressTimerRef.current) {\n          clearTimeout(longPressTimerRef.current);\n          longPressTimerRef.current = null;\n        }\n        touchStartPosRef.current = null;\n      }}\n      onContextMenu={event => onContextMenu(event, day.date)}\n    >\n      <div className={monthDateNumberContainer}>\n        {showWeekNumbers && dayIndex === 0 && screenSize !== 'mobile' && (\n          <span className='df-month-week-number'>\n            {getWeekNumber(day.date)}\n          </span>\n        )}\n        <span\n          className={monthDateNumber}\n          data-today={day.isToday ? 'true' : undefined}\n          data-other-month={belongsToCurrentMonth ? undefined : 'true'}\n        >\n          {day.day === 1 && screenSize === 'desktop'\n            ? day.date.toLocaleDateString(locale, {\n                month: 'short',\n                day: 'numeric',\n              })\n            : day.day}\n        </span>\n      </div>\n\n      <div className='df-month-day-cell-content'>\n        {maskHiddenOverlayRows && (\n          <div\n            className='df-month-day-cell-overlay-mask'\n            style={{\n              top: `${displaySlotLimit * rowSpacing}px`,\n              height: `${hiddenOverlayHeight}px`,\n            }}\n          />\n        )}\n        {renderElements}\n\n        {hasMoreEvents && (\n          <div\n            className={cn(monthMoreEvents, 'df-month-day-cell-more-events')}\n            data-layout={screenSize === 'desktop' ? 'desktop' : 'mobile'}\n            onClick={event => {\n              event.stopPropagation();\n              if (onMoreEventsClick) {\n                onMoreEventsClick(day.date);\n              } else {\n                onSelectDate?.(day.date);\n                onChangeView?.(ViewType.DAY);\n              }\n            }}\n          >\n            +{hiddenEventsCount}\n            {screenSize === 'desktop' ? ` ${moreLabel}` : ''}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default WeekDayCell;\n"
  },
  {
    "path": "packages/core/src/components/monthView/__tests__/WeekComponent.test.tsx",
    "content": "import { render, within } from '@testing-library/preact';\nimport { Temporal } from 'temporal-polyfill';\n\nimport WeekComponent from '@/components/monthView/WeekComponent';\nimport { CalendarApp } from '@/core/CalendarApp';\nimport { ViewType } from '@/types';\nimport { generateWeekData } from '@/utils/calendarDataUtils';\n\nconst createAllDayEvent = (\n  id: string,\n  title: string,\n  start: string,\n  end: string = start,\n  calendarId: string = 'work'\n) => ({\n  id,\n  title,\n  allDay: true,\n  calendarId,\n  start: Temporal.PlainDate.from(start),\n  end: Temporal.PlainDate.from(end),\n});\n\nconst createTimedEvent = (\n  id: string,\n  title: string,\n  day: number,\n  hour: number\n) => ({\n  id,\n  title,\n  allDay: false,\n  calendarId: 'work',\n  start: Temporal.PlainDateTime.from({\n    year: 2026,\n    month: 3,\n    day,\n    hour,\n    minute: 0,\n  }),\n  end: Temporal.PlainDateTime.from({\n    year: 2026,\n    month: 3,\n    day,\n    hour: hour + 1,\n    minute: 0,\n  }),\n});\n\nconst createZonedTimedEvent = (\n  id: string,\n  title: string,\n  start: string,\n  end: string\n) => ({\n  id,\n  title,\n  allDay: false,\n  calendarId: 'work',\n  start: Temporal.ZonedDateTime.from(start),\n  end: Temporal.ZonedDateTime.from(end),\n});\n\nconst sortByCalendarId = (\n  a: { calendarId?: string },\n  b: { calendarId?: string }\n) => a.calendarId!.localeCompare(b.calendarId!);\n\nconst createRequiredWeekProps = () => ({\n  detailPanelEventId: null,\n  onDetailPanelToggle: jest.fn(),\n  onCalendarDrop: jest.fn(() => null),\n});\n\ndescribe('WeekComponent', () => {\n  it('renders month multi-day timed event times in app timezone', () => {\n    const event = createZonedTimedEvent(\n      'brainstorm',\n      'Brainstorm',\n      '2026-05-04T12:30:00+00:00[UTC]',\n      '2026-05-04T15:30:00+00:00[UTC]'\n    );\n\n    const app = new CalendarApp({\n      views: [],\n      plugins: [],\n      events: [event],\n      defaultView: ViewType.MONTH,\n      timeZone: 'Australia/Sydney',\n      calendars: [\n        {\n          id: 'work',\n          name: 'Work',\n          colors: {\n            lineColor: '#2563eb',\n            eventColor: '#dbeafe',\n            eventSelectedColor: '#bfdbfe',\n            textColor: '#1e3a8a',\n          },\n        },\n      ],\n    });\n\n    const calendarRef = { current: document.createElement('div') } as {\n      current: HTMLDivElement;\n    };\n\n    const { container } = render(\n      <WeekComponent\n        currentMonth='May'\n        currentYear={2026}\n        newlyCreatedEventId={null}\n        screenSize='desktop'\n        isScrolling={false}\n        isDragging={false}\n        showWeekNumbers={false}\n        item={{\n          index: 0,\n          weekData: generateWeekData(new Date(2026, 4, 4)),\n          top: 0,\n          height: 113,\n        }}\n        weekHeight={113}\n        events={[event]}\n        dragState={{\n          active: false,\n          mode: null,\n          eventId: null,\n          targetDate: null,\n          startDate: null,\n          endDate: null,\n        }}\n        calendarRef={calendarRef}\n        onEventUpdate={jest.fn()}\n        onEventDelete={jest.fn()}\n        onDetailPanelOpen={jest.fn()}\n        {...createRequiredWeekProps()}\n        app={app}\n        appTimeZone='Australia/Sydney'\n      />\n    );\n\n    expect(container.textContent).toContain('22:30');\n    expect(container.textContent).toContain('ends 01:30');\n    expect(container.textContent).not.toContain('12:30');\n    expect(container.textContent).not.toContain('ends 15:30');\n  });\n\n  it('lets each month cell decide independently whether to show 3 rows plus more or 4 rows', () => {\n    const onEventUpdate = jest.fn();\n    const onEventDelete = jest.fn();\n    const onDetailPanelOpen = jest.fn();\n\n    const events = [\n      createAllDayEvent('a', 'Event A', '2026-03-12', '2026-03-14'),\n      createAllDayEvent('b', 'Event B', '2026-03-12'),\n      createAllDayEvent('c', 'Event C', '2026-03-12'),\n      createAllDayEvent('d', 'Event D', '2026-03-12'),\n      createTimedEvent('t1', 'Timed 1', 13, 9),\n      createTimedEvent('t2', 'Timed 2', 13, 10),\n      createTimedEvent('t3', 'Timed 3', 13, 11),\n      createTimedEvent('t4', 'Timed 4', 13, 12),\n    ];\n\n    const app = new CalendarApp({\n      views: [],\n      plugins: [],\n      events,\n      defaultView: ViewType.MONTH,\n      calendars: [\n        {\n          id: 'work',\n          name: 'Work',\n          colors: {\n            lineColor: '#2563eb',\n            eventColor: '#dbeafe',\n            eventSelectedColor: '#bfdbfe',\n            textColor: '#1e3a8a',\n          },\n        },\n      ],\n    });\n\n    const calendarRef = {\n      current: document.createElement('div'),\n    } as { current: HTMLDivElement };\n\n    const { container } = render(\n      <WeekComponent\n        currentMonth='March'\n        currentYear={2026}\n        newlyCreatedEventId={null}\n        screenSize='desktop'\n        isScrolling={false}\n        isDragging={false}\n        showWeekNumbers={false}\n        item={{\n          index: 0,\n          weekData: generateWeekData(new Date(2026, 2, 9)),\n          top: 0,\n          height: 113,\n        }}\n        weekHeight={113}\n        events={events}\n        dragState={{\n          active: false,\n          mode: null,\n          eventId: null,\n          targetDate: null,\n          startDate: null,\n          endDate: null,\n        }}\n        calendarRef={calendarRef}\n        onEventUpdate={onEventUpdate}\n        onEventDelete={onEventDelete}\n        onDetailPanelOpen={onDetailPanelOpen}\n        {...createRequiredWeekProps()}\n        app={app}\n      />\n    );\n\n    const march12Cell = container.querySelector('[data-date=\"2026-03-12\"]');\n    const march13Cell = container.querySelector('[data-date=\"2026-03-13\"]');\n\n    expect(march12Cell).not.toBeNull();\n    expect(march13Cell).not.toBeNull();\n    expect(\n      within(march12Cell as HTMLElement).queryByText(/\\+\\d+ more/)\n    ).toBeNull();\n    expect(\n      within(march13Cell as HTMLElement).getByText('+2 more')\n    ).toBeTruthy();\n  });\n\n  it('ensures maxSlotsWithMore is always less than maxSlots when more events exist', () => {\n    const events = [\n      createAllDayEvent('a', 'Event A', '2026-03-12'),\n      createAllDayEvent('b', 'Event B', '2026-03-12'),\n      createAllDayEvent('c', 'Event C', '2026-03-12'),\n      createAllDayEvent('d', 'Event D', '2026-03-12'),\n      createAllDayEvent('e', 'Event E', '2026-03-12'),\n    ];\n\n    const app = new CalendarApp({\n      views: [],\n      plugins: [],\n      events,\n      defaultView: ViewType.MONTH,\n      calendars: [\n        {\n          id: 'work',\n          name: 'Work',\n          colors: {\n            lineColor: '#2563eb',\n            eventColor: '#dbeafe',\n            eventSelectedColor: '#bfdbfe',\n            textColor: '#1e3a8a',\n          },\n        },\n      ],\n    });\n\n    const calendarRef = { current: document.createElement('div') } as {\n      current: HTMLDivElement;\n    };\n\n    // Set weekHeight such that availableHeight is 80px.\n    // 80 / 17 = 4.7, floor = 4. So maxSlots = 4.\n    // (80 - 20) / 17 = 3.5, floor = 3. So maxSlotsWithMoreRaw = 3.\n    // We want to verify that maxSlotsWithMore = min(3, 4-1) = 3, not 4.\n    const weekHeight = 80 + 33; // 113\n\n    const { container } = render(\n      <WeekComponent\n        currentMonth='March'\n        currentYear={2026}\n        newlyCreatedEventId={null}\n        screenSize='desktop'\n        isScrolling={false}\n        isDragging={false}\n        showWeekNumbers={false}\n        item={{\n          index: 0,\n          weekData: generateWeekData(new Date(2026, 2, 9)),\n          top: 0,\n          height: weekHeight,\n        }}\n        weekHeight={weekHeight}\n        events={events}\n        dragState={{\n          active: false,\n          mode: null,\n          eventId: null,\n          targetDate: null,\n          startDate: null,\n          endDate: null,\n        }}\n        calendarRef={calendarRef}\n        onEventUpdate={jest.fn()}\n        onEventDelete={jest.fn()}\n        onDetailPanelOpen={jest.fn()}\n        {...createRequiredWeekProps()}\n        app={app}\n      />\n    );\n\n    const march12Cell = container.querySelector('[data-date=\"2026-03-12\"]');\n    expect(march12Cell).not.toBeNull();\n\n    // It should show \"+ 2 more\" because it should only show 3 events (5 total - 3 shown = 2 more)\n    // If it incorrectly showed 4 events, it would say \"+ 1 more\"\n    expect(\n      within(march12Cell as HTMLElement).getByText('+2 more')\n    ).toBeTruthy();\n  });\n\n  it('hides multi-day events on all days if hidden on any day (consistent overflow)', () => {\n    const events = [\n      createAllDayEvent('a', 'Event A', '2026-03-12', '2026-03-13'),\n      createAllDayEvent('b', 'Event B', '2026-03-12', '2026-03-13'),\n      createAllDayEvent('c', 'Event C', '2026-03-12', '2026-03-13'),\n      createAllDayEvent('d', 'Event D', '2026-03-12', '2026-03-13'),\n      createTimedEvent('t1', 'Timed 1', 12, 9), // Adding a 5th event on Mar 12 only\n    ];\n\n    const app = new CalendarApp({\n      views: [],\n      plugins: [],\n      events,\n      defaultView: ViewType.MONTH,\n      calendars: [\n        {\n          id: 'work',\n          name: 'Work',\n          colors: {\n            lineColor: '#2563eb',\n            eventColor: '#dbeafe',\n            eventSelectedColor: '#bfdbfe',\n            textColor: '#1e3a8a',\n          },\n        },\n      ],\n    });\n\n    const calendarRef = { current: document.createElement('div') } as {\n      current: HTMLDivElement;\n    };\n\n    // Set weekHeight such that maxSlots = 4 and maxSlotsWithMore = 3\n    // availableHeight = 80: floor(80/17) = 4, floor((80-20)/17) = 3\n    const weekHeight = 80 + 33; // 113\n\n    const { container } = render(\n      <WeekComponent\n        currentMonth='March'\n        currentYear={2026}\n        newlyCreatedEventId={null}\n        screenSize='desktop'\n        isScrolling={false}\n        isDragging={false}\n        showWeekNumbers={false}\n        item={{\n          index: 0,\n          weekData: generateWeekData(new Date(2026, 2, 9)),\n          top: 0,\n          height: weekHeight,\n        }}\n        weekHeight={weekHeight}\n        events={events}\n        dragState={{\n          active: false,\n          mode: null,\n          eventId: null,\n          targetDate: null,\n          startDate: null,\n          endDate: null,\n        }}\n        calendarRef={calendarRef}\n        onEventUpdate={jest.fn()}\n        onEventDelete={jest.fn()}\n        onDetailPanelOpen={jest.fn()}\n        {...createRequiredWeekProps()}\n        app={app}\n      />\n    );\n\n    const march12Cell = container.querySelector('[data-date=\"2026-03-12\"]');\n    const march13Cell = container.querySelector('[data-date=\"2026-03-13\"]');\n\n    // Mar 12 has 5 events total. Limit is 3. Shows \"+ 2 more\".\n    expect(\n      within(march12Cell as HTMLElement).getByText('+2 more')\n    ).toBeTruthy();\n\n    // Mar 13 has 4 events total. Normally it can show all 4 (limit is 4).\n    // But since Event D is hidden on Mar 12, it should also be hidden on Mar 13.\n    // So Mar 13 should switch to \"more\" mode (limit 3) and show \"+ 1 more\".\n    expect(\n      within(march13Cell as HTMLElement).getByText('+1 more')\n    ).toBeTruthy();\n\n    // Verify Event D is NOT in the document (it was the 4th all-day event, should be hidden)\n    expect(container.querySelector('[data-event-id=\"d\"]')).toBeNull();\n    // Verify Event A is in the document (it should be visible)\n    expect(container.querySelector('[data-event-id=\"a\"]')).not.toBeNull();\n  });\n\n  it('keeps same-calendar all-day events grouped in MonthView when a sort comparator is provided', () => {\n    const events = [\n      createAllDayEvent('a', 'Event A', '2026-03-12', '2026-03-15', 'a-cal'),\n      createAllDayEvent('b', 'Event B', '2026-03-12', '2026-03-12', 'b-cal'),\n      createAllDayEvent('c', 'Event C', '2026-03-12', '2026-03-12', 'a-cal'),\n    ];\n\n    const app = new CalendarApp({\n      views: [],\n      plugins: [],\n      events,\n      defaultView: ViewType.MONTH,\n      allDaySortComparator: sortByCalendarId,\n      calendars: [\n        {\n          id: 'a-cal',\n          name: 'A',\n          colors: {\n            lineColor: '#2563eb',\n            eventColor: '#dbeafe',\n            eventSelectedColor: '#bfdbfe',\n            textColor: '#1e3a8a',\n          },\n        },\n        {\n          id: 'b-cal',\n          name: 'B',\n          colors: {\n            lineColor: '#16a34a',\n            eventColor: '#dcfce7',\n            eventSelectedColor: '#bbf7d0',\n            textColor: '#166534',\n          },\n        },\n      ],\n    });\n\n    const calendarRef = {\n      current: document.createElement('div'),\n    } as { current: HTMLDivElement };\n\n    const { getByText } = render(\n      <WeekComponent\n        currentMonth='March'\n        currentYear={2026}\n        newlyCreatedEventId={null}\n        screenSize='desktop'\n        isScrolling={false}\n        isDragging={false}\n        showWeekNumbers={false}\n        item={{\n          index: 0,\n          weekData: generateWeekData(new Date(2026, 2, 9)),\n          top: 0,\n          height: 119,\n        }}\n        weekHeight={119}\n        events={events}\n        dragState={{\n          active: false,\n          mode: null,\n          eventId: null,\n          targetDate: null,\n          startDate: null,\n          endDate: null,\n        }}\n        calendarRef={calendarRef}\n        onEventUpdate={jest.fn()}\n        onEventDelete={jest.fn()}\n        onDetailPanelOpen={jest.fn()}\n        {...createRequiredWeekProps()}\n        app={app}\n      />\n    );\n\n    const eventA = getByText('Event A');\n    const eventB = getByText('Event B');\n    const eventC = getByText('Event C');\n\n    expect(eventA.compareDocumentPosition(eventC)).toBe(\n      Node.DOCUMENT_POSITION_FOLLOWING\n    );\n    expect(eventC.compareDocumentPosition(eventB)).toBe(\n      Node.DOCUMENT_POSITION_FOLLOWING\n    );\n  });\n\n  it('renders the dragged visible event first in the month cell during drag preview', () => {\n    const events = [\n      createTimedEvent('a', 'Event A', 12, 9),\n      createTimedEvent('b', 'Event B', 12, 10),\n      createTimedEvent('c', 'Event C', 12, 11),\n    ];\n\n    const app = new CalendarApp({\n      views: [],\n      plugins: [],\n      events,\n      defaultView: ViewType.MONTH,\n      calendars: [\n        {\n          id: 'work',\n          name: 'Work',\n          colors: {\n            lineColor: '#2563eb',\n            eventColor: '#dbeafe',\n            eventSelectedColor: '#bfdbfe',\n            textColor: '#1e3a8a',\n          },\n        },\n      ],\n    });\n\n    const calendarRef = {\n      current: document.createElement('div'),\n    } as { current: HTMLDivElement };\n\n    const { container } = render(\n      <WeekComponent\n        currentMonth='March'\n        currentYear={2026}\n        newlyCreatedEventId={null}\n        screenSize='desktop'\n        isScrolling={false}\n        isDragging={true}\n        showWeekNumbers={false}\n        item={{\n          index: 0,\n          weekData: generateWeekData(new Date(2026, 2, 9)),\n          top: 0,\n          height: 140,\n        }}\n        weekHeight={140}\n        events={events}\n        dragState={{\n          active: true,\n          mode: 'move',\n          eventId: 'b',\n          targetDate: new Date(2026, 2, 12),\n          startDate: new Date(2026, 2, 12),\n          endDate: new Date(2026, 2, 12, 1),\n        }}\n        calendarRef={calendarRef}\n        onEventUpdate={jest.fn()}\n        onEventDelete={jest.fn()}\n        onDetailPanelOpen={jest.fn()}\n        {...createRequiredWeekProps()}\n        app={app}\n      />\n    );\n\n    const march12Cell = container.querySelector('[data-date=\"2026-03-12\"]');\n    expect(march12Cell).not.toBeNull();\n\n    const renderedEvents = Array.from(\n      (march12Cell as HTMLElement).querySelectorAll('[data-event-id]')\n    ).map(node => (node as HTMLElement).dataset.eventId);\n\n    expect(renderedEvents.slice(0, 3)).toEqual(['b', 'a', 'c']);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/monthView/util.tsx",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport {\n  CalendarDays,\n  Gift,\n  Heart,\n  MapPin,\n  Star,\n} from '@/components/common/Icons';\nimport { eventIcon } from '@/styles/classNames';\nimport { Event } from '@/types';\nimport { daysDifference, temporalToVisualDate } from '@/utils';\nimport { createAllDayDisplayComparator } from '@/utils/allDaySort';\nimport { extractHourFromDate } from '@/utils/helpers';\nimport { logger } from '@/utils/logger';\n\nexport interface MultiDayEventSegment {\n  id: string;\n  originalEventId: string;\n  event: Event;\n  startDayIndex: number;\n  endDayIndex: number;\n  segmentType:\n    | 'start'\n    | 'middle'\n    | 'end'\n    | 'single'\n    | 'start-week-end'\n    | 'end-week-start';\n  totalDays: number;\n  segmentIndex: number;\n  isFirstSegment: boolean;\n  isLastSegment: boolean;\n  yPosition?: number;\n}\n\nexport interface MonthDayLayoutData {\n  totalSlotsNeeded: number;\n  hasMore: boolean;\n  limit: number;\n  timedEventsOnly: Event[];\n  gapLayers: number[];\n  occupiedLayers: Set<number>;\n  maxOccupiedLayer: number;\n  segmentIsHidden: Set<string>;\n}\n\nconst ROW_HEIGHT = 16;\n\nexport const getEventIcon = (event: Event) => {\n  if (event.icon === false) return null;\n  if (event.icon !== undefined && typeof event.icon !== 'boolean') {\n    return event.icon;\n  }\n\n  const title = event.title.toLowerCase();\n\n  if (\n    title.includes('holiday') ||\n    title.includes('vacation') ||\n    title.includes('假期')\n  ) {\n    return <Gift className={eventIcon} />;\n  }\n  if (\n    title.includes('birthday') ||\n    title.includes('anniversary') ||\n    title.includes('生日')\n  ) {\n    return <Heart className={eventIcon} />;\n  }\n  if (\n    title.includes('conference') ||\n    title.includes('meeting') ||\n    title.includes('会议') ||\n    title.includes('研讨')\n  ) {\n    return <Star className={eventIcon} />;\n  }\n  if (\n    title.includes('trip') ||\n    title.includes('travel') ||\n    title.includes('旅行')\n  ) {\n    return <MapPin className={eventIcon} />;\n  }\n\n  return <CalendarDays className={eventIcon} />;\n};\n\n// Per-event cache for analyzeMultiDayEventsForWeek. Keyed by event reference,\n// inner key encodes the week context. During drag/resize 99%+ of events keep\n// their references so this turns the per-tick scan from O(N) date math into\n// nearly free Map lookups.\nconst weekEventSegmentsCache = new WeakMap<\n  Event,\n  Map<string, MultiDayEventSegment[]>\n>();\n\n// Analyze multi-day events and generate segments for the current week (supports all-day events and multi-day regular events)\nexport const analyzeMultiDayEventsForWeek = (\n  events: Event[],\n  weekStart: Date,\n  daysInWeek: number = 7,\n  secondaryTimeZone?: string\n): MultiDayEventSegment[] => {\n  const segments: MultiDayEventSegment[] = [];\n\n  const weekStartMs = weekStart.getTime();\n  const contextKey = `${weekStartMs}|${daysInWeek}|${secondaryTimeZone ?? ''}`;\n\n  // Get the date range of the current week\n  const weekEnd = new Date(weekStart);\n  weekEnd.setDate(weekStart.getDate() + (daysInWeek - 1));\n  weekEnd.setHours(23, 59, 59, 999);\n\n  events.forEach(event => {\n    // Cache lookup — unchanged refs reuse computed segments\n    let perEvent = weekEventSegmentsCache.get(event);\n    if (perEvent) {\n      const cached = perEvent.get(contextKey);\n      if (cached) {\n        for (let i = 0; i < cached.length; i++) segments.push(cached[i]);\n        return;\n      }\n    }\n    const eventOutput: MultiDayEventSegment[] = [];\n    const pushSegment = (seg: MultiDayEventSegment) => {\n      eventOutput.push(seg);\n      segments.push(seg);\n    };\n    const finishEvent = () => {\n      if (!perEvent) {\n        perEvent = new Map();\n        weekEventSegmentsCache.set(event, perEvent);\n      }\n      perEvent.set(contextKey, eventOutput);\n    };\n    // Use start and end as the event's start and end times\n    const eventStartFull = temporalToVisualDate(event.start, secondaryTimeZone);\n    const eventEndFull = event.end\n      ? temporalToVisualDate(event.end, secondaryTimeZone)\n      : eventStartFull;\n\n    // Get the date portion\n    const eventStartDate = new Date(eventStartFull);\n    eventStartDate.setHours(0, 0, 0, 0);\n    const eventEndDate = new Date(eventEndFull);\n    eventEndDate.setHours(0, 0, 0, 0);\n\n    // For regular events, if the end time is midnight 00:00 and duration is less than 24 hours,\n    // adjust the end date to the same day as the start date to avoid misidentifying as a multi-day event\n    let adjustedEventEndDate = new Date(eventEndDate);\n    if (!event.allDay) {\n      const endHasTime =\n        eventEndFull.getHours() !== 0 ||\n        eventEndFull.getMinutes() !== 0 ||\n        eventEndFull.getSeconds() !== 0;\n      if (!endHasTime) {\n        // End time is 00:00:00, check duration\n        const durationMs = eventEndFull.getTime() - eventStartFull.getTime();\n        const ONE_DAY_MS = 24 * 60 * 60 * 1000;\n        if (durationMs > 0 && durationMs < ONE_DAY_MS) {\n          // Duration is less than 24 hours, set end date to previous day\n          adjustedEventEndDate = new Date(eventEndDate);\n          adjustedEventEndDate.setDate(adjustedEventEndDate.getDate() - 1);\n        }\n      }\n    }\n\n    // Check if it spans multiple days (using adjusted end date)\n    const isMultiDay = daysDifference(eventStartDate, adjustedEventEndDate) > 0;\n\n    // For single-day all-day events, also create segment for display in WeekView's all-day area\n    if (!isMultiDay && event.allDay) {\n      // Check if event is within the current week\n      if (eventStartDate < weekStart || eventStartDate > weekEnd) {\n        finishEvent();\n        return;\n      }\n\n      // Calculate dayIndex within the current week (0=Monday, 6=Sunday)\n      const dayIndex = Math.floor(\n        (eventStartDate.getTime() - weekStart.getTime()) / (24 * 60 * 60 * 1000)\n      );\n\n      if (dayIndex >= 0 && dayIndex <= daysInWeek - 1) {\n        pushSegment({\n          id: `${event.id}-week-${weekStart.getTime()}`,\n          originalEventId: event.id,\n          event,\n          startDayIndex: dayIndex,\n          endDayIndex: dayIndex,\n          segmentType: 'single',\n          totalDays: 1,\n          segmentIndex: 0,\n          isFirstSegment: true,\n          isLastSegment: true,\n        });\n      }\n      finishEvent();\n      return;\n    }\n\n    // Only process multi-day events (all-day or regular)\n    if (!isMultiDay) {\n      finishEvent();\n      return;\n    }\n\n    // For all-day events, set end time to end of day\n    // For regular events, if end time is not midnight 00:00, that day should be included; if 00:00, subtract 1ms to point to previous day\n    const eventStart = eventStartDate;\n    let eventEnd: Date;\n    if (event.allDay) {\n      eventEnd = new Date(eventEndDate);\n      eventEnd.setHours(23, 59, 59, 999);\n    } else {\n      // For regular events, if original end time's hours, minutes, and seconds are all 0, the event ended at the start of the day\n      // In this case, the end date should be decreased by 1 day\n      const endHasTime =\n        eventEndFull.getHours() !== 0 ||\n        eventEndFull.getMinutes() !== 0 ||\n        eventEndFull.getSeconds() !== 0;\n      if (endHasTime) {\n        // Has specific time, use end of day\n        eventEnd = new Date(eventEndDate);\n        eventEnd.setHours(23, 59, 59, 999);\n      } else {\n        // No specific time (00:00:00), subtract 1 millisecond to point to previous day\n        eventEnd = new Date(eventEndDate);\n        eventEnd.setTime(eventEnd.getTime() - 1);\n      }\n    }\n\n    // Check if event intersects with the current week\n    if (eventEnd < weekStart || eventStart > weekEnd) {\n      finishEvent();\n      return;\n    }\n\n    // Calculate actual start and end dates within the current week\n    const weekEventStart = eventStart < weekStart ? weekStart : eventStart;\n    const weekEventEnd = eventEnd > weekEnd ? weekEnd : eventEnd;\n\n    // Calculate weekday index for start and end (0=Monday, 6=Sunday)\n    const startDayIndex = Math.max(\n      0,\n      Math.floor(\n        (weekEventStart.getTime() - weekStart.getTime()) / (24 * 60 * 60 * 1000)\n      )\n    );\n    const endDayIndex = Math.min(\n      daysInWeek - 1,\n      Math.floor(\n        (weekEventEnd.getTime() - weekStart.getTime()) / (24 * 60 * 60 * 1000)\n      )\n    );\n\n    // Determine segment type\n    const isFirstSegment = eventStart >= weekStart;\n    const isLastSegment = eventEnd <= weekEnd;\n    const isWeekBoundary =\n      startDayIndex === 0 || endDayIndex === daysInWeek - 1;\n\n    let segmentType: MultiDayEventSegment['segmentType'];\n\n    if (isFirstSegment && isLastSegment) {\n      segmentType = 'single';\n    } else if (isFirstSegment) {\n      segmentType =\n        isWeekBoundary && endDayIndex === daysInWeek - 1\n          ? 'start-week-end'\n          : 'start';\n    } else if (isLastSegment) {\n      segmentType =\n        isWeekBoundary && startDayIndex === 0 ? 'end-week-start' : 'end';\n    } else {\n      segmentType = 'middle';\n    }\n\n    const totalDays = daysDifference(eventStart, eventEnd) + 1;\n\n    pushSegment({\n      id: `${event.id}-week-${weekStart.getTime()}`,\n      originalEventId: event.id,\n      event,\n      startDayIndex,\n      endDayIndex,\n      segmentType,\n      totalDays,\n      segmentIndex: 0, // Can be calculated as needed\n      isFirstSegment,\n      isLastSegment,\n    });\n    finishEvent();\n  });\n\n  return segments;\n};\n\nexport const organizeMultiDaySegments = (\n  multiDaySegments: MultiDayEventSegment[],\n  comparator?: (a: Event, b: Event) => number\n) => {\n  const compareEvents = comparator\n    ? comparator\n    : createAllDayDisplayComparator(\n        multiDaySegments.map(segment => segment.event),\n        (() => {\n          const calendarOrder = new Map<string | undefined, number>();\n          multiDaySegments.forEach(segment => {\n            const id = segment.event.calendarId;\n            if (!calendarOrder.has(id)) {\n              calendarOrder.set(id, calendarOrder.size);\n            }\n          });\n\n          return (a: Event, b: Event) =>\n            (calendarOrder.get(a.calendarId) ?? 0) -\n            (calendarOrder.get(b.calendarId) ?? 0);\n        })()\n      );\n\n  const sortedSegments = [...multiDaySegments].toSorted((a, b) => {\n    if (compareEvents) {\n      const displayPriority = compareEvents(a.event, b.event);\n      if (displayPriority !== 0) {\n        return displayPriority;\n      }\n    }\n\n    const aDays = a.endDayIndex - a.startDayIndex + 1;\n    const bDays = b.endDayIndex - b.startDayIndex + 1;\n\n    if (a.startDayIndex > b.startDayIndex) {\n      return 1;\n    }\n\n    if (aDays !== bDays) {\n      return bDays - aDays;\n    }\n\n    return a.startDayIndex - b.startDayIndex;\n  });\n\n  const segmentsWithPosition: MultiDayEventSegment[] = [];\n\n  sortedSegments.forEach(segment => {\n    let yPosition = 0;\n    let positionFound = false;\n\n    while (!positionFound) {\n      let hasConflict = false;\n      for (const existingSegment of segmentsWithPosition) {\n        const yConflict =\n          Math.abs((existingSegment.yPosition ?? 0) - yPosition) < ROW_HEIGHT;\n        const timeConflict = !(\n          segment.endDayIndex < existingSegment.startDayIndex ||\n          segment.startDayIndex > existingSegment.endDayIndex\n        );\n        if (yConflict && timeConflict) {\n          hasConflict = true;\n          break;\n        }\n      }\n\n      if (hasConflict) {\n        yPosition += ROW_HEIGHT;\n      } else {\n        positionFound = true;\n      }\n    }\n\n    segmentsWithPosition.push({ ...segment, yPosition });\n  });\n\n  const layers: MultiDayEventSegment[][] = [];\n\n  segmentsWithPosition.forEach(segment => {\n    const layerIndex = Math.floor((segment.yPosition ?? 0) / ROW_HEIGHT);\n\n    if (!layers[layerIndex]) {\n      layers[layerIndex] = [];\n    }\n\n    layers[layerIndex].push(segment);\n  });\n\n  layers.forEach(layer => {\n    layer.sort((a, b) => {\n      if (compareEvents) {\n        const displayPriority = compareEvents(a.event, b.event);\n        if (displayPriority !== 0) {\n          return displayPriority;\n        }\n      }\n\n      return a.startDayIndex - b.startDayIndex;\n    });\n  });\n\n  return layers;\n};\n\nexport const constructRenderEvents = (\n  events: Event[],\n  weekStart: Date,\n  appTimeZone?: string\n): Event[] => {\n  const renderEvents: Event[] = [];\n  const weekEnd = new Date(weekStart);\n  weekEnd.setDate(weekEnd.getDate() + 6);\n  weekEnd.setHours(23, 59, 59, 999);\n\n  events.forEach(event => {\n    if (!event.start || !event.end) {\n      logger.warn('Event missing start or end date:', event);\n      return;\n    }\n\n    const start = temporalToVisualDate(event.start, appTimeZone);\n    const end = event.end\n      ? temporalToVisualDate(event.end, appTimeZone)\n      : start;\n    const startDate = new Date(start);\n    startDate.setHours(0, 0, 0, 0);\n    const endDate = new Date(end);\n    endDate.setHours(0, 0, 0, 0);\n\n    let adjustedEndDate = new Date(endDate);\n    if (!event.allDay) {\n      const endHasTime =\n        end.getHours() !== 0 ||\n        end.getMinutes() !== 0 ||\n        end.getSeconds() !== 0;\n      if (!endHasTime) {\n        const durationMs = end.getTime() - start.getTime();\n        const ONE_DAY_MS = 24 * 60 * 60 * 1000;\n        if (durationMs > 0 && durationMs < ONE_DAY_MS) {\n          adjustedEndDate = new Date(endDate);\n          adjustedEndDate.setDate(adjustedEndDate.getDate() - 1);\n        }\n      }\n    }\n\n    const isMultiDay =\n      startDate.toDateString() !== adjustedEndDate.toDateString();\n\n    if (isMultiDay && !event.allDay) {\n      return;\n    }\n\n    if (isMultiDay && event.allDay) {\n      let current = new Date(start);\n      if (current < weekStart) {\n        current = new Date(weekStart);\n        current.setHours(0, 0, 0, 0);\n      }\n\n      const loopEnd = end > weekEnd ? weekEnd : end;\n\n      for (\n        let t = start.getTime();\n        t <= loopEnd.getTime();\n        t += 24 * 60 * 60 * 1000\n      ) {\n        const currentLoopDate = new Date(t);\n        if (currentLoopDate < weekStart) continue;\n\n        const currentTemporal = Temporal.PlainDate.from({\n          year: currentLoopDate.getFullYear(),\n          month: currentLoopDate.getMonth() + 1,\n          day: currentLoopDate.getDate(),\n        });\n\n        renderEvents.push({\n          ...event,\n          start: currentTemporal,\n          end: currentTemporal,\n          day: current.getDay(),\n        });\n      }\n    } else {\n      renderEvents.push({\n        ...event,\n        start: event.start,\n        end: event.end,\n        day: start.getDay(),\n      });\n    }\n  });\n\n  return renderEvents;\n};\n\nexport const sortDayEvents = (events: Event[]): Event[] =>\n  [...events].toSorted((a, b) => {\n    if (a.allDay !== b.allDay) {\n      return a.allDay ? -1 : 1;\n    }\n\n    if (a.allDay && b.allDay) return 0;\n\n    return extractHourFromDate(a.start) - extractHourFromDate(b.start);\n  });\n\nexport const createDateString = (date: Date): string => {\n  const year = date.getFullYear();\n  const month = String(date.getMonth() + 1).padStart(2, '0');\n  const day = String(date.getDate()).padStart(2, '0');\n  return `${year}-${month}-${day}`;\n};\n\ntype RegularEventSegment = {\n  dayIndex: number;\n  startHour: number;\n  endHour: number;\n  isFirst: boolean;\n  isLast: boolean;\n};\n\n// Per-event cache: keys combine (weekStart, daysInWeek, tz) so identical event\n// references reuse results across calls within the same and subsequent renders.\n// WeakMap lets entries get GC'd when events are replaced (immutable updates).\nconst regularSegmentsCache = new WeakMap<\n  Event,\n  Map<string, RegularEventSegment[]>\n>();\n\n// Check if a regular event spans multiple days and return time segment information for each day\nexport const analyzeMultiDayRegularEvent = (\n  event: Event,\n  weekStart: Date,\n  daysInWeek: number = 7,\n  secondaryTimeZone?: string\n): RegularEventSegment[] => {\n  if (event.allDay) return [];\n\n  const cacheKey = `${weekStart.getTime()}|${daysInWeek}|${secondaryTimeZone ?? ''}`;\n  let perEvent = regularSegmentsCache.get(event);\n  if (perEvent) {\n    const cached = perEvent.get(cacheKey);\n    if (cached) return cached;\n  }\n\n  const eventStart = temporalToVisualDate(event.start, secondaryTimeZone);\n  const eventEnd = event.end\n    ? temporalToVisualDate(event.end, secondaryTimeZone)\n    : eventStart;\n\n  // Get the date portion (without time)\n  const startDate = new Date(eventStart);\n  startDate.setHours(0, 0, 0, 0);\n  const endDate = new Date(eventEnd);\n  endDate.setHours(0, 0, 0, 0);\n\n  const cacheResult = (value: RegularEventSegment[]) => {\n    if (!perEvent) {\n      perEvent = new Map();\n      regularSegmentsCache.set(event, perEvent);\n    }\n    perEvent.set(cacheKey, value);\n    return value;\n  };\n\n  // Check if it spans multiple days\n  const daySpan = daysDifference(startDate, endDate);\n  if (daySpan === 0) return cacheResult([]);\n\n  const endHasExplicitTime =\n    eventEnd.getHours() !== 0 ||\n    eventEnd.getMinutes() !== 0 ||\n    eventEnd.getSeconds() !== 0 ||\n    eventEnd.getMilliseconds() !== 0;\n\n  const DAY_IN_MS = 24 * 60 * 60 * 1000;\n  const durationMs = eventEnd.getTime() - eventStart.getTime();\n\n  if (\n    !event.allDay &&\n    daySpan === 1 &&\n    !endHasExplicitTime &&\n    durationMs < DAY_IN_MS\n  ) {\n    return cacheResult([]);\n  }\n\n  const lastDayOffset = endHasExplicitTime ? daySpan : Math.max(0, daySpan - 1);\n\n  // Generate segments for each day\n  const segments: RegularEventSegment[] = [];\n\n  for (let i = 0; i <= lastDayOffset; i++) {\n    const currentDate = new Date(startDate);\n    currentDate.setDate(startDate.getDate() + i);\n\n    // Calculate the index of the current date in the week\n    const dayIndex = Math.floor(\n      (currentDate.getTime() - weekStart.getTime()) / (24 * 60 * 60 * 1000)\n    );\n\n    // Skip dates not in the current week\n    if (dayIndex < 0 || dayIndex > daysInWeek - 1) continue;\n\n    const isFirst = i === 0;\n    const isLast = i === lastDayOffset;\n\n    // Calculate start and end hours for the day\n    const startHour = isFirst\n      ? eventStart.getHours() + eventStart.getMinutes() / 60\n      : 0;\n    const endHour = isLast\n      ? endHasExplicitTime\n        ? eventEnd.getHours() + eventEnd.getMinutes() / 60\n        : 24\n      : 24;\n\n    segments.push({\n      dayIndex,\n      startHour,\n      endHour,\n      isFirst,\n      isLast,\n    });\n  }\n\n  return cacheResult(segments);\n};\n"
  },
  {
    "path": "packages/core/src/components/search/MobileSearchDialog.tsx",
    "content": "import { createPortal } from 'preact/compat';\nimport { useRef, useEffect } from 'preact/hooks';\n\nimport { ArrowLeft, X } from '@/components/common/Icons';\nimport { useLocale } from '@/locale';\nimport { mobileFullscreen } from '@/styles/classNames';\nimport { CalendarSearchEvent } from '@/types/search';\n\nimport SearchResultsList from './SearchResultsList';\n\ninterface MobileSearchDialogProps {\n  isOpen: boolean;\n  onClose: () => void;\n  keyword: string;\n  onSearchChange: (value: string) => void;\n  results: CalendarSearchEvent[];\n  loading: boolean;\n  onResultClick?: (event: CalendarSearchEvent) => void;\n  emptyText?: string | Record<string, string>;\n}\n\nconst MobileSearchDialog = ({\n  isOpen,\n  onClose,\n  keyword,\n  onSearchChange,\n  results,\n  loading,\n  onResultClick,\n  emptyText,\n}: MobileSearchDialogProps) => {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const { t } = useLocale();\n\n  useEffect(() => {\n    if (isOpen) {\n      setTimeout(() => {\n        inputRef.current?.focus();\n      }, 100);\n      document.body.style.overflow = 'hidden';\n    } else {\n      document.body.style.overflow = '';\n    }\n\n    return () => {\n      document.body.style.overflow = '';\n    };\n  }, [isOpen]);\n\n  if (!isOpen || typeof window === 'undefined') return null;\n\n  return createPortal(\n    <div className={mobileFullscreen}>\n      {/* Header with Back button and Search Input */}\n      <div className='df-search-dialog-header'>\n        <button\n          type='button'\n          onClick={onClose}\n          className='df-search-dialog-back-btn'\n        >\n          <ArrowLeft className='df-search-dialog-back-icon' />\n        </button>\n        <div className='df-search-dialog-input-wrap'>\n          <input\n            ref={inputRef}\n            type='text'\n            placeholder={t('search') || 'Search'}\n            value={keyword}\n            onChange={e => {\n              const val = (e.target as HTMLInputElement).value;\n              if (val !== keyword) onSearchChange(val);\n            }}\n            className='df-search-dialog-input'\n          />\n          {keyword && (\n            <button\n              type='button'\n              onClick={() => {\n                if (keyword !== '') onSearchChange('');\n              }}\n              className='df-search-dialog-input-clear'\n            >\n              <X className='df-search-dialog-clear-icon' />\n            </button>\n          )}\n        </div>\n      </div>\n\n      {/* Results List */}\n      <div className='df-search-dialog-results'>\n        <SearchResultsList\n          loading={loading}\n          results={results}\n          keyword={keyword}\n          onResultClick={e => {\n            onResultClick?.(e);\n          }}\n          emptyText={emptyText}\n        />\n      </div>\n    </div>,\n    document.body\n  );\n};\n\nexport default MobileSearchDialog;\n"
  },
  {
    "path": "packages/core/src/components/search/SearchDrawer.tsx",
    "content": "import { CalendarSearchEvent } from '@/types/search';\n\nimport SearchResultsList from './SearchResultsList';\n\ninterface SearchDrawerProps {\n  isOpen: boolean;\n  onClose: () => void;\n  loading: boolean;\n  results: CalendarSearchEvent[];\n  keyword: string;\n  onResultClick?: (event: CalendarSearchEvent) => void;\n  emptyText?: string | Record<string, string>;\n}\n\nconst SearchDrawer = ({\n  isOpen,\n  loading,\n  results,\n  keyword,\n  onResultClick,\n  emptyText,\n}: SearchDrawerProps) => (\n  <div className='df-search-drawer' data-open={isOpen ? 'true' : 'false'}>\n    <div className='df-search-drawer-content'>\n      <SearchResultsList\n        loading={loading}\n        results={results}\n        keyword={keyword}\n        onResultClick={onResultClick}\n        emptyText={emptyText}\n      />\n    </div>\n  </div>\n);\n\nexport default SearchDrawer;\n"
  },
  {
    "path": "packages/core/src/components/search/SearchResultsList.tsx",
    "content": "import { useMemo } from 'preact/hooks';\n\nimport { Loader2 } from '@/components/common/Icons';\nimport { useLocale } from '@/locale/useLocale';\nimport { CalendarSearchEvent } from '@/types/search';\nimport {\n  groupSearchResults,\n  getSearchHeaderInfo,\n  getDateObj,\n  normalizeDate,\n} from '@/utils/searchUtils';\n\ninterface SearchResultsListProps {\n  loading: boolean;\n  results: CalendarSearchEvent[];\n  keyword: string;\n  onResultClick?: (event: CalendarSearchEvent) => void;\n  emptyText?: string | Record<string, string>;\n}\n\nconst SearchIconPlaceholder = () => (\n  <svg\n    className='df-search-results-state-icon'\n    fill='none'\n    viewBox='0 0 24 24'\n    stroke='currentColor'\n  >\n    <path\n      strokeLinecap='round'\n      strokeLinejoin='round'\n      strokeWidth={1}\n      d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'\n    />\n  </svg>\n);\n\nconst SearchResultsList = ({\n  loading,\n  results,\n  keyword,\n  onResultClick,\n  emptyText,\n}: SearchResultsListProps) => {\n  const { t, locale } = useLocale();\n\n  const today = useMemo(() => normalizeDate(new Date()), []);\n\n  const groupedEvents = useMemo(\n    () => groupSearchResults(results, today),\n    [results, today]\n  );\n\n  const getTime = (d: unknown) => getDateObj(d);\n\n  const getEmptyText = () => {\n    if (typeof emptyText === 'string') return emptyText;\n    if (emptyText && typeof emptyText === 'object') {\n      return emptyText[locale] || emptyText['en'] || 'No results found';\n    }\n    return t('noResults') || 'No results found';\n  };\n\n  if (loading) {\n    return (\n      <div className='df-search-results-state'>\n        <Loader2 className='df-search-results-loader' />\n        <span>Loading...</span>\n      </div>\n    );\n  }\n\n  if (results.length === 0) {\n    return keyword ? (\n      <div className='df-search-results-state'>\n        <SearchIconPlaceholder />\n        <span style={{ marginTop: '0.5rem', fontSize: '0.875rem' }}>\n          {getEmptyText()}\n        </span>\n      </div>\n    ) : null;\n  }\n\n  return (\n    <div className='df-search-results'>\n      {groupedEvents.map(group => {\n        const { title, tone } = getSearchHeaderInfo(\n          group.date,\n          today,\n          locale,\n          t\n        );\n\n        return (\n          <div key={group.date.getTime()} className='df-search-results-group'>\n            <h3 className='df-search-results-date-header' data-tone={tone}>\n              {title}\n            </h3>\n            <div className='df-search-results-events'>\n              {group.events.map(event => {\n                const start = getTime(event.start);\n                const end = getTime(event.end);\n\n                const timeOpt: Intl.DateTimeFormatOptions = {\n                  hour: '2-digit',\n                  minute: '2-digit',\n                };\n                const startTimeStr = event.allDay\n                  ? t('allDay') || 'All Day'\n                  : start.toLocaleTimeString(locale, timeOpt);\n                const endTimeStr = event.allDay\n                  ? ''\n                  : end.toLocaleTimeString(locale, timeOpt);\n\n                return (\n                  <div key={event.id}>\n                    <div\n                      className='df-search-results-event'\n                      onClick={() => onResultClick?.(event)}\n                    >\n                      <div className='df-search-results-event-inner'>\n                        <div\n                          className='df-search-results-color-bar'\n                          style={{ backgroundColor: event.color || '#3b82f6' }}\n                        />\n                        <div className='df-search-results-event-content'>\n                          <div className='df-search-results-event-title'>\n                            {event.title}\n                          </div>\n                          <div className='df-search-results-event-time'>\n                            <div>{startTimeStr}</div>\n                            {endTimeStr && (\n                              <div className='df-search-results-end-time'>\n                                {endTimeStr}\n                              </div>\n                            )}\n                          </div>\n                        </div>\n                      </div>\n                    </div>\n                    <div className='df-search-results-divider' />\n                  </div>\n                );\n              })}\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default SearchResultsList;\n"
  },
  {
    "path": "packages/core/src/components/weekView/AllDayRow.tsx",
    "content": "import { RefObject, JSX } from 'preact';\nimport { useEffect, useState, useMemo } from 'preact/hooks';\n\nimport CalendarEventComponent from '@/components/calendarEvent';\nimport { GridContextMenu } from '@/components/contextMenu';\nimport { MultiDayEventSegment } from '@/components/monthView/util';\nimport {\n  allDayRow,\n  allDayLabel,\n  allDayContent,\n  allDayCell,\n  weekDayHeader,\n  weekDayCell,\n  dateNumber,\n} from '@/styles/classNames';\nimport { Event, WeekDayDragState, ViewType, ICalendarApp } from '@/types';\nimport {\n  getEventsForDay,\n  getAllDayEventsForDay,\n  scrollbarTakesSpace,\n} from '@/utils';\n\nimport { CompactHeader } from './CompactHeader';\n\ninterface AllDayRowProps {\n  app: ICalendarApp;\n  weekDaysLabels: string[];\n  mobileWeekDaysLabels: string[];\n  weekDates: Array<{\n    date: number;\n    month: string;\n    fullDate: Date;\n    isToday: boolean;\n  }>;\n  fullWeekDates?: Array<{\n    date: number;\n    month: string;\n    fullDate: Date;\n    isToday: boolean;\n    isCurrent: boolean;\n    dayName: string;\n  }>;\n  isSlidingView?: boolean;\n  mobilePageStart?: Date;\n  currentWeekStart: Date;\n  currentWeekEvents: Event[];\n  gridWidth: string;\n  allDayAreaHeight: number;\n  organizedAllDaySegments: Array<MultiDayEventSegment & { row: number }>;\n  allDayLabelText: string;\n  isMobile: boolean;\n  isTouch: boolean;\n  showAllDay?: boolean;\n  calendarRef: RefObject<HTMLDivElement>;\n  allDayRowRef: RefObject<HTMLDivElement>;\n  topFrozenContentRef: RefObject<HTMLDivElement>;\n  ALL_DAY_HEIGHT: number;\n  HOUR_HEIGHT: number;\n  FIRST_HOUR: number;\n  dragState: WeekDayDragState | null;\n  isDragging: boolean;\n  primaryTzLabel?: string;\n  secondaryTzLabel?: string;\n  secondaryTimeSlots?: string[];\n  handleMoveStart: (e: MouseEvent | TouchEvent, event: Event) => void;\n  handleResizeStart: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  handleEventUpdate: (event: Event) => void;\n  handleEventDelete: (id: string) => void;\n  setDraftEvent: (event: Event | null) => void;\n  setIsDrawerOpen: (isOpen: boolean) => void;\n  onDateChange?: (date: Date) => void;\n  onGridDateClick?: (date: Date, events: Event[]) => void;\n  onGridDateDoubleClick?: (date: Date, events: Event[]) => void;\n  newlyCreatedEventId: string | null;\n  setNewlyCreatedEventId: (id: string | null) => void;\n  selectedEventId: string | null;\n  setSelectedEventId: (id: string | null) => void;\n  detailPanelEventId: string | null;\n  setDetailPanelEventId: (id: string | null) => void;\n  handleCreateAllDayEvent?: (\n    e: MouseEvent | TouchEvent,\n    dayIndex: number\n  ) => void;\n  handleDragOver: (e: DragEvent) => void;\n  handleDrop: (\n    e: DragEvent,\n    date: Date,\n    hour?: number,\n    allDay?: boolean\n  ) => void;\n  useEventDetailPanel?: boolean;\n}\n\nexport const AllDayRow = (props: AllDayRowProps) => {\n  const {\n    app,\n    weekDaysLabels,\n    mobileWeekDaysLabels,\n    weekDates,\n    fullWeekDates,\n    isSlidingView,\n    mobilePageStart,\n    currentWeekStart,\n    currentWeekEvents,\n    gridWidth,\n    allDayAreaHeight,\n    organizedAllDaySegments,\n    allDayLabelText,\n    isMobile,\n    isTouch,\n    showAllDay = true,\n    calendarRef,\n    allDayRowRef,\n    topFrozenContentRef,\n    ALL_DAY_HEIGHT,\n    HOUR_HEIGHT,\n    FIRST_HOUR,\n    dragState,\n    isDragging,\n    primaryTzLabel,\n    secondaryTzLabel,\n    secondaryTimeSlots,\n    handleMoveStart,\n    handleResizeStart,\n    handleEventUpdate,\n    handleEventDelete,\n    setDraftEvent,\n    setIsDrawerOpen,\n    onDateChange,\n    onGridDateClick,\n    onGridDateDoubleClick,\n    newlyCreatedEventId,\n    setNewlyCreatedEventId,\n    selectedEventId,\n    setSelectedEventId,\n    detailPanelEventId,\n    setDetailPanelEventId,\n    handleCreateAllDayEvent,\n    handleDragOver,\n    handleDrop,\n    useEventDetailPanel,\n  } = props;\n  const columnStyle: JSX.CSSProperties = { flexShrink: 0 };\n  const [contextMenu, setContextMenu] = useState<{\n    x: number;\n    y: number;\n    date: Date;\n  } | null>(null);\n  const hasScrollbarSpace = useMemo(() => scrollbarTakesSpace(), []);\n  const hasSecondaryTz = !!secondaryTimeSlots && secondaryTimeSlots.length > 0;\n  // On mobile the time column is too narrow for dual labels — hide secondary TZ display\n  const showSecondaryTz = hasSecondaryTz && !isMobile;\n  const isEditable = app.canMutateFromUI();\n\n  useEffect(() => {\n    if (isEditable) return;\n    setContextMenu(null);\n  }, [isEditable]);\n\n  const handleContextMenu = (e: MouseEvent, dayIndex: number) => {\n    e.preventDefault();\n    if (isMobile || !isEditable) return;\n\n    const date = new Date(currentWeekStart);\n    date.setDate(currentWeekStart.getDate() + dayIndex);\n    date.setHours(0, 0, 0, 0);\n\n    setContextMenu({ x: e.clientX, y: e.clientY, date });\n  };\n\n  return (\n    <div className='df-week-all-day'>\n      {/* Mobile 7-day Header with Segmented Control */}\n      {isSlidingView && fullWeekDates && mobilePageStart && (\n        <CompactHeader\n          app={app}\n          fullWeekDates={fullWeekDates}\n          mobilePageStart={mobilePageStart}\n          onDateChange={onDateChange}\n        />\n      )}\n\n      <div\n        className='df-week-all-day-shell'\n        data-show-all-day={showAllDay ? 'true' : 'false'}\n        onContextMenu={e => e.preventDefault()}\n      >\n        {/* Left Frozen Column - outside scroll area, matching TimeGrid sidebar */}\n        {showAllDay && (\n          <div\n            className='df-week-all-day-side'\n            onContextMenu={e => e.preventDefault()}\n          >\n            {/* Header spacer - flexes to match weekday header height */}\n            <div\n              className='df-week-all-day-side-spacer'\n              data-sliding-view={isSlidingView ? 'true' : 'false'}\n            >\n              {/* Timezone header: secondary LEFT, primary RIGHT */}\n              {showSecondaryTz && (\n                <div className='df-time-column-tz-header df-time-column-tz-header-compact'>\n                  <span className='df-time-column-tz-label'>\n                    {secondaryTzLabel}\n                  </span>\n                  <span className='df-time-column-tz-label'>\n                    {primaryTzLabel}\n                  </span>\n                </div>\n              )}\n            </div>\n            {/* All Day Label */}\n            <div\n              className={`${allDayLabel} df-week-all-day-label`}\n              style={{ minHeight: `${allDayAreaHeight}px` }}\n            >\n              {allDayLabelText}\n            </div>\n          </div>\n        )}\n\n        {/* Top Frozen Content - overflow hidden, content positioned via transform */}\n        <div\n          className='df-week-all-day-content-wrap'\n          style={{\n            scrollbarGutter: 'stable',\n            minHeight: showAllDay\n              ? `${allDayAreaHeight + (isSlidingView ? 0 : 36)}px`\n              : 'auto',\n          }}\n        >\n          <div\n            ref={topFrozenContentRef}\n            className='df-week-all-day-content'\n            style={{\n              width: gridWidth,\n              minWidth: '100%',\n              transform: isSlidingView\n                ? 'translateX(calc(-100% / 3))'\n                : undefined,\n            }}\n          >\n            {/* Weekday titles row */}\n            {!isSlidingView && (\n              <div\n                className={weekDayHeader}\n                data-scrollbar-space={hasScrollbarSpace ? 'true' : 'false'}\n                style={{\n                  marginRight: hasScrollbarSpace ? '-50px' : '0px',\n                  paddingRight: hasScrollbarSpace ? '50px' : '0px',\n                }}\n              >\n                {weekDaysLabels.map((day, i) => (\n                  <div\n                    key={i}\n                    className={`${weekDayCell} df-week-all-day-weekday-cell`}\n                    data-mobile={isMobile ? 'true' : 'false'}\n                    style={columnStyle}\n                  >\n                    {isMobile ? (\n                      <>\n                        <div className='df-week-all-day-weekday-label'>\n                          {mobileWeekDaysLabels[i]}\n                        </div>\n                        <div\n                          className={`${dateNumber} df-week-all-day-date-number df-week-all-day-date-number-mobile`}\n                          data-today={weekDates[i].isToday ? 'true' : undefined}\n                        >\n                          {weekDates[i].date}\n                        </div>\n                      </>\n                    ) : (\n                      <>\n                        <div className='df-week-all-day-weekday-name'>\n                          {day}\n                        </div>\n                        <div\n                          className={`${dateNumber} df-week-all-day-date-number`}\n                          data-today={weekDates[i].isToday ? 'true' : undefined}\n                        >\n                          {weekDates[i].date}\n                        </div>\n                      </>\n                    )}\n                  </div>\n                ))}\n              </div>\n            )}\n\n            {/* All-day event area */}\n            {showAllDay && (\n              <div\n                className={`${allDayRow} df-week-all-day-row`}\n                ref={allDayRowRef}\n                style={{ minHeight: `${allDayAreaHeight}px` }}\n              >\n                <div\n                  className={`${allDayContent} df-week-all-day-row-content`}\n                  data-scrollbar-space={\n                    isMobile || !hasScrollbarSpace ? 'false' : 'true'\n                  }\n                  style={{ minHeight: `${allDayAreaHeight}px` }}\n                  onDragOver={handleDragOver}\n                  onDrop={e => {\n                    const rect = e.currentTarget.getBoundingClientRect();\n                    const relativeX = e.clientX - rect.left;\n                    const dayIndex = Math.floor(\n                      relativeX / (rect.width / weekDaysLabels.length)\n                    );\n\n                    const dropDate = new Date(currentWeekStart);\n                    dropDate.setDate(currentWeekStart.getDate() + dayIndex);\n\n                    handleDrop(e, dropDate, undefined, true);\n                  }}\n                >\n                  {weekDaysLabels.map((_, dayIndex) => {\n                    const dropDate = new Date(currentWeekStart);\n                    dropDate.setDate(currentWeekStart.getDate() + dayIndex);\n                    return (\n                      <div\n                        key={`allday-${dayIndex}`}\n                        className={`${allDayCell} df-week-all-day-cell ${dayIndex === weekDaysLabels.length - 1 && (isMobile || !hasScrollbarSpace) ? 'df-week-all-day-cell-no-border' : ''}`}\n                        style={{\n                          minHeight: `${allDayAreaHeight}px`,\n                          ...columnStyle,\n                        }}\n                        onClick={() => {\n                          const clickedDate = new Date(currentWeekStart);\n                          clickedDate.setDate(\n                            currentWeekStart.getDate() + dayIndex\n                          );\n                          if (onGridDateClick) {\n                            const dayEvents = [\n                              ...getEventsForDay(dayIndex, currentWeekEvents),\n                              ...getAllDayEventsForDay(\n                                dayIndex,\n                                currentWeekEvents,\n                                currentWeekStart\n                              ),\n                            ];\n                            onGridDateClick(clickedDate, dayEvents);\n                          } else {\n                            onDateChange?.(clickedDate);\n                          }\n                        }}\n                        onMouseDown={e =>\n                          handleCreateAllDayEvent?.(e, dayIndex)\n                        }\n                        onDblClick={e => {\n                          const clickedDate = new Date(currentWeekStart);\n                          clickedDate.setDate(\n                            currentWeekStart.getDate() + dayIndex\n                          );\n                          if (onGridDateDoubleClick) {\n                            const dayEvents = [\n                              ...getEventsForDay(dayIndex, currentWeekEvents),\n                              ...getAllDayEventsForDay(\n                                dayIndex,\n                                currentWeekEvents,\n                                currentWeekStart\n                              ),\n                            ];\n                            onGridDateDoubleClick(clickedDate, dayEvents);\n                          } else {\n                            handleCreateAllDayEvent?.(e, dayIndex);\n                          }\n                        }}\n                        onContextMenu={e => handleContextMenu(e, dayIndex)}\n                      />\n                    );\n                  })}\n                  {/* Multi-day event overlay */}\n                  <div className='df-week-all-day-event-layer'>\n                    {organizedAllDaySegments.map(segment => (\n                      <CalendarEventComponent\n                        key={segment.event.id}\n                        event={segment.event}\n                        segment={segment}\n                        segmentIndex={segment.row}\n                        isAllDay={true}\n                        isMultiDay={true}\n                        allDayHeight={ALL_DAY_HEIGHT}\n                        calendarRef={calendarRef}\n                        viewType={ViewType.WEEK}\n                        columnsPerRow={weekDaysLabels.length}\n                        isBeingDragged={\n                          isDragging &&\n                          (dragState as WeekDayDragState)?.eventId ===\n                            segment.event.id &&\n                          (dragState as WeekDayDragState)?.mode === 'move'\n                        }\n                        hourHeight={HOUR_HEIGHT}\n                        firstHour={FIRST_HOUR}\n                        onMoveStart={handleMoveStart}\n                        onResizeStart={handleResizeStart}\n                        onEventUpdate={handleEventUpdate}\n                        onEventDelete={handleEventDelete}\n                        newlyCreatedEventId={newlyCreatedEventId}\n                        onDetailPanelOpen={() => setNewlyCreatedEventId(null)}\n                        selectedEventId={selectedEventId}\n                        detailPanelEventId={detailPanelEventId}\n                        onEventSelect={(eventId: string | null) => {\n                          const isViewable =\n                            app.getReadOnlyConfig(eventId ?? undefined)\n                              .viewable !== false;\n                          const evt = organizedAllDaySegments.find(\n                            currentSegment =>\n                              currentSegment.event.id === eventId\n                          )?.event;\n\n                          if ((isMobile || isTouch) && evt && isViewable) {\n                            setDraftEvent(evt);\n                            setIsDrawerOpen(true);\n                            return;\n                          }\n\n                          setSelectedEventId(eventId);\n                        }}\n                        onEventLongPress={(eventId: string) => {\n                          if (isMobile || isTouch) setSelectedEventId(eventId);\n                        }}\n                        onDetailPanelToggle={(eventId: string | null) =>\n                          setDetailPanelEventId(eventId)\n                        }\n                        useEventDetailPanel={useEventDetailPanel}\n                        app={app}\n                        isMobile={isMobile}\n                        isSlidingView={isSlidingView}\n                        enableTouch={isTouch}\n                      />\n                    ))}\n                  </div>\n                </div>\n              </div>\n            )}\n          </div>\n        </div>\n        {isEditable && contextMenu && (\n          <GridContextMenu\n            x={contextMenu.x}\n            y={contextMenu.y}\n            date={contextMenu.date}\n            viewType={ViewType.WEEK}\n            onClose={() => setContextMenu(null)}\n            app={app}\n            onCreateEvent={() => {\n              const currentDayIndex = Math.floor(\n                (contextMenu.date.getTime() - currentWeekStart.getTime()) /\n                  (24 * 60 * 60 * 1000)\n              );\n              handleCreateAllDayEvent?.(\n                {\n                  clientX: contextMenu.x,\n                  clientY: contextMenu.y,\n                } as MouseEvent,\n                currentDayIndex\n              );\n            }}\n          />\n        )}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/weekView/CompactHeader.tsx",
    "content": "import { ICalendarApp } from '@/types';\n\ninterface CompactHeaderProps {\n  app: ICalendarApp;\n  fullWeekDates?: Array<{\n    date: number;\n    month: string;\n    fullDate: Date;\n    isToday: boolean;\n    isCurrent: boolean;\n    dayName: string;\n  }>;\n  mobilePageStart?: Date;\n  onDateChange?: (date: Date) => void;\n}\n\nconst formatLabel = (name: string) => {\n  const n = name.toLowerCase();\n  if (\n    n.startsWith('tu') ||\n    n.startsWith('th') ||\n    n.startsWith('sa') ||\n    n.startsWith('su')\n  ) {\n    return name.slice(0, 2);\n  }\n  return name.slice(0, 1);\n};\n\nexport const CompactHeader = ({\n  app,\n  fullWeekDates = [],\n  mobilePageStart,\n  onDateChange,\n}: CompactHeaderProps) => (\n  <div className='df-compact-header'>\n    {/* Weekday labels row */}\n    <div className='df-compact-header-labels'>\n      {fullWeekDates.map((day, index) => (\n        <div key={`label-${index}`} className='df-compact-header-label-cell'>\n          <span\n            className='df-compact-header-label'\n            data-today={day.isToday ? 'true' : 'false'}\n          >\n            {formatLabel(day.dayName)}\n          </span>\n        </div>\n      ))}\n    </div>\n\n    {/* Dates row with capsule */}\n    <div className='df-compact-header-dates'>\n      {(() => {\n        if (!mobilePageStart) return null;\n\n        const columnsPerPage = 2;\n\n        const startIndex = fullWeekDates.findIndex(\n          d => d.fullDate.getTime() === mobilePageStart.getTime()\n        );\n\n        const capsuleLeft =\n          startIndex === -1\n            ? '0'\n            : `calc(${((startIndex + 0.5) / 7) * 100}% - 16px)`;\n        // Width covers 2 columns\n        const capsuleWidth = `calc(${(1 / 7) * 100}% + 32px)`;\n\n        return (\n          <>\n            <div\n              className='df-compact-header-capsule'\n              style={{\n                left: capsuleLeft,\n                top: 0,\n                width: capsuleWidth,\n                height: '32px',\n              }}\n            />\n\n            {fullWeekDates.map((day, index) => {\n              const isSelected = day.isCurrent;\n              const isInsidePill =\n                index >= startIndex && index < startIndex + columnsPerPage;\n\n              return (\n                <div\n                  key={`date-${index}`}\n                  className='df-compact-header-date-button'\n                  style={{ height: '32px' }}\n                  onClick={() => {\n                    app.setCurrentDate(day.fullDate);\n                    onDateChange?.(day.fullDate);\n                  }}\n                >\n                  <div\n                    className='df-compact-header-date-pill'\n                    data-selected={isSelected ? 'true' : 'false'}\n                    data-today={day.isToday ? 'true' : 'false'}\n                    data-inside-pill={isInsidePill ? 'true' : 'false'}\n                  >\n                    {day.date}\n                    {day.isToday && !isSelected && (\n                      <div className='df-compact-header-today-dot' />\n                    )}\n                  </div>\n                </div>\n              );\n            })}\n          </>\n        );\n      })()}\n    </div>\n  </div>\n);\n"
  },
  {
    "path": "packages/core/src/components/weekView/TimeGrid.tsx",
    "content": "import { RefObject, CSSProperties, TargetedEvent } from 'preact';\nimport {\n  useEffect,\n  useState,\n  useRef,\n  useMemo,\n  useCallback,\n} from 'preact/hooks';\n\nimport CalendarEventComponent from '@/components/calendarEvent';\nimport { GridContextMenu } from '@/components/contextMenu';\nimport { analyzeMultiDayRegularEvent } from '@/components/monthView/util';\nimport {\n  timeSlot,\n  timeLabel,\n  timeGridRow,\n  timeGridCell,\n  currentTimeLine,\n  currentTimeLabel,\n  timeGridBoundary,\n} from '@/styles/classNames';\nimport {\n  EventLayout,\n  Event as CalendarEvent,\n  WeekDayDragState,\n  ViewType,\n  ICalendarApp,\n} from '@/types';\nimport {\n  formatTime,\n  getEventsForDay,\n  getAllDayEventsForDay,\n  scrollbarTakesSpace,\n} from '@/utils';\nimport {\n  startPendingCreate,\n  finalizeCreateOnDblClick,\n} from '@/views/utils/dragCreate';\n\ninterface TimeGridProps {\n  app: ICalendarApp;\n  timeSlots: Array<{ hour: number; label: string }>;\n  weekDaysLabels: string[];\n  currentWeekStart: Date;\n  currentWeekEvents: CalendarEvent[];\n  eventLayouts: Map<number, Map<string, EventLayout>>;\n  gridWidth: string;\n  isMobile: boolean;\n  isSlidingView: boolean;\n  isTouch: boolean;\n  scrollerRef: RefObject<HTMLDivElement>;\n  timeGridRef: RefObject<HTMLDivElement>;\n  leftFrozenContentRef: RefObject<HTMLDivElement>;\n  swipeContentRef: RefObject<HTMLDivElement>;\n  calendarRef: RefObject<HTMLDivElement>;\n  handleScroll: (e: TargetedEvent<HTMLDivElement, globalThis.Event>) => void;\n  secondaryTimeSlots?: string[];\n  handleCreateStart?: (\n    e: MouseEvent | TouchEvent,\n    dayIndex: number,\n    hour: number\n  ) => void;\n  handleTouchStart: (e: TouchEvent, dayIndex: number, hour: number) => void;\n  handleTouchEnd: () => void;\n  handleTouchMove: (e: TouchEvent) => void;\n  handleDragOver: (e: DragEvent) => void;\n  handleDrop: (\n    e: DragEvent,\n    date: Date,\n    hour?: number,\n    allDay?: boolean\n  ) => void;\n  dragState: WeekDayDragState | null;\n  isDragging: boolean;\n  handleMoveStart: (e: MouseEvent | TouchEvent, event: CalendarEvent) => void;\n  handleResizeStart: (\n    e: MouseEvent | TouchEvent,\n    event: CalendarEvent,\n    direction: string\n  ) => void;\n  handleEventUpdate: (event: CalendarEvent) => void;\n  handleEventDelete: (id: string) => void;\n  setDraftEvent: (event: CalendarEvent | null) => void;\n  setIsDrawerOpen: (isOpen: boolean) => void;\n\n  onDateChange?: (date: Date) => void;\n  onGridDateClick?: (date: Date, events: CalendarEvent[]) => void;\n  onGridDateDoubleClick?: (date: Date, events: CalendarEvent[]) => void;\n  newlyCreatedEventId: string | null;\n  setNewlyCreatedEventId: (id: string | null) => void;\n  selectedEventId: string | null;\n  setSelectedEventId: (eventId: string | null) => void;\n  detailPanelEventId: string | null;\n  setDetailPanelEventId: (eventId: string | null) => void;\n  useEventDetailPanel?: boolean;\n  isCurrentWeek: boolean;\n  currentTime: Date | null;\n  HOUR_HEIGHT: number;\n  FIRST_HOUR: number;\n  LAST_HOUR: number;\n  showStartOfDayLabel: boolean;\n  timeFormat?: '12h' | '24h';\n  appTimeZone?: string;\n}\n\nexport const TimeGrid = ({\n  app,\n  timeSlots,\n  weekDaysLabels,\n  currentWeekStart,\n  currentWeekEvents,\n  eventLayouts,\n  gridWidth,\n  isMobile,\n  isSlidingView,\n  isTouch,\n  scrollerRef,\n  timeGridRef,\n  leftFrozenContentRef,\n  swipeContentRef,\n  calendarRef,\n  handleScroll,\n  handleCreateStart,\n  handleTouchStart,\n  handleTouchEnd,\n  handleTouchMove,\n  handleDragOver,\n  handleDrop,\n  dragState,\n  isDragging,\n  handleMoveStart,\n  handleResizeStart,\n  handleEventUpdate,\n  handleEventDelete,\n  setDraftEvent,\n  setIsDrawerOpen,\n  onDateChange,\n  onGridDateClick,\n  onGridDateDoubleClick,\n  newlyCreatedEventId,\n  setNewlyCreatedEventId,\n  selectedEventId,\n  setSelectedEventId,\n  detailPanelEventId,\n  setDetailPanelEventId,\n  useEventDetailPanel,\n  isCurrentWeek,\n  currentTime,\n  HOUR_HEIGHT,\n  FIRST_HOUR,\n  LAST_HOUR,\n  showStartOfDayLabel,\n  timeFormat = '24h',\n  secondaryTimeSlots,\n  appTimeZone,\n}: TimeGridProps) => {\n  const hasSecondaryTz = !!secondaryTimeSlots && secondaryTimeSlots.length > 0;\n  // On mobile the time column is too narrow for dual labels — hide secondary TZ display\n  const showSecondaryTz = hasSecondaryTz && !isMobile;\n  const columnStyle: CSSProperties = { flexShrink: 0 };\n  const prevHighlightedEventId = useRef(app.state.highlightedEventId);\n  const [contextMenu, setContextMenu] = useState<{\n    x: number;\n    y: number;\n    date: Date;\n  } | null>(null);\n  const hasScrollbarSpace = useMemo(() => scrollbarTakesSpace(), []);\n  const isEditable = app.canMutateFromUI();\n\n  useEffect(() => {\n    if (isEditable) return;\n    setContextMenu(null);\n  }, [isEditable]);\n\n  const allEventSegmentsByDay = useMemo(() => {\n    const result = new Map<\n      number,\n      Array<{\n        event: CalendarEvent;\n        segmentInfo?: {\n          startHour: number;\n          endHour: number;\n          isFirst: boolean;\n          isLast: boolean;\n          dayIndex?: number;\n        };\n      }>\n    >();\n\n    weekDaysLabels.forEach((_, dayIndex) => {\n      const segments: Array<{\n        event: CalendarEvent;\n        segmentInfo?: {\n          startHour: number;\n          endHour: number;\n          isFirst: boolean;\n          isLast: boolean;\n          dayIndex?: number;\n        };\n      }> = [];\n\n      const dayEvents = getEventsForDay(dayIndex, currentWeekEvents);\n      dayEvents.forEach(event => {\n        const multiDaySegs = analyzeMultiDayRegularEvent(\n          event,\n          currentWeekStart,\n          weekDaysLabels.length,\n          appTimeZone\n        );\n        if (multiDaySegs.length > 0) {\n          const seg = multiDaySegs.find(s => s.dayIndex === dayIndex);\n          if (seg) {\n            segments.push({ event, segmentInfo: { ...seg, dayIndex } });\n          }\n        } else {\n          segments.push({ event });\n        }\n      });\n\n      currentWeekEvents.forEach(event => {\n        if (event.allDay || event.day === dayIndex) return;\n        const multiDaySegs = analyzeMultiDayRegularEvent(\n          event,\n          currentWeekStart,\n          weekDaysLabels.length,\n          appTimeZone\n        );\n        const seg = multiDaySegs.find(s => s.dayIndex === dayIndex);\n        if (seg) {\n          segments.push({ event, segmentInfo: { ...seg, dayIndex } });\n        }\n      });\n\n      result.set(dayIndex, segments);\n    });\n\n    return result;\n  }, [currentWeekEvents, currentWeekStart, weekDaysLabels.length, appTimeZone]);\n\n  const onEventSelectImplRef = useRef<(eventId: string | null) => void>(() => {\n    // No-op\n  });\n  onEventSelectImplRef.current = (eventId: string | null) => {\n    const isViewable =\n      app.getReadOnlyConfig(eventId ?? undefined).viewable !== false;\n    const evt = currentWeekEvents.find(e => e.id === eventId);\n    if ((isMobile || isTouch) && evt && isViewable) {\n      setDraftEvent(evt);\n      setIsDrawerOpen(true);\n      return;\n    }\n    setSelectedEventId(eventId);\n    if (app.state.highlightedEventId) {\n      app.highlightEvent(null);\n      prevHighlightedEventId.current = null;\n    }\n  };\n  const stableOnEventSelect = useCallback(\n    (eventId: string | null) => onEventSelectImplRef.current(eventId),\n    []\n  );\n  const stableOnDetailPanelOpen = useCallback(\n    () => setNewlyCreatedEventId(null),\n    [setNewlyCreatedEventId]\n  );\n  const stableOnEventLongPress = useCallback(\n    (eventId: string) => {\n      if (isMobile || isTouch) setSelectedEventId(eventId);\n    },\n    [isMobile, isTouch, setSelectedEventId]\n  );\n  const stableOnDetailPanelToggle = useCallback(\n    (eventId: string | null) => setDetailPanelEventId(eventId),\n    [setDetailPanelEventId]\n  );\n\n  /** Returns the fractional hour at the given clientY within the time grid. */\n  const getGridHour = (clientY: number) => {\n    if (!timeGridRef.current) return FIRST_HOUR;\n    const rect = timeGridRef.current.getBoundingClientRect();\n    return FIRST_HOUR + (clientY - rect.top) / HOUR_HEIGHT;\n  };\n\n  const handleContextMenu = (e: MouseEvent, dayIndex: number, hour: number) => {\n    e.preventDefault();\n    if (isMobile || !isEditable) return;\n\n    const date = new Date(currentWeekStart);\n    date.setDate(currentWeekStart.getDate() + dayIndex);\n\n    if (timeGridRef.current) {\n      const rect = (timeGridRef.current as HTMLElement).getBoundingClientRect();\n      const relativeY = e.clientY - rect.top;\n      // Convert relativeY to hours based on grid scale\n      const floatHour = relativeY / HOUR_HEIGHT + FIRST_HOUR;\n      const h = Math.floor(floatHour);\n      const m = Math.floor((floatHour - h) * 60);\n\n      // Snap to 15 minutes for better UX\n      const snappedMinutes = Math.round(m / 15) * 15;\n      const finalHour = snappedMinutes === 60 ? h + 1 : h;\n      const finalMinutes = snappedMinutes === 60 ? 0 : snappedMinutes;\n\n      date.setHours(finalHour, finalMinutes, 0, 0);\n    } else {\n      date.setHours(hour, 0, 0, 0);\n    }\n\n    setContextMenu({ x: e.clientX, y: e.clientY, date });\n  };\n\n  return (\n    <div className='df-week-time-grid'>\n      {/* Single scrolling container using CSS Grid (auto | 1fr).\n          \"auto\" sizes the time-label column from the semantic time-column rules\n          without needing JS.\n          Both columns share the same scroll context so they scroll\n          vertically together natively — no JS transform sync needed. */}\n      <div\n        ref={scrollerRef}\n        className='df-calendar-content df-week-time-grid-scroller df-week-time-grid-scroller-shell'\n        data-sliding-view={gridWidth === '300%' ? 'true' : 'false'}\n        style={{ gridTemplateColumns: 'auto 1fr' }}\n        onScroll={handleScroll}\n      >\n        {/* Time label column — first grid column (auto width from semantic CSS).\n            No overflow-hidden: renders at full content height alongside the grid. */}\n        <div\n          ref={leftFrozenContentRef}\n          className='df-week-time-grid-time-column'\n          data-secondary-tz={showSecondaryTz ? 'true' : 'false'}\n          onContextMenu={e => e.preventDefault()}\n        >\n          {/* Top boundary spacer — expands to include timezone header when active */}\n          <div\n            className='df-time-column-spacer df-time-week-column-spacer'\n            data-secondary-tz={showSecondaryTz ? 'true' : 'false'}\n          >\n            {showSecondaryTz ? (\n              <>\n                {/* Start-of-day label */}\n                <div className='df-time-column-tz-row df-time-column-tz-row-boundary'>\n                  <span className='df-time-column-tz-value'>\n                    {showStartOfDayLabel ? (secondaryTimeSlots?.[0] ?? '') : ''}\n                  </span>\n                  <span className='df-time-column-tz-value'>\n                    {showStartOfDayLabel\n                      ? formatTime(FIRST_HOUR, 0, timeFormat)\n                      : ''}\n                  </span>\n                </div>\n              </>\n            ) : (\n              <div className='df-time-column-boundary-label'>\n                {showStartOfDayLabel\n                  ? formatTime(FIRST_HOUR, 0, timeFormat)\n                  : ''}\n              </div>\n            )}\n          </div>\n          {timeSlots.map((slot, slotIndex) => (\n            <div key={slotIndex} className={timeSlot}>\n              {showSecondaryTz ? (\n                <div className='df-time-column-tz-row'>\n                  <span className='df-time-column-tz-value'>\n                    {showStartOfDayLabel && slotIndex === 0\n                      ? ''\n                      : (secondaryTimeSlots?.[slotIndex] ?? '')}\n                  </span>\n                  <span className='df-time-column-tz-value'>\n                    {showStartOfDayLabel && slotIndex === 0 ? '' : slot.label}\n                  </span>\n                </div>\n              ) : (\n                <div className={timeLabel}>\n                  {showStartOfDayLabel && slotIndex === 0 ? '' : slot.label}\n                </div>\n              )}\n            </div>\n          ))}\n          <div className='df-week-time-grid-boundary-tail'>\n            {showSecondaryTz ? (\n              <div className='df-time-column-tz-row'>\n                <span className='df-time-column-tz-value'>\n                  {secondaryTimeSlots?.[0] ?? ''}\n                </span>\n                <span className='df-time-column-tz-value'>\n                  {formatTime(0, 0, timeFormat)}\n                </span>\n              </div>\n            ) : (\n              <div className={timeLabel}>{formatTime(0, 0, timeFormat)}</div>\n            )}\n          </div>\n          {/* Current Time Label */}\n          {isCurrentWeek &&\n            currentTime &&\n            (() => {\n              const now = currentTime;\n              const hours = now.getHours() + now.getMinutes() / 60;\n              if (hours < FIRST_HOUR || hours > LAST_HOUR) return null;\n\n              const topPx = (hours - FIRST_HOUR) * HOUR_HEIGHT;\n\n              return (\n                <div\n                  className='df-week-time-grid-current-time-label'\n                  style={{\n                    top: `${topPx}px`,\n                    marginTop: '0.75rem',\n                  }}\n                >\n                  <div className={currentTimeLabel}>\n                    {formatTime(hours, 0, timeFormat, false)}\n                  </div>\n                </div>\n              );\n            })()}\n        </div>\n\n        {/* Grid content — second grid column (1fr), swipe target on mobile.\n            gridWidth is relative to this grid track (scroller - sidebarWidth). */}\n        <div\n          ref={swipeContentRef}\n          className='df-week-time-grid-content'\n          style={{\n            width: gridWidth,\n            minWidth: '100%',\n            transform: isSlidingView\n              ? 'translateX(calc(-100% / 3))'\n              : undefined,\n          }}\n        >\n          {/* Time Grid */}\n          <div className='df-week-time-grid-grid'>\n            {/* Top boundary — height must match left column spacer */}\n            <div\n              className={`${timeGridBoundary} df-time-grid-boundary-top df-week-time-grid-boundary-row`}\n              data-scrollbar-space={\n                isMobile || !hasScrollbarSpace ? 'false' : 'true'\n              }\n            >\n              {weekDaysLabels.map((_, dayIndex) => (\n                <div\n                  key={`boundary_${dayIndex}`}\n                  className='df-week-time-grid-boundary-cell'\n                  style={columnStyle}\n                />\n              ))}\n            </div>\n            <div\n              ref={timeGridRef}\n              className='df-week-time-grid-grid-inner'\n              onDragOver={handleDragOver}\n              onDrop={e => {\n                const rect = timeGridRef.current?.getBoundingClientRect();\n                if (!rect) return;\n\n                const daysToShow = weekDaysLabels.length;\n                const relativeX = e.clientX - rect.left;\n                const relativeY = e.clientY - rect.top;\n\n                const dayIndex = Math.floor(\n                  relativeX / (rect.width / daysToShow)\n                );\n                const dropHour = Math.floor(\n                  FIRST_HOUR + relativeY / HOUR_HEIGHT\n                );\n\n                const dropDate = new Date(currentWeekStart);\n                dropDate.setDate(currentWeekStart.getDate() + dayIndex);\n\n                handleDrop(e, dropDate, dropHour);\n              }}\n            >\n              {/* Current time line */}\n              {isCurrentWeek &&\n                currentTime &&\n                (() => {\n                  const now = currentTime;\n                  const hours = now.getHours() + now.getMinutes() / 60;\n                  if (hours < FIRST_HOUR || hours > LAST_HOUR) return null;\n\n                  const today = new Date(now);\n                  today.setHours(0, 0, 0, 0);\n                  const start = new Date(currentWeekStart);\n                  start.setHours(0, 0, 0, 0);\n                  const diffTime = today.getTime() - start.getTime();\n                  const todayIndex = Math.round(\n                    diffTime / (1000 * 60 * 60 * 24)\n                  );\n                  const topPx = (hours - FIRST_HOUR) * HOUR_HEIGHT;\n\n                  return (\n                    <div\n                      className={currentTimeLine}\n                      style={{\n                        top: `${topPx}px`,\n                        height: 0,\n                        zIndex: 20,\n                      }}\n                    >\n                      <div className='df-week-time-grid-current-time-spacer'>\n                        {/* Empty left part since it is in frozen column now */}\n                      </div>\n\n                      <div className='df-week-time-grid-current-time-track'>\n                        {weekDaysLabels.map((_, idx) => (\n                          <div\n                            key={idx}\n                            className='df-week-time-grid-current-time-cell'\n                          >\n                            <div\n                              className='df-week-time-grid-current-line-rail'\n                              data-today={idx === todayIndex ? 'true' : 'false'}\n                              style={{\n                                zIndex: 9999,\n                              }}\n                            >\n                              {idx === todayIndex && todayIndex !== 0 && (\n                                <div className='df-week-time-grid-current-line-dot' />\n                              )}\n                            </div>\n                          </div>\n                        ))}\n                      </div>\n                    </div>\n                  );\n                })()}\n\n              {timeSlots.map((slot, slotIndex) => (\n                <div\n                  key={slotIndex}\n                  className={timeGridRow}\n                  data-scrollbar-space={\n                    isMobile || !hasScrollbarSpace ? 'false' : 'true'\n                  }\n                >\n                  {weekDaysLabels.map((_, dayIndex) => (\n                    <div\n                      key={`${slotIndex}-${dayIndex}`}\n                      className={`${timeGridCell} df-week-time-grid-cell`}\n                      style={columnStyle}\n                      onClick={() => {\n                        const clickedDate = new Date(currentWeekStart);\n                        clickedDate.setDate(\n                          currentWeekStart.getDate() + dayIndex\n                        );\n                        if (onGridDateClick) {\n                          const dayEvents = [\n                            ...getEventsForDay(dayIndex, currentWeekEvents),\n                            ...getAllDayEventsForDay(\n                              dayIndex,\n                              currentWeekEvents,\n                              currentWeekStart\n                            ),\n                          ];\n                          onGridDateClick(clickedDate, dayEvents);\n                        } else {\n                          onDateChange?.(clickedDate);\n                        }\n                      }}\n                      onMouseDown={e => {\n                        startPendingCreate(\n                          e,\n                          dayIndex,\n                          getGridHour(e.clientY),\n                          isTouch,\n                          handleCreateStart,\n                          isSlidingView\n                        );\n                      }}\n                      onDblClick={e => {\n                        const clickedDate = new Date(currentWeekStart);\n                        clickedDate.setDate(\n                          currentWeekStart.getDate() + dayIndex\n                        );\n                        if (onGridDateDoubleClick) {\n                          const dayEvents = [\n                            ...getEventsForDay(dayIndex, currentWeekEvents),\n                            ...getAllDayEventsForDay(\n                              dayIndex,\n                              currentWeekEvents,\n                              currentWeekStart\n                            ),\n                          ];\n                          onGridDateDoubleClick(clickedDate, dayEvents);\n                        } else {\n                          handleCreateStart?.(\n                            e,\n                            dayIndex,\n                            getGridHour(e.clientY)\n                          );\n                          finalizeCreateOnDblClick();\n                        }\n                      }}\n                      onTouchStart={e =>\n                        handleTouchStart(e, dayIndex, slot.hour)\n                      }\n                      onTouchEnd={handleTouchEnd}\n                      onTouchMove={handleTouchMove}\n                      onContextMenu={e =>\n                        handleContextMenu(e, dayIndex, slot.hour)\n                      }\n                    />\n                  ))}\n                </div>\n              ))}\n\n              {/* Bottom boundary */}\n              <div\n                className={`${timeGridBoundary} df-time-grid-boundary-bottom df-week-time-grid-boundary-row`}\n                data-scrollbar-space={\n                  isMobile || !hasScrollbarSpace ? 'false' : 'true'\n                }\n              >\n                {weekDaysLabels.map((_, dayIndex) => (\n                  <div\n                    key={`24-${dayIndex}`}\n                    className='df-week-time-grid-boundary-cell'\n                    style={columnStyle}\n                  />\n                ))}\n              </div>\n\n              {/* Event layer */}\n              {weekDaysLabels.map((_, dayIndex) => {\n                const daysToShow = weekDaysLabels.length;\n                const allEventSegments =\n                  allEventSegmentsByDay.get(dayIndex) ?? [];\n\n                return (\n                  <div\n                    key={`events-day-${dayIndex}`}\n                    className='df-week-time-grid-event-layer'\n                    style={{\n                      left: `calc(${(100 / daysToShow) * dayIndex}%)`,\n                      width: `${100 / daysToShow}%`,\n                      height: '100%',\n                    }}\n                  >\n                    {allEventSegments.map(({ event, segmentInfo }) => {\n                      const dayLayouts = eventLayouts.get(dayIndex);\n                      const eventLayout = dayLayouts?.get(event.id);\n\n                      return (\n                        <CalendarEventComponent\n                          key={\n                            segmentInfo\n                              ? `${event.id}-seg-${dayIndex}`\n                              : event.id\n                          }\n                          event={event}\n                          layout={eventLayout}\n                          viewType={ViewType.WEEK}\n                          calendarRef={calendarRef}\n                          columnsPerRow={daysToShow}\n                          isBeingDragged={\n                            isDragging &&\n                            (dragState as WeekDayDragState)?.eventId ===\n                              event.id &&\n                            (dragState as WeekDayDragState)?.mode === 'move'\n                          }\n                          hourHeight={HOUR_HEIGHT}\n                          firstHour={FIRST_HOUR}\n                          onMoveStart={handleMoveStart}\n                          onResizeStart={handleResizeStart}\n                          onEventUpdate={handleEventUpdate}\n                          onEventDelete={handleEventDelete}\n                          newlyCreatedEventId={newlyCreatedEventId}\n                          onDetailPanelOpen={stableOnDetailPanelOpen}\n                          selectedEventId={selectedEventId}\n                          detailPanelEventId={detailPanelEventId}\n                          onEventSelect={stableOnEventSelect}\n                          onEventLongPress={stableOnEventLongPress}\n                          onDetailPanelToggle={stableOnDetailPanelToggle}\n                          useEventDetailPanel={useEventDetailPanel}\n                          multiDaySegmentInfo={segmentInfo}\n                          app={app}\n                          isMobile={isMobile}\n                          isSlidingView={isSlidingView}\n                          enableTouch={isTouch}\n                          appTimeZone={appTimeZone}\n                        />\n                      );\n                    })}\n                  </div>\n                );\n              })}\n            </div>\n          </div>\n        </div>\n      </div>\n      {isEditable && contextMenu && (\n        <GridContextMenu\n          x={contextMenu.x}\n          y={contextMenu.y}\n          date={contextMenu.date}\n          viewType={ViewType.WEEK}\n          onClose={() => setContextMenu(null)}\n          app={app}\n          onCreateEvent={() => {\n            if (handleCreateStart) {\n              // Calculate dayIndex relative to currentWeekStart\n              const startOfDay = new Date(currentWeekStart);\n              startOfDay.setHours(0, 0, 0, 0);\n              const targetDate = new Date(contextMenu.date);\n              targetDate.setHours(0, 0, 0, 0);\n\n              const diffTime = targetDate.getTime() - startOfDay.getTime();\n              const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));\n              const preciseHour =\n                contextMenu.date.getHours() +\n                contextMenu.date.getMinutes() / 60;\n\n              const syntheticEvent = {\n                preventDefault: () => {\n                  /* noop */\n                },\n                stopPropagation: () => {\n                  /* noop */\n                },\n                clientX: contextMenu.x,\n                clientY: contextMenu.y,\n              } as unknown as MouseEvent;\n              handleCreateStart(syntheticEvent, diffDays, preciseHour);\n            }\n          }}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/weekView/__tests__/util.timezone.test.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { filterWeekEvents } from '@/components/weekView/util';\nimport { Event } from '@/types';\n\ndescribe('filterWeekEvents', () => {\n  it('computes day indexes from the event local/original date in Week view', () => {\n    const currentWeekStart = new Date(2026, 3, 6);\n\n    const events: Event[] = [\n      {\n        id: 'tuesday-event',\n        title: 'Tuesday Event',\n        start: Temporal.PlainDateTime.from('2026-04-07T18:00:00'),\n        end: Temporal.PlainDateTime.from('2026-04-07T19:00:00'),\n      },\n      {\n        id: 'wednesday-event',\n        title: 'Wednesday Event',\n        start: Temporal.PlainDateTime.from('2026-04-08T09:00:00'),\n        end: Temporal.PlainDateTime.from('2026-04-08T10:00:00'),\n      },\n    ];\n\n    const results = filterWeekEvents(events, currentWeekStart, 7);\n\n    expect(results.map(event => event.day)).toEqual([1, 2]);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/weekView/util.ts",
    "content": "import { EventLayoutCalculator } from '@/components/eventLayout';\nimport {\n  analyzeMultiDayRegularEvent,\n  analyzeMultiDayEventsForWeek,\n  MultiDayEventSegment,\n} from '@/components/monthView/util';\nimport { Event, EventLayout } from '@/types';\nimport { createDateWithHour, getDateByDayIndex } from '@/utils';\nimport { createAllDayDisplayComparator } from '@/utils/allDaySort';\nimport { extractHourFromDate, getEventEndHour } from '@/utils/helpers';\nimport {\n  dateToZonedDateTime,\n  temporalToDate,\n  temporalToVisualDate,\n} from '@/utils/temporalTypeGuards';\n\n// ... existing code ...\n\n// Build the projected event list for a single day plus the source-event refs\n// that contributed (for cache-key comparison).\nconst buildDayEventsForLayout = (\n  currentWeekEvents: Event[],\n  day: number,\n  currentWeekStart: Date,\n  daysToShow: number,\n  appTimeZone?: string\n): { events: Event[]; sources: Event[] } => {\n  const dayEventsForLayout: Event[] = [];\n  const sourceRefs: Event[] = [];\n\n  currentWeekEvents.forEach(event => {\n    if (event.allDay) return;\n\n    const segments = analyzeMultiDayRegularEvent(\n      event,\n      currentWeekStart,\n      daysToShow,\n      appTimeZone\n    );\n\n    if (segments.length > 0) {\n      const segment = segments.find(s => s.dayIndex === day);\n      if (segment) {\n        const segmentEndHour = segment.endHour >= 24 ? 23.99 : segment.endHour;\n\n        const virtualEvent: Event = {\n          ...event,\n          start: dateToZonedDateTime(\n            createDateWithHour(\n              getDateByDayIndex(currentWeekStart, day),\n              segment.startHour\n            ) as Date\n          ),\n          end: dateToZonedDateTime(\n            createDateWithHour(\n              getDateByDayIndex(currentWeekStart, day),\n              segmentEndHour\n            ) as Date\n          ),\n          day: day,\n          _originalStartHour: extractHourFromDate(event.start),\n          _originalEndHour: getEventEndHour(event),\n        };\n        dayEventsForLayout.push(virtualEvent);\n        sourceRefs.push(event);\n      }\n    } else if (event.day === day) {\n      const toVisual = (t: Event['start']) =>\n        appTimeZone ? temporalToVisualDate(t, appTimeZone) : temporalToDate(t);\n      dayEventsForLayout.push({\n        ...event,\n        start: dateToZonedDateTime(toVisual(event.start), appTimeZone),\n        end: dateToZonedDateTime(\n          toVisual(event.end ?? event.start),\n          appTimeZone\n        ),\n        day,\n        _originalStartHour: extractHourFromDate(event.start),\n        _originalEndHour: getEventEndHour(event),\n      });\n      sourceRefs.push(event);\n    }\n  });\n\n  return { events: dayEventsForLayout, sources: sourceRefs };\n};\n\n// Per-week-context layout cache. Each entry stores the source-event refs that\n// contributed to a day plus the resulting layouts. On the next render, if the\n// day's source refs match (element-wise), we reuse the cached layouts. During\n// drag/resize only one event ref changes, so 5/7 of days hit the cache.\ntype LayoutCacheEntry = {\n  contextKey: string;\n  perDay: Map<number, { sources: Event[]; layouts: Map<string, EventLayout> }>;\n};\nlet layoutCacheEntry: LayoutCacheEntry | null = null;\n\nconst sameRefs = (a: Event[], b: Event[]): boolean => {\n  if (a.length !== b.length) return false;\n  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n  return true;\n};\n\n// Calculate event layouts for the entire week\nexport const calculateEventLayouts = (\n  currentWeekEvents: Event[],\n  currentWeekStart: Date,\n  daysToShow: number = 7,\n  appTimeZone?: string\n): Map<number, Map<string, EventLayout>> => {\n  const contextKey = `${currentWeekStart.getTime()}|${daysToShow}|${appTimeZone ?? ''}`;\n  if (!layoutCacheEntry || layoutCacheEntry.contextKey !== contextKey) {\n    layoutCacheEntry = { contextKey, perDay: new Map() };\n  }\n  const cache = layoutCacheEntry.perDay;\n\n  const allLayouts = new Map<number, Map<string, EventLayout>>();\n\n  for (let day = 0; day < daysToShow; day++) {\n    const { events: dayEvents, sources } = buildDayEventsForLayout(\n      currentWeekEvents,\n      day,\n      currentWeekStart,\n      daysToShow,\n      appTimeZone\n    );\n\n    const cached = cache.get(day);\n    if (cached && sameRefs(cached.sources, sources)) {\n      allLayouts.set(day, cached.layouts);\n      continue;\n    }\n\n    const dayLayouts = EventLayoutCalculator.calculateDayEventLayouts(\n      dayEvents,\n      { viewType: 'week' }\n    );\n    cache.set(day, { sources, layouts: dayLayouts });\n    allLayouts.set(day, dayLayouts);\n  }\n\n  return allLayouts;\n};\n\nexport const getWeekStart = (date: Date, startOfWeek: number = 1): Date => {\n  const day = date.getDay();\n  const diff = (day - startOfWeek + 7) % 7;\n  const start = new Date(date);\n  start.setDate(date.getDate() - diff);\n  start.setHours(0, 0, 0, 0);\n  return start;\n};\n\n// Per-event cache for filter/day classification. Keyed by event reference;\n// inner key encodes the week context so events from different weeks coexist.\n// During drag/resize 99%+ of events are unchanged refs so this turns the\n// per-tick filter from O(N) toDate calls into ~1 actual computation.\nconst weekFilterCache = new WeakMap<\n  Event,\n  Map<string, { inWeek: boolean; day: number }>\n>();\n\nconst classifyEventForWeek = (\n  event: Event,\n  contextKey: string,\n  weekStartMs: number,\n  weekEndMs: number,\n  daysToShow: number,\n  appTimeZone?: string\n): { inWeek: boolean; day: number } => {\n  let perEvent = weekFilterCache.get(event);\n  if (perEvent) {\n    const cached = perEvent.get(contextKey);\n    if (cached) return cached;\n  }\n\n  const toDate = (temporal: Event['start']) =>\n    appTimeZone\n      ? temporalToVisualDate(temporal, appTimeZone)\n      : temporalToDate(temporal);\n\n  const eventStart = toDate(event.start);\n  const startDayMs = new Date(eventStart).setHours(0, 0, 0, 0);\n  const eventEnd = toDate(event.end ?? event.start);\n  const endDayMs = new Date(eventEnd).setHours(23, 59, 59, 999);\n\n  const inWeek = endDayMs >= weekStartMs && startDayMs <= weekEndMs;\n  const dayDiff = Math.floor(\n    (startDayMs - weekStartMs) / (24 * 60 * 60 * 1000)\n  );\n  const day = Math.max(0, Math.min(daysToShow - 1, dayDiff));\n\n  const result = { inWeek, day };\n  if (!perEvent) {\n    perEvent = new Map();\n    weekFilterCache.set(event, perEvent);\n  }\n  perEvent.set(contextKey, result);\n  return result;\n};\n\n// Filter events for the current week\nexport const filterWeekEvents = (\n  events: Event[],\n  currentWeekStart: Date,\n  daysToShow: number = 7,\n  appTimeZone?: string\n): Event[] => {\n  const weekStartMs = currentWeekStart.getTime();\n  const weekEnd = new Date(currentWeekStart);\n  weekEnd.setDate(currentWeekStart.getDate() + (daysToShow - 1));\n  weekEnd.setHours(23, 59, 59, 999);\n  const weekEndMs = weekEnd.getTime();\n  const contextKey = `${weekStartMs}|${daysToShow}|${appTimeZone ?? ''}`;\n\n  const result: Event[] = [];\n  for (let i = 0; i < events.length; i++) {\n    const event = events[i];\n    const { inWeek, day } = classifyEventForWeek(\n      event,\n      contextKey,\n      weekStartMs,\n      weekEndMs,\n      daysToShow,\n      appTimeZone\n    );\n    if (!inWeek) continue;\n\n    // Preserve reference when day is already correct so downstream WeakMap\n    // caches (e.g. analyzeMultiDayRegularEvent) can hit on repeated renders.\n    result.push(event.day === day ? event : { ...event, day });\n  }\n  return result;\n};\n\n// Organize all-day segments\nexport const organizeAllDaySegments = (\n  currentWeekEvents: Event[],\n  currentWeekStart: Date,\n  daysToShow: number = 7,\n  comparator?: (a: Event, b: Event) => number\n) => {\n  const multiDaySegments = analyzeMultiDayEventsForWeek(\n    currentWeekEvents,\n    currentWeekStart,\n    daysToShow\n  );\n  const segments = multiDaySegments.filter(\n    (seg: MultiDayEventSegment) => seg.event.allDay\n  );\n\n  if (comparator) {\n    segments.sort((a: MultiDayEventSegment, b: MultiDayEventSegment) =>\n      comparator(a.event, b.event)\n    );\n  } else {\n    // Default: group by calendar (first-seen order), then preserve load order\n    const calendarOrder = new Map<string | undefined, number>();\n    segments.forEach((seg: MultiDayEventSegment) => {\n      const id = seg.event.calendarId;\n      if (!calendarOrder.has(id)) calendarOrder.set(id, calendarOrder.size);\n    });\n    const compareByDisplayPriority = createAllDayDisplayComparator(\n      segments.map(segment => segment.event),\n      (left, right) =>\n        (calendarOrder.get(left.calendarId) ?? 0) -\n        (calendarOrder.get(right.calendarId) ?? 0)\n    );\n    segments.sort((a: MultiDayEventSegment, b: MultiDayEventSegment) =>\n      compareByDisplayPriority(a.event, b.event)\n    );\n  }\n\n  const segmentsWithRow: Array<MultiDayEventSegment & { row: number }> = [];\n\n  segments.forEach((segment: MultiDayEventSegment) => {\n    let row = 0;\n    let foundRow = false;\n\n    while (!foundRow) {\n      let hasConflict = false;\n      for (const existing of segmentsWithRow) {\n        if (existing.row === row) {\n          const conflict = !(\n            segment.endDayIndex < existing.startDayIndex ||\n            segment.startDayIndex > existing.endDayIndex\n          );\n          if (conflict) {\n            hasConflict = true;\n            break;\n          }\n        }\n      }\n\n      if (hasConflict) {\n        row++;\n      } else {\n        foundRow = true;\n      }\n    }\n\n    segmentsWithRow.push({ ...segment, row });\n  });\n\n  return segmentsWithRow;\n};\n\n// Calculate new event layout\nexport const calculateNewEventLayout = (\n  targetDay: number,\n  startHour: number,\n  endHour: number,\n  currentWeekEvents: Event[],\n  currentWeekStart: Date,\n  daysToShow: number = 7,\n  appTimeZone?: string\n): EventLayout | null => {\n  const startDate = getDateByDayIndex(currentWeekStart, targetDay);\n  const endDate = getDateByDayIndex(currentWeekStart, targetDay);\n  startDate.setHours(Math.floor(startHour), (startHour % 1) * 60, 0, 0);\n  endDate.setHours(Math.floor(endHour), (endHour % 1) * 60, 0, 0);\n\n  const tempEvent: Event = {\n    id: '-1',\n    title: 'Temp',\n    day: targetDay,\n    start: dateToZonedDateTime(startDate, appTimeZone),\n    end: dateToZonedDateTime(endDate, appTimeZone),\n    calendarId: 'blue',\n    allDay: false,\n  };\n\n  const allLayouts = calculateEventLayouts(\n    [...currentWeekEvents, tempEvent],\n    currentWeekStart,\n    daysToShow,\n    appTimeZone\n  );\n  return allLayouts.get(targetDay)?.get('-1') || null;\n};\n\n// Calculate drag layout — only computes the target day for performance\nexport const calculateDragLayout = (\n  draggedEvent: Event,\n  targetDay: number,\n  targetStartHour: number,\n  targetEndHour: number,\n  currentWeekEvents: Event[],\n  currentWeekStart: Date,\n  daysToShow: number = 7,\n  appTimeZone?: string\n): EventLayout | null => {\n  const tempEvents = currentWeekEvents.map(e => {\n    if (e.id !== draggedEvent.id) return e;\n\n    const eventDateForCalc = getDateByDayIndex(currentWeekStart, targetDay);\n    const newStartDate = createDateWithHour(\n      eventDateForCalc,\n      targetStartHour\n    ) as Date;\n    const newEndDate = createDateWithHour(\n      eventDateForCalc,\n      targetEndHour\n    ) as Date;\n    const newStart = dateToZonedDateTime(newStartDate, appTimeZone);\n    const newEnd = dateToZonedDateTime(newEndDate, appTimeZone);\n\n    return { ...e, day: targetDay, start: newStart, end: newEnd };\n  });\n\n  const dayLayouts = EventLayoutCalculator.calculateDayEventLayouts(\n    buildDayEventsForLayout(\n      tempEvents,\n      targetDay,\n      currentWeekStart,\n      daysToShow,\n      appTimeZone\n    ).events,\n    { viewType: 'week' }\n  );\n  return dayLayouts.get(draggedEvent.id) || null;\n};\n"
  },
  {
    "path": "packages/core/src/components/yearView/DefaultYearView.tsx",
    "content": "import { RefObject } from 'preact';\nimport {\n  useMemo,\n  useRef,\n  useEffect,\n  useLayoutEffect,\n  useState,\n  useCallback,\n} from 'preact/hooks';\n\nimport ViewHeader from '@/components/common/ViewHeader';\nimport { GridContextMenu } from '@/components/contextMenu';\nimport {\n  getEventDayRange,\n  getEventsForYearDate,\n  groupDaysIntoRows,\n} from '@/components/yearView/utils';\nimport { YearRowComponent } from '@/components/yearView/YearRowComponent';\nimport { useLocale } from '@/locale';\nimport { useDragForView } from '@/plugins/dragBridge';\nimport {\n  monthViewContainer,\n  scrollContainer,\n  scrollbarHide,\n} from '@/styles/classNames';\nimport {\n  Event,\n  ViewType,\n  MonthEventDragState,\n  ICalendarApp,\n  YearViewConfig,\n} from '@/types';\nimport { dateToPlainDate, dateToZonedDateTime, hasEventChanged } from '@/utils';\n\nexport interface YearViewProps {\n  app: ICalendarApp;\n  calendarRef: RefObject<HTMLDivElement>;\n  useEventDetailPanel?: boolean;\n  config?: YearViewConfig;\n  selectedEventId?: string | null;\n  onEventSelect?: (eventId: string | null) => void;\n  detailPanelEventId?: string | null;\n  onDetailPanelToggle?: (eventId: string | null) => void;\n}\n\nexport const DefaultYearView = ({\n  app,\n  calendarRef,\n  useEventDetailPanel,\n  config,\n  selectedEventId: propSelectedEventId,\n  onEventSelect: propOnEventSelect,\n  detailPanelEventId: propDetailPanelEventId,\n  onDetailPanelToggle: propOnDetailPanelToggle,\n}: YearViewProps) => {\n  const { t, locale } = useLocale();\n  const currentDate = app.getCurrentDate();\n  const currentYear = currentDate.getFullYear();\n  const rawEvents = app.getEvents();\n  const appTimeZone = app.timeZone;\n  const scrollElementRef = useRef<HTMLDivElement>(null);\n  // Stable bucket refs: reused when element refs are identical so YearRowComponent\n  // memo comparator can bail out on rows whose events didn't change.\n  const stableBucketsRef = useRef<Event[][]>([]);\n  const MIN_YEAR_CELL_WIDTH = 80;\n\n  const [columnsPerRow, setColumnsPerRow] = useState(7);\n  const [isLayoutReady, setIsLayoutReady] = useState(false);\n  const [isMobile, setIsMobile] = useState(() => {\n    if (typeof window !== 'undefined') {\n      return window.innerWidth < 768;\n    }\n    return false;\n  });\n\n  const [internalSelectedId, setInternalSelectedId] = useState<string | null>(\n    null\n  );\n  const [internalDetailPanelEventId, setInternalDetailPanelEventId] = useState<\n    string | null\n  >(null);\n\n  const selectedEventId =\n    propSelectedEventId === undefined\n      ? internalSelectedId\n      : propSelectedEventId;\n  const detailPanelEventId =\n    propDetailPanelEventId === undefined\n      ? internalDetailPanelEventId\n      : propDetailPanelEventId;\n\n  const [newlyCreatedEventId, setNewlyCreatedEventId] = useState<string | null>(\n    null\n  );\n\n  const setSelectedEventId = useCallback(\n    (id: string | null) => {\n      if (propOnEventSelect) {\n        propOnEventSelect(id);\n      } else {\n        setInternalSelectedId(id);\n      }\n    },\n    [propOnEventSelect]\n  );\n\n  const setDetailPanelEventId = useCallback(\n    (id: string | null) => {\n      if (propOnDetailPanelToggle) {\n        propOnDetailPanelToggle(id);\n      } else {\n        setInternalDetailPanelEventId(id);\n      }\n    },\n    [propOnDetailPanelToggle]\n  );\n\n  const handleDetailPanelOpen = useCallback(() => {\n    setNewlyCreatedEventId(null);\n  }, []);\n\n  const [contextMenu, setContextMenu] = useState<{\n    x: number;\n    y: number;\n    date: Date;\n  } | null>(null);\n  const isEditable = app.canMutateFromUI();\n\n  const handleRowContextMenu = useCallback(\n    (menu: { x: number; y: number; date: Date } | null) => {\n      if (!isEditable) return;\n      setContextMenu(menu);\n    },\n    [isEditable]\n  );\n\n  useEffect(() => {\n    if (isEditable) return;\n    setContextMenu(null);\n  }, [isEditable]);\n\n  useEffect(() => {\n    const handleClickOutside = (e: MouseEvent) => {\n      if (e.button === 2) return; // Ignore right clicks\n\n      const target = e.target as HTMLElement;\n\n      const clickedEvent = target.closest('[data-event-id]');\n      const clickedPanel = target.closest('[data-event-detail-panel]');\n      const clickedDialog = target.closest('[data-event-detail-dialog]');\n      const clickedRangePicker = target.closest('[data-range-picker-popup]');\n      const clickedCalendarPicker = target.closest(\n        '[data-calendar-picker-dropdown]'\n      );\n      const clickedContextMenu = target.closest('.df-context-menu');\n\n      if (\n        !clickedEvent &&\n        !clickedPanel &&\n        !clickedDialog &&\n        !clickedRangePicker &&\n        !clickedCalendarPicker &&\n        !clickedContextMenu\n      ) {\n        setSelectedEventId(null);\n        setDetailPanelEventId(null);\n        setContextMenu(null);\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => document.removeEventListener('mousedown', handleClickOutside);\n  }, []);\n\n  const resizeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useLayoutEffect(() => {\n    const container = scrollElementRef.current;\n    if (!container) return;\n\n    const applyMeasuredLayout = (width: number) => {\n      if (width <= 0) return;\n\n      const cols = Math.max(1, Math.floor(width / MIN_YEAR_CELL_WIDTH));\n      setColumnsPerRow(cols);\n      setIsMobile(width < 768);\n      setIsLayoutReady(true);\n    };\n\n    // Measure once immediately so first paint already uses the real container width.\n    applyMeasuredLayout(container.clientWidth);\n\n    const observer = new ResizeObserver(entries => {\n      if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current);\n\n      resizeTimeoutRef.current = setTimeout(() => {\n        const width = entries[0].contentRect.width;\n        applyMeasuredLayout(width);\n      }, 60);\n    });\n\n    observer.observe(container);\n    return () => {\n      observer.disconnect();\n      if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current);\n    };\n  }, []);\n\n  // Sync highlighted event from app state\n  const prevHighlightedEventId = useRef(app.state.highlightedEventId);\n\n  useEffect(() => {\n    if (app.state.highlightedEventId) {\n      setSelectedEventId(app.state.highlightedEventId);\n\n      requestAnimationFrame(() => {\n        const container = scrollElementRef.current;\n        if (!container) return;\n\n        const el = container.querySelector(\n          `[data-event-id=\"${app.state.highlightedEventId}\"]`\n        ) as HTMLElement | null;\n        if (!el) return;\n\n        const containerRect = container.getBoundingClientRect();\n        const elRect = el.getBoundingClientRect();\n        const targetTop =\n          elRect.top -\n          containerRect.top +\n          container.scrollTop -\n          container.clientHeight / 2 +\n          elRect.height / 2;\n\n        container.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });\n      });\n    } else if (prevHighlightedEventId.current) {\n      setSelectedEventId(null);\n    }\n    prevHighlightedEventId.current = app.state.highlightedEventId;\n  }, [app.state.highlightedEventId]);\n\n  // Drag and Drop Hook\n  const { handleMoveStart, handleResizeStart, dragState, isDragging } =\n    useDragForView(app, {\n      calendarRef,\n      viewType: ViewType.YEAR,\n      onEventsUpdate: (updateFunc, isResizing, source) => {\n        const newEvents = updateFunc(rawEvents);\n\n        // Build a Map for O(1) lookups — the previous O(N²) .find() in .filter()\n        // caused ~100M string comparisons/tick with 10000 events.\n        const prevMap = new Map(rawEvents.map(e => [e.id, e]));\n        const eventsToUpdate = newEvents.filter(newEvent => {\n          const old = prevMap.get(newEvent.id);\n          return (\n            old !== undefined &&\n            old !== newEvent &&\n            hasEventChanged(old, newEvent)\n          );\n        });\n\n        if (eventsToUpdate.length > 0) {\n          app.applyEventsChanges(\n            {\n              update: eventsToUpdate.map(e => ({ id: e.id, updates: e })),\n            },\n            isResizing,\n            source\n          );\n        }\n      },\n      currentWeekStart: new Date(),\n      events: rawEvents,\n      onEventCreate: event => {\n        app.addEvent(event);\n      },\n      onEventEdit: event => {\n        setNewlyCreatedEventId(event.id);\n      },\n      isMobile,\n    });\n  const yearDragState = dragState as MonthEventDragState;\n\n  // Get config value\n  const showTimedEvents = config?.showTimedEventsInYearView ?? false;\n\n  const getDayEvents = useCallback(\n    (date: Date) => getEventsForYearDate(rawEvents, date, appTimeZone),\n    [rawEvents, appTimeZone]\n  );\n\n  const handleCellClick = useCallback(\n    (date: Date) => {\n      const clickAction = config?.gridDateClick;\n\n      if (typeof clickAction === 'function') {\n        clickAction(date, getDayEvents(date));\n        return;\n      }\n\n      if (clickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.DAY);\n        return;\n      }\n\n      if (clickAction === 'none') {\n        return;\n      }\n\n      app.selectDate(date);\n    },\n    [config?.gridDateClick, getDayEvents, app]\n  );\n\n  // Handle double click on cell - route to custom config when provided,\n  // otherwise preserve the existing create-event behavior.\n  const handleCellDoubleClick = useCallback(\n    (e: unknown, date: Date) => {\n      const dblClickAction = config?.gridDateDoubleClick ?? 'create-event';\n\n      if (typeof dblClickAction === 'function') {\n        dblClickAction(date, getDayEvents(date));\n        return;\n      }\n\n      if (dblClickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.DAY);\n        return;\n      }\n\n      if (dblClickAction === 'none') {\n        return;\n      }\n\n      // 'create-event' (default)\n      if (!app.canMutateFromUI()) return;\n      const writableCal = app\n        .getCalendarRegistry()\n        .getDefaultWritableCalendar();\n      if (!writableCal) return;\n\n      const startTime = new Date(date);\n      startTime.setHours(9, 0, 0, 0);\n      const endTime = new Date(date);\n      endTime.setHours(10, 0, 0, 0);\n\n      const newEvent: Event = {\n        id: `event-${Date.now()}`,\n        title: t('newEvent') || 'New Event',\n        start: dateToZonedDateTime(startTime, appTimeZone),\n        end: dateToZonedDateTime(endTime, appTimeZone),\n        allDay: false,\n        calendarId: writableCal.id,\n      };\n      app.addEvent(newEvent);\n      setNewlyCreatedEventId(newEvent.id);\n    },\n    [config?.gridDateDoubleClick, getDayEvents, app, t, app.timeZone]\n  );\n\n  // Generate all days for the current year\n  const yearDays = useMemo(() => {\n    const days: Date[] = [];\n    const start = new Date(currentYear, 0, 1);\n    const end = new Date(currentYear, 11, 31);\n\n    for (\n      let time = start.getTime();\n      time <= end.getTime();\n      time += 24 * 60 * 60 * 1000\n    ) {\n      days.push(new Date(time));\n    }\n    return days;\n  }, [currentYear]);\n\n  // Group days into rows\n  const rows = useMemo(\n    () => groupDaysIntoRows(yearDays, columnsPerRow),\n    [yearDays, columnsPerRow]\n  );\n\n  // Filter events for the current year (uses per-event cache via getEventDayRange)\n  const yearEvents = useMemo(() => {\n    const yearStartMs = new Date(currentYear, 0, 1).getTime();\n    const yearEndMs = new Date(currentYear, 11, 31, 23, 59, 59).getTime();\n\n    const result: Event[] = [];\n    for (let i = 0; i < rawEvents.length; i++) {\n      const event = rawEvents[i];\n      if (!event.start) continue;\n      if (!showTimedEvents && !event.allDay) continue;\n      const range = getEventDayRange(event, appTimeZone);\n      if (range.startMs <= yearEndMs && range.endMsEod >= yearStartMs) {\n        result.push(event);\n      }\n    }\n    return result;\n  }, [rawEvents, currentYear, showTimedEvents, appTimeZone]);\n\n  // Bucket events into rows in a single O(N + R) pass instead of O(N x R).\n  // Most events span 1-2 weeks; we walk forward through rows from the first\n  // overlap and stop once the event's end is past the row.\n  const eventsByRow = useMemo(() => {\n    const rowBoundaries = rows.map(rowDays => {\n      if (rowDays.length === 0) return null;\n      const firstDay = rowDays[0];\n      const lastDay = rowDays.at(-1);\n      if (!firstDay || !lastDay) return null;\n      return {\n        rowStartMs: new Date(\n          firstDay.getFullYear(),\n          firstDay.getMonth(),\n          firstDay.getDate()\n        ).getTime(),\n        rowEndMs: new Date(\n          lastDay.getFullYear(),\n          lastDay.getMonth(),\n          lastDay.getDate(),\n          23,\n          59,\n          59,\n          999\n        ).getTime(),\n      };\n    });\n\n    const newBuckets: Event[][] = rows.map(() => []);\n\n    for (let i = 0; i < yearEvents.length; i++) {\n      const event = yearEvents[i];\n      const range = getEventDayRange(event, appTimeZone);\n      for (let r = 0; r < rowBoundaries.length; r++) {\n        const b = rowBoundaries[r];\n        if (!b) continue;\n        if (range.startMs > b.rowEndMs) continue;\n        if (range.endMsEod < b.rowStartMs) break;\n        newBuckets[r].push(event);\n      }\n    }\n\n    // Stabilize: reuse previous bucket array ref when element refs are identical.\n    // This lets YearRowComponent's memo comparator (which checks events===) bail\n    // out for rows whose events didn't change during drag.\n    const prev = stableBucketsRef.current;\n    const stable = newBuckets.map((bucket, i) => {\n      const prevBucket = prev[i];\n      if (\n        prevBucket &&\n        prevBucket.length === bucket.length &&\n        bucket.every((e, j) => e === prevBucket[j])\n      ) {\n        return prevBucket;\n      }\n      return bucket;\n    });\n    stableBucketsRef.current = stable;\n    return stable;\n  }, [rows, yearEvents, appTimeZone]);\n\n  const dragPreviewEvent = useMemo(() => {\n    if (\n      !isDragging ||\n      !yearDragState.eventId ||\n      !yearDragState.startDate ||\n      !yearDragState.endDate ||\n      (yearDragState.mode !== 'move' && yearDragState.mode !== 'resize')\n    ) {\n      return null;\n    }\n\n    const baseEvent = yearEvents.find(\n      event => event.id === yearDragState.eventId\n    );\n    if (!baseEvent) return null;\n\n    return {\n      ...baseEvent,\n      start: baseEvent.allDay\n        ? dateToPlainDate(yearDragState.startDate)\n        : dateToZonedDateTime(yearDragState.startDate, appTimeZone),\n      end: baseEvent.allDay\n        ? dateToPlainDate(yearDragState.endDate)\n        : dateToZonedDateTime(yearDragState.endDate, appTimeZone),\n    } as Event;\n  }, [\n    isDragging,\n    yearDragState.eventId,\n    yearDragState.startDate,\n    yearDragState.endDate,\n    yearDragState.mode,\n    yearEvents,\n    appTimeZone,\n  ]);\n\n  const getCustomTitle = () => {\n    const isAsianLocale = locale.startsWith('zh') || locale.startsWith('ja');\n    return isAsianLocale ? `${currentYear}年` : `${currentYear}`;\n  };\n\n  return (\n    <div className={monthViewContainer} onContextMenu={e => e.preventDefault()}>\n      <ViewHeader\n        calendar={app}\n        viewType={ViewType.YEAR}\n        currentDate={currentDate}\n        customTitle={getCustomTitle()}\n        onPrevious={() => {\n          const newDate = new Date(currentDate);\n          newDate.setFullYear(newDate.getFullYear() - 1);\n          app.setCurrentDate(newDate);\n        }}\n        onNext={() => {\n          const newDate = new Date(currentDate);\n          newDate.setFullYear(newDate.getFullYear() + 1);\n          app.setCurrentDate(newDate);\n        }}\n        onToday={() => {\n          app.goToToday();\n        }}\n      />\n\n      <div\n        ref={scrollElementRef}\n        className={`df-year-default-scroll ${scrollContainer} ${scrollbarHide}`}\n      >\n        <div\n          className='df-year-default-rows'\n          style={{\n            opacity: isLayoutReady ? 1 : 0,\n            transition: 'opacity 0.2s ease',\n          }}\n        >\n          {rows.map((rowDays, index) => (\n            <YearRowComponent\n              key={index}\n              rowDays={rowDays}\n              events={eventsByRow[index]}\n              columnsPerRow={columnsPerRow}\n              app={app}\n              calendarRef={calendarRef}\n              locale={locale}\n              isDragging={isDragging}\n              dragState={yearDragState}\n              dragPreviewEvent={dragPreviewEvent}\n              onMoveStart={handleMoveStart}\n              onResizeStart={handleResizeStart}\n              onSelectDate={handleCellClick}\n              onCreateStart={handleCellDoubleClick}\n              selectedEventId={selectedEventId}\n              onEventSelect={setSelectedEventId}\n              onMoreEventsClick={app.onMoreEventsClick}\n              newlyCreatedEventId={newlyCreatedEventId}\n              onDetailPanelOpen={handleDetailPanelOpen}\n              detailPanelEventId={detailPanelEventId}\n              onDetailPanelToggle={setDetailPanelEventId}\n              useEventDetailPanel={useEventDetailPanel}\n              onContextMenu={handleRowContextMenu}\n              appTimeZone={appTimeZone}\n              isMobile={isMobile}\n            />\n          ))}\n        </div>\n      </div>\n      {isEditable && contextMenu && (\n        <GridContextMenu\n          x={contextMenu.x}\n          y={contextMenu.y}\n          date={contextMenu.date}\n          viewType={ViewType.YEAR}\n          onClose={() => setContextMenu(null)}\n          app={app}\n          onCreateEvent={() => {\n            if (contextMenu && contextMenu.date) {\n              const syntheticEvent = {\n                preventDefault: () => {\n                  /* noop */\n                },\n                stopPropagation: () => {\n                  /* noop */\n                },\n                clientX: contextMenu.x,\n                clientY: contextMenu.y,\n              } as unknown;\n              handleCellDoubleClick(syntheticEvent, contextMenu.date);\n            }\n          }}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/yearView/FixedWeekMonthRow.tsx",
    "content": "import { RefObject } from 'preact';\nimport { memo } from 'preact/compat';\nimport { useCallback, useMemo } from 'preact/hooks';\n\nimport { CalendarEvent } from '@/components/calendarEvent';\nimport {\n  Event,\n  MonthEventDragState,\n  ViewType,\n  EventDetailContentRenderer,\n  EventDetailDialogRenderer,\n  ICalendarApp,\n} from '@/types';\nimport { getTodayInTimeZone } from '@/utils';\n\nimport {\n  createPreviewMonthSegment,\n  eventOverlapsMonth,\n  FixedWeekMonthData,\n} from './utils';\n\ninterface FixedWeekMonthRowProps {\n  monthData: FixedWeekMonthData;\n  currentYear: number;\n  startOfWeek: number;\n  totalColumns: number;\n  app: ICalendarApp;\n  calendarRef: RefObject<HTMLDivElement>;\n  isDragging: boolean;\n  dragState: MonthEventDragState;\n  dragPreviewEvent?: Event | null;\n  selectedEventId: string | null;\n  onMoveStart?: (e: MouseEvent | TouchEvent, event: Event) => void;\n  onResizeStart?: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  onSelectDate: (date: Date) => void;\n  onCreateStart?: (e: MouseEvent | TouchEvent, targetDate: Date) => void;\n  onEventSelect: (eventId: string | null) => void;\n  newlyCreatedEventId?: string | null;\n  onDetailPanelOpen?: () => void;\n  detailPanelEventId: string | null;\n  onDetailPanelToggle: (eventId: string | null) => void;\n  customDetailPanelContent?: EventDetailContentRenderer;\n  customEventDetailDialog?: EventDetailDialogRenderer;\n  useEventDetailPanel?: boolean;\n  onContextMenu: (e: MouseEvent, date: Date) => void;\n  appTimeZone?: string;\n  isMobile?: boolean;\n}\n\nexport const FixedWeekMonthRow = memo(\n  ({\n    monthData,\n    currentYear,\n    startOfWeek,\n    totalColumns,\n    app,\n    calendarRef,\n    isDragging,\n    dragState,\n    dragPreviewEvent,\n    selectedEventId,\n    onMoveStart,\n    onResizeStart,\n    onSelectDate,\n    onCreateStart,\n    onEventSelect,\n    newlyCreatedEventId,\n    onDetailPanelOpen,\n    detailPanelEventId,\n    onDetailPanelToggle,\n    customDetailPanelContent: _customDetailPanelContent,\n    customEventDetailDialog: _customEventDetailDialog,\n    useEventDetailPanel,\n    onContextMenu,\n    appTimeZone,\n    isMobile,\n  }: FixedWeekMonthRowProps) => {\n    const today = useMemo(() => {\n      const now = getTodayInTimeZone(appTimeZone);\n      now.setHours(0, 0, 0, 0);\n      return now;\n    }, [appTimeZone]);\n\n    const handleEventUpdate = useCallback(\n      (updated: Event) => app.updateEvent(updated.id, updated),\n      [app]\n    );\n\n    const handleEventDelete = useCallback(\n      (id: string) => app.deleteEvent(id),\n      [app]\n    );\n\n    const isMovePreviewActive =\n      isDragging &&\n      dragState.mode === 'move' &&\n      !!dragPreviewEvent &&\n      dragPreviewEvent.id === dragState.eventId;\n\n    const dragPreviewSegment = useMemo(\n      () =>\n        isMovePreviewActive\n          ? createPreviewMonthSegment(\n              dragPreviewEvent,\n              monthData.monthIndex,\n              currentYear,\n              startOfWeek,\n              appTimeZone\n            )\n          : null,\n      [\n        isMovePreviewActive,\n        dragPreviewEvent,\n        monthData.monthIndex,\n        currentYear,\n        startOfWeek,\n        appTimeZone,\n      ]\n    );\n\n    const renderedSegments = useMemo(() => {\n      if (!isMovePreviewActive || !dragState.eventId) {\n        return monthData.eventSegments;\n      }\n\n      const staticSegments = monthData.eventSegments.filter(\n        segment => segment.event.id !== dragState.eventId\n      );\n\n      return dragPreviewSegment\n        ? [...staticSegments, dragPreviewSegment]\n        : staticSegments;\n    }, [\n      isMovePreviewActive,\n      dragState.eventId,\n      monthData.eventSegments,\n      dragPreviewSegment,\n    ]);\n\n    return (\n      <div\n        className='df-year-fixed-month-row'\n        style={{\n          minHeight: `${monthData.minHeight}px`,\n          transition: 'min-height 180ms cubic-bezier(0.22, 1, 0.36, 1)',\n        }}\n      >\n        <div\n          className='df-year-fixed-background-grid'\n          style={{\n            gridTemplateColumns: `repeat(${totalColumns}, minmax(0, 1fr))`,\n          }}\n        >\n          {monthData.days.map((date, dayIndex) => {\n            const dayOfWeek = (dayIndex + startOfWeek) % 7;\n            const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;\n\n            if (!date) {\n              return (\n                <div\n                  key={`empty-${monthData.monthIndex}-${dayIndex}`}\n                  className='df-year-fixed-empty-cell'\n                  data-weekend={isWeekend ? 'true' : 'false'}\n                />\n              );\n            }\n\n            const isToday = date.getTime() === today.getTime();\n            const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;\n\n            return (\n              <div\n                key={date.getTime()}\n                data-date={dateString}\n                className='df-year-fixed-day-cell'\n                data-dragging={isDragging ? 'true' : 'false'}\n                data-weekend={isWeekend ? 'true' : 'false'}\n                onClick={() => onSelectDate(date)}\n                onDblClick={e => onCreateStart?.(e, date)}\n                onContextMenu={e => onContextMenu(e, date)}\n              >\n                <span\n                  className='df-year-fixed-day-number'\n                  data-today={isToday ? 'true' : 'false'}\n                >\n                  {date.getDate()}\n                </span>\n              </div>\n            );\n          })}\n        </div>\n\n        {renderedSegments.length > 0 && (\n          <div className='df-year-fixed-event-layer' style={{ top: 20 }}>\n            <div className='df-year-fixed-event-layer-inner'>\n              {renderedSegments.map(segment => (\n                <div key={segment.id} className='df-year-fixed-event-hitbox'>\n                  <CalendarEvent\n                    event={segment.event}\n                    isAllDay={!!segment.event.allDay}\n                    viewType={ViewType.YEAR}\n                    yearSegment={segment}\n                    columnsPerRow={totalColumns}\n                    isBeingDragged={\n                      isDragging && dragState.eventId === segment.event.id\n                    }\n                    selectedEventId={selectedEventId}\n                    onMoveStart={onMoveStart}\n                    onResizeStart={isMobile ? undefined : onResizeStart}\n                    onEventSelect={onEventSelect}\n                    onDetailPanelToggle={onDetailPanelToggle}\n                    newlyCreatedEventId={newlyCreatedEventId}\n                    onDetailPanelOpen={onDetailPanelOpen}\n                    calendarRef={calendarRef}\n                    app={app}\n                    detailPanelEventId={detailPanelEventId}\n                    useEventDetailPanel={useEventDetailPanel}\n                    firstHour={0}\n                    hourHeight={0}\n                    onEventUpdate={handleEventUpdate}\n                    onEventDelete={handleEventDelete}\n                    appTimeZone={appTimeZone}\n                    isMobile={isMobile}\n                    enableTouch={isMobile}\n                  />\n                </div>\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n    );\n  },\n  (prevProps, nextProps) => {\n    const prevPreviewId = prevProps.dragPreviewEvent?.id;\n    const nextPreviewId = nextProps.dragPreviewEvent?.id;\n    const monthContainsDraggedEvent =\n      (!!prevPreviewId &&\n        prevProps.monthData.monthEvents.some(\n          event => event.id === prevPreviewId\n        )) ||\n      (!!nextPreviewId &&\n        nextProps.monthData.monthEvents.some(\n          event => event.id === nextPreviewId\n        ));\n    const prevOverlaps = eventOverlapsMonth(\n      prevProps.dragPreviewEvent,\n      prevProps.currentYear,\n      prevProps.monthData.monthIndex,\n      prevProps.appTimeZone\n    );\n    const nextOverlaps = eventOverlapsMonth(\n      nextProps.dragPreviewEvent,\n      nextProps.currentYear,\n      nextProps.monthData.monthIndex,\n      nextProps.appTimeZone\n    );\n\n    if (monthContainsDraggedEvent || prevOverlaps || nextOverlaps) {\n      return false;\n    }\n\n    return (\n      prevProps.monthData === nextProps.monthData &&\n      prevProps.currentYear === nextProps.currentYear &&\n      prevProps.startOfWeek === nextProps.startOfWeek &&\n      prevProps.totalColumns === nextProps.totalColumns &&\n      prevProps.app === nextProps.app &&\n      prevProps.calendarRef === nextProps.calendarRef &&\n      prevProps.selectedEventId === nextProps.selectedEventId &&\n      prevProps.onMoveStart === nextProps.onMoveStart &&\n      prevProps.onResizeStart === nextProps.onResizeStart &&\n      prevProps.onSelectDate === nextProps.onSelectDate &&\n      prevProps.onCreateStart === nextProps.onCreateStart &&\n      prevProps.onEventSelect === nextProps.onEventSelect &&\n      prevProps.newlyCreatedEventId === nextProps.newlyCreatedEventId &&\n      prevProps.onDetailPanelOpen === nextProps.onDetailPanelOpen &&\n      prevProps.detailPanelEventId === nextProps.detailPanelEventId &&\n      prevProps.onDetailPanelToggle === nextProps.onDetailPanelToggle &&\n      prevProps.customDetailPanelContent ===\n        nextProps.customDetailPanelContent &&\n      prevProps.customEventDetailDialog === nextProps.customEventDetailDialog &&\n      prevProps.useEventDetailPanel === nextProps.useEventDetailPanel &&\n      prevProps.onContextMenu === nextProps.onContextMenu &&\n      prevProps.appTimeZone === nextProps.appTimeZone &&\n      prevProps.isMobile === nextProps.isMobile\n    );\n  }\n);\n\n(FixedWeekMonthRow as { displayName?: string }).displayName =\n  'FixedWeekMonthRow';\n"
  },
  {
    "path": "packages/core/src/components/yearView/FixedWeekYearView.tsx",
    "content": "import { RefObject, JSX } from 'preact';\nimport {\n  useMemo,\n  useRef,\n  useCallback,\n  useState,\n  useEffect,\n} from 'preact/hooks';\n\nimport ViewHeader from '@/components/common/ViewHeader';\nimport { GridContextMenu } from '@/components/contextMenu';\nimport { useLocale } from '@/locale';\nimport { useDragForView } from '@/plugins/dragBridge';\nimport {\n  Event,\n  MonthEventDragState,\n  ViewType,\n  ICalendarApp,\n  YearViewConfig,\n} from '@/types';\nimport {\n  hasEventChanged,\n  scrollbarTakesSpace,\n  dateToZonedDateTime,\n} from '@/utils';\n\nimport { FixedWeekMonthRow } from './FixedWeekMonthRow';\nimport {\n  buildEffectiveFixedWeekMonthsData,\n  buildFixedWeekMonthsData,\n  createFixedWeekDragPreviewEvent,\n  FixedWeekMonthData,\n  getEventDayRange,\n  getEventsForYearDate,\n  getFixedWeekLabels,\n  getFixedWeekTotalColumns,\n} from './utils';\n\ninterface FixedWeekYearViewProps {\n  app: ICalendarApp;\n  calendarRef: RefObject<HTMLDivElement>;\n  useEventDetailPanel?: boolean;\n  config?: YearViewConfig;\n  selectedEventId?: string | null;\n  onEventSelect?: (eventId: string | null) => void;\n  detailPanelEventId?: string | null;\n  onDetailPanelToggle?: (eventId: string | null) => void;\n}\n\nexport const FixedWeekYearView = ({\n  app,\n  calendarRef,\n  useEventDetailPanel,\n  config,\n  selectedEventId: propSelectedEventId,\n  onEventSelect: propOnEventSelect,\n  detailPanelEventId: propDetailPanelEventId,\n  onDetailPanelToggle: propOnDetailPanelToggle,\n}: FixedWeekYearViewProps) => {\n  const { t, locale, getWeekDaysLabels } = useLocale();\n  const currentDate = app.getCurrentDate();\n  const currentYear = currentDate.getFullYear();\n  const rawEvents = app.getEvents();\n  const appTimeZone = app.timeZone;\n  const startOfWeek = config?.startOfWeek ?? 1;\n\n  // Refs for synchronized scrolling\n  const weekLabelsRef = useRef<HTMLDivElement>(null);\n  const monthLabelsRef = useRef<HTMLDivElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n\n  // State for scrollbar dimensions (to sync padding)\n  const [scrollbarWidth, setScrollbarWidth] = useState(0);\n  const [scrollbarHeight, setScrollbarHeight] = useState(0);\n\n  const [isMobile, setIsMobile] = useState(() =>\n    typeof window === 'undefined' ? false : window.innerWidth < 768\n  );\n\n  // State for event selection and detail panel\n  const [internalSelectedId, setInternalSelectedId] = useState<string | null>(\n    null\n  );\n  const [internalDetailPanelEventId, setInternalDetailPanelEventId] = useState<\n    string | null\n  >(null);\n\n  const hasScrollbarSpace = useMemo(() => scrollbarTakesSpace(), []);\n\n  const selectedEventId =\n    propSelectedEventId === undefined\n      ? internalSelectedId\n      : propSelectedEventId;\n  const detailPanelEventId =\n    propDetailPanelEventId === undefined\n      ? internalDetailPanelEventId\n      : propDetailPanelEventId;\n\n  const setSelectedEventId = useCallback(\n    (id: string | null) => {\n      if (propOnEventSelect) {\n        propOnEventSelect(id);\n      } else {\n        setInternalSelectedId(id);\n      }\n    },\n    [propOnEventSelect]\n  );\n\n  const setDetailPanelEventId = useCallback(\n    (id: string | null) => {\n      if (propOnDetailPanelToggle) {\n        propOnDetailPanelToggle(id);\n      } else {\n        setInternalDetailPanelEventId(id);\n      }\n    },\n    [propOnDetailPanelToggle]\n  );\n\n  const [newlyCreatedEventId, setNewlyCreatedEventId] = useState<string | null>(\n    null\n  );\n\n  const handleDetailPanelOpen = useCallback(\n    () => setNewlyCreatedEventId(null),\n    []\n  );\n\n  const [contextMenu, setContextMenu] = useState<{\n    x: number;\n    y: number;\n    date: Date;\n  } | null>(null);\n  const isEditable = app.canMutateFromUI();\n\n  const handleContextMenu = useCallback(\n    (e: MouseEvent, date: Date) => {\n      e.preventDefault();\n      e.stopPropagation();\n      if (!isEditable) return;\n      setContextMenu({ x: e.clientX, y: e.clientY, date });\n    },\n    [isEditable]\n  );\n\n  useEffect(() => {\n    if (isEditable) return;\n    setContextMenu(null);\n  }, [isEditable]);\n\n  useEffect(() => {\n    const handleResize = () => setIsMobile(window.innerWidth < 768);\n    window.addEventListener('resize', handleResize);\n    return () => window.removeEventListener('resize', handleResize);\n  }, []);\n\n  // Click outside handler\n  useEffect(() => {\n    const handleClickOutside = (e: MouseEvent) => {\n      const target = e.target as HTMLElement;\n      const clickedEvent = target.closest('[data-event-id]');\n      const clickedPanel = target.closest('[data-event-detail-panel]');\n      const clickedDialog = target.closest('[data-event-detail-dialog]');\n      const clickedRangePicker = target.closest('[data-range-picker-popup]');\n      const clickedCalendarPicker = target.closest(\n        '[data-calendar-picker-dropdown]'\n      );\n\n      if (\n        !clickedEvent &&\n        !clickedPanel &&\n        !clickedDialog &&\n        !clickedRangePicker &&\n        !clickedCalendarPicker\n      ) {\n        setSelectedEventId(null);\n        setDetailPanelEventId(null);\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => document.removeEventListener('mousedown', handleClickOutside);\n  }, []);\n\n  // Sync highlighted event from app state — scroll to it and select it\n  const prevHighlightedEventId = useRef(app.state.highlightedEventId);\n\n  const measureScrollbars = useCallback(() => {\n    if (contentRef.current) {\n      const el = contentRef.current;\n      const hScrollbar = el.offsetHeight - el.clientHeight;\n      const vScrollbar = el.offsetWidth - el.clientWidth;\n\n      setScrollbarHeight(prev => (prev === hScrollbar ? prev : hScrollbar));\n      setScrollbarWidth(prev => (prev === vScrollbar ? prev : vScrollbar));\n    }\n  }, []);\n\n  useEffect(() => {\n    const el = contentRef.current;\n    if (!el) return;\n\n    measureScrollbars();\n    const observer = new ResizeObserver(() => {\n      measureScrollbars();\n    });\n\n    observer.observe(el);\n    return () => {\n      observer.disconnect();\n    };\n  }, [measureScrollbars]);\n\n  useEffect(() => {\n    if (app.state.highlightedEventId) {\n      setSelectedEventId(app.state.highlightedEventId);\n\n      requestAnimationFrame(() => {\n        const container = contentRef.current;\n        if (!container) return;\n\n        const el = container.querySelector(\n          `[data-event-id=\"${app.state.highlightedEventId}\"]`\n        ) as HTMLElement | null;\n        if (!el) return;\n\n        const containerRect = container.getBoundingClientRect();\n        const elRect = el.getBoundingClientRect();\n        const targetTop =\n          elRect.top -\n          containerRect.top +\n          container.scrollTop -\n          container.clientHeight / 2 +\n          elRect.height / 2;\n        const targetLeft =\n          elRect.left -\n          containerRect.left +\n          container.scrollLeft -\n          container.clientWidth / 2 +\n          elRect.width / 2;\n\n        container.scrollTo({\n          top: Math.max(0, targetTop),\n          left: Math.max(0, targetLeft),\n          behavior: 'smooth',\n        });\n      });\n    } else if (prevHighlightedEventId.current) {\n      setSelectedEventId(null);\n    }\n    prevHighlightedEventId.current = app.state.highlightedEventId;\n  }, [app.state.highlightedEventId]);\n\n  // Calculate the maximum number of columns required for the current year\n  const totalColumns = useMemo(\n    () => getFixedWeekTotalColumns(currentYear, startOfWeek),\n    [currentYear, startOfWeek]\n  );\n\n  // Drag and Drop Hook\n  const { handleMoveStart, handleResizeStart, dragState, isDragging } =\n    useDragForView(app, {\n      calendarRef,\n      viewType: ViewType.YEAR,\n      onEventsUpdate: (updateFunc, isResizing, source) => {\n        const newEvents = updateFunc(rawEvents);\n\n        // Build a Map for O(1) lookups — the previous O(N²) .find() in .filter()\n        // caused ~100M string comparisons/tick with 10000 events.\n        const prevMap = new Map(rawEvents.map(e => [e.id, e]));\n        const eventsToUpdate = newEvents.filter(newEvent => {\n          const old = prevMap.get(newEvent.id);\n          return (\n            old !== undefined &&\n            old !== newEvent &&\n            hasEventChanged(old, newEvent)\n          );\n        });\n\n        if (eventsToUpdate.length > 0) {\n          app.applyEventsChanges(\n            {\n              update: eventsToUpdate.map(e => ({ id: e.id, updates: e })),\n            },\n            isResizing,\n            source\n          );\n        }\n      },\n      currentWeekStart: new Date(),\n      events: rawEvents,\n      onEventCreate: event => {\n        app.addEvent(event);\n      },\n      onEventEdit: event => {\n        setNewlyCreatedEventId(event.id);\n      },\n      isMobile,\n    });\n  const yearDragState = dragState as MonthEventDragState;\n\n  // Get config value\n  const showTimedEvents = config?.showTimedEventsInYearView ?? false;\n\n  const getDayEvents = useCallback(\n    (date: Date) => getEventsForYearDate(rawEvents, date, appTimeZone),\n    [rawEvents, appTimeZone]\n  );\n\n  const handleCellClick = useCallback(\n    (date: Date) => {\n      const clickAction = config?.gridDateClick;\n\n      if (typeof clickAction === 'function') {\n        clickAction(date, getDayEvents(date));\n        return;\n      }\n\n      if (clickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.DAY);\n        return;\n      }\n\n      if (clickAction === 'none') {\n        return;\n      }\n\n      app.selectDate(date);\n    },\n    [config?.gridDateClick, getDayEvents, app]\n  );\n\n  // Handle double click on cell - route to custom config when provided,\n  // otherwise preserve the existing create-event behavior.\n  const handleCellDoubleClick = useCallback(\n    (e: unknown, date: Date) => {\n      const dblClickAction = config?.gridDateDoubleClick ?? 'create-event';\n\n      if (typeof dblClickAction === 'function') {\n        dblClickAction(date, getDayEvents(date));\n        return;\n      }\n\n      if (dblClickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.DAY);\n        return;\n      }\n\n      if (dblClickAction === 'none') {\n        return;\n      }\n\n      // 'create-event' (default)\n      if (!app.canMutateFromUI()) return;\n      const writableCal = app\n        .getCalendarRegistry()\n        .getDefaultWritableCalendar();\n      if (!writableCal) return;\n\n      const startTime = new Date(date);\n      startTime.setHours(9, 0, 0, 0);\n      const endTime = new Date(date);\n      endTime.setHours(10, 0, 0, 0);\n\n      const newEvent: Event = {\n        id: `event-${Date.now()}`,\n        title: t('newEvent') || 'New Event',\n        start: dateToZonedDateTime(startTime, appTimeZone),\n        end: dateToZonedDateTime(endTime, appTimeZone),\n        allDay: false,\n        calendarId: writableCal.id,\n      };\n      app.addEvent(newEvent);\n      setNewlyCreatedEventId(newEvent.id);\n    },\n    [config?.gridDateDoubleClick, getDayEvents, app, t, appTimeZone]\n  );\n\n  // Generate week header labels\n  const weekLabels = useMemo(\n    () =>\n      getFixedWeekLabels({\n        locale,\n        totalColumns,\n        startOfWeek,\n        getWeekDaysLabels,\n      }),\n    [locale, totalColumns, startOfWeek, getWeekDaysLabels]\n  );\n\n  // Filter events for the current year (uses per-event cache via getEventDayRange)\n  const yearEvents = useMemo(() => {\n    const yearStartMs = new Date(currentYear, 0, 1).getTime();\n    const yearEndMs = new Date(currentYear, 11, 31, 23, 59, 59).getTime();\n\n    const result: Event[] = [];\n    for (let i = 0; i < rawEvents.length; i++) {\n      const event = rawEvents[i];\n      if (!event.start) continue;\n      if (!showTimedEvents && !event.allDay) continue;\n      const range = getEventDayRange(event, appTimeZone);\n      if (range.startMs <= yearEndMs && range.endMsEod >= yearStartMs) {\n        result.push(event);\n      }\n    }\n    return result;\n  }, [rawEvents, currentYear, showTimedEvents, appTimeZone]);\n\n  // Generate data for all 12 months with event segments\n  const monthsData = useMemo<FixedWeekMonthData[]>(\n    () =>\n      buildFixedWeekMonthsData({\n        currentYear,\n        locale,\n        totalColumns,\n        yearEvents,\n        startOfWeek,\n        appTimeZone,\n      }),\n    [currentYear, locale, totalColumns, yearEvents, startOfWeek, appTimeZone]\n  );\n\n  const dragPreviewEvent = useMemo(\n    () =>\n      createFixedWeekDragPreviewEvent({\n        isDragging,\n        dragState: yearDragState,\n        yearEvents,\n        appTimeZone,\n      }),\n    [isDragging, yearDragState, yearEvents, appTimeZone]\n  );\n\n  const isMovePreviewActive =\n    isDragging &&\n    yearDragState.mode === 'move' &&\n    !!dragPreviewEvent &&\n    dragPreviewEvent.id === yearDragState.eventId;\n\n  const effectiveMonthsData = useMemo(\n    () =>\n      buildEffectiveFixedWeekMonthsData({\n        monthsData,\n        dragPreviewEvent,\n        isMovePreviewActive,\n        currentYear,\n        startOfWeek,\n        appTimeZone,\n      }),\n    [\n      monthsData,\n      dragPreviewEvent,\n      isMovePreviewActive,\n      currentYear,\n      startOfWeek,\n      appTimeZone,\n    ]\n  );\n\n  const monthHeightSignature = useMemo(\n    () => effectiveMonthsData.map(month => month.minHeight).join(','),\n    [effectiveMonthsData]\n  );\n\n  useEffect(() => {\n    measureScrollbars();\n  }, [measureScrollbars, monthHeightSignature]);\n\n  // Handle scroll synchronization\n  const handleContentScroll = useCallback(\n    (e: JSX.TargetedEvent<HTMLDivElement, globalThis.Event>) => {\n      const target = e.currentTarget;\n      if (weekLabelsRef.current) {\n        weekLabelsRef.current.scrollLeft = target.scrollLeft;\n      }\n      if (monthLabelsRef.current) {\n        monthLabelsRef.current.scrollTop = target.scrollTop;\n      }\n    },\n    []\n  );\n\n  const getCustomTitle = () => {\n    const isAsianLocale = locale.startsWith('zh') || locale.startsWith('ja');\n    return isAsianLocale ? `${currentYear}年` : `${currentYear}`;\n  };\n\n  return (\n    <div className='df-year-fixed' onContextMenu={e => e.preventDefault()}>\n      {/* Year Header */}\n      <div className='df-year-fixed-header-span'>\n        <ViewHeader\n          calendar={app}\n          viewType={ViewType.YEAR}\n          currentDate={currentDate}\n          customTitle={getCustomTitle()}\n          onPrevious={() => {\n            const newDate = new Date(currentDate);\n            newDate.setFullYear(newDate.getFullYear() - 1);\n            app.setCurrentDate(newDate);\n          }}\n          onNext={() => {\n            const newDate = new Date(currentDate);\n            newDate.setFullYear(newDate.getFullYear() + 1);\n            app.setCurrentDate(newDate);\n          }}\n          onToday={() => {\n            app.goToToday();\n          }}\n        />\n      </div>\n\n      {/* Corner - Fixed */}\n      <div className='df-year-fixed-corner' />\n\n      {/* Week Labels Header */}\n      <div ref={weekLabelsRef} className='df-year-fixed-week-header'>\n        <div\n          className='df-year-fixed-week-header-inner'\n          style={{ minWidth: `calc(1352px + ${scrollbarWidth}px)` }}\n        >\n          <div\n            className='df-year-fixed-week-grid'\n            style={{\n              gridTemplateColumns: `repeat(${totalColumns}, minmax(0, 1fr))`,\n              minWidth: '1352px',\n            }}\n          >\n            {weekLabels.map((label, i) => {\n              const dayOfWeek = (i + startOfWeek) % 7;\n              const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;\n              return (\n                <div\n                  key={`label-${i}`}\n                  className='df-year-fixed-week-label'\n                  data-weekend={isWeekend ? 'true' : 'false'}\n                >\n                  {label}\n                </div>\n              );\n            })}\n          </div>\n          {/* Spacer to compensate for vertical scrollbar in content area */}\n          {scrollbarWidth > 0 && (\n            <div\n              className='df-year-fixed-week-spacer'\n              data-scrollbar-space={hasScrollbarSpace ? 'true' : 'false'}\n              style={{ width: `${scrollbarWidth}px` }}\n            />\n          )}\n        </div>\n      </div>\n\n      {/* Month Labels Sidebar */}\n      <div ref={monthLabelsRef} className='df-year-fixed-month-sidebar'>\n        <div className='df-year-fixed-month-sidebar-inner'>\n          {effectiveMonthsData.map(month => (\n            <div\n              key={month.monthIndex}\n              className='df-year-fixed-month-label'\n              style={{\n                minHeight: `${month.minHeight}px`,\n                transition: 'min-height 180ms cubic-bezier(0.22, 1, 0.36, 1)',\n              }}\n            >\n              {month.monthName}\n            </div>\n          ))}\n          {/* Spacer to compensate for horizontal scrollbar in content area */}\n          {scrollbarHeight > 0 && (\n            <div\n              className='df-year-fixed-month-spacer'\n              data-scrollbar-space={hasScrollbarSpace ? 'true' : 'false'}\n              style={{ height: `${scrollbarHeight}px` }}\n            />\n          )}\n        </div>\n      </div>\n\n      {/* Days Grid Content - Scrollable */}\n      <div\n        ref={contentRef}\n        className='df-year-fixed-content'\n        onScroll={handleContentScroll}\n      >\n        <div\n          className='df-year-fixed-content-inner'\n          style={{ minWidth: '1352px' }}\n        >\n          {effectiveMonthsData.map(monthData => (\n            <FixedWeekMonthRow\n              key={monthData.monthIndex}\n              monthData={monthData}\n              currentYear={currentYear}\n              startOfWeek={startOfWeek}\n              totalColumns={totalColumns}\n              app={app}\n              calendarRef={calendarRef}\n              isDragging={isDragging}\n              dragState={yearDragState}\n              dragPreviewEvent={dragPreviewEvent}\n              selectedEventId={selectedEventId}\n              onMoveStart={handleMoveStart}\n              onResizeStart={handleResizeStart}\n              onSelectDate={handleCellClick}\n              onCreateStart={handleCellDoubleClick}\n              onEventSelect={setSelectedEventId}\n              newlyCreatedEventId={newlyCreatedEventId}\n              onDetailPanelOpen={handleDetailPanelOpen}\n              detailPanelEventId={detailPanelEventId}\n              onDetailPanelToggle={setDetailPanelEventId}\n              useEventDetailPanel={useEventDetailPanel}\n              onContextMenu={handleContextMenu}\n              appTimeZone={appTimeZone}\n              isMobile={isMobile}\n            />\n          ))}\n        </div>\n      </div>\n      {isEditable && contextMenu && (\n        <GridContextMenu\n          x={contextMenu.x}\n          y={contextMenu.y}\n          date={contextMenu.date}\n          viewType={ViewType.YEAR}\n          onClose={() => setContextMenu(null)}\n          app={app}\n          onCreateEvent={() => {\n            const syntheticEvent = {\n              preventDefault: () => {\n                /* noop */\n              },\n              stopPropagation: () => {\n                /* noop */\n              },\n              clientX: contextMenu.x,\n              clientY: contextMenu.y,\n            } as unknown;\n            handleCellDoubleClick(syntheticEvent, contextMenu.date);\n          }}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/yearView/GridDayPopup.tsx",
    "content": "import { ComponentChildren } from 'preact';\nimport { createPortal } from 'preact/compat';\nimport { useEffect, useRef } from 'preact/hooks';\n\nimport { useLocale } from '@/locale';\nimport { ICalendarApp, Event } from '@/types';\nimport { temporalToVisualDate } from '@/utils';\n\ninterface GridDayPopupProps {\n  date: Date;\n  events: Event[];\n  anchorEl: HTMLElement;\n  /** Pre-calculated position so the popup renders at the right spot on frame 1. */\n  position: { top: number; left: number };\n  onClose: () => void;\n  locale: string;\n  app: ICalendarApp;\n  customContent?: (date: Date, events: Event[]) => ComponentChildren;\n  appTimeZone?: string;\n}\n\nexport const GridDayPopup = ({\n  date,\n  events,\n  anchorEl,\n  position,\n  onClose,\n  locale,\n  app,\n  customContent,\n  appTimeZone,\n}: GridDayPopupProps) => {\n  const { t } = useLocale();\n  const popupRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const handleDown = (e: MouseEvent) => {\n      const target = e.target as Node;\n      if (\n        popupRef.current &&\n        !popupRef.current.contains(target) &&\n        !anchorEl.contains(target)\n      ) {\n        onClose();\n      }\n    };\n    const handleKey = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') onClose();\n    };\n    document.addEventListener('mousedown', handleDown);\n    document.addEventListener('keydown', handleKey);\n    return () => {\n      document.removeEventListener('mousedown', handleDown);\n      document.removeEventListener('keydown', handleKey);\n    };\n  }, [anchorEl, onClose]);\n\n  const calendars = app.getCalendars();\n  const calendarMap = new Map(calendars.map(c => [c.id, c]));\n\n  const dateLabel = date.toLocaleDateString(locale, {\n    weekday: 'long',\n    month: 'long',\n    day: 'numeric',\n  });\n\n  const defaultContent = (\n    <>\n      <div className='df-year-popup-header'>\n        <div className='df-year-popup-title'>{dateLabel}</div>\n      </div>\n      <div className='df-year-popup-body'>\n        {events.length === 0 ? (\n          <div className='df-year-popup-empty'>No events</div>\n        ) : (\n          events.map(event => {\n            const cal = event.calendarId\n              ? calendarMap.get(event.calendarId)\n              : undefined;\n            const color = cal?.colors?.lineColor ?? '#3b82f6';\n\n            let timeStr = '';\n            if (!event.allDay && event.start) {\n              const startDate = temporalToVisualDate(event.start, appTimeZone);\n              timeStr = startDate.toLocaleTimeString(locale, {\n                hour: '2-digit',\n                minute: '2-digit',\n                hour12: false,\n              });\n              if (event.end) {\n                const endDate = temporalToVisualDate(event.end, appTimeZone);\n                timeStr += ` – ${endDate.toLocaleTimeString(locale, {\n                  hour: '2-digit',\n                  minute: '2-digit',\n                  hour12: false,\n                })}`;\n              }\n            }\n\n            return (\n              <div key={event.id} className='df-year-popup-event'>\n                <div\n                  className='df-year-popup-dot'\n                  style={{ backgroundColor: color }}\n                />\n                <div className='df-year-popup-event-main'>\n                  <div className='df-year-popup-event-title'>{event.title}</div>\n                  {event.allDay && (\n                    <div className='df-year-popup-event-meta'>\n                      {t('allDay')}\n                    </div>\n                  )}\n                  {timeStr && (\n                    <div className='df-year-popup-event-meta'>{timeStr}</div>\n                  )}\n                </div>\n              </div>\n            );\n          })\n        )}\n      </div>\n    </>\n  );\n\n  return createPortal(\n    <div\n      ref={popupRef}\n      data-grid-day-popup\n      className='df-year-popup df-animate-in df-fade-in df-zoom-in-95'\n      style={{ top: position.top, left: position.left }}\n    >\n      {customContent ? customContent(date, events) : defaultContent}\n    </div>,\n    document.body\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/yearView/GridYearView.tsx",
    "content": "import { useMemo, useState, useCallback, useEffect } from 'preact/hooks';\n\nimport ViewHeader from '@/components/common/ViewHeader';\nimport { useLocale } from '@/locale';\nimport { monthViewContainer } from '@/styles/classNames';\nimport { Event, ICalendarApp, ViewType, YearViewConfig } from '@/types';\nimport {\n  getTodayInTimeZone,\n  temporalToVisualDate,\n  generateUniKey,\n  dateToZonedDateTime,\n} from '@/utils';\n\nimport { GridDayPopup } from './GridDayPopup';\n\ninterface GridYearViewProps {\n  app: ICalendarApp;\n  config?: YearViewConfig;\n}\n\n/** Returns inline background color for event concentration using CSS variables */\nfunction getIntensityStyle(\n  count: number,\n  levels: number\n): { backgroundColor?: string } {\n  if (count === 0) return {};\n  const step = Math.min(count, levels);\n  return { backgroundColor: `var(--heat-${step})` };\n}\n\nfunction getHeatLevel(count: number, levels: number): number {\n  if (count === 0) return 0;\n  return Math.min(count, levels);\n}\n\n/** Build a map from 'YYYY-MM-DD' → Event[] for events in the year */\nfunction buildDayEventMap(\n  events: Event[],\n  year: number,\n  showTimedEvents: boolean,\n  appTimeZone?: string\n): Map<string, Event[]> {\n  const map = new Map<string, Event[]>();\n\n  const yearStart = new Date(year, 0, 1);\n  yearStart.setHours(0, 0, 0, 0);\n  const yearEnd = new Date(year, 11, 31, 23, 59, 59, 999);\n\n  for (const event of events) {\n    if (!event.start) continue;\n    if (!showTimedEvents && !event.allDay) continue;\n\n    const start = temporalToVisualDate(event.start, appTimeZone);\n    const end = event.end\n      ? temporalToVisualDate(event.end, appTimeZone)\n      : new Date(start);\n    start.setHours(0, 0, 0, 0);\n    end.setHours(0, 0, 0, 0);\n\n    if (start > yearEnd || end < yearStart) continue;\n\n    const clampedStart =\n      start < yearStart ? new Date(yearStart) : new Date(start);\n    const clampedEnd = end > yearEnd ? new Date(year, 11, 31) : new Date(end);\n    clampedStart.setHours(0, 0, 0, 0);\n    clampedEnd.setHours(0, 0, 0, 0);\n\n    const DAY_MS = 24 * 60 * 60 * 1000;\n    const startMs = clampedStart.getTime();\n    const days = Math.round((clampedEnd.getTime() - startMs) / DAY_MS);\n    for (let i = 0; i <= days; i++) {\n      const d = new Date(startMs + i * DAY_MS);\n      const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n      const existing = map.get(key);\n      if (existing) {\n        existing.push(event);\n      } else {\n        map.set(key, [event]);\n      }\n    }\n  }\n\n  return map;\n}\n\n/** Calculate popup position from anchor element synchronously on click. */\nfunction calcPopupPosition(anchorEl: HTMLElement): {\n  top: number;\n  left: number;\n} {\n  const rect = anchorEl.getBoundingClientRect();\n  const POPUP_W = 256; // w-64\n  const POPUP_H = 220; // generous estimate\n  const pad = 8;\n  const vw = window.innerWidth;\n  const vh = window.innerHeight;\n\n  let left = rect.right + pad;\n  if (left + POPUP_W > vw - pad) left = rect.left - POPUP_W - pad;\n  left = Math.max(pad, Math.min(left, vw - POPUP_W - pad));\n\n  let top = rect.top;\n  if (top + POPUP_H > vh - pad) top = vh - POPUP_H - pad;\n  top = Math.max(pad, top);\n\n  return { top, left };\n}\n\nexport const GridYearView = ({ app, config }: GridYearViewProps) => {\n  const { t: translate, locale, getWeekDaysLabels: getLabels } = useLocale();\n  const currentDate = app.getCurrentDate();\n  const currentYear = currentDate.getFullYear();\n  const rawEvents = app.getEvents();\n  const startOfWeek = config?.startOfWeek ?? 1;\n  const showTimedEvents = config?.showTimedEventsInYearView ?? false;\n  const heatmapLevels = config?.gridHeatmapLevels ?? 5;\n  const appTimeZone = app.timeZone;\n\n  const today = useMemo(() => {\n    const t = getTodayInTimeZone(appTimeZone);\n    t.setHours(0, 0, 0, 0);\n    return t;\n  }, [appTimeZone]);\n\n  // Popup state — position is pre-calculated on click so the popup renders\n  // at the correct coordinates on its very first frame (no flash).\n  const [popup, setPopup] = useState<{\n    date: Date;\n    monthIndex: number;\n    anchorEl: HTMLElement;\n    position: { top: number; left: number };\n  } | null>(null);\n\n  // Click-outside handler for popup (also used by cells to close on outside click)\n  useEffect(() => {\n    if (!popup) return;\n    const handleClickOutside = (e: MouseEvent) => {\n      const target = e.target as HTMLElement;\n      if (target.closest('[data-grid-day-popup]')) return;\n      if (target.closest('[data-grid-day-cell]')) return;\n      setPopup(null);\n    };\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => document.removeEventListener('mousedown', handleClickOutside);\n  }, [popup]);\n\n  // Heatmap event map (respects showTimedEvents filter)\n  const heatmapEventMap = useMemo(\n    () =>\n      buildDayEventMap(rawEvents, currentYear, showTimedEvents, appTimeZone),\n    [rawEvents, currentYear, showTimedEvents, appTimeZone]\n  );\n\n  // Full event map for popup (shows all events regardless of filter)\n  const fullEventMap = useMemo(\n    () => buildDayEventMap(rawEvents, currentYear, true, appTimeZone),\n    [rawEvents, currentYear, appTimeZone]\n  );\n\n  // Week day header labels (narrow: M T W T F S S)\n  const weekDayLabels = useMemo(() => {\n    const labels = getLabels(locale, 'narrow', startOfWeek);\n    return labels.map(l =>\n      locale.startsWith('zh') ? l.at(-1)! : l.charAt(0).toUpperCase()\n    );\n  }, [locale, getLabels, startOfWeek]);\n\n  // Build 12 month data structures — always exactly 42 cells filled with real\n  // dates (prev/next month overflow), like a standard calendar widget.\n  const monthsData = useMemo(() => {\n    const months = [];\n    for (let m = 0; m < 12; m++) {\n      const monthStart = new Date(currentYear, m, 1);\n      const daysInMonth = new Date(currentYear, m + 1, 0).getDate();\n      const firstDayOfWeek = monthStart.getDay();\n      const paddingStart = (firstDayOfWeek - startOfWeek + 7) % 7;\n\n      const cells: Array<{ date: Date; isCurrentMonth: boolean }> = [];\n\n      // Trailing days from previous month\n      for (let i = paddingStart - 1; i >= 0; i--) {\n        cells.push({\n          date: new Date(currentYear, m, -i),\n          isCurrentMonth: false,\n        });\n      }\n      // Current month days\n      for (let d = 1; d <= daysInMonth; d++) {\n        cells.push({ date: new Date(currentYear, m, d), isCurrentMonth: true });\n      }\n      // Leading days from next month to reach exactly 42\n      let nextDay = 1;\n      while (cells.length < 42) {\n        cells.push({\n          date: new Date(currentYear, m + 1, nextDay++),\n          isCurrentMonth: false,\n        });\n      }\n\n      const monthName = monthStart.toLocaleDateString(locale, {\n        month: 'long',\n      });\n      const formattedName =\n        monthName.charAt(0).toUpperCase() + monthName.slice(1);\n\n      months.push({ monthIndex: m, monthName: formattedName, cells });\n    }\n    return months;\n  }, [currentYear, locale, startOfWeek]);\n\n  const handleDateClick = useCallback(\n    (e: MouseEvent, date: Date, monthIndex: number) => {\n      const clickAction = config?.gridDateClick ?? 'popup';\n\n      if (typeof clickAction === 'function') {\n        const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;\n        clickAction(date, fullEventMap.get(key) ?? []);\n        return;\n      }\n\n      if (clickAction === 'popup') {\n        const anchorEl = e.currentTarget as HTMLElement;\n        // Toggle: close if same cell (same date + same panel) is clicked again\n        if (\n          popup &&\n          popup.date.getTime() === date.getTime() &&\n          popup.monthIndex === monthIndex\n        ) {\n          setPopup(null);\n        } else {\n          setPopup({\n            date,\n            monthIndex,\n            anchorEl,\n            position: calcPopupPosition(anchorEl),\n          });\n        }\n      } else if (clickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.DAY);\n      }\n      // 'none' → do nothing\n    },\n    [config, app, popup, fullEventMap]\n  );\n\n  const handleDateDoubleClick = useCallback(\n    (date: Date) => {\n      const dblClickAction = config?.gridDateDoubleClick ?? 'create-event';\n\n      if (typeof dblClickAction === 'function') {\n        const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;\n        dblClickAction(date, fullEventMap.get(key) ?? []);\n        return;\n      }\n\n      if (dblClickAction === 'create-event') {\n        if (!app.canMutateFromUI()) return;\n        const writableCal = app\n          .getCalendarRegistry()\n          .getDefaultWritableCalendar();\n        if (!writableCal) return;\n\n        const startTime = new Date(date);\n        startTime.setHours(9, 0, 0, 0);\n        const endTime = new Date(date);\n        endTime.setHours(10, 0, 0, 0);\n\n        const newEvent: Event = {\n          id: generateUniKey(),\n          title: translate('newEvent') || 'New Event',\n          start: dateToZonedDateTime(startTime, appTimeZone),\n          end: dateToZonedDateTime(endTime, appTimeZone),\n          allDay: false,\n          calendarId: writableCal.id,\n        };\n        setPopup(null);\n        app.addEvent(newEvent);\n        return;\n      }\n\n      if (dblClickAction === 'day-view') {\n        setPopup(null);\n        app.setCurrentDate(date);\n        app.changeView(ViewType.DAY);\n      }\n      // 'none' → do nothing\n    },\n    [config, app, fullEventMap, translate, appTimeZone]\n  );\n\n  const getCustomTitle = () => {\n    const isAsianLocale = locale.startsWith('zh') || locale.startsWith('ja');\n    return isAsianLocale ? `${currentYear}年` : `${currentYear}`;\n  };\n\n  const popupEvents = useMemo(() => {\n    if (!popup) return [];\n    const key = `${popup.date.getFullYear()}-${String(popup.date.getMonth() + 1).padStart(2, '0')}-${String(popup.date.getDate()).padStart(2, '0')}`;\n    const events = fullEventMap.get(key) ?? [];\n    return [...events].toSorted((a, b) => {\n      const aAllDay = !!a.allDay;\n      const bAllDay = !!b.allDay;\n      if (aAllDay !== bAllDay) return aAllDay ? -1 : 1;\n      return 0;\n    });\n  }, [popup, fullEventMap]);\n\n  return (\n    <div className={monthViewContainer}>\n      <ViewHeader\n        calendar={app}\n        viewType={ViewType.YEAR}\n        currentDate={currentDate}\n        customTitle={getCustomTitle()}\n        onPrevious={() => {\n          const d = new Date(currentDate);\n          d.setFullYear(d.getFullYear() - 1);\n          app.setCurrentDate(d);\n        }}\n        onNext={() => {\n          const d = new Date(currentDate);\n          d.setFullYear(d.getFullYear() + 1);\n          app.setCurrentDate(d);\n        }}\n        onToday={() => app.goToToday()}\n      />\n\n      {/* 4-col × 3-row grid that fills all remaining space — no scroll */}\n      <div\n        className='df-year-grid'\n        style={{\n          gridTemplateColumns: 'repeat(4, 1fr)',\n          gridTemplateRows: 'repeat(3, 1fr)',\n        }}\n      >\n        {monthsData.map(month => (\n          <div\n            key={month.monthIndex}\n            className='df-year-grid-month df-year-grid-month'\n          >\n            {/* Month name */}\n            <div className='df-year-grid-month-title'>{month.monthName}</div>\n\n            {/* Container for labels and cells to ensure alignment and fit */}\n            <div className='df-year-grid-month-body'>\n              <div\n                className='df-year-grid-calendar'\n                style={{\n                  gridTemplateColumns: 'repeat(7, 1fr)',\n                  gridTemplateRows: 'repeat(7, 1fr)',\n                }}\n              >\n                {/* Day-of-week headers */}\n                {weekDayLabels.map((label, i) => (\n                  <div key={i} className='df-year-grid-weekday'>\n                    {label}\n                  </div>\n                ))}\n\n                {/* Day cells — exactly 42 cells (6 rows × 7 cols) */}\n                {month.cells.map(({ date, isCurrentMonth }, i) => {\n                  const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;\n                  const eventCount = isCurrentMonth\n                    ? (heatmapEventMap.get(key)?.length ?? 0)\n                    : 0;\n                  const heatLevel = getHeatLevel(eventCount, heatmapLevels);\n                  const intensityStyle = isCurrentMonth\n                    ? getIntensityStyle(eventCount, heatmapLevels)\n                    : {};\n                  const isToday = date.getTime() === today.getTime();\n                  return (\n                    <div\n                      key={`${month.monthIndex}-${i}`}\n                      data-grid-day-cell\n                      className='df-year-grid-day'\n                      data-current-month={isCurrentMonth ? 'true' : 'false'}\n                      style={intensityStyle}\n                      onClick={e => handleDateClick(e, date, month.monthIndex)}\n                      onDblClick={() => handleDateDoubleClick(date)}\n                    >\n                      <div className='df-year-grid-day-inner'>\n                        <span\n                          className='df-year-grid-day-number'\n                          data-today={isToday ? 'true' : 'false'}\n                          data-current-month={isCurrentMonth ? 'true' : 'false'}\n                          data-has-events={eventCount > 0 ? 'true' : 'false'}\n                          data-heat-level={String(heatLevel)}\n                        >\n                          {date.getDate()}\n                        </span>\n                      </div>\n                    </div>\n                  );\n                })}\n              </div>\n            </div>\n          </div>\n        ))}\n      </div>\n\n      {popup && (\n        <GridDayPopup\n          date={popup.date}\n          events={popupEvents}\n          anchorEl={popup.anchorEl}\n          position={popup.position}\n          onClose={() => setPopup(null)}\n          locale={locale}\n          app={app}\n          customContent={config?.gridPopupContent}\n          appTimeZone={appTimeZone}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/components/yearView/YearDayCell.tsx",
    "content": "import { memo } from 'preact/compat';\n\nimport { useLocale } from '@/locale';\n\ninterface YearDayCellProps {\n  date: Date;\n  isToday: boolean;\n  locale: string;\n  onSelectDate: (date: Date) => void;\n  onCreateStart?: (e: MouseEvent | TouchEvent, targetDate: Date) => void;\n  onMoreEventsClick?: (date: Date) => void;\n  moreCount?: number;\n  onContextMenu?: (e: MouseEvent, date: Date) => void;\n}\n\nexport const YearDayCell = memo(\n  ({\n    date,\n    isToday,\n    locale,\n    onSelectDate,\n    onCreateStart,\n    onMoreEventsClick,\n    moreCount = 0,\n    onContextMenu,\n  }: YearDayCellProps) => {\n    const { t } = useLocale();\n    const day = date.getDate();\n    const isFirstDay = day === 1;\n    const monthLabel = date\n      .toLocaleDateString(locale, { month: 'short' })\n      .toUpperCase();\n    const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;\n\n    return (\n      <div\n        className='df-year-day-cell'\n        data-first-day={isFirstDay ? 'true' : 'false'}\n        style={{ aspectRatio: '1/1' }}\n        onClick={() => onSelectDate(date)}\n        onDblClick={e => onCreateStart?.(e, date)}\n        onContextMenu={e => {\n          e.preventDefault();\n          e.stopPropagation();\n          onContextMenu?.(e, date);\n        }}\n        data-date={dateString}\n      >\n        <div className='df-year-day-cell-header'>\n          {isFirstDay && (\n            <span className='df-year-day-cell-month-pill'>{monthLabel}</span>\n          )}\n          <span\n            className='df-year-day-cell-date'\n            data-today={isToday ? 'true' : 'false'}\n          >\n            {day}\n          </span>\n        </div>\n\n        {moreCount > 0 && (\n          <div className='df-year-day-cell-more-wrap'>\n            <span\n              className='df-year-day-cell-more'\n              onClick={e => {\n                e.stopPropagation();\n                onMoreEventsClick?.(date);\n              }}\n            >\n              +{moreCount} {t('more')}\n            </span>\n          </div>\n        )}\n      </div>\n    );\n  }\n);\n\n(YearDayCell as { displayName?: string }).displayName = 'YearDayCell';\n"
  },
  {
    "path": "packages/core/src/components/yearView/YearRowComponent.tsx",
    "content": "import { RefObject } from 'preact';\nimport { memo } from 'preact/compat';\nimport { useCallback, useMemo } from 'preact/hooks';\n\nimport { CalendarEvent } from '@/components/calendarEvent';\nimport { Event, MonthEventDragState, ICalendarApp, ViewType } from '@/types';\nimport { getTodayInTimeZone } from '@/utils';\n\nimport { analyzeMultiDayEventsForRow, getEventDayRange } from './utils';\nimport { YearDayCell } from './YearDayCell';\n\ninterface YearRowComponentProps {\n  rowDays: Date[];\n  events: Event[];\n  columnsPerRow: number;\n  app: ICalendarApp;\n  calendarRef: RefObject<HTMLDivElement>;\n  locale: string;\n  isDragging: boolean;\n  dragState: MonthEventDragState;\n  onMoveStart?: (e: MouseEvent | TouchEvent, event: Event) => void;\n  onResizeStart?: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  onSelectDate: (date: Date) => void;\n  onCreateStart?: (e: MouseEvent | TouchEvent, targetDate: Date) => void;\n  selectedEventId: string | null;\n  onEventSelect: (eventId: string | null) => void;\n  onMoreEventsClick?: (date: Date) => void;\n  newlyCreatedEventId?: string | null;\n  onDetailPanelOpen?: () => void;\n  detailPanelEventId: string | null;\n  onDetailPanelToggle: (eventId: string | null) => void;\n  useEventDetailPanel?: boolean;\n  onContextMenu: (menu: { x: number; y: number; date: Date } | null) => void;\n  appTimeZone?: string;\n  dragPreviewEvent?: Event | null;\n  isMobile?: boolean;\n}\n\nconst eventOverlapsRow = (\n  event: Event | null | undefined,\n  rowDays: Date[],\n  appTimeZone?: string\n) => {\n  if (!event || rowDays.length === 0) return false;\n\n  const firstDay = rowDays[0];\n  const lastDay = rowDays.at(-1);\n  if (!firstDay || !lastDay) return false;\n\n  const rowStartMs = new Date(\n    firstDay.getFullYear(),\n    firstDay.getMonth(),\n    firstDay.getDate()\n  ).getTime();\n  const rowEndMs = new Date(\n    lastDay.getFullYear(),\n    lastDay.getMonth(),\n    lastDay.getDate(),\n    23,\n    59,\n    59,\n    999\n  ).getTime();\n\n  const range = getEventDayRange(event, appTimeZone);\n  return range.startMs <= rowEndMs && range.endMsEod >= rowStartMs;\n};\n\nconst createPreviewRowSegment = (\n  event: Event | null | undefined,\n  rowDays: Date[],\n  columnsPerRow: number,\n  appTimeZone?: string\n) => {\n  if (!event || rowDays.length === 0) return null;\n\n  const firstDay = rowDays[0];\n  const lastDay = rowDays.at(-1);\n  if (!firstDay || !lastDay) return null;\n\n  const rowStartMs = new Date(\n    firstDay.getFullYear(),\n    firstDay.getMonth(),\n    firstDay.getDate()\n  ).getTime();\n  const rowEndMs = new Date(\n    lastDay.getFullYear(),\n    lastDay.getMonth(),\n    lastDay.getDate(),\n    23,\n    59,\n    59,\n    999\n  ).getTime();\n\n  const range = getEventDayRange(event, appTimeZone);\n\n  if (range.startMs > rowEndMs || range.endMs < rowStartMs) {\n    return null;\n  }\n\n  let startCellIndex = Math.round(\n    (Math.max(range.startMs, rowStartMs) - rowStartMs) / (1000 * 60 * 60 * 24)\n  );\n  let endCellIndex = Math.round(\n    (Math.min(range.endMs, rowEndMs) - rowStartMs) / (1000 * 60 * 60 * 24)\n  );\n\n  startCellIndex = Math.max(0, Math.min(startCellIndex, columnsPerRow - 1));\n  endCellIndex = Math.max(0, Math.min(endCellIndex, columnsPerRow - 1));\n\n  return {\n    id: `${event.id}::preview-year-${rowStartMs}`,\n    event,\n    startCellIndex,\n    endCellIndex,\n    isFirstSegment: range.startMs >= rowStartMs,\n    isLastSegment: range.endMs <= rowEndMs,\n    visualRowIndex: 0,\n  };\n};\n\nexport const YearRowComponent = memo(\n  ({\n    rowDays,\n    events,\n    columnsPerRow,\n    app,\n    calendarRef,\n    locale,\n    isDragging,\n    dragState,\n    onMoveStart,\n    onResizeStart,\n    onSelectDate,\n    onCreateStart,\n    selectedEventId,\n    onEventSelect,\n    onMoreEventsClick,\n    newlyCreatedEventId,\n    onDetailPanelOpen,\n    detailPanelEventId,\n    onDetailPanelToggle,\n    useEventDetailPanel,\n    onContextMenu,\n    appTimeZone,\n    dragPreviewEvent,\n    isMobile,\n  }: YearRowComponentProps) => {\n    const MAX_VISIBLE_ROWS = 3;\n    const HEADER_HEIGHT = 26;\n    const today = useMemo(() => {\n      const now = getTodayInTimeZone(appTimeZone);\n      now.setHours(0, 0, 0, 0);\n      return now;\n    }, [appTimeZone]);\n\n    const handleContextMenu = useCallback(\n      (e: MouseEvent, date: Date) => {\n        e.preventDefault();\n        e.stopPropagation();\n        onContextMenu({ x: e.clientX, y: e.clientY, date });\n      },\n      [onContextMenu]\n    );\n\n    const handleEventUpdate = useCallback(\n      (updated: Event) => app.updateEvent(updated.id, updated),\n      [app]\n    );\n    const handleEventDelete = useCallback(\n      (id: string) => app.deleteEvent(id),\n      [app]\n    );\n\n    const segments = useMemo(\n      () =>\n        analyzeMultiDayEventsForRow(\n          events,\n          rowDays,\n          columnsPerRow,\n          app.state.allDaySortComparator,\n          appTimeZone\n        ),\n      [\n        events,\n        rowDays,\n        columnsPerRow,\n        app.state.allDaySortComparator,\n        appTimeZone,\n      ]\n    );\n\n    const isMovePreviewActive =\n      isDragging &&\n      dragState.mode === 'move' &&\n      !!dragPreviewEvent &&\n      dragPreviewEvent.id === dragState.eventId;\n\n    const effectiveSegments = useMemo(() => {\n      if (!dragPreviewEvent) return segments;\n      if (isMovePreviewActive) return segments;\n\n      const adjustedEvents = events.filter(\n        event => event.id !== dragPreviewEvent.id\n      );\n      if (eventOverlapsRow(dragPreviewEvent, rowDays, appTimeZone)) {\n        adjustedEvents.push(dragPreviewEvent);\n      }\n\n      return analyzeMultiDayEventsForRow(\n        adjustedEvents,\n        rowDays,\n        columnsPerRow,\n        app.state.allDaySortComparator,\n        appTimeZone\n      );\n    }, [\n      dragPreviewEvent,\n      isMovePreviewActive,\n      segments,\n      events,\n      rowDays,\n      columnsPerRow,\n      app.state.allDaySortComparator,\n      appTimeZone,\n    ]);\n\n    const dragPreviewSegment = useMemo(\n      () =>\n        isMovePreviewActive\n          ? createPreviewRowSegment(\n              dragPreviewEvent,\n              rowDays,\n              columnsPerRow,\n              appTimeZone\n            )\n          : null,\n      [\n        isMovePreviewActive,\n        dragPreviewEvent,\n        rowDays,\n        columnsPerRow,\n        appTimeZone,\n      ]\n    );\n\n    const { visibleSegments, moreCounts } = useMemo(() => {\n      // 1. Calculate how many events are in each column\n      const colCounts = Array.from({ length: rowDays.length }).fill(\n        0\n      ) as number[];\n      effectiveSegments.forEach(segment => {\n        // Be careful with boundaries\n        const start = Math.max(0, segment.startCellIndex);\n        const end = Math.min(rowDays.length - 1, segment.endCellIndex);\n\n        for (let i = start; i <= end; i++) {\n          colCounts[i]++;\n        }\n      });\n\n      const visible: typeof effectiveSegments = [];\n      const counts = Array.from({ length: rowDays.length }).fill(0) as number[];\n\n      // 2. Determine visibility for each segment\n      effectiveSegments.forEach(segment => {\n        let isVisible = true;\n        const start = Math.max(0, segment.startCellIndex);\n        const end = Math.min(rowDays.length - 1, segment.endCellIndex);\n\n        // Check each column this segment spans\n        for (let i = start; i <= end; i++) {\n          const count = colCounts[i];\n          // If column has more than MAX, must reserve the last slot (MAX-1) for \"More\"\n          // So valid indices are 0 to MAX-2.\n          // If column has <= MAX, can use 0 to MAX-1.\n          const maxAllowedIndex =\n            count > MAX_VISIBLE_ROWS\n              ? MAX_VISIBLE_ROWS - 2\n              : MAX_VISIBLE_ROWS - 1;\n\n          if (segment.visualRowIndex > maxAllowedIndex) {\n            isVisible = false;\n            break;\n          }\n        }\n\n        if (isVisible) {\n          visible.push(segment);\n        } else {\n          // Increment hidden count for covered days\n          for (let i = start; i <= end; i++) {\n            counts[i]++;\n          }\n        }\n      });\n\n      return { visibleSegments: visible, moreCounts: counts };\n    }, [effectiveSegments, rowDays.length]);\n\n    const renderedSegments = useMemo(() => {\n      if (!isMovePreviewActive || !dragState.eventId) {\n        return effectiveSegments;\n      }\n\n      const staticSegments = effectiveSegments.filter(\n        segment => segment.event.id !== dragState.eventId\n      );\n\n      return dragPreviewSegment\n        ? [...staticSegments, dragPreviewSegment]\n        : staticSegments;\n    }, [\n      isMovePreviewActive,\n      dragState.eventId,\n      effectiveSegments,\n      dragPreviewSegment,\n    ]);\n\n    return (\n      <div\n        className='df-year-row'\n        style={{\n          display: 'grid',\n          gridTemplateColumns: `repeat(${columnsPerRow}, 1fr)`,\n        }}\n        onContextMenu={e => e.preventDefault()}\n      >\n        {/* Background Cells */}\n        {rowDays.map((date, index) => {\n          const isToday = date.getTime() === today.getTime();\n          return (\n            <YearDayCell\n              key={date.getTime()}\n              date={date}\n              isToday={isToday}\n              locale={locale}\n              onSelectDate={onSelectDate}\n              onCreateStart={onCreateStart}\n              onMoreEventsClick={onMoreEventsClick}\n              moreCount={moreCounts[index]}\n              onContextMenu={handleContextMenu}\n            />\n          );\n        })}\n        <div\n          className='df-year-row-event-layer'\n          style={{\n            top: HEADER_HEIGHT,\n            bottom: 0,\n            left: 0,\n            right: 0,\n          }}\n          onContextMenu={e => e.preventDefault()}\n        >\n          <div className='df-year-row-event-layer-inner'>\n            {(isMovePreviewActive && dragState.eventId\n              ? renderedSegments.filter(\n                  segment =>\n                    visibleSegments.some(\n                      visibleSegment => visibleSegment.id === segment.id\n                    ) || segment.id === dragPreviewSegment?.id\n                )\n              : visibleSegments\n            ).map(segment => (\n              <div key={segment.id} className='df-year-row-event-hitbox'>\n                <CalendarEvent\n                  event={segment.event}\n                  isAllDay={!!segment.event.allDay}\n                  viewType={ViewType.YEAR}\n                  yearSegment={segment}\n                  columnsPerRow={columnsPerRow}\n                  isBeingDragged={\n                    isDragging && dragState.eventId === segment.event.id\n                  }\n                  selectedEventId={selectedEventId}\n                  onMoveStart={onMoveStart}\n                  onResizeStart={isMobile ? undefined : onResizeStart}\n                  onEventSelect={onEventSelect}\n                  onDetailPanelToggle={onDetailPanelToggle}\n                  newlyCreatedEventId={newlyCreatedEventId}\n                  onDetailPanelOpen={onDetailPanelOpen}\n                  calendarRef={calendarRef}\n                  app={app}\n                  detailPanelEventId={detailPanelEventId}\n                  useEventDetailPanel={useEventDetailPanel}\n                  // Required props for CalendarEvent\n                  firstHour={0}\n                  hourHeight={0}\n                  onEventUpdate={handleEventUpdate}\n                  onEventDelete={handleEventDelete}\n                  appTimeZone={appTimeZone}\n                  isMobile={isMobile}\n                  enableTouch={isMobile}\n                />\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n    );\n  },\n  (prevProps, nextProps) => {\n    const prevPreviewId = prevProps.dragPreviewEvent?.id;\n    const nextPreviewId = nextProps.dragPreviewEvent?.id;\n    const rowContainsDraggedEvent =\n      (!!prevPreviewId &&\n        prevProps.events.some(event => event.id === prevPreviewId)) ||\n      (!!nextPreviewId &&\n        nextProps.events.some(event => event.id === nextPreviewId));\n    const prevOverlaps = eventOverlapsRow(\n      prevProps.dragPreviewEvent,\n      prevProps.rowDays,\n      prevProps.appTimeZone\n    );\n    const nextOverlaps = eventOverlapsRow(\n      nextProps.dragPreviewEvent,\n      nextProps.rowDays,\n      nextProps.appTimeZone\n    );\n\n    if (rowContainsDraggedEvent || prevOverlaps || nextOverlaps) {\n      return false;\n    }\n\n    return (\n      prevProps.rowDays === nextProps.rowDays &&\n      prevProps.events === nextProps.events &&\n      prevProps.columnsPerRow === nextProps.columnsPerRow &&\n      prevProps.app === nextProps.app &&\n      prevProps.calendarRef === nextProps.calendarRef &&\n      prevProps.locale === nextProps.locale &&\n      prevProps.onMoveStart === nextProps.onMoveStart &&\n      prevProps.onResizeStart === nextProps.onResizeStart &&\n      prevProps.onSelectDate === nextProps.onSelectDate &&\n      prevProps.onCreateStart === nextProps.onCreateStart &&\n      prevProps.selectedEventId === nextProps.selectedEventId &&\n      prevProps.onEventSelect === nextProps.onEventSelect &&\n      prevProps.onMoreEventsClick === nextProps.onMoreEventsClick &&\n      prevProps.newlyCreatedEventId === nextProps.newlyCreatedEventId &&\n      prevProps.onDetailPanelOpen === nextProps.onDetailPanelOpen &&\n      prevProps.detailPanelEventId === nextProps.detailPanelEventId &&\n      prevProps.onDetailPanelToggle === nextProps.onDetailPanelToggle &&\n      prevProps.useEventDetailPanel === nextProps.useEventDetailPanel &&\n      prevProps.onContextMenu === nextProps.onContextMenu &&\n      prevProps.appTimeZone === nextProps.appTimeZone &&\n      prevProps.isMobile === nextProps.isMobile\n    );\n  }\n);\n\n(YearRowComponent as { displayName?: string }).displayName = 'YearRowComponent';\n"
  },
  {
    "path": "packages/core/src/components/yearView/__tests__/DefaultYearView.test.tsx",
    "content": "import { fireEvent, render, waitFor } from '@testing-library/preact';\n\nimport { DefaultYearView } from '@/components/yearView/DefaultYearView';\nimport { FixedWeekYearView } from '@/components/yearView/FixedWeekYearView';\nimport { CalendarApp } from '@/core/CalendarApp';\nimport { ViewType } from '@/types';\n\ndescribe('DefaultYearView', () => {\n  const originalClientWidth = Object.getOwnPropertyDescriptor(\n    HTMLElement.prototype,\n    'clientWidth'\n  );\n\n  afterEach(() => {\n    if (originalClientWidth) {\n      // eslint-disable-next-line no-extend-native\n      Object.defineProperty(\n        HTMLElement.prototype,\n        'clientWidth',\n        originalClientWidth\n      );\n    }\n  });\n\n  it('uses the measured container width on first render instead of an initial window-width guess', async () => {\n    // eslint-disable-next-line no-extend-native\n    Object.defineProperty(HTMLElement.prototype, 'clientWidth', {\n      configurable: true,\n      get() {\n        return 1200;\n      },\n    });\n\n    const app = new CalendarApp({\n      views: [],\n      plugins: [],\n      events: [],\n      defaultView: ViewType.YEAR,\n      calendars: [\n        {\n          id: 'work',\n          name: 'Work',\n          colors: {\n            lineColor: '#2563eb',\n            eventColor: '#dbeafe',\n            eventSelectedColor: '#bfdbfe',\n            textColor: '#1e3a8a',\n          },\n        },\n      ],\n    });\n\n    const calendarRef = {\n      current: document.createElement('div'),\n    } as { current: HTMLDivElement };\n\n    const { container } = render(\n      <DefaultYearView app={app} calendarRef={calendarRef} />\n    );\n\n    await waitFor(() => {\n      const rows = container.querySelectorAll('.df-year-row');\n      expect(rows.length).toBeGreaterThan(0);\n      expect(rows[0]?.querySelectorAll('[data-date]').length).toBe(15);\n    });\n  });\n\n  it('uses gridDateDoubleClick callback in year-canvas mode when provided', async () => {\n    const onGridDateDoubleClick = jest.fn();\n\n    const app = new CalendarApp({\n      views: [],\n      plugins: [],\n      events: [],\n      defaultView: ViewType.YEAR,\n      initialDate: new Date(2026, 0, 1),\n      calendars: [\n        {\n          id: 'work',\n          name: 'Work',\n          colors: {\n            lineColor: '#2563eb',\n            eventColor: '#dbeafe',\n            eventSelectedColor: '#bfdbfe',\n            textColor: '#1e3a8a',\n          },\n        },\n      ],\n    });\n\n    const calendarRef = {\n      current: document.createElement('div'),\n    } as { current: HTMLDivElement };\n\n    const { container } = render(\n      <DefaultYearView\n        app={app}\n        calendarRef={calendarRef}\n        config={{ gridDateDoubleClick: onGridDateDoubleClick }}\n      />\n    );\n\n    await waitFor(() => {\n      expect(\n        container.querySelector('[data-date=\"2026-01-01\"]')\n      ).not.toBeNull();\n    });\n\n    fireEvent.dblClick(container.querySelector('[data-date=\"2026-01-01\"]')!);\n\n    expect(onGridDateDoubleClick).toHaveBeenCalledTimes(1);\n    expect(onGridDateDoubleClick.mock.calls[0][0]).toEqual(\n      new Date(2026, 0, 1)\n    );\n    expect(onGridDateDoubleClick.mock.calls[0][1]).toEqual([]);\n  });\n\n  it('uses gridDateDoubleClick callback in fixed-week mode when provided', async () => {\n    const onGridDateDoubleClick = jest.fn();\n\n    const app = new CalendarApp({\n      views: [],\n      plugins: [],\n      events: [],\n      defaultView: ViewType.YEAR,\n      initialDate: new Date(2026, 0, 1),\n      calendars: [\n        {\n          id: 'work',\n          name: 'Work',\n          colors: {\n            lineColor: '#2563eb',\n            eventColor: '#dbeafe',\n            eventSelectedColor: '#bfdbfe',\n            textColor: '#1e3a8a',\n          },\n        },\n      ],\n    });\n\n    const calendarRef = {\n      current: document.createElement('div'),\n    } as { current: HTMLDivElement };\n\n    const { container } = render(\n      <FixedWeekYearView\n        app={app}\n        calendarRef={calendarRef}\n        config={{\n          mode: 'fixed-week',\n          gridDateDoubleClick: onGridDateDoubleClick,\n        }}\n      />\n    );\n\n    await waitFor(() => {\n      expect(\n        container.querySelector('[data-date=\"2026-01-01\"]')\n      ).not.toBeNull();\n    });\n\n    fireEvent.dblClick(container.querySelector('[data-date=\"2026-01-01\"]')!);\n\n    expect(onGridDateDoubleClick).toHaveBeenCalledTimes(1);\n    expect(onGridDateDoubleClick.mock.calls[0][0]).toEqual(\n      new Date(2026, 0, 1)\n    );\n    expect(onGridDateDoubleClick.mock.calls[0][1]).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/yearView/__tests__/utils.test.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { analyzeMultiDayEventsForRow } from '@/components/yearView/utils';\nimport { Event } from '@/types';\n\nconst createDate = (year: number, month: number, day: number) =>\n  new Date(year, month - 1, day);\n\ndescribe('YearView utils - analyzeMultiDayEventsForRow', () => {\n  const createAllDayEvent = (\n    id: string,\n    start: string,\n    end: string\n  ): Event => ({\n    id,\n    title: id,\n    allDay: true,\n    start: Temporal.PlainDate.from(start),\n    end: Temporal.PlainDate.from(end),\n  });\n\n  const createTimedEvent = (id: string, start: string, end: string): Event => ({\n    id,\n    title: id,\n    allDay: false,\n    start: Temporal.PlainDateTime.from(start),\n    end: Temporal.PlainDateTime.from(end),\n  });\n\n  it('should sort and pack multi-day and timed events correctly (MonthView consistency)', () => {\n    // March 18-24, 2026 (7 days row)\n    const rowDays = [\n      createDate(2026, 3, 18),\n      createDate(2026, 3, 19),\n      createDate(2026, 3, 20),\n      createDate(2026, 3, 21),\n      createDate(2026, 3, 22),\n      createDate(2026, 3, 23),\n      createDate(2026, 3, 24),\n    ];\n\n    const events: Event[] = [\n      createAllDayEvent('Event A', '2026-03-18', '2026-03-22'),\n      createAllDayEvent('Event B', '2026-03-19', '2026-03-23'),\n      createTimedEvent('Event C', '2026-03-21T10:00:00', '2026-03-21T11:00:00'),\n      createAllDayEvent('Event D', '2026-03-23', '2026-03-23'),\n      createTimedEvent('Event E', '2026-03-23T14:00:00', '2026-03-23T15:00:00'),\n    ];\n\n    const segments = analyzeMultiDayEventsForRow(events, rowDays, 7);\n\n    const getSegment = (id: string) => segments.find(s => s.event.id === id);\n\n    // Expected Packing Logic:\n    // Row 0: Event A (18-22). 23rd is free. Event D (23) can fit in Row 0.\n    // Row 1: Event B (19-23). Overlaps A (19-22) and D (23).\n    // Row 2: Event C (21). Overlaps A (Row 0) and B (Row 1).\n    // Row 2: Event E (23). Overlaps D (Row 0) and B (Row 1).\n\n    const segA = getSegment('Event A');\n    const segB = getSegment('Event B');\n    const segC = getSegment('Event C');\n    const segD = getSegment('Event D');\n    const segE = getSegment('Event E');\n\n    expect(segA?.visualRowIndex).toBe(0);\n    expect(segD?.visualRowIndex).toBe(0);\n\n    expect(segB?.visualRowIndex).toBe(1);\n\n    expect(segC?.visualRowIndex).toBe(2);\n    expect(segE?.visualRowIndex).toBe(2);\n  });\n\n  it('should prioritize all-day events over timed events regardless of input order', () => {\n    const rowDays = [createDate(2026, 3, 18)];\n    const events: Event[] = [\n      createTimedEvent('Timed', '2026-03-18T10:00:00', '2026-03-18T11:00:00'),\n      createAllDayEvent('AllDay', '2026-03-18', '2026-03-18'),\n    ];\n\n    const segments = analyzeMultiDayEventsForRow(events, rowDays, 1);\n\n    const timedSeg = segments.find(s => s.event.id === 'Timed');\n    const allDaySeg = segments.find(s => s.event.id === 'AllDay');\n\n    // All-day should be in row 0, timed in row 1\n    expect(allDaySeg?.visualRowIndex).toBe(0);\n    expect(timedSeg?.visualRowIndex).toBe(1);\n  });\n\n  it('should not throw TypeError', () => {\n    const rowDays = [createDate(2026, 3, 18)];\n    const events: Event[] = [\n      createAllDayEvent('Test', '2026-03-18', '2026-03-18'),\n    ];\n\n    expect(() => {\n      analyzeMultiDayEventsForRow(events, rowDays, 1);\n    }).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/core/src/components/yearView/utils.ts",
    "content": "import { Event } from '@/types';\nimport {\n  dateToPlainDate,\n  dateToZonedDateTime,\n  temporalToVisualDate,\n} from '@/utils';\nimport { createAllDayDisplayComparator } from '@/utils/allDaySort';\n\nexport type EventDayRange = {\n  startMs: number; // 00:00 of the start day\n  endMs: number; // 00:00 of the end day\n  endMsEod: number; // 23:59:59.999 of the end day\n  startYear: number;\n  startMonth: number;\n  startDate: number;\n  endYear: number;\n  endMonth: number;\n  endDate: number;\n};\n\nconst eventDayRangeCache = new WeakMap<Event, Map<string, EventDayRange>>();\n\nexport const getEventDayRange = (\n  event: Event,\n  appTimeZone?: string\n): EventDayRange => {\n  const tzKey = appTimeZone ?? '';\n  let perEvent = eventDayRangeCache.get(event);\n  if (perEvent) {\n    const cached = perEvent.get(tzKey);\n    if (cached) return cached;\n  }\n  const start = temporalToVisualDate(event.start, appTimeZone);\n  const end = event.end ? temporalToVisualDate(event.end, appTimeZone) : start;\n  const startYear = start.getFullYear();\n  const startMonth = start.getMonth();\n  const startDate = start.getDate();\n  const endYear = end.getFullYear();\n  const endMonth = end.getMonth();\n  const endDate = end.getDate();\n  const startMs = new Date(startYear, startMonth, startDate).getTime();\n  const endMs = new Date(endYear, endMonth, endDate).getTime();\n  const endMsEod = new Date(\n    endYear,\n    endMonth,\n    endDate,\n    23,\n    59,\n    59,\n    999\n  ).getTime();\n  const result: EventDayRange = {\n    startMs,\n    endMs,\n    endMsEod,\n    startYear,\n    startMonth,\n    startDate,\n    endYear,\n    endMonth,\n    endDate,\n  };\n  if (!perEvent) {\n    perEvent = new Map();\n    eventDayRangeCache.set(event, perEvent);\n  }\n  perEvent.set(tzKey, result);\n  return result;\n};\n\nexport const getEventsForYearDate = (\n  events: Event[],\n  date: Date,\n  appTimeZone?: string\n): Event[] => {\n  const targetStartMs = new Date(\n    date.getFullYear(),\n    date.getMonth(),\n    date.getDate()\n  ).getTime();\n  const targetEndMs = new Date(\n    date.getFullYear(),\n    date.getMonth(),\n    date.getDate(),\n    23,\n    59,\n    59,\n    999\n  ).getTime();\n\n  return events.filter(event => {\n    if (!event.start) return false;\n    const range = getEventDayRange(event, appTimeZone);\n    return range.startMs <= targetEndMs && range.endMsEod >= targetStartMs;\n  });\n};\n\nexport interface YearMultiDaySegment {\n  id: string;\n  event: Event;\n  startCellIndex: number; // 0 to columnsPerRow-1\n  endCellIndex: number; // 0 to columnsPerRow-1\n  isFirstSegment: boolean;\n  isLastSegment: boolean;\n  visualRowIndex: number; // Vertical slot index within the row to avoid overlap\n}\n\n/**\n * Groups an array of days into rows based on the number of columns per row.\n */\nexport function groupDaysIntoRows(\n  yearDays: Date[],\n  columnsPerRow: number\n): Date[][] {\n  const rows: Date[][] = [];\n  for (let i = 0; i < yearDays.length; i += columnsPerRow) {\n    rows.push(yearDays.slice(i, i + columnsPerRow));\n  }\n  return rows;\n}\n\n/**\n * Analyzes events for a specific row of days and returns segments for multi-day events.\n * It also calculates the vertical layout (visualRowIndex) to prevent overlaps.\n */\nexport function analyzeMultiDayEventsForRow(\n  events: Event[],\n  rowDays: Date[],\n  columnsPerRow: number,\n  comparator?: (a: Event, b: Event) => number,\n  appTimeZone?: string\n): YearMultiDaySegment[] {\n  if (rowDays.length === 0) return [];\n\n  const firstDay = rowDays[0];\n  const lastDay = rowDays.at(-1);\n  if (!firstDay || !lastDay) return [];\n\n  const rowStartMs = new Date(\n    firstDay.getFullYear(),\n    firstDay.getMonth(),\n    firstDay.getDate()\n  ).getTime();\n\n  const rowEndMs = new Date(\n    lastDay.getFullYear(),\n    lastDay.getMonth(),\n    lastDay.getDate(),\n    23,\n    59,\n    59,\n    999\n  ).getTime();\n\n  // 1. Filter and normalize events that overlap with this row (cached per event)\n  const eventsWithDates: Array<{\n    event: Event;\n    startMs: number;\n    endMs: number;\n  }> = [];\n  for (let i = 0; i < events.length; i++) {\n    const event = events[i];\n    const range = getEventDayRange(event, appTimeZone);\n    if (range.startMs <= rowEndMs && range.endMs >= rowStartMs) {\n      eventsWithDates.push({\n        event,\n        startMs: range.startMs,\n        endMs: range.endMs,\n      });\n    }\n  }\n\n  if (eventsWithDates.length === 0) return [];\n\n  // 2. Sort events based on the all-day display priority\n  // This matches MonthView and WeekView logic.\n  const allDayComparator = createAllDayDisplayComparator(\n    eventsWithDates.map(d => d.event),\n    comparator\n  );\n  eventsWithDates.sort((a, b) => {\n    // Priority 1: All-day events always before timed events\n    const aAllDay = !!a.event.allDay;\n    const bAllDay = !!b.event.allDay;\n    if (aAllDay !== bAllDay) {\n      return aAllDay ? -1 : 1;\n    }\n    // Priority 2: Standard all-day sort logic (multi-day first, then calendar, then comparator)\n    return allDayComparator(a.event, b.event);\n  });\n\n  const segments: YearMultiDaySegment[] = [];\n  const occupiedSlots: boolean[][] = []; // [visualRowIndex][colIndex]\n\n  eventsWithDates.forEach(({ event, startMs, endMs }) => {\n    // Calculate start and end indices in the current row\n    let startCellIndex = Math.round(\n      (startMs - rowStartMs) / (1000 * 60 * 60 * 24)\n    );\n    let endCellIndex = Math.round((endMs - rowStartMs) / (1000 * 60 * 60 * 24));\n\n    // Clamp indices to row boundaries\n    startCellIndex = Math.max(0, Math.min(startCellIndex, columnsPerRow - 1));\n    endCellIndex = Math.max(0, Math.min(endCellIndex, columnsPerRow - 1));\n\n    // Determine if it's the very first/last segment of the entire event\n    const isFirstSegment = startMs >= rowStartMs;\n    const isLastSegment = endMs <= rowEndMs;\n\n    // Determine visualRowIndex (simple packing algorithm)\n    let visualRowIndex = 0;\n    while (true) {\n      if (!occupiedSlots[visualRowIndex]) {\n        occupiedSlots[visualRowIndex] = [];\n      }\n\n      let overlap = false;\n      for (let i = startCellIndex; i <= endCellIndex; i++) {\n        if (occupiedSlots[visualRowIndex][i]) {\n          overlap = true;\n          break;\n        }\n      }\n\n      if (!overlap) {\n        // Found a slot, mark it occupied\n        for (let i = startCellIndex; i <= endCellIndex; i++) {\n          occupiedSlots[visualRowIndex][i] = true;\n        }\n        break;\n      }\n      visualRowIndex++;\n    }\n\n    segments.push({\n      id: `${event.id}::year-${rowStartMs}`,\n      event,\n      startCellIndex,\n      endCellIndex,\n      isFirstSegment,\n      isLastSegment,\n      visualRowIndex,\n    });\n  });\n\n  return segments;\n}\n\nexport interface MonthEventSegment extends YearMultiDaySegment {\n  monthIndex: number;\n}\n\nexport interface FixedWeekMonthData {\n  monthIndex: number;\n  monthName: string;\n  days: (Date | null)[];\n  monthEvents: Event[];\n  eventSegments: MonthEventSegment[];\n  minHeight: number;\n}\n\nexport const EVENT_ROW_SPACING = 18;\nexport const DATE_HEADER_HEIGHT = 20;\nexport const MIN_ROW_HEIGHT = 60;\n\nexport const eventOverlapsMonth = (\n  event: Event | null | undefined,\n  year: number,\n  monthIndex: number,\n  appTimeZone?: string\n) => {\n  if (!event) return false;\n\n  const monthStartMs = new Date(year, monthIndex, 1).getTime();\n  const monthEndMs = new Date(\n    year,\n    monthIndex + 1,\n    0,\n    23,\n    59,\n    59,\n    999\n  ).getTime();\n  const range = getEventDayRange(event, appTimeZone);\n  return range.startMs <= monthEndMs && range.endMsEod >= monthStartMs;\n};\n\nexport const getFixedWeekTotalColumns = (\n  currentYear: number,\n  startOfWeek: number\n) => {\n  let maxSlots = 0;\n  for (let month = 0; month < 12; month++) {\n    const monthStart = new Date(currentYear, month, 1);\n    const daysInMonth = new Date(currentYear, month + 1, 0).getDate();\n    const monthStartDay = monthStart.getDay();\n    const padding = (monthStartDay - startOfWeek + 7) % 7;\n    const slots = padding + daysInMonth;\n    if (slots > maxSlots) {\n      maxSlots = slots;\n    }\n  }\n  return maxSlots;\n};\n\nexport const getFixedWeekLabels = ({\n  locale,\n  totalColumns,\n  startOfWeek,\n  getWeekDaysLabels,\n}: {\n  locale: string;\n  totalColumns: number;\n  startOfWeek: number;\n  getWeekDaysLabels: (\n    locale: string,\n    format?: 'long' | 'short' | 'narrow',\n    startOfWeek?: number\n  ) => string[];\n}) => {\n  const labels = getWeekDaysLabels(locale, 'short', startOfWeek);\n\n  const formattedLabels = labels.map(label => {\n    if (locale.startsWith('zh')) {\n      return label.at(-1) ?? label;\n    }\n    const twoChars = label.slice(0, 2);\n    return twoChars.charAt(0).toUpperCase() + twoChars.slice(1).toLowerCase();\n  });\n\n  const result = [];\n  for (let i = 0; i < totalColumns; i++) {\n    result.push(formattedLabels[i % 7]);\n  }\n  return result;\n};\n\nexport const createFixedWeekDragPreviewEvent = ({\n  isDragging,\n  dragState,\n  yearEvents,\n  appTimeZone,\n}: {\n  isDragging: boolean;\n  dragState: {\n    eventId: string | null;\n    startDate: Date | null;\n    endDate: Date | null;\n    mode: 'create' | 'move' | 'resize' | null;\n  };\n  yearEvents: Event[];\n  appTimeZone?: string;\n}) => {\n  if (\n    !isDragging ||\n    !dragState.eventId ||\n    !dragState.startDate ||\n    !dragState.endDate ||\n    (dragState.mode !== 'move' && dragState.mode !== 'resize')\n  ) {\n    return null;\n  }\n\n  const baseEvent = yearEvents.find(event => event.id === dragState.eventId);\n  if (!baseEvent) return null;\n\n  return {\n    ...baseEvent,\n    start: baseEvent.allDay\n      ? dateToPlainDate(dragState.startDate)\n      : dateToZonedDateTime(dragState.startDate, appTimeZone),\n    end: baseEvent.allDay\n      ? dateToPlainDate(dragState.endDate)\n      : dateToZonedDateTime(dragState.endDate, appTimeZone),\n  } as Event;\n};\n\nexport const createPreviewMonthSegment = (\n  event: Event | null | undefined,\n  monthIndex: number,\n  year: number,\n  startOfWeek: number = 1,\n  appTimeZone?: string\n): MonthEventSegment | null => {\n  if (!event) return null;\n\n  const monthStart = new Date(year, monthIndex, 1);\n  const daysInMonth = new Date(year, monthIndex + 1, 0).getDate();\n  const monthStartDay = monthStart.getDay();\n  const paddingStart = (monthStartDay - startOfWeek + 7) % 7;\n  const monthStartMs = monthStart.getTime();\n  const monthEndMs = new Date(\n    year,\n    monthIndex,\n    daysInMonth,\n    23,\n    59,\n    59,\n    999\n  ).getTime();\n\n  const range = getEventDayRange(event, appTimeZone);\n\n  if (range.startMs > monthEndMs || range.endMs < monthStartMs) {\n    return null;\n  }\n\n  const clampedStartMs = Math.max(range.startMs, monthStartMs);\n  const clampedEndMs = Math.min(range.endMs, monthEndMs);\n\n  return {\n    id: `${event.id}::preview-month-${monthIndex}`,\n    event,\n    startCellIndex: paddingStart + (new Date(clampedStartMs).getDate() - 1),\n    endCellIndex: paddingStart + (new Date(clampedEndMs).getDate() - 1),\n    isFirstSegment: range.startMonth === monthIndex && range.startYear === year,\n    isLastSegment: range.endMonth === monthIndex && range.endYear === year,\n    visualRowIndex: 0,\n    monthIndex,\n  };\n};\n\nexport function analyzeEventsForMonth(\n  events: Event[],\n  monthIndex: number,\n  year: number,\n  startOfWeek: number = 1,\n  appTimeZone?: string\n): { segments: MonthEventSegment[]; maxVisualRow: number } {\n  const monthStart = new Date(year, monthIndex, 1);\n  const daysInMonth = new Date(year, monthIndex + 1, 0).getDate();\n  const monthStartDay = monthStart.getDay();\n  const paddingStart = (monthStartDay - startOfWeek + 7) % 7;\n\n  const monthStartMs = monthStart.getTime();\n  const monthEndMs = new Date(\n    year,\n    monthIndex,\n    daysInMonth,\n    23,\n    59,\n    59,\n    999\n  ).getTime();\n\n  const monthEventsWithDates: Array<{\n    event: Event;\n    startMs: number;\n    endMs: number;\n    range: EventDayRange;\n  }> = [];\n  for (let i = 0; i < events.length; i++) {\n    const event = events[i];\n    if (!event.start) continue;\n    const range = getEventDayRange(event, appTimeZone);\n    if (range.startMs <= monthEndMs && range.endMs >= monthStartMs) {\n      monthEventsWithDates.push({\n        event,\n        startMs: range.startMs,\n        endMs: range.endMs,\n        range,\n      });\n    }\n  }\n\n  if (monthEventsWithDates.length === 0) {\n    return { segments: [], maxVisualRow: -1 };\n  }\n\n  const allDayComparator = createAllDayDisplayComparator(\n    monthEventsWithDates.map(i => i.event)\n  );\n  monthEventsWithDates.sort((a, b) => {\n    const aAllDay = !!a.event.allDay;\n    const bAllDay = !!b.event.allDay;\n    if (aAllDay !== bAllDay) {\n      return aAllDay ? -1 : 1;\n    }\n    return allDayComparator(a.event, b.event);\n  });\n\n  const segments: MonthEventSegment[] = [];\n  const occupiedSlots: boolean[][] = [];\n\n  monthEventsWithDates.forEach(({ event, startMs, endMs, range }) => {\n    const clampedStartMs = Math.max(startMs, monthStartMs);\n    const clampedEndMs = Math.min(endMs, monthEndMs);\n\n    const startDay = new Date(clampedStartMs).getDate();\n    const endDay = new Date(clampedEndMs).getDate();\n\n    const startCellIndex = paddingStart + (startDay - 1);\n    const endCellIndex = paddingStart + (endDay - 1);\n\n    const isFirstSegment =\n      range.startMonth === monthIndex && range.startYear === year;\n    const isLastSegment =\n      range.endMonth === monthIndex && range.endYear === year;\n\n    let visualRowIndex = 0;\n    while (true) {\n      if (!occupiedSlots[visualRowIndex]) {\n        occupiedSlots[visualRowIndex] = [];\n      }\n\n      let overlap = false;\n      for (let i = startCellIndex; i <= endCellIndex; i++) {\n        if (occupiedSlots[visualRowIndex][i]) {\n          overlap = true;\n          break;\n        }\n      }\n\n      if (!overlap) {\n        for (let i = startCellIndex; i <= endCellIndex; i++) {\n          occupiedSlots[visualRowIndex][i] = true;\n        }\n        break;\n      }\n      visualRowIndex++;\n    }\n\n    segments.push({\n      id: `${event.id}::month-${monthIndex}`,\n      event,\n      startCellIndex,\n      endCellIndex,\n      isFirstSegment,\n      isLastSegment,\n      visualRowIndex,\n      monthIndex,\n    });\n  });\n\n  const maxVisualRow =\n    segments.length > 0 ? Math.max(...segments.map(s => s.visualRowIndex)) : -1;\n\n  return { segments, maxVisualRow };\n}\n\n// Per-month segment cache. Keyed by (year|startOfWeek|totalColumns|tz).\n// For each month, we store the source event refs that fed analyzeEventsForMonth.\n// If the refs are identical (element-wise), the cached segment objects are\n// reused, so CalendarEvent's memo() can bail out — the yearSegment prop keeps\n// the same reference across drag ticks for all unaffected months.\ntype MonthSegmentCacheEntry = {\n  contextKey: string;\n  perMonth: Map<\n    number,\n    {\n      sources: Event[];\n      result: { segments: MonthEventSegment[]; maxVisualRow: number };\n    }\n  >;\n};\nlet monthSegmentCache: MonthSegmentCacheEntry | null = null;\n\nconst sameEventRefs = (a: Event[], b: Event[]): boolean => {\n  if (a.length !== b.length) return false;\n  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n  return true;\n};\n\nexport const buildFixedWeekMonthsData = ({\n  currentYear,\n  locale,\n  totalColumns,\n  yearEvents,\n  startOfWeek,\n  appTimeZone,\n}: {\n  currentYear: number;\n  locale: string;\n  totalColumns: number;\n  yearEvents: Event[];\n  startOfWeek: number;\n  appTimeZone?: string;\n}): FixedWeekMonthData[] => {\n  const contextKey = `${currentYear}|${startOfWeek}|${totalColumns}|${appTimeZone ?? ''}`;\n  if (!monthSegmentCache || monthSegmentCache.contextKey !== contextKey) {\n    monthSegmentCache = { contextKey, perMonth: new Map() };\n  }\n  const segCache = monthSegmentCache.perMonth;\n\n  // Pre-compute month boundaries once instead of 12 times inside loops\n  const monthStartMs: number[] = Array.from({ length: 12 });\n  const monthEndMs: number[] = Array.from({ length: 12 });\n  for (let m = 0; m < 12; m++) {\n    monthStartMs[m] = new Date(currentYear, m, 1).getTime();\n    monthEndMs[m] = new Date(currentYear, m + 1, 0, 23, 59, 59, 999).getTime();\n  }\n\n  // Single O(N) pass: bucket events into the months they overlap\n  const monthEventsByIndex: Event[][] = Array.from({ length: 12 }, () => []);\n  for (let i = 0; i < yearEvents.length; i++) {\n    const event = yearEvents[i];\n    if (!event.start) continue;\n    const range = getEventDayRange(event, appTimeZone);\n    // Find first month where event ends after month start, then iterate forward\n    // until event start exceeds month end. Most events span 1-2 months.\n    for (let m = 0; m < 12; m++) {\n      if (range.startMs > monthEndMs[m]) continue;\n      if (range.endMsEod < monthStartMs[m]) break;\n      monthEventsByIndex[m].push(event);\n    }\n  }\n\n  const data: FixedWeekMonthData[] = [];\n\n  for (let month = 0; month < 12; month++) {\n    const monthStart = new Date(currentYear, month, 1);\n    const daysInMonth = new Date(currentYear, month + 1, 0).getDate();\n    const monthStartDay = monthStart.getDay();\n    const paddingStart = (monthStartDay - startOfWeek + 7) % 7;\n\n    const days: (Date | null)[] = [];\n\n    for (let i = 0; i < paddingStart; i++) {\n      days.push(null);\n    }\n\n    for (let i = 1; i <= daysInMonth; i++) {\n      days.push(new Date(currentYear, month, i));\n    }\n\n    while (days.length < totalColumns) {\n      days.push(null);\n    }\n\n    const rawMonthName = monthStart.toLocaleDateString(locale, {\n      month: 'short',\n    });\n    const monthName =\n      rawMonthName.charAt(0).toUpperCase() +\n      rawMonthName.slice(1).toLowerCase();\n\n    const monthEvents = monthEventsByIndex[month];\n\n    // Cache lookup: if this month's event refs are unchanged, reuse cached\n    // segment objects so downstream CalendarEvent memo() can bail out.\n    let analysisResult: { segments: MonthEventSegment[]; maxVisualRow: number };\n    const cachedMonth = segCache.get(month);\n    if (cachedMonth && sameEventRefs(cachedMonth.sources, monthEvents)) {\n      analysisResult = cachedMonth.result;\n    } else {\n      analysisResult = analyzeEventsForMonth(\n        monthEvents,\n        month,\n        currentYear,\n        startOfWeek,\n        appTimeZone\n      );\n      segCache.set(month, { sources: monthEvents, result: analysisResult });\n    }\n    const { segments: eventSegments, maxVisualRow } = analysisResult;\n\n    const eventRows = maxVisualRow + 1;\n    const minHeight = Math.max(\n      MIN_ROW_HEIGHT,\n      DATE_HEADER_HEIGHT + eventRows * EVENT_ROW_SPACING\n    );\n\n    data.push({\n      monthIndex: month,\n      monthName,\n      days,\n      monthEvents,\n      eventSegments,\n      minHeight,\n    });\n  }\n\n  return data;\n};\n\nexport const buildEffectiveFixedWeekMonthsData = ({\n  monthsData,\n  dragPreviewEvent,\n  isMovePreviewActive,\n  currentYear,\n  startOfWeek,\n  appTimeZone,\n}: {\n  monthsData: FixedWeekMonthData[];\n  dragPreviewEvent: Event | null;\n  isMovePreviewActive: boolean;\n  currentYear: number;\n  startOfWeek: number;\n  appTimeZone?: string;\n}) =>\n  monthsData.map(month => {\n    if (isMovePreviewActive) {\n      return month;\n    }\n\n    const monthIsAffectedByPreview =\n      !!dragPreviewEvent &&\n      (month.monthEvents.some(event => event.id === dragPreviewEvent.id) ||\n        eventOverlapsMonth(\n          dragPreviewEvent,\n          currentYear,\n          month.monthIndex,\n          appTimeZone\n        ));\n\n    if (!monthIsAffectedByPreview) {\n      return month;\n    }\n\n    const adjustedEvents = month.monthEvents.filter(\n      event => event.id !== dragPreviewEvent?.id\n    );\n\n    if (\n      dragPreviewEvent &&\n      eventOverlapsMonth(\n        dragPreviewEvent,\n        currentYear,\n        month.monthIndex,\n        appTimeZone\n      )\n    ) {\n      adjustedEvents.push(dragPreviewEvent);\n    }\n\n    const { segments: eventSegments, maxVisualRow } = analyzeEventsForMonth(\n      adjustedEvents,\n      month.monthIndex,\n      currentYear,\n      startOfWeek,\n      appTimeZone\n    );\n\n    return {\n      ...month,\n      eventSegments,\n      minHeight: Math.max(\n        MIN_ROW_HEIGHT,\n        DATE_HEADER_HEIGHT + (maxVisualRow + 1) * EVENT_ROW_SPACING\n      ),\n    };\n  });\n"
  },
  {
    "path": "packages/core/src/contexts/ThemeContext.tsx",
    "content": "import { h, createContext, ComponentChildren } from 'preact';\nimport {\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useState,\n  useCallback,\n  useMemo,\n} from 'preact/hooks';\n\nimport { ThemeMode } from '@/types/calendarTypes';\nimport { resolveAppliedTheme } from '@/utils/themeUtils';\n\n/**\n * Theme Context Type\n */\nexport interface ThemeContextType {\n  /** Current theme mode (can be 'auto') */\n  theme: ThemeMode;\n  /** Effective theme (resolved, never 'auto') */\n  effectiveTheme: 'light' | 'dark';\n  /** Set theme mode */\n  setTheme: (mode: ThemeMode) => void;\n}\n\n/**\n * Theme Context\n */\nconst ThemeContext = createContext<ThemeContextType | undefined>(undefined);\n\n/**\n * Theme Provider Props\n */\nexport interface ThemeProviderProps {\n  children: ComponentChildren;\n  /** Initial theme mode */\n  initialTheme?: ThemeMode;\n  /** Callback when theme changes */\n  onThemeChange?: (theme: ThemeMode, effectiveTheme: 'light' | 'dark') => void;\n}\n\n/**\n * Theme Provider Component\n *\n * Manages theme state and applies it to the document root.\n * Supports 'light', 'dark', and 'auto' modes.\n *\n * @example\n * ```tsx\n * <ThemeProvider initialTheme=\"auto\">\n *   <App />\n * </ThemeProvider>\n * ```\n */\nexport const ThemeProvider = ({\n  children,\n  initialTheme = 'light',\n  onThemeChange,\n}: ThemeProviderProps) => {\n  const [theme, setThemeState] = useState<ThemeMode>(initialTheme as ThemeMode);\n  const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>(() => {\n    if (typeof window === 'undefined' || !window.matchMedia) {\n      return 'light';\n    }\n    return window.matchMedia('(prefers-color-scheme: dark)').matches\n      ? 'dark'\n      : 'light';\n  });\n\n  // Compute effective theme (resolve 'auto' to actual theme)\n  const effectiveTheme: 'light' | 'dark' =\n    theme === 'auto' ? systemTheme : theme;\n\n  /**\n   * Sync initialTheme prop changes to internal state\n   */\n  useEffect(() => {\n    setThemeState(initialTheme as ThemeMode);\n  }, [initialTheme]);\n\n  /**\n   * Set theme mode\n   */\n  const setTheme = useCallback((mode: ThemeMode) => {\n    setThemeState(mode);\n  }, []);\n\n  /**\n   * Listen to system theme changes (for 'auto' mode)\n   */\n  useEffect(() => {\n    if (typeof window === 'undefined' || !window.matchMedia) {\n      return;\n    }\n\n    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n\n    const handleChange = (e: MediaQueryListEvent | MediaQueryList) => {\n      const newSystemTheme = e.matches ? 'dark' : 'light';\n      setSystemTheme(newSystemTheme);\n    };\n\n    // Listen for changes\n    // Use addEventListener if available (modern browsers), fallback to addListener\n    if (mediaQuery.addEventListener) {\n      mediaQuery.addEventListener('change', handleChange);\n    } else if (mediaQuery.addListener) {\n      mediaQuery.addListener(handleChange);\n    }\n\n    return () => {\n      if (mediaQuery.removeEventListener) {\n        mediaQuery.removeEventListener('change', handleChange);\n      } else if (mediaQuery.removeListener) {\n        mediaQuery.removeListener(handleChange);\n      }\n    };\n  }, []);\n\n  /**\n   * Apply theme to document root\n   * useLayoutEffect ensures the class is applied synchronously before the\n   * browser paints, preventing a flash of the wrong theme when the OS is in\n   * dark mode but the user has explicitly set mode: 'light'.\n   */\n  useLayoutEffect(() => {\n    if (typeof document === 'undefined') {\n      return;\n    }\n\n    const root = document.documentElement;\n\n    // When in auto mode, respect any existing host overrides (like global dark mode toggles)\n    const appliedTheme = resolveAppliedTheme(effectiveTheme);\n    const targetTheme = theme === 'auto' ? appliedTheme : effectiveTheme;\n\n    // Always normalize the root theme classes so the document never ends up\n    // with both `light` and `dark` attached at the same time.\n    root.classList.remove('light', 'dark');\n    root.classList.add(targetTheme);\n\n    // Track which theme DayFlow applied for other consumers if needed\n    // Use a unique dataset key to avoid clashing with next-themes or other libraries\n    if (theme === 'auto') {\n      if (root.dataset.dfThemeOverride) {\n        delete root.dataset.dfThemeOverride;\n      }\n    } else if (root.dataset.dfThemeOverride !== targetTheme) {\n      root.dataset.dfThemeOverride = targetTheme;\n    }\n\n    // Set data attribute for CSS selectors, using a scoped name\n    if (root.dataset.dfTheme !== targetTheme) {\n      root.dataset.dfTheme = targetTheme;\n    }\n  }, [effectiveTheme, theme, systemTheme]);\n\n  /**\n   * Notify parent of theme changes\n   */\n  useEffect(() => {\n    if (onThemeChange) {\n      onThemeChange(theme, effectiveTheme);\n    }\n  }, [theme, effectiveTheme, onThemeChange]);\n\n  const value: ThemeContextType = useMemo(\n    () => ({\n      theme,\n      effectiveTheme,\n      setTheme,\n    }),\n    [theme, effectiveTheme, setTheme]\n  );\n\n  return (\n    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>\n  );\n};\n\nexport const useTheme = (): ThemeContextType => {\n  const context = useContext(ThemeContext);\n\n  if (context === undefined) {\n    throw new Error('useTheme must be used within a ThemeProvider');\n  }\n\n  return context;\n};\n"
  },
  {
    "path": "packages/core/src/core/CalendarApp.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { Locale } from '@/locale/types';\nimport { isValidLocale } from '@/locale/utils';\nimport {\n  CalendarAppConfig,\n  CalendarAppState,\n  CalendarCallbacks,\n  CalendarType,\n  CalendarView,\n  CalendarViewType,\n  Event,\n  ICalendarApp,\n  RangeChangeReason,\n  ReadOnlyConfig,\n  ViewType,\n} from '@/types';\nimport { ThemeMode } from '@/types/calendarTypes';\nimport { compareViews } from '@/utils/calendarApp';\nimport { isDeepEqual } from '@/utils/helpers';\nimport { normalizeTimeZoneValue } from '@/utils/timeZoneUtils';\n\nimport {\n  CalendarRegistry,\n  setDefaultCalendarRegistry,\n} from './calendarRegistry';\nimport { EventManager } from './events/EventManager';\nimport { NavigationController } from './navigation/NavigationController';\nimport {\n  canMutateFromUI,\n  getReadOnlyConfig,\n} from './permissions/CalendarPermissions';\nimport { PluginManager } from './plugins/PluginManager';\n\nexport class CalendarApp implements ICalendarApp {\n  public state: CalendarAppState;\n  private callbacks: CalendarCallbacks;\n  private calendarRegistry: CalendarRegistry;\n  private themeChangeListeners: Set<(theme: ThemeMode) => void>;\n  private listeners: Set<(app: ICalendarApp) => void>;\n  private eventManager: EventManager;\n  private navigation: NavigationController;\n  private pluginManager: PluginManager;\n  private useEventDetailDialog: boolean;\n  private useEventDetailPanel: boolean;\n  private useCalendarHeader: boolean;\n\n  constructor(config: CalendarAppConfig) {\n    const resolvedTimeZone =\n      normalizeTimeZoneValue(config.timeZone) ??\n      Intl.DateTimeFormat().resolvedOptions().timeZone;\n    const defaultCurrentDate = (() => {\n      const d = Temporal.Now.plainDateISO(resolvedTimeZone);\n      return new Date(d.year, d.month - 1, d.day);\n    })();\n    this.state = {\n      currentView: config.defaultView || ViewType.WEEK,\n      currentDate: config.initialDate || defaultCurrentDate,\n      events: config.events || [],\n      switcherMode: config.switcherMode || 'buttons',\n      plugins: new Map(),\n      views: new Map(),\n      locale: CalendarApp.resolveLocale(config.locale),\n      highlightedEventId: null,\n      selectedEventId: null,\n      readOnly: config.readOnly || false,\n      overrides: [],\n      allDaySortComparator: config.allDaySortComparator,\n      timeZone: resolvedTimeZone,\n    };\n\n    this.callbacks = config.callbacks || {};\n    this.themeChangeListeners = new Set();\n    this.listeners = new Set();\n\n    this.calendarRegistry = new CalendarRegistry(\n      config.calendars,\n      config.defaultCalendar,\n      config.theme?.mode || 'light'\n    );\n    setDefaultCalendarRegistry(this.calendarRegistry);\n\n    this.eventManager = new EventManager(\n      this.state,\n      this.calendarRegistry,\n      () => this.callbacks,\n      this.notify,\n      this.triggerRender,\n      config.events || []\n    );\n\n    this.navigation = new NavigationController(\n      this.state,\n      () => this.callbacks,\n      this.notify,\n      this.state.currentDate\n    );\n\n    this.pluginManager = new PluginManager(this.state, this.notify);\n\n    this.useEventDetailDialog = config.useEventDetailDialog ?? false;\n    this.useEventDetailPanel = config.useEventDetailPanel ?? true;\n    this.useCalendarHeader = config.useCalendarHeader ?? true;\n\n    config.views.forEach(view => this.state.views.set(view.type, view));\n    config.plugins?.forEach(plugin => this.pluginManager.install(plugin, this));\n\n    this.handleVisibleRangeChange('initial');\n  }\n\n  private static resolveLocale(locale?: string | Locale): string | Locale {\n    if (!locale) return 'en-US';\n    if (typeof locale === 'string') {\n      return isValidLocale(locale) ? locale : 'en-US';\n    }\n    if (\n      locale &&\n      typeof locale === 'object' &&\n      !isValidLocale((locale as Locale).code)\n    ) {\n      return { ...(locale as Locale), code: 'en-US' };\n    }\n    return locale;\n  }\n\n  // Subscription\n\n  subscribe = (listener: (app: ICalendarApp) => void): (() => void) => {\n    this.listeners.add(listener);\n    return () => this.listeners.delete(listener);\n  };\n\n  private notify = (): void => {\n    this.listeners.forEach(listener => listener(this));\n  };\n\n  triggerRender = (): void => {\n    this.callbacks.onRender?.();\n    this.notify();\n  };\n\n  // Navigation (delegated)\n\n  changeView = (view: CalendarViewType): void =>\n    this.navigation.changeView(view);\n\n  getCurrentView = (): CalendarView => this.navigation.getCurrentView();\n\n  emitVisibleRange = (\n    start: Date,\n    end: Date,\n    reason?: RangeChangeReason\n  ): void => this.navigation.emitVisibleRange(start, end, reason);\n\n  handleVisibleRangeChange = (reason: RangeChangeReason): void =>\n    this.navigation.handleVisibleRangeChange(reason);\n\n  setCurrentDate = (date: Date): void => this.navigation.setCurrentDate(date);\n\n  getCurrentDate = (): Date => this.navigation.getCurrentDate();\n\n  setVisibleMonth = (date: Date): void => this.navigation.setVisibleMonth(date);\n\n  getVisibleMonth = (): Date => this.navigation.getVisibleMonth();\n\n  goToToday = (): void => this.navigation.goToToday();\n  goToPrevious = (): void => this.navigation.goToPrevious();\n  goToNext = (): void => this.navigation.goToNext();\n\n  selectDate = (date: Date): void => this.navigation.selectDate(date);\n\n  // Events (delegated)\n\n  undo = (): void => this.eventManager.undo();\n\n  applyEventsChanges = (\n    changes: {\n      add?: Event[];\n      update?: Array<{ id: string; updates: Partial<Event> }>;\n      delete?: string[];\n    },\n    isPending?: boolean,\n    source?: 'drag' | 'resize'\n  ): void => this.eventManager.applyEventsChanges(changes, isPending, source);\n\n  addEvent = (event: Event): void => this.eventManager.addEvent(event);\n\n  addExternalEvents = (calendarId: string, events: Event[]): void =>\n    this.eventManager.addExternalEvents(calendarId, events);\n\n  updateEvent = (\n    id: string,\n    eventUpdate: Partial<Event>,\n    isPending?: boolean,\n    source?: 'drag' | 'resize'\n  ): Promise<void> =>\n    this.eventManager.updateEvent(id, eventUpdate, isPending, source);\n\n  deleteEvent = (id: string): Promise<void> =>\n    this.eventManager.deleteEvent(id);\n\n  getAllEvents = (): Event[] => this.eventManager.getAllEvents();\n  getEvents = (): Event[] => this.eventManager.getEvents();\n\n  onEventClick = (event: Event): void => this.eventManager.onEventClick(event);\n\n  onEventDoubleClick = (\n    event: Event,\n    e: MouseEvent\n  ): boolean | undefined | Promise<boolean | undefined> =>\n    this.eventManager.onEventDoubleClick(event, e);\n\n  onMoreEventsClick = (date: Date): void =>\n    this.eventManager.onMoreEventsClick(date);\n\n  onEventDetailToggle = (eventId: string | null): void =>\n    this.eventManager.onEventDetailToggle(eventId);\n\n  onMobileEventDetailToggle = (event: Event | null): void =>\n    this.eventManager.onMobileEventDetailToggle(event);\n\n  highlightEvent = (eventId: string | null): void =>\n    this.eventManager.highlightEvent(eventId);\n\n  selectEvent = (eventId: string | null): void =>\n    this.eventManager.selectEvent(eventId);\n\n  dismissUI = (): void => this.eventManager.dismissUI();\n\n  // Permissions (pure functions)\n\n  getReadOnlyConfig = (id?: string): ReadOnlyConfig =>\n    getReadOnlyConfig(\n      this.state.readOnly,\n      id,\n      this.calendarRegistry,\n      this.state.events\n    );\n\n  canMutateFromUI = (id?: string): boolean =>\n    canMutateFromUI(\n      this.state.readOnly,\n      id,\n      this.calendarRegistry,\n      this.state.events\n    );\n\n  // Calendars\n\n  getCalendars = (): CalendarType[] => this.calendarRegistry.getAll();\n\n  reorderCalendars = (fromIndex: number, toIndex: number): void => {\n    this.calendarRegistry.reorder(fromIndex, toIndex);\n    this.callbacks.onCalendarReorder?.(fromIndex, toIndex);\n    this.triggerRender();\n  };\n\n  setCalendarVisibility = (calendarId: string, visible: boolean): void => {\n    this.calendarRegistry.setVisibility(calendarId, visible);\n    this.triggerRender();\n  };\n\n  setAllCalendarsVisibility = (visible: boolean): void => {\n    this.calendarRegistry.setAllVisibility(visible);\n    this.triggerRender();\n  };\n\n  updateCalendar = (\n    id: string,\n    updates: Partial<CalendarType>,\n    isPending?: boolean\n  ): void => {\n    this.calendarRegistry.updateCalendar(id, updates);\n    if (isPending) {\n      this.notify();\n      return;\n    }\n    const updatedCalendar = this.calendarRegistry.get(id);\n    if (updatedCalendar) {\n      this.callbacks.onCalendarUpdate?.(updatedCalendar);\n    }\n    this.triggerRender();\n  };\n\n  createCalendar = async (calendar: CalendarType): Promise<void> => {\n    this.calendarRegistry.register(calendar);\n    await this.callbacks.onCalendarCreate?.(calendar);\n    this.triggerRender();\n  };\n\n  deleteCalendar = async (id: string): Promise<void> => {\n    this.calendarRegistry.unregister(id);\n    await this.callbacks.onCalendarDelete?.(id);\n    this.triggerRender();\n  };\n\n  mergeCalendars = async (\n    sourceId: string,\n    targetId: string\n  ): Promise<void> => {\n    const store = this.eventManager.getStore();\n    const sourceEvents = store\n      .getAllEvents()\n      .filter(e => e.calendarId === sourceId);\n\n    this.eventManager.pushToUndo();\n    store.beginTransaction();\n    sourceEvents.forEach(event =>\n      store.updateEvent(event.id, { calendarId: targetId })\n    );\n    await store.endTransaction();\n\n    await this.deleteCalendar(sourceId);\n    await this.callbacks.onCalendarMerge?.(sourceId, targetId);\n    // onRender and notify are triggered by store batch-change listener\n  };\n\n  // Plugins (delegated)\n\n  getPlugin = <T = unknown>(name: string): T | undefined =>\n    this.pluginManager.getPlugin<T>(name);\n\n  hasPlugin = (name: string): boolean => this.pluginManager.hasPlugin(name);\n\n  getPluginConfig = (pluginName: string): Record<string, unknown> =>\n    this.pluginManager.getPluginConfig(pluginName);\n\n  updatePluginConfig = (\n    pluginName: string,\n    config: Record<string, unknown>\n  ): void => this.pluginManager.updatePluginConfig(pluginName, config);\n\n  // Theme\n\n  setTheme = (mode: ThemeMode): void => {\n    this.calendarRegistry.setTheme(mode);\n    this.themeChangeListeners.forEach(listener => listener(mode));\n    this.triggerRender();\n  };\n\n  getTheme = (): ThemeMode => this.calendarRegistry.getTheme();\n\n  subscribeThemeChange = (\n    callback: (theme: ThemeMode) => void\n  ): (() => void) => {\n    this.themeChangeListeners.add(callback);\n    return () => this.unsubscribeThemeChange(callback);\n  };\n\n  unsubscribeThemeChange = (callback: (theme: ThemeMode) => void): void => {\n    this.themeChangeListeners.delete(callback);\n  };\n\n  // ── Config ───────────────────────────────────────────────────────────────────\n\n  getViewConfig = (viewType: CalendarViewType): Record<string, unknown> => {\n    const view = this.state.views.get(viewType);\n    return view?.config || {};\n  };\n\n  getCalendarRegistry = (): CalendarRegistry => this.calendarRegistry;\n  getUseEventDetailDialog = (): boolean => this.useEventDetailDialog;\n  getUseEventDetailPanel = (): boolean => this.useEventDetailPanel;\n  getCalendarHeaderConfig = (): boolean => this.useCalendarHeader;\n\n  get timeZone(): string {\n    return this.state.timeZone;\n  }\n\n  setOverrides = (overrides: string[]): void => {\n    this.state.overrides = overrides;\n    this.triggerRender();\n  };\n\n  updateConfig = (config: Partial<CalendarAppConfig>): void => {\n    let hasChanged = false;\n\n    if (\n      config.useEventDetailDialog !== undefined &&\n      config.useEventDetailDialog !== this.useEventDetailDialog\n    ) {\n      this.useEventDetailDialog = config.useEventDetailDialog;\n      hasChanged = true;\n    }\n    if (\n      config.useEventDetailPanel !== undefined &&\n      config.useEventDetailPanel !== this.useEventDetailPanel\n    ) {\n      this.useEventDetailPanel = config.useEventDetailPanel;\n      hasChanged = true;\n    }\n    if (\n      config.useCalendarHeader !== undefined &&\n      config.useCalendarHeader !== this.useCalendarHeader\n    ) {\n      this.useCalendarHeader = config.useCalendarHeader;\n      hasChanged = true;\n    }\n    if (\n      config.readOnly !== undefined &&\n      !isDeepEqual(config.readOnly, this.state.readOnly)\n    ) {\n      this.state.readOnly = config.readOnly;\n      hasChanged = true;\n    }\n    if (config.callbacks !== undefined) {\n      this.callbacks = config.callbacks;\n    }\n    if (\n      config.theme?.mode !== undefined &&\n      config.theme.mode !== this.getTheme()\n    ) {\n      this.setTheme(config.theme.mode);\n      // setTheme already triggers re-render\n    }\n    if (\n      config.switcherMode !== undefined &&\n      config.switcherMode !== this.state.switcherMode\n    ) {\n      this.state.switcherMode = config.switcherMode;\n      hasChanged = true;\n    }\n    if (config.locale !== undefined) {\n      const newLocale = CalendarApp.resolveLocale(config.locale);\n      if (!isDeepEqual(newLocale, this.state.locale)) {\n        this.state.locale = newLocale;\n        hasChanged = true;\n      }\n    }\n    if (\n      config.allDaySortComparator !== undefined &&\n      config.allDaySortComparator !== this.state.allDaySortComparator\n    ) {\n      this.state.allDaySortComparator = config.allDaySortComparator;\n      hasChanged = true;\n    }\n    if (config.timeZone !== undefined) {\n      const newTz =\n        normalizeTimeZoneValue(config.timeZone) ??\n        Intl.DateTimeFormat().resolvedOptions().timeZone;\n      if (newTz !== this.state.timeZone) {\n        this.state.timeZone = newTz;\n        hasChanged = true;\n      }\n    }\n    if (config.views !== undefined) {\n      const newViews = new Map(this.state.views);\n      let viewsChanged = false;\n      let viewsUpdated = false;\n      config.views.forEach(view => {\n        const existingView = newViews.get(view.type);\n        const diff = compareViews(existingView, view);\n\n        if (diff.hasChanges) {\n          newViews.set(view.type, view);\n          viewsUpdated = true;\n        }\n\n        viewsChanged = viewsChanged || diff.requiresRender;\n      });\n\n      if (viewsUpdated) {\n        this.state.views = newViews;\n      }\n\n      if (viewsChanged) {\n        hasChanged = true;\n      }\n    }\n\n    if (config.calendars !== undefined) {\n      let calendarsChanged = false;\n      for (const cal of config.calendars) {\n        const existing = this.calendarRegistry.get(cal.id);\n        if (existing && existing.source !== cal.source) {\n          this.calendarRegistry.updateCalendar(cal.id, { source: cal.source });\n          calendarsChanged = true;\n        }\n      }\n      if (calendarsChanged) hasChanged = true;\n    }\n\n    if (hasChanged) {\n      this.triggerRender();\n    }\n  };\n}\n"
  },
  {
    "path": "packages/core/src/core/CalendarStore.ts",
    "content": "import { EventChange } from '@/types/core';\nimport { Event } from '@/types/event';\n\n/**\n * CalendarStore\n *\n * Responsible for:\n * - Managing state mutations (events)\n * - Handling transactions\n * - Dispatching change notifications\n * - Normalizing batched changes\n *\n * Note: Business logic (validation, overlaps, etc.) belongs in plugins, not here.\n */\nexport class CalendarStore {\n  // In-memory storage\n  private events = new Map<string, Event>();\n\n  // Transaction state\n  private isInTransaction = false;\n  private pendingChanges: EventChange[] = [];\n\n  // Callbacks\n  public onEventChange?: (change: EventChange) => void | Promise<void>;\n  public onEventBatchChange?: (changes: EventChange[]) => void | Promise<void>;\n\n  constructor(initialEvents: Event[] = []) {\n    initialEvents.forEach(e => this.events.set(e.id, e));\n  }\n\n  // Transaction Management\n\n  public beginTransaction(): void {\n    if (this.isInTransaction) {\n      console.warn(\n        'Transaction already in progress. Nested transactions are not supported.'\n      );\n      return;\n    }\n    this.isInTransaction = true;\n    this.pendingChanges = [];\n  }\n\n  public endTransaction(): void | Promise<void> {\n    if (!this.isInTransaction) return;\n\n    // Normalize changes: merge updates, handle create+delete pairs, etc.\n    const normalizedChanges = CalendarStore.normalizeChanges(\n      this.pendingChanges\n    );\n\n    // Reset transaction state\n    this.isInTransaction = false;\n    this.pendingChanges = [];\n\n    // Dispatch batch update if there are effective changes\n    if (normalizedChanges.length > 0) {\n      return this.onEventBatchChange?.(normalizedChanges);\n    }\n  }\n\n  // CRUD Operations\n\n  public createEvent(event: Event): void | Promise<void> {\n    if (this.events.has(event.id)) {\n      throw new Error(`Event with ID ${event.id} already exists.`);\n    }\n\n    this.events.set(event.id, event);\n    return this.emitChange({ type: 'create', event });\n  }\n\n  public updateEvent(\n    id: string,\n    updates: Partial<Event>\n  ): void | Promise<void> {\n    const existingEvent = this.events.get(id);\n    if (!existingEvent) {\n      throw new Error(`Event with id ${id} not found`);\n    }\n\n    const updatedEvent = { ...existingEvent, ...updates };\n    this.events.set(id, updatedEvent);\n    return this.emitChange({\n      type: 'update',\n      before: existingEvent,\n      after: updatedEvent,\n    });\n  }\n\n  public deleteEvent(id: string): void | Promise<void> {\n    const event = this.events.get(id);\n    if (!event) {\n      return;\n    }\n\n    this.events.delete(id);\n    return this.emitChange({ type: 'delete', event });\n  }\n\n  // Read Operations\n\n  public getEvent(id: string): Event | undefined {\n    return this.events.get(id);\n  }\n\n  public getAllEvents(): Event[] {\n    return Array.from(this.events.values());\n  }\n\n  // Internal Logic\n\n  private emitChange(change: EventChange): void | Promise<void> {\n    if (this.isInTransaction) {\n      this.pendingChanges.push(change);\n    } else {\n      return this.onEventChange?.(change);\n    }\n  }\n\n  /**\n   * Pure function to normalize a list of changes.\n   * Merges multiple changes for the same ID into a single effective change.\n   */\n  private static normalizeChanges(changes: EventChange[]): EventChange[] {\n    // Map to track the net effect for each event ID\n    const changeMap = new Map<string, EventChange>();\n\n    for (const change of changes) {\n      const id =\n        change.type === 'delete'\n          ? change.event.id\n          : change.type === 'update'\n            ? change.after.id\n            : change.event.id;\n\n      const prev = changeMap.get(id);\n\n      if (!prev) {\n        changeMap.set(id, change);\n        continue;\n      }\n\n      // Merge logic based on the type of the previous change\n      if (prev.type === 'create') {\n        // PREV: Create(A)\n        if (change.type === 'update') {\n          // + CURR: Update(A->B)\n          // = Create(B)\n          changeMap.set(id, { type: 'create', event: change.after });\n        } else if (change.type === 'delete') {\n          // + CURR: Delete(A)\n          // = Cancel out\n          changeMap.delete(id);\n        }\n      } else if (prev.type === 'update') {\n        // PREV: Update(A->B)\n        if (change.type === 'update') {\n          // + CURR: Update(B->C)\n          // = Update(A->C)\n          changeMap.set(id, {\n            type: 'update',\n            before: prev.before,\n            after: change.after,\n          });\n        } else if (change.type === 'delete') {\n          // + CURR: Delete(B)\n          // = Delete(A)  (The original state A is now gone)\n          changeMap.set(id, { type: 'delete', event: prev.before });\n        }\n      } else if (prev.type === 'delete' && change.type === 'create') {\n        // PREV: Delete(A)\n        // + CURR: Create(B) (where B.id === A.id)\n        // = Update(A->B)\n        changeMap.set(id, {\n          type: 'update',\n          before: prev.event,\n          after: change.event,\n        });\n      }\n    }\n\n    return Array.from(changeMap.values());\n  }\n}\n"
  },
  {
    "path": "packages/core/src/core/__tests__/CalendarApp.test.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { CalendarApp } from '@/core/CalendarApp';\nimport { createDayView } from '@/factories/createDayView';\nimport { createMonthView } from '@/factories/createMonthView';\nimport { createWeekView } from '@/factories/createWeekView';\nimport { createYearView } from '@/factories/createYearView';\nimport { ViewType } from '@/types';\n\nconst component = () => null;\n\ndescribe('CalendarApp', () => {\n  describe('Event Management', () => {\n    it('should add an event', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        defaultView: ViewType.WEEK,\n      });\n\n      const event = {\n        id: 'test-1',\n        title: 'Test Event',\n        start: Temporal.Now.plainDateISO(),\n        end: Temporal.Now.plainDateISO(),\n      };\n\n      app.addEvent(event);\n      const events = app.getAllEvents();\n\n      expect(events).toHaveLength(1);\n      expect(events[0]).toEqual({\n        ...event,\n        calendarId: 'blue',\n      });\n    });\n\n    it('should update an event', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [\n          {\n            id: 'test-1',\n            title: 'Original Title',\n            start: Temporal.Now.plainDateISO(),\n            end: Temporal.Now.plainDateISO(),\n          },\n        ],\n        defaultView: ViewType.WEEK,\n      });\n\n      app.updateEvent('test-1', { title: 'Updated Title' });\n      const events = app.getAllEvents();\n\n      expect(events[0].title).toBe('Updated Title');\n    });\n\n    it('updates an event once and preserves the assigned calendarId', async () => {\n      const onEventUpdate = jest.fn();\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [\n          {\n            id: 'test-update-once',\n            title: 'Original Title',\n            start: Temporal.Now.plainDateISO(),\n            end: Temporal.Now.plainDateISO(),\n          },\n        ],\n        calendars: [\n          {\n            id: 'work',\n            name: 'Work',\n            colors: {\n              eventColor: '#000',\n              eventSelectedColor: '#111',\n              lineColor: '#222',\n              textColor: '#333',\n            },\n          },\n        ],\n        defaultCalendar: 'work',\n        callbacks: { onEventUpdate },\n      });\n\n      await app.updateEvent('test-update-once', { title: 'Updated Title' });\n\n      expect(app.getAllEvents()).toEqual([\n        expect.objectContaining({\n          id: 'test-update-once',\n          title: 'Updated Title',\n          calendarId: 'work',\n        }),\n      ]);\n      expect(onEventUpdate).toHaveBeenCalledTimes(1);\n      expect(onEventUpdate).toHaveBeenCalledWith(\n        expect.objectContaining({\n          id: 'test-update-once',\n          title: 'Updated Title',\n          calendarId: 'work',\n        })\n      );\n    });\n\n    it('should delete an event', async () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [\n          {\n            id: 'test-1',\n            title: 'Test Event',\n            start: Temporal.Now.plainDateISO(),\n            end: Temporal.Now.plainDateISO(),\n          },\n        ],\n        defaultView: ViewType.WEEK,\n      });\n\n      await app.deleteEvent('test-1');\n      const events = app.getAllEvents();\n\n      expect(events).toHaveLength(0);\n    });\n\n    it('should throw error when updating non-existent event', async () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        defaultView: ViewType.WEEK,\n      });\n\n      await expect(\n        app.updateEvent('non-existent', { title: 'New Title' })\n      ).rejects.toThrow('Event with id non-existent not found');\n    });\n\n    it('allows programmatic event changes while readOnly is true', async () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        defaultView: ViewType.WEEK,\n        readOnly: true,\n      });\n\n      const event = {\n        id: 'readonly-1',\n        title: 'Read-only Event',\n        start: Temporal.Now.plainDateISO(),\n        end: Temporal.Now.plainDateISO(),\n      };\n\n      app.addEvent(event);\n      await app.updateEvent(event.id, { title: 'Updated in Read-only' });\n      await app.deleteEvent(event.id);\n\n      expect(app.getAllEvents()).toHaveLength(0);\n    });\n\n    it('shows initial events without calendarId by assigning the default calendar', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [\n          {\n            id: 'initial-no-calendar',\n            title: 'Initial Event',\n            start: Temporal.Now.plainDateISO(),\n            end: Temporal.Now.plainDateISO(),\n          },\n        ],\n        calendars: [\n          {\n            id: 'work',\n            name: 'Work',\n            colors: {\n              eventColor: '#000',\n              eventSelectedColor: '#111',\n              lineColor: '#222',\n              textColor: '#333',\n            },\n          },\n        ],\n        defaultCalendar: 'work',\n      });\n\n      expect(app.getAllEvents()).toEqual([\n        expect.objectContaining({\n          id: 'initial-no-calendar',\n          calendarId: 'work',\n        }),\n      ]);\n      expect(app.getEvents()).toEqual([\n        expect.objectContaining({\n          id: 'initial-no-calendar',\n          calendarId: 'work',\n        }),\n      ]);\n    });\n\n    it('applies batched programmatic changes while readOnly is true', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        defaultView: ViewType.WEEK,\n        readOnly: true,\n      });\n\n      const event = {\n        id: 'readonly-batch',\n        title: 'Batch Event',\n        start: Temporal.Now.plainDateISO(),\n        end: Temporal.Now.plainDateISO(),\n      };\n\n      app.applyEventsChanges({ add: [event] });\n      app.applyEventsChanges({\n        update: [{ id: event.id, updates: { title: 'Batch Updated' } }],\n      });\n\n      expect(app.getAllEvents()).toEqual([\n        { ...event, title: 'Batch Updated', calendarId: 'blue' },\n      ]);\n\n      app.applyEventsChanges({ delete: [event.id] });\n      expect(app.getAllEvents()).toHaveLength(0);\n    });\n\n    it('exposes a consistent canMutateFromUI helper', () => {\n      const editableApp = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        readOnly: false,\n      });\n      const readOnlyApp = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        readOnly: true,\n      });\n      const partialReadOnlyApp = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        readOnly: { draggable: false, viewable: true },\n      });\n\n      expect(editableApp.canMutateFromUI()).toBe(true);\n      expect(readOnlyApp.canMutateFromUI()).toBe(false);\n      expect(partialReadOnlyApp.canMutateFromUI()).toBe(false);\n    });\n\n    it('canMutateFromUI should respect per-calendar read-only status', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [\n          {\n            id: 'event-1',\n            title: 'Subscribed Event',\n            start: Temporal.Now.plainDateISO(),\n            end: Temporal.Now.plainDateISO(),\n            calendarId: 'sub-cal',\n          },\n          {\n            id: 'event-2',\n            title: 'Regular Event',\n            start: Temporal.Now.plainDateISO(),\n            end: Temporal.Now.plainDateISO(),\n            calendarId: 'reg-cal',\n          },\n        ],\n        calendars: [\n          {\n            id: 'sub-cal',\n            name: 'Subscribed',\n            colors: {\n              eventColor: '#000',\n              eventSelectedColor: '#000',\n              lineColor: '#000',\n              textColor: '#000',\n            },\n            subscription: {\n              url: 'http://example.com/cal.ics',\n              status: 'ready',\n            },\n          },\n          {\n            id: 'reg-cal',\n            name: 'Regular',\n            colors: {\n              eventColor: '#000',\n              eventSelectedColor: '#000',\n              lineColor: '#000',\n              textColor: '#000',\n            },\n          },\n        ],\n        readOnly: false,\n      });\n\n      // Default subscribed calendar is read-only\n      expect(app.canMutateFromUI('sub-cal')).toBe(false);\n      expect(app.canMutateFromUI('event-1')).toBe(false);\n\n      // Regular calendar is editable\n      expect(app.canMutateFromUI('reg-cal')).toBe(true);\n      expect(app.canMutateFromUI('event-2')).toBe(true);\n\n      // Explicit read-only on regular calendar\n      app.updateCalendar('reg-cal', { readOnly: true });\n      expect(app.canMutateFromUI('reg-cal')).toBe(false);\n      expect(app.canMutateFromUI('event-2')).toBe(false);\n\n      // Explicit override for subscribed calendar\n      app.updateCalendar('sub-cal', { readOnly: false });\n      expect(app.canMutateFromUI('sub-cal')).toBe(true);\n      expect(app.canMutateFromUI('event-1')).toBe(true);\n\n      // Global read-only overrides everything\n      app.updateConfig({ readOnly: true });\n      expect(app.canMutateFromUI('sub-cal')).toBe(false);\n      expect(app.canMutateFromUI('reg-cal')).toBe(false);\n    });\n  });\n\n  describe('View Management', () => {\n    it('should change view', () => {\n      const app = new CalendarApp({\n        views: [createMonthView(), createWeekView(), createDayView()],\n        plugins: [],\n        events: [],\n        defaultView: ViewType.WEEK,\n      });\n\n      app.changeView(ViewType.MONTH);\n      expect(app.state.currentView).toBe(ViewType.MONTH);\n    });\n\n    it('should navigate to today', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        defaultView: ViewType.WEEK,\n      });\n\n      const today = new Date();\n      app.goToToday();\n\n      const appDate = app.state.currentDate;\n      expect(appDate.getFullYear()).toBe(today.getFullYear());\n      expect(appDate.getMonth()).toBe(today.getMonth());\n      expect(appDate.getDate()).toBe(today.getDate());\n    });\n  });\n\n  describe('Locale Management', () => {\n    it('should default to en-US locale', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n      });\n\n      expect(app.state.locale).toBe('en-US');\n    });\n\n    it('should accept any provided locale string', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        locale: 'ja',\n      });\n\n      expect(app.state.locale).toBe('ja');\n    });\n\n    it('should accept arbitrary locale string (validation handled by consumer)', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        locale: 'fr-CA',\n      });\n\n      expect(app.state.locale).toBe('fr-CA');\n    });\n\n    it('should accept Locale object', () => {\n      const customLocale = {\n        code: 'custom',\n        messages: { today: 'Today Custom' },\n      };\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        locale: customLocale,\n      });\n\n      expect(app.state.locale).toBe(customLocale);\n    });\n\n    it('should fallback to en-US for invalid locale string', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        locale: '!!!',\n      });\n\n      expect(app.state.locale).toBe('en-US');\n    });\n\n    it('should fallback to en-US for Locale object with invalid code', () => {\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        locale: { code: '!!invalid!!', messages: {} },\n      });\n\n      expect((app.state.locale as unknown as { code: string }).code).toBe(\n        'en-US'\n      );\n    });\n  });\n\n  describe('Config Updates', () => {\n    it('updates day/week secondaryTimeZone and month/year visualTimeZone independently', () => {\n      const app = new CalendarApp({\n        views: [\n          createDayView(),\n          createWeekView(),\n          createMonthView(),\n          createYearView(),\n        ],\n        plugins: [],\n        events: [],\n      });\n\n      app.updateConfig({\n        views: [\n          createDayView({ secondaryTimeZone: 'Asia/Tokyo' }),\n          createWeekView({ secondaryTimeZone: 'Asia/Tokyo' }),\n          createMonthView({}),\n          createYearView({}),\n        ],\n      });\n\n      expect(app.getViewConfig(ViewType.DAY)).toMatchObject({\n        secondaryTimeZone: 'Asia/Tokyo',\n      });\n      expect(app.getViewConfig(ViewType.WEEK)).toMatchObject({\n        secondaryTimeZone: 'Asia/Tokyo',\n      });\n\n      app.updateConfig({\n        views: [\n          createDayView({ secondaryTimeZone: 'America/New_York' }),\n          createWeekView({ secondaryTimeZone: 'America/New_York' }),\n          createMonthView({}),\n          createYearView({}),\n        ],\n      });\n\n      expect(app.getViewConfig(ViewType.DAY)).toMatchObject({\n        secondaryTimeZone: 'America/New_York',\n      });\n      expect(app.getViewConfig(ViewType.WEEK)).toMatchObject({\n        secondaryTimeZone: 'America/New_York',\n      });\n    });\n\n    it('keeps navigation and app.state in sync after repeated view config updates', () => {\n      const app = new CalendarApp({\n        views: [\n          createDayView(),\n          createWeekView(),\n          createMonthView(),\n          createYearView(),\n        ],\n        plugins: [],\n        events: [],\n        defaultView: ViewType.MONTH,\n      });\n\n      app.updateConfig({\n        views: [\n          createDayView({ secondaryTimeZone: 'Asia/Tokyo' }),\n          createWeekView({ secondaryTimeZone: 'Asia/Tokyo' }),\n          createMonthView(),\n          createYearView(),\n        ],\n      });\n\n      app.updateConfig({\n        views: [\n          createDayView({ secondaryTimeZone: 'America/New_York' }),\n          createWeekView({ secondaryTimeZone: 'America/New_York' }),\n          createMonthView(),\n          createYearView(),\n        ],\n      });\n\n      app.changeView(ViewType.WEEK);\n\n      expect(app.state.currentView).toBe(ViewType.WEEK);\n      expect(app.getCurrentView().type).toBe(ViewType.WEEK);\n      expect(app.getViewConfig(ViewType.WEEK)).toMatchObject({\n        secondaryTimeZone: 'America/New_York',\n      });\n    });\n\n    it('does not trigger a render when allDaySortComparator is unchanged', () => {\n      const onRender = jest.fn();\n      const comparator = jest.fn(() => 0);\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        callbacks: { onRender },\n        allDaySortComparator: comparator,\n      });\n\n      onRender.mockClear();\n      app.updateConfig({ allDaySortComparator: comparator });\n\n      expect(onRender).not.toHaveBeenCalled();\n    });\n\n    it('triggers a render when allDaySortComparator changes', () => {\n      const onRender = jest.fn();\n      const comparatorA = jest.fn(() => 0);\n      const comparatorB = jest.fn(() => 0);\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        callbacks: { onRender },\n        allDaySortComparator: comparatorA,\n      });\n\n      onRender.mockClear();\n      app.updateConfig({ allDaySortComparator: comparatorB });\n\n      expect(onRender).toHaveBeenCalledTimes(1);\n      expect(app.state.allDaySortComparator).toBe(comparatorB);\n    });\n\n    it('updates view config function references without forcing a render', () => {\n      const onRender = jest.fn();\n      const resolverA = jest.fn(() => 'a');\n      const resolverB = jest.fn(() => 'b');\n      const app = new CalendarApp({\n        views: [\n          {\n            type: 'resource-grid',\n            component,\n            config: { getResourceId: resolverA },\n          },\n        ],\n        plugins: [],\n        events: [],\n        callbacks: { onRender },\n      });\n\n      onRender.mockClear();\n      app.updateConfig({\n        views: [\n          {\n            type: 'resource-grid',\n            component,\n            config: { getResourceId: resolverB },\n          },\n        ],\n      });\n\n      expect(onRender).not.toHaveBeenCalled();\n      expect(\n        (\n          app.getViewConfig('resource-grid') as {\n            getResourceId?: typeof resolverB;\n          }\n        ).getResourceId\n      ).toBe(resolverB);\n    });\n\n    it('replaces callbacks instead of retaining removed handlers', () => {\n      const onEventClick = jest.fn();\n      const app = new CalendarApp({\n        views: [],\n        plugins: [],\n        events: [],\n        callbacks: { onEventClick },\n      });\n      const event = {\n        id: 'callback-event',\n        title: 'Callback Event',\n        start: Temporal.Now.plainDateISO(),\n        end: Temporal.Now.plainDateISO(),\n      };\n\n      app.onEventClick(event);\n      app.updateConfig({ callbacks: {} });\n      app.onEventClick(event);\n\n      expect(onEventClick).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/src/core/__tests__/WeekViewConfig.test.ts",
    "content": "import { createWeekView } from '@/factories/createWeekView';\n\ndescribe('WeekView Configuration', () => {\n  it('should create week view', () => {\n    const view = createWeekView();\n    expect(view.type).toBe('week');\n  });\n});\n"
  },
  {
    "path": "packages/core/src/core/__tests__/calendarRegistry.test.ts",
    "content": "import { CalendarRegistry } from '@/core/calendarRegistry';\nimport { CalendarType } from '@/types/calendarTypes';\n\nconst mockColors = {\n  eventColor: '#eff6ff',\n  eventSelectedColor: '#3b82f6',\n  lineColor: '#3b82f6',\n  textColor: '#1e3a8a',\n};\n\ndescribe('CalendarRegistry', () => {\n  let registry: CalendarRegistry;\n  const calendars: CalendarType[] = [\n    { id: '1', name: 'Cal 1', colors: mockColors, isVisible: true },\n    { id: '2', name: 'Cal 2', colors: mockColors, isVisible: true },\n    { id: '3', name: 'Cal 3', colors: mockColors, isVisible: false },\n  ];\n\n  beforeEach(() => {\n    registry = new CalendarRegistry(calendars);\n  });\n\n  describe('Visibility Enforcement', () => {\n    it('should ensure at least one visible during initialization', () => {\n      const allHidden = [\n        { id: '1', name: 'Cal 1', colors: mockColors, isVisible: false },\n        { id: '2', name: 'Cal 2', colors: mockColors, isVisible: false },\n      ];\n      const newRegistry = new CalendarRegistry(allHidden);\n      expect(newRegistry.getVisible()).toHaveLength(1);\n    });\n\n    it('should prevent hiding the last visible calendar', () => {\n      // Hide second one, leaving only the first one visible\n      registry.setVisibility('2', false);\n      expect(registry.getVisible()).toHaveLength(1);\n      expect(registry.getVisible()[0].id).toBe('1');\n\n      // Try to hide the last visible one\n      registry.setVisibility('1', false);\n      expect(registry.getVisible()).toHaveLength(1);\n      expect(registry.getVisible()[0].id).toBe('1');\n    });\n\n    it('should ensure at least one visible after setAllVisibility(false)', () => {\n      registry.setAllVisibility(false);\n      const visible = registry.getVisible();\n      expect(visible.length).toBeGreaterThanOrEqual(1);\n      expect(visible.length).toBe(1);\n    });\n\n    it('should ensure at least one visible after unregistering the only visible calendar', () => {\n      // Setup: 1 visible, 1 hidden\n      registry.setVisibility('2', false);\n      expect(registry.getVisible()).toHaveLength(1);\n      expect(registry.getVisible()[0].id).toBe('1');\n\n      // Unregister the only visible one\n      registry.unregister('1');\n\n      // Now either 2 or 3 should have become visible\n      const visible = registry.getVisible();\n      expect(visible.length).toBeGreaterThanOrEqual(1);\n      expect(registry.has('1')).toBe(false);\n    });\n\n    it('should handle setAllVisibility(true)', () => {\n      registry.setAllVisibility(true);\n      expect(registry.getVisible()).toHaveLength(3);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/src/core/calendarRegistry.ts",
    "content": "// Calendar Registry - Manages calendar types and color resolution\n\nimport { CalendarType, ThemeMode, CalendarColors } from '@/types/calendarTypes';\n\nconst isWritable = (cal: CalendarType) => !cal.readOnly && !cal.subscription;\n\n/**\n * Default calendar types\n */\nexport const DEFAULT_CALENDAR_TYPES: CalendarType[] = [\n  {\n    id: 'blue',\n    name: 'Blue',\n    isDefault: true,\n    colors: {\n      eventColor: '#eff6ff',\n      eventSelectedColor: 'rgba(59, 130, 246)',\n      lineColor: '#3b82f6',\n      textColor: '#1e3a8a', // text-blue-900\n    },\n    darkColors: {\n      eventColor: 'rgba(30, 64, 175, 0.8)',\n      eventSelectedColor: 'rgba(30, 58, 138, 1)',\n      lineColor: '#3b82f6',\n      textColor: '#dbeafe',\n    },\n  },\n  {\n    id: 'green',\n    name: 'Green',\n    isDefault: true,\n    colors: {\n      eventColor: '#f0fdf4',\n      eventSelectedColor: 'rgba(16, 185, 129, 1)',\n      lineColor: '#10b981',\n      textColor: '#064e3b',\n    },\n    darkColors: {\n      eventColor: 'rgba(4, 120, 87, 0.8)',\n      eventSelectedColor: 'rgba(6, 78, 59, 1)',\n      lineColor: '#10b981',\n      textColor: '#d1fae5',\n    },\n  },\n  {\n    id: 'purple',\n    name: 'Purple',\n    isDefault: true,\n    colors: {\n      eventColor: '#faf5ff',\n      eventSelectedColor: 'rgba(139, 92, 246, 1)',\n      lineColor: '#8b5cf6',\n      textColor: '#5b21b6',\n    },\n    darkColors: {\n      eventColor: 'rgba(109, 40, 217, 0.8)',\n      eventSelectedColor: 'rgba(91, 33, 182, 1)',\n      lineColor: '#8b5cf6',\n      textColor: '#ede9fe',\n    },\n  },\n  {\n    id: 'yellow',\n    name: 'Yellow',\n    isDefault: true,\n    colors: {\n      eventColor: '#fefce8',\n      eventSelectedColor: 'rgba(245, 158, 11, 1)',\n      lineColor: '#f59e0b',\n      textColor: '#78350f',\n    },\n    darkColors: {\n      eventColor: 'rgba(180, 83, 9, 0.8)',\n      eventSelectedColor: 'rgba(120, 53, 15, 1)',\n      lineColor: '#f59e0b',\n      textColor: '#fef3c7',\n    },\n  },\n  {\n    id: 'red',\n    name: 'Red',\n    isDefault: true,\n    colors: {\n      eventColor: '#fef2f2',\n      eventSelectedColor: 'rgba(239, 68, 68, 1)',\n      lineColor: '#ef4444',\n      textColor: '#7f1d1d',\n    },\n    darkColors: {\n      eventColor: 'rgba(185, 28, 28, 0.8)',\n      eventSelectedColor: 'rgba(127, 29, 29, 1)',\n      lineColor: '#ef4444',\n      textColor: '#fee2e2',\n    },\n  },\n  {\n    id: 'orange',\n    name: 'Orange',\n    isDefault: true,\n    colors: {\n      eventColor: '#fff7edb3',\n      eventSelectedColor: 'rgba(249, 115, 22, 1)',\n      lineColor: '#f97316',\n      textColor: '#7c2d12',\n    },\n    darkColors: {\n      eventColor: 'rgba(194, 65, 12, 0.8)',\n      eventSelectedColor: 'rgba(124, 45, 18, 1)',\n      lineColor: '#f97316',\n      textColor: '#fed7aa',\n    },\n  },\n  {\n    id: 'pink',\n    name: 'Pink',\n    isDefault: true,\n    colors: {\n      eventColor: '#fdf2f8',\n      eventSelectedColor: 'rgba(236, 72, 153, 1)',\n      lineColor: '#ec4899',\n      textColor: '#831843',\n    },\n    darkColors: {\n      eventColor: 'rgba(190, 24, 93, 0.8)',\n      eventSelectedColor: 'rgba(131, 24, 67, 1)',\n      lineColor: '#ec4899',\n      textColor: '#fce7f3',\n    },\n  },\n  {\n    id: 'teal',\n    name: 'Teal',\n    isDefault: true,\n    colors: {\n      eventColor: '#f0fdfa',\n      eventSelectedColor: 'rgba(20, 184, 166, 1)',\n      lineColor: '#14b8a6',\n      textColor: '#134e4a',\n    },\n    darkColors: {\n      eventColor: 'rgba(15, 118, 110, 0.8)',\n      eventSelectedColor: 'rgba(19, 78, 74, 1)',\n      lineColor: '#14b8a6',\n      textColor: '#ccfbf1',\n    },\n  },\n  {\n    id: 'indigo',\n    name: 'Indigo',\n    isDefault: true,\n    colors: {\n      eventColor: '#eef2ffb3',\n      eventSelectedColor: 'rgba(99, 102, 241, 1)',\n      lineColor: '#6366f1',\n      textColor: '#312e81',\n    },\n    darkColors: {\n      eventColor: 'rgba(67, 56, 202, 0.8)',\n      eventSelectedColor: 'rgba(49, 46, 129, 1)',\n      lineColor: '#6366f1',\n      textColor: '#e0e7ff',\n    },\n  },\n  {\n    id: 'gray',\n    name: 'Gray',\n    isDefault: true,\n    colors: {\n      eventColor: '#f9fafbb3',\n      eventSelectedColor: 'rgba(107, 114, 128, 1)',\n      lineColor: '#6b7280',\n      textColor: '#1f2937',\n    },\n    darkColors: {\n      eventColor: 'rgba(75, 85, 99, 0.8)',\n      eventSelectedColor: 'rgba(31, 41, 55, 1)',\n      lineColor: '#6b7280',\n      textColor: '#f3f4f6',\n    },\n  },\n];\n\n/**\n * Calendar Registry\n * Manages calendar types and provides color resolution\n */\nexport class CalendarRegistry {\n  private calendars: Map<string, CalendarType>;\n  private defaultCalendarId: string;\n  private currentTheme: ThemeMode;\n\n  constructor(\n    customCalendars?: CalendarType[],\n    defaultCalendarId?: string,\n    theme: ThemeMode = 'light'\n  ) {\n    this.calendars = new Map();\n    this.defaultCalendarId = defaultCalendarId || 'blue';\n    this.currentTheme = theme;\n\n    // If custom calendars are provided, hide default calendars by default\n    const shouldHideDefaults = customCalendars && customCalendars.length > 0;\n\n    // Override with custom calendars\n    if (customCalendars) {\n      customCalendars.forEach(calendar => {\n        this.calendars.set(calendar.id, calendar);\n      });\n    } else {\n      // Register default calendars\n      DEFAULT_CALENDAR_TYPES.forEach(calendar => {\n        this.calendars.set(calendar.id, {\n          ...calendar,\n          // Hide defaults if custom calendars are provided\n          isVisible: shouldHideDefaults ? false : calendar.isVisible,\n        });\n      });\n    }\n\n    // Enforce \"at least one visible\" rule on initialization\n    if (this.calendars.size > 0 && this.getVisible().length === 0) {\n      const firstId = this.calendars.keys().next().value;\n      if (firstId) {\n        const first = this.calendars.get(firstId)!;\n        this.calendars.set(firstId, { ...first, isVisible: true });\n      }\n    }\n  }\n\n  /**\n   * Register a new calendar type\n   */\n  register(calendar: CalendarType): void {\n    this.calendars.set(calendar.id, calendar);\n  }\n\n  /**\n   * Unregister a calendar type\n   */\n  unregister(calendarId: string): boolean {\n    const calendar = this.calendars.get(calendarId);\n    const wasVisible = calendar?.isVisible !== false;\n    const deleted = this.calendars.delete(calendarId);\n\n    if (deleted && wasVisible) {\n      const remainingVisible = this.getVisible();\n      if (remainingVisible.length === 0) {\n        const firstRemaining = this.getAll()[0];\n        if (firstRemaining) {\n          this.setVisibility(firstRemaining.id, true);\n        }\n      }\n    }\n    return deleted;\n  }\n\n  /**\n   * Get a calendar type by ID\n   */\n  get(calendarId: string): CalendarType | undefined {\n    return this.calendars.get(calendarId);\n  }\n\n  /**\n   * Get all calendar types\n   */\n  getAll(): CalendarType[] {\n    return Array.from(this.calendars.values());\n  }\n\n  /**\n   * Get visible calendar types\n   */\n  getVisible(): CalendarType[] {\n    return this.getAll().filter(cal => cal.isVisible !== false);\n  }\n\n  /**\n   * Check if a calendar exists\n   */\n  has(calendarId: string): boolean {\n    return this.calendars.has(calendarId);\n  }\n\n  /**\n   * Reorder calendars\n   * @param fromIndex - Source index\n   * @param toIndex - Destination index\n   */\n  reorder(fromIndex: number, toIndex: number): void {\n    const entries = Array.from(this.calendars.entries());\n    if (\n      fromIndex < 0 ||\n      fromIndex >= entries.length ||\n      toIndex < 0 ||\n      toIndex >= entries.length\n    ) {\n      return;\n    }\n\n    const [removed] = entries.splice(fromIndex, 1);\n    entries.splice(toIndex, 0, removed);\n\n    this.calendars.clear();\n    entries.forEach(([key, value]) => {\n      this.calendars.set(key, value);\n    });\n  }\n\n  /**\n   * Update visibility of a specific calendar type\n   */\n  setVisibility(calendarId: string, visible: boolean): void {\n    const calendar = this.calendars.get(calendarId);\n    if (!calendar) return;\n\n    if (!visible) {\n      const visibleCount = this.getVisible().length;\n      if (visibleCount <= 1 && calendar.isVisible !== false) {\n        return; // Prevent hiding the last visible one\n      }\n    }\n\n    this.calendars.set(calendarId, {\n      ...calendar,\n      isVisible: visible,\n    });\n  }\n\n  /**\n   * Update visibility for all calendar types\n   */\n  setAllVisibility(visible: boolean): void {\n    this.calendars.forEach((calendar, id) => {\n      this.calendars.set(id, {\n        ...calendar,\n        isVisible: visible,\n      });\n    });\n\n    if (!visible && this.calendars.size > 0) {\n      // Force first one to be visible to ensure \"at least one\" rule\n      const firstId = this.calendars.keys().next().value;\n      if (firstId) {\n        const first = this.calendars.get(firstId)!;\n        this.calendars.set(firstId, { ...first, isVisible: true });\n      }\n    }\n  }\n\n  /**\n   * Update calendar properties\n   */\n  updateCalendar(calendarId: string, updates: Partial<CalendarType>): void {\n    const calendar = this.calendars.get(calendarId);\n    if (!calendar) return;\n\n    this.calendars.set(calendarId, {\n      ...calendar,\n      ...updates,\n    });\n  }\n\n  /**\n   * Set the default calendar ID\n   */\n  setDefaultCalendar(calendarId: string): void {\n    if (!this.has(calendarId)) {\n      throw new Error(`Calendar type '${calendarId}' does not exist`);\n    }\n    this.defaultCalendarId = calendarId;\n  }\n\n  /**\n   * Get the default calendar ID\n   */\n  getDefaultCalendarId(): string {\n    return this.defaultCalendarId;\n  }\n\n  /**\n   * Get the default calendar type\n   */\n  getDefaultCalendar(): CalendarType {\n    const calendar = this.get(this.defaultCalendarId);\n    if (!calendar) {\n      // Fallback to first available calendar\n      return this.getAll()[0];\n    }\n    return calendar;\n  }\n\n  /**\n   * Get the first writable (non-readOnly, non-subscription) calendar for event creation.\n   * Prefers the default calendar; falls back to the first writable calendar.\n   * Returns undefined if every calendar is read-only.\n   */\n  getDefaultWritableCalendar(): CalendarType | undefined {\n    const defaultCal = this.getDefaultCalendar();\n    if (defaultCal && isWritable(defaultCal)) return defaultCal;\n    return this.getAll().find(isWritable);\n  }\n\n  /**\n   * Set the current theme\n   */\n  setTheme(theme: ThemeMode): void {\n    this.currentTheme = theme;\n  }\n\n  /**\n   * Get the current theme\n   */\n  getTheme(): ThemeMode {\n    return this.currentTheme;\n  }\n\n  /**\n   * Resolve colors for a calendar ID based on current theme\n   */\n  resolveColors(calendarId?: string, theme?: ThemeMode): CalendarColors {\n    const activeTheme = theme || this.currentTheme;\n    const isDark = CalendarRegistry.isDarkTheme(activeTheme);\n\n    // Try to get the specified calendar\n    let calendar: CalendarType | undefined;\n    if (calendarId) {\n      calendar = this.get(calendarId);\n    }\n\n    // Fall back to default calendar if not found\n    if (!calendar) {\n      calendar = this.getDefaultCalendar();\n    }\n\n    // Return appropriate colors based on theme\n    if (isDark && calendar.darkColors) {\n      return calendar.darkColors;\n    }\n    return calendar.colors;\n  }\n\n  /**\n   * Get selected background color\n   */\n  getSelectedBgColor(calendarId?: string, theme?: ThemeMode): string {\n    const colors = this.resolveColors(calendarId, theme);\n    return colors.eventSelectedColor;\n  }\n\n  /**\n   * Get line color\n   */\n  getLineColor(calendarId?: string, theme?: ThemeMode): string {\n    const colors = this.resolveColors(calendarId, theme);\n    return colors.lineColor;\n  }\n\n  /**\n   * Get text color\n   */\n  getTextColor(calendarId?: string, theme?: ThemeMode): string {\n    const colors = this.resolveColors(calendarId, theme);\n    return colors.textColor;\n  }\n\n  /**\n   * Check if the current theme is dark\n   */\n  private static isDarkTheme(theme: ThemeMode): boolean {\n    if (theme === 'dark') return true;\n    if (theme === 'light') return false;\n\n    // For 'auto' mode, check system preference\n    if (typeof window !== 'undefined' && window.matchMedia) {\n      return window.matchMedia('(prefers-color-scheme: dark)').matches;\n    }\n\n    return false;\n  }\n\n  /**\n   * Validate calendar configuration\n   */\n  static validate(calendar: Partial<CalendarType>): string[] {\n    const errors: string[] = [];\n\n    if (!calendar.id) {\n      errors.push('Calendar type must have an id');\n    }\n\n    if (!calendar.name) {\n      errors.push('Calendar type must have a name');\n    }\n\n    if (calendar.colors) {\n      if (!calendar.colors.eventColor) {\n        errors.push('Calendar colors must include eventColor');\n      }\n      if (!calendar.colors.eventSelectedColor) {\n        errors.push('Calendar colors must include eventSelectedColor');\n      }\n      if (!calendar.colors.lineColor) {\n        errors.push('Calendar colors must include lineColor');\n      }\n      if (!calendar.colors.textColor) {\n        errors.push('Calendar colors must include textColor');\n      }\n    } else {\n      errors.push('Calendar type must have colors configuration');\n    }\n\n    return errors;\n  }\n}\n\n/**\n * Default registry instance for internal use\n * This is used when helper functions are called outside of CalendarApp context\n * CalendarApp will set this registry when initialized\n */\nlet defaultRegistry = new CalendarRegistry();\n\n/**\n * Get the default calendar registry\n * Used internally by helper functions for color resolution\n * @internal\n */\nexport function getDefaultCalendarRegistry(): CalendarRegistry {\n  return defaultRegistry;\n}\n\n/**\n * Set the default calendar registry\n * Used internally by CalendarApp to sync its registry with the global default\n * @internal\n */\nexport function setDefaultCalendarRegistry(registry: CalendarRegistry): void {\n  defaultRegistry = registry;\n}\n\n/**\n * Get calendar colors for a specific hex color\n * Tries to match with default calendar types, otherwise generates generic colors\n */\nexport function getCalendarColorsForHex(hex: string): {\n  colors: CalendarColors;\n  darkColors?: CalendarColors;\n} {\n  const match = DEFAULT_CALENDAR_TYPES.find(\n    c => c.colors.lineColor.toLowerCase() === hex.toLowerCase()\n  );\n  if (match) {\n    return { colors: match.colors, darkColors: match.darkColors };\n  }\n\n  return {\n    colors: {\n      eventColor: hex + '1A', // ~10% opacity\n      eventSelectedColor: hex,\n      lineColor: hex,\n      textColor: hex,\n    },\n    darkColors: {\n      eventColor: hex + 'CC', // ~80% opacity\n      eventSelectedColor: hex,\n      lineColor: hex,\n      textColor: '#ffffff',\n    },\n  };\n}\n"
  },
  {
    "path": "packages/core/src/core/config.ts",
    "content": "import { CalendarConfig } from '@/types';\nimport { getLineColor as resolveLineColor } from '@/utils/colorUtils';\n\nexport const defaultDragConfig = {\n  HOUR_HEIGHT: 72,\n  FIRST_HOUR: 0,\n  LAST_HOUR: 24,\n  MIN_DURATION: 0.25,\n  TIME_COLUMN_WIDTH: 80,\n  ALL_DAY_HEIGHT: 28,\n\n  // line color via Calendar Registry\n  getLineColor: (color: string) => resolveLineColor(color),\n\n  getDynamicPadding: (drag: { endHour: number; startHour: number }) => {\n    const duration = drag.endHour - drag.startHour;\n    return duration <= 0.25 ? 'df-p-compact' : 'df-p-standard';\n  },\n};\n\nexport const defaultViewConfigs = {\n  day: {\n    showWeekends: true,\n    showAllDay: true,\n    scrollToCurrentTime: true,\n    showEventDots: false,\n  },\n  week: {\n    showWeekends: true,\n    showAllDay: true,\n    startOfWeek: 1, // Monday\n    scrollToCurrentTime: true,\n    showEventDots: false,\n  },\n  month: {\n    showWeekends: true,\n    showAllDay: true,\n    weekHeight: 120,\n    showEventDots: false,\n    eventHeight: 16,\n  },\n  agenda: {\n    daysToShow: 14,\n    showEmptyDays: true,\n    timeFormat: '24h',\n  },\n};\n\nexport const defaultCalendarConfig: CalendarConfig = {\n  drag: defaultDragConfig,\n  views: defaultViewConfigs,\n};\n\n// Utility function to check if value is an object\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction isObject(item: any): item is Record<string, any> {\n  return item && typeof item === 'object' && !Array.isArray(item);\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function deepMerge<T extends Record<string, any>>(\n  target: T,\n  ...sources: Partial<T>[]\n): T {\n  if (!sources.length) return target;\n\n  const source = sources.shift();\n  if (!source) return target;\n\n  for (const key in source) {\n    if (source[key] !== undefined) {\n      if (isObject(target[key]) && isObject(source[key])) {\n        target[key] = deepMerge(target[key], source[key]);\n      } else {\n        target[key] = source[key] as T[Extract<keyof T, string>];\n      }\n    }\n  }\n\n  return deepMerge(target, ...sources);\n}\n\nexport function createCalendarConfig(\n  overrides?: Partial<CalendarConfig>\n): CalendarConfig {\n  return deepMerge(\n    JSON.parse(JSON.stringify(defaultCalendarConfig)), // Deep copy default configuration\n    overrides || {}\n  );\n}\n\nexport function createDragConfig(\n  overrides?: Partial<typeof defaultDragConfig>\n) {\n  return deepMerge(\n    JSON.parse(JSON.stringify(defaultDragConfig)),\n    overrides || {}\n  );\n}\n\nexport function createViewConfig(\n  viewType: 'day' | 'week' | 'month' | 'agenda',\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  overrides?: Record<string, any>\n) {\n  const defaultConfig = defaultViewConfigs[viewType];\n  return deepMerge(JSON.parse(JSON.stringify(defaultConfig)), overrides || {});\n}\n\n// Configuration validation function\nexport function validateCalendarConfig(\n  config: Partial<CalendarConfig>\n): string[] {\n  const errors: string[] = [];\n\n  // Validate drag configuration\n  if (config.drag) {\n    const { drag } = config;\n    if (\n      drag.HOUR_HEIGHT &&\n      (typeof drag.HOUR_HEIGHT !== 'number' || drag.HOUR_HEIGHT <= 0)\n    ) {\n      errors.push('HOUR_HEIGHT must be a positive number');\n    }\n    if (\n      drag.FIRST_HOUR &&\n      (typeof drag.FIRST_HOUR !== 'number' ||\n        drag.FIRST_HOUR < 0 ||\n        drag.FIRST_HOUR >= 24)\n    ) {\n      errors.push('FIRST_HOUR must be a number between 0 and 23');\n    }\n    if (\n      drag.LAST_HOUR &&\n      (typeof drag.LAST_HOUR !== 'number' ||\n        drag.LAST_HOUR <= 0 ||\n        drag.LAST_HOUR > 24)\n    ) {\n      errors.push('LAST_HOUR must be a number between 1 and 24');\n    }\n    if (\n      drag.FIRST_HOUR &&\n      drag.LAST_HOUR &&\n      drag.FIRST_HOUR >= drag.LAST_HOUR\n    ) {\n      errors.push('FIRST_HOUR must be less than LAST_HOUR');\n    }\n  }\n\n  return errors;\n}\n\nexport class ConfigManager {\n  private config: CalendarConfig;\n\n  constructor(initialConfig?: Partial<CalendarConfig>) {\n    this.config = createCalendarConfig(initialConfig);\n  }\n\n  getConfig(): CalendarConfig {\n    return JSON.parse(JSON.stringify(this.config));\n  }\n\n  getDragConfig() {\n    return this.config.drag;\n  }\n\n  getViewConfig(viewType: 'day' | 'week' | 'month' | 'agenda') {\n    return this.config.views[viewType];\n  }\n\n  updateConfig(updates: Partial<CalendarConfig>): void {\n    const errors = validateCalendarConfig(updates);\n    if (errors.length > 0) {\n      throw new Error(`Configuration validation failed: ${errors.join(', ')}`);\n    }\n    this.config = deepMerge(this.config, updates);\n  }\n\n  resetConfig(newConfig?: Partial<CalendarConfig>): void {\n    this.config = createCalendarConfig(newConfig);\n  }\n}\n"
  },
  {
    "path": "packages/core/src/core/events/EventManager.ts",
    "content": "import { CalendarRegistry } from '@/core/calendarRegistry';\nimport { CalendarStore } from '@/core/CalendarStore';\nimport {\n  CalendarAppState,\n  CalendarCallbacks,\n  Event,\n  EventChange,\n} from '@/types';\nimport { logger } from '@/utils/logger';\n\nexport class EventManager {\n  private store: CalendarStore;\n  private undoStack: Array<{ type: string; data: unknown }> = [];\n  private pendingSnapshot: Event[] | null = null;\n  private pendingChangeSource: 'drag' | 'resize' | null = null;\n  private externalEvents: Map<string, Event[]> = new Map();\n  private readonly MAX_UNDO_STACK = 50;\n\n  constructor(\n    private state: CalendarAppState,\n    private registry: CalendarRegistry,\n    private getCallbacks: () => CalendarCallbacks,\n    private notify: () => void,\n    private triggerRender: () => void,\n    initialEvents: Event[]\n  ) {\n    const normalizedInitialEvents = initialEvents.map(event =>\n      this.normalizeEvent(event)\n    );\n    this.state.events = normalizedInitialEvents;\n    this.store = new CalendarStore(normalizedInitialEvents);\n    this.setupStoreListeners();\n  }\n\n  private normalizeEvent(event: Event): Event {\n    if (event.calendarId || (event.calendarIds?.length ?? 0) > 0) {\n      return event;\n    }\n\n    const fallbackCalendarId = this.registry.getDefaultCalendar()?.id;\n    if (!fallbackCalendarId) {\n      return event;\n    }\n\n    return {\n      ...event,\n      calendarId: fallbackCalendarId,\n    };\n  }\n\n  private normalizeEventUpdate(\n    existingEvent: Event,\n    updates: Partial<Event>\n  ): Partial<Event> {\n    return this.normalizeEvent({\n      ...existingEvent,\n      ...updates,\n    });\n  }\n\n  private setupStoreListeners(): void {\n    this.store.onEventChange = (change: EventChange) => {\n      this.syncExternalEventsToState();\n\n      let callbackPromise = null;\n      if (change.type === 'create') {\n        callbackPromise = this.getCallbacks().onEventCreate?.(change.event);\n      } else if (change.type === 'update') {\n        callbackPromise = this.getCallbacks().onEventUpdate?.(change.after);\n      }\n\n      this.triggerRender();\n      this.notify();\n      return callbackPromise ?? undefined;\n    };\n\n    this.store.onEventBatchChange = (_changes: EventChange[]) => {\n      this.syncExternalEventsToState();\n\n      let callbackPromise = null;\n      if (\n        this.pendingChangeSource !== 'drag' &&\n        this.pendingChangeSource !== 'resize'\n      ) {\n        callbackPromise = this.getCallbacks().onEventBatchChange?.(_changes);\n      }\n      this.pendingChangeSource = null;\n\n      this.triggerRender();\n      this.notify();\n      return callbackPromise ?? undefined;\n    };\n  }\n\n  /** Expose the store for operations that need direct store access (e.g. mergeCalendars). */\n  getStore(): CalendarStore {\n    return this.store;\n  }\n\n  pushToUndo(eventsSnapshot?: Event[]): void {\n    this.undoStack.push({\n      type: 'events_snapshot',\n      data: eventsSnapshot || [...this.state.events],\n    });\n    if (this.undoStack.length > this.MAX_UNDO_STACK) {\n      this.undoStack.shift();\n    }\n  }\n\n  undo(): void {\n    if (this.undoStack.length === 0) return;\n\n    const lastState = this.undoStack.pop();\n    if (lastState?.type === 'events_snapshot') {\n      this.state.events = lastState.data as Event[];\n      this.store = new CalendarStore(this.state.events);\n      this.setupStoreListeners();\n      this.triggerRender();\n      this.notify();\n    }\n  }\n\n  applyEventsChanges(\n    changes: {\n      add?: Event[];\n      update?: Array<{ id: string; updates: Partial<Event> }>;\n      delete?: string[];\n    },\n    isPending?: boolean,\n    source?: 'drag' | 'resize'\n  ): void {\n    if (isPending) {\n      if (!this.pendingSnapshot) {\n        this.pendingSnapshot = [...this.state.events];\n      }\n    } else if (this.pendingSnapshot) {\n      this.pushToUndo(this.pendingSnapshot);\n      this.pendingSnapshot = null;\n    } else {\n      this.pushToUndo();\n    }\n\n    if (isPending) {\n      let newEvents = [...this.state.events];\n\n      if (changes.delete) {\n        const deleteIds = new Set(changes.delete);\n        newEvents = newEvents.filter(e => !deleteIds.has(e.id));\n      }\n      if (changes.update) {\n        changes.update.forEach(({ id, updates }) => {\n          const index = newEvents.findIndex(e => e.id === id);\n          if (index !== -1) {\n            newEvents[index] = this.normalizeEvent({\n              ...newEvents[index],\n              ...updates,\n            });\n          }\n        });\n      }\n      if (changes.add) {\n        newEvents = [\n          ...newEvents,\n          ...changes.add.map(event => this.normalizeEvent(event)),\n        ];\n      }\n\n      this.state.events = newEvents;\n      this.notify();\n      return;\n    }\n\n    if (source) {\n      this.pendingChangeSource = source;\n    }\n    this.store.beginTransaction();\n\n    if (changes.delete) {\n      changes.delete.forEach(id => this.store.deleteEvent(id));\n    }\n    if (changes.update) {\n      changes.update.forEach(({ id, updates }) => {\n        try {\n          const existingEvent = this.store.getEvent(id);\n          if (!existingEvent) {\n            throw new Error(`Event with id ${id} not found`);\n          }\n          this.store.updateEvent(\n            id,\n            this.normalizeEventUpdate(existingEvent, updates)\n          );\n        } catch (e) {\n          logger.warn(`Failed to update event ${id}:`, e);\n        }\n      });\n    }\n    if (changes.add) {\n      changes.add.forEach(event => {\n        try {\n          this.store.createEvent(this.normalizeEvent(event));\n        } catch (e) {\n          logger.warn(`Failed to create event ${event.id}:`, e);\n        }\n      });\n    }\n\n    this.store.endTransaction();\n  }\n\n  addEvent(event: Event): void {\n    this.pendingSnapshot = null;\n    this.pushToUndo();\n    this.store.createEvent(this.normalizeEvent(event));\n  }\n\n  addExternalEvents(calendarId: string, events: Event[]): void {\n    const eventsWithCalendarId = events.map(event => ({\n      ...event,\n      calendarId,\n    }));\n    this.externalEvents.set(calendarId, eventsWithCalendarId);\n    this.syncExternalEventsToState();\n    this.notify();\n  }\n\n  private syncExternalEventsToState(): void {\n    const coreEvents = this.store.getAllEvents();\n    const eventsById = new Map<string, Event>();\n\n    coreEvents.forEach(event => eventsById.set(event.id, event));\n\n    if (this.externalEvents.size > 0) {\n      for (const events of this.externalEvents.values()) {\n        events.forEach(event => eventsById.set(event.id, event));\n      }\n    }\n\n    this.state.events = Array.from(eventsById.values());\n  }\n\n  async updateEvent(\n    id: string,\n    eventUpdate: Partial<Event>,\n    isPending?: boolean,\n    source?: 'drag' | 'resize'\n  ): Promise<void> {\n    if (source) {\n      this.pendingChangeSource = source;\n    }\n\n    if (isPending) {\n      if (!this.pendingSnapshot) {\n        this.pendingSnapshot = [...this.state.events];\n      }\n    } else if (this.pendingSnapshot) {\n      this.pushToUndo(this.pendingSnapshot);\n      this.pendingSnapshot = null;\n    } else {\n      this.pushToUndo();\n    }\n\n    if (isPending) {\n      const eventIndex = this.state.events.findIndex(e => e.id === id);\n      if (eventIndex === -1) throw new Error(`Event with id ${id} not found`);\n\n      const updatedEvent = this.normalizeEvent({\n        ...this.state.events[eventIndex],\n        ...eventUpdate,\n      });\n      this.state.events = [\n        ...this.state.events.slice(0, eventIndex),\n        updatedEvent,\n        ...this.state.events.slice(eventIndex + 1),\n      ];\n      this.notify();\n      return;\n    }\n\n    try {\n      const existingEvent = this.store.getEvent(id);\n      if (!existingEvent) {\n        throw new Error(`Event with id ${id} not found`);\n      }\n\n      await this.store.updateEvent(\n        id,\n        this.normalizeEventUpdate(existingEvent, eventUpdate)\n      );\n    } finally {\n      if (source && this.pendingChangeSource === source) {\n        this.pendingChangeSource = null;\n      }\n    }\n  }\n\n  async deleteEvent(id: string): Promise<void> {\n    await this.getCallbacks().onEventDelete?.(id);\n    this.pendingSnapshot = null;\n    this.pushToUndo();\n    await this.store.deleteEvent(id);\n  }\n\n  getAllEvents(): Event[] {\n    return [...this.state.events];\n  }\n\n  getEvents(): Event[] {\n    const allEvents = this.state.events || [];\n    const visibleCalendars = new Set(\n      this.registry\n        .getAll()\n        .filter(calendar => calendar.isVisible !== false)\n        .map(calendar => calendar.id)\n    );\n    return allEvents.filter(event => {\n      const ids =\n        event.calendarIds ?? (event.calendarId ? [event.calendarId] : []);\n      if (ids.length === 0) return false;\n      return ids.some(id => visibleCalendars.has(id));\n    });\n  }\n\n  onEventClick(event: Event): void {\n    this.getCallbacks().onEventClick?.(event);\n  }\n\n  onEventDoubleClick(\n    event: Event,\n    e: MouseEvent\n  ): boolean | undefined | Promise<boolean | undefined> {\n    return this.getCallbacks().onEventDoubleClick?.(event, e);\n  }\n\n  onMoreEventsClick(date: Date): void {\n    this.getCallbacks().onMoreEventsClick?.(date);\n  }\n\n  onEventDetailToggle(eventId: string | null): void {\n    this.getCallbacks().onEventDetailToggle?.(eventId);\n    this.notify();\n  }\n\n  onMobileEventDetailToggle(event: Event | null): void {\n    this.getCallbacks().onMobileEventDetailToggle?.(event);\n    this.notify();\n  }\n\n  highlightEvent(eventId: string | null): void {\n    if (this.state.highlightedEventId === eventId) return;\n    this.state.highlightedEventId = eventId;\n    this.getCallbacks().onRender?.();\n    this.notify();\n  }\n\n  selectEvent(eventId: string | null): void {\n    if (this.state.selectedEventId === eventId) return;\n    this.state.selectedEventId = eventId;\n    this.getCallbacks().onRender?.();\n    this.notify();\n  }\n\n  dismissUI(): void {\n    this.getCallbacks().onDismissUI?.();\n    this.notify();\n  }\n}\n"
  },
  {
    "path": "packages/core/src/core/index.ts",
    "content": "// Core module export file\nexport * from './useCalendarApp';\nexport * from './config';\nexport * from '@/renderer/CalendarRoot';\n\nexport { CalendarApp } from './CalendarApp';\n\n// Re-export types from @/types for convenience\nexport { ViewType } from '@/types';\n\nexport type {\n  CalendarPlugin,\n  CalendarView,\n  CalendarCallbacks,\n  CalendarAppConfig,\n  CalendarAppState,\n  UseCalendarAppReturn,\n  CalendarConfig,\n} from '@/types';\n"
  },
  {
    "path": "packages/core/src/core/navigation/NavigationController.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport {\n  CalendarAppState,\n  CalendarCallbacks,\n  CalendarView,\n  CalendarViewType,\n  RangeChangeReason,\n  ViewType,\n} from '@/types';\nimport { getWeekRange } from '@/utils/dateRangeUtils';\n\nexport class NavigationController {\n  private visibleMonth: Date;\n\n  private static getAgendaPageDays(view?: CalendarView): number {\n    const pageDays = Number(view?.config?.daysToShow);\n    return Number.isFinite(pageDays) && pageDays > 0\n      ? Math.floor(pageDays)\n      : 14;\n  }\n\n  constructor(\n    private state: CalendarAppState,\n    private getCallbacks: () => CalendarCallbacks,\n    private notify: () => void,\n    initialDate: Date\n  ) {\n    this.visibleMonth = new Date(\n      initialDate.getFullYear(),\n      initialDate.getMonth(),\n      1\n    );\n  }\n\n  changeView(view: CalendarViewType): void {\n    if (!this.state.views.has(view)) {\n      throw new Error(`View ${view} is not registered`);\n    }\n    this.state.currentView = view;\n    this.state.highlightedEventId = null;\n    this.getCallbacks().onViewChange?.(view);\n    this.handleVisibleRangeChange('viewChange');\n    this.notify();\n  }\n\n  getCurrentView(): CalendarView {\n    const view = this.state.views.get(this.state.currentView);\n    if (!view) {\n      throw new Error(\n        `Current view ${this.state.currentView} is not registered`\n      );\n    }\n    return view;\n  }\n\n  emitVisibleRange(\n    start: Date,\n    end: Date,\n    reason: RangeChangeReason = 'navigation'\n  ): void {\n    this.getCallbacks().onVisibleRangeChange?.(\n      new Date(start),\n      new Date(end),\n      reason\n    );\n  }\n\n  handleVisibleRangeChange(reason: RangeChangeReason): void {\n    const view = this.state.views.get(this.state.currentView);\n    switch (view?.type) {\n      case ViewType.DAY: {\n        const start = new Date(this.state.currentDate);\n        start.setHours(0, 0, 0, 0);\n        const end = new Date(start);\n        end.setDate(end.getDate() + 1);\n        this.emitVisibleRange(start, end, reason);\n        break;\n      }\n      case ViewType.WEEK: {\n        const startOfWeek = (view?.config?.startOfWeek as number) ?? 1;\n        const { monday } = getWeekRange(this.state.currentDate, startOfWeek);\n        const start = new Date(monday);\n        const end = new Date(monday);\n        end.setDate(end.getDate() + 7);\n        this.emitVisibleRange(start, end, reason);\n        break;\n      }\n      case ViewType.MONTH: {\n        if (reason === 'navigation') {\n          // MonthView emits its own range based on virtual scroll position\n          break;\n        }\n        const firstDayOfMonth = new Date(\n          this.state.currentDate.getFullYear(),\n          this.state.currentDate.getMonth(),\n          1\n        );\n        const startOfWeek = (view?.config?.startOfWeek as number) ?? 1;\n        const { monday } = getWeekRange(firstDayOfMonth, startOfWeek);\n        const start = new Date(monday);\n        const end = new Date(monday);\n        end.setDate(end.getDate() + 42);\n        this.emitVisibleRange(start, end, reason);\n        break;\n      }\n      case ViewType.AGENDA: {\n        const start = new Date(this.state.currentDate);\n        start.setHours(0, 0, 0, 0);\n        const end = new Date(start);\n        end.setDate(\n          end.getDate() + NavigationController.getAgendaPageDays(view)\n        );\n        this.emitVisibleRange(start, end, reason);\n        break;\n      }\n      case ViewType.YEAR: {\n        const start = new Date(this.state.currentDate.getFullYear(), 0, 1);\n        start.setHours(0, 0, 0, 0);\n        const end = new Date(this.state.currentDate.getFullYear(), 11, 31);\n        end.setDate(end.getDate() + 1);\n        this.emitVisibleRange(start, end, reason);\n        break;\n      }\n      default:\n        break;\n    }\n  }\n\n  setCurrentDate(date: Date): void {\n    this.state.currentDate = new Date(date);\n    this.getCallbacks().onDateChange?.(this.state.currentDate);\n    this.setVisibleMonth(this.state.currentDate);\n    this.handleVisibleRangeChange('navigation');\n    this.notify();\n  }\n\n  getCurrentDate(): Date {\n    return new Date(this.state.currentDate);\n  }\n\n  setVisibleMonth(date: Date): void {\n    const next = new Date(date.getFullYear(), date.getMonth(), 1);\n    if (\n      this.visibleMonth.getFullYear() === next.getFullYear() &&\n      this.visibleMonth.getMonth() === next.getMonth()\n    ) {\n      return;\n    }\n    this.visibleMonth = next;\n    this.notify();\n  }\n\n  getVisibleMonth(): Date {\n    return new Date(this.visibleMonth);\n  }\n\n  goToToday(): void {\n    const todayInTz = Temporal.Now.plainDateISO(this.state.timeZone);\n    const today = new Date(todayInTz.year, todayInTz.month - 1, todayInTz.day);\n    this.setCurrentDate(today);\n  }\n\n  goToPrevious(): void {\n    const newDate = new Date(this.state.currentDate);\n    switch (this.state.currentView) {\n      case ViewType.DAY:\n        newDate.setDate(newDate.getDate() - 1);\n        break;\n      case ViewType.WEEK:\n        newDate.setDate(newDate.getDate() - 7);\n        break;\n      case ViewType.MONTH:\n        newDate.setMonth(newDate.getMonth() - 1);\n        break;\n      case ViewType.AGENDA: {\n        const view = this.state.views.get(this.state.currentView);\n        newDate.setDate(\n          newDate.getDate() - NavigationController.getAgendaPageDays(view)\n        );\n        break;\n      }\n      case ViewType.YEAR:\n        newDate.setFullYear(newDate.getFullYear() - 1);\n        break;\n      default:\n        break;\n    }\n    this.setCurrentDate(newDate);\n  }\n\n  goToNext(): void {\n    const newDate = new Date(this.state.currentDate);\n    switch (this.state.currentView) {\n      case ViewType.DAY:\n        newDate.setDate(newDate.getDate() + 1);\n        break;\n      case ViewType.WEEK:\n        newDate.setDate(newDate.getDate() + 7);\n        break;\n      case ViewType.MONTH:\n        newDate.setMonth(newDate.getMonth() + 1);\n        break;\n      case ViewType.AGENDA: {\n        const view = this.state.views.get(this.state.currentView);\n        newDate.setDate(\n          newDate.getDate() + NavigationController.getAgendaPageDays(view)\n        );\n        break;\n      }\n      case ViewType.YEAR:\n        newDate.setFullYear(newDate.getFullYear() + 1);\n        break;\n      default:\n        break;\n    }\n    this.setCurrentDate(newDate);\n  }\n\n  selectDate(date: Date): void {\n    this.setCurrentDate(date);\n    this.getCallbacks().onDateChange?.(new Date(date));\n  }\n}\n"
  },
  {
    "path": "packages/core/src/core/permissions/CalendarPermissions.ts",
    "content": "import { CalendarRegistry } from '@/core/calendarRegistry';\nimport { Event, ReadOnlyConfig } from '@/types';\n\n/**\n * Compute the effective draggable/viewable flags for a given event or calendar ID,\n * taking into account global readOnly config, per-calendar readOnly, and subscription status.\n */\nexport function getReadOnlyConfig(\n  readOnly: boolean | ReadOnlyConfig,\n  id: string | undefined,\n  registry: CalendarRegistry,\n  events: Event[]\n): ReadOnlyConfig {\n  let draggable = true;\n  let viewable = true;\n\n  if (readOnly === true) {\n    draggable = false;\n    viewable = false;\n  } else if (readOnly !== false) {\n    draggable = (readOnly as ReadOnlyConfig).draggable ?? true;\n    viewable = (readOnly as ReadOnlyConfig).viewable ?? true;\n  }\n\n  if (id && (draggable || viewable)) {\n    let calendarId = id;\n    const event = events.find(e => e.id === id);\n    if (event?.calendarId) calendarId = event.calendarId;\n\n    const calendar = registry.get(calendarId);\n    if (calendar) {\n      if (calendar.readOnly === true) {\n        draggable = false;\n      }\n      if (calendar.subscription && calendar.readOnly === undefined) {\n        draggable = false;\n      }\n    }\n  }\n\n  return { draggable, viewable };\n}\n\n/**\n * Returns true if the given event or calendar ID allows UI mutations\n * (drag, resize, edit). Respects global readOnly, per-calendar readOnly,\n * and subscription status.\n */\nexport function canMutateFromUI(\n  readOnly: boolean | ReadOnlyConfig,\n  id: string | undefined,\n  registry: CalendarRegistry,\n  events: Event[]\n): boolean {\n  if (readOnly !== false) return false;\n\n  if (id) {\n    let calendarId = id;\n    const event = events.find(e => e.id === id);\n    if (event?.calendarId) calendarId = event.calendarId;\n\n    const calendar = registry.get(calendarId);\n    if (calendar) {\n      if (calendar.readOnly !== undefined) return !calendar.readOnly;\n      if (calendar.subscription) return false;\n    }\n  }\n\n  return true;\n}\n"
  },
  {
    "path": "packages/core/src/core/plugins/PluginManager.ts",
    "content": "import { CalendarAppState, CalendarPlugin, ICalendarApp } from '@/types';\nimport { logger } from '@/utils/logger';\n\nexport class PluginManager {\n  constructor(\n    private state: CalendarAppState,\n    private notify: () => void\n  ) {}\n\n  install(plugin: CalendarPlugin, app: ICalendarApp): void {\n    if (this.state.plugins.has(plugin.name)) {\n      logger.warn(`Plugin ${plugin.name} is already installed`);\n      return;\n    }\n    this.state.plugins.set(plugin.name, plugin);\n    plugin.install(app);\n  }\n\n  getPlugin<T = unknown>(name: string): T | undefined {\n    const plugin = this.state.plugins.get(name);\n    return plugin?.api as T;\n  }\n\n  hasPlugin(name: string): boolean {\n    return this.state.plugins.has(name);\n  }\n\n  getPluginConfig(pluginName: string): Record<string, unknown> {\n    const plugin = this.state.plugins.get(pluginName);\n    return plugin?.config || {};\n  }\n\n  updatePluginConfig(\n    pluginName: string,\n    config: Record<string, unknown>\n  ): void {\n    const plugin = this.state.plugins.get(pluginName);\n    if (plugin) {\n      plugin.config = { ...plugin.config, ...config };\n      this.notify();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/core/useCalendarApp.ts",
    "content": "import {\n  useState,\n  useCallback,\n  useMemo,\n  useEffect,\n  useRef,\n} from 'preact/hooks';\n\nimport {\n  CalendarAppConfig,\n  CalendarViewType,\n  UseCalendarAppReturn,\n  CalendarType,\n  RangeChangeReason,\n  Event,\n} from '@/types';\nimport {\n  createConfigSyncSnapshot,\n  createNormalizedCalendarAppConfigGetter,\n  syncCalendarAppConfig,\n} from '@/utils/calendarApp';\n\nimport { CalendarApp } from './CalendarApp';\n\nexport function useCalendarApp(\n  config: CalendarAppConfig\n): UseCalendarAppReturn {\n  const configRef = useRef(config);\n  configRef.current = config;\n  const getNormalizedConfig = useMemo(\n    () => createNormalizedCalendarAppConfigGetter(() => configRef.current),\n    []\n  );\n  const normalizedConfig = useMemo(\n    () => getNormalizedConfig(),\n    [config, getNormalizedConfig]\n  );\n\n  // Create calendar application instance\n  const app = useMemo(() => new CalendarApp(normalizedConfig), []);\n\n  // Reactive state - synchronize state from app instance\n  const [currentView, setCurrentView] = useState<CalendarViewType>(\n    app.state.currentView\n  );\n  const [currentDate, setCurrentDateState] = useState<Date>(\n    app.state.currentDate\n  );\n  const [events, setEvents] = useState<Event[]>(app.getEvents());\n  // Component re-render trigger\n  const [, forceUpdate] = useState({});\n  const updateTimerRef = useRef<number | null>(null);\n\n  const triggerUpdate = useCallback(() => {\n    if (updateTimerRef.current !== null) return;\n    updateTimerRef.current = requestAnimationFrame(() => {\n      forceUpdate({});\n      updateTimerRef.current = null;\n    });\n  }, []);\n\n  useEffect(\n    () => () => {\n      if (updateTimerRef.current !== null) {\n        cancelAnimationFrame(updateTimerRef.current);\n      }\n    },\n    []\n  );\n\n  // Synchronize state changes\n  useEffect(() => {\n    const originalChangeView = app.changeView;\n    app.changeView = (view: CalendarViewType) => {\n      originalChangeView(view);\n      setCurrentView(view);\n    };\n\n    const originalSetCurrentDate = app.setCurrentDate;\n    app.setCurrentDate = (date: Date) => {\n      originalSetCurrentDate(date);\n      setCurrentDateState(new Date(date));\n    };\n\n    const originalAddEvent = app.addEvent;\n    app.addEvent = (event: Event) => {\n      originalAddEvent(event);\n      setEvents([...app.getEvents()]);\n    };\n\n    const originalAddExternalEvents = app.addExternalEvents;\n    app.addExternalEvents = (calendarId: string, newEvents: Event[]) => {\n      originalAddExternalEvents(calendarId, newEvents);\n      setEvents([...app.getEvents()]);\n    };\n\n    const originalUpdateEvent = app.updateEvent;\n    app.updateEvent = (\n      id: string,\n      eventUpdate: Partial<Event>,\n      isPending?: boolean,\n      source?: 'drag' | 'resize'\n    ) => {\n      const result = originalUpdateEvent(id, eventUpdate, isPending, source);\n      setEvents([...app.getEvents()]);\n      return result;\n    };\n\n    const originalDeleteEvent = app.deleteEvent;\n    app.deleteEvent = (id: string) => {\n      const result = originalDeleteEvent(id);\n      setEvents([...app.getEvents()]);\n      return result;\n    };\n\n    const originalSetCalendarVisibility = app.setCalendarVisibility;\n    app.setCalendarVisibility = (calendarId: string, visible: boolean) => {\n      originalSetCalendarVisibility(calendarId, visible);\n      setEvents([...app.getEvents()]);\n    };\n\n    const originalSetAllCalendarsVisibility = app.setAllCalendarsVisibility;\n    app.setAllCalendarsVisibility = (visible: boolean) => {\n      originalSetAllCalendarsVisibility(visible);\n      setEvents([...app.getEvents()]);\n    };\n\n    const originalSetVisibleMonth = app.setVisibleMonth;\n    app.setVisibleMonth = (date: Date) => {\n      originalSetVisibleMonth(date);\n    };\n\n    const originalReorderCalendars = app.reorderCalendars;\n    app.reorderCalendars = (fromIndex: number, toIndex: number) => {\n      originalReorderCalendars(fromIndex, toIndex);\n    };\n\n    const originalUpdateCalendar = app.updateCalendar;\n    app.updateCalendar = (\n      id: string,\n      updates: Partial<CalendarType>,\n      isPending?: boolean\n    ) => {\n      originalUpdateCalendar(id, updates, isPending);\n    };\n\n    const originalCreateCalendar = app.createCalendar;\n    app.createCalendar = (calendar: CalendarType) => {\n      const result = originalCreateCalendar(calendar);\n      setEvents([...app.getEvents()]);\n      return result;\n    };\n\n    const originalDeleteCalendar = app.deleteCalendar;\n    app.deleteCalendar = (id: string) => {\n      const result = originalDeleteCalendar(id);\n      setEvents([...app.getEvents()]);\n      return result;\n    };\n\n    const originalMergeCalendars = app.mergeCalendars;\n    app.mergeCalendars = (sourceId: string, targetId: string) => {\n      const result = originalMergeCalendars(sourceId, targetId);\n      setEvents([...app.getEvents()]);\n      return result;\n    };\n\n    const originalHighlightEvent = app.highlightEvent;\n    app.highlightEvent = (eventId: string | null) => {\n      originalHighlightEvent(eventId);\n    };\n\n    const originalUndo = app.undo;\n    app.undo = () => {\n      originalUndo();\n      setEvents([...app.getEvents()]);\n    };\n\n    return () => {\n      // Cleanup work, if needed\n    };\n  }, [app]);\n\n  // Synchronize state on initialization\n  useEffect(() => {\n    setCurrentView(app.state.currentView);\n    setCurrentDateState(app.state.currentDate);\n    setEvents(app.getEvents());\n  }, [app]);\n\n  // Synchronize configuration updates\n  const syncSnapshotRef = useRef(createConfigSyncSnapshot(normalizedConfig));\n  useEffect(() => {\n    syncSnapshotRef.current = syncCalendarAppConfig(\n      app,\n      syncSnapshotRef.current,\n      normalizedConfig\n    );\n  }, [app, normalizedConfig]);\n\n  // Wrapped methods to ensure state synchronization\n  const changeView = useCallback(\n    (view: CalendarViewType) => {\n      app.changeView(view);\n      triggerUpdate();\n    },\n    [app, triggerUpdate]\n  );\n\n  const setCurrentDate = useCallback(\n    (date: Date) => {\n      app.setCurrentDate(date);\n      triggerUpdate();\n    },\n    [app, triggerUpdate]\n  );\n\n  const addEvent = useCallback(\n    (event: Event) => {\n      app.addEvent(event);\n      triggerUpdate();\n    },\n    [app, triggerUpdate]\n  );\n\n  const applyEventsChanges = useCallback(\n    (\n      changes: {\n        add?: Event[];\n        update?: Array<{ id: string; updates: Partial<Event> }>;\n        delete?: string[];\n      },\n      isPending?: boolean\n    ) => {\n      app.applyEventsChanges(changes, isPending);\n      triggerUpdate();\n    },\n    [app, triggerUpdate]\n  );\n\n  const updateEvent = useCallback(\n    (\n      id: string,\n      event: Partial<Event>,\n      isPending?: boolean,\n      source?: 'drag' | 'resize'\n    ) => {\n      const result = app.updateEvent(id, event, isPending, source);\n      triggerUpdate();\n      return result;\n    },\n    [app, triggerUpdate]\n  );\n\n  const deleteEvent = useCallback(\n    (id: string) => {\n      const result = app.deleteEvent(id);\n      triggerUpdate();\n      return result;\n    },\n    [app, triggerUpdate]\n  );\n\n  const undo = useCallback(() => {\n    app.undo();\n    triggerUpdate();\n  }, [app, triggerUpdate]);\n\n  // Navigation methods\n  const goToToday = useCallback(() => {\n    app.goToToday();\n    triggerUpdate();\n  }, [app, triggerUpdate]);\n\n  const goToPrevious = useCallback(() => {\n    app.goToPrevious();\n    triggerUpdate();\n  }, [app, triggerUpdate]);\n\n  const goToNext = useCallback(() => {\n    app.goToNext();\n    triggerUpdate();\n  }, [app, triggerUpdate]);\n\n  const selectDate = useCallback(\n    (date: Date) => {\n      app.selectDate(date);\n      triggerUpdate();\n    },\n    [app, triggerUpdate]\n  );\n\n  const setCalendarVisibility = useCallback(\n    (calendarId: string, visible: boolean) => {\n      app.setCalendarVisibility(calendarId, visible);\n      triggerUpdate();\n    },\n    [app, triggerUpdate]\n  );\n\n  const setAllCalendarsVisibility = useCallback(\n    (visible: boolean) => {\n      app.setAllCalendarsVisibility(visible);\n      triggerUpdate();\n    },\n    [app, triggerUpdate]\n  );\n\n  const emitVisibleRange = useCallback(\n    (start: Date, end: Date, reason?: RangeChangeReason) => {\n      app.emitVisibleRange(start, end, reason);\n    },\n    [app]\n  );\n\n  return {\n    app,\n    currentView,\n    currentDate,\n    events,\n    applyEventsChanges,\n    changeView,\n    setCurrentDate,\n    addEvent,\n    updateEvent,\n    deleteEvent,\n    goToToday,\n    goToPrevious,\n    goToNext,\n    selectDate,\n    undo,\n    getCalendars: () => app.getCalendars(),\n    createCalendar: (calendar: CalendarType) => app.createCalendar(calendar),\n    mergeCalendars: (sourceId: string, targetId: string) =>\n      app.mergeCalendars(sourceId, targetId),\n    setCalendarVisibility,\n    setAllCalendarsVisibility,\n    getAllEvents: () => app.getAllEvents(),\n    highlightEvent: (eventId: string | null) => app.highlightEvent(eventId),\n    setVisibleMonth: (date: Date) => app.setVisibleMonth(date),\n    getVisibleMonth: () => app.getVisibleMonth(),\n    emitVisibleRange,\n    canMutateFromUI: () => app.canMutateFromUI(),\n    readOnlyConfig: app.getReadOnlyConfig(),\n  };\n}\n"
  },
  {
    "path": "packages/core/src/factories/ViewAdapter.tsx",
    "content": "import { useMemo, useCallback } from 'preact/hooks';\n\nimport {\n  CalendarViewType,\n  ViewType,\n  ViewAdapterProps,\n  BaseViewProps,\n  EventsService,\n  DragService,\n  Event,\n} from '@/types';\n\nexport const ViewAdapter = ({\n  originalComponent: OriginalComponent,\n  app,\n  viewType,\n  config,\n  useEventDetailPanel,\n  calendarRef,\n  switcherMode,\n  meta,\n  selectedEventId,\n  detailPanelEventId,\n  onEventSelect,\n  onDateChange,\n  onDetailPanelToggle,\n}: ViewAdapterProps) => {\n  // Get plugin services\n  const eventsService = app.getPlugin<EventsService>('events');\n  const dragService = app.getPlugin<DragService>('drag');\n\n  // Basic state\n  const currentDate = app.getCurrentDate();\n  const currentView = app.state.currentView;\n  const events = app.getEvents();\n\n  // Event handlers\n  const handleEventUpdate = useCallback(\n    (event: Event) => {\n      if (eventsService) {\n        return eventsService.update(event.id, event);\n      }\n      return app.updateEvent(event.id, event);\n    },\n    [eventsService, app]\n  );\n\n  const handleEventDelete = useCallback(\n    (eventId: string) => {\n      if (eventsService) {\n        return eventsService.delete(eventId);\n      }\n      return app.deleteEvent(eventId);\n    },\n    [eventsService, app]\n  );\n\n  const handleEventCreate = useCallback(\n    (event: Event) => {\n      if (eventsService) {\n        eventsService.add(event);\n      } else {\n        app.addEvent(event);\n      }\n    },\n    [eventsService, app]\n  );\n\n  const handleDateChange = useCallback(\n    (date: Date) => {\n      if (onDateChange) {\n        onDateChange(date);\n      } else {\n        app.setCurrentDate(date);\n      }\n    },\n    [app, onDateChange]\n  );\n\n  const handleViewChange = useCallback(\n    (view: CalendarViewType) => {\n      app.changeView(view);\n    },\n    [app]\n  );\n\n  // Merge configuration\n  const mergedConfig = useMemo(() => {\n    const viewConfig = app.getViewConfig(viewType);\n    return { ...viewConfig, ...config };\n  }, [config, app.state.views, viewType]);\n\n  // Prepare props to pass to original component\n  const viewProps: BaseViewProps = useMemo(\n    () => ({\n      app,\n      currentDate,\n      currentView,\n      events,\n      onEventUpdate: handleEventUpdate,\n      onEventDelete: handleEventDelete,\n      onEventCreate: handleEventCreate,\n      onDateChange: handleDateChange,\n      onViewChange: handleViewChange,\n      config: mergedConfig,\n      useEventDetailPanel,\n      calendarRef,\n      switcherMode,\n      meta,\n      selectedEventId,\n      detailPanelEventId,\n      onEventSelect,\n      onDetailPanelToggle,\n    }),\n    [\n      app,\n      currentDate,\n      currentView,\n      events,\n      handleEventUpdate,\n      handleEventDelete,\n      handleEventCreate,\n      handleDateChange,\n      handleViewChange,\n      mergedConfig,\n      useEventDetailPanel,\n      calendarRef,\n      switcherMode,\n      meta,\n      selectedEventId,\n      detailPanelEventId,\n      onEventSelect,\n      onDetailPanelToggle,\n    ]\n  );\n\n  // Special handling: prepare compatible props for existing components\n  const compatProps = useMemo(() => {\n    // Create a calendar object compatible with the existing API\n    const calendarCompat = {\n      currentDate,\n      events,\n      setEvents: (newEvents: Event[]) => {\n        // Clear existing events and add new events\n        events.forEach(event => handleEventDelete(event.id));\n        newEvents.forEach(event => handleEventCreate(event));\n      },\n      updateEvent: handleEventUpdate,\n      deleteEvent: handleEventDelete,\n      addEvent: handleEventCreate,\n      goToPrevious: () => {\n        const newDate = new Date(currentDate);\n        switch (currentView) {\n          case ViewType.DAY:\n            newDate.setDate(newDate.getDate() - 1);\n            break;\n          case ViewType.WEEK:\n            newDate.setDate(newDate.getDate() - 7);\n            break;\n          case ViewType.MONTH:\n            newDate.setMonth(newDate.getMonth() - 1);\n            break;\n          default:\n            break;\n        }\n        handleDateChange(newDate);\n      },\n      goToNext: () => {\n        const newDate = new Date(currentDate);\n        switch (currentView) {\n          case ViewType.DAY:\n            newDate.setDate(newDate.getDate() + 1);\n            break;\n          case ViewType.WEEK:\n            newDate.setDate(newDate.getDate() + 7);\n            break;\n          case ViewType.MONTH:\n            newDate.setMonth(newDate.getMonth() + 1);\n            break;\n          default:\n            break;\n        }\n        handleDateChange(newDate);\n      },\n      goToToday: () => {\n        handleDateChange(new Date());\n      },\n      changeView: handleViewChange,\n      selectDate: handleDateChange,\n    };\n\n    return {\n      calendar: calendarCompat,\n      ...viewProps,\n      // Pass plugin services (if original component needs them)\n      eventsService,\n      dragService,\n    };\n  }, [\n    currentDate,\n    events,\n    currentView,\n    handleEventUpdate,\n    handleEventDelete,\n    handleEventCreate,\n    handleDateChange,\n    handleViewChange,\n    viewProps,\n    eventsService,\n    dragService,\n  ]);\n\n  return <OriginalComponent {...compatProps} />;\n};\n\nexport default ViewAdapter;\n"
  },
  {
    "path": "packages/core/src/factories/createAgendaView.ts",
    "content": "// Factory function for creating Agenda view\nimport { h } from 'preact';\n\nimport {\n  AgendaViewConfig,\n  AgendaViewProps,\n  ViewAdapterProps,\n  ViewFactory,\n  ViewType,\n} from '@/types';\nimport AgendaView from '@/views/AgendaView';\n\nimport { ViewAdapter } from './ViewAdapter';\n\nconst defaultAgendaViewConfig: AgendaViewConfig = {\n  daysToShow: 14,\n  showEmptyDays: true,\n  timeFormat: '24h',\n};\n\nconst AgendaViewAdapter = (props: AgendaViewProps) =>\n  h(ViewAdapter, {\n    ...(props as ViewAdapterProps),\n    viewType: ViewType.AGENDA,\n    originalComponent: AgendaView,\n    className: 'df-agenda-view-factory',\n  });\n\nAgendaViewAdapter.displayName = 'AgendaViewAdapter';\n\nexport const createAgendaView: ViewFactory<AgendaViewConfig> = (\n  config = {}\n) => {\n  const finalConfig = { ...defaultAgendaViewConfig, ...config };\n\n  return {\n    type: ViewType.AGENDA,\n    label: 'Agenda',\n    component: AgendaViewAdapter,\n    config: finalConfig,\n  };\n};\n\nexport default createAgendaView;\n"
  },
  {
    "path": "packages/core/src/factories/createDayView.ts",
    "content": "// Factory function for creating Day view\nimport { h } from 'preact';\n\nimport {\n  DayViewConfig,\n  DayViewProps,\n  ViewAdapterProps,\n  ViewFactory,\n  ViewType,\n} from '@/types';\nimport DayView from '@/views/DayView';\n\nimport { ViewAdapter } from './ViewAdapter';\n\n// Default Day view configuration\nconst defaultDayViewConfig: DayViewConfig = {\n  // Day view specific configuration\n};\n\n// Stable adapter component\nconst DayViewAdapter = (props: DayViewProps) =>\n  h(ViewAdapter, {\n    ...(props as ViewAdapterProps),\n    viewType: ViewType.DAY,\n    originalComponent: DayView,\n    className: 'df-day-view-factory',\n  });\n\nDayViewAdapter.displayName = 'DayViewAdapter';\n\n// Day view factory function\nexport const createDayView: ViewFactory<DayViewConfig> = (config = {}) => {\n  // Merge configuration\n  const finalConfig = { ...defaultDayViewConfig, ...config };\n\n  return {\n    type: ViewType.DAY,\n    component: DayViewAdapter,\n    config: finalConfig,\n  };\n};\n\nexport default createDayView;\n"
  },
  {
    "path": "packages/core/src/factories/createMonthView.ts",
    "content": "// Factory function for creating Month view\nimport { h } from 'preact';\n\nimport {\n  MonthViewConfig,\n  MonthViewProps,\n  ViewAdapterProps,\n  ViewFactory,\n  ViewType,\n} from '@/types';\nimport MonthView from '@/views/MonthView';\n\nimport { ViewAdapter } from './ViewAdapter';\n\n// Default Month view configuration\nconst defaultMonthViewConfig: MonthViewConfig = {\n  // Month view specific configuration\n  showWeekNumbers: false,\n  showMonthIndicator: true,\n  startOfWeek: 1, // Monday\n};\n\n// Stable adapter component\nconst MonthViewAdapter = (props: MonthViewProps) =>\n  h(ViewAdapter, {\n    ...(props as ViewAdapterProps),\n    viewType: ViewType.MONTH,\n    originalComponent: MonthView,\n    className: 'df-month-view-factory',\n  });\n\nMonthViewAdapter.displayName = 'MonthViewAdapter';\n\n// Month view factory function\nexport const createMonthView: ViewFactory<MonthViewConfig> = (config = {}) => {\n  // Merge configuration\n  const finalConfig = { ...defaultMonthViewConfig, ...config };\n\n  return {\n    type: ViewType.MONTH,\n    component: MonthViewAdapter,\n    config: finalConfig,\n  };\n};\n\nexport default createMonthView;\n"
  },
  {
    "path": "packages/core/src/factories/createWeekView.ts",
    "content": "// Factory function for creating Week view\nimport { h } from 'preact';\n\nimport {\n  ViewAdapterProps,\n  ViewFactory,\n  ViewType,\n  WeekViewConfig,\n  WeekViewProps,\n} from '@/types';\nimport WeekView from '@/views/WeekView';\n\nimport { ViewAdapter } from './ViewAdapter';\n\n// Default Week view configuration\nconst defaultWeekViewConfig: WeekViewConfig = {\n  // Week view specific configuration\n  startOfWeek: 1, // Monday\n};\n\n// Stable adapter component\nconst WeekViewAdapter = (props: WeekViewProps) =>\n  h(ViewAdapter, {\n    ...(props as ViewAdapterProps),\n    viewType: ViewType.WEEK,\n    originalComponent: WeekView,\n    className: 'df-week-view-factory',\n  });\n\nWeekViewAdapter.displayName = 'WeekViewAdapter';\n\n// Week view factory function\nexport const createWeekView: ViewFactory<WeekViewConfig> = (config = {}) => {\n  // Merge configuration\n  const finalConfig = { ...defaultWeekViewConfig, ...config };\n\n  return {\n    type: ViewType.WEEK,\n    component: WeekViewAdapter,\n    config: finalConfig,\n  };\n};\n\nexport default createWeekView;\n"
  },
  {
    "path": "packages/core/src/factories/createYearView.ts",
    "content": "// Factory function for creating Year view\nimport { h } from 'preact';\n\nimport {\n  ViewAdapterProps,\n  ViewFactory,\n  ViewType,\n  YearViewConfig,\n  YearViewProps,\n} from '@/types';\nimport YearView from '@/views/YearView';\n\nimport { ViewAdapter } from './ViewAdapter';\n\n// Default Year view configuration\nconst defaultYearViewConfig: YearViewConfig = {\n  // Year view specific configuration\n};\n\n// Stable adapter component\nconst YearViewAdapter = (props: YearViewProps) =>\n  h(ViewAdapter, {\n    ...(props as ViewAdapterProps),\n    viewType: ViewType.YEAR,\n    originalComponent: YearView,\n    className: 'df-year-view-factory',\n  });\n\nYearViewAdapter.displayName = 'YearViewAdapter';\n\n// Year view factory function\nexport const createYearView: ViewFactory<YearViewConfig> = (config = {}) => {\n  // Merge configuration\n  const finalConfig = { ...defaultYearViewConfig, ...config };\n\n  return {\n    type: ViewType.YEAR,\n    component: YearViewAdapter,\n    config: finalConfig,\n  };\n};\n\nexport default createYearView;\n"
  },
  {
    "path": "packages/core/src/factories/index.ts",
    "content": "// View factory module export file\nexport * from './createDayView';\nexport * from './createWeekView';\nexport * from './createMonthView';\nexport * from './createAgendaView';\nexport * from './createYearView';\n\n// Import for internal use\nimport { createAgendaView } from './createAgendaView';\nimport { createDayView } from './createDayView';\nimport { createMonthView } from './createMonthView';\nimport { createWeekView } from './createWeekView';\n\n// Convenient view creation function\nexport function createStandardViews(config?: {\n  day?: Partial<import('@/types').DayViewConfig>;\n  week?: Partial<import('@/types').WeekViewConfig>;\n  month?: Partial<import('@/types').MonthViewConfig>;\n  agenda?: Partial<import('@/types').AgendaViewConfig>;\n}) {\n  const views = [\n    createDayView(config?.day),\n    createWeekView(config?.week),\n    createMonthView(config?.month),\n  ];\n\n  if (config?.agenda) {\n    views.push(createAgendaView(config.agenda));\n  }\n\n  return views;\n}\n"
  },
  {
    "path": "packages/core/src/hooks/__tests__/useCalendarDrop.test.tsx",
    "content": "import { act, renderHook } from '@testing-library/preact';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { useCalendarDrop } from '@/hooks/useCalendarDrop';\nimport { Event, ICalendarApp } from '@/types';\n\njest.mock('@/locale', () => ({\n  useLocale: () => ({\n    t: (key: string, params?: Record<string, string>) =>\n      params?.calendarName ? `${key}:${params.calendarName}` : key,\n  }),\n}));\n\ndescribe('useCalendarDrop', () => {\n  it('creates timed events in app.timeZone for day/week drops', () => {\n    const app = {\n      timeZone: 'Asia/Shanghai',\n      addEvent: jest.fn(),\n      getCalendarRegistry: jest.fn(() => ({\n        get: jest.fn(() => ({\n          readOnly: false,\n          subscription: false,\n        })),\n      })),\n    } as unknown as ICalendarApp;\n\n    const { result } = renderHook(() => useCalendarDrop({ app }));\n\n    const dataTransfer = {\n      getData: jest.fn(() =>\n        JSON.stringify({\n          calendarId: 'work',\n          calendarName: 'Work',\n          calendarColors: {\n            lineColor: '#2563eb',\n            eventColor: '#dbeafe',\n            eventSelectedColor: '#bfdbfe',\n            textColor: '#1e3a8a',\n          },\n        })\n      ),\n    };\n\n    const event = {\n      preventDefault: jest.fn(),\n      dataTransfer,\n    } as unknown as DragEvent;\n\n    let created: Event | null = null;\n    act(() => {\n      created = result.current.handleDrop(\n        event,\n        new Date(2026, 3, 2),\n        15,\n        false\n      );\n    });\n\n    expect(created).not.toBeNull();\n    expect(app.addEvent).toHaveBeenCalledTimes(1);\n    const createdEvent = (app.addEvent as jest.Mock).mock.calls[0][0] as Event;\n    expect(app.addEvent).toHaveBeenCalledWith(createdEvent);\n    expect(createdEvent.start).toBeInstanceOf(Temporal.ZonedDateTime);\n    expect(createdEvent.end).toBeInstanceOf(Temporal.ZonedDateTime);\n    expect(String(createdEvent.start)).toContain('[Asia/Shanghai]');\n    expect(String(createdEvent.end)).toContain('[Asia/Shanghai]');\n    expect((createdEvent.start as Temporal.ZonedDateTime).hour).toBe(15);\n    expect((createdEvent.end as Temporal.ZonedDateTime).hour).toBe(16);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/hooks/useCalendarDrop.ts",
    "content": "import { useCallback } from 'preact/hooks';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { useLocale } from '@/locale';\nimport { Event, CalendarColors, ICalendarApp } from '@/types';\nimport { dateToZonedDateTime } from '@/utils';\n\nexport interface CalendarDropData {\n  calendarId: string;\n  calendarName: string;\n  calendarColors: CalendarColors;\n  calendarIcon?: string;\n}\n\nexport interface CalendarDropOptions {\n  app: ICalendarApp;\n  onEventCreated?: (event: Event) => void;\n}\n\nexport interface CalendarDropReturn {\n  handleDrop: (\n    e: DragEvent,\n    dropDate: Date,\n    dropHour?: number,\n    isAllDay?: boolean\n  ) => Event | null;\n  handleDragOver: (e: DragEvent) => void;\n}\n\n/**\n * Hook to handle dropping calendar from sidebar to create events\n */\nexport function useCalendarDrop(\n  options: CalendarDropOptions\n): CalendarDropReturn {\n  const { app, onEventCreated } = options;\n  const { t } = useLocale();\n\n  const handleDragOver = useCallback((e: DragEvent) => {\n    // Check if the drag data is from a calendar\n    if (\n      e.dataTransfer &&\n      e.dataTransfer.types.includes('application/x-dayflow-calendar')\n    ) {\n      e.preventDefault();\n      e.dataTransfer.dropEffect = 'copy';\n    }\n  }, []);\n\n  const handleDrop = useCallback(\n    (\n      e: DragEvent,\n      dropDate: Date,\n      dropHour?: number,\n      isAllDay?: boolean\n    ): Event | null => {\n      e.preventDefault();\n\n      if (!e.dataTransfer) return null;\n\n      // Get calendar data from drag event\n      const dragDataStr = e.dataTransfer.getData(\n        'application/x-dayflow-calendar'\n      );\n      if (!dragDataStr) {\n        return null;\n      }\n\n      try {\n        const dragData: CalendarDropData = JSON.parse(dragDataStr);\n\n        // Create event based on drop location\n        let start:\n          | Temporal.PlainDate\n          | Temporal.PlainDateTime\n          | Temporal.ZonedDateTime;\n        let end:\n          | Temporal.PlainDate\n          | Temporal.PlainDateTime\n          | Temporal.ZonedDateTime;\n        let allDay = false;\n\n        if (isAllDay) {\n          // For All-day area - create all-day event using PlainDate (no time component)\n          const plainDate = Temporal.PlainDate.from({\n            year: dropDate.getFullYear(),\n            month: dropDate.getMonth() + 1,\n            day: dropDate.getDate(),\n          });\n          start = plainDate;\n          end = plainDate;\n          allDay = true;\n        } else if (dropHour === undefined) {\n          // For Month view - create timed event 9:00-10:00\n          const startDate = new Date(dropDate);\n          startDate.setHours(9, 0, 0, 0);\n          const endDate = new Date(startDate);\n          endDate.setHours(10, 0, 0, 0);\n          start = dateToZonedDateTime(startDate, app.timeZone);\n          end = dateToZonedDateTime(endDate, app.timeZone);\n        } else {\n          // For Day/Week view with specific hour\n          const startDate = new Date(dropDate);\n          startDate.setHours(dropHour, 0, 0, 0);\n          const endDate = new Date(startDate);\n          endDate.setHours(dropHour + 1, 0, 0, 0);\n          start = dateToZonedDateTime(startDate, app.timeZone);\n          end = dateToZonedDateTime(endDate, app.timeZone);\n        }\n\n        // Block drop if the target calendar is read-only\n        const targetCalendar = app\n          .getCalendarRegistry()\n          .get(dragData.calendarId);\n        if (targetCalendar?.readOnly || targetCalendar?.subscription) {\n          return null;\n        }\n\n        // Generate unique event ID\n        const eventId = `event-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n\n        // Create new event\n        const newEvent: Event = {\n          id: eventId,\n          title: allDay\n            ? t('newAllDayCalendarEvent', {\n                calendarName: dragData.calendarName,\n              })\n            : t('newCalendarEvent', { calendarName: dragData.calendarName }),\n          description: '',\n          start,\n          end,\n          calendarId: dragData.calendarId,\n          allDay,\n        };\n\n        // Add event to calendar\n        app.addEvent(newEvent);\n\n        // Trigger callback\n        onEventCreated?.(newEvent);\n\n        return newEvent;\n      } catch (error) {\n        console.error('Error creating event from calendar drop:', error);\n        return null;\n      }\n    },\n    [app, onEventCreated]\n  );\n\n  return {\n    handleDrop,\n    handleDragOver,\n  };\n}\n"
  },
  {
    "path": "packages/core/src/hooks/useDebouncedValue.ts",
    "content": "import { useEffect, useState } from 'preact/hooks';\n\nexport const useDebouncedValue = <T>(value: T, delay: number): T => {\n  const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      setDebouncedValue(value);\n    }, delay);\n\n    return () => {\n      clearTimeout(timer);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n};\n"
  },
  {
    "path": "packages/core/src/hooks/useWeekViewSwipe.ts",
    "content": "import { JSX, RefObject } from 'preact';\nimport { useCallback, useEffect, useRef, useState } from 'preact/hooks';\n\nimport { ICalendarApp } from '@/types';\n\ninterface UseWeekViewSwipeParams {\n  app: ICalendarApp;\n  columnsPerPage: number;\n  currentDate: Date;\n  displayDays: number;\n  isSlidingView: boolean;\n  scrollerRef: RefObject<HTMLDivElement>;\n  swipeContentRef: RefObject<HTMLDivElement>;\n  topFrozenContentRef: RefObject<HTMLDivElement>;\n}\n\nexport const useWeekViewSwipe = ({\n  app,\n  columnsPerPage,\n  currentDate,\n  displayDays,\n  isSlidingView,\n  scrollerRef,\n  swipeContentRef,\n  topFrozenContentRef,\n}: UseWeekViewSwipeParams) => {\n  const [mobilePageStart, setMobilePageStart] = useState<Date>(() => {\n    const date = new Date(currentDate);\n    date.setHours(0, 0, 0, 0);\n    return date;\n  });\n  const [swipeOffset, setSwipeOffset] = useState(0);\n  const [isTransitioning, setIsTransitioning] = useState(false);\n  const touchStartPos = useRef({ x: 0, y: 0 });\n  const isHorizontalSwipe = useRef(false);\n  const liveSwipeOffsetRef = useRef(0);\n  const activePointerIdRef = useRef<number | null>(null);\n  const hasCapturedPointerRef = useRef(false);\n\n  useEffect(() => {\n    if (!isSlidingView) return;\n\n    setMobilePageStart(prev => {\n      const target = new Date(currentDate);\n      target.setHours(0, 0, 0, 0);\n\n      const windowStart = new Date(prev);\n      const windowEnd = new Date(prev);\n      windowEnd.setDate(windowEnd.getDate() + columnsPerPage - 1);\n\n      if (target >= windowStart && target <= windowEnd) {\n        return prev;\n      }\n\n      return target;\n    });\n  }, [columnsPerPage, currentDate, isSlidingView]);\n\n  const handleScroll = useCallback(\n    (e: JSX.TargetedEvent<HTMLDivElement, globalThis.Event>) => {\n      const { scrollLeft } = e.currentTarget;\n      if (!topFrozenContentRef.current) return;\n\n      const baseTranslateX = isSlidingView ? 'calc(-100% / 3)' : '0px';\n      const horizontalOffset = isSlidingView\n        ? `${swipeOffset}px`\n        : `-${scrollLeft}px`;\n\n      topFrozenContentRef.current.style.transform = `translateX(calc(${baseTranslateX} + ${horizontalOffset}))`;\n      topFrozenContentRef.current.style.transition =\n        isSlidingView && isTransitioning ? 'transform 0.3s ease-out' : 'none';\n    },\n    [isSlidingView, isTransitioning, swipeOffset, topFrozenContentRef]\n  );\n\n  useEffect(() => {\n    if (!isSlidingView) return;\n\n    const scroller = scrollerRef.current;\n    if (!scroller) return;\n\n    const startSwipe = (clientX: number, clientY: number) => {\n      touchStartPos.current = {\n        x: clientX,\n        y: clientY,\n      };\n      isHorizontalSwipe.current = false;\n      liveSwipeOffsetRef.current = 0;\n      hasCapturedPointerRef.current = false;\n      setIsTransitioning(false);\n    };\n\n    const moveSwipe = (\n      clientX: number,\n      clientY: number,\n      preventDefault?: () => void\n    ) => {\n      if (isTransitioning) return;\n\n      const deltaX = clientX - touchStartPos.current.x;\n      const deltaY = clientY - touchStartPos.current.y;\n\n      if (\n        !isHorizontalSwipe.current &&\n        Math.abs(deltaX) > 10 &&\n        Math.abs(deltaX) > Math.abs(deltaY)\n      ) {\n        isHorizontalSwipe.current = true;\n        if (\n          activePointerIdRef.current !== null &&\n          !hasCapturedPointerRef.current\n        ) {\n          scroller.setPointerCapture?.(activePointerIdRef.current);\n          hasCapturedPointerRef.current = true;\n        }\n      }\n\n      if (!isHorizontalSwipe.current) return;\n\n      preventDefault?.();\n\n      const containerWidth = scroller.clientWidth;\n      const maxOffset = containerWidth / 2;\n      const offset = Math.max(-maxOffset, Math.min(maxOffset, deltaX));\n      const transform = `translateX(calc(-100% / 3 + ${offset}px))`;\n\n      if (topFrozenContentRef.current) {\n        topFrozenContentRef.current.style.transition = 'none';\n        topFrozenContentRef.current.style.transform = transform;\n      }\n\n      if (swipeContentRef.current) {\n        swipeContentRef.current.style.transition = 'none';\n        swipeContentRef.current.style.transform = transform;\n      }\n\n      liveSwipeOffsetRef.current = offset;\n    };\n\n    const endSwipe = () => {\n      if (!isHorizontalSwipe.current) {\n        liveSwipeOffsetRef.current = 0;\n        return;\n      }\n\n      const offset = liveSwipeOffsetRef.current;\n      const threshold = 100;\n      const containerWidth =\n        swipeContentRef.current?.clientWidth || scroller.clientWidth;\n      const dayWidth = containerWidth / displayDays;\n\n      if (offset > threshold) {\n        setIsTransitioning(true);\n        setSwipeOffset(dayWidth);\n        setTimeout(() => {\n          const nextDate = new Date(mobilePageStart);\n          nextDate.setDate(nextDate.getDate() - 1);\n          setMobilePageStart(nextDate);\n          app.setCurrentDate(nextDate);\n          setSwipeOffset(0);\n          liveSwipeOffsetRef.current = 0;\n          setIsTransitioning(false);\n        }, 300);\n      } else if (offset < -threshold) {\n        setIsTransitioning(true);\n        setSwipeOffset(-dayWidth);\n        setTimeout(() => {\n          const nextDate = new Date(mobilePageStart);\n          nextDate.setDate(nextDate.getDate() + 1);\n          setMobilePageStart(nextDate);\n          app.setCurrentDate(nextDate);\n          setSwipeOffset(0);\n          liveSwipeOffsetRef.current = 0;\n          setIsTransitioning(false);\n        }, 300);\n      } else {\n        setIsTransitioning(true);\n        setSwipeOffset(0);\n        liveSwipeOffsetRef.current = 0;\n        setTimeout(() => {\n          setIsTransitioning(false);\n        }, 300);\n      }\n\n      isHorizontalSwipe.current = false;\n    };\n\n    const handleScrollerTouchStart = (e: TouchEvent) => {\n      startSwipe(e.touches[0].clientX, e.touches[0].clientY);\n    };\n\n    const handleScrollerTouchMove = (e: TouchEvent) => {\n      moveSwipe(e.touches[0].clientX, e.touches[0].clientY, () => {\n        if (e.cancelable) e.preventDefault();\n      });\n    };\n\n    const handleScrollerTouchEnd = () => {\n      endSwipe();\n    };\n\n    const handleScrollerPointerDown = (e: PointerEvent) => {\n      if (e.pointerType === 'touch' || e.button !== 0) return;\n\n      activePointerIdRef.current = e.pointerId;\n      startSwipe(e.clientX, e.clientY);\n    };\n\n    const handleScrollerPointerMove = (e: PointerEvent) => {\n      if (activePointerIdRef.current !== e.pointerId) return;\n\n      moveSwipe(e.clientX, e.clientY, () => {\n        if (e.cancelable) e.preventDefault();\n      });\n    };\n\n    const handleScrollerPointerEnd = (e: PointerEvent) => {\n      if (activePointerIdRef.current !== e.pointerId) return;\n\n      if (hasCapturedPointerRef.current) {\n        scroller.releasePointerCapture?.(e.pointerId);\n      }\n      hasCapturedPointerRef.current = false;\n      activePointerIdRef.current = null;\n      endSwipe();\n    };\n\n    scroller.addEventListener('touchstart', handleScrollerTouchStart, {\n      capture: true,\n      passive: true,\n    });\n    scroller.addEventListener('touchmove', handleScrollerTouchMove, {\n      capture: true,\n      passive: false,\n    });\n    scroller.addEventListener('touchend', handleScrollerTouchEnd, {\n      capture: true,\n      passive: true,\n    });\n    scroller.addEventListener('pointerdown', handleScrollerPointerDown);\n    scroller.addEventListener('pointermove', handleScrollerPointerMove);\n    scroller.addEventListener('pointerup', handleScrollerPointerEnd);\n    scroller.addEventListener('pointercancel', handleScrollerPointerEnd);\n\n    return () => {\n      scroller.removeEventListener(\n        'touchstart',\n        handleScrollerTouchStart,\n        true\n      );\n      scroller.removeEventListener('touchmove', handleScrollerTouchMove, true);\n      scroller.removeEventListener('touchend', handleScrollerTouchEnd, true);\n      scroller.removeEventListener('pointerdown', handleScrollerPointerDown);\n      scroller.removeEventListener('pointermove', handleScrollerPointerMove);\n      scroller.removeEventListener('pointerup', handleScrollerPointerEnd);\n      scroller.removeEventListener('pointercancel', handleScrollerPointerEnd);\n    };\n  }, [\n    app,\n    displayDays,\n    isSlidingView,\n    isTransitioning,\n    mobilePageStart,\n    scrollerRef,\n    swipeContentRef,\n    topFrozenContentRef,\n  ]);\n\n  useEffect(() => {\n    if (!isSlidingView) {\n      if (topFrozenContentRef.current) {\n        topFrozenContentRef.current.style.transform = '';\n        topFrozenContentRef.current.style.transition = '';\n      }\n\n      if (swipeContentRef.current) {\n        swipeContentRef.current.style.transform = '';\n        swipeContentRef.current.style.transition = '';\n      }\n      return;\n    }\n\n    const baseTranslateX = 'calc(-100% / 3)';\n    const transition = isTransitioning ? 'transform 0.3s ease-out' : 'none';\n    const transform = `translateX(calc(${baseTranslateX} + ${swipeOffset}px))`;\n\n    if (topFrozenContentRef.current) {\n      topFrozenContentRef.current.style.transition = transition;\n      topFrozenContentRef.current.style.transform = transform;\n    }\n\n    if (swipeContentRef.current) {\n      swipeContentRef.current.style.transition = transition;\n      swipeContentRef.current.style.transform = transform;\n    }\n  }, [\n    isSlidingView,\n    isTransitioning,\n    swipeContentRef,\n    swipeOffset,\n    topFrozenContentRef,\n  ]);\n\n  const goToPrevious = useCallback(() => {\n    if (!isSlidingView) return false;\n\n    const nextCurrentDate = new Date(currentDate);\n    nextCurrentDate.setDate(nextCurrentDate.getDate() - 1);\n\n    setMobilePageStart(prev => {\n      const next = new Date(prev);\n      next.setDate(next.getDate() - 1);\n      return next;\n    });\n    app.setCurrentDate(nextCurrentDate);\n\n    return true;\n  }, [app, currentDate, isSlidingView]);\n\n  const goToNext = useCallback(() => {\n    if (!isSlidingView) return false;\n\n    const nextCurrentDate = new Date(currentDate);\n    nextCurrentDate.setDate(nextCurrentDate.getDate() + 1);\n\n    setMobilePageStart(prev => {\n      const next = new Date(prev);\n      next.setDate(next.getDate() + 1);\n      return next;\n    });\n    app.setCurrentDate(nextCurrentDate);\n\n    return true;\n  }, [app, currentDate, isSlidingView]);\n\n  return {\n    handleScroll,\n    goToNext,\n    goToPrevious,\n    mobilePageStart,\n  };\n};\n"
  },
  {
    "path": "packages/core/src/hooks/virtualScroll/index.ts",
    "content": "// Main entry for virtual scroll hooks\nexport * from './useVirtualScroll';\nexport * from './useVirtualMonthScroll';\n"
  },
  {
    "path": "packages/core/src/hooks/virtualScroll/useVirtualMonthScroll.ts",
    "content": "import { JSX } from 'preact';\nimport {\n  useState,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useCallback,\n} from 'preact/hooks';\n\nimport { WeeksData } from '@/types';\nimport {\n  UseVirtualMonthScrollProps,\n  UseVirtualMonthScrollReturn,\n  VIRTUAL_MONTH_SCROLL_CONFIG,\n  VirtualWeekItem,\n  WeekDataCache,\n} from '@/types/monthView';\nimport { generateWeekData, generateWeekRange } from '@/utils';\n\nlet cachedConfig: {\n  weekHeight: number;\n  screenSize: 'mobile' | 'tablet' | 'desktop';\n  weeksPerView: number;\n} | null = null;\n\n// Responsive configuration Hook\nexport const useResponsiveMonthConfig = () => {\n  const [config, setConfig] = useState<{\n    weekHeight: number;\n    screenSize: 'mobile' | 'tablet' | 'desktop';\n    weeksPerView: number;\n  }>(() => {\n    // During initialization (SSR/Hydration), use cached value or default\n    if (cachedConfig) return cachedConfig;\n\n    return {\n      weekHeight: VIRTUAL_MONTH_SCROLL_CONFIG.WEEK_HEIGHT,\n      screenSize: 'desktop',\n      weeksPerView: 6,\n    };\n  });\n\n  useEffect(() => {\n    const updateConfig = () => {\n      const width = window.innerWidth;\n      const height = window.innerHeight;\n\n      const headerHeight = 150;\n      const availableHeight = height - headerHeight;\n      const weeksPerView = 6;\n      const dynamicWeekHeight = Math.max(\n        80,\n        Math.floor(availableHeight / weeksPerView)\n      );\n\n      const newConfig = (() => {\n        if (width < 768) {\n          return {\n            weekHeight: Math.max(\n              VIRTUAL_MONTH_SCROLL_CONFIG.MOBILE_WEEK_HEIGHT,\n              dynamicWeekHeight\n            ),\n            screenSize: 'mobile' as const,\n            weeksPerView,\n          };\n        } else if (width < 1024) {\n          return {\n            weekHeight: Math.max(\n              VIRTUAL_MONTH_SCROLL_CONFIG.TABLET_WEEK_HEIGHT,\n              dynamicWeekHeight\n            ),\n            screenSize: 'tablet' as const,\n            weeksPerView,\n          };\n        }\n        return {\n          weekHeight: Math.max(\n            VIRTUAL_MONTH_SCROLL_CONFIG.WEEK_HEIGHT,\n            dynamicWeekHeight\n          ),\n          screenSize: 'desktop' as const,\n          weeksPerView,\n        };\n      })();\n\n      // Update global cache\n      cachedConfig = newConfig;\n\n      // fix: In the mobile month view, when events are initially rendered, only the event start time is shown,\n      // but it should show only the event title instead.\n      // always sync local state on mount/resize, but skip if effectively the same\n      setConfig(prev => {\n        if (\n          prev.screenSize === newConfig.screenSize &&\n          prev.weekHeight === newConfig.weekHeight &&\n          prev.weeksPerView === newConfig.weeksPerView\n        ) {\n          return prev;\n        }\n        return newConfig;\n      });\n    };\n\n    updateConfig();\n    window.addEventListener('resize', updateConfig);\n    return () => window.removeEventListener('resize', updateConfig);\n  }, []);\n\n  return config;\n};\n\n// Virtual scroll main Hook\nexport const useVirtualMonthScroll = ({\n  currentDate,\n  weekHeight,\n  onCurrentMonthChange,\n  initialWeeksToLoad = 104,\n  locale = 'en-US',\n  startOfWeek = 1,\n  isEnabled = true,\n  snapToMonth = false,\n}: UseVirtualMonthScrollProps): UseVirtualMonthScrollReturn => {\n  const targetNavigationRef = useRef<{ month: string; year: number } | null>(\n    null\n  );\n\n  const getMonthName = useCallback(\n    (monthIndex: number, year: number) => {\n      const date = new Date(year, monthIndex, 1);\n      const isAsian = locale.startsWith('zh') || locale.startsWith('ja');\n      return date.toLocaleDateString(locale, {\n        month: isAsian ? 'short' : 'long',\n      });\n    },\n    [locale]\n  );\n\n  const initialWeeksData = useMemo(() => {\n    const firstDayOfMonth = new Date(currentDate);\n    firstDayOfMonth.setDate(1);\n    firstDayOfMonth.setHours(0, 0, 0, 0);\n    return generateWeekRange(firstDayOfMonth, initialWeeksToLoad, startOfWeek);\n  }, [currentDate, initialWeeksToLoad, startOfWeek]);\n\n  const initialScrollTop = useMemo(() => {\n    const firstDayOfMonth = new Date(currentDate);\n    firstDayOfMonth.setDate(1);\n    firstDayOfMonth.setHours(0, 0, 0, 0);\n\n    const targetWeekIndex = initialWeeksData.findIndex(week =>\n      week.days.some(\n        day => day.date.toDateString() === firstDayOfMonth.toDateString()\n      )\n    );\n\n    return targetWeekIndex > 0 ? targetWeekIndex * weekHeight : 0;\n  }, [initialWeeksData, currentDate, weekHeight]);\n\n  const [scrollTop, setScrollTop] = useState(initialScrollTop);\n  const [containerHeight, setContainerHeight] = useState(600);\n  const [currentMonth, setCurrentMonth] = useState(\n    getMonthName(currentDate.getMonth(), currentDate.getFullYear())\n  );\n  const [currentYear, setCurrentYear] = useState(currentDate.getFullYear());\n  const [isScrolling, setIsScrolling] = useState(false);\n  const [weeksData, setWeeksData] = useState<WeeksData[]>(initialWeeksData);\n  const [isNavigating, setIsNavigating] = useState(false);\n  const isNavigatingRef = useRef(false);\n  const [isInitialized, setIsInitialized] = useState(false);\n  const previousDateRef = useRef<Date>(currentDate);\n\n  // References and cache\n  const scrollElementRef = useRef<HTMLDivElement>(null);\n  const scrollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const weekDataCache = useRef(new WeekDataCache());\n  const lastScrollTime = useRef(0);\n  const lastScrollTop = useRef(0);\n  const loadingRef = useRef(false);\n\n  // Callback ref for scroll element\n  const scrollElementRefCallback = useCallback(\n    (element: HTMLDivElement | null) => {\n      if (element) {\n        (\n          scrollElementRef as React.MutableRefObject<HTMLDivElement | null>\n        ).current = element;\n      }\n    },\n    []\n  );\n\n  // Cached week data retrieval\n  const getCachedWeekData = useCallback((weekStartDate: Date): WeeksData => {\n    let weekData = weekDataCache.current.get(weekStartDate);\n\n    if (!weekData) {\n      weekData = generateWeekData(weekStartDate);\n      weekDataCache.current.set(weekStartDate, weekData);\n    }\n\n    return weekData;\n  }, []);\n\n  // Dynamically load more week data\n  const loadMoreWeeks = useCallback(\n    (direction: 'past' | 'future', count: number = 26) => {\n      if (loadingRef.current) return;\n      loadingRef.current = true;\n\n      setTimeout(() => {\n        try {\n          if (direction === 'future') {\n            const lastWeek = weeksData.at(-1)!;\n            const newWeeks: WeeksData[] = [];\n\n            for (let i = 1; i <= count; i++) {\n              const weekStart = new Date(lastWeek.startDate);\n              weekStart.setDate(weekStart.getDate() + i * 7);\n              newWeeks.push(getCachedWeekData(weekStart));\n            }\n\n            setWeeksData(prev => [...prev, ...newWeeks]);\n          } else {\n            const firstWeek = weeksData[0];\n            const newWeeks: WeeksData[] = [];\n\n            for (let i = count; i >= 1; i--) {\n              const weekStart = new Date(firstWeek.startDate);\n              weekStart.setDate(weekStart.getDate() - i * 7);\n              newWeeks.push(getCachedWeekData(weekStart));\n            }\n\n            const addedHeight = newWeeks.length * weekHeight;\n            setWeeksData(prev => [...newWeeks, ...prev]);\n\n            requestAnimationFrame(() => {\n              setScrollTop(prev => prev + addedHeight);\n              if (scrollElementRef.current) {\n                scrollElementRef.current.scrollTop += addedHeight;\n              }\n            });\n          }\n        } finally {\n          setTimeout(() => {\n            loadingRef.current = false;\n          }, 200);\n        }\n      }, 0);\n    },\n    [weeksData, getCachedWeekData, weekHeight]\n  );\n\n  // Virtual scroll calculation - fixed 6 weeks display\n  const virtualData = useMemo(() => {\n    const totalHeight = weeksData.length * weekHeight;\n    const FIXED_WEEKS_TO_SHOW = 6;\n\n    const startIndex = Math.floor(scrollTop / weekHeight);\n    let displayStartIndex = Math.max(0, startIndex);\n\n    displayStartIndex = Math.min(\n      displayStartIndex,\n      Math.max(0, weeksData.length - FIXED_WEEKS_TO_SHOW)\n    );\n\n    const displayEndIndex = Math.min(\n      weeksData.length - 1,\n      displayStartIndex + FIXED_WEEKS_TO_SHOW - 1\n    );\n\n    // Buffer: 15 weeks before and after\n    const bufferStart = Math.max(0, displayStartIndex - 15);\n    const bufferEnd = Math.min(weeksData.length - 1, displayEndIndex + 15);\n\n    const visibleItems: VirtualWeekItem[] = [];\n    for (let i = bufferStart; i <= bufferEnd; i++) {\n      visibleItems.push({\n        index: i,\n        weekData: weeksData[i],\n        top: i * weekHeight,\n        height: weekHeight,\n      });\n    }\n\n    return {\n      totalHeight,\n      visibleItems,\n      displayStartIndex,\n    };\n  }, [scrollTop, containerHeight, weekHeight, weeksData]);\n\n  // Detect currently displayed main month\n  const detectCurrentMonth = useCallback(\n    (visibleItems: VirtualWeekItem[]) => {\n      if (\n        isNavigating ||\n        isScrolling ||\n        visibleItems.length === 0 ||\n        !isInitialized\n      ) {\n        return;\n      }\n\n      const viewportCenter = scrollTop + containerHeight / 2;\n      const centerItem =\n        visibleItems.find(\n          item =>\n            item.top <= viewportCenter &&\n            item.top + item.height > viewportCenter\n        ) || visibleItems[Math.floor(visibleItems.length / 2)];\n\n      if (!centerItem) return;\n\n      const monthDayCounts: Record<string, number> = {};\n\n      centerItem.weekData.days.forEach(day => {\n        const monthKey = `${getMonthName(day.month, day.year)}-${day.year}`;\n        monthDayCounts[monthKey] = (monthDayCounts[monthKey] || 0) + 1;\n      });\n\n      let weekDominantMonth = '';\n      let weekDominantYear = 0;\n      let maxDays = 0;\n\n      Object.entries(monthDayCounts).forEach(([monthKey, days]) => {\n        if (days > maxDays) {\n          maxDays = days;\n          const [month, year] = monthKey.split('-');\n          weekDominantMonth = month;\n          weekDominantYear = Number.parseInt(year, 10);\n        }\n      });\n\n      if (weekDominantMonth && weekDominantYear) {\n        if (targetNavigationRef.current) {\n          if (\n            weekDominantMonth === targetNavigationRef.current.month &&\n            weekDominantYear === targetNavigationRef.current.year\n          ) {\n            targetNavigationRef.current = null;\n\n            if (\n              weekDominantMonth !== currentMonth ||\n              weekDominantYear !== currentYear\n            ) {\n              setCurrentMonth(weekDominantMonth);\n              setCurrentYear(weekDominantYear);\n              onCurrentMonthChange?.(weekDominantMonth, weekDominantYear);\n            }\n          }\n        } else if (\n          weekDominantMonth !== currentMonth ||\n          weekDominantYear !== currentYear\n        ) {\n          setCurrentMonth(weekDominantMonth);\n          setCurrentYear(weekDominantYear);\n          onCurrentMonthChange?.(weekDominantMonth, weekDominantYear);\n        }\n      }\n    },\n    [\n      containerHeight,\n      currentMonth,\n      currentYear,\n      isNavigating,\n      isScrolling,\n      onCurrentMonthChange,\n      scrollTop,\n      isInitialized,\n    ]\n  );\n\n  // Scroll handler\n  const handleScroll = useCallback(\n    (e: JSX.TargetedEvent<HTMLDivElement, globalThis.Event>) => {\n      const now = performance.now();\n      if (\n        now - lastScrollTime.current <\n        VIRTUAL_MONTH_SCROLL_CONFIG.SCROLL_THROTTLE\n      )\n        return;\n      lastScrollTime.current = now;\n\n      const element = e.currentTarget;\n      const newScrollTop = element.scrollTop;\n      lastScrollTop.current = newScrollTop;\n\n      setScrollTop(newScrollTop);\n\n      // Only trigger automatic data loading in non-navigation state\n      if (!isNavigating) {\n        requestAnimationFrame(() => {\n          const { scrollHeight, clientHeight } = element;\n\n          if (\n            newScrollTop + clientHeight > scrollHeight - weekHeight * 10 &&\n            !loadingRef.current\n          ) {\n            loadMoreWeeks('future', 26);\n          }\n\n          if (newScrollTop < weekHeight * 10 && !loadingRef.current) {\n            loadMoreWeeks('past', 26);\n          }\n        });\n      }\n\n      setIsScrolling(true);\n      if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);\n      scrollTimeoutRef.current = setTimeout(() => {\n        setIsScrolling(false);\n      }, VIRTUAL_MONTH_SCROLL_CONFIG.SCROLL_DEBOUNCE);\n    },\n    [weekHeight, loadMoreWeeks, isNavigating]\n  );\n\n  // Scroll to specified date\n  const scrollToDate = useCallback(\n    (targetDate: Date, smooth = true) => {\n      if (!scrollElementRef.current) return;\n\n      setIsNavigating(true);\n      isNavigatingRef.current = true;\n\n      const resetNavigatingState = () => {\n        const delay = smooth ? 500 : 200;\n        setTimeout(() => {\n          setIsNavigating(false);\n          isNavigatingRef.current = false;\n        }, delay);\n      };\n\n      const targetWeekIndex = weeksData.findIndex(week =>\n        week.days.some(\n          day => day.date.toDateString() === targetDate.toDateString()\n        )\n      );\n\n      if (targetWeekIndex !== -1) {\n        const targetTop = targetWeekIndex * weekHeight;\n\n        scrollElementRef.current.scrollTo({\n          top: targetTop,\n          behavior: smooth ? 'smooth' : 'auto',\n        });\n        resetNavigatingState();\n        return;\n      }\n\n      // Calculate start of the week for the target date\n      const dayOfWeek = targetDate.getDay();\n      const diff = (dayOfWeek - startOfWeek + 7) % 7;\n      const targetWeekStart = new Date(targetDate);\n      targetWeekStart.setDate(targetDate.getDate() - diff);\n      targetWeekStart.setHours(0, 0, 0, 0);\n\n      const firstWeek = weeksData[0];\n      const lastWeek = weeksData.at(-1)!;\n\n      let weeksDiff = 0;\n      let needsPastData = false;\n      let needsFutureData = false;\n\n      if (targetWeekStart < firstWeek.startDate) {\n        weeksDiff = Math.ceil(\n          (firstWeek.startDate.getTime() - targetWeekStart.getTime()) /\n            (7 * 24 * 60 * 60 * 1000)\n        );\n        needsPastData = true;\n      } else if (targetWeekStart > lastWeek.startDate) {\n        weeksDiff = Math.ceil(\n          (targetWeekStart.getTime() - lastWeek.startDate.getTime()) /\n            (7 * 24 * 60 * 60 * 1000)\n        );\n        needsFutureData = true;\n      }\n\n      const maxBatchSize = 52;\n      const batchSize = Math.min(weeksDiff + 10, maxBatchSize);\n\n      if (needsPastData) {\n        const newWeeks: WeeksData[] = [];\n        for (let i = batchSize; i >= 1; i--) {\n          const weekStart = new Date(firstWeek.startDate);\n          weekStart.setDate(weekStart.getDate() - i * 7);\n          newWeeks.push(getCachedWeekData(weekStart));\n        }\n\n        const addedHeight = newWeeks.length * weekHeight;\n        setWeeksData(prev => [...newWeeks, ...prev]);\n\n        requestAnimationFrame(() => {\n          const combinedWeeks = [...newWeeks, ...weeksData];\n          const newTargetIndex = combinedWeeks.findIndex(week =>\n            week.days.some(\n              day => day.date.toDateString() === targetDate.toDateString()\n            )\n          );\n\n          if (scrollElementRef.current && newTargetIndex !== -1) {\n            const targetTop = newTargetIndex * weekHeight;\n            scrollElementRef.current.scrollTop += addedHeight;\n            setScrollTop(prev => prev + addedHeight);\n\n            setTimeout(() => {\n              if (scrollElementRef.current) {\n                scrollElementRef.current.scrollTo({\n                  top: targetTop,\n                  behavior: smooth ? 'smooth' : 'auto',\n                });\n              }\n            }, 50);\n          }\n          resetNavigatingState();\n        });\n      } else if (needsFutureData) {\n        const newWeeks: WeeksData[] = [];\n        for (let i = 1; i <= batchSize; i++) {\n          const weekStart = new Date(lastWeek.startDate);\n          weekStart.setDate(weekStart.getDate() + i * 7);\n          newWeeks.push(getCachedWeekData(weekStart));\n        }\n\n        setWeeksData(prev => [...prev, ...newWeeks]);\n\n        requestAnimationFrame(() => {\n          const combinedWeeks = [...weeksData, ...newWeeks];\n          const newTargetIndex = combinedWeeks.findIndex(week =>\n            week.days.some(\n              day => day.date.toDateString() === targetDate.toDateString()\n            )\n          );\n\n          if (scrollElementRef.current && newTargetIndex !== -1) {\n            const targetTop = newTargetIndex * weekHeight;\n            scrollElementRef.current.scrollTo({\n              top: targetTop,\n              behavior: smooth ? 'smooth' : 'auto',\n            });\n          }\n          resetNavigatingState();\n        });\n      } else {\n        resetNavigatingState();\n      }\n    },\n    [weeksData, weekHeight, getCachedWeekData]\n  );\n\n  // Navigation functions\n  const handlePreviousMonth = useCallback(() => {\n    const startIdx = virtualData.displayStartIndex;\n    const endIdx = Math.min(weeksData.length - 1, startIdx + 5);\n    const counts: Record<string, number> = {};\n    for (let i = startIdx; i <= endIdx; i++) {\n      weeksData[i]?.days.forEach(d => {\n        const k = `${d.month}-${d.year}`;\n        counts[k] = (counts[k] || 0) + 1;\n      });\n    }\n    let dominantMonth = 0;\n    let dominantYear = 0;\n    let max = 0;\n    for (const [key, count] of Object.entries(counts)) {\n      if (count > max) {\n        max = count;\n        [dominantMonth, dominantYear] = key.split('-').map(Number);\n      }\n    }\n    const targetDate = new Date(dominantYear, dominantMonth - 1, 1);\n    scrollToDate(targetDate);\n  }, [virtualData.displayStartIndex, weeksData, scrollToDate]);\n\n  const handleNextMonth = useCallback(() => {\n    // Use the dominant month of the visible range so navigation is correct\n    // even when the display week's first day falls in the prior month.\n    const startIdx = virtualData.displayStartIndex;\n    const endIdx = Math.min(weeksData.length - 1, startIdx + 5);\n    const counts: Record<string, number> = {};\n    for (let i = startIdx; i <= endIdx; i++) {\n      weeksData[i]?.days.forEach(d => {\n        const k = `${d.month}-${d.year}`;\n        counts[k] = (counts[k] || 0) + 1;\n      });\n    }\n    let dominantMonth = 0;\n    let dominantYear = 0;\n    let max = 0;\n    for (const [key, count] of Object.entries(counts)) {\n      if (count > max) {\n        max = count;\n        [dominantMonth, dominantYear] = key.split('-').map(Number);\n      }\n    }\n    const targetDate = new Date(dominantYear, dominantMonth + 1, 1);\n    scrollToDate(targetDate);\n  }, [virtualData.displayStartIndex, weeksData, scrollToDate]);\n\n  const handleToday = useCallback(() => {\n    const today = new Date();\n    const todayMonth = getMonthName(today.getMonth(), today.getFullYear());\n    const todayYear = today.getFullYear();\n\n    // Create date of first day of current month\n    const firstDayOfMonth = new Date(todayYear, today.getMonth(), 1);\n\n    targetNavigationRef.current = { month: todayMonth, year: todayYear };\n    setCurrentMonth(todayMonth);\n    setCurrentYear(todayYear);\n    onCurrentMonthChange?.(todayMonth, todayYear);\n\n    // Find week containing first day of current month\n    const targetWeekIndex = weeksData.findIndex(week =>\n      week.days.some(\n        day => day.date.toDateString() === firstDayOfMonth.toDateString()\n      )\n    );\n\n    if (targetWeekIndex === -1) {\n      // If first day of current month not found in current data, use scrollToDate method\n      setIsNavigating(true);\n      isNavigatingRef.current = true;\n      requestAnimationFrame(() => {\n        scrollToDate(firstDayOfMonth, true);\n        setTimeout(() => {\n          isNavigatingRef.current = false;\n          setIsNavigating(false);\n        }, 200);\n      });\n    } else {\n      const targetTop = targetWeekIndex * weekHeight;\n      const element = scrollElementRef.current;\n\n      if (element) {\n        // First set navigation state (set both state and ref)\n        setIsNavigating(true);\n        isNavigatingRef.current = true;\n\n        // Wait for next frame to ensure isNavigating state is updated\n        requestAnimationFrame(() => {\n          // Set scrollTop state, trigger virtual scroll recalculation\n          setScrollTop(targetTop);\n\n          // Wait another frame to set DOM scrollTop\n          requestAnimationFrame(() => {\n            if (element) {\n              element.scrollTop = targetTop;\n\n              // Delay reset navigation state\n              setTimeout(() => {\n                isNavigatingRef.current = false;\n                setIsNavigating(false);\n              }, 200);\n            }\n          });\n        });\n      }\n    }\n  }, [weeksData, weekHeight, onCurrentMonthChange, scrollToDate]);\n\n  // Detect current month change\n  useEffect(() => {\n    detectCurrentMonth(virtualData.visibleItems);\n  }, [virtualData.visibleItems, detectCurrentMonth]);\n\n  useEffect(() => {\n    const previousDate = previousDateRef.current;\n    const prevMonth = previousDate.getMonth();\n    const prevYear = previousDate.getFullYear();\n    const nextMonth = currentDate.getMonth();\n    const nextYear = currentDate.getFullYear();\n\n    if (prevMonth !== nextMonth || prevYear !== nextYear) {\n      // Check if the new date is already visible in the current viewport\n      const FIXED_WEEKS_TO_SHOW = 6;\n      const startIndex = virtualData.displayStartIndex;\n      const endIndex = Math.min(\n        weeksData.length - 1,\n        startIndex + FIXED_WEEKS_TO_SHOW - 1\n      );\n\n      // Check if nextMonth/nextYear is already the dominant displayed month.\n      // Using dominant-month logic instead of raw date presence avoids a false\n      // positive when the 1st of the next month appears in the last row of the\n      // current month's 6-week grid (e.g. April 1 visible in March's view).\n      const monthDayCounts: Record<string, number> = {};\n      for (let i = startIndex; i <= endIndex; i++) {\n        const week = weeksData[i];\n        if (week) {\n          week.days.forEach(day => {\n            const key = `${day.month}-${day.year}`;\n            monthDayCounts[key] = (monthDayCounts[key] || 0) + 1;\n          });\n        }\n      }\n      let dominantKey = '';\n      let maxDayCount = 0;\n      for (const [key, count] of Object.entries(monthDayCounts)) {\n        if (count > maxDayCount) {\n          maxDayCount = count;\n          dominantKey = key;\n        }\n      }\n      const isVisible = dominantKey === `${nextMonth}-${nextYear}`;\n\n      if (!isVisible) {\n        const firstDayOfMonth = new Date(nextYear, nextMonth, 1);\n        const monthName = getMonthName(nextMonth, nextYear);\n\n        targetNavigationRef.current = { month: monthName, year: nextYear };\n        setCurrentMonth(monthName);\n        setCurrentYear(nextYear);\n        onCurrentMonthChange?.(monthName, nextYear);\n        scrollToDate(firstDayOfMonth, true);\n      }\n    }\n\n    previousDateRef.current = currentDate;\n  }, [\n    currentDate,\n    onCurrentMonthChange,\n    scrollToDate,\n    virtualData,\n    weeksData,\n    getMonthName,\n  ]);\n\n  // Container size listener\n  useEffect(() => {\n    const element = scrollElementRef.current;\n    if (!element) return;\n\n    const resizeObserver = new ResizeObserver(([entry]) => {\n      setContainerHeight(entry.contentRect.height);\n    });\n\n    resizeObserver.observe(element);\n    return () => resizeObserver.disconnect();\n  }, []);\n\n  useLayoutEffect(() => {\n    const element = scrollElementRef.current;\n    if (!element || isInitialized || !isEnabled) return;\n\n    if (initialScrollTop > 0) {\n      element.scrollTop = initialScrollTop;\n    }\n    setIsInitialized(true);\n  }, [isInitialized, initialScrollTop, isEnabled]);\n\n  // Snap-to-month: when scrolling stops, smooth-scroll to the start of the\n  // dominant month so the view always shows a clean month boundary.\n  useEffect(() => {\n    if (!snapToMonth || isScrolling || isNavigating || !isInitialized) return;\n\n    const startIndex = Math.floor(scrollTop / weekHeight);\n    const endIndex = Math.min(weeksData.length - 1, startIndex + 5);\n\n    const counts: Record<string, number> = {};\n    for (let i = startIndex; i <= endIndex; i++) {\n      weeksData[i]?.days.forEach(d => {\n        const k = `${d.month}-${d.year}`;\n        counts[k] = (counts[k] || 0) + 1;\n      });\n    }\n\n    let dominantMonth = 0;\n    let dominantYear = 0;\n    let max = 0;\n    for (const [key, count] of Object.entries(counts)) {\n      if (count > max) {\n        max = count;\n        [dominantMonth, dominantYear] = key.split('-').map(Number);\n      }\n    }\n\n    const targetDate = new Date(dominantYear, dominantMonth, 1);\n    const targetWeekIndex = weeksData.findIndex(week =>\n      week.days.some(\n        day => day.date.toDateString() === targetDate.toDateString()\n      )\n    );\n\n    if (targetWeekIndex === -1) return;\n\n    const targetTop = targetWeekIndex * weekHeight;\n    if (Math.abs(targetTop - scrollTop) > 5) {\n      // Small additional delay on top of SCROLL_DEBOUNCE for a natural feel\n      const snapTimeout = setTimeout(() => {\n        scrollToDate(targetDate, true);\n      }, 200);\n      return () => clearTimeout(snapTimeout);\n    }\n  }, [\n    isScrolling,\n    snapToMonth,\n    isNavigating,\n    isInitialized,\n    scrollTop,\n    weekHeight,\n    weeksData,\n    scrollToDate,\n  ]);\n\n  // Cleanup\n  useEffect(\n    () => () => {\n      if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);\n    },\n    []\n  );\n\n  return {\n    scrollTop,\n    containerHeight,\n    currentMonth,\n    currentYear,\n    isScrolling,\n    isNavigating,\n    virtualData,\n    scrollElementRef,\n    handleScroll,\n    scrollToDate,\n    handlePreviousMonth,\n    handleNextMonth,\n    handleToday,\n    setScrollTop,\n    setContainerHeight,\n    setCurrentMonth,\n    setCurrentYear,\n    setIsScrolling,\n    cache: weekDataCache.current,\n    scrollElementRefCallback,\n    weeksData,\n  };\n};\n"
  },
  {
    "path": "packages/core/src/hooks/virtualScroll/useVirtualScroll.ts",
    "content": "import { JSX } from 'preact';\nimport {\n  useState,\n  useEffect,\n  useMemo,\n  useRef,\n  useCallback,\n} from 'preact/hooks';\n\nimport {\n  UseVirtualScrollProps,\n  UseVirtualScrollReturn,\n  VirtualItem,\n} from '@/types';\n\n// Virtual scroll configuration\nexport const VIRTUAL_SCROLL_CONFIG = {\n  // Basic configuration\n  OVERSCAN: 0, // Number of additional years to render before and after (0 for best performance)\n  BUFFER_SIZE: 20, // Cache size (reduced for better performance)\n  MIN_YEAR: 1970, // Minimum year\n  MAX_YEAR: 2100, // Maximum year\n\n  // Performance configuration\n  SCROLL_THROTTLE: 8, // Scroll throttle for smooth scrolling (8ms = ~120fps)\n  SCROLL_DEBOUNCE: 100, // Scroll end detection (reduced for faster response)\n  CACHE_CLEANUP_THRESHOLD: 30, // Cache cleanup threshold\n\n  // Responsive configuration - initial minimum height for year items\n  // Actual height will be measured by ResizeObserver and adjusted automatically\n  // These values are just initial estimates for the first render\n  MOBILE_YEAR_HEIGHT: 1200, // Mobile: initial min height\n  TABLET_YEAR_HEIGHT: 900, // Tablet: initial min height\n  YEAR_HEIGHT: 700, // Desktop: initial min height\n} as const;\n\n// Performance monitoring\nlet metrics = {\n  scrollEvents: 0,\n  renderTime: [] as number[],\n  cacheHits: 0,\n  cacheMisses: 0,\n  startTime: Date.now(),\n  frameDrops: 0,\n  avgScrollDelta: 0,\n  totalScrollDistance: 0,\n};\n\nexport const trackScrollEvent = (scrollDelta: number = 0) => {\n  metrics.scrollEvents++;\n  metrics.totalScrollDistance += Math.abs(scrollDelta);\n  metrics.avgScrollDelta = metrics.totalScrollDistance / metrics.scrollEvents;\n};\n\nexport const trackRenderTime = (time: number) => {\n  metrics.renderTime.push(time);\n  if (time > 16.67) {\n    // Exceeds one frame time\n    metrics.frameDrops++;\n  }\n  // Keep only the last 100 render times\n  if (metrics.renderTime.length > 100) {\n    metrics.renderTime.shift();\n  }\n};\n\nexport const trackCacheHit = () => {\n  metrics.cacheHits++;\n};\n\nexport const trackCacheMiss = () => {\n  metrics.cacheMisses++;\n};\n\nexport const getMetrics = () => {\n  const avgRenderTime =\n    metrics.renderTime.length > 0\n      ? metrics.renderTime.reduce((a, b) => a + b, 0) /\n        metrics.renderTime.length\n      : 0;\n\n  const cacheHitRate =\n    metrics.cacheHits + metrics.cacheMisses > 0\n      ? (metrics.cacheHits / (metrics.cacheHits + metrics.cacheMisses)) * 100\n      : 0;\n\n  const uptime = Date.now() - metrics.startTime;\n  const fps = avgRenderTime > 0 ? 1000 / avgRenderTime : 0;\n\n  return {\n    scrollEvents: metrics.scrollEvents,\n    avgRenderTime: Math.round(avgRenderTime * 100) / 100,\n    cacheHitRate: Math.round(cacheHitRate * 100) / 100,\n    uptime: Math.round(uptime / 1000),\n    scrollEventsPerSecond:\n      Math.round((metrics.scrollEvents / (uptime / 1000)) * 100) / 100,\n    estimatedFPS: Math.round(fps),\n    frameDrops: metrics.frameDrops,\n    avgScrollDelta: Math.round(metrics.avgScrollDelta * 100) / 100,\n  };\n};\n\nexport const resetMetrics = () => {\n  metrics = {\n    scrollEvents: 0,\n    renderTime: [],\n    cacheHits: 0,\n    cacheMisses: 0,\n    startTime: Date.now(),\n    frameDrops: 0,\n    avgScrollDelta: 0,\n    totalScrollDistance: 0,\n  };\n};\n\n// Responsive configuration Hook\nexport const useResponsiveConfig = () => {\n  const [config, setConfig] = useState<{\n    yearHeight:\n      | typeof VIRTUAL_SCROLL_CONFIG.YEAR_HEIGHT\n      | typeof VIRTUAL_SCROLL_CONFIG.MOBILE_YEAR_HEIGHT\n      | typeof VIRTUAL_SCROLL_CONFIG.TABLET_YEAR_HEIGHT;\n    screenSize: 'mobile' | 'tablet' | 'desktop';\n  }>({\n    yearHeight: VIRTUAL_SCROLL_CONFIG.YEAR_HEIGHT,\n    screenSize: 'desktop',\n  });\n\n  useEffect(() => {\n    const updateConfig = () => {\n      const width = window.innerWidth;\n      if (width < 640) {\n        // Only trigger mobile layout on very small screens (< 640px)\n        setConfig({\n          yearHeight: VIRTUAL_SCROLL_CONFIG.MOBILE_YEAR_HEIGHT,\n          screenSize: 'mobile',\n        });\n      } else if (width < 900) {\n        // Tablet layout for medium screens (640px - 900px)\n        setConfig({\n          yearHeight: VIRTUAL_SCROLL_CONFIG.TABLET_YEAR_HEIGHT,\n          screenSize: 'tablet',\n        });\n      } else {\n        // Desktop layout for larger screens (>= 900px)\n        setConfig({\n          yearHeight: VIRTUAL_SCROLL_CONFIG.YEAR_HEIGHT,\n          screenSize: 'desktop',\n        });\n      }\n    };\n\n    updateConfig();\n    window.addEventListener('resize', updateConfig);\n    return () => window.removeEventListener('resize', updateConfig);\n  }, []);\n\n  return config;\n};\n\n// Virtual scroll main Hook\nexport const useVirtualScroll = ({\n  currentDate,\n  yearHeight,\n  onCurrentYearChange,\n}: UseVirtualScrollProps): UseVirtualScrollReturn => {\n  // State management - start directly from correct position\n  const initialYear = currentDate.getFullYear();\n  const initialIndex = initialYear - VIRTUAL_SCROLL_CONFIG.MIN_YEAR;\n  const initialScrollTop = initialIndex * yearHeight;\n\n  const [scrollTop, setScrollTop] = useState(initialScrollTop);\n  const [containerHeight, setContainerHeight] = useState(600);\n  const [currentYear, setCurrentYear] = useState(initialYear);\n  const [isScrolling, setIsScrolling] = useState(false);\n\n  // References\n  const scrollElementRef = useRef<HTMLDivElement | null>(null);\n  const scrollTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const lastScrollTime = useRef(0);\n  const lastScrollTop = useRef(0);\n  const previousYearHeightRef = useRef(yearHeight);\n  useEffect(() => {\n    if (previousYearHeightRef.current === yearHeight) return;\n    previousYearHeightRef.current = yearHeight;\n\n    const targetTop =\n      (currentYear - VIRTUAL_SCROLL_CONFIG.MIN_YEAR) * yearHeight;\n\n    setScrollTop(targetTop);\n    lastScrollTop.current = targetTop;\n\n    if (scrollElementRef.current) {\n      scrollElementRef.current.scrollTop = targetTop;\n    }\n  }, [yearHeight, currentYear]);\n\n  // Virtual scroll calculation - optimize initial render\n  const virtualData = useMemo(() => {\n    const startTime = performance.now();\n\n    const totalYears =\n      VIRTUAL_SCROLL_CONFIG.MAX_YEAR - VIRTUAL_SCROLL_CONFIG.MIN_YEAR + 1;\n    const totalHeight = totalYears * yearHeight;\n\n    // Use current scrollTop, already at correct position initially\n    const startIndex = Math.floor(scrollTop / yearHeight);\n    const endIndex = Math.min(\n      totalYears - 1,\n      Math.ceil((scrollTop + containerHeight) / yearHeight)\n    );\n\n    const bufferStart = Math.max(\n      0,\n      startIndex - VIRTUAL_SCROLL_CONFIG.OVERSCAN\n    );\n    const bufferEnd = Math.min(\n      totalYears - 1,\n      endIndex + VIRTUAL_SCROLL_CONFIG.OVERSCAN\n    );\n\n    const visibleItems: VirtualItem[] = [];\n    for (let i = bufferStart; i <= bufferEnd; i++) {\n      visibleItems.push({\n        index: i,\n        year: VIRTUAL_SCROLL_CONFIG.MIN_YEAR + i,\n        top: i * yearHeight,\n        height: yearHeight,\n      });\n    }\n\n    const renderTime = performance.now() - startTime;\n    trackRenderTime(renderTime);\n\n    return { totalHeight, visibleItems };\n  }, [scrollTop, containerHeight, yearHeight]);\n\n  // Scroll handling - remove initialization check\n  const handleScroll = useCallback(\n    (e: JSX.TargetedEvent<HTMLDivElement, globalThis.Event>) => {\n      const now = performance.now();\n      if (now - lastScrollTime.current < VIRTUAL_SCROLL_CONFIG.SCROLL_THROTTLE)\n        return;\n      lastScrollTime.current = now;\n\n      const element = e.currentTarget;\n      const newScrollTop = element.scrollTop;\n      const scrollDelta = Math.abs(newScrollTop - lastScrollTop.current);\n      lastScrollTop.current = newScrollTop;\n\n      trackScrollEvent(scrollDelta);\n\n      requestAnimationFrame(() => {\n        setScrollTop(newScrollTop);\n\n        // Calculate year at the top of the viewport (for sticky header)\n        const topPos = newScrollTop;\n        const newYear = Math.floor(\n          VIRTUAL_SCROLL_CONFIG.MIN_YEAR + topPos / yearHeight\n        );\n\n        if (\n          newYear !== currentYear &&\n          newYear >= VIRTUAL_SCROLL_CONFIG.MIN_YEAR &&\n          newYear <= VIRTUAL_SCROLL_CONFIG.MAX_YEAR\n        ) {\n          setCurrentYear(newYear);\n          onCurrentYearChange?.(newYear);\n        }\n      });\n\n      setIsScrolling(true);\n      if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);\n      scrollTimeoutRef.current = setTimeout(() => {\n        setIsScrolling(false);\n      }, VIRTUAL_SCROLL_CONFIG.SCROLL_DEBOUNCE);\n    },\n    [containerHeight, currentYear, yearHeight, onCurrentYearChange]\n  );\n\n  // Container size listener - remove complex initialization logic\n  useEffect(() => {\n    const element = scrollElementRef.current;\n    if (!element) return;\n\n    // Immediately set correct scroll position\n    element.scrollTop = initialScrollTop;\n    lastScrollTop.current = initialScrollTop;\n\n    const resizeObserver = new ResizeObserver(([entry]) => {\n      setContainerHeight(entry.contentRect.height);\n    });\n\n    resizeObserver.observe(element);\n    return () => resizeObserver.disconnect();\n  }, [initialScrollTop]);\n\n  // Scroll to specified year\n  const scrollToYear = useCallback(\n    (targetYear: number, smooth = true) => {\n      if (!scrollElementRef.current) return;\n\n      const targetIndex = targetYear - VIRTUAL_SCROLL_CONFIG.MIN_YEAR;\n      const targetTop = targetIndex * yearHeight;\n\n      scrollElementRef.current.scrollTo({\n        top: Math.max(0, targetTop),\n        behavior: smooth ? 'smooth' : 'auto',\n      });\n    },\n    [yearHeight]\n  );\n\n  // Navigation functions\n  const handlePreviousYear = useCallback(() => {\n    const target = Math.max(VIRTUAL_SCROLL_CONFIG.MIN_YEAR, currentYear - 1);\n    setCurrentYear(target);\n    scrollToYear(target);\n  }, [currentYear, scrollToYear]);\n\n  const handleNextYear = useCallback(() => {\n    const target = Math.min(VIRTUAL_SCROLL_CONFIG.MAX_YEAR, currentYear + 1);\n    setCurrentYear(target);\n    scrollToYear(target);\n  }, [currentYear, scrollToYear]);\n\n  const handleToday = useCallback(() => {\n    const todayYear = new Date().getFullYear();\n    setCurrentYear(todayYear);\n    scrollToYear(todayYear);\n  }, [scrollToYear]);\n\n  // Cleanup\n  useEffect(\n    () => () => {\n      if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);\n    },\n    []\n  );\n\n  return {\n    scrollTop,\n    containerHeight,\n    currentYear,\n    isScrolling,\n    virtualData,\n    scrollElementRef,\n    handleScroll,\n    scrollToYear,\n    handlePreviousYear,\n    handleNextYear,\n    handleToday,\n    setScrollTop,\n    setContainerHeight,\n    setCurrentYear,\n    setIsScrolling,\n  };\n};\n"
  },
  {
    "path": "packages/core/src/index.ts",
    "content": "// Core library entry file\n\n// Calendar App and Registry\nexport { CalendarApp } from './core/CalendarApp';\nexport type { ICalendarApp } from './types';\nexport { CalendarRegistry } from './core/calendarRegistry';\n\n// Renderer\nexport { CalendarRenderer } from './renderer/CalendarRenderer';\nexport { CustomRenderingStore } from './renderer/CustomRenderingStore';\nexport type { CustomRendering } from './renderer/CustomRenderingStore';\n\n// Types\nexport * from './types';\n\n// Utils\nexport * from './utils';\nexport { subscribeCalendar } from './utils/subscriptionUtils';\nexport type { SubscribeResult } from './utils/subscriptionUtils';\n\n// Locale\nexport * from './locale';\n\n// Factories\nexport * from './factories';\n\n// Plugins\nexport { createEventsPlugin } from './plugins/eventsPlugin';\nexport {\n  registerDragImplementation,\n  useDragForView,\n} from './plugins/dragBridge';\nexport {\n  registerSidebarImplementation,\n  useSidebarBridge,\n} from './plugins/sidebarBridge';\nexport type { SidebarBridgeReturn } from './plugins/sidebarBridge';\n\n// Context Menu Primitives\nexport {\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuLabel,\n  ContextMenuColorPicker,\n  GridContextMenu,\n  EventContextMenu,\n} from './components/contextMenu';\n\n// Calendar Registry helpers\nexport { getCalendarColorsForHex } from './core/calendarRegistry';\n\n// Common Components\nexport { LoadingButton } from './components/common/LoadingButton';\nexport { BlossomColorPicker } from './components/common/BlossomColorPicker';\nexport { DefaultColorPicker } from './components/common/DefaultColorPicker';\nexport { RangePicker as DayflowRangePicker } from '@dayflow/ui-range-picker';\nexport type { RangePickerProps, ZonedRange } from '@dayflow/ui-range-picker';\nexport { MiniCalendar } from './components/common/MiniCalendar';\nexport { CreateCalendarDialog } from './components/common/CreateCalendarDialog';\nexport { default as DefaultEventDetailPanel } from './components/common/DefaultEventDetailPanel';\nexport { default as DefaultEventDetailDialog } from './components/common/DefaultEventDetailDialog';\nexport { ContentSlot } from './renderer/ContentSlot';\nexport { CalendarEvent } from './components/calendarEvent';\nexport type { CalendarEventProps } from './components/calendarEvent/types';\nexport { EventLayoutCalculator } from './components/eventLayout';\n\n// Icons\nexport {\n  PanelRightClose,\n  PanelRightOpen,\n  ChevronRight,\n  ChevronDown,\n  Check,\n  ChevronsUpDown,\n  Plus,\n  AudioLines,\n  Loader2,\n  AlertCircle,\n} from './components/common/Icons';\n\n// Sidebar classNames\nexport {\n  sidebarContainer,\n  sidebarHeader,\n  sidebarHeaderToggle,\n  sidebarHeaderTitle,\n  cancelButton,\n  calendarPickerDropdown,\n} from './styles/classNames';\n\n// Year view utilities\nexport {\n  buildFixedWeekMonthsData,\n  getFixedWeekTotalColumns,\n  groupDaysIntoRows,\n  analyzeMultiDayEventsForRow,\n} from './components/yearView/utils';\nexport { getEventIcon } from './components/monthView/util';\nexport type {\n  FixedWeekMonthData,\n  MonthEventSegment,\n  YearMultiDaySegment,\n} from './components/yearView/utils';\n\n// Preact interop (re-export so plugins use the same preact instance as core)\nexport { createPortal } from 'preact/compat';\n"
  },
  {
    "path": "packages/core/src/locale/LocaleContext.tsx",
    "content": "import { createContext } from 'preact';\n\nimport type { LocaleCode, TranslationKey } from './types';\n\nexport interface LocaleContextValue {\n  locale: LocaleCode;\n  t: (key: TranslationKey, vars?: Record<string, string>) => string;\n  getWeekDaysLabels: (\n    locale: string,\n    format?: 'long' | 'short' | 'narrow',\n    startOfWeek?: number\n  ) => string[];\n  getMonthLabels: (\n    locale: string,\n    format?: 'long' | 'short' | 'narrow' | 'numeric' | '2-digit'\n  ) => string[];\n  isDefault?: boolean;\n}\n\nexport const LocaleContext = createContext<LocaleContextValue>({\n  locale: 'en-US',\n  t: key => key,\n  getWeekDaysLabels: () => [],\n  getMonthLabels: () => [],\n  isDefault: true,\n});\n"
  },
  {
    "path": "packages/core/src/locale/LocaleProvider.tsx",
    "content": "import { ComponentChildren } from 'preact';\nimport { useMemo } from 'preact/hooks';\n\nimport { getWeekDaysLabels, getMonthLabels } from './intl';\nimport { LocaleContext } from './LocaleContext';\nimport { t as translate } from './translator';\nimport type {\n  LocaleCode,\n  LocaleMessages,\n  TranslationKey,\n  Locale,\n} from './types';\nimport { isValidLocale } from './utils';\n\nexport interface LocaleProviderProps {\n  locale?: LocaleCode | Locale;\n  messages?: LocaleMessages;\n  children?: ComponentChildren;\n}\n\nexport const LocaleProvider = ({\n  locale = 'en-US',\n  messages,\n  children,\n}: LocaleProviderProps) => {\n  const resolvedLocale = useMemo(() => {\n    if (typeof locale === 'string') {\n      const code = isValidLocale(locale) ? locale : 'en-US';\n      return { code, messages: undefined };\n    }\n\n    // If it's a Locale object, ensure its code is valid\n    if (\n      locale &&\n      typeof locale !== 'string' &&\n      !isValidLocale((locale as Locale).code)\n    ) {\n      return { ...(locale as Locale), code: 'en-US' as LocaleCode };\n    }\n\n    return locale || { code: 'en-US' };\n  }, [locale]);\n\n  const value = useMemo(() => {\n    const currentCode = resolvedLocale.code;\n\n    return {\n      locale: currentCode,\n      t: (key: TranslationKey, vars?: Record<string, string>) => {\n        // Resolve text: 1. Custom messages -> 2. Locale object messages -> 3. Global fallback\n        let text =\n          messages?.[key] ??\n          resolvedLocale.messages?.[key] ??\n          translate(key, currentCode);\n\n        // 4. Replace variables if any\n        if (vars) {\n          Object.entries(vars).forEach(([k, v]) => {\n            text = text.replaceAll(new RegExp(`{${k}}`, 'g'), v);\n          });\n        }\n\n        return text;\n      },\n      getWeekDaysLabels,\n      getMonthLabels,\n      isDefault: false,\n    };\n  }, [resolvedLocale, messages]);\n\n  return (\n    <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/locale/index.ts",
    "content": "export * from './types';\nexport * from './translator';\nexport * from './intl';\nexport * from './locales';\nexport * from './utils';\nexport * from './LocaleContext';\nexport * from './useLocale';\nexport * from './LocaleProvider';\n"
  },
  {
    "path": "packages/core/src/locale/intl.ts",
    "content": "/**\n * Encapsulates Intl API calls for standard calendar terms.\n */\nexport function getIntlLabel(\n  key: 'today' | 'day' | 'week' | 'month' | 'year',\n  locale: string\n): string | null {\n  try {\n    if (key === 'today') {\n      const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });\n      return (\n        rtf.formatToParts(0, 'day').find(p => p.type === 'literal')?.value ??\n        null\n      );\n    }\n\n    if (key === 'week') {\n      const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'always' });\n      return (\n        rtf.formatToParts(1, 'week').find(p => p.type === 'unit')?.value ?? null\n      );\n    }\n\n    const dn = new Intl.DisplayNames(locale, { type: 'dateTimeField' });\n    return dn.of(key) ?? null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Capitalizes the first letter of a string.\n */\nexport function capitalize(str: string): string {\n  return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n/**\n * Get localized weekday labels starting from startOfWeek (0: Sun, 1: Mon, etc.)\n */\nexport const getWeekDaysLabels = (\n  locale: string,\n  format: 'long' | 'short' | 'narrow' = 'short',\n  startOfWeek: number = 1\n): string[] => {\n  const labels: string[] = [];\n  // Use a known date (2024-01-07 was a Sunday)\n  const baseDate = new Date(2024, 0, 7);\n  for (let i = 0; i < 7; i++) {\n    const date = new Date(baseDate);\n    date.setDate(baseDate.getDate() + startOfWeek + i);\n    try {\n      labels.push(date.toLocaleDateString(locale, { weekday: format }));\n    } catch {\n      labels.push(date.toLocaleDateString('en-US', { weekday: format }));\n    }\n  }\n  return labels;\n};\n\n/**\n * Get localized month labels\n */\nexport const getMonthLabels = (\n  locale: string,\n  format: 'long' | 'short' | 'narrow' | 'numeric' | '2-digit' = 'long'\n): string[] => {\n  const labels: string[] = [];\n  for (let i = 0; i < 12; i++) {\n    const date = new Date(2024, i, 1);\n    try {\n      labels.push(date.toLocaleDateString(locale, { month: format }));\n    } catch {\n      labels.push(date.toLocaleDateString('en-US', { month: format }));\n    }\n  }\n  return labels;\n};\n"
  },
  {
    "path": "packages/core/src/locale/locales/en.ts",
    "content": "import type { Locale } from '@/locale/types';\n\nconst en: Locale = {\n  code: 'en-US',\n  messages: {\n    allDay: 'All day',\n    noEvents: 'No events for this day',\n    more: 'more',\n    eventTitle: 'Event Title',\n    dateRange: 'Date Range',\n    timeRange: 'Time Range',\n    note: 'Note',\n    addNotePlaceholder: 'Add a note...',\n    setAsAllDay: 'Set as All-day',\n    setAsTimed: 'Set as Timed Event',\n    delete: 'Delete',\n    confirm: 'Confirm',\n    cancel: 'Cancel',\n    today: 'Today',\n    day: 'Day',\n    week: 'Week',\n    month: 'Month',\n    year: 'Year',\n    newEvent: 'New Event',\n    newAllDayEvent: 'New All-day Event',\n    newCalendarEvent: 'New {calendarName} Event',\n    newAllDayCalendarEvent: 'New {calendarName} All-day Event',\n    save: 'Save',\n    deleteCalendar: 'Delete {calendarName}?',\n    deleteCalendarMessage:\n      'Do you want to delete {calendarName} or merge its events into another existing calendar?',\n    merge: 'Merge',\n    confirmDeleteTitle:\n      'Are you sure you want to delete the calendar {calendarName}?',\n    confirmDeleteMessage:\n      'If you delete this calendar, all events associated with the calendar will also be deleted.',\n    mergeConfirmTitle: 'Merge {sourceName} with {targetName}?',\n    mergeConfirmMessage:\n      'Are you sure you want to merge {sourceName} with {targetName}?\\nDoing so will move all the events from {sourceName} to {targetName} and {sourceName} will be deleted.\\nThis cannot be undone.',\n    expandSidebar: 'Expand calendar sidebar',\n    collapseSidebar: 'Collapse calendar sidebar',\n    calendars: 'Calendars',\n    createCalendar: 'Create New Calendar',\n    calendarNamePlaceholder: 'e.g. Work',\n    customColor: 'Custom Color...',\n    create: 'Create',\n    calendarOptions: 'Calendar Options',\n    untitled: 'Untitled',\n    search: 'Search',\n    noResults: 'No results found',\n    calendar: 'Calendar',\n    starts: 'Starts',\n    ends: 'Ends',\n    notes: 'Notes',\n    titlePlaceholder: 'Title',\n    notesPlaceholder: 'Notes',\n    editEvent: 'Edit Event',\n    viewEvent: 'View Event',\n    done: 'Done',\n    quickCreateEvent: 'Quick Create Event',\n    quickCreatePlaceholder: 'Movie at 7pm on Friday',\n    noSuggestions: 'Type to create',\n    newCalendar: 'New Calendar',\n    refreshAll: 'Refresh All',\n    tomorrow: 'Tomorrow',\n    importCalendar: 'Import Calendar',\n    exportCalendar: 'Export Calendar',\n    addSchedule: 'Add Schedule',\n    importCalendarMessage:\n      'This calendar contains new events. Please select a target calendar.',\n    ok: 'OK',\n    cut: 'Cut',\n    copy: 'Copy',\n    pasteHere: 'Paste Here',\n    eventSummary: 'Summary',\n    subscribeCalendar: 'Subscribe to Calendar',\n    subscribeCalendarTitle:\n      'Enter the URL of the calendar you want to subscribe to.',\n    calendarUrl: 'Calendar URL',\n    calendarUrlPlaceholder: 'https://example.com/calendar.ics',\n    subscribe: 'Subscribe',\n    fetchingCalendar: 'Fetching calendar...',\n    subscribeError:\n      'Failed to fetch calendar. Please check the URL and try again.',\n  },\n};\n\nexport default en;\n"
  },
  {
    "path": "packages/core/src/locale/locales/index.ts",
    "content": "import { Locale } from '@/locale/types';\n\nimport en from './en';\n\nexport { en };\n\n/**\n * Global locale registry for the core library.\n * Default includes English.\n */\nexport const LOCALES: Record<string, Locale> = {\n  en,\n};\n\nexport type SupportedLang = string;\n\n/**\n * Registers a new locale in the global registry.\n * This allows plugins to provide additional translations.\n *\n * @param locale The locale object to register\n */\nexport function registerLocale(locale: Locale) {\n  const lang = locale.code.split('-')[0].toLowerCase();\n  LOCALES[lang] = locale;\n}\n"
  },
  {
    "path": "packages/core/src/locale/translator.ts",
    "content": "import { getIntlLabel, capitalize } from './intl';\nimport { LOCALES } from './locales';\nimport type { TranslationKey, LocaleCode } from './types';\nimport { normalizeLocale } from './utils';\n\n/**\n * Core translation function.\n * 1. Try Intl API for standard terms.\n * 2. Fall back to language-specific dictionary.\n * 3. Fall back to English dictionary.\n * 4. Fall back to the key itself.\n */\nexport function t(key: TranslationKey, locale: LocaleCode = 'en-US'): string {\n  // 1. Try Intl API for specific keys\n  if (['today', 'day', 'week', 'month', 'year'].includes(key)) {\n    const intl = getIntlLabel(key as 'day' | 'week' | 'month' | 'year', locale);\n    if (intl) return capitalize(intl);\n  }\n\n  // 2. Dictionary Lookup\n  const lang = normalizeLocale(locale);\n  const localeObj = LOCALES[lang];\n  const text = localeObj?.messages?.[key];\n\n  if (text) return text;\n\n  // 3. Fallback to English\n  const enMessages = LOCALES.en.messages;\n  return enMessages?.[key] || key;\n}\n"
  },
  {
    "path": "packages/core/src/locale/types.ts",
    "content": "export type LocaleCode = string;\n\nexport type TranslationKey =\n  | 'allDay'\n  | 'noEvents'\n  | 'more'\n  | 'eventTitle'\n  | 'dateRange'\n  | 'timeRange'\n  | 'note'\n  | 'addNotePlaceholder'\n  | 'setAsAllDay'\n  | 'setAsTimed'\n  | 'delete'\n  | 'confirm'\n  | 'cancel'\n  | 'today'\n  | 'day'\n  | 'week'\n  | 'month'\n  | 'year'\n  | 'newEvent'\n  | 'newAllDayEvent'\n  | 'newCalendarEvent'\n  | 'newAllDayCalendarEvent'\n  | 'save'\n  | 'deleteCalendar'\n  | 'deleteCalendarMessage'\n  | 'merge'\n  | 'confirmDeleteTitle'\n  | 'confirmDeleteMessage'\n  | 'mergeConfirmTitle'\n  | 'mergeConfirmMessage'\n  | 'expandSidebar'\n  | 'collapseSidebar'\n  | 'calendars'\n  | 'createCalendar'\n  | 'calendarNamePlaceholder'\n  | 'customColor'\n  | 'create'\n  | 'calendarOptions'\n  | 'untitled'\n  | 'search'\n  | 'noResults'\n  | 'calendar'\n  | 'starts'\n  | 'ends'\n  | 'notes'\n  | 'titlePlaceholder'\n  | 'notesPlaceholder'\n  | 'editEvent'\n  | 'viewEvent'\n  | 'done'\n  | 'quickCreateEvent'\n  | 'quickCreatePlaceholder'\n  | 'noSuggestions'\n  | 'newCalendar'\n  | 'refreshAll'\n  | 'tomorrow'\n  | 'importCalendar'\n  | 'exportCalendar'\n  | 'addSchedule'\n  | 'importCalendarMessage'\n  | 'ok'\n  | 'cut'\n  | 'copy'\n  | 'pasteHere'\n  | 'eventSummary'\n  | 'subscribeCalendar'\n  | 'subscribeCalendarTitle'\n  | 'calendarUrl'\n  | 'calendarUrlPlaceholder'\n  | 'subscribe'\n  | 'fetchingCalendar'\n  | 'subscribeError'\n  | 'calendarAlreadySubscribed';\n\nexport type LocaleDict = Partial<Record<TranslationKey, string>>;\nexport type LocaleMessages = Partial<Record<TranslationKey, string>>;\n\nexport interface Locale {\n  code: string;\n  messages: LocaleDict;\n}\n"
  },
  {
    "path": "packages/core/src/locale/useLocale.ts",
    "content": "import { useContext } from 'preact/hooks';\n\nimport { LocaleContext, LocaleContextValue } from './LocaleContext';\n\n/**\n * Hook to use the locale context in functional components.\n */\nexport function useLocale(): LocaleContextValue {\n  return useContext(LocaleContext);\n}\n"
  },
  {
    "path": "packages/core/src/locale/utils.ts",
    "content": "import { LOCALES, SupportedLang } from './locales';\n\n/**\n * Normalizes a locale string to a supported language code.\n * e.g., 'en-US' -> 'en', 'zh-CN' -> 'zh'\n */\nexport function normalizeLocale(locale: string): SupportedLang {\n  const lang = locale.split('-')[0].toLowerCase();\n\n  if (lang in LOCALES) {\n    return lang as SupportedLang;\n  }\n\n  return 'en';\n}\n\n/**\n * Checks if a string is a valid locale identifier.\n */\nexport function isValidLocale(locale: string): boolean {\n  try {\n    const _ = new Intl.DateTimeFormat(locale);\n    return true;\n  } catch {\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/plugins/dragBridge.ts",
    "content": "import type { ICalendarApp, DragHookOptions, DragHookReturn } from '@/types';\n\ntype DragForViewFn = (\n  app: ICalendarApp,\n  options: DragHookOptions\n) => DragHookReturn;\n\nlet impl: DragForViewFn | null = null;\n\nexport function registerDragImplementation(fn: DragForViewFn) {\n  impl = fn;\n}\n\nconst NO_OP: DragHookReturn = {\n  handleMoveStart: () => {\n    /* noop */\n  },\n  handleCreateStart: () => {\n    /* noop */\n  },\n  handleResizeStart: undefined,\n  handleCreateAllDayEvent: () => {\n    /* noop */\n  },\n  dragState: {\n    active: false,\n    mode: null,\n    eventId: null,\n    targetDate: null,\n    startDate: null,\n    endDate: null,\n  },\n  isDragging: false,\n};\n\nexport function useDragForView(\n  app: ICalendarApp,\n  options: DragHookOptions\n): DragHookReturn {\n  if (impl) return impl(app, options);\n  return NO_OP;\n}\n"
  },
  {
    "path": "packages/core/src/plugins/eventsPlugin.ts",
    "content": "// Event management plugin\nimport {\n  CalendarPlugin,\n  ICalendarApp,\n  Event,\n  EventsService,\n  EventsPluginConfig,\n} from '@/types';\nimport { recalculateEventDays } from '@/utils';\nimport { temporalToDate } from '@/utils/temporal';\n\nexport const defaultEventsConfig: EventsPluginConfig = {\n  enableAutoRecalculate: true,\n  enableValidation: true,\n  defaultEvents: [],\n  maxEventsPerDay: 50,\n};\n\n// Utility function to get current week start time\nfunction getCurrentWeekStart(date: Date): Date {\n  const day = date.getDay();\n  const diff = date.getDate() - day + (day === 0 ? -6 : 1);\n  const monday = new Date(date);\n  monday.setDate(diff);\n  monday.setHours(0, 0, 0, 0);\n  return monday;\n}\n\nexport function createEventsPlugin(\n  config: EventsPluginConfig = {}\n): CalendarPlugin {\n  const finalConfig = { ...defaultEventsConfig, ...config };\n  let app: ICalendarApp;\n\n  const eventsService: EventsService = {\n    getAll: () => app.getAllEvents(),\n\n    getById: (id: string) => app.getAllEvents().find(event => event.id === id),\n\n    add: (event: Event) => {\n      // Validate event\n      if (finalConfig.enableValidation) {\n        const errors = eventsService.validateEvent(event);\n        if (errors.length > 0) {\n          throw new Error(`Event validation failed: ${errors.join(', ')}`);\n        }\n      }\n\n      // Check daily event count limit\n      if (finalConfig.maxEventsPerDay && event.day !== undefined) {\n        const dayEvents = eventsService.getByDay(\n          event.day,\n          app.getCurrentDate()\n        );\n        if (dayEvents.length >= finalConfig.maxEventsPerDay) {\n          throw new Error(\n            `Maximum events per day (${finalConfig.maxEventsPerDay}) exceeded`\n          );\n        }\n      }\n\n      app.addEvent(event);\n\n      // Automatically recalculate day field\n      if (finalConfig.enableAutoRecalculate) {\n        const currentWeekStart = getCurrentWeekStart(app.getCurrentDate());\n        // Update day field for all events\n        app.state.events = recalculateEventDays(\n          app.getAllEvents(),\n          currentWeekStart\n        );\n      }\n    },\n\n    update: async (id: string, updates: Partial<Event>) => {\n      const existingEvent = eventsService.getById(id);\n      if (!existingEvent) {\n        throw new Error(`Event with id ${id} not found`);\n      }\n\n      const updatedEvent = { ...existingEvent, ...updates };\n\n      // Validate updated event\n      if (finalConfig.enableValidation) {\n        const errors = eventsService.validateEvent(updatedEvent);\n        if (errors.length > 0) {\n          throw new Error(`Event validation failed: ${errors.join(', ')}`);\n        }\n      }\n\n      await app.updateEvent(id, updates);\n\n      // Automatically recalculate day field\n      if (finalConfig.enableAutoRecalculate) {\n        const currentWeekStart = getCurrentWeekStart(app.getCurrentDate());\n        app.state.events = recalculateEventDays(\n          app.getAllEvents(),\n          currentWeekStart\n        );\n      }\n    },\n\n    delete: async (id: string) => {\n      await app.deleteEvent(id);\n    },\n\n    getByDate: (date: Date) =>\n      app.getAllEvents().filter(event => {\n        const eventDate = temporalToDate(event.start);\n        eventDate.setHours(0, 0, 0, 0);\n        const targetDate = new Date(date);\n        targetDate.setHours(0, 0, 0, 0);\n        return eventDate.getTime() === targetDate.getTime();\n      }),\n\n    getByDateRange: (startDate: Date, endDate: Date) =>\n      app.getAllEvents().filter(event => {\n        // Check if event start or end time is within range\n        const eventStart = temporalToDate(event.start);\n        const eventEnd = temporalToDate(event.end);\n        return (\n          (eventStart >= startDate && eventStart <= endDate) ||\n          (eventEnd >= startDate && eventEnd <= endDate) ||\n          (eventStart <= startDate && eventEnd >= endDate)\n        );\n      }),\n\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    getByDay: (dayIndex: number, _weekStart: Date) =>\n      app.getAllEvents().filter(event => event.day === dayIndex),\n\n    getAllDayEvents: (dayIndex: number, events: Event[]) =>\n      events.filter(event => event.day === dayIndex && event.allDay),\n\n    recalculateEventDays: (events: Event[], weekStart: Date) =>\n      recalculateEventDays(events, weekStart),\n\n    validateEvent: (event: Partial<Event>) => {\n      const errors: string[] = [];\n\n      if (!event.title || event.title.trim() === '') {\n        errors.push('Event title is required');\n      }\n\n      if (!event.start) {\n        errors.push('Event start time is required');\n      }\n\n      if (!event.end) {\n        errors.push('Event end time is required');\n      }\n\n      if (\n        event.start &&\n        event.end &&\n        !event.allDay &&\n        event.start >= event.end\n      ) {\n        errors.push('Start time must be before end time');\n      }\n\n      // Commented out day range check, because month view needs to support cross-week events,\n      // day value may exceed 0-6 range\n      // if (event.day !== undefined && (event.day < 0 || event.day > 6)) {\n      //   errors.push('Day must be between 0 and 6');\n      // }\n\n      return errors;\n    },\n\n    filterEvents: (events: Event[], filter: (event: Event) => boolean) =>\n      events.filter(filter),\n  };\n\n  return {\n    name: 'events',\n    config: finalConfig,\n    install: (calendarApp: ICalendarApp) => {\n      app = calendarApp;\n    },\n    api: eventsService,\n  };\n}\n"
  },
  {
    "path": "packages/core/src/plugins/index.ts",
    "content": "// Plugin module export file\nimport { createEventsPlugin } from './eventsPlugin';\n\nexport * from './eventsPlugin';\nexport * from './dragBridge';\n\n// Convenient plugin package creation function\nexport function createStandardPlugins(config?: {\n  events?: Partial<import('../types').EventsPluginConfig>;\n}) {\n  return [createEventsPlugin(config?.events)];\n}\n"
  },
  {
    "path": "packages/core/src/plugins/sidebarBridge.ts",
    "content": "import type { ICalendarApp, TNode } from '@/types';\n\nexport interface SidebarBridgeReturn {\n  enabled: boolean;\n  width: string;\n  isCollapsed: boolean;\n  toggleCollapsed: () => void;\n  miniWidth: string;\n  content: TNode | null;\n  extraContent: TNode | null;\n  safeAreaLeft: number;\n}\n\ntype SidebarBridgeFn = (app: ICalendarApp) => SidebarBridgeReturn;\n\nlet impl: SidebarBridgeFn | null = null;\n\nexport function registerSidebarImplementation(fn: SidebarBridgeFn) {\n  impl = fn;\n}\n\nconst NO_OP: SidebarBridgeReturn = {\n  enabled: false,\n  width: '0px',\n  isCollapsed: false,\n  toggleCollapsed: () => {\n    /* noop */\n  },\n  miniWidth: '0px',\n  content: null,\n  extraContent: null,\n  safeAreaLeft: 0,\n};\n\nexport function useSidebarBridge(app: ICalendarApp): SidebarBridgeReturn {\n  if (impl && app.hasPlugin('sidebar')) return impl(app);\n  return NO_OP;\n}\n"
  },
  {
    "path": "packages/core/src/renderer/CalendarRenderer.tsx",
    "content": "import { render, h } from 'preact';\n\nimport { ICalendarApp } from '@/types';\nimport { isDeepEqual } from '@/utils/compareUtils';\n\nimport { CalendarRoot } from './CalendarRoot';\nimport { CustomRenderingContext } from './CustomRenderingContext';\nimport { CustomRenderingStore } from './CustomRenderingStore';\n\nexport class CalendarRenderer {\n  private container: HTMLElement | null = null;\n  private customRenderingStore: CustomRenderingStore;\n  private unsubscribe: (() => void) | null = null;\n  private renderRequested = false;\n  private extraProps: Record<string, unknown> = {};\n\n  constructor(\n    private app: ICalendarApp,\n    initialOverrides?: string[]\n  ) {\n    this.customRenderingStore = new CustomRenderingStore(initialOverrides);\n    if (initialOverrides) {\n      this.app.setOverrides(initialOverrides);\n    }\n    // Subscribe to app state changes to trigger Preact re-renders\n    this.unsubscribe = app.subscribe(() => this.requestRender());\n  }\n\n  setProps(props: Record<string, unknown>): void {\n    if (isDeepEqual(this.extraProps, props)) return;\n    this.extraProps = props;\n    this.requestRender();\n  }\n\n  private requestRender(): void {\n    if (this.renderRequested) return;\n    this.renderRequested = true;\n    requestAnimationFrame(() => {\n      this.render();\n      this.renderRequested = false;\n    });\n  }\n\n  /**\n   * Mount the calendar to a DOM container.\n   * Renders synchronously so the content is in the DOM before the first paint.\n   * Subsequent updates (triggered by app state changes) still use requestAnimationFrame.\n   */\n  mount(container: HTMLElement): void {\n    this.container = container;\n    this.render();\n  }\n\n  /**\n   * Unmount the calendar and cleanup.\n   */\n  unmount(): void {\n    if (this.container) {\n      render(null, this.container);\n      this.container = null;\n    }\n    if (this.unsubscribe) {\n      this.unsubscribe();\n      this.unsubscribe = null;\n    }\n  }\n\n  getCustomRenderingStore(): CustomRenderingStore {\n    return this.customRenderingStore;\n  }\n\n  private render(): void {\n    if (!this.container) return;\n\n    render(\n      h(\n        CustomRenderingContext.Provider,\n        {\n          value: this.customRenderingStore,\n        },\n        h(CalendarRoot, { app: this.app, ...this.extraProps })\n      ),\n      this.container\n    );\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderer/CalendarRoot.tsx",
    "content": "import { h, ComponentChildren } from 'preact';\nimport { createPortal } from 'preact/compat';\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useContext,\n  useState,\n} from 'preact/hooks';\n\nimport CalendarHeader from '@/components/common/CalendarHeader';\nimport { CreateCalendarDialog } from '@/components/common/CreateCalendarDialog';\nimport DefaultEventDetailDialog from '@/components/common/DefaultEventDetailDialog';\nimport { QuickCreateEventPopup } from '@/components/common/QuickCreateEventPopup';\nimport { MobileEventDrawer } from '@/components/mobileEventDrawer';\nimport MobileSearchDialog from '@/components/search/MobileSearchDialog';\nimport SearchDrawer from '@/components/search/SearchDrawer';\nimport { ThemeProvider } from '@/contexts/ThemeContext';\nimport { LocaleProvider } from '@/locale/LocaleProvider';\nimport { LocaleCode, Locale, LocaleMessages } from '@/locale/types';\nimport { useLocale } from '@/locale/useLocale';\nimport { useSidebarBridge } from '@/plugins/sidebarBridge';\nimport {\n  EventDetailDialogRenderer,\n  ICalendarApp,\n  TNode,\n  Event as CalendarEvent,\n  TitleBarSlotProps,\n  MobileEventProps,\n} from '@/types';\nimport { ThemeMode } from '@/types/calendarTypes';\nimport { CalendarSearchProps } from '@/types/search';\n\nimport { ContentSlot } from './ContentSlot';\nimport { CustomRenderingContext } from './CustomRenderingContext';\nimport { useAppSubscription } from './hooks/useAppSubscription';\nimport { useEventDialogController } from './hooks/useEventDialogController';\nimport { useQuickCreateController } from './hooks/useQuickCreateController';\nimport { useResponsive } from './hooks/useResponsive';\nimport { useSearchController } from './hooks/useSearchController';\n\ninterface CalendarRootProps {\n  app: ICalendarApp;\n  meta?: Record<string, unknown>;\n  customMessages?: LocaleMessages;\n  search?: CalendarSearchProps;\n  titleBarSlot?: TNode | ((context: TitleBarSlotProps) => TNode);\n  collapsedSafeAreaLeft?: number;\n}\n\n// Internal locale gate — only wraps with LocaleProvider when no parent\n// provider is already present (e.g. when used inside a Vue/Angular adapter\n// that sets up its own locale context).\nconst CalendarInternalLocaleProvider = ({\n  locale,\n  messages,\n  children,\n}: {\n  locale: LocaleCode | Locale;\n  messages?: LocaleMessages;\n  children: ComponentChildren;\n}) => {\n  const context = useLocale();\n\n  if (!context.isDefault) {\n    return children;\n  }\n\n  return (\n    <LocaleProvider locale={locale} messages={messages}>\n      {children}\n    </LocaleProvider>\n  );\n};\n\nexport const CalendarRoot = ({\n  app,\n  meta,\n  customMessages,\n  search: searchConfig,\n  titleBarSlot,\n  collapsedSafeAreaLeft,\n}: CalendarRootProps) => {\n  const customRenderingStore = useContext(CustomRenderingContext);\n  // App subscription & sync\n  const { tick, selectedEventId } = useAppSubscription(app);\n  // Responsive breakpoint\n  const { isMobile } = useResponsive();\n  // Search\n  const search = useSearchController(app, searchConfig);\n  // Event detail dialog / panel\n  const effectiveEventDetailDialog: EventDetailDialogRenderer | undefined =\n    app.getUseEventDetailDialog() ? DefaultEventDetailDialog : undefined;\n\n  const eventDialog = useEventDialogController(\n    app,\n    effectiveEventDetailDialog,\n    tick\n  );\n\n  const useEventDetailPanel = app.getUseEventDetailPanel();\n\n  // Sidebar\n  const sidebar = useSidebarBridge(app);\n  // Quick-create (desktop popup + mobile drawer)\n  const quickCreate = useQuickCreateController(app, isMobile, sidebar.enabled);\n  // Theme\n  const [theme, setTheme] = useState<ThemeMode>(() => app.getTheme());\n\n  useEffect(\n    () => app.subscribeThemeChange(newTheme => setTheme(newTheme)),\n    [app]\n  );\n\n  const handleThemeChange = useCallback(\n    (newTheme: ThemeMode) => app.setTheme(newTheme),\n    [app]\n  );\n  // Cross-cutting: dismiss UI\n  // Patches the app callback so that app.dismissUI() collapses any open\n  // UI layer (detail panel or mobile drawer) and chains the previous handler.\n  useEffect(() => {\n    const callbacks = (\n      app as unknown as {\n        callbacks: {\n          onDismissUI?: () => void;\n          onEventDetailToggle?: (id: string | null) => void;\n          onMobileEventDetailToggle?: (event: CalendarEvent | null) => void;\n        };\n      }\n    ).callbacks;\n    const prevDismiss = callbacks.onDismissUI;\n    const prevDetailToggle = callbacks.onEventDetailToggle;\n    const prevMobileDetailToggle = callbacks.onMobileEventDetailToggle;\n\n    callbacks.onDismissUI = () => {\n      if (eventDialog.detailPanelEventId) {\n        eventDialog.setDetailPanelEventId(null);\n      }\n      if (quickCreate.isMobileDrawerOpen) {\n        quickCreate.setIsMobileDrawerOpen(false);\n      }\n      prevDismiss?.();\n    };\n\n    callbacks.onEventDetailToggle = (id: string | null) => {\n      eventDialog.setDetailPanelEventId(id);\n      prevDetailToggle?.(id);\n    };\n\n    callbacks.onMobileEventDetailToggle = (event: CalendarEvent | null) => {\n      quickCreate.setMobileDraftEvent(event);\n      quickCreate.setIsMobileDrawerOpen(Boolean(event));\n      prevMobileDetailToggle?.(event);\n    };\n\n    return () => {\n      callbacks.onDismissUI = prevDismiss;\n      callbacks.onEventDetailToggle = prevDetailToggle;\n      callbacks.onMobileEventDetailToggle = prevMobileDetailToggle;\n    };\n  }, [app, eventDialog, quickCreate]);\n\n  // On mobile, route event-tap detail requests to the MobileEventDrawer instead\n  // of the floating desktop panel. detailPanelEventId is set by handleTouchEnd\n  // (via onDetailPanelToggle) when canOpenDetail is true.\n  useEffect(() => {\n    if (!isMobile || !eventDialog.detailPanelEventId) return;\n\n    const rawEventId = eventDialog.detailPanelEventId.split('::')[0];\n    const event = app\n      .getEvents()\n      .find((e: CalendarEvent) => e.id === rawEventId);\n    if (event) {\n      quickCreate.setMobileDraftEvent(event);\n      quickCreate.setIsMobileDrawerOpen(true);\n    }\n    eventDialog.setDetailPanelEventId(null);\n  }, [eventDialog.detailPanelEventId, isMobile]);\n\n  const handleDateSelect = useCallback(\n    (date: Date) => {\n      app.setCurrentDate(date);\n      app.selectEvent(null);\n    },\n    [app]\n  );\n\n  const handleEventSelect = useCallback(\n    (id: string | null) => app.selectEvent(id),\n    [app]\n  );\n\n  // Layout helpers\n  const calendarRef = useRef<HTMLDivElement>(null!);\n\n  const viewProps = {\n    app,\n    config: app.getCurrentView().config || {},\n    useEventDetailPanel,\n    switcherMode: app.state.switcherMode,\n    calendarRef,\n    meta,\n    selectedEventId,\n    onEventSelect: handleEventSelect,\n    onDateChange: handleDateSelect,\n    detailPanelEventId: eventDialog.detailPanelEventId,\n    onDetailPanelToggle: eventDialog.setDetailPanelEventId,\n  };\n\n  // Stable args object so the titleBarSlot ContentSlot does not re-register\n  // on every CalendarRoot render.\n  const titleBarSlotArgs = useMemo(\n    () => ({\n      isCollapsed: sidebar.isCollapsed,\n      toggleCollapsed: sidebar.toggleCollapsed,\n    }),\n    [sidebar.isCollapsed, sidebar.toggleCollapsed]\n  );\n\n  const hasSafeAreaLeftValue =\n    collapsedSafeAreaLeft !== undefined && collapsedSafeAreaLeft !== null;\n  const miniSidebarWidth = hasSafeAreaLeftValue ? '0px' : sidebar.miniWidth;\n\n  const safeAreaLeft =\n    hasSafeAreaLeftValue && sidebar.isCollapsed\n      ? collapsedSafeAreaLeft!\n      : sidebar.safeAreaLeft;\n\n  const headerConfig = app.getCalendarHeaderConfig();\n\n  const headerProps = {\n    calendar: app,\n    switcherMode: app.state.switcherMode,\n    onAddCalendar: quickCreate.handleAddButtonClick,\n    onSearchChange: search.setSearchKeyword,\n    onSearchClick: search.handleSearchClick,\n    searchValue: search.searchKeyword,\n    isSearchOpen: search.isSearchOpen,\n    isEditable: app.canMutateFromUI(),\n    ...(safeAreaLeft > 0 ? { safeAreaLeft } : {}),\n  };\n\n  const renderHeader = () => {\n    if (headerConfig === false) return null;\n    return (\n      <ContentSlot\n        store={customRenderingStore}\n        generatorName='calendarHeader'\n        generatorArgs={headerProps}\n        defaultContent={h(CalendarHeader, headerProps)}\n      />\n    );\n  };\n\n  // Only create the Preact fallback portal when the slot is NOT yet overridden\n  // by a framework adapter, to prevent a brief flash of the default dialog\n  // while React/Vue sets up its override via setOverrides().\n  const renderEventDetailDialog = () => {\n    if (!eventDialog.dialogProps) return null;\n\n    const DialogComponent = effectiveEventDetailDialog!;\n    const portalTarget = typeof document === 'undefined' ? null : document.body;\n    if (!portalTarget) return null;\n\n    const isOverridden =\n      customRenderingStore?.isOverridden('eventDetailDialog');\n\n    return (\n      <ContentSlot\n        store={customRenderingStore}\n        generatorName='eventDetailDialog'\n        generatorArgs={eventDialog.dialogProps}\n        defaultContent={\n          isOverridden\n            ? null\n            : createPortal(\n                h(DialogComponent, eventDialog.dialogProps),\n                portalTarget\n              )\n        }\n      />\n    );\n  };\n\n  const ViewComponent = app.getCurrentView().component;\n  const MobileEventDrawerComponent = MobileEventDrawer;\n\n  const mobileEventDetailArgs: MobileEventProps = {\n    isOpen: quickCreate.isMobileDrawerOpen,\n    onClose: () => {\n      quickCreate.setIsMobileDrawerOpen(false);\n      quickCreate.setMobileDraftEvent(null);\n    },\n    onSave: (event: CalendarEvent) => {\n      const exists = app\n        .getEvents()\n        .some((e: CalendarEvent) => e.id === event.id);\n      if (exists) {\n        app.updateEvent(event.id, event);\n      } else {\n        app.addEvent(event);\n      }\n      quickCreate.setIsMobileDrawerOpen(false);\n      quickCreate.setMobileDraftEvent(null);\n    },\n    onEventDelete: (id: string) => {\n      app.deleteEvent(id);\n      quickCreate.setIsMobileDrawerOpen(false);\n      quickCreate.setMobileDraftEvent(null);\n    },\n    draftEvent: quickCreate.mobileDraftEvent,\n    app: app,\n  };\n\n  return (\n    <ThemeProvider initialTheme={theme} onThemeChange={handleThemeChange}>\n      <CalendarInternalLocaleProvider\n        locale={app.state.locale}\n        messages={customMessages}\n      >\n        <div className='df-calendar-container'>\n          <ContentSlot\n            store={customRenderingStore}\n            generatorName='titleBarSlot'\n            generatorArgs={titleBarSlotArgs}\n            defaultContent={\n              titleBarSlot &&\n              (typeof titleBarSlot === 'function'\n                ? titleBarSlot(titleBarSlotArgs)\n                : titleBarSlot)\n            }\n          />\n\n          {sidebar.enabled && (\n            <aside\n              className='df-calendar-sidebar-aside'\n              style={{ width: sidebar.width }}\n            >\n              {sidebar.content}\n            </aside>\n          )}\n\n          <div\n            className='df-calendar-shell'\n            data-sidebar-enabled={sidebar.enabled ? 'true' : 'false'}\n            data-sidebar-collapsed={sidebar.isCollapsed}\n            style={{\n              marginLeft: sidebar.enabled\n                ? `calc(${\n                    sidebar.isCollapsed ? miniSidebarWidth : sidebar.width\n                  } - 1px)`\n                : 0,\n            }}\n          >\n            {renderHeader()}\n\n            <div className='df-calendar-content-wrap' ref={calendarRef}>\n              <div className='df-calendar-renderer'>\n                <div className='df-calendar-view-container'>\n                  <ViewComponent {...viewProps} />\n                </div>\n\n                <SearchDrawer\n                  isOpen={search.isSearchOpen}\n                  onClose={search.handleSearchClose}\n                  loading={search.searchLoading}\n                  results={search.searchResults}\n                  keyword={search.searchKeyword}\n                  onResultClick={e =>\n                    search.handleSearchResultClick(e, 'desktop')\n                  }\n                  emptyText={searchConfig?.emptyText}\n                />\n              </div>\n\n              <MobileSearchDialog\n                isOpen={search.isMobileSearchOpen}\n                onClose={search.handleMobileSearchClose}\n                keyword={search.searchKeyword}\n                onSearchChange={search.setSearchKeyword}\n                results={search.searchResults}\n                loading={search.searchLoading}\n                onResultClick={e => search.handleSearchResultClick(e, 'mobile')}\n                emptyText={searchConfig?.emptyText}\n              />\n            </div>\n          </div>\n\n          <QuickCreateEventPopup\n            app={app}\n            anchorRef={quickCreate.quickCreateAnchorRef}\n            isOpen={quickCreate.isQuickCreateOpen}\n            onClose={() => quickCreate.setIsQuickCreateOpen(false)}\n          />\n\n          <ContentSlot\n            store={customRenderingStore}\n            generatorName='mobileEventDetail'\n            generatorArgs={mobileEventDetailArgs}\n            defaultContent={\n              <MobileEventDrawerComponent {...mobileEventDetailArgs} />\n            }\n          />\n\n          {sidebar.extraContent}\n          {quickCreate.isCreateCalendarOpen && (\n            <CreateCalendarDialog\n              app={app}\n              onClose={() => quickCreate.setIsCreateCalendarOpen(false)}\n              onCreate={async calendar => {\n                await app.createCalendar(calendar);\n                quickCreate.setIsCreateCalendarOpen(false);\n              }}\n            />\n          )}\n          {renderEventDetailDialog()}\n        </div>\n      </CalendarInternalLocaleProvider>\n    </ThemeProvider>\n  );\n};\n"
  },
  {
    "path": "packages/core/src/renderer/ContentSlot.tsx",
    "content": "import { ComponentChildren } from 'preact';\nimport {\n  useRef,\n  useEffect,\n  useLayoutEffect,\n  useContext,\n  useState,\n} from 'preact/hooks';\n\nimport { CustomRenderingContext } from './CustomRenderingContext';\nimport { CustomRenderingStore } from './CustomRenderingStore';\n\ninterface ContentSlotProps {\n  generatorName: string | null; // e.g. 'eventContent'\n  generatorArgs?: unknown; // e.g. { event, view }\n  defaultContent?: ComponentChildren; // Preact vnode as fallback\n  store?: CustomRenderingStore | null;\n}\n\nlet guid = 0;\nfunction generateId() {\n  return 'df-slot-' + guid++;\n}\n\n/**\n * Preact component: Creates a placeholder <div> and registers it with CustomRenderingStore.\n * If a framework adapter (React/Vue) is present, it will portal its content into this <div>.\n * Otherwise, it displays defaultContent.\n */\nexport function ContentSlot({\n  generatorName,\n  generatorArgs,\n  defaultContent,\n  store: propStore,\n}: ContentSlotProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const contextStore = useContext(CustomRenderingContext);\n  const store = propStore || contextStore;\n  const idRef = useRef<string | null>(null);\n  const [isOverridden, setIsOverridden] = useState(() =>\n    generatorName ? (store?.isOverridden(generatorName) ?? false) : false\n  );\n\n  if (!idRef.current) {\n    idRef.current = generateId();\n  }\n\n  // Register/Unregister the container once, and subscribe only to override changes.\n  // useLayoutEffect so registration happens synchronously before\n  // the browser paints — eliminating the one-frame gap where the React portal\n  // has no container and the slot appears blank.\n  useLayoutEffect(() => {\n    if (!containerRef.current || !store || !generatorName) return;\n\n    const id = idRef.current!;\n    store.register({\n      id,\n      containerEl: containerRef.current,\n      generatorName,\n      generatorArgs, // Initial args\n    });\n\n    // Subscribe only to override changes — not to every register/unregister.\n    // This avoids the O(N²) cascade where each slot registration forces all\n    // N ContentSlots to re-render.\n    const unsubscribe = store.subscribeToOverrides(() => {\n      setIsOverridden(store.isOverridden(generatorName));\n    });\n\n    return () => {\n      store.unregister(id);\n      unsubscribe();\n    };\n  }, [store, generatorName]); // Re-run if store or generatorName changes\n\n  // Update args when they change, without unregistering\n  useEffect(() => {\n    if (!store || !idRef.current || !generatorName) return;\n\n    const id = idRef.current;\n    store.register({\n      id,\n      containerEl: containerRef.current!,\n      generatorName,\n      generatorArgs,\n    });\n  }, [generatorName, generatorArgs]);\n\n  const isEventSlot = generatorName?.startsWith('eventContent');\n  const isSidebarSlot = generatorName === 'sidebar';\n\n  return (\n    <div\n      ref={containerRef}\n      className={`df-content-slot ${isEventSlot || isSidebarSlot ? 'df-content-slot-stacked' : ''}`}\n    >\n      {!isOverridden && defaultContent}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/core/src/renderer/CustomRenderingContext.ts",
    "content": "import { createContext } from 'preact';\n\nimport { CustomRenderingStore } from './CustomRenderingStore';\n\nexport const CustomRenderingContext =\n  createContext<CustomRenderingStore | null>(null);\n"
  },
  {
    "path": "packages/core/src/renderer/CustomRenderingStore.ts",
    "content": "export interface CustomRendering {\n  id: string;\n  containerEl: HTMLElement; // The placeholder div created by Preact\n  generatorName: string; // e.g. 'eventContent', 'titleBarSlot'\n  generatorArgs: unknown; // Arguments passed to the framework-specific generator\n}\n\nexport type CustomRenderingListener = (\n  map: Map<string, CustomRendering>\n) => void;\n\nexport class CustomRenderingStore {\n  private renderings = new Map<string, CustomRendering>();\n  private overrides = new Set<string>();\n  private listeners = new Set<CustomRenderingListener>();\n  private overrideListeners = new Set<() => void>();\n\n  constructor(initialOverrides?: string[]) {\n    if (initialOverrides && initialOverrides.length > 0) {\n      this.overrides = new Set(initialOverrides);\n    }\n  }\n\n  /**\n   * Register a new custom rendering placeholder.\n   * Called by the ContentSlot Preact component.\n   */\n  register(rendering: CustomRendering): void {\n    this.renderings.set(rendering.id, rendering);\n    this.notify();\n  }\n\n  /**\n   * Unregister a custom rendering placeholder.\n   */\n  unregister(id: string): void {\n    if (this.renderings.delete(id)) {\n      this.notify();\n    }\n  }\n\n  /**\n   * Set the list of generator names that have custom overrides.\n   * Called by framework adapters (React/Vue/etc.)\n   */\n  setOverrides(names: string[]): void {\n    this.overrides = new Set(names);\n    this.notify();\n    this.notifyOverrides();\n  }\n\n  /**\n   * Check if a specific generator has a custom override.\n   */\n  isOverridden(generatorName: string): boolean {\n    return this.overrides.has(generatorName);\n  }\n\n  /**\n   * Subscribe to updates of the renderings map.\n   * Called by the framework adapter (React/Vue/etc.)\n   */\n  subscribe(listener: CustomRenderingListener): () => void {\n    this.listeners.add(listener);\n    // Initial sync\n    listener(this.renderings);\n    return () => {\n      this.listeners.delete(listener);\n    };\n  }\n\n  /**\n   * Subscribe only to override changes (setOverrides calls).\n   * Used by ContentSlot components to avoid re-rendering on every register/unregister.\n   */\n  subscribeToOverrides(listener: () => void): () => void {\n    this.overrideListeners.add(listener);\n    listener();\n    return () => {\n      this.overrideListeners.delete(listener);\n    };\n  }\n\n  private notify(): void {\n    this.listeners.forEach(fn => fn(this.renderings));\n  }\n\n  private notifyOverrides(): void {\n    this.overrideListeners.forEach(fn => fn());\n  }\n}\n"
  },
  {
    "path": "packages/core/src/renderer/hooks/__tests__/useSearchController.test.ts",
    "content": "import { renderHook, act } from '@testing-library/preact';\n\nimport { useSearchController } from '@/renderer/hooks/useSearchController';\nimport { ICalendarApp } from '@/types';\nimport { CalendarSearchEvent } from '@/types/search';\n\ndescribe('useSearchController', () => {\n  let mockApp: ICalendarApp;\n\n  beforeEach(() => {\n    mockApp = {\n      state: {\n        highlightedEventId: null,\n        locale: 'en-US',\n        switcherMode: 'select',\n        calendarRegistry: {\n          get: jest.fn(),\n          resolveColors: jest.fn(() => ({ lineColor: '#000' })),\n        },\n      },\n      selectEvent: jest.fn(),\n      highlightEvent: jest.fn(),\n      setCurrentDate: jest.fn(),\n      getEvents: jest.fn(() => []),\n      getCalendarRegistry: jest.fn(() => ({\n        get: jest.fn(),\n        resolveColors: jest.fn(() => ({ lineColor: '#000' })),\n      })),\n      subscribeThemeChange: jest.fn(() => () => {\n        /* unsubscribe */\n      }),\n      getTheme: jest.fn(() => 'light'),\n    } as unknown as ICalendarApp;\n  });\n\n  it('should call onResultClick when provided', () => {\n    const onResultClick = jest.fn();\n    const searchConfig = { onResultClick };\n    const { result } = renderHook(() =>\n      useSearchController(mockApp, searchConfig)\n    );\n\n    const event = {\n      id: '1',\n      title: 'Test Event',\n      start: new Date(),\n    } as unknown as CalendarSearchEvent;\n    act(() => {\n      result.current.handleSearchResultClick(event, 'desktop');\n    });\n\n    expect(onResultClick).toHaveBeenCalledWith(\n      expect.objectContaining({\n        event,\n        app: mockApp,\n        source: 'desktop',\n      })\n    );\n\n    // Check if defaultAction is passed and works\n    const callArgs = onResultClick.mock.calls[0][0];\n    act(() => {\n      callArgs.defaultAction();\n    });\n    expect(mockApp.setCurrentDate).toHaveBeenCalled();\n    expect(mockApp.highlightEvent).toHaveBeenCalledWith('1');\n  });\n\n  it('should execute defaultAction when onResultClick is not provided', () => {\n    const searchConfig = {};\n    const { result } = renderHook(() =>\n      useSearchController(mockApp, searchConfig)\n    );\n\n    const event = {\n      id: '1',\n      title: 'Test Event',\n      start: new Date(),\n    } as unknown as CalendarSearchEvent;\n    act(() => {\n      result.current.handleSearchResultClick(event, 'desktop');\n    });\n\n    expect(mockApp.setCurrentDate).toHaveBeenCalled();\n    expect(mockApp.highlightEvent).toHaveBeenCalledWith('1');\n  });\n\n  it('should close search when closeSearch is called', () => {\n    const onResultClick = jest.fn(({ closeSearch }) => {\n      closeSearch();\n    });\n    const searchConfig = { onResultClick };\n    const { result } = renderHook(() =>\n      useSearchController(mockApp, searchConfig)\n    );\n\n    // Open desktop search first\n    act(() => {\n      result.current.setIsSearchOpen(true);\n    });\n    expect(result.current.isSearchOpen).toBe(true);\n\n    const event = {\n      id: '1',\n      title: 'Test Event',\n      start: new Date(),\n    } as unknown as CalendarSearchEvent;\n    act(() => {\n      result.current.handleSearchResultClick(event, 'desktop');\n    });\n\n    expect(result.current.isSearchOpen).toBe(false);\n    expect(mockApp.highlightEvent).not.toHaveBeenCalledWith(null);\n\n    // Test mobile close\n    act(() => {\n      result.current.setIsMobileSearchOpen(true);\n    });\n    expect(result.current.isMobileSearchOpen).toBe(true);\n\n    act(() => {\n      result.current.handleSearchResultClick(event, 'mobile');\n    });\n    expect(result.current.isMobileSearchOpen).toBe(false);\n  });\n\n  it('keeps highlight when custom navigation calls defaultAction and closeSearch', () => {\n    const onResultClick = jest.fn(({ defaultAction, closeSearch }) => {\n      defaultAction();\n      closeSearch();\n    });\n    const searchConfig = { onResultClick };\n    const { result } = renderHook(() =>\n      useSearchController(mockApp, searchConfig)\n    );\n\n    act(() => {\n      result.current.setIsSearchOpen(true);\n    });\n\n    const event = {\n      id: 'focus-event',\n      title: 'Focus Event',\n      start: new Date(),\n    } as unknown as CalendarSearchEvent;\n\n    act(() => {\n      result.current.handleSearchResultClick(event, 'desktop');\n    });\n\n    expect(mockApp.highlightEvent).toHaveBeenCalledWith('focus-event');\n    expect(mockApp.highlightEvent).not.toHaveBeenCalledWith(null);\n    expect(result.current.isSearchOpen).toBe(false);\n  });\n\n  it('does not clear highlight when config identity changes while search is empty', () => {\n    mockApp.state.highlightedEventId = 'event-1';\n\n    const { rerender } = renderHook(\n      ({ config }: { config: { onResultClick: jest.Mock } }) =>\n        useSearchController(mockApp, config),\n      {\n        initialProps: {\n          config: {\n            onResultClick: jest.fn(),\n          },\n        },\n      }\n    );\n\n    expect(mockApp.highlightEvent).not.toHaveBeenCalledWith(null);\n\n    rerender({\n      config: {\n        onResultClick: jest.fn(),\n      },\n    });\n\n    expect(mockApp.highlightEvent).not.toHaveBeenCalledWith(null);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/renderer/hooks/useAppSubscription.ts",
    "content": "import { useState, useEffect } from 'preact/hooks';\n\nimport { ICalendarApp } from '@/types';\n\nexport interface AppSubscriptionResult {\n  tick: number;\n  selectedEventId: string | null;\n}\n\n/**\n * Subscribes to the app instance, drives re-renders on state changes,\n * and keeps selectedEventId in sync with app.state.\n *\n * The two setState calls inside a single subscriber let React/Preact\n * batch them into one render automatically.\n */\nexport function useAppSubscription(app: ICalendarApp): AppSubscriptionResult {\n  const [tick, setTick] = useState(0);\n  const [selectedEventId, setSelectedEventId] = useState<string | null>(\n    app.state.selectedEventId ?? null\n  );\n\n  useEffect(() => {\n    setTick(0);\n    setSelectedEventId(app.state.selectedEventId ?? null);\n  }, [app]);\n\n  useEffect(\n    () =>\n      app.subscribe(appInstance => {\n        setTick(t => t + 1);\n        setSelectedEventId(prev => {\n          const next = appInstance.state.selectedEventId ?? null;\n          return prev === next ? prev : next;\n        });\n      }),\n    [app]\n  );\n\n  return { tick, selectedEventId };\n}\n"
  },
  {
    "path": "packages/core/src/renderer/hooks/useEventDialogController.ts",
    "content": "import {\n  useState,\n  useEffect,\n  useCallback,\n  useMemo,\n  useRef,\n} from 'preact/hooks';\n\nimport { ICalendarApp, Event, EventDetailDialogRenderer } from '@/types';\nimport { isPlainDate } from '@/utils/temporal';\n\nexport interface DialogProps {\n  event: Event;\n  isOpen: boolean;\n  isAllDay: boolean;\n  onEventUpdate: (evt: Event) => void;\n  onEventDelete: (id: string) => void;\n  onClose: () => void;\n  app: ICalendarApp;\n}\n\nexport interface EventDialogController {\n  detailPanelEventId: string | null;\n  setDetailPanelEventId: (id: string | null) => void;\n  /** Memoized props object ready to spread into the dialog component. */\n  dialogProps: DialogProps | null;\n}\n\n/**\n * Manages the event detail dialog/panel lifecycle:\n * - Resets state on app instance changes (client-side navigation).\n * - Memoizes dialogProps so ContentSlot only re-registers when data changes.\n * - Exposes stable handler callbacks (close, update, delete).\n *\n * `tick` is passed in from useAppSubscription so dialogProps stays fresh\n * when the underlying event is edited while the dialog is open.\n */\nexport function useEventDialogController(\n  app: ICalendarApp,\n  effectiveEventDetailDialog: EventDetailDialogRenderer | undefined,\n  tick: number\n): EventDialogController {\n  const [detailPanelEventId, setDetailPanelEventId] = useState<string | null>(\n    null\n  );\n  const dialogEventRef = useRef<Event | null>(null);\n\n  // Reset when the app instance is replaced (client-side navigation).\n  useEffect(() => {\n    setDetailPanelEventId(null);\n    dialogEventRef.current = null;\n  }, [app]);\n\n  const handleDialogClose = useCallback(() => {\n    setDetailPanelEventId(null);\n    dialogEventRef.current = null;\n    app.selectEvent(null);\n  }, [app]);\n\n  const handleDialogEventUpdate = useCallback(\n    (evt: Event) => app.updateEvent(evt.id, evt),\n    [app]\n  );\n\n  const handleDialogEventDelete = useCallback(\n    async (id: string) => {\n      await app.deleteEvent(id);\n      setDetailPanelEventId(null);\n      app.selectEvent(null);\n    },\n    [app]\n  );\n\n  // tick is included so the event object stays fresh when app state changes\n  // (e.g. user edits event title while dialog is open).\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const dialogProps = useMemo<DialogProps | null>(() => {\n    if (!effectiveEventDetailDialog || !detailPanelEventId) return null;\n\n    const rawEventId = detailPanelEventId.split('::')[0];\n    const selectedEvent = app\n      .getEvents()\n      .find((e: Event) => e.id === rawEventId);\n    if (selectedEvent) {\n      dialogEventRef.current = selectedEvent;\n    }\n\n    const dialogEvent =\n      selectedEvent ??\n      (dialogEventRef.current?.id === rawEventId\n        ? dialogEventRef.current\n        : null);\n    if (!dialogEvent) return null;\n\n    return {\n      event: dialogEvent,\n      isOpen: true,\n      isAllDay: isPlainDate(dialogEvent.start),\n      onEventUpdate: handleDialogEventUpdate,\n      onEventDelete: handleDialogEventDelete,\n      onClose: handleDialogClose,\n      app,\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [\n    tick,\n    detailPanelEventId,\n    effectiveEventDetailDialog,\n    handleDialogClose,\n    handleDialogEventUpdate,\n    handleDialogEventDelete,\n    app,\n  ]);\n\n  return { detailPanelEventId, setDetailPanelEventId, dialogProps };\n}\n"
  },
  {
    "path": "packages/core/src/renderer/hooks/useQuickCreateController.ts",
    "content": "import { JSX, RefObject } from 'preact';\nimport { useState, useCallback, useRef } from 'preact/hooks';\n\nimport { ICalendarApp, Event } from '@/types';\nimport { generateUniKey } from '@/utils/helpers';\nimport { dateToZonedDateTime } from '@/utils/temporalTypeGuards';\nimport { getNextHourRangeInTimeZone } from '@/utils/timeUtils';\n\nexport interface QuickCreateController {\n  isQuickCreateOpen: boolean;\n  setIsQuickCreateOpen: (open: boolean) => void;\n  quickCreateAnchorRef: RefObject<HTMLElement>;\n  isMobileDrawerOpen: boolean;\n  setIsMobileDrawerOpen: (open: boolean) => void;\n  mobileDraftEvent: Event | null;\n  setMobileDraftEvent: (event: Event | null) => void;\n  handleAddButtonClick: (\n    e: JSX.TargetedMouseEvent<HTMLElement> | JSX.TargetedTouchEvent<HTMLElement>\n  ) => void;\n  isCreateCalendarOpen: boolean;\n  setIsCreateCalendarOpen: (open: boolean) => void;\n}\n\n/**\n * Manages the \"add event\" affordance for both desktop (QuickCreateEventPopup)\n * and mobile (MobileEventDrawer with a pre-populated draft event).\n */\nexport function useQuickCreateController(\n  app: ICalendarApp,\n  isMobile: boolean,\n  sidebarEnabled: boolean\n): QuickCreateController {\n  const [isQuickCreateOpen, setIsQuickCreateOpen] = useState(false);\n  const quickCreateAnchorRef = useRef<HTMLElement>(null!);\n  const [isMobileDrawerOpen, setIsMobileDrawerOpen] = useState(false);\n  const [mobileDraftEvent, setMobileDraftEvent] = useState<Event | null>(null);\n  const [isCreateCalendarOpen, setIsCreateCalendarOpen] = useState(false);\n\n  const handleCreateCalendar = useCallback(() => {\n    setIsCreateCalendarOpen(true);\n  }, []);\n\n  const handleAddButtonClick = useCallback(\n    (\n      e:\n        | JSX.TargetedMouseEvent<HTMLElement>\n        | JSX.TargetedTouchEvent<HTMLElement>\n    ) => {\n      const isEditable = app.canMutateFromUI();\n      if (!isEditable) return;\n      const writableCal = app\n        .getCalendarRegistry()\n        .getDefaultWritableCalendar();\n      if (!writableCal) return;\n\n      if (isMobile) {\n        const { start, end } = getNextHourRangeInTimeZone(app.timeZone);\n\n        const draft: Event = {\n          id: generateUniKey(),\n          title: '',\n          start: dateToZonedDateTime(start, app.timeZone),\n          end: dateToZonedDateTime(end, app.timeZone),\n          calendarId: writableCal.id,\n        };\n        setMobileDraftEvent(draft);\n        setIsMobileDrawerOpen(true);\n        return;\n      }\n\n      if (sidebarEnabled) {\n        // Desktop: Toggle popup\n        if (isQuickCreateOpen) {\n          setIsQuickCreateOpen(false);\n        } else {\n          (\n            quickCreateAnchorRef as unknown as { current: EventTarget | null }\n          ).current = e.currentTarget;\n          setIsQuickCreateOpen(true);\n        }\n      } else {\n        // Sidebar disabled -> Add Button creates calendar\n        handleCreateCalendar();\n      }\n    },\n    [isMobile, isQuickCreateOpen, app, sidebarEnabled, handleCreateCalendar]\n  );\n\n  return {\n    isQuickCreateOpen,\n    setIsQuickCreateOpen,\n    quickCreateAnchorRef,\n    isMobileDrawerOpen,\n    setIsMobileDrawerOpen,\n    mobileDraftEvent,\n    setMobileDraftEvent,\n    handleAddButtonClick,\n    isCreateCalendarOpen,\n    setIsCreateCalendarOpen,\n  };\n}\n"
  },
  {
    "path": "packages/core/src/renderer/hooks/useResponsive.ts",
    "content": "import { useState, useEffect } from 'preact/hooks';\n\nexport interface ResponsiveResult {\n  isMobile: boolean;\n}\n\n/**\n * Tracks viewport width and returns whether the current breakpoint is mobile\n * (≤1024 px).  Subscribes to window resize to stay current.\n */\nexport function useResponsive(): ResponsiveResult {\n  const [isMobile, setIsMobile] = useState(false);\n\n  useEffect(() => {\n    const checkMobile = () => {\n      setIsMobile(window.matchMedia('(max-width: 1024px)').matches);\n    };\n    checkMobile();\n    window.addEventListener('resize', checkMobile);\n    return () => window.removeEventListener('resize', checkMobile);\n  }, []);\n\n  return { isMobile };\n}\n"
  },
  {
    "path": "packages/core/src/renderer/hooks/useSearchController.ts",
    "content": "import { useState, useEffect, useCallback, useRef } from 'preact/hooks';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { ICalendarApp } from '@/types';\nimport { CalendarSearchProps, CalendarSearchEvent } from '@/types/search';\nimport { temporalToDate } from '@/utils/temporal';\n\nexport interface SearchController {\n  searchKeyword: string;\n  setSearchKeyword: (kw: string) => void;\n  isSearchOpen: boolean;\n  setIsSearchOpen: (open: boolean) => void;\n  isMobileSearchOpen: boolean;\n  setIsMobileSearchOpen: (open: boolean) => void;\n  searchLoading: boolean;\n  searchResults: CalendarSearchEvent[];\n  handleSearchResultClick: (\n    event: CalendarSearchEvent,\n    source?: 'desktop' | 'mobile'\n  ) => void;\n  handleSearchClick: () => void;\n  handleSearchClose: () => void;\n  handleMobileSearchClose: () => void;\n}\n\n/**\n * Manages all search state: keyword, debounce, loading, results, drawer\n * visibility for desktop and mobile, and highlight sync.\n */\nexport function useSearchController(\n  app: ICalendarApp,\n  searchConfig: CalendarSearchProps | undefined\n): SearchController {\n  const [searchKeyword, setSearchKeyword] = useState('');\n  const [isSearchOpen, setIsSearchOpen] = useState(false);\n  const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false);\n  const [searchLoading, setSearchLoading] = useState(false);\n  const [searchResults, setSearchResults] = useState<CalendarSearchEvent[]>([]);\n  const prevSearchKeywordRef = useRef('');\n  const prevIsSearchOpenRef = useRef(false);\n\n  // Sync highlighted event → selected event whenever the app highlights one\n  // (e.g. after navigating to a search result).\n  useEffect(() => {\n    if (app.state.highlightedEventId) {\n      app.selectEvent(app.state.highlightedEventId);\n    }\n  }, [app.state.highlightedEventId, app]);\n\n  // Clear highlight when search drawer closes.\n  useEffect(() => {\n    if (\n      prevIsSearchOpenRef.current &&\n      !isSearchOpen &&\n      app.state.highlightedEventId !== null\n    ) {\n      app.highlightEvent(null);\n    }\n    prevIsSearchOpenRef.current = isSearchOpen;\n  }, [isSearchOpen, app]);\n\n  // Debounced search execution.\n  useEffect(() => {\n    if (!searchKeyword.trim()) {\n      setIsSearchOpen(false);\n      setSearchResults([]);\n      if (\n        prevSearchKeywordRef.current.trim() &&\n        app.state.highlightedEventId !== null\n      ) {\n        app.highlightEvent(null);\n      }\n      prevSearchKeywordRef.current = searchKeyword;\n      return;\n    }\n\n    const debounceDelay = searchConfig?.debounceDelay ?? 300;\n\n    const performSearch = async () => {\n      setSearchLoading(true);\n      setIsSearchOpen(true);\n\n      try {\n        let results: CalendarSearchEvent[] = [];\n\n        if (searchConfig?.customSearch) {\n          const currentEvents = app.getEvents().map(e => ({\n            ...e,\n            color:\n              app.getCalendarRegistry().get(e.calendarId || '')?.colors\n                .lineColor ||\n              app.getCalendarRegistry().resolveColors().lineColor,\n          }));\n          results = searchConfig.customSearch({\n            keyword: searchKeyword,\n            events: currentEvents,\n          });\n        } else if (searchConfig?.onSearch) {\n          results = await searchConfig.onSearch(searchKeyword);\n        } else {\n          const keywordLower = searchKeyword.toLowerCase();\n          results = app\n            .getEvents()\n            .filter(\n              e =>\n                e.title.toLowerCase().includes(keywordLower) ||\n                (e.description &&\n                  e.description.toLowerCase().includes(keywordLower))\n            )\n            .map(e => ({\n              ...e,\n              color:\n                app.getCalendarRegistry().get(e.calendarId || '')?.colors\n                  .lineColor ||\n                app.getCalendarRegistry().resolveColors().lineColor,\n            }));\n        }\n\n        setSearchResults(results);\n        searchConfig?.onSearchStateChange?.({\n          keyword: searchKeyword,\n          loading: false,\n          results,\n        });\n      } catch (error) {\n        console.error('Search failed', error);\n        setSearchResults([]);\n      } finally {\n        setSearchLoading(false);\n      }\n    };\n\n    const timer = setTimeout(performSearch, debounceDelay);\n    prevSearchKeywordRef.current = searchKeyword;\n    return () => clearTimeout(timer);\n  }, [searchKeyword, searchConfig, app]);\n\n  const handleSearchResultClick = useCallback(\n    (event: CalendarSearchEvent, source: 'desktop' | 'mobile' = 'desktop') => {\n      const defaultAction = () => {\n        let date: Date;\n        if (event.start instanceof Date) {\n          date = event.start;\n        } else if (typeof event.start === 'string') {\n          date = new Date(event.start);\n        } else {\n          date = temporalToDate(\n            event.start as\n              | Temporal.PlainDate\n              | Temporal.PlainDateTime\n              | Temporal.ZonedDateTime\n          );\n        }\n        app.setCurrentDate(date);\n        app.highlightEvent(event.id);\n\n        if (isMobileSearchOpen) {\n          setIsMobileSearchOpen(false);\n        }\n      };\n\n      const closeSearch = () => {\n        if (source === 'mobile') {\n          setIsMobileSearchOpen(false);\n        } else {\n          setIsSearchOpen(false);\n        }\n      };\n\n      if (searchConfig?.onResultClick) {\n        searchConfig.onResultClick({\n          event,\n          app,\n          source,\n          defaultAction,\n          closeSearch,\n        });\n      } else {\n        defaultAction();\n      }\n    },\n    [app, isMobileSearchOpen, searchConfig]\n  );\n\n  // Opens the mobile search dialog and resets the keyword.\n  const handleSearchClick = useCallback(() => {\n    setSearchKeyword('');\n    setIsMobileSearchOpen(true);\n  }, []);\n\n  const handleSearchClose = useCallback(() => {\n    setIsSearchOpen(false);\n    setSearchKeyword('');\n    app.highlightEvent(null);\n  }, [app]);\n\n  const handleMobileSearchClose = useCallback(() => {\n    setIsMobileSearchOpen(false);\n    setSearchKeyword('');\n    app.highlightEvent(null);\n  }, [app]);\n\n  return {\n    searchKeyword,\n    setSearchKeyword,\n    isSearchOpen,\n    setIsSearchOpen,\n    isMobileSearchOpen,\n    setIsMobileSearchOpen,\n    searchLoading,\n    searchResults,\n    handleSearchResultClick,\n    handleSearchClick,\n    handleSearchClose,\n    handleMobileSearchClose,\n  };\n}\n"
  },
  {
    "path": "packages/core/src/setupTests.ts",
    "content": "import '@testing-library/jest-dom';\n\n// Mock ResizeObserver\nglobal.ResizeObserver = class ResizeObserver {\n  // eslint-disable-next-line class-methods-use-this\n  observe() {\n    /* noop */\n  }\n  // eslint-disable-next-line class-methods-use-this\n  unobserve() {\n    /* noop */\n  }\n  // eslint-disable-next-line class-methods-use-this\n  disconnect() {\n    /* noop */\n  }\n};\n"
  },
  {
    "path": "packages/core/src/styles/__tests__/dist-css.test.ts",
    "content": "/**\n * CSS dist output integrity tests\n *\n * These tests guard against the two recurring CSS issues:\n *\n * Issue #69 / hccullen PR — two problems when DayFlow CSS is used alongside\n * host-app Tailwind CSS:\n *   1. Tailwind utility class conflicts break responsive layouts\n *      (e.g. `flex-col` emitted by DayFlow overrides host `md:flex-row`)\n *   2. Missing scoped `--color-*` mappings cause DayFlow's colors to bleed\n *      into, or be overridden by, the host app's Tailwind theme\n *\n * Run after `pnpm build` — tests read compiled artifacts from dist/.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n// ─── helpers ───────────────────────────────────────────────────────────────\n\n// eslint-disable-next-line unicorn/prefer-module\nconst DIST = path.resolve(__dirname, '../../../dist');\n\nfunction readDist(filename: string): string {\n  const full = path.join(DIST, filename);\n  if (!fs.existsSync(full)) {\n    throw new Error(\n      `Dist file not found: ${full}\\nRun \"pnpm build\" before running CSS integrity tests.`\n    );\n  }\n  return fs.readFileSync(full, 'utf-8');\n}\n\n/**\n * Extract the content inside the first CSS block whose selector line matches\n * the given regex. Returns null if not found.\n */\nfunction extractBlock(css: string, selectorPattern: RegExp): string | null {\n  // Match the selector line(s) followed by { ... }\n  // Handles multi-line selectors like \".df-calendar-container,\\n.df-portal {\"\n  const re = new RegExp(\n    selectorPattern.source + String.raw`\\s*\\{([^}]+)\\}`,\n    selectorPattern.flags\n  );\n  const match = css.match(re);\n  return match ? match[1] : null;\n}\n\nfunction extractToken(css: string, token: string): string | undefined {\n  return css.match(new RegExp(`${token}:\\\\s*([^;]+)`))?.[1]?.trim();\n}\n\n// ─── fixtures ──────────────────────────────────────────────────────────────\n\n// All --color-* tokens that must be mapped inside the DayFlow scoped root\n// block so that host-Tailwind-generated utilities resolve to DayFlow's own\n// design tokens rather than the host application's theme.\nconst REQUIRED_COLOR_MAPPINGS: string[] = [\n  '--color-background',\n  '--color-foreground',\n  '--color-hover',\n  '--color-border',\n  '--color-card',\n  '--color-card-foreground',\n  '--color-muted',\n  '--color-muted-foreground',\n  '--color-primary',\n  '--color-primary-foreground',\n  '--color-secondary',\n  '--color-secondary-foreground',\n  '--color-destructive',\n  '--color-destructive-foreground',\n  // Tailwind v4 arbitrary CSS-variable syntax: hover:bg-(--hover)\n  // generates background-color: var(--hover); this must be defined.\n  '--hover',\n];\n\n// Bare Tailwind utility selectors that must NOT appear in styles.components.css.\n// If they appear at the top level, they conflict with host-app Tailwind\n// instances and can break responsive layouts (issue #69 / hccullen PR).\n// Patterns are anchored with ^ + multiline flag so they only match when the\n// utility class is the *entire* selector on a line — not when it appears as\n// part of a compound selector like `.df-portal .bg-primary {`.\nconst FORBIDDEN_BARE_UTILITIES = [\n  // layout — the class most clearly demonstrated to break responsive design\n  String.raw`^\\.flex-col\\s*\\{`,\n  String.raw`^\\.flex-row\\s*\\{`,\n  String.raw`^\\.md\\\\:flex-row\\s*\\{`,\n  // semantic color utilities — would override host theme colors outside\n  // the DayFlow container\n  String.raw`^\\.bg-background\\s*\\{`,\n  String.raw`^\\.bg-primary\\s*\\{`,\n  String.raw`^\\.text-primary\\s*\\{`,\n  String.raw`^\\.bg-secondary\\s*\\{`,\n  String.raw`^\\.border-border\\s*\\{`,\n  String.raw`^\\.ring-primary\\s*\\{`,\n];\n\n// Returns true for selectors that are permitted in styles.components.css:\n// DayFlow component classes (.df-*), color-picker dependency (.bcp-*),\n// and dark-mode scope (.dark ...).\nfunction isDayFlowSelector(sel: string): boolean {\n  return (\n    sel.startsWith('.df-') ||\n    sel.startsWith('.bcp-') ||\n    sel.startsWith('.dark ')\n  );\n}\n\nconst SCOPE_BLOCK_SELECTOR =\n  /\\.df-calendar-container,\\s*\\n?\\s*\\.df-dialog-container,\\s*\\n?\\s*\\.df-event-detail-panel,\\s*\\n?\\s*\\.df-portal,\\s*\\n?\\s*\\.df-range-picker/;\n\n// ─── tests ─────────────────────────────────────────────────────────────────\n\ndescribe('CSS dist output integrity', () => {\n  let componentsCss: string;\n  let fullCss: string;\n\n  beforeAll(() => {\n    componentsCss = readDist('styles.components.css');\n    fullCss = readDist('styles.css');\n  });\n\n  // ── Guard #1: styles.components.css must not emit bare Tailwind utilities ──\n  //\n  // This is the root cause of issue #69 and the hccullen PR.\n  // styles.components.css is designed for projects that have their own Tailwind\n  // instance. If it also emits utility classes, two Tailwind instances fight\n  // over cascade order and responsive breakpoints break.\n  describe('styles.components.css — no bare Tailwind utility classes emitted', () => {\n    test.each(FORBIDDEN_BARE_UTILITIES)(\n      'must not contain top-level utility selector: %s',\n      pattern => {\n        expect(componentsCss).not.toMatch(new RegExp(pattern, 'm'));\n      }\n    );\n\n    it('only emits df-* and bcp-* namespaced selectors at the top level', () => {\n      // Every top-level rule (lines starting with \".\") must belong to DayFlow\n      // (.df-*), the color-picker dependency (.bcp-*), or a dark-mode\n      // modifier (.dark ...) — never a bare Tailwind utility.\n      const topLevelSelectors = componentsCss\n        .split('\\n')\n        .filter(line => /^\\.[a-z]/.test(line.trim())) // lines starting with \".\"\n        .map(line => line.trim());\n\n      const violations = topLevelSelectors.filter(\n        sel => !isDayFlowSelector(sel)\n      );\n\n      expect(violations).toEqual([]);\n    });\n\n    it('does not ship unresolved @source directives', () => {\n      expect(componentsCss).not.toMatch(/@source\\s+/);\n      expect(fullCss).not.toMatch(/@source\\s+/);\n    });\n  });\n\n  // ── Guard #2: All color token mappings present in the scoped root block ──\n  //\n  // The scoped DayFlow root block remaps Tailwind's semantic --color-* tokens\n  // to DayFlow's own --df-color-* variables.\n  // Missing entries cause host-Tailwind-generated utility classes (e.g.\n  // bg-secondary, border-border) to resolve to the host theme's colors\n  // instead of DayFlow's, producing wrong colors inside the calendar.\n  describe.each([\n    ['styles.components.css', () => componentsCss],\n    ['styles.css', () => fullCss],\n  ] as const)('%s — scoped root mapping block', (_file, getCss) => {\n    it('block exists in the file', () => {\n      const block = extractBlock(getCss(), SCOPE_BLOCK_SELECTOR);\n      expect(block).not.toBeNull();\n    });\n\n    test.each(REQUIRED_COLOR_MAPPINGS)(\n      'maps %s to a DayFlow variable',\n      token => {\n        const block = extractBlock(getCss(), SCOPE_BLOCK_SELECTOR);\n        // Each token must be present and point to a --df-color-* variable\n        expect(block).toContain(token);\n        const tokenLine = block!.split('\\n').find(l => l.includes(token));\n        expect(tokenLine).toMatch(/var\\(--df-color-|var\\(--df-/);\n      }\n    );\n  });\n\n  // ── Guard #3: Component rules use CSS variables, not hardcoded gray values ──\n  //\n  // Before this fix, .df-calendar-container used @apply bg-white dark:bg-gray-900\n  // which hardcodes specific gray values and ignores the --df-color-* theme\n  // variables — meaning theme customization via CSS variable overrides had no\n  // effect on bundled-Tailwind users.\n  describe.each([\n    ['styles.components.css', () => componentsCss],\n    ['styles.css', () => fullCss],\n  ] as const)(\n    '%s — component rules use CSS variables not hardcoded colors',\n    (_file, getCss) => {\n      it('.df-calendar-container background uses var(--df-color-background)', () => {\n        expect(getCss()).toMatch(\n          /background-color:\\s*var\\(--df-color-background\\)/\n        );\n      });\n\n      it('.df-calendar-container border uses var(--df-color-border)', () => {\n        expect(getCss()).toMatch(\n          /border:\\s*1px solid var\\(--df-color-border\\)/\n        );\n      });\n\n      it('.df-week-header border-color uses var(--df-color-border)', () => {\n        expect(getCss()).toMatch(\n          /\\.df-week-header[\\s\\S]{0,200}border-color:\\s*var\\(--df-color-border\\)/\n        );\n      });\n\n      it('.df-event-detail-panel background uses var(--df-color-card)', () => {\n        expect(getCss()).toMatch(\n          /\\.df-event-detail-panel[\\s\\S]{0,200}background-color:\\s*var\\(--df-color-card\\)/\n        );\n      });\n\n      it('no DayFlow component rule hardcodes rgb(255 255 255) as background', () => {\n        // Utility classes like .bg-white { background-color: rgb(255 255 255) }\n        // are expected in styles.css; the concern is component-level rules\n        // (.df-*) using hardcoded white instead of var(--df-color-background).\n        const matches = getCss().matchAll(\n          /\\.(df-[a-z-]+)\\s*\\{[^}]*background-color:\\s*rgb\\(255 255 255\\)[^}]*}/g\n        );\n        expect([...matches]).toEqual([]);\n      });\n\n      it('no DayFlow component rule hardcodes a specific gray as border-color', () => {\n        // Hardcoded border-color: var(--color-gray-200) in a .df-* rule would\n        // break when the host overrides Tailwind's gray palette.\n        const matches = getCss().matchAll(\n          /\\.(df-[a-z-]+)\\s*\\{[^}]*border-color:\\s*var\\(--color-gray-\\d+\\)[^}]*}/g\n        );\n        expect([...matches]).toEqual([]);\n      });\n    }\n  );\n\n  describe('styles.components.css — semantic fallback coverage', () => {\n    it('no longer relies on dark:bg-primary/20 fallback after df-* migration', () => {\n      expect(componentsCss).not.toMatch(/\\.dark\\\\:bg-primary\\\\\\/20/);\n    });\n\n    it('no longer relies on shadow-primary fallback after df-* migration', () => {\n      expect(componentsCss).not.toMatch(/\\.shadow-primary\\\\(?:\\/20)?/);\n    });\n\n    it('no longer relies on destructive hover/focus fallback after df-* migration', () => {\n      expect(componentsCss).not.toMatch(/\\.hover\\\\:bg-destructive:hover/);\n      expect(componentsCss).not.toMatch(/\\.focus\\\\:bg-destructive:focus/);\n      expect(componentsCss).not.toMatch(\n        /\\.hover\\\\:text-destructive-foreground:hover/\n      );\n      expect(componentsCss).not.toMatch(\n        /\\.focus\\\\:text-destructive-foreground:focus/\n      );\n    });\n\n    it('includes portal and range picker scope selectors', () => {\n      expect(componentsCss).toMatch(\n        /df-portal[\\s\\S]*df-range-picker|df-range-picker[\\s\\S]*df-portal/\n      );\n    });\n  });\n\n  describe.each([\n    ['styles.components.css', () => componentsCss],\n    ['styles.css', () => fullCss],\n  ] as const)('%s — df-* semantic classes are emitted', (_file, getCss) => {\n    it('includes df-fill-primary and df-tint-primary rules', () => {\n      expect(getCss()).toMatch(/\\.df-fill-primary\\s*\\{/);\n      expect(getCss()).toMatch(/\\.df-tint-primary\\s*\\{/);\n    });\n\n    it('includes df-focus-ring and df-ring-primary-solid rules', () => {\n      expect(getCss()).toMatch(/\\.df-focus-ring:focus\\s*\\{/);\n      expect(getCss()).toMatch(/\\.df-ring-primary-solid\\s*\\{/);\n    });\n\n    it('includes df-fill-destructive and df-hover-destructive rules', () => {\n      expect(getCss()).toMatch(/\\.df-fill-destructive\\s*\\{/);\n      expect(getCss()).toMatch(/\\.df-hover-destructive:hover\\s*\\{/);\n    });\n  });\n\n  describe('shared foundation parity', () => {\n    it('shares core token values across both dist files', () => {\n      [\n        '--df-color-background',\n        '--df-color-primary',\n        '--df-color-border',\n      ].forEach(token => {\n        expect(extractToken(fullCss, token)).toBe(\n          extractToken(componentsCss, token)\n        );\n      });\n    });\n\n    it('shares system-preference dark tokens across both dist files', () => {\n      const pattern =\n        /@media\\s*\\(prefers-color-scheme:\\s*dark\\)[\\s\\S]*?--df-color-background:\\s*([^;]+)/;\n      expect(fullCss.match(pattern)?.[1]?.trim()).toBe(\n        componentsCss.match(pattern)?.[1]?.trim()\n      );\n    });\n\n    it('keeps df-portal scope in both dist files', () => {\n      expect(fullCss).toMatch(/\\.df-portal/);\n      expect(componentsCss).toMatch(/\\.df-portal/);\n    });\n  });\n\n  describe('styles.css — host reset compatibility', () => {\n    it('does not ship cascade layers that can lose to host unlayered resets', () => {\n      expect(fullCss).not.toMatch(/@layer\\s+/);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/src/styles/classNames.ts",
    "content": "/**\n * Shared DayFlow semantic class constants used across core components.\n * All exports use df-* semantic classes; atomic utility classes are prohibited.\n */\n\n// ==================== Container Styles ====================\n\n/**\n * Calendar main container\n * Used for the root container of WeekView and DayView\n */\nexport const calendarContainer = 'df-calendar';\n\n/**\n * MonthView container\n */\nexport const monthViewContainer = 'df-month-view';\n\n// ==================== Navigation Bar Styles ====================\n\n/**\n * Top navigation bar container\n */\nexport const headerContainer = 'df-view-header-container';\n\n/**\n * Title text style\n */\nexport const headerTitle = 'df-view-header-title';\n\n/**\n * Subtitle text style\n */\nexport const headerSubtitle = 'df-view-header-subtitle';\n\n// ==================== Button Styles ====================\n\n/**\n * Cancel button\n */\nexport const cancelButton = 'df-btn-sm df-btn-sm-ghost';\n\n// ==================== Grid Styles ====================\n\n/**\n * 7-column grid container (weekday titles)\n */\nexport const weekGrid = 'df-week-grid';\n\n/**\n * Week title row (MonthView)\n */\nexport const weekHeaderRow = 'df-week-header-row';\n\n/**\n * Weekday labels\n */\nexport const dayLabel = 'df-day-label';\n\n/**\n * WeekView week title\n */\nexport const weekDayHeader = 'df-week-header';\n\n/**\n * WeekView week title cell\n */\nexport const weekDayCell = 'df-week-day-cell';\n\n/**\n * Date number style\n */\nexport const dateNumber = 'df-date-number';\n\n// ==================== Scroll Area Styles ====================\n\n/**\n * Virtual scroll container\n */\nexport const scrollContainer = 'df-scroll-container';\n\n/**\n * Month day cell\n */\nexport const monthDayCell = 'df-month-day-cell';\n\n/**\n * Month date number container\n */\nexport const monthDateNumberContainer = 'df-month-date-number-container';\n\n/**\n * Month date number\n */\nexport const monthDateNumber = 'df-month-date-number';\n\n/**\n * Month more events indicator\n */\nexport const monthMoreEvents = 'df-month-more-events';\n\n/**\n * Month title (sticky)\n */\nexport const monthTitle = 'df-month-title';\n\n/**\n * Calendar content area (week/day view)\n */\nexport const calendarContent = 'df-calendar-content';\n\n/**\n * Hide scrollbar\n */\nexport const scrollbarHide = 'df-scrollbar-hide';\n\n// ==================== Time-Related Styles ====================\n\n/**\n * Time column container\n */\nexport const timeColumn = 'df-time-column';\n\n/**\n * Time slot\n */\nexport const timeSlot = 'df-time-slot';\n\n/**\n * Time label\n */\nexport const timeLabel = 'df-time-label';\n\n/**\n * Time grid row\n */\nexport const timeGridRow = 'df-time-grid-row';\n\n/**\n * Time grid cell\n */\nexport const timeGridCell = 'df-time-grid-cell';\n\n/**\n * Current time line container\n */\nexport const currentTimeLine = 'df-current-time-line';\n\n/**\n * Current time label\n */\nexport const currentTimeLabel = 'df-current-time-label';\n\n/**\n * Current time line bar\n */\nexport const currentTimeLineBar = 'df-current-time-bar';\n\n// ==================== All-Day Event Area ====================\n\n/**\n * All-day event row container\n */\nexport const allDayRow = 'df-all-day-row';\n\n/**\n * All-day event label\n */\nexport const allDayLabel = 'df-all-day-label';\n\n/**\n * All-day event content area\n */\nexport const allDayContent = 'df-all-day-content';\n\n/**\n * All-day event cell\n */\nexport const allDayCell = 'df-all-day-cell';\n\n// ==================== Event Styles ====================\n\n/**\n * Base event style\n */\nexport const baseEvent = 'df-event';\n\n/**\n * MonthView all-day event content\n */\nexport const monthAllDayContent = 'df-event-month-all-day';\n\n/**\n * MonthView regular event content\n */\nexport const monthRegularContent = 'df-event-month-regular';\n\n/**\n * Event title (small)\n */\nexport const eventTitleSmall = 'df-event-title';\n\n/**\n * Event time text\n */\nexport const eventTime = 'df-event-time';\n\n/**\n * Event color bar (Day/Week view timed events)\n */\nexport const eventColorBar = 'df-event-color-bar';\n\n/**\n * Event color bar (Month view regular events)\n */\nexport const monthEventColorBar = 'df-event-month-color-bar';\n\n/**\n * Event icon\n */\nexport const eventIcon = 'df-event-icon-svg';\n\n// ==================== Resize Handles ====================\n\n/**\n * Resize handle (top)\n */\nexport const resizeHandleTop =\n  'df-event-resize-handle df-event-resize-handle-top';\n\n/**\n * Resize handle (bottom)\n */\nexport const resizeHandleBottom =\n  'df-event-resize-handle df-event-resize-handle-bottom';\n\n/**\n * Resize handle (left)\n */\nexport const resizeHandleLeft =\n  'df-event-resize-handle df-event-resize-handle-left df-resize-handle';\n\n/**\n * Resize handle (right)\n */\nexport const resizeHandleRight =\n  'df-event-resize-handle df-event-resize-handle-right df-resize-handle';\n\n// ==================== Mini Calendar Styles (DayView) ====================\n\n/**\n * Mini calendar container\n */\nexport const miniCalendarContainer = 'df-mini-calendar';\n\n/**\n * Mini calendar grid\n */\nexport const miniCalendarGrid = 'df-mini-calendar-grid';\n\n/**\n * Mini calendar weekday title\n */\nexport const miniCalendarDayHeader = 'df-mini-calendar-header';\n\n/**\n * Mini calendar date cell base style\n */\nexport const miniCalendarDay = 'df-mini-calendar-day';\n\n// ==================== Navigation Button Styles ====================\n\n/**\n * Calendar navigation button (prev/next arrows)\n * Used in TodayBox component for navigation\n */\nexport const calendarNavButton = 'df-nav-button df-calendar-nav-button';\n\n/**\n * Calendar today button\n * Used in TodayBox component for \"Today\" button\n */\nexport const calendarTodayButton = 'df-today-button df-calendar-today-button';\n\n// ==================== Panel & Dialog Styles ====================\n\n/**\n * Fixed event detail panel\n * Used in DefaultEventDetailPanel, EventDetailPanelWithContent\n */\nexport const eventDetailPanel = 'df-event-detail-panel df-portal';\n\n/**\n * Event detail dialog container\n */\nexport const dialogContainer = 'df-dialog-container';\n\n/**\n * Calendar picker dropdown (for selecting calendar for an event)\n */\nexport const calendarPickerDropdown =\n  'df-portal df-calendar-picker-dropdown df-animate-in df-fade-in df-zoom-in-95';\n\n// ==================== Time Grid Boundary Styles ====================\n\n/**\n * Time grid bottom boundary (midnight line)\n * Used in TimeGrid.tsx and DayContent.tsx\n */\nexport const timeGridBoundary = 'df-time-grid-boundary';\n\n/**\n * Midnight time label\n */\nexport const midnightLabel = 'df-midnight-label';\n\n// ==================== Sidebar Styles ====================\n\n/**\n * Sidebar container\n */\nexport const sidebarContainer = 'df-sidebar';\n\n/**\n * Sidebar header\n */\nexport const sidebarHeader = 'df-sidebar-header';\n\n/**\n * Sidebar header toggle button\n */\nexport const sidebarHeaderToggle = 'df-sidebar-toggle';\n\n/**\n * Sidebar header title\n */\nexport const sidebarHeaderTitle = 'df-sidebar-header-title';\n\n/**\n * Mobile fullscreen overlay\n */\nexport const mobileFullscreen = 'df-mobile-fullscreen';\n\n// ==================== Form Input Styles ====================\n\n/**\n * Icon button (square, no text)\n */\nexport const iconButton = 'df-icon-btn';\n\n// ==================== Utility Styles ====================\n\n// ==================== Combined Style Utility Functions ====================\n\n/**\n * Combine multiple class names\n * @param classNames - Array of class name strings\n * @returns Combined class name string\n */\nexport const cn = (\n  ...classNames: (string | undefined | null | false)[]\n): string => classNames.filter(Boolean).join(' ');\n\n/**\n * Combine class names based on condition\n * @param base - Base class name\n * @param condition - Condition\n * @param whenTrue - Class name when condition is true\n * @param whenFalse - Class name when condition is false\n * @returns Combined class name string\n */\nexport const conditional = (\n  base: string,\n  condition: boolean,\n  whenTrue: string,\n  whenFalse?: string\n): string => cn(base, condition ? whenTrue : whenFalse);\n"
  },
  {
    "path": "packages/core/src/styles/core/common/forms-dialogs.css",
    "content": "@layer components {\n  /* ===================================================================\n   * Shared form elements  (.df-form-*)\n   * =================================================================== */\n\n  .df-form-label {\n    display: block;\n    margin-bottom: 0.25rem;\n    font-size: 0.75rem;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-form-input {\n    width: 100%;\n    border-radius: 0.5rem;\n    border: 1px solid var(--df-color-border);\n    padding: 0.375rem 0.75rem;\n    font-size: 0.875rem;\n    color: var(--df-color-foreground);\n    background-color: var(--df-color-card);\n    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n    transition:\n      border-color 150ms,\n      box-shadow 150ms;\n    outline: none;\n  }\n\n  .df-form-input:focus {\n    border-color: var(--df-color-primary);\n    box-shadow:\n      0 0 0 0px var(--df-color-background),\n      0 0 0 2px var(--df-color-primary);\n  }\n\n  .df-form-input:disabled,\n  .df-form-input[readonly] {\n    opacity: 0.5;\n  }\n\n  .df-form-textarea {\n    width: 100%;\n    border-radius: 0.5rem;\n    border: 1px solid var(--df-color-border);\n    padding: 0.5rem 0.75rem;\n    font-size: 0.875rem;\n    color: var(--df-color-foreground);\n    background-color: var(--df-color-card);\n    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n    resize: none;\n    transition:\n      border-color 150ms,\n      box-shadow 150ms;\n    outline: none;\n  }\n\n  .df-form-textarea:focus {\n    border-color: var(--df-color-primary);\n    box-shadow:\n      0 0 0 0px var(--df-color-background),\n      0 0 0 2px var(--df-color-primary);\n  }\n\n  .df-form-textarea:disabled,\n  .df-form-textarea[readonly] {\n    opacity: 0.5;\n  }\n\n  .df-form-row {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 0.75rem;\n    margin-bottom: 1rem;\n  }\n\n  .df-form-field {\n    flex: 1;\n  }\n\n  .df-form-actions {\n    display: flex;\n    gap: 0.5rem;\n  }\n\n  .df-btn-sm {\n    border-radius: 0.25rem;\n    padding: 0.25rem 0.5rem;\n    font-size: 0.75rem;\n    font-weight: 500;\n    cursor: pointer;\n    transition: all 150ms;\n    border: none;\n  }\n\n  .df-btn-sm:disabled {\n    opacity: 0.5;\n    cursor: default;\n  }\n\n  .df-btn-sm-ghost {\n    border: 1px solid var(--df-color-border);\n    color: var(--df-color-foreground);\n    background: transparent;\n  }\n\n  .df-btn-sm-ghost:hover:not(:disabled) {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-btn-md {\n    border-radius: 0.5rem;\n    padding: 0.5rem 0.75rem;\n    font-size: 0.75rem;\n    font-weight: 500;\n    cursor: pointer;\n    transition: all 150ms;\n    border: none;\n  }\n\n  .df-btn-md:disabled {\n    cursor: not-allowed;\n    opacity: 0.5;\n  }\n\n  .df-btn-md-secondary {\n    border: 1px solid var(--df-color-border);\n    color: var(--df-color-foreground);\n    background: transparent;\n  }\n\n  .df-btn-md-secondary:hover {\n    background-color: var(--df-color-hover);\n  }\n\n  /* ===================================================================\n   * CreateCalendarDialog  (.df-create-calendar-dialog__)\n   * =================================================================== */\n\n  .df-create-calendar-dialog-backdrop {\n    position: fixed;\n    inset: 0;\n    z-index: 10000;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background-color: rgb(0 0 0 / 0.5);\n  }\n\n  .df-create-calendar-dialog-panel {\n    width: 100%;\n    max-width: 24rem;\n    border-radius: 0.5rem;\n    background-color: var(--df-color-card);\n    padding: 1.5rem;\n    box-shadow:\n      0 20px 25px -5px rgb(0 0 0 / 0.1),\n      0 8px 10px -6px rgb(0 0 0 / 0.1);\n  }\n\n  .df-create-calendar-dialog-title {\n    font-size: 1.125rem;\n    font-weight: 600;\n    color: var(--df-color-foreground);\n    margin-bottom: 1.5rem;\n  }\n\n  .df-create-calendar-dialog-title-compact {\n    margin-bottom: 1rem;\n  }\n\n  .df-create-calendar-dialog-color-row {\n    display: flex;\n    align-items: center;\n    gap: 0.75rem;\n    margin-bottom: 1rem;\n  }\n\n  .df-create-calendar-dialog-color-preview {\n    flex-shrink: 0;\n    height: 2.25rem;\n    width: 2.25rem;\n    border-radius: 0.375rem;\n    border: 1px solid var(--df-color-border);\n    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n  }\n\n  .df-create-calendar-dialog-color-section {\n    margin-bottom: 1.5rem;\n  }\n\n  .df-create-calendar-dialog-color-grid {\n    margin-bottom: 1rem;\n    display: grid;\n    grid-template-columns: repeat(7, minmax(0, 1fr));\n    gap: 1rem;\n  }\n\n  .df-create-calendar-dialog-color-btn {\n    height: 1.75rem;\n    width: 1.75rem;\n    border-radius: 9999px;\n    border: 1px solid var(--df-color-border);\n    transition: transform 150ms;\n    cursor: pointer;\n    padding: 0;\n    background: none;\n  }\n\n  .df-create-calendar-dialog-color-btn:hover {\n    transform: scale(1.1);\n  }\n\n  .df-create-calendar-dialog-color-btn[data-selected='true'] {\n    transform: scale(1.1);\n    outline: 2px solid var(--df-color-primary);\n    outline-offset: 2px;\n  }\n\n  .df-create-calendar-dialog-custom-color-btn {\n    display: flex;\n    width: 100%;\n    align-items: center;\n    gap: 0.5rem;\n    border-radius: 0.375rem;\n    border: 1px solid var(--df-color-border);\n    padding: 0.25rem 0.5rem;\n    font-size: 0.875rem;\n    font-weight: 500;\n    color: var(--df-color-foreground);\n    background: transparent;\n    cursor: pointer;\n    transition: background-color 150ms;\n  }\n\n  .df-create-calendar-dialog-custom-color-btn:hover {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-create-calendar-dialog-picker-overlay {\n    position: fixed;\n    inset: 0;\n    z-index: 10001;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background-color: rgb(0 0 0 / 0.4);\n    backdrop-filter: blur(4px);\n  }\n\n  .df-create-calendar-dialog-picker-inner {\n    position: relative;\n  }\n\n  .df-create-calendar-dialog-picker-card {\n    border-radius: 0.5rem;\n    border: 1px solid var(--df-color-border);\n    background-color: var(--df-color-card);\n    padding: 1rem;\n    box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);\n  }\n\n  .df-create-calendar-dialog-picker-actions {\n    margin-top: 1rem;\n    display: flex;\n    justify-content: flex-end;\n    gap: 0.5rem;\n    border-top: 1px solid var(--df-color-border);\n    padding-top: 0.75rem;\n  }\n\n  .df-create-calendar-dialog-blossom-row {\n    display: flex;\n    align-items: center;\n    gap: 1rem;\n    margin-bottom: 2rem;\n  }\n\n  .df-create-calendar-dialog-blossom-input-area {\n    flex: 1;\n  }\n\n  .df-create-calendar-dialog-blossom-picker-wrap {\n    position: relative;\n    flex-shrink: 0;\n    height: 2.25rem;\n    width: 2.25rem;\n  }\n\n  .df-create-calendar-dialog-blossom-picker-inner {\n    position: absolute;\n    inset: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .df-create-calendar-dialog-blossom-picker {\n    z-index: 50;\n  }\n\n  .df-create-calendar-dialog-actions {\n    display: flex;\n    justify-content: flex-end;\n    gap: 0.5rem;\n  }\n\n  /* ===================================================================\n   * EventDetail Dialog / Panel\n   * =================================================================== */\n\n  .df-event-dialog-backdrop {\n    position: fixed;\n    inset: 0;\n    background-color: rgb(0 0 0 / 0.6);\n  }\n\n  .df-event-dialog-overlay {\n    position: fixed;\n    inset: 0;\n    z-index: 10000;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    pointer-events: none;\n  }\n\n  .df-event-dialog-overlay > * {\n    pointer-events: auto;\n  }\n\n  .dark .df-event-dialog-backdrop {\n    background-color: rgb(0 0 0 / 0.8);\n  }\n\n  .df-event-dialog-close {\n    position: absolute;\n    top: 1rem;\n    right: 1rem;\n    border: none;\n    background: transparent;\n    cursor: pointer;\n    color: var(--df-color-muted-foreground);\n    transition: color 150ms;\n  }\n\n  .df-event-dialog-close:hover {\n    color: var(--df-color-foreground);\n  }\n\n  .df-event-dialog-close:disabled {\n    opacity: 0.5;\n  }\n\n  .df-event-dialog-notes-row {\n    margin-bottom: 1rem;\n  }\n\n  .df-event-dialog-time-row {\n    margin-bottom: 1rem;\n  }\n\n  .df-event-panel-content {\n    padding: 1rem;\n  }\n\n  /* ===================================================================\n   * Event detail panel (fixed floating panel)\n   * =================================================================== */\n\n  .df-event-detail-panel {\n    position: fixed;\n    border-width: 1px;\n    border-style: solid;\n    border-radius: 0.5rem;\n    box-shadow:\n      0 10px 15px -3px rgb(0 0 0 / 0.1),\n      0 4px 6px -4px rgb(0 0 0 / 0.1);\n  }\n\n  /* ===================================================================\n   * Dialog container (centered modal content box)\n   * =================================================================== */\n\n  .df-dialog-container {\n    position: relative;\n    border-width: 1px;\n    border-style: solid;\n    border-radius: 0.5rem;\n    padding: 1.5rem;\n    max-width: 28rem;\n    width: 100%;\n    margin-inline: 1rem;\n    box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);\n  }\n\n  /* Close button icon sizing */\n  .df-event-dialog-close svg {\n    width: 1.25rem;\n    height: 1.25rem;\n  }\n\n  /* ===================================================================\n   * LoadingButton  (.df-loading-btn)\n   * =================================================================== */\n\n  .df-loading-btn {\n    position: relative;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    transition: all 150ms;\n  }\n\n  .df-loading-btn[data-loading='true'] {\n    cursor: not-allowed;\n    opacity: 0.7;\n  }\n\n  @keyframes df-spin {\n    from {\n      transform: rotate(0deg);\n    }\n    to {\n      transform: rotate(360deg);\n    }\n  }\n\n  .df-loading-btn-spinner {\n    margin-right: 0.5rem;\n    width: 1rem;\n    height: 1rem;\n    animation: df-spin 1s linear infinite;\n  }\n\n  .df-loading-btn-track {\n    opacity: 0.25;\n  }\n\n  .df-loading-btn-fill {\n    opacity: 0.75;\n  }\n\n  .df-loading-btn-label {\n    display: inline-flex;\n    align-items: center;\n  }\n\n  /* ===================================================================\n   * DefaultColorPicker wrapper\n   * =================================================================== */\n\n  .df-default-color-picker {\n    display: flex;\n    justify-content: center;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/styles/core/common/header-controls.css",
    "content": "@layer components {\n  /* ===================================================================\n   * CalendarHeader  (.df-header + .df-header-left/mid/right)\n   * =================================================================== */\n\n  .df-header {\n    display: flex;\n    flex-shrink: 0;\n    align-items: center;\n    justify-content: space-between;\n    border-bottom: 1px solid transparent;\n    padding-block: 0.25rem;\n    padding-right: 0.5rem;\n    transition:\n      border-color 200ms,\n      background-color 200ms,\n      color 200ms;\n  }\n\n  .df-header[data-bordered='true'] {\n    border-bottom-color: var(--df-color-border);\n  }\n\n  .df-calendar-container .df-header-left {\n    display: flex;\n    align-items: center;\n  }\n\n  #dayflow-add-event-btn {\n    color: #6b7282;\n  }\n\n  .df-calendar-container .df-header-mid {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .df-calendar-container .df-header-right {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    gap: 0.75rem;\n  }\n\n  /* ===================================================================\n   * ViewHeader  (.df-view-header-container)\n   * =================================================================== */\n\n  .df-view-header-container {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 0.5rem;\n    position: relative;\n  }\n\n  .df-view-header-title-area {\n    flex: 1;\n  }\n\n  .df-view-header-title {\n    font-size: 1.5rem;\n    line-height: 2rem;\n    font-weight: 700;\n    color: var(--df-color-foreground);\n  }\n\n  .df-view-header-subtitle {\n    margin-top: 0.75rem;\n    line-height: 1.5rem;\n    color: var(--df-color-muted-foreground);\n    font-size: 0.875rem;\n  }\n\n  .df-view-header-subtitle-row {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: end;\n    gap: 0.5rem;\n    margin-top: 0.5rem;\n  }\n\n  .df-view-header-subtitle-row .df-view-header-subtitle {\n    margin-top: 0;\n  }\n\n  .df-view-header-subtitle-meta {\n    display: inline-flex;\n    align-items: center;\n    gap: 0.375rem;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-view-header-nav {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n  }\n\n  .df-view-header-year-stack {\n    position: relative;\n    overflow: hidden;\n    height: 1.5em;\n  }\n\n  .df-view-header-year-title {\n    position: absolute;\n    top: 0;\n    left: 0;\n    will-change: transform;\n  }\n\n  /* ===================================================================\n   * ViewSwitcher  (.df-view-switcher / .df-view-switcher-select)\n   * =================================================================== */\n\n  .df-view-switcher {\n    display: inline-flex;\n    height: 1.75rem;\n    align-items: center;\n    gap: 0.25rem;\n    border-radius: 0.5rem;\n    background-color: rgb(243 244 246);\n    padding: 0.125rem;\n  }\n\n  .dark .df-view-switcher {\n    background-color: rgb(31 41 55);\n  }\n\n  .df-view-switcher-btn {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    height: 1.5rem;\n    border-radius: 0.375rem;\n    padding-inline: 1rem;\n    font-size: 0.875rem;\n    font-weight: 500;\n    transition: all 200ms;\n    color: rgb(75 85 99);\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    outline: none;\n    white-space: nowrap;\n    width: max-content;\n    flex-shrink: 0;\n  }\n\n  .dark .df-view-switcher-btn {\n    color: rgb(156 163 175);\n  }\n\n  .df-view-switcher-btn:hover {\n    background-color: rgb(249 250 251);\n    color: rgb(17 24 39);\n  }\n\n  .dark .df-view-switcher-btn:hover {\n    background-color: rgb(55 65 81);\n    color: rgb(243 244 246);\n  }\n\n  .df-view-switcher-btn[data-active='true'] {\n    background-color: white;\n    color: rgb(17 24 39);\n    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n  }\n\n  .dark .df-view-switcher-btn[data-active='true'] {\n    background-color: rgb(55 65 81);\n    color: rgb(243 244 246);\n  }\n\n  .df-view-switcher-select {\n    position: relative;\n    display: inline-block;\n  }\n\n  .df-view-switcher-select-trigger {\n    display: flex;\n    height: 1.75rem;\n    min-width: 7.5rem;\n    align-items: center;\n    justify-content: space-between;\n    gap: 0.5rem;\n    border-radius: 0.5rem;\n    border: 1px solid var(--df-color-border);\n    background-color: var(--df-color-background);\n    padding-inline: 0.75rem;\n    font-size: 0.875rem;\n    font-weight: 500;\n    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n    transition: background-color 200ms;\n    color: var(--df-color-foreground);\n    cursor: pointer;\n    outline: none;\n  }\n\n  .df-view-switcher-select-trigger:hover {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-view-switcher-select-chevron {\n    transition: transform 200ms;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-view-switcher-select-chevron[data-open='true'] {\n    transform: rotate(180deg);\n  }\n\n  .df-view-switcher-select-dropdown {\n    position: absolute;\n    top: 100%;\n    left: 0;\n    z-index: 50;\n    margin-top: 0.25rem;\n    width: 100%;\n    min-width: 7.5rem;\n    overflow: hidden;\n    border-radius: 0.5rem;\n    border: 1px solid var(--df-color-border);\n    background-color: var(--df-color-background);\n    box-shadow:\n      0 10px 15px -3px rgb(0 0 0 / 0.1),\n      0 4px 6px -4px rgb(0 0 0 / 0.1);\n  }\n\n  .df-view-switcher-select-list {\n    padding: 0.25rem;\n  }\n\n  .df-view-switcher-select-option {\n    width: 100%;\n    border-radius: 0.25rem;\n    padding: 0.25rem 0.75rem;\n    text-align: left;\n    font-size: 0.875rem;\n    transition: background-color 150ms;\n    color: var(--df-color-foreground);\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    outline: none;\n  }\n\n  .df-view-switcher-select-option:hover {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-view-switcher-select-option[data-active='true'] {\n    font-weight: 500;\n  }\n\n  /* ===================================================================\n   * MiniCalendar extras  (.df-mini-calendar__*)\n   * =================================================================== */\n\n  .df-mini-calendar-body {\n    padding: 0.75rem;\n  }\n\n  .df-mini-calendar-header-nav {\n    display: flex;\n    margin-bottom: 0.75rem;\n    align-items: center;\n    justify-content: space-between;\n  }\n\n  .df-mini-calendar-nav-btn {\n    display: flex;\n    height: 1.75rem;\n    width: 1.75rem;\n    align-items: center;\n    justify-content: center;\n    border-radius: 9999px;\n    border: none;\n    background: transparent;\n    cursor: pointer;\n    color: var(--df-color-muted-foreground);\n    transition: background-color 150ms;\n  }\n\n  .df-mini-calendar-nav-btn:hover {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-mini-calendar-month-label {\n    font-size: 0.875rem;\n    font-weight: 600;\n    color: rgb(55 65 81);\n  }\n\n  .dark .df-mini-calendar-month-label {\n    color: rgb(229 231 235);\n  }\n\n  .df-mini-calendar-day-cell {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    padding-block: 0.25rem;\n  }\n\n  .df-mini-calendar-day-number {\n    position: relative;\n    z-index: 10;\n  }\n\n  .df-mini-calendar-dots {\n    position: absolute;\n    bottom: 0.125rem;\n    display: flex;\n    gap: 1px;\n  }\n\n  .df-mini-calendar-dot {\n    height: 0.25rem;\n    width: 0.25rem;\n    border-radius: 9999px;\n  }\n\n  /* ===================================================================\n   * CalendarPicker  (.df-calendar-picker)\n   * =================================================================== */\n\n  .df-calendar-picker {\n    position: relative;\n    display: inline-block;\n  }\n\n  .df-calendar-picker-trigger {\n    display: flex;\n    height: 2rem;\n    align-items: center;\n    gap: 0.5rem;\n    border-radius: 0.375rem;\n    border: none;\n    background-color: var(--df-color-muted);\n    padding: 0.25rem 0.5rem;\n    cursor: pointer;\n    transition: background-color 150ms;\n  }\n\n  .df-calendar-picker-trigger:hover {\n    background-color: color-mix(in srgb, var(--df-color-muted), black 4%);\n  }\n\n  .dark .df-calendar-picker-trigger:hover {\n    background-color: color-mix(in srgb, var(--df-color-muted), white 8%);\n  }\n\n  .df-calendar-picker-trigger-mobile {\n    padding: 0.375rem 0.75rem;\n    gap: 0.5rem;\n    height: auto;\n  }\n\n  .df-calendar-picker-trigger:disabled {\n    cursor: default;\n    opacity: 0.5;\n  }\n\n  .df-calendar-picker-color-swatch {\n    flex-shrink: 0;\n    border-radius: 0.25rem;\n    height: 1rem;\n    width: 1rem;\n  }\n\n  .df-calendar-picker-color-swatch-sm {\n    border-radius: 0.25rem;\n    height: 0.75rem;\n    width: 0.75rem;\n  }\n\n  .df-calendar-picker-trigger-mobile .df-calendar-picker-color-swatch-sm,\n  .df-calendar-picker-option-mobile .df-calendar-picker-color-swatch-sm {\n    border-radius: 9999px;\n  }\n\n  .df-calendar-picker-label {\n    font-size: 0.875rem;\n    font-weight: 500;\n    color: var(--df-color-foreground);\n    white-space: nowrap;\n  }\n\n  .df-calendar-picker-option {\n    display: flex;\n    cursor: pointer;\n    align-items: center;\n    padding: 0.25rem 0.5rem;\n    transition: background-color 150ms;\n  }\n\n  .df-calendar-picker-option:hover {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-calendar-picker-option-mobile {\n    padding: 0.5rem 1rem;\n  }\n\n  .df-calendar-picker-option[data-selected='true'] {\n    font-weight: 600;\n  }\n\n  .df-calendar-picker-option-mobile[data-selected='true'] {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-calendar-picker-option-inner {\n    display: flex;\n    min-width: 0;\n    flex: 1;\n    align-items: center;\n    margin-right: 0.75rem;\n  }\n\n  .df-calendar-picker-check-area {\n    display: flex;\n    width: 1.25rem;\n    justify-content: center;\n    margin-right: 0.5rem;\n    flex-shrink: 0;\n  }\n\n  .df-calendar-picker-option-label {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    font-size: 0.875rem;\n    color: var(--df-color-foreground);\n  }\n\n  /* CalendarPicker dropdown container (used by both mobile and desktop variants) */\n  .df-calendar-picker-dropdown {\n    overflow: hidden;\n    border-radius: 0.375rem;\n    border: 1px solid var(--df-color-border);\n    background-color: var(--df-color-card);\n    box-shadow:\n      0 10px 15px -3px rgb(0 0 0 / 0.1),\n      0 4px 6px -4px rgb(0 0 0 / 0.1);\n    transition: all 200ms;\n    transform-origin: top right;\n  }\n\n  /* Icon sizing inside trigger and check-area */\n  .df-calendar-picker-trigger svg,\n  .df-calendar-picker-check-area svg {\n    width: 1rem;\n    height: 1rem;\n  }\n\n  /* Color swatch spacing when inside an option row */\n  .df-calendar-picker-option .df-calendar-picker-color-swatch-sm {\n    margin-right: 0.5rem;\n  }\n\n  /* ===================================================================\n   * Navigation (TodayBox)  (.df-navigation)\n   * =================================================================== */\n\n  .df-navigation {\n    display: flex;\n    align-items: center;\n    gap: 0.25rem;\n  }\n\n  /* Icon sizing + hover scale inside nav buttons */\n  .df-calendar-nav-button svg {\n    width: 1rem;\n    height: 1rem;\n    transition: transform 150ms;\n  }\n\n  .df-calendar-nav-button:hover svg {\n    transform: scale(1.1);\n  }\n\n  /* ===================================================================\n   * MiniCalendar grid and cells\n   * =================================================================== */\n\n  .df-mini-calendar-nav-btn svg {\n    width: 1rem;\n    height: 1rem;\n  }\n\n  .df-mini-calendar-grid {\n    display: grid;\n    grid-template-columns: repeat(7, minmax(0, 1fr));\n    gap: 0.25rem;\n    font-size: 0.75rem;\n    justify-items: center;\n  }\n\n  .df-mini-calendar-header {\n    text-align: center;\n    color: var(--df-color-muted-foreground);\n    font-weight: 500;\n    padding-block: 0.25rem;\n    height: 1.5rem;\n    width: 1.5rem;\n  }\n\n  .df-mini-calendar-day {\n    font-size: 0.75rem;\n    height: 1.5rem;\n    width: 1.5rem;\n    color: var(--df-color-foreground);\n    border-radius: 0.25rem;\n  }\n\n  .df-mini-calendar-day[data-other-month='true'] {\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-mini-calendar-day[data-today='true'] {\n    background-color: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n    border-radius: 9999px;\n  }\n\n  .df-mini-calendar-day[data-selected='true'] {\n    background-color: var(--df-color-secondary);\n    color: var(--df-color-secondary-foreground);\n    border-radius: 9999px;\n    font-weight: 500;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/styles/core/events/calendar-event.css",
    "content": "@layer components {\n  /* ===================================================================\n   * Calendar event root contract\n   * =================================================================== */\n\n  .df-event,\n  .df-month-segment-event {\n    min-width: 0;\n    user-select: none;\n    -webkit-user-select: none;\n    pointer-events: auto;\n    color: inherit;\n  }\n\n  .df-event {\n    position: relative;\n    overflow: hidden;\n  }\n\n  .df-event[data-view='month'][data-multi-day='true'] {\n    position: static;\n    overflow: visible;\n  }\n\n  .df-event[data-touch-handles='true'] {\n    overflow: visible;\n  }\n\n  .df-event[data-view='day'],\n  .df-event[data-view='week'] {\n    display: flex;\n    flex-direction: column;\n    box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);\n  }\n\n  .df-event[data-view='day'][data-all-day='false'],\n  .df-event[data-view='week'][data-all-day='false'] {\n    border-radius: 0.25rem;\n  }\n\n  .df-event[data-dragging='true'],\n  .df-month-segment-event[data-dragging='true'] {\n    border-radius: 0.25rem;\n  }\n\n  .df-event[data-view='year'] {\n    display: flex;\n    overflow: hidden;\n    padding-inline: 0.25rem;\n    white-space: nowrap;\n    cursor: pointer;\n    transition:\n      background-color 150ms,\n      color 150ms;\n  }\n\n  .df-event[data-view='year'] > .df-content-slot {\n    width: 100%;\n    min-width: 0;\n  }\n\n  .df-event[data-view='month'][data-all-day='false'] {\n    border-radius: 0.25rem;\n  }\n\n  .df-event[data-view='month'][data-month-stack='true'] {\n    margin-bottom: 2px;\n  }\n\n  .df-event[data-view='month'][data-month-stack='true']:last-child {\n    margin-bottom: 0;\n  }\n\n  .df-event[data-all-day='true']:not([data-view='year']) {\n    margin-block: 0.125rem;\n  }\n\n  .df-event[data-all-day='true']:not(\n      [data-view='year']\n    )[data-segment-shape='full'] {\n    border-radius: 0.75rem;\n  }\n\n  .df-event[data-all-day='true']:not(\n      [data-view='year']\n    )[data-segment-shape='start'] {\n    border-radius: 0.75rem 0 0 0.75rem;\n  }\n\n  .df-event[data-all-day='true']:not(\n      [data-view='year']\n    )[data-segment-shape='end'] {\n    border-radius: 0 0.75rem 0.75rem 0;\n  }\n\n  .df-event[data-all-day='true']:not(\n      [data-view='year']\n    )[data-segment-shape='middle'] {\n    border-radius: 0;\n  }\n\n  .df-event[data-view='year'][data-segment-shape='full'],\n  .df-month-segment-event[data-segment-shape='full'] {\n    border-radius: 0.25rem;\n  }\n\n  .df-event[data-view='year'][data-segment-shape='start'],\n  .df-month-segment-event[data-segment-shape='start'] {\n    border-radius: 0.25rem 0 0 0.25rem;\n  }\n\n  .df-event[data-view='year'][data-segment-shape='end'],\n  .df-month-segment-event[data-segment-shape='end'] {\n    border-radius: 0 0.25rem 0.25rem 0;\n  }\n\n  .df-event[data-view='year'][data-segment-shape='middle'],\n  .df-month-segment-event[data-segment-shape='middle'] {\n    border-radius: 0;\n  }\n\n  /* ===================================================================\n   * Shared event internals\n   * =================================================================== */\n\n  .df-event-all-day-shell {\n    position: absolute;\n    inset: 0;\n    display: flex;\n    align-items: center;\n    overflow: hidden;\n    padding-inline: 0.25rem 0.25rem;\n    padding-left: 0.75rem;\n  }\n\n  .df-event-all-day-shell .df-event-icon-slot {\n    margin-right: 0.5rem;\n  }\n\n  .df-event-content-row {\n    display: flex;\n    min-width: 0;\n    flex: 1;\n    align-items: center;\n    overflow: hidden;\n  }\n\n  .df-event-timed-content {\n    display: flex;\n    height: 100%;\n    flex-direction: column;\n    overflow: hidden;\n    padding: 0.25rem;\n    padding-left: 0.75rem;\n  }\n\n  .df-event-timed-content[data-density='compact'] {\n    padding-block: 0;\n  }\n\n  .df-event-icon-slot {\n    margin-right: 0.375rem;\n    display: inline-flex;\n    flex-shrink: 0;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .df-event-icon-svg {\n    display: block;\n    height: 0.75rem;\n    width: 0.75rem;\n    flex-shrink: 0;\n  }\n\n  .df-event-title {\n    overflow: hidden;\n    padding-right: 0.25rem;\n    font-size: 0.75rem;\n    font-weight: 500;\n    line-height: 1rem;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n  }\n\n  .df-event-title-tight {\n    line-height: 1.2;\n  }\n\n  .df-event-time {\n    overflow: hidden;\n    font-size: 0.75rem;\n    line-height: 1rem;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n  }\n\n  .df-event-color-bar {\n    position: absolute;\n    top: 0.25rem;\n    bottom: 0.25rem;\n    left: 0.25rem;\n    width: 3px;\n    border-radius: 9999px;\n  }\n\n  .df-event-color-bar-overlay {\n    position: absolute;\n    inset: 0;\n    pointer-events: none;\n  }\n\n  .df-event-resize-handle {\n    position: absolute;\n    z-index: 20;\n    transition: opacity 150ms;\n  }\n\n  .df-event-resize-handle-top,\n  .df-event-resize-handle-bottom {\n    left: 0;\n    width: 100%;\n    height: 0.375rem;\n    z-index: 10;\n    cursor: ns-resize;\n  }\n\n  .df-event-resize-handle-top {\n    top: 0;\n    border-radius: 0.25rem 0.25rem 0 0;\n  }\n\n  .df-event-resize-handle-bottom {\n    bottom: 0;\n    border-radius: 0 0 0.25rem 0.25rem;\n  }\n\n  .df-event-resize-handle-left,\n  .df-event-resize-handle-right {\n    top: 0;\n    bottom: 0;\n    width: 0.25rem;\n    cursor: ew-resize;\n    opacity: 0;\n  }\n\n  .df-event-resize-handle-left {\n    left: 0;\n  }\n\n  .df-event-resize-handle-right {\n    right: 0;\n  }\n\n  .df-event:hover .df-event-resize-handle-left,\n  .df-event:hover .df-event-resize-handle-right,\n  .df-event[data-selected='true'] .df-event-resize-handle-left,\n  .df-event[data-selected='true'] .df-event-resize-handle-right,\n  .df-month-segment-event:hover .df-event-resize-handle-left,\n  .df-month-segment-event:hover .df-event-resize-handle-right,\n  .df-month-segment-event[data-selected='true'] .df-event-resize-handle-left,\n  .df-month-segment-event[data-selected='true'] .df-event-resize-handle-right {\n    opacity: 1;\n  }\n\n  .df-event-touch-resize-indicator {\n    position: absolute;\n    z-index: 50;\n    box-sizing: border-box;\n    height: 0.625rem;\n    width: 0.625rem;\n    border-radius: 9999px;\n    border: 2px solid currentcolor;\n    background-color: #fff;\n    pointer-events: auto;\n  }\n\n  .df-event-touch-resize-indicator[data-axis='vertical'][data-position='top'] {\n    top: -0.375rem;\n    right: 1.25rem;\n  }\n\n  .df-event-touch-resize-indicator[data-axis='vertical'][data-position='bottom'] {\n    bottom: -0.375rem;\n    left: 1.25rem;\n  }\n\n  .df-event-touch-resize-indicator[data-axis='horizontal'] {\n    top: 50%;\n    transform: translateY(-50%);\n  }\n\n  .df-event-touch-resize-indicator[data-axis='horizontal'][data-position='left'] {\n    left: 1.25rem;\n  }\n\n  .df-event-touch-resize-indicator[data-axis='horizontal'][data-position='right'] {\n    right: 1.25rem;\n  }\n\n  /* ===================================================================\n   * Month event variants\n   * =================================================================== */\n\n  .df-event-month-all-day,\n  .df-event-month-regular {\n    display: flex;\n    min-width: 0;\n    align-items: center;\n    font-size: 0.75rem;\n    line-height: 1rem;\n    cursor: pointer;\n  }\n\n  .df-event-month-all-day {\n    padding-inline: 0.25rem;\n    overflow: hidden;\n    border-radius: 0.25rem;\n  }\n\n  .df-event-month-regular {\n    height: var(--df-month-event-height, 1rem);\n    justify-content: space-between;\n    padding-inline: 0.125rem;\n  }\n\n  .df-event-month-main {\n    display: flex;\n    min-width: 0;\n    flex: 1;\n    align-items: center;\n  }\n\n  .df-event-month-color-bar,\n  .df-event-year-indicator {\n    display: inline-block;\n    height: 0.75rem;\n    width: 0.1875rem;\n    flex-shrink: 0;\n    border-radius: 9999px;\n  }\n\n  .df-event-month-color-bar {\n    height: calc(var(--df-month-event-height, 1rem) - 0.25rem);\n    margin-right: 0.25rem;\n  }\n\n  .df-event-month-title {\n    display: block;\n    min-width: 0;\n    flex: 1;\n    overflow: hidden;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n  }\n\n  .df-event-month-title.df-mobile-mask-fade {\n    text-overflow: clip;\n  }\n\n  .df-event-month-all-day .df-event-icon-slot,\n  .df-event-month-all-day .df-event-icon-svg {\n    margin-right: 0.25rem;\n  }\n\n  .df-event-month-time {\n    margin-left: 0.25rem;\n    flex-shrink: 0;\n    font-size: 0.75rem;\n    line-height: 1rem;\n  }\n\n  .df-event-holiday-icon {\n    display: inline-block;\n    margin-right: 0.25rem;\n    flex-shrink: 0;\n    color: rgb(202 138 4);\n  }\n\n  .df-event[data-selected='true'] .df-event-month-title,\n  .df-event[data-selected='true'] .df-event-month-time,\n  .df-event[data-selected='true'] .df-event-month-all-day .df-event-icon-svg,\n  .df-month-segment-event[data-selected='true'] .df-event-month-title,\n  .df-month-segment-event[data-selected='true'] .df-event-month-time {\n    color: #fff;\n  }\n\n  .df-event[data-selected='true'] .df-event-holiday-icon {\n    color: rgb(254 240 138);\n  }\n\n  /* ===================================================================\n   * Year event variants\n   * =================================================================== */\n\n  .df-event-year-content {\n    display: flex;\n    height: 100%;\n    width: 100%;\n    min-width: 0;\n    align-items: center;\n    overflow: hidden;\n    pointer-events: auto;\n  }\n\n  .df-event-year-content-timed {\n    gap: 0.25rem;\n  }\n\n  .df-event-year-icon-badge {\n    display: inline-flex;\n    height: 12px;\n    width: 12px;\n    box-sizing: border-box;\n    padding: 0.125rem;\n    align-items: center;\n    justify-content: center;\n    border-radius: 9999px;\n    color: #fff;\n  }\n\n  .df-event-year-icon-badge .df-event-icon-svg {\n    margin-right: 0;\n    height: 0.5rem;\n    width: 0.5rem;\n  }\n\n  .df-event-year-main {\n    min-width: 0;\n    flex: 1;\n  }\n\n  .df-event-year-title {\n    display: block;\n    overflow: hidden;\n    font-size: 12px;\n    line-height: 16px;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n  }\n\n  .df-event-year-title-fade {\n    text-overflow: clip;\n    mask-image: linear-gradient(to right, black 70%, transparent 100%);\n    -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%);\n  }\n\n  .df-event-year-title-strong {\n    flex: 1;\n    min-width: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    font-weight: 500;\n  }\n\n  .df-event-year-title-strong.df-event-year-title-fade {\n    text-overflow: clip;\n    mask-image: linear-gradient(to right, black 70%, transparent 100%);\n    -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%);\n  }\n\n  .df-event-year-tail {\n    margin-left: auto;\n    flex-shrink: 0;\n    color: rgb(255 255 255 / 0.8);\n  }\n\n  .dark .df-event-year-tail {\n    color: rgb(255 255 255 / 0.9);\n  }\n\n  .df-event-year-tail-dot {\n    height: 0.375rem;\n    width: 0.375rem;\n    border-radius: 9999px;\n    background-color: rgb(255 255 255 / 0.6);\n  }\n\n  .dark .df-event-year-tail-dot {\n    background-color: rgb(255 255 255 / 0.8);\n  }\n\n  /* ===================================================================\n   * Month multi-day segment shell\n   * =================================================================== */\n\n  .df-month-segment-event {\n    position: absolute;\n    display: flex;\n    align-items: center;\n    padding-inline: 0.25rem;\n    font-size: 0.75rem;\n    transition:\n      box-shadow 200ms,\n      transform 500ms cubic-bezier(0.22, 1, 0.36, 1);\n  }\n\n  .df-month-segment-event:hover {\n    box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);\n  }\n\n  .dark .df-month-segment-event:hover {\n    box-shadow:\n      0 10px 15px -3px rgb(0 0 0 / 0.3),\n      0 4px 6px -4px rgb(0 0 0 / 0.3);\n  }\n\n  .df-month-segment-event-body {\n    min-width: 0;\n    flex: 1;\n  }\n\n  .df-month-segment-event-all-day {\n    display: flex;\n    min-width: 0;\n    width: 100%;\n    align-items: center;\n    overflow: hidden;\n  }\n\n  .df-month-segment-event-all-day-main {\n    min-width: 0;\n    flex: 1;\n  }\n\n  .df-month-segment-event-all-day-title {\n    display: block;\n    overflow: hidden;\n    font-size: 0.75rem;\n    line-height: 1rem;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n  }\n\n  .df-month-segment-event-time {\n    font-size: 0.75rem;\n    font-weight: 500;\n    line-height: 1rem;\n    white-space: nowrap;\n  }\n\n  .df-month-segment-event-time-spaced {\n    margin-left: 0.5rem;\n  }\n\n  .df-month-segment-event-time-overlay {\n    position: absolute;\n  }\n\n  .df-month-segment-event-tail-time {\n    margin-left: auto;\n    font-size: 0.75rem;\n    font-weight: 500;\n    line-height: 1rem;\n    white-space: nowrap;\n  }\n\n  .df-month-segment-event-tail {\n    margin-left: 0.25rem;\n    flex-shrink: 0;\n    color: rgb(255 255 255 / 0.8);\n  }\n\n  .dark .df-month-segment-event-tail {\n    color: rgb(255 255 255 / 0.9);\n  }\n\n  .df-month-segment-event-tail-dot {\n    height: 0.375rem;\n    width: 0.375rem;\n    border-radius: 9999px;\n    background-color: rgb(255 255 255 / 0.6);\n  }\n\n  .dark .df-month-segment-event-tail-dot {\n    background-color: rgb(255 255 255 / 0.8);\n  }\n}\n"
  },
  {
    "path": "packages/core/src/styles/core/overlays/mobile-event-drawer.css",
    "content": "@layer components {\n  .df-mobile-event-drawer {\n    position: fixed;\n    inset: 0;\n    z-index: 10000;\n    display: flex;\n    align-items: flex-end;\n    pointer-events: none;\n  }\n\n  .df-mobile-event-drawer-backdrop {\n    position: absolute;\n    inset: 0;\n    background-color: rgb(0 0 0 / 0.3);\n    transition: opacity 300ms;\n    pointer-events: auto;\n    touch-action: none;\n  }\n\n  .df-mobile-event-drawer-backdrop[data-closing='true'] {\n    opacity: 0;\n  }\n\n  .df-mobile-event-drawer-backdrop[data-closing='false'] {\n    opacity: 1;\n  }\n\n  .df-mobile-event-drawer-panel {\n    position: relative;\n    display: flex;\n    height: 85vh;\n    width: 100%;\n    flex-direction: column;\n    overflow: hidden;\n    border-radius: 1rem 1rem 0 0;\n    background-color: rgb(243 244 246);\n    box-shadow:\n      0 20px 25px -5px rgb(0 0 0 / 0.1),\n      0 8px 10px -6px rgb(0 0 0 / 0.1);\n    pointer-events: auto;\n  }\n\n  .dark .df-mobile-event-drawer-panel {\n    background-color: rgb(31 41 55);\n  }\n\n  .df-mobile-event-drawer-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    border-bottom: 1px solid var(--df-color-border);\n    background-color: var(--df-color-card);\n    padding: 1rem;\n  }\n\n  .dark .df-mobile-event-drawer-header {\n    border-bottom-color: var(--df-color-border);\n    background-color: var(--df-color-card);\n  }\n\n  .df-mobile-event-drawer-title {\n    font-size: 1.125rem;\n    font-weight: 600;\n    color: var(--df-color-foreground);\n  }\n\n  .df-mobile-event-drawer-header-action {\n    min-width: 3rem;\n    border: none;\n    background: transparent;\n    padding: 0.25rem 0.5rem;\n    color: var(--df-color-muted-foreground);\n    transition: color 150ms;\n    cursor: pointer;\n  }\n\n  .df-mobile-event-drawer-header-action:hover:not(:disabled) {\n    color: var(--df-color-foreground);\n  }\n\n  .df-mobile-event-drawer-header-action-primary {\n    font-weight: 700;\n    color: var(--df-color-primary);\n  }\n\n  .df-mobile-event-drawer-header-action-disabled,\n  .df-mobile-event-drawer-header-action:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n  }\n\n  .df-mobile-event-drawer-header-spacer {\n    width: 3rem;\n  }\n\n  .df-mobile-event-drawer-body {\n    flex: 1;\n    overflow-y: auto;\n    padding: 1rem;\n  }\n\n  .df-mobile-event-drawer-body > * + * {\n    margin-top: 1rem;\n  }\n\n  .df-mobile-event-drawer-section {\n    overflow: hidden;\n    border-radius: 0.5rem;\n    background-color: var(--df-color-card);\n  }\n\n  .dark .df-mobile-event-drawer-section {\n    background-color: var(--df-color-card);\n  }\n\n  .df-mobile-event-drawer-section-framed {\n    padding: 0.75rem 1rem;\n  }\n\n  .df-mobile-event-drawer-section-danger {\n    color: rgb(239 68 68);\n  }\n\n  .df-mobile-event-drawer-title-input {\n    width: 100%;\n    border: none;\n    background: transparent;\n    font-size: 1.25rem;\n    font-weight: 500;\n    color: var(--df-color-foreground);\n    outline: none;\n  }\n\n  .df-mobile-event-drawer-title-input::placeholder,\n  .df-mobile-event-drawer-notes::placeholder {\n    color: rgb(156 163 175);\n  }\n\n  .df-mobile-event-drawer-notes {\n    min-height: 5rem;\n    width: 100%;\n    border: none;\n    background: transparent;\n    font-size: 1rem;\n    color: var(--df-color-foreground);\n    resize: none;\n    outline: none;\n  }\n\n  .df-mobile-event-drawer-row {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 0.75rem;\n  }\n\n  .df-mobile-event-drawer-row + .df-mobile-event-drawer-row {\n    border-top: 1px solid rgb(243 244 246);\n  }\n\n  .dark .df-mobile-event-drawer-row + .df-mobile-event-drawer-row {\n    border-top-color: rgb(31 41 55);\n  }\n\n  .df-mobile-event-drawer-row-padded {\n    padding: 0.75rem 1rem;\n  }\n\n  .df-mobile-event-drawer-label {\n    color: rgb(55 65 81);\n  }\n\n  .dark .df-mobile-event-drawer-label {\n    color: rgb(209 213 219);\n  }\n\n  .df-mobile-event-drawer-controls {\n    display: flex;\n    gap: 0.5rem;\n  }\n\n  .df-mobile-event-drawer-picker-trigger {\n    border: none;\n    border-radius: 0.375rem;\n    background-color: rgb(243 244 246);\n    padding: 0.25rem 0.75rem;\n    color: rgb(55 65 81);\n    transition:\n      background-color 150ms,\n      color 150ms;\n    cursor: pointer;\n  }\n\n  .dark .df-mobile-event-drawer-picker-trigger {\n    background-color: rgb(31 41 55);\n    color: rgb(209 213 219);\n  }\n\n  .df-mobile-event-drawer-picker-trigger[data-active='true'] {\n    background-color: rgb(229 231 235);\n    color: var(--df-color-primary);\n  }\n\n  .dark .df-mobile-event-drawer-picker-trigger[data-active='true'] {\n    background-color: rgb(55 65 81);\n    color: rgb(255 255 255);\n  }\n\n  .df-mobile-event-drawer-picker-trigger:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n  }\n\n  .df-mobile-event-drawer-expander {\n    overflow: hidden;\n    max-height: 0;\n    transition: max-height 300ms ease-in-out;\n  }\n\n  .df-mobile-event-drawer-expander[data-kind='calendar'][data-expanded='true'] {\n    max-height: 26rem;\n  }\n\n  .df-mobile-event-drawer-expander[data-kind='time'][data-expanded='true'] {\n    max-height: 20rem;\n  }\n\n  .df-mobile-event-drawer-expander-content {\n    border-top: 1px solid rgb(243 244 246);\n  }\n\n  .dark .df-mobile-event-drawer-expander-content {\n    border-top-color: rgb(31 41 55);\n  }\n\n  .df-mobile-event-drawer-delete-button {\n    width: 100%;\n    border: none;\n    background: transparent;\n    padding: 0.75rem 1rem;\n    text-align: left;\n    font-weight: 500;\n    color: rgb(239 68 68);\n    cursor: pointer;\n  }\n\n  .df-mobile-switch {\n    display: inline-flex;\n    height: 1.75rem;\n    width: 3rem;\n    align-items: center;\n    border-radius: 9999px;\n    padding: 0.25rem;\n    transition:\n      background-color 150ms,\n      opacity 150ms;\n    cursor: pointer;\n    background-color: rgb(209 213 219);\n  }\n\n  .df-mobile-switch[data-checked='true'] {\n    background-color: rgb(34 197 94);\n  }\n\n  .df-mobile-switch[data-disabled='true'] {\n    opacity: 0.5;\n    cursor: default;\n  }\n\n  .df-mobile-switch-thumb {\n    height: 1.25rem;\n    width: 1.25rem;\n    border-radius: 9999px;\n    background-color: var(--df-color-card);\n    box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);\n    transition: transform 150ms;\n  }\n\n  .df-mobile-switch[data-checked='true'] .df-mobile-switch-thumb {\n    transform: translateX(1.25rem);\n  }\n\n  .df-time-wheel {\n    position: relative;\n    margin-top: 0.5rem;\n    display: flex;\n    height: 14rem;\n    overflow: hidden;\n    border-radius: 0.5rem;\n  }\n\n  .df-time-wheel-column {\n    flex: 1;\n    overflow-y: auto;\n    scroll-snap-type: y mandatory;\n    touch-action: pan-y;\n    -ms-overflow-style: none;\n    scrollbar-width: none;\n  }\n\n  .df-time-wheel-column::-webkit-scrollbar {\n    display: none;\n  }\n\n  .df-time-wheel-spacer {\n    height: 96px;\n  }\n\n  .df-time-wheel-option {\n    display: flex;\n    height: 2rem;\n    cursor: pointer;\n    align-items: center;\n    scroll-snap-align: center;\n  }\n\n  .df-time-wheel-option-hour {\n    justify-content: flex-end;\n    padding-right: 1.25rem;\n  }\n\n  .df-time-wheel-option-minute {\n    justify-content: flex-start;\n    padding-left: 1.25rem;\n  }\n\n  .df-time-wheel-value {\n    width: 2.5rem;\n    text-align: center;\n    font-size: 1.25rem;\n    transition: color 200ms;\n    color: rgb(156 163 175);\n  }\n\n  .dark .df-time-wheel-value {\n    color: rgb(107 114 128);\n  }\n\n  .df-time-wheel-value[data-selected='true'] {\n    font-weight: 700;\n    color: rgb(17 24 39);\n  }\n\n  .dark .df-time-wheel-value[data-selected='true'] {\n    color: rgb(255 255 255);\n  }\n\n  .df-time-wheel-selection {\n    position: absolute;\n    top: 6rem;\n    right: 0.5rem;\n    left: 0.5rem;\n    height: 2rem;\n    border-radius: 0.5rem;\n    border: 1px solid rgb(209 213 219);\n    background-color: rgb(209 213 219 / 0.2);\n    pointer-events: none;\n  }\n\n  .dark .df-time-wheel-selection {\n    border-color: rgb(75 85 99);\n    background-color: rgb(107 114 128 / 0.2);\n  }\n}\n"
  },
  {
    "path": "packages/core/src/styles/core/overlays/quick-create.css",
    "content": "@layer components {\n  /* ===================================================================\n   * QuickCreateEventPopup  (.df-quick-create)\n   * =================================================================== */\n\n  .df-quick-create {\n    position: fixed;\n    z-index: 1000;\n    display: flex;\n    width: 21.25rem;\n    flex-direction: column;\n    border-radius: 0.75rem;\n    border: 1px solid var(--df-color-border);\n    background-color: var(--df-color-card);\n    box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);\n  }\n\n  .df-quick-create[data-ready='true'] {\n    animation:\n      df-fade-in 150ms cubic-bezier(0.22, 1, 0.36, 1),\n      df-zoom-in 150ms cubic-bezier(0.22, 1, 0.36, 1);\n  }\n\n  .df-quick-create-header {\n    padding: 1rem 1rem 0.5rem;\n  }\n\n  .df-quick-create-input-wrap {\n    position: relative;\n  }\n\n  .df-quick-create-title {\n    margin-bottom: 0.5rem;\n    font-size: 0.75rem;\n    font-weight: 600;\n    letter-spacing: 0.05em;\n    text-transform: uppercase;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-quick-create-list {\n    max-height: 18.75rem;\n    flex: 1;\n    overflow-y: auto;\n    padding: 0.25rem 0.5rem;\n  }\n\n  .df-quick-create-empty {\n    padding: 0.75rem 1rem;\n    text-align: center;\n    font-size: 0.875rem;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-quick-create-item {\n    display: flex;\n    cursor: pointer;\n    align-items: center;\n    border-radius: 0.5rem;\n    padding: 0.5rem 1rem;\n    transition: background-color 150ms;\n  }\n\n  .df-quick-create-item:hover {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-quick-create-item[data-selected='true'] {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 10%,\n      transparent\n    );\n    color: var(--df-color-primary);\n    outline: 1px solid\n      color-mix(in srgb, var(--df-color-primary) 20%, transparent);\n    outline-offset: -1px;\n  }\n\n  .df-quick-create-color-bar {\n    margin-right: 0.75rem;\n    height: 2rem;\n    width: 0.25rem;\n    flex-shrink: 0;\n    border-radius: 9999px;\n  }\n\n  .df-quick-create-item-content {\n    display: flex;\n    min-width: 0;\n    flex: 1;\n    flex-direction: column;\n    gap: 0.125rem;\n  }\n\n  .df-quick-create-item-title {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    font-size: 0.875rem;\n    font-weight: 500;\n    color: var(--df-color-foreground);\n  }\n\n  .df-quick-create-item-badge {\n    display: inline-block;\n    border-radius: 0.25rem;\n    background-color: var(--df-color-muted);\n    padding-inline: 0.25rem;\n    font-size: 0.625rem;\n    font-weight: 600;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-quick-create-item-time {\n    font-size: 0.75rem;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-quick-create-arrow {\n    position: absolute;\n    height: 0.75rem;\n    width: 0.75rem;\n    background-color: var(--df-color-card);\n    border-color: var(--df-color-border);\n    transform: translateX(-50%) rotate(45deg);\n  }\n\n  .df-quick-create-arrow[data-placement='bottom'] {\n    border-right-width: 1px;\n    border-right-style: solid;\n    border-bottom-width: 1px;\n    border-bottom-style: solid;\n    bottom: -0.375rem;\n  }\n\n  .df-quick-create-arrow[data-placement='top'] {\n    border-top-width: 1px;\n    border-top-style: solid;\n    border-left-width: 1px;\n    border-left-style: solid;\n    top: -0.375rem;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/styles/core/search/search.css",
    "content": "@layer components {\n  /* ===================================================================\n   * Search group (always visible — shown/hidden via JS based on isMobile)\n   * =================================================================== */\n\n  .df-search-group {\n    position: relative;\n    display: block;\n  }\n\n  .df-search-group-icon {\n    pointer-events: none;\n    position: absolute;\n    inset-block: 0;\n    left: 0;\n    display: flex;\n    align-items: center;\n    padding-left: 0.75rem;\n    color: var(--df-color-muted-foreground);\n    transition: color 150ms;\n  }\n\n  .df-search-group:focus-within .df-search-group-icon {\n    color: var(--df-color-primary);\n  }\n\n  .df-search-group-input {\n    padding-left: 2.25rem;\n    padding-right: 2rem;\n    padding-block: 0.25rem;\n    height: 1.75rem;\n    font-size: 0.875rem;\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.5rem;\n    color: var(--df-color-foreground);\n    background-color: var(--df-color-background);\n    outline: none;\n    transition:\n      border-color 150ms,\n      box-shadow 150ms;\n    resize: none;\n  }\n\n  .df-search-group-input::placeholder {\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-search-group-input:focus {\n    border-color: var(--df-color-primary);\n    box-shadow:\n      0 0 0 0px var(--df-color-background),\n      0 0 0 2px var(--df-color-primary);\n  }\n\n  .df-search-group-clear {\n    position: absolute;\n    inset-block: 0;\n    right: 0;\n    display: flex;\n    align-items: center;\n    padding-right: 0.5rem;\n    border: none;\n    background: transparent;\n    cursor: pointer;\n    color: var(--df-color-muted-foreground);\n    transition: color 150ms;\n  }\n\n  .df-search-group-clear:hover {\n    color: var(--df-color-foreground);\n  }\n\n  /* ===================================================================\n   * SearchDrawer  (.df-search-drawer)\n   * =================================================================== */\n\n  .df-search-drawer {\n    position: relative;\n    display: none;\n    height: 100%;\n    flex-direction: column;\n    overflow: hidden;\n    background-color: var(--df-color-background);\n    transition: width 300ms ease-in-out;\n    user-select: none;\n    width: 0;\n  }\n\n  @media (min-width: 768px) {\n    .df-search-drawer {\n      display: flex;\n    }\n  }\n\n  .df-search-drawer[data-open='true'] {\n    width: 16rem;\n    border-left: 1px solid var(--df-color-border);\n  }\n\n  .df-search-drawer[data-open='false'] {\n    width: 0;\n  }\n\n  .df-search-drawer-content {\n    min-width: 16rem;\n    flex: 1;\n    overflow-y: auto;\n  }\n\n  /* ===================================================================\n   * MobileSearchDialog  (.df-search-dialog__)\n   * =================================================================== */\n\n  .df-search-dialog-header {\n    display: flex;\n    align-items: center;\n    padding: 0.5rem;\n    gap: 0.5rem;\n    border-bottom: 1px solid var(--df-color-border);\n  }\n\n  .df-search-dialog-back-btn {\n    padding: 0.5rem;\n    border: none;\n    background: transparent;\n    cursor: pointer;\n    color: var(--df-color-muted-foreground);\n    transition: color 150ms;\n  }\n\n  .df-search-dialog-back-btn:hover {\n    color: var(--df-color-foreground);\n  }\n\n  .df-search-dialog-back-icon {\n    height: 1.5rem;\n    width: 1.5rem;\n  }\n\n  .df-search-dialog-input-wrap {\n    position: relative;\n    flex: 1;\n  }\n\n  .df-search-dialog-input {\n    width: 100%;\n    border-radius: 9999px;\n    border: none;\n    background-color: var(--df-color-muted);\n    padding: 0.5rem 2.5rem 0.5rem 0.75rem;\n    color: var(--df-color-foreground);\n    outline: none;\n    transition: box-shadow 150ms;\n  }\n\n  .df-search-dialog-input:focus {\n    box-shadow:\n      0 0 0 0px var(--df-color-background),\n      0 0 0 2px var(--df-color-primary);\n  }\n\n  .df-search-dialog-input-clear {\n    position: absolute;\n    inset-block: 0;\n    right: 0;\n    display: flex;\n    align-items: center;\n    padding-right: 0.75rem;\n    border: none;\n    background: transparent;\n    cursor: pointer;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-search-dialog-clear-icon {\n    height: 1rem;\n    width: 1rem;\n  }\n\n  .df-search-dialog-results {\n    flex: 1;\n    overflow-y: auto;\n    padding: 0.5rem;\n    user-select: none;\n  }\n\n  /* ===================================================================\n   * SearchResultsList  (.df-search-results)\n   * =================================================================== */\n\n  .df-search-results {\n    display: flex;\n    flex-direction: column;\n    gap: 1.5rem;\n  }\n\n  .df-search-results-state {\n    display: flex;\n    height: 10rem;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-search-results-state-icon {\n    color: rgb(209 213 219);\n    margin-bottom: 0.5rem;\n    width: 3rem;\n    height: 3rem;\n  }\n\n  .dark .df-search-results-state-icon {\n    color: rgb(75 85 99);\n  }\n\n  .df-search-results-date-header {\n    position: sticky;\n    top: 0;\n    z-index: 10;\n    margin-bottom: 0.25rem;\n    background-color: var(--df-color-background);\n    padding: 0.25rem 0.5rem;\n    font-size: 0.875rem;\n    font-weight: 500;\n    border-bottom: 1px solid var(--df-color-border);\n  }\n\n  .df-search-results-date-header[data-tone='default'] {\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-search-results-date-header[data-tone='today'] {\n    color: var(--df-color-primary);\n  }\n\n  .df-search-results-date-header[data-tone='upcoming'] {\n    color: var(--df-color-foreground);\n  }\n\n  .df-search-results-loader {\n    margin-bottom: 0.5rem;\n    height: 2rem;\n    width: 2rem;\n    animation: df-spin 1s linear infinite;\n  }\n\n  .df-search-results-events {\n    display: flex;\n    flex-direction: column;\n  }\n\n  .df-search-results-event {\n    margin-inline: 0.5rem;\n    margin-bottom: 0.25rem;\n    border-radius: 0.5rem;\n    padding: 0.5rem;\n    cursor: pointer;\n    transition: background-color 150ms;\n  }\n\n  .df-search-results-event:hover {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-search-results-event-inner {\n    display: flex;\n    align-items: stretch;\n    gap: 0.75rem;\n  }\n\n  .df-search-results-color-bar {\n    width: 0.25rem;\n    flex-shrink: 0;\n    border-radius: 9999px;\n  }\n\n  .df-search-results-event-content {\n    display: flex;\n    min-width: 0;\n    flex: 1;\n    align-items: flex-start;\n    justify-content: space-between;\n  }\n\n  .df-search-results-event-title {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    padding-right: 0.5rem;\n    font-size: 0.875rem;\n    font-weight: 500;\n    color: var(--df-color-foreground);\n  }\n\n  .df-search-results-event-time {\n    display: flex;\n    flex-shrink: 0;\n    flex-direction: column;\n    align-items: flex-end;\n    font-size: 0.75rem;\n    line-height: 1.25;\n    color: var(--df-color-foreground);\n  }\n\n  .df-search-results-end-time {\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-search-results-divider {\n    margin-inline: 0.5rem;\n    border-bottom: 1px solid var(--df-color-border);\n  }\n}\n"
  },
  {
    "path": "packages/core/src/styles/core/views/layout.css",
    "content": "@layer components {\n  /* ===================================================================\n   * View layout overrides\n   * =================================================================== */\n\n  .df-day-view {\n    display: flex;\n    height: 100%;\n    background-color: var(--df-color-muted);\n  }\n\n  .df-calendar {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    overflow: hidden;\n    height: 100%;\n    user-select: none;\n  }\n\n  .df-calendar-shell {\n    position: relative;\n    z-index: 10;\n    flex: 1 1 0%;\n    overflow: hidden;\n    border-left: 0;\n    background-color: var(--df-color-background);\n    transition:\n      margin-left 200ms ease-in-out,\n      border-color 200ms ease-in-out,\n      box-shadow 200ms ease-in-out,\n      background-color 200ms ease-in-out;\n  }\n\n  .df-calendar-shell[data-sidebar-enabled='true'] {\n    border-left: 1px solid var(--df-color-border);\n  }\n\n  .df-calendar-shell[data-sidebar-collapsed='true'] {\n    border-left-color: var(--df-color-border);\n    box-shadow:\n      0 20px 25px -5px rgb(0 0 0 / 0.1),\n      0 8px 10px -6px rgb(0 0 0 / 0.1);\n  }\n\n  .df-icon-btn {\n    display: flex;\n    height: 1.75rem;\n    width: 1.75rem;\n    align-items: center;\n    justify-content: center;\n    border-radius: 0.25rem;\n    border: none;\n    background: transparent;\n    cursor: pointer;\n    transition: background-color 150ms;\n  }\n\n  .df-icon-btn svg {\n    width: 1rem;\n    height: 1rem;\n  }\n\n  .df-icon-btn:hover {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-week-header {\n    display: flex;\n    border-bottom: 1px solid var(--df-color-border);\n  }\n\n  .df-week-day-cell {\n    display: flex;\n    flex: 1;\n    justify-content: center;\n    align-items: center;\n    text-align: center;\n    color: var(--df-color-muted-foreground);\n    font-size: 0.875rem;\n    padding: 0.25rem;\n    user-select: none;\n  }\n\n  .df-date-number {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    height: 1.5rem;\n    width: 1.5rem;\n    border-radius: 9999px;\n    font-size: 0.875rem;\n    margin-top: 0.25rem;\n    user-select: none;\n  }\n\n  .df-date-number[data-today='true'],\n  .df-month-date-number[data-today='true'] {\n    background-color: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n  }\n\n  .df-month-date-number[data-other-month='true'] {\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-month-date-number-container {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    text-align: right;\n    padding-inline: 0.5rem;\n    height: 33px;\n    position: relative;\n    z-index: 20;\n  }\n\n  .df-month-date-number {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    height: 1.5rem;\n    min-width: 1.5rem;\n    border-radius: 9999px;\n    font-size: 0.875rem;\n    font-weight: 500;\n    white-space: nowrap;\n    padding-inline: 0.25rem;\n  }\n\n  .df-month-day-cell {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    border-right: 1px solid var(--df-color-border);\n  }\n\n  .df-month-grid {\n    display: grid;\n    grid-template-rows: repeat(6, minmax(0, 1fr));\n    height: 100%;\n    overflow: hidden;\n  }\n\n  .df-scroll-container {\n    flex: 1;\n    overflow: auto;\n    will-change: scroll-position;\n    overscroll-behavior: contain;\n  }\n\n  .df-week-header-row {\n    position: sticky;\n    top: 0;\n    z-index: 10;\n    border-bottom: 1px solid var(--df-color-border);\n    background-color: var(--df-color-background);\n  }\n\n  .df-week-grid {\n    display: grid;\n    grid-template-columns: repeat(7, minmax(0, 1fr));\n  }\n\n  .df-week-header-row .df-week-grid {\n    padding-inline: 0.5rem;\n  }\n\n  .df-day-label {\n    text-align: right;\n    color: var(--df-color-muted-foreground);\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    padding: 0.5rem 0.5rem 0.5rem 0;\n    user-select: none;\n  }\n\n  .df-month-view {\n    display: flex;\n    height: 100%;\n    flex-direction: column;\n    user-select: none;\n  }\n\n  .df-agenda-view {\n    display: flex;\n    height: 100%;\n    flex-direction: column;\n    background-color: var(--df-color-background);\n  }\n\n  .df-agenda-scroll {\n    flex: 1;\n    overflow-y: auto;\n    padding-bottom: 1.5rem;\n  }\n\n  .df-agenda-day-section {\n    display: grid;\n    grid-template-columns: minmax(9rem, 16rem) minmax(0, 1fr);\n    gap: 1.5rem;\n    padding: 1.5rem 1.75rem;\n    border-bottom: 1px solid var(--df-color-border);\n    cursor: default;\n  }\n\n  .df-agenda-day-section[data-today='true'] {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 4%,\n      var(--df-color-background)\n    );\n  }\n\n  .df-agenda-day-rail {\n    display: flex;\n    align-items: flex-start;\n    gap: 1rem;\n  }\n\n  .df-agenda-day-number {\n    min-width: 4.5rem;\n    font-size: clamp(2.5rem, 5vw, 4rem);\n    line-height: 0.95;\n    font-weight: 300;\n    color: var(--df-color-foreground);\n  }\n\n  .df-agenda-day-number[data-today='true'] {\n    color: var(--df-color-primary);\n  }\n\n  .df-agenda-day-meta {\n    display: flex;\n    flex-direction: column;\n    gap: 0.25rem;\n    padding-top: 0.25rem;\n    text-align: left;\n  }\n\n  .df-agenda-day-month {\n    font-size: 0.95rem;\n    color: var(--df-color-foreground);\n  }\n\n  .df-agenda-day-name {\n    font-size: 0.95rem;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-agenda-day-meta:hover {\n    opacity: 0.85;\n  }\n\n  .df-agenda-day-content {\n    display: flex;\n    flex-direction: column;\n    gap: 0.75rem;\n  }\n\n  .df-agenda-item {\n    display: grid;\n    grid-template-columns: minmax(8.5rem, 14rem) minmax(0, 1fr);\n    gap: 1rem;\n    align-items: center;\n  }\n\n  .df-agenda-item-all-day {\n    align-items: center;\n  }\n\n  .df-agenda-time {\n    font-size: 0.95rem;\n    color: var(--df-color-foreground);\n    white-space: nowrap;\n  }\n\n  .df-agenda-event {\n    display: inline-flex;\n    align-items: center;\n    gap: 0.875rem;\n    width: fit-content;\n    max-width: 100%;\n    padding: 0.125rem 0;\n    border: none;\n    background: transparent;\n    color: inherit;\n    text-align: left;\n    cursor: pointer;\n    border-radius: 0.75rem;\n  }\n\n  .df-agenda-event[data-selected='true'] {\n    box-shadow: none;\n  }\n\n  .df-agenda-event:focus,\n  .df-agenda-event:focus-visible,\n  .df-agenda-event:active {\n    outline: none;\n    box-shadow: none;\n  }\n\n  .df-agenda-dot {\n    width: 0.625rem;\n    height: 0.625rem;\n    border-radius: 9999px;\n    flex-shrink: 0;\n  }\n\n  .df-agenda-title {\n    min-width: 0;\n    font-size: 1rem;\n    line-height: 1.4;\n    color: var(--df-color-foreground);\n  }\n\n  .df-agenda-badge {\n    display: inline-flex;\n    align-items: center;\n    max-width: 100%;\n    min-height: 1rem;\n    padding: 0.3rem 0.875rem;\n    border: none;\n    border-radius: 0.35rem;\n    font-size: 0.95rem;\n    line-height: 1.2;\n    font-weight: 500;\n    white-space: nowrap;\n  }\n\n  .df-agenda-badge[data-continued-left='true'] {\n    padding-left: 1rem;\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n    clip-path: polygon(0 50%, 0.7rem 0, 100% 0, 100% 100%, 0.7rem 100%);\n  }\n\n  .df-agenda-badge[data-continued-right='true'] {\n    padding-right: 1rem;\n    border-top-right-radius: 0;\n    border-bottom-right-radius: 0;\n    clip-path: polygon(\n      0 0,\n      calc(100% - 0.7rem) 0,\n      100% 50%,\n      calc(100% - 0.7rem) 100%,\n      0 100%\n    );\n  }\n\n  .df-agenda-badge[data-continued-left='true'][data-continued-right='true'] {\n    clip-path: polygon(\n      0 50%,\n      0.7rem 0,\n      calc(100% - 0.7rem) 0,\n      100% 50%,\n      calc(100% - 0.7rem) 100%,\n      0.7rem 100%\n    );\n  }\n\n  .df-agenda-all-day-events {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    gap: 0.625rem;\n    min-width: 0;\n  }\n\n  .df-agenda-all-day-event {\n    min-width: 0;\n  }\n\n  .df-agenda-empty,\n  .df-agenda-empty-row {\n    color: var(--df-color-muted-foreground);\n    font-size: 0.95rem;\n  }\n\n  .df-agenda-empty {\n    padding: 2rem 1.75rem;\n  }\n\n  .df-month-view-fade-scroller {\n    overflow: hidden;\n  }\n\n  .df-month-view-virtual-scroller {\n    overflow: hidden auto;\n    overscroll-behavior: contain;\n  }\n\n  .df-month-view-fade-scroller[data-layout-ready='false'],\n  .df-month-view-virtual-scroller[data-layout-ready='false'] {\n    visibility: hidden;\n  }\n\n  .df-month-view-fade-scroller[data-layout-ready='true'],\n  .df-month-view-virtual-scroller[data-layout-ready='true'] {\n    visibility: visible;\n  }\n\n  .df-calendar-content {\n    position: relative;\n    overflow-y: auto;\n  }\n\n  .df-time-column {\n    position: relative;\n    flex-shrink: 0;\n  }\n\n  .df-time-column[data-secondary-tz='false'] {\n    width: 3rem;\n  }\n\n  .df-time-column[data-secondary-tz='true'] {\n    width: 5rem;\n  }\n\n  .df-time-slot {\n    position: relative;\n    display: flex;\n    height: var(--df-hour-height, 4.5rem);\n  }\n\n  .df-time-label {\n    position: absolute;\n    top: 0;\n    right: 0.5rem;\n    transform: translateY(-50%);\n    font-size: 10px;\n    color: var(--df-color-muted-foreground);\n    user-select: none;\n  }\n\n  .df-time-grid-row {\n    display: flex;\n    height: var(--df-hour-height, 4.5rem);\n    border-top: 1px solid var(--df-color-border);\n  }\n\n  @media (max-width: 768px) {\n    .df-agenda-day-section {\n      display: block;\n      padding: 0;\n    }\n\n    .df-agenda-day-section[data-today='true'] {\n      background-color: color-mix(\n        in srgb,\n        var(--df-color-primary) 8%,\n        var(--df-color-background)\n      );\n    }\n\n    .df-agenda-day-rail {\n      display: flex;\n      align-items: flex-start;\n      gap: 0.875rem;\n      padding: 0.875rem 0.875rem 0.625rem;\n    }\n\n    .df-agenda-day-number {\n      min-width: 3.5rem;\n      font-size: 2.9rem;\n      line-height: 0.9;\n    }\n\n    .df-agenda-day-meta {\n      display: flex;\n      flex-direction: column;\n      gap: 0.125rem;\n      padding-top: 0.125rem;\n      min-width: 0;\n    }\n\n    .df-agenda-day-month {\n      font-size: 0.82rem;\n      line-height: 1.15;\n      color: var(--df-color-foreground);\n    }\n\n    .df-agenda-day-name {\n      font-size: 0.82rem;\n      line-height: 1.15;\n      color: var(--df-color-muted-foreground);\n    }\n\n    .df-agenda-day-content {\n      display: flex;\n      flex-direction: column;\n      gap: 0;\n      padding: 0 0.5rem 0.75rem;\n    }\n\n    .df-agenda-day-content > * + * {\n      border-top: 1px solid var(--df-color-border);\n    }\n\n    .df-agenda-item {\n      grid-template-columns: minmax(0, 1fr) auto;\n      gap: 0.75rem;\n      align-items: flex-start;\n      padding: 0.75rem 0.375rem;\n    }\n\n    .df-agenda-item-all-day {\n      grid-template-columns: minmax(0, 1fr);\n      gap: 0.5rem;\n      align-items: flex-start;\n      padding: 0.75rem 0.375rem;\n    }\n\n    .df-agenda-time {\n      grid-column: 2;\n      grid-row: 1;\n      padding-top: 0.1rem;\n      font-size: 0.76rem;\n      line-height: 1.25;\n      color: var(--df-color-muted-foreground);\n      white-space: nowrap;\n      text-align: right;\n    }\n\n    .df-agenda-item-all-day .df-agenda-time {\n      grid-column: 1;\n      grid-row: 1;\n      padding-top: 0;\n      text-align: left;\n    }\n\n    .df-agenda-all-day-events {\n      grid-column: 1;\n      grid-row: 2;\n      min-width: 0;\n      flex-direction: column;\n      align-items: stretch;\n      gap: 0.5rem;\n    }\n\n    .df-agenda-all-day-event {\n      width: 100%;\n      min-width: 0;\n    }\n\n    .df-agenda-event {\n      grid-column: 1;\n      grid-row: 1;\n      gap: 0.75rem;\n      width: 100%;\n      min-width: 0;\n      align-items: flex-start;\n      justify-content: flex-start;\n      padding: 0;\n    }\n\n    .df-agenda-title {\n      min-width: 0;\n      font-size: 0.95rem;\n      line-height: 1.35;\n      word-break: break-word;\n    }\n\n    .df-agenda-dot {\n      margin-top: 0.22rem;\n    }\n\n    .df-agenda-badge {\n      max-width: 100%;\n      white-space: normal;\n      word-break: break-word;\n      line-height: 1.15;\n    }\n\n    .df-agenda-empty-row {\n      padding: 0.75rem 0.375rem 0;\n    }\n  }\n\n  .df-time-grid-cell {\n    position: relative;\n    flex: 1;\n    border-right: 1px solid var(--df-color-border);\n    user-select: none;\n  }\n\n  .df-time-grid-row[data-scrollbar-space='false']\n    .df-time-grid-cell:last-child {\n    border-right: 0;\n  }\n\n  .df-current-time-line {\n    pointer-events: none;\n    position: absolute;\n    top: 0;\n    left: 0;\n    display: flex;\n    width: 100%;\n  }\n\n  .df-current-time-label {\n    margin-left: 0.5rem;\n    border-radius: 0.25rem;\n    padding: 0 0.375rem;\n    font-size: 0.75rem;\n    font-weight: 700;\n  }\n\n  .df-current-time-bar {\n    position: relative;\n    height: 0.125rem;\n    width: 100%;\n    border-radius: 0.25rem;\n  }\n\n  .df-all-day-row {\n    display: flex;\n    align-items: center;\n    /* border-bottom: 1px solid var(--df-color-border); */\n  }\n\n  .df-all-day-label {\n    display: flex;\n    width: 3rem;\n    flex-shrink: 0;\n    align-items: center;\n    justify-content: flex-end;\n    padding: 0.25rem;\n    font-size: 10px;\n    font-weight: 500;\n    color: var(--df-color-muted-foreground);\n    user-select: none;\n  }\n\n  .df-all-day-content {\n    position: relative;\n    display: flex;\n    flex: 1;\n  }\n\n  .df-all-day-cell {\n    position: relative;\n    flex: 1;\n    border-right: 1px solid var(--df-color-border);\n  }\n\n  .df-all-day-content[data-scrollbar-space='false']\n    .df-all-day-cell:last-child {\n    border-right: 0;\n  }\n\n  .df-mini-calendar {\n    border-bottom: 1px solid var(--df-color-border);\n    padding-inline: 0.5rem;\n  }\n\n  .df-time-grid-boundary {\n    position: relative;\n    height: 0.75rem;\n    /* border-top: 1px solid var(--df-color-border); */\n  }\n\n  .df-time-grid-boundary-top {\n    border-top-color: transparent;\n  }\n\n  .df-time-grid-boundary-bottom {\n    border-top: 1px solid var(--df-color-border);\n  }\n\n  .df-midnight-label {\n    position: absolute;\n    top: 0;\n    transform: translateY(-50%);\n    font-size: 12px;\n    color: var(--df-color-muted-foreground);\n    user-select: none;\n  }\n\n  .df-midnight-label-offset {\n    left: -2.375rem;\n  }\n\n  .df-mobile-fullscreen {\n    position: fixed;\n    inset: 0;\n    z-index: 9999;\n    display: flex;\n    flex-direction: column;\n    background-color: var(--df-color-background);\n  }\n\n  .df-time-column-spacer {\n    position: relative;\n    height: 0.75rem;\n  }\n\n  .df-time-day-column-spacer[data-secondary-tz='true'],\n  .df-time-week-column-spacer[data-secondary-tz='true'] {\n    height: 0.75rem;\n  }\n\n  .df-time-column-tz-header,\n  .df-time-column-tz-row {\n    display: flex;\n    width: 100%;\n    align-items: center;\n    justify-content: space-evenly;\n    color: var(--df-color-muted-foreground);\n    user-select: none;\n  }\n\n  .df-time-column-tz-header {\n    gap: 0.25rem;\n    padding: 0.25rem 0.25rem 0.125rem 0;\n  }\n\n  .df-time-column-tz-header-compact {\n    gap: 0.125rem;\n    padding: 0.5rem 0 0.125rem;\n  }\n\n  .df-time-column-tz-label {\n    font-size: 9px;\n  }\n\n  .df-time-column-tz-row {\n    position: absolute;\n    top: 0;\n    right: 0;\n    transform: translateY(-50%);\n    gap: 0.25rem;\n  }\n\n  .df-time-column-tz-row-boundary {\n    top: auto;\n    bottom: -0.25rem;\n    transform: none;\n  }\n\n  .df-time-column-tz-value,\n  .df-time-column-boundary-label {\n    font-size: 10px;\n  }\n\n  .df-time-column-boundary-label {\n    position: absolute;\n    right: 0.5rem;\n    bottom: -0.25rem;\n    color: var(--df-color-muted-foreground);\n    user-select: none;\n  }\n\n  .df-day-content {\n    flex: none;\n    width: 100%;\n    border-right: 1px solid var(--df-color-border);\n    background-color: var(--df-color-background);\n  }\n\n  .df-day-content-layout {\n    position: relative;\n    display: flex;\n    height: 100%;\n    flex-direction: column;\n  }\n\n  .df-day-content-all-day-row {\n    position: relative;\n    align-items: stretch;\n    border-top: 1px solid var(--df-color-border);\n    border-bottom: 1px solid var(--df-color-border);\n  }\n\n  .df-day-content-all-day-grid {\n    position: relative;\n    display: flex;\n    flex: 1;\n    align-self: stretch;\n  }\n\n  .df-day-content-all-day-grid[data-scrollbar-space='true'] {\n    border-right: 1px solid var(--df-color-border);\n  }\n\n  .df-day-content-all-day-lane {\n    position: relative;\n    width: 100%;\n  }\n\n  .df-day-content-all-day-spacer[data-scrollbar-space='true'] {\n    padding-right: 0.6875rem;\n  }\n\n  .df-day-content-grid-inner {\n    position: relative;\n    display: flex;\n  }\n\n  .df-day-content-current-time-side {\n    display: flex;\n    width: 3rem;\n    align-items: center;\n  }\n\n  .df-current-time-line[data-secondary-tz='true']\n    .df-day-content-current-time-side {\n    width: 5rem;\n  }\n\n  .df-day-content-current-time-side-inner {\n    position: relative;\n    display: flex;\n    width: 100%;\n    align-items: center;\n  }\n\n  .df-day-content-current-time-rail {\n    display: flex;\n    flex: 1;\n    align-items: center;\n  }\n\n  .df-day-content-grid-column {\n    flex: 1;\n    user-select: none;\n  }\n\n  .df-day-content-grid-boundary-bottom {\n    border-top: 1px solid var(--df-color-border);\n  }\n\n  .df-day-content-grid-boundary[data-scrollbar-space='true'] {\n    border-right: 1px solid var(--df-color-border);\n  }\n\n  .df-day-content-grid-boundary-bottom[data-scrollbar-space='true'] {\n    border-right: 1px solid var(--df-color-border);\n  }\n\n  .df-day-content-grid-rows {\n    position: relative;\n  }\n\n  .df-day-content-grid-rows[data-scrollbar-space='true'] {\n    border-right: 1px solid var(--df-color-border);\n  }\n\n  .df-day-content-bottom-tz-row {\n    top: -0.625rem;\n    transform: none;\n  }\n\n  .df-day-content-event-layer {\n    pointer-events: none;\n    position: absolute;\n    inset: 0;\n  }\n\n  .df-right-panel {\n    display: none;\n    flex: none;\n    width: 30%;\n    background-color: var(--df-color-background);\n  }\n\n  .df-right-panel-layout {\n    display: flex;\n    height: 100%;\n    flex-direction: column;\n  }\n\n  .df-right-panel-calendar-shell {\n    display: flex;\n    flex-direction: column;\n  }\n\n  .df-right-panel-calendar-header {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    gap: 0.5rem;\n  }\n\n  .df-right-panel-header-spacer {\n    position: relative;\n    min-width: 0;\n    flex: 1;\n    padding: 0.5rem;\n    font-size: 1.5rem;\n    line-height: 2rem;\n  }\n\n  .df-right-panel-events {\n    flex: 1;\n    overflow-y: auto;\n  }\n\n  .df-right-panel-events-inner {\n    padding: 1rem;\n  }\n\n  .df-right-panel-date-heading {\n    position: sticky;\n    top: 0;\n    z-index: 10;\n    margin-bottom: 0.75rem;\n    background-color: var(--df-color-background);\n    padding-block: 0.5rem;\n    font-size: 1.125rem;\n    font-weight: 600;\n    color: var(--df-color-foreground);\n  }\n\n  .df-right-panel-empty {\n    font-size: 0.875rem;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-right-panel-list {\n    display: flex;\n    flex-direction: column;\n    gap: 0.2rem;\n  }\n\n  .df-right-panel-event-card {\n    cursor: pointer;\n    transition: background-color 150ms;\n    min-height: 2.75rem;\n    display: flex;\n    flex-direction: column;\n  }\n\n  .df-right-panel-event-item {\n    margin-bottom: 0.5rem;\n  }\n\n  .df-week-time-grid {\n    position: relative;\n    display: flex;\n    flex: 1;\n    overflow: hidden;\n  }\n\n  .df-week-time-grid-scroller {\n    position: relative;\n    flex: 1;\n    overflow: auto;\n  }\n\n  .df-week-time-grid-scroller-shell {\n    display: grid;\n  }\n\n  .df-week-time-grid-scroller[data-sliding-view='true'] {\n    overflow-x: hidden;\n  }\n\n  .df-week-time-grid-scroller[data-sliding-view='false'] {\n    scroll-snap-type: x mandatory;\n  }\n\n  .df-week-time-grid-time-column {\n    position: sticky;\n    left: 0;\n    z-index: 10;\n    width: 3rem;\n    flex-shrink: 0;\n    background-color: var(--df-color-background);\n  }\n\n  .df-week-time-grid-time-column[data-secondary-tz='true'] {\n    width: 5rem;\n  }\n\n  .df-week-time-grid-boundary-tail {\n    position: relative;\n  }\n\n  .df-week-time-grid-current-time-label {\n    pointer-events: none;\n    position: absolute;\n    left: 0;\n    z-index: 20;\n    display: flex;\n    width: 100%;\n    align-items: center;\n    justify-content: flex-end;\n    transform: translateY(-50%);\n  }\n\n  .df-week-time-grid-content {\n    display: flex;\n  }\n\n  .df-week-time-grid-grid {\n    flex: 1;\n  }\n\n  .df-week-time-grid-grid-inner {\n    position: relative;\n  }\n\n  .df-week-time-grid-boundary-row {\n    display: flex;\n  }\n\n  .df-week-time-grid-boundary-cell {\n    position: relative;\n    flex: 1;\n    border-right: 1px solid var(--df-color-border);\n  }\n\n  .df-week-time-grid-boundary-row[data-scrollbar-space='false']\n    .df-week-time-grid-boundary-cell:last-child {\n    border-right: 0;\n  }\n\n  .df-week-time-grid-current-time-spacer {\n    display: flex;\n    width: 0;\n    align-items: center;\n  }\n\n  .df-week-time-grid-current-time-track {\n    display: flex;\n    flex: 1;\n  }\n\n  .df-week-time-grid-current-time-cell {\n    display: flex;\n    flex: 1;\n    align-items: center;\n  }\n\n  .df-week-time-grid-current-line-rail {\n    position: relative;\n    height: 0.125rem;\n    width: 100%;\n    border-radius: 0.25rem;\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 30%,\n      transparent\n    );\n  }\n\n  .df-week-time-grid-current-line-rail[data-today='true'] {\n    background-color: var(--df-color-primary);\n  }\n\n  .df-week-time-grid-current-line-dot {\n    position: absolute;\n    top: -0.1875rem;\n    left: -0.25rem;\n    height: 0.5rem;\n    width: 0.5rem;\n    border-radius: 9999px;\n    background-color: var(--df-color-primary);\n  }\n\n  .df-week-time-grid-cell {\n    scroll-snap-align: start;\n  }\n\n  .df-week-time-grid-event-layer {\n    pointer-events: none;\n    position: absolute;\n    top: 0;\n  }\n\n  .df-week-all-day {\n    display: flex;\n    width: 100%;\n    flex-direction: column;\n  }\n\n  .df-week-all-day-shell {\n    position: relative;\n    z-index: 10;\n    display: flex;\n    flex: none;\n  }\n\n  .df-week-all-day-shell[data-show-all-day='true'] {\n    border-bottom: 1px solid var(--df-color-border);\n  }\n\n  .df-week-all-day-side {\n    z-index: 20;\n    display: flex;\n    width: 3rem;\n    flex-shrink: 0;\n    flex-direction: column;\n    background-color: var(--df-color-background);\n  }\n\n  .df-week-all-day-side-spacer {\n    display: flex;\n    flex: 1;\n    align-items: center;\n    border-bottom: 1px solid var(--df-color-border);\n    transition: all 300ms ease-in-out;\n  }\n\n  .df-week-all-day-side-spacer[data-sliding-view='true'] {\n    display: none;\n  }\n\n  .df-week-all-day-label,\n  .df-week-all-day-row,\n  .df-week-all-day-row-content,\n  .df-week-all-day-cell,\n  .df-week-all-day-content-wrap {\n    transition: min-height 300ms ease-in-out;\n  }\n\n  .df-week-all-day-content-wrap {\n    position: relative;\n    flex: 1;\n    overflow: hidden;\n  }\n\n  .df-week-all-day-content {\n    display: flex;\n    flex-direction: column;\n  }\n\n  .df-week-all-day-weekday-cell[data-mobile='true'] {\n    flex-direction: column;\n    gap: 0;\n  }\n\n  .df-week-all-day-weekday-label {\n    font-size: 12px;\n    line-height: 1.1;\n    font-weight: 500;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-week-all-day-weekday-name {\n    margin-top: 0.25rem;\n    margin-right: 0.25rem;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 0.875rem;\n  }\n\n  .df-week-all-day-date-number-mobile {\n    height: 1.75rem;\n    width: 1.75rem;\n    font-size: 1rem;\n    font-weight: 500;\n  }\n\n  .df-week-all-day-row {\n    position: relative;\n    border-bottom-color: transparent;\n  }\n\n  .df-week-all-day-cell-no-border {\n    border-right: 0;\n  }\n\n  .df-week-all-day-event-layer {\n    pointer-events: none;\n    position: absolute;\n    inset: 0;\n  }\n\n  .df-compact-header {\n    display: flex;\n    width: 100%;\n    flex-direction: column;\n    border-bottom: 1px solid var(--df-color-border);\n    background-color: var(--df-color-background);\n    padding-block: 0.75rem;\n  }\n\n  .df-compact-header-labels,\n  .df-compact-header-dates {\n    position: relative;\n    display: grid;\n    grid-template-columns: repeat(7, minmax(0, 1fr));\n  }\n\n  .df-compact-header-labels {\n    margin-bottom: 0.25rem;\n  }\n\n  .df-compact-header-dates {\n    overflow: hidden;\n  }\n\n  .df-compact-header-label-cell,\n  .df-compact-header-date-button {\n    position: relative;\n    z-index: 10;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .df-compact-header-date-button {\n    cursor: pointer;\n  }\n\n  .df-compact-header-label {\n    font-size: 10px;\n    font-weight: 500;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-compact-header-label[data-today='true'] {\n    color: var(--df-color-primary);\n  }\n\n  .df-compact-header-capsule {\n    position: absolute;\n    border-radius: 9999px;\n    background-color: var(--df-color-hover);\n    transition: all 300ms ease;\n  }\n\n  .df-compact-header-date-pill {\n    position: relative;\n    display: flex;\n    height: 2rem;\n    width: 2rem;\n    align-items: center;\n    justify-content: center;\n    border-radius: 9999px;\n    font-size: 0.875rem;\n    font-weight: 500;\n    color: var(--df-color-muted-foreground);\n    transition: all 300ms ease;\n  }\n\n  .df-compact-header-date-pill[data-selected='true'] {\n    background-color: var(--df-color-foreground);\n    color: var(--df-color-background);\n    box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);\n  }\n\n  .df-compact-header-date-pill[data-selected='false'][data-today='true'] {\n    color: var(--df-color-primary);\n    font-weight: 700;\n  }\n\n  .df-compact-header-date-pill[data-selected='false'][data-inside-pill='true'] {\n    color: var(--df-color-foreground);\n  }\n\n  .df-compact-header-today-dot {\n    position: absolute;\n    bottom: 0.25rem;\n    height: 0.25rem;\n    width: 0.25rem;\n    border-radius: 9999px;\n    background-color: var(--df-color-primary);\n  }\n\n  .df-month-week {\n    position: relative;\n    border-bottom: 1px solid var(--df-color-border);\n    user-select: none;\n  }\n\n  .df-month-week-inner {\n    display: flex;\n    height: 100%;\n    flex-direction: column;\n  }\n\n  .df-month-week-grid-shell {\n    position: relative;\n    height: 100%;\n  }\n\n  .df-month-week-grid {\n    display: grid;\n    height: 100%;\n    grid-template-columns: repeat(7, minmax(0, 1fr));\n  }\n\n  .df-month-week-event-layer {\n    pointer-events: none;\n    position: absolute;\n    left: 0;\n    right: 0;\n  }\n\n  .df-month-week-event-layer-row {\n    position: absolute;\n    inset: 0;\n  }\n\n  .df-month-day-cell-surface {\n    color: var(--df-color-foreground);\n  }\n\n  .df-month-day-cell-surface[data-other-month='true'] {\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-month-day-cell-surface[data-trailing-border='false'] {\n    border-right-color: transparent;\n  }\n\n  .df-month-day-cell-content {\n    pointer-events: none;\n    position: relative;\n    flex: 1;\n    overflow: hidden;\n    padding-inline: 0.25rem;\n  }\n\n  .df-month-day-cell-overlay-mask {\n    pointer-events: none;\n    position: absolute;\n    left: 0;\n    right: 0;\n    z-index: 100;\n    background-color: var(--df-color-background);\n  }\n\n  .df-month-week-number {\n    margin-left: 0.25rem;\n    margin-right: auto;\n    font-size: 10px;\n    font-weight: 500;\n    color: color-mix(in srgb, var(--df-color-muted-foreground) 85%, white 15%);\n  }\n\n  .df-month-more-events {\n    position: relative;\n    z-index: 20;\n    cursor: pointer;\n    color: var(--df-color-muted-foreground);\n    font-size: 0.75rem;\n    text-decoration: underline;\n    text-decoration-color: transparent;\n    text-underline-offset: 2px;\n    transition:\n      color 150ms,\n      text-decoration-color 150ms;\n  }\n\n  .df-month-more-events:hover {\n    color: var(--df-color-foreground);\n    text-decoration-color: currentColor;\n  }\n\n  .df-month-day-cell-more-events {\n    pointer-events: auto;\n    z-index: 100;\n  }\n\n  .df-month-day-cell-more-events[data-layout='desktop'] {\n    text-align: left;\n    font-weight: 400;\n  }\n\n  .df-month-day-cell-more-events[data-layout='mobile'] {\n    text-align: center;\n    font-weight: 500;\n  }\n\n  .df-month-title {\n    position: absolute;\n    top: 2.5rem;\n    left: 0;\n    z-index: 30;\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-background) 50%,\n      transparent\n    );\n    padding: 0.5rem;\n    transition:\n      opacity 300ms,\n      background-color 300ms;\n  }\n\n  .df-month-title[data-visible='false'] {\n    pointer-events: none;\n    opacity: 0;\n  }\n\n  .df-month-title[data-visible='true'] {\n    pointer-events: auto;\n    opacity: 1;\n  }\n\n  .df-month-title-label {\n    font-size: 1.5rem;\n    line-height: 2rem;\n    font-weight: 700;\n    color: var(--df-color-foreground);\n  }\n\n  .df-year-popup {\n    position: fixed;\n    z-index: 50;\n    width: 16rem;\n    overflow: hidden;\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.75rem;\n    background-color: var(--df-color-background);\n    box-shadow:\n      0 20px 25px -5px rgb(0 0 0 / 0.1),\n      0 8px 10px -6px rgb(0 0 0 / 0.1);\n    animation-duration: 100ms;\n  }\n\n  .df-year-popup-header {\n    border-bottom: 1px solid\n      color-mix(in srgb, var(--df-color-border) 70%, white 30%);\n    padding: 0.75rem 1rem;\n  }\n\n  .df-year-popup-title {\n    font-size: 0.875rem;\n    font-weight: 600;\n    color: var(--df-color-foreground);\n  }\n\n  .df-year-popup-body {\n    max-height: 15rem;\n    overflow-y: auto;\n    padding-block: 0.25rem;\n  }\n\n  .df-year-popup-empty {\n    padding: 0.75rem 1rem;\n    text-align: center;\n    font-size: 0.75rem;\n    color: color-mix(in srgb, var(--df-color-muted-foreground) 85%, white 15%);\n  }\n\n  .df-year-popup-event {\n    display: flex;\n    align-items: flex-start;\n    gap: 0.625rem;\n    border-radius: 0.25rem;\n    padding: 0.375rem 0.75rem;\n    transition: background-color 150ms;\n  }\n\n  .df-year-popup-event:hover {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-background),\n      var(--df-color-hover) 90%\n    );\n  }\n\n  .df-year-popup-dot {\n    margin-top: 0.375rem;\n    height: 0.5rem;\n    width: 0.5rem;\n    flex-shrink: 0;\n    border-radius: 9999px;\n  }\n\n  .df-year-popup-event-main {\n    min-width: 0;\n    flex: 1;\n  }\n\n  .df-year-popup-event-title {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    font-size: 0.75rem;\n    font-weight: 500;\n    color: var(--df-color-foreground);\n  }\n\n  .df-year-popup-event-meta {\n    font-size: 10px;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-year-day-cell {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    overflow: hidden;\n    border-right: 1px solid\n      color-mix(in srgb, var(--df-color-border) 55%, white 45%);\n    border-bottom: 1px solid\n      color-mix(in srgb, var(--df-color-border) 55%, white 45%);\n    background-color: var(--df-color-background);\n    user-select: none;\n  }\n\n  .df-year-day-cell[data-first-day='true'] {\n    border-left: 2px solid var(--df-color-primary);\n  }\n\n  .df-year-day-cell-header {\n    display: flex;\n    height: 1.5rem;\n    flex-shrink: 0;\n    align-items: center;\n    padding: 0.25rem;\n  }\n\n  .df-year-day-cell-month-pill {\n    border-radius: 0.25rem;\n    background-color: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n    padding: 0.125rem 0.25rem;\n    font-size: 9px;\n    line-height: 1;\n    font-weight: 700;\n  }\n\n  .df-year-day-cell-date {\n    margin-left: auto;\n    font-size: 10px;\n    font-weight: 500;\n    color: color-mix(in srgb, var(--df-color-foreground) 78%, transparent);\n  }\n\n  .df-year-day-cell-date[data-today='true'] {\n    display: flex;\n    height: 1.25rem;\n    width: 1.25rem;\n    align-items: center;\n    justify-content: center;\n    border-radius: 9999px;\n    background-color: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n  }\n\n  .df-year-day-cell-more-wrap {\n    position: absolute;\n    bottom: 0.125rem;\n    left: 0.25rem;\n    z-index: 20;\n  }\n\n  .df-year-day-cell-more {\n    cursor: pointer;\n    font-size: 0.75rem;\n    font-weight: 500;\n    color: var(--df-color-muted-foreground);\n    text-decoration: underline;\n    text-decoration-color: transparent;\n    text-underline-offset: 2px;\n    transition:\n      color 150ms,\n      text-decoration-color 150ms;\n  }\n\n  .df-year-day-cell-more:hover {\n    color: var(--df-color-foreground);\n    text-decoration-color: currentColor;\n  }\n\n  .df-year-grid {\n    display: grid;\n    min-height: 0;\n    flex: 1;\n    gap: 0.75rem;\n    padding: 0.75rem;\n  }\n\n  .df-year-grid-month {\n    display: flex;\n    height: 100%;\n    min-height: 0;\n    flex-direction: column;\n    border: 1px solid color-mix(in srgb, var(--df-color-border) 55%, white 45%);\n    border-radius: 0.5rem;\n    background-color: var(--df-color-background);\n    padding: 0.5rem;\n  }\n\n  .df-year-grid-month-title {\n    margin-bottom: 0.25rem;\n    flex-shrink: 0;\n    font-size: 0.75rem;\n    font-weight: 600;\n    color: var(--df-color-foreground);\n  }\n\n  .df-year-grid-month-body {\n    display: flex;\n    min-height: 0;\n    flex: 1;\n    overflow: hidden;\n  }\n\n  .df-year-grid-calendar {\n    display: grid;\n    height: 100%;\n    width: 100%;\n    gap: 1px;\n  }\n\n  .df-year-grid-weekday {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 9px;\n    font-weight: 500;\n    color: color-mix(in srgb, var(--df-color-muted-foreground) 80%, white 20%);\n  }\n\n  .df-year-grid-day {\n    cursor: pointer;\n    border-radius: 0.25rem;\n    transition:\n      opacity 150ms,\n      background-color 150ms;\n  }\n\n  .df-year-grid-day:hover {\n    opacity: 0.8;\n  }\n\n  .df-year-grid-day-inner {\n    display: flex;\n    height: 100%;\n    width: 100%;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .df-year-grid-day-number {\n    display: flex;\n    height: 1rem;\n    width: 1rem;\n    align-items: center;\n    justify-content: center;\n    border-radius: 9999px;\n    font-size: 9px;\n    font-weight: 500;\n    color: #6b7280;\n  }\n\n  .df-year-grid-day-number[data-current-month='false'] {\n    color: #d1d5db;\n  }\n\n  .df-year-grid-day-number[data-current-month='true'][data-has-events='true'] {\n    color: #1f2937;\n  }\n\n  .df-year-grid-day-number[data-current-month='true'][data-has-events='true'][data-heat-level='4'],\n  .df-year-grid-day-number[data-current-month='true'][data-has-events='true'][data-heat-level='5'] {\n    color: #fff;\n  }\n\n  .dark .df-year-grid-day-number {\n    color: #9ca3af;\n  }\n\n  .dark .df-year-grid-day-number[data-current-month='false'] {\n    color: #4b5563;\n  }\n\n  .dark\n    .df-year-grid-day-number[data-current-month='true'][data-has-events='true'] {\n    color: #f3f4f6;\n  }\n\n  .dark\n    .df-year-grid-day-number[data-current-month='true'][data-has-events='true'][data-heat-level='5'] {\n    color: #1f2937;\n  }\n\n  [data-df-theme='dark'] .df-year-grid-day-number {\n    color: #9ca3af;\n  }\n\n  [data-df-theme='dark'] .df-year-grid-day-number[data-current-month='false'] {\n    color: #4b5563;\n  }\n\n  [data-df-theme='dark']\n    .df-year-grid-day-number[data-current-month='true'][data-has-events='true'] {\n    color: #f3f4f6;\n  }\n\n  [data-df-theme='dark']\n    .df-year-grid-day-number[data-current-month='true'][data-has-events='true'][data-heat-level='5'] {\n    color: #1f2937;\n  }\n\n  .df-year-grid-day-number[data-today='true'] {\n    background-color: var(--df-color-primary);\n    color: #fff;\n    font-weight: 700;\n  }\n\n  [data-df-theme='light'] .df-year-grid-day-number[data-today='true'],\n  .light .df-year-grid-day-number[data-today='true'] {\n    color: #fff;\n  }\n\n  .dark .df-year-grid-day-number[data-today='true'] {\n    color: #000;\n  }\n\n  [data-df-theme='dark'] .df-year-grid-day-number[data-today='true'] {\n    color: #000;\n  }\n\n  .df-year-fixed {\n    height: 100%;\n    overflow: hidden;\n    background-color: var(--df-color-background);\n    user-select: none;\n    display: grid;\n    grid-template-columns: 3rem 1fr;\n    grid-template-rows: auto auto 1fr;\n  }\n\n  .df-year-fixed-header-span {\n    grid-column: span 2 / span 2;\n  }\n\n  .df-year-fixed-corner {\n    z-index: 30;\n    border-right: 1px solid var(--df-color-border);\n    border-bottom: 1px solid var(--df-color-border);\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-muted) 70%,\n      var(--df-color-background)\n    );\n  }\n\n  .df-year-fixed-week-header {\n    overflow: hidden;\n    border-bottom: 1px solid var(--df-color-border);\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-muted) 70%,\n      var(--df-color-background)\n    );\n  }\n\n  .df-year-fixed-week-header-inner {\n    display: flex;\n  }\n\n  .df-year-fixed-week-grid {\n    display: grid;\n    flex: 1;\n  }\n\n  .df-year-fixed-week-label {\n    border-right: 1px solid var(--df-color-border);\n    padding-block: 0.5rem;\n    text-align: center;\n    font-size: 10px;\n    font-weight: 700;\n    letter-spacing: 0.08em;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-year-fixed-week-spacer {\n    flex-shrink: 0;\n    border-left: 1px solid var(--df-color-border);\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-muted) 70%,\n      var(--df-color-background)\n    );\n  }\n\n  .df-year-fixed-month-sidebar {\n    overflow: hidden;\n    border-right: 1px solid var(--df-color-border);\n    background-color: var(--df-color-background);\n  }\n\n  .df-year-fixed-month-sidebar-inner,\n  .df-year-fixed-content-inner {\n    display: flex;\n    min-height: 100%;\n    flex-direction: column;\n  }\n\n  .df-year-fixed-month-label {\n    display: flex;\n    flex-shrink: 0;\n    flex-grow: 1;\n    align-items: center;\n    justify-content: center;\n    border-bottom: 1px solid var(--df-color-border);\n    font-size: 10px;\n    font-weight: 700;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-year-fixed-month-spacer {\n    flex-shrink: 0;\n    border-top: 1px solid var(--df-color-border);\n    background-color: var(--df-color-background);\n  }\n\n  .df-year-fixed-content {\n    overflow: auto;\n  }\n\n  .df-year-fixed-month-row {\n    position: relative;\n    flex-shrink: 0;\n    flex-grow: 1;\n  }\n\n  .df-year-fixed-background-grid {\n    position: absolute;\n    inset: 0;\n    z-index: 0;\n    display: grid;\n  }\n\n  .df-year-fixed-empty-cell,\n  .df-year-fixed-day-cell {\n    border-right: 1px solid var(--df-color-border);\n    border-bottom: 1px solid var(--df-color-border);\n  }\n\n  .df-year-fixed-empty-cell {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-muted) 80%,\n      transparent\n    );\n  }\n\n  .df-year-fixed-day-cell {\n    position: relative;\n    display: flex;\n    cursor: pointer;\n    align-items: flex-start;\n    justify-content: flex-end;\n    padding: 0.125rem;\n    transition: background-color 150ms;\n  }\n\n  .df-year-fixed-day-cell[data-dragging='false']:hover {\n    background-color: color-mix(in srgb, #dbeafe 70%, transparent);\n  }\n\n  .df-year-fixed-empty-cell[data-weekend='true'],\n  .df-year-fixed-day-cell[data-weekend='true'] {\n    background-color: color-mix(\n      in srgb,\n      #dbeafe 35%,\n      var(--df-color-background)\n    );\n  }\n\n  .dark .df-year-grid-month {\n    border-color: #1e2938;\n  }\n\n  .dark .df-year-day-cell {\n    border-right-color: var(--df-color-border);\n    border-bottom-color: var(--df-color-border);\n  }\n\n  .dark .df-year-fixed-corner,\n  .dark .df-year-fixed-week-header,\n  .dark .df-year-fixed-week-spacer,\n  .dark .df-year-fixed-week-label {\n    background-color: #101828;\n  }\n\n  .df-year-fixed-month-spacer[data-scrollbar-space='true'],\n  .df-year-fixed-week-spacer[data-scrollbar-space='true'] {\n    border: 0;\n  }\n\n  .dark .df-year-fixed-week-label {\n    color: #d0d5dd;\n  }\n\n  .dark .df-year-fixed-empty-cell[data-weekend='true'],\n  .dark .df-year-fixed-day-cell[data-weekend='true'] {\n    background-color: #142246;\n  }\n\n  .df-year-fixed-day-number {\n    display: flex;\n    height: 1.25rem;\n    width: 1.25rem;\n    align-items: center;\n    justify-content: center;\n    border-radius: 9999px;\n    font-size: 10px;\n    font-weight: 500;\n    color: color-mix(in srgb, var(--df-color-foreground) 78%, transparent);\n  }\n\n  .df-year-fixed-day-number[data-today='true'] {\n    background-color: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n    font-weight: 700;\n    box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);\n  }\n\n  .df-year-fixed-event-layer,\n  .df-year-row-event-layer {\n    pointer-events: none;\n    position: absolute;\n    inset: 0;\n    z-index: 20;\n  }\n\n  .df-year-fixed-event-layer-inner,\n  .df-year-row-event-layer-inner {\n    position: relative;\n    height: 100%;\n    width: 100%;\n  }\n\n  .df-year-fixed-event-hitbox,\n  .df-year-row-event-hitbox {\n    pointer-events: auto;\n  }\n\n  .df-year-row {\n    position: relative;\n    width: 100%;\n  }\n\n  .df-year-default-scroll {\n    overflow: hidden auto;\n  }\n\n  .df-year-default-rows {\n    display: flex;\n    width: 100%;\n    flex-direction: column;\n    border-top: 1px solid var(--df-color-border);\n    border-left: 1px solid var(--df-color-border);\n  }\n\n  @media (min-width: 768px) {\n    .df-right-panel {\n      display: block;\n    }\n\n    .df-time-column[data-secondary-tz='false'],\n    .df-day-content-current-time-side,\n    .df-week-time-grid-time-column,\n    .df-week-all-day-side,\n    .df-all-day-label {\n      width: 5rem;\n    }\n\n    .df-current-time-line[data-secondary-tz='true']\n      .df-day-content-current-time-side,\n    .df-time-column[data-secondary-tz='true'] {\n      width: 5.5rem;\n    }\n\n    .df-week-time-grid-time-column[data-secondary-tz='true'] {\n      width: 5rem;\n    }\n\n    .df-time-label,\n    .df-time-column-tz-value,\n    .df-time-column-boundary-label {\n      font-size: 12px;\n    }\n\n    .df-time-column-tz-label {\n      font-size: 10px;\n    }\n\n    .df-all-day-label {\n      font-size: 0.75rem;\n    }\n\n    .df-day-content[data-switcher-mode='buttons'] {\n      width: 70%;\n    }\n\n    .df-day-content[data-switcher-mode='select'] {\n      width: 70%;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/styles/core-components.css",
    "content": "/**\n * core-components.css\n *\n * Semantic CSS classes for DayFlow core components.\n * This file acts as the single import manifest for grouped core styles.\n */\n\n@import './core/common/header-controls.css';\n@import './core/common/forms-dialogs.css';\n@import './core/events/calendar-event.css';\n@import './core/overlays/mobile-event-drawer.css';\n@import './core/search/search.css';\n@import './core/overlays/quick-create.css';\n@import './core/views/layout.css';\n"
  },
  {
    "path": "packages/core/src/styles/library-imports.css",
    "content": "@import '@dayflow/blossom-color-picker/styles.css';\n@import '../../../plugins/drag/src/styles/drag.css';\n@import './shared-foundation.css';\n@import './core-components.css';\n@import '../../../plugins/sidebar/src/styles/sidebar.css';\n@import '../../../ui/context-menu/src/styles/context-menu.css';\n@import '../../../ui/range-picker/src/styles/range-picker.css';\n"
  },
  {
    "path": "packages/core/src/styles/shared-foundation.css",
    "content": "@layer df-theme {\n  :where(:root) {\n    --df-color-background: rgb(255 255 255);\n    --df-color-foreground: rgb(46 46 46);\n    --df-color-hover: rgb(245 245 245);\n    --df-color-border: rgb(229 229 229);\n    --df-color-card: rgb(255 255 255);\n    --df-color-card-foreground: rgb(46 46 46);\n    --df-color-muted: rgb(243 244 246);\n    --df-color-muted-foreground: rgb(107 114 128);\n    --df-color-primary: rgb(46 46 46);\n    --df-color-primary-foreground: rgb(255 255 255);\n    --df-color-secondary: rgb(100 116 139);\n    --df-color-secondary-foreground: rgb(255 255 255);\n    --df-color-destructive: rgb(212 36 34);\n    --df-color-destructive-foreground: rgb(255 255 255);\n    --df-heat-1: #ebf5ff;\n    --df-heat-2: #cfe8ff;\n    --df-heat-3: #91d5ff;\n    --df-heat-4: #60a5fa;\n    --df-heat-5: #3b82f6;\n  }\n\n  :where(.dark) {\n    --df-color-background: rgb(16 24 40);\n    --df-color-hover: rgb(30 41 59);\n    --df-color-foreground: rgb(229 229 229);\n    --df-color-card: rgb(16 24 40);\n    --df-color-card-foreground: rgb(229 229 229);\n    --df-color-border: rgb(51 65 85);\n    --df-color-muted: rgb(30 41 59);\n    --df-color-muted-foreground: rgb(148 163 184);\n    --df-color-primary: rgb(229 229 229);\n    --df-color-primary-foreground: rgb(15 23 42);\n    --df-color-secondary: rgb(148 163 184);\n    --df-color-secondary-foreground: rgb(15 23 42);\n    --df-color-destructive: rgb(147 70 69);\n    --df-color-destructive-foreground: rgb(254 242 242);\n    --df-heat-1: #1e3a5f;\n    --df-heat-2: #2563eb;\n    --df-heat-3: #1e40af;\n    --df-heat-4: #3b82f6;\n    --df-heat-5: #93c5fd;\n  }\n\n  @media (prefers-color-scheme: dark) {\n    :root:not(.light):not(.dark) {\n      --df-color-background: rgb(21 21 21);\n      --df-color-hover: rgb(28 28 28);\n      --df-color-foreground: rgb(229 229 229);\n      --df-color-border: rgb(56 56 56);\n      --df-color-card: rgb(31 41 55);\n      --df-color-card-foreground: rgb(229 229 229);\n      --df-color-muted: rgb(55 65 81);\n      --df-color-muted-foreground: rgb(156 163 175);\n      --df-color-primary: rgb(229 229 229);\n      --df-color-primary-foreground: rgb(23 23 23);\n      --df-color-secondary: rgb(156 163 175);\n      --df-color-secondary-foreground: rgb(23 23 23);\n      --df-color-destructive: rgb(147 70 69);\n      --df-color-destructive-foreground: rgb(254 242 242);\n      --df-heat-1: #1e3a5f;\n      --df-heat-2: #2563eb;\n      --df-heat-3: #1e40af;\n      --df-heat-4: #3b82f6;\n      --df-heat-5: #93c5fd;\n    }\n  }\n}\n\n@layer components {\n  .df-calendar-container {\n    color-scheme: light dark;\n    position: relative;\n    display: flex;\n    flex-direction: row;\n    overflow: hidden;\n    user-select: none;\n    -webkit-user-select: none;\n    border-radius: 0.5rem;\n    background-color: var(--df-color-background);\n    border: 1px solid var(--df-color-border);\n    box-shadow:\n      0 10px 15px -3px rgb(0 0 0 / 0.1),\n      0 4px 6px -4px rgb(0 0 0 / 0.1);\n    --df-calendar-height: 800px;\n    height: var(--df-calendar-height, 800px);\n    font-family: Arial;\n  }\n\n  .df-calendar-sidebar-aside {\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    z-index: 0;\n    height: 100%;\n  }\n\n  .df-calendar-shell {\n    display: flex;\n    flex-direction: column;\n    flex: 1;\n    min-width: 0;\n    height: 100%;\n    transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n  }\n\n  .df-calendar-content-wrap {\n    position: relative;\n    flex: 1;\n    overflow: hidden;\n  }\n\n  .df-calendar-renderer {\n    position: relative;\n    display: flex;\n    height: 100%;\n    flex-direction: row;\n  }\n\n  .df-calendar-view-container {\n    height: 100%;\n    flex: 1;\n    overflow: hidden;\n  }\n  .df-calendar-container,\n  .df-dialog-container,\n  .df-event-detail-panel,\n  .df-portal,\n  .df-range-picker {\n    --color-background: var(--df-color-background);\n    --color-foreground: var(--df-color-foreground);\n    --color-hover: var(--df-color-hover);\n    --color-border: var(--df-color-border);\n    --color-card: var(--df-color-card);\n    --color-card-foreground: var(--df-color-card-foreground);\n    --color-muted: var(--df-color-muted);\n    --color-muted-foreground: var(--df-color-muted-foreground);\n    --color-primary: var(--df-color-primary);\n    --color-primary-foreground: var(--df-color-primary-foreground);\n    --color-secondary: var(--df-color-secondary);\n    --color-secondary-foreground: var(--df-color-secondary-foreground);\n    --color-destructive: var(--df-color-destructive);\n    --color-destructive-foreground: var(--df-color-destructive-foreground);\n    --hover: var(--df-color-hover);\n  }\n\n  .df-calendar-container .df-header,\n  .df-calendar-container .df-calendar,\n  .df-calendar-container .df-month-view {\n    background-color: var(--df-color-background);\n  }\n\n  .df-calendar-container .df-sidebar {\n    background-color: var(--df-color-background);\n    border-color: var(--df-color-border);\n  }\n\n  .df-calendar-container .df-week-header-row,\n  .df-calendar-container .df-week-header {\n    background-color: var(--df-color-background);\n    border-color: var(--df-color-border);\n  }\n\n  .df-calendar-container .df-month-day-cell,\n  .df-calendar-container .df-time-grid-cell,\n  .df-calendar-container .df-all-day-cell,\n  .df-calendar-container .df-time-column,\n  .df-calendar-container .df-time-grid-row,\n  .df-calendar-container .df-time-grid-boundary,\n  .df-calendar-container .df-all-day-row,\n  .df-calendar-container .df-mini-calendar {\n    border-color: var(--df-color-border);\n  }\n\n  .df-calendar-container .df-year-grid-month {\n    --heat-1: var(--df-heat-1);\n    --heat-2: var(--df-heat-2);\n    --heat-3: var(--df-heat-3);\n    --heat-4: var(--df-heat-4);\n    --heat-5: var(--df-heat-5);\n  }\n\n  .df-calendar-time-column {\n    width: 5rem;\n    flex-shrink: 0;\n    border-color: var(--df-color-border);\n  }\n\n  .df-calendar-nav-button {\n    position: relative;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    width: 1.75rem;\n    height: 1.75rem;\n    padding: 0.25rem;\n    border: 1px solid var(--df-color-border);\n    background-color: var(--df-color-background);\n    color: rgb(55 65 81);\n    border-radius: 0.375rem;\n    box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);\n    transition-property:\n      color, background-color, border-color, box-shadow, text-decoration-color,\n      fill, stroke;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n  }\n\n  .df-calendar-nav-button:hover {\n    background-color: #f9fafb;\n    border-color: color-mix(in srgb, var(--df-color-border), white 10%);\n    box-shadow:\n      0 1px 2px rgb(0 0 0 / 0.05),\n      0 1px 3px rgb(0 0 0 / 0.08);\n  }\n\n  .dark .df-calendar-nav-button {\n    color: rgb(209 213 219);\n  }\n\n  .dark .df-calendar-nav-button:hover {\n    background-color: rgb(55 65 81);\n  }\n\n  .df-calendar-today-button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    height: 1.75rem;\n    padding: 0.25rem 1rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    border: 1px solid var(--df-color-border);\n    background-color: var(--df-color-background);\n    color: rgb(55 65 81);\n    border-radius: 0.375rem;\n    box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);\n    transition-property:\n      color, background-color, border-color, box-shadow, text-decoration-color,\n      fill, stroke;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n  }\n\n  .dark .df-calendar-today-button {\n    color: rgb(229 231 235);\n  }\n\n  .df-calendar-today-button:hover {\n    background-color: #f9fafb;\n    border-color: color-mix(in srgb, var(--df-color-border), white 10%);\n    box-shadow:\n      0 1px 2px rgb(0 0 0 / 0.05),\n      0 1px 3px rgb(0 0 0 / 0.08);\n  }\n\n  .dark .df-calendar-today-button:hover {\n    background-color: rgb(55 65 81);\n  }\n\n  .df-calendar-checkbox {\n    margin-right: 0.5rem;\n    display: inline-flex;\n    height: 1rem;\n    width: 1rem;\n    cursor: pointer;\n    align-items: center;\n    justify-content: center;\n    border-radius: 0.25rem;\n    border: 1px solid var(--df-color-border);\n    appearance: none;\n    background-color: transparent;\n    position: relative;\n  }\n\n  .df-calendar-checkbox:focus-visible {\n    outline: 2px solid var(--checkbox-color, var(--df-color-primary));\n    outline-offset: 2px;\n  }\n\n  .df-calendar-checkbox:checked {\n    background-color: var(--checkbox-color, var(--df-color-primary));\n    border-color: var(--checkbox-color, var(--df-color-primary));\n  }\n\n  .df-calendar-checkbox:checked::after {\n    content: '';\n    display: block;\n    width: 0.25rem;\n    height: 0.5rem;\n    border: solid var(--df-color-primary-foreground);\n    border-width: 0 2px 2px 0;\n    transform: rotate(45deg);\n  }\n\n  .df-event-detail-panel,\n  .df-dialog-container {\n    background-color: var(--df-color-card);\n    border-color: var(--df-color-border);\n  }\n\n  .df-calendar-container .df-header-left,\n  .df-calendar-container .df-header-mid,\n  .df-calendar-container .df-header-right {\n    flex: 1;\n  }\n\n  .df-current-time-bar {\n    background-color: var(--df-color-primary);\n  }\n\n  .df-current-time-label {\n    background-color: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n  }\n\n  .df-content-slot-stacked {\n    display: flex;\n    flex: 1 1 auto;\n    flex-direction: column;\n    height: 100%;\n  }\n\n  .df-search-group:focus-within .df-search-focus-within-primary {\n    color: var(--df-color-primary);\n  }\n\n  .df-fill-primary {\n    background-color: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n  }\n\n  .df-fill-secondary {\n    background-color: var(--df-color-secondary);\n    color: var(--df-color-secondary-foreground);\n  }\n\n  .df-fill-destructive {\n    background-color: var(--df-color-destructive);\n    color: var(--df-color-destructive-foreground);\n  }\n\n  .df-tint-primary {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 10%,\n      transparent\n    );\n    color: var(--df-color-primary);\n  }\n\n  .df-tint-primary-md {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 20%,\n      transparent\n    );\n    color: var(--df-color-primary);\n  }\n\n  .df-tint-primary-lg {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 30%,\n      transparent\n    );\n    color: var(--df-color-primary);\n  }\n\n  .dark .df-dark-tint-primary-md {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 20%,\n      transparent\n    );\n  }\n\n  .df-hover-primary:hover {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 10%,\n      transparent\n    );\n    color: var(--df-color-primary);\n  }\n\n  .df-hover-primary-md:hover {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 20%,\n      transparent\n    );\n    color: var(--df-color-primary);\n  }\n\n  .dark .df-hover-primary-dark-md:hover {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 20%,\n      transparent\n    );\n  }\n\n  .df-hover-primary-solid:hover {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 90%,\n      transparent\n    );\n  }\n\n  .df-hover-base:hover {\n    background-color: var(--df-color-hover);\n  }\n\n  .df-hover-muted:hover {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-muted) 80%,\n      transparent\n    );\n  }\n\n  .df-p-compact {\n    padding: 0.125rem 0.25rem;\n  }\n\n  .df-p-standard {\n    padding: 0.25rem;\n  }\n\n  .df-shrink-0 {\n    flex-shrink: 0;\n  }\n\n  .df-shadow-sm {\n    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n  }\n\n  .dark .df-shadow-sm {\n    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.5);\n  }\n\n  .df-shadow-md {\n    box-shadow:\n      0 4px 6px -1px rgb(0 0 0 / 0.1),\n      0 2px 4px -1px rgb(0 0 0 / 0.06);\n  }\n\n  .dark .df-shadow-md {\n    box-shadow:\n      0 4px 6px -1px rgb(0 0 0 / 0.5),\n      0 2px 4px -1px rgb(0 0 0 / 0.3);\n  }\n\n  .df-hover-destructive:hover {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-destructive) 90%,\n      transparent\n    );\n  }\n\n  .df-hover-fill-destructive:hover {\n    background-color: var(--df-color-destructive);\n    color: var(--df-color-destructive-foreground);\n  }\n\n  .df-focus-fill-destructive:focus {\n    background-color: var(--df-color-destructive);\n    color: var(--df-color-destructive-foreground);\n  }\n\n  .df-text-primary {\n    color: var(--df-color-primary);\n  }\n\n  .df-text-muted {\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-text-primary-fg {\n    color: var(--df-color-primary-foreground);\n  }\n\n  .df-text-secondary-fg {\n    color: var(--df-color-secondary-foreground);\n  }\n\n  .df-text-destructive {\n    color: var(--df-color-destructive);\n  }\n\n  .df-text-destructive-fg {\n    color: var(--df-color-destructive-foreground);\n  }\n\n  .df-bg-base {\n    background-color: var(--df-color-background);\n  }\n\n  .df-bg-card {\n    background-color: var(--df-color-card);\n  }\n\n  .df-bg-sidebar {\n    background-color: var(--df-color-muted);\n  }\n\n  .df-bg-secondary {\n    background-color: var(--df-color-muted);\n  }\n\n  .df-bg-tertiary {\n    background-color: var(--df-color-border);\n  }\n\n  .df-border-base {\n    border: 1px solid var(--df-color-border);\n  }\n\n  .df-border-light {\n    border: 1px solid\n      color-mix(in srgb, var(--df-color-border) 50%, transparent);\n  }\n\n  .df-border-strong {\n    border: 1px solid var(--df-color-primary);\n  }\n\n  .df-border-primary {\n    border-color: var(--df-color-primary);\n  }\n\n  .df-border-primary-soft {\n    border-color: color-mix(in srgb, var(--df-color-primary) 50%, transparent);\n  }\n\n  .df-ring-primary {\n    --tw-ring-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 20%,\n      transparent\n    );\n  }\n\n  .df-ring-primary-solid {\n    --tw-ring-color: var(--df-color-primary);\n  }\n\n  .df-shadow-primary {\n    --tw-shadow-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 20%,\n      transparent\n    );\n  }\n\n  .df-focus-ring:focus {\n    border-color: var(--df-color-primary);\n    box-shadow:\n      0 0 0 0px var(--df-color-background),\n      0 0 0 2px var(--df-color-primary);\n  }\n\n  .df-focus-ring-only:focus {\n    box-shadow:\n      0 0 0 0px var(--df-color-background),\n      0 0 0 2px var(--df-color-primary);\n  }\n\n  .df-focus-border-primary:focus {\n    border-color: var(--df-color-primary);\n  }\n\n  .df-all-day-event-animate {\n    animation: df-event-pop-in 0.3s ease-out forwards;\n  }\n\n  .df-animate-in {\n    animation-duration: 200ms;\n    animation-fill-mode: forwards;\n    animation-timing-function: cubic-bezier(0, 0, 0.2, 1);\n  }\n\n  .df-fade-in {\n    animation-name: df-fade-in;\n  }\n\n  .df-zoom-in-95 {\n    animation-name: df-zoom-in;\n  }\n\n  .df-fade-in.df-zoom-in-95 {\n    animation-name: df-fade-in, df-zoom-in;\n  }\n\n  .df-animate-slide-up {\n    animation: df-slide-up 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n  }\n\n  .df-animate-slide-down {\n    animation: df-slide-down 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n  }\n\n  @media (max-width: 768px) {\n    .df-calendar-container {\n      --df-calendar-height: 550px;\n    }\n  }\n\n  @media (max-width: 767px) {\n    .df-mobile-mask-fade {\n      mask-image: linear-gradient(to right, black 70%, transparent 100%);\n      -webkit-mask-image: linear-gradient(\n        to right,\n        black 70%,\n        transparent 100%\n      );\n    }\n  }\n\n  .df-drag-indicator-title-mask {\n    overflow: hidden;\n    white-space: nowrap;\n    text-overflow: clip;\n    mask-image: linear-gradient(to right, black 78%, transparent 100%);\n    -webkit-mask-image: linear-gradient(to right, black 78%, transparent 100%);\n    mask-repeat: no-repeat;\n    -webkit-mask-repeat: no-repeat;\n  }\n}\n\n.df-calendar-container *::-webkit-scrollbar {\n  width: 2px;\n  height: 2px;\n}\n\n.df-calendar-container *::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n.df-calendar-container *::-webkit-scrollbar-thumb {\n  background: color-mix(in srgb, var(--df-color-border), transparent 18%);\n  border-radius: 1px;\n}\n\n.dark .df-calendar-container *::-webkit-scrollbar-thumb {\n  background: color-mix(in srgb, var(--df-color-border), white 12%);\n}\n\n.df-calendar-container *::-webkit-scrollbar-thumb:hover {\n  background: color-mix(in srgb, var(--df-color-muted-foreground), white 8%);\n}\n\n.df-calendar-container * {\n  scrollbar-width: thin;\n  scrollbar-color: color-mix(in srgb, var(--df-color-border), transparent 18%)\n    transparent;\n}\n\n.dark .df-calendar-container * {\n  scrollbar-color: color-mix(in srgb, var(--df-color-border), white 12%)\n    transparent;\n}\n\n.df-calendar-container.df-drag-active,\n.df-calendar-container .df-drag-active {\n  user-select: none;\n  -webkit-user-select: none;\n}\n\n.df-calendar-container.df-drag-active .df-event,\n.df-calendar-container.df-drag-active .df-month-segment-event,\n.df-calendar-container.df-drag-active .df-month-more-events,\n.df-calendar-container.df-drag-active .df-month-title,\n.df-calendar-container.df-drag-active .df-year-event-content {\n  pointer-events: none;\n}\n\n.df-calendar-container .df-cursor-ns-resize,\n.df-calendar-container .df-cursor-ns-resize * {\n  cursor: ns-resize;\n}\n\nbody.df-cursor-ns-resize,\nbody.df-cursor-ns-resize * {\n  cursor: ns-resize !important;\n}\n\n.df-calendar-container .df-cursor-ew-resize,\n.df-calendar-container .df-cursor-ew-resize * {\n  cursor: ew-resize;\n}\n\nbody.df-cursor-ew-resize,\nbody.df-cursor-ew-resize * {\n  cursor: ew-resize !important;\n}\n\n.df-calendar-container .df-cursor-grabbing,\n.df-calendar-container .df-cursor-grabbing * {\n  cursor: grabbing;\n}\n\nbody.df-cursor-grabbing,\nbody.df-cursor-grabbing * {\n  cursor: grabbing !important;\n}\n\n.df-scrollbar-hide::-webkit-scrollbar {\n  display: none;\n}\n\n.df-scrollbar-hide {\n  -ms-overflow-style: none;\n  scrollbar-width: none;\n}\n\n@keyframes df-fade-out {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n  }\n}\n\n@keyframes df-fade-in {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes df-event-pop-in {\n  from {\n    opacity: 0;\n    transform: scale(0.95) translateY(-4px);\n  }\n\n  to {\n    opacity: 1;\n    transform: scale(1) translateY(0);\n  }\n}\n\n@keyframes df-zoom-in {\n  from {\n    transform: scale(0.95);\n  }\n\n  to {\n    transform: scale(1);\n  }\n}\n\n@keyframes df-slide-up {\n  from {\n    transform: translateY(100%);\n  }\n\n  to {\n    transform: translateY(0);\n  }\n}\n\n@keyframes df-slide-down {\n  from {\n    transform: translateY(0);\n  }\n\n  to {\n    transform: translateY(100%);\n  }\n}\n"
  },
  {
    "path": "packages/core/src/styles/tailwind-components.css",
    "content": "/* styles.components.css — for projects that already have Tailwind CSS\n *\n * This file includes DayFlow's component styles and theme variables only.\n * It does NOT emit Tailwind utility classes, so it will not conflict with\n * the host application's own Tailwind instance.\n *\n */\n\n/* Establish the CSS cascade layer order explicitly so that DayFlow's\n * component styles (border-color, background-color, etc.) always win over\n * Tailwind's preflight reset (@layer base), regardless of whether the host\n * app imports this file before or after `@import 'tailwindcss'`.\n *\n * Priority (highest → lowest): utilities > components > df-theme > base > theme\n */\n@layer theme, base, df-theme, components, utilities;\n\n/* @tailwindcss/theme is imported here only to resolve @apply at build time.\n * It does not emit utility classes into the compiled output. */\n@import 'tailwindcss/theme' layer(theme);\n@import './library-imports.css';\n\n@variant dark (.dark &);\n"
  },
  {
    "path": "packages/core/src/styles/tailwind.css",
    "content": "@import 'tailwindcss/preflight' layer(base);\n@import 'tailwindcss/theme' layer(theme);\n@import 'tailwindcss/utilities' source(none);\n@import './library-imports.css';\n\n@variant dark (.dark &);\n\n/*\n * Map Tailwind's internal color tokens to the library's namespaced variables.\n * Using @theme inline means generated utilities reference --df-color-* directly\n * (e.g. `.bg-primary { background-color: var(--df-color-primary) }`),\n * so the host's own --color-primary is never touched.\n */\n@theme inline {\n  --color-background: var(--df-color-background);\n  --color-foreground: var(--df-color-foreground);\n  --color-hover: var(--df-color-hover);\n  --color-border: var(--df-color-border);\n  --color-card: var(--df-color-card);\n  --color-card-foreground: var(--df-color-card-foreground);\n  --color-muted: var(--df-color-muted);\n  --color-muted-foreground: var(--df-color-muted-foreground);\n\n  --color-primary: var(--df-color-primary);\n  --color-primary-foreground: var(--df-color-primary-foreground);\n  --color-secondary: var(--df-color-secondary);\n  --color-secondary-foreground: var(--df-color-secondary-foreground);\n\n  --color-destructive: var(--df-color-destructive);\n  --color-destructive-foreground: var(--df-color-destructive-foreground);\n}\n"
  },
  {
    "path": "packages/core/src/types/calendar.ts",
    "content": "// Calendar and date data type definitions\n\n/**\n * Date data interface\n * Represents detailed information for a single date\n */\nexport interface DayData {\n  date: Date;\n  day: number;\n  month: number;\n  year: number;\n  monthName: string;\n  shortMonthName: string;\n  isToday: boolean;\n}\n\n/**\n * Week data interface\n * Represents a week's data, containing date information for 7 days\n */\nexport interface WeeksData {\n  days: DayData[];\n  startDate: Date;\n  monthYear: {\n    month: string;\n    monthIndex: number;\n    year: number;\n  };\n}\n"
  },
  {
    "path": "packages/core/src/types/calendarTypes.ts",
    "content": "// Calendar Types System - Type Definitions\n\n/**\n * Color configuration for a calendar type\n */\nexport interface CalendarColors {\n  /** Event background color */\n  eventColor: string;\n  /** Selected event background color */\n  eventSelectedColor: string;\n  /** Border/line color */\n  lineColor: string;\n  /** Text color */\n  textColor: string;\n}\n\n/**\n * Calendar Type Definition\n * Represents a category of events (e.g., Work, Personal, Study)\n */\nexport interface CalendarType {\n  /** Unique identifier (e.g., 'work', 'personal') */\n  id: string;\n\n  /** Display name (e.g., 'Work', 'Personal') */\n  name: string;\n\n  /** Optional description */\n  description?: string;\n\n  /** Light mode colors */\n  colors: CalendarColors;\n\n  /** Dark mode colors (optional, falls back to light mode if not provided) */\n  darkColors?: CalendarColors;\n\n  /** Optional icon (emoji or icon name) */\n  icon?: string;\n\n  /** Whether this is a system default type */\n  isDefault?: boolean;\n\n  /** Whether events of this type should be visible */\n  isVisible?: boolean;\n\n  /** Whether this calendar is read-only (disables UI mutations like drag/edit) */\n  readOnly?: boolean;\n\n  /** The source of this calendar (e.g., 'Google Calendar', 'iCloud', 'Outlook') */\n  source?: string;\n\n  /** Subscription status and metadata */\n  subscription?: {\n    url: string;\n    status: 'loading' | 'ready' | 'error';\n    meta?: Record<string, unknown>;\n  };\n}\n\n/**\n * Theme mode\n */\nexport type ThemeMode = 'light' | 'dark' | 'auto';\n\n/**\n * System-level theme colors\n */\nexport interface ThemeColors {\n  background?: string;\n  text?: string;\n  border?: string;\n  [key: string]: string | undefined;\n}\n\n/**\n * Theme configuration\n */\nexport interface ThemeConfig {\n  /** Current theme mode */\n  mode: ThemeMode;\n\n  /** System-level theme colors (optional) */\n  colors?: ThemeColors;\n}\n\n/**\n * Calendar configuration for the app\n */\nexport interface CalendarsConfig {\n  /** Array of calendar types */\n  calendars?: CalendarType[];\n\n  /** Default calendar for new events */\n  defaultCalendar?: string;\n\n  /** Theme configuration */\n  theme?: ThemeConfig;\n}\n"
  },
  {
    "path": "packages/core/src/types/config.ts",
    "content": "// Configuration-related type definitions\n\nimport { ViewType } from './core';\n\n/**\n * Drag configuration type\n * Defines configuration parameters for drag functionality\n */\nexport interface DragConfig {\n  viewType: ViewType;\n\n  // Color utility function\n  getLineColor: (color: string) => string;\n\n  // Dynamic padding utility function\n  getDynamicPadding: (drag: { endHour: number; startHour: number }) => string;\n\n  // Layout constants\n  HOUR_HEIGHT: number;\n  FIRST_HOUR: number;\n  LAST_HOUR: number;\n  MIN_DURATION: number;\n  TIME_COLUMN_WIDTH: number;\n  ALL_DAY_HEIGHT: number;\n}\n"
  },
  {
    "path": "packages/core/src/types/core.ts",
    "content": "// oxlint-disable typescript/no-explicit-any\n// Core type definitions\nimport { AnyComponent, ComponentChildren } from 'preact';\n\nimport { ViewSwitcherMode } from '@/components/common/ViewHeader';\nimport { CalendarRegistry } from '@/core/calendarRegistry';\nimport { Locale } from '@/locale/types';\n\nimport { CalendarType, ThemeConfig, ThemeMode } from './calendarTypes';\nimport { Event } from './event';\nimport { EventLayout } from './layout';\nimport { TimeZoneValue } from './timezone';\n\n/** Generic type for framework-specific components */\nexport type TComponent = AnyComponent<any, any>;\n/** Generic type for framework-specific nodes/elements */\nexport type TNode = ComponentChildren;\n\n/**\n * View type enum\n */\nexport enum ViewType {\n  DAY = 'day',\n  WEEK = 'week',\n  MONTH = 'month',\n  YEAR = 'year',\n  AGENDA = 'agenda',\n  RESOURCE = 'resource',\n}\n\nexport type CalendarViewType = ViewType | string;\n\n/**\n * Plugin interface\n * Defines the basic structure of calendar plugins\n */\nexport interface CalendarPlugin {\n  name: string;\n  install: (app: ICalendarApp) => void;\n  config?: any;\n  api?: unknown;\n}\n\n/**\n * View interface\n * Defines the basic structure of calendar views\n */\nexport interface CalendarView {\n  type: CalendarViewType;\n  label?: string;\n  component: TComponent;\n  config?: Record<string, unknown>;\n}\n\nexport type RangeChangeReason =\n  | 'initial'\n  | 'navigation'\n  | 'viewChange'\n  | 'scroll';\n\nexport type EventChange =\n  | { type: 'create'; event: Event }\n  | { type: 'update'; before: Event; after: Event }\n  | { type: 'delete'; event: Event };\n\n/**\n * Calendar callbacks interface\n * Defines calendar event callback functions\n */\nexport interface CalendarCallbacks {\n  onEventBatchChange?: (changes: EventChange[]) => void | Promise<void>;\n  onViewChange?: (view: CalendarViewType) => void | Promise<void>;\n  onEventCreate?: (event: Event) => void | Promise<void>;\n  onEventUpdate?: (event: Event) => void | Promise<void>;\n  onEventDelete?: (eventId: string) => void | Promise<void>;\n  onDateChange?: (date: Date) => void | Promise<void>;\n  onRender?: () => void | Promise<void>;\n  onVisibleRangeChange?: (\n    start: Date,\n    end: Date,\n    reason: RangeChangeReason\n  ) => void | Promise<void>;\n  onCalendarUpdate?: (calendar: CalendarType) => void | Promise<void>;\n  onCalendarCreate?: (calendar: CalendarType) => void | Promise<void>;\n  onCalendarDelete?: (calendarId: string) => void | Promise<void>;\n  onCalendarMerge?: (\n    sourceId: string,\n    targetId: string\n  ) => void | Promise<void>;\n  onCalendarReorder?: (\n    fromIndex: number,\n    toIndex: number\n  ) => void | Promise<void>;\n  onEventClick?: (event: Event) => void | Promise<void>;\n  onEventDoubleClick?: (\n    event: Event,\n    e: MouseEvent\n  ) => boolean | undefined | Promise<boolean | undefined>;\n  onMoreEventsClick?: (date: Date) => void | Promise<void>;\n  onDismissUI?: () => void | Promise<void>;\n  /**\n   * Toggle event detail panel or dialog.\n   * If eventId is null, closes the detail UI.\n   */\n  onEventDetailToggle?: (eventId: string | null) => void;\n  /**\n   * Toggle the mobile event detail drawer.\n   * Pass an event to open it, or null to close it.\n   */\n  onMobileEventDetailToggle?: (event: Event | null) => void;\n}\n\nexport interface CalendarHeaderProps {\n  calendar: ICalendarApp;\n  switcherMode?: ViewSwitcherMode;\n  onAddCalendar?: (e: MouseEvent | TouchEvent | any) => void;\n  onSearchChange?: (value: string) => void;\n  /** Triggered when search icon is clicked (typically on mobile) */\n  onSearchClick?: () => void;\n  searchValue?: string;\n  isSearchOpen?: boolean;\n  isEditable?: boolean;\n  /** Left safe area padding (px) to avoid overlapping with traffic light buttons in macMode */\n  safeAreaLeft?: number;\n}\n\n/** Args passed to all eventContent* slot renderers. */\nexport interface EventContentSlotArgs {\n  event: Event;\n  viewType: ViewType;\n  isAllDay: boolean;\n  isMobile: boolean;\n  isSelected: boolean;\n  isDragging: boolean;\n  layout?: EventLayout;\n}\n\n/** Args passed to the eventContextMenu slot renderer. */\nexport interface EventContextMenuSlotArgs {\n  event: Event;\n  onClose: () => void;\n}\n\n/** Args passed to the gridContextMenu slot renderer. */\nexport interface GridContextMenuSlotArgs {\n  date: Date;\n  viewType?: ViewType;\n  onClose: () => void;\n}\n\n/**\n * Calendar application configuration\n * Used to initialize CalendarApp\n */\n/**\n * Comparator function for sorting all-day events across all views.\n * Return negative if `a` should appear before `b`, positive if after, 0 if equal.\n */\nexport type AllDaySortComparator = (a: Event, b: Event) => number;\n\nexport interface CalendarAppConfig {\n  views: CalendarView[];\n  plugins?: CalendarPlugin[];\n  events?: Event[];\n  callbacks?: CalendarCallbacks;\n  defaultView?: CalendarViewType;\n  initialDate?: Date;\n  switcherMode?: ViewSwitcherMode;\n  calendars?: CalendarType[];\n  defaultCalendar?: string;\n  theme?: ThemeConfig;\n  useEventDetailDialog?: boolean;\n  useEventDetailPanel?: boolean;\n  useCalendarHeader?: boolean;\n  locale?: string | Locale;\n  readOnly?: boolean | ReadOnlyConfig;\n  /** Custom sort comparator for all-day events, applied in day/week/month/year views. */\n  allDaySortComparator?: AllDaySortComparator;\n  /**\n   * Global display and editing timezone for all views.\n   * Controls how event times are projected and how drag/resize/create operations interpret wall-clock time.\n   * Defaults to the user's system timezone.\n   * Switching this field only triggers a re-render — it never calls onEventUpdate or any persistence callback.\n   */\n  timeZone?: TimeZoneValue;\n}\n\n/**\n * Read-only configuration\n */\nexport interface ReadOnlyConfig {\n  draggable?: boolean; // Whether to allow dragging\n  viewable?: boolean; // Whether to allow inspecting (open detail panel/dialog/drawer)\n}\n\n/**\n * Calendar application state\n * Internal state of CalendarApp\n */\nexport interface CalendarAppState {\n  currentView: CalendarViewType;\n  currentDate: Date;\n  events: Event[];\n  plugins: Map<string, CalendarPlugin>;\n  views: Map<CalendarViewType, CalendarView>;\n  switcherMode?: ViewSwitcherMode;\n\n  locale: string | Locale;\n  highlightedEventId?: string | null;\n  selectedEventId?: string | null;\n  readOnly: boolean | ReadOnlyConfig;\n  overrides: string[];\n  allDaySortComparator?: AllDaySortComparator;\n  /** Resolved global timezone (IANA string). See CalendarAppConfig.timeZone. */\n  timeZone: string;\n}\n\n/**\n * Calendar application instance\n * Core interface of CalendarApp\n */\nexport interface ICalendarApp {\n  // State\n  state: CalendarAppState;\n  getReadOnlyConfig: (id?: string) => ReadOnlyConfig;\n  canMutateFromUI: (id?: string) => boolean;\n\n  // Subscription management\n  subscribe: (listener: (app: ICalendarApp) => void) => () => void;\n\n  // View management\n  changeView: (view: CalendarViewType) => void;\n  getCurrentView: () => CalendarView;\n  getViewConfig: (viewType: CalendarViewType) => Record<string, unknown>;\n\n  // Date management\n  setCurrentDate: (date: Date) => void;\n  getCurrentDate: () => Date;\n  goToToday: () => void;\n  goToPrevious: () => void;\n  goToNext: () => void;\n  selectDate: (date: Date) => void;\n\n  // Undo management\n  undo: () => void;\n\n  // Event management\n  applyEventsChanges: (\n    changes: {\n      add?: Event[];\n      update?: Array<{ id: string; updates: Partial<Event> }>;\n      delete?: string[];\n    },\n    isPending?: boolean,\n    source?: 'drag' | 'resize'\n  ) => void;\n  addEvent: (event: Event) => void;\n  /** Add events from external sources (like subscriptions) without persisting to main DB */\n  addExternalEvents: (calendarId: string, events: Event[]) => void;\n  updateEvent: (\n    id: string,\n    event: Partial<Event>,\n    isPending?: boolean,\n    source?: 'drag' | 'resize'\n  ) => Promise<void>;\n  deleteEvent: (id: string) => Promise<void>;\n  getEvents: () => Event[];\n  getAllEvents: () => Event[];\n  onEventClick: (event: Event) => void;\n  onEventDoubleClick: (\n    event: Event,\n    e: MouseEvent\n  ) => boolean | undefined | Promise<boolean | undefined>;\n  onMoreEventsClick: (date: Date) => void;\n  onEventDetailToggle: (eventId: string | null) => void;\n  onMobileEventDetailToggle: (event: Event | null) => void;\n  highlightEvent: (eventId: string | null) => void;\n  selectEvent: (eventId: string | null) => void;\n  getCalendars: () => CalendarType[];\n  reorderCalendars: (fromIndex: number, toIndex: number) => void;\n  setCalendarVisibility: (calendarId: string, visible: boolean) => void;\n  setAllCalendarsVisibility: (visible: boolean) => void;\n  updateCalendar: (\n    id: string,\n    updates: Partial<CalendarType>,\n    isPending?: boolean\n  ) => void;\n  createCalendar: (calendar: CalendarType) => Promise<void>;\n  deleteCalendar: (id: string) => Promise<void>;\n  mergeCalendars: (sourceId: string, targetId: string) => Promise<void>;\n  setVisibleMonth: (date: Date) => void;\n  getVisibleMonth: () => Date;\n  emitVisibleRange: (\n    start: Date,\n    end: Date,\n    reason?: RangeChangeReason\n  ) => void;\n\n  // UI Signals\n  dismissUI: () => void;\n\n  // Plugin management\n  getPlugin: <T = unknown>(name: string) => T | undefined;\n  hasPlugin: (name: string) => boolean;\n\n  // Calendar Header\n  getCalendarHeaderConfig: () => boolean;\n\n  // Trigger render callback (internal use, notify subscribers)\n  triggerRender: () => void;\n\n  // Get CalendarRegistry instance\n  getCalendarRegistry: () => CalendarRegistry;\n\n  // Get whether to use event detail dialog\n  getUseEventDetailDialog: () => boolean;\n\n  // Get whether to use event detail panel\n  getUseEventDetailPanel: () => boolean;\n\n  // Update configuration dynamically\n  updateConfig: (config: Partial<CalendarAppConfig>) => void;\n\n  /** The resolved global display/edit timezone (IANA string). */\n  readonly timeZone: string;\n\n  // Overrides management\n  setOverrides: (overrides: string[]) => void;\n\n  // Theme management\n  setTheme: (mode: ThemeMode) => void;\n  getTheme: () => ThemeMode;\n  subscribeThemeChange: (callback: (theme: ThemeMode) => void) => () => void;\n  unsubscribeThemeChange: (callback: (theme: ThemeMode) => void) => void;\n}\n\n/**\n * useCalendarApp Hook return type\n * Calendar application interface provided for React components\n */\nexport interface UseCalendarAppReturn {\n  app: ICalendarApp;\n  currentView: CalendarViewType;\n  currentDate: Date;\n  events: Event[];\n  applyEventsChanges: (\n    changes: {\n      add?: Event[];\n      update?: Array<{ id: string; updates: Partial<Event> }>;\n      delete?: string[];\n    },\n    isPending?: boolean,\n    source?: 'drag' | 'resize'\n  ) => void;\n  changeView: (view: CalendarViewType) => void;\n  setCurrentDate: (date: Date) => void;\n  addEvent: (event: Event) => void;\n  updateEvent: (\n    id: string,\n    event: Partial<Event>,\n    isPending?: boolean,\n    source?: 'drag' | 'resize'\n  ) => Promise<void>;\n  deleteEvent: (id: string) => Promise<void>;\n  goToToday: () => void;\n  goToPrevious: () => void;\n  goToNext: () => void;\n  selectDate: (date: Date) => void;\n  undo: () => void;\n  getCalendars: () => CalendarType[];\n  createCalendar: (calendar: CalendarType) => Promise<void>;\n  mergeCalendars: (sourceId: string, targetId: string) => Promise<void>;\n  setCalendarVisibility: (calendarId: string, visible: boolean) => void;\n  setAllCalendarsVisibility: (visible: boolean) => void;\n  getAllEvents: () => Event[];\n  highlightEvent: (eventId: string | null) => void;\n  setVisibleMonth: (date: Date) => void;\n  getVisibleMonth: () => Date;\n  emitVisibleRange: (\n    start: Date,\n    end: Date,\n    reason?: RangeChangeReason\n  ) => void;\n  canMutateFromUI: (id?: string) => boolean;\n  readOnlyConfig: ReadOnlyConfig;\n}\n\n/**\n * Calendar configuration system type\n * Contains drag and view configurations\n */\nexport interface CalendarConfig {\n  locale?: string;\n  drag: {\n    HOUR_HEIGHT: number;\n    FIRST_HOUR: number;\n    LAST_HOUR: number;\n    MIN_DURATION: number;\n    TIME_COLUMN_WIDTH: number;\n    ALL_DAY_HEIGHT: number;\n    getLineColor: (color: string) => string;\n    getDynamicPadding: (drag: { endHour: number; startHour: number }) => string;\n  };\n  views: {\n    day: Record<string, unknown>;\n    week: Record<string, unknown>;\n    month: Record<string, unknown>;\n    agenda: Record<string, unknown>;\n  };\n}\n\nexport interface UseCalendarReturn {\n  // State\n  view: CalendarViewType;\n  currentDate: Date;\n  events: Event[];\n  currentWeekStart: Date;\n\n  // Actions\n  changeView: (view: CalendarViewType) => void;\n  goToToday: () => void;\n  goToPrevious: () => void;\n  goToNext: () => void;\n  selectDate: (date: Date) => void;\n  updateEvent: (\n    eventId: string,\n    updates: Partial<Event>,\n    isPending?: boolean\n  ) => Promise<void>;\n  deleteEvent: (eventId: string) => Promise<void>;\n  addEvent: (event: Omit<Event, 'id'>) => void;\n  setEvents: (events: Event[] | ((prev: Event[]) => Event[])) => void;\n}\n"
  },
  {
    "path": "packages/core/src/types/dragIndicator.ts",
    "content": "import { RefObject, ComponentChildren } from 'preact';\n\n// Drag-related type definitions\nimport { DragConfig, ICalendarApp, ViewType } from '@/types';\n\nimport { Event } from './event';\nimport { EventLayout } from './layout';\n\n/**\n * Drag mode type\n */\nexport type Mode = 'create' | 'move' | 'resize' | null;\n\n/**\n * Drag reference interface\n * Stores state information for drag operations\n */\nexport interface DragRef {\n  active: boolean;\n  mode: Mode;\n  eventId: string | null;\n  dayIndex: number;\n  startX: number;\n  startY: number;\n  startHour: number;\n  endHour: number;\n  originalDay: number;\n  originalStartHour: number;\n  originalEndHour: number;\n  resizeDirection: string | null;\n  hourOffset: number | null;\n  duration: number;\n  lastRawMouseHour: number | null;\n  lastUpdateTime: number;\n  initialMouseY: number;\n  lastClientY: number;\n  allDay: boolean;\n  eventDate?: Date;\n  calendarIds?: string[];\n}\n\n/**\n * Event detail position interface\n * Used to position event detail popup\n */\nexport interface EventDetailPosition {\n  top: number;\n  left: number;\n  eventHeight: number;\n  eventMiddleY: number;\n  isSunday?: boolean;\n}\n\nexport interface DragIndicatorProps {\n  drag: DragRef;\n  color?: string;\n  title?: string;\n  layout?: EventLayout | null;\n  allDay: boolean;\n  formatTime: (hour: number) => string;\n  getLineColor: (color: string) => string;\n  getDynamicPadding: (drag: DragRef) => string;\n  locale?: string;\n  isMobile?: boolean;\n  /** True when the indicator container has a light background (e.g. diagonal multi-calendar pattern).\n   *  Renderers should use dark text instead of white when this is set. */\n  isLightBackground?: boolean;\n  /** Pre-computed line colors for all calendars on this event.\n   *  Single-element array for single-calendar; multi-element for multi-calendar gradient bar. */\n  calendarLineColors?: string[];\n}\n\nexport interface DragIndicatorRenderer {\n  renderAllDayContent: (props: DragIndicatorProps) => ComponentChildren;\n  renderRegularContent: (props: DragIndicatorProps) => ComponentChildren;\n  renderDefaultContent: (props: DragIndicatorProps) => ComponentChildren;\n}\n\nexport interface UnifiedDragRef extends DragRef {\n  // Month view specific properties\n  targetDate?: Date | null;\n  originalDate?: Date | null;\n  originalEvent?: Event | null;\n  dragOffset?: number;\n  dragOffsetY?: number;\n  originalStartDate?: Date | null;\n  originalEndDate?: Date | null;\n  eventDate?: Date;\n  originalStartTime?: { hour: number; minute: number; second: number } | null;\n  originalEndTime?: { hour: number; minute: number; second: number } | null;\n  sourceElement?: HTMLElement | null;\n  indicatorVisible?: boolean;\n  // Week/Day view all-day event cross-day property\n  eventDurationDays?: number;\n  // Number of days the current segment occupies (for cross-week MultiDayEvent)\n  currentSegmentDays?: number;\n  // dayIndex when dragging starts (for cross-day event fragment dragging)\n  startDragDayIndex?: number;\n  // Initial rendered all-day indicator geometry for Day/Week views\n  initialIndicatorLeft?: number;\n  initialIndicatorTop?: number;\n  initialIndicatorWidth?: number;\n  initialIndicatorHeight?: number;\n  indicatorContainer?: HTMLElement | null;\n  // Event properties needed for deferred indicator creation\n  calendarId?: string;\n  calendarIds?: string[];\n  title?: string;\n}\n\nexport interface useDragProps extends Partial<DragConfig> {\n  calendarRef: RefObject<HTMLDivElement>;\n  allDayRowRef?: RefObject<HTMLDivElement>; // Required for Week/Day views\n  timeGridRef?: RefObject<HTMLDivElement>; // Optional, used for translated grid layouts\n  viewType: ViewType;\n  onEventsUpdate: (\n    updateFunc: (events: Event[]) => Event[],\n    isResizing?: boolean,\n    source?: 'drag' | 'resize'\n  ) => void;\n  onEventCreate: (event: Event) => void;\n  onEventEdit?: (event: Event) => void; // Required for Month view\n  calculateNewEventLayout?: (\n    dayIndex: number,\n    startHour: number,\n    endHour: number\n  ) => EventLayout | null; // Required for Week/Day views\n  calculateDragLayout?: (\n    event: Event,\n    targetDay: number,\n    targetStartHour: number,\n    targetEndHour: number\n  ) => EventLayout | null; // Required for Week/Day views\n  currentWeekStart: Date;\n  events: Event[];\n  renderer?: DragIndicatorRenderer; // Required for Week/Day views\n  app?: ICalendarApp;\n  isMobile?: boolean;\n  gridWidth?: string;\n  displayDays?: number;\n}\n\n// Unified drag state type definitions\nexport type MonthDragState = {\n  active: boolean;\n  mode: 'create' | 'move' | 'resize' | null;\n  eventId: string | null;\n  targetDate: Date | null;\n  startDate: Date | null;\n  endDate: Date | null;\n};\n\nexport type WeekDayDragState = {\n  active: boolean;\n  mode: 'create' | 'move' | 'resize' | null;\n  eventId: string | null;\n  dayIndex: number;\n  startHour: number;\n  endHour: number;\n  allDay: boolean;\n};\n\n// Unified return value interface\nexport interface useDragReturn {\n  // Common methods\n  createDragIndicator: (\n    drag: UnifiedDragRef,\n    color?: string,\n    title?: string,\n    layout?: EventLayout | null,\n    sourceElement?: HTMLElement\n  ) => void;\n  updateDragIndicator: (\n    ...args: (number | boolean | EventLayout | null | undefined)[]\n  ) => void;\n  removeDragIndicator: () => void;\n  handleCreateAllDayEvent?: (\n    e: MouseEvent | TouchEvent,\n    dayIndex: number\n  ) => void; // Week/Day views\n  handleCreateStart: (\n    e: MouseEvent | TouchEvent,\n    ...args: (Date | number)[]\n  ) => void;\n  handleMoveStart: (e: MouseEvent | TouchEvent, event: Event) => void;\n  handleResizeStart: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  dragState: MonthDragState | WeekDayDragState;\n  isDragging: boolean;\n  // Week/Day view specific\n  pixelYToHour?: (y: number) => number;\n  getColumnDayIndex?: (x: number) => number;\n}\n\n/**\n * Month view event drag state (alias for MonthDragState, maintains backward compatibility)\n */\nexport type MonthEventDragState = MonthDragState;\n\n/**\n * Drag state Hook return value\n */\nexport interface UseDragStateReturn {\n  // Refs\n  dragRef: RefObject<UnifiedDragRef>;\n  currentDragRef: RefObject<{ x: number; y: number }>;\n\n  // State\n  dragState: MonthDragState | WeekDayDragState;\n  setDragState: (\n    val:\n      | MonthDragState\n      | WeekDayDragState\n      | ((\n          prev: MonthDragState | WeekDayDragState\n        ) => MonthDragState | WeekDayDragState)\n  ) => void;\n\n  // Methods\n  resetDragState: () => void;\n  throttledSetEvents: (\n    updateFunc: (events: Event[]) => Event[],\n    dragState?: string\n  ) => void;\n}\n\n/**\n * Drag common utilities Hook return value\n */\nexport interface UseDragCommonReturn {\n  // Week/Day view utilities\n  pixelYToHour: (y: number) => number;\n  getColumnDayIndex: (x: number) => number;\n  checkIfInAllDayArea: (clientY: number) => boolean;\n  handleDirectScroll: (clientY: number) => void;\n\n  // Month view utilities\n  daysDifference: (date1: Date, date2: Date) => number;\n  addDaysToDate: (date: Date, days: number) => Date;\n  getTargetDateFromPosition: (clientX: number, clientY: number) => Date | null;\n\n  // Constants\n  ONE_DAY_MS: number;\n}\n\n/**\n * Drag management Hook return value\n */\nexport interface UseDragManagerReturn {\n  dragIndicatorRef: RefObject<HTMLDivElement | null>;\n  removeDragIndicator: () => void;\n  createDragIndicator: (\n    drag: UnifiedDragRef,\n    color?: string,\n    title?: string,\n    layout?: EventLayout | null,\n    sourceElement?: HTMLElement\n  ) => void;\n  updateDragIndicator: (\n    ...args: (number | boolean | EventLayout | null | undefined)[]\n  ) => void;\n}\n\n/**\n * Drag handler Hook return value\n */\nexport interface UseDragHandlersReturn {\n  handleDragMove: (e: MouseEvent | TouchEvent) => void;\n  handleDragEnd: (e: MouseEvent | TouchEvent) => void;\n  handleCreateStart: (\n    e: MouseEvent | TouchEvent,\n    ...args: (Date | number)[]\n  ) => void;\n  handleMoveStart: (e: MouseEvent | TouchEvent, event: Event) => void;\n  handleResizeStart: (\n    e: MouseEvent | TouchEvent,\n    event: Event,\n    direction: string\n  ) => void;\n  handleUniversalDragMove: (e: MouseEvent | TouchEvent) => void;\n  handleUniversalDragEnd: (e?: MouseEvent | TouchEvent) => void;\n}\n\n/**\n * Drag handler Hook parameters\n */\nexport interface UseDragHandlersParams {\n  options: useDragProps;\n  common: UseDragCommonReturn;\n  state: UseDragStateReturn;\n  manager: UseDragManagerReturn;\n}\n\nexport interface UseMonthDragReturn {\n  // Month view specific utilities\n  daysDifference: (date1: Date, date2: Date) => number;\n  addDaysToDate: (date: Date, days: number) => Date;\n  getTargetDateFromPosition: (clientX: number, clientY: number) => Date | null;\n}\n\nexport interface UseMonthDragParams {\n  options: useDragProps;\n  common: UseDragCommonReturn;\n  state: UseDragStateReturn;\n  manager: UseDragManagerReturn;\n}\n\nexport interface UseWeekDayDragReturn {\n  handleCreateAllDayEvent: (\n    e: MouseEvent | TouchEvent,\n    dayIndex: number\n  ) => void;\n  pixelYToHour: (y: number) => number;\n  getColumnDayIndex: (x: number) => number;\n}\n\nexport interface UseWeekDayDragParams {\n  options: useDragProps;\n  common: UseDragCommonReturn;\n  state: UseDragStateReturn;\n  manager: UseDragManagerReturn;\n  handleDragMove: (e: MouseEvent) => void;\n  handleDragEnd: (e: MouseEvent) => void;\n}\n"
  },
  {
    "path": "packages/core/src/types/event.ts",
    "content": "import { ComponentChildren } from 'preact';\nimport { Temporal } from 'temporal-polyfill';\n\n/**\n * Calendar event interface (using Temporal API)\n * Unified event data structure supporting single-day, cross-day, and all-day events\n */\nexport interface Event {\n  id: string;\n  title: string;\n  description?: string;\n\n  // Using Temporal API to represent time\n  // - Temporal.PlainDate: All-day events (date only)\n  // - Temporal.PlainDateTime: Local events with time (date + time, no timezone) ✨ Recommended default\n  // - Temporal.ZonedDateTime: Cross-timezone events (date + time + timezone)\n  start: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime;\n  end: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime;\n  // Pending: make allDay to internal derived field, because we can infer allDay from start/end types(PlainDate)\n  allDay?: boolean;\n\n  // all day icon\n  icon?: boolean | ComponentChildren;\n\n  // Calendar type reference\n  calendarId?: string;\n  /** Multi-calendar support: list of calendar IDs this event belongs to.\n   *  When present, takes precedence over calendarId for visibility and color rendering.\n   *  The event is visible as long as at least one listed calendar is visible. */\n  calendarIds?: string[];\n\n  meta?: Record<string, unknown>;\n\n  // Internal use fields (for rendering and layout calculation)\n  day?: number;\n  /** Original start hour (used for stable cross-day layout) */\n  _originalStartHour?: number;\n  /** Original end hour (used for stable cross-day layout) */\n  _originalEndHour?: number;\n}\n"
  },
  {
    "path": "packages/core/src/types/eventDetail.ts",
    "content": "// oxlint-disable typescript/no-explicit-any\nimport { AnyComponent, RefObject } from 'preact';\n\nimport { ICalendarApp } from '@/types';\n\nimport { EventDetailPosition } from './dragIndicator';\nimport { Event } from './event';\n\n// Re-export EventDetailPosition for convenience\nexport type { EventDetailPosition } from './dragIndicator';\n\n/**\n * Event detail panel Props\n */\nexport interface EventDetailPanelProps {\n  /** Current event data */\n  event: Event;\n  /** Panel position information */\n  position: EventDetailPosition;\n  /** Panel DOM reference */\n  panelRef: RefObject<HTMLDivElement>;\n  /** Whether the event is all-day */\n  isAllDay: boolean;\n  /** Event visibility state */\n  eventVisibility:\n    | 'standard'\n    | 'sticky-top'\n    | 'sticky-bottom'\n    | 'sticky-left'\n    | 'sticky-right';\n  /** Calendar container reference */\n  calendarRef: RefObject<HTMLDivElement>;\n  /** Selected event element reference */\n  selectedEventElementRef: RefObject<HTMLElement | null>;\n  /** Event update callback */\n  onEventUpdate: (updatedEvent: Event) => void | Promise<void>;\n  /** Event delete callback */\n  onEventDelete: (eventId: string) => void | Promise<void>;\n  /** Close panel callback (optional) */\n  onClose?: () => void;\n}\n\n/**\n * Custom event detail panel renderer (full panel including positioning and styling)\n */\nexport type EventDetailPanelRenderer = AnyComponent<EventDetailPanelProps, any>;\n\n/**\n * Event detail content Props (excluding panel container, content only)\n */\nexport interface EventDetailContentProps {\n  /** Current event data */\n  event: Event;\n  /** Whether the event is all-day */\n  isAllDay: boolean;\n  /** Event update callback */\n  onEventUpdate: (updatedEvent: Event) => void | Promise<void>;\n  /** Event delete callback */\n  onEventDelete: (eventId: string) => void | Promise<void>;\n  /** Close panel callback (optional) */\n  onClose?: () => void;\n  app?: ICalendarApp;\n}\n\n/**\n * Custom event detail content renderer (content only, will be wrapped in default panel)\n */\nexport type EventDetailContentRenderer = AnyComponent<\n  EventDetailContentProps,\n  any\n>;\n\n/**\n * Event detail dialog Props\n */\nexport interface EventDetailDialogProps {\n  /** Current event data */\n  event: Event;\n  /** Whether the dialog is open */\n  isOpen: boolean;\n  /** Whether the event is all-day */\n  isAllDay: boolean;\n  /** Event update callback */\n  onEventUpdate: (updatedEvent: Event) => void | Promise<void>;\n  /** Event delete callback */\n  onEventDelete: (eventId: string) => void | Promise<void>;\n  /** Close dialog callback */\n  onClose: () => void;\n  app?: ICalendarApp;\n}\n\n/**\n * Custom event detail dialog renderer (Dialog/Modal mode)\n */\nexport type EventDetailDialogRenderer = AnyComponent<\n  EventDetailDialogProps,\n  any\n>;\n"
  },
  {
    "path": "packages/core/src/types/factory.ts",
    "content": "import { AnyComponent, ComponentChildren, RefObject } from 'preact';\n\nimport { ViewSwitcherMode } from '@/components/common/ViewHeader';\n\nimport { CalendarView, CalendarViewType, ViewType, ICalendarApp } from './core';\nimport { Event } from './event';\nimport { EventLayout } from './layout';\nimport { TimeZoneValue } from './timezone';\n\n/**\n * Common Props interface for view components\n * Base properties for all view components\n */\nexport interface BaseViewProps<TConfig = unknown> {\n  // Core application instance\n  app: ICalendarApp;\n\n  // Base state\n  currentDate?: Date; // Optional as they might be derived or passed via app\n  currentView?: CalendarViewType;\n  events?: Event[];\n\n  // Event management - Optional as they can be derived from app\n  onEventUpdate?: (event: Event) => void | Promise<void>;\n  onEventDelete?: (eventId: string) => void | Promise<void>;\n  onEventCreate?: (event: Event) => void;\n\n  // Navigation control\n  onDateChange?: (date: Date) => void;\n  onViewChange?: (view: CalendarViewType) => void;\n\n  // View-specific configuration\n  config: TConfig;\n  // Selection control\n  selectedEventId?: string | null;\n  onEventSelect?: (eventId: string | null) => void;\n  detailPanelEventId?: string | null;\n  onDetailPanelToggle?: (eventId: string | null) => void;\n\n  // Customization\n  useEventDetailPanel?: boolean;\n  calendarRef: RefObject<HTMLDivElement>;\n  switcherMode?: ViewSwitcherMode;\n  meta?: Record<string, unknown>;\n}\n\n/**\n * Day view specific Props\n */\nexport type DayViewProps = BaseViewProps<DayViewConfig>;\n\n/**\n * Week view specific Props\n */\nexport type WeekViewProps = BaseViewProps<WeekViewConfig>;\n\n/**\n * Month view specific Props\n */\nexport type MonthViewProps = BaseViewProps<MonthViewConfig>;\n\n/**\n * Agenda view specific Props\n */\nexport type AgendaViewProps = BaseViewProps<AgendaViewConfig>;\n\n/**\n * Year view specific Props\n */\nexport type YearViewProps = BaseViewProps<YearViewConfig>;\n\n/**\n * View factory configuration interface\n * Base configuration for creating views\n */\nexport interface ViewFactoryConfig {\n  // Shared layout properties\n  hourHeight?: number;\n  firstHour?: number;\n  lastHour?: number;\n  allDayHeight?: number;\n  timeFormat?: '12h' | '24h';\n}\n\n/**\n * Day view factory configuration\n */\nexport interface DayViewConfig extends ViewFactoryConfig {\n  showAllDay?: boolean;\n  scrollToCurrentTime?: boolean;\n  secondaryTimeZone?: TimeZoneValue;\n  showEventDots?: boolean;\n}\n\n/**\n * Week view factory configuration\n */\nexport interface WeekViewConfig extends ViewFactoryConfig {\n  showWeekends?: boolean;\n  showAllDay?: boolean;\n  startOfWeek?: number;\n  scrollToCurrentTime?: boolean;\n  secondaryTimeZone?: TimeZoneValue;\n  showEventDots?: boolean;\n  /**\n   * Action when a date cell is clicked.\n   */\n  gridDateClick?: 'day-view' | 'none' | ((date: Date, events: Event[]) => void);\n  /**\n   * Action when a date cell is double-clicked.\n   * - 'create-event' (default): create a 1-hour timed event at the clicked position (hour)\n   * - 'day-view': navigate to the Day View\n   * - 'none': no action\n   * - function: custom handler\n   */\n  gridDateDoubleClick?:\n    | 'create-event'\n    | 'day-view'\n    | 'none'\n    | ((date: Date, events: Event[]) => void);\n}\n\n/**\n * Month scroll / navigation configuration\n */\nexport interface MonthScrollConfig {\n  /** Disable continuous scrolling; only Prev/Next buttons switch months */\n  disabled?: boolean;\n  /**\n   * Transition animation when switching months in disabled-scroll mode.\n   * - 'slide'  (default) – vertical slide up/down\n   * - 'fade'             – horizontal fade-slide (left ↔ right)\n   */\n  transition?: 'slide' | 'fade';\n}\n\n/**\n * Month view factory configuration\n */\nexport interface MonthViewConfig extends ViewFactoryConfig {\n  showWeekNumbers?: boolean;\n  showMonthIndicator?: boolean;\n  startOfWeek?: number;\n  snapToMonth?: boolean;\n  /** Scroll / navigation behavior for the month view */\n  scroll?: MonthScrollConfig;\n  showEventDots?: boolean;\n  /** Height in pixels of each event row in the month grid. Default: 16 */\n  eventHeight?: number;\n  /**\n   * Action when a date cell is clicked.\n   */\n  gridDateClick?:\n    | 'week-view'\n    | 'day-view'\n    | 'none'\n    | ((date: Date, events: Event[]) => void);\n  /**\n   * Action when a date cell is double-clicked.\n   * - 'create-event' (default): create a timed event from 9:00 to 10:00 on the clicked date\n   * - 'week-view': navigate to the Week View\n   * - 'day-view': navigate to the Day View\n   * - 'none': no action\n   * - function: custom handler\n   */\n  gridDateDoubleClick?:\n    | 'create-event'\n    | 'week-view'\n    | 'day-view'\n    | 'none'\n    | ((date: Date, events: Event[]) => void);\n}\n\n/**\n * Agenda view factory configuration\n */\nexport interface AgendaViewConfig extends ViewFactoryConfig {\n  /** Number of days rendered in one agenda page. */\n  daysToShow?: number;\n  /** Whether empty dates should still render a section in the list. */\n  showEmptyDays?: boolean;\n  /**\n   * Action when a date section is clicked.\n   */\n  gridDateClick?: 'day-view' | 'none' | ((date: Date, events: Event[]) => void);\n  /**\n   * Action when a date section is double-clicked.\n   * - 'day-view' (default): navigate to the Day View\n   * - 'none': no action\n   * - function: custom handler\n   */\n  gridDateDoubleClick?:\n    | 'day-view'\n    | 'none'\n    | ((date: Date, events: Event[]) => void);\n}\n\n/**\n * Year view factory configuration\n */\nexport interface YearViewConfig extends ViewFactoryConfig {\n  mode?: 'year-canvas' | 'fixed-week' | 'grid';\n  showTimedEventsInYearView?: boolean;\n  startOfWeek?: number;\n  /** Scroll / navigation behavior for the month view */\n  scroll?: MonthScrollConfig;\n  showEventDots?: boolean;\n  /**\n   * Grid mode: action when a date cell is clicked.\n   */\n  gridDateClick?:\n    | 'popup'\n    | 'day-view'\n    | 'none'\n    | ((date: Date, events: Event[]) => void);\n  /**\n   * Grid mode: action when a date cell is double-clicked.\n   * - 'create-event' (default): create a timed event from 9:00 to 10:00 on the clicked date\n   * - 'day-view': navigate to the Day View\n   * - 'none': no action\n   * - function: custom handler\n   */\n  gridDateDoubleClick?:\n    | 'create-event'\n    | 'day-view'\n    | 'none'\n    | ((date: Date, events: Event[]) => void);\n  /**\n   * Grid mode: render custom popup content.\n   * Receives the clicked date and its events; return null/undefined to use the default popup.\n   */\n  gridPopupContent?: (date: Date, events: Event[]) => ComponentChildren;\n  /**\n   * Grid mode: number of heatmap intensity levels.\n   * @default 5\n   */\n  gridHeatmapLevels?: number;\n}\n\n/**\n * View adapter Props\n * Adapter properties for wrapping original components\n */\nexport interface ViewAdapterProps extends BaseViewProps {\n  viewType: CalendarViewType;\n  // oxlint-disable-next-line typescript/no-explicit-any\n  originalComponent: AnyComponent<any, any>;\n  config: ViewFactoryConfig;\n  className?: string;\n}\n\n/**\n * Drag integration Props\n * Properties for integrating drag functionality into views\n */\nexport interface DragIntegrationProps {\n  app: ICalendarApp;\n  viewType: ViewType;\n  calendarRef: RefObject<HTMLDivElement>;\n  allDayRowRef?: RefObject<HTMLDivElement>;\n  events: Event[];\n  onEventsUpdate: (updateFunc: (events: Event[]) => Event[]) => void;\n  onEventCreate: (event: Event) => void;\n  calculateNewEventLayout?: (\n    dayIndex: number,\n    startHour: number,\n    endHour: number\n  ) => EventLayout | null;\n  calculateDragLayout?: (\n    event: Event,\n    targetDay: number,\n    targetStartHour: number,\n    targetEndHour: number\n  ) => EventLayout | null;\n  currentWeekStart: Date;\n}\n\n/**\n * Virtual scroll integration Props\n * Properties for integrating virtual scroll functionality into views\n */\nexport interface VirtualScrollIntegrationProps {\n  app: ICalendarApp;\n  currentDate: Date;\n  weekHeight?: number;\n  onCurrentMonthChange?: (month: string, year: number) => void;\n  initialWeeksToLoad?: number;\n}\n\n/**\n * Factory function return type\n * Type definition for view factory functions\n */\nexport interface ViewFactory<TConfig = ViewFactoryConfig> {\n  (config?: TConfig): CalendarView;\n}\n"
  },
  {
    "path": "packages/core/src/types/hook.ts",
    "content": "// Hook-related type definitions\n\n/**\n * Virtual scroll item interface (YearView)\n */\nexport interface VirtualItem {\n  index: number;\n  year: number;\n  top: number;\n  height: number;\n}\n\n/**\n * Virtual scroll Hook parameters interface (YearView)\n */\nexport interface UseVirtualScrollProps {\n  currentDate: Date;\n  yearHeight: number;\n  onCurrentYearChange?: (year: number) => void;\n}\n\nimport { JSX, RefObject } from 'preact';\n\n/**\n * Virtual scroll item interface (YearView)\n */\n// ...\n/**\n * Virtual scroll Hook return value interface (YearView)\n */\nexport interface UseVirtualScrollReturn {\n  scrollTop: number;\n  containerHeight: number;\n  currentYear: number;\n  isScrolling: boolean;\n  virtualData: {\n    totalHeight: number;\n    visibleItems: VirtualItem[];\n  };\n  scrollElementRef: RefObject<HTMLDivElement>;\n  handleScroll: (\n    e: JSX.TargetedEvent<HTMLDivElement, globalThis.Event>\n  ) => void;\n  scrollToYear: (targetYear: number, smooth?: boolean) => void;\n  handlePreviousYear: () => void;\n  handleNextYear: () => void;\n  handleToday: () => void;\n  setScrollTop: (val: number | ((prev: number) => number)) => void;\n  setContainerHeight: (val: number | ((prev: number) => number)) => void;\n  setCurrentYear: (val: number | ((prev: number) => number)) => void;\n  setIsScrolling: (val: boolean | ((prev: boolean) => boolean)) => void;\n}\n"
  },
  {
    "path": "packages/core/src/types/index.ts",
    "content": "// Types module entry file - Re-export all type definitions\n\n// Core types\nexport * from './core';\n\n// Calendar and date data types\nexport * from './calendar';\n\n// Event core types\nexport * from './event';\n\n// Event layout types\nexport * from './layout';\n\n// Drag-related types\nexport * from './dragIndicator';\n\n// Month view virtual scroll types\nexport * from './monthView';\n\n// View factory types\nexport * from './factory';\n\n// Plugin types\nexport * from './plugin';\n\n// Configuration types\nexport * from './config';\n\n// Hook types\nexport * from './hook';\n\n// Event detail panel types\nexport * from './eventDetail';\n\n// Mobile event types\nexport * from './mobileEvent';\n\n// TimeZone types\nexport * from './timezone';\n\n// Calendar types and Theme types\nexport * from './calendarTypes';\n\n// Search types\nexport * from './search';\n"
  },
  {
    "path": "packages/core/src/types/layout.ts",
    "content": "// Event layout related type definitions\nimport { Event } from './event';\n\n/**\n * Event layout configuration constants\n * Controls visual presentation of events in the calendar\n */\nexport const LAYOUT_CONFIG = {\n  INDENT_STEP: 2,\n  MIN_WIDTH: 25,\n  MARGIN_BETWEEN: 2,\n  CONTAINER_WIDTH: 320,\n  OVERLAP_THRESHOLD: 0.25,\n  EDGE_MARGIN: 3,\n  MAX_LOAD_IMBALANCE: 0,\n  REBALANCE_THRESHOLD: 2,\n} as const;\n\n/**\n * Event layout interface\n * Defines position and styling of events in the UI\n */\nexport interface EventLayout {\n  id: string;\n  left: number;\n  width: number;\n  zIndex: number;\n  level: number;\n  isPrimary: boolean;\n  indentOffset: number;\n  importance: number;\n}\n\n/**\n * Nested layer interface\n * Represents hierarchical relationships of events\n */\nexport interface NestedLayer {\n  events: Event[];\n  level: number;\n  parentEvent?: Event;\n  timeSlot?: { start: number; end: number };\n}\n\n/**\n * Event group interface\n * Represents a group of related events and their nested structure\n */\nexport interface EventGroup {\n  events: Event[];\n  startHour: number;\n  endHour: number;\n  primaryEvent?: Event;\n  nestedStructure: NestedLayer[];\n  specialLayoutRules?: SpecialLayoutRule[];\n  originalBranchMap?: Map<string, Event>;\n}\n\n/**\n * Event relationship information interface\n * Describes relationships of events in nested structures\n */\nexport interface EventRelations {\n  directChildren: Event[];\n  allDescendants: Event[];\n  directParent: Event | null;\n  layer: NestedLayer | null;\n  subtreeSize: number;\n  isLeaf: boolean;\n}\n\n/**\n * Subtree analysis interface\n * Used to analyze structural information of event trees\n */\nexport interface SubtreeAnalysis {\n  rootEvent: Event;\n  allDescendants: Event[];\n  timeSpan: { start: number; end: number; duration: number };\n  descendantCount: number;\n  maxDepth: number;\n  branchPath: Event[];\n}\n\n/**\n * Balance strategy interface\n * Used for balance algorithms to optimize event layouts\n */\nexport interface BalanceStrategy {\n  type: 'count_balance' | 'timespan_balance';\n  transfers: TransferOperation[];\n  specialLayoutRules: SpecialLayoutRule[];\n}\n\n/**\n * Transfer operation interface\n * Describes movement of events in layout optimization\n */\nexport interface TransferOperation {\n  event: Event;\n  fromParent: Event;\n  toParent: Event;\n  reason: string;\n}\n\n/**\n * Special layout rule interface\n * Defines layout constraints for specific events\n */\nexport interface SpecialLayoutRule {\n  eventId: string;\n  layoutType:\n    | 'align_with_ancestor'\n    | 'full_width'\n    | 'full_width_from_level'\n    | 'align_with_sibling';\n  referenceEvent?: Event; // Reference event (for alignment)\n  targetLevel?: number; // Target level\n  reason?: string; // Reason for applying the rule\n}\n"
  },
  {
    "path": "packages/core/src/types/mobileEvent.ts",
    "content": "import { ICalendarApp } from './core';\nimport { Event } from './event';\n\n/**\n * Mobile event drawer/dialog Props\n */\nexport interface MobileEventProps {\n  /** Whether the drawer/dialog is open */\n  isOpen: boolean;\n  /** Callback to close the drawer/dialog */\n  onClose: () => void;\n  /** Callback to save the event (creates or updates) */\n  onSave: (event: Event) => void;\n  /** Callback to delete an existing event by id */\n  onEventDelete?: (id: string) => void;\n  /** Current event data (newly created template or existing event) */\n  draftEvent: Event | null;\n  /** The ICalendarApp instance providing built-in services */\n  app: ICalendarApp;\n  /** Time format for event display */\n  timeFormat?: '12h' | '24h';\n}\n"
  },
  {
    "path": "packages/core/src/types/monthView.ts",
    "content": "import { JSX, RefObject } from 'preact';\n\nimport { WeeksData } from './calendar';\n\nexport interface UseVirtualMonthScrollProps {\n  currentDate: Date;\n  weekHeight: number;\n  onCurrentMonthChange?: (month: string, year: number) => void;\n  initialWeeksToLoad?: number;\n  locale?: string;\n  startOfWeek?: number;\n  isEnabled?: boolean;\n  snapToMonth?: boolean;\n}\n\n// Hook return value interface\nexport interface UseVirtualMonthScrollReturn {\n  scrollTop: number;\n  containerHeight: number;\n  currentMonth: string;\n  currentYear: number;\n  isScrolling: boolean;\n  isNavigating: boolean;\n  virtualData: {\n    totalHeight: number;\n    visibleItems: VirtualWeekItem[];\n    displayStartIndex: number; // Index of the first week that should actually be displayed\n  };\n  scrollElementRef: RefObject<HTMLDivElement>;\n  handleScroll: (\n    e: JSX.TargetedEvent<HTMLDivElement, globalThis.Event>\n  ) => void;\n  scrollToDate: (targetDate: Date, smooth?: boolean) => void;\n  handlePreviousMonth: () => void;\n  handleNextMonth: () => void;\n  handleToday: () => void;\n  setScrollTop: (val: number | ((prev: number) => number)) => void;\n  setContainerHeight: (val: number | ((prev: number) => number)) => void;\n  setCurrentMonth: (val: string | ((prev: string) => string)) => void;\n  setCurrentYear: (val: number | ((prev: number) => number)) => void;\n  setIsScrolling: (val: boolean | ((prev: boolean) => boolean)) => void;\n  cache: WeekDataCache;\n  scrollElementRefCallback: (element: HTMLDivElement | null) => void;\n  weeksData: WeeksData[];\n}\n\n// Virtual scroll configuration\nexport const VIRTUAL_MONTH_SCROLL_CONFIG = {\n  OVERSCAN: 6,\n  BUFFER_SIZE: 100,\n  MIN_YEAR: 1900,\n  MAX_YEAR: 2200,\n  SCROLL_THROTTLE: 8,\n  SCROLL_DEBOUNCE: 150,\n  CACHE_CLEANUP_THRESHOLD: 200,\n  MOBILE_WEEK_HEIGHT: 80,\n  TABLET_WEEK_HEIGHT: 90,\n  WEEK_HEIGHT: 119,\n} as const;\n\n// Virtual scroll item interface\nexport interface VirtualWeekItem {\n  index: number;\n  weekData: WeeksData;\n  top: number;\n  height: number;\n}\n\n// High-performance week data cache class\nexport class WeekDataCache {\n  private cache = new Map<string, WeeksData>();\n  private accessOrder: string[] = [];\n  private maxSize: number;\n\n  constructor(maxSize: number = VIRTUAL_MONTH_SCROLL_CONFIG.BUFFER_SIZE) {\n    this.maxSize = maxSize;\n  }\n\n  private static getKey(date: Date): string {\n    return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;\n  }\n\n  get(weekStartDate: Date): WeeksData | undefined {\n    const key = WeekDataCache.getKey(weekStartDate);\n    const data = this.cache.get(key);\n    if (data) {\n      this.updateAccessOrder(key);\n      return data;\n    }\n    return undefined;\n  }\n\n  set(weekStartDate: Date, data: WeeksData): void {\n    const key = WeekDataCache.getKey(weekStartDate);\n\n    if (this.cache.size >= this.maxSize) {\n      const oldestKey = this.accessOrder.shift();\n      if (oldestKey !== undefined) {\n        this.cache.delete(oldestKey);\n      }\n    }\n\n    this.cache.set(key, data);\n    this.updateAccessOrder(key);\n  }\n\n  private updateAccessOrder(key: string): void {\n    const index = this.accessOrder.indexOf(key);\n    if (index > -1) {\n      this.accessOrder.splice(index, 1);\n    }\n    this.accessOrder.push(key);\n  }\n\n  getSize(): number {\n    return this.cache.size;\n  }\n\n  clear(): void {\n    this.cache.clear();\n    this.accessOrder = [];\n  }\n}\n"
  },
  {
    "path": "packages/core/src/types/plugin.ts",
    "content": "import { CalendarType } from './calendarTypes';\n// Plugin-related type definitions\nimport { DragConfig } from './config';\nimport { ICalendarApp, ViewType } from './core';\nimport { MonthDragState, WeekDayDragState } from './dragIndicator';\nimport { Event } from './event';\nimport { EventLayout } from './layout';\n\n// Sidebar Plugin Slot Args\nexport interface TitleBarSlotProps {\n  isCollapsed: boolean;\n  toggleCollapsed: () => void;\n}\n\nexport interface SidebarHeaderSlotArgs {\n  isCollapsed: boolean;\n  onCollapseToggle: () => void;\n}\n\nexport interface CreateCalendarDialogColorPickerProps {\n  color: string;\n  onChange: (color: { hex: string }) => void;\n  onAccept?: () => void;\n  onCancel?: () => void;\n  // oxlint-disable-next-line typescript/no-explicit-any\n  styles?: any;\n}\n\nexport interface ColorPickerProps {\n  color: string;\n  onChange: (color: { hex: string }) => void;\n  onChangeComplete?: (color: { hex: string }) => void;\n}\n\nexport interface CreateCalendarDialogProps {\n  onClose: () => void;\n  onCreate: (calendar: CalendarType) => void | Promise<void>;\n  app: ICalendarApp;\n}\n\n// Events Plugin\n/**\n * Events service interface\n * Provides various event management functions\n */\nexport interface EventsService {\n  // Basic event operations\n  getAll: () => Event[];\n  getById: (id: string) => Event | undefined;\n  add: (event: Event) => void;\n  update: (id: string, updates: Partial<Event>) => void | Promise<void>;\n  delete: (id: string) => void | Promise<void>;\n\n  // Event queries\n  getByDate: (date: Date) => Event[];\n  getByDateRange: (startDate: Date, endDate: Date) => Event[];\n  getByDay: (dayIndex: number, weekStart: Date) => Event[];\n  getAllDayEvents: (dayIndex: number, events: Event[]) => Event[];\n\n  // Event calculation and recalculation\n  recalculateEventDays: (events: Event[], weekStart: Date) => Event[];\n\n  // Event validation\n  validateEvent: (event: Partial<Event>) => string[];\n\n  // Event filtering\n  filterEvents: (events: Event[], filter: (event: Event) => boolean) => Event[];\n}\n\n/**\n * Events plugin configuration\n */\nexport interface EventsPluginConfig {\n  enableAutoRecalculate?: boolean;\n  enableValidation?: boolean;\n  defaultEvents?: Event[];\n  maxEventsPerDay?: number;\n}\n\n/**\n * Drag Hook options\n */\nexport interface DragHookOptions extends Partial<DragConfig> {\n  calendarRef: unknown;\n  allDayRowRef?: unknown;\n  viewType: ViewType;\n  onEventsUpdate: (\n    updateFunc: (events: Event[]) => Event[],\n    isResizing?: boolean,\n    source?: 'drag' | 'resize'\n  ) => void;\n  onEventCreate: (event: Event) => void;\n  onEventEdit: (event: Event) => void;\n  currentWeekStart: Date;\n  events: Event[];\n  calculateNewEventLayout?: (\n    dayIndex: number,\n    startHour: number,\n    endHour: number\n  ) => EventLayout | null;\n  calculateDragLayout?: (\n    event: Event,\n    targetDay: number,\n    targetStartHour: number,\n    targetEndHour: number\n  ) => EventLayout | null;\n  isMobile?: boolean;\n  timeGridRef?: unknown;\n  gridWidth?: string;\n  displayDays?: number;\n}\n\n/**\n * Drag Hook return value\n */\nexport interface DragHookReturn {\n  handleMoveStart?: (e: unknown, event: Event) => void;\n  handleCreateStart?: (e: unknown, ...args: (Date | number)[]) => void;\n  handleResizeStart?: (e: unknown, event: Event, direction: string) => void;\n  handleCreateAllDayEvent?: (e: unknown, dayIndex: number) => void;\n  dragState: MonthDragState | WeekDayDragState;\n  isDragging: boolean;\n}\n\n/**\n * Drag plugin configuration\n */\nexport interface DragPluginConfig {\n  // Feature toggles\n  enableDrag: boolean;\n  enableResize: boolean;\n  enableCreate: boolean;\n  enableAllDayCreate: boolean;\n\n  // View support\n  supportedViews: ViewType[];\n\n  // Drag/resize callbacks\n  onEventDrop?: (\n    updatedEvent: Event,\n    originalEvent: Event\n  ) => void | Promise<void>;\n  onEventResize?: (\n    updatedEvent: Event,\n    originalEvent: Event\n  ) => void | Promise<void>;\n\n  // Allow additional properties\n  [key: string]: unknown;\n}\n\n/**\n * Drag service interface\n * Provides drag capability for views\n */\nexport interface DragService {\n  // Get drag configuration\n  getConfig: () => DragPluginConfig;\n  updateConfig: (updates: Partial<DragPluginConfig>) => void;\n\n  // Check if view supports dragging\n  isViewSupported: (viewType: ViewType) => boolean;\n}\n"
  },
  {
    "path": "packages/core/src/types/search.ts",
    "content": "import { ICalendarApp } from '@/types';\n\nimport { Event } from './event';\n\nexport type CalendarSearchEvent = Event & {\n  color?: string; // For calendar color\n  [key: string]: unknown;\n};\n\nexport interface CalendarSearchProps {\n  /**\n   * Debounce delay in ms\n   * @default 300\n   */\n  debounceDelay?: number;\n\n  /**\n   * Async search method\n   */\n  onSearch?: (keyword: string) => Promise<CalendarSearchEvent[]>;\n\n  /**\n   * Custom search logic (takes over completely)\n   */\n  customSearch?: (params: {\n    keyword: string;\n    events: CalendarSearchEvent[];\n  }) => CalendarSearchEvent[];\n\n  /**\n   * Search state callback\n   */\n  onSearchStateChange?: (state: {\n    keyword: string;\n    loading: boolean;\n    results: CalendarSearchEvent[];\n  }) => void;\n\n  /**\n   * Empty result text\n   */\n  emptyText?: string | Record<string, string>;\n\n  /**\n   * Custom handler for search result clicks\n   */\n  onResultClick?: (params: {\n    event: CalendarSearchEvent;\n    app: ICalendarApp;\n    source: 'desktop' | 'mobile';\n    defaultAction: () => void;\n    closeSearch: () => void;\n  }) => void | Promise<void>;\n}\n"
  },
  {
    "path": "packages/core/src/types/timezone.ts",
    "content": "/* oxlint-disable typescript/no-duplicate-enum-values */\n/**\n * Common IANA TimeZone identifiers.\n * This enum provides a convenient way for users to specify secondary timezones\n * without having to look up the exact Temporal/Intl TimeZone strings.\n */\nexport enum TimeZone {\n  // UTC/GMT\n  UTC = 'UTC',\n\n  // North America\n  NEW_YORK = 'America/New_York',\n  CHICAGO = 'America/Chicago',\n  DENVER = 'America/Denver',\n  LOS_ANGELES = 'America/Los_Angeles',\n  TORONTO = 'America/Toronto',\n  VANCOUVER = 'America/Vancouver',\n  PHOENIX = 'America/Phoenix',\n  ANCHORAGE = 'America/Anchorage',\n  HONOLULU = 'Pacific/Honolulu',\n  MEXICO_CITY = 'America/Mexico_City',\n  WINNIPEG = 'America/Winnipeg',\n  HALIFAX = 'America/Halifax',\n  ST_JOHNS = 'America/St_Johns',\n  DETROIT = 'America/Detroit',\n\n  // Europe\n  LONDON = 'Europe/London',\n  PARIS = 'Europe/Paris',\n  BERLIN = 'Europe/Berlin',\n  MADRID = 'Europe/Madrid',\n  ROME = 'Europe/Rome',\n  AMSTERDAM = 'Europe/Amsterdam',\n  ZURICH = 'Europe/Zurich',\n  STOCKHOLM = 'Europe/Stockholm',\n  OSLO = 'Europe/Oslo',\n  COPENHAGEN = 'Europe/Copenhagen',\n  MOSCOW = 'Europe/Moscow',\n  ISTANBUL = 'Europe/Istanbul',\n  DUBLIN = 'Europe/Dublin',\n  LISBON = 'Europe/Lisbon',\n  PRAGUE = 'Europe/Prague',\n  VIENNA = 'Europe/Vienna',\n  WARSAW = 'Europe/Warsaw',\n  BRUSSELS = 'Europe/Brussels',\n  ATHENS = 'Europe/Athens',\n  BUCHAREST = 'Europe/Bucharest',\n  HELSINKI = 'Europe/Helsinki',\n  KYIV = 'Europe/Kyiv',\n  BUDAPEST = 'Europe/Budapest',\n  BELGRADE = 'Europe/Belgrade',\n  LUXEMBOURG = 'Europe/Luxembourg',\n  MONACO = 'Europe/Monaco',\n  REYKJAVIK = 'Atlantic/Reykjavik',\n\n  // Asia\n  TOKYO = 'Asia/Tokyo',\n  SHANGHAI = 'Asia/Shanghai',\n  HONG_KONG = 'Asia/Hong_Kong',\n  TAIPEI = 'Asia/Taipei',\n  SEOUL = 'Asia/Seoul',\n  SINGAPORE = 'Asia/Singapore',\n  HANOI = 'Asia/Ho_Chi_Minh',\n  BANGKOK = 'Asia/Bangkok',\n  JAKARTA = 'Asia/Jakarta',\n  KUALA_LUMPUR = 'Asia/Kuala_Lumpur',\n  MANILA = 'Asia/Manila',\n  DUBAI = 'Asia/Dubai',\n  KOLKATA = 'Asia/Kolkata',\n  RIYADH = 'Asia/Riyadh',\n  TEHRAN = 'Asia/Tehran',\n  JERUSALEM = 'Asia/Jerusalem',\n  TEL_AVIV = 'Asia/Tel_Aviv',\n  BAGHDAD = 'Asia/Baghdad',\n  DHAKA = 'Asia/Dhaka',\n  KARACHI = 'Asia/Karachi',\n  KABUL = 'Asia/Kabul',\n  KATHMANDU = 'Asia/Kathmandu',\n  COLOMBO = 'Asia/Colombo',\n  TASHKENT = 'Asia/Tashkent',\n  ALMATY = 'Asia/Almaty',\n  PHNOM_PENH = 'Asia/Phnom_Penh',\n  VIENTIANE = 'Asia/Vientiane',\n  MUSCAT = 'Asia/Muscat',\n\n  // Oceania\n  SYDNEY = 'Australia/Sydney',\n  MELBOURNE = 'Australia/Melbourne',\n  BRISBANE = 'Australia/Brisbane',\n  PERTH = 'Australia/Perth',\n  ADELAIDE = 'Australia/Adelaide',\n  DARWIN = 'Australia/Darwin',\n  HOBART = 'Australia/Hobart',\n  AUCKLAND = 'Pacific/Auckland',\n  FIJI = 'Pacific/Fiji',\n  GUAM = 'Pacific/Guam',\n  NOUMEA = 'Pacific/Noumea',\n  PAGO_PAGO = 'Pacific/Pago_Pago',\n  PORT_MORESBY = 'Pacific/Port_Moresby',\n\n  // South America\n  SAO_PAULO = 'America/Sao_Paulo',\n  BUENOS_AIRES = 'America/Argentina/Buenos_Aires',\n  SANTIAGO = 'America/Santiago',\n  LIMA = 'America/Lima',\n  BOGOTA = 'America/Bogota',\n  CARACAS = 'America/Caracas',\n  LA_PAZ = 'America/La_Paz',\n  MONTEVIDEO = 'America/Montevideo',\n  QUITO = 'America/Quito',\n  ASUNCION = 'America/Asuncion',\n  GEORGETOWN = 'America/Guyana',\n\n  // Africa\n  CAIRO = 'Africa/Cairo',\n  JOHANNESBURG = 'Africa/Johannesburg',\n  LAGOS = 'Africa/Lagos',\n  NAIROBI = 'Africa/Nairobi',\n  CASABLANCA = 'Africa/Casablanca',\n  ALGIERS = 'Africa/Algiers',\n  TUNIS = 'Africa/Tunis',\n  ADDIS_ABABA = 'Africa/Addis_Ababa',\n  ACCRA = 'Africa/Accra',\n  DAKAR = 'Africa/Dakar',\n  LUANDA = 'Africa/Luanda',\n  ANTANANARIVO = 'Indian/Antananarivo',\n  KINSHASA = 'Africa/Kinshasa',\n  DAR_ES_SALAAM = 'Africa/Dar_es_Salaam',\n\n  // Antarctica\n  MCMURDO = 'Antarctica/McMurdo',\n  CASEY = 'Antarctica/Casey',\n}\n\n/**\n * Type helper for secondaryTimeZone configuration\n * Allows using either the TimeZone enum or a raw IANA string.\n */\nexport type TimeZoneValue = TimeZone | string;\n"
  },
  {
    "path": "packages/core/src/utils/__tests__/allDaySort.test.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { organizeAllDayEvents } from '@/components/dayView/util';\nimport { organizeAllDaySegments } from '@/components/weekView/util';\nimport { sortAllDayByTitle } from '@/utils/allDaySort';\n\nconst createAllDayEvent = (\n  id: string,\n  calendarId: string,\n  start: string,\n  end: string = start\n) => ({\n  id,\n  title: id,\n  calendarId,\n  allDay: true,\n  start: Temporal.PlainDate.from(start),\n  end: Temporal.PlainDate.from(end),\n});\n\ndescribe('all-day display priority', () => {\n  it('keeps multi-day all-day events above single-day events in DayView by default', () => {\n    const events = [\n      createAllDayEvent('single', 'a-calendar', '2026-03-12'),\n      createAllDayEvent('multi', 'z-calendar', '2026-03-10', '2026-03-14'),\n    ];\n\n    const organized = organizeAllDayEvents(events);\n\n    expect(organized.find(event => event.id === 'multi')?.row).toBe(0);\n    expect(organized.find(event => event.id === 'single')?.row).toBe(1);\n  });\n\n  it('keeps earlier multi-day all-day events above later ones in WeekView by default', () => {\n    const events = [\n      createAllDayEvent('later', 'a-calendar', '2026-03-11', '2026-03-13'),\n      createAllDayEvent('earlier', 'z-calendar', '2026-03-10', '2026-03-14'),\n      createAllDayEvent('single', 'm-calendar', '2026-03-12'),\n    ];\n\n    const organized = organizeAllDaySegments(\n      events,\n      new Date('2026-03-09T00:00:00'),\n      7\n    );\n\n    expect(organized.find(segment => segment.event.id === 'earlier')?.row).toBe(\n      0\n    );\n    expect(organized.find(segment => segment.event.id === 'later')?.row).toBe(\n      1\n    );\n    expect(organized.find(segment => segment.event.id === 'single')?.row).toBe(\n      2\n    );\n  });\n\n  it('keeps same-calendar all-day events grouped under their multi-day event in WeekView by default', () => {\n    const events = [\n      createAllDayEvent('a', 'z-calendar', '2026-03-12', '2026-03-15'),\n      createAllDayEvent('b', 'a-calendar', '2026-03-12'),\n      createAllDayEvent('c', 'z-calendar', '2026-03-12'),\n    ];\n\n    const organized = organizeAllDaySegments(\n      events,\n      new Date('2026-03-09T00:00:00'),\n      7\n    );\n\n    expect(organized.find(segment => segment.event.id === 'a')?.row).toBe(0);\n    expect(organized.find(segment => segment.event.id === 'c')?.row).toBe(1);\n    expect(organized.find(segment => segment.event.id === 'b')?.row).toBe(2);\n  });\n\n  it('lets title sorting override multi-day priority when using sortAllDayByTitle', () => {\n    const events = [\n      {\n        ...createAllDayEvent('zulu', 'work', '2026-03-12', '2026-03-14'),\n        title: 'Zulu',\n      },\n      {\n        ...createAllDayEvent('alpha', 'work', '2026-03-12'),\n        title: 'Alpha',\n      },\n    ];\n\n    const organized = organizeAllDayEvents(events, sortAllDayByTitle);\n\n    expect(organized.find(event => event.id === 'alpha')?.row).toBe(0);\n    expect(organized.find(event => event.id === 'zulu')?.row).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/utils/__tests__/crossRegionDrag.test.ts",
    "content": "import { roundToTimeStep } from '@/utils';\nimport { restoreTimedDragFromAllDayTransition } from '@/utils/crossRegionDrag';\n\ndescribe('restoreTimedDragFromAllDayTransition', () => {\n  it('preserves the original timed duration when returning from all-day area before drop', () => {\n    const restored = restoreTimedDragFromAllDayTransition({\n      wasOriginallyAllDay: false,\n      mouseHour: 9.75,\n      hourOffset: 0,\n      duration: 2,\n      originalStartHour: 12,\n      originalEndHour: 14,\n      firstHour: 0,\n      lastHour: 24,\n      minDuration: 0.25,\n      roundToTimeStep,\n    });\n\n    expect(restored.duration).toBe(2);\n    expect(restored.startHour).toBe(9.75);\n    expect(restored.endHour).toBe(11.75);\n  });\n\n  it('uses a one-hour default duration only for events that were originally all-day', () => {\n    const restored = restoreTimedDragFromAllDayTransition({\n      wasOriginallyAllDay: true,\n      mouseHour: 13.1,\n      hourOffset: 0.5,\n      duration: 3,\n      originalStartHour: 0,\n      originalEndHour: 0,\n      firstHour: 0,\n      lastHour: 24,\n      minDuration: 0.25,\n      roundToTimeStep,\n    });\n\n    expect(restored.duration).toBe(1);\n    expect(restored.startHour).toBe(roundToTimeStep(13.1));\n    expect(restored.endHour).toBe(restored.startHour + 1);\n    expect(restored.hourOffset).toBe(0);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/utils/__tests__/eventHelpers.test.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { createAllDayEvent } from '@/utils/eventHelpers';\n\ndescribe('eventHelpers', () => {\n  describe('createAllDayEvent', () => {\n    it('creates an all-day event from the object API and preserves calendarId', () => {\n      const event = createAllDayEvent({\n        id: 'conference',\n        title: 'Conference',\n        start: new Date(2026, 5, 7),\n        calendarId: 'work',\n      });\n\n      expect(event).toEqual({\n        id: 'conference',\n        title: 'Conference',\n        description: undefined,\n        start: Temporal.PlainDate.from('2026-06-07'),\n        end: Temporal.PlainDate.from('2026-06-07'),\n        allDay: true,\n        calendarId: 'work',\n        meta: undefined,\n      });\n    });\n\n    it('supports multi-day all-day events from the object API', () => {\n      const event = createAllDayEvent({\n        id: 'retreat',\n        title: 'Team Retreat',\n        start: Temporal.PlainDate.from('2026-06-07'),\n        end: Temporal.PlainDate.from('2026-06-09'),\n      });\n\n      expect(event.start).toEqual(Temporal.PlainDate.from('2026-06-07'));\n      expect(event.end).toEqual(Temporal.PlainDate.from('2026-06-09'));\n      expect(event.allDay).toBe(true);\n    });\n\n    it('keeps the legacy positional signature working with calendarId', () => {\n      const event = createAllDayEvent(\n        'legacy',\n        'Legacy Conference',\n        new Date(2026, 5, 7),\n        new Date(2026, 5, 8),\n        'travel'\n      );\n\n      expect(event.calendarId).toBe('travel');\n      expect(event.start).toEqual(Temporal.PlainDate.from('2026-06-07'));\n      expect(event.end).toEqual(Temporal.PlainDate.from('2026-06-08'));\n    });\n\n    it('supports legacy positional options objects', () => {\n      const event = createAllDayEvent(\n        'legacy-options',\n        'Legacy Options',\n        new Date(2026, 5, 7),\n        {\n          calendarId: 'personal',\n          description: 'Old callsite with options',\n          meta: { source: 'test' },\n        }\n      );\n\n      expect(event.calendarId).toBe('personal');\n      expect(event.description).toBe('Old callsite with options');\n      expect(event.meta).toEqual({ source: 'test' });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/src/utils/__tests__/helpers.test.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport {\n  isMultiDayEvent,\n  extractHourFromDate,\n  formatTime,\n  roundToTimeStep,\n  generateUniKey,\n  isSameDay,\n} from '@/utils/helpers';\n\ndescribe('helpers', () => {\n  describe('isMultiDayEvent', () => {\n    it('should return false for same-day events', () => {\n      const start = Temporal.PlainDate.from('2025-01-15');\n      const end = Temporal.PlainDate.from('2025-01-15');\n\n      expect(isMultiDayEvent(start, end)).toBe(false);\n    });\n\n    it('should return true for multi-day events', () => {\n      const start = Temporal.PlainDate.from('2025-01-15');\n      const end = Temporal.PlainDate.from('2025-01-17');\n\n      expect(isMultiDayEvent(start, end)).toBe(true);\n    });\n\n    it('should handle ZonedDateTime', () => {\n      const start = Temporal.ZonedDateTime.from('2025-01-15T09:00:00[UTC]');\n      const end = Temporal.ZonedDateTime.from('2025-01-17T10:00:00[UTC]');\n\n      expect(isMultiDayEvent(start, end)).toBe(true);\n    });\n  });\n\n  describe('extractHourFromDate', () => {\n    it('should extract hour from Date', () => {\n      const date = new Date('2025-01-15T14:30:00');\n      const hour = extractHourFromDate(date);\n\n      expect(hour).toBe(14.5);\n    });\n\n    it('should extract hour from ZonedDateTime', () => {\n      const zdt = Temporal.ZonedDateTime.from('2025-01-15T09:15:00[UTC]');\n      const hour = extractHourFromDate(zdt);\n\n      expect(hour).toBe(9.25);\n    });\n  });\n\n  describe('formatTime', () => {\n    it('should format time correctly', () => {\n      expect(formatTime(9, 0)).toBe('09:00');\n      expect(formatTime(14, 30)).toBe('14:30');\n      expect(formatTime(23, 45)).toBe('23:45');\n    });\n  });\n\n  describe('roundToTimeStep', () => {\n    it('should round to nearest 15 minutes', () => {\n      expect(roundToTimeStep(9.1)).toBe(9);\n      expect(roundToTimeStep(9.3)).toBe(9.25);\n      expect(roundToTimeStep(9.6)).toBe(9.5);\n      expect(roundToTimeStep(9.9)).toBe(10);\n    });\n  });\n\n  describe('generateUniKey', () => {\n    it('should generate unique keys', () => {\n      const key1 = generateUniKey();\n      const key2 = generateUniKey();\n\n      expect(key1).not.toBe(key2);\n      expect(typeof key1).toBe('string');\n      expect(key1.length).toBeGreaterThan(0);\n    });\n  });\n\n  describe('isSameDay', () => {\n    it('should return true for same day dates', () => {\n      const date1 = new Date('2025-01-15T09:00:00');\n      const date2 = new Date('2025-01-15T14:00:00');\n\n      expect(isSameDay(date1, date2)).toBe(true);\n    });\n\n    it('should return false for different days', () => {\n      const date1 = new Date('2025-01-15T09:00:00');\n      const date2 = new Date('2025-01-16T09:00:00');\n\n      expect(isSameDay(date1, date2)).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/src/utils/__tests__/timeUtils.test.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport {\n  generateSecondaryTimeSlots,\n  getNextHourRangeInTimeZone,\n  getTodayInTimeZone,\n  restoreVisualEventToCanonical,\n} from '@/utils/timeUtils';\n\ndescribe('generateSecondaryTimeSlots', () => {\n  it('uses the visible reference date for DST-sensitive timezone conversion', () => {\n    const slots = [{ hour: 15, label: '15:00' }];\n\n    const result = generateSecondaryTimeSlots(\n      slots,\n      'Asia/Shanghai',\n      '24h',\n      new Date(2026, 3, 2),\n      'Australia/Sydney'\n    );\n\n    expect(result).toEqual(['12:00']);\n  });\n});\n\ndescribe('timezone-aware current date helpers', () => {\n  afterEach(() => {\n    jest.restoreAllMocks();\n  });\n\n  it('returns today using the app timezone wall date', () => {\n    jest\n      .spyOn(Temporal.Now, 'plainDateISO')\n      .mockReturnValue(Temporal.PlainDate.from('2026-04-06'));\n\n    const result = getTodayInTimeZone('Asia/Shanghai');\n\n    expect(result).toEqual(new Date(2026, 3, 6));\n  });\n\n  it('builds the next-hour draft range from the app timezone wall clock', () => {\n    jest\n      .spyOn(Temporal.Now, 'zonedDateTimeISO')\n      .mockReturnValue(\n        Temporal.ZonedDateTime.from('2026-04-05T23:20:00+08:00[Asia/Shanghai]')\n      );\n\n    const result = getNextHourRangeInTimeZone('Asia/Shanghai');\n\n    expect(result).toEqual({\n      start: new Date(2026, 3, 6, 0, 0, 0, 0),\n      end: new Date(2026, 3, 6, 1, 0, 0, 0),\n    });\n  });\n});\n\ndescribe('restoreVisualEventToCanonical', () => {\n  it('converts an edited app-timezone zdt back into the original event timezone', () => {\n    const originalEvent = {\n      id: 'event-1',\n      title: 'Customer Call',\n      start: Temporal.ZonedDateTime.from(\n        '2026-04-02T15:30:00+11:00[Australia/Sydney]'\n      ),\n      end: Temporal.ZonedDateTime.from(\n        '2026-04-02T16:30:00+11:00[Australia/Sydney]'\n      ),\n      allDay: false,\n    };\n\n    const visualEvent = {\n      ...originalEvent,\n      start: Temporal.ZonedDateTime.from(\n        '2026-04-02T12:30:00+08:00[Asia/Shanghai]'\n      ),\n      end: Temporal.ZonedDateTime.from(\n        '2026-04-02T14:00:00+08:00[Asia/Shanghai]'\n      ),\n    };\n\n    const result = restoreVisualEventToCanonical(\n      originalEvent,\n      visualEvent,\n      'Asia/Shanghai'\n    );\n\n    expect(result.start.toString()).toBe(\n      '2026-04-02T15:30:00+11:00[Australia/Sydney]'\n    );\n    expect(result.end.toString()).toBe(\n      '2026-04-02T17:00:00+11:00[Australia/Sydney]'\n    );\n  });\n});\n"
  },
  {
    "path": "packages/core/src/utils/__tests__/timeZoneUtils.test.ts",
    "content": "import { normalizeTimeZoneValue } from '@/utils/timeZoneUtils';\n\ndescribe('normalizeTimeZoneValue', () => {\n  it('canonicalizes uppercase timezone variants from enum-like values', () => {\n    expect(normalizeTimeZoneValue('AMERICA/QUITO')).toBe('America/Quito');\n    expect(normalizeTimeZoneValue('AUSTRALIA/SYDNEY')).toBe('Australia/Sydney');\n  });\n});\n"
  },
  {
    "path": "packages/core/src/utils/allDaySort.ts",
    "content": "import { Event } from '@/types/event';\nimport { temporalToDate } from '@/utils/temporal';\n\nexport const getAllDayRangeMetrics = (event: Event) => {\n  const startDay = temporalToDate(event.start);\n  startDay.setHours(0, 0, 0, 0);\n\n  const endDay = temporalToDate(event.end);\n  endDay.setHours(0, 0, 0, 0);\n\n  return {\n    startDay,\n    endDay,\n    isMultiDay: endDay.getTime() > startDay.getTime(),\n  };\n};\n\n/**\n * Sort all-day events alphabetically by title.\n * Exported so React consumers can pass a stable comparator reference.\n */\nexport const sortAllDayByTitle = (a: Event, b: Event): number =>\n  a.title.localeCompare(b.title);\n\nexport const compareAllDayDisplayPriority = (\n  a: Event,\n  b: Event,\n  comparator?: (a: Event, b: Event) => number\n): number => {\n  const aRange = getAllDayRangeMetrics(a);\n  const bRange = getAllDayRangeMetrics(b);\n\n  if (aRange.isMultiDay !== bRange.isMultiDay) {\n    return aRange.isMultiDay ? -1 : 1;\n  }\n\n  if (aRange.isMultiDay && bRange.isMultiDay) {\n    const startDiff = aRange.startDay.getTime() - bRange.startDay.getTime();\n    if (startDiff !== 0) {\n      return startDiff;\n    }\n\n    const endDiff = bRange.endDay.getTime() - aRange.endDay.getTime();\n    if (endDiff !== 0) {\n      return endDiff;\n    }\n  }\n\n  if (comparator) {\n    const comparatorResult = comparator(a, b);\n    if (comparatorResult !== 0) {\n      return comparatorResult;\n    }\n  }\n\n  return aRange.startDay.getTime() - bRange.startDay.getTime();\n};\n\nexport const createAllDayDisplayComparator = (\n  events: Event[],\n  comparator?: (a: Event, b: Event) => number\n) => {\n  const calendarGroupMeta = new Map<\n    string | undefined,\n    {\n      hasMultiDay: boolean;\n      earliestMultiDayStart: number;\n    }\n  >();\n\n  events.forEach(event => {\n    const { isMultiDay, startDay } = getAllDayRangeMetrics(event);\n    const existing = calendarGroupMeta.get(event.calendarId);\n\n    if (!existing) {\n      calendarGroupMeta.set(event.calendarId, {\n        hasMultiDay: isMultiDay,\n        earliestMultiDayStart: isMultiDay\n          ? startDay.getTime()\n          : Number.POSITIVE_INFINITY,\n      });\n      return;\n    }\n\n    if (isMultiDay) {\n      existing.hasMultiDay = true;\n      existing.earliestMultiDayStart = Math.min(\n        existing.earliestMultiDayStart,\n        startDay.getTime()\n      );\n    }\n  });\n\n  return (a: Event, b: Event) => {\n    const aMeta = calendarGroupMeta.get(a.calendarId);\n    const bMeta = calendarGroupMeta.get(b.calendarId);\n\n    if ((aMeta?.hasMultiDay ?? false) !== (bMeta?.hasMultiDay ?? false)) {\n      return aMeta?.hasMultiDay ? -1 : 1;\n    }\n\n    if (\n      (aMeta?.hasMultiDay ?? false) &&\n      (bMeta?.hasMultiDay ?? false) &&\n      a.calendarId !== b.calendarId\n    ) {\n      const groupStartDiff =\n        (aMeta?.earliestMultiDayStart ?? Number.POSITIVE_INFINITY) -\n        (bMeta?.earliestMultiDayStart ?? Number.POSITIVE_INFINITY);\n      if (groupStartDiff !== 0) {\n        return groupStartDiff;\n      }\n    }\n\n    if (a.calendarId === b.calendarId) {\n      return compareAllDayDisplayPriority(a, b, comparator);\n    }\n\n    return compareAllDayDisplayPriority(a, b, comparator);\n  };\n};\n"
  },
  {
    "path": "packages/core/src/utils/calendarApp/configSync.ts",
    "content": "import type { CalendarAppConfig } from '@/types';\nimport { isDeepEqual } from '@/utils/helpers';\n\nexport type SyncableCalendarAppConfig = Pick<\n  CalendarAppConfig,\n  | 'allDaySortComparator'\n  | 'calendars'\n  | 'locale'\n  | 'readOnly'\n  | 'switcherMode'\n  | 'theme'\n  | 'timeZone'\n  | 'useCalendarHeader'\n  | 'useEventDetailDialog'\n  | 'useEventDetailPanel'\n  | 'views'\n>;\n\nexport type CalendarAppConfigSyncSnapshot = {\n  callbacks: CalendarAppConfig['callbacks'];\n  syncableConfig: SyncableCalendarAppConfig;\n};\n\ntype CalendarAppConfigUpdater = {\n  updateConfig: (config: Partial<CalendarAppConfig>) => void;\n};\n\nexport function pickSyncableConfig(\n  config: CalendarAppConfig\n): SyncableCalendarAppConfig {\n  return {\n    allDaySortComparator: config.allDaySortComparator,\n    calendars: config.calendars,\n    locale: config.locale,\n    readOnly: config.readOnly,\n    switcherMode: config.switcherMode,\n    theme: config.theme,\n    timeZone: config.timeZone,\n    useCalendarHeader: config.useCalendarHeader,\n    useEventDetailDialog: config.useEventDetailDialog,\n    useEventDetailPanel: config.useEventDetailPanel,\n    views: config.views,\n  };\n}\n\nexport function createConfigSyncSnapshot(\n  config: CalendarAppConfig\n): CalendarAppConfigSyncSnapshot {\n  return {\n    callbacks: config.callbacks,\n    syncableConfig: pickSyncableConfig(config),\n  };\n}\n\nexport function getCallbackConfigUpdate(\n  previous: CalendarAppConfig['callbacks'],\n  next: CalendarAppConfig['callbacks']\n): Pick<CalendarAppConfig, 'callbacks'> | null {\n  if (previous === next) {\n    return null;\n  }\n\n  return { callbacks: next };\n}\n\nexport function getSyncConfigUpdates(\n  previous: SyncableCalendarAppConfig,\n  next: SyncableCalendarAppConfig\n): Partial<CalendarAppConfig> {\n  const updates: Partial<CalendarAppConfig> = {};\n\n  if (previous.views !== next.views) updates.views = next.views;\n  if (!isDeepEqual(previous.calendars, next.calendars)) {\n    updates.calendars = next.calendars;\n  }\n  if (!isDeepEqual(previous.theme, next.theme)) updates.theme = next.theme;\n  if (previous.useEventDetailDialog !== next.useEventDetailDialog) {\n    updates.useEventDetailDialog = next.useEventDetailDialog;\n  }\n  if (previous.useEventDetailPanel !== next.useEventDetailPanel) {\n    updates.useEventDetailPanel = next.useEventDetailPanel;\n  }\n  if (previous.useCalendarHeader !== next.useCalendarHeader) {\n    updates.useCalendarHeader = next.useCalendarHeader;\n  }\n  if (previous.switcherMode !== next.switcherMode) {\n    updates.switcherMode = next.switcherMode;\n  }\n  if (!isDeepEqual(previous.locale, next.locale)) updates.locale = next.locale;\n  if (!isDeepEqual(previous.readOnly, next.readOnly)) {\n    updates.readOnly = next.readOnly;\n  }\n  if (previous.allDaySortComparator !== next.allDaySortComparator) {\n    updates.allDaySortComparator = next.allDaySortComparator;\n  }\n  if (previous.timeZone !== next.timeZone) updates.timeZone = next.timeZone;\n\n  return updates;\n}\n\nexport function syncCalendarAppConfig(\n  app: CalendarAppConfigUpdater,\n  previousSnapshot: CalendarAppConfigSyncSnapshot,\n  config: CalendarAppConfig\n): CalendarAppConfigSyncSnapshot {\n  const nextSnapshot = createConfigSyncSnapshot(config);\n  const callbackUpdate = getCallbackConfigUpdate(\n    previousSnapshot.callbacks,\n    nextSnapshot.callbacks\n  );\n\n  if (callbackUpdate) {\n    app.updateConfig(callbackUpdate);\n  }\n\n  const syncUpdates = getSyncConfigUpdates(\n    previousSnapshot.syncableConfig,\n    nextSnapshot.syncableConfig\n  );\n\n  if (Object.keys(syncUpdates).length > 0) {\n    app.updateConfig(syncUpdates);\n  }\n\n  return nextSnapshot;\n}\n"
  },
  {
    "path": "packages/core/src/utils/calendarApp/index.ts",
    "content": "export * from './configSync';\nexport * from './normalizedConfig';\nexport * from './viewConfigComparison';\n"
  },
  {
    "path": "packages/core/src/utils/calendarApp/normalizedConfig.ts",
    "content": "import type { AllDaySortComparator, CalendarAppConfig } from '@/types';\n\ntype CalendarAppConfigGetter = () => CalendarAppConfig;\n\nexport function createNormalizedCalendarAppConfigGetter(\n  getConfig: CalendarAppConfigGetter\n): () => CalendarAppConfig {\n  let allDaySortComparator: AllDaySortComparator | undefined =\n    getConfig().allDaySortComparator;\n\n  const stableAllDaySortComparator: AllDaySortComparator = (a, b) =>\n    allDaySortComparator?.(a, b) ?? 0;\n\n  return () => {\n    const config = getConfig();\n    allDaySortComparator = config.allDaySortComparator;\n\n    return {\n      ...config,\n      allDaySortComparator: config.allDaySortComparator\n        ? stableAllDaySortComparator\n        : undefined,\n    };\n  };\n}\n"
  },
  {
    "path": "packages/core/src/utils/calendarApp/viewConfigComparison.ts",
    "content": "import type { CalendarView } from '@/types';\n\nexport type ViewConfigComparison = {\n  hasChanges: boolean;\n  requiresRender: boolean;\n};\n\nconst unchangedViewConfig: ViewConfigComparison = {\n  hasChanges: false,\n  requiresRender: false,\n};\n\nconst compareViewConfigValues = (\n  previous: unknown,\n  next: unknown\n): ViewConfigComparison => {\n  if (previous === next) return unchangedViewConfig;\n\n  const previousType = typeof previous;\n  const nextType = typeof next;\n\n  if (previousType === 'function' || nextType === 'function') {\n    if (previousType === 'function' && nextType === 'function') {\n      return {\n        hasChanges: true,\n        // Function references can change because the host rerendered. Update the\n        // stored config, but do not force a calendar rerender for that alone.\n        requiresRender: false,\n      };\n    }\n\n    return {\n      hasChanges: true,\n      requiresRender: true,\n    };\n  }\n\n  if (previous instanceof Date && next instanceof Date) {\n    const changed = previous.getTime() !== next.getTime();\n    return {\n      hasChanges: changed,\n      requiresRender: changed,\n    };\n  }\n\n  if (Array.isArray(previous) || Array.isArray(next)) {\n    if (!Array.isArray(previous) || !Array.isArray(next)) {\n      return {\n        hasChanges: true,\n        requiresRender: true,\n      };\n    }\n\n    if (previous.length !== next.length) {\n      return {\n        hasChanges: true,\n        requiresRender: true,\n      };\n    }\n\n    let hasChanges = false;\n    let requiresRender = false;\n\n    previous.forEach((value, index) => {\n      const diff = compareViewConfigValues(value, next[index]);\n      hasChanges = hasChanges || diff.hasChanges;\n      requiresRender = requiresRender || diff.requiresRender;\n    });\n\n    return { hasChanges, requiresRender };\n  }\n\n  if (\n    previousType !== 'object' ||\n    previous === null ||\n    nextType !== 'object' ||\n    next === null\n  ) {\n    return {\n      hasChanges: true,\n      requiresRender: true,\n    };\n  }\n\n  if (Object.getPrototypeOf(previous) !== Object.getPrototypeOf(next)) {\n    return {\n      hasChanges: true,\n      requiresRender: true,\n    };\n  }\n\n  const previousRecord = previous as Record<string, unknown>;\n  const nextRecord = next as Record<string, unknown>;\n  const previousKeys = Object.keys(previousRecord);\n  const nextKeys = Object.keys(nextRecord);\n\n  if (previousKeys.length !== nextKeys.length) {\n    return {\n      hasChanges: true,\n      requiresRender: true,\n    };\n  }\n\n  let hasChanges = false;\n  let requiresRender = false;\n\n  for (const key of previousKeys) {\n    if (!nextKeys.includes(key)) {\n      return {\n        hasChanges: true,\n        requiresRender: true,\n      };\n    }\n\n    const diff = compareViewConfigValues(previousRecord[key], nextRecord[key]);\n    hasChanges = hasChanges || diff.hasChanges;\n    requiresRender = requiresRender || diff.requiresRender;\n  }\n\n  return { hasChanges, requiresRender };\n};\n\nexport const compareViews = (\n  previousView: CalendarView | undefined,\n  nextView: CalendarView\n): ViewConfigComparison => {\n  if (!previousView) {\n    return {\n      hasChanges: true,\n      requiresRender: true,\n    };\n  }\n\n  if (\n    previousView.component !== nextView.component ||\n    previousView.label !== nextView.label\n  ) {\n    return {\n      hasChanges: true,\n      requiresRender: true,\n    };\n  }\n\n  return compareViewConfigValues(previousView.config, nextView.config);\n};\n"
  },
  {
    "path": "packages/core/src/utils/calendarDataUtils.ts",
    "content": "/**\n * Calendar Data Generation Utilities\n *\n * This module provides utilities for generating calendar data structures\n * including days, weeks, and month/year metadata.\n */\n\nimport { DayData, WeeksData } from '@/types';\n\nimport { monthNames, shortMonthNames } from './dateConstants';\nimport { getWeekRange } from './dateRangeUtils';\n\n// ============================================================================\n// Calendar Data Generation Tools\n// ============================================================================\n\n/**\n * Generate day data object from a date\n * @param date Date to generate data for\n * @returns Day data with date, day, month, year, and isToday flag\n */\nexport const generateDayData = (date: string | number | Date) => {\n  const clonedDate = new Date(date);\n  const day = clonedDate.getDate();\n  const month = clonedDate.getMonth();\n  const year = clonedDate.getFullYear();\n  const today = new Date();\n\n  return {\n    date: clonedDate,\n    day,\n    month,\n    year,\n    monthName: monthNames[month],\n    shortMonthName: shortMonthNames[month],\n    isToday:\n      today.getDate() === day &&\n      today.getMonth() === month &&\n      today.getFullYear() === year,\n  };\n};\n\n/**\n * Determine which month and year a week belongs to (based on majority of days)\n * @param days Array of day data\n * @returns Month name, month index, and year\n */\nexport const getMonthYearOfWeek = (days: DayData[]) => {\n  // Count occurrences of each month\n  const monthCounts: Record<string, number> = {};\n  days.forEach((day: { month: number; year: number }) => {\n    const key = `${day.month}-${day.year}`;\n    monthCounts[key] = (monthCounts[key] || 0) + 1;\n  });\n\n  // Find the most frequent month\n  let maxCount = 0;\n  let dominantMonthYear = '';\n\n  Object.entries(monthCounts).forEach(([key, count]) => {\n    if (count > maxCount) {\n      maxCount = count;\n      dominantMonthYear = key;\n    }\n  });\n\n  const [monthIndex, year] = dominantMonthYear.split('-').map(Number);\n\n  return {\n    month: monthNames[monthIndex],\n    monthIndex,\n    year,\n  };\n};\n\n/**\n * Generate week data (7 days starting from given date)\n * @param startDate Week start date\n * @returns Week data with days array, startDate, and monthYear\n */\nexport const generateWeekData = (startDate: string | number | Date) => {\n  const week = [];\n  const startDateClone = new Date(startDate);\n\n  for (let i = 0; i < 7; i++) {\n    const date = new Date(startDateClone);\n    week.push(generateDayData(date));\n    startDateClone.setDate(startDateClone.getDate() + 1);\n  }\n\n  return {\n    days: week,\n    startDate: new Date(startDate),\n    monthYear: getMonthYearOfWeek(week),\n  };\n};\n\n/**\n * Generate weeks data around a central date\n * @param centralDate Central date for range\n * @param monthsToLoad Number of months to load (default: 3)\n * @returns Array of week data\n */\nexport const generateWeeksData = (centralDate: Date, monthsToLoad = 3) => {\n  // Calculate weeks to load (~4-5 weeks per month)\n  const weeksToLoad = monthsToLoad * 5;\n\n  const { monday: centralMonday } = getWeekRange(centralDate);\n  const weeks = [];\n\n  // Generate weeks before current week\n  const prevWeeks = Math.floor(weeksToLoad / 2);\n  const startDate = new Date(centralMonday);\n  startDate.setDate(startDate.getDate() - 7 * prevWeeks);\n\n  // Generate all weeks\n  for (let i = 0; i < weeksToLoad; i++) {\n    const weekStartDate = new Date(startDate);\n    weekStartDate.setDate(weekStartDate.getDate() + i * 7);\n    weeks.push(generateWeekData(weekStartDate));\n  }\n\n  return weeks;\n};\n\n/**\n * Generate week range around a center date\n * @param centerDate Center date for range\n * @param totalWeeks Total number of weeks to generate\n * @param startOfWeek Week start day (0: Sunday, 1: Monday, etc.)\n * @returns Array of weeks data\n */\nexport function generateWeekRange(\n  centerDate: Date,\n  totalWeeks: number,\n  startOfWeek: number = 1\n): WeeksData[] {\n  const weeks: WeeksData[] = [];\n  const startOffset = Math.floor(totalWeeks / 2);\n\n  // Find start of the week for the center date\n  const dayOfWeek = centerDate.getDay();\n  const diff = (dayOfWeek - startOfWeek + 7) % 7;\n  const centerWeekStart = new Date(centerDate);\n  centerWeekStart.setDate(centerDate.getDate() - diff);\n  centerWeekStart.setHours(0, 0, 0, 0);\n\n  // Calculate start week\n  const startWeek = new Date(centerWeekStart);\n  startWeek.setDate(startWeek.getDate() - startOffset * 7);\n\n  // Generate all weeks\n  for (let i = 0; i < totalWeeks; i++) {\n    const weekStart = new Date(startWeek);\n    weekStart.setDate(weekStart.getDate() + i * 7);\n    weeks.push(generateWeekData(weekStart));\n  }\n  return weeks;\n}\n"
  },
  {
    "path": "packages/core/src/utils/clipboardStore.ts",
    "content": "import { Event } from '@/types';\n\nclass ClipboardStore {\n  private lastCopiedEvent: Event | null = null;\n\n  setEvent(event: Event) {\n    this.lastCopiedEvent = event;\n  }\n\n  getEvent(): Event | null {\n    return this.lastCopiedEvent;\n  }\n\n  hasEvent(): boolean {\n    return this.lastCopiedEvent !== null;\n  }\n\n  clear() {\n    this.lastCopiedEvent = null;\n  }\n}\n\nexport const clipboardStore = new ClipboardStore();\n"
  },
  {
    "path": "packages/core/src/utils/colorUtils.ts",
    "content": "/**\n * Color Utilities\n *\n * This module provides utilities for resolving event colors using the calendar registry.\n * All color functions return actual color values (not CSS classes) for inline styles.\n */\n\nimport {\n  getDefaultCalendarRegistry,\n  CalendarRegistry,\n} from '@/core/calendarRegistry';\n\n// ============================================================================\n// Color Tools\n// ============================================================================\n\n/**\n * Get event background color (actual color value, not CSS class)\n * Use this for inline styles\n */\nexport const getEventBgColor = (\n  calendarIdOrColor: string,\n  registry?: CalendarRegistry\n): string => {\n  const reg = registry || getDefaultCalendarRegistry();\n  const colors = reg.resolveColors(calendarIdOrColor);\n  return colors.eventColor;\n};\n\n/**\n * Get event text color (actual color value, not CSS class)\n * Use this for inline styles\n */\nexport const getEventTextColor = (\n  calendarIdOrColor: string,\n  registry?: CalendarRegistry\n): string => {\n  const reg = registry || getDefaultCalendarRegistry();\n  const colors = reg.resolveColors(calendarIdOrColor);\n  return colors.textColor;\n};\n\n/**\n * Get selected background color\n * Now uses the calendar registry for color resolution\n */\nexport const getSelectedBgColor = (\n  calendarIdOrColor: string,\n  registry?: CalendarRegistry\n): string => {\n  const reg = registry || getDefaultCalendarRegistry();\n  const colors = reg.resolveColors(calendarIdOrColor);\n  return colors.eventSelectedColor;\n};\n\n/**\n * Get line color\n * Now uses the calendar registry for color resolution\n */\nexport const getLineColor = (\n  calendarIdOrColor: string,\n  registry?: CalendarRegistry\n): string => {\n  const reg = registry || getDefaultCalendarRegistry();\n  const colors = reg.resolveColors(calendarIdOrColor);\n  return colors.lineColor;\n};\n\n/**\n * Resolve the primary calendar ID for an event.\n * Uses calendarIds[0] when available, otherwise falls back to calendarId.\n */\nexport const getPrimaryCalendarId = (event: {\n  calendarId?: string;\n  calendarIds?: string[];\n}): string => (event.calendarIds?.[0] ?? event.calendarId) || 'blue';\n\n/**\n * Resolve all line colors for an event's calendar(s).\n * Returns an array with one entry per calendar ID.\n */\nexport const getCalendarLineColors = (\n  event: { calendarId?: string; calendarIds?: string[] },\n  registry?: CalendarRegistry\n): string[] => {\n  const reg = registry || getDefaultCalendarRegistry();\n  const ids =\n    event.calendarIds ?? (event.calendarId ? [event.calendarId] : ['blue']);\n  return ids.map(id => reg.getLineColor(id));\n};\n\n/**\n * Build a CSS gradient string for the multi-calendar color bar.\n * For a single color it returns the plain color value (no gradient).\n * For multiple colors it builds an equal-segment `linear-gradient(to bottom, ...)`.\n */\nexport const buildColorBarGradient = (colors: string[]): string => {\n  if (colors.length === 1) return colors[0];\n  const stops: string[] = [];\n  colors.forEach((color, i) => {\n    const pct0 = (i / colors.length) * 100;\n    const pct1 = ((i + 1) / colors.length) * 100;\n    stops.push(`${color} ${pct0}%`, `${color} ${pct1}%`);\n  });\n  return `linear-gradient(to bottom, ${stops.join(', ')})`;\n};\n\n/**\n * Build a 45° repeating stripe for narrow multi-calendar color bars.\n * Uses the same angle and stripe width as the event background pattern, but\n * accepts line colors so the left bar remains stronger than the event fill.\n */\nexport const buildDiagonalColorBarGradient = (\n  colors: string[],\n  stripeWidth = 6\n): string => {\n  if (colors.length === 1) return colors[0];\n  const stops: string[] = [];\n  colors.forEach((color, i) => {\n    const s = i * stripeWidth;\n    const e = (i + 1) * stripeWidth;\n    stops.push(`${color} ${s}px`, `${color} ${e}px`);\n  });\n  return `repeating-linear-gradient(-45deg, ${stops.join(', ')})`;\n};\n\n/**\n * Resolve all eventColor values for an event's calendar(s).\n * Used to build the diagonal stripe background.\n */\nexport const getCalendarEventBgColors = (\n  event: { calendarId?: string; calendarIds?: string[] },\n  registry?: CalendarRegistry\n): string[] => {\n  const reg = registry || getDefaultCalendarRegistry();\n  const ids =\n    event.calendarIds ?? (event.calendarId ? [event.calendarId] : ['blue']);\n  return ids.map(id => reg.resolveColors(id).eventColor);\n};\n\n/**\n * Build a 45° repeating diagonal stripe CSS background for multi-calendar events.\n * For a single color returns the plain color value (no pattern).\n * Each stripe is `stripeWidth` px wide (default 6px).\n */\nexport const buildDiagonalPatternBackground = (\n  colors: string[],\n  stripeWidth = 6\n): string => {\n  if (colors.length === 1) return colors[0];\n  const stops: string[] = [];\n  colors.forEach((color, i) => {\n    const s = i * stripeWidth;\n    const e = (i + 1) * stripeWidth;\n    stops.push(`${color} ${s}px`, `${color} ${e}px`);\n  });\n  return `repeating-linear-gradient(-45deg, ${stops.join(', ')})`;\n};\n"
  },
  {
    "path": "packages/core/src/utils/compareUtils.ts",
    "content": "/**\n * Performs a deep comparison between two values to determine if they are equivalent.\n * Supports primitives, Date objects, and plain objects/arrays.\n */\nexport function isDeepEqual(a: unknown, b: unknown): boolean {\n  if (a === b) return true;\n\n  if (a instanceof Date && b instanceof Date) {\n    return a.getTime() === b.getTime();\n  }\n\n  if (\n    typeof a !== 'object' ||\n    a === null ||\n    typeof b !== 'object' ||\n    b === null\n  ) {\n    return false;\n  }\n\n  if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {\n    return false;\n  }\n\n  const keysA = Object.keys(a);\n  const keysB = Object.keys(b);\n\n  if (keysA.length !== keysB.length) {\n    return false;\n  }\n\n  const objA = a as Record<string, unknown>;\n  const objB = b as Record<string, unknown>;\n\n  for (const key of keysA) {\n    if (!keysB.includes(key) || !isDeepEqual(objA[key], objB[key])) {\n      return false;\n    }\n  }\n\n  return true;\n}\n"
  },
  {
    "path": "packages/core/src/utils/crossRegionDrag.ts",
    "content": "interface RestoreTimedDragOptions {\n  wasOriginallyAllDay: boolean;\n  mouseHour: number;\n  hourOffset: number | null | undefined;\n  duration: number;\n  originalStartHour: number;\n  originalEndHour: number;\n  firstHour: number;\n  lastHour: number;\n  minDuration: number;\n  roundToTimeStep: (hour: number) => number;\n}\n\ninterface RestoredTimedDragState {\n  startHour: number;\n  endHour: number;\n  duration: number;\n  hourOffset: number;\n}\n\nexport const restoreTimedDragFromAllDayTransition = ({\n  wasOriginallyAllDay,\n  mouseHour,\n  hourOffset,\n  duration,\n  originalStartHour,\n  originalEndHour,\n  firstHour,\n  lastHour,\n  minDuration,\n  roundToTimeStep,\n}: RestoreTimedDragOptions): RestoredTimedDragState => {\n  const restoredDuration = wasOriginallyAllDay\n    ? 1\n    : Math.max(\n        minDuration,\n        duration || originalEndHour - originalStartHour || 1\n      );\n\n  let restoredStartHour = wasOriginallyAllDay\n    ? roundToTimeStep(mouseHour)\n    : roundToTimeStep(mouseHour + (hourOffset ?? 0));\n\n  restoredStartHour = Math.max(\n    firstHour,\n    Math.min(lastHour - restoredDuration, restoredStartHour)\n  );\n\n  return {\n    startHour: restoredStartHour,\n    endHour: restoredStartHour + restoredDuration,\n    duration: restoredDuration,\n    hourOffset: wasOriginallyAllDay ? 0 : (hourOffset ?? 0),\n  };\n};\n"
  },
  {
    "path": "packages/core/src/utils/dateConstants.ts",
    "content": "/**\n * Date Constants\n *\n * This module provides constant arrays for day and month names in various formats.\n * Used throughout the application for displaying date information.\n */\n\n// ============================================================================\n// Date Constants\n// ============================================================================\n\n/**\n * Week day abbreviations (Mon-Sun)\n */\nexport const weekDays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\n\n/**\n * Full week day names (Monday-Sunday)\n */\nexport const weekDaysFullName = [\n  'Monday',\n  'Tuesday',\n  'Wednesday',\n  'Thursday',\n  'Friday',\n  'Saturday',\n  'Sunday',\n];\n\n/**\n * Full month names (January-December)\n */\nexport const monthNames = [\n  'January',\n  'February',\n  'March',\n  'April',\n  'May',\n  'June',\n  'July',\n  'August',\n  'September',\n  'October',\n  'November',\n  'December',\n];\n\n/**\n * Month abbreviations (Jan-Dec)\n */\nexport const shortMonthNames = [\n  'Jan',\n  'Feb',\n  'Mar',\n  'Apr',\n  'May',\n  'Jun',\n  'Jul',\n  'Aug',\n  'Sep',\n  'Oct',\n  'Nov',\n  'Dec',\n];\n"
  },
  {
    "path": "packages/core/src/utils/dateFormat.ts",
    "content": "// Date formatting utility functions\n// Ensures consistent server and client rendering, avoiding hydration errors\n\nimport { Temporal } from 'temporal-polyfill';\n\nimport { isPlainDate, isZonedDateTime } from './temporal';\n\n/**\n * Format date to DD/MM/YYYY format\n * Ensures consistent server and client rendering\n */\nexport const formatDateConsistent = (\n  date?:\n    | Date\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n): string => {\n  if (!date) return '';\n\n  let day: number, month: number, year: number;\n\n  if (date instanceof Date) {\n    year = date.getFullYear();\n    month = date.getMonth() + 1;\n    day = date.getDate();\n  } else if (isPlainDate(date)) {\n    year = date.year;\n    month = date.month;\n    day = date.day;\n  } else if (isZonedDateTime(date)) {\n    year = date.year;\n    month = date.month;\n    day = date.day;\n  } else {\n    // PlainDateTime\n    year = date.year;\n    month = date.month;\n    day = date.day;\n  }\n\n  const monthStr = String(month).padStart(2, '0');\n  const dayStr = String(day).padStart(2, '0');\n  return `${dayStr}/${monthStr}/${year}`;\n};\n\n/**\n * Format month and year to consistent format\n */\nexport const formatMonthYear = (date: Date): string => {\n  const months = [\n    'January',\n    'February',\n    'March',\n    'April',\n    'May',\n    'June',\n    'July',\n    'August',\n    'September',\n    'October',\n    'November',\n    'December',\n  ];\n  return `${months[date.getMonth()]} ${date.getFullYear()}`;\n};\n\n/**\n * Format date to \"DD MMM YYYY\" format (e.g., \"15 Jan 2025\")\n * Used for event detail display\n */\nexport const formatDate = (\n  temporal: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime\n): string => {\n  const months = [\n    'Jan',\n    'Feb',\n    'Mar',\n    'Apr',\n    'May',\n    'Jun',\n    'Jul',\n    'Aug',\n    'Sep',\n    'Oct',\n    'Nov',\n    'Dec',\n  ];\n  // All Temporal types have year, month, day properties\n  const year = temporal.year;\n  const month = temporal.month;\n  const day = temporal.day;\n  return `${day} ${months[month - 1]} ${year}`;\n};\n"
  },
  {
    "path": "packages/core/src/utils/dateRangeUtils.ts",
    "content": "/**\n * Date Range Utilities\n *\n * This module provides utilities for calculating date ranges, particularly\n * for week-based operations (Monday-Sunday).\n */\n\nimport { weekDays } from './dateConstants';\n\n// ============================================================================\n// Date Range Utilities\n// ============================================================================\n\n/**\n * Get the week range for a given date\n * @param date Input date\n * @param startOfWeek Week start day (0: Sunday, 1: Monday, etc.)\n * @returns Object with monday and sunday dates (monday and sunday here are just start/end of week)\n */\nexport const getWeekRange = (date: Date, startOfWeek: number = 1) => {\n  const day = date.getDay();\n  // (day - startOfWeek + 7) % 7 gives how many days since the start of the week\n  const diff = (day - startOfWeek + 7) % 7;\n  const start = new Date(date);\n  start.setDate(date.getDate() - diff);\n  start.setHours(0, 0, 0, 0);\n\n  const end = new Date(start);\n  end.setDate(start.getDate() + 6);\n  end.setHours(23, 59, 59, 999);\n\n  return { monday: start, sunday: end }; // Keep property names for compatibility or refactor\n};\n\n/**\n * Get current week dates with today indicator\n * @param startOfWeek Week start day (0: Sun, 1: Mon, etc.)\n * @returns Array of 7 date objects with date, month, and isToday flag\n */\nexport const getCurrentWeekDates = (startOfWeek: number = 1) => {\n  const currentDate = new Date();\n  const today = new Date();\n  const day = currentDate.getDay();\n  const diff = (day - startOfWeek + 7) % 7;\n\n  return weekDays.map((_, index) => {\n    const date = new Date(currentDate);\n    date.setDate(currentDate.getDate() - diff + index);\n    return {\n      date: date.getDate(),\n      month: date.toLocaleString('default', { month: 'short' }),\n      isToday:\n        date.getDate() === today.getDate() &&\n        date.getMonth() === today.getMonth() &&\n        date.getFullYear() === today.getFullYear(),\n    };\n  });\n};\n"
  },
  {
    "path": "packages/core/src/utils/dateTimeUtils.ts",
    "content": "/**\n * Date and Time Conversion Utilities\n *\n * This module provides utilities for converting between Date and Temporal API objects,\n * extracting time components, and performing date comparisons.\n *\n * All functions support both legacy Date objects and modern Temporal API types\n * for backward compatibility and future-proofing.\n */\n\nimport { Temporal } from 'temporal-polyfill';\n\nimport {\n  extractHourFromTemporal,\n  createTemporalWithHour,\n  getStartOfTemporal,\n  getEndOfTemporal,\n  isPlainDate,\n  isSamePlainDate,\n} from './temporal';\n\n// ============================================================================\n// Date/Time Conversion Tools (Event-specific - Temporal Compatible)\n// ============================================================================\n\n/**\n * Extract hour number from Date or Temporal object (with decimal for minutes, e.g., 14.5 = 14:30)\n * @param dateTime Date or Temporal object\n * @returns Hour number (0-24, supports decimals)\n */\nexport const extractHourFromDate = (\n  dateTime:\n    | Date\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n): number => {\n  if (dateTime instanceof Date) {\n    const hours = dateTime.getHours();\n    const minutes = dateTime.getMinutes();\n    return hours + minutes / 60;\n  }\n  return extractHourFromTemporal(dateTime);\n};\n\n/**\n * Create a new date-time object based on given date but set to specified hour\n * @param baseDateTime Base date-time\n * @param hour Hour number (supports decimals, e.g., 14.5 = 14:30)\n * @returns Date or Temporal (PlainDateTime or ZonedDateTime)\n */\nexport const createDateWithHour = (\n  baseDateTime:\n    | Date\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime,\n  hour: number\n): Date | Temporal.PlainDateTime | Temporal.ZonedDateTime => {\n  if (baseDateTime instanceof Date) {\n    const newDate = new Date(baseDateTime);\n    const hours = Math.floor(hour);\n    const minutes = Math.round((hour - hours) * 60);\n    newDate.setHours(hours, minutes, 0, 0);\n    return newDate;\n  }\n  return createTemporalWithHour(baseDateTime, hour);\n};\n\n/**\n * Get start of day (00:00:00.000)\n * @param dateTime Date-time\n * @returns Start of day\n */\nexport const getStartOfDay = (\n  dateTime:\n    | Date\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n): Date | Temporal.ZonedDateTime => {\n  if (dateTime instanceof Date) {\n    const newDate = new Date(dateTime);\n    newDate.setHours(0, 0, 0, 0);\n    return newDate;\n  }\n  return getStartOfTemporal(dateTime);\n};\n\n/**\n * Get end of day (23:59:59.999)\n * @param dateTime Date-time\n * @returns End of day\n */\nexport const getEndOfDay = (\n  dateTime:\n    | Date\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n): Date | Temporal.ZonedDateTime => {\n  if (dateTime instanceof Date) {\n    const newDate = new Date(dateTime);\n    newDate.setHours(23, 59, 59, 999);\n    return newDate;\n  }\n  return getEndOfTemporal(dateTime);\n};\n\n/**\n * Get the ISO week number for a given date\n * @param date Date object\n * @returns Week number (1-53)\n */\nexport const getWeekNumber = (date: Date): number => {\n  const d = new Date(\n    Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())\n  );\n  const dayNum = d.getUTCDay() || 7;\n  d.setUTCDate(d.getUTCDate() + 4 - dayNum);\n  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));\n  return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);\n};\n\n/**\n * Check if two dates are on the same day\n * @param date1 Date 1\n * @param date2 Date 2\n * @returns Whether they are the same day\n */\nexport const isSameDay = (\n  date1:\n    | Date\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime,\n  date2:\n    | Date\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n): boolean => {\n  if (date1 instanceof Date && date2 instanceof Date) {\n    return (\n      date1.getFullYear() === date2.getFullYear() &&\n      date1.getMonth() === date2.getMonth() &&\n      date1.getDate() === date2.getDate()\n    );\n  }\n\n  const temporal1 =\n    date1 instanceof Date\n      ? Temporal.PlainDate.from({\n          year: date1.getFullYear(),\n          month: date1.getMonth() + 1,\n          day: date1.getDate(),\n        })\n      : isPlainDate(date1)\n        ? date1\n        : date1.toPlainDate();\n\n  const temporal2 =\n    date2 instanceof Date\n      ? Temporal.PlainDate.from({\n          year: date2.getFullYear(),\n          month: date2.getMonth() + 1,\n          day: date2.getDate(),\n        })\n      : isPlainDate(date2)\n        ? date2\n        : date2.toPlainDate();\n\n  return isSamePlainDate(temporal1, temporal2);\n};\n\n/**\n * Check if event spans multiple days\n * @param start Start time\n * @param end End time\n * @returns Whether it's a multi-day event\n */\nexport const isMultiDayEvent = (\n  start:\n    | Date\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime,\n  end:\n    | Date\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n): boolean => !isSameDay(start, end);\n"
  },
  {
    "path": "packages/core/src/utils/eventHelpers.ts",
    "content": "/**\n * Event Helper Functions\n *\n * This module provides simplified APIs for creating events.\n * Provides multiple layers of abstraction:\n * - Simple API: createSimpleEvent() - for local events without timezone complexity\n * - Advanced API: createEventWithTimeZone() - for timezone-aware events\n * - Direct API: Users can still use Temporal API directly for full control\n */\n\nimport { Temporal } from 'temporal-polyfill';\n\nimport { Event } from '@/types';\n\nimport {\n  dateToPlainDate,\n  dateToPlainDateTime,\n  dateToZonedDateTime,\n} from './temporalTypeGuards';\n\n// ============================================================================\n// Type Definitions\n// ============================================================================\n\n/**\n * Event creation parameters - supports both Date and Temporal types\n * For local events (no timezone)\n */\nexport interface CreateEventParams {\n  id: string;\n  title: string;\n  description?: string;\n\n  // Flexible time input - accepts Date or Temporal types\n  // - Date: Will be converted to PlainDateTime (for timed events) or PlainDate (for allDay events)\n  // - Temporal.PlainDate: All-day events (date only)\n  // - Temporal.PlainDateTime: Local events with time (date+time, no timezone) ✨ Recommended default\n  start: Date | Temporal.PlainDate | Temporal.PlainDateTime;\n  end: Date | Temporal.PlainDate | Temporal.PlainDateTime;\n\n  // Event properties\n  allDay?: boolean;\n  calendarId?: string;\n  meta?: Record<string, unknown>;\n}\n\nexport type CreateAllDayEventDateInput = Date | Temporal.PlainDate;\n\nexport interface CreateAllDayEventParams extends Omit<\n  CreateEventParams,\n  'start' | 'end' | 'allDay'\n> {\n  start: CreateAllDayEventDateInput;\n  end?: CreateAllDayEventDateInput;\n}\n\n/**\n * Timezone event creation parameters\n * For events that need explicit timezone handling\n */\nexport interface CreateTimezoneEventParams {\n  id: string;\n  title: string;\n  description?: string;\n\n  // Flexible time input - accepts Date or ZonedDateTime\n  // - Date: Will be converted to ZonedDateTime using the specified timezone\n  // - Temporal.ZonedDateTime: Timezone-aware events (date+time+timezone)\n  start: Date | Temporal.ZonedDateTime;\n  end: Date | Temporal.ZonedDateTime;\n\n  // Required timezone for Date conversion\n  // Only used when start/end are Date objects\n  timeZone: string; // e.g., 'America/New_York', 'Asia/Shanghai'\n\n  // Event properties\n  calendarId?: string;\n  meta?: Record<string, unknown>;\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Convert input to Temporal type for local events\n */\nfunction normalizeLocalTime(\n  time: Date | Temporal.PlainDate | Temporal.PlainDateTime,\n  allDay: boolean = false\n): Temporal.PlainDate | Temporal.PlainDateTime {\n  // Already Temporal type - return as is\n  if (\n    time instanceof Temporal.PlainDate ||\n    time instanceof Temporal.PlainDateTime\n  ) {\n    return time;\n  }\n\n  // Date object - convert based on allDay flag\n  if (time instanceof Date) {\n    return allDay ? dateToPlainDate(time) : dateToPlainDateTime(time);\n  }\n\n  throw new Error(`Invalid time type: ${typeof time}`);\n}\n\nfunction isAllDayDateInput(\n  value: unknown\n): value is CreateAllDayEventDateInput {\n  return value instanceof Date || value instanceof Temporal.PlainDate;\n}\n\n/**\n * Convert input to ZonedDateTime\n */\nfunction normalizeZonedTime(\n  time: Date | Temporal.ZonedDateTime,\n  timeZone: string\n): Temporal.ZonedDateTime {\n  // Already ZonedDateTime - return as is\n  if (time instanceof Temporal.ZonedDateTime) {\n    return time;\n  }\n\n  // Date object - convert to ZonedDateTime\n  if (time instanceof Date) {\n    return dateToZonedDateTime(time, timeZone);\n  }\n\n  throw new Error(`Invalid time type: ${typeof time}`);\n}\n\n// ============================================================================\n// Event Creation Functions\n// ============================================================================\n\n/**\n * Create local event (recommended for most use cases)\n *\n * Supports flexible input types:\n * - Date objects (automatically converted)\n * - Temporal.PlainDate (for all-day events)\n * - Temporal.PlainDateTime (for timed events)\n *\n * @example\n * // Using Date objects\n * createEvent({\n *   id: '1',\n *   title: 'Team Meeting',\n *   start: new Date(2025, 0, 15, 14, 30),\n *   end: new Date(2025, 0, 15, 16, 0),\n * });\n *\n * @example\n * // Using Temporal.PlainDateTime\n * createEvent({\n *   id: '2',\n *   title: 'Workshop',\n *   start: Temporal.PlainDateTime.from('2025-01-15T09:00'),\n *   end: Temporal.PlainDateTime.from('2025-01-15T17:00'),\n * });\n *\n * @example\n * // All-day event\n * createEvent({\n *   id: '3',\n *   title: 'Birthday',\n *   start: new Date(2025, 0, 15),\n *   end: new Date(2025, 0, 15),\n *   allDay: true,\n * });\n */\nexport function createEvent(params: CreateEventParams): Event {\n  const start = normalizeLocalTime(params.start, params.allDay);\n  const end = normalizeLocalTime(params.end, params.allDay);\n\n  return {\n    id: params.id,\n    title: params.title,\n    description: params.description,\n    start,\n    end,\n    allDay: params.allDay ?? false,\n    calendarId: params.calendarId,\n    meta: params.meta,\n  };\n}\n\n// ============================================================================\n// Timezone Event Creation\n// ============================================================================\n\n/**\n * Create timezone-aware event\n *\n * Use this when you need explicit timezone control, such as:\n * - International meetings across timezones\n * - Flight schedules\n * - Events that need to show in different timezones\n *\n * Supports flexible input types:\n * - Date objects (converted using specified timezone)\n * - Temporal.ZonedDateTime (used directly)\n *\n * @example\n * // Using Date objects\n * createTimezoneEvent({\n *   id: '1',\n *   title: 'International Conference',\n *   start: new Date(2025, 0, 15, 14, 0),\n *   end: new Date(2025, 0, 15, 16, 0),\n *   timeZone: 'America/New_York'\n * });\n *\n * @example\n * // Using ZonedDateTime\n * createTimezoneEvent({\n *   id: '2',\n *   title: 'Asia-US Sync',\n *   start: Temporal.ZonedDateTime.from('2025-01-15T09:00[Asia/Shanghai]'),\n *   end: Temporal.ZonedDateTime.from('2025-01-15T10:00[Asia/Shanghai]'),\n *   timeZone: 'Asia/Shanghai', // Only used if start/end are Date objects\n * });\n */\nexport function createTimezoneEvent(params: CreateTimezoneEventParams): Event {\n  const start = normalizeZonedTime(params.start, params.timeZone);\n  const end = normalizeZonedTime(params.end, params.timeZone);\n\n  return {\n    id: params.id,\n    title: params.title,\n    description: params.description,\n    start,\n    end,\n    allDay: false, // Timezone events are always timed events\n    calendarId: params.calendarId,\n    meta: params.meta,\n  };\n}\n\n// ============================================================================\n// Batch Creation Helpers\n// ============================================================================\n\n/**\n * Create multiple local events at once\n */\nexport function createEvents(paramsArray: CreateEventParams[]): Event[] {\n  return paramsArray.map(params => createEvent(params));\n}\n\n/**\n * Create multiple timezone-aware events at once\n */\nexport function createTimezoneEvents(\n  paramsArray: CreateTimezoneEventParams[]\n): Event[] {\n  return paramsArray.map(params => createTimezoneEvent(params));\n}\n\n// ============================================================================\n// Quick Creation Shortcuts\n// ============================================================================\n\n/**\n * Quick create all-day event.\n *\n * Preferred API:\n * createAllDayEvent({\n *   id: '1',\n *   title: 'Conference',\n *   start: new Date(2025, 0, 15),\n *   end: new Date(2025, 0, 17),\n *   calendarId: 'work',\n * });\n *\n * Legacy positional signature is still supported for backward compatibility:\n * createAllDayEvent('1', 'Conference', new Date(2025, 0, 15));\n */\nexport function createAllDayEvent(params: CreateAllDayEventParams): Event;\nexport function createAllDayEvent(\n  id: string,\n  title: string,\n  start: CreateAllDayEventDateInput,\n  end?: CreateAllDayEventDateInput,\n  calendarId?: string\n): Event;\nexport function createAllDayEvent(\n  id: string,\n  title: string,\n  start: CreateAllDayEventDateInput,\n  options?: Omit<CreateAllDayEventParams, 'id' | 'title' | 'start'>\n): Event;\nexport function createAllDayEvent(\n  paramsOrId: CreateAllDayEventParams | string,\n  title?: string,\n  start?: CreateAllDayEventDateInput,\n  endOrOptions?:\n    | CreateAllDayEventDateInput\n    | Omit<CreateAllDayEventParams, 'id' | 'title' | 'start'>,\n  legacyCalendarId?: string\n): Event {\n  if (typeof paramsOrId === 'object' && paramsOrId !== null) {\n    const params = paramsOrId;\n    return createEvent({\n      ...params,\n      start: params.start,\n      end: params.end ?? params.start,\n      allDay: true,\n    });\n  }\n\n  if (!title || !start) {\n    throw new Error(\n      'createAllDayEvent requires either a params object or id, title, and start arguments'\n    );\n  }\n\n  const legacyOptions =\n    endOrOptions && !isAllDayDateInput(endOrOptions) ? endOrOptions : undefined;\n  const endDate =\n    endOrOptions && isAllDayDateInput(endOrOptions) ? endOrOptions : start;\n  const calendarId = legacyOptions?.calendarId ?? legacyCalendarId;\n\n  return createEvent({\n    id: paramsOrId,\n    title,\n    start,\n    end: endDate,\n    allDay: true,\n    description: legacyOptions?.description,\n    calendarId,\n    meta: legacyOptions?.meta,\n  });\n}\n"
  },
  {
    "path": "packages/core/src/utils/eventUtils.ts",
    "content": "/**\n * Event Utilities\n *\n * This module provides utilities for event operations including:\n * - Filtering events by day and type\n * - Calculating day indices based on week start dates\n * - Creating and updating events with proper date/time handling\n * - Converting between Date and Temporal API objects\n */\n\nimport { Temporal } from 'temporal-polyfill';\n\nimport { Event } from '@/types';\n\nimport { temporalToDate } from './temporal';\n\n// ============================================================================\n// Event Tools\n// ============================================================================\n\n/**\n * Get regular (non-all-day) events for a specific day index\n * @param dayIndex Day index (0-6, Monday-Sunday)\n * @param events Array of events\n * @returns Filtered events\n */\nexport const getEventsForDay = (dayIndex: number, events: Event[]) =>\n  events.filter(event => event.day === dayIndex && !event.allDay);\n\n/**\n * Get all-day events for a specific day index\n * Supports date range checking when weekStart is provided\n * @param dayIndex Day index (0-6, Monday-Sunday)\n * @param events Array of events\n * @param weekStart Optional week start date for accurate date range checking\n * @returns Filtered all-day events\n */\nexport const getAllDayEventsForDay = (\n  dayIndex: number,\n  events: Event[],\n  weekStart?: Date\n) =>\n  events.filter(event => {\n    if (!event.allDay) return false;\n\n    // If no weekStart provided, use simple logic (backward compatibility)\n    if (!weekStart) {\n      return event.day === dayIndex;\n    }\n\n    // Calculate actual date for current dayIndex\n    const currentDate = new Date(weekStart);\n    currentDate.setDate(weekStart.getDate() + dayIndex);\n    currentDate.setHours(0, 0, 0, 0);\n\n    // Get event's start and end dates (normalized to 00:00:00)\n    const eventStartDate = temporalToDate(event.start);\n    eventStartDate.setHours(0, 0, 0, 0);\n\n    const eventEndDate = temporalToDate(event.end);\n    eventEndDate.setHours(0, 0, 0, 0);\n\n    // Check if current date is within event's date range (inclusive)\n    return currentDate >= eventStartDate && currentDate <= eventEndDate;\n  });\n\n/**\n * Get Date object for a specific day index relative to week start\n * @param weekStart Week start date\n * @param dayIndex Day index (0-6)\n * @returns Date object\n */\nexport const getDateByDayIndex = (weekStart: Date, dayIndex: number): Date => {\n  const date = new Date(weekStart);\n  date.setDate(weekStart.getDate() + dayIndex);\n  return date;\n};\n\n/**\n * Update event's date and dayIndex\n * @param event Event to update\n * @param newDayIndex New day index\n * @param weekStart Week start date\n * @returns Updated event\n */\nexport const updateEventDateAndDay = (\n  event: Event,\n  newDayIndex: number,\n  weekStart: Date\n): Event => {\n  const newDate = getDateByDayIndex(weekStart, newDayIndex);\n  return {\n    ...event,\n    day: newDayIndex,\n    start: event.allDay\n      ? Temporal.PlainDate.from({\n          year: newDate.getFullYear(),\n          month: newDate.getMonth() + 1,\n          day: newDate.getDate(),\n        })\n      : Temporal.ZonedDateTime.from({\n          year: newDate.getFullYear(),\n          month: newDate.getMonth() + 1,\n          day: newDate.getDate(),\n          hour: newDate.getHours(),\n          minute: newDate.getMinutes(),\n          timeZone: Temporal.Now.timeZoneId(),\n        }),\n    end: event.allDay\n      ? Temporal.PlainDate.from({\n          year: newDate.getFullYear(),\n          month: newDate.getMonth() + 1,\n          day: newDate.getDate(),\n        })\n      : Temporal.ZonedDateTime.from({\n          year: newDate.getFullYear(),\n          month: newDate.getMonth() + 1,\n          day: newDate.getDate(),\n          hour: newDate.getHours(),\n          minute: newDate.getMinutes(),\n          timeZone: Temporal.Now.timeZoneId(),\n        }),\n  };\n};\n\n/**\n * Convert Date to Temporal (PlainDate or ZonedDateTime based on allDay flag)\n * @param date Date object\n * @param allDay Whether it's an all-day event\n * @returns Temporal object\n */\nconst dateToTemporal = (\n  date: Date,\n  allDay: boolean\n): Temporal.PlainDate | Temporal.ZonedDateTime => {\n  if (allDay) {\n    return Temporal.PlainDate.from({\n      year: date.getFullYear(),\n      month: date.getMonth() + 1,\n      day: date.getDate(),\n    });\n  }\n  return Temporal.ZonedDateTime.from({\n    year: date.getFullYear(),\n    month: date.getMonth() + 1,\n    day: date.getDate(),\n    hour: date.getHours(),\n    minute: date.getMinutes(),\n    second: date.getSeconds(),\n    millisecond: date.getMilliseconds(),\n    timeZone: Temporal.Now.timeZoneId(),\n  });\n};\n\n/**\n * Create new event with Date fields set\n * @param eventData Event data without start/end\n * @param weekStart Week start date\n * @returns Complete event with Temporal start/end\n */\nexport const createEventWithDate = (\n  eventData: Omit<Event, 'start' | 'end'>,\n  weekStart: Date\n): Event => {\n  const eventDate = getDateByDayIndex(weekStart, eventData.day ?? 0);\n  const allDay = eventData.allDay ?? false;\n  return {\n    ...eventData,\n    start: dateToTemporal(eventDate, allDay),\n    end: dateToTemporal(eventDate, allDay),\n  };\n};\n\n/**\n * Calculate day index based on real date and current week start\n * @param eventDate Event date\n * @param weekStart Week start date\n * @returns Day index (can be outside 0-6 range for events outside current week)\n */\nexport const calculateDayIndex = (eventDate: Date, weekStart: Date): number => {\n  // Get event date start time (00:00:00)\n  const eventDateStart = new Date(eventDate);\n  eventDateStart.setHours(0, 0, 0, 0);\n\n  // Get week start date start time (00:00:00)\n  const weekStartCopy = new Date(weekStart);\n  weekStartCopy.setHours(0, 0, 0, 0);\n  const diffTime = eventDateStart.getTime() - weekStartCopy.getTime();\n\n  return Math.floor(diffTime / (1000 * 60 * 60 * 24)); // Note: Not limited to 0-6 range, as events can be outside current week\n};\n\n/**\n * Check if event is within current week range\n * @param eventDate Event date\n * @param weekStart Week start date\n * @returns Whether event is in the week\n */\nexport const isEventInWeek = (eventDate: Date, weekStart: Date): boolean => {\n  const dayIndex = calculateDayIndex(eventDate, weekStart);\n\n  return dayIndex >= 0 && dayIndex <= 6;\n};\n\n/**\n * Recalculate day field for all events (based on current week)\n * @param events Array of events\n * @param weekStart Week start date\n * @returns Events with updated day indices\n */\nexport const recalculateEventDays = (\n  events: Event[],\n  weekStart: Date\n): Event[] =>\n  events.map(event => {\n    const eventDate = temporalToDate(event.start);\n    const newDay = calculateDayIndex(eventDate, weekStart);\n\n    return {\n      ...event,\n      day: newDay,\n    };\n  });\n\n/**\n * Get day index by Date (relative to week start)\n * @param weekStart Week start date\n * @param targetDate Target date\n * @returns Day index\n */\nexport const getDayIndexByDate = (\n  weekStart: Date,\n  targetDate: Date\n): number => {\n  // Ensure both are start of day for comparison\n  const weekStartCopy = new Date(weekStart);\n  weekStartCopy.setHours(0, 0, 0, 0);\n\n  const targetDateCopy = new Date(targetDate);\n  targetDateCopy.setHours(0, 0, 0, 0);\n\n  const diffTime = targetDateCopy.getTime() - weekStartCopy.getTime();\n\n  // Don't limit return value, return actual day difference\n  return Math.floor(diffTime / (1000 * 60 * 60 * 24));\n};\n\n/**\n * Get events within specified week range\n * @param events Array of events\n * @param weekStart Week start date\n * @returns Filtered and recalculated events\n */\nexport const getEventsForWeek = (events: Event[], weekStart: Date): Event[] => {\n  const weekEnd = new Date(weekStart);\n  weekEnd.setDate(weekStart.getDate() + 6);\n  weekEnd.setHours(23, 59, 59, 999);\n\n  return events\n    .filter(event => {\n      const eventDate = temporalToDate(event.start);\n      return eventDate >= weekStart && eventDate <= weekEnd;\n    })\n    .map(event => {\n      // Recalculate day based on event's real date relative to current week start\n      const dayIndex = calculateDayIndex(\n        temporalToDate(event.start),\n        weekStart\n      );\n      return {\n        ...event,\n        day: dayIndex,\n      };\n    });\n};\n\n/**\n * Create event with real date\n * @param eventData Event data without start/end\n * @param weekStart Week start date\n * @returns Complete event\n */\nexport const createEventWithRealDate = (\n  eventData: Omit<Event, 'start' | 'end'>,\n  weekStart: Date\n): Event => {\n  const eventDate = new Date(weekStart);\n  eventDate.setDate(weekStart.getDate() + (eventData.day ?? 0));\n  const allDay = eventData.allDay ?? false;\n  return {\n    ...eventData,\n    start: dateToTemporal(eventDate, allDay),\n    end: dateToTemporal(eventDate, allDay),\n  };\n};\n\n/**\n * Update event with real date\n * @param event Event to update\n * @param newDayIndex New day index\n * @param weekStart Week start date\n * @returns Updated event\n */\nexport const updateEventWithRealDate = (\n  event: Event,\n  newDayIndex: number,\n  weekStart: Date\n): Event => {\n  const newDate = new Date(weekStart);\n  newDate.setDate(weekStart.getDate() + newDayIndex);\n  const allDay = event.allDay ?? false;\n  return {\n    ...event,\n    day: newDayIndex,\n    start: dateToTemporal(newDate, allDay),\n    end: dateToTemporal(newDate, allDay),\n  };\n};\n\n// ============================================================================\n// New Helper Functions (Support PlainDateTime)\n// ============================================================================\n\n/**\n * Create event with PlainDateTime (default for local events)\n * This is the recommended function for creating events without timezone complexity\n */\nexport const createEventWithPlainDateTime = (\n  eventData: Omit<Event, 'start' | 'end'>,\n  weekStart: Date\n): Event => {\n  const eventDate = getDateByDayIndex(weekStart, eventData.day ?? 0);\n  const allDay = eventData.allDay ?? false;\n\n  if (allDay) {\n    const plainDate = Temporal.PlainDate.from({\n      year: eventDate.getFullYear(),\n      month: eventDate.getMonth() + 1,\n      day: eventDate.getDate(),\n    });\n    return {\n      ...eventData,\n      start: plainDate,\n      end: plainDate,\n    };\n  }\n\n  // Use PlainDateTime for timed events (no timezone)\n  const plainDateTime = Temporal.PlainDateTime.from({\n    year: eventDate.getFullYear(),\n    month: eventDate.getMonth() + 1,\n    day: eventDate.getDate(),\n    hour: eventDate.getHours(),\n    minute: eventDate.getMinutes(),\n    second: eventDate.getSeconds(),\n    millisecond: eventDate.getMilliseconds(),\n  });\n\n  return {\n    ...eventData,\n    start: plainDateTime,\n    end: plainDateTime,\n  };\n};\n\n/**\n * Create event with ZonedDateTime (for timezone-aware events)\n * Use when explicit timezone control is needed\n */\nexport const createEventWithZonedDateTime = (\n  eventData: Omit<Event, 'start' | 'end'>,\n  weekStart: Date,\n  timeZone: string\n): Event => {\n  const eventDate = getDateByDayIndex(weekStart, eventData.day ?? 0);\n  const allDay = eventData.allDay ?? false;\n\n  if (allDay) {\n    const plainDate = Temporal.PlainDate.from({\n      year: eventDate.getFullYear(),\n      month: eventDate.getMonth() + 1,\n      day: eventDate.getDate(),\n    });\n    return {\n      ...eventData,\n      start: plainDate,\n      end: plainDate,\n    };\n  }\n\n  // Use ZonedDateTime with explicit timezone\n  const zonedDateTime = Temporal.ZonedDateTime.from({\n    year: eventDate.getFullYear(),\n    month: eventDate.getMonth() + 1,\n    day: eventDate.getDate(),\n    hour: eventDate.getHours(),\n    minute: eventDate.getMinutes(),\n    second: eventDate.getSeconds(),\n    millisecond: eventDate.getMilliseconds(),\n    timeZone: timeZone,\n  });\n\n  return {\n    ...eventData,\n    start: zonedDateTime,\n    end: zonedDateTime,\n  };\n};\n\n/**\n * Compare two events for equality\n * @param event1 First event\n * @param event2 Second event\n * @returns Whether events are equal in content\n */\nexport const isEventDeepEqual = (\n  event1: Event | null,\n  event2: Event | null\n): boolean => {\n  if (event1 === event2) return true;\n  if (!event1 || !event2) return false;\n\n  return (\n    event1.title === event2.title &&\n    event1.calendarId === event2.calendarId &&\n    (event1.description || '') === (event2.description || '') &&\n    !!event1.allDay === !!event2.allDay &&\n    event1.start.toString() === event2.start.toString() &&\n    event1.end.toString() === event2.end.toString()\n  );\n};\n\n/**\n * Check if event's primary fields (start, end, title) have changed.\n * This is commonly used to determine if an event update should be persisted\n * after a drag or resize operation.\n *\n * @param oldEvent Original event\n * @param newEvent Updated event\n * @returns Whether primary fields have changed\n */\nexport const hasEventChanged = (oldEvent: Event, newEvent: Event): boolean =>\n  temporalToDate(oldEvent.start).getTime() !==\n    temporalToDate(newEvent.start).getTime() ||\n  temporalToDate(oldEvent.end).getTime() !==\n    temporalToDate(newEvent.end).getTime() ||\n  oldEvent.title !== newEvent.title ||\n  oldEvent.day !== newEvent.day ||\n  !!oldEvent.allDay !== !!newEvent.allDay;\n"
  },
  {
    "path": "packages/core/src/utils/helpers.ts",
    "content": "/**\n * Helpers Module - Backward Compatibility Exports\n *\n * This file has been refactored into multiple specialized modules for better organization.\n * All functions are re-exported here for backward compatibility with existing code.\n *\n * New code should import directly from the specific utility modules:\n * - dateTimeUtils.ts - Date/time conversion and comparison functions\n * - colorUtils.ts - Color resolution functions\n * - timeUtils.ts - Time formatting and calculations\n * - dateConstants.ts - Day and month name constants\n * - dateRangeUtils.ts - Week range calculations\n * - calendarDataUtils.ts - Calendar data generation\n * - eventUtils.ts - Event filtering and manipulation\n * - testDataUtils.ts - Test/demo data generation\n * - utilityFunctions.ts - General utility functions\n */\n\n// Re-export Date/Time utilities\nexport {\n  extractHourFromDate,\n  createDateWithHour,\n  getStartOfDay,\n  getEndOfDay,\n  isSameDay,\n  isMultiDayEvent,\n} from './dateTimeUtils';\n\n// Re-export Color utilities\nexport {\n  getEventBgColor,\n  getEventTextColor,\n  getSelectedBgColor,\n  getLineColor,\n  getPrimaryCalendarId,\n  getCalendarLineColors,\n  buildColorBarGradient,\n  buildDiagonalColorBarGradient,\n  getCalendarEventBgColors,\n  buildDiagonalPatternBackground,\n} from './colorUtils';\n\n// Re-export Time utilities\nexport {\n  TIME_STEP,\n  formatTime,\n  formatEventTimeRange,\n  roundToTimeStep,\n  getEventEndHour,\n  generateSecondaryTimeSlots,\n  getTimezoneDisplayLabel,\n} from './timeUtils';\n\n// Re-export Date constants\nexport {\n  weekDays,\n  weekDaysFullName,\n  monthNames,\n  shortMonthNames,\n} from './dateConstants';\n\n// Re-export Date range utilities\nexport { getWeekRange, getCurrentWeekDates } from './dateRangeUtils';\n\n// Re-export Calendar data generation utilities\nexport {\n  generateDayData,\n  generateWeekData,\n  getMonthYearOfWeek,\n  generateWeeksData,\n  generateWeekRange,\n} from './calendarDataUtils';\n\n// Re-export Event utilities\nexport {\n  getEventsForDay,\n  getAllDayEventsForDay,\n  getDateByDayIndex,\n  updateEventDateAndDay,\n  createEventWithDate,\n  calculateDayIndex,\n  isEventInWeek,\n  recalculateEventDays,\n  getDayIndexByDate,\n  getEventsForWeek,\n  createEventWithRealDate,\n  updateEventWithRealDate,\n  isEventDeepEqual,\n} from './eventUtils';\n\n// Re-export Test data utilities\nexport { getTestEvents } from './testDataUtils';\n\n// Re-export General utilities\nexport { generateUniKey } from './utilityFunctions';\nexport { isDeepEqual } from './compareUtils';\n"
  },
  {
    "path": "packages/core/src/utils/ics/__tests__/ics.test.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { Event } from '@/types/event';\nimport { generateICS } from '@/utils/ics/icsGenerator';\nimport { parseICS } from '@/utils/ics/icsParser';\n\ndescribe('ICS Utilities', () => {\n  const mockDate = Temporal.PlainDateTime.from('2025-01-15T10:00:00');\n  const mockEndDate = Temporal.PlainDateTime.from('2025-01-15T12:00:00');\n\n  const mockEvent: Event = {\n    id: 'test-event-1',\n    title: 'Test Event',\n    description: 'This is a test event',\n    start: mockDate,\n    end: mockEndDate,\n    allDay: false,\n    meta: {\n      location: 'Test Location',\n      categories: ['Work', 'Meeting'],\n    },\n  };\n\n  const simpleICS = `BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//DayFlow//DayFlow Calendar//EN\nBEGIN:VEVENT\nUID:test-uid-123\nDTSTAMP:20250101T000000Z\nDTSTART:20250115T100000\nDTEND:20250115T120000\nSUMMARY:Test Event\nDESCRIPTION:This is a test event\nLOCATION:Test Location\nCATEGORIES:Work,Meeting\nEND:VEVENT\nEND:VCALENDAR`;\n\n  describe('parseICS', () => {\n    it('should parse a simple ICS string correctly', () => {\n      const result = parseICS(simpleICS, { generateNewIds: false });\n\n      expect(result.success).toBe(true);\n      expect(result.events.length).toBe(1);\n\n      const event = result.events[0];\n      expect(event.title).toBe('Test Event');\n      expect(event.description).toBe('This is a test event');\n      expect(event.meta?.location).toBe('Test Location');\n      expect(event.meta?.categories).toEqual(['Work', 'Meeting']);\n      expect(event.meta?.originalUid).toBe('test-uid-123');\n\n      // Check dates (converted to ZonedDateTime in parser)\n      // The parser uses local/default TZ for floating times.\n      // We can check if fields match.\n      expect(event.start.year).toBe(2025);\n      expect(event.start.month).toBe(1);\n      expect(event.start.day).toBe(15);\n      expect((event.start as Temporal.ZonedDateTime).hour).toBe(10);\n    });\n\n    it('should handle all-day events', () => {\n      const allDayICS = `BEGIN:VCALENDAR\nBEGIN:VEVENT\nUID:all-day-1\nDTSTART;VALUE=DATE:20250115\nSUMMARY:All Day Event\nEND:VEVENT\nEND:VCALENDAR`;\n\n      const result = parseICS(allDayICS);\n      expect(result.events[0].allDay).toBe(true);\n      expect(result.events[0].start instanceof Temporal.PlainDate).toBe(true);\n      expect((result.events[0].start as Temporal.PlainDate).day).toBe(15);\n    });\n  });\n\n  describe('generateICS', () => {\n    it('should generate ICS string containing event details', () => {\n      const ics = generateICS([mockEvent]);\n\n      expect(ics).toContain('BEGIN:VCALENDAR');\n      expect(ics).toContain('BEGIN:VEVENT');\n      expect(ics).toContain('SUMMARY:Test Event');\n      expect(ics).toContain('DESCRIPTION:This is a test event');\n      expect(ics).toContain('LOCATION:Test Location');\n      expect(ics).toContain('CATEGORIES:Work,Meeting'); // Escaped comma\n      expect(ics).toContain('END:VCALENDAR');\n    });\n  });\n\n  describe('Round Trip', () => {\n    it('should preserve event data after generate -> parse', () => {\n      // Generate\n      const generatedICS = generateICS([mockEvent]);\n\n      // Parse back\n      const result = parseICS(generatedICS, { generateNewIds: false });\n\n      expect(result.success).toBe(true);\n      expect(result.events.length).toBe(1);\n\n      const parsedEvent = result.events[0];\n\n      expect(parsedEvent.title).toBe(mockEvent.title);\n      expect(parsedEvent.description).toBe(mockEvent.description);\n      expect(parsedEvent.meta?.location).toBe(mockEvent.meta?.location);\n\n      // Date comparison might need care due to types (PlainDateTime vs ZonedDateTime)\n      // But values should match\n      expect(parsedEvent.start.year).toBe(mockEvent.start.year);\n      expect((parsedEvent.start as unknown as { minute: number }).minute).toBe(\n        (mockEvent.start as unknown as { minute: number }).minute\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/src/utils/ics/icsGenerator.ts",
    "content": "/**\n * ICS Generator\n *\n * Generates iCalendar (.ics) files from DayFlow Event objects.\n * Supports standard RFC 5545 components.\n */\n\nimport { Event } from '@/types/event';\n\nimport { ICSExportOptions } from './types';\nimport { generateVEvent, escapeICSValue } from './utils';\n\n/**\n * Generate ICS content string from events\n *\n * @param events - List of DayFlow events to export\n * @param options - Export options\n * @returns ICS file content string\n */\nexport function generateICS(\n  events: Event[],\n  options: ICSExportOptions = {}\n): string {\n  const {\n    calendarName = 'DayFlow Calendar',\n    productId = '-//DayFlow//DayFlow Calendar//EN',\n  } = options;\n\n  const lines: string[] = [\n    'BEGIN:VCALENDAR',\n    'VERSION:2.0',\n    `PRODID:${productId}`,\n    'CALSCALE:GREGORIAN',\n    'METHOD:PUBLISH',\n    `X-WR-CALNAME:${escapeICSValue(calendarName)}`,\n  ];\n\n  // 2. VEVENTs\n  events.forEach(event => {\n    lines.push(...generateVEvent(event));\n  });\n\n  // 3. Footer\n  lines.push('END:VCALENDAR');\n\n  // Join with CRLF (RFC 5545 standard)\n  return lines.join('\\r\\n');\n}\n\n/**\n * Trigger download of ICS file (Browser only)\n *\n * @param events - Events to export\n * @param options - Export options\n */\nexport function downloadICS(\n  events: Event[],\n  options: ICSExportOptions = {}\n): void {\n  const content = generateICS(events, options);\n  const filename = `${options.filename || 'calendar'}.ics`;\n\n  const blob = new Blob([content], { type: 'text/calendar;charset=utf-8' });\n  const url = URL.createObjectURL(blob);\n\n  const link = document.createElement('a');\n  link.href = url;\n  link.setAttribute('download', filename);\n  document.body.append(link);\n  link.click();\n  link.remove();\n  URL.revokeObjectURL(url);\n}\n"
  },
  {
    "path": "packages/core/src/utils/ics/icsParser.ts",
    "content": "/**\n * ICS Parser\n *\n * Parses iCalendar (.ics) format into DayFlow Event objects.\n * Supports standard RFC 5545 VEVENT components.\n */\n\nimport { ICSImportOptions, ICSImportResult } from './types';\nimport {\n  parseVEventLines,\n  convertToDayFlowEvent,\n  extractVEvents,\n} from './utils';\n\n/**\n * Main function to parse ICS content string\n *\n * @param icsContent - Raw string content of the .ics file\n * @param options - Import options\n * @returns Result object containing success flag, events, and errors\n */\nexport function parseICS(\n  icsContent: string,\n  options: ICSImportOptions = {}\n): ICSImportResult {\n  const result: ICSImportResult = {\n    success: false,\n    events: [],\n    errors: [],\n    totalParsed: 0,\n    totalImported: 0,\n  };\n\n  try {\n    // 1. Unfold lines (handle split lines starting with space/tab)\n    const unfoldedContent = icsContent.replaceAll(/(\\r\\n|\\n|\\r)[ \\t]/g, '');\n\n    // 2. Split into lines and normalize line endings\n    const lines = unfoldedContent.split(/\\r\\n|\\n|\\r/);\n\n    // 3. Extract VEVENT blocks\n    const vevents = extractVEvents(lines);\n    result.totalParsed = vevents.length;\n\n    // 4. Parse each VEVENT\n    vevents.forEach((veventLines, index) => {\n      try {\n        const icsEvent = parseVEventLines(veventLines);\n        const dayflowEvent = convertToDayFlowEvent(icsEvent, options);\n        result.events.push(dayflowEvent);\n      } catch (e: unknown) {\n        result.errors.push({\n          line: 0,\n          message: e instanceof Error ? e.message : 'Unknown parsing error',\n          eventUid: `index-${index}`,\n        });\n      }\n    });\n\n    result.success = result.errors.length === 0;\n    result.totalImported = result.events.length;\n  } catch (e: unknown) {\n    result.errors.push({\n      message: `Fatal parsing error: ${e instanceof Error ? e.message : 'Unknown error'}`,\n    });\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "packages/core/src/utils/ics/index.ts",
    "content": "/**\n * ICS Utilities Module\n */\n\nimport { parseICS } from './icsParser';\nimport { ICSImportResult, ICSImportOptions } from './types';\n\nexport * from './types';\nexport * from './utils';\nexport * from './icsParser';\nexport * from './icsGenerator';\n\n/**\n * Import events from an ICS file object\n *\n * @param file - The File object (from input[type=\"file\"])\n * @param options - Import options\n * @returns Promise resolving to import result\n */\nexport async function importICSFile(\n  file: File,\n  options?: ICSImportOptions\n): Promise<ICSImportResult> {\n  try {\n    const content = await file.text();\n    if (!content) {\n      throw new Error('File content is empty');\n    }\n    const result = parseICS(content, options);\n    return result;\n  } catch (err) {\n    const message =\n      err instanceof Error\n        ? err.message\n        : typeof err === 'string'\n          ? err\n          : 'Failed to read file';\n    return {\n      success: false,\n      events: [],\n      errors: [{ message }],\n      totalParsed: 0,\n      totalImported: 0,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/core/src/utils/ics/types.ts",
    "content": "/**\n * ICS (iCalendar) Types\n *\n * Type definitions for ICS file import/export functionality.\n * Based on RFC 5545 iCalendar specification.\n */\n\nimport { Event } from '@/types/event';\n\n/**\n * ICS VEVENT raw data structure (intermediate format after parsing)\n */\nexport interface ICSVEvent {\n  /** Unique identifier */\n  uid: string;\n  /** Event summary/title */\n  summary: string;\n  /** Event description */\n  description?: string;\n  /** Start date/time in ICS format */\n  dtstart: string;\n  /** End date/time in ICS format */\n  dtend: string;\n  /** DTSTART parameters */\n  dtstartParams?: ICSDateParams;\n  /** DTEND parameters */\n  dtendParams?: ICSDateParams;\n  /** Event location */\n  location?: string;\n  /** Event categories */\n  categories?: string[];\n}\n\n/**\n * ICS date/time parameters\n */\nexport interface ICSDateParams {\n  /** Value type: DATE for all-day, DATE-TIME for timed events */\n  value?: 'DATE' | 'DATE-TIME';\n  /** Timezone identifier (e.g., \"America/New_York\") */\n  tzid?: string;\n}\n\n/**\n * ICS import options\n */\nexport interface ICSImportOptions {\n  /** Default calendar ID for imported events */\n  calendarId?: string;\n  /** Generate new IDs for imported events (default: true) */\n  generateNewIds?: boolean;\n  /** ID prefix for generated IDs (default: \"ics-\") */\n  idPrefix?: string;\n  /** Default timezone when ICS has no timezone info */\n  defaultTimeZone?: string;\n}\n\n/**\n * ICS export options\n */\nexport interface ICSExportOptions {\n  /** Calendar name in exported file */\n  calendarName?: string;\n  /** Product identifier */\n  productId?: string;\n  /** Include timezone information */\n  includeTimezone?: boolean;\n  /** Export filename (without extension) */\n  filename?: string;\n}\n\n/**\n * ICS import result\n */\nexport interface ICSImportResult {\n  /** Whether import completed without errors */\n  success: boolean;\n  /** Successfully imported events */\n  events: Event[];\n  /** Parse errors encountered */\n  errors: ICSParseError[];\n  /** Total VEVENTs found in file */\n  totalParsed: number;\n  /** Successfully imported event count */\n  totalImported: number;\n}\n\n/**\n * ICS parse error\n */\nexport interface ICSParseError {\n  /** Line number where error occurred */\n  line?: number;\n  /** Error message */\n  message: string;\n  /** UID of the event that failed (if available) */\n  eventUid?: string;\n}\n"
  },
  {
    "path": "packages/core/src/utils/ics/utils.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { Event } from '@/types/event';\nimport {\n  isPlainDate,\n  isPlainDateTime,\n  isZonedDateTime,\n} from '@/utils/temporalTypeGuards';\nimport { generateUniKey } from '@/utils/utilityFunctions';\n\nimport { ICSVEvent, ICSDateParams, ICSImportOptions } from './types';\n\n// ============================================================================\n// Date Utilities\n// ============================================================================\n\n/**\n * Pad number to 2 digits\n */\nexport function pad2(num: number | string): string {\n  return String(num).padStart(2, '0');\n}\n\n/**\n * Parse ICS date string to Temporal type\n *\n * @param dateStr - ICS date string\n * @param params - Date parameters (VALUE, TZID)\n * @param defaultTimeZone - Default timezone when none specified\n * @returns Temporal.PlainDate, PlainDateTime, or ZonedDateTime\n */\nexport function parseICSDate(\n  dateStr: string,\n  params?: ICSDateParams,\n  defaultTimeZone?: string\n): Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime {\n  const cleanStr = dateStr.trim();\n  const ICS_DATE_REGEX = /^(\\d{4})(\\d{2})(\\d{2})$/;\n  const ICS_DATETIME_REGEX = /^(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})$/;\n  const ICS_DATETIME_UTC_REGEX =\n    /^(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})Z$/;\n\n  // All-day event (DATE format)\n  if (params?.value === 'DATE' || ICS_DATE_REGEX.test(cleanStr)) {\n    const match = cleanStr.match(ICS_DATE_REGEX);\n    if (match) {\n      return Temporal.PlainDate.from({\n        year: Number.parseInt(match[1], 10),\n        month: Number.parseInt(match[2], 10),\n        day: Number.parseInt(match[3], 10),\n      });\n    }\n  }\n\n  // UTC time (ends with Z)\n  if (ICS_DATETIME_UTC_REGEX.test(cleanStr)) {\n    const match = cleanStr.match(ICS_DATETIME_UTC_REGEX);\n    if (match) {\n      const instant = Temporal.Instant.from(\n        `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}Z`\n      );\n      // Convert to local timezone or specified default\n      const tz = defaultTimeZone || Temporal.Now.timeZoneId();\n      return instant.toZonedDateTimeISO(tz);\n    }\n  }\n\n  // Local time (with or without timezone)\n  const match = cleanStr.match(ICS_DATETIME_REGEX);\n  if (match) {\n    const dateTime = {\n      year: Number.parseInt(match[1], 10),\n      month: Number.parseInt(match[2], 10),\n      day: Number.parseInt(match[3], 10),\n      hour: Number.parseInt(match[4], 10),\n      minute: Number.parseInt(match[5], 10),\n      second: Number.parseInt(match[6], 10),\n    };\n\n    // If timezone specified, return ZonedDateTime\n    if (params?.tzid) {\n      return Temporal.ZonedDateTime.from({\n        ...dateTime,\n        timeZone: params.tzid,\n      });\n    }\n\n    // Otherwise return PlainDateTime\n    return Temporal.PlainDateTime.from(dateTime);\n  }\n\n  throw new Error(`Invalid ICS date format: ${dateStr}`);\n}\n\n/**\n * Format Temporal type to ICS date string\n *\n * @param temporal - Temporal date/time object\n * @param allDay - Force all-day format\n * @returns Object with value and optional params\n */\nexport function formatICSDate(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime,\n  allDay: boolean = false\n): { value: string; params?: Record<string, string> } {\n  // All-day event\n  if (allDay || isPlainDate(temporal)) {\n    const pd = isPlainDate(temporal) ? temporal : temporal.toPlainDate();\n    return {\n      value: `${pd.year}${pad2(pd.month)}${pad2(pd.day)}`,\n      params: { VALUE: 'DATE' },\n    };\n  }\n\n  // ZonedDateTime - convert to UTC\n  if (isZonedDateTime(temporal)) {\n    const instant = temporal.toInstant();\n    const utc = instant.toZonedDateTimeISO('UTC');\n    return {\n      value: `${utc.year}${pad2(utc.month)}${pad2(utc.day)}T${pad2(utc.hour)}${pad2(utc.minute)}${pad2(utc.second)}Z`,\n    };\n  }\n\n  // PlainDateTime - local time (no timezone marker)\n  if (isPlainDateTime(temporal)) {\n    return {\n      value: `${temporal.year}${pad2(temporal.month)}${pad2(temporal.day)}T${pad2(temporal.hour)}${pad2(temporal.minute)}${pad2(temporal.second)}`,\n    };\n  }\n\n  throw new Error('Unsupported Temporal type');\n}\n\n/**\n * Format a Date to ICS timestamp (UTC format for DTSTAMP)\n */\nexport function formatDateToICSTimestamp(date: Date): string {\n  const year = date.getUTCFullYear();\n  const month = pad2(date.getUTCMonth() + 1);\n  const day = pad2(date.getUTCDate());\n  const hour = pad2(date.getUTCHours());\n  const minute = pad2(date.getUTCMinutes());\n  const second = pad2(date.getUTCSeconds());\n  return `${year}${month}${day}T${hour}${minute}${second}Z`;\n}\n\n// ============================================================================\n// String Utilities\n// ============================================================================\n\nexport function escapeICSValue(value: string): string {\n  if (!value) return '';\n  return value\n    .replaceAll('\\\\', '\\\\\\\\')\n    .replaceAll(';', '\\\\;')\n    .replaceAll(',', '\\\\,')\n    .replaceAll('\\n', '\\\\n');\n}\n\nexport function unescapeICSValue(value: string): string {\n  if (!value) return '';\n  return value\n    .replaceAll('\\\\,', ',')\n    .replaceAll('\\\\;', ';')\n    .replaceAll(/\\\\[nN]/g, '\\n')\n    .replaceAll('\\\\\\\\', '\\\\');\n}\n\nexport function foldLine(line: string): string {\n  if (line.length <= 75) return line;\n  const chunks = [];\n  let remaining = line;\n  chunks.push(remaining.slice(0, 75));\n  remaining = remaining.slice(75);\n  while (remaining.length > 0) {\n    chunks.push(' ' + remaining.slice(0, 74));\n    remaining = remaining.slice(74);\n  }\n  return chunks.join('\\r\\n');\n}\n\nexport function formatProperty(\n  name: string,\n  value: string,\n  params?: Record<string, string>\n): string {\n  let line = name;\n  if (params) {\n    Object.entries(params).forEach(([key, val]) => {\n      line += `;${key}=${val}`;\n    });\n  }\n  line += `:${value}`;\n  return foldLine(line);\n}\n\n// ============================================================================\n// Internal Logic\n// ============================================================================\n\nexport function generateVEvent(event: Event): string[] {\n  const lines: string[] = ['BEGIN:VEVENT'];\n  const uid = event.meta?.originalUid || `${event.id}@dayflow`;\n  lines.push(`UID:${uid}`);\n  lines.push(`DTSTAMP:${formatDateToICSTimestamp(new Date())}`);\n  const startICS = formatICSDate(event.start, event.allDay);\n  // For all-day events, ICS DTEND is exclusive (day after the last inclusive\n  // day). DayFlow stores inclusive end dates, so add 1 day on export.\n  let endICS: { value: string; params?: Record<string, string> };\n  if (event.allDay) {\n    const endDate = isPlainDate(event.end)\n      ? event.end\n      : (event.end as Temporal.ZonedDateTime).toPlainDate();\n    const exclusiveEnd = endDate.add({ days: 1 });\n    endICS = {\n      value: `${exclusiveEnd.year}${pad2(exclusiveEnd.month)}${pad2(exclusiveEnd.day)}`,\n      params: { VALUE: 'DATE' },\n    };\n  } else {\n    endICS = formatICSDate(event.end, false);\n  }\n  lines.push(formatProperty('DTSTART', startICS.value, startICS.params));\n  lines.push(formatProperty('DTEND', endICS.value, endICS.params));\n  lines.push(formatProperty('SUMMARY', escapeICSValue(event.title)));\n  if (event.description) {\n    lines.push(\n      formatProperty('DESCRIPTION', escapeICSValue(event.description))\n    );\n  }\n  if (event.meta?.location) {\n    lines.push(\n      formatProperty('LOCATION', escapeICSValue(event.meta.location as string))\n    );\n  }\n  if (event.meta?.categories && Array.isArray(event.meta.categories)) {\n    const cats = event.meta.categories.map(escapeICSValue).join(',');\n    lines.push(formatProperty('CATEGORIES', cats));\n  }\n  lines.push('END:VEVENT');\n  return lines;\n}\n\nexport function extractVEvents(lines: string[]): string[][] {\n  const vevents: string[][] = [];\n  let currentEventLines: string[] | null = null;\n  let inVEvent = false;\n  for (const line of lines) {\n    const trimmed = line.trim();\n    const upperLine = trimmed.toUpperCase();\n    if (upperLine.startsWith('BEGIN:VEVENT')) {\n      inVEvent = true;\n      currentEventLines = [];\n      continue;\n    }\n    if (upperLine.startsWith('END:VEVENT')) {\n      if (inVEvent && currentEventLines) {\n        vevents.push(currentEventLines);\n      }\n      inVEvent = false;\n      currentEventLines = null;\n      continue;\n    }\n    if (inVEvent && currentEventLines) {\n      currentEventLines.push(line);\n    }\n  }\n  return vevents;\n}\n\nexport function parseVEventLines(lines: string[]): ICSVEvent {\n  const event: Partial<ICSVEvent> = {};\n  for (const line of lines) {\n    const colonIndex = line.indexOf(':');\n    if (colonIndex === -1) continue;\n    const propertyPart = line.slice(0, colonIndex);\n    const value = line.slice(colonIndex + 1);\n    const [rawName, ...params] = propertyPart.split(';');\n    const name = rawName.trim().toUpperCase();\n    const paramObj: Record<string, string> = {};\n    params.forEach(p => {\n      const [key, val] = p.split('=');\n      if (key && val) {\n        paramObj[key.trim().toUpperCase()] = val.trim();\n      }\n    });\n    switch (name) {\n      case 'UID':\n        event.uid = value.trim();\n        break;\n      case 'SUMMARY':\n        event.summary = unescapeICSValue(value);\n        break;\n      case 'DESCRIPTION':\n        event.description = unescapeICSValue(value);\n        break;\n      case 'LOCATION':\n        event.location = unescapeICSValue(value);\n        break;\n      case 'DTSTART':\n        event.dtstart = value.trim();\n        event.dtstartParams = {\n          value: paramObj['VALUE'] as 'DATE' | 'DATE-TIME',\n          tzid: paramObj['TZID'],\n        };\n        break;\n      case 'DTEND':\n        event.dtend = value.trim();\n        event.dtendParams = {\n          value: paramObj['VALUE'] as 'DATE' | 'DATE-TIME',\n          tzid: paramObj['TZID'],\n        };\n        break;\n      case 'CATEGORIES':\n        event.categories = value\n          .split(',')\n          .map(c => unescapeICSValue(c.trim()));\n        break;\n      default:\n        break;\n    }\n  }\n  if (!event.dtstart) throw new Error('Missing DTSTART in VEVENT');\n  if (!event.uid) event.uid = generateUniKey();\n  return event as ICSVEvent;\n}\n\nexport function convertToDayFlowEvent(\n  icsEvent: ICSVEvent,\n  options: ICSImportOptions\n): Event {\n  const {\n    calendarId = 'default',\n    generateNewIds = true,\n    idPrefix = 'ics-',\n    defaultTimeZone,\n  } = options;\n  const id = generateNewIds ? `${idPrefix}${generateUniKey()}` : icsEvent.uid;\n  const startTemporal = parseICSDate(\n    icsEvent.dtstart,\n    icsEvent.dtstartParams,\n    defaultTimeZone\n  );\n  let endTemporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime;\n  if (icsEvent.dtend) {\n    endTemporal = parseICSDate(\n      icsEvent.dtend,\n      icsEvent.dtendParams,\n      defaultTimeZone\n    );\n  } else if (isPlainDate(startTemporal)) {\n    endTemporal = startTemporal.add({ days: 1 });\n  } else {\n    endTemporal = (startTemporal as Temporal.PlainDateTime).add({ hours: 1 });\n  }\n  const allDay =\n    icsEvent.dtstartParams?.value === 'DATE' || isPlainDate(startTemporal);\n  const tz = defaultTimeZone || Temporal.Now.timeZoneId();\n  let finalStart: Temporal.ZonedDateTime | Temporal.PlainDate;\n  let finalEnd: Temporal.ZonedDateTime | Temporal.PlainDate;\n  if (isPlainDate(startTemporal)) {\n    // All-day event: keep as PlainDate to preserve the all-day invariant\n    finalStart = startTemporal;\n  } else if (isPlainDateTime(startTemporal)) {\n    try {\n      finalStart = (startTemporal as Temporal.PlainDateTime).toZonedDateTime(\n        tz\n      );\n    } catch {\n      finalStart = Temporal.ZonedDateTime.from({\n        ...startTemporal,\n        timeZone: tz,\n      });\n    }\n  } else {\n    finalStart = startTemporal;\n  }\n  if (isPlainDate(endTemporal)) {\n    // RFC 5545: DTEND for all-day events is exclusive (the day after the last\n    // day). DayFlow uses inclusive end dates, so subtract 1 day.\n    // Keep as PlainDate to preserve the all-day invariant.\n    finalEnd = endTemporal.subtract({ days: 1 });\n  } else if (isPlainDateTime(endTemporal)) {\n    try {\n      finalEnd = (endTemporal as Temporal.PlainDateTime).toZonedDateTime(tz);\n    } catch {\n      finalEnd = Temporal.ZonedDateTime.from({ ...endTemporal, timeZone: tz });\n    }\n  } else {\n    finalEnd = endTemporal as Temporal.ZonedDateTime;\n  }\n  return {\n    id,\n    calendarId,\n    title: icsEvent.summary || '(No Title)',\n    description: icsEvent.description,\n    start: finalStart,\n    end: finalEnd,\n    allDay,\n    meta: {\n      location: icsEvent.location,\n      originalUid: icsEvent.uid,\n      categories: icsEvent.categories,\n    },\n  };\n}\n"
  },
  {
    "path": "packages/core/src/utils/index.ts",
    "content": "// Utils module entry file - Re-export all utility functions and constants\n\n// All common utility functions\nexport * from './helpers';\nexport * from './dateTimeUtils';\n\n// Date formatting (preserving original exports)\nexport * from './dateFormat';\n\n// Temporal API utility functions (using latest implementation from temporalTypeGuards)\nexport {\n  // Type guards\n  isPlainDate,\n  isPlainDateTime,\n  isZonedDateTime,\n  // Temporal to Date conversions\n  temporalToDate,\n  temporalToVisualDate,\n  temporalToVisualTemporal,\n  plainDateToDate,\n  plainDateTimeToDate,\n  // Date to Temporal conversions\n  dateToPlainDate,\n  dateToPlainDateTime,\n  dateToZonedDateTime,\n  // Utility functions\n  extractHourFromTemporal,\n  setHourInTemporal,\n  isSameTemporal,\n  getPlainDate,\n} from './temporalTypeGuards';\n\n// Export unique functions from temporal.ts\nexport {\n  isDate,\n  zonedDateTimeToDate,\n  createTemporalWithHour,\n  isSamePlainDate,\n  isMultiDayTemporalEvent,\n  getStartOfTemporal,\n  getEndOfTemporal,\n  daysBetween,\n  daysDifference,\n  addDays,\n  now,\n  today,\n} from './temporal';\n\n// Style utilities\nexport * from './styleUtils';\nexport * from './timeUtils';\n\n// Theme utilities\nexport * from './themeUtils';\n\n// Event creation helper functions\nexport * from './eventHelpers';\nexport * from './eventUtils';\n\n// Search utilities\nexport * from './searchUtils';\n\n// Clipboard store\nexport * from './clipboardStore';\n\n// ICS utilities\nexport * from './ics';\n\n// All-day sort presets\nexport * from './allDaySort';\n\n// Cross-region drag helpers\nexport * from './crossRegionDrag';\nexport * from './timeZoneUtils';\nexport * from './calendarApp';\n"
  },
  {
    "path": "packages/core/src/utils/logger.ts",
    "content": "/**\n * Logger utility for DayFlow calendar\n * Only logs in development mode\n */\n\ntype LogLevel = 'log' | 'warn' | 'error' | 'debug';\n\nclass Logger {\n  private isDevelopment: boolean;\n\n  constructor() {\n    this.isDevelopment =\n      (globalThis as unknown as { process?: { env?: { NODE_ENV?: string } } })\n        .process?.env?.NODE_ENV !== 'production';\n  }\n\n  private formatMessage(\n    level: LogLevel,\n    message: string,\n    ...args: unknown[]\n  ): void {\n    if (!this.isDevelopment) return;\n\n    const timestamp = new Date().toISOString();\n    const prefix = `[DayFlow ${level.toUpperCase()}] ${timestamp}:`;\n\n    switch (level) {\n      case 'log':\n        console.log(prefix, message, ...args);\n        break;\n      case 'warn':\n        console.warn(prefix, message, ...args);\n        break;\n      case 'error':\n        console.error(prefix, message, ...args);\n        break;\n      case 'debug':\n        console.debug(prefix, message, ...args);\n        break;\n      default:\n        break;\n    }\n  }\n\n  log(message: string, ...args: unknown[]): void {\n    this.formatMessage('log', message, ...args);\n  }\n\n  warn(message: string, ...args: unknown[]): void {\n    this.formatMessage('warn', message, ...args);\n  }\n\n  error(message: string, ...args: unknown[]): void {\n    this.formatMessage('error', message, ...args);\n  }\n\n  debug(message: string, ...args: unknown[]): void {\n    this.formatMessage('debug', message, ...args);\n  }\n}\n\nexport const logger = new Logger();\n"
  },
  {
    "path": "packages/core/src/utils/searchUtils.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { TranslationKey } from '@/locale/types';\nimport { CalendarSearchEvent } from '@/types/search';\n\nimport { temporalToDate } from './temporal';\n\n/**\n * Helper to get date object from event start\n * @param dateInput Date, string, or Temporal object\n * @returns Date object\n */\nexport const getDateObj = (dateInput: unknown): Date => {\n  try {\n    if (dateInput instanceof Date) return dateInput;\n    if (typeof dateInput === 'string') return new Date(dateInput);\n    return temporalToDate(\n      dateInput as\n        | Temporal.PlainDate\n        | Temporal.PlainDateTime\n        | Temporal.ZonedDateTime\n    );\n  } catch {\n    return new Date();\n  }\n};\n\n/**\n * Helper to normalize date (reset time to 00:00:00)\n * @param date Date object\n * @returns Normalized Date object\n */\nexport const normalizeDate = (date: Date): Date => {\n  const d = new Date(date);\n  d.setHours(0, 0, 0, 0);\n  return d;\n};\n\n/**\n * Helper to get header text and semantic tone for a date group in search results\n * @param groupDate The date of the group\n * @param today Reference today date (normalized)\n * @param locale Locale string\n * @param t Translation function\n * @returns Object with title and tone\n */\nexport const getSearchHeaderInfo = (\n  groupDate: Date,\n  today: Date,\n  locale: string,\n  t: (key: TranslationKey) => string\n): { title: string; tone: 'default' | 'today' | 'upcoming' } => {\n  const diffTime = groupDate.getTime() - today.getTime();\n  const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));\n\n  let title = '';\n  let tone: 'default' | 'today' | 'upcoming' = 'default';\n\n  if (diffDays === 0) {\n    // Today\n    title = t('today') || 'Today';\n    tone = 'today';\n  } else if (diffDays === 1 || diffDays === 2) {\n    // Tomorrow or Day after tomorrow\n    try {\n      const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });\n      const relative = rtf.format(diffDays, 'day');\n      title = relative.charAt(0).toUpperCase() + relative.slice(1);\n      tone = 'upcoming';\n    } catch {\n      title = groupDate.toLocaleDateString(locale, { weekday: 'long' });\n    }\n  } else {\n    // Others\n    title = groupDate.toLocaleDateString(locale, {\n      year: 'numeric',\n      month: 'long',\n      day: 'numeric',\n      weekday: 'long',\n    });\n  }\n\n  return { title, tone };\n};\n\n/**\n * Helper to group search results by date\n * @param results List of search events\n * @param today Reference today date (normalized)\n * @returns Array of grouped events\n */\nexport const groupSearchResults = (\n  results: CalendarSearchEvent[],\n  today: Date\n): Array<{ date: Date; events: CalendarSearchEvent[] }> => {\n  const groupsMap = new Map<\n    number,\n    { date: Date; events: CalendarSearchEvent[] }\n  >();\n\n  results.forEach(event => {\n    const dateObj = getDateObj(event.start);\n    const normalized = normalizeDate(dateObj);\n    const timeKey = normalized.getTime();\n\n    if (!groupsMap.has(timeKey)) {\n      groupsMap.set(timeKey, { date: normalized, events: [] });\n    }\n    groupsMap.get(timeKey)?.events.push(event);\n  });\n\n  // Ensure \"Today\" group exists\n  const todayKey = today.getTime();\n  if (!groupsMap.has(todayKey)) {\n    groupsMap.set(todayKey, { date: today, events: [] });\n  }\n\n  // Sort groups by time\n  const sortedGroups = Array.from(groupsMap.values()).toSorted(\n    (a, b) => a.date.getTime() - b.date.getTime()\n  );\n\n  return sortedGroups;\n};\n"
  },
  {
    "path": "packages/core/src/utils/styleUtils.ts",
    "content": "/**\n * Style Utility Functions\n *\n * This module provides utility functions for working with CSS styles.\n */\n\nconst DEFAULT_WIDTH = '240px';\n\n/**\n * Normalize a width value to a CSS string\n *\n * Converts numeric values to pixels and validates string values.\n * Returns a default width if the input is invalid.\n *\n * @param width - Width as number (pixels) or CSS string\n * @param defaultWidth - Default width to use if input is invalid (default: '240px')\n * @returns Normalized CSS width string\n *\n * @example\n * ```ts\n * normalizeCssWidth(300) // '300px'\n * normalizeCssWidth('20rem') // '20rem'\n * normalizeCssWidth('') // '240px'\n * normalizeCssWidth(undefined) // '240px'\n * ```\n */\nexport function normalizeCssWidth(\n  width?: number | string,\n  defaultWidth: string = DEFAULT_WIDTH\n): string {\n  if (typeof width === 'number') {\n    return `${width}px`;\n  }\n  if (typeof width === 'string' && width.trim().length > 0) {\n    return width;\n  }\n  return defaultWidth;\n}\n\nlet cachedScrollbarTakesSpace: boolean | null = null;\n\n/**\n * Check if the calendar's scrollbar takes up space in the layout.\n *\n * Tests inside a .df-calendar-container element so the library's scoped\n * scrollbar CSS applies (scrollbar-width: thin; ::-webkit-scrollbar { width: 2px }).\n * Also overrides any host-app CSS that hides scrollbars (e.g. display: none),\n * since we need to measure the actual rendered scrollbar width.\n *\n * @returns true if the calendar scrollbar takes space, false otherwise\n */\nexport function scrollbarTakesSpace(): boolean {\n  if (typeof document === 'undefined') return false;\n  if (cachedScrollbarTakesSpace !== null) return cachedScrollbarTakesSpace;\n\n  // Override host-app ::-webkit-scrollbar { display: none } so measurement is accurate\n  const styleEl = document.createElement('style');\n  styleEl.textContent =\n    '.df-calendar-container .__df_measure__::-webkit-scrollbar { display: block !important; }';\n  document.head.append(styleEl);\n\n  // Test inside .df-calendar-container so scoped scrollbar CSS applies\n  const container = document.createElement('div');\n  container.className = 'df-calendar-container';\n  container.style.cssText =\n    'position:absolute;top:-9999px;width:100px;height:100px;overflow:hidden';\n\n  const div = document.createElement('div');\n  div.className = '__df_measure__';\n  div.style.cssText = 'width:100px;height:100px;overflow:scroll';\n\n  container.append(div);\n  document.body.append(container);\n\n  const takesSpace = div.offsetWidth - div.clientWidth > 0;\n\n  container.remove();\n  styleEl.remove();\n\n  cachedScrollbarTakesSpace = takesSpace;\n  return takesSpace;\n}\n"
  },
  {
    "path": "packages/core/src/utils/subscriptionUtils.ts",
    "content": "import { getCalendarColorsForHex } from '@/core/calendarRegistry';\nimport { CalendarType, Event } from '@/types';\nimport { parseICS } from '@/utils/ics/icsParser';\nimport { generateUniKey } from '@/utils/utilityFunctions';\n\nexport interface SubscribeResult {\n  calendar: CalendarType;\n  events: Event[];\n}\n\n/**\n * Utility to fetch and parse a calendar subscription.\n * Does not perform any storage operations.\n */\nexport async function subscribeCalendar(url: string): Promise<SubscribeResult> {\n  const response = await fetch(url);\n  if (!response.ok) throw new Error(`HTTP ${response.status}`);\n  const icsContent = await response.text();\n\n  const result = parseICS(icsContent);\n\n  // Extract REAL calendar name from ICS or fallback to hostname\n  const nameMatch = icsContent.match(/X-WR-CALNAME[^:]*:([^\\r\\n]+)/);\n  const calendarName = nameMatch ? nameMatch[1].trim() : new URL(url).hostname;\n\n  const presetColors = [\n    '#3b82f6',\n    '#10b981',\n    '#8b5cf6',\n    '#f59e0b',\n    '#ef4444',\n    '#f97316',\n    '#ec4899',\n    '#14b8a6',\n    '#6366f1',\n    '#6b7280',\n  ];\n  const randomColor =\n    presetColors[Math.floor(Math.random() * presetColors.length)];\n  const { colors, darkColors } = getCalendarColorsForHex(randomColor);\n\n  return {\n    calendar: {\n      id: generateUniKey(),\n      name: calendarName,\n      isVisible: true,\n      colors,\n      darkColors,\n      subscription: {\n        url,\n        status: 'ready',\n      },\n    } as CalendarType,\n    events: result.events,\n  };\n}\n"
  },
  {
    "path": "packages/core/src/utils/temporal.ts",
    "content": "/**\n * Temporal API utility functions\n * Provides date-time processing, conversion, and compatibility support\n */\n\nimport { Temporal } from 'temporal-polyfill';\n// ============================================================================\n// Type Guards\n// ============================================================================\n\n/**\n * Check if value is Temporal.PlainDate\n * Uses multiple methods to check, handling polyfill and serialization issues\n */\nexport function isPlainDate(date: unknown): date is Temporal.PlainDate {\n  if (!date || typeof date !== 'object') return false;\n  if (date instanceof Date) return false;\n\n  // Method 1: instanceof check\n  if (date instanceof Temporal.PlainDate) {\n    return true;\n  }\n\n  const d = date as Record<string, unknown>;\n\n  // Method 2: check constructor.name\n  if (d.constructor?.name === 'PlainDate') {\n    return true;\n  }\n\n  // Method 3: check if no time properties (PlainDate characteristic)\n  // PlainDate has no hour/minute properties, but ZonedDateTime does\n  return (\n    !('hour' in d) &&\n    !('timeZone' in d) &&\n    'year' in d &&\n    'month' in d &&\n    'day' in d\n  );\n}\n\n/**\n * Check if value is Temporal.ZonedDateTime\n */\nexport function isZonedDateTime(date: unknown): date is Temporal.ZonedDateTime {\n  if (!date || typeof date !== 'object') return false;\n  if (date instanceof Date) return false;\n  const d = date as Record<string, unknown>;\n  return (\n    date instanceof Temporal.ZonedDateTime || ('timeZone' in d && 'year' in d)\n  );\n}\n\n/**\n * Check if value is Date object\n */\nexport function isDate(value: unknown): value is Date {\n  return value instanceof Date;\n}\n\n// ============================================================================\n// Date ↔ Temporal Conversion\n// ============================================================================\n\n/**\n * Convert Date to Temporal.ZonedDateTime\n * @param date Date object\n * @param timeZone Timezone (defaults to system timezone)\n * @returns Temporal.ZonedDateTime\n */\nexport function dateToZonedDateTime(\n  date: Date,\n  timeZone: string = Temporal.Now.timeZoneId()\n): Temporal.ZonedDateTime {\n  return Temporal.Instant.fromEpochMilliseconds(\n    date.getTime()\n  ).toZonedDateTimeISO(timeZone);\n}\n\n/**\n * Convert Date to Temporal.PlainDate\n * @param date Date object\n * @returns Temporal.PlainDate\n */\nexport function dateToPlainDate(date: Date): Temporal.PlainDate {\n  return Temporal.PlainDate.from({\n    year: date.getFullYear(),\n    month: date.getMonth() + 1,\n    day: date.getDate(),\n  });\n}\n\n/**\n * Convert Temporal.ZonedDateTime to Date\n * @param zdt Temporal.ZonedDateTime\n * @returns Date object\n */\nexport function zonedDateTimeToDate(zdt: Temporal.ZonedDateTime): Date {\n  if (typeof zdt.epochMilliseconds === 'number') {\n    return new Date(zdt.epochMilliseconds);\n  }\n  // Fallback for plain objects\n  const d = zdt as unknown as Record<string, number>;\n  return new Date(d.year, d.month - 1, d.day, d.hour, d.minute);\n}\n\n/**\n * Convert Temporal.PlainDate to Date\n * @param plainDate Temporal.PlainDate\n * @param timeZone Timezone (optional)\n * @returns Date object (time set to 00:00:00)\n */\nexport function plainDateToDate(\n  plainDate: Temporal.PlainDate,\n  timeZone: string = Temporal.Now.timeZoneId()\n): Date {\n  if (typeof plainDate.toZonedDateTime === 'function') {\n    try {\n      const zdt = plainDate.toZonedDateTime({\n        timeZone,\n        plainTime: Temporal.PlainTime.from({ hour: 0, minute: 0 }),\n      });\n      return zonedDateTimeToDate(zdt);\n    } catch {\n      // Fallback\n    }\n  }\n  // Fallback for plain objects (JSON) or error\n  return new Date(plainDate.year, plainDate.month - 1, plainDate.day);\n}\n\n/**\n * Convert Temporal (PlainDate | PlainDateTime | ZonedDateTime) or Date to Date\n * @param temporal Temporal date-time object or native Date\n * @param timeZone Timezone (optional, only used for PlainDate)\n * @returns Date object\n */\nexport function temporalToDate(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date,\n  timeZone?: string\n): Date {\n  if (temporal instanceof Date) {\n    return temporal;\n  }\n\n  if (isPlainDate(temporal)) {\n    return plainDateToDate(temporal as Temporal.PlainDate, timeZone);\n  }\n\n  // Check if PlainDateTime\n  if (\n    temporal &&\n    typeof temporal === 'object' &&\n    'hour' in temporal &&\n    !('timeZone' in temporal)\n  ) {\n    // PlainDateTime: convert to Date in local time\n    const pdt = temporal as Temporal.PlainDateTime;\n    return new Date(\n      pdt.year,\n      pdt.month - 1,\n      pdt.day,\n      pdt.hour,\n      pdt.minute,\n      pdt.second || 0,\n      pdt.millisecond || 0\n    );\n  }\n\n  // At this point, temporal must be ZonedDateTime or looks like it\n  if (temporal && typeof temporal === 'object' && 'year' in temporal) {\n    const zdt = temporal as Temporal.ZonedDateTime;\n    if (timeZone && typeof zdt.withTimeZone === 'function') {\n      const shifted = zdt.withTimeZone(timeZone);\n      // To get the local date in the target timezone, we use its year/month/day\n      return new Date(\n        shifted.year,\n        shifted.month - 1,\n        shifted.day,\n        shifted.hour,\n        shifted.minute\n      );\n    }\n    return zonedDateTimeToDate(zdt);\n  }\n\n  // Last resort\n  return new Date(temporal as string | number);\n}\n\n// ============================================================================\n// Date-time Extraction and Calculation\n// ============================================================================\n\n/**\n * Extract hour number (with decimals) from Temporal object\n * @param temporal Temporal time object\n * @returns Hour number (0-24, supports decimals), returns 0 if PlainDate\n */\nexport function extractHourFromTemporal(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date\n): number {\n  if (temporal instanceof Date) {\n    return temporal.getHours() + temporal.getMinutes() / 60;\n  }\n\n  if (isPlainDate(temporal)) {\n    return 0; // PlainDate has no time information\n  }\n\n  // Additional safety check: if no hour property, return 0\n  if (\n    temporal === null ||\n    typeof temporal !== 'object' ||\n    !('hour' in temporal) ||\n    (temporal as unknown as Record<string, unknown>).hour === undefined\n  ) {\n    return 0;\n  }\n\n  const t = temporal as unknown as Record<string, number>;\n  const hours = t.hour;\n  const minutes = t.minute ?? 0;\n  return hours + minutes / 60;\n}\n\n/**\n * Create new Temporal object with specified hour\n * @param temporal Base Temporal object\n * @param hour Hour number (supports decimals)\n * @returns New Temporal (PlainDateTime or ZonedDateTime)\n */\nexport function createTemporalWithHour(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime,\n  hour: number\n): Temporal.PlainDateTime | Temporal.ZonedDateTime {\n  const hours = Math.floor(hour);\n  const minutes = Math.round((hour - hours) * 60);\n\n  if (isPlainDate(temporal)) {\n    // Convert PlainDate to PlainDateTime\n    return Temporal.PlainDateTime.from({\n      year: temporal.year,\n      month: temporal.month,\n      day: temporal.day,\n      hour: hours,\n      minute: minutes,\n    });\n  }\n\n  // Real Temporal instance check\n  if (typeof (temporal as { with?: unknown }).with === 'function') {\n    return (temporal as Temporal.PlainDateTime).with({\n      hour: hours,\n      minute: minutes,\n      second: 0,\n      millisecond: 0,\n    });\n  }\n\n  // Fallback for plain objects\n  const t = temporal as unknown as Record<string, number>;\n  return Temporal.PlainDateTime.from({\n    year: t.year,\n    month: t.month,\n    day: t.day,\n    hour: hours,\n    minute: minutes,\n  });\n}\n\n/**\n * Check if two Temporal dates are on the same day\n */\nexport function isSamePlainDate(\n  date1:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date,\n  date2:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date\n): boolean {\n  if (date1 instanceof Date && date2 instanceof Date) {\n    return date1.toDateString() === date2.toDateString();\n  }\n\n  const getPD = (d: unknown) => {\n    if (d instanceof Date) return dateToPlainDate(d);\n    if (isPlainDate(d)) return d;\n    if (typeof (d as { toPlainDate?: unknown }).toPlainDate === 'function')\n      return (d as Temporal.PlainDateTime).toPlainDate();\n    // Fallback for plain objects\n    const t = d as Record<string, number>;\n    return Temporal.PlainDate.from({\n      year: t.year,\n      month: t.month,\n      day: t.day,\n    });\n  };\n\n  const plain1 = getPD(date1);\n  const plain2 = getPD(date2);\n  return Temporal.PlainDate.compare(plain1, plain2) === 0;\n}\n\n/**\n * Check if event spans multiple days\n */\nexport function isMultiDayTemporalEvent(\n  start:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date,\n  end:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date\n): boolean {\n  return !isSamePlainDate(start, end);\n}\n\n/**\n * Get start time of Temporal date (00:00:00)\n */\nexport function getStartOfTemporal(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime,\n  timeZone: string = Temporal.Now.timeZoneId()\n): Temporal.ZonedDateTime {\n  if (\n    typeof (temporal as { toZonedDateTime?: unknown }).toZonedDateTime ===\n    'function'\n  ) {\n    const plainDate = isPlainDate(temporal)\n      ? temporal\n      : (temporal as Temporal.PlainDateTime).toPlainDate();\n    return plainDate.toZonedDateTime({\n      timeZone,\n      plainTime: Temporal.PlainTime.from({ hour: 0, minute: 0 }),\n    });\n  }\n  // Fallback\n  const t = temporal as unknown as Record<string, number>;\n  const pd = isPlainDate(temporal)\n    ? temporal\n    : Temporal.PlainDate.from({\n        year: t.year,\n        month: t.month,\n        day: t.day,\n      });\n  return pd.toZonedDateTime({\n    timeZone,\n    plainTime: Temporal.PlainTime.from({ hour: 0, minute: 0 }),\n  });\n}\n\n/**\n * Get end time of Temporal date (23:59:59.999)\n */\nexport function getEndOfTemporal(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime,\n  timeZone: string = Temporal.Now.timeZoneId()\n): Temporal.ZonedDateTime {\n  if (\n    typeof (temporal as { toZonedDateTime?: unknown }).toZonedDateTime ===\n    'function'\n  ) {\n    const plainDate = isPlainDate(temporal)\n      ? temporal\n      : (temporal as Temporal.PlainDateTime).toPlainDate();\n    return plainDate.toZonedDateTime({\n      timeZone,\n      plainTime: Temporal.PlainTime.from({\n        hour: 23,\n        minute: 59,\n        second: 59,\n        millisecond: 999,\n      }),\n    });\n  }\n  // Fallback\n  const t = temporal as unknown as Record<string, number>;\n  const pd = isPlainDate(temporal)\n    ? temporal\n    : Temporal.PlainDate.from({\n        year: t.year,\n        month: t.month,\n        day: t.day,\n      });\n  return pd.toZonedDateTime({\n    timeZone,\n    plainTime: Temporal.PlainTime.from({\n      hour: 23,\n      minute: 59,\n      second: 59,\n      millisecond: 999,\n    }),\n  });\n}\n\n/**\n * Calculate days difference between two Temporal dates\n */\nexport function daysBetween(\n  start: Temporal.PlainDate | Temporal.ZonedDateTime,\n  end: Temporal.PlainDate | Temporal.ZonedDateTime\n): number {\n  const getPD = (d: unknown) => {\n    if (isPlainDate(d)) return d;\n    if (typeof (d as { toPlainDate?: unknown }).toPlainDate === 'function')\n      return (d as Temporal.PlainDateTime).toPlainDate();\n    const t = d as Record<string, number>;\n    return Temporal.PlainDate.from({\n      year: t.year,\n      month: t.month,\n      day: t.day,\n    });\n  };\n  const plainStart = getPD(start);\n  const plainEnd = getPD(end);\n  return plainStart.until(plainEnd).days;\n}\n\n/**\n * Calculate days difference between two Date objects (ignoring time component)\n */\nexport function daysDifference(date1: Date, date2: Date): number {\n  const oneDay = 24 * 60 * 60 * 1000;\n  const firstDate = new Date(\n    date1.getFullYear(),\n    date1.getMonth(),\n    date1.getDate()\n  );\n  const secondDate = new Date(\n    date2.getFullYear(),\n    date2.getMonth(),\n    date2.getDate()\n  );\n  return Math.round((secondDate.getTime() - firstDate.getTime()) / oneDay);\n}\n\n/**\n * Add specified days to a date\n */\nexport function addDays(date: Date, days: number): Date {\n  const result = new Date(date);\n  result.setDate(result.getDate() + days);\n  return result;\n}\n\n/**\n * Get current time (Temporal.ZonedDateTime)\n */\nexport function now(\n  timeZone: string = Temporal.Now.timeZoneId()\n): Temporal.ZonedDateTime {\n  return Temporal.Now.zonedDateTimeISO(timeZone);\n}\n\n/**\n * Get today's date (Temporal.PlainDate)\n */\nexport function today(\n  timeZone: string = Temporal.Now.timeZoneId()\n): Temporal.PlainDate {\n  return Temporal.Now.plainDateISO(timeZone);\n}\n"
  },
  {
    "path": "packages/core/src/utils/temporalTypeGuards.ts",
    "content": "/**\n * Temporal Type Guards and Conversion Utilities\n *\n * This module provides type guards for distinguishing between different Temporal types\n * and unified conversion functions for internal processing.\n */\n\nimport { Temporal } from 'temporal-polyfill';\n\nimport { logger } from '@/utils/logger';\n\nimport { normalizeTimeZoneValue } from './timeZoneUtils';\n\n// ============================================================================\n// Type Guards\n// ============================================================================\n\n/**\n * Check if temporal is PlainDate (date only, no time)\n */\nexport function isPlainDate(temporal: unknown): temporal is Temporal.PlainDate {\n  return (\n    temporal !== null &&\n    typeof temporal === 'object' &&\n    !('hour' in temporal) &&\n    'year' in temporal &&\n    'month' in temporal &&\n    'day' in temporal &&\n    !(temporal instanceof Date)\n  );\n}\n\n/**\n * Check if temporal is PlainDateTime (date + time, no timezone)\n */\nexport function isPlainDateTime(\n  temporal: unknown\n): temporal is Temporal.PlainDateTime {\n  return (\n    temporal !== null &&\n    typeof temporal === 'object' &&\n    'hour' in temporal &&\n    !('timeZone' in temporal) &&\n    !('timeZoneId' in temporal) &&\n    'year' in temporal &&\n    !(temporal instanceof Date)\n  );\n}\n\n/**\n * Check if temporal is ZonedDateTime (date + time + timezone)\n */\nexport function isZonedDateTime(\n  temporal: unknown\n): temporal is Temporal.ZonedDateTime {\n  return (\n    temporal !== null &&\n    typeof temporal === 'object' &&\n    ('timeZone' in temporal || 'timeZoneId' in temporal) &&\n    'year' in temporal &&\n    !(temporal instanceof Date)\n  );\n}\n\n// ============================================================================\n// Conversion Functions\n// ============================================================================\n\n/**\n * Convert any Temporal type or Date to Date (for internal processing)\n * Handles all three Temporal types and native Date uniformly\n */\nexport function temporalToDate(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date\n): Date {\n  if (temporal instanceof Date) {\n    return temporal;\n  }\n\n  if (isPlainDate(temporal)) {\n    // PlainDate: create Date at midnight local time\n    return new Date(temporal.year, temporal.month - 1, temporal.day);\n  }\n\n  if (isPlainDateTime(temporal)) {\n    // PlainDateTime: create Date with specified time in local timezone\n    return new Date(\n      temporal.year,\n      temporal.month - 1,\n      temporal.day,\n      temporal.hour,\n      temporal.minute,\n      temporal.second || 0,\n      temporal.millisecond || 0\n    );\n  }\n\n  if (isZonedDateTime(temporal)) {\n    // ZonedDateTime: convert via Instant to preserve timezone information\n    try {\n      // If it's a real Temporal instance\n      if (\n        typeof (temporal as Temporal.ZonedDateTime).toInstant === 'function'\n      ) {\n        const instant = (temporal as Temporal.ZonedDateTime).toInstant();\n        return new Date(instant.epochMilliseconds);\n      }\n      // If it has epochMilliseconds property (some polyfills/serialized)\n      if (\n        typeof (temporal as unknown as Record<string, unknown>)\n          .epochMilliseconds === 'number'\n      ) {\n        return new Date(\n          (temporal as unknown as Record<string, unknown>)\n            .epochMilliseconds as number\n        );\n      }\n      // Fallback for plain objects that look like ZonedDateTime (from JSON)\n      return new Date(\n        temporal.year,\n        temporal.month - 1,\n        temporal.day,\n        temporal.hour,\n        temporal.minute\n      );\n    } catch {\n      return new Date(\n        temporal.year,\n        temporal.month - 1,\n        temporal.day,\n        temporal.hour,\n        temporal.minute\n      );\n    }\n  }\n\n  // Fallback for other types\n  return new Date(temporal as string | number);\n}\n\n/**\n * Convert any Temporal type or Date to a \"Visual\" Date based on a target timezone.\n * Unlike temporalToDate, this shifts the wall time to match the target timezone\n * while returning a local Date object that reflects that shifted time.\n */\nexport function temporalToVisualDate(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date,\n  targetTz?: string\n): Date {\n  if (temporal instanceof Date) {\n    return temporal;\n  }\n\n  if (!targetTz) {\n    return temporalToDate(temporal);\n  }\n\n  const normalizedTargetTz = normalizeTimeZoneValue(targetTz);\n  if (!normalizedTargetTz) {\n    return temporalToDate(temporal);\n  }\n\n  try {\n    // 1. Handle ZonedDateTime (either instance or plain object)\n    if (isZonedDateTime(temporal)) {\n      const zdt =\n        typeof (temporal as unknown as Record<string, unknown>).withTimeZone ===\n        'function'\n          ? (temporal as Temporal.ZonedDateTime)\n          : Temporal.ZonedDateTime.from(temporal as Temporal.ZonedDateTimeLike);\n      const shifted = zdt.withTimeZone(normalizedTargetTz);\n      return new Date(\n        shifted.year,\n        shifted.month - 1,\n        shifted.day,\n        shifted.hour,\n        shifted.minute,\n        shifted.second || 0,\n        shifted.millisecond || 0\n      );\n    }\n\n    // 2. Handle PlainDateTime (either instance or plain object)\n    if (isPlainDateTime(temporal)) {\n      const pdt =\n        typeof (temporal as unknown as Record<string, unknown>)\n          .toZonedDateTime === 'function'\n          ? (temporal as Temporal.PlainDateTime)\n          : Temporal.PlainDateTime.from(temporal as Temporal.PlainDateTimeLike);\n      // Assume PlainDateTime is in local wall time, convert to ZDT using local TZ then shift\n      const zdt = pdt.toZonedDateTime(Temporal.Now.timeZoneId());\n      const shifted = zdt.withTimeZone(normalizedTargetTz);\n      return new Date(\n        shifted.year,\n        shifted.month - 1,\n        shifted.day,\n        shifted.hour,\n        shifted.minute,\n        shifted.second || 0,\n        shifted.millisecond || 0\n      );\n    }\n  } catch (e) {\n    logger.error('Failed to shift visual timezone:', e);\n  }\n\n  // PlainDate remains unchanged or error fallback\n  return temporalToDate(temporal);\n}\n\n/**\n * Convert any Temporal type to a \"Visual\" Temporal based on a target timezone.\n * Unlike temporalToVisualDate, this returns a Temporal object (ZonedDateTime).\n */\nexport function temporalToVisualTemporal(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime,\n  targetTz?: string\n): Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime {\n  if (!targetTz) {\n    return temporal;\n  }\n\n  const normalizedTargetTz = normalizeTimeZoneValue(targetTz);\n  if (!normalizedTargetTz) {\n    return temporal;\n  }\n\n  try {\n    if (isZonedDateTime(temporal)) {\n      const zdt =\n        typeof (temporal as unknown as Record<string, unknown>).withTimeZone ===\n        'function'\n          ? (temporal as Temporal.ZonedDateTime)\n          : Temporal.ZonedDateTime.from(temporal as Temporal.ZonedDateTimeLike);\n      return zdt.withTimeZone(normalizedTargetTz);\n    }\n\n    if (isPlainDateTime(temporal)) {\n      const pdt =\n        typeof (temporal as unknown as Record<string, unknown>)\n          .toZonedDateTime === 'function'\n          ? (temporal as Temporal.PlainDateTime)\n          : Temporal.PlainDateTime.from(temporal as Temporal.PlainDateTimeLike);\n      return pdt\n        .toZonedDateTime(Temporal.Now.timeZoneId())\n        .withTimeZone(normalizedTargetTz);\n    }\n  } catch (e) {\n    logger.error('Failed to shift visual temporal:', e);\n  }\n\n  return temporal;\n}\n\n/**\n * Convert Date to PlainDate (for all-day events)\n */\nexport function dateToPlainDate(date: Date): Temporal.PlainDate {\n  return Temporal.PlainDate.from({\n    year: date.getFullYear(),\n    month: date.getMonth() + 1,\n    day: date.getDate(),\n  });\n}\n\n/**\n * Convert Date to PlainDateTime (for local events without timezone)\n */\nexport function dateToPlainDateTime(date: Date): Temporal.PlainDateTime {\n  return Temporal.PlainDateTime.from({\n    year: date.getFullYear(),\n    month: date.getMonth() + 1,\n    day: date.getDate(),\n    hour: date.getHours(),\n    minute: date.getMinutes(),\n    second: date.getSeconds(),\n    millisecond: date.getMilliseconds(),\n  });\n}\n\n/**\n * Convert Date to ZonedDateTime (for timezone-aware events)\n */\nexport function dateToZonedDateTime(\n  date: Date,\n  timeZone: string = Temporal.Now.timeZoneId()\n): Temporal.ZonedDateTime {\n  return Temporal.ZonedDateTime.from({\n    year: date.getFullYear(),\n    month: date.getMonth() + 1,\n    day: date.getDate(),\n    hour: date.getHours(),\n    minute: date.getMinutes(),\n    second: date.getSeconds(),\n    millisecond: date.getMilliseconds(),\n    timeZone: timeZone,\n  });\n}\n\n/**\n * Convert PlainDateTime to Date\n */\nexport function plainDateTimeToDate(pdt: Temporal.PlainDateTime): Date {\n  return new Date(\n    pdt.year,\n    pdt.month - 1,\n    pdt.day,\n    pdt.hour,\n    pdt.minute,\n    pdt.second || 0,\n    pdt.millisecond || 0\n  );\n}\n\n/**\n * Convert PlainDate to Date (at midnight)\n */\nexport function plainDateToDate(pd: Temporal.PlainDate): Date {\n  return new Date(pd.year, pd.month - 1, pd.day);\n}\n\n// ============================================================================\n// Hour Extraction (supports all types)\n// ============================================================================\n\n/**\n * Extract hour from any Temporal type (with decimal for minutes)\n * @returns Hour number (0-24, with decimals, e.g., 14.5 = 14:30)\n */\nexport function extractHourFromTemporal(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date\n): number {\n  if (temporal instanceof Date) {\n    return temporal.getHours() + temporal.getMinutes() / 60;\n  }\n\n  if (isPlainDate(temporal)) {\n    return 0; // PlainDate has no time component\n  }\n\n  // Both PlainDateTime and ZonedDateTime have hour/minute\n  const t = temporal as unknown as { hour: number; minute: number };\n  return t.hour + t.minute / 60;\n}\n\n/**\n * Create a new Temporal with specified hour (supports PlainDateTime and ZonedDateTime)\n * @param temporal Base temporal object\n * @param hour Hour with decimals (e.g., 14.5 = 14:30)\n */\nexport function setHourInTemporal(\n  temporal: Temporal.PlainDateTime | Temporal.ZonedDateTime,\n  hour: number\n): Temporal.PlainDateTime | Temporal.ZonedDateTime {\n  const hours = Math.floor(hour);\n  const minutes = Math.round((hour - hours) * 60);\n\n  if (isZonedDateTime(temporal)) {\n    return temporal.with({\n      hour: hours,\n      minute: minutes,\n      second: 0,\n      millisecond: 0,\n    });\n  }\n\n  // PlainDateTime\n  return (temporal as Temporal.PlainDateTime).with({\n    hour: hours,\n    minute: minutes,\n    second: 0,\n    millisecond: 0,\n  });\n}\n\n// ============================================================================\n// Comparison Functions (supports all types)\n// ============================================================================\n\n/**\n * Get PlainDate from any Temporal type or Date\n */\nexport function getPlainDate(\n  temporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date\n): Temporal.PlainDate {\n  if (temporal instanceof Date) {\n    return dateToPlainDate(temporal);\n  }\n  if (isPlainDate(temporal)) {\n    return temporal;\n  }\n  if (isPlainDateTime(temporal)) {\n    return temporal.toPlainDate();\n  }\n  return (\n    temporal as unknown as { toPlainDate: () => Temporal.PlainDate }\n  ).toPlainDate();\n}\n\n/**\n * Check if two Temporal objects represent the same day\n */\nexport function isSameTemporal(\n  t1:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date,\n  t2:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime\n    | Date\n): boolean {\n  if (t1 instanceof Date && t2 instanceof Date) {\n    return (\n      t1.getFullYear() === t2.getFullYear() &&\n      t1.getMonth() === t2.getMonth() &&\n      t1.getDate() === t2.getDate()\n    );\n  }\n\n  // Convert to PlainDate for comparison\n  const date1 = getPlainDate(t1);\n  const date2 = getPlainDate(t2);\n\n  return date1.equals(date2);\n}\n"
  },
  {
    "path": "packages/core/src/utils/testDataUtils.ts",
    "content": "/**\n * Test Data Generation Utilities\n *\n * This module provides test/demo event data for calendar development and testing.\n * The generated events are relative to the current week for consistent demo experience.\n */\n\nimport { Temporal } from 'temporal-polyfill';\n\nimport { Event } from '@/types';\n\n// ============================================================================\n// Test Data Generation\n// ============================================================================\n\n/**\n * Generate test events for calendar demo/testing\n * Events are generated relative to the current week (Monday-Sunday)\n * Includes various overlapping events, all-day events, and edge cases\n * @returns Array of test events\n */\nexport function getTestEvents(): Event[] {\n  // Get current time\n  const now = new Date();\n  // Get current day of week (0=Sunday, 1=Monday, ...6=Saturday)\n  const dayOfWeek = now.getDay(); // Sunday is 0, Monday is 1, ...Saturday is 6\n  // ISO Monday as week start, calculate this week's Monday date\n  const monday = new Date(now);\n  const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; // If Sunday, go back 6 days; otherwise normal\n  monday.setDate(now.getDate() + diffToMonday);\n  monday.setHours(0, 0, 0, 0);\n\n  // Original event template (only keeps day, startHour, endHour, title, calendarId, etc.)\n  const template = [\n    {\n      id: '15',\n      title: 'C-早开始',\n      day: 3,\n      startHour: 8.5,\n      endHour: 10,\n      calendarId: 'teal',\n    },\n    {\n      id: '16',\n      title: 'D-晚开始',\n      day: 3,\n      startHour: 9,\n      endHour: 10.5,\n      calendarId: 'red',\n    },\n    {\n      id: '21',\n      title: 'A',\n      day: 0,\n      startHour: 14,\n      endHour: 16,\n      calendarId: 'blue',\n    },\n    {\n      id: '22',\n      title: 'B',\n      day: 0,\n      startHour: 14.5,\n      endHour: 16,\n      calendarId: 'green',\n    },\n    {\n      id: '23',\n      title: 'C',\n      day: 0,\n      startHour: 14.5,\n      endHour: 16,\n      calendarId: 'purple',\n    },\n    {\n      id: '24',\n      title: 'D',\n      day: 0,\n      startHour: 15,\n      endHour: 16,\n      calendarId: 'yellow',\n    },\n    {\n      id: '25',\n      title: 'E',\n      day: 0,\n      startHour: 15,\n      endHour: 16,\n      calendarId: 'red',\n    },\n    {\n      id: '26',\n      title: 'F',\n      day: 0,\n      startHour: 15.5,\n      endHour: 16,\n      calendarId: 'orange',\n    },\n    {\n      id: '27',\n      title: 'G',\n      day: 0,\n      startHour: 15,\n      endHour: 16,\n      calendarId: 'pink',\n    },\n    {\n      id: '28',\n      title: 'H',\n      day: 0,\n      startHour: 15.5,\n      endHour: 16,\n      calendarId: 'teal',\n    },\n    {\n      id: '29',\n      title: 'L',\n      day: 0,\n      startHour: 15.5,\n      endHour: 16,\n      calendarId: 'teal',\n    },\n    {\n      id: '30',\n      title: 'I',\n      day: 0,\n      startHour: 15.5,\n      endHour: 16,\n      calendarId: 'blue',\n    },\n    {\n      id: '99',\n      title: 'X',\n      day: 0,\n      startHour: 15.5,\n      endHour: 16,\n      calendarId: 'blue',\n    },\n    {\n      id: '6',\n      title: '假日',\n      day: 0,\n      startHour: 0,\n      endHour: 0,\n      calendarId: 'blue',\n      allDay: true,\n    },\n    {\n      id: '7',\n      title: '研讨会',\n      day: 2,\n      startHour: 0,\n      endHour: 0,\n      calendarId: 'green',\n      allDay: true,\n    },\n    {\n      id: '8',\n      title: '团队建设',\n      day: 4,\n      startHour: 0,\n      endHour: 0,\n      calendarId: 'purple',\n      allDay: true,\n    },\n    {\n      id: '41',\n      title: 'A',\n      day: 3,\n      startHour: 16,\n      endHour: 18.25,\n      calendarId: 'blue',\n    },\n    {\n      id: '42',\n      title: 'B',\n      day: 3,\n      startHour: 16.5,\n      endHour: 18,\n      calendarId: 'green',\n    },\n    {\n      id: '43',\n      title: 'C',\n      day: 3,\n      startHour: 16.75,\n      endHour: 17.75,\n      calendarId: 'purple',\n    },\n    {\n      id: '44',\n      title: 'D',\n      day: 3,\n      startHour: 17,\n      endHour: 19,\n      calendarId: 'yellow',\n    },\n    {\n      id: '45',\n      title: 'E',\n      day: 3,\n      startHour: 17.75,\n      endHour: 18.75,\n      calendarId: 'red',\n    },\n    {\n      id: '46',\n      title: 'X',\n      day: 2,\n      startHour: 15,\n      endHour: 15.75,\n      calendarId: 'orange',\n    },\n    {\n      id: '47',\n      title: 'Y',\n      day: 2,\n      startHour: 15.5,\n      endHour: 19.5,\n      calendarId: 'pink',\n    },\n    {\n      id: '48',\n      title: 'Z',\n      day: 2,\n      startHour: 15,\n      endHour: 15.75,\n      calendarId: 'teal',\n    },\n    {\n      id: '59',\n      title: 'Q',\n      day: 2,\n      startHour: 17,\n      endHour: 18,\n      calendarId: 'teal',\n    },\n    {\n      id: '60',\n      title: 'W',\n      day: 2,\n      startHour: 15.5,\n      endHour: 18,\n      calendarId: 'teal',\n    },\n    {\n      id: '31',\n      title: 'A',\n      day: 6,\n      startHour: 14,\n      endHour: 16,\n      calendarId: 'blue',\n    },\n    {\n      id: '32',\n      title: 'B',\n      day: 6,\n      startHour: 14.5,\n      endHour: 16,\n      calendarId: 'green',\n    },\n    {\n      id: '33',\n      title: 'C',\n      day: 6,\n      startHour: 14.5,\n      endHour: 16,\n      calendarId: 'purple',\n    },\n    {\n      id: '34',\n      title: 'D',\n      day: 6,\n      startHour: 15,\n      endHour: 16,\n      calendarId: 'yellow',\n    },\n    {\n      id: '35',\n      title: 'E',\n      day: 6,\n      startHour: 15.5,\n      endHour: 16.5,\n      calendarId: 'red',\n    },\n    {\n      id: '36',\n      title: 'F',\n      day: 6,\n      startHour: 16.25,\n      endHour: 16.75,\n      calendarId: 'orange',\n    },\n    {\n      id: '37',\n      title: 'G',\n      day: 6,\n      startHour: 16,\n      endHour: 17.25,\n      calendarId: 'pink',\n    },\n    {\n      id: '51',\n      title: '下周会议1',\n      day: 1,\n      startHour: 9,\n      endHour: 10,\n      calendarId: 'blue',\n    },\n    {\n      id: '52',\n      title: '下周会议2',\n      day: 3,\n      startHour: 14,\n      endHour: 15,\n      calendarId: 'green',\n    },\n    {\n      id: '81',\n      title: 'A',\n      day: 5,\n      startHour: 14,\n      endHour: 16,\n      calendarId: 'blue',\n    },\n    {\n      id: '82',\n      title: 'B',\n      day: 5,\n      startHour: 14.5,\n      endHour: 16,\n      calendarId: 'green',\n    },\n    {\n      id: '83',\n      title: 'C',\n      day: 5,\n      startHour: 14.5,\n      endHour: 16,\n      calendarId: 'purple',\n    },\n    {\n      id: '84',\n      title: 'D',\n      day: 5,\n      startHour: 15,\n      endHour: 16,\n      calendarId: 'yellow',\n    },\n    {\n      id: '85',\n      title: 'E',\n      day: 5,\n      startHour: 15,\n      endHour: 16,\n      calendarId: 'red',\n    },\n    {\n      id: '86',\n      title: 'F',\n      day: 5,\n      startHour: 15.75,\n      endHour: 17,\n      calendarId: 'orange',\n    },\n    {\n      id: '87',\n      title: 'G',\n      day: 5,\n      startHour: 14.75,\n      endHour: 17.5,\n      calendarId: 'pink',\n    },\n  ];\n\n  const events: Event[] = template.map(e => {\n    // Calculate event date\n    const eventDate = new Date(monday);\n    eventDate.setDate(monday.getDate() + e.day);\n\n    // If it's an all-day event\n    if (e.allDay) {\n      return {\n        id: e.id,\n        title: e.title,\n        start: Temporal.PlainDate.from({\n          year: eventDate.getFullYear(),\n          month: eventDate.getMonth() + 1,\n          day: eventDate.getDate(),\n        }),\n        end: Temporal.PlainDate.from({\n          year: eventDate.getFullYear(),\n          month: eventDate.getMonth() + 1,\n          day: eventDate.getDate(),\n        }),\n        allDay: true,\n        calendarId: e.calendarId,\n        day: e.day,\n      };\n    }\n\n    // Regular event, using Temporal.ZonedDateTime\n    const startHour = Math.floor(e.startHour);\n    const startMinute = Math.round((e.startHour - startHour) * 60);\n    const endHour = Math.floor(e.endHour);\n    const endMinute = Math.round((e.endHour - endHour) * 60);\n\n    return {\n      id: e.id,\n      title: e.title,\n      start: Temporal.ZonedDateTime.from({\n        year: eventDate.getFullYear(),\n        month: eventDate.getMonth() + 1,\n        day: eventDate.getDate(),\n        hour: startHour,\n        minute: startMinute,\n        timeZone: Temporal.Now.timeZoneId(),\n      }),\n      end: Temporal.ZonedDateTime.from({\n        year: eventDate.getFullYear(),\n        month: eventDate.getMonth() + 1,\n        day: eventDate.getDate(),\n        hour: endHour,\n        minute: endMinute,\n        timeZone: Temporal.Now.timeZoneId(),\n      }),\n      allDay: false,\n      calendarId: e.calendarId,\n      day: e.day,\n    };\n  });\n\n  return events;\n}\n"
  },
  {
    "path": "packages/core/src/utils/themeUtils.ts",
    "content": "/**\n * Resolve the currently applied theme on the document.\n */\nexport const resolveAppliedTheme = (\n  effectiveTheme: 'light' | 'dark'\n): 'light' | 'dark' => {\n  if (typeof document === 'undefined') {\n    return effectiveTheme;\n  }\n\n  const root = document.documentElement;\n\n  const overrideAttributes = [\n    root.dataset.dfThemeOverride,\n    root.dataset.dfTheme,\n    root.dataset.theme,\n  ];\n\n  for (const attr of overrideAttributes) {\n    if (attr === 'light' || attr === 'dark') {\n      return attr;\n    }\n  }\n\n  if (root.classList.contains('dark')) {\n    return 'dark';\n  }\n\n  if (root.classList.contains('light')) {\n    return 'light';\n  }\n\n  return effectiveTheme;\n};\n"
  },
  {
    "path": "packages/core/src/utils/throttle.ts",
    "content": "/**\n * Creates a throttled function that only invokes the provided function at most once\n * per every wait milliseconds.\n *\n * @param func - The function to throttle\n * @param wait - The number of milliseconds to throttle invocations to\n * @returns The throttled function\n */\nexport function throttle<T extends (...args: unknown[]) => unknown>(\n  func: T,\n  wait: number\n): T & { cancel: () => void } {\n  let timeout: ReturnType<typeof setTimeout> | null = null;\n  let previous = 0;\n\n  const throttled = function (this: unknown, ...args: Parameters<T>) {\n    const now = Date.now();\n    const remaining = wait - (now - previous);\n\n    if (remaining <= 0 || remaining > wait) {\n      if (timeout) {\n        clearTimeout(timeout);\n        timeout = null;\n      }\n      previous = now;\n      func.apply(this, args);\n    } else if (!timeout) {\n      timeout = setTimeout(() => {\n        previous = Date.now();\n        timeout = null;\n        func.apply(this, args);\n      }, remaining);\n    }\n  } as T & { cancel: () => void };\n\n  throttled.cancel = () => {\n    if (timeout) {\n      clearTimeout(timeout);\n      timeout = null;\n    }\n    previous = 0;\n  };\n\n  return throttled;\n}\n"
  },
  {
    "path": "packages/core/src/utils/timeUtils.ts",
    "content": "/**\n * Time Utilities\n *\n * This module provides utilities for time formatting, calculations, and time step operations.\n * Handles 24-hour format, time rounding, and special cases like midnight crossings.\n */\n\nimport { Temporal } from 'temporal-polyfill';\n\nimport { Event, TimeZoneValue } from '@/types';\n\n// Temporal polyfill implementations expose the timezone in different shapes.\ninterface TemporalZoneShape {\n  timeZoneId?: string;\n  timeZone?: string | { id?: string };\n}\n\nimport { extractHourFromDate } from './dateTimeUtils';\nimport { temporalToDate } from './temporal';\nimport {\n  dateToPlainDate,\n  dateToPlainDateTime,\n  dateToZonedDateTime,\n  isPlainDate,\n  isPlainDateTime,\n  isZonedDateTime,\n} from './temporalTypeGuards';\nimport { normalizeTimeZoneValue } from './timeZoneUtils';\n\nconst zonedDateTimeToWallClockDate = (zdt: Temporal.ZonedDateTime): Date =>\n  new Date(\n    zdt.year,\n    zdt.month - 1,\n    zdt.day,\n    zdt.hour,\n    zdt.minute,\n    zdt.second,\n    Math.floor(zdt.millisecond)\n  );\n\n// ============================================================================\n// Time Tools\n// ============================================================================\n\n/**\n * Time step for calendar grid (0.25 = 15 minutes)\n */\nexport const TIME_STEP = 0.25;\n\n/**\n * Format hours and minutes to HH:MM format or 12h format (e.g. 1AM)\n * @param hours Hour number (supports decimals, e.g., 14.5 = 14:30)\n * @param minutes Optional minutes (if not provided, extracted from decimal hours)\n * @param format Time format ('12h' or '24h', defaults to '24h')\n * @param showUnits Whether to show AM/PM for 12h format (defaults to true)\n * @returns Formatted time string (e.g., \"14:30\" or \"2PM\")\n */\nexport const formatTime = (\n  hours: number,\n  minutes = 0,\n  format: '12h' | '24h' = '24h',\n  showUnits = true\n) => {\n  const h = Math.floor(hours);\n  const m = minutes || Math.round((hours - h) * 60);\n\n  if (format === '12h') {\n    const period = h >= 12 ? 'PM' : 'AM';\n    const displayHour = h % 12 || 12;\n    if (m === 0) {\n      return `${displayHour}${showUnits ? ` ${period}` : ''}`;\n    }\n    return `${displayHour}:${m.toString().padStart(2, '0')}${showUnits ? ` ${period}` : ''}`;\n  }\n\n  return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;\n};\n\n/**\n * Get event end hour (handles cross-day events less than 24 hours)\n * When an event ends exactly at midnight of the next day and duration is less than 24 hours,\n * it should be treated as 24:00 of the current day in week/day views to avoid displaying as next day 00:00.\n * @param event Event object\n * @returns End hour (0-24)\n */\nexport const getEventEndHour = (event: Event): number => {\n  if (!event.end) return 0;\n\n  const endHour = extractHourFromDate(event.end);\n  if (event.allDay || !event.start) {\n    return endHour;\n  }\n\n  const startDate = temporalToDate(event.start);\n  const endDate = temporalToDate(event.end);\n\n  const crossesDay =\n    startDate.getFullYear() !== endDate.getFullYear() ||\n    startDate.getMonth() !== endDate.getMonth() ||\n    startDate.getDate() !== endDate.getDate();\n\n  if (!crossesDay) {\n    return endHour;\n  }\n\n  const endsExactlyAtMidnight =\n    endHour === 0 &&\n    endDate.getMinutes() === 0 &&\n    endDate.getSeconds() === 0 &&\n    endDate.getMilliseconds() === 0;\n\n  if (endsExactlyAtMidnight) {\n    const durationMs = endDate.getTime() - startDate.getTime();\n    const ONE_DAY_MS = 24 * 60 * 60 * 1000;\n    if (durationMs > 0 && durationMs < ONE_DAY_MS) {\n      return 24;\n    }\n  }\n\n  return endHour;\n};\n\n/**\n * Format event time range as a string\n * @param event Event object\n * @param format Time format ('12h' or '24h', defaults to '24h')\n * @returns Formatted time range (e.g., \"14:00 - 16:00\" or \"All day\")\n */\nexport const formatEventTimeRange = (\n  event: Event,\n  format: '12h' | '24h' = '24h'\n) => {\n  const startHour = extractHourFromDate(event.start);\n  const endHour = getEventEndHour(event);\n  return `${formatTime(startHour, 0, format)} - ${formatTime(endHour, 0, format)}`;\n};\n\n/**\n * Round hour to nearest time step\n * @param hour Hour number\n * @returns Rounded hour\n */\nexport const roundToTimeStep = (hour: number) => {\n  const step = TIME_STEP;\n  return Math.round(hour / step) * step;\n};\n\n// ============================================================================\n// Secondary Time Zone Utilities\n// ============================================================================\n\n/**\n * Generate secondary timezone time labels for each primary time slot.\n * Uses Temporal to correctly handle DST and timezone conversions.\n *\n * @param timeSlots Primary time slots array\n * @param secondaryTimeZone Secondary IANA timezone identifier\n * @param timeFormat Time format ('12h' or '24h')\n * @param referenceDate Reference date used for DST-accurate conversion (defaults to today)\n * @returns Array of formatted time strings for the secondary timezone\n */\nexport const generateSecondaryTimeSlots = (\n  timeSlots: Array<{ hour: number; label: string }>,\n  secondaryTimeZone: TimeZoneValue,\n  timeFormat: '12h' | '24h' = '24h',\n  referenceDate: Date = new Date(),\n  baseTimeZone?: string\n): string[] => {\n  const normalizedSecondaryTimeZone = normalizeTimeZoneValue(secondaryTimeZone);\n  if (!normalizedSecondaryTimeZone) {\n    return timeSlots.map(() => '');\n  }\n\n  const primaryTimeZone = baseTimeZone ?? Temporal.Now.timeZoneId();\n  const year = referenceDate.getFullYear();\n  const month = referenceDate.getMonth() + 1;\n  const day = referenceDate.getDate();\n\n  return timeSlots.map(slot => {\n    try {\n      const hour = slot.hour % 24;\n      const primaryZDT = Temporal.ZonedDateTime.from({\n        year,\n        month,\n        day,\n        hour,\n        minute: 0,\n        second: 0,\n        timeZone: primaryTimeZone,\n      });\n      const secondaryZDT = primaryZDT.withTimeZone(normalizedSecondaryTimeZone);\n      return formatTime(secondaryZDT.hour, secondaryZDT.minute, timeFormat);\n    } catch {\n      return '';\n    }\n  });\n};\n\n/**\n * Get a short display label for a timezone, e.g. \"CST\" or \"GMT+8\".\n *\n * @param timeZone IANA timezone identifier\n * @param date Reference date for DST-aware abbreviation (defaults to today)\n * @returns Short timezone label\n */\nexport const getTimezoneDisplayLabel = (\n  timeZone: TimeZoneValue,\n  date: Date = new Date()\n): string => {\n  const normalizedTimeZone = normalizeTimeZoneValue(timeZone);\n  if (!normalizedTimeZone) {\n    return '';\n  }\n\n  try {\n    const parts = new Intl.DateTimeFormat('en', {\n      timeZone: normalizedTimeZone,\n      timeZoneName: 'short',\n    }).formatToParts(date);\n    return (\n      parts.find(p => p.type === 'timeZoneName')?.value ?? normalizedTimeZone\n    );\n  } catch {\n    return normalizedTimeZone;\n  }\n};\n\nexport const getNowInTimeZone = (timeZone?: TimeZoneValue): Date => {\n  const normalizedTimeZone =\n    (timeZone ? normalizeTimeZoneValue(timeZone) : undefined) ??\n    Temporal.Now.timeZoneId();\n\n  return zonedDateTimeToWallClockDate(\n    Temporal.Now.zonedDateTimeISO(normalizedTimeZone)\n  );\n};\n\nexport const getTodayInTimeZone = (timeZone?: TimeZoneValue): Date => {\n  const normalizedTimeZone =\n    (timeZone ? normalizeTimeZoneValue(timeZone) : undefined) ??\n    Temporal.Now.timeZoneId();\n  const today = Temporal.Now.plainDateISO(normalizedTimeZone);\n  return new Date(today.year, today.month - 1, today.day);\n};\n\nexport const getNextHourRangeInTimeZone = (\n  timeZone?: TimeZoneValue\n): { start: Date; end: Date } => {\n  const normalizedTimeZone =\n    (timeZone ? normalizeTimeZoneValue(timeZone) : undefined) ??\n    Temporal.Now.timeZoneId();\n  const nextHourStart = Temporal.Now.zonedDateTimeISO(normalizedTimeZone)\n    .add({ hours: 1 })\n    .with({\n      minute: 0,\n      second: 0,\n      millisecond: 0,\n      microsecond: 0,\n      nanosecond: 0,\n    });\n\n  return {\n    start: zonedDateTimeToWallClockDate(nextHourStart),\n    end: zonedDateTimeToWallClockDate(nextHourStart.add({ hours: 1 })),\n  };\n};\n\nconst getTemporalZoneId = (temporal: Temporal.ZonedDateTime): string => {\n  const v = temporal as unknown as TemporalZoneShape;\n  return (\n    v.timeZoneId ||\n    (typeof v.timeZone === 'string' ? v.timeZone : v.timeZone?.id) ||\n    Temporal.Now.timeZoneId()\n  );\n};\n\nexport const restoreVisualTimedTemporalToCanonical = (\n  visualTemporal: Temporal.PlainDateTime | Temporal.ZonedDateTime | Date,\n  originalTemporal:\n    | Temporal.PlainDate\n    | Temporal.PlainDateTime\n    | Temporal.ZonedDateTime,\n  appTimeZone?: TimeZoneValue\n): Temporal.PlainDateTime | Temporal.ZonedDateTime => {\n  const visualDate = temporalToDate(visualTemporal);\n\n  if (isZonedDateTime(originalTemporal)) {\n    const editingTimeZone =\n      normalizeTimeZoneValue(appTimeZone) ?? Temporal.Now.timeZoneId();\n    const originalZone = getTemporalZoneId(originalTemporal);\n    return dateToZonedDateTime(visualDate, editingTimeZone)\n      .toInstant()\n      .toZonedDateTimeISO(originalZone);\n  }\n\n  if (isPlainDateTime(originalTemporal)) {\n    return dateToPlainDateTime(visualDate);\n  }\n\n  if (isZonedDateTime(visualTemporal)) {\n    return visualTemporal;\n  }\n\n  return dateToZonedDateTime(\n    visualDate,\n    normalizeTimeZoneValue(appTimeZone) ?? Temporal.Now.timeZoneId()\n  );\n};\n\nexport const restoreVisualEventToCanonical = (\n  originalEvent: Event,\n  visualEvent: Event,\n  appTimeZone?: TimeZoneValue\n): Event => {\n  if (visualEvent.allDay) {\n    return {\n      ...visualEvent,\n      allDay: true,\n      start: isPlainDate(visualEvent.start)\n        ? visualEvent.start\n        : dateToPlainDate(temporalToDate(visualEvent.start)),\n      end: isPlainDate(visualEvent.end)\n        ? visualEvent.end\n        : dateToPlainDate(temporalToDate(visualEvent.end)),\n    };\n  }\n\n  return {\n    ...visualEvent,\n    allDay: false,\n    start: restoreVisualTimedTemporalToCanonical(\n      visualEvent.start as Temporal.PlainDateTime | Temporal.ZonedDateTime,\n      originalEvent.allDay ? visualEvent.start : originalEvent.start,\n      appTimeZone\n    ),\n    end: restoreVisualTimedTemporalToCanonical(\n      visualEvent.end as Temporal.PlainDateTime | Temporal.ZonedDateTime,\n      originalEvent.allDay ? visualEvent.end : originalEvent.end,\n      appTimeZone\n    ),\n  };\n};\n"
  },
  {
    "path": "packages/core/src/utils/timeZoneUtils.ts",
    "content": "import { TimeZone, TimeZoneValue } from '@/types/timezone';\n\nconst timeZoneCache = new Map<string, string>();\n\nfunction canonicalizeTimeZoneCandidate(timeZone: string): string | undefined {\n  try {\n    return new Intl.DateTimeFormat('en', {\n      timeZone,\n    }).resolvedOptions().timeZone;\n  } catch {\n    return undefined;\n  }\n}\n\nexport function normalizeTimeZoneValue(\n  timeZone?: TimeZoneValue\n): string | undefined {\n  if (!timeZone || typeof timeZone !== 'string') {\n    return undefined;\n  }\n\n  const trimmed = timeZone.trim();\n  if (!trimmed) {\n    return undefined;\n  }\n\n  const cached = timeZoneCache.get(trimmed);\n  if (cached) {\n    return cached;\n  }\n\n  const canonical = canonicalizeTimeZoneCandidate(trimmed);\n  if (canonical) {\n    timeZoneCache.set(trimmed, canonical);\n    return canonical;\n  }\n\n  const enumMatch = Object.values(TimeZone).find(\n    value => value.toLowerCase() === trimmed.toLowerCase()\n  );\n  if (enumMatch) {\n    const canonicalEnumMatch = canonicalizeTimeZoneCandidate(enumMatch);\n    if (canonicalEnumMatch) {\n      timeZoneCache.set(trimmed, canonicalEnumMatch);\n      return canonicalEnumMatch;\n    }\n    timeZoneCache.set(trimmed, enumMatch);\n    return enumMatch;\n  }\n\n  if (typeof Intl.supportedValuesOf === 'function') {\n    const intlMatch = Intl.supportedValuesOf('timeZone').find(\n      value => value.toLowerCase() === trimmed.toLowerCase()\n    );\n    if (intlMatch) {\n      timeZoneCache.set(trimmed, intlMatch);\n      return intlMatch;\n    }\n  }\n\n  const slashNormalized = trimmed\n    .split('/')\n    .map(segment =>\n      segment\n        .split('_')\n        .map(part =>\n          part\n            ? part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()\n            : part\n        )\n        .join('_')\n    )\n    .join('/');\n  const canonicalSlashNormalized =\n    canonicalizeTimeZoneCandidate(slashNormalized);\n  if (canonicalSlashNormalized) {\n    timeZoneCache.set(trimmed, canonicalSlashNormalized);\n    return canonicalSlashNormalized;\n  }\n\n  const upperPrefixNormalized = trimmed\n    .split('/')\n    .map((segment, index) =>\n      index === 0\n        ? segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase()\n        : segment\n    )\n    .join('/');\n  const canonicalUpperPrefixNormalized = canonicalizeTimeZoneCandidate(\n    upperPrefixNormalized\n  );\n  if (canonicalUpperPrefixNormalized) {\n    timeZoneCache.set(trimmed, canonicalUpperPrefixNormalized);\n    return canonicalUpperPrefixNormalized;\n  }\n\n  return trimmed;\n}\n"
  },
  {
    "path": "packages/core/src/utils/utilityFunctions.ts",
    "content": "/**\n * General Utility Functions\n *\n * This module provides general-purpose utility functions that don't fit\n * into other specialized categories.\n */\n\n// ============================================================================\n// General Utilities\n// ============================================================================\n\n/**\n * Generate unique key (using timestamp and random number combination)\n * @returns Unique key string\n */\nexport function generateUniKey() {\n  return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n}\n"
  },
  {
    "path": "packages/core/src/views/AgendaView.tsx",
    "content": "import { RefObject } from 'preact';\nimport {\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'preact/hooks';\n\nimport { EventDetailPanel } from '@/components/calendarEvent/components/EventDetailPanel';\nimport ViewHeader from '@/components/common/ViewHeader';\nimport { useResponsiveMonthConfig } from '@/hooks/virtualScroll';\nimport { useLocale } from '@/locale';\nimport { ContentSlot } from '@/renderer/ContentSlot';\nimport { CustomRenderingContext } from '@/renderer/CustomRenderingContext';\nimport { AgendaViewProps, Event, ICalendarApp } from '@/types';\nimport {\n  compareAllDayDisplayPriority,\n  formatTime,\n  getEventBgColor,\n  getEventTextColor,\n  getTodayInTimeZone,\n  getLineColor,\n  getPrimaryCalendarId,\n  temporalToVisualDate,\n} from '@/utils';\n\ntype AgendaEntry = {\n  event: Event;\n  timeLabel: string;\n  sortMs: number;\n  renderAsBadge: boolean;\n  isAllDay: boolean;\n  isMultiDayAllDay: boolean;\n  continuesFromPreviousDay: boolean;\n  continuesToNextDay: boolean;\n  color: string;\n  backgroundColor: string;\n  textColor: string;\n};\n\ntype AgendaDayGroup = {\n  date: Date;\n  allDayEntries: AgendaEntry[];\n  timedEntries: AgendaEntry[];\n};\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nconst normalizeDate = (date: Date): Date => {\n  const next = new Date(date);\n  next.setHours(0, 0, 0, 0);\n  return next;\n};\n\nconst addDays = (date: Date, amount: number): Date => {\n  const next = new Date(date);\n  next.setDate(next.getDate() + amount);\n  return next;\n};\n\nconst isSameDate = (a: Date, b: Date): boolean =>\n  a.getFullYear() === b.getFullYear() &&\n  a.getMonth() === b.getMonth() &&\n  a.getDate() === b.getDate();\n\nconst isMidnight = (date: Date): boolean =>\n  date.getHours() === 0 &&\n  date.getMinutes() === 0 &&\n  date.getSeconds() === 0 &&\n  date.getMilliseconds() === 0;\n\nconst getEventRangeForAgenda = (event: Event, appTimeZone: string) => {\n  const start = temporalToVisualDate(event.start, appTimeZone);\n  const end = event.end ? temporalToVisualDate(event.end, appTimeZone) : start;\n  const startDay = normalizeDate(start);\n  const endDay = normalizeDate(end);\n\n  let effectiveEndDay = new Date(endDay);\n  if (!event.allDay && !isSameDate(startDay, endDay) && isMidnight(end)) {\n    effectiveEndDay = addDays(effectiveEndDay, -1);\n  }\n\n  if (effectiveEndDay < startDay) {\n    effectiveEndDay = new Date(startDay);\n  }\n\n  return {\n    start,\n    end,\n    startDay,\n    effectiveEndDay,\n  };\n};\n\nconst formatAgendaTitle = (\n  start: Date,\n  endExclusive: Date,\n  locale: string\n): string => {\n  const endInclusive = addDays(endExclusive, -1);\n  const sameYear = start.getFullYear() === endInclusive.getFullYear();\n  const sameMonth = sameYear && start.getMonth() === endInclusive.getMonth();\n\n  if (sameMonth) {\n    return `${start.toLocaleDateString(locale, { month: 'long' })} ${start.getDate()} - ${endInclusive.getDate()}, ${start.getFullYear()}`;\n  }\n\n  if (sameYear) {\n    return `${start.toLocaleDateString(locale, { month: 'short', day: 'numeric' })} - ${endInclusive.toLocaleDateString(locale, { month: 'short', day: 'numeric' })}, ${start.getFullYear()}`;\n  }\n\n  return `${start.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })} - ${endInclusive.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })}`;\n};\n\nconst formatAgendaTimeLabel = (\n  event: Event,\n  day: Date,\n  appTimeZone: string,\n  timeFormat: '12h' | '24h'\n): { timeLabel: string; renderAsBadge: boolean; sortMs: number } => {\n  const { start, end, startDay, effectiveEndDay } = getEventRangeForAgenda(\n    event,\n    appTimeZone\n  );\n  const multiDay = effectiveEndDay.getTime() > startDay.getTime();\n\n  if (event.allDay) {\n    return {\n      timeLabel: 'All day',\n      renderAsBadge: true,\n      sortMs: day.getTime() - DAY_MS,\n    };\n  }\n\n  if (!multiDay) {\n    return {\n      timeLabel: `${formatTime(start.getHours(), start.getMinutes(), timeFormat)} - ${formatTime(end.getHours(), end.getMinutes(), timeFormat)}`,\n      renderAsBadge: false,\n      sortMs: start.getTime(),\n    };\n  }\n\n  if (isSameDate(day, startDay)) {\n    return {\n      timeLabel: `Starts ${formatTime(start.getHours(), start.getMinutes(), timeFormat)}`,\n      renderAsBadge: true,\n      sortMs: start.getTime(),\n    };\n  }\n\n  if (isSameDate(day, effectiveEndDay)) {\n    return {\n      timeLabel: `Ends ${formatTime(end.getHours(), end.getMinutes(), timeFormat)}`,\n      renderAsBadge: false,\n      sortMs: day.getTime(),\n    };\n  }\n\n  return {\n    timeLabel: 'All day',\n    renderAsBadge: true,\n    sortMs: day.getTime() - DAY_MS / 2,\n  };\n};\n\nconst buildAgendaGroups = (\n  app: ICalendarApp,\n  events: Event[],\n  startDate: Date,\n  daysToShow: number,\n  showEmptyDays: boolean,\n  timeFormat: '12h' | '24h'\n): AgendaDayGroup[] => {\n  const registry = app.getCalendarRegistry();\n  const appTimeZone = app.timeZone;\n  const groups: AgendaDayGroup[] = [];\n\n  for (let index = 0; index < daysToShow; index += 1) {\n    const day = addDays(startDate, index);\n    const dayStart = normalizeDate(day);\n    const dayEnd = addDays(dayStart, 1);\n\n    const entries = events\n      .filter(event => {\n        const { startDay, effectiveEndDay } = getEventRangeForAgenda(\n          event,\n          appTimeZone\n        );\n        return startDay < dayEnd && effectiveEndDay >= dayStart;\n      })\n      .map(event => {\n        const { startDay, effectiveEndDay } = getEventRangeForAgenda(\n          event,\n          appTimeZone\n        );\n        const { timeLabel, renderAsBadge, sortMs } = formatAgendaTimeLabel(\n          event,\n          dayStart,\n          appTimeZone,\n          timeFormat\n        );\n        const calendarId = getPrimaryCalendarId(event);\n        const isMultiDayAllDay =\n          event.allDay === true &&\n          effectiveEndDay.getTime() > startDay.getTime();\n\n        return {\n          event,\n          timeLabel,\n          sortMs,\n          renderAsBadge,\n          isAllDay: event.allDay === true,\n          isMultiDayAllDay,\n          continuesFromPreviousDay:\n            isMultiDayAllDay && !isSameDate(dayStart, startDay),\n          continuesToNextDay:\n            isMultiDayAllDay && !isSameDate(dayStart, effectiveEndDay),\n          color: getLineColor(calendarId, registry),\n          backgroundColor: getEventBgColor(calendarId, registry),\n          textColor: getEventTextColor(calendarId, registry),\n        };\n      });\n\n    const allDayEntries = entries\n      .filter(entry => entry.isAllDay)\n      .toSorted((a, b) => {\n        if (a.isMultiDayAllDay !== b.isMultiDayAllDay) {\n          return a.isMultiDayAllDay ? -1 : 1;\n        }\n\n        const allDayPriority = compareAllDayDisplayPriority(\n          a.event,\n          b.event,\n          app.state.allDaySortComparator\n        );\n        if (allDayPriority !== 0) {\n          return allDayPriority;\n        }\n\n        return a.event.title.localeCompare(b.event.title);\n      });\n\n    const timedEntries = entries\n      .filter(entry => !entry.isAllDay)\n      .toSorted((a, b) => {\n        if (a.renderAsBadge !== b.renderAsBadge) {\n          return a.renderAsBadge ? -1 : 1;\n        }\n\n        if (a.sortMs !== b.sortMs) {\n          return a.sortMs - b.sortMs;\n        }\n\n        return a.event.title.localeCompare(b.event.title);\n      });\n\n    if (showEmptyDays || allDayEntries.length > 0 || timedEntries.length > 0) {\n      groups.push({ date: dayStart, allDayEntries, timedEntries });\n    }\n  }\n\n  return groups;\n};\n\nconst AgendaView = ({\n  app,\n  config,\n  useEventDetailPanel,\n  calendarRef,\n  selectedEventId,\n  detailPanelEventId,\n  onEventSelect,\n  onDetailPanelToggle,\n}: AgendaViewProps & { calendarRef: RefObject<HTMLDivElement> }) => {\n  const { locale } = useLocale();\n  const { screenSize } = useResponsiveMonthConfig();\n  const isMobile = screenSize !== 'desktop';\n  const customRenderingStore = useContext(CustomRenderingContext);\n  const currentDate = app.getCurrentDate();\n  const events = app.getEvents();\n\n  const daysToShow =\n    Number.isFinite(config.daysToShow) && (config.daysToShow ?? 0) > 0\n      ? Math.floor(config.daysToShow as number)\n      : 14;\n  const showEmptyDays = config.showEmptyDays !== false;\n  const timeFormat = config.timeFormat ?? '24h';\n\n  const rangeStart = useMemo(() => normalizeDate(currentDate), [currentDate]);\n  const rangeEnd = useMemo(\n    () => addDays(rangeStart, daysToShow),\n    [daysToShow, rangeStart]\n  );\n\n  const groups = useMemo(\n    () =>\n      buildAgendaGroups(\n        app,\n        events,\n        rangeStart,\n        daysToShow,\n        showEmptyDays,\n        timeFormat\n      ),\n    [app, daysToShow, events, rangeStart, showEmptyDays, timeFormat]\n  );\n\n  const title = useMemo(\n    () => formatAgendaTitle(rangeStart, rangeEnd, locale),\n    [locale, rangeEnd, rangeStart]\n  );\n  const today = useMemo(() => getTodayInTimeZone(app.timeZone), [app.timeZone]);\n  const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const detailPanelRef = useRef<HTMLDivElement>(null);\n  const selectedEventElementRef = useRef<HTMLElement | null>(null);\n  const [detailPanelPosition, setDetailPanelPosition] = useState<{\n    top: number;\n    left: number;\n    eventHeight: number;\n    eventMiddleY: number;\n    isSunday?: boolean;\n  } | null>(null);\n\n  useEffect(\n    () => () => {\n      if (clickTimerRef.current) {\n        clearTimeout(clickTimerRef.current);\n      }\n    },\n    []\n  );\n\n  useEffect(() => {\n    if (!detailPanelEventId) {\n      setDetailPanelPosition(null);\n    }\n  }, [detailPanelEventId]);\n\n  useEffect(() => {\n    if (!detailPanelEventId || !detailPanelPosition) return;\n\n    const handleClickOutside = (e: MouseEvent) => {\n      const target = e.target as HTMLElement;\n      const clickedInsideEvent =\n        selectedEventElementRef.current?.contains(target) ?? false;\n      const clickedOnSameEvent =\n        target.closest(\n          `[data-event-id=\"${detailPanelEventId.split('::')[0]}\"]`\n        ) !== null;\n      const clickedInsidePanel =\n        detailPanelRef.current?.contains(target) ?? false;\n      const clickedInsideDetailDialog = target.closest(\n        '[data-event-detail-dialog]'\n      );\n      const clickedInsideRangePickerPopup = target.closest(\n        '[data-range-picker-popup]'\n      );\n      const clickedInsideCalendarPickerDropdown = target.closest(\n        '[data-calendar-picker-dropdown]'\n      );\n\n      if (\n        !clickedInsideEvent &&\n        !clickedOnSameEvent &&\n        !clickedInsidePanel &&\n        !clickedInsideDetailDialog &&\n        !clickedInsideRangePickerPopup &&\n        !clickedInsideCalendarPickerDropdown\n      ) {\n        setDetailPanelPosition(null);\n        onDetailPanelToggle?.(null);\n        onEventSelect?.(null);\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => {\n      document.removeEventListener('mousedown', handleClickOutside);\n    };\n  }, [\n    detailPanelEventId,\n    detailPanelPosition,\n    onDetailPanelToggle,\n    onEventSelect,\n  ]);\n\n  const handleEventClick = useCallback(\n    (event: Event) => {\n      if (clickTimerRef.current) {\n        clearTimeout(clickTimerRef.current);\n        clickTimerRef.current = null;\n      }\n\n      if (isMobile) {\n        app.onEventClick(event);\n        onEventSelect?.(event.id);\n        if (app.getReadOnlyConfig(event.id).viewable !== false) {\n          onDetailPanelToggle?.(event.id);\n        }\n        return;\n      }\n\n      clickTimerRef.current = setTimeout(() => {\n        app.onEventClick(event);\n        onEventSelect?.(event.id);\n        if (useEventDetailPanel !== false) {\n          onDetailPanelToggle?.(null);\n        }\n        clickTimerRef.current = null;\n      }, 180);\n    },\n    [app, isMobile, onDetailPanelToggle, onEventSelect, useEventDetailPanel]\n  );\n\n  const handleEventDoubleClick = useCallback(\n    (event: Event, nativeEvent: MouseEvent) => {\n      if (clickTimerRef.current) {\n        clearTimeout(clickTimerRef.current);\n        clickTimerRef.current = null;\n      }\n      onEventSelect?.(null);\n      selectedEventElementRef.current =\n        nativeEvent.currentTarget as HTMLElement;\n      Promise.resolve(app.onEventDoubleClick(event, nativeEvent))\n        .then(result => {\n          if (result === false) return;\n          if (app.getReadOnlyConfig(event.id).viewable !== false) {\n            const target = selectedEventElementRef.current;\n            if (\n              target &&\n              useEventDetailPanel !== false &&\n              !app.getUseEventDetailDialog()\n            ) {\n              const rect = target.getBoundingClientRect();\n              const panelWidth = 320;\n              const gap = 12;\n              const viewportPadding = 16;\n              const viewportHeight = window.innerHeight;\n              const viewportWidth = window.innerWidth;\n              const placeLeft =\n                rect.right + gap + panelWidth > viewportWidth - viewportPadding;\n              const left = placeLeft\n                ? Math.max(viewportPadding, rect.left - panelWidth - gap)\n                : Math.min(\n                    viewportWidth - panelWidth - viewportPadding,\n                    rect.right + gap\n                  );\n              const top = Math.min(\n                Math.max(viewportPadding, rect.top + rect.height / 2 - 180),\n                viewportHeight - 380\n              );\n\n              setDetailPanelPosition({\n                top,\n                left,\n                eventHeight: rect.height,\n                eventMiddleY: rect.top + rect.height / 2,\n                isSunday: placeLeft,\n              });\n            }\n            onDetailPanelToggle?.(event.id);\n          }\n        })\n        .catch(() => {\n          if (app.getReadOnlyConfig(event.id).viewable !== false) {\n            const target = selectedEventElementRef.current;\n            if (\n              target &&\n              useEventDetailPanel !== false &&\n              !app.getUseEventDetailDialog()\n            ) {\n              const rect = target.getBoundingClientRect();\n              const panelWidth = 320;\n              const gap = 12;\n              const viewportPadding = 16;\n              const viewportHeight = window.innerHeight;\n              const viewportWidth = window.innerWidth;\n              const placeLeft =\n                rect.right + gap + panelWidth > viewportWidth - viewportPadding;\n              const left = placeLeft\n                ? Math.max(viewportPadding, rect.left - panelWidth - gap)\n                : Math.min(\n                    viewportWidth - panelWidth - viewportPadding,\n                    rect.right + gap\n                  );\n              const top = Math.min(\n                Math.max(viewportPadding, rect.top + rect.height / 2 - 180),\n                viewportHeight - 380\n              );\n\n              setDetailPanelPosition({\n                top,\n                left,\n                eventHeight: rect.height,\n                eventMiddleY: rect.top + rect.height / 2,\n                isSunday: placeLeft,\n              });\n            }\n            onDetailPanelToggle?.(event.id);\n          }\n        });\n    },\n    [app, onDetailPanelToggle, onEventSelect, useEventDetailPanel]\n  );\n\n  const handleGridDateClick = useCallback(\n    (date: Date, dayEvents: Event[]) => {\n      const clickAction = config?.gridDateClick;\n      if (!clickAction) return;\n\n      if (typeof clickAction === 'function') {\n        clickAction(date, dayEvents);\n        return;\n      }\n\n      if (clickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView('day');\n      }\n    },\n    [app, config?.gridDateClick]\n  );\n\n  const handleGridDateDoubleClick = useCallback(\n    (date: Date, dayEvents: Event[]) => {\n      const dblClickAction = config?.gridDateDoubleClick ?? 'day-view';\n\n      if (typeof dblClickAction === 'function') {\n        dblClickAction(date, dayEvents);\n        return;\n      }\n\n      if (dblClickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView('day');\n      }\n    },\n    [app, config?.gridDateDoubleClick]\n  );\n\n  const renderAgendaEventButton = (entry: AgendaEntry) => (\n    <button\n      type='button'\n      className='df-agenda-event'\n      data-selected={selectedEventId === entry.event.id ? 'true' : 'false'}\n      onClick={e => {\n        e.stopPropagation();\n        handleEventClick(entry.event);\n      }}\n      onDblClick={e => {\n        e.stopPropagation();\n        handleEventDoubleClick(entry.event, e as unknown as MouseEvent);\n      }}\n    >\n      {entry.renderAsBadge ? (\n        <span\n          className='df-agenda-badge'\n          data-continued-left={\n            entry.continuesFromPreviousDay ? 'true' : 'false'\n          }\n          data-continued-right={entry.continuesToNextDay ? 'true' : 'false'}\n          style={{\n            backgroundColor: entry.backgroundColor,\n            color: entry.textColor,\n          }}\n        >\n          {entry.event.title}\n        </span>\n      ) : (\n        <span\n          className='df-agenda-dot'\n          style={{ backgroundColor: entry.color }}\n        />\n      )}\n      {!entry.renderAsBadge && (\n        <span className='df-agenda-title'>{entry.event.title}</span>\n      )}\n    </button>\n  );\n\n  const detailPanelEvent =\n    detailPanelPosition && detailPanelEventId\n      ? (events.find(event => event.id === detailPanelEventId.split('::')[0]) ??\n        null)\n      : null;\n\n  const contentSlotRenderer = useCallback(\n    (contentProps: {\n      event: Event;\n      isAllDay: boolean;\n      onEventUpdate: (updatedEvent: Event) => void | Promise<void>;\n      onEventDelete: (eventId: string) => void | Promise<void>;\n      onClose?: () => void;\n      app?: ICalendarApp;\n    }) => (\n      <ContentSlot\n        store={customRenderingStore}\n        generatorName='eventDetailContent'\n        generatorArgs={contentProps}\n      />\n    ),\n    [customRenderingStore]\n  );\n\n  return (\n    <div className='df-agenda-view'>\n      <ViewHeader\n        calendar={app}\n        viewType='month'\n        currentDate={rangeStart}\n        customTitle={title}\n        onPrevious={() => app.goToPrevious()}\n        onNext={() => app.goToNext()}\n        onToday={() => app.goToToday()}\n      />\n\n      <div className='df-agenda-scroll'>\n        {groups.length === 0 ? (\n          <div className='df-agenda-empty'>No events in this range.</div>\n        ) : (\n          groups.map(group => (\n            <section\n              key={group.date.toISOString()}\n              className='df-agenda-day-section'\n              data-today={isSameDate(group.date, today) ? 'true' : 'false'}\n              onClick={() =>\n                handleGridDateClick(group.date, [\n                  ...group.allDayEntries.map(entry => entry.event),\n                  ...group.timedEntries.map(entry => entry.event),\n                ])\n              }\n              onDblClick={() =>\n                handleGridDateDoubleClick(group.date, [\n                  ...group.allDayEntries.map(entry => entry.event),\n                  ...group.timedEntries.map(entry => entry.event),\n                ])\n              }\n            >\n              <div className='df-agenda-day-rail'>\n                <div\n                  className='df-agenda-day-number'\n                  data-today={isSameDate(group.date, today) ? 'true' : 'false'}\n                >\n                  {group.date.getDate()}\n                </div>\n                <div className='df-agenda-day-meta'>\n                  <div className='df-agenda-day-month'>\n                    {group.date.toLocaleDateString(locale, {\n                      month: 'short',\n                      year: 'numeric',\n                    })}\n                  </div>\n                  <div className='df-agenda-day-name'>\n                    {group.date.toLocaleDateString(locale, { weekday: 'long' })}\n                  </div>\n                </div>\n              </div>\n\n              <div className='df-agenda-day-content'>\n                {group.allDayEntries.length === 0 &&\n                group.timedEntries.length === 0 ? (\n                  <div className='df-agenda-empty-row'>No events</div>\n                ) : (\n                  <>\n                    {group.allDayEntries.length > 0 && (\n                      <div className='df-agenda-item df-agenda-item-all-day'>\n                        <div className='df-agenda-time'>All day</div>\n                        <div className='df-agenda-all-day-events'>\n                          {group.allDayEntries.map(entry => (\n                            <div\n                              key={`${group.date.toISOString()}-${entry.event.id}`}\n                              className='df-agenda-all-day-event'\n                            >\n                              {renderAgendaEventButton(entry)}\n                            </div>\n                          ))}\n                        </div>\n                      </div>\n                    )}\n\n                    {group.timedEntries.map(entry => (\n                      <div\n                        key={`${group.date.toISOString()}-${entry.event.id}`}\n                        className='df-agenda-item'\n                      >\n                        <div className='df-agenda-time'>{entry.timeLabel}</div>\n                        {renderAgendaEventButton(entry)}\n                      </div>\n                    ))}\n                  </>\n                )}\n              </div>\n            </section>\n          ))\n        )}\n      </div>\n\n      {detailPanelEvent &&\n        useEventDetailPanel !== false &&\n        !app.getUseEventDetailDialog() && (\n          <>\n            <div\n              style={{\n                position: 'fixed',\n                top: 0,\n                left: 0,\n                right: 0,\n                bottom: 0,\n                zIndex: 9998,\n                pointerEvents: 'none',\n              }}\n            />\n            <EventDetailPanel\n              showDetailPanel={true}\n              detailPanelPosition={detailPanelPosition}\n              event={detailPanelEvent}\n              detailPanelRef={detailPanelRef}\n              isAllDay={detailPanelEvent.allDay === true}\n              eventVisibility='standard'\n              calendarRef={calendarRef}\n              selectedEventElementRef={selectedEventElementRef}\n              onEventUpdate={event => app.updateEvent(event.id, event)}\n              onEventDelete={id => app.deleteEvent(id)}\n              handlePanelClose={() => {\n                setDetailPanelPosition(null);\n                onDetailPanelToggle?.(null);\n                onEventSelect?.(null);\n              }}\n              customRenderingStore={customRenderingStore}\n              contentSlotRenderer={contentSlotRenderer}\n              app={app}\n            />\n          </>\n        )}\n    </div>\n  );\n};\n\nexport default AgendaView;\n"
  },
  {
    "path": "packages/core/src/views/DayView.tsx",
    "content": "import { JSX, RefObject } from 'preact';\nimport {\n  useState,\n  useEffect,\n  useMemo,\n  useCallback,\n  useRef,\n  useLayoutEffect,\n} from 'preact/hooks';\n\nimport { DayContent } from '@/components/dayView/DayContent';\nimport { RightPanel } from '@/components/dayView/RightPanel';\nimport {\n  filterDayEvents,\n  normalizeLayoutEvents,\n  organizeAllDayEvents,\n  calculateNewEventLayout,\n  calculateDragLayout,\n} from '@/components/dayView/util';\nimport { EventLayoutCalculator } from '@/components/eventLayout';\nimport { getWeekStart } from '@/components/weekView/util';\nimport { defaultDragConfig } from '@/core/config';\nimport { useCalendarDrop } from '@/hooks/useCalendarDrop';\nimport { useResponsiveMonthConfig } from '@/hooks/virtualScroll';\nimport { useDragForView } from '@/plugins/dragBridge';\nimport {\n  Event,\n  DayViewProps,\n  ViewType as DragViewType,\n  WeekDayDragState,\n} from '@/types';\nimport {\n  formatTime,\n  extractHourFromDate,\n  generateSecondaryTimeSlots,\n  getTimezoneDisplayLabel,\n  getNowInTimeZone,\n  getTodayInTimeZone,\n  hasEventChanged,\n} from '@/utils';\n\nconst DayView = ({\n  app,\n  config,\n  useEventDetailPanel,\n  calendarRef,\n  switcherMode = 'buttons',\n  selectedEventId: propSelectedEventId,\n  onEventSelect: propOnEventSelect,\n  onDateChange,\n  detailPanelEventId: propDetailPanelEventId,\n  onDetailPanelToggle: propOnDetailPanelToggle,\n}: DayViewProps & { calendarRef: RefObject<HTMLDivElement> }) => {\n  const events = app.getEvents();\n  const { screenSize } = useResponsiveMonthConfig();\n  const isMobile = screenSize !== 'desktop';\n  const [isTouch, setIsTouch] = useState(false);\n\n  // Configuration from the typed config object\n  const {\n    hourHeight: configHourHeight = defaultDragConfig.HOUR_HEIGHT,\n    firstHour: configFirstHour = defaultDragConfig.FIRST_HOUR,\n    lastHour: configLastHour = defaultDragConfig.LAST_HOUR,\n    allDayHeight: configAllDayHeight = defaultDragConfig.ALL_DAY_HEIGHT,\n    showAllDay = true,\n    timeFormat = '24h',\n    secondaryTimeZone,\n    showEventDots = true,\n  } = config;\n\n  const HOUR_HEIGHT = configHourHeight;\n  const FIRST_HOUR = configFirstHour;\n  const LAST_HOUR = configLastHour;\n  const ALL_DAY_HEIGHT = configAllDayHeight;\n\n  const showStartOfDayLabel = !showAllDay;\n\n  useEffect(() => {\n    setIsTouch('ontouchstart' in window || navigator.maxTouchPoints > 0);\n  }, []);\n\n  const [currentTime, setCurrentTime] = useState<Date | null>(null);\n  const [internalSelectedId, setInternalSelectedId] = useState<string | null>(\n    null\n  );\n  const [internalDetailPanelEventId, setInternalDetailPanelEventId] = useState<\n    string | null\n  >(null);\n\n  const selectedEventId =\n    propSelectedEventId === undefined\n      ? internalSelectedId\n      : propSelectedEventId;\n  const detailPanelEventId =\n    propDetailPanelEventId === undefined\n      ? internalDetailPanelEventId\n      : propDetailPanelEventId;\n\n  const selectedEvent = useMemo(\n    () =>\n      selectedEventId\n        ? events.find(e => e.id === selectedEventId) || null\n        : null,\n    [selectedEventId, events]\n  );\n\n  const setSelectedEventId = (id: string | null) => {\n    if (propOnEventSelect) {\n      propOnEventSelect(id);\n    } else {\n      setInternalSelectedId(id);\n    }\n  };\n\n  const setDetailPanelEventId = (id: string | null) => {\n    if (propOnDetailPanelToggle) {\n      propOnDetailPanelToggle(id);\n    } else {\n      setInternalDetailPanelEventId(id);\n    }\n  };\n\n  const [newlyCreatedEventId, setNewlyCreatedEventId] = useState<string | null>(\n    null\n  );\n\n  const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const currentDate = app.getCurrentDate();\n  // const visibleMonthDate = app.getVisibleMonth();\n  // Visible Month State\n  const [visibleMonth, setVisibleMonth] = useState(currentDate);\n  const prevDateRef = useRef(currentDate.getTime());\n\n  if (currentDate.getTime() !== prevDateRef.current) {\n    prevDateRef.current = currentDate.getTime();\n    if (\n      currentDate.getFullYear() !== visibleMonth.getFullYear() ||\n      currentDate.getMonth() !== visibleMonth.getMonth()\n    ) {\n      setVisibleMonth(currentDate);\n    }\n  }\n\n  const handleMonthChange = useCallback(\n    (offset: number) => {\n      setVisibleMonth(prev => {\n        const next = new Date(prev.getFullYear(), prev.getMonth() + offset, 1);\n        app.setVisibleMonth(next);\n        return next;\n      });\n    },\n    [app]\n  );\n\n  // Sync highlighted event from app state\n  const prevHighlightedEventId = useRef(app.state.highlightedEventId);\n\n  useEffect(() => {\n    const hasChanged =\n      app.state.highlightedEventId !== prevHighlightedEventId.current;\n\n    if (hasChanged) {\n      if (app.state.highlightedEventId) {\n        // setSelectedEventId is called here, but should be careful about loops if propOnEventSelect updates app state\n        // In this case, highlight comes from app state, so just sync local selection\n        setSelectedEventId(app.state.highlightedEventId);\n\n        const currentEvents = app.getEvents();\n        const event = currentEvents.find(\n          e => e.id === app.state.highlightedEventId\n        );\n        if (event && !event.allDay) {\n          const startHour = extractHourFromDate(event.start);\n          const scrollContainer = calendarRef.current?.querySelector(\n            '.df-calendar-content'\n          );\n          if (scrollContainer) {\n            const top = (startHour - FIRST_HOUR) * HOUR_HEIGHT;\n            requestAnimationFrame(() => {\n              scrollContainer.scrollTo({\n                top: Math.max(0, top - 100),\n                behavior: 'smooth',\n              });\n            });\n          }\n        }\n      } else if (\n        prevHighlightedEventId.current &&\n        selectedEventId === prevHighlightedEventId.current\n      ) {\n        setSelectedEventId(null);\n      }\n    }\n    prevHighlightedEventId.current = app.state.highlightedEventId;\n  }, [app.state.highlightedEventId, FIRST_HOUR, HOUR_HEIGHT, calendarRef, app]);\n\n  // References\n  const allDayRowRef = useRef<HTMLDivElement>(null);\n  const timeGridRef = useRef<HTMLDivElement>(null);\n\n  // Calculate the week start time for the current date\n  const currentWeekStart = useMemo(\n    () => getWeekStart(currentDate),\n    [currentDate]\n  );\n\n  const appTimeZone = app.timeZone;\n\n  // Events for the current date\n  const currentDayEvents = useMemo(\n    () => filterDayEvents(events, currentDate, currentWeekStart, appTimeZone),\n    [events, currentDate, currentWeekStart, appTimeZone]\n  );\n\n  // Prepare events for layout calculation\n  const layoutEvents = useMemo(\n    () => normalizeLayoutEvents(currentDayEvents, currentDate, appTimeZone),\n    [currentDayEvents, currentDate, appTimeZone]\n  );\n\n  // Calculate event layouts\n  const eventLayouts = useMemo(\n    () =>\n      EventLayoutCalculator.calculateDayEventLayouts(layoutEvents, {\n        viewType: 'day',\n      }),\n    [layoutEvents]\n  );\n\n  // Organize all-day events into rows to avoid overlap\n  const organizedAllDayEvents = useMemo(\n    () =>\n      organizeAllDayEvents(currentDayEvents, app.state.allDaySortComparator),\n    [currentDayEvents, app.state.allDaySortComparator]\n  );\n\n  const allDayAreaHeight = useMemo(() => {\n    if (organizedAllDayEvents.length === 0) return ALL_DAY_HEIGHT;\n    const maxRow = Math.max(...organizedAllDayEvents.map(e => e.row));\n    return (maxRow + 1) * ALL_DAY_HEIGHT;\n  }, [organizedAllDayEvents, ALL_DAY_HEIGHT]);\n\n  const sidebarWidth = secondaryTimeZone && !isMobile ? 88 : isMobile ? 48 : 80;\n\n  const handleEventsUpdate = useCallback(\n    (\n      updateFunc: (events: Event[]) => Event[],\n      _isResizing?: boolean,\n      source?: 'drag' | 'resize'\n    ) => {\n      const prevEvents = currentDayEvents;\n      const newEvents = updateFunc(prevEvents);\n\n      const prevMap = new Map(prevEvents.map(e => [e.id, e]));\n      const newSet = new Set(newEvents.map(e => e.id));\n\n      const eventsToDelete = prevEvents.filter(e => !newSet.has(e.id));\n      const eventsToAdd = newEvents.filter(e => !prevMap.has(e.id));\n      // Reference equality short-circuits hasEventChanged for unchanged events\n      const eventsToUpdate = newEvents.filter(e => {\n        const old = prevMap.get(e.id);\n        return old !== undefined && old !== e && hasEventChanged(old, e);\n      });\n\n      // Apply batched changes.\n      // Non-drag updates notify onEventBatchChange; drag/resize persistence is\n      // handled separately via onEventDrop/onEventResize.\n      app.applyEventsChanges(\n        {\n          delete: eventsToDelete.map(e => e.id),\n          add: eventsToAdd,\n          update: eventsToUpdate.map(e => ({ id: e.id, updates: e })),\n        },\n        undefined,\n        source\n      );\n    },\n    [currentDayEvents, app]\n  );\n\n  const handleEventCreate = useCallback(\n    (event: Event) => {\n      if (isMobile) {\n        app.onMobileEventDetailToggle(event);\n      } else {\n        app.addEvent(event);\n        setNewlyCreatedEventId(event.id);\n      }\n    },\n    [isMobile, app]\n  );\n\n  const handleEventEdit = useCallback(() => {\n    /* noop */\n  }, []);\n\n  const handleCalculateNewEventLayout = useCallback(\n    (targetDay: number, startHour: number, endHour: number) =>\n      calculateNewEventLayout(\n        targetDay,\n        startHour,\n        endHour,\n        currentDate,\n        layoutEvents,\n        appTimeZone\n      ),\n    [currentDate, layoutEvents, appTimeZone]\n  );\n\n  const handleCalculateDragLayout = useCallback(\n    (\n      draggedEvent: Event,\n      targetDay: number,\n      targetStartHour: number,\n      targetEndHour: number\n    ) =>\n      calculateDragLayout(\n        draggedEvent,\n        targetDay,\n        targetStartHour,\n        targetEndHour,\n        currentDate,\n        layoutEvents,\n        appTimeZone\n      ),\n    [currentDate, layoutEvents, appTimeZone]\n  );\n\n  const dragOptions = useMemo(\n    () => ({\n      calendarRef,\n      allDayRowRef: showAllDay ? allDayRowRef : undefined,\n      timeGridRef,\n      viewType: DragViewType.DAY,\n      onEventsUpdate: handleEventsUpdate,\n      onEventCreate: handleEventCreate,\n      onEventEdit: handleEventEdit,\n      currentWeekStart,\n      events: currentDayEvents,\n      calculateNewEventLayout: handleCalculateNewEventLayout,\n      calculateDragLayout: handleCalculateDragLayout,\n      TIME_COLUMN_WIDTH: sidebarWidth,\n      isMobile,\n      HOUR_HEIGHT,\n      FIRST_HOUR,\n    }),\n    [\n      calendarRef,\n      showAllDay,\n      handleEventsUpdate,\n      handleEventCreate,\n      handleEventEdit,\n      currentWeekStart,\n      currentDayEvents,\n      handleCalculateNewEventLayout,\n      handleCalculateDragLayout,\n      sidebarWidth,\n      isMobile,\n      HOUR_HEIGHT,\n      FIRST_HOUR,\n    ]\n  );\n\n  // Use drag functionality provided by the plugin\n  const {\n    handleMoveStart,\n    handleCreateStart,\n    handleResizeStart,\n    handleCreateAllDayEvent,\n    dragState,\n    isDragging,\n  } = useDragForView(app, dragOptions);\n\n  const handleTouchStart = (e: TouchEvent, dayIndex: number) => {\n    if (!isMobile && !isTouch) return;\n    const touch = e.touches[0];\n    const clientX = touch.clientX;\n    const clientY = touch.clientY;\n    const target = e.currentTarget as HTMLElement;\n\n    longPressTimerRef.current = setTimeout(() => {\n      const rect = (calendarRef.current as HTMLElement)\n        ?.querySelector('.df-calendar-content')\n        ?.getBoundingClientRect();\n\n      if (!rect) return;\n      const container = (calendarRef.current as HTMLElement)?.querySelector(\n        '.df-calendar-content'\n      );\n      const scrollTop = container ? container.scrollTop : 0;\n      const relativeY = clientY - rect.top + scrollTop;\n      const clickedHour = FIRST_HOUR + relativeY / HOUR_HEIGHT;\n\n      const mockEvent = {\n        preventDefault: () => {\n          /* noop */\n        },\n        stopPropagation: () => {\n          /* noop */\n        },\n        touches: [{ clientX, clientY }],\n        changedTouches: [{ clientX, clientY }],\n        target: target,\n        currentTarget: target,\n        cancelable: true,\n      } as unknown as TouchEvent;\n\n      handleCreateStart?.(mockEvent, dayIndex, clickedHour);\n    }, 500);\n  };\n\n  const handleTouchEnd = () => {\n    if (longPressTimerRef.current) {\n      clearTimeout(longPressTimerRef.current);\n      longPressTimerRef.current = null;\n    }\n  };\n\n  const handleTouchMove = () => {\n    if (longPressTimerRef.current) {\n      clearTimeout(longPressTimerRef.current);\n      longPressTimerRef.current = null;\n    }\n  };\n\n  // Use calendar drop functionality\n  const { handleDrop, handleDragOver } = useCalendarDrop({\n    app,\n    onEventCreated: (event: Event) => {\n      setNewlyCreatedEventId(event.id);\n    },\n  });\n\n  // Event handling functions\n  const handleEventUpdate = (updatedEvent: Event) =>\n    app.updateEvent(updatedEvent.id, updatedEvent);\n\n  const handleEventDelete = (eventId: string) => app.deleteEvent(eventId);\n\n  const timeSlots = Array.from({ length: 24 }, (_, i) => ({\n    hour: i + FIRST_HOUR,\n    label: formatTime(i + FIRST_HOUR, 0, timeFormat),\n  }));\n\n  const secondaryTimeSlots = useMemo(\n    () =>\n      secondaryTimeZone\n        ? generateSecondaryTimeSlots(\n            timeSlots,\n            secondaryTimeZone,\n            timeFormat,\n            currentDate,\n            appTimeZone\n          )\n        : undefined,\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [secondaryTimeZone, timeFormat, FIRST_HOUR, currentDate, appTimeZone]\n  );\n\n  const primaryTzLabel = useMemo(\n    () =>\n      secondaryTimeZone\n        ? getTimezoneDisplayLabel(appTimeZone, currentDate)\n        : undefined,\n    [secondaryTimeZone, appTimeZone, currentDate]\n  );\n\n  const secondaryTzLabel = useMemo(\n    () =>\n      secondaryTimeZone\n        ? getTimezoneDisplayLabel(secondaryTimeZone, currentDate)\n        : undefined,\n    [secondaryTimeZone, currentDate]\n  );\n\n  // Date selection handling\n  const handleDateSelect = useCallback(\n    (date: Date) => {\n      const nextDate = new Date(\n        date.getFullYear(),\n        date.getMonth(),\n        date.getDate()\n      );\n      app.setCurrentDate(nextDate);\n      setVisibleMonth(new Date(nextDate.getFullYear(), nextDate.getMonth(), 1));\n    },\n    [app]\n  );\n  // Check if it is today in app timezone\n  const isToday = useMemo(() => {\n    const todayLocal = getTodayInTimeZone(appTimeZone);\n    return currentDate.toDateString() === todayLocal.toDateString();\n  }, [currentDate, appTimeZone]);\n\n  // Sync scroll on mount\n  useLayoutEffect(() => {\n    if (config.scrollToCurrentTime) {\n      const scrollContainer = calendarRef.current?.querySelector(\n        '.df-calendar-content'\n      );\n      if (scrollContainer) {\n        const now = getNowInTimeZone(appTimeZone);\n        const hour = now.getHours() + now.getMinutes() / 60;\n        const containerHeight = (scrollContainer as HTMLElement).clientHeight;\n\n        scrollContainer.scrollTop = Math.max(\n          0,\n          (hour - FIRST_HOUR) * HOUR_HEIGHT - containerHeight / 2\n        );\n      }\n    }\n  }, [appTimeZone, config.scrollToCurrentTime, FIRST_HOUR, HOUR_HEIGHT]); // Run on mount and timezone changes\n\n  // Timer\n  useEffect(() => {\n    setCurrentTime(getNowInTimeZone(appTimeZone));\n    const timer = setInterval(\n      () => setCurrentTime(getNowInTimeZone(appTimeZone)),\n      60_000\n    );\n    return () => clearInterval(timer);\n  }, [appTimeZone]);\n\n  return (\n    <div\n      className='df-day-view'\n      style={{ '--df-hour-height': `${HOUR_HEIGHT}px` } as JSX.CSSProperties}\n    >\n      <DayContent\n        app={app}\n        currentDate={currentDate}\n        currentWeekStart={currentWeekStart}\n        events={events}\n        currentDayEvents={currentDayEvents}\n        organizedAllDayEvents={organizedAllDayEvents}\n        allDayAreaHeight={allDayAreaHeight}\n        timeSlots={timeSlots}\n        eventLayouts={eventLayouts}\n        isToday={isToday}\n        currentTime={currentTime}\n        selectedEventId={selectedEventId}\n        setSelectedEventId={setSelectedEventId}\n        newlyCreatedEventId={newlyCreatedEventId}\n        setNewlyCreatedEventId={setNewlyCreatedEventId}\n        detailPanelEventId={detailPanelEventId}\n        setDetailPanelEventId={setDetailPanelEventId}\n        dragState={dragState as WeekDayDragState | null}\n        isDragging={isDragging}\n        handleMoveStart={\n          handleMoveStart as (e: MouseEvent | TouchEvent, event: Event) => void\n        }\n        handleResizeStart={\n          handleResizeStart as (\n            e: MouseEvent | TouchEvent,\n            event: Event,\n            direction: string\n          ) => void\n        }\n        handleCreateStart={\n          handleCreateStart as (\n            e: MouseEvent | TouchEvent,\n            dayIndex: number,\n            hour: number\n          ) => void\n        }\n        handleCreateAllDayEvent={\n          handleCreateAllDayEvent as (\n            e: MouseEvent | TouchEvent,\n            dayIndex: number\n          ) => void\n        }\n        handleTouchStart={handleTouchStart}\n        handleTouchEnd={handleTouchEnd}\n        handleTouchMove={handleTouchMove}\n        handleDragOver={handleDragOver}\n        handleDrop={handleDrop}\n        handleEventUpdate={handleEventUpdate}\n        handleEventDelete={handleEventDelete}\n        onDateChange={onDateChange}\n        useEventDetailPanel={useEventDetailPanel}\n        calendarRef={calendarRef}\n        allDayRowRef={allDayRowRef}\n        timeGridRef={timeGridRef}\n        switcherMode={switcherMode}\n        isMobile={isMobile}\n        isTouch={isTouch}\n        setDraftEvent={event => app.onMobileEventDetailToggle(event)}\n        setIsDrawerOpen={isOpen => {\n          if (!isOpen) app.onMobileEventDetailToggle(null);\n        }}\n        ALL_DAY_HEIGHT={ALL_DAY_HEIGHT}\n        HOUR_HEIGHT={HOUR_HEIGHT}\n        FIRST_HOUR={FIRST_HOUR}\n        LAST_HOUR={LAST_HOUR}\n        showAllDay={showAllDay}\n        showStartOfDayLabel={showStartOfDayLabel}\n        timeFormat={timeFormat}\n        secondaryTimeSlots={secondaryTimeSlots}\n        primaryTzLabel={primaryTzLabel}\n        secondaryTzLabel={secondaryTzLabel}\n        appTimeZone={appTimeZone}\n      />\n      <RightPanel\n        app={app}\n        currentDate={currentDate}\n        visibleMonth={visibleMonth}\n        currentDayEvents={currentDayEvents}\n        selectedEvent={selectedEvent}\n        setSelectedEvent={e => setSelectedEventId(e ? e.id : null)}\n        handleMonthChange={handleMonthChange}\n        handleDateSelect={handleDateSelect}\n        switcherMode={switcherMode}\n        timeFormat={timeFormat}\n        showEventDots={showEventDots}\n        appTimeZone={appTimeZone}\n        calendarRef={calendarRef}\n      />\n    </div>\n  );\n};\n\nexport default DayView;\n"
  },
  {
    "path": "packages/core/src/views/MonthView.tsx",
    "content": "import { RefObject } from 'preact';\nimport {\n  useState,\n  useMemo,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useCallback,\n} from 'preact/hooks';\n\nimport ViewHeader from '@/components/common/ViewHeader';\nimport WeekComponent from '@/components/monthView/WeekComponent';\nimport { useCalendarDrop } from '@/hooks/useCalendarDrop';\nimport { useDebouncedValue } from '@/hooks/useDebouncedValue';\nimport {\n  useVirtualMonthScroll,\n  useResponsiveMonthConfig,\n} from '@/hooks/virtualScroll';\nimport { useLocale } from '@/locale';\nimport { useDragForView } from '@/plugins/dragBridge';\nimport {\n  monthViewContainer,\n  weekHeaderRow,\n  weekGrid,\n  dayLabel,\n  scrollContainer,\n} from '@/styles/classNames';\nimport {\n  Event,\n  MonthEventDragState,\n  ViewType,\n  MonthViewProps,\n  WeeksData,\n} from '@/types';\nimport {\n  dateToPlainDate,\n  dateToZonedDateTime,\n  hasEventChanged,\n  generateWeekData,\n  temporalToVisualDate,\n  generateUniKey,\n} from '@/utils';\n\n/** Compute the 6 weeks that fill a month-view grid for the given date. */\nconst getMonthWeeks = (date: Date, startOfWeek: number): WeeksData[] => {\n  const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);\n  const diff = (firstDay.getDay() - startOfWeek + 7) % 7;\n  const gridStart = new Date(firstDay);\n  gridStart.setDate(firstDay.getDate() - diff);\n  gridStart.setHours(0, 0, 0, 0);\n\n  const weeks: WeeksData[] = [];\n  for (let i = 0; i < 6; i++) {\n    const weekStart = new Date(gridStart);\n    weekStart.setDate(gridStart.getDate() + i * 7);\n    weeks.push(generateWeekData(weekStart));\n  }\n  return weeks;\n};\n\nconst getWeekStartForDate = (date: Date, startOfWeek: number) => {\n  const weekStart = new Date(date);\n  weekStart.setHours(0, 0, 0, 0);\n  const day = weekStart.getDay();\n  const diff = (day - startOfWeek + 7) % 7;\n  weekStart.setDate(weekStart.getDate() - diff);\n  weekStart.setHours(0, 0, 0, 0);\n  return weekStart;\n};\n\nconst getBucketedEventDateRange = (event: Event, appTimeZone?: string) => {\n  const startFull = temporalToVisualDate(event.start, appTimeZone);\n  const endFull = event.end\n    ? temporalToVisualDate(event.end, appTimeZone)\n    : startFull;\n\n  const startDate = new Date(startFull);\n  startDate.setHours(0, 0, 0, 0);\n\n  const endDate = new Date(endFull);\n  endDate.setHours(0, 0, 0, 0);\n\n  let adjustedEnd = new Date(endDate);\n\n  if (!event.allDay) {\n    const hasTimeComponent =\n      endFull.getHours() !== 0 ||\n      endFull.getMinutes() !== 0 ||\n      endFull.getSeconds() !== 0 ||\n      endFull.getMilliseconds() !== 0;\n\n    if (!hasTimeComponent) {\n      adjustedEnd.setDate(adjustedEnd.getDate() - 1);\n    }\n  }\n\n  if (adjustedEnd < startDate) {\n    adjustedEnd = new Date(startDate);\n  }\n\n  return { startDate, adjustedEnd };\n};\n\nconst eventOverlapsWeek = (\n  event: Event,\n  weekStart: Date,\n  startOfWeek: number,\n  appTimeZone?: string\n) => {\n  const { startDate, adjustedEnd } = getBucketedEventDateRange(\n    event,\n    appTimeZone\n  );\n  const eventWeekStart = getWeekStartForDate(startDate, startOfWeek).getTime();\n  const eventWeekEnd = getWeekStartForDate(adjustedEnd, startOfWeek).getTime();\n  const targetWeek = getWeekStartForDate(weekStart, startOfWeek).getTime();\n\n  return targetWeek >= eventWeekStart && targetWeek <= eventWeekEnd;\n};\n\nconst STATIC_TRANSITION_DURATION = 300;\nconst EMPTY_WEEK_EVENTS: Event[] = [];\n\nconst MonthView = ({\n  app,\n  config,\n  onDateChange: _,\n  useEventDetailPanel,\n  calendarRef,\n  selectedEventId: propSelectedEventId,\n  onEventSelect: propOnEventSelect,\n  detailPanelEventId: propDetailPanelEventId,\n  onDetailPanelToggle: propOnDetailPanelToggle,\n}: MonthViewProps & { calendarRef: RefObject<HTMLDivElement> }) => {\n  const { t, getWeekDaysLabels, getMonthLabels, locale } = useLocale();\n  const currentDate = app.getCurrentDate();\n  const rawEvents = app.getEvents();\n  const startOfWeek = config.startOfWeek ?? 1;\n  const appTimeZone = app.timeZone;\n\n  const scrollDisabled = config.scroll?.disabled === true;\n  const isFadeMode = scrollDisabled && config.scroll?.transition === 'fade';\n\n  const [fadeDisplayDate, setFadeDisplayDate] = useState(currentDate);\n  const [isFadingOut, setIsFadingOut] = useState(false);\n  const [fadeDirection, setFadeDirection] = useState<'forward' | 'backward'>(\n    'forward'\n  );\n  const prevFadeDateRef = useRef(currentDate);\n  const fadeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const currentDateYear = currentDate.getFullYear();\n  const currentDateMonth = currentDate.getMonth();\n\n  useEffect(() => {\n    if (!isFadeMode) return;\n    if (\n      currentDateYear === prevFadeDateRef.current.getFullYear() &&\n      currentDateMonth === prevFadeDateRef.current.getMonth()\n    )\n      return;\n\n    const isForward = currentDate > prevFadeDateRef.current;\n    prevFadeDateRef.current = currentDate;\n    setFadeDirection(isForward ? 'forward' : 'backward');\n\n    if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);\n    setIsFadingOut(true);\n\n    // After fade-out completes, swap the displayed month and fade back in\n    fadeTimerRef.current = setTimeout(() => {\n      setFadeDisplayDate(currentDate);\n      setIsFadingOut(false);\n    }, STATIC_TRANSITION_DURATION);\n\n    // Only clean up on unmount or rapid navigation (month changed again)\n    return () => {\n      if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);\n    };\n  }, [currentDateYear, currentDateMonth, isFadeMode]);\n\n  const fadeWeeks = useMemo(\n    () => (isFadeMode ? getMonthWeeks(fadeDisplayDate, startOfWeek) : []),\n    [isFadeMode, fadeDisplayDate, startOfWeek]\n  );\n\n  // Slight horizontal nudge in the direction of navigation, fades alongside opacity\n  const fadeStyle = useMemo((): Record<string, string | number> => {\n    const xOut = fadeDirection === 'forward' ? '-6%' : '6%';\n    return {\n      opacity: isFadingOut ? 0 : 1,\n      transform: isFadingOut ? `translateX(${xOut})` : 'translateX(0)',\n      transition: `opacity ${STATIC_TRANSITION_DURATION}ms ease, transform ${STATIC_TRANSITION_DURATION}ms ease`,\n    };\n  }, [isFadingOut, fadeDirection]);\n  const calendarSignature = app\n    .getCalendars()\n    .map(c => c.id + c.colors.lineColor)\n    .join('-');\n  const previousEventsRef = useRef<Event[] | null>(null);\n  const DEFAULT_WEEK_HEIGHT = 119;\n  // Stabilize events reference so week calculations do not rerun on every scroll frame\n  const events = useMemo(() => {\n    const previous = previousEventsRef.current;\n\n    if (\n      previous &&\n      previous.length === rawEvents.length &&\n      previous.every((event, index) => event === rawEvents[index])\n    ) {\n      return previous;\n    }\n\n    previousEventsRef.current = rawEvents;\n    return rawEvents;\n  }, [rawEvents]);\n\n  const eventsByWeek = useMemo(() => {\n    const map = new Map<number, Event[]>();\n\n    const getWeekStart = (date: Date) => {\n      const weekStart = new Date(date);\n      weekStart.setHours(0, 0, 0, 0);\n      const day = weekStart.getDay();\n      const diff = (day - startOfWeek + 7) % 7;\n      weekStart.setDate(weekStart.getDate() - diff);\n      weekStart.setHours(0, 0, 0, 0);\n      return weekStart;\n    };\n\n    const addToWeek = (weekTime: number, event: Event) => {\n      const bucket = map.get(weekTime);\n      if (bucket) {\n        bucket.push(event);\n      } else {\n        map.set(weekTime, [event]);\n      }\n    };\n\n    events.forEach(event => {\n      if (!event.start) return;\n\n      const startFull = temporalToVisualDate(event.start, appTimeZone);\n      const endFull = event.end\n        ? temporalToVisualDate(event.end, appTimeZone)\n        : startFull;\n\n      // Normalize to day boundaries\n      const startDate = new Date(startFull);\n      startDate.setHours(0, 0, 0, 0);\n\n      const endDate = new Date(endFull);\n      endDate.setHours(0, 0, 0, 0);\n\n      let adjustedEnd = new Date(endDate);\n\n      // Match WeekComponent's logic for non all-day events ending at midnight\n      if (!event.allDay) {\n        const hasTimeComponent =\n          endFull.getHours() !== 0 ||\n          endFull.getMinutes() !== 0 ||\n          endFull.getSeconds() !== 0 ||\n          endFull.getMilliseconds() !== 0;\n\n        if (!hasTimeComponent) {\n          adjustedEnd.setDate(adjustedEnd.getDate() - 1);\n        }\n      }\n\n      if (adjustedEnd < startDate) {\n        adjustedEnd = new Date(startDate);\n      }\n\n      const weekStart = getWeekStart(startDate);\n      const weekEnd = getWeekStart(adjustedEnd);\n\n      let cursorTime = weekStart.getTime();\n      const endTime = weekEnd.getTime();\n\n      while (cursorTime <= endTime) {\n        addToWeek(cursorTime, event);\n        const nextWeek = new Date(cursorTime);\n        nextWeek.setDate(nextWeek.getDate() + 7);\n        nextWeek.setHours(0, 0, 0, 0);\n        cursorTime = nextWeek.getTime();\n      }\n    });\n\n    return map;\n  }, [events, startOfWeek, appTimeZone]);\n\n  // Responsive configuration\n  const { screenSize } = useResponsiveMonthConfig();\n  const [isTouch, setIsTouch] = useState(false);\n\n  useEffect(() => {\n    setIsTouch('ontouchstart' in window || navigator.maxTouchPoints > 0);\n  }, []);\n\n  // Fixed weekHeight to prevent fluctuations during scrolling\n  // Initialize with estimated value based on window height to minimize initial adjustment\n  const [weekHeight, setWeekHeight] = useState(() => {\n    if (typeof window === 'undefined') return DEFAULT_WEEK_HEIGHT;\n    const estimatedHeaderHeight = 150;\n    const estimatedContainerHeight = window.innerHeight - estimatedHeaderHeight;\n    return Math.max(80, Math.ceil(estimatedContainerHeight / 6));\n  });\n  const [isWeekHeightInitialized, setIsWeekHeightInitialized] = useState(\n    () => typeof window !== 'undefined'\n  );\n  const previousWeekHeightRef = useRef(weekHeight);\n\n  const previousVisibleWeeksRef = useRef<typeof virtualData.visibleItems>([]);\n\n  // ID of newly created event, used to automatically display detail panel\n  const [newlyCreatedEventId, setNewlyCreatedEventId] = useState<string | null>(\n    null\n  );\n\n  // Selected event ID, used for cross-week MultiDayEvent selected state synchronization\n  const [internalSelectedId, setInternalSelectedId] = useState<string | null>(\n    null\n  );\n  const [internalDetailPanelEventId, setInternalDetailPanelEventId] = useState<\n    string | null\n  >(null);\n\n  const selectedEventId =\n    propSelectedEventId === undefined\n      ? internalSelectedId\n      : propSelectedEventId;\n  const detailPanelEventId =\n    propDetailPanelEventId === undefined\n      ? internalDetailPanelEventId\n      : propDetailPanelEventId;\n\n  const setSelectedEventId = useCallback(\n    (id: string | null) => {\n      if (propOnEventSelect) {\n        propOnEventSelect(id);\n      } else {\n        setInternalSelectedId(id);\n      }\n    },\n    [propOnEventSelect]\n  );\n\n  const setDetailPanelEventId = useCallback(\n    (id: string | null) => {\n      if (propOnDetailPanelToggle) {\n        propOnDetailPanelToggle(id);\n      } else {\n        setInternalDetailPanelEventId(id);\n      }\n    },\n    [propOnDetailPanelToggle]\n  );\n\n  // Sync highlighted event from app state\n  const prevHighlightedEventId = useRef(app.state.highlightedEventId);\n\n  useEffect(() => {\n    if (app.state.highlightedEventId) {\n      setSelectedEventId(app.state.highlightedEventId);\n    } else if (prevHighlightedEventId.current) {\n      // Only clear if previously had a highlighted event\n      setSelectedEventId(null);\n    }\n    prevHighlightedEventId.current = app.state.highlightedEventId;\n  }, [app.state.highlightedEventId]);\n\n  // Calculate the week start time for the current date (used for event day field calculation)\n  const currentWeekStart = useMemo(() => {\n    const day = currentDate.getDay();\n    const diff = (day - startOfWeek + 7) % 7;\n    const start = new Date(currentDate);\n    start.setDate(currentDate.getDate() - diff);\n    start.setHours(0, 0, 0, 0);\n    return start;\n  }, [currentDate, startOfWeek]);\n\n  const {\n    handleMoveStart,\n    handleCreateStart,\n    handleResizeStart,\n    dragState,\n    isDragging,\n  } = useDragForView(app, {\n    calendarRef,\n    viewType: ViewType.MONTH,\n    onEventsUpdate: (\n      updateFunc: (events: Event[]) => Event[],\n      isResizing?: boolean,\n      source?: 'drag' | 'resize'\n    ) => {\n      const prevEvents = events;\n      const newEvents = updateFunc(prevEvents);\n\n      const prevMap = new Map(prevEvents.map(e => [e.id, e]));\n      const newSet = new Set(newEvents.map(e => e.id));\n\n      const eventsToDelete = prevEvents.filter(e => !newSet.has(e.id));\n      const eventsToAdd = newEvents.filter(e => !prevMap.has(e.id));\n      const eventsToUpdate = newEvents.filter(e => {\n        const old = prevMap.get(e.id);\n        return old !== undefined && old !== e && hasEventChanged(old, e);\n      });\n\n      // Apply batched changes.\n      // Non-drag updates notify onEventBatchChange; drag/resize persistence is\n      // handled separately via onEventDrop/onEventResize.\n      app.applyEventsChanges(\n        {\n          delete: eventsToDelete.map(e => e.id),\n          add: eventsToAdd,\n          update: eventsToUpdate.map(e => ({ id: e.id, updates: e })),\n        },\n        isResizing,\n        source\n      );\n    },\n    onEventCreate: (event: Event) => {\n      if (screenSize === 'desktop') {\n        app.addEvent(event);\n      } else {\n        app.onMobileEventDetailToggle(event);\n      }\n    },\n    onEventEdit: (event: Event) => {\n      // double-click create event then auto open detail panel\n      setNewlyCreatedEventId(event.id);\n    },\n    currentWeekStart,\n    events,\n    isMobile: screenSize !== 'desktop',\n  });\n  const monthDragState = dragState as MonthEventDragState;\n\n  // Use calendar drop functionality\n  const { handleDrop, handleDragOver } = useCalendarDrop({\n    app,\n    onEventCreated: (event: Event) => {\n      setNewlyCreatedEventId(event.id);\n    },\n  });\n\n  const weekDaysLabels = useMemo(\n    () => getWeekDaysLabels(locale, 'short', startOfWeek),\n    [locale, getWeekDaysLabels, startOfWeek]\n  );\n\n  const dragPreviewEvent = useMemo(() => {\n    if (\n      !isDragging ||\n      !monthDragState.eventId ||\n      !monthDragState.startDate ||\n      !monthDragState.endDate ||\n      (monthDragState.mode !== 'move' && monthDragState.mode !== 'resize')\n    ) {\n      return null;\n    }\n\n    const baseEvent = events.find(event => event.id === monthDragState.eventId);\n    if (!baseEvent) return null;\n\n    return {\n      ...baseEvent,\n      start: baseEvent.allDay\n        ? dateToPlainDate(monthDragState.startDate)\n        : dateToZonedDateTime(monthDragState.startDate, appTimeZone),\n      end: baseEvent.allDay\n        ? dateToPlainDate(monthDragState.endDate)\n        : dateToZonedDateTime(monthDragState.endDate, appTimeZone),\n    } as Event;\n  }, [\n    isDragging,\n    monthDragState.eventId,\n    monthDragState.startDate,\n    monthDragState.endDate,\n    monthDragState.mode,\n    events,\n    appTimeZone,\n  ]);\n\n  const getWeekEventsWithPreview = useCallback(\n    (weekStartDate: Date) => {\n      const baseEvents =\n        eventsByWeek.get(weekStartDate.getTime()) ?? EMPTY_WEEK_EVENTS;\n\n      if (!dragPreviewEvent) {\n        return baseEvents;\n      }\n\n      const containsOriginalEvent = baseEvents.some(\n        event => event.id === dragPreviewEvent.id\n      );\n      const previewOverlapsWeek = eventOverlapsWeek(\n        dragPreviewEvent,\n        weekStartDate,\n        startOfWeek,\n        appTimeZone\n      );\n\n      if (!containsOriginalEvent && !previewOverlapsWeek) {\n        return baseEvents;\n      }\n\n      const adjustedEvents = baseEvents.filter(\n        event => event.id !== dragPreviewEvent.id\n      );\n\n      if (previewOverlapsWeek) {\n        adjustedEvents.push(dragPreviewEvent);\n      }\n\n      return adjustedEvents;\n    },\n    [eventsByWeek, dragPreviewEvent, startOfWeek, appTimeZone]\n  );\n\n  const {\n    currentMonth,\n    currentYear,\n    isScrolling,\n    virtualData,\n    weeksData,\n    scrollElementRef,\n    isNavigating,\n    handleScroll,\n    handlePreviousMonth,\n    handleNextMonth,\n    handleToday,\n    setScrollTop,\n  } = useVirtualMonthScroll({\n    currentDate,\n    weekHeight,\n    onCurrentMonthChange: (monthName: string, year: number) => {\n      const isAsian = locale.startsWith('zh') || locale.startsWith('ja');\n      const localizedMonths = getMonthLabels(\n        locale,\n        isAsian ? 'short' : 'long'\n      );\n      const monthIndex = localizedMonths.indexOf(monthName);\n\n      if (monthIndex >= 0) {\n        app.setVisibleMonth(new Date(year, monthIndex, 1));\n      }\n    },\n    initialWeeksToLoad: 156,\n    locale: locale,\n    startOfWeek: startOfWeek,\n    isEnabled: isWeekHeightInitialized,\n    snapToMonth: config.snapToMonth,\n  });\n\n  const previousStartIndexRef = useRef(0);\n\n  // Calculate actual container height and remaining space\n  const [actualContainerHeight, setActualContainerHeight] = useState(0);\n  const remainingSpace = useMemo(\n    () => actualContainerHeight - weekHeight * 6,\n    [actualContainerHeight, weekHeight]\n  );\n\n  const { visibleWeeks, startIndex: effectiveStartIndex } = useMemo(() => {\n    const { visibleItems, displayStartIndex } = virtualData;\n\n    const startIdx = visibleItems.findIndex(\n      item => item.index === displayStartIndex\n    );\n\n    if (startIdx === -1) {\n      // Fallback handling: return previous data\n      if (previousVisibleWeeksRef.current.length > 0) {\n        return {\n          visibleWeeks: previousVisibleWeeksRef.current,\n          startIndex: previousStartIndexRef.current,\n        };\n      }\n      return { visibleWeeks: [], startIndex: displayStartIndex };\n    }\n\n    // Pre-render 2 weeks before displayStartIndex as scroll-up buffer.\n    // This prevents blank gaps when React's scrollTop state lags behind the\n    // DOM scroll position during fast upward scrolling.\n    const SCROLL_UP_BUFFER = 2;\n    const bufferedStartIdx = Math.max(0, startIdx - SCROLL_UP_BUFFER);\n    const targetWeeks = visibleItems.slice(bufferedStartIdx, startIdx + 8);\n    const effectiveIdx =\n      visibleItems[bufferedStartIdx]?.index ?? displayStartIndex;\n\n    if (targetWeeks.length >= 6) {\n      previousVisibleWeeksRef.current = targetWeeks;\n      previousStartIndexRef.current = effectiveIdx;\n    }\n\n    return { visibleWeeks: targetWeeks, startIndex: effectiveIdx };\n  }, [virtualData]);\n\n  const topSpacerHeight = useMemo(\n    () => effectiveStartIndex * weekHeight,\n    [effectiveStartIndex, weekHeight]\n  );\n\n  const initialLoadRef = useRef(true);\n  const pendingNavigation = useRef(false);\n  const debouncedDisplayStartIndex = useDebouncedValue(\n    virtualData.displayStartIndex,\n    250\n  );\n\n  useEffect(() => {\n    if (isNavigating) {\n      pendingNavigation.current = true;\n    }\n  }, [isNavigating]);\n\n  useEffect(() => {\n    if (initialLoadRef.current) {\n      initialLoadRef.current = false;\n      return;\n    }\n\n    const startWeek = weeksData[debouncedDisplayStartIndex];\n    if (!startWeek) return;\n\n    const start = new Date(startWeek.startDate);\n    start.setHours(0, 0, 0, 0);\n\n    const end = new Date(start);\n    end.setDate(end.getDate() + 42 + 7); // visible month + buffer for partial scroll\n\n    app.emitVisibleRange(\n      start,\n      end,\n      pendingNavigation.current ? 'navigation' : 'scroll'\n    );\n\n    pendingNavigation.current = false;\n  }, [app, weeksData, debouncedDisplayStartIndex]);\n\n  const bottomSpacerHeight = useMemo(() => {\n    const total = virtualData.totalHeight;\n    const occupied =\n      effectiveStartIndex * weekHeight +\n      visibleWeeks.length * weekHeight +\n      remainingSpace;\n    return Math.max(0, total - occupied);\n  }, [\n    virtualData.totalHeight,\n    effectiveStartIndex,\n    weekHeight,\n    remainingSpace,\n    visibleWeeks.length,\n  ]);\n\n  // ResizeObserver - Track container height and correct weekHeight if estimate was inaccurate\n  useEffect(() => {\n    const element = scrollElementRef.current;\n    if (!element) return;\n\n    const resizeObserver = new ResizeObserver(entries => {\n      for (const entry of entries) {\n        const containerHeight = entry.contentRect.height;\n        // Save actual container height for other calculations\n        setActualContainerHeight(containerHeight);\n\n        if (containerHeight > 0) {\n          // Use Math.ceil so 6 rows always fill (or slightly exceed) the\n          // container height, preventing blank space at the bottom row.\n          const calculatedWeekHeight = Math.max(\n            80,\n            Math.ceil(containerHeight / 6)\n          );\n\n          // Only update if the accurate measurement differs from our estimate\n          if (calculatedWeekHeight !== previousWeekHeightRef.current) {\n            const currentScrollTop = element.scrollTop;\n            if (currentScrollTop > 0) {\n              const currentWeekIndex = Math.round(\n                currentScrollTop / previousWeekHeightRef.current\n              );\n              const newScrollTop = currentWeekIndex * calculatedWeekHeight;\n              element.scrollTop = newScrollTop;\n              setScrollTop(newScrollTop);\n            }\n\n            setWeekHeight(calculatedWeekHeight);\n            previousWeekHeightRef.current = calculatedWeekHeight;\n          }\n        }\n      }\n    });\n\n    resizeObserver.observe(element);\n\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, [scrollElementRef, setScrollTop]);\n\n  // Synchronously estimate weekHeight from window size and mark as initialized immediately\n  // to avoid blank flash while waiting for ResizeObserver. ResizeObserver will correct\n  // if the estimate is inaccurate.\n  useLayoutEffect(() => {\n    const estimatedHeaderHeight = 150;\n    const estimatedContainerHeight = window.innerHeight - estimatedHeaderHeight;\n    const height = Math.max(80, Math.ceil(estimatedContainerHeight / 6));\n    setWeekHeight(height);\n    previousWeekHeightRef.current = height;\n    setIsWeekHeightInitialized(true);\n  }, []);\n\n  // Block user-initiated scroll in disabled (no-fade) virtual-scroll mode.\n  // Keep onScroll active so virtual scroll state still updates on programmatic\n  // scrollTo(). Only wheel/touch events are blocked (non-passive → preventDefault works).\n  useEffect(() => {\n    if (isFadeMode || !scrollDisabled) return;\n    const el = scrollElementRef.current;\n    if (!el) return;\n    const block = (e: globalThis.Event) => e.preventDefault();\n    el.addEventListener('wheel', block, { passive: false });\n    el.addEventListener('touchmove', block, { passive: false });\n    return () => {\n      el.removeEventListener('wheel', block);\n      el.removeEventListener('touchmove', block);\n    };\n  }, [isFadeMode, scrollDisabled, scrollElementRef]);\n\n  const handleEventUpdate = useCallback(\n    (updatedEvent: Event) => app.updateEvent(updatedEvent.id, updatedEvent),\n    [app]\n  );\n\n  const handleEventDelete = useCallback(\n    (eventId: string) => app.deleteEvent(eventId),\n    [app]\n  );\n\n  const handleChangeView = (view: ViewType) => {\n    app.changeView(view);\n  };\n\n  // Stable callbacks for WeekComponent props so memo() can bail out during scroll\n  const handleDetailPanelOpen = useCallback(\n    () => setNewlyCreatedEventId(null),\n    []\n  );\n\n  const handleWeekEventSelect = useCallback(\n    (eventId: string | null) => {\n      const isViewable = app.getReadOnlyConfig().viewable;\n      if ((screenSize !== 'desktop' || isTouch) && eventId && isViewable) {\n        const evt = events.find(e => e.id === eventId);\n        if (evt) {\n          app.onMobileEventDetailToggle(evt);\n          return;\n        }\n      }\n      setSelectedEventId(eventId);\n    },\n    [screenSize, isTouch, events, setSelectedEventId, app]\n  );\n\n  const handleWeekEventLongPress = useCallback(\n    (eventId: string) => {\n      if (screenSize !== 'desktop' || isTouch) setSelectedEventId(eventId);\n    },\n    [screenSize, isTouch, setSelectedEventId]\n  );\n\n  const getCustomTitle = () => {\n    const isAsianLocale = locale.startsWith('zh') || locale.startsWith('ja');\n\n    if (isFadeMode) {\n      const labels = getMonthLabels(locale, isAsianLocale ? 'short' : 'long');\n      const monthName = labels[fadeDisplayDate.getMonth()];\n      const year = fadeDisplayDate.getFullYear();\n      return isAsianLocale ? `${year}年${monthName}` : `${monthName} ${year}`;\n    }\n\n    return isAsianLocale\n      ? `${currentYear}年${currentMonth}`\n      : `${currentMonth} ${currentYear}`;\n  };\n\n  const handleGridDateClick = useCallback(\n    (date: Date, dayEvents: Event[]) => {\n      const clickAction = config?.gridDateClick;\n      if (!clickAction) {\n        app.selectDate(date);\n        return;\n      }\n\n      if (typeof clickAction === 'function') {\n        clickAction(date, dayEvents);\n        return;\n      }\n\n      if (clickAction === 'week-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.WEEK);\n      } else if (clickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.DAY);\n      }\n      // 'none' → do nothing\n    },\n    [config.gridDateClick, app]\n  );\n\n  const handleGridDateDoubleClick = useCallback(\n    (e: MouseEvent | TouchEvent, date: Date, dayEvents: Event[]) => {\n      const dblClickAction = config?.gridDateDoubleClick ?? 'create-event';\n\n      if (typeof dblClickAction === 'function') {\n        dblClickAction(date, dayEvents);\n        return;\n      }\n\n      if (dblClickAction === 'create-event') {\n        if (!app.canMutateFromUI()) return;\n        const writableCal = app\n          .getCalendarRegistry()\n          .getDefaultWritableCalendar();\n        if (!writableCal) return;\n\n        const startTime = new Date(date);\n        startTime.setHours(9, 0, 0, 0);\n        const endTime = new Date(date);\n        endTime.setHours(10, 0, 0, 0);\n\n        const newEvent: Event = {\n          id: generateUniKey(),\n          title: t('newEvent') || 'New Event',\n          start: dateToZonedDateTime(startTime, appTimeZone),\n          end: dateToZonedDateTime(endTime, appTimeZone),\n          allDay: false,\n          calendarId: writableCal.id,\n        };\n        if (screenSize === 'desktop') {\n          app.addEvent(newEvent);\n          setNewlyCreatedEventId(newEvent.id);\n        } else {\n          app.onMobileEventDetailToggle(newEvent);\n        }\n        return;\n      }\n\n      if (dblClickAction === 'week-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.WEEK);\n      } else if (dblClickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.DAY);\n      }\n      // 'none' → do nothing\n    },\n    [config.gridDateDoubleClick, app, screenSize, t, appTimeZone]\n  );\n\n  return (\n    <div className={monthViewContainer}>\n      <ViewHeader\n        calendar={app}\n        viewType={ViewType.MONTH}\n        currentDate={currentDate}\n        customTitle={getCustomTitle()}\n        onPrevious={() => {\n          app.goToPrevious();\n          if (!isFadeMode) handlePreviousMonth();\n        }}\n        onNext={() => {\n          app.goToNext();\n          if (!isFadeMode) handleNextMonth();\n        }}\n        onToday={() => {\n          app.goToToday();\n          if (!isFadeMode) handleToday();\n        }}\n      />\n\n      <div className={weekHeaderRow} onContextMenu={e => e.preventDefault()}>\n        <div className={weekGrid}>\n          {weekDaysLabels.map((day, i) => (\n            <div key={`${day}-${i}`} className={dayLabel}>\n              {day}\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {isFadeMode ? (\n        <div\n          ref={scrollElementRef}\n          className={`${scrollContainer} df-month-view-fade-scroller`}\n          data-layout-ready={isWeekHeightInitialized ? 'true' : 'false'}\n        >\n          <div className='df-month-view-fade-body' style={fadeStyle}>\n            {fadeWeeks.map((weekData, index) => {\n              const weekEvents = getWeekEventsWithPreview(weekData.startDate);\n              const item = {\n                index,\n                weekData,\n                top: index * weekHeight,\n                height: weekHeight,\n              };\n              const adjustedItem =\n                index === 5\n                  ? { ...item, height: item.height + remainingSpace }\n                  : item;\n              return (\n                <WeekComponent\n                  key={`week-${weekData.startDate.getTime()}`}\n                  item={adjustedItem}\n                  weekHeight={weekHeight}\n                  eventHeight={config.eventHeight}\n                  showWeekNumbers={config.showWeekNumbers}\n                  showMonthIndicator={false}\n                  currentMonth={''}\n                  currentYear={0}\n                  screenSize={screenSize}\n                  isScrolling={false}\n                  calendarRef={calendarRef}\n                  events={weekEvents}\n                  onEventUpdate={handleEventUpdate}\n                  onEventDelete={handleEventDelete}\n                  onMoveStart={handleMoveStart}\n                  onCreateStart={handleCreateStart}\n                  onResizeStart={handleResizeStart}\n                  isDragging={isDragging}\n                  dragState={monthDragState}\n                  newlyCreatedEventId={newlyCreatedEventId}\n                  onDetailPanelOpen={handleDetailPanelOpen}\n                  onMoreEventsClick={app.onMoreEventsClick}\n                  onChangeView={handleChangeView}\n                  onSelectDate={app.selectDate}\n                  onGridDateClick={handleGridDateClick}\n                  onGridDateDoubleClick={handleGridDateDoubleClick}\n                  selectedEventId={selectedEventId}\n                  onEventSelect={handleWeekEventSelect}\n                  onEventLongPress={handleWeekEventLongPress}\n                  detailPanelEventId={detailPanelEventId}\n                  onDetailPanelToggle={setDetailPanelEventId}\n                  useEventDetailPanel={useEventDetailPanel}\n                  onCalendarDrop={handleDrop}\n                  onCalendarDragOver={handleDragOver}\n                  calendarSignature={calendarSignature}\n                  app={app}\n                  enableTouch={isTouch}\n                  appTimeZone={appTimeZone}\n                />\n              );\n            })}\n          </div>\n        </div>\n      ) : (\n        // Virtual-scroll mode (default, or disabled without fade)\n        <div\n          ref={scrollElementRef}\n          className={`${scrollContainer} df-month-view-virtual-scroller`}\n          data-layout-ready={isWeekHeightInitialized ? 'true' : 'false'}\n          onScroll={handleScroll}\n        >\n          <div style={{ height: topSpacerHeight }} />\n          {visibleWeeks.map((item, index) => {\n            const weekEvents = getWeekEventsWithPreview(\n              item.weekData.startDate\n            );\n\n            const adjustedItem =\n              index === 5\n                ? { ...item, height: item.height + remainingSpace }\n                : item;\n\n            const weekIsScrolling =\n              config.showMonthIndicator !== false &&\n              item.weekData.days.some(d => d.day === 1)\n                ? isScrolling\n                : false;\n\n            return (\n              <WeekComponent\n                key={`week-${item.weekData.startDate.getTime()}`}\n                item={adjustedItem}\n                weekHeight={weekHeight}\n                eventHeight={config.eventHeight}\n                showWeekNumbers={config.showWeekNumbers}\n                showMonthIndicator={config.showMonthIndicator}\n                currentMonth={currentMonth}\n                currentYear={currentYear}\n                screenSize={screenSize}\n                isScrolling={weekIsScrolling}\n                calendarRef={calendarRef}\n                events={weekEvents}\n                onEventUpdate={handleEventUpdate}\n                onEventDelete={handleEventDelete}\n                onMoveStart={handleMoveStart}\n                onCreateStart={handleCreateStart}\n                onResizeStart={handleResizeStart}\n                isDragging={isDragging}\n                dragState={monthDragState}\n                newlyCreatedEventId={newlyCreatedEventId}\n                onDetailPanelOpen={handleDetailPanelOpen}\n                onMoreEventsClick={app.onMoreEventsClick}\n                onChangeView={handleChangeView}\n                onSelectDate={app.selectDate}\n                onGridDateClick={handleGridDateClick}\n                onGridDateDoubleClick={handleGridDateDoubleClick}\n                selectedEventId={selectedEventId}\n                onEventSelect={handleWeekEventSelect}\n                onEventLongPress={handleWeekEventLongPress}\n                detailPanelEventId={detailPanelEventId}\n                onDetailPanelToggle={setDetailPanelEventId}\n                useEventDetailPanel={useEventDetailPanel}\n                onCalendarDrop={handleDrop}\n                onCalendarDragOver={handleDragOver}\n                calendarSignature={calendarSignature}\n                app={app}\n                enableTouch={isTouch}\n                appTimeZone={appTimeZone}\n              />\n            );\n          })}\n          <div style={{ height: bottomSpacerHeight }} />\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default MonthView;\n"
  },
  {
    "path": "packages/core/src/views/WeekView.tsx",
    "content": "import { JSX, RefObject } from 'preact';\nimport {\n  useState,\n  useEffect,\n  useMemo,\n  useRef,\n  useLayoutEffect,\n  useCallback,\n} from 'preact/hooks';\n\nimport ViewHeader from '@/components/common/ViewHeader';\nimport { AllDayRow } from '@/components/weekView/AllDayRow';\nimport { TimeGrid } from '@/components/weekView/TimeGrid';\nimport {\n  getWeekStart,\n  filterWeekEvents,\n  organizeAllDaySegments,\n  calculateEventLayouts,\n  calculateNewEventLayout,\n  calculateDragLayout,\n} from '@/components/weekView/util';\nimport { defaultDragConfig } from '@/core/config';\nimport { useCalendarDrop } from '@/hooks/useCalendarDrop';\nimport { useWeekViewSwipe } from '@/hooks/useWeekViewSwipe';\nimport { useResponsiveMonthConfig } from '@/hooks/virtualScroll';\nimport { useLocale } from '@/locale';\nimport { useDragForView } from '@/plugins/dragBridge';\nimport { calendarContainer } from '@/styles/classNames';\nimport {\n  Event as CalendarEvent,\n  ViewType,\n  WeekViewProps,\n  ViewType as DragViewType,\n  WeekDayDragState,\n} from '@/types';\nimport {\n  extractHourFromDate,\n  generateSecondaryTimeSlots,\n  getTimezoneDisplayLabel,\n  hasEventChanged,\n  formatTime,\n  getNowInTimeZone,\n  getTodayInTimeZone,\n  generateUniKey,\n  dateToPlainDate,\n} from '@/utils';\n\nimport {\n  buildFullWeekDates,\n  buildMobileWeekDayLabels,\n  buildWeekDates,\n  buildWeekDayLabels,\n} from './utils/weekView';\n\nconst WeekView = ({\n  app,\n  config,\n  onDateChange,\n  useEventDetailPanel,\n  calendarRef,\n  selectedEventId: propSelectedEventId,\n  onEventSelect: propOnEventSelect,\n  detailPanelEventId: propDetailPanelEventId,\n  onDetailPanelToggle: propOnDetailPanelToggle,\n}: WeekViewProps & { calendarRef: RefObject<HTMLDivElement> }) => {\n  const { t, getWeekDaysLabels, locale } = useLocale();\n\n  // Stabilize currentDate reference to avoid unnecessary re-renders\n  // app.getCurrentDate() returns a new Date object every time\n  const rawCurrentDate = app.getCurrentDate();\n  const currentDate = useMemo(() => rawCurrentDate, [rawCurrentDate.getTime()]);\n\n  const events = app.getEvents();\n  const { screenSize } = useResponsiveMonthConfig();\n  const isMobile = screenSize !== 'desktop';\n  const timeGridRef = useRef<HTMLDivElement>(null);\n  const [isTouch, setIsTouch] = useState(false);\n\n  // Configuration from the typed config object\n  const {\n    hourHeight: configHourHeight = defaultDragConfig.HOUR_HEIGHT,\n    firstHour: configFirstHour = defaultDragConfig.FIRST_HOUR,\n    lastHour: configLastHour = defaultDragConfig.LAST_HOUR,\n    allDayHeight: configAllDayHeight = defaultDragConfig.ALL_DAY_HEIGHT,\n    showAllDay = true,\n    timeFormat = '24h',\n    secondaryTimeZone,\n  } = config;\n\n  const sidebarWidth =\n    secondaryTimeZone && screenSize !== 'mobile'\n      ? 88\n      : screenSize === 'mobile'\n        ? 48\n        : 80;\n\n  // Use standardized names internally (matching previous uppercase names for compatibility with minimal changes)\n  const HOUR_HEIGHT = configHourHeight;\n  const FIRST_HOUR = configFirstHour;\n  const LAST_HOUR = configLastHour;\n  const ALL_DAY_HEIGHT = configAllDayHeight;\n\n  const showStartOfDayLabel = !showAllDay;\n\n  useEffect(() => {\n    setIsTouch('ontouchstart' in window || navigator.maxTouchPoints > 0);\n  }, []);\n\n  const isMobileView = screenSize !== 'desktop';\n  const columnsPerPage = isMobileView ? 2 : 7;\n  const isSlidingView = isMobileView;\n\n  const startOfWeek = config.startOfWeek ?? 1;\n\n  const gridWidth = isSlidingView ? '300%' : '100%';\n\n  const standardWeekStart = useMemo(\n    () => getWeekStart(currentDate, startOfWeek),\n    [currentDate, startOfWeek]\n  );\n\n  const displayDays = isSlidingView ? columnsPerPage * 3 : 7;\n\n  const [currentTime, setCurrentTime] = useState<Date | null>(null);\n\n  const [internalSelectedId, setInternalSelectedId] = useState<string | null>(\n    null\n  );\n  const [internalDetailPanelEventId, setInternalDetailPanelEventId] = useState<\n    string | null\n  >(null);\n\n  const selectedEventId =\n    propSelectedEventId === undefined\n      ? internalSelectedId\n      : propSelectedEventId;\n  const detailPanelEventId =\n    propDetailPanelEventId === undefined\n      ? internalDetailPanelEventId\n      : propDetailPanelEventId;\n\n  const setSelectedEventId = (id: string | null) => {\n    if (propOnEventSelect) {\n      propOnEventSelect(id);\n    } else {\n      setInternalSelectedId(id);\n    }\n  };\n\n  const setDetailPanelEventId = (id: string | null) => {\n    if (propOnDetailPanelToggle) {\n      propOnDetailPanelToggle(id);\n    } else {\n      setInternalDetailPanelEventId(id);\n    }\n  };\n\n  const [newlyCreatedEventId, setNewlyCreatedEventId] = useState<string | null>(\n    null\n  );\n\n  const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  // References\n  const allDayRowRef = useRef<HTMLDivElement>(null);\n  const scrollerRef = useRef<HTMLDivElement>(null);\n  const topFrozenContentRef = useRef<HTMLDivElement>(null);\n  const leftFrozenContentRef = useRef<HTMLDivElement>(null);\n  const swipeContentRef = useRef<HTMLDivElement>(null);\n\n  const { handleScroll, goToNext, goToPrevious, mobilePageStart } =\n    useWeekViewSwipe({\n      app,\n      columnsPerPage,\n      currentDate,\n      displayDays,\n      isSlidingView,\n      scrollerRef,\n      swipeContentRef,\n      topFrozenContentRef,\n    });\n\n  const currentWeekStart = isSlidingView ? mobilePageStart : standardWeekStart;\n  const displayStart = useMemo(() => {\n    if (!isSlidingView) return currentWeekStart;\n    const date = new Date(currentWeekStart);\n    date.setDate(date.getDate() - columnsPerPage);\n    return date;\n  }, [columnsPerPage, currentWeekStart, isSlidingView]);\n\n  const appTimeZone = app.timeZone;\n\n  //CalendarEvents for the current week (or custom range)\n  const currentWeekEvents = useMemo(\n    () => filterWeekEvents(events, displayStart, displayDays, appTimeZone),\n    [events, displayStart, displayDays, appTimeZone]\n  );\n\n  // Sync highlighted event from app state\n  const prevHighlightedEventId = useRef(app.state.highlightedEventId);\n\n  useEffect(() => {\n    const hasChanged =\n      app.state.highlightedEventId !== prevHighlightedEventId.current;\n\n    if (hasChanged) {\n      if (app.state.highlightedEventId) {\n        setSelectedEventId(app.state.highlightedEventId);\n\n        // Auto scroll to highlighted event\n        const highlightedEvent = currentWeekEvents.find(\n          e => e.id === app.state.highlightedEventId\n        );\n        if (highlightedEvent && !highlightedEvent.allDay) {\n          const startHour = extractHourFromDate(highlightedEvent.start);\n          const scrollContainer = scrollerRef.current;\n          if (scrollContainer) {\n            const top = (startHour - FIRST_HOUR) * HOUR_HEIGHT;\n            // Scroll with some padding using requestAnimationFrame for smoother performance\n            requestAnimationFrame(() => {\n              scrollContainer.scrollTo({\n                top: Math.max(0, top - 100),\n                behavior: 'smooth',\n              });\n            });\n          }\n        }\n      } else if (\n        prevHighlightedEventId.current &&\n        selectedEventId === prevHighlightedEventId.current\n      ) {\n        setSelectedEventId(null);\n      }\n    }\n    prevHighlightedEventId.current = app.state.highlightedEventId;\n  }, [\n    app.state.highlightedEventId,\n    currentWeekEvents,\n    FIRST_HOUR,\n    HOUR_HEIGHT,\n  ]);\n\n  // Organize the hierarchy of all-day events to avoid overlap\n  const organizedAllDaySegments = useMemo(\n    () =>\n      organizeAllDaySegments(\n        currentWeekEvents,\n        displayStart,\n        displayDays,\n        app.state.allDaySortComparator\n      ),\n    [\n      currentWeekEvents,\n      displayStart,\n      displayDays,\n      app.state.allDaySortComparator,\n    ]\n  );\n\n  // Calculate the required height for the all-day event area\n  const allDayAreaHeight = useMemo(() => {\n    const relevantSegments = isSlidingView\n      ? organizedAllDaySegments.filter(\n          s =>\n            s.endDayIndex >= columnsPerPage &&\n            s.startDayIndex <= columnsPerPage * 2 - 1\n        )\n      : organizedAllDaySegments;\n\n    if (relevantSegments.length === 0) return ALL_DAY_HEIGHT;\n    const maxRow = Math.max(...relevantSegments.map(s => s.row));\n    return ALL_DAY_HEIGHT + maxRow * ALL_DAY_HEIGHT;\n  }, [organizedAllDaySegments, ALL_DAY_HEIGHT, isSlidingView, columnsPerPage]);\n\n  // Calculate event layouts\n  const eventLayouts = useMemo(\n    () =>\n      calculateEventLayouts(\n        currentWeekEvents,\n        displayStart,\n        displayDays,\n        appTimeZone\n      ),\n    [currentWeekEvents, displayStart, displayDays, appTimeZone]\n  );\n\n  const handleEventsUpdate = useCallback(\n    (\n      updateFunc: (events: CalendarEvent[]) => CalendarEvent[],\n      isResizing?: boolean,\n      source?: 'drag' | 'resize'\n    ) => {\n      const prevEvents = currentWeekEvents;\n      const newEvents = updateFunc(prevEvents);\n\n      // Build a Map for O(1) lookups instead of O(N) .find() per element.\n      // The previous O(N²) pattern (filter + find) was the primary drag bottleneck.\n      const prevMap = new Map(prevEvents.map(e => [e.id, e]));\n      const newSet = new Set(newEvents.map(e => e.id));\n\n      const eventsToDelete = prevEvents.filter(e => !newSet.has(e.id));\n      const eventsToAdd = newEvents.filter(e => !prevMap.has(e.id));\n      // Reference equality short-circuits hasEventChanged for unchanged events\n      const eventsToUpdate = newEvents.filter(e => {\n        const old = prevMap.get(e.id);\n        return old !== undefined && old !== e && hasEventChanged(old, e);\n      });\n\n      // Apply batched changes.\n      // Non-drag updates notify onEventBatchChange; drag/resize persistence is\n      // handled separately via onEventDrop/onEventResize.\n      app.applyEventsChanges(\n        {\n          delete: eventsToDelete.map(e => e.id),\n          add: eventsToAdd,\n          update: eventsToUpdate.map(e => ({ id: e.id, updates: e })),\n        },\n        isResizing,\n        source\n      );\n    },\n    [currentWeekEvents, app]\n  );\n\n  const handleEventCreate = useCallback(\n    (event: CalendarEvent) => {\n      if (isMobile) {\n        app.onMobileEventDetailToggle(event);\n      } else {\n        app.addEvent(event);\n        setNewlyCreatedEventId(event.id);\n      }\n    },\n    [isMobile, app]\n  );\n\n  const handleEventEdit = useCallback(() => {\n    /* noop */\n  }, []);\n\n  const handleCalculateNewEventLayout = useCallback(\n    (targetDay: number, startHour: number, endHour: number) =>\n      calculateNewEventLayout(\n        targetDay,\n        startHour,\n        endHour,\n        currentWeekEvents,\n        displayStart,\n        displayDays,\n        appTimeZone\n      ),\n    [currentWeekEvents, displayStart, displayDays, appTimeZone]\n  );\n\n  const handleCalculateDragLayout = useCallback(\n    (\n      draggedEvent: CalendarEvent,\n      targetDay: number,\n      targetStartHour: number,\n      targetEndHour: number\n    ) =>\n      calculateDragLayout(\n        draggedEvent,\n        targetDay,\n        targetStartHour,\n        targetEndHour,\n        currentWeekEvents,\n        displayStart,\n        displayDays,\n        appTimeZone\n      ),\n    [currentWeekEvents, displayStart, displayDays, appTimeZone]\n  );\n\n  const dragOptions = useMemo(\n    () => ({\n      calendarRef,\n      allDayRowRef: showAllDay ? allDayRowRef : undefined,\n      timeGridRef,\n      viewType: DragViewType.WEEK,\n      onEventsUpdate: handleEventsUpdate,\n      onEventCreate: handleEventCreate,\n      onEventEdit: handleEventEdit,\n      currentWeekStart: displayStart,\n      events: currentWeekEvents,\n      calculateNewEventLayout: handleCalculateNewEventLayout,\n      calculateDragLayout: handleCalculateDragLayout,\n      TIME_COLUMN_WIDTH: sidebarWidth,\n      isMobile,\n      gridWidth,\n      displayDays,\n      HOUR_HEIGHT,\n      FIRST_HOUR,\n    }),\n    [\n      calendarRef,\n      showAllDay,\n      handleEventsUpdate,\n      handleEventCreate,\n      handleEventEdit,\n      displayStart,\n      currentWeekEvents,\n      handleCalculateNewEventLayout,\n      handleCalculateDragLayout,\n      sidebarWidth,\n      isMobile,\n      gridWidth,\n      displayDays,\n      HOUR_HEIGHT,\n      FIRST_HOUR,\n    ]\n  );\n\n  const {\n    handleMoveStart,\n    handleCreateStart,\n    handleResizeStart,\n    handleCreateAllDayEvent,\n    dragState,\n    isDragging,\n  } = useDragForView(app, dragOptions);\n\n  const handleTouchStart = (e: TouchEvent, dayIndex: number, hour: number) => {\n    if (!isMobile && !isTouch) return;\n    const touch = e.touches[0];\n    const clientX = touch.clientX;\n    const clientY = touch.clientY;\n    const target = e.currentTarget as HTMLElement;\n\n    longPressTimerRef.current = setTimeout(() => {\n      const mockEvent = {\n        preventDefault: () => {\n          /* noop */\n        },\n        stopPropagation: () => {\n          /* noop */\n        },\n        touches: [{ clientX, clientY }],\n        changedTouches: [{ clientX, clientY }],\n        target: target,\n        currentTarget: target,\n        cancelable: true,\n      } as unknown as TouchEvent;\n\n      handleCreateStart?.(mockEvent, dayIndex, hour);\n    }, 500);\n  };\n\n  const handleTouchEnd = () => {\n    if (longPressTimerRef.current) {\n      clearTimeout(longPressTimerRef.current);\n      longPressTimerRef.current = null;\n    }\n  };\n\n  const handleTouchMove = () => {\n    if (longPressTimerRef.current) {\n      clearTimeout(longPressTimerRef.current);\n      longPressTimerRef.current = null;\n    }\n  };\n\n  // Use calendar drop functionality\n  const { handleDrop, handleDragOver } = useCalendarDrop({\n    app,\n    onEventCreated: (event: CalendarEvent) => {\n      setNewlyCreatedEventId(event.id);\n    },\n  });\n\n  const weekDaysLabels = useMemo(\n    () =>\n      buildWeekDayLabels({\n        displayDays,\n        displayStart,\n        getWeekDaysLabels,\n        isSlidingView,\n        locale,\n        startOfWeek,\n      }),\n    [\n      displayDays,\n      displayStart,\n      getWeekDaysLabels,\n      isSlidingView,\n      locale,\n      startOfWeek,\n    ]\n  );\n\n  const mobileWeekDaysLabels = useMemo(\n    () =>\n      buildMobileWeekDayLabels(\n        isMobile,\n        locale,\n        getWeekDaysLabels,\n        weekDaysLabels\n      ),\n    [getWeekDaysLabels, isMobile, locale, weekDaysLabels]\n  );\n\n  const allDayLabelText = useMemo(() => t('allDay'), [t]);\n\n  const timeSlots = Array.from({ length: 24 }, (_, i) => ({\n    hour: i + FIRST_HOUR,\n    label: formatTime(i + FIRST_HOUR, 0, timeFormat),\n  }));\n\n  const secondaryTimeSlots = useMemo(\n    () =>\n      secondaryTimeZone\n        ? generateSecondaryTimeSlots(\n            timeSlots,\n            secondaryTimeZone,\n            timeFormat,\n            currentDate,\n            appTimeZone\n          )\n        : undefined,\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [secondaryTimeZone, timeFormat, FIRST_HOUR, currentDate, appTimeZone]\n  );\n\n  const primaryTzLabel = useMemo(\n    () =>\n      secondaryTimeZone\n        ? getTimezoneDisplayLabel(appTimeZone, currentDate)\n        : undefined,\n    [secondaryTimeZone, appTimeZone, currentDate]\n  );\n\n  const secondaryTzLabel = useMemo(\n    () =>\n      secondaryTimeZone\n        ? getTimezoneDisplayLabel(secondaryTimeZone, currentDate)\n        : undefined,\n    [secondaryTimeZone, currentDate]\n  );\n\n  const weekDates = useMemo(\n    () => buildWeekDates(displayStart, weekDaysLabels, locale, appTimeZone),\n    [appTimeZone, displayStart, locale, weekDaysLabels]\n  );\n\n  const fullWeekDates = useMemo(\n    () =>\n      buildFullWeekDates(standardWeekStart, locale, currentDate, appTimeZone),\n    [appTimeZone, currentDate, locale, standardWeekStart]\n  );\n\n  //CalendarEvent handling functions\n  const handleEventUpdate = (updatedEvent: CalendarEvent) =>\n    app.updateEvent(updatedEvent.id, updatedEvent);\n\n  const handleEventDelete = (eventId: string) => app.deleteEvent(eventId);\n\n  // Check if it is the current week\n  const isCurrentWeek = useMemo(() => {\n    const today = getTodayInTimeZone(appTimeZone);\n    today.setHours(0, 0, 0, 0);\n    const start = new Date(displayStart);\n    start.setHours(0, 0, 0, 0);\n    const end = new Date(start);\n    end.setDate(start.getDate() + displayDays);\n\n    return today >= start && today < end;\n  }, [displayStart, displayDays, appTimeZone]);\n\n  // Initial scroll to current time\n  useLayoutEffect(() => {\n    if (config.scrollToCurrentTime && scrollerRef.current) {\n      const scrollContainer = scrollerRef.current;\n      const now = getNowInTimeZone(appTimeZone);\n      const hour = now.getHours() + now.getMinutes() / 60;\n      const containerHeight = scrollContainer.clientHeight;\n\n      scrollContainer.scrollTop = Math.max(\n        0,\n        (hour - FIRST_HOUR) * HOUR_HEIGHT - containerHeight / 2\n      );\n      // leftFrozenContentRef is now inside the scroller, so it scrolls natively — no sync needed.\n    }\n  }, [appTimeZone, config.scrollToCurrentTime, FIRST_HOUR, HOUR_HEIGHT]); // Run on mount and timezone changes\n\n  // Timer\n  useEffect(() => {\n    setCurrentTime(getNowInTimeZone(appTimeZone));\n    const timer = setInterval(\n      () => setCurrentTime(getNowInTimeZone(appTimeZone)),\n      60_000\n    );\n    return () => clearInterval(timer);\n  }, [appTimeZone]);\n\n  const handleGridDateClick = useCallback(\n    (date: Date, dayEvents: CalendarEvent[]) => {\n      const clickAction = config?.gridDateClick;\n      if (!clickAction) {\n        onDateChange?.(date);\n        return;\n      }\n\n      if (typeof clickAction === 'function') {\n        clickAction(date, dayEvents);\n        return;\n      }\n\n      if (clickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.DAY);\n      }\n      // 'none' → do nothing\n    },\n    [config.gridDateClick, app, onDateChange]\n  );\n\n  const handleGridDateDoubleClick = useCallback(\n    (date: Date, dayEvents: CalendarEvent[]) => {\n      const dblClickAction = config?.gridDateDoubleClick ?? 'create-event';\n\n      if (typeof dblClickAction === 'function') {\n        dblClickAction(date, dayEvents);\n        return;\n      }\n\n      if (dblClickAction === 'create-event') {\n        if (!app.canMutateFromUI()) return;\n        const writableCal = app\n          .getCalendarRegistry()\n          .getDefaultWritableCalendar();\n        if (!writableCal) return;\n        const plainDate = dateToPlainDate(date);\n        const newEvent: CalendarEvent = {\n          id: generateUniKey(),\n          title: t('newEvent') || 'New Event',\n          start: plainDate,\n          end: plainDate,\n          allDay: true,\n          calendarId: writableCal.id,\n        };\n        if (isMobile) {\n          app.onMobileEventDetailToggle(newEvent);\n        } else {\n          app.addEvent(newEvent);\n          setNewlyCreatedEventId(newEvent.id);\n        }\n        return;\n      }\n\n      if (dblClickAction === 'day-view') {\n        app.setCurrentDate(date);\n        app.changeView(ViewType.DAY);\n      }\n      // 'none' → do nothing\n    },\n    [config.gridDateDoubleClick, app, isMobile, t]\n  );\n\n  return (\n    <div\n      className={`${calendarContainer} df-week-view`}\n      style={{ '--df-hour-height': `${HOUR_HEIGHT}px` } as JSX.CSSProperties}\n    >\n      {/* Header navigation */}\n      <ViewHeader\n        calendar={app}\n        viewType={ViewType.WEEK}\n        currentDate={currentDate}\n        onPrevious={() => {\n          if (!goToPrevious()) {\n            app.goToPrevious();\n          }\n        }}\n        onNext={() => {\n          if (!goToNext()) {\n            app.goToNext();\n          }\n        }}\n        onToday={() => app.goToToday()}\n      />\n\n      <AllDayRow\n        app={app}\n        weekDaysLabels={weekDaysLabels}\n        mobileWeekDaysLabels={mobileWeekDaysLabels}\n        weekDates={weekDates}\n        fullWeekDates={fullWeekDates}\n        isSlidingView={isSlidingView}\n        mobilePageStart={mobilePageStart}\n        currentWeekStart={displayStart}\n        currentWeekEvents={currentWeekEvents}\n        gridWidth={gridWidth}\n        allDayAreaHeight={allDayAreaHeight}\n        organizedAllDaySegments={organizedAllDaySegments}\n        allDayLabelText={allDayLabelText}\n        isMobile={isMobile}\n        isTouch={isTouch}\n        showAllDay={showAllDay}\n        calendarRef={calendarRef}\n        allDayRowRef={allDayRowRef}\n        topFrozenContentRef={topFrozenContentRef}\n        ALL_DAY_HEIGHT={ALL_DAY_HEIGHT}\n        HOUR_HEIGHT={HOUR_HEIGHT}\n        FIRST_HOUR={FIRST_HOUR}\n        dragState={dragState as WeekDayDragState | null}\n        isDragging={isDragging}\n        secondaryTimeSlots={secondaryTimeSlots}\n        primaryTzLabel={primaryTzLabel}\n        secondaryTzLabel={secondaryTzLabel}\n        handleMoveStart={\n          handleMoveStart as (\n            e: MouseEvent | TouchEvent,\n            event: CalendarEvent\n          ) => void\n        }\n        handleResizeStart={\n          handleResizeStart as (\n            e: MouseEvent | TouchEvent,\n            event: CalendarEvent,\n            direction: string\n          ) => void\n        }\n        handleEventUpdate={handleEventUpdate}\n        handleEventDelete={handleEventDelete}\n        setDraftEvent={event => app.onMobileEventDetailToggle(event)}\n        setIsDrawerOpen={isOpen => {\n          if (!isOpen) app.onMobileEventDetailToggle(null);\n        }}\n        onDateChange={onDateChange}\n        onGridDateClick={handleGridDateClick}\n        onGridDateDoubleClick={handleGridDateDoubleClick}\n        newlyCreatedEventId={newlyCreatedEventId}\n        setNewlyCreatedEventId={setNewlyCreatedEventId}\n        selectedEventId={selectedEventId}\n        setSelectedEventId={setSelectedEventId}\n        detailPanelEventId={detailPanelEventId}\n        setDetailPanelEventId={setDetailPanelEventId}\n        handleCreateAllDayEvent={handleCreateAllDayEvent}\n        handleDragOver={handleDragOver}\n        handleDrop={handleDrop}\n        useEventDetailPanel={useEventDetailPanel}\n      />\n\n      <TimeGrid\n        app={app}\n        timeSlots={timeSlots}\n        weekDaysLabels={weekDaysLabels}\n        currentWeekStart={displayStart}\n        currentWeekEvents={currentWeekEvents}\n        eventLayouts={eventLayouts}\n        gridWidth={gridWidth}\n        isMobile={isMobile}\n        isSlidingView={isSlidingView}\n        isTouch={isTouch}\n        scrollerRef={scrollerRef}\n        timeGridRef={timeGridRef}\n        leftFrozenContentRef={leftFrozenContentRef}\n        swipeContentRef={swipeContentRef}\n        calendarRef={calendarRef}\n        handleScroll={handleScroll}\n        handleCreateStart={handleCreateStart}\n        handleTouchStart={handleTouchStart}\n        handleTouchEnd={handleTouchEnd}\n        handleTouchMove={handleTouchMove}\n        handleDragOver={handleDragOver}\n        handleDrop={handleDrop}\n        dragState={dragState as WeekDayDragState | null}\n        isDragging={isDragging}\n        handleMoveStart={\n          handleMoveStart as (\n            e: MouseEvent | TouchEvent,\n            event: CalendarEvent\n          ) => void\n        }\n        handleResizeStart={\n          handleResizeStart as (\n            e: MouseEvent | TouchEvent,\n            event: CalendarEvent,\n            direction: string\n          ) => void\n        }\n        handleEventUpdate={handleEventUpdate}\n        handleEventDelete={handleEventDelete}\n        setDraftEvent={event => app.onMobileEventDetailToggle(event)}\n        setIsDrawerOpen={isOpen => {\n          if (!isOpen) app.onMobileEventDetailToggle(null);\n        }}\n        onDateChange={onDateChange}\n        onGridDateClick={handleGridDateClick}\n        onGridDateDoubleClick={\n          config?.gridDateDoubleClick &&\n          config.gridDateDoubleClick !== 'create-event'\n            ? handleGridDateDoubleClick\n            : undefined\n        }\n        newlyCreatedEventId={newlyCreatedEventId}\n        setNewlyCreatedEventId={setNewlyCreatedEventId}\n        selectedEventId={selectedEventId}\n        setSelectedEventId={setSelectedEventId}\n        detailPanelEventId={detailPanelEventId}\n        setDetailPanelEventId={setDetailPanelEventId}\n        useEventDetailPanel={useEventDetailPanel}\n        isCurrentWeek={isCurrentWeek}\n        currentTime={currentTime}\n        HOUR_HEIGHT={HOUR_HEIGHT}\n        FIRST_HOUR={FIRST_HOUR}\n        LAST_HOUR={LAST_HOUR}\n        showStartOfDayLabel={showStartOfDayLabel}\n        timeFormat={timeFormat}\n        secondaryTimeSlots={secondaryTimeSlots}\n        appTimeZone={appTimeZone}\n      />\n    </div>\n  );\n};\n\nexport default WeekView;\n"
  },
  {
    "path": "packages/core/src/views/YearView.tsx",
    "content": "import { DefaultYearView } from '@/components/yearView/DefaultYearView';\nimport { FixedWeekYearView } from '@/components/yearView/FixedWeekYearView';\nimport { GridYearView } from '@/components/yearView/GridYearView';\nimport { YearViewProps } from '@/types';\n\nconst YearView = (props: YearViewProps) => {\n  const mode = props.config?.mode || 'year-canvas';\n\n  if (mode === 'fixed-week') {\n    return <FixedWeekYearView {...props} />;\n  }\n\n  if (mode === 'grid') {\n    return <GridYearView app={props.app} config={props.config} />;\n  }\n\n  return <DefaultYearView {...props} />;\n};\n\nexport default YearView;\n"
  },
  {
    "path": "packages/core/src/views/utils/__tests__/weekView.test.ts",
    "content": "import {\n  buildFullWeekDates,\n  buildMobileWeekDayLabels,\n  buildWeekDayLabels,\n} from '@/views/utils/weekView';\n\nconst getWeekDaysLabels = (\n  _locale: string,\n  format: 'long' | 'short' | 'narrow' = 'short'\n) => {\n  if (format === 'narrow') {\n    return ['M', 'T', 'W', 'T', 'F', 'S', 'S'];\n  }\n\n  return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\n};\n\ndescribe('weekView utils', () => {\n  it('builds sliding-view weekday labels from the display start date', () => {\n    const labels = buildWeekDayLabels({\n      displayDays: 3,\n      displayStart: new Date(2026, 3, 9),\n      getWeekDaysLabels,\n      isSlidingView: true,\n      locale: 'en-US',\n      startOfWeek: 1,\n    });\n\n    expect(labels).toEqual(['Thu', 'Fri', 'Sat']);\n  });\n\n  it('keeps Tuesday and Thursday distinct in english mobile labels', () => {\n    const labels = buildMobileWeekDayLabels(true, 'en-US', getWeekDaysLabels, [\n      'Mon',\n      'Tue',\n      'Wed',\n      'Thu',\n      'Fri',\n      'Sat',\n      'Sun',\n    ]);\n\n    expect(labels).toEqual(['M', 'Tu', 'W', 'Th', 'F', 'Sa', 'Su']);\n  });\n\n  it('marks the current day in full week dates', () => {\n    const dates = buildFullWeekDates(\n      new Date(2026, 3, 6),\n      'en-US',\n      new Date(2026, 3, 9),\n      'UTC'\n    );\n\n    expect(dates).toHaveLength(7);\n    expect(dates[3]?.isCurrent).toBe(true);\n    expect(dates[0]?.dayName).toBe('Mon');\n  });\n});\n"
  },
  {
    "path": "packages/core/src/views/utils/dragCreate.ts",
    "content": "/**\n * Shared drag-to-create utilities for Day/Week view time-grid cells.\n *\n * Interaction contract:\n *   - Single click  → no event created\n *   - Mousedown + drag (≥ THRESHOLD px) → activates create drag\n *   - Double-click  → creates a 1-hour event immediately (no resize mode)\n */\n\ntype CreateStartFn = (\n  e: MouseEvent | TouchEvent,\n  dayIndex: number,\n  hour: number\n) => void;\n\nconst DRAG_CREATE_THRESHOLD = 5;\n\n/**\n * Starts a pending drag-create interaction on mousedown.\n *\n * Attaches temporary document-level listeners and only calls\n * `handleCreateStart` after the cursor moves ≥ DRAG_CREATE_THRESHOLD pixels,\n * so a plain click never creates an event.\n *\n * In sliding-view mode (week view on narrow desktop windows), a predominantly\n * horizontal drag cancels the pending create without triggering it, letting\n * the swipe-navigation handler take over instead.\n *\n * Also dispatches a synthetic mousemove immediately after activation to sync\n * the drag indicator to the actual cursor position, eliminating the brief\n * 1-hour flash that would otherwise appear on the first frame.\n */\nexport function startPendingCreate(\n  e: MouseEvent,\n  dayIndex: number,\n  hour: number,\n  isTouch: boolean,\n  handleCreateStart: CreateStartFn | undefined,\n  isSlidingView?: boolean\n): void {\n  if (isTouch || e.button !== 0) return;\n\n  let active = true;\n\n  // Store handlers on an object so each handler can reference the other via\n  // property lookup at call-time, avoiding circular forward-reference errors.\n  const handlers = {\n    move(moveEvent: MouseEvent) {\n      if (!active) return;\n      const dx = moveEvent.clientX - e.clientX;\n      const dy = moveEvent.clientY - e.clientY;\n      const dist = Math.hypot(dx, dy);\n\n      if (dist < DRAG_CREATE_THRESHOLD) return;\n\n      active = false;\n      document.removeEventListener('mousemove', handlers.move);\n      document.removeEventListener('mouseup', handlers.up);\n\n      // In sliding-view mode, once we have enough movement to determine intent,\n      // check direction: horizontal dominance means swipe-to-navigate, not create.\n      if (isSlidingView && Math.abs(dx) >= Math.abs(dy)) return;\n\n      handleCreateStart?.(e, dayIndex, hour);\n      // Sync indicator to current cursor before the first render frame.\n      document.dispatchEvent(\n        new MouseEvent('mousemove', {\n          clientX: moveEvent.clientX,\n          clientY: moveEvent.clientY,\n          bubbles: true,\n          cancelable: false,\n        })\n      );\n    },\n    up() {\n      active = false;\n      document.removeEventListener('mousemove', handlers.move);\n      document.removeEventListener('mouseup', handlers.up);\n    },\n  };\n\n  document.addEventListener('mousemove', handlers.move);\n  document.addEventListener('mouseup', handlers.up);\n}\n\n/**\n * Dispatches a synthetic mouseup to immediately finalize a just-created event.\n *\n * Call this right after `handleCreateStart` in an onDblClick handler to\n * prevent the interactive drag-resize mode from activating — the event is\n * committed at the default 1-hour duration without any further mouse input.\n */\nexport function finalizeCreateOnDblClick(): void {\n  document.dispatchEvent(\n    new MouseEvent('mouseup', {\n      bubbles: true,\n      cancelable: false,\n    })\n  );\n}\n"
  },
  {
    "path": "packages/core/src/views/utils/weekView.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nexport interface WeekDateDisplay {\n  date: number;\n  month: string;\n  fullDate: Date;\n  isToday: boolean;\n}\n\nexport interface FullWeekDateDisplay extends WeekDateDisplay {\n  isCurrent: boolean;\n  dayName: string;\n}\n\ninterface BuildWeekDayLabelsParams {\n  displayDays: number;\n  displayStart: Date;\n  getWeekDaysLabels: (\n    locale: string,\n    format?: 'long' | 'short' | 'narrow',\n    startOfWeek?: number\n  ) => string[];\n  isSlidingView: boolean;\n  locale: string;\n  startOfWeek: number;\n}\n\nconst getTodayKeyInTimeZone = (timeZone: string) => {\n  const todayInTz = Temporal.Now.plainDateISO(timeZone);\n  const todayLocal = new Date(\n    todayInTz.year,\n    todayInTz.month - 1,\n    todayInTz.day\n  );\n\n  return todayLocal.toDateString();\n};\n\nexport const buildWeekDayLabels = ({\n  displayDays,\n  displayStart,\n  getWeekDaysLabels,\n  isSlidingView,\n  locale,\n  startOfWeek,\n}: BuildWeekDayLabelsParams) => {\n  if (isSlidingView) {\n    return Array.from({ length: displayDays }, (_, index) => {\n      const date = new Date(displayStart);\n      date.setDate(date.getDate() + index);\n      return date.toLocaleDateString(locale, { weekday: 'short' });\n    });\n  }\n\n  return getWeekDaysLabels(locale, 'short', startOfWeek);\n};\n\nexport const buildMobileWeekDayLabels = (\n  isMobile: boolean,\n  locale: string,\n  getWeekDaysLabels: (\n    locale: string,\n    format?: 'long' | 'short' | 'narrow',\n    startOfWeek?: number\n  ) => string[],\n  weekDaysLabels: string[]\n) => {\n  if (!isMobile) return [];\n\n  const lang = locale.split('-')[0].toLowerCase();\n  if (lang === 'zh' || lang === 'ja') {\n    return getWeekDaysLabels(locale, 'narrow');\n  }\n\n  return weekDaysLabels.map(label => {\n    if (lang === 'en') {\n      if (label.startsWith('Tu')) return 'Tu';\n      if (label.startsWith('Th')) return 'Th';\n      if (label.startsWith('Sa')) return 'Sa';\n      if (label.startsWith('Su')) return 'Su';\n    }\n\n    return label.charAt(0);\n  });\n};\n\nexport const buildWeekDates = (\n  displayStart: Date,\n  weekDaysLabels: string[],\n  locale: string,\n  appTimeZone: string\n): WeekDateDisplay[] => {\n  const todayKey = getTodayKeyInTimeZone(appTimeZone);\n\n  return weekDaysLabels.map((_, index) => {\n    const date = new Date(displayStart);\n    date.setDate(displayStart.getDate() + index);\n\n    return {\n      date: date.getDate(),\n      month: date.toLocaleString(locale, { month: 'short' }),\n      fullDate: new Date(date),\n      isToday: date.toDateString() === todayKey,\n    };\n  });\n};\n\nexport const buildFullWeekDates = (\n  standardWeekStart: Date,\n  locale: string,\n  currentDate: Date,\n  appTimeZone: string\n): FullWeekDateDisplay[] => {\n  const todayKey = getTodayKeyInTimeZone(appTimeZone);\n  const currentDateMidnight = new Date(\n    currentDate.getFullYear(),\n    currentDate.getMonth(),\n    currentDate.getDate()\n  ).getTime();\n\n  return Array.from({ length: 7 }, (_, index) => {\n    const date = new Date(standardWeekStart);\n    date.setDate(standardWeekStart.getDate() + index);\n    const dateOnly = new Date(\n      date.getFullYear(),\n      date.getMonth(),\n      date.getDate()\n    );\n\n    return {\n      date: date.getDate(),\n      month: date.toLocaleString(locale, { month: 'short' }),\n      fullDate: new Date(date),\n      isToday: date.toDateString() === todayKey,\n      isCurrent: dateOnly.getTime() === currentDateMidnight,\n      dayName: date.toLocaleDateString(locale, { weekday: 'short' }),\n    };\n  });\n};\n"
  },
  {
    "path": "packages/core/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"declarationDir\": \"dist/types\",\n    \"declarationMap\": false,\n    \"sourceMap\": false,\n    \"stripInternal\": true,\n    \"module\": \"ESNext\",\n    \"target\": \"ES2015\",\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": false,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"src/app/**\",\n    \"**/*.test.ts\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.ts\",\n    \"**/*.spec.tsx\"\n  ]\n}\n"
  },
  {
    "path": "packages/core/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n      \"react\": [\"../../node_modules/preact/compat\"],\n      \"react-dom\": [\"../../node_modules/preact/compat\"],\n      \"react/jsx-runtime\": [\"../../node_modules/preact/compat/jsx-runtime\"]\n    }\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\", \"src/app/**\"]\n}\n"
  },
  {
    "path": "packages/core/vite.config.ts",
    "content": "import { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport preactPlugin from '@preact/preset-vite';\nimport { defineConfig } from 'vite';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst workspaceRoot = resolve(__dirname, '../../');\n\nexport default defineConfig({\n  plugins: [preactPlugin({ reactAliasesEnabled: false })],\n  root: workspaceRoot,\n  publicDir: resolve(__dirname, 'public'),\n  resolve: {\n    alias: [\n      { find: '@', replacement: resolve(__dirname, 'src') },\n      { find: '@drag', replacement: resolve(__dirname, '../plugins/drag/src') },\n      {\n        find: '@sidebar',\n        replacement: resolve(__dirname, '../plugins/sidebar/src'),\n      },\n      {\n        find: '@keyboard-shortcuts',\n        replacement: resolve(__dirname, '../plugins/keyboard-shortcuts/src'),\n      },\n      {\n        find: '@localization',\n        replacement: resolve(__dirname, '../plugins/localization/src'),\n      },\n      {\n        find: '@dayflow/ui-context-menu',\n        replacement: resolve(__dirname, '../ui/context-menu/src/index.ts'),\n      },\n      {\n        find: '@dayflow/ui-range-picker',\n        replacement: resolve(__dirname, '../ui/range-picker/src/index.ts'),\n      },\n      {\n        find: '@ui-range-picker',\n        replacement: resolve(__dirname, '../ui/range-picker/src'),\n      },\n      {\n        find: '@dayflow/core',\n        replacement: resolve(__dirname, 'src/index.ts'),\n      },\n      {\n        find: '@dayflow/react',\n        replacement: resolve(__dirname, '../react/src/index.ts'),\n      },\n      { find: '@examples', replacement: resolve(__dirname, '../../examples') },\n      {\n        find: '@dayflow/plugin-sidebar',\n        replacement: resolve(__dirname, '../plugins/sidebar/src/index.ts'),\n      },\n      {\n        find: '@dayflow/plugin-keyboard-shortcuts',\n        replacement: resolve(\n          __dirname,\n          '../plugins/keyboard-shortcuts/src/index.ts'\n        ),\n      },\n      {\n        find: '@dayflow/plugin-drag',\n        replacement: resolve(__dirname, '../plugins/drag/src/index.ts'),\n      },\n      {\n        find: '@dayflow/plugin-localization',\n        replacement: resolve(__dirname, '../plugins/localization/src/index.ts'),\n      },\n      // specific CSS aliases FIRST\n      {\n        find: '@dayflow/resource-grid/dist/styles.css',\n        replacement: resolve(\n          __dirname,\n          '../resource-grid/src/styles/tailwind-components.css'\n        ),\n      },\n      {\n        find: '@dayflow/resource-grid/dist/styles.components.css',\n        replacement: resolve(\n          __dirname,\n          '../resource-grid/src/styles/tailwind-components.css'\n        ),\n      },\n      {\n        find: '@grid/styles.css',\n        replacement: resolve(\n          __dirname,\n          '../resource-grid/src/styles/tailwind-components.css'\n        ),\n      },\n      // Then general ones\n      {\n        find: '@dayflow/resource-grid',\n        replacement: resolve(__dirname, '../resource-grid/src/index.ts'),\n      },\n      {\n        find: '@grid',\n        replacement: resolve(__dirname, '../resource-grid/src'),\n      },\n      {\n        find: 'preact/hooks',\n        replacement: resolve(workspaceRoot, 'node_modules/preact/hooks'),\n      },\n      {\n        find: 'preact/compat',\n        replacement: resolve(workspaceRoot, 'node_modules/preact/compat'),\n      },\n      {\n        find: 'preact/jsx-runtime',\n        replacement: resolve(workspaceRoot, 'node_modules/preact/jsx-runtime'),\n      },\n      {\n        find: 'preact/jsx-dev-runtime',\n        replacement: resolve(workspaceRoot, 'node_modules/preact/jsx-runtime'),\n      }, // Preact usually uses same for dev\n      {\n        find: 'preact/debug',\n        replacement: resolve(workspaceRoot, 'node_modules/preact/debug'),\n      },\n      {\n        find: 'preact',\n        replacement: resolve(workspaceRoot, 'node_modules/preact'),\n      },\n      {\n        find: 'react',\n        replacement: resolve(workspaceRoot, 'node_modules/preact/compat'),\n      },\n      {\n        find: 'react-dom',\n        replacement: resolve(workspaceRoot, 'node_modules/preact/compat'),\n      },\n      {\n        find: 'react/jsx-runtime',\n        replacement: resolve(\n          workspaceRoot,\n          'node_modules/preact/compat/jsx-runtime'\n        ),\n      },\n    ],\n  },\n  server: {\n    port: 5529,\n    open: true,\n    fs: {\n      allow: [workspaceRoot],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/create-dayflow/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/create-dayflow/README.md",
    "content": "# create-dayflow\n\nInteractive setup wizard for DayFlow — the universal calendar component library for React, Vue, Svelte, and Angular.\n\n## Usage\n\nYou can use this CLI to quickly set up DayFlow in your project. It will help you install the necessary adapter for your framework, pick plugins, and even configure Tailwind CSS for you.\n\nRun the following command in your project root:\n\n```bash\nnpx create-dayflow\n```\n\nOr with your preferred package manager:\n\n```bash\npnpm dlx create-dayflow\nyarn dlx create-dayflow\nbun x create-dayflow\n```\n\n## Features\n\n- **Framework Detection**: Detects your framework and suggests the correct `@dayflow` adapter.\n- **Plugin Selection**: Choose from Drag & Drop, Sidebar, Keyboard Shortcuts, and Localization.\n- **Tailwind Integration**: Automatically adds `styles.components.css` and the optional class-based dark-mode variant for `theme.mode`.\n- **Package Manager Detection**: Identifies your lockfile and uses the appropriate package manager.\n\n## Documentation\n\nFor full documentation and examples, visit [https://calendar.dayflow.studio](https://calendar.dayflow.studio).\n\n## License\n\n[MIT](LICENSE)\n"
  },
  {
    "path": "packages/create-dayflow/package.json",
    "content": "{\n  \"name\": \"create-dayflow\",\n  \"version\": \"1.2.0\",\n  \"description\": \"Interactive CLI to add DayFlow calendar to your project\",\n  \"keywords\": [\n    \"angular\",\n    \"calendar\",\n    \"cli\",\n    \"create-dayflow\",\n    \"dayflow\",\n    \"react\",\n    \"setup\",\n    \"svelte\",\n    \"vue\"\n  ],\n  \"homepage\": \"https://calendar.dayflow.studio\",\n  \"bugs\": {\n    \"url\": \"https://github.com/dayflow-js/dayflow/issues\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"Jayce Li\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/dayflow-js/dayflow.git\",\n    \"directory\": \"packages/create-dayflow\"\n  },\n  \"bin\": {\n    \"create-dayflow\": \"./dist/index.js\"\n  },\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@clack/prompts\": \"^0.9.1\",\n    \"picocolors\": \"^1.1.1\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.10.2\",\n    \"tsup\": \"^8.4.0\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/create-dayflow/src/index.ts",
    "content": "import { spawn } from 'node:child_process';\nimport {\n  readFileSync,\n  writeFileSync,\n  readdirSync,\n  statSync,\n  existsSync,\n} from 'node:fs';\nimport { resolve, relative } from 'node:path';\n\nimport * as p from '@clack/prompts';\nimport pc from 'picocolors';\n\nconst FRAMEWORKS = [\n  { value: 'react', label: 'React', hint: '@dayflow/react' },\n  { value: 'vue', label: 'Vue', hint: '@dayflow/vue' },\n  { value: 'svelte', label: 'Svelte', hint: '@dayflow/svelte' },\n  { value: 'angular', label: 'Angular', hint: '@dayflow/angular' },\n] as const;\n\ntype Framework = (typeof FRAMEWORKS)[number]['value'];\n\nconst ADAPTER_PACKAGE: Record<Framework, string> = {\n  react: '@dayflow/react',\n  vue: '@dayflow/vue',\n  svelte: '@dayflow/svelte',\n  angular: '@dayflow/angular',\n};\n\nconst PLUGINS = [\n  {\n    value: 'drag',\n    label: 'Drag & Drop',\n    hint: '@dayflow/plugin-drag',\n  },\n  {\n    value: 'keyboard-shortcuts',\n    label: 'Keyboard Shortcuts',\n    hint: '@dayflow/plugin-keyboard-shortcuts',\n  },\n  {\n    value: 'localization',\n    label: 'Localization',\n    hint: '@dayflow/plugin-localization',\n  },\n  {\n    value: 'sidebar',\n    label: 'Sidebar',\n    hint: '@dayflow/plugin-sidebar',\n  },\n] as const;\n\ntype Plugin = (typeof PLUGINS)[number]['value'];\n\nconst PLUGIN_PACKAGE: Record<Plugin, string> = {\n  drag: '@dayflow/plugin-drag',\n  'keyboard-shortcuts': '@dayflow/plugin-keyboard-shortcuts',\n  localization: '@dayflow/plugin-localization',\n  sidebar: '@dayflow/plugin-sidebar',\n};\n\nconst PACKAGE_MANAGERS = [\n  { value: 'npm', label: 'npm', hint: 'npm install' },\n  { value: 'pnpm', label: 'pnpm', hint: 'pnpm add' },\n  { value: 'yarn', label: 'yarn', hint: 'yarn add' },\n  { value: 'bun', label: 'bun', hint: 'bun add' },\n] as const;\n\ntype PackageManager = (typeof PACKAGE_MANAGERS)[number]['value'];\n\nfunction detectPackageManager(): PackageManager {\n  const cwd = process.cwd();\n  if (\n    existsSync(resolve(cwd, 'bun.lockb')) ||\n    existsSync(resolve(cwd, 'bun.lock'))\n  )\n    return 'bun';\n  if (existsSync(resolve(cwd, 'pnpm-lock.yaml'))) return 'pnpm';\n  if (existsSync(resolve(cwd, 'yarn.lock'))) return 'yarn';\n  return 'npm';\n}\n\nfunction getInstallCommand(pm: PackageManager, packages: string[]): string {\n  const pkgs = packages.join(' ');\n  switch (pm) {\n    case 'npm':\n      return `npm install ${pkgs}`;\n    case 'pnpm':\n      return `pnpm add ${pkgs}`;\n    case 'yarn':\n      return `yarn add ${pkgs}`;\n    case 'bun':\n      return `bun add ${pkgs}`;\n    default:\n      return `npm install ${pkgs}`;\n  }\n}\n\nfunction getPluginImportName(plugin: Plugin): string {\n  switch (plugin) {\n    case 'drag':\n      return 'DragPlugin';\n    case 'keyboard-shortcuts':\n      return 'KeyboardShortcutsPlugin';\n    case 'localization':\n      return 'LocalizationPlugin';\n    case 'sidebar':\n      return 'SidebarPlugin';\n    default:\n      return '';\n  }\n}\n\nfunction buildNextSteps(\n  framework: Framework,\n  plugins: Plugin[],\n  isTailwind: boolean\n): string {\n  const imports: string[] = [];\n\n  if (isTailwind) {\n    imports.push(`@import '@dayflow/core/dist/styles.components.css'`);\n    imports.push(`@import 'tailwindcss'`);\n    imports.push(\n      `@variant dark (.dark &); /* if you plan to use theme.mode */`\n    );\n  } else {\n    imports.push(`import '@dayflow/core/dist/styles.css'`);\n  }\n\n  switch (framework) {\n    case 'react':\n      imports.push(`import { DayFlowCalendar } from '@dayflow/react'`);\n      break;\n    case 'vue':\n      imports.push(`import { DayFlowCalendar } from '@dayflow/vue'`);\n      break;\n    case 'svelte':\n      imports.push(`import { DayFlowCalendar } from '@dayflow/svelte'`);\n      break;\n    case 'angular':\n      imports.push(`import { DayFlowCalendarModule } from '@dayflow/angular'`);\n      break;\n    default:\n      break;\n  }\n\n  for (const plugin of plugins) {\n    imports.push(\n      `import { ${getPluginImportName(plugin)} } from '${PLUGIN_PACKAGE[plugin]}'`\n    );\n  }\n\n  return imports.join('\\n');\n}\n\nfunction findCssFile(dir: string): string | null {\n  const commonNames = [\n    'globals.css',\n    'global.css',\n    'index.css',\n    'app.css',\n    'main.css',\n    'style.css',\n  ];\n\n  // 1. Check top-level common directories\n  const commonDirs = ['src', 'app', 'styles'];\n  for (const d of commonDirs) {\n    const fullDir = resolve(dir, d);\n    if (existsSync(fullDir) && statSync(fullDir).isDirectory()) {\n      for (const name of commonNames) {\n        const file = resolve(fullDir, name);\n        if (existsSync(file)) return file;\n      }\n    }\n  }\n\n  // 2. Fallback: shallow search in src/ and app/ for ANY .css\n  for (const d of ['src', 'app']) {\n    const fullDir = resolve(dir, d);\n    if (existsSync(fullDir) && statSync(fullDir).isDirectory()) {\n      try {\n        const files = readdirSync(fullDir);\n        const css = files.find(f => f.endsWith('.css'));\n        if (css) return resolve(fullDir, css);\n      } catch {\n        // ignore errors\n      }\n    }\n  }\n\n  // 3. Fallback to current directory for any of the common names\n  for (const name of commonNames) {\n    const file = resolve(dir, name);\n    if (existsSync(file)) return file;\n  }\n\n  return null;\n}\n\nasync function main() {\n  console.log();\n  p.intro(\n    `${pc.bgCyan(pc.black(' DayFlow '))} ${pc.dim('— Calendar component setup')}`\n  );\n\n  // Step 1: framework\n  const framework = await p.select<Framework>({\n    message: 'Which framework are you using?',\n    options: FRAMEWORKS.map(f => ({\n      value: f.value,\n      label: f.label,\n      hint: f.hint,\n    })),\n  });\n\n  if (p.isCancel(framework)) {\n    p.cancel('Setup cancelled.');\n    process.exit(0);\n  }\n\n  // Step 2: plugins\n  const selectedPlugins = await p.multiselect<Plugin>({\n    message: 'Select plugins to install (space to toggle, enter to confirm)',\n    options: PLUGINS.map(plugin => ({\n      value: plugin.value,\n      label: plugin.label,\n      hint: plugin.hint,\n    })),\n    required: false,\n  });\n\n  if (p.isCancel(selectedPlugins)) {\n    p.cancel('Setup cancelled.');\n    process.exit(0);\n  }\n\n  // Step 3: Tailwind CSS\n  const isTailwind = await p.confirm({\n    message: 'Are you using Tailwind CSS?',\n    initialValue: true,\n  });\n\n  if (p.isCancel(isTailwind)) {\n    p.cancel('Setup cancelled.');\n    process.exit(0);\n  }\n\n  // Step 4: package manager\n  const detectedPm = detectPackageManager();\n  const packageManager = await p.select<PackageManager>({\n    message: 'Which package manager do you use?',\n    options: PACKAGE_MANAGERS.map(pm => ({\n      value: pm.value,\n      label: pm.label,\n      hint: pm.hint,\n      ...(pm.value === detectedPm\n        ? { label: `${pm.label} ${pc.dim('(detected)')}` }\n        : {}),\n    })),\n    initialValue: detectedPm,\n  });\n\n  if (p.isCancel(packageManager)) {\n    p.cancel('Setup cancelled.');\n    process.exit(0);\n  }\n\n  // Build packages list\n  const packages: string[] = [\n    '@dayflow/core',\n    ADAPTER_PACKAGE[framework as Framework],\n  ];\n\n  for (const plugin of selectedPlugins as Plugin[]) {\n    packages.push(PLUGIN_PACKAGE[plugin]);\n  }\n\n  // Summary\n  console.log();\n  const note = [\n    `${pc.bold('Framework:')}  ${pc.cyan(ADAPTER_PACKAGE[framework as Framework])}`,\n    selectedPlugins.length > 0\n      ? `${pc.bold('Plugins:')}    ${(selectedPlugins as Plugin[]).map(plugin => PLUGIN_PACKAGE[plugin]).join(', ')}`\n      : null,\n    `${pc.bold('Packages:')}   ${packages.join(' ')}`,\n    `${pc.bold('Command:')}    ${pc.dim(getInstallCommand(packageManager as PackageManager, packages))}`,\n  ]\n    .filter(Boolean)\n    .join('\\n');\n\n  p.note(note, 'Installation plan');\n\n  const confirmed = await p.confirm({\n    message: 'Proceed with installation?',\n    initialValue: true,\n  });\n\n  if (p.isCancel(confirmed) || !confirmed) {\n    p.cancel('Setup cancelled.');\n    process.exit(0);\n  }\n\n  // Install\n  const spinner = p.spinner();\n  spinner.start('Installing packages...');\n\n  const cmd = getInstallCommand(packageManager as PackageManager, packages);\n\n  const result = await new Promise<{ code: number; stderr: string }>(done => {\n    let stderr = '';\n    const child = spawn(cmd, {\n      shell: true,\n      cwd: process.cwd(),\n      stdio: ['ignore', 'ignore', 'pipe'],\n    });\n    child.stderr.on('data', (chunk: Buffer) => {\n      stderr += chunk.toString();\n    });\n    child.on('close', code => done({ code: code ?? 1, stderr }));\n  });\n\n  if (result.code !== 0) {\n    spinner.stop('Installation failed');\n    console.log();\n    console.log(pc.red('Error output:'));\n    console.log(result.stderr);\n    process.exit(1);\n  }\n\n  spinner.stop('Packages installed successfully');\n\n  // Step 5: Configure CSS for Tailwind\n  if (isTailwind) {\n    const rootCss = findCssFile(process.cwd());\n    if (rootCss) {\n      try {\n        const content = readFileSync(rootCss, 'utf-8');\n        const lines = content.split('\\n');\n        const hasDayFlowImport = content.includes(\n          '@dayflow/core/dist/styles.components.css'\n        );\n        const hasTailwindImport = content.includes(\"@import 'tailwindcss'\");\n        const hasDarkVariant = content.includes('@variant dark (.dark &);');\n        let finalLines = [...lines];\n\n        if (!hasDayFlowImport) {\n          const tailwindImportIndex = finalLines.findIndex(line =>\n            line.trim().includes(\"@import 'tailwindcss'\")\n          );\n          const dayFlowImport =\n            \"/* DayFlow Tailwind Setup */\\n@import '@dayflow/core/dist/styles.components.css';\";\n\n          if (tailwindImportIndex === -1) {\n            finalLines.unshift(dayFlowImport);\n            if (!hasTailwindImport) {\n              finalLines.splice(1, 0, \"@import 'tailwindcss';\");\n            }\n          } else {\n            finalLines.splice(tailwindImportIndex, 0, dayFlowImport);\n          }\n        } else if (!hasTailwindImport) {\n          const dayFlowImportIndex = finalLines.findIndex(line =>\n            line.includes('@dayflow/core/dist/styles.components.css')\n          );\n          finalLines.splice(\n            dayFlowImportIndex + 1,\n            0,\n            \"@import 'tailwindcss';\"\n          );\n        }\n\n        if (!hasDarkVariant) {\n          let lastImportIndex = -1;\n          for (let i = 0; i < finalLines.length; i++) {\n            if (finalLines[i].trim().startsWith('@import')) {\n              lastImportIndex = i;\n            }\n          }\n\n          finalLines.splice(\n            lastImportIndex + 1,\n            0,\n            '/* DayFlow: class-based dark mode so theme.mode works correctly */\\n@variant dark (.dark &);'\n          );\n        }\n\n        if (finalLines.join('\\n') !== content) {\n          writeFileSync(rootCss, finalLines.join('\\n'));\n          p.log.success(\n            `${pc.green('✓')} Added DayFlow configuration to ${pc.cyan(relative(process.cwd(), rootCss))}`\n          );\n        }\n      } catch (err) {\n        p.log.warn(\n          `Could not update CSS file: ${err instanceof Error ? err.message : String(err)}`\n        );\n      }\n    } else {\n      p.log.warn('Could not find a root CSS file to update automatically.');\n    }\n  }\n\n  // Done\n  const nextSteps = buildNextSteps(\n    framework as Framework,\n    selectedPlugins as Plugin[],\n    isTailwind as boolean\n  );\n  p.note(nextSteps, 'Next steps');\n\n  p.outro(\n    `${pc.green('✓')} DayFlow is ready! See ${pc.cyan('https://dayflow-js.github.io/calendar/docs/introduction')} for documentation.`\n  );\n}\n\nmain().catch(err => {\n  console.error(pc.red('Unexpected error:'), err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/create-dayflow/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"declarationMap\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/create-dayflow/tsup.config.ts",
    "content": "import { defineConfig } from 'tsup';\n\nexport default defineConfig({\n  entry: ['src/index.ts'],\n  format: ['esm'],\n  target: 'node18',\n  clean: true,\n  banner: {\n    js: '#!/usr/bin/env node',\n  },\n});\n"
  },
  {
    "path": "packages/plugins/drag/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/plugins/drag/README.md",
    "content": "# DayFlow\n\n**English** | [中文](README.zh.md) | [日本語](README.ja.md) | [Getting Started & Contributing](CONTRIBUTING.md)\n\nA flexible and feature-rich calendar component library for **React, Vue, Angular, and Svelte** with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/core?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/core)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/dayflow/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/dayflow)](https://github.com/dayflow-js/dayflow/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/dayflow/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n\n---\n"
  },
  {
    "path": "packages/plugins/drag/jest.config.mjs",
    "content": "export default {\n  preset: 'ts-jest',\n  testEnvironment: 'jsdom',\n  roots: ['<rootDir>/src'],\n  testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],\n  moduleNameMapper: {\n    '^@drag/(.*)$': '<rootDir>/src/$1',\n    '^@dayflow/core$': '<rootDir>/../../core/src/index.ts',\n    '^@dayflow/ui-range-picker$':\n      '<rootDir>/../../ui/range-picker/src/index.ts',\n    '^@dayflow/ui-context-menu$':\n      '<rootDir>/../../ui/context-menu/src/index.ts',\n    '^@ui-range-picker/(.*)$': '<rootDir>/../../ui/range-picker/src/$1',\n    '^@/(.*)$': '<rootDir>/../../core/src/$1',\n    '^preact$': '<rootDir>/../../../node_modules/preact/dist/preact.js',\n    '^preact/hooks$':\n      '<rootDir>/../../../node_modules/preact/hooks/dist/hooks.js',\n    '^preact/jsx-runtime$':\n      '<rootDir>/../../../node_modules/preact/jsx-runtime/dist/jsxRuntime.js',\n    '^preact/compat$':\n      '<rootDir>/../../../node_modules/preact/compat/dist/compat.js',\n  },\n};\n"
  },
  {
    "path": "packages/plugins/drag/package.json",
    "content": "{\n  \"name\": \"@dayflow/plugin-drag\",\n  \"version\": \"1.5.2\",\n  \"description\": \"Drag plugin for DayFlow calendar - event move, resize, and create via mouse/touch\",\n  \"keywords\": [\n    \"calendar\",\n    \"dayflow\",\n    \"drag\",\n    \"drop\",\n    \"plugin\",\n    \"resize\"\n  ],\n  \"license\": \"MIT\",\n  \"author\": \"Jayce Li\",\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && rollup -c\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"postbuild\": \"rimraf dist/types\",\n    \"prebuild\": \"rimraf dist\",\n    \"test\": \"jest\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@dayflow/core\": \"workspace:*\",\n    \"@rollup/plugin-commonjs\": \"catalog:\",\n    \"@rollup/plugin-node-resolve\": \"catalog:\",\n    \"@rollup/plugin-typescript\": \"catalog:\",\n    \"@types/jest\": \"catalog:\",\n    \"jest\": \"catalog:\",\n    \"jest-environment-jsdom\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-dts\": \"catalog:\",\n    \"temporal-polyfill\": \"catalog:\",\n    \"ts-jest\": \"catalog:\",\n    \"typescript\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"@dayflow/core\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/drag/rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport typescript from '@rollup/plugin-typescript';\nimport { dts } from 'rollup-plugin-dts';\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: [\n      {\n        file: 'dist/index.js',\n        format: 'esm',\n        sourcemap: false,\n        exports: 'named',\n      },\n    ],\n    plugins: [\n      resolve(),\n      commonjs(),\n      typescript({\n        tsconfig: './tsconfig.build.json',\n      }),\n    ],\n    external: [\n      '@dayflow/core',\n      'temporal-polyfill',\n      'preact',\n      'preact/hooks',\n      'preact/compat',\n    ],\n  },\n  {\n    input: 'dist/types/index.d.ts',\n    output: [{ file: 'dist/index.d.ts', format: 'es' }],\n    plugins: [dts()],\n    external: ['temporal-polyfill', 'preact', 'preact/hooks', 'preact/compat'],\n  },\n];\n"
  },
  {
    "path": "packages/plugins/drag/src/components/DefaultDragIndicator.tsx",
    "content": "import {\n  DragIndicatorRenderer,\n  buildDiagonalPatternBackground,\n} from '@dayflow/core';\n\nconst colorBarClipPath =\n  'inset(0.25rem calc(100% - 0.25rem - 3px) 0.25rem 0.25rem round 9999px)';\n\nconst CalendarDaysIcon = ({ className }: { className?: string }) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width='24'\n    height='24'\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M8 2v4' />\n    <path d='M16 2v4' />\n    <rect width='18' height='18' x='3' y='4' rx='2' />\n    <path d='M3 10h18' />\n    <path d='M8 14h.01' />\n    <path d='M12 14h.01' />\n    <path d='M16 14h.01' />\n    <path d='M8 18h.01' />\n    <path d='M12 18h.01' />\n    <path d='M16 18h.01' />\n  </svg>\n);\n\nexport const DefaultDragIndicatorRenderer: DragIndicatorRenderer = {\n  renderAllDayContent: ({\n    title,\n    color: _color,\n    isMobile,\n    isLightBackground,\n  }) => (\n    <div className='df-drag-indicator-all-day'>\n      <CalendarDaysIcon\n        className='df-drag-indicator-icon'\n        data-light={isLightBackground}\n      />\n      <div\n        className={`df-event-title df-event-title-tight ${isMobile ? 'df-mobile-mask-fade' : ''}`}\n        data-light={isLightBackground}\n      >\n        {title}\n      </div>\n    </div>\n  ),\n\n  renderRegularContent: ({\n    drag,\n    title,\n    layout: _layout,\n    formatTime,\n    getLineColor,\n    getDynamicPadding,\n    color,\n    isMobile,\n    isLightBackground,\n    calendarLineColors,\n  }) => {\n    const lineColors =\n      calendarLineColors && calendarLineColors.length > 0\n        ? calendarLineColors\n        : [getLineColor(color || 'blue')];\n    const colorBarValue = buildDiagonalPatternBackground(lineColors);\n\n    const colorBarContent =\n      lineColors.length > 1 ? (\n        <div\n          className='df-event-color-bar-overlay'\n          style={{\n            background: colorBarValue,\n            clipPath: colorBarClipPath,\n          }}\n        />\n      ) : (\n        <div\n          className='df-event-color-bar'\n          style={{\n            backgroundColor: colorBarValue,\n          }}\n        />\n      );\n\n    const rawPadding = getDynamicPadding(drag);\n    const density = rawPadding.includes('compact') ? 'compact' : 'default';\n\n    return (\n      <div className='df-drag-indicator-regular-wrapper'>\n        {colorBarContent}\n        <div\n          className='df-event-timed-content'\n          data-density={density}\n          data-light={isLightBackground}\n        >\n          <div\n            className={`df-event-title ${drag.endHour - drag.startHour <= 0.25 ? 'df-event-title-tight' : ''} ${isMobile ? 'df-mobile-mask-fade' : ''}`}\n            data-light={isLightBackground}\n          >\n            {title}\n          </div>\n          {!drag.allDay && drag.endHour - drag.startHour > 0.5 && (\n            <div className='df-event-time' data-light={isLightBackground}>\n              {formatTime(drag.startHour)} - {formatTime(drag.endHour)}\n            </div>\n          )}\n        </div>\n      </div>\n    );\n  },\n\n  renderDefaultContent: ({ drag: _drag, title, allDay, isMobile }) => {\n    if (allDay) {\n      return (\n        <div className='df-drag-indicator-all-day'>\n          <CalendarDaysIcon className='df-drag-indicator-icon' />\n          <div\n            className={`df-event-title df-event-title-tight ${isMobile ? 'df-mobile-mask-fade' : ''}`}\n          >\n            {title}\n          </div>\n        </div>\n      );\n    }\n\n    return (\n      <div className='df-drag-indicator-regular-wrapper'>\n        <div className='df-fill-primary df-event-color-bar' />\n        <div className='df-event-timed-content' data-density='default'>\n          <div\n            className={`df-text-primary df-event-title ${isMobile ? 'df-mobile-mask-fade' : ''}`}\n          >\n            {title}\n          </div>\n        </div>\n      </div>\n    );\n  },\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/components/DragIndicatorComponent.tsx",
    "content": "import {\n  DragIndicatorProps,\n  DragIndicatorRenderer,\n  useLocale,\n} from '@dayflow/core';\n\nimport { DefaultDragIndicatorRenderer } from './DefaultDragIndicator';\n\ninterface DragIndicatorComponentProps extends DragIndicatorProps {\n  renderer?: DragIndicatorRenderer;\n}\n\nconst DragIndicatorComponent = ({\n  drag,\n  color,\n  title,\n  layout,\n  allDay,\n  formatTime,\n  getLineColor,\n  getDynamicPadding,\n  renderer = DefaultDragIndicatorRenderer,\n  isMobile,\n  isLightBackground,\n}: DragIndicatorComponentProps) => {\n  const { t } = useLocale();\n  const eventTitle = title || (allDay ? t('newAllDayEvent') : t('newEvent'));\n\n  // Compute line colors for all calendars on this event\n  const calendarLineColors: string[] =\n    drag.calendarIds && drag.calendarIds.length > 0\n      ? drag.calendarIds.map(id => getLineColor(id))\n      : color\n        ? [getLineColor(color)]\n        : [];\n  const hasCalendarColors = calendarLineColors.length > 0;\n\n  const renderContent = () => {\n    if (color || hasCalendarColors) {\n      if (allDay) {\n        return renderer.renderAllDayContent({\n          drag,\n          color,\n          title: eventTitle,\n          layout,\n          allDay,\n          formatTime,\n          getLineColor,\n          getDynamicPadding,\n          isMobile,\n          isLightBackground,\n          calendarLineColors,\n        });\n      }\n      return renderer.renderRegularContent({\n        drag,\n        color,\n        title: eventTitle,\n        layout,\n        allDay,\n        formatTime,\n        getLineColor,\n        getDynamicPadding,\n        isMobile,\n        isLightBackground,\n        calendarLineColors,\n      });\n    }\n\n    return renderer.renderDefaultContent({\n      drag,\n      color,\n      title: eventTitle,\n      layout,\n      allDay,\n      formatTime,\n      getLineColor,\n      getDynamicPadding,\n      isMobile,\n      isLightBackground,\n      calendarLineColors,\n    });\n  };\n\n  return <div className='df-drag-indicator-content'>{renderContent()}</div>;\n};\n\nexport default DragIndicatorComponent;\n"
  },
  {
    "path": "packages/plugins/drag/src/components/MonthDragIndicator.tsx",
    "content": "import { Event, daysDifference, useLocale } from '@dayflow/core';\n\nconst CalendarIcon = ({ className }: { className?: string }) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width='24'\n    height='24'\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M8 2v4' />\n    <path d='M16 2v4' />\n    <rect width='18' height='18' x='3' y='4' rx='2' />\n    <path d='M3 10h18' />\n  </svg>\n);\n\ninterface MonthDragIndicatorProps {\n  event: Event;\n  isCreating: boolean;\n  targetDate: Date | null;\n  isMultiDay?: boolean;\n  startDate?: Date | null;\n  endDate?: Date | null;\n  isMobile?: boolean;\n}\n\nconst MonthDragIndicatorComponent = ({\n  event,\n  isCreating,\n  isMultiDay = false,\n  startDate,\n  endDate,\n  isMobile: _isMobile,\n}: MonthDragIndicatorProps) => {\n  const { t } = useLocale();\n  const getDisplayContent = () => {\n    if (isCreating) {\n      return {\n        title: t('newEvent'),\n        icon: <CalendarIcon className='df-drag-indicator-icon' />,\n        showDateRange: false,\n      };\n    }\n\n    if (isMultiDay && startDate && endDate) {\n      const duration = daysDifference(startDate, endDate) + 1;\n      return {\n        title: event.title.replace(/ \\(\\d+天\\)$/, ''),\n        showDateRange: true,\n        duration,\n      };\n    }\n\n    return {\n      title: event.title,\n      showDateRange: false,\n    };\n  };\n\n  const content = getDisplayContent();\n\n  return (\n    <div className='df-drag-indicator-month'>\n      {content.icon ? (\n        <div className='df-drag-indicator-month-icon-wrap'>{content.icon}</div>\n      ) : null}\n      <div className='df-drag-indicator-month-content'>\n        <div className='df-drag-indicator-title-mask df-drag-indicator-month-title'>\n          {content.title}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default MonthDragIndicatorComponent;\n"
  },
  {
    "path": "packages/plugins/drag/src/components/__tests__/MonthDragIndicator.test.tsx",
    "content": "import { LocaleProvider } from '@dayflow/core';\nimport MonthDragIndicatorComponent from '@drag/components/MonthDragIndicator';\nimport { render } from 'preact';\nimport { Temporal } from 'temporal-polyfill';\n\ndescribe('MonthDragIndicatorComponent', () => {\n  it('keeps long all-day titles clipped within the drag indicator width', () => {\n    const container = document.createElement('div');\n    document.body.append(container);\n\n    const event = {\n      id: 'event-1',\n      title:\n        'An extremely long all-day event title that should stay clipped inside the month drag indicator',\n      allDay: true,\n      start: Temporal.PlainDate.from('2026-04-11'),\n      end: Temporal.PlainDate.from('2026-04-11'),\n      calendarId: 'default',\n    };\n\n    render(\n      <LocaleProvider locale='en'>\n        <MonthDragIndicatorComponent\n          event={event}\n          isCreating={false}\n          targetDate={new Date('2026-04-11T00:00:00.000Z')}\n        />\n      </LocaleProvider>,\n      container\n    );\n\n    const root = container.firstElementChild as HTMLElement | null;\n    const title = Array.from(container.querySelectorAll('div'))\n      .toReversed()\n      .find(element =>\n        element.textContent?.includes('An extremely long all-day event title')\n      ) as HTMLElement | undefined;\n\n    expect(root).not.toBeNull();\n    expect(root?.className).toContain('df-drag-indicator-month');\n    expect(title).toBeDefined();\n    expect(title?.className).toContain('df-drag-indicator-title-mask');\n    expect(title?.className).toContain('df-drag-indicator-month-title');\n\n    render(null, container);\n    container.remove();\n  });\n});\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/index.ts",
    "content": "export * from './useDrag';\nexport * from './useDragCommon';\nexport * from './useDragState';\nexport * from './useDragManager';\nexport * from './useDragHandlers';\nexport * from './useWeekDayDrag';\nexport * from './useMonthDrag';\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/useDrag.ts",
    "content": "import {\n  useDragProps,\n  useDragReturn,\n  ViewType,\n  getLineColor,\n} from '@dayflow/core';\nimport { defaultDragConfig } from '@drag/utils/defaultDragConfig';\nimport { useMemo } from 'preact/hooks';\n\nimport { useDragCommon } from './useDragCommon';\nimport { useDragHandlers } from './useDragHandlers';\nimport { useDragManager } from './useDragManager';\nimport { useDragState } from './useDragState';\nimport { useMonthDrag } from './useMonthDrag';\nimport { useWeekDayDrag } from './useWeekDayDrag';\n\nexport const useDrag = (options: useDragProps): useDragReturn => {\n  // Merge default configuration with user-provided configuration\n  const config = useMemo(\n    () => ({\n      ...defaultDragConfig,\n      ...options,\n      getLineColor: (color: string) => {\n        if (options.getLineColor) {\n          return options.getLineColor(color);\n        }\n        return getLineColor(color, options.app?.getCalendarRegistry());\n      },\n    }),\n    [options]\n  );\n\n  const { viewType } = config;\n  const isDateGridView =\n    viewType === ViewType.MONTH || viewType === ViewType.YEAR;\n\n  // Initialize common utility functions (shared utility methods)\n  const common = useDragCommon(config);\n\n  // Initialize state management (drag state and refs)\n  const state = useDragState(config);\n\n  // Initialize indicator manager (create, update, remove indicators)\n  const manager = useDragManager(config);\n\n  // Initialize drag event handlers (all event handling logic)\n  const handlers = useDragHandlers({\n    options: config,\n    common,\n    state,\n    manager,\n  });\n\n  // Initialize view-specific features\n  const weekDaySpecific = useWeekDayDrag({\n    options: config,\n    common,\n    state,\n    manager,\n    handleDragMove: handlers.handleDragMove,\n    handleDragEnd: handlers.handleDragEnd,\n  });\n\n  const monthSpecific = useMonthDrag({\n    options: config,\n    common,\n    state,\n    manager,\n  });\n\n  // Combine and return complete interface\n  return {\n    // Indicator management methods\n    createDragIndicator: manager.createDragIndicator,\n    updateDragIndicator: manager.updateDragIndicator,\n    removeDragIndicator: manager.removeDragIndicator,\n\n    // Drag event handler methods\n    handleCreateStart: handlers.handleCreateStart,\n    handleMoveStart: handlers.handleMoveStart,\n    handleResizeStart: handlers.handleResizeStart,\n\n    // State\n    dragState: state.dragState,\n    isDragging: state.dragState.active,\n\n    // Week/Day view specific methods (optional)\n    ...(isDateGridView\n      ? {\n          // Month view specific methods\n          daysDifference: monthSpecific.daysDifference,\n          addDaysToDate: monthSpecific.addDaysToDate,\n          getTargetDateFromPosition: monthSpecific.getTargetDateFromPosition,\n        }\n      : {\n          // Week/Day view specific methods\n          handleCreateAllDayEvent: weekDaySpecific.handleCreateAllDayEvent,\n          pixelYToHour: weekDaySpecific.pixelYToHour,\n          getColumnDayIndex: weekDaySpecific.getColumnDayIndex,\n        }),\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/useDragCommon.ts",
    "content": "import {\n  useDragProps,\n  ViewType,\n  UseDragCommonReturn,\n  daysDifference as utilsDaysDifference,\n  addDays as utilsAddDays,\n} from '@dayflow/core';\n// Shared utility hook providing common utility functions for drag operations\nimport { useCallback, useMemo } from 'preact/hooks';\n\nexport const useDragCommon = (options: useDragProps): UseDragCommonReturn => {\n  const {\n    calendarRef,\n    allDayRowRef,\n    viewType,\n    HOUR_HEIGHT = 72,\n    FIRST_HOUR = 0,\n    LAST_HOUR = 24,\n  } = options;\n\n  // View type check\n  const isMonthView = viewType === ViewType.MONTH;\n  const isWeekView = viewType === ViewType.WEEK;\n\n  // Week/Day view utility functions\n  const pixelYToHour = useCallback(\n    (y: number) => {\n      if (isMonthView || !calendarRef.current) return FIRST_HOUR;\n      const calendarContent = calendarRef.current.querySelector(\n        '.df-calendar-content'\n      );\n      if (!calendarContent) return FIRST_HOUR;\n\n      const contentRect = calendarContent.getBoundingClientRect();\n      const scrollTop = calendarContent.scrollTop;\n      // Measure the actual offset from .df-calendar-content top to the first time grid row,\n      // accounting for any boundary elements (top boundary) above the grid\n      const firstGridRow = calendarContent.querySelector('.df-time-grid-row');\n      const gridOffset = firstGridRow\n        ? firstGridRow.getBoundingClientRect().top - contentRect.top + scrollTop\n        : 0;\n      const relativeY = y - contentRect.top + scrollTop - gridOffset;\n      const hour = relativeY / HOUR_HEIGHT + FIRST_HOUR;\n      return Math.max(FIRST_HOUR, Math.min(LAST_HOUR, hour));\n    },\n    [calendarRef, FIRST_HOUR, HOUR_HEIGHT, LAST_HOUR, isMonthView]\n  );\n\n  const getColumnDayIndex = useCallback(\n    (x: number) => {\n      if (isMonthView || !calendarRef.current) return 0;\n\n      // Use the translated grid element if available to correctly handle mobile swipe offset\n      const gridElement =\n        options.timeGridRef?.current ||\n        calendarRef.current.querySelector('.df-time-grid-row') ||\n        calendarRef.current.querySelector('.df-calendar-content');\n\n      if (!gridElement) return 0;\n\n      const gridRect = gridElement.getBoundingClientRect();\n      const relativeX = x - gridRect.left;\n\n      let totalWidth = gridRect.width;\n      const daysToShow = options.displayDays || (isWeekView ? 7 : 1);\n      const dayColumnWidth = totalWidth / daysToShow;\n\n      const columnIndex = Math.floor(relativeX / dayColumnWidth);\n      return Math.max(0, Math.min(daysToShow - 1, columnIndex));\n    },\n    [\n      calendarRef,\n      isMonthView,\n      isWeekView,\n      options.timeGridRef,\n      options.displayDays,\n    ]\n  );\n\n  const handleDirectScroll = useCallback(\n    (clientY: number) => {\n      if (isMonthView || !calendarRef.current) return;\n      const calendarContent = calendarRef.current.querySelector(\n        '.df-calendar-content'\n      );\n      if (!calendarContent) return;\n\n      const rect = calendarContent.getBoundingClientRect();\n      if (clientY < rect.top) {\n        calendarContent.scrollTop += clientY - rect.top;\n      } else if (clientY + 40 > rect.bottom) {\n        calendarContent.scrollTop += clientY + 40 - rect.bottom;\n      }\n    },\n    [calendarRef, isMonthView]\n  );\n\n  const checkIfInAllDayArea = useCallback(\n    (clientY: number): boolean => {\n      if (isMonthView || !allDayRowRef?.current) return false;\n      const allDayRect = allDayRowRef.current.getBoundingClientRect();\n      return clientY >= allDayRect.top && clientY <= allDayRect.bottom;\n    },\n    [allDayRowRef, isMonthView]\n  );\n\n  // Month view utility functions\n  const ONE_DAY_MS = useMemo(() => 24 * 60 * 60 * 1000, []);\n\n  // Use unified functions from utils\n  const daysDifference = useCallback(\n    (date1: Date, date2: Date): number => utilsDaysDifference(date1, date2),\n    []\n  );\n\n  const addDaysToDate = useCallback(\n    (date: Date, days: number): Date => utilsAddDays(date, days),\n    []\n  );\n\n  const getTargetDateFromPosition = useCallback(\n    (clientX: number, clientY: number): Date | null => {\n      if (\n        (viewType !== ViewType.MONTH && viewType !== ViewType.YEAR) ||\n        !calendarRef.current\n      )\n        return null;\n\n      const hitElements = document.elementsFromPoint(clientX, clientY);\n      for (const element of hitElements) {\n        let dateElement = element as HTMLElement | null;\n        let searchDepth = 0;\n\n        while (\n          dateElement &&\n          !Object.hasOwn(dateElement.dataset, 'date') &&\n          searchDepth < 10\n        ) {\n          dateElement = dateElement.parentElement as HTMLElement | null;\n          searchDepth++;\n        }\n\n        if (dateElement && Object.hasOwn(dateElement.dataset, 'date')) {\n          const dateStr = dateElement.dataset.date;\n          if (dateStr) {\n            return new Date(dateStr + 'T00:00:00');\n          }\n        }\n      }\n\n      return null;\n    },\n    [calendarRef, viewType]\n  );\n\n  return {\n    pixelYToHour,\n    getColumnDayIndex,\n    checkIfInAllDayArea,\n    handleDirectScroll,\n    daysDifference,\n    addDaysToDate,\n    getTargetDateFromPosition,\n    ONE_DAY_MS,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/useDragHandlers.ts",
    "content": "import {\n  Event,\n  MonthDragState,\n  ViewType,\n  WeekDayDragState,\n  UseDragHandlersReturn,\n  UseDragHandlersParams,\n  DragService,\n  roundToTimeStep,\n  TIME_STEP,\n  getDateByDayIndex,\n  useLocale,\n  temporalToDate,\n  dateToZonedDateTime,\n  dateToPlainDate,\n} from '@dayflow/core';\nimport {\n  buildDateGridCreateEvent,\n  buildDateGridDropResult,\n  buildDateGridMoveStartData,\n  buildDateGridPreviewUpdate,\n  shouldActivateDateGridMove,\n} from '@drag/hooks/utils/dateGridDrag';\nimport {\n  addDocumentDragListeners,\n  applyGlobalDragCursor,\n  clearGlobalDragCursor,\n  getClientCoordinates,\n  isTouchLikeEvent,\n  removeDocumentDragListeners,\n} from '@drag/hooks/utils/dragInteraction';\nimport {\n  canonicalizeEditedEvent as canonicalizeEditedEventForTimeZone,\n  getAppTimeZone as getAppTimeZoneFromApp,\n  getDayIndexForDate as getDayIndexForDateInWeek,\n  getEffectiveDaySpan as getEffectiveDaySpanForEvent,\n  getEventDateForEditing as getEventDateForEditingInTimeZone,\n  getTimedEventHoursForEditing as getTimedEventHoursForEditingInTimeZone,\n} from '@drag/hooks/utils/eventEditing';\nimport { resolveDragSourceElement } from '@drag/hooks/utils/resolveDragSourceElement';\nimport {\n  buildWeekDayCreateEvent,\n  buildWeekDayDropEvent,\n  finalizeWeekDayDragHours,\n} from '@drag/hooks/utils/weekDay/completion';\nimport {\n  buildCrossRegionAllDayPreview,\n  buildCrossRegionTimedPreview,\n  buildUniversalMoveDropResult,\n  shouldActivateUniversalMoveIndicator,\n} from '@drag/hooks/utils/weekDay/crossRegion';\nimport {\n  buildAllDayCreateMovePreview,\n  buildAllDayResizePreview,\n  buildCrossDayTimedResizePreview,\n  buildSingleDayTimedResizePreview,\n  buildTimedMovePreview,\n  buildWeekDayCreateStartData,\n  buildWeekDayMoveStartData,\n  buildWeekDayResizeStartData,\n  getAllDayEventDurationDays,\n} from '@drag/hooks/utils/weekDay/drag';\nimport { buildWeekDayDragLayout } from '@drag/hooks/utils/weekDay/layout';\nimport {\n  buildSingleDayTimedResizeEventUpdate,\n  buildTimedCreatePreview,\n} from '@drag/hooks/utils/weekDay/preview';\nimport { useCallback, useRef } from 'preact/hooks';\n\ntype InternalDragRef = {\n  pendingMove?: boolean;\n};\n\nexport const useDragHandlers = (\n  params: UseDragHandlersParams\n): UseDragHandlersReturn => {\n  const { t } = useLocale();\n  const { options, common, state, manager } = params;\n  const {\n    viewType,\n    onEventsUpdate,\n    onEventCreate,\n    onEventEdit,\n    calculateNewEventLayout,\n    calculateDragLayout,\n    currentWeekStart,\n    events,\n    allDayRowRef,\n    FIRST_HOUR = 0,\n    LAST_HOUR = 24,\n    MIN_DURATION = 0.25,\n    app,\n  } = options;\n\n  const appTimeZone = getAppTimeZoneFromApp(app);\n  const getAppTimeZone = () => appTimeZone;\n\n  const getEventDateForEditing = (temporal: Event['start']) =>\n    getEventDateForEditingInTimeZone(temporal, appTimeZone);\n\n  const canonicalizeEditedEvent = (\n    originalEvent: Event,\n    visualEvent: Event\n  ): Event =>\n    canonicalizeEditedEventForTimeZone(originalEvent, visualEvent, appTimeZone);\n\n  const getTimedEventHoursForEditing = (event: Event) =>\n    getTimedEventHoursForEditingInTimeZone(event, appTimeZone);\n\n  const {\n    dragRef,\n    currentDragRef,\n    setDragState,\n    resetDragState,\n    throttledSetEvents,\n  } = state;\n  const { removeDragIndicator, createDragIndicator, updateDragIndicator } =\n    manager;\n  const {\n    pixelYToHour,\n    getColumnDayIndex,\n    checkIfInAllDayArea,\n    handleDirectScroll,\n    daysDifference,\n    addDaysToDate,\n    getTargetDateFromPosition,\n  } = common;\n\n  const isDateGridView =\n    viewType === ViewType.MONTH || viewType === ViewType.YEAR;\n  const isDayView = viewType === ViewType.DAY;\n  const shouldPreviewDateGridEventChanges = false;\n  const dateGridPreviewFrameRef = useRef<number | null>(null);\n  const latestDateGridPointerRef = useRef<{\n    clientX: number;\n    clientY: number;\n  } | null>(null);\n\n  type DateCellRect = {\n    date: string;\n    left: number;\n    top: number;\n    right: number;\n    bottom: number;\n  };\n  const dateCellCacheRef = useRef<DateCellRect[]>([]);\n\n  const buildDateCellCache = useCallback(() => {\n    if (!options.calendarRef?.current) return;\n    const cells =\n      options.calendarRef.current.querySelectorAll<HTMLElement>('[data-date]');\n    const cache: DateCellRect[] = [];\n    for (const cell of cells) {\n      const date = cell.dataset.date;\n      if (!date) continue;\n      const rect = cell.getBoundingClientRect();\n      cache.push({\n        date,\n        left: rect.left,\n        top: rect.top,\n        right: rect.right,\n        bottom: rect.bottom,\n      });\n    }\n    dateCellCacheRef.current = cache;\n  }, [options.calendarRef]);\n\n  const getCachedTargetDate = useCallback(\n    (clientX: number, clientY: number): Date | null => {\n      for (const cell of dateCellCacheRef.current) {\n        if (\n          clientX >= cell.left &&\n          clientX <= cell.right &&\n          clientY >= cell.top &&\n          clientY <= cell.bottom\n        ) {\n          return new Date(cell.date + 'T00:00:00');\n        }\n      }\n      return null;\n    },\n    []\n  );\n\n  const TIME_STEP_MS = TIME_STEP * 60 * 60 * 1000;\n  const getEffectiveDaySpan = (\n    start: Date,\n    end: Date,\n    isAllDay: boolean = false\n  ): number => getEffectiveDaySpanForEvent(start, end, isAllDay);\n\n  const getDayIndexForDate = (date: Date, fallback: number = 0): number =>\n    getDayIndexForDateInWeek(currentWeekStart, date, fallback);\n\n  const cancelScheduledDateGridPreview = useCallback(() => {\n    if (dateGridPreviewFrameRef.current !== null) {\n      cancelAnimationFrame(dateGridPreviewFrameRef.current);\n      dateGridPreviewFrameRef.current = null;\n    }\n    latestDateGridPointerRef.current = null;\n  }, []);\n\n  const applyDateGridPreview = useCallback(\n    (clientX: number, clientY: number) => {\n      const drag = dragRef.current as typeof dragRef.current & InternalDragRef;\n      if (!drag || (!drag.active && !drag.pendingMove)) return;\n\n      const targetDate =\n        getCachedTargetDate(clientX, clientY) ??\n        getTargetDateFromPosition(clientX, clientY);\n      if (!targetDate) return;\n\n      const previewUpdate = buildDateGridPreviewUpdate({\n        addDaysToDate,\n        daysDifference,\n        drag,\n        targetDate,\n      });\n      if (!previewUpdate) return;\n\n      if (previewUpdate.kind === 'target-only') {\n        if (drag.targetDate?.getTime() === previewUpdate.targetDate.getTime()) {\n          return;\n        }\n        drag.targetDate = previewUpdate.targetDate;\n        return;\n      }\n\n      const nextTargetMs = previewUpdate.targetDate.getTime();\n      const nextStartMs = previewUpdate.startDate.getTime();\n      const nextEndMs = previewUpdate.endDate.getTime();\n      const currentTargetMs = drag.targetDate?.getTime();\n      const currentStartMs = drag.originalStartDate?.getTime();\n      const currentEndMs = drag.originalEndDate?.getTime();\n\n      if (\n        currentTargetMs === nextTargetMs &&\n        currentStartMs === nextStartMs &&\n        currentEndMs === nextEndMs\n      ) {\n        return;\n      }\n\n      drag.originalStartDate = new Date(previewUpdate.startDate.getTime());\n      drag.originalEndDate = new Date(previewUpdate.endDate.getTime());\n      drag.targetDate = new Date(previewUpdate.targetDate.getTime());\n\n      if (options.isMobile) {\n        // On mobile, setDragState triggers a full re-render of the calendar\n        // which blocks the UI thread long enough to freeze the drag indicator.\n        // Instead, highlight the target cell directly in the DOM — no re-render.\n        const calendarEl = options.calendarRef?.current;\n        if (calendarEl) {\n          calendarEl\n            .querySelectorAll<HTMLElement>('[data-drag-over]')\n            .forEach(el => {\n              delete el.dataset.dragOver;\n            });\n          const d = previewUpdate.targetDate;\n          const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n          const targetEl = calendarEl.querySelector<HTMLElement>(\n            `[data-date=\"${dateStr}\"]`\n          );\n          if (targetEl) {\n            targetEl.dataset.dragOver = 'true';\n          }\n        }\n      } else {\n        setDragState(prev => {\n          if ('targetDate' in prev) {\n            return {\n              ...prev,\n              targetDate: previewUpdate.targetDate,\n              startDate: previewUpdate.startDate,\n              endDate: previewUpdate.endDate,\n            } as MonthDragState;\n          }\n          return prev;\n        });\n      }\n\n      const isAllDay = drag.originalEvent?.allDay || false;\n      const newStartTemporal = isAllDay\n        ? dateToPlainDate(previewUpdate.startDate)\n        : dateToZonedDateTime(previewUpdate.startDate, getAppTimeZone());\n      const newEndTemporal = isAllDay\n        ? dateToPlainDate(previewUpdate.endDate)\n        : dateToZonedDateTime(previewUpdate.endDate, getAppTimeZone());\n\n      if (shouldPreviewDateGridEventChanges) {\n        throttledSetEvents(\n          (prev: Event[]) =>\n            prev.map(event =>\n              event.id === drag.eventId\n                ? {\n                    ...event,\n                    start: newStartTemporal,\n                    end: newEndTemporal,\n                    title: event.title,\n                  }\n                : event\n            ),\n          drag.mode ?? undefined\n        );\n      }\n    },\n    [\n      dragRef,\n      getCachedTargetDate,\n      getTargetDateFromPosition,\n      setDragState,\n      throttledSetEvents,\n      shouldPreviewDateGridEventChanges,\n      daysDifference,\n      addDaysToDate,\n      getAppTimeZone,\n    ]\n  );\n\n  // Cross-region drag move (Week/Day view specific) - complete version\n  const handleUniversalDragMove = useCallback(\n    (e: MouseEvent | TouchEvent) => {\n      const readOnlyConfig = app?.getReadOnlyConfig();\n      const isDraggable = readOnlyConfig?.draggable !== false;\n      const isEditable = !app?.state.readOnly;\n      const drag = dragRef.current as typeof dragRef.current & InternalDragRef;\n      if (!drag) return;\n      if (drag.mode === 'move' && !isDraggable) return;\n      if ((drag.mode === 'resize' || drag.mode === 'create') && !isEditable)\n        return;\n\n      // Prevent scrolling on touch devices\n      if (e.cancelable) {\n        e.preventDefault();\n        e.stopPropagation();\n      }\n\n      const { clientX, clientY } = getClientCoordinates(e);\n\n      if (!drag || !drag.active) return;\n\n      // Deferred indicator creation for move mode\n      if (drag.mode === 'move' && !drag.indicatorVisible) {\n        if (\n          !shouldActivateUniversalMoveIndicator({\n            clientX,\n            clientY,\n            startX: drag.startX,\n            startY: drag.startY,\n          })\n        ) {\n          return;\n        }\n\n        createDragIndicator(\n          drag,\n          drag.calendarId,\n          drag.title,\n          null,\n          drag.sourceElement || undefined\n        );\n        drag.indicatorVisible = true;\n      }\n\n      // Set cursor based on drag mode and direction\n      if (drag.mode === 'resize') {\n        applyGlobalDragCursor(\n          'resize',\n          drag.allDay ? 'ew-resize' : 'ns-resize'\n        );\n      } else {\n        applyGlobalDragCursor('move', 'grabbing');\n      }\n\n      const isInAllDayArea = checkIfInAllDayArea(clientY);\n      const newDayIndex = isDayView\n        ? drag.dayIndex\n        : getColumnDayIndex(clientX);\n\n      if (isInAllDayArea) {\n        // Switch to all-day area\n        if (drag.allDay) {\n          setDragState(prev => ({\n            ...prev,\n            dayIndex: newDayIndex,\n            startHour: 0,\n            endHour: 0,\n            allDay: true,\n          }));\n        } else {\n          const { dragState: allDayDragState, dragUpdates } =\n            buildCrossRegionAllDayPreview({\n              currentWeekStart,\n              drag,\n              newDayIndex,\n            });\n          Object.assign(drag, dragUpdates);\n          removeDragIndicator();\n          drag.indicatorVisible = false;\n          const event = events?.find(target => target.id === drag.eventId);\n          // When switching regions, don't pass source element, use calculation method\n          drag.calendarIds = event?.calendarIds;\n          createDragIndicator(drag, event?.calendarId, event?.title);\n          drag.sourceElement = null;\n          drag.indicatorVisible = true;\n          setDragState(allDayDragState);\n        }\n        drag.dayIndex = newDayIndex;\n        updateDragIndicator(newDayIndex, 0, 0, true);\n      } else {\n        // Switch to regular time area\n        handleDirectScroll(clientY);\n        const mouseHour = pixelYToHour(clientY);\n\n        if (drag.allDay) {\n          const { dragState: timedDragState, dragUpdates } =\n            buildCrossRegionTimedPreview({\n              currentWeekStart,\n              drag,\n              firstHour: FIRST_HOUR,\n              lastHour: LAST_HOUR,\n              mouseHour,\n              newDayIndex,\n              roundToTimeStep,\n              timeStep: TIME_STEP,\n            });\n          Object.assign(drag, dragUpdates);\n          removeDragIndicator();\n          drag.indicatorVisible = false;\n          const event = events?.find(target => target.id === drag.eventId);\n          // When switching regions, don't pass source element, use calculation method\n          drag.calendarIds = event?.calendarIds;\n          createDragIndicator(drag, event?.calendarId, event?.title);\n          drag.sourceElement = null;\n          drag.indicatorVisible = true;\n          setDragState(timedDragState);\n        } else {\n          const { dragState: timedDragState, dragUpdates } =\n            buildCrossRegionTimedPreview({\n              currentWeekStart,\n              drag,\n              firstHour: FIRST_HOUR,\n              lastHour: LAST_HOUR,\n              mouseHour,\n              newDayIndex,\n              roundToTimeStep,\n              timeStep: TIME_STEP,\n            });\n          Object.assign(drag, dragUpdates);\n          setDragState(timedDragState);\n        }\n\n        // Calculate layout\n        const dragLayout =\n          drag.mode === 'move'\n            ? buildWeekDayDragLayout({\n                calculateDragLayout,\n                dayIndex: newDayIndex,\n                endHour: drag.endHour,\n                eventId: drag.eventId,\n                events,\n                roundToTimeStep,\n                startHour: drag.startHour,\n              })\n            : null;\n        updateDragIndicator(\n          newDayIndex,\n          roundToTimeStep(drag.startHour),\n          roundToTimeStep(drag.endHour),\n          false,\n          dragLayout\n        );\n      }\n    },\n    [\n      calculateDragLayout,\n      checkIfInAllDayArea,\n      createDragIndicator,\n      events,\n      FIRST_HOUR,\n      getColumnDayIndex,\n      handleDirectScroll,\n      LAST_HOUR,\n      isDayView,\n      pixelYToHour,\n      removeDragIndicator,\n      updateDragIndicator,\n      dragRef,\n      setDragState,\n      currentWeekStart,\n    ]\n  );\n\n  // Cross-region drag end (Week/Day view specific) - complete version\n  const handleUniversalDragEnd = useCallback(() => {\n    const drag = dragRef.current as typeof dragRef.current & InternalDragRef;\n    if (!drag || !drag.active) return;\n\n    const readOnlyConfig = app?.getReadOnlyConfig();\n    const isDraggable = readOnlyConfig?.draggable !== false;\n    const isEditable = !app?.state.readOnly;\n\n    if (drag.mode === 'move' && !isDraggable) return;\n    if ((drag.mode === 'resize' || drag.mode === 'create') && !isEditable)\n      return;\n\n    clearGlobalDragCursor();\n\n    // If dragging but threshold not met (indicator not visible), treat as click/cancel\n    if (drag.mode === 'move' && !drag.indicatorVisible) {\n      removeDocumentDragListeners(\n        handleUniversalDragMove,\n        handleUniversalDragEnd\n      );\n      resetDragState();\n      return;\n    }\n\n    if (drag.mode !== 'move' || !drag.eventId) return;\n\n    // Precompute updatedEvent to fire onEventDrop callback\n    const targetEvent = events?.find(e => e.id === drag.eventId);\n    if (targetEvent) {\n      const { originalEvent, updatedEvent } = buildUniversalMoveDropResult({\n        appTimeZone,\n        canonicalizeEditedEvent,\n        currentWeekStart,\n        drag,\n        getEffectiveDaySpan,\n        minDuration: MIN_DURATION,\n        roundToTimeStep,\n        targetEvent,\n      });\n\n      const dragConfig = app?.getPlugin<DragService>('drag')?.getConfig();\n      dragConfig?.onEventDrop?.(updatedEvent, originalEvent);\n\n      onEventsUpdate?.(\n        prev =>\n          prev.map(event => (event.id === drag.eventId ? updatedEvent : event)),\n        false,\n        'drag'\n      );\n    }\n\n    removeDocumentDragListeners(\n      handleUniversalDragMove,\n      handleUniversalDragEnd\n    );\n    removeDragIndicator();\n    resetDragState();\n  }, [\n    handleUniversalDragMove,\n    removeDragIndicator,\n    resetDragState,\n    onEventsUpdate,\n    MIN_DURATION,\n    dragRef,\n  ]);\n\n  // Drag move handler - complete version\n  const handleDragMove = useCallback(\n    (e: MouseEvent | TouchEvent) => {\n      const drag = dragRef.current as typeof dragRef.current & InternalDragRef;\n      if (!drag || (!drag.active && !drag.pendingMove)) return;\n\n      const readOnlyConfig = app?.getReadOnlyConfig();\n      const isDraggable = readOnlyConfig?.draggable !== false;\n      const isEditable = !app?.state.readOnly;\n\n      if (drag.mode === 'move' && !isDraggable) return;\n      if ((drag.mode === 'resize' || drag.mode === 'create') && !isEditable)\n        return;\n\n      // Prevent scrolling on touch devices\n      if (e.cancelable) {\n        e.preventDefault();\n        e.stopPropagation();\n      }\n\n      const { clientX, clientY } = getClientCoordinates(e);\n\n      if (!drag || (!drag.active && !drag.pendingMove)) return;\n\n      // Set cursor based on drag mode and direction\n      if (drag.mode === 'resize') {\n        applyGlobalDragCursor(\n          'resize',\n          isDateGridView || drag.allDay ? 'ew-resize' : 'ns-resize'\n        );\n      } else {\n        applyGlobalDragCursor(\n          drag.mode === 'create' ? 'create' : 'move',\n          'grabbing'\n        );\n      }\n\n      if (isDateGridView) {\n        // Month view drag logic\n        if (drag.mode !== 'resize') {\n          if (drag.mode === 'move') {\n            if (drag.pendingMove) {\n              if (\n                !shouldActivateDateGridMove({\n                  clientX,\n                  clientY,\n                  startX: drag.startX,\n                  startY: drag.startY,\n                })\n              ) {\n                return;\n              }\n\n              drag.pendingMove = false;\n              drag.active = true;\n\n              setDragState({\n                active: true,\n                mode: 'move',\n                eventId: drag.eventId,\n                targetDate: drag.targetDate ?? null,\n                startDate: drag.originalStartDate ?? null,\n                endDate: drag.originalEndDate ?? null,\n              });\n            }\n\n            if (\n              !drag.indicatorVisible &&\n              shouldActivateDateGridMove({\n                clientX,\n                clientY,\n                startX: drag.startX,\n                startY: drag.startY,\n              })\n            ) {\n              createDragIndicator(\n                drag,\n                drag.originalEvent?.calendarId,\n                drag.originalEvent?.title,\n                null,\n                drag.sourceElement || undefined\n              );\n              drag.indicatorVisible = true;\n            }\n          }\n\n          if (drag.indicatorVisible) {\n            updateDragIndicator(clientX, clientY);\n          }\n        }\n\n        latestDateGridPointerRef.current = { clientX, clientY };\n        if (dateGridPreviewFrameRef.current === null) {\n          dateGridPreviewFrameRef.current = requestAnimationFrame(() => {\n            dateGridPreviewFrameRef.current = null;\n            const latestPointer = latestDateGridPointerRef.current;\n            if (!latestPointer) return;\n            applyDateGridPreview(latestPointer.clientX, latestPointer.clientY);\n          });\n        }\n\n        return;\n      }\n\n      // Week/Day view drag logic\n      if (!drag.allDay) {\n        handleDirectScroll(clientY);\n      }\n      drag.lastClientY = clientY;\n      const mouseHour = pixelYToHour(clientY);\n\n      // Handle All-Day Create Drag\n      if (drag.mode === 'create' && drag.allDay) {\n        const { distance, newDayIndex } = buildAllDayCreateMovePreview({\n          clientX,\n          clientY,\n          drag,\n          getColumnDayIndex,\n          isDayView,\n        });\n\n        if (!drag.indicatorVisible) {\n          if (distance < 3) return;\n          createDragIndicator(drag, 'blue', t('newAllDayEvent'));\n          drag.indicatorVisible = true;\n        }\n\n        drag.dayIndex = newDayIndex;\n\n        updateDragIndicator(newDayIndex, 0, 0, true);\n        return;\n      }\n\n      if (drag.mode === 'resize') {\n        if (drag.allDay) {\n          // All-day event horizontal resize (by day)\n          const targetDayIndex = isDayView\n            ? drag.dayIndex\n            : getColumnDayIndex(clientX);\n          const { newStartDate, newEndDate } = buildAllDayResizePreview({\n            currentWeekStart,\n            drag,\n            getDateByDayIndex,\n            targetDayIndex,\n          });\n\n          drag.originalStartDate = new Date(newStartDate.getTime());\n          drag.originalEndDate = new Date(newEndDate.getTime());\n\n          // Update event\n          const newStartTemporal = dateToPlainDate(newStartDate);\n          const newEndTemporal = dateToPlainDate(newEndDate);\n\n          throttledSetEvents(\n            (prev: Event[]) =>\n              prev.map(event => {\n                if (event.id !== drag.eventId) return event;\n\n                return {\n                  ...event,\n                  start: newStartTemporal,\n                  end: newEndTemporal,\n                  allDay: true,\n                };\n              }),\n            drag.mode\n          );\n        } else {\n          // Regular event resize (supports multi-day)\n          const currentEvent = events?.find(\n            target => target.id === drag.eventId\n          );\n          if (!currentEvent) return;\n\n          if (!isDayView) {\n            const originalEvent = drag.originalEvent || currentEvent;\n            const targetDayIndex = getColumnDayIndex(clientX);\n            const {\n              indicatorEndHour,\n              indicatorStartHour,\n              newEndDate,\n              newStartDate,\n              startDayIndex,\n            } = buildCrossDayTimedResizePreview({\n              currentWeekStart,\n              drag: {\n                ...drag,\n                dayIndex: targetDayIndex,\n              },\n              firstHour: FIRST_HOUR,\n              getDateByDayIndex,\n              getDayIndexForDate,\n              getEventDateForEditing,\n              lastHour: LAST_HOUR,\n              mouseHour,\n              originalEvent,\n              roundToTimeStep,\n              timeStepMs: TIME_STEP_MS,\n            });\n\n            drag.originalStartDate = new Date(newStartDate.getTime());\n            drag.originalEndDate = new Date(newEndDate.getTime());\n            drag.startHour = indicatorStartHour;\n            drag.endHour = indicatorEndHour;\n            drag.dayIndex = startDayIndex;\n\n            throttledSetEvents(\n              (prev: Event[]) =>\n                prev.map(event => {\n                  if (event.id !== drag.eventId) return event;\n\n                  return {\n                    ...event,\n                    start: dateToZonedDateTime(newStartDate, getAppTimeZone()),\n                    end: dateToZonedDateTime(newEndDate, getAppTimeZone()),\n                    day: startDayIndex,\n                  };\n                }),\n              drag.mode\n            );\n\n            updateDragIndicator(\n              drag.dayIndex,\n              indicatorStartHour,\n              indicatorEndHour,\n              false\n            );\n            return;\n          }\n\n          const { endDayIndex, newEndHour, newStartHour, startDayIndex } =\n            buildSingleDayTimedResizePreview({\n              currentEvent,\n              drag,\n              firstHour: FIRST_HOUR,\n              getEffectiveDaySpan,\n              lastHour: LAST_HOUR,\n              mouseHour,\n              timeStep: TIME_STEP,\n            });\n\n          const [roundedStart, roundedEnd] = [\n            roundToTimeStep(newStartHour),\n            roundToTimeStep(newEndHour),\n          ];\n          drag.startHour = newStartHour;\n          drag.endHour = newEndHour;\n          drag.dayIndex = startDayIndex;\n\n          throttledSetEvents(\n            (prev: Event[]) =>\n              prev.map(event => {\n                if (event.id !== drag.eventId) return event;\n\n                const { newEndDate, newStartDate, updatedEvent } =\n                  buildSingleDayTimedResizeEventUpdate({\n                    appTimeZone,\n                    currentWeekStart,\n                    endDayIndex,\n                    event,\n                    getDateByDayIndex,\n                    roundedEnd,\n                    roundedStart,\n                    startDayIndex,\n                  });\n\n                drag.originalStartDate = new Date(newStartDate.getTime());\n                drag.originalEndDate = new Date(newEndDate.getTime());\n\n                return updatedEvent;\n              }),\n            drag.mode\n          );\n\n          updateDragIndicator(drag.dayIndex, roundedStart, roundedEnd, false);\n        }\n      } else if (drag.mode === 'create') {\n        const { endHour, startHour } = buildTimedCreatePreview({\n          clientY,\n          drag,\n          firstHour: FIRST_HOUR,\n          isMobile: !!options.isMobile,\n          lastHour: LAST_HOUR,\n          mouseHour,\n          roundToTimeStep,\n          timeStep: TIME_STEP,\n        });\n        drag.startHour = startHour;\n        drag.endHour = endHour;\n\n        // Remove setDragState, only update at drag end\n\n        const newEventLayout = calculateNewEventLayout?.(\n          drag.dayIndex,\n          drag.startHour,\n          drag.endHour\n        );\n        updateDragIndicator(\n          drag.dayIndex,\n          drag.startHour,\n          drag.endHour,\n          false,\n          newEventLayout\n        );\n      } else if (drag.mode === 'move') {\n        const {\n          dayIndex: newDayIndex,\n          endHour: newEndHour,\n          startHour: newStartHour,\n        } = buildTimedMovePreview({\n          clientX,\n          drag,\n          firstHour: FIRST_HOUR,\n          getColumnDayIndex,\n          isDayView,\n          lastHour: LAST_HOUR,\n          mouseHour,\n          roundToTimeStep,\n        });\n        drag.dayIndex = newDayIndex;\n        drag.startHour = newStartHour;\n        drag.endHour = newEndHour;\n\n        // Remove setDragState, only update at drag end\n\n        // Calculate layout and update drag indicator\n        const dragLayout = buildWeekDayDragLayout({\n          calculateDragLayout,\n          dayIndex: newDayIndex,\n          endHour: newEndHour,\n          eventId: drag.eventId,\n          events,\n          roundToTimeStep,\n          startHour: newStartHour,\n        });\n        updateDragIndicator(\n          newDayIndex,\n          roundToTimeStep(newStartHour),\n          roundToTimeStep(newEndHour),\n          false,\n          dragLayout\n        );\n      }\n    },\n    [\n      isDateGridView,\n      isDayView,\n      updateDragIndicator,\n      getTargetDateFromPosition,\n      throttledSetEvents,\n      daysDifference,\n      addDaysToDate,\n      FIRST_HOUR,\n      LAST_HOUR,\n      calculateNewEventLayout,\n      getColumnDayIndex,\n      pixelYToHour,\n      handleDirectScroll,\n      calculateDragLayout,\n      events,\n      dragRef,\n      createDragIndicator,\n    ]\n  );\n\n  // Drag end handler - complete version\n  const handleDragEnd = useCallback(\n    (e: MouseEvent | TouchEvent) => {\n      const drag = dragRef.current as typeof dragRef.current & InternalDragRef;\n      if (!drag || (!drag.active && !drag.pendingMove)) return;\n\n      const readOnlyConfig = app?.getReadOnlyConfig();\n      const isDraggable = readOnlyConfig?.draggable !== false;\n      const isEditable = !app?.state.readOnly;\n\n      if (drag.mode === 'move' && !isDraggable) return;\n      if ((drag.mode === 'resize' || drag.mode === 'create') && !isEditable)\n        return;\n\n      clearGlobalDragCursor();\n\n      // Remove mobile DOM drag-over highlights (if any were applied)\n      if (isDateGridView && options.isMobile) {\n        options.calendarRef?.current\n          ?.querySelectorAll<HTMLElement>('[data-drag-over]')\n          .forEach(el => {\n            delete el.dataset.dragOver;\n          });\n      }\n\n      // If dragging but threshold not met (indicator not visible), treat as click/cancel\n      if (\n        (drag.mode === 'move' || drag.mode === 'create') &&\n        !drag.indicatorVisible\n      ) {\n        removeDocumentDragListeners(handleDragMove, handleDragEnd);\n        resetDragState();\n        return;\n      }\n\n      const { clientX, clientY } = getClientCoordinates(e);\n\n      if (!drag || (!drag.active && !drag.pendingMove)) return;\n\n      if (isDateGridView) {\n        cancelScheduledDateGridPreview();\n        applyDateGridPreview(clientX, clientY);\n        const dateGridDropResult = buildDateGridDropResult({\n          appTimeZone,\n          canonicalizeEditedEvent,\n          clientX,\n          clientY,\n          drag,\n          events,\n          getTargetDateFromPosition,\n        });\n\n        if (\n          dateGridDropResult &&\n          (dateGridDropResult.kind === 'resize' ||\n            dateGridDropResult.kind === 'move')\n        ) {\n          setDragState(prev => {\n            if ('targetDate' in prev) {\n              return {\n                ...prev,\n                targetDate: dateGridDropResult.startDate,\n                startDate: dateGridDropResult.startDate,\n                endDate: dateGridDropResult.endDate,\n              } as MonthDragState;\n            }\n            return prev;\n          });\n\n          const dragConfig = app?.getPlugin<DragService>('drag')?.getConfig();\n          if (dateGridDropResult.kind === 'resize') {\n            dragConfig?.onEventResize?.(\n              dateGridDropResult.updatedEvent,\n              dateGridDropResult.originalEvent\n            );\n          } else {\n            dragConfig?.onEventDrop?.(\n              dateGridDropResult.updatedEvent,\n              dateGridDropResult.originalEvent\n            );\n          }\n\n          onEventsUpdate?.(\n            prev =>\n              prev.map(event =>\n                event.id === drag.eventId\n                  ? dateGridDropResult.updatedEvent\n                  : event\n              ),\n            false,\n            dateGridDropResult.kind === 'resize' ? 'resize' : 'drag'\n          );\n        } else if (dateGridDropResult?.kind === 'restore') {\n          onEventsUpdate?.(\n            prev =>\n              prev.map(event =>\n                event.id === drag.eventId\n                  ? dateGridDropResult.originalEvent\n                  : event\n              ),\n            false,\n            'drag'\n          );\n        }\n      } else {\n        // Week/Day view drag end logic\n        const { finalEndHour, finalStartHour } = finalizeWeekDayDragHours({\n          drag,\n          getEffectiveDaySpan,\n          minDuration: MIN_DURATION,\n          roundToTimeStep,\n        });\n\n        if (drag.mode === 'create') {\n          // Update state at drag end (Week/Day view)\n          setDragState(prev => {\n            if ('dayIndex' in prev) {\n              return {\n                ...prev,\n                dayIndex: drag.dayIndex,\n                startHour: finalStartHour,\n                endHour: finalEndHour,\n              } as WeekDayDragState;\n            }\n            return prev;\n          });\n\n          const writableCalendar = app\n            ?.getCalendarRegistry()\n            ?.getDefaultWritableCalendar();\n          if (!writableCalendar) return;\n          onEventCreate?.(\n            buildWeekDayCreateEvent({\n              appTimeZone,\n              currentWeekStart,\n              drag,\n              finalEndHour,\n              finalStartHour,\n              getDateByDayIndex,\n              title: drag.allDay ? t('newAllDayEvent') : t('newEvent'),\n              writableCalendarId: writableCalendar.id,\n            })\n          );\n        } else if (drag.mode === 'move' || drag.mode === 'resize') {\n          // Update state at drag end (Week/Day view)\n          setDragState(prev => {\n            if ('dayIndex' in prev) {\n              return {\n                ...prev,\n                dayIndex: drag.dayIndex,\n                startHour: finalStartHour,\n                endHour: finalEndHour,\n              } as WeekDayDragState;\n            }\n            return prev;\n          });\n\n          const originalEventWeekDay =\n            drag.originalEvent ||\n            events?.find(eventItem => eventItem.id === drag.eventId);\n          const weekDayDropMode = drag.mode;\n\n          // Precompute updatedEvent to fire onEventDrop/onEventResize callback\n          let updatedEventWeekDay: Event | undefined;\n          if (\n            originalEventWeekDay &&\n            (weekDayDropMode === 'move' || weekDayDropMode === 'resize')\n          ) {\n            updatedEventWeekDay = buildWeekDayDropEvent({\n              appTimeZone,\n              canonicalizeEditedEvent,\n              currentWeekStart,\n              drag: {\n                ...drag,\n                mode: weekDayDropMode,\n              },\n              finalEndHour,\n              finalStartHour,\n              getDateByDayIndex,\n              originalEvent: originalEventWeekDay,\n            });\n\n            const dragConfig = app?.getPlugin<DragService>('drag')?.getConfig();\n            if (drag.mode === 'move') {\n              dragConfig?.onEventDrop?.(\n                updatedEventWeekDay,\n                originalEventWeekDay\n              );\n            } else {\n              dragConfig?.onEventResize?.(\n                updatedEventWeekDay,\n                originalEventWeekDay\n              );\n            }\n          }\n\n          const dragSource = drag.mode === 'move' ? 'drag' : 'resize';\n\n          // For move and resize operations, we need to finalize the changes in the store\n          onEventsUpdate?.(\n            prev =>\n              prev.map(event => {\n                if (event.id !== drag.eventId) return event;\n                return updatedEventWeekDay ?? event;\n              }),\n            false,\n            dragSource\n          );\n        }\n      }\n\n      removeDocumentDragListeners(handleDragMove, handleDragEnd);\n      clearGlobalDragCursor();\n      removeDragIndicator();\n      drag.indicatorVisible = false;\n      drag.sourceElement = null;\n      resetDragState();\n    },\n    [\n      isDateGridView,\n      applyDateGridPreview,\n      cancelScheduledDateGridPreview,\n      handleDragMove,\n      removeDragIndicator,\n      resetDragState,\n      getTargetDateFromPosition,\n      throttledSetEvents,\n      MIN_DURATION,\n      currentWeekStart,\n      onEventCreate,\n      onEventsUpdate,\n      dragRef,\n      setDragState,\n    ]\n  );\n\n  // Create event start - complete version\n  const handleCreateStart = useCallback(\n    (e: MouseEvent | TouchEvent, ...args: (Date | number)[]) => {\n      if (app?.state.readOnly) return; // Non-editable if readOnly exists\n      if (!app?.getCalendarRegistry()?.getDefaultWritableCalendar()) return; // All calendars are read-only\n\n      // Prevent scrolling on touch devices\n      if ('cancelable' in e && e.cancelable) {\n        e.preventDefault();\n      }\n      e.stopPropagation();\n      if (dragRef.current?.active) return;\n\n      const { clientX, clientY } = getClientCoordinates(e);\n\n      if (isDateGridView) {\n        // Month view create event\n        const [targetDate] = args as [Date];\n        const newEvent = buildDateGridCreateEvent({\n          appTimeZone,\n          calendarId:\n            app?.getCalendarRegistry()?.getDefaultWritableCalendar()?.id ??\n            'blue',\n          targetDate,\n          title: t('newEvent'),\n        });\n\n        onEventCreate?.(newEvent);\n\n        if (onEventEdit) {\n          setTimeout(() => {\n            onEventEdit(newEvent);\n          }, 50);\n        }\n      } else {\n        // Week/Day view create event\n        const [dayIndex, startHour] = args as [number, number];\n        const drag = dragRef.current as typeof dragRef.current &\n          InternalDragRef;\n        if (!drag) return;\n        const { dragState: weekDayDragState, dragUpdates } =\n          buildWeekDayCreateStartData({\n            clientX,\n            clientY,\n            currentWeekStart,\n            dayIndex,\n            getDateByDayIndex,\n            isMobile: !!options.isMobile,\n            roundToTimeStep,\n            startHour,\n            timeStep: TIME_STEP,\n          });\n\n        Object.assign(drag, dragUpdates);\n        setDragState(weekDayDragState);\n\n        const newEventLayout = calculateNewEventLayout?.(\n          dayIndex,\n          drag.startHour,\n          drag.endHour\n        );\n        const writableCalId =\n          app?.getCalendarRegistry()?.getDefaultWritableCalendar()?.id ??\n          'blue';\n        createDragIndicator(drag, writableCalId, t('newEvent'), newEventLayout);\n        drag.sourceElement = null;\n        drag.indicatorVisible = true;\n        addDocumentDragListeners(handleDragMove, handleDragEnd);\n      }\n    },\n    [\n      isDateGridView,\n      onEventCreate,\n      onEventEdit,\n      currentWeekStart,\n      calculateNewEventLayout,\n      createDragIndicator,\n      handleDragMove,\n      handleDragEnd,\n      dragRef,\n      setDragState,\n    ]\n  );\n\n  // Touch cancel handler — cleans up drag state without applying a drop.\n  const handleDragCancel = useCallback(() => {\n    const drag = dragRef.current as typeof dragRef.current & InternalDragRef;\n    if (!drag || (!drag.active && !drag.pendingMove)) return;\n\n    clearGlobalDragCursor();\n\n    if (isDateGridView && options.isMobile) {\n      options.calendarRef?.current\n        ?.querySelectorAll<HTMLElement>('[data-drag-over]')\n        .forEach(el => {\n          delete el.dataset.dragOver;\n        });\n    }\n\n    removeDragIndicator();\n    removeDocumentDragListeners(\n      handleDragMove,\n      handleDragEnd,\n      handleDragCancel\n    );\n    resetDragState();\n  }, [\n    isDateGridView,\n    dragRef,\n    handleDragMove,\n    handleDragEnd,\n    removeDragIndicator,\n    resetDragState,\n  ]);\n\n  // Move event start - complete version\n  const handleMoveStart = useCallback(\n    (e: MouseEvent | TouchEvent, event: Event) => {\n      // Prevent scrolling on touch devices\n      if (\n        'cancelable' in e &&\n        e.cancelable &&\n        ('touches' in e || 'changedTouches' in e)\n      ) {\n        e.preventDefault();\n      }\n      e.stopPropagation();\n      if (dragRef.current?.active) return;\n\n      const { clientX, clientY } = getClientCoordinates(e);\n\n      const drag = dragRef.current as typeof dragRef.current & InternalDragRef;\n      if (!drag) return;\n      const sourceElement = resolveDragSourceElement(\n        e.currentTarget as HTMLElement | null\n      );\n      const sourceRect = sourceElement.getBoundingClientRect();\n\n      if (isDateGridView) {\n        // Month view move start\n        const eventStartDate = event.allDay\n          ? temporalToDate(event.start)\n          : getEventDateForEditing(event.start);\n        const eventEndDate = temporalToDate(event.end);\n\n        // Calculate event day span\n        let eventDurationDays = 1;\n        if (event.allDay && event.start && event.end) {\n          eventDurationDays = getAllDayEventDurationDays(\n            eventStartDate,\n            eventEndDate,\n            true\n          );\n        }\n\n        const grabDate = getTargetDateFromPosition(clientX, clientY);\n        const normalizedEventStart = new Date(eventStartDate);\n        normalizedEventStart.setHours(0, 0, 0, 0);\n        const grabDayOffset = grabDate\n          ? Math.max(0, daysDifference(normalizedEventStart, grabDate))\n          : 0;\n\n        const {\n          currentDragOffset,\n          dragState: monthDragState,\n          dragUpdates,\n        } = buildDateGridMoveStartData({\n          clientX,\n          clientY,\n          event,\n          eventDurationDays,\n          eventEndDate,\n          eventStartDate,\n          grabDayOffset,\n          isTouchLike: isTouchLikeEvent(e),\n          sourceElement,\n          sourceRect,\n        });\n\n        currentDragRef.current = currentDragOffset;\n        Object.assign(drag, dragUpdates);\n\n        if (drag.active) {\n          setDragState(monthDragState);\n        }\n\n        drag.sourceElement = sourceElement;\n        drag.indicatorVisible = false;\n\n        buildDateCellCache();\n        // Prevent the browser's compositor from intercepting touch for scroll\n        // during drag; cleared in clearGlobalDragCursor on drag end.\n        if (isTouchLikeEvent(e)) document.body.style.touchAction = 'none';\n\n        if (isTouchLikeEvent(e)) {\n          createDragIndicator(\n            drag,\n            drag.originalEvent?.calendarId,\n            drag.originalEvent?.title,\n            null,\n            sourceElement || undefined\n          );\n          drag.indicatorVisible = true;\n        }\n\n        addDocumentDragListeners(\n          handleDragMove,\n          handleDragEnd,\n          handleDragCancel\n        );\n      } else {\n        // Week/Day view move start\n        const mouseHour = pixelYToHour(clientY);\n        const editingHours = getTimedEventHoursForEditing(event);\n        const { dragState: weekDayDragState, dragUpdates } =\n          buildWeekDayMoveStartData({\n            allDayRowElement: allDayRowRef?.current ?? null,\n            clientX,\n            clientY,\n            editingHours,\n            event,\n            mouseHour,\n            sourceElement,\n            sourceRect,\n          });\n\n        Object.assign(drag, dragUpdates);\n        setDragState(weekDayDragState);\n\n        drag.sourceElement = sourceElement;\n        drag.indicatorVisible = false;\n\n        // Week/Day view uses cross-region drag support\n        if (isTouchLikeEvent(e)) document.body.style.touchAction = 'none';\n        addDocumentDragListeners(\n          handleUniversalDragMove,\n          handleUniversalDragEnd,\n          handleDragCancel\n        );\n      }\n    },\n    [\n      isDateGridView,\n      createDragIndicator,\n      handleDragCancel,\n      handleDragEnd,\n      handleDragMove,\n      handleUniversalDragMove,\n      handleUniversalDragEnd,\n      pixelYToHour,\n      dragRef,\n      currentDragRef,\n      setDragState,\n      allDayRowRef,\n      buildDateCellCache,\n    ]\n  );\n\n  // Resize start - complete version\n  const handleResizeStart = useCallback(\n    (e: MouseEvent | TouchEvent, event: Event, direction: string) => {\n      if (app?.state.readOnly) return;\n\n      // Prevent scrolling on touch devices\n      if ('cancelable' in e && e.cancelable) {\n        e.preventDefault();\n      }\n      e.stopPropagation();\n      if (dragRef.current?.active) return;\n\n      const { clientX, clientY } = getClientCoordinates(e);\n\n      const drag = dragRef.current as typeof dragRef.current & InternalDragRef;\n      if (!drag) return;\n\n      if (isDateGridView) {\n        // Month view resize start\n        const originalDate = temporalToDate(event.start);\n        const initialStartDate = temporalToDate(event.start);\n        const initialEndDate = temporalToDate(event.end);\n        const originalStartTime = {\n          hour: initialStartDate.getHours(),\n          minute: initialStartDate.getMinutes(),\n          second: initialStartDate.getSeconds(),\n        };\n        const originalEndTime = {\n          hour: initialEndDate.getHours(),\n          minute: initialEndDate.getMinutes(),\n          second: initialEndDate.getSeconds(),\n        };\n\n        drag.active = true;\n        drag.mode = 'resize';\n        drag.eventId = event.id;\n        drag.startX = clientX;\n        drag.startY = clientY;\n        drag.targetDate =\n          direction === 'left' ? initialStartDate : initialEndDate;\n        drag.originalDate = originalDate;\n        drag.originalEvent = { ...event };\n        drag.lastUpdateTime = Date.now();\n        drag.resizeDirection = direction as 'left' | 'right';\n        drag.originalStartDate = initialStartDate;\n        drag.originalEndDate = initialEndDate;\n        drag.originalStartTime = originalStartTime;\n        drag.originalEndTime = originalEndTime;\n\n        buildDateCellCache();\n        setDragState({\n          active: true,\n          mode: 'resize',\n          eventId: event.id,\n          targetDate: direction === 'left' ? initialStartDate : initialEndDate,\n          startDate: initialStartDate,\n          endDate: initialEndDate,\n        });\n      } else if (event.allDay) {\n        const { dragState: weekDayDragState, dragUpdates } =\n          buildWeekDayResizeStartData({\n            clientX,\n            clientY,\n            direction,\n            editingHours: getTimedEventHoursForEditing(event),\n            event,\n            mouseHour: 0,\n          });\n\n        Object.assign(drag, dragUpdates);\n        setDragState(weekDayDragState);\n      } else {\n        // Regular event resize (vertical by hour)\n        const mouseHour = pixelYToHour(clientY);\n        const editingHours = getTimedEventHoursForEditing(event);\n        const { dragState: weekDayDragState, dragUpdates } =\n          buildWeekDayResizeStartData({\n            clientX,\n            clientY,\n            direction,\n            editingHours,\n            event,\n            mouseHour,\n          });\n\n        Object.assign(drag, dragUpdates);\n        setDragState(weekDayDragState);\n      }\n\n      if (isTouchLikeEvent(e)) document.body.style.touchAction = 'none';\n      addDocumentDragListeners(handleDragMove, handleDragEnd, handleDragCancel);\n    },\n    [\n      isDateGridView,\n      handleDragMove,\n      handleDragEnd,\n      handleDragCancel,\n      pixelYToHour,\n      dragRef,\n      setDragState,\n      buildDateCellCache,\n    ]\n  );\n\n  return {\n    handleDragMove,\n    handleDragEnd,\n    handleCreateStart,\n    handleMoveStart,\n    handleResizeStart,\n    handleUniversalDragMove,\n    handleUniversalDragEnd,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/useDragManager.ts",
    "content": "import {\n  EventLayout,\n  Event,\n  UnifiedDragRef,\n  useDragProps,\n  ViewType,\n  UseDragManagerReturn,\n  getSelectedBgColor,\n  getEventTextColor,\n  getCalendarEventBgColors,\n  buildDiagonalPatternBackground,\n  formatTime,\n  useLocale,\n  LocaleProvider,\n  dateToZonedDateTime,\n} from '@dayflow/core';\nimport DragIndicatorComponent from '@drag/components/DragIndicatorComponent';\nimport MonthDragIndicatorComponent from '@drag/components/MonthDragIndicator';\nimport { isUsableIndicatorBackground } from '@drag/hooks/utils/indicatorColor';\nimport { h, render } from 'preact';\nimport { useRef, useCallback } from 'preact/hooks';\n\nexport const useDragManager = (options: useDragProps): UseDragManagerReturn => {\n  const { t, locale } = useLocale();\n  const {\n    calendarRef,\n    allDayRowRef,\n    timeGridRef,\n    viewType,\n    getLineColor,\n    getDynamicPadding,\n    renderer,\n    HOUR_HEIGHT = 72,\n    FIRST_HOUR = 0,\n    TIME_COLUMN_WIDTH = 80,\n    ALL_DAY_HEIGHT = 60,\n    app,\n    isMobile,\n  } = options;\n\n  const isDateGridView =\n    viewType === ViewType.MONTH || viewType === ViewType.YEAR;\n  const isDayView = viewType === ViewType.DAY;\n\n  // Measure offset from .df-calendar-content top to the first time grid row,\n  // accounting for boundary elements (e.g. top boundary) above the grid\n  const getGridOffset = useCallback(() => {\n    const containerEl = calendarRef.current?.querySelector(\n      '.df-calendar-content'\n    );\n    if (!containerEl) return 0;\n    const firstGridRow = containerEl.querySelector('.df-time-grid-row');\n    if (!firstGridRow) return 0;\n    return (\n      firstGridRow.getBoundingClientRect().top -\n      containerEl.getBoundingClientRect().top +\n      containerEl.scrollTop\n    );\n  }, [calendarRef]);\n\n  const dragIndicatorRef = useRef<HTMLDivElement | null>(null);\n  const dragPropsRef = useRef<{\n    drag: UnifiedDragRef;\n    color?: string;\n    title?: string;\n    layout?: EventLayout | null;\n  } | null>(null);\n\n  // Remove drag indicator\n  const removeDragIndicator = useCallback(() => {\n    if (dragIndicatorRef.current) {\n      render(null, dragIndicatorRef.current);\n      dragIndicatorRef.current.remove();\n      dragIndicatorRef.current = null;\n    }\n    dragPropsRef.current = null;\n  }, []);\n\n  // Create drag indicator\n  const createDragIndicator = useCallback(\n    (\n      drag: UnifiedDragRef,\n      color?: string,\n      title?: string,\n      layout?: EventLayout | null,\n      sourceElement?: HTMLElement\n    ) => {\n      removeDragIndicator();\n\n      // Tracks whether the indicator has a light background (used for text color in renderer)\n      let isLightIndicator = false;\n\n      const indicator = document.createElement('div');\n      indicator.style.position = isDateGridView ? 'fixed' : 'absolute';\n      indicator.style.pointerEvents = 'none';\n      indicator.style.zIndex = '1000';\n\n      if (isDateGridView) {\n        // indicator logic\n        indicator.style.opacity = '0.9';\n\n        let indicatorWidth: number;\n        let indicatorHeight: number;\n\n        if (sourceElement) {\n          const sourceRect = sourceElement.getBoundingClientRect();\n          // Use single day cell width for indicator (Apple Calendar style)\n          const dayCellEl = calendarRef.current?.querySelector('[data-date]');\n          indicatorWidth = dayCellEl\n            ? Math.round(dayCellEl.getBoundingClientRect().width)\n            : Math.min(sourceRect.width, 120);\n          indicatorHeight = sourceRect.height;\n          indicator.className = `df-drag-indicator-month-pill ${sourceElement.className}`;\n        } else {\n          indicatorWidth = 120;\n          indicatorHeight = 22;\n          indicator.className = 'df-drag-indicator-manual-pill';\n        }\n\n        indicator.style.width = `${indicatorWidth}px`;\n        indicator.style.height = `${indicatorHeight}px`;\n        indicator.style.left = '0px';\n        indicator.style.top = '0px';\n        indicator.style.willChange = 'transform';\n\n        indicator.style.transform = `translate3d(${drag.startX - indicatorWidth / 2}px, ${drag.startY - indicatorHeight / 2}px, 0)`;\n\n        document.body.append(indicator);\n\n        // Save props for subsequent updates\n        dragPropsRef.current = { drag, color, title, layout };\n\n        // Render month view content\n        const now = new Date();\n        const nowTemporal = dateToZonedDateTime(now);\n        const eventForComponent =\n          drag.originalEvent ||\n          ({\n            id: String(Date.now()),\n            color: color || 'blue',\n            title: title || t('newEvent'),\n            start: nowTemporal,\n            end: nowTemporal,\n            allDay: false,\n            day: 0,\n          } as Event);\n\n        render(\n          h(\n            LocaleProvider,\n            { locale },\n            h(MonthDragIndicatorComponent, {\n              event: eventForComponent,\n              isCreating: drag.mode === 'create',\n              targetDate: drag.targetDate || null,\n              startDate: drag.originalStartDate || null,\n              endDate: drag.originalEndDate || null,\n              isMobile,\n            })\n          ),\n          indicator\n        );\n      } else {\n        // Week/Day view indicator\n        const isMultiCalendarIndicator =\n          !!drag.calendarIds && drag.calendarIds.length > 1;\n        const targetContainer = drag.allDay\n          ? drag.indicatorContainer || allDayRowRef?.current\n          : timeGridRef?.current ||\n            calendarRef.current?.querySelector('.df-calendar-content');\n        const isInsideTimeGrid =\n          !drag.allDay && targetContainer === timeGridRef?.current;\n\n        if (sourceElement) {\n          const sourceRect = sourceElement.getBoundingClientRect();\n          let containerRect;\n\n          if (drag.allDay) {\n            containerRect = allDayRowRef?.current?.getBoundingClientRect();\n          } else {\n            containerRect = calendarRef.current\n              ?.querySelector('.df-calendar-content')\n              ?.getBoundingClientRect();\n          }\n\n          if (containerRect) {\n            if (drag.allDay && isDayView) {\n              indicator.style.left = `${sourceRect.left - containerRect.left}px`;\n              indicator.style.top = `${sourceRect.top - containerRect.top}px`;\n              indicator.style.width = `${sourceRect.width}px`;\n              indicator.style.height = `${sourceRect.height}px`;\n            } else if (drag.allDay && !isDayView) {\n              indicator.style.left = `${sourceRect.left - containerRect.left}px`;\n              indicator.style.top = `${sourceRect.top - containerRect.top}px`;\n              indicator.style.width = `${sourceRect.width}px`;\n              indicator.style.height = `${sourceRect.height}px`;\n            } else {\n              const top = (drag.startHour - FIRST_HOUR) * HOUR_HEIGHT;\n              const containerEl = calendarRef.current?.querySelector(\n                '.df-calendar-content'\n              );\n              const scrollLeft = containerEl?.scrollLeft || 0;\n              const gridOffset = isInsideTimeGrid ? 0 : getGridOffset();\n\n              indicator.style.left = isInsideTimeGrid\n                ? `${sourceRect.left - (timeGridRef?.current?.getBoundingClientRect().left || 0)}px`\n                : `${sourceRect.left - containerRect.left + scrollLeft}px`;\n              indicator.style.top = isInsideTimeGrid\n                ? `${top + 3}px`\n                : `${top + 3 + gridOffset}px`;\n              indicator.style.width = `${sourceRect.width}px`;\n              indicator.style.height = `${sourceRect.height}px`;\n            }\n\n            indicator.className = sourceElement.className;\n            indicator.style.margin = '0';\n            indicator.style.opacity = '0.8';\n            indicator.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';\n            indicator.style.borderRadius = drag.allDay ? '0.75rem' : '0.25rem';\n          }\n        } else if (drag.allDay) {\n          // Calculate position logic\n          indicator.style.top = '2px';\n          indicator.style.height = `${ALL_DAY_HEIGHT - 4}px`;\n          indicator.style.marginBottom = '3px';\n          indicator.className = 'df-drag-indicator-all-day-pill';\n          indicator.style.borderRadius = '0.75rem';\n          indicator.style.boxShadow = '0 1px 2px rgb(0 0 0 / 0.12)';\n\n          if (isDayView) {\n            indicator.style.left = `${TIME_COLUMN_WIDTH}px`;\n            const gutterOffset = isMobile ? 0 : 11;\n            indicator.style.width = `calc(100% - ${TIME_COLUMN_WIDTH}px - ${2 + gutterOffset}px)`;\n          } else {\n            const totalWidth =\n              options.gridWidth || (isMobile ? '175%' : '100%');\n            const daysToShow = options.displayDays || 7;\n            const dayColumnWidth = `calc(${totalWidth} / ${daysToShow})`;\n            indicator.style.left = `calc(${dayColumnWidth} * ${drag.dayIndex})`;\n            indicator.style.width = `calc(${dayColumnWidth} - 2px)`;\n          }\n        } else {\n          const gridOffset = isInsideTimeGrid ? 0 : getGridOffset();\n          const top = (drag.startHour - FIRST_HOUR) * HOUR_HEIGHT;\n          const height = (drag.endHour - drag.startHour) * HOUR_HEIGHT;\n          indicator.style.top = isInsideTimeGrid\n            ? `${top + 3}px`\n            : `${top + 3 + gridOffset}px`;\n          indicator.style.height = `${height - 4}px`;\n          indicator.style.color = '#fff';\n          indicator.className = 'df-drag-indicator-regular-pill';\n          indicator.style.borderRadius = '0.25rem';\n          indicator.style.boxShadow = '0 1px 2px rgb(0 0 0 / 0.12)';\n\n          const daysToShow = options.displayDays || 7;\n          const totalWidth = isInsideTimeGrid\n            ? '100%'\n            : options.gridWidth || (isMobile && !isDayView ? '175%' : '100%');\n\n          if (layout) {\n            if (isDayView) {\n              indicator.style.left = isInsideTimeGrid\n                ? `${(layout.left / 100) * 100}%`\n                : `${TIME_COLUMN_WIDTH}px`;\n              indicator.style.width = isInsideTimeGrid\n                ? `${layout.width}%`\n                : `calc(((100% - ${TIME_COLUMN_WIDTH}px) * ${layout.width / 100}) - 3px)`;\n            } else {\n              const dayWidth = `calc(${totalWidth} / ${daysToShow})`;\n              indicator.style.left = `calc((${dayWidth} * ${drag.dayIndex}) + (${dayWidth} * ${layout.left / 100}))`;\n              indicator.style.width = `calc((${dayWidth} * ${(layout.width - 1) / 100}))`;\n            }\n            indicator.style.zIndex = String(1000);\n          } else if (isDayView) {\n            indicator.style.left = isInsideTimeGrid\n              ? '0px'\n              : `${TIME_COLUMN_WIDTH}px`;\n            indicator.style.width = isInsideTimeGrid\n              ? '100%'\n              : `calc(100% - ${TIME_COLUMN_WIDTH}px - 3px)`;\n          } else {\n            const dayColumnWidth = `calc(${totalWidth} / ${daysToShow})`;\n            indicator.style.left = `calc(${dayColumnWidth} * ${drag.dayIndex})`;\n            indicator.style.width = `calc(${dayColumnWidth} - 3px)`;\n          }\n        }\n\n        // Add to corresponding container\n        targetContainer?.append(indicator);\n\n        // Save props for subsequent updates\n        dragPropsRef.current = { drag, color, title, layout };\n\n        // Determine if this will be a light-background indicator (multi-calendar)\n        isLightIndicator = isMultiCalendarIndicator;\n\n        // Render Week/Day view content\n        render(\n          h(\n            LocaleProvider,\n            { locale },\n            h(DragIndicatorComponent, {\n              drag,\n              color,\n              title,\n              layout,\n              allDay: drag.allDay,\n              formatTime: formatTime,\n              getLineColor: getLineColor || (() => ''),\n              getDynamicPadding: getDynamicPadding || (() => 'df-p-standard'),\n              renderer,\n              isMobile,\n              isLightBackground: isLightIndicator,\n            })\n          ),\n          indicator\n        );\n      }\n\n      // Set color\n      if (isDateGridView && sourceElement) {\n        // Month/Year: prefer the rendered event shell color so the drag pill\n        // exactly matches the event, even when custom content handles the drag\n        // start or the color comes from computed styles instead of inline styles.\n        const computedStyle = window.getComputedStyle(sourceElement);\n\n        // Multi-calendar events use CSS `background` shorthand (gradient pattern).\n        // Check inline `background` first so we capture the diagonal stripe pattern.\n        const inlineBackground = sourceElement.style.background;\n        if (inlineBackground && inlineBackground !== '') {\n          indicator.style.background = inlineBackground;\n          // Source element text color is already set to the dark event text color.\n          const resolvedTextColor = sourceElement.style.color\n            ? sourceElement.style.color\n            : computedStyle.color || 'inherit';\n          indicator.style.color = resolvedTextColor;\n        } else {\n          const resolvedBackgroundColor = isUsableIndicatorBackground(\n            sourceElement.style.backgroundColor\n          )\n            ? sourceElement.style.backgroundColor\n            : isUsableIndicatorBackground(computedStyle.backgroundColor)\n              ? computedStyle.backgroundColor\n              : color\n                ? getSelectedBgColor(color, app?.getCalendarRegistry())\n                : '';\n          const resolvedTextColor = sourceElement.style.color\n            ? sourceElement.style.color\n            : computedStyle.color || '#fff';\n\n          if (resolvedBackgroundColor) {\n            indicator.style.backgroundColor = resolvedBackgroundColor;\n            indicator.style.color = resolvedTextColor;\n          } else {\n            indicator.className +=\n              ' df-tint-primary df-drag-indicator-ghost df-border-primary-soft';\n          }\n        }\n      } else if (drag.calendarIds && drag.calendarIds.length > 1) {\n        // Day/Week view — multi-calendar event: light diagonal pattern background\n        const indicatorBgColors = getCalendarEventBgColors(\n          { calendarIds: drag.calendarIds },\n          app?.getCalendarRegistry()\n        );\n        indicator.style.background =\n          buildDiagonalPatternBackground(indicatorBgColors);\n        indicator.style.color = getEventTextColor(\n          drag.calendarIds[0],\n          app?.getCalendarRegistry()\n        );\n        isLightIndicator = true;\n      } else if (color) {\n        indicator.style.backgroundColor = getSelectedBgColor(\n          color,\n          app?.getCalendarRegistry()\n        );\n        indicator.style.color = '#fff';\n      } else if (drag.calendarIds && drag.calendarIds.length > 0) {\n        indicator.style.backgroundColor = getSelectedBgColor(\n          drag.calendarIds[0],\n          app?.getCalendarRegistry()\n        );\n        indicator.style.color = '#fff';\n      } else {\n        indicator.className +=\n          ' df-tint-primary df-drag-indicator-ghost df-border-primary-soft';\n      }\n\n      dragIndicatorRef.current = indicator;\n    },\n    [\n      removeDragIndicator,\n      isDateGridView,\n      isDayView,\n      allDayRowRef,\n      calendarRef,\n      formatTime,\n      getLineColor,\n      getDynamicPadding,\n      renderer,\n      TIME_COLUMN_WIDTH,\n      ALL_DAY_HEIGHT,\n      FIRST_HOUR,\n      HOUR_HEIGHT,\n      getGridOffset,\n    ]\n  );\n\n  // Update drag indicator\n  const updateDragIndicator = useCallback(\n    (...args: (number | boolean | EventLayout | null | undefined)[]) => {\n      const indicator = dragIndicatorRef.current;\n      if (!indicator) return;\n\n      if (isDateGridView) {\n        const [clientX, clientY] = args as [number, number];\n        const width = Number.parseFloat(indicator.style.width) || 120;\n        const height = Number.parseFloat(indicator.style.height) || 22;\n\n        indicator.style.transform = `translate3d(${clientX - width / 2}px, ${clientY - height / 2}px, 0)`;\n        indicator.style.transition = 'none';\n      } else {\n        const [dayIndex, startHour, endHour, isAllDay = false, layout] =\n          args as [number, number, number, boolean?, EventLayout?];\n        const currentDrag = dragPropsRef.current?.drag;\n\n        // Ensure in correct container\n        const targetContainer = isAllDay\n          ? currentDrag?.indicatorContainer || allDayRowRef?.current\n          : timeGridRef?.current ||\n            calendarRef.current?.querySelector('.df-calendar-content');\n\n        if (indicator.parentElement !== targetContainer) {\n          targetContainer?.append(indicator);\n        }\n\n        const isInsideTimeGrid =\n          !isAllDay && targetContainer === timeGridRef?.current;\n\n        if (isAllDay) {\n          const hasInitialAllDayGeometry =\n            currentDrag?.initialIndicatorLeft !== undefined &&\n            currentDrag?.initialIndicatorTop !== undefined &&\n            currentDrag?.initialIndicatorWidth !== undefined &&\n            currentDrag?.initialIndicatorHeight !== undefined;\n\n          if (hasInitialAllDayGeometry) {\n            indicator.style.top = `${currentDrag!.initialIndicatorTop}px`;\n            indicator.style.width = `${currentDrag!.initialIndicatorWidth}px`;\n            indicator.style.height = `${currentDrag!.initialIndicatorHeight}px`;\n\n            if (isDayView) {\n              indicator.style.left = `${currentDrag!.initialIndicatorLeft}px`;\n            } else {\n              const containerWidth =\n                targetContainer?.getBoundingClientRect().width || 0;\n              const daysToShow = options.displayDays || 7;\n              const dayColumnWidth = containerWidth / daysToShow;\n              const startDayIndex = currentDrag!.startDragDayIndex ?? dayIndex;\n              const dayDelta = dayIndex - startDayIndex;\n              indicator.style.left = `${currentDrag!.initialIndicatorLeft! + dayDelta * dayColumnWidth}px`;\n            }\n          } else if (isDayView) {\n            indicator.style.top = '2px';\n            indicator.style.left = `${TIME_COLUMN_WIDTH}px`;\n            const gutterOffset = isMobile ? 0 : 11;\n            indicator.style.width = `calc(100% - ${TIME_COLUMN_WIDTH}px - ${2 + gutterOffset}px)`;\n            indicator.style.height = `${ALL_DAY_HEIGHT - 4}px`;\n          } else {\n            indicator.style.top = '2px';\n            const totalWidth =\n              options.gridWidth || (isMobile && !isDayView ? '175%' : '100%');\n            const daysToShow = options.displayDays || 7;\n            const dayColumnWidth = `calc(${totalWidth} / ${daysToShow})`;\n            indicator.style.left = `calc(${dayColumnWidth} * ${dayIndex})`;\n            indicator.style.width = `calc(${dayColumnWidth} - 2px)`;\n            indicator.style.height = `${ALL_DAY_HEIGHT - 4}px`;\n          }\n          indicator.style.marginBottom = '3px';\n          indicator.className = indicator.className.replace(\n            'df-drag-indicator-regular-pill',\n            'df-drag-indicator-all-day-pill'\n          );\n          indicator.style.borderRadius = '0.75rem';\n        } else {\n          const gridOffset = isInsideTimeGrid ? 0 : getGridOffset();\n          const top = (startHour - FIRST_HOUR) * HOUR_HEIGHT;\n          const height = (endHour - startHour) * HOUR_HEIGHT;\n          indicator.style.top = isInsideTimeGrid\n            ? `${top + 3}px`\n            : `${top + 3 + gridOffset}px`;\n          indicator.style.height = `${height - 4}px`;\n          indicator.style.marginBottom = '0';\n          indicator.className = indicator.className.replace(\n            'df-drag-indicator-all-day-pill',\n            'df-drag-indicator-regular-pill'\n          );\n          indicator.style.borderRadius = '0.25rem';\n\n          const daysToShow = options.displayDays || 7;\n          const totalWidth = isInsideTimeGrid\n            ? '100%'\n            : options.gridWidth || (isMobile && !isDayView ? '175%' : '100%');\n\n          if (layout) {\n            if (isDayView) {\n              indicator.style.left = isInsideTimeGrid\n                ? `${(layout.left / 100) * 100}%`\n                : `calc(${TIME_COLUMN_WIDTH}px + ((100% - ${TIME_COLUMN_WIDTH}px) * ${layout.left / 100}))`;\n              indicator.style.width = isInsideTimeGrid\n                ? `${layout.width}%`\n                : `calc(((100% - ${TIME_COLUMN_WIDTH}px) * ${layout.width / 100}) - 3px)`;\n            } else {\n              const dayWidth = `calc(${totalWidth} / ${daysToShow})`;\n              indicator.style.left = `calc((${dayWidth} * ${dayIndex}) + (${dayWidth} * ${layout.left / 100}))`;\n              indicator.style.width = `calc((${dayWidth} * ${(layout.width - 1) / 100}))`;\n            }\n            indicator.style.zIndex = String(layout.zIndex + 10);\n          } else if (isDayView) {\n            indicator.style.left = isInsideTimeGrid\n              ? '0px'\n              : `${TIME_COLUMN_WIDTH}px`;\n            indicator.style.width = isInsideTimeGrid\n              ? '100%'\n              : `calc(100% - ${TIME_COLUMN_WIDTH}px - 3px)`;\n          } else {\n            const dayColumnWidth = `calc(${totalWidth} / ${daysToShow})`;\n            indicator.style.left = `calc(${dayColumnWidth} * ${dayIndex})`;\n            indicator.style.width = `calc(${dayColumnWidth} - 3px)`;\n          }\n        }\n\n        indicator.style.cursor = 'grabbing';\n\n        // Re-render React component to update drag data\n        if (dragPropsRef.current) {\n          const updatedDrag = {\n            ...dragPropsRef.current.drag,\n            dayIndex,\n            startHour,\n            endHour,\n            allDay: isAllDay,\n          };\n\n          dragPropsRef.current.drag = updatedDrag;\n\n          const currentColor = dragPropsRef.current.color;\n          const rerenderDrag = dragPropsRef.current.drag;\n          const currentIsLight =\n            !!rerenderDrag.calendarIds && rerenderDrag.calendarIds.length > 1;\n\n          render(\n            h(\n              LocaleProvider,\n              { locale },\n              h(DragIndicatorComponent, {\n                drag: updatedDrag,\n                color: currentColor,\n                title: dragPropsRef.current.title,\n                layout: layout || dragPropsRef.current.layout,\n                allDay: isAllDay,\n                formatTime: formatTime,\n                getLineColor: getLineColor || (() => ''),\n                getDynamicPadding: getDynamicPadding || (() => 'df-p-standard'),\n                renderer,\n                isMobile,\n                isLightBackground: currentIsLight,\n              })\n            ),\n            indicator\n          );\n        }\n      }\n    },\n    [\n      isDateGridView,\n      allDayRowRef,\n      formatTime,\n      calendarRef,\n      isDayView,\n      ALL_DAY_HEIGHT,\n      TIME_COLUMN_WIDTH,\n      FIRST_HOUR,\n      HOUR_HEIGHT,\n      getLineColor,\n      getDynamicPadding,\n      renderer,\n      getGridOffset,\n    ]\n  );\n\n  return {\n    dragIndicatorRef,\n    removeDragIndicator,\n    createDragIndicator,\n    updateDragIndicator,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/useDragState.ts",
    "content": "// oxlint-disable typescript/no-explicit-any\nimport {\n  MonthDragState,\n  UnifiedDragRef,\n  useDragProps,\n  ViewType,\n  WeekDayDragState,\n  UseDragStateReturn,\n  Event as CalendarEvent,\n} from '@dayflow/core';\nimport { throttle } from '@drag/utils/throttle';\nimport { useRef, useCallback, useState, useMemo } from 'preact/hooks';\n\ntype InternalDragRef = UnifiedDragRef & { pendingMove?: boolean };\n\nexport const useDragState = (options: useDragProps): UseDragStateReturn => {\n  const { viewType, onEventsUpdate } = options;\n\n  const isDateGridView =\n    viewType === ViewType.MONTH || viewType === ViewType.YEAR;\n\n  // Drag reference\n  const dragRef = useRef<UnifiedDragRef>({\n    active: false,\n    mode: null,\n    eventId: null,\n    startX: 0,\n    startY: 0,\n    dayIndex: 0,\n    startHour: 0,\n    endHour: 0,\n    originalDay: 0,\n    originalStartHour: 0,\n    originalEndHour: 0,\n    resizeDirection: null,\n    hourOffset: null,\n    duration: 0,\n    lastRawMouseHour: null,\n    lastUpdateTime: 0,\n    initialMouseY: 0,\n    lastClientY: 0,\n    allDay: false,\n    // Month view specific\n    targetDate: null,\n    originalDate: null,\n    originalEvent: null,\n    dragOffset: 0,\n    dragOffsetY: 0,\n    originalStartDate: null,\n    originalEndDate: null,\n    eventDate: undefined,\n    originalStartTime: null,\n    originalEndTime: null,\n    sourceElement: null,\n    indicatorVisible: false,\n  } as InternalDragRef);\n\n  const currentDragRef = useRef({ x: 0, y: 0 });\n\n  // Initial drag state\n  const initialDragState = useMemo(\n    () =>\n      isDateGridView\n        ? ({\n            active: false,\n            mode: null,\n            eventId: null,\n            targetDate: null,\n            startDate: null,\n            endDate: null,\n          } as MonthDragState)\n        : ({\n            active: false,\n            mode: null,\n            eventId: null,\n            dayIndex: 0,\n            startHour: 0,\n            endHour: 0,\n            allDay: false,\n          } as WeekDayDragState),\n    [isDateGridView]\n  );\n\n  const [dragState, setDragState] = useState<MonthDragState | WeekDayDragState>(\n    initialDragState\n  );\n\n  const throttledSetEventsByMode = useMemo(() => {\n    const createThrottledUpdater = (wait: number) =>\n      throttle(\n        ((\n          updateFunc: (events: CalendarEvent[]) => CalendarEvent[],\n          interactionType?: string\n        ) => onEventsUpdate(updateFunc, interactionType === 'resize')) as any,\n        wait\n      );\n\n    if (isDateGridView) {\n      const shared = createThrottledUpdater(16);\n      return {\n        default: shared,\n        move: shared,\n        resize: shared,\n        create: shared,\n      };\n    }\n\n    return {\n      default: createThrottledUpdater(16),\n      move: createThrottledUpdater(16),\n      create: createThrottledUpdater(16),\n      resize: createThrottledUpdater(16),\n    };\n  }, [isDateGridView, onEventsUpdate]);\n\n  const throttledSetEvents = useCallback(\n    (\n      updateFunc: (events: CalendarEvent[]) => CalendarEvent[],\n      interactionType?: string\n    ) => {\n      const throttledUpdater =\n        throttledSetEventsByMode[\n          interactionType as keyof typeof throttledSetEventsByMode\n        ] ?? throttledSetEventsByMode.default;\n      throttledUpdater(updateFunc, interactionType);\n    },\n    [throttledSetEventsByMode]\n  );\n\n  // Reset state\n  const resetDragState = useCallback(() => {\n    throttledSetEventsByMode.default.cancel();\n    throttledSetEventsByMode.move.cancel();\n    throttledSetEventsByMode.resize.cancel();\n    throttledSetEventsByMode.create.cancel();\n\n    if (isDateGridView) {\n      setDragState({\n        active: false,\n        mode: null,\n        eventId: null,\n        targetDate: null,\n        startDate: null,\n        endDate: null,\n      });\n    } else {\n      setDragState({\n        active: false,\n        mode: null,\n        eventId: null,\n        dayIndex: 0,\n        startHour: 0,\n        endHour: 0,\n        allDay: false,\n      });\n    }\n\n    dragRef.current = {\n      active: false,\n      mode: null,\n      eventId: null,\n      startX: 0,\n      startY: 0,\n      dayIndex: 0,\n      startHour: 0,\n      endHour: 0,\n      originalDay: 0,\n      originalStartHour: 0,\n      originalEndHour: 0,\n      duration: 0,\n      resizeDirection: null,\n      hourOffset: null,\n      lastRawMouseHour: null,\n      lastUpdateTime: 0,\n      initialMouseY: 0,\n      lastClientY: 0,\n      allDay: false,\n      targetDate: null,\n      originalDate: null,\n      originalEvent: null,\n      dragOffset: 0,\n      dragOffsetY: 0,\n      originalStartDate: null,\n      originalEndDate: null,\n      eventDate: undefined,\n      originalStartTime: null,\n      originalEndTime: null,\n      sourceElement: null,\n      indicatorVisible: false,\n      initialIndicatorLeft: undefined,\n      initialIndicatorTop: undefined,\n      initialIndicatorWidth: undefined,\n      initialIndicatorHeight: undefined,\n      indicatorContainer: null,\n    } as InternalDragRef;\n  }, [isDateGridView, throttledSetEventsByMode]);\n\n  return {\n    dragRef,\n    currentDragRef,\n    dragState,\n    setDragState,\n    resetDragState,\n    throttledSetEvents,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/useMonthDrag.ts",
    "content": "import { UseMonthDragParams, UseMonthDragReturn } from '@dayflow/core';\n\nexport const useMonthDrag = (\n  params: UseMonthDragParams\n): UseMonthDragReturn => {\n  const { common } = params;\n  const { daysDifference, addDaysToDate, getTargetDateFromPosition } = common;\n\n  return {\n    daysDifference,\n    addDaysToDate,\n    getTargetDateFromPosition,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/useWeekDayDrag.ts",
    "content": "import {\n  ViewType,\n  UseWeekDayDragParams,\n  UseWeekDayDragReturn,\n  Event,\n  getDateByDayIndex,\n  useLocale,\n  dateToPlainDate,\n} from '@dayflow/core';\n// Week/Day view specific implementation\nimport { useCallback } from 'preact/hooks';\n\nexport const useWeekDayDrag = (\n  params: UseWeekDayDragParams\n): UseWeekDayDragReturn => {\n  const { t } = useLocale();\n  const { options, common, state, handleDragMove, handleDragEnd } = params;\n  const { viewType, currentWeekStart, app } = options;\n  const { dragRef, setDragState } = state;\n  const { pixelYToHour, getColumnDayIndex } = common;\n\n  const isMonthView = viewType === ViewType.MONTH;\n\n  // Create all-day event\n  const handleCreateAllDayEvent = useCallback(\n    (e: MouseEvent | TouchEvent, dayIndex: number) => {\n      if (app?.state.readOnly) return;\n      if (isMonthView) return;\n\n      if ('cancelable' in e && e.cancelable) {\n        e.preventDefault();\n      }\n      e.stopPropagation();\n\n      if (e.type === 'dblclick') {\n        const eventDate = currentWeekStart\n          ? getDateByDayIndex(currentWeekStart, dayIndex)\n          : new Date();\n        const startTemporal = dateToPlainDate(eventDate);\n        const endTemporal = dateToPlainDate(eventDate);\n\n        const newEvent: Event = {\n          id: String(Date.now()),\n          title: t('newAllDayEvent'),\n          day: dayIndex,\n          start: startTemporal,\n          end: endTemporal,\n          calendarId:\n            app?.getCalendarRegistry()?.getDefaultCalendar()?.id ?? 'blue',\n          allDay: true,\n        };\n\n        options.onEventCreate(newEvent);\n        return;\n      }\n\n      if (dragRef.current?.active) return;\n\n      const drag = dragRef.current;\n      if (!drag) return;\n\n      let clientX, clientY;\n      if ('touches' in e && e.touches.length > 0) {\n        clientX = e.touches[0].clientX;\n        clientY = e.touches[0].clientY;\n      } else {\n        clientX = (e as MouseEvent).clientX;\n        clientY = (e as MouseEvent).clientY;\n      }\n\n      Object.assign(drag, {\n        active: true,\n        mode: 'create',\n        eventId: null,\n        startX: clientX,\n        startY: clientY,\n        dayIndex,\n        allDay: true,\n        eventDate: currentWeekStart\n          ? getDateByDayIndex(currentWeekStart, dayIndex)\n          : new Date(),\n      });\n\n      setDragState({\n        active: true,\n        mode: 'create',\n        eventId: null,\n        dayIndex,\n        startHour: 0,\n        endHour: 0,\n        allDay: true,\n      });\n\n      // Do not create indicator immediately for drag (mousedown), wait for move\n      drag.indicatorVisible = false;\n\n      drag.sourceElement = null; // Clear source element\n\n      document.addEventListener('mousemove', handleDragMove);\n      document.addEventListener('mouseup', handleDragEnd);\n    },\n    [\n      isMonthView,\n      currentWeekStart,\n      handleDragEnd,\n      handleDragMove,\n      dragRef,\n      setDragState,\n      app?.state.locale,\n      options,\n    ]\n  );\n\n  return {\n    handleCreateAllDayEvent,\n    pixelYToHour,\n    getColumnDayIndex,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/__tests__/dateGridDrag.test.ts",
    "content": "import { dateToZonedDateTime, temporalToDate } from '@dayflow/core';\nimport {\n  buildDateGridCreateEvent,\n  buildDateGridMoveStartData,\n  shouldActivateDateGridMove,\n} from '@drag/hooks/utils/dateGridDrag';\n\ndescribe('dateGridDrag', () => {\n  it('builds a default 9-10am month/year create event', () => {\n    const event = buildDateGridCreateEvent({\n      appTimeZone: 'UTC',\n      calendarId: 'work',\n      targetDate: new Date('2026-04-09T00:00:00Z'),\n      title: 'newEvent',\n    });\n\n    expect(event.calendarId).toBe('work');\n    expect(event.title).toBe('newEvent');\n    expect(event.allDay).toBe(false);\n    expect(temporalToDate(event.start).toISOString()).toBe(\n      '2026-04-09T09:00:00.000Z'\n    );\n    expect(temporalToDate(event.end).toISOString()).toBe(\n      '2026-04-09T10:00:00.000Z'\n    );\n  });\n\n  it('builds month/year move drag state with deferred mouse activation', () => {\n    const sourceElement = document.createElement('div');\n    sourceElement.dataset.segmentDays = '3';\n\n    const { currentDragOffset, dragState, dragUpdates } =\n      buildDateGridMoveStartData({\n        clientX: 140,\n        clientY: 60,\n        event: {\n          id: 'event-1',\n          title: 'Trip',\n          start: dateToZonedDateTime(new Date('2026-04-09T09:00:00Z'), 'UTC'),\n          end: dateToZonedDateTime(new Date('2026-04-11T10:00:00Z'), 'UTC'),\n          allDay: false,\n          calendarId: 'work',\n        },\n        eventDurationDays: 3,\n        eventEndDate: new Date('2026-04-11T10:00:00Z'),\n        eventStartDate: new Date('2026-04-09T09:00:00Z'),\n        isTouchLike: false,\n        sourceElement,\n        sourceRect: {\n          left: 100,\n          top: 20,\n          width: 80,\n          height: 24,\n        } as DOMRect,\n      });\n\n    expect(currentDragOffset).toEqual({ x: 40, y: 40 });\n    expect(dragState.eventId).toBe('event-1');\n    expect(dragUpdates.active).toBe(false);\n    expect(dragUpdates.pendingMove).toBe(true);\n    expect(dragUpdates.currentSegmentDays).toBe(3);\n    expect(dragUpdates.dragOffset).toBe(40);\n    expect(dragUpdates.dragOffsetY).toBe(12);\n  });\n\n  it('activates month/year move after threshold', () => {\n    expect(\n      shouldActivateDateGridMove({\n        clientX: 14,\n        clientY: 14,\n        startX: 10,\n        startY: 10,\n      })\n    ).toBe(true);\n    expect(\n      shouldActivateDateGridMove({\n        clientX: 11,\n        clientY: 11,\n        startX: 10,\n        startY: 10,\n      })\n    ).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/__tests__/eventEditing.test.ts",
    "content": "import {\n  getDayIndexForDate,\n  getEffectiveDaySpan,\n} from '@drag/hooks/utils/eventEditing';\n\ndescribe('eventEditing', () => {\n  it('treats overnight timed events ending at midnight within 24 hours as same-day spans', () => {\n    const start = new Date('2026-04-09T12:00:00');\n    const end = new Date('2026-04-10T00:00:00');\n\n    expect(getEffectiveDaySpan(start, end, false)).toBe(0);\n  });\n\n  it('keeps real multi-day timed events spanning beyond 24 hours as cross-day spans', () => {\n    const start = new Date('2026-04-09T12:00:00');\n    const end = new Date('2026-04-11T00:00:00');\n\n    expect(getEffectiveDaySpan(start, end, false)).toBe(2);\n  });\n\n  it('calculates day indexes relative to the current week start', () => {\n    const weekStart = new Date('2026-04-06T00:00:00');\n    const target = new Date('2026-04-09T15:00:00');\n\n    expect(getDayIndexForDate(weekStart, target)).toBe(3);\n    expect(getDayIndexForDate(undefined, target, 8)).toBe(8);\n  });\n});\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/__tests__/indicatorColor.test.ts",
    "content": "import {\n  getColorAlpha,\n  isUsableIndicatorBackground,\n} from '@drag/hooks/utils/indicatorColor';\n\ndescribe('indicatorColor', () => {\n  it('treats transparent colors as unusable indicator backgrounds', () => {\n    expect(getColorAlpha('transparent')).toBe(0);\n    expect(getColorAlpha('rgba(0, 0, 0, 0)')).toBe(0);\n    expect(getColorAlpha('rgb(0 0 0 / 0)')).toBe(0);\n    expect(isUsableIndicatorBackground('rgba(37, 99, 235, 0.2)')).toBe(false);\n  });\n\n  it('accepts solid and sufficiently opaque colors', () => {\n    expect(getColorAlpha('#2563eb')).toBe(1);\n    expect(getColorAlpha('#2563eb99')).toBeCloseTo(0.6, 1);\n    expect(isUsableIndicatorBackground('#2563eb')).toBe(true);\n    expect(isUsableIndicatorBackground('rgba(37, 99, 235, 0.6)')).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/__tests__/resolveDragSourceElement.test.ts",
    "content": "import { resolveDragSourceElement } from '@drag/hooks/utils/resolveDragSourceElement';\n\ndescribe('resolveDragSourceElement', () => {\n  it('prefers the colored month multi-day segment over the outer event shell', () => {\n    document.body.innerHTML = `\n      <div data-event-id=\"event-1\">\n        <div data-segment-days=\"3\">\n          <span id=\"label\">Event title</span>\n        </div>\n      </div>\n    `;\n\n    const label = document.querySelector('#label') as HTMLElement;\n    const resolved = resolveDragSourceElement(label);\n\n    expect(resolved.dataset.segmentDays).toBe('3');\n    expect(resolved.dataset.eventId).toBeUndefined();\n  });\n\n  it('falls back to the event shell when no segment wrapper exists', () => {\n    document.body.innerHTML = `\n      <div data-event-id=\"event-2\">\n        <span id=\"content\">Event title</span>\n      </div>\n    `;\n\n    const content = document.querySelector('#content') as HTMLElement;\n    const resolved = resolveDragSourceElement(content);\n\n    expect(resolved.dataset.eventId).toBe('event-2');\n  });\n});\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/dateGridDrag.ts",
    "content": "import {\n  Event,\n  MonthDragState,\n  dateToPlainDate,\n  dateToZonedDateTime,\n  temporalToDate,\n} from '@dayflow/core';\n\ntype DateGridDragLike = {\n  eventId?: string | null;\n  mode?: string | null;\n  originalEvent?: Event | null;\n  originalStartDate?: Date | null;\n  originalEndDate?: Date | null;\n  originalStartTime?:\n    | { hour: number; minute: number; second: number }\n    | null\n    | undefined;\n  originalEndTime?:\n    | { hour: number; minute: number; second: number }\n    | null\n    | undefined;\n  resizeDirection?: string | null;\n  targetDate?: Date | null;\n  grabDayOffset?: number;\n};\n\ntype DateGridPreviewRangeUpdate = {\n  kind: 'range';\n  targetDate: Date;\n  startDate: Date;\n  endDate: Date;\n};\n\ntype DateGridPreviewTargetUpdate = {\n  kind: 'target-only';\n  targetDate: Date;\n};\n\nexport type DateGridPreviewUpdate =\n  | DateGridPreviewRangeUpdate\n  | DateGridPreviewTargetUpdate\n  | null;\n\ntype BuildDateGridPreviewUpdateParams = {\n  addDaysToDate: (date: Date, days: number) => Date;\n  daysDifference: (from: Date, to: Date) => number;\n  drag: DateGridDragLike;\n  targetDate: Date;\n};\n\ntype BuildDateGridCreateEventParams = {\n  appTimeZone: string;\n  calendarId: string;\n  targetDate: Date;\n  title: string;\n};\n\ntype DateGridMoveStartDragUpdates = {\n  active: boolean;\n  pendingMove: boolean;\n  mode: 'move';\n  eventId: string;\n  startX: number;\n  startY: number;\n  targetDate: Date;\n  originalDate: Date;\n  originalEvent: Event;\n  lastUpdateTime: number;\n  originalStartDate: Date;\n  originalEndDate: Date;\n  eventDurationDays: number;\n  currentSegmentDays: number;\n  dragOffset: number;\n  dragOffsetY: number;\n  grabDayOffset: number;\n};\n\ntype BuildDateGridMoveStartDataParams = {\n  clientX: number;\n  clientY: number;\n  event: Event;\n  eventDurationDays: number;\n  eventEndDate: Date;\n  eventStartDate: Date;\n  grabDayOffset?: number;\n  isTouchLike: boolean;\n  sourceElement: HTMLElement;\n  sourceRect: DOMRect;\n};\n\nexport const shouldActivateDateGridMove = ({\n  clientX,\n  clientY,\n  startX,\n  startY,\n}: {\n  clientX: number;\n  clientY: number;\n  startX: number;\n  startY: number;\n}) => Math.hypot(clientX - startX, clientY - startY) >= 3;\n\nconst applyTimeToDate = (\n  date: Date,\n  isAllDay: boolean,\n  timeInfo: { hour: number; minute: number; second: number } | null | undefined\n) => {\n  if (isAllDay) {\n    date.setHours(0, 0, 0, 0);\n    return;\n  }\n\n  if (timeInfo) {\n    date.setHours(timeInfo.hour, timeInfo.minute, timeInfo.second ?? 0, 0);\n  }\n};\n\nexport const buildDateGridCreateEvent = ({\n  appTimeZone,\n  calendarId,\n  targetDate,\n  title,\n}: BuildDateGridCreateEventParams): Event => {\n  const startTime = new Date(targetDate);\n  startTime.setHours(9, 0, 0, 0);\n  const endTime = new Date(targetDate);\n  endTime.setHours(10, 0, 0, 0);\n\n  return {\n    id: String(Date.now()),\n    title,\n    start: dateToZonedDateTime(startTime, appTimeZone),\n    end: dateToZonedDateTime(endTime, appTimeZone),\n    day: targetDate.getDay(),\n    calendarId,\n    allDay: false,\n  };\n};\n\nexport const buildDateGridMoveStartData = ({\n  clientX,\n  clientY,\n  event,\n  eventDurationDays,\n  eventEndDate,\n  eventStartDate,\n  grabDayOffset = 0,\n  isTouchLike,\n  sourceElement,\n  sourceRect,\n}: BuildDateGridMoveStartDataParams): {\n  currentDragOffset: { x: number; y: number };\n  dragState: MonthDragState;\n  dragUpdates: DateGridMoveStartDragUpdates;\n} => {\n  const segmentDaysAttr = sourceElement.dataset.segmentDays;\n  const currentSegmentDays = segmentDaysAttr\n    ? Number.parseInt(segmentDaysAttr, 10)\n    : eventDurationDays;\n\n  const dragUpdates: DateGridMoveStartDragUpdates = {\n    active: isTouchLike,\n    pendingMove: !isTouchLike,\n    mode: 'move',\n    eventId: event.id,\n    startX: clientX,\n    startY: clientY,\n    targetDate: eventStartDate,\n    originalDate: eventStartDate,\n    originalEvent: { ...event },\n    lastUpdateTime: Date.now(),\n    originalStartDate: eventStartDate,\n    originalEndDate: eventEndDate,\n    eventDurationDays,\n    currentSegmentDays,\n    dragOffset: sourceRect.width / 2,\n    dragOffsetY: sourceRect.height / 2,\n    grabDayOffset,\n  };\n\n  return {\n    currentDragOffset: {\n      x: clientX - sourceRect.left,\n      y: clientY - sourceRect.top,\n    },\n    dragState: {\n      active: true,\n      mode: 'move',\n      eventId: event.id,\n      targetDate: eventStartDate,\n      startDate: eventStartDate,\n      endDate: eventEndDate,\n    },\n    dragUpdates,\n  };\n};\n\nexport const buildDateGridPreviewUpdate = ({\n  addDaysToDate,\n  daysDifference,\n  drag,\n  targetDate,\n}: BuildDateGridPreviewUpdateParams): DateGridPreviewUpdate => {\n  if (drag.mode === 'resize' && drag.originalEvent && drag.resizeDirection) {\n    const originalDate = temporalToDate(drag.originalEvent.start);\n    let newStartDate: Date;\n    let newEndDate: Date;\n    const isAllDay = drag.originalEvent.allDay ?? false;\n    const startTimeInfo = drag.originalStartTime;\n    const endTimeInfo = drag.originalEndTime;\n\n    if (drag.resizeDirection === 'left') {\n      newStartDate = new Date(targetDate);\n      applyTimeToDate(newStartDate, isAllDay, startTimeInfo);\n\n      const endBase = temporalToDate(drag.originalEvent.end) || originalDate;\n      newEndDate = new Date(endBase);\n      applyTimeToDate(newEndDate, isAllDay, endTimeInfo);\n      if (newStartDate > newEndDate) {\n        newStartDate = newEndDate;\n      }\n    } else {\n      const startBase =\n        temporalToDate(drag.originalEvent.start) || originalDate;\n      newStartDate = new Date(startBase);\n      applyTimeToDate(newStartDate, isAllDay, startTimeInfo);\n\n      newEndDate = new Date(targetDate);\n      applyTimeToDate(newEndDate, isAllDay, endTimeInfo);\n      if (newEndDate < newStartDate) {\n        newEndDate = newStartDate;\n      }\n    }\n\n    return {\n      kind: 'range',\n      targetDate: drag.resizeDirection === 'left' ? newStartDate : newEndDate,\n      startDate: newStartDate,\n      endDate: newEndDate,\n    };\n  }\n\n  if (drag.mode === 'move') {\n    if (drag.originalStartDate && drag.originalEndDate) {\n      const normalizedOriginalStart = new Date(drag.originalStartDate);\n      normalizedOriginalStart.setHours(0, 0, 0, 0);\n\n      const grabOffsetDays = drag.grabDayOffset ?? 0;\n      const adjustedTargetDate =\n        grabOffsetDays > 0\n          ? addDaysToDate(targetDate, -grabOffsetDays)\n          : targetDate;\n\n      const dragOffsetDays = daysDifference(\n        normalizedOriginalStart,\n        adjustedTargetDate\n      );\n      const newStartDate = addDaysToDate(\n        drag.originalStartDate,\n        dragOffsetDays\n      );\n      const newEndDate = addDaysToDate(drag.originalEndDate, dragOffsetDays);\n\n      if (drag.originalStartDate.getTime() === newStartDate.getTime()) {\n        return null;\n      }\n\n      return {\n        kind: 'range',\n        targetDate: newStartDate,\n        startDate: newStartDate,\n        endDate: newEndDate,\n      };\n    }\n\n    if (drag.targetDate?.getTime() !== targetDate.getTime()) {\n      return {\n        kind: 'target-only',\n        targetDate,\n      };\n    }\n  }\n\n  return null;\n};\n\ntype DateGridDropResultBase = {\n  originalEvent: Event;\n};\n\nexport type DateGridDropResult =\n  | (DateGridDropResultBase & {\n      kind: 'resize';\n      updatedEvent: Event;\n      startDate: Date;\n      endDate: Date;\n    })\n  | (DateGridDropResultBase & {\n      kind: 'move';\n      updatedEvent: Event;\n      startDate: Date;\n      endDate: Date;\n    })\n  | {\n      kind: 'restore';\n      originalEvent: Event;\n    }\n  | null;\n\ntype BuildDateGridDropResultParams = {\n  appTimeZone: string;\n  canonicalizeEditedEvent: (originalEvent: Event, visualEvent: Event) => Event;\n  clientX: number;\n  clientY: number;\n  drag: DateGridDragLike;\n  events?: Event[];\n  getTargetDateFromPosition: (clientX: number, clientY: number) => Date | null;\n};\n\nconst toEventTemporalRange = (\n  startDate: Date,\n  endDate: Date,\n  isAllDay: boolean,\n  appTimeZone: string\n) => ({\n  start: isAllDay\n    ? dateToPlainDate(startDate)\n    : dateToZonedDateTime(startDate, appTimeZone),\n  end: isAllDay\n    ? dateToPlainDate(endDate)\n    : dateToZonedDateTime(endDate, appTimeZone),\n});\n\nexport const buildDateGridDropResult = ({\n  appTimeZone,\n  canonicalizeEditedEvent,\n  clientX,\n  clientY,\n  drag,\n  events,\n  getTargetDateFromPosition,\n}: BuildDateGridDropResultParams): DateGridDropResult => {\n  if (\n    drag.mode === 'resize' &&\n    drag.eventId &&\n    drag.originalStartDate &&\n    drag.originalEndDate\n  ) {\n    const originalEvent =\n      drag.originalEvent || events?.find(event => event.id === drag.eventId);\n    if (!originalEvent) return null;\n\n    const temporals = toEventTemporalRange(\n      drag.originalStartDate,\n      drag.originalEndDate,\n      drag.originalEvent?.allDay || false,\n      appTimeZone\n    );\n\n    return {\n      kind: 'resize',\n      originalEvent,\n      updatedEvent: canonicalizeEditedEvent(originalEvent, {\n        ...originalEvent,\n        ...temporals,\n      }),\n      startDate: drag.originalStartDate,\n      endDate: drag.originalEndDate,\n    };\n  }\n\n  if (drag.mode !== 'move') return null;\n\n  if (drag.eventId && drag.originalStartDate && drag.originalEndDate) {\n    const originalEvent =\n      drag.originalEvent || events?.find(event => event.id === drag.eventId);\n\n    const originalEventStart = drag.originalEvent?.start\n      ? temporalToDate(drag.originalEvent.start)\n      : null;\n    const hasMoved =\n      originalEventStart &&\n      originalEventStart.getTime() !== drag.originalStartDate.getTime();\n\n    if (hasMoved && originalEvent) {\n      const temporals = toEventTemporalRange(\n        drag.originalStartDate,\n        drag.originalEndDate,\n        drag.originalEvent?.allDay || false,\n        appTimeZone\n      );\n\n      return {\n        kind: 'move',\n        originalEvent,\n        updatedEvent: canonicalizeEditedEvent(originalEvent, {\n          ...originalEvent,\n          ...temporals,\n        }),\n        startDate: drag.originalStartDate,\n        endDate: drag.originalEndDate,\n      };\n    }\n\n    if (drag.originalEvent) {\n      return {\n        kind: 'restore',\n        originalEvent: drag.originalEvent,\n      };\n    }\n  }\n\n  const finalTargetDate =\n    getTargetDateFromPosition(clientX, clientY) || drag.targetDate;\n\n  if (!drag.eventId || !finalTargetDate) return null;\n\n  const originalEvent =\n    drag.originalEvent || events?.find(event => event.id === drag.eventId);\n  if (!originalEvent) return null;\n\n  const eventStartDate = temporalToDate(originalEvent.start);\n  const eventEndDate = temporalToDate(originalEvent.end);\n\n  const newStartDate = new Date(finalTargetDate);\n  newStartDate.setHours(\n    eventStartDate.getHours(),\n    eventStartDate.getMinutes(),\n    0,\n    0\n  );\n\n  const newEndDate = new Date(finalTargetDate);\n  newEndDate.setHours(eventEndDate.getHours(), eventEndDate.getMinutes(), 0, 0);\n\n  const temporals = toEventTemporalRange(\n    newStartDate,\n    newEndDate,\n    originalEvent.allDay || false,\n    appTimeZone\n  );\n\n  return {\n    kind: 'move',\n    originalEvent,\n    updatedEvent: canonicalizeEditedEvent(originalEvent, {\n      ...originalEvent,\n      ...temporals,\n    }),\n    startDate: newStartDate,\n    endDate: newEndDate,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/dragInteraction.ts",
    "content": "export const getClientCoordinates = (e: MouseEvent | TouchEvent) => {\n  let clientX, clientY;\n  if ('touches' in e && e.touches.length > 0) {\n    clientX = e.touches[0].clientX;\n    clientY = e.touches[0].clientY;\n  } else if ('changedTouches' in e && e.changedTouches.length > 0) {\n    clientX = e.changedTouches[0].clientX;\n    clientY = e.changedTouches[0].clientY;\n  } else {\n    const mouseEvent = e as MouseEvent;\n    clientX = mouseEvent.clientX;\n    clientY = mouseEvent.clientY;\n  }\n\n  return { clientX, clientY };\n};\n\nexport const isTouchLikeEvent = (e: MouseEvent | TouchEvent) =>\n  'touches' in e || 'changedTouches' in e;\n\nexport const applyGlobalDragCursor = (\n  mode: 'move' | 'resize' | 'create',\n  cursor: 'grabbing' | 'ew-resize' | 'ns-resize'\n) => {\n  document.body.classList.add('df-drag-active');\n  document.body.style.cursor = cursor;\n\n  if (mode === 'move' || mode === 'create') {\n    document.body.classList.add('df-cursor-grabbing');\n    return;\n  }\n\n  document.body.classList.add(\n    cursor === 'ew-resize' ? 'df-cursor-ew-resize' : 'df-cursor-ns-resize'\n  );\n};\n\nexport const clearGlobalDragCursor = () => {\n  document.body.style.cursor = '';\n  document.body.style.touchAction = '';\n  document.body.classList.remove(\n    'df-drag-active',\n    'df-cursor-ns-resize',\n    'df-cursor-ew-resize',\n    'df-cursor-grabbing'\n  );\n};\n\nexport const addDocumentDragListeners = (\n  moveHandler: (e: MouseEvent | TouchEvent) => void,\n  endHandler: (e: MouseEvent | TouchEvent) => void,\n  cancelHandler?: () => void\n) => {\n  document.addEventListener('mousemove', moveHandler);\n  document.addEventListener('mouseup', endHandler);\n  document.addEventListener('touchmove', moveHandler, {\n    capture: true,\n    passive: false,\n  });\n  document.addEventListener('touchend', endHandler);\n  if (cancelHandler) {\n    document.addEventListener('touchcancel', cancelHandler);\n  }\n};\n\nexport const removeDocumentDragListeners = (\n  moveHandler: (e: MouseEvent | TouchEvent) => void,\n  endHandler: (e: MouseEvent | TouchEvent) => void,\n  cancelHandler?: () => void\n) => {\n  document.removeEventListener('mousemove', moveHandler);\n  document.removeEventListener('mouseup', endHandler);\n  document.removeEventListener('touchmove', moveHandler, {\n    capture: true,\n  });\n  document.removeEventListener('touchend', endHandler);\n  if (cancelHandler) {\n    document.removeEventListener('touchcancel', cancelHandler);\n  }\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/eventEditing.ts",
    "content": "import {\n  Event,\n  ICalendarApp,\n  dateToZonedDateTime,\n  extractHourFromDate,\n  getEventEndHour,\n  restoreVisualEventToCanonical,\n  temporalToVisualDate,\n} from '@dayflow/core';\nimport { Temporal } from 'temporal-polyfill';\n\nconst DAY_IN_MS = 24 * 60 * 60 * 1000;\n\nexport interface TimedEventHoursForEditing {\n  startDate: Date;\n  endDate: Date;\n  startHour: number;\n  endHour: number;\n}\n\nexport const getAppTimeZone = (app?: ICalendarApp | null) => {\n  if (!app) return Temporal.Now.timeZoneId();\n\n  const appWithTimeZone = app as typeof app & {\n    timeZone?: string;\n    state?: { timeZone?: string };\n  };\n\n  return (\n    appWithTimeZone.timeZone ??\n    appWithTimeZone.state?.timeZone ??\n    Temporal.Now.timeZoneId()\n  );\n};\n\nexport const getEventDateForEditing = (\n  temporal: Event['start'],\n  appTimeZone: string\n) => temporalToVisualDate(temporal, appTimeZone);\n\nexport const canonicalizeEditedEvent = (\n  originalEvent: Event,\n  visualEvent: Event,\n  appTimeZone: string\n): Event =>\n  restoreVisualEventToCanonical(originalEvent, visualEvent, appTimeZone);\n\nexport const getTimedEventHoursForEditing = (\n  event: Event,\n  appTimeZone: string\n): TimedEventHoursForEditing => {\n  const startDate = getEventDateForEditing(event.start, appTimeZone);\n  const endDate = getEventDateForEditing(event.end ?? event.start, appTimeZone);\n\n  return {\n    startDate,\n    endDate,\n    startHour: extractHourFromDate(startDate),\n    endHour: getEventEndHour({\n      ...event,\n      start: dateToZonedDateTime(startDate, appTimeZone),\n      end: dateToZonedDateTime(endDate, appTimeZone),\n    }),\n  };\n};\n\nexport const getEffectiveDaySpan = (\n  start: Date,\n  end: Date,\n  isAllDay: boolean = false\n): number => {\n  const startDate = new Date(start);\n  startDate.setHours(0, 0, 0, 0);\n  const endDate = new Date(end);\n  endDate.setHours(0, 0, 0, 0);\n\n  let span = Math.floor((endDate.getTime() - startDate.getTime()) / DAY_IN_MS);\n  if (span <= 0) return 0;\n\n  if (!isAllDay && span === 1) {\n    const isMidnightEnd =\n      end.getHours() === 0 &&\n      end.getMinutes() === 0 &&\n      end.getSeconds() === 0 &&\n      end.getMilliseconds() === 0;\n    const durationMs = end.getTime() - start.getTime();\n    if (isMidnightEnd && durationMs < DAY_IN_MS) {\n      return 0;\n    }\n  }\n\n  return span;\n};\n\nexport const getDayIndexForDate = (\n  currentWeekStart: Date | undefined,\n  date: Date,\n  fallback: number = 0\n): number => {\n  if (!currentWeekStart) return fallback;\n\n  const start = new Date(currentWeekStart);\n  start.setHours(0, 0, 0, 0);\n  const target = new Date(date);\n  target.setHours(0, 0, 0, 0);\n\n  return Math.floor((target.getTime() - start.getTime()) / DAY_IN_MS);\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/indicatorColor.ts",
    "content": "export const getColorAlpha = (color?: string | null): number => {\n  if (!color || color === 'transparent') return 0;\n\n  const normalized = color.trim().toLowerCase();\n  if (normalized === 'rgba(0, 0, 0, 0)' || normalized === 'rgb(0 0 0 / 0)') {\n    return 0;\n  }\n\n  const rgbaMatch = normalized.match(\n    /^rgba?\\(\\s*[\\d.]+\\s*[, ]\\s*[\\d.]+\\s*[, ]\\s*[\\d.]+(?:\\s*[,/]\\s*([\\d.]+))?\\s*\\)$/\n  );\n  if (rgbaMatch) {\n    return rgbaMatch[1] ? Number.parseFloat(rgbaMatch[1]) : 1;\n  }\n\n  const hexMatch = normalized.match(/^#([\\da-f]{4}|[\\da-f]{8})$/i);\n  if (hexMatch) {\n    const hex = hexMatch[1];\n    if (hex.length === 4) {\n      return Number.parseInt(hex[3] + hex[3], 16) / 255;\n    }\n    return Number.parseInt(hex.slice(6, 8), 16) / 255;\n  }\n\n  return 1;\n};\n\nexport const isUsableIndicatorBackground = (\n  color?: string | null,\n  minAlpha: number = 0.4\n) => getColorAlpha(color) >= minAlpha;\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/resolveDragSourceElement.ts",
    "content": "export const resolveDragSourceElement = (\n  element: HTMLElement | null\n): HTMLElement => {\n  if (!element) {\n    return document.body;\n  }\n\n  return (\n    element.closest<HTMLElement>('[data-segment-days]') ??\n    element.closest<HTMLElement>('[data-event-id]') ??\n    element\n  );\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/weekDay/__tests__/completion.test.ts",
    "content": "import { dateToZonedDateTime, temporalToDate } from '@dayflow/core';\nimport {\n  buildWeekDayCreateEvent,\n  buildWeekDayDropEvent,\n  finalizeWeekDayDragHours,\n} from '@drag/hooks/utils/weekDay/completion';\n\ndescribe('weekDayCompletion', () => {\n  it('keeps cross-day timed resize hours untouched when finalizing', () => {\n    const result = finalizeWeekDayDragHours({\n      drag: {\n        allDay: false,\n        endHour: 24,\n        mode: 'resize',\n        originalStartDate: new Date('2026-04-09T22:00:00'),\n        originalEndDate: new Date('2026-04-10T02:00:00'),\n        resizeDirection: 'bottom',\n        startHour: 22,\n      },\n      getEffectiveDaySpan: () => 1,\n      minDuration: 0.25,\n      roundToTimeStep: value => value,\n    });\n\n    expect(result.hasCrossDayTimedResize).toBe(true);\n    expect(result.finalStartHour).toBe(22);\n    expect(result.finalEndHour).toBe(24);\n  });\n\n  it('builds a week/day create event with the target day and hours', () => {\n    const event = buildWeekDayCreateEvent({\n      appTimeZone: 'UTC',\n      currentWeekStart: new Date('2026-04-06T00:00:00'),\n      drag: {\n        allDay: false,\n        dayIndex: 4,\n      },\n      finalEndHour: 11,\n      finalStartHour: 9,\n      getDateByDayIndex: (weekStart, dayIndex) => {\n        const nextDate = new Date(weekStart);\n        nextDate.setDate(weekStart.getDate() + dayIndex);\n        return nextDate;\n      },\n      title: 'newEvent',\n      writableCalendarId: 'work',\n    });\n\n    expect(event.day).toBe(4);\n    expect(event.calendarId).toBe('work');\n    expect(temporalToDate(event.start).toISOString()).toBe(\n      '2026-04-10T09:00:00.000Z'\n    );\n    expect(temporalToDate(event.end).toISOString()).toBe(\n      '2026-04-10T11:00:00.000Z'\n    );\n  });\n\n  it('builds a canonicalized timed resize drop event', () => {\n    const originalEvent = {\n      id: 'event-3',\n      title: 'Resize me',\n      day: 2,\n      start: dateToZonedDateTime(new Date('2026-04-08T09:00:00'), 'UTC'),\n      end: dateToZonedDateTime(new Date('2026-04-08T10:00:00'), 'UTC'),\n      allDay: false,\n      calendarId: 'work',\n    };\n\n    const updatedEvent = buildWeekDayDropEvent({\n      appTimeZone: 'UTC',\n      canonicalizeEditedEvent: (_original, visual) => ({\n        ...visual,\n        title: `${visual.title} canonical`,\n      }),\n      currentWeekStart: new Date('2026-04-06T00:00:00'),\n      drag: {\n        allDay: false,\n        dayIndex: 4,\n        mode: 'move',\n      },\n      finalEndHour: 11,\n      finalStartHour: 9,\n      getDateByDayIndex: (weekStart, dayIndex) => {\n        const nextDate = new Date(weekStart);\n        nextDate.setDate(weekStart.getDate() + dayIndex);\n        return nextDate;\n      },\n      originalEvent,\n    });\n\n    expect(updatedEvent.title).toBe('Resize me canonical');\n    expect(updatedEvent.day).toBe(4);\n    expect(temporalToDate(updatedEvent.start).toISOString()).toBe(\n      '2026-04-10T09:00:00.000Z'\n    );\n    expect(temporalToDate(updatedEvent.end).toISOString()).toBe(\n      '2026-04-10T11:00:00.000Z'\n    );\n  });\n});\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/weekDay/__tests__/crossRegion.test.ts",
    "content": "import {\n  dateToPlainDate,\n  dateToZonedDateTime,\n  temporalToDate,\n} from '@dayflow/core';\nimport {\n  buildCrossRegionAllDayPreview,\n  buildCrossRegionTimedPreview,\n  buildUniversalMoveDropResult,\n  shouldActivateUniversalMoveIndicator,\n} from '@drag/hooks/utils/weekDay/crossRegion';\n\ndescribe('crossRegionDrag', () => {\n  it('activates universal move indicator after threshold', () => {\n    expect(\n      shouldActivateUniversalMoveIndicator({\n        clientX: 13,\n        clientY: 14,\n        startX: 10,\n        startY: 10,\n      })\n    ).toBe(true);\n    expect(\n      shouldActivateUniversalMoveIndicator({\n        clientX: 11,\n        clientY: 11,\n        startX: 10,\n        startY: 10,\n      })\n    ).toBe(false);\n  });\n\n  it('builds all-day cross-region preview state', () => {\n    const { dragState, dragUpdates } = buildCrossRegionAllDayPreview({\n      currentWeekStart: new Date('2026-04-06T00:00:00Z'),\n      drag: {\n        allDay: false,\n        dayIndex: 2,\n        duration: 1,\n        endHour: 10,\n        eventId: 'event-1',\n        originalEndHour: 10,\n        originalStartHour: 9,\n        startHour: 9,\n      },\n      newDayIndex: 4,\n    });\n\n    expect(dragState).toMatchObject({\n      eventId: 'event-1',\n      dayIndex: 4,\n      startHour: 0,\n      endHour: 0,\n      allDay: true,\n    });\n    expect(dragUpdates).toMatchObject({\n      allDay: true,\n      dayIndex: 4,\n      startHour: 0,\n      endHour: 0,\n    });\n  });\n\n  it('builds timed cross-region preview state with clamped hours', () => {\n    const { dragState, dragUpdates } = buildCrossRegionTimedPreview({\n      currentWeekStart: new Date('2026-04-06T00:00:00Z'),\n      drag: {\n        allDay: false,\n        dayIndex: 2,\n        duration: 2,\n        endHour: 11,\n        eventId: 'event-1',\n        hourOffset: 0,\n        originalEndHour: 11,\n        originalStartHour: 9,\n        startHour: 9,\n      },\n      firstHour: 0,\n      lastHour: 24,\n      mouseHour: 23,\n      newDayIndex: 5,\n      roundToTimeStep: value => value,\n      timeStep: 0.25,\n    });\n\n    expect(dragState).toMatchObject({\n      eventId: 'event-1',\n      dayIndex: 5,\n      startHour: 22,\n      endHour: 24,\n      allDay: false,\n    });\n    expect(dragUpdates).toMatchObject({\n      allDay: false,\n      dayIndex: 5,\n      startHour: 22,\n      endHour: 24,\n    });\n  });\n\n  it('builds a moved timed event for cross-region drops', () => {\n    const targetEvent = {\n      id: 'event-1',\n      title: 'Focus',\n      day: 2,\n      start: dateToZonedDateTime(new Date('2026-04-08T09:00:00'), 'UTC'),\n      end: dateToZonedDateTime(new Date('2026-04-08T10:00:00'), 'UTC'),\n      allDay: false,\n      calendarId: 'work',\n    };\n\n    const { updatedEvent, originalEvent } = buildUniversalMoveDropResult({\n      appTimeZone: 'UTC',\n      canonicalizeEditedEvent: (_original, visual) => visual,\n      currentWeekStart: new Date('2026-04-06T00:00:00'),\n      drag: {\n        allDay: false,\n        dayIndex: 4,\n        endHour: 10,\n        originalDay: 2,\n        originalEvent: targetEvent,\n        startDragDayIndex: 2,\n        startHour: 9,\n      },\n      getEffectiveDaySpan: () => 0,\n      minDuration: 0.25,\n      roundToTimeStep: value => value,\n      targetEvent,\n    });\n\n    expect(originalEvent).toBe(targetEvent);\n    expect(updatedEvent.day).toBe(4);\n    expect(temporalToDate(updatedEvent.start).toISOString()).toBe(\n      '2026-04-10T09:00:00.000Z'\n    );\n    expect(temporalToDate(updatedEvent.end).toISOString()).toBe(\n      '2026-04-10T10:00:00.000Z'\n    );\n  });\n\n  it('preserves all-day span when moving across days', () => {\n    const targetEvent = {\n      id: 'event-2',\n      title: 'Trip',\n      day: 1,\n      start: dateToPlainDate(new Date('2026-04-07T00:00:00')),\n      end: dateToPlainDate(new Date('2026-04-09T00:00:00')),\n      allDay: true,\n      calendarId: 'travel',\n    };\n\n    const { updatedEvent } = buildUniversalMoveDropResult({\n      appTimeZone: 'UTC',\n      canonicalizeEditedEvent: (_original, visual) => visual,\n      currentWeekStart: new Date('2026-04-06T00:00:00'),\n      drag: {\n        allDay: true,\n        dayIndex: 3,\n        endHour: 0,\n        eventDurationDays: 2,\n        originalDay: 1,\n        originalEvent: targetEvent,\n        originalStartDate: new Date('2026-04-07T00:00:00'),\n        startDragDayIndex: 1,\n        startHour: 0,\n      },\n      getEffectiveDaySpan: () => 0,\n      minDuration: 0.25,\n      roundToTimeStep: value => value,\n      targetEvent,\n    });\n\n    expect(updatedEvent.day).toBe(3);\n    expect(\n      (updatedEvent.start as { year: number; month: number; day: number }).year\n    ).toBe(2026);\n    expect(\n      (updatedEvent.start as { year: number; month: number; day: number }).month\n    ).toBe(4);\n    expect(\n      (updatedEvent.start as { year: number; month: number; day: number }).day\n    ).toBe(9);\n    expect(\n      (updatedEvent.end as { year: number; month: number; day: number }).year\n    ).toBe(2026);\n    expect(\n      (updatedEvent.end as { year: number; month: number; day: number }).month\n    ).toBe(4);\n    expect(\n      (updatedEvent.end as { year: number; month: number; day: number }).day\n    ).toBe(11);\n  });\n});\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/weekDay/__tests__/drag.test.ts",
    "content": "import {\n  buildWeekDayCreateStartData,\n  getAllDayEventDurationDays,\n} from '@drag/hooks/utils/weekDay/drag';\n\ndescribe('weekDayDrag', () => {\n  it('calculates all-day durations with and without inclusive end dates', () => {\n    const start = new Date('2026-04-09T00:00:00');\n    const end = new Date('2026-04-11T00:00:00');\n\n    expect(getAllDayEventDurationDays(start, end, true)).toBe(3);\n    expect(getAllDayEventDurationDays(start, end, false)).toBe(2);\n  });\n\n  it('builds desktop week/day create drag state', () => {\n    const { dragState, dragUpdates } = buildWeekDayCreateStartData({\n      clientX: 100,\n      clientY: 200,\n      currentWeekStart: new Date('2026-04-06T00:00:00Z'),\n      dayIndex: 3,\n      getDateByDayIndex: (weekStart, dayIndex) => {\n        const nextDate = new Date(weekStart);\n        nextDate.setDate(weekStart.getDate() + dayIndex);\n        return nextDate;\n      },\n      isMobile: false,\n      roundToTimeStep: value => value,\n      startHour: 9,\n      timeStep: 0.25,\n    });\n\n    expect(dragState.dayIndex).toBe(3);\n    expect(dragState.startHour).toBe(9);\n    expect(dragState.endHour).toBe(10);\n    expect(dragUpdates.duration).toBe(1);\n    expect(dragUpdates.hourOffset).toBe(0);\n  });\n});\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/weekDay/__tests__/layout.test.ts",
    "content": "import { dateToZonedDateTime } from '@dayflow/core';\nimport { buildWeekDayDragLayout } from '@drag/hooks/utils/weekDay/layout';\n\ndescribe('weekDay layout', () => {\n  it('returns null when event cannot be resolved', () => {\n    expect(\n      buildWeekDayDragLayout({\n        calculateDragLayout: () => ({ width: 1 }) as never,\n        dayIndex: 2,\n        endHour: 10,\n        eventId: 'missing',\n        events: [],\n        roundToTimeStep: value => value,\n        startHour: 9,\n      })\n    ).toBeNull();\n  });\n\n  it('uses rounded hours when building drag layout', () => {\n    const calculateDragLayout = jest.fn(() => ({ width: 42 }) as never);\n    const result = buildWeekDayDragLayout({\n      calculateDragLayout,\n      dayIndex: 3,\n      endHour: 10.2,\n      eventId: 'event-1',\n      events: [\n        {\n          id: 'event-1',\n          title: 'Focus',\n          day: 2,\n          start: dateToZonedDateTime(new Date('2026-04-08T09:00:00Z'), 'UTC'),\n          end: dateToZonedDateTime(new Date('2026-04-08T10:00:00Z'), 'UTC'),\n          allDay: false,\n          calendarId: 'work',\n        },\n      ],\n      roundToTimeStep: value => Math.round(value),\n      startHour: 9.4,\n    });\n\n    expect(result).toEqual({ width: 42 });\n    expect(calculateDragLayout).toHaveBeenCalledWith(\n      expect.objectContaining({ id: 'event-1' }),\n      3,\n      9,\n      10\n    );\n  });\n});\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/weekDay/__tests__/preview.test.ts",
    "content": "import { dateToZonedDateTime, temporalToDate } from '@dayflow/core';\nimport {\n  buildSingleDayTimedResizeEventUpdate,\n  buildTimedCreatePreview,\n} from '@drag/hooks/utils/weekDay/preview';\n\ndescribe('weekDayPreview', () => {\n  it('builds timed create preview for mobile drag', () => {\n    const preview = buildTimedCreatePreview({\n      clientY: 0,\n      drag: {\n        duration: 2,\n        endHour: 11,\n        originalStartHour: 9,\n        hourOffset: 0,\n        startHour: 9,\n        startY: 0,\n      },\n      firstHour: 0,\n      isMobile: true,\n      lastHour: 24,\n      mouseHour: 23,\n      roundToTimeStep: value => value,\n      timeStep: 0.25,\n    });\n\n    expect(preview.startHour).toBe(22);\n    expect(preview.endHour).toBe(24);\n  });\n\n  it('builds single-day timed resize event dates', () => {\n    const { newEndDate, newStartDate, updatedEvent } =\n      buildSingleDayTimedResizeEventUpdate({\n        appTimeZone: 'UTC',\n        currentWeekStart: new Date('2026-04-06T00:00:00Z'),\n        endDayIndex: 4,\n        event: {\n          id: 'event-1',\n          title: 'Focus',\n          start: dateToZonedDateTime(new Date('2026-04-08T09:00:00Z'), 'UTC'),\n          end: dateToZonedDateTime(new Date('2026-04-08T10:00:00Z'), 'UTC'),\n          day: 2,\n          calendarId: 'work',\n          allDay: false,\n        },\n        getDateByDayIndex: (weekStart, dayIndex) => {\n          const nextDate = new Date(weekStart);\n          nextDate.setDate(weekStart.getDate() + dayIndex);\n          return nextDate;\n        },\n        roundedEnd: 11,\n        roundedStart: 9,\n        startDayIndex: 4,\n      });\n\n    expect(newStartDate.getFullYear()).toBe(2026);\n    expect(newStartDate.getMonth()).toBe(3);\n    expect(newStartDate.getDate()).toBe(10);\n    expect(newStartDate.getHours()).toBe(9);\n    expect(newEndDate.getFullYear()).toBe(2026);\n    expect(newEndDate.getMonth()).toBe(3);\n    expect(newEndDate.getDate()).toBe(10);\n    expect(newEndDate.getHours()).toBe(11);\n    expect(updatedEvent.day).toBe(4);\n    expect(temporalToDate(updatedEvent.start).toISOString()).toBe(\n      '2026-04-10T09:00:00.000Z'\n    );\n    expect(temporalToDate(updatedEvent.end).toISOString()).toBe(\n      '2026-04-10T11:00:00.000Z'\n    );\n  });\n});\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/weekDay/completion.ts",
    "content": "import {\n  Event,\n  createDateWithHour,\n  dateToPlainDate,\n  dateToZonedDateTime,\n  temporalToDate,\n} from '@dayflow/core';\n\ntype WeekDayFinalizeDragLike = {\n  allDay: boolean;\n  endHour: number;\n  mode?: 'create' | 'move' | 'resize' | null;\n  originalEndDate?: Date | null;\n  originalStartDate?: Date | null;\n  resizeDirection?: string | null;\n  startHour: number;\n};\n\ntype WeekDayCreateDragLike = {\n  allDay: boolean;\n  dayIndex: number;\n};\n\ntype WeekDayDropDragLike = {\n  allDay: boolean;\n  dayIndex: number;\n  mode?: 'move' | 'resize' | null;\n  originalEndDate?: Date | null;\n  originalEvent?: Event | null;\n  originalStartDate?: Date | null;\n};\n\nexport const finalizeWeekDayDragHours = ({\n  drag,\n  getEffectiveDaySpan,\n  minDuration,\n  roundToTimeStep,\n}: {\n  drag: WeekDayFinalizeDragLike;\n  getEffectiveDaySpan: (start: Date, end: Date, isAllDay?: boolean) => number;\n  minDuration: number;\n  roundToTimeStep: (value: number) => number;\n}) => {\n  const hasCrossDayTimedResize =\n    !drag.allDay &&\n    drag.mode === 'resize' &&\n    !!drag.originalStartDate &&\n    !!drag.originalEndDate &&\n    getEffectiveDaySpan(drag.originalStartDate, drag.originalEndDate, false) >\n      0;\n\n  let finalStartHour = roundToTimeStep(drag.startHour);\n  let finalEndHour = roundToTimeStep(drag.endHour);\n\n  if (!hasCrossDayTimedResize && finalEndHour - finalStartHour < minDuration) {\n    if (drag.resizeDirection === 'top') {\n      finalStartHour = finalEndHour - minDuration;\n    } else {\n      finalEndHour = finalStartHour + minDuration;\n    }\n  }\n\n  return {\n    finalEndHour,\n    finalStartHour,\n    hasCrossDayTimedResize,\n  };\n};\n\nexport const buildWeekDayCreateEvent = ({\n  appTimeZone,\n  currentWeekStart,\n  drag,\n  finalEndHour,\n  finalStartHour,\n  getDateByDayIndex,\n  title,\n  writableCalendarId,\n}: {\n  appTimeZone: string;\n  currentWeekStart?: Date;\n  drag: WeekDayCreateDragLike;\n  finalEndHour: number;\n  finalStartHour: number;\n  getDateByDayIndex: (weekStart: Date, dayIndex: number) => Date;\n  title: string;\n  writableCalendarId: string;\n}): Event => {\n  const eventDate = currentWeekStart\n    ? getDateByDayIndex(currentWeekStart, drag.dayIndex)\n    : new Date();\n  const startDate = createDateWithHour(eventDate, finalStartHour) as Date;\n  const endDate = createDateWithHour(eventDate, finalEndHour) as Date;\n\n  return {\n    id: String(Date.now()),\n    title,\n    day: drag.dayIndex,\n    start: drag.allDay\n      ? dateToPlainDate(startDate)\n      : dateToZonedDateTime(startDate, appTimeZone),\n    end: drag.allDay\n      ? dateToPlainDate(endDate)\n      : dateToZonedDateTime(endDate, appTimeZone),\n    calendarId: writableCalendarId,\n    allDay: drag.allDay,\n  };\n};\n\nexport const buildWeekDayDropEvent = ({\n  appTimeZone,\n  canonicalizeEditedEvent,\n  currentWeekStart,\n  drag,\n  finalEndHour,\n  finalStartHour,\n  getDateByDayIndex,\n  originalEvent,\n}: {\n  appTimeZone: string;\n  canonicalizeEditedEvent: (originalEvent: Event, visualEvent: Event) => Event;\n  currentWeekStart?: Date;\n  drag: WeekDayDropDragLike;\n  finalEndHour: number;\n  finalStartHour: number;\n  getDateByDayIndex: (weekStart: Date, dayIndex: number) => Date;\n  originalEvent: Event;\n}): Event => {\n  if (drag.allDay) {\n    const newStart = dateToPlainDate(\n      drag.originalStartDate || temporalToDate(originalEvent.start)\n    );\n    const newEnd = dateToPlainDate(\n      drag.originalEndDate || temporalToDate(originalEvent.end)\n    );\n\n    return {\n      ...originalEvent,\n      day: drag.dayIndex,\n      start: newStart,\n      end: newEnd,\n      allDay: true,\n    };\n  }\n\n  const eventStartDate = temporalToDate(originalEvent.start);\n  const startDateObj =\n    drag.mode === 'resize' && drag.originalStartDate\n      ? new Date(drag.originalStartDate)\n      : ((currentWeekStart\n          ? createDateWithHour(\n              getDateByDayIndex(currentWeekStart, drag.dayIndex),\n              finalStartHour\n            )\n          : createDateWithHour(eventStartDate, finalStartHour)) as Date);\n  const endDateObj =\n    drag.mode === 'resize' && drag.originalEndDate\n      ? new Date(drag.originalEndDate)\n      : ((currentWeekStart\n          ? createDateWithHour(\n              getDateByDayIndex(currentWeekStart, drag.dayIndex),\n              finalEndHour\n            )\n          : createDateWithHour(eventStartDate, finalEndHour)) as Date);\n\n  return canonicalizeEditedEvent(originalEvent, {\n    ...originalEvent,\n    day: drag.dayIndex,\n    start: dateToZonedDateTime(startDateObj, appTimeZone),\n    end: dateToZonedDateTime(endDateObj, appTimeZone),\n    allDay: false,\n  });\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/weekDay/crossRegion.ts",
    "content": "import {\n  Event,\n  WeekDayDragState,\n  createDateWithHour,\n  dateToPlainDate,\n  dateToZonedDateTime,\n  getDateByDayIndex,\n  getEndOfDay,\n  restoreTimedDragFromAllDayTransition,\n  temporalToDate,\n} from '@dayflow/core';\n\ntype CrossRegionMoveDragLike = {\n  allDay: boolean;\n  dayIndex: number;\n  duration: number;\n  endHour: number;\n  eventId?: string | null;\n  eventDate?: Date;\n  hourOffset?: number | null;\n  originalEndHour: number;\n  originalEvent?: Event | null;\n  originalStartHour: number;\n  startHour: number;\n};\n\ntype BuildCrossRegionAllDayPreviewParams = {\n  currentWeekStart?: Date;\n  drag: CrossRegionMoveDragLike;\n  newDayIndex: number;\n};\n\ntype BuildCrossRegionTimedPreviewParams = {\n  currentWeekStart?: Date;\n  drag: CrossRegionMoveDragLike;\n  firstHour: number;\n  lastHour: number;\n  mouseHour: number;\n  newDayIndex: number;\n  roundToTimeStep: (value: number) => number;\n  timeStep: number;\n};\n\ntype UniversalMoveDropDragLike = {\n  allDay: boolean;\n  dayIndex: number;\n  endHour: number;\n  eventDurationDays?: number;\n  originalDay: number;\n  originalEvent?: Event | null;\n  originalStartDate?: Date | null;\n  startDragDayIndex?: number;\n  startHour: number;\n};\n\ntype BuildUniversalMoveDropResultParams = {\n  appTimeZone: string;\n  canonicalizeEditedEvent: (originalEvent: Event, visualEvent: Event) => Event;\n  currentWeekStart?: Date;\n  drag: UniversalMoveDropDragLike;\n  getEffectiveDaySpan: (start: Date, end: Date, isAllDay?: boolean) => number;\n  minDuration: number;\n  roundToTimeStep: (value: number) => number;\n  targetEvent: Event;\n};\n\nexport const shouldActivateUniversalMoveIndicator = ({\n  clientX,\n  clientY,\n  startX,\n  startY,\n}: {\n  clientX: number;\n  clientY: number;\n  startX: number;\n  startY: number;\n}) => Math.hypot(clientX - startX, clientY - startY) >= 3;\n\nexport const buildCrossRegionAllDayPreview = ({\n  currentWeekStart,\n  drag,\n  newDayIndex,\n}: BuildCrossRegionAllDayPreviewParams): {\n  dragState: WeekDayDragState;\n  dragUpdates: Partial<CrossRegionMoveDragLike>;\n} => ({\n  dragState: {\n    active: true,\n    mode: 'move',\n    eventId: drag.eventId ?? null,\n    dayIndex: newDayIndex,\n    startHour: 0,\n    endHour: 0,\n    allDay: true,\n  },\n  dragUpdates: {\n    allDay: true,\n    dayIndex: newDayIndex,\n    startHour: 0,\n    endHour: 0,\n    eventDate: currentWeekStart\n      ? getDateByDayIndex(currentWeekStart, newDayIndex)\n      : new Date(),\n  },\n});\n\nexport const buildCrossRegionTimedPreview = ({\n  currentWeekStart,\n  drag,\n  firstHour,\n  lastHour,\n  mouseHour,\n  newDayIndex,\n  roundToTimeStep,\n  timeStep,\n}: BuildCrossRegionTimedPreviewParams): {\n  dragState: WeekDayDragState;\n  dragUpdates: Partial<CrossRegionMoveDragLike>;\n} => {\n  if (drag.allDay) {\n    const restoredTimedDrag = restoreTimedDragFromAllDayTransition({\n      wasOriginallyAllDay: drag.originalEvent?.allDay ?? false,\n      mouseHour,\n      hourOffset: drag.hourOffset,\n      duration: drag.duration,\n      originalStartHour: drag.originalStartHour,\n      originalEndHour: drag.originalEndHour,\n      firstHour,\n      lastHour,\n      minDuration: timeStep,\n      roundToTimeStep,\n    });\n\n    return {\n      dragState: {\n        active: true,\n        mode: 'move',\n        eventId: drag.eventId ?? null,\n        dayIndex: newDayIndex,\n        startHour: roundToTimeStep(restoredTimedDrag.startHour),\n        endHour: roundToTimeStep(restoredTimedDrag.endHour),\n        allDay: false,\n      },\n      dragUpdates: {\n        allDay: false,\n        dayIndex: newDayIndex,\n        startHour: restoredTimedDrag.startHour,\n        endHour: restoredTimedDrag.endHour,\n        duration: restoredTimedDrag.duration,\n        hourOffset: restoredTimedDrag.hourOffset,\n        eventDate: currentWeekStart\n          ? getDateByDayIndex(currentWeekStart, newDayIndex)\n          : new Date(),\n      },\n    };\n  }\n\n  let newStartHour = roundToTimeStep(mouseHour + (drag.hourOffset ?? 0));\n  newStartHour = Math.max(\n    firstHour,\n    Math.min(lastHour - drag.duration, newStartHour)\n  );\n\n  return {\n    dragState: {\n      active: true,\n      mode: 'move',\n      eventId: drag.eventId ?? null,\n      dayIndex: newDayIndex,\n      startHour: roundToTimeStep(newStartHour),\n      endHour: roundToTimeStep(newStartHour + drag.duration),\n      allDay: false,\n    },\n    dragUpdates: {\n      allDay: false,\n      dayIndex: newDayIndex,\n      startHour: newStartHour,\n      endHour: newStartHour + drag.duration,\n      eventDate: currentWeekStart\n        ? getDateByDayIndex(currentWeekStart, newDayIndex)\n        : new Date(),\n    },\n  };\n};\n\nexport const buildUniversalMoveDropResult = ({\n  appTimeZone,\n  canonicalizeEditedEvent,\n  currentWeekStart,\n  drag,\n  getEffectiveDaySpan,\n  minDuration,\n  roundToTimeStep,\n  targetEvent,\n}: BuildUniversalMoveDropResultParams): {\n  originalEvent: Event;\n  updatedEvent: Event;\n} => {\n  let finalStartHour = drag.startHour;\n  let finalEndHour = drag.endHour;\n\n  if (!drag.allDay) {\n    finalStartHour = roundToTimeStep(drag.startHour);\n    finalEndHour = roundToTimeStep(drag.endHour);\n    if (finalEndHour - finalStartHour < minDuration) {\n      finalEndHour = finalStartHour + minDuration;\n    }\n  }\n\n  const startDragDay = drag.startDragDayIndex ?? drag.originalDay;\n  const dayOffset = drag.dayIndex - startDragDay;\n  const newDay = drag.originalDay + dayOffset;\n\n  let visualEvent: Event;\n\n  if (drag.allDay) {\n    const originalStartDate = drag.originalStartDate\n      ? new Date(drag.originalStartDate)\n      : temporalToDate(targetEvent.start);\n    originalStartDate.setHours(0, 0, 0, 0);\n\n    const newStartDate = new Date(originalStartDate);\n    newStartDate.setDate(newStartDate.getDate() + dayOffset);\n\n    const newEndDate = new Date(newStartDate);\n    if (drag.eventDurationDays && drag.eventDurationDays > 0) {\n      newEndDate.setDate(newEndDate.getDate() + drag.eventDurationDays);\n    }\n\n    visualEvent = {\n      ...targetEvent,\n      day: newDay,\n      start: dateToPlainDate(newStartDate),\n      end: dateToPlainDate(getEndOfDay(newEndDate) as Date),\n      allDay: true,\n    };\n  } else {\n    const originalStart = temporalToDate(targetEvent.start);\n    const originalEnd = temporalToDate(targetEvent.end);\n    const isOriginallyMultiDay =\n      getEffectiveDaySpan(\n        originalStart,\n        originalEnd,\n        targetEvent.allDay ?? false\n      ) > 0;\n\n    if (isOriginallyMultiDay && !targetEvent.allDay) {\n      const newStartDate = new Date(originalStart);\n      newStartDate.setDate(newStartDate.getDate() + dayOffset);\n\n      const newEndDate = new Date(originalEnd);\n      newEndDate.setDate(newEndDate.getDate() + dayOffset);\n\n      visualEvent = {\n        ...targetEvent,\n        day: newDay,\n        start: dateToZonedDateTime(newStartDate, appTimeZone),\n        end: dateToZonedDateTime(newEndDate, appTimeZone),\n        allDay: false,\n      };\n    } else {\n      const newEventDate = currentWeekStart\n        ? getDateByDayIndex(currentWeekStart, drag.dayIndex)\n        : new Date();\n\n      const startDateObj = createDateWithHour(\n        newEventDate,\n        finalStartHour\n      ) as Date;\n      const endDateObj = createDateWithHour(newEventDate, finalEndHour) as Date;\n\n      visualEvent = {\n        ...targetEvent,\n        day: newDay,\n        start: dateToZonedDateTime(startDateObj, appTimeZone),\n        end: dateToZonedDateTime(endDateObj, appTimeZone),\n        allDay: false,\n      };\n    }\n  }\n\n  return {\n    originalEvent: drag.originalEvent || targetEvent,\n    updatedEvent: canonicalizeEditedEvent(targetEvent, visualEvent),\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/weekDay/drag.ts",
    "content": "import {\n  Event,\n  WeekDayDragState,\n  createDateWithHour,\n  temporalToDate,\n} from '@dayflow/core';\n\nconst DAY_IN_MS = 24 * 60 * 60 * 1000;\n\ntype EditingHours = {\n  startDate: Date;\n  endDate: Date;\n  startHour: number;\n  endHour: number;\n};\n\ntype SegmentInfo = {\n  dayIndex: number;\n  startHour: number;\n  endHour: number;\n};\n\ntype MoveStartDragUpdates = {\n  active: true;\n  mode: 'move';\n  eventId: string;\n  startX: number;\n  startY: number;\n  dayIndex: number;\n  startHour: number;\n  endHour: number;\n  originalDay: number;\n  originalStartHour: number;\n  originalEndHour: number;\n  allDay: boolean;\n  eventDate: Date;\n  eventDurationDays: number;\n  startDragDayIndex: number;\n  initialIndicatorLeft?: number;\n  initialIndicatorTop?: number;\n  initialIndicatorWidth?: number;\n  initialIndicatorHeight?: number;\n  indicatorContainer: HTMLElement | null;\n  calendarId?: string;\n  calendarIds?: string[];\n  title?: string;\n  hourOffset?: number;\n  duration?: number;\n};\n\ntype ResizeStartDragUpdates = {\n  active: true;\n  mode: 'resize';\n  eventId: string;\n  startX: number;\n  startY: number;\n  allDay?: boolean;\n  resizeDirection: string;\n  originalStartDate: Date;\n  originalEndDate: Date;\n  originalEvent: Event;\n  dayIndex: number;\n  startHour?: number;\n  endHour?: number;\n  originalDay?: number;\n  originalStartHour?: number;\n  originalEndHour?: number;\n  lastUpdateTime?: number;\n  initialMouseY?: number;\n  hourOffset?: number;\n};\n\ntype WeekDayMoveStartParams = {\n  allDayRowElement?: HTMLDivElement | null;\n  clientX: number;\n  clientY: number;\n  editingHours: EditingHours;\n  event: Event;\n  mouseHour: number;\n  sourceElement: HTMLElement;\n  sourceRect: DOMRect;\n};\n\ntype WeekDayResizeStartParams = {\n  clientX: number;\n  clientY: number;\n  direction: string;\n  editingHours: EditingHours;\n  event: Event;\n  mouseHour: number;\n};\n\ntype WeekDayCreateStartDragUpdates = {\n  active: true;\n  mode: 'create';\n  eventId: null;\n  startX: number;\n  startY: number;\n  dayIndex: number;\n  startHour: number;\n  endHour: number;\n  originalStartHour: number;\n  allDay: false;\n  eventDate: Date;\n  duration: number;\n  hourOffset: number;\n};\n\ntype WeekDayCreateStartParams = {\n  clientX: number;\n  clientY: number;\n  currentWeekStart?: Date;\n  dayIndex: number;\n  getDateByDayIndex: (weekStart: Date, dayIndex: number) => Date;\n  isMobile: boolean;\n  roundToTimeStep: (value: number) => number;\n  startHour: number;\n  timeStep: number;\n};\n\ntype CreateAllDayPreviewDragLike = {\n  dayIndex: number;\n  startX: number;\n  startY: number;\n};\n\ntype ResizeAllDayPreviewDragLike = {\n  originalStartDate?: Date | null;\n  originalEndDate?: Date | null;\n  resizeDirection?: string | null;\n};\n\ntype TimedResizePreviewDragLike = {\n  dayIndex: number;\n  endHour: number;\n  hourOffset?: number | null;\n  originalDay: number;\n  originalEndHour: number;\n  originalStartHour: number;\n  resizeDirection?: string | null;\n  startHour: number;\n};\n\ntype TimedMovePreviewDragLike = {\n  dayIndex: number;\n  duration: number;\n  hourOffset?: number | null;\n};\n\nexport const getAllDayEventDurationDays = (\n  start: Date,\n  end: Date,\n  inclusive: boolean\n) => {\n  const startDate = new Date(start);\n  startDate.setHours(0, 0, 0, 0);\n  const endDate = new Date(end);\n  endDate.setHours(0, 0, 0, 0);\n\n  const diff = Math.floor(\n    (endDate.getTime() - startDate.getTime()) / DAY_IN_MS\n  );\n  return inclusive ? Math.max(1, diff + 1) : Math.max(0, diff);\n};\n\nexport const buildWeekDayMoveStartData = ({\n  allDayRowElement,\n  clientX,\n  clientY,\n  editingHours,\n  event,\n  mouseHour,\n  sourceElement,\n  sourceRect,\n}: WeekDayMoveStartParams): {\n  dragUpdates: MoveStartDragUpdates;\n  dragState: WeekDayDragState;\n} => {\n  const segmentInfo = (\n    event as unknown as {\n      _segmentInfo?: SegmentInfo;\n    }\n  )._segmentInfo;\n  const isSegment = !!segmentInfo;\n\n  const currentDayIndex = isSegment ? segmentInfo.dayIndex : (event.day ?? 0);\n  const currentStartHour = isSegment\n    ? segmentInfo.startHour\n    : editingHours.startHour;\n  const eventDurationDays =\n    event.allDay && event.start && event.end\n      ? getAllDayEventDurationDays(\n          temporalToDate(event.start),\n          temporalToDate(event.end),\n          false\n        )\n      : 0;\n\n  const eventStartDate = temporalToDate(event.start);\n  const indicatorContainer = event.allDay\n    ? ((sourceElement.offsetParent as HTMLElement | null) ??\n      allDayRowElement ??\n      null)\n    : null;\n  const allDayContainerRect = event.allDay\n    ? indicatorContainer?.getBoundingClientRect()\n    : null;\n\n  const dragUpdates: MoveStartDragUpdates = {\n    active: true,\n    mode: 'move',\n    eventId: event.id,\n    startX: clientX,\n    startY: clientY,\n    dayIndex: currentDayIndex,\n    startHour: currentStartHour,\n    endHour: isSegment ? segmentInfo.endHour : editingHours.endHour,\n    originalDay: event.day ?? 0,\n    originalStartHour: editingHours.startHour,\n    originalEndHour: editingHours.endHour,\n    allDay: event.allDay || false,\n    eventDate: eventStartDate,\n    eventDurationDays,\n    startDragDayIndex: currentDayIndex,\n    initialIndicatorLeft: allDayContainerRect\n      ? sourceRect.left - allDayContainerRect.left\n      : undefined,\n    initialIndicatorTop: allDayContainerRect\n      ? sourceRect.top - allDayContainerRect.top\n      : undefined,\n    initialIndicatorWidth: allDayContainerRect ? sourceRect.width : undefined,\n    initialIndicatorHeight: allDayContainerRect ? sourceRect.height : undefined,\n    indicatorContainer,\n    calendarId: event.calendarId,\n    calendarIds: event.calendarIds,\n    title: event.title,\n  };\n\n  if (!event.allDay) {\n    dragUpdates.hourOffset = currentStartHour - mouseHour;\n    const durationInMs =\n      editingHours.endDate.getTime() - editingHours.startDate.getTime();\n    dragUpdates.duration = durationInMs / (1000 * 60 * 60);\n  }\n\n  return {\n    dragUpdates,\n    dragState: {\n      active: true,\n      mode: 'move',\n      eventId: event.id,\n      dayIndex: event.day ?? 0,\n      startHour: editingHours.startHour,\n      endHour: editingHours.endHour,\n      allDay: event.allDay || false,\n    },\n  };\n};\n\nexport const buildWeekDayResizeStartData = ({\n  clientX,\n  clientY,\n  direction,\n  editingHours,\n  event,\n  mouseHour,\n}: WeekDayResizeStartParams): {\n  dragUpdates: ResizeStartDragUpdates;\n  dragState: WeekDayDragState;\n} => {\n  if (event.allDay) {\n    const initialStartDate = temporalToDate(event.start);\n    const initialEndDate = temporalToDate(event.end);\n\n    return {\n      dragUpdates: {\n        active: true,\n        mode: 'resize',\n        eventId: event.id,\n        startX: clientX,\n        startY: clientY,\n        allDay: true,\n        resizeDirection: direction,\n        originalStartDate: initialStartDate,\n        originalEndDate: initialEndDate,\n        originalEvent: { ...event },\n        dayIndex: event.day ?? 0,\n      },\n      dragState: {\n        active: true,\n        mode: 'resize',\n        eventId: event.id,\n        dayIndex: event.day ?? 0,\n        startHour: 0,\n        endHour: 0,\n        allDay: true,\n      },\n    };\n  }\n\n  return {\n    dragUpdates: {\n      active: true,\n      mode: 'resize',\n      eventId: event.id,\n      startX: clientX,\n      startY: clientY,\n      dayIndex: event.day ?? 0,\n      startHour: editingHours.startHour,\n      endHour: editingHours.endHour,\n      originalDay: event.day ?? 0,\n      originalStartHour: editingHours.startHour,\n      originalEndHour: editingHours.endHour,\n      resizeDirection: direction,\n      lastUpdateTime: Date.now(),\n      initialMouseY: mouseHour,\n      originalStartDate: editingHours.startDate,\n      originalEndDate: editingHours.endDate,\n      hourOffset:\n        direction === 'top'\n          ? editingHours.startHour - mouseHour\n          : editingHours.endHour - mouseHour,\n      allDay: false,\n      originalEvent: { ...event },\n    },\n    dragState: {\n      active: true,\n      mode: 'resize',\n      eventId: event.id,\n      dayIndex: event.day ?? 0,\n      startHour: editingHours.startHour,\n      endHour: editingHours.endHour,\n      allDay: false,\n    },\n  };\n};\n\nexport const buildWeekDayCreateStartData = ({\n  clientX,\n  clientY,\n  currentWeekStart,\n  dayIndex,\n  getDateByDayIndex,\n  isMobile,\n  roundToTimeStep,\n  startHour,\n  timeStep,\n}: WeekDayCreateStartParams): {\n  dragState: WeekDayDragState;\n  dragUpdates: WeekDayCreateStartDragUpdates;\n} => {\n  const initialDuration = isMobile ? 1 : timeStep * 4;\n  const hourOffset = isMobile ? -initialDuration / 2 : 0;\n  const adjustedStart = roundToTimeStep(startHour + hourOffset);\n\n  const dragUpdates: WeekDayCreateStartDragUpdates = {\n    active: true,\n    mode: 'create',\n    eventId: null,\n    startX: clientX,\n    startY: clientY,\n    dayIndex,\n    startHour: adjustedStart,\n    endHour: adjustedStart + initialDuration,\n    originalStartHour: adjustedStart,\n    allDay: false,\n    eventDate: currentWeekStart\n      ? getDateByDayIndex(currentWeekStart, dayIndex)\n      : new Date(),\n    duration: initialDuration,\n    hourOffset,\n  };\n\n  return {\n    dragState: {\n      active: true,\n      mode: 'create',\n      eventId: null,\n      dayIndex,\n      startHour: dragUpdates.startHour,\n      endHour: dragUpdates.endHour,\n      allDay: false,\n    },\n    dragUpdates,\n  };\n};\n\nexport const buildAllDayCreateMovePreview = ({\n  clientX,\n  clientY,\n  drag,\n  getColumnDayIndex,\n  isDayView,\n}: {\n  clientX: number;\n  clientY: number;\n  drag: CreateAllDayPreviewDragLike;\n  getColumnDayIndex: (clientX: number) => number;\n  isDayView: boolean;\n}) => ({\n  distance: Math.hypot(clientX - drag.startX, clientY - drag.startY),\n  newDayIndex: isDayView ? drag.dayIndex : getColumnDayIndex(clientX),\n});\n\nexport const buildAllDayResizePreview = ({\n  currentWeekStart,\n  drag,\n  getDateByDayIndex,\n  targetDayIndex,\n}: {\n  currentWeekStart?: Date;\n  drag: ResizeAllDayPreviewDragLike;\n  getDateByDayIndex: (weekStart: Date, dayIndex: number) => Date;\n  targetDayIndex: number;\n}) => {\n  let newStartDate = drag.originalStartDate || new Date();\n  let newEndDate = drag.originalEndDate || new Date();\n\n  if (drag.resizeDirection === 'left') {\n    newStartDate = currentWeekStart\n      ? getDateByDayIndex(currentWeekStart, targetDayIndex)\n      : new Date();\n    if (newStartDate > newEndDate) {\n      newStartDate = newEndDate;\n    }\n  } else if (drag.resizeDirection === 'right') {\n    newEndDate = currentWeekStart\n      ? getDateByDayIndex(currentWeekStart, targetDayIndex)\n      : new Date();\n    if (newEndDate < newStartDate) {\n      newEndDate = newStartDate;\n    }\n  }\n\n  return { newStartDate, newEndDate };\n};\n\nexport const buildCrossDayTimedResizePreview = ({\n  currentWeekStart,\n  drag,\n  firstHour,\n  getDateByDayIndex,\n  getDayIndexForDate,\n  getEventDateForEditing,\n  lastHour,\n  mouseHour,\n  originalEvent,\n  roundToTimeStep,\n  timeStepMs,\n}: {\n  currentWeekStart?: Date;\n  drag: TimedResizePreviewDragLike;\n  firstHour: number;\n  getDateByDayIndex: (weekStart: Date, dayIndex: number) => Date;\n  getDayIndexForDate: (date: Date, fallback: number) => number;\n  getEventDateForEditing: (temporal: Event['start']) => Date;\n  lastHour: number;\n  mouseHour: number;\n  originalEvent: Event;\n  roundToTimeStep: (value: number) => number;\n  timeStepMs: number;\n}) => {\n  const targetDayIndex = currentWeekStart ? drag.dayIndex : drag.dayIndex;\n  const resolvedTargetDayIndex = targetDayIndex;\n  const proposedHour = roundToTimeStep(\n    Math.max(firstHour, Math.min(lastHour, mouseHour + (drag.hourOffset ?? 0)))\n  );\n  const pointerBaseDate = currentWeekStart\n    ? getDateByDayIndex(currentWeekStart, resolvedTargetDayIndex)\n    : getEventDateForEditing(originalEvent.start);\n  const pointerDate = createDateWithHour(pointerBaseDate, proposedHour) as Date;\n\n  const anchorStartDate = getEventDateForEditing(originalEvent.start);\n  const anchorEndDate = getEventDateForEditing(\n    originalEvent.end ?? originalEvent.start\n  );\n\n  let newStartDate = new Date(anchorStartDate);\n  let newEndDate = new Date(anchorEndDate);\n\n  if (drag.resizeDirection === 'bottom') {\n    if (pointerDate.getTime() >= anchorStartDate.getTime()) {\n      newStartDate = new Date(anchorStartDate);\n      newEndDate = new Date(\n        Math.max(pointerDate.getTime(), anchorStartDate.getTime() + timeStepMs)\n      );\n    } else {\n      newStartDate = new Date(\n        Math.min(pointerDate.getTime(), anchorStartDate.getTime() - timeStepMs)\n      );\n      newEndDate = new Date(anchorStartDate);\n    }\n  } else if (drag.resizeDirection === 'right') {\n    const pointerEndDate = createDateWithHour(\n      pointerBaseDate,\n      anchorEndDate.getHours() + anchorEndDate.getMinutes() / 60\n    ) as Date;\n    if (pointerEndDate.getTime() >= anchorStartDate.getTime()) {\n      newStartDate = new Date(anchorStartDate);\n      newEndDate = new Date(\n        Math.max(\n          pointerEndDate.getTime(),\n          anchorStartDate.getTime() + timeStepMs\n        )\n      );\n    } else {\n      newStartDate = new Date(\n        Math.min(\n          pointerEndDate.getTime(),\n          anchorStartDate.getTime() - timeStepMs\n        )\n      );\n      newEndDate = new Date(anchorStartDate);\n    }\n  } else if (drag.resizeDirection === 'top') {\n    if (pointerDate.getTime() <= anchorEndDate.getTime()) {\n      newStartDate = new Date(\n        Math.min(pointerDate.getTime(), anchorEndDate.getTime() - timeStepMs)\n      );\n      newEndDate = new Date(anchorEndDate);\n    } else {\n      newStartDate = new Date(anchorEndDate);\n      newEndDate = new Date(\n        Math.max(pointerDate.getTime(), anchorEndDate.getTime() + timeStepMs)\n      );\n    }\n  }\n\n  const startDayIndex = getDayIndexForDate(newStartDate, drag.originalDay);\n  const endDayIndex = getDayIndexForDate(newEndDate, startDayIndex);\n  const indicatorStartHour =\n    newStartDate.getHours() + newStartDate.getMinutes() / 60;\n  const indicatorEndHour =\n    startDayIndex === endDayIndex\n      ? newEndDate.getHours() + newEndDate.getMinutes() / 60\n      : lastHour;\n\n  return {\n    endDayIndex,\n    indicatorEndHour,\n    indicatorStartHour,\n    newEndDate,\n    newStartDate,\n    startDayIndex,\n  };\n};\n\nexport const buildSingleDayTimedResizePreview = ({\n  currentEvent,\n  drag,\n  firstHour,\n  getEffectiveDaySpan,\n  lastHour,\n  mouseHour,\n  timeStep,\n}: {\n  currentEvent: Event;\n  drag: TimedResizePreviewDragLike;\n  firstHour: number;\n  getEffectiveDaySpan: (start: Date, end: Date, isAllDay?: boolean) => number;\n  lastHour: number;\n  mouseHour: number;\n  timeStep: number;\n}) => {\n  let newStartHour = drag.startHour;\n  let newEndHour = drag.endHour;\n\n  const eventStart = temporalToDate(currentEvent.start);\n  const eventEnd = temporalToDate(currentEvent.end);\n  const span = getEffectiveDaySpan(\n    eventStart,\n    eventEnd,\n    currentEvent.allDay ?? false\n  );\n  const eventEndDayIndex = (currentEvent.day ?? 0) + span;\n\n  let endDayIndex = eventEndDayIndex;\n  let startDayIndex = drag.originalDay;\n\n  if (drag.resizeDirection === 'top') {\n    const targetDayIndex = drag.dayIndex;\n    const proposedStartHour = mouseHour + (drag.hourOffset ?? 0);\n\n    if (targetDayIndex < eventEndDayIndex) {\n      startDayIndex = targetDayIndex;\n      newStartHour = Math.max(firstHour, Math.min(lastHour, proposedStartHour));\n    } else {\n      startDayIndex = eventEndDayIndex;\n\n      if (proposedStartHour > drag.originalEndHour) {\n        newStartHour = drag.originalEndHour;\n        newEndHour = proposedStartHour;\n      } else {\n        newStartHour = Math.max(firstHour, proposedStartHour);\n        if (drag.originalEndHour - newStartHour < timeStep) {\n          newStartHour = drag.originalEndHour - timeStep;\n        }\n      }\n    }\n  } else if (drag.resizeDirection === 'bottom') {\n    const targetDayIndex = drag.dayIndex;\n    const proposedEndHour = mouseHour + (drag.hourOffset ?? 0);\n\n    if (targetDayIndex === drag.dayIndex) {\n      if (proposedEndHour < drag.originalStartHour) {\n        newEndHour = drag.originalStartHour;\n        newStartHour = proposedEndHour;\n      } else {\n        newEndHour = Math.min(lastHour, proposedEndHour);\n        if (newEndHour - drag.startHour < timeStep) {\n          newEndHour = drag.startHour + timeStep;\n        }\n      }\n    } else {\n      endDayIndex = targetDayIndex;\n      newEndHour = Math.max(firstHour, Math.min(lastHour, proposedEndHour));\n    }\n  }\n\n  if (endDayIndex === startDayIndex) {\n    newStartHour = Math.max(firstHour, Math.min(newStartHour, newEndHour));\n    newEndHour = Math.min(lastHour, Math.max(newStartHour, newEndHour));\n  }\n\n  return {\n    endDayIndex,\n    newEndHour,\n    newStartHour,\n    startDayIndex,\n  };\n};\n\nexport const buildTimedMovePreview = ({\n  clientX,\n  drag,\n  firstHour,\n  getColumnDayIndex,\n  isDayView,\n  lastHour,\n  mouseHour,\n  roundToTimeStep,\n}: {\n  clientX: number;\n  drag: TimedMovePreviewDragLike;\n  firstHour: number;\n  getColumnDayIndex: (clientX: number) => number;\n  isDayView: boolean;\n  lastHour: number;\n  mouseHour: number;\n  roundToTimeStep: (value: number) => number;\n}) => {\n  let newStartHour = roundToTimeStep(mouseHour + (drag.hourOffset ?? 0));\n  newStartHour = Math.max(\n    firstHour,\n    Math.min(lastHour - drag.duration, newStartHour)\n  );\n\n  return {\n    dayIndex: isDayView ? drag.dayIndex : getColumnDayIndex(clientX),\n    endHour: newStartHour + drag.duration,\n    startHour: newStartHour,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/weekDay/layout.ts",
    "content": "import { Event, EventLayout } from '@dayflow/core';\n\nexport const buildWeekDayDragLayout = ({\n  calculateDragLayout,\n  dayIndex,\n  endHour,\n  eventId,\n  events,\n  roundToTimeStep,\n  startHour,\n}: {\n  calculateDragLayout?: (\n    event: Event,\n    dayIndex: number,\n    startHour: number,\n    endHour: number\n  ) => EventLayout | null;\n  dayIndex: number;\n  endHour: number;\n  eventId?: string | null;\n  events?: Event[];\n  roundToTimeStep: (value: number) => number;\n  startHour: number;\n}): EventLayout | null => {\n  if (!eventId || !calculateDragLayout) return null;\n\n  const draggedEvent = events?.find(target => target.id === eventId);\n  if (!draggedEvent) return null;\n\n  return calculateDragLayout(\n    draggedEvent,\n    dayIndex,\n    roundToTimeStep(startHour),\n    roundToTimeStep(endHour)\n  );\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/hooks/utils/weekDay/preview.ts",
    "content": "import {\n  Event,\n  createDateWithHour,\n  dateToZonedDateTime,\n  temporalToDate,\n} from '@dayflow/core';\n\ntype SingleDayTimedResizeEventParams = {\n  appTimeZone: string;\n  currentWeekStart?: Date;\n  endDayIndex: number;\n  event: Event;\n  getDateByDayIndex: (weekStart: Date, dayIndex: number) => Date;\n  roundedEnd: number;\n  roundedStart: number;\n  startDayIndex: number;\n};\n\ntype TimedCreatePreviewDragLike = {\n  duration?: number;\n  endHour: number;\n  originalStartHour: number;\n  hourOffset?: number | null;\n  startHour: number;\n  startY: number;\n};\n\nexport const buildSingleDayTimedResizeEventUpdate = ({\n  appTimeZone,\n  currentWeekStart,\n  endDayIndex,\n  event,\n  getDateByDayIndex,\n  roundedEnd,\n  roundedStart,\n  startDayIndex,\n}: SingleDayTimedResizeEventParams) => {\n  const eventStartDate = temporalToDate(event.start);\n  const newStartDate = createDateWithHour(\n    currentWeekStart\n      ? getDateByDayIndex(currentWeekStart, startDayIndex)\n      : eventStartDate,\n    roundedStart\n  ) as Date;\n  const endDate = currentWeekStart\n    ? getDateByDayIndex(currentWeekStart, endDayIndex)\n    : eventStartDate;\n  const newEndDate = createDateWithHour(endDate, roundedEnd) as Date;\n\n  return {\n    newEndDate,\n    newStartDate,\n    updatedEvent: {\n      ...event,\n      start: dateToZonedDateTime(newStartDate, appTimeZone),\n      end: dateToZonedDateTime(newEndDate, appTimeZone),\n      day: startDayIndex,\n    },\n  };\n};\n\nexport const buildTimedCreatePreview = ({\n  clientY,\n  drag,\n  firstHour,\n  isMobile,\n  lastHour,\n  mouseHour,\n  roundToTimeStep,\n  timeStep,\n}: {\n  clientY: number;\n  drag: TimedCreatePreviewDragLike;\n  firstHour: number;\n  isMobile: boolean;\n  lastHour: number;\n  mouseHour: number;\n  roundToTimeStep: (value: number) => number;\n  timeStep: number;\n}) => {\n  if (isMobile) {\n    const newHour = roundToTimeStep(mouseHour + (drag.hourOffset ?? 0));\n    const safeStartHour = Math.max(\n      firstHour,\n      Math.min(lastHour - (drag.duration || 1), newHour)\n    );\n\n    return {\n      endHour: safeStartHour + (drag.duration || 1),\n      startHour: safeStartHour,\n    };\n  }\n\n  const newHour = roundToTimeStep(mouseHour);\n  const [newStartHour, newEndHour] =\n    clientY < drag.startY\n      ? [newHour, Math.max(newHour + timeStep, drag.originalStartHour)]\n      : [drag.startHour, Math.max(drag.startHour + timeStep, newHour)];\n\n  return {\n    endHour: newEndHour,\n    startHour: newStartHour,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/index.ts",
    "content": "export { createDragPlugin, isDragService, createDragConfig } from './plugin';\n"
  },
  {
    "path": "packages/plugins/drag/src/plugin.ts",
    "content": "// oxlint-disable typescript/no-explicit-any\nimport {\n  registerDragImplementation,\n  CalendarPlugin,\n  ICalendarApp,\n  ViewType,\n  DragHookOptions,\n  DragHookReturn,\n  DragPluginConfig,\n  DragService,\n} from '@dayflow/core';\n\nimport { useDrag } from './hooks';\n\nexport function createDragPlugin(\n  config: Partial<DragPluginConfig> = {}\n): CalendarPlugin {\n  const finalConfig: DragPluginConfig = {\n    enableDrag: true,\n    enableResize: true,\n    enableCreate: true,\n    enableAllDayCreate: true,\n    supportedViews: [\n      ViewType.DAY,\n      ViewType.WEEK,\n      ViewType.MONTH,\n      ViewType.YEAR,\n      ViewType.RESOURCE,\n    ],\n    ...config,\n  };\n\n  const dragService: DragService = {\n    getConfig: () => finalConfig,\n    updateConfig: (updates: Partial<DragPluginConfig>) => {\n      Object.assign(finalConfig, updates);\n    },\n    isViewSupported: (viewType: ViewType): boolean =>\n      finalConfig.supportedViews.includes(viewType),\n  };\n\n  return {\n    name: 'drag',\n    config: finalConfig,\n    install: (_app: ICalendarApp) => {\n      if (\n        (globalThis as unknown as { process?: { env?: { NODE_ENV?: string } } })\n          .process?.env?.NODE_ENV !== 'production'\n      ) {\n        console.log('[DayFlow] Drag plugin installed');\n      }\n\n      registerDragImplementation(\n        (app: ICalendarApp, options: DragHookOptions): DragHookReturn => {\n          const result = useDrag({ ...options, app } as any);\n\n          const currentDragService = app.getPlugin<DragService>('drag');\n          if (!currentDragService) {\n            return {\n              handleMoveStart: () => {\n                /* noop */\n              },\n              handleCreateStart: () => {\n                /* noop */\n              },\n              handleResizeStart: () => {\n                /* noop */\n              },\n              handleCreateAllDayEvent: undefined,\n              dragState: result.dragState,\n              isDragging: false,\n            };\n          }\n\n          const cfg = currentDragService.getConfig();\n          const isSupported = currentDragService.isViewSupported(\n            options.viewType\n          );\n          const readOnlyConfig = app.getReadOnlyConfig();\n          const isDraggable = readOnlyConfig.draggable !== false;\n          const isEditable = !app.state.readOnly;\n\n          if (!isSupported) {\n            console.info(\n              `Drag functionality is not supported for ${options.viewType} view.`\n            );\n          }\n\n          return {\n            handleMoveStart:\n              isSupported && cfg.enableDrag && isDraggable\n                ? (result.handleMoveStart as any)\n                : () => {\n                    /* noop */\n                  },\n            handleCreateStart:\n              isSupported && cfg.enableCreate && isEditable\n                ? (result.handleCreateStart as any)\n                : () => {\n                    /* noop */\n                  },\n            handleResizeStart:\n              isSupported && cfg.enableResize && isEditable\n                ? (result.handleResizeStart as any)\n                : undefined,\n            handleCreateAllDayEvent:\n              isSupported && cfg.enableAllDayCreate && isEditable\n                ? (result.handleCreateAllDayEvent as any)\n                : () => {\n                    /* noop */\n                  },\n            dragState: result.dragState,\n            isDragging: isSupported && isDraggable ? result.isDragging : false,\n          };\n        }\n      );\n    },\n    api: dragService,\n  };\n}\n\nexport function isDragService(obj: unknown): obj is DragService {\n  return (\n    typeof obj === 'object' &&\n    obj !== null &&\n    'getConfig' in obj &&\n    'updateConfig' in obj &&\n    'isViewSupported' in obj\n  );\n}\n\nexport function createDragConfig(\n  overrides: Partial<DragPluginConfig> = {}\n): DragPluginConfig {\n  return {\n    enableDrag: true,\n    enableResize: true,\n    enableCreate: true,\n    enableAllDayCreate: true,\n    supportedViews: [\n      ViewType.DAY,\n      ViewType.WEEK,\n      ViewType.MONTH,\n      ViewType.YEAR,\n      ViewType.RESOURCE,\n    ],\n    ...overrides,\n  };\n}\n"
  },
  {
    "path": "packages/plugins/drag/src/styles/drag.css",
    "content": "/* Mobile drag-over highlight: applied directly to [data-date] cells via DOM\n   on mobile to avoid setState re-renders that would freeze the drag animation */\n[data-date][data-drag-over='true'] {\n  background-color: color-mix(\n    in srgb,\n    var(--df-color-primary, #3b82f6) 12%,\n    var(--df-color-background, #fff)\n  ) !important;\n}\n\n/* ── DayFlow Plugin – Drag Indicator ────────────────────────────────────── */\n\n@layer components {\n  /* Common container for all types of drag indicators */\n  .df-drag-indicator-content {\n    position: relative;\n    height: 100%;\n    width: 100%;\n    overflow: hidden;\n  }\n\n  .df-drag-indicator-icon {\n    margin-right: 0.5rem;\n    height: 0.75rem;\n    width: 0.75rem;\n  }\n\n  .df-drag-indicator-all-day {\n    display: flex;\n    height: 100%;\n    align-items: center;\n    padding-left: 0.75rem;\n    overflow: hidden;\n  }\n\n  .df-drag-indicator-icon[data-light='false'] {\n    color: white;\n  }\n\n  /* Specific styles for regular timed indicators */\n  .df-drag-indicator-regular-wrapper {\n    position: relative;\n    height: 100%;\n    overflow: hidden;\n  }\n\n  .df-drag-indicator-regular-wrapper[data-light='false'] {\n    color: white;\n  }\n\n  /* Mobile interaction handles */\n  .df-drag-indicator-mobile-handle {\n    position: absolute;\n    z-index: 50;\n    height: 0.625rem;\n    width: 0.625rem;\n    border-radius: 9999px;\n    border-width: 2px;\n    background-color: white;\n  }\n\n  .df-drag-indicator-mobile-handle-top {\n    top: -0.375rem;\n    right: 1.25rem;\n  }\n\n  .df-drag-indicator-mobile-handle-bottom {\n    bottom: -0.375rem;\n    left: 1.25rem;\n  }\n\n  /* Shared Pills / Shapes */\n  .df-drag-indicator-month-pill,\n  .df-drag-indicator-regular-pill,\n  .df-drag-indicator-all-day-pill {\n    border-radius: 0.25rem;\n    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n  }\n\n  .df-drag-indicator-manual-pill {\n    border-radius: 0.25rem;\n    font-size: 0.75rem;\n    padding: 0.125rem 0.5rem;\n  }\n\n  .df-drag-indicator-ghost {\n    border-style: dashed;\n    border-width: 1px;\n  }\n\n  .df-drag-indicator-default-bar {\n    position: absolute;\n    top: 0.25rem;\n    bottom: 0.25rem;\n    left: 0.125rem;\n    width: 0.125rem;\n    border-radius: 9999px;\n  }\n\n  /* Month/Year View Specific */\n  .df-drag-indicator-month {\n    position: relative;\n    display: flex;\n    height: 100%;\n    width: 100%;\n    min-width: 0;\n    align-items: center;\n    overflow: hidden;\n    border-radius: 0.25rem;\n    padding-left: 0.5rem;\n    font-size: 0.75rem;\n    font-weight: 500;\n    color: white;\n  }\n\n  .df-drag-indicator-month-icon-wrap {\n    margin-right: 0.25rem;\n    flex-shrink: 0;\n    position: relative;\n    z-index: 10;\n  }\n\n  .df-drag-indicator-month-content {\n    min-width: 0;\n    flex: 1;\n    position: relative;\n    z-index: 10;\n  }\n\n  .df-drag-indicator-month-title {\n    display: block;\n    width: 100%;\n    overflow: hidden;\n    font-weight: 500;\n    white-space: nowrap;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/drag/src/utils/defaultDragConfig.ts",
    "content": "import { getLineColor } from '@dayflow/core';\n\nexport const defaultDragConfig = {\n  HOUR_HEIGHT: 72,\n  FIRST_HOUR: 0,\n  LAST_HOUR: 24,\n  MIN_DURATION: 0.25,\n  TIME_COLUMN_WIDTH: 80,\n  ALL_DAY_HEIGHT: 28,\n\n  getLineColor: (color: string) => getLineColor(color),\n\n  getDynamicPadding: (drag: { endHour: number; startHour: number }) => {\n    const duration = drag.endHour - drag.startHour;\n    return duration <= 0.25 ? 'df-p-compact' : 'df-p-standard';\n  },\n};\n"
  },
  {
    "path": "packages/plugins/drag/src/utils/throttle.ts",
    "content": "/**\n * Creates a throttled function that only invokes the provided function at most once\n * per every wait milliseconds.\n */\nexport function throttle<T extends (...args: unknown[]) => unknown>(\n  func: T,\n  wait: number\n): T & { cancel: () => void } {\n  let timeout: ReturnType<typeof setTimeout> | null = null;\n  let previous = 0;\n\n  const throttled = function (this: unknown, ...args: Parameters<T>) {\n    const now = Date.now();\n    const remaining = wait - (now - previous);\n\n    if (remaining <= 0 || remaining > wait) {\n      if (timeout) {\n        clearTimeout(timeout);\n        timeout = null;\n      }\n      previous = now;\n      func.apply(this, args);\n    } else if (!timeout) {\n      timeout = setTimeout(() => {\n        previous = Date.now();\n        timeout = null;\n        func.apply(this, args);\n      }, remaining);\n    }\n  } as T & { cancel: () => void };\n\n  throttled.cancel = () => {\n    if (timeout) {\n      clearTimeout(timeout);\n      timeout = null;\n    }\n    previous = 0;\n  };\n\n  return throttled;\n}\n"
  },
  {
    "path": "packages/plugins/drag/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"declarationDir\": \"dist/types\",\n    \"declarationMap\": false,\n    \"sourceMap\": false,\n    \"module\": \"ESNext\",\n    \"target\": \"ES2015\",\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": false\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"**/*.test.ts\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.ts\",\n    \"**/*.spec.tsx\"\n  ]\n}\n"
  },
  {
    "path": "packages/plugins/drag/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"paths\": {\n      \"@drag/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/plugins/keyboard-shortcuts/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/plugins/keyboard-shortcuts/README.md",
    "content": "# DayFlow\n\n**English** | [中文](README.zh.md) | [日本語](README.ja.md) | [Getting Started & Contributing](CONTRIBUTING.md)\n\nA flexible and feature-rich calendar component library for **React, Vue, Angular, and Svelte** with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/core?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/core)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/dayflow/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/dayflow)](https://github.com/dayflow-js/dayflow/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/dayflow/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n\n---\n"
  },
  {
    "path": "packages/plugins/keyboard-shortcuts/package.json",
    "content": "{\n  \"name\": \"@dayflow/plugin-keyboard-shortcuts\",\n  \"version\": \"1.5.2\",\n  \"description\": \"Keyboard shortcuts plugin for DayFlow calendar\",\n  \"keywords\": [\n    \"calendar\",\n    \"dayflow\",\n    \"keyboard\",\n    \"plugin\",\n    \"shortcuts\"\n  ],\n  \"license\": \"MIT\",\n  \"author\": \"Jayce Li\",\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && rollup -c\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"postbuild\": \"rimraf dist/types\",\n    \"prebuild\": \"rimraf dist\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@dayflow/core\": \"workspace:*\",\n    \"@rollup/plugin-commonjs\": \"catalog:\",\n    \"@rollup/plugin-node-resolve\": \"catalog:\",\n    \"@rollup/plugin-typescript\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-dts\": \"catalog:\",\n    \"temporal-polyfill\": \"catalog:\",\n    \"typescript\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"@dayflow/core\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/keyboard-shortcuts/rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport typescript from '@rollup/plugin-typescript';\nimport { dts } from 'rollup-plugin-dts';\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: [\n      {\n        file: 'dist/index.js',\n        format: 'esm',\n        sourcemap: false,\n        exports: 'named',\n      },\n    ],\n    plugins: [\n      resolve(),\n      commonjs(),\n      typescript({\n        tsconfig: './tsconfig.build.json',\n      }),\n    ],\n    external: ['@dayflow/core', 'temporal-polyfill'],\n  },\n  {\n    input: 'dist/types/index.d.ts',\n    output: [{ file: 'dist/index.d.ts', format: 'es' }],\n    plugins: [dts()],\n    external: ['temporal-polyfill'],\n  },\n];\n"
  },
  {
    "path": "packages/plugins/keyboard-shortcuts/src/index.ts",
    "content": "export * from './plugin';\n"
  },
  {
    "path": "packages/plugins/keyboard-shortcuts/src/plugin.ts",
    "content": "import {\n  CalendarPlugin,\n  ICalendarApp,\n  Event,\n  ViewType,\n  clipboardStore,\n  generateUniKey,\n  temporalToDate,\n  dateToZonedDateTime,\n  dateToPlainDate,\n  getWeekRange,\n  temporalToVisualDate,\n} from '@dayflow/core';\n\nexport interface KeyboardShortcutsConfig {\n  /**\n   * Whether to enable default shortcuts\n   * @default true\n   */\n  enabled?: boolean;\n  /**\n   * Custom key mappings\n   */\n  keyMap?: {\n    today?: string;\n    search?: string;\n    prev?: string;\n    next?: string;\n    undo?: string;\n    redo?: string;\n    copy?: string;\n    cut?: string;\n    paste?: string;\n    delete?: string;\n    newEvent?: string;\n    edit?: string;\n    print?: string;\n  };\n  callbacks?: {\n    undo?: (app: ICalendarApp) => void | Promise<void>;\n    redo?: (app: ICalendarApp) => void | Promise<void>;\n    delete?: (app: ICalendarApp, event?: Event) => void | Promise<void>;\n    print?: (app: ICalendarApp) => void | Promise<void>;\n  };\n}\n\nfunction handleTabNavigation(app: ICalendarApp, reverse: boolean) {\n  const navigationState = (\n    app as ICalendarApp & {\n      __dayflowKeyboardState?: { lastNavigatedEventId: string | null };\n    }\n  ).__dayflowKeyboardState;\n  const events = app.getEvents();\n  const currentView = app.state.currentView;\n  const currentDate = app.getCurrentDate();\n  const appTimeZone = app.timeZone;\n\n  let visibleEvents: Event[] = [];\n\n  const getVisualRange = (event: Event) => {\n    const start = temporalToVisualDate(event.start, appTimeZone);\n    const end = event.end\n      ? temporalToVisualDate(event.end, appTimeZone)\n      : new Date(start);\n    return { start, end };\n  };\n\n  const overlapsRange = (\n    event: Event,\n    rangeStart: Date,\n    rangeEnd: Date\n  ): boolean => {\n    const { start, end } = getVisualRange(event);\n    return start < rangeEnd && end >= rangeStart;\n  };\n\n  switch (currentView) {\n    case ViewType.DAY: {\n      visibleEvents = events.filter(e => {\n        const dayStart = new Date(currentDate);\n        dayStart.setHours(0, 0, 0, 0);\n        const dayEnd = new Date(dayStart);\n        dayEnd.setDate(dayEnd.getDate() + 1);\n        return overlapsRange(e, dayStart, dayEnd);\n      });\n      break;\n    }\n    case ViewType.WEEK: {\n      const weekConfig = app.getViewConfig(ViewType.WEEK) as\n        | { startOfWeek?: number }\n        | undefined;\n      const startOfWeek = weekConfig?.startOfWeek ?? 1;\n      const { monday: start } = getWeekRange(currentDate, startOfWeek);\n      const end = new Date(start);\n      end.setDate(end.getDate() + 7);\n      visibleEvents = events.filter(e => overlapsRange(e, start, end));\n      break;\n    }\n    case ViewType.MONTH: {\n      const visibleMonth = app.getVisibleMonth();\n      const start = new Date(\n        visibleMonth.getFullYear(),\n        visibleMonth.getMonth(),\n        1\n      );\n      const end = new Date(\n        visibleMonth.getFullYear(),\n        visibleMonth.getMonth() + 1,\n        1\n      );\n      visibleEvents = events.filter(e => overlapsRange(e, start, end));\n      break;\n    }\n    case ViewType.YEAR: {\n      const year = currentDate.getFullYear();\n      const yearConfig = app.getViewConfig(ViewType.YEAR);\n      const showTimedEvents =\n        (yearConfig as { showTimedEventsInYearView?: boolean })\n          .showTimedEventsInYearView ?? false;\n      visibleEvents = events.filter(e => {\n        if (!showTimedEvents && !e.allDay) return false;\n        const rangeStart = new Date(year, 0, 1);\n        rangeStart.setHours(0, 0, 0, 0);\n        const rangeEnd = new Date(year + 1, 0, 1);\n        return overlapsRange(e, rangeStart, rangeEnd);\n      });\n      break;\n    }\n    default:\n      break;\n  }\n\n  visibleEvents.sort((a, b) => {\n    const dateA = temporalToVisualDate(a.start, appTimeZone);\n    const dateB = temporalToVisualDate(b.start, appTimeZone);\n    const timeDiff = dateA.getTime() - dateB.getTime();\n    if (timeDiff !== 0) return timeDiff;\n    if (a.allDay && !b.allDay) return -1;\n    if (!a.allDay && b.allDay) return 1;\n    return 0;\n  });\n\n  if (visibleEvents.length === 0) return;\n\n  let nextIndex = 0;\n  const selectedId =\n    app.state.selectedEventId ??\n    app.state.highlightedEventId ??\n    navigationState?.lastNavigatedEventId ??\n    null;\n  if (selectedId) {\n    const currentIndex = visibleEvents.findIndex(e => e.id === selectedId);\n    if (currentIndex !== -1) {\n      if (reverse) {\n        nextIndex = currentIndex - 1;\n        if (nextIndex < 0) nextIndex = visibleEvents.length - 1;\n      } else {\n        nextIndex = currentIndex + 1;\n        if (nextIndex >= visibleEvents.length) nextIndex = 0;\n      }\n    }\n  }\n\n  const nextEvent = visibleEvents[nextIndex];\n  if (nextEvent) {\n    if (navigationState) {\n      navigationState.lastNavigatedEventId = nextEvent.id;\n    }\n    app.selectEvent(nextEvent.id);\n    app.highlightEvent(nextEvent.id);\n  }\n}\n\nasync function handlePaste(app: ICalendarApp) {\n  try {\n    let eventData = clipboardStore.getEvent();\n    if (!eventData) {\n      const text = await navigator.clipboard.readText();\n      if (text) {\n        try {\n          eventData = JSON.parse(text);\n        } catch (err) {\n          console.error('Failed to parse clipboard text:', err);\n        }\n      }\n    }\n\n    if (\n      eventData &&\n      typeof eventData === 'object' &&\n      (eventData as { title?: string }).title\n    ) {\n      const originalStart = temporalToDate(eventData.start as unknown as Date);\n      const originalEnd = temporalToDate(eventData.end as unknown as Date);\n      const duration = originalEnd.getTime() - originalStart.getTime();\n\n      let targetStart = new Date();\n      const selectedId = app.state.selectedEventId;\n\n      if (selectedId) {\n        const selectedEvent = app.getEvents().find(e => e.id === selectedId);\n        if (selectedEvent) {\n          targetStart = temporalToDate(selectedEvent.start);\n        } else {\n          targetStart = new Date(app.getCurrentDate());\n        }\n      } else {\n        targetStart = new Date(app.getCurrentDate());\n      }\n\n      targetStart.setHours(\n        originalStart.getHours(),\n        originalStart.getMinutes(),\n        originalStart.getSeconds(),\n        0\n      );\n\n      const targetEnd = new Date(\n        targetStart.getTime() + (duration > 0 ? duration : 3600000)\n      );\n      const cleanEventData = eventData;\n\n      const newEvent: Event = {\n        ...cleanEventData,\n        id: generateUniKey(),\n        start: eventData.allDay\n          ? dateToPlainDate(targetStart)\n          : dateToZonedDateTime(targetStart, app.timeZone),\n        end: eventData.allDay\n          ? dateToPlainDate(targetEnd)\n          : dateToZonedDateTime(targetEnd, app.timeZone),\n        calendarId:\n          eventData.calendarId &&\n          app.getCalendarRegistry().has(eventData.calendarId)\n            ? eventData.calendarId\n            : app.getCalendarRegistry().getDefaultCalendarId() || 'default',\n      };\n\n      app.addEvent(newEvent);\n      app.selectEvent(newEvent.id);\n      app.highlightEvent(newEvent.id);\n    }\n  } catch (err) {\n    console.error('Failed to paste', err);\n  }\n}\n\n/**\n * Programmatic API returned by `app.getPlugin<KeyboardShortcutsService>('keyboard-shortcuts')`.\n *\n * @example\n * const kb = app.getPlugin<KeyboardShortcutsService>('keyboard-shortcuts');\n * kb?.disable(); // suppress shortcuts while a custom modal is open\n * kb?.enable();  // restore when the modal closes\n */\nexport interface KeyboardShortcutsService {\n  /** Resume handling keyboard shortcuts. */\n  enable: () => void;\n  /** Suppress all keyboard shortcuts until re-enabled. */\n  disable: () => void;\n  /** Returns whether shortcuts are currently active. */\n  isEnabled: () => boolean;\n}\n\nexport function createKeyboardShortcutsPlugin(\n  config: KeyboardShortcutsConfig = {}\n): CalendarPlugin {\n  const { enabled = true, keyMap = {}, callbacks = {} } = config;\n\n  const state = { enabled };\n\n  const keyboardService: KeyboardShortcutsService = {\n    enable: () => {\n      state.enabled = true;\n    },\n    disable: () => {\n      state.enabled = false;\n    },\n    isEnabled: () => state.enabled,\n  };\n\n  return {\n    name: 'keyboard-shortcuts',\n    api: keyboardService,\n    install(app: ICalendarApp) {\n      if (!enabled) return;\n\n      const keyboardApp = app as ICalendarApp & {\n        __dayflowKeyboardState?: { lastNavigatedEventId: string | null };\n      };\n      keyboardApp.__dayflowKeyboardState ??= { lastNavigatedEventId: null };\n\n      const handleKeyDown = async (e: KeyboardEvent) => {\n        if (!state.enabled) return;\n\n        const activeElement = document.activeElement;\n        const isTyping =\n          activeElement &&\n          (activeElement.tagName === 'INPUT' ||\n            activeElement.tagName === 'TEXTAREA' ||\n            (activeElement as HTMLElement).isContentEditable);\n\n        // 1. Search (Cmd/Ctrl + F)\n        const searchKey = keyMap.search || 'f';\n        if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === searchKey) {\n          e.preventDefault();\n          const searchInput = document.querySelector(\n            '#dayflow-search-input'\n          ) as HTMLElement | null;\n          if (searchInput) {\n            searchInput.focus();\n          }\n          return;\n        }\n\n        // 2. Print (Cmd/Ctrl + P) — only intercept when a custom callback is provided;\n        // otherwise the print plugin handles its own Cmd+P listener\n        const printKey = keyMap.print || 'p';\n        if (\n          callbacks.print &&\n          (e.metaKey || e.ctrlKey) &&\n          e.key.toLowerCase() === printKey\n        ) {\n          e.preventDefault();\n          await callbacks.print(app);\n          return;\n        }\n\n        // 3. Today (Cmd/Ctrl + T)\n        const todayKey = keyMap.today || 't';\n        if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === todayKey) {\n          e.preventDefault();\n          app.goToToday();\n          return;\n        }\n\n        // 3. Quick Create (Cmd/Ctrl + N)\n        const newEventKey = keyMap.newEvent || 'n';\n        if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === newEventKey) {\n          e.preventDefault();\n          const addBtn = document.querySelector(\n            '#dayflow-add-event-btn'\n          ) as HTMLElement | null;\n          if (addBtn) {\n            addBtn.click();\n          }\n          return;\n        }\n\n        // 4. Dismiss (Esc)\n        if (e.key === 'Escape') {\n          app.dismissUI();\n          return;\n        }\n\n        // Navigation (Left/Right) - only if not typing\n        if (!isTyping) {\n          const prevKey = keyMap.prev || 'ArrowLeft';\n          const nextKey = keyMap.next || 'ArrowRight';\n\n          if (e.key === prevKey) {\n            e.preventDefault();\n            app.goToPrevious();\n            return;\n          }\n          if (e.key === nextKey) {\n            e.preventDefault();\n            app.goToNext();\n            return;\n          }\n        }\n\n        // 5. Tab Navigation\n        if (e.key === 'Tab') {\n          e.preventDefault();\n          handleTabNavigation(app, e.shiftKey);\n          return;\n        }\n\n        // 6. Clipboard & Undo/Redo Operations (Cmd/Ctrl + C/X/V/Z/Y)\n        if ((e.metaKey || e.ctrlKey) && !isTyping) {\n          const undoKey = keyMap.undo || 'z';\n          const redoKey = keyMap.redo || 'y';\n          const copyKey = keyMap.copy || 'c';\n          const cutKey = keyMap.cut || 'x';\n          const pasteKey = keyMap.paste || 'v';\n\n          switch (e.key.toLowerCase()) {\n            case undoKey: {\n              if (e.shiftKey) {\n                // Cmd/Ctrl + Shift + Z → redo\n                e.preventDefault();\n                if (callbacks.redo) {\n                  await callbacks.redo(app);\n                } else {\n                  (app as ICalendarApp & { redo?: () => void }).redo?.();\n                }\n                break;\n              }\n              e.preventDefault();\n              if (callbacks.undo) {\n                await callbacks.undo(app);\n              } else {\n                app.undo();\n              }\n              break;\n            }\n            case redoKey: {\n              // Cmd/Ctrl + Y → redo\n              e.preventDefault();\n              if (callbacks.redo) {\n                await callbacks.redo(app);\n              } else {\n                (app as ICalendarApp & { redo?: () => void }).redo?.();\n              }\n              break;\n            }\n            case copyKey: {\n              const selectedIdC = app.state.selectedEventId;\n              if (selectedIdC) {\n                const event = app.getEvents().find(ev => ev.id === selectedIdC);\n                if (event) {\n                  try {\n                    await navigator.clipboard.writeText(\n                      JSON.stringify(event, null, 2)\n                    );\n                    clipboardStore.setEvent(event);\n                  } catch (err) {\n                    console.error('Failed to copy event', err);\n                  }\n                }\n              }\n              break;\n            }\n            case cutKey: {\n              const selectedIdX = app.state.selectedEventId;\n              if (selectedIdX) {\n                const event = app.getEvents().find(ev => ev.id === selectedIdX);\n                if (event) {\n                  try {\n                    await navigator.clipboard.writeText(\n                      JSON.stringify(event, null, 2)\n                    );\n                    clipboardStore.setEvent(event);\n                    app.deleteEvent(event.id);\n                    app.selectEvent(null);\n                  } catch (err) {\n                    console.error('Failed to cut event', err);\n                  }\n                }\n              }\n              break;\n            }\n            case pasteKey: {\n              handlePaste(app);\n              break;\n            }\n            default:\n              break;\n          }\n        }\n\n        // 7. Delete (Backspace/Delete)\n        const deleteKey = keyMap.delete || 'Delete';\n        if (e.key === 'Backspace' || e.key === deleteKey) {\n          if (isTyping) return;\n\n          const selectedIdD = app.state.selectedEventId;\n          if (selectedIdD) {\n            if (callbacks.delete) {\n              const event = app.getEvents().find(ev => ev.id === selectedIdD);\n              await callbacks.delete(app, event);\n            } else {\n              app.deleteEvent(selectedIdD);\n              app.selectEvent(null);\n            }\n          }\n        }\n\n        // 8. Edit (Enter)\n        const editKey = keyMap.edit || 'Enter';\n        if (e.key === editKey) {\n          if (isTyping) {\n            console.log('[DayFlow Keyboard] Enter ignored: user is typing');\n            return;\n          }\n\n          const selectedIdE = app.state.selectedEventId;\n          console.log(\n            '[DayFlow Keyboard] Enter pressed, selectedId:',\n            selectedIdE\n          );\n          if (selectedIdE) {\n            e.preventDefault();\n            e.stopPropagation();\n            app.onEventDetailToggle(selectedIdE);\n          }\n        }\n      };\n      if (typeof window !== 'undefined') {\n        window.addEventListener('keydown', handleKeyDown);\n      }\n\n      // Cleanup is tricky for plugins as there's no uninstall yet in DayFlow core,\n      // but we can store it or just let it live with the app instance.\n    },\n  };\n}\n"
  },
  {
    "path": "packages/plugins/keyboard-shortcuts/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"declarationDir\": \"dist/types\",\n    \"declarationMap\": false,\n    \"sourceMap\": false,\n    \"module\": \"ESNext\",\n    \"target\": \"ES2015\",\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": false\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"**/*.test.ts\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.ts\",\n    \"**/*.spec.tsx\"\n  ]\n}\n"
  },
  {
    "path": "packages/plugins/keyboard-shortcuts/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"declaration\": true,\n    \"declarationMap\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/plugins/localization/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/plugins/localization/README.md",
    "content": "# DayFlow\n\n**English** | [中文](README.zh.md) | [日本語](README.ja.md) | [Getting Started & Contributing](CONTRIBUTING.md)\n\nA flexible and feature-rich calendar component library for **React, Vue, Angular, and Svelte** with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/core?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/core)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/dayflow/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/dayflow)](https://github.com/dayflow-js/dayflow/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/dayflow/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n\n---\n"
  },
  {
    "path": "packages/plugins/localization/package.json",
    "content": "{\n  \"name\": \"@dayflow/plugin-localization\",\n  \"version\": \"1.5.2\",\n  \"description\": \"Localization plugin for DayFlow calendar, providing support for multiple languages\",\n  \"keywords\": [\n    \"calendar\",\n    \"dayflow\",\n    \"i18n\",\n    \"localization\",\n    \"plugin\"\n  ],\n  \"license\": \"MIT\",\n  \"author\": \"Jayce Li\",\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.build.json && rollup -c\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"postbuild\": \"rimraf dist/types\",\n    \"prebuild\": \"rimraf dist\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@dayflow/core\": \"workspace:*\",\n    \"@rollup/plugin-commonjs\": \"catalog:\",\n    \"@rollup/plugin-node-resolve\": \"catalog:\",\n    \"@rollup/plugin-typescript\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-dts\": \"catalog:\",\n    \"typescript\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"@dayflow/core\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/localization/rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport typescript from '@rollup/plugin-typescript';\nimport { dts } from 'rollup-plugin-dts';\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: [\n      {\n        file: 'dist/index.js',\n        format: 'esm',\n        sourcemap: false,\n        exports: 'named',\n      },\n    ],\n    plugins: [\n      resolve(),\n      commonjs(),\n      typescript({\n        tsconfig: './tsconfig.build.json',\n      }),\n    ],\n    external: ['@dayflow/core'],\n  },\n  {\n    input: 'dist/types/index.d.ts',\n    output: [{ file: 'dist/index.d.ts', format: 'es' }],\n    plugins: [dts()],\n  },\n];\n"
  },
  {
    "path": "packages/plugins/localization/src/index.ts",
    "content": "export * from './locales';\nexport * from './plugin';\n"
  },
  {
    "path": "packages/plugins/localization/src/locales/de.ts",
    "content": "import type { DayflowLocale } from './types';\n\nconst de: DayflowLocale = {\n  code: 'de-DE',\n  messages: {\n    allDay: 'Ganztägig',\n    noEvents: 'Heute keine Termine',\n    more: 'mehr',\n    eventTitle: 'Ereignistitel',\n    dateRange: 'Datumsbereich',\n    timeRange: 'Zeitbereich',\n    note: 'Notiz',\n    addNotePlaceholder: 'Notiz hinzufügen...',\n    setAsAllDay: 'Als ganztägig festlegen',\n    setAsTimed: 'Als zeitlich begrenztes Ereignis festlegen',\n    delete: 'Löschen',\n    confirm: 'Bestätigen',\n    cancel: 'Abbrechen',\n    today: 'Heute',\n    day: 'Tag',\n    week: 'Woche',\n    month: 'Monat',\n    year: 'Jahr',\n    newEvent: 'Neues Ereignis',\n    newAllDayEvent: 'Neues ganztägiges Ereignis',\n    newCalendarEvent: 'Neues Ereignis in {calendarName}',\n    newAllDayCalendarEvent: 'Neues ganztägiges Ereignis in {calendarName}',\n    save: 'Speichern',\n    deleteCalendar: '{calendarName} löschen?',\n    deleteCalendarMessage:\n      'Möchten Sie {calendarName} löschen oder dessen Ereignisse in einen anderen bestehenden Kalender zusammenführen?',\n    merge: 'Zusammenführen',\n    confirmDeleteTitle:\n      'Sind Sie sicher, dass Sie den Kalender {calendarName} löschen möchten?',\n    confirmDeleteMessage:\n      'Wenn Sie diesen Kalender löschen, werden auch alle damit verbundenen Ereignisse gelöscht.',\n    mergeConfirmTitle: '{sourceName} mit {targetName} zusammenführen?',\n    mergeConfirmMessage:\n      'Sind Sie sicher, dass Sie {sourceName} mit {targetName} zusammenführen möchten?\\nDadurch werden alle Ereignisse von {sourceName} nach {targetName} verschoben und {sourceName} wird gelöscht.\\nDies kann nicht rückgängig gemacht werden.',\n    expandSidebar: 'Kalender-Seitenleiste erweitern',\n    collapseSidebar: 'Kalender-Seitenleiste einklappen',\n    calendars: 'Kalender',\n    createCalendar: 'Neuen Kalender erstellen',\n    calendarNamePlaceholder: 'z.B. Arbeit',\n    customColor: 'Benutzerdefinierte Farbe...',\n    create: 'Erstellen',\n    calendarOptions: 'Kalenderoptionen',\n    untitled: 'Unbenannt',\n    search: 'Suchen',\n    noResults: 'Keine Ergebnisse gefunden',\n    calendar: 'Kalender',\n    starts: 'Beginnt',\n    ends: 'Endet',\n    notes: 'Notizen',\n    titlePlaceholder: 'Titel',\n    notesPlaceholder: 'Notizen',\n    editEvent: 'Ereignis bearbeiten',\n    done: 'Fertig',\n    quickCreateEvent: 'Ereignis schnell erstellen',\n    quickCreatePlaceholder: 'Film am Freitag um 19 Uhr',\n    noSuggestions: 'Tippen zum Erstellen',\n    newCalendar: 'Neuer Kalender',\n    refreshAll: 'Alles aktualisieren',\n    tomorrow: 'Morgen',\n    importCalendar: 'Kalender importieren',\n    exportCalendar: 'Kalender exportieren',\n    addSchedule: 'Termin hinzufügen',\n    importCalendarMessage:\n      'Dieser Kalender enthält neue Termine. Bitte wählen Sie einen Zielkalender.',\n    ok: 'OK',\n    cut: 'Ausschneiden',\n    copy: 'Kopieren',\n    pasteHere: 'Hier einfügen',\n    eventSummary: 'Zusammenfassung',\n    viewEvent: 'Ereignis anzeigen',\n    subscribeCalendar: 'Kalender abonnieren',\n    subscribeCalendarTitle:\n      'Geben Sie die URL des Kalenders ein, den Sie abonnieren möchten.',\n    calendarUrl: 'Kalender-URL',\n    calendarUrlPlaceholder: 'https://example.com/calendar.ics',\n    subscribe: 'Abonnieren',\n    fetchingCalendar: 'Kalender wird abgerufen...',\n    subscribeError:\n      'Kalender konnte nicht abgerufen werden. Bitte überprüfen Sie die URL und versuchen Sie es erneut.',\n  },\n  packages: {\n    scheduler: {\n      previousPeriod: 'Vorheriger Zeitraum',\n      nextPeriod: 'Nächster Zeitraum',\n      quarter: 'Quartal',\n      days: 'Tage',\n      weeks: 'Wochen',\n      months: 'Monate',\n      quarters: 'Quartale',\n      years: 'Jahre',\n      views: 'Ansichten',\n      resources: 'Ressourcen',\n      resizePanel: 'Größe des Panels ändern',\n      searchPlaceholder: 'Suchen…',\n      noResults: 'Keine Termine gefunden',\n      typeToSearch: 'Eingabe zum Suchen',\n      openSearch: 'Suche öffnen',\n      closeSearch: 'Suche schließen',\n      clearSearch: 'Suche leeren',\n      close: 'Schließen',\n      title: 'Titel',\n      untitledEvent: 'Unbenanntes Ereignis',\n      type: 'Typ',\n      addNotePlaceholder: 'Notiz hinzufügen',\n      timeRange: 'Zeitraum',\n      milestone: 'Meilenstein',\n      typeFixedHint: 'Der Typ ist in diesem Showcase-Beispiel festgelegt.',\n      progressFill: 'Fortschrittsfüllung',\n      progressBar: 'Fortschrittsbalken',\n      sample: 'Beispiel',\n      titleAbove: 'Titel oben',\n      titleCenter: 'Titel mittig',\n      titleBelow: 'Titel unten',\n      filled: 'Gefüllt',\n      bordered: 'Umrandet',\n      outlined: 'Konturiert',\n      dashed: 'Gestrichelt',\n      line: 'Linie',\n      dashedLine: 'Gestrichelte Linie',\n      indented: 'Eingerückt',\n      rounded: 'Abgerundet',\n      gradient: 'Verlauf',\n      progress: 'Fortschritt',\n      pattern: 'Muster',\n      flag: 'Flagge',\n      diamond: 'Diamant',\n      mergeWith: 'Zusammenführen mit…',\n      moveEventsTo: 'Termine verschieben nach…',\n      areYouSure: 'Sind Sie sicher?',\n      cannotBeUndone: 'Diese Aktion kann nicht rückgängig gemacht werden.',\n    },\n  },\n};\n\nexport default de;\n"
  },
  {
    "path": "packages/plugins/localization/src/locales/es.ts",
    "content": "import type { DayflowLocale } from './types';\n\nconst es: DayflowLocale = {\n  code: 'es-ES',\n  messages: {\n    allDay: 'Todo el día',\n    noEvents: 'No hay eventos hoy',\n    more: 'más',\n    eventTitle: 'Título del evento',\n    dateRange: 'Rango de fechas',\n    timeRange: 'Rango de tiempo',\n    note: 'Nota',\n    addNotePlaceholder: 'Añadir una nota...',\n    setAsAllDay: 'Establecer como todo el día',\n    setAsTimed: 'Establecer como evento con horario',\n    delete: 'Eliminar',\n    confirm: 'Confirmar',\n    cancel: 'Cancelar',\n    today: 'Hoy',\n    day: 'Día',\n    week: 'Semana',\n    month: 'Mes',\n    year: 'Año',\n    newEvent: 'Nuevo evento',\n    newAllDayEvent: 'Nuevo evento de todo el día',\n    newCalendarEvent: 'Nuevo evento en {calendarName}',\n    newAllDayCalendarEvent: 'Nuevo evento de todo el día en {calendarName}',\n    save: 'Guardar',\n    deleteCalendar: '¿Eliminar {calendarName}?',\n    deleteCalendarMessage:\n      '¿Quieres eliminar {calendarName} o fusionar sus eventos en otro calendario existente?',\n    merge: 'Fusionar',\n    confirmDeleteTitle:\n      '¿Seguro que quieres eliminar el calendario {calendarName}?',\n    confirmDeleteMessage:\n      'Si eliminas este calendario, también se eliminarán todos los eventos asociados.',\n    mergeConfirmTitle: '¿Fusionar {sourceName} con {targetName}?',\n    mergeConfirmMessage:\n      '¿Seguro que quieres fusionar {sourceName} con {targetName}?\\nEsto moverá todos los eventos de {sourceName} a {targetName} y se eliminará {sourceName}.\\nEsta acción no se puede deshacer.',\n    expandSidebar: 'Expandir barra lateral del calendario',\n    collapseSidebar: 'Contraer barra lateral del calendario',\n    calendars: 'Calendarios',\n    createCalendar: 'Crear nuevo calendario',\n    calendarNamePlaceholder: 'ej. Trabajo',\n    customColor: 'Color personalizado...',\n    create: 'Crear',\n    calendarOptions: 'Opciones de calendario',\n    untitled: 'Sin título',\n    search: 'Buscar',\n    noResults: 'No se encontraron resultados',\n    calendar: 'Calendario',\n    starts: 'Empieza',\n    ends: 'Termina',\n    notes: 'Notas',\n    titlePlaceholder: 'Título',\n    notesPlaceholder: 'Notas',\n    editEvent: 'Editar evento',\n    done: 'Hecho',\n    quickCreateEvent: 'Creación rápida de eventos',\n    quickCreatePlaceholder: 'Película a las 7pm el viernes',\n    noSuggestions: 'Escribe para crear',\n    newCalendar: 'Nuevo calendario',\n    refreshAll: 'Actualizar todo',\n    tomorrow: 'Mañana',\n    importCalendar: 'Importar calendario',\n    exportCalendar: 'Exportar calendario',\n    addSchedule: 'Añadir evento',\n    importCalendarMessage:\n      'Este calendario contiene nuevos eventos. Seleccione un calendario de destino.',\n    ok: 'Aceptar',\n    cut: 'Cortar',\n    copy: 'Copiar',\n    pasteHere: 'Pegar aquí',\n    eventSummary: 'Resumen',\n    viewEvent: 'Ver evento',\n    subscribeCalendar: 'Suscribirse a un calendario',\n    subscribeCalendarTitle:\n      'Introduce la URL del calendario al que quieres suscribirte.',\n    calendarUrl: 'URL del calendario',\n    calendarUrlPlaceholder: 'https://example.com/calendar.ics',\n    subscribe: 'Suscribirse',\n    fetchingCalendar: 'Obteniendo calendario...',\n    subscribeError:\n      'No se pudo obtener el calendario. Comprueba la URL e inténtalo de nuevo.',\n  },\n  packages: {\n    scheduler: {\n      previousPeriod: 'Periodo anterior',\n      nextPeriod: 'Siguiente periodo',\n      quarter: 'Trimestre',\n      days: 'Días',\n      weeks: 'Semanas',\n      months: 'Meses',\n      quarters: 'Trimestres',\n      years: 'Años',\n      views: 'Vistas',\n      resources: 'Recursos',\n      resizePanel: 'Cambiar tamaño del panel',\n      searchPlaceholder: 'Buscar…',\n      noResults: 'No se encontraron eventos',\n      typeToSearch: 'Escribe para buscar',\n      openSearch: 'Abrir búsqueda',\n      closeSearch: 'Cerrar búsqueda',\n      clearSearch: 'Borrar búsqueda',\n      close: 'Cerrar',\n      title: 'Título',\n      untitledEvent: 'Evento sin título',\n      type: 'Tipo',\n      addNotePlaceholder: 'Añadir una nota',\n      milestone: 'Hito',\n      typeFixedHint: 'El tipo está fijado en este ejemplo de muestra.',\n      progressFill: 'Relleno de progreso',\n      progressBar: 'Barra de progreso',\n      sample: 'Ejemplo',\n      titleAbove: 'Título arriba',\n      titleCenter: 'Título centrado',\n      titleBelow: 'Título abajo',\n      filled: 'Relleno',\n      bordered: 'Bordeado',\n      outlined: 'Contorno',\n      dashed: 'Discontinuo',\n      line: 'Línea',\n      dashedLine: 'Línea discontinua',\n      indented: 'Sangrado',\n      rounded: 'Redondeado',\n      gradient: 'Degradado',\n      progress: 'Progreso',\n      pattern: 'Patrón',\n      flag: 'Bandera',\n      diamond: 'Diamante',\n      mergeWith: 'Fusionar con…',\n      moveEventsTo: 'Mover eventos a…',\n      areYouSure: '¿Estás seguro?',\n      cannotBeUndone: 'Esta acción no se puede deshacer.',\n    },\n  },\n};\n\nexport default es;\n"
  },
  {
    "path": "packages/plugins/localization/src/locales/fr.ts",
    "content": "import type { DayflowLocale } from './types';\n\nconst fr: DayflowLocale = {\n  code: 'fr-FR',\n  messages: {\n    allDay: 'Toute la journée',\n    noEvents: \"Aucun événement aujourd'hui\",\n    more: 'de plus',\n    eventTitle: \"Titre de l'événement\",\n    dateRange: 'Plage de dates',\n    timeRange: 'Plage horaire',\n    note: 'Note',\n    addNotePlaceholder: 'Ajouter une note...',\n    setAsAllDay: 'Définir comme toute la journée',\n    setAsTimed: 'Définir comme événement horaire',\n    delete: 'Supprimer',\n    confirm: 'Confirmer',\n    cancel: 'Annuler',\n    today: \"Aujourd'hui\",\n    day: 'Jour',\n    week: 'Semaine',\n    month: 'Mois',\n    year: 'Année',\n    newEvent: 'Nouvel événement',\n    newAllDayEvent: 'Nouvel événement toute la journée',\n    newCalendarEvent: 'Nouvel événement dans {calendarName}',\n    newAllDayCalendarEvent:\n      'Nouvel événement toute la journée dans {calendarName}',\n    save: 'Enregistrer',\n    deleteCalendar: 'Supprimer {calendarName} ?',\n    deleteCalendarMessage:\n      'Voulez-vous supprimer {calendarName} ou fusionner ses événements dans un autre calendrier existant ?',\n    merge: 'Fusionner',\n    confirmDeleteTitle:\n      'Êtes-vous sûr de vouloir supprimer le calendrier {calendarName} ?',\n    confirmDeleteMessage:\n      'Si vous supprimez ce calendrier, tous les événements associés seront également supprimés.',\n    mergeConfirmTitle: 'Fusionner {sourceName} avec {targetName} ?',\n    mergeConfirmMessage:\n      'Êtes-vous sûr de vouloir fusionner {sourceName} avec {targetName} ?\\nCela déplacera tous les événements de {sourceName} vers {targetName} et {sourceName} sera supprimé.\\nCette action est irréversible.',\n    expandSidebar: 'Développer la barre latérale',\n    collapseSidebar: 'Réduire la barre latérale',\n    calendars: 'Calendriers',\n    createCalendar: 'Créer un nouveau calendrier',\n    calendarNamePlaceholder: 'ex. Travail',\n    customColor: 'Couleur personnalisée...',\n    create: 'Créer',\n    calendarOptions: 'Options du calendrier',\n    untitled: 'Sans titre',\n    search: 'Rechercher',\n    noResults: 'Aucun résultat trouvé',\n    calendar: 'Calendrier',\n    starts: 'Début',\n    ends: 'Fin',\n    notes: 'Notes',\n    titlePlaceholder: 'Titre',\n    notesPlaceholder: 'Notes',\n    editEvent: \"Modifier l'événement\",\n    done: 'Terminé',\n    quickCreateEvent: 'Création rapide',\n    quickCreatePlaceholder: 'Film à 19h vendredi',\n    noSuggestions: 'Tapez pour créer',\n    newCalendar: 'Nouveau calendrier',\n    refreshAll: 'Tout actualiser',\n    tomorrow: 'Demain',\n    importCalendar: 'Importer un calendrier',\n    exportCalendar: 'Exporter le calendrier',\n    addSchedule: 'Ajouter un événement',\n    importCalendarMessage:\n      'Ce calendrier contient de nouveaux événements. Veuillez sélectionner un calendrier cible.',\n    ok: 'OK',\n    cut: 'Couper',\n    copy: 'Copier',\n    pasteHere: 'Coller ici',\n    eventSummary: 'Résumé',\n    viewEvent: \"Voir l'événement\",\n    subscribeCalendar: \"S'abonner à un calendrier\",\n    subscribeCalendarTitle:\n      \"Entrez l'URL du calendrier auquel vous souhaitez vous abonner.\",\n    calendarUrl: 'URL du calendrier',\n    calendarUrlPlaceholder: 'https://example.com/calendar.ics',\n    subscribe: \"S'abonner\",\n    fetchingCalendar: 'Récupération du calendrier...',\n    subscribeError:\n      \"Impossible de récupérer le calendrier. Vérifiez l'URL et réessayez.\",\n  },\n  packages: {\n    scheduler: {\n      previousPeriod: 'Période précédente',\n      nextPeriod: 'Période suivante',\n      quarter: 'Trimestre',\n      days: 'Jours',\n      weeks: 'Semaines',\n      months: 'Mois',\n      quarters: 'Trimestres',\n      years: 'Années',\n      views: 'Vues',\n      resources: 'Ressources',\n      resizePanel: 'Redimensionner le panneau',\n      searchPlaceholder: 'Rechercher…',\n      noResults: 'Aucun événement trouvé',\n      typeToSearch: 'Tapez pour rechercher',\n      openSearch: 'Ouvrir la recherche',\n      closeSearch: 'Fermer la recherche',\n      clearSearch: 'Effacer la recherche',\n      close: 'Fermer',\n      title: 'Titre',\n      untitledEvent: 'Événement sans titre',\n      type: 'Type',\n      addNotePlaceholder: 'Ajouter une note',\n      milestone: 'Jalon',\n      typeFixedHint: 'Le type est fixé dans cet exemple de démonstration.',\n      progressFill: 'Remplissage de progression',\n      progressBar: 'Barre de progression',\n      sample: 'Exemple',\n      titleAbove: 'Titre au-dessus',\n      titleCenter: 'Titre centré',\n      titleBelow: 'Titre en dessous',\n      filled: 'Rempli',\n      bordered: 'Bordé',\n      outlined: 'Contour',\n      dashed: 'Pointillé',\n      line: 'Ligne',\n      dashedLine: 'Ligne pointillée',\n      indented: 'Indenté',\n      rounded: 'Arrondi',\n      gradient: 'Dégradé',\n      progress: 'Progression',\n      pattern: 'Motif',\n      flag: 'Drapeau',\n      diamond: 'Diamant',\n      mergeWith: 'Fusionner avec…',\n      moveEventsTo: 'Déplacer les événements vers…',\n      areYouSure: 'Êtes-vous sûr ?',\n      cannotBeUndone: 'Cette action est irréversible.',\n    },\n  },\n};\n\nexport default fr;\n"
  },
  {
    "path": "packages/plugins/localization/src/locales/index.ts",
    "content": "import de from './de';\nimport es from './es';\nimport fr from './fr';\nimport ja from './ja';\nimport ko from './ko';\nimport zh from './zh';\n\nexport * from './types';\nexport { zh, ja, ko, fr, de, es };\n\nexport const LOCALES = {\n  zh,\n  ja,\n  ko,\n  fr,\n  de,\n  es,\n};\n"
  },
  {
    "path": "packages/plugins/localization/src/locales/ja.ts",
    "content": "import type { DayflowLocale } from './types';\n\nconst ja: DayflowLocale = {\n  code: 'ja-JP',\n  messages: {\n    allDay: '終日',\n    noEvents: '本日は予定がありません',\n    more: '件',\n    eventTitle: '予定名',\n    dateRange: '日付範囲',\n    timeRange: '時間範囲',\n    note: 'メモ',\n    addNotePlaceholder: 'メモを追加...',\n    setAsAllDay: '終日予定に設定',\n    setAsTimed: '時間指定の予定に設定',\n    delete: '削除',\n    confirm: '確認',\n    cancel: 'キャンセル',\n    today: '今日',\n    day: '日',\n    week: '週',\n    month: '月',\n    year: '年',\n    newEvent: '新規予定',\n    newAllDayEvent: '新規終日予定',\n    newCalendarEvent: '新規 {calendarName} 予定',\n    newAllDayCalendarEvent: '新規 {calendarName} 終日予定',\n    save: '保存',\n    deleteCalendar: 'カレンダー {calendarName} を削除しますか？',\n    deleteCalendarMessage:\n      'カレンダー {calendarName} を削除しますか？それともイベントを別の既存のカレンダーにマージしますか？',\n    merge: 'マージ',\n    confirmDeleteTitle:\n      'カレンダー {calendarName} を削除してもよろしいですか？',\n    confirmDeleteMessage:\n      'このカレンダーを削除すると、関連するすべてのイベントも削除されます。',\n    mergeConfirmTitle: '{sourceName} を {targetName} にマージしますか？',\n    mergeConfirmMessage:\n      '{sourceName} を {targetName} にマージしてもよろしいですか？\\nこれにより、{sourceName} のすべてのイベントが {targetName} に移動し、{sourceName} は削除されます。\\nこの操作は元に戻せません。',\n    expandSidebar: 'カレンダーサイドバーを展開',\n    collapseSidebar: 'カレンダーサイドバーを折りたたむ',\n    calendars: 'カレンダー',\n    createCalendar: '新しいカレンダーを作成',\n    calendarNamePlaceholder: '例：仕事',\n    customColor: 'カスタムカラー...',\n    create: '作成',\n    calendarOptions: 'カレンダーオプション',\n    untitled: '無題',\n    search: '検索',\n    noResults: '結果が見つかりません',\n    calendar: 'カレンダー',\n    starts: '開始',\n    ends: '終了',\n    notes: 'メモ',\n    titlePlaceholder: '予定名',\n    notesPlaceholder: 'メモ',\n    editEvent: '予定を編集',\n    done: '完了',\n    quickCreateEvent: 'クイック作成',\n    quickCreatePlaceholder: '金曜日7時から映画',\n    noSuggestions: '入力して作成',\n    newCalendar: '新しいカレンダー',\n    refreshAll: 'すべて更新',\n    tomorrow: '明日',\n    importCalendar: 'カレンダーをインポート',\n    exportCalendar: 'カレンダーをエクスポート',\n    addSchedule: '予定を追加',\n    importCalendarMessage:\n      'このカレンダーには新しい予定が含まれています。ターゲットカレンダーを選択してください。',\n    ok: 'OK',\n    cut: '切り取り',\n    copy: 'コピー',\n    pasteHere: 'ここに貼り付け',\n    eventSummary: '概要',\n    viewEvent: '予定を表示',\n    subscribeCalendar: 'カレンダーを登録',\n    subscribeCalendarTitle: '登録するカレンダーのURLを入力してください。',\n    calendarUrl: 'カレンダーURL',\n    calendarUrlPlaceholder: 'https://example.com/calendar.ics',\n    subscribe: '登録',\n    fetchingCalendar: 'カレンダーを取得中...',\n    subscribeError:\n      'カレンダーの取得に失敗しました。URLを確認して再試行してください。',\n  },\n  packages: {\n    scheduler: {\n      previousPeriod: '前の期間',\n      nextPeriod: '次の期間',\n      quarter: '四半期',\n      days: '日',\n      weeks: '週',\n      months: '月',\n      quarters: '四半期',\n      years: '年',\n      views: 'ビュー',\n      resources: 'リソース',\n      resizePanel: 'パネルのサイズを変更',\n      searchPlaceholder: '検索…',\n      noResults: '予定が見つかりません',\n      typeToSearch: '入力して検索',\n      openSearch: '検索を開く',\n      closeSearch: '検索を閉じる',\n      clearSearch: '検索をクリア',\n      close: '閉じる',\n      title: 'タイトル',\n      untitledEvent: '無題の予定',\n      type: '種類',\n      addNotePlaceholder: 'メモを追加',\n      milestone: 'マイルストーン',\n      typeFixedHint: 'このショーケースサンプルでは種類が固定されています。',\n      progressFill: '進捗塗り',\n      progressBar: '進捗バー',\n      sample: 'サンプル',\n      titleAbove: 'タイトル上',\n      titleCenter: 'タイトル中央',\n      titleBelow: 'タイトル下',\n      filled: '塗りつぶし',\n      bordered: '枠線',\n      outlined: 'アウトライン',\n      dashed: '破線',\n      line: 'ライン',\n      dashedLine: '破線ライン',\n      indented: 'インデント',\n      rounded: '角丸',\n      gradient: 'グラデーション',\n      progress: '進捗',\n      pattern: 'パターン',\n      flag: 'フラッグ',\n      diamond: 'ダイヤ',\n      mergeWith: 'マージ先…',\n      moveEventsTo: '予定を移動…',\n      areYouSure: '本当によろしいですか？',\n      cannotBeUndone: 'この操作は元に戻せません。',\n    },\n  },\n};\n\nexport default ja;\n"
  },
  {
    "path": "packages/plugins/localization/src/locales/ko.ts",
    "content": "import type { DayflowLocale } from './types';\n\nconst ko: DayflowLocale = {\n  code: 'ko-KR',\n  messages: {\n    allDay: '종일',\n    noEvents: '오늘 일정이 없습니다',\n    more: '개',\n    eventTitle: '일정 제목',\n    dateRange: '날짜 범위',\n    timeRange: '시간 범위',\n    note: '메모',\n    addNotePlaceholder: '메모 추가...',\n    setAsAllDay: '종일 일정으로 설정',\n    setAsTimed: '시간 지정 일정으로 설정',\n    delete: '삭제',\n    confirm: '확인',\n    cancel: '취소',\n    today: '오늘',\n    day: '일',\n    week: '주',\n    month: '월',\n    year: '년',\n    newEvent: '새 일정',\n    newAllDayEvent: '새 종일 일정',\n    newCalendarEvent: '{calendarName}의 새 일정',\n    newAllDayCalendarEvent: '{calendarName}의 새 종일 일정',\n    save: '저장',\n    deleteCalendar: '{calendarName} 삭제?',\n    deleteCalendarMessage:\n      '{calendarName}을(를) 삭제하시겠습니까, 아니면 이벤트를 다른 기존 캘린더로 병합하시겠습니까?',\n    merge: '병합',\n    confirmDeleteTitle: '{calendarName} 캘린더를 삭제하시겠습니까?',\n    confirmDeleteMessage:\n      '이 캘린더를 삭제하면 연결된 모든 이벤트도 삭제됩니다.',\n    mergeConfirmTitle: '{sourceName}을(를) {targetName}(으)로 병합?',\n    mergeConfirmMessage:\n      '{sourceName}을(를) {targetName}(으)로 병합하시겠습니까?\\n이렇게 하면 {sourceName}의 모든 이벤트가 {targetName}(으)로 이동하고 {sourceName}은(는) 삭제됩니다.\\n이 작업은 취소할 수 없습니다.',\n    expandSidebar: '캘린더 사이드바 펼치기',\n    collapseSidebar: '캘린더 사이드바 접기',\n    calendars: '캘린더',\n    createCalendar: '새 캘린더 만들기',\n    calendarNamePlaceholder: '예: 업무',\n    customColor: '사용자 지정 색상...',\n    create: '만들기',\n    calendarOptions: '캘린더 옵션',\n    untitled: '무제',\n    search: '검색',\n    noResults: '결과 없음',\n    calendar: '캘린더',\n    starts: '시작',\n    ends: '종료',\n    notes: '메모',\n    titlePlaceholder: '제목',\n    notesPlaceholder: '메모',\n    editEvent: '일정 편집',\n    done: '완료',\n    quickCreateEvent: '빠른 일정 생성',\n    quickCreatePlaceholder: '금요일 오후 7시에 영화',\n    noSuggestions: '입력하여 만들기',\n    newCalendar: '새 캘린더',\n    refreshAll: '모두 새로 고침',\n    tomorrow: '내일',\n    importCalendar: '캘린더 가져오기',\n    exportCalendar: '캘린더 내보내기',\n    addSchedule: '일정 추가',\n    importCalendarMessage:\n      '이 캘린더에는 새로운 일정이 포함되어 있습니다. 대상 캘린더를 선택하세요.',\n    ok: '확인',\n    cut: '잘라내기',\n    copy: '복사',\n    pasteHere: '여기에 붙여넣기',\n    eventSummary: '요약',\n    viewEvent: '일정 보기',\n    subscribeCalendar: '캘린더 구독',\n    subscribeCalendarTitle: '구독할 캘린더의 URL을 입력하세요.',\n    calendarUrl: '캘린더 URL',\n    calendarUrlPlaceholder: 'https://example.com/calendar.ics',\n    subscribe: '구독',\n    fetchingCalendar: '캘린더를 가져오는 중...',\n    subscribeError:\n      '캘린더를 가져오지 못했습니다. URL을 확인하고 다시 시도하세요.',\n  },\n  packages: {\n    scheduler: {\n      previousPeriod: '이전 기간',\n      nextPeriod: '다음 기간',\n      quarter: '분기',\n      days: '일',\n      weeks: '주',\n      months: '월',\n      quarters: '분기',\n      years: '년',\n      views: '보기',\n      resources: '리소스',\n      resizePanel: '패널 크기 조절',\n      searchPlaceholder: '검색…',\n      noResults: '일정이 없습니다',\n      typeToSearch: '입력하여 검색',\n      openSearch: '검색 열기',\n      closeSearch: '검색 닫기',\n      clearSearch: '검색 지우기',\n      close: '닫기',\n      title: '제목',\n      untitledEvent: '제목 없는 일정',\n      type: '유형',\n      addNotePlaceholder: '메모 추가',\n      milestone: '마일스톤',\n      typeFixedHint: '이 쇼케이스 샘플에서는 유형이 고정되어 있습니다.',\n      progressFill: '진행 채우기',\n      progressBar: '진행 막대',\n      sample: '샘플',\n      titleAbove: '제목 위',\n      titleCenter: '제목 가운데',\n      titleBelow: '제목 아래',\n      filled: '채우기',\n      bordered: '테두리',\n      outlined: '외곽선',\n      dashed: '점선',\n      line: '라인',\n      dashedLine: '점선 라인',\n      indented: '들여쓰기',\n      rounded: '둥근 모서리',\n      gradient: '그라데이션',\n      progress: '진행률',\n      pattern: '패턴',\n      flag: '플래그',\n      diamond: '다이아몬드',\n      mergeWith: '병합 대상…',\n      moveEventsTo: '일정 이동…',\n      areYouSure: '정말 삭제하시겠습니까?',\n      cannotBeUndone: '이 작업은 취소할 수 없습니다.',\n    },\n  },\n};\n\nexport default ko;\n"
  },
  {
    "path": "packages/plugins/localization/src/locales/types.ts",
    "content": "export type LocaleMessages = Record<string, string>;\nexport type LocaleNamespace = 'core' | 'scheduler' | string;\n\nexport interface DayflowLocale {\n  code: string;\n  messages: LocaleMessages;\n  packages?: Record<string, LocaleMessages | undefined>;\n}\n"
  },
  {
    "path": "packages/plugins/localization/src/locales/zh.ts",
    "content": "import type { DayflowLocale } from './types';\n\nconst zh: DayflowLocale = {\n  code: 'zh-CN',\n  messages: {\n    allDay: '全天',\n    noEvents: '今日无日程',\n    more: '个',\n    eventTitle: '事件标题',\n    dateRange: '日期范围',\n    timeRange: '时间范围',\n    note: '备注',\n    addNotePlaceholder: '添加备注...',\n    setAsAllDay: '设为全天',\n    setAsTimed: '设为普通事件',\n    delete: '删除',\n    confirm: '确认',\n    cancel: '取消',\n    today: '今天',\n    day: '日',\n    week: '周',\n    month: '月',\n    year: '年',\n    newEvent: '新建日程',\n    newAllDayEvent: '新建全天日程',\n    newCalendarEvent: '新建 {calendarName} 日程',\n    newAllDayCalendarEvent: '新建 {calendarName} 全天日程',\n    save: '保存',\n    deleteCalendar: '删除日历 {calendarName}?',\n    deleteCalendarMessage:\n      '您想删除日历 {calendarName} 还是将其事件合并到另一个现有日历中？',\n    merge: '合并',\n    confirmDeleteTitle: '您确定要删除日历 {calendarName} 吗？',\n    confirmDeleteMessage: '如果您删除此日历，与其关联的所有事件也将被删除。',\n    mergeConfirmTitle: '将 {sourceName} 合并到 {targetName}?',\n    mergeConfirmMessage:\n      '您确定要将 {sourceName} 合并到 {targetName} 吗？\\n这样做会将 {sourceName} 中的所有事件移动到 {targetName}，并且 {sourceName} 将被删除。\\n此操作无法撤销。',\n    expandSidebar: '展开日历侧边栏',\n    collapseSidebar: '收起日历侧边栏',\n    calendars: '日历',\n    createCalendar: '创建新日历',\n    calendarNamePlaceholder: '例如：工作',\n    customColor: '自定义颜色...',\n    create: '创建',\n    calendarOptions: '日历选项',\n    untitled: '未命名',\n    search: '搜索',\n    noResults: '无结果',\n    calendar: '日历',\n    starts: '开始',\n    ends: '结束',\n    notes: '备注',\n    titlePlaceholder: '标题',\n    notesPlaceholder: '备注',\n    editEvent: '编辑日程',\n    done: '完成',\n    quickCreateEvent: '快速创建日程',\n    quickCreatePlaceholder: '周五晚7点看电影',\n    noSuggestions: '输入内容以创建',\n    newCalendar: '新增日历',\n    refreshAll: '全部刷新',\n    tomorrow: '明天',\n    importCalendar: '导入日历',\n    exportCalendar: '导出日历',\n    addSchedule: '添加日程',\n    importCalendarMessage: '该日历包含新日程。请选取目标日历',\n    ok: '好',\n    cut: '剪切',\n    copy: '复制',\n    pasteHere: '粘贴在此处',\n    eventSummary: '简介',\n    viewEvent: '查看日程',\n    subscribeCalendar: '订阅日历',\n    subscribeCalendarTitle: '输入您要订阅的日历地址。',\n    calendarUrl: '日历 URL',\n    calendarUrlPlaceholder: 'https://example.com/calendar.ics',\n    subscribe: '订阅',\n    fetchingCalendar: '正在获取日历...',\n    subscribeError: '获取日历失败。请检查地址并重试。',\n  },\n  packages: {\n    scheduler: {\n      previousPeriod: '上一时间段',\n      nextPeriod: '下一时间段',\n      quarter: '季',\n      days: '天',\n      weeks: '周',\n      months: '月',\n      quarters: '季度',\n      years: '年',\n      views: '视图',\n      resources: '资源',\n      resizePanel: '调整面板大小',\n      searchPlaceholder: '搜索…',\n      noResults: '未找到日程',\n      typeToSearch: '输入以搜索日程',\n      openSearch: '打开搜索',\n      closeSearch: '关闭搜索',\n      clearSearch: '清除搜索',\n      close: '关闭',\n      title: '标题',\n      untitledEvent: '未命名日程',\n      type: '类型',\n      addNotePlaceholder: '添加备注',\n      milestone: '里程碑',\n      typeFixedHint: '此展示示例中的类型是固定的。',\n      progressFill: '进度填充',\n      progressBar: '进度条',\n      sample: '示例',\n      titleAbove: '标题上方',\n      titleCenter: '标题居中',\n      titleBelow: '标题下方',\n      filled: '填充',\n      bordered: '边框',\n      outlined: '轮廓',\n      dashed: '虚线',\n      line: '线条',\n      dashedLine: '虚线线条',\n      indented: '缩进',\n      rounded: '圆角',\n      gradient: '渐变',\n      progress: '进度',\n      pattern: '纹理',\n      flag: '旗标',\n      diamond: '钻石',\n      mergeWith: '合并到…',\n      moveEventsTo: '移动日程到…',\n      pasteHere: '粘贴到此处',\n      areYouSure: '确认删除？',\n      cannotBeUndone: '此操作无法撤销。',\n    },\n  },\n};\n\nexport default zh;\n"
  },
  {
    "path": "packages/plugins/localization/src/plugin.ts",
    "content": "import { registerLocale } from '@dayflow/core';\nimport type { Locale as CoreLocale } from '@dayflow/core';\n\nimport type {\n  DayflowLocale,\n  LocaleMessages,\n  LocaleNamespace,\n} from './locales/types';\n\nexport interface LocalizationHost {\n  registerLocale?: unknown;\n  triggerRender?: unknown;\n}\n\nexport type LocalizationPluginCleanup = () => void;\n\nexport interface LocalizationPlugin {\n  name: string;\n  install: (app: LocalizationHost) => LocalizationPluginCleanup | undefined;\n}\n\nexport interface LocalizationConfig {\n  locales: DayflowLocale[];\n  /**\n   * Locale message namespace to register for app-level registries.\n   *\n   * `messages` is the default DayFlow calendar namespace. Package-specific\n   * surfaces can add entries under `packages`, for example `scheduler` today\n   * and `gantt` later.\n   */\n  namespace?: LocaleNamespace;\n}\n\nconst CORE_NAMESPACE = 'core';\nconst DEFAULT_APP_REGISTRY_NAMESPACE = 'scheduler';\n\nfunction hasAppLocaleRegistry(\n  app: LocalizationHost\n): app is LocalizationHost & {\n  registerLocale: (locale: DayflowLocale) => void;\n} {\n  return typeof app.registerLocale === 'function';\n}\n\nexport function getLocaleMessages(\n  locale: DayflowLocale,\n  namespace: LocaleNamespace = CORE_NAMESPACE\n): LocaleMessages {\n  if (namespace === CORE_NAMESPACE) {\n    return locale.messages;\n  }\n\n  return {\n    ...locale.messages,\n    ...locale.packages?.[namespace],\n  };\n}\n\nexport function createLocaleForNamespace(\n  locale: DayflowLocale,\n  namespace?: LocaleNamespace\n): DayflowLocale {\n  return {\n    code: locale.code,\n    messages: getLocaleMessages(locale, namespace),\n  };\n}\n\n/**\n * Creates a localization plugin to register additional locales.\n *\n * Calendar apps use the default `messages` namespace. Apps with their own\n * locale registry, such as scheduler, can use package namespaces:\n * `createLocalizationPlugin({ locales, namespace: 'scheduler' })`.\n *\n * @param config Plugin configuration containing locales to register\n * @returns A localization plugin instance\n */\nexport function createLocalizationPlugin(\n  config: LocalizationConfig\n): LocalizationPlugin {\n  return {\n    name: 'localization',\n    install(app: LocalizationHost) {\n      const namespace =\n        config.namespace ??\n        (hasAppLocaleRegistry(app)\n          ? DEFAULT_APP_REGISTRY_NAMESPACE\n          : CORE_NAMESPACE);\n\n      if (config.locales) {\n        config.locales.forEach(locale => {\n          const scopedLocale = createLocaleForNamespace(locale, namespace);\n\n          if (hasAppLocaleRegistry(app)) {\n            app.registerLocale(scopedLocale);\n          } else {\n            registerLocale(scopedLocale as CoreLocale);\n          }\n        });\n      }\n\n      if (typeof app.triggerRender === 'function') {\n        app.triggerRender();\n      }\n\n      // oxlint-disable-next-line unicorn/no-useless-undefined\n      return undefined;\n    },\n  };\n}\n"
  },
  {
    "path": "packages/plugins/localization/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"declarationDir\": \"dist/types\",\n    \"declarationMap\": false,\n    \"sourceMap\": false,\n    \"module\": \"ESNext\",\n    \"target\": \"ES2015\",\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": false\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"**/*.test.ts\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.ts\",\n    \"**/*.spec.tsx\"\n  ]\n}\n"
  },
  {
    "path": "packages/plugins/localization/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"declaration\": true,\n    \"declarationMap\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/plugins/sidebar/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/plugins/sidebar/README.md",
    "content": "# DayFlow\n\n**English** | [中文](README.zh.md) | [日本語](README.ja.md) | [Getting Started & Contributing](CONTRIBUTING.md)\n\nA flexible and feature-rich calendar component library for **React, Vue, Angular, and Svelte** with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/core?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/core)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/dayflow/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/dayflow)](https://github.com/dayflow-js/dayflow/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/dayflow/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n\n---\n"
  },
  {
    "path": "packages/plugins/sidebar/package.json",
    "content": "{\n  \"name\": \"@dayflow/plugin-sidebar\",\n  \"version\": \"1.5.2\",\n  \"description\": \"Sidebar plugin for DayFlow calendar\",\n  \"keywords\": [\n    \"calendar\",\n    \"dayflow\",\n    \"plugin\",\n    \"sidebar\"\n  ],\n  \"license\": \"MIT\",\n  \"author\": \"Jayce Li\",\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"*.css\",\n    \"**/*.css\"\n  ],\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\"\n    },\n    \"./dist/styles.css\": \"./dist/styles.css\",\n    \"./dist/styles.components.css\": \"./dist/styles.components.css\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && rollup -c && pnpm run build:css && pnpm run check:css\",\n    \"build:css\": \"node ./scripts/build-css.mjs\",\n    \"check:css\": \"pnpm run check:css:source && pnpm run check:css:dist\",\n    \"check:css:dist\": \"node ../../core/scripts/check-dist-styling.mjs --package-root .\",\n    \"check:css:source\": \"node ../../core/scripts/check-semantic-css.mjs\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"postbuild\": \"rimraf dist/types\",\n    \"prebuild\": \"rimraf dist\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@dayflow/blossom-color-picker\": \"catalog:\",\n    \"@dayflow/core\": \"workspace:*\",\n    \"@rollup/plugin-commonjs\": \"catalog:\",\n    \"@rollup/plugin-node-resolve\": \"catalog:\",\n    \"@rollup/plugin-typescript\": \"catalog:\",\n    \"@tailwindcss/postcss\": \"catalog:\",\n    \"autoprefixer\": \"catalog:\",\n    \"postcss\": \"catalog:\",\n    \"postcss-import\": \"catalog:\",\n    \"preact\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-dts\": \"catalog:\",\n    \"tailwindcss\": \"catalog:\",\n    \"temporal-polyfill\": \"catalog:\",\n    \"tsc-alias\": \"^1.8.16\",\n    \"typescript\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"@dayflow/core\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/sidebar/rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport typescript from '@rollup/plugin-typescript';\nimport { dts } from 'rollup-plugin-dts';\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: [\n      {\n        file: 'dist/index.js',\n        format: 'esm',\n        sourcemap: false,\n        exports: 'named',\n      },\n    ],\n    plugins: [\n      resolve(),\n      commonjs(),\n      typescript({\n        tsconfig: './tsconfig.build.json',\n      }),\n    ],\n    external: [\n      '@dayflow/core',\n      '@dayflow/blossom-color-picker',\n      'preact',\n      'preact/hooks',\n      'preact/compat',\n      'temporal-polyfill',\n    ],\n  },\n  {\n    input: 'dist/types/index.d.ts',\n    output: [{ file: 'dist/index.d.ts', format: 'es' }],\n    plugins: [dts()],\n    external: ['temporal-polyfill', '@dayflow/blossom-color-picker'],\n  },\n];\n"
  },
  {
    "path": "packages/plugins/sidebar/scripts/build-css.mjs",
    "content": "import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport tailwindcss from '@tailwindcss/postcss';\nimport autoprefixer from 'autoprefixer';\nimport postcss from 'postcss';\nimport postcssImport from 'postcss-import';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst root = path.resolve(__dirname, '..');\n\nasync function buildCss(inputFile, outputFile) {\n  const input = path.join(root, inputFile);\n  const output = path.join(root, outputFile);\n\n  const css = await fs.readFile(input, 'utf8');\n\n  const result = await postcss([\n    postcssImport(),\n    tailwindcss,\n    autoprefixer,\n  ]).process(css, { from: input, to: output });\n\n  await fs.mkdir(path.dirname(output), { recursive: true });\n  await fs.writeFile(output, result.css);\n\n  if (result.map) {\n    await fs.writeFile(`${output}.map`, result.map.toString());\n  }\n\n  console.log('CSS built successfully ->', path.relative(root, output));\n}\n\nawait buildCss('src/styles/tailwind.css', 'dist/styles.css');\nawait buildCss(\n  'src/styles/tailwind-components.css',\n  'dist/styles.components.css'\n);\n"
  },
  {
    "path": "packages/plugins/sidebar/src/DefaultCalendarSidebar.tsx",
    "content": "import {\n  hexToHsl,\n  lightnessToSliderValue,\n} from '@dayflow/blossom-color-picker';\nimport {\n  createPortal,\n  Event as CalendarEvent,\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuLabel,\n  ContextMenuColorPicker,\n  getCalendarColorsForHex,\n  BlossomColorPicker,\n  ContentSlot,\n  DefaultColorPicker,\n  MiniCalendar,\n  useLocale,\n  importICSFile,\n  downloadICS,\n  generateUniKey,\n  subscribeCalendar,\n  CalendarType,\n} from '@dayflow/core';\nimport { JSX } from 'preact';\nimport { useCallback, useState, useRef, useEffect } from 'preact/hooks';\n\nimport { CalendarList } from './components/CalendarList';\nimport { DeleteCalendarDialog } from './components/DeleteCalendarDialog';\nimport {\n  ImportCalendarDialog,\n  NEW_CALENDAR_ID,\n} from './components/ImportCalendarDialog';\nimport { MergeCalendarDialog } from './components/MergeCalendarDialog';\nimport { MergeMenuItem } from './components/MergeMenuItem';\nimport { SidebarHeader } from './components/SidebarHeader';\nimport { SubscribeCalendarDialog } from './components/SubscribeCalendarDialog';\nimport type { CalendarSidebarRenderProps } from './plugin';\n\nconst DefaultCalendarSidebar = ({\n  app,\n  calendars,\n  toggleCalendarVisibility,\n  isCollapsed,\n  setCollapsed,\n  showEventDots,\n  renderCalendarContextMenu,\n  renderSidebarHeader,\n  editingCalendarId: propEditingCalendarId,\n  setEditingCalendarId: propSetEditingCalendarId,\n  onCreateCalendar,\n  onSubscribeCalendar,\n  onLoadSubscription,\n  onReorder,\n  componentsOrder = ['calendarList', 'miniCalendar'],\n}: CalendarSidebarRenderProps) => {\n  const { t } = useLocale();\n\n  // Detect if custom color picker slot is provided\n  const hasCustomPicker = app.state.overrides.includes('colorPicker');\n\n  const [localEditingCalendarId, setLocalEditingCalendarId] = useState<\n    string | null\n  >(null);\n  const editingCalendarId =\n    propEditingCalendarId === undefined\n      ? localEditingCalendarId\n      : propEditingCalendarId;\n  const setEditingCalendarId =\n    propSetEditingCalendarId || setLocalEditingCalendarId;\n  // File input ref for import\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const contextMenuRef = useRef<HTMLDivElement>(null);\n\n  // Track loaded subscription URLs to avoid redundant fetching\n  const loadedSubscriptionsRef = useRef<Set<string>>(new Set());\n\n  // Auto-load subscriptions on mount or when calendars change\n  useEffect(() => {\n    calendars.forEach(async calendar => {\n      if (\n        calendar.subscription?.url &&\n        !loadedSubscriptionsRef.current.has(calendar.subscription.url)\n      ) {\n        loadedSubscriptionsRef.current.add(calendar.subscription.url);\n\n        try {\n          if (onLoadSubscription) {\n            await onLoadSubscription(calendar);\n          } else {\n            const { events } = await subscribeCalendar(\n              calendar.subscription.url\n            );\n            app.addExternalEvents(calendar.id, events);\n          }\n        } catch (err) {\n          console.error(`Failed to auto-load calendar ${calendar.name}:`, err);\n          app.updateCalendar(calendar.id, {\n            subscription: {\n              ...calendar.subscription!,\n              status: 'error',\n            },\n          });\n        }\n      }\n    });\n  }, [app, calendars, onLoadSubscription]);\n\n  const handleMonthChange = useCallback(\n    (offset: number) => {\n      const current = app.getVisibleMonth();\n      const next = new Date(\n        current.getFullYear(),\n        current.getMonth() + offset,\n        1\n      );\n      app.setVisibleMonth(next);\n    },\n    [app]\n  );\n\n  // Context Menu State\n  const [contextMenu, setContextMenu] = useState<{\n    x: number;\n    y: number;\n    calendarId: string;\n    rowRect?: DOMRect;\n  } | null>(null);\n\n  // Sidebar Context Menu State (Background)\n  const [sidebarContextMenu, setSidebarContextMenu] = useState<{\n    x: number;\n    y: number;\n  } | null>(null);\n\n  const [customColorPicker, setCustomColorPicker] = useState<{\n    x: number;\n    y: number;\n    calendarId: string;\n    initialColor: {\n      hue: number;\n      saturation: number;\n      lightness: number;\n      alpha: number;\n      layer: 'inner' | 'outer';\n    };\n    currentColor: string; // For react-color mode\n  } | null>(null);\n\n  // Merge Calendar State\n  const [mergeState, setMergeState] = useState<{\n    sourceId: string;\n    targetId: string;\n  } | null>(null);\n\n  // Delete Calendar State\n  const [deleteState, setDeleteState] = useState<{\n    calendarId: string;\n    step: 'initial' | 'confirm_delete';\n  } | null>(null);\n\n  // Import Calendar State\n  const [importState, setImportState] = useState<{\n    events: CalendarEvent[];\n    filename: string;\n  } | null>(null);\n\n  // Subscribe Calendar State\n  const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);\n\n  const handleContextMenu = useCallback(\n    (e: JSX.TargetedMouseEvent<HTMLElement>, calendarId: string) => {\n      e.preventDefault();\n      e.stopPropagation(); // Stop propagation to prevent sidebar context menu\n      setContextMenu({\n        x: e.clientX,\n        y: e.clientY,\n        calendarId,\n        rowRect: e.currentTarget.getBoundingClientRect(),\n      });\n      setSidebarContextMenu(null);\n    },\n    []\n  );\n\n  const handleSidebarContextMenu = useCallback(\n    (e: JSX.TargetedMouseEvent<HTMLElement>) => {\n      e.preventDefault();\n      setSidebarContextMenu({\n        x: e.clientX,\n        y: e.clientY,\n      });\n      setContextMenu(null);\n    },\n    []\n  );\n\n  const handleCloseContextMenu = useCallback(() => {\n    setContextMenu(null);\n  }, []);\n\n  const handleCloseSidebarContextMenu = useCallback(() => {\n    setSidebarContextMenu(null);\n  }, []);\n\n  const handleDeleteCalendar = useCallback(() => {\n    if (contextMenu) {\n      setDeleteState({ calendarId: contextMenu.calendarId, step: 'initial' });\n      handleCloseContextMenu();\n    }\n  }, [contextMenu, handleCloseContextMenu]);\n\n  const handleColorSelect = useCallback(\n    (color: string) => {\n      if (contextMenu) {\n        const { colors, darkColors } = getCalendarColorsForHex(color);\n        app.updateCalendar(contextMenu.calendarId, {\n          colors,\n          darkColors,\n        });\n        handleCloseContextMenu();\n      }\n    },\n    [app, contextMenu, handleCloseContextMenu]\n  );\n\n  const handleCustomColor = useCallback(() => {\n    if (contextMenu) {\n      const calendar = calendars.find(c => c.id === contextMenu.calendarId);\n      if (calendar) {\n        const { l, h } = hexToHsl(calendar.colors.lineColor);\n        // Calculate slider position from lightness\n        const sliderValue = lightnessToSliderValue(l);\n\n        let x = contextMenu.x;\n        let y = contextMenu.y;\n\n        if (contextMenu.rowRect) {\n          // Position it on the row (roughly where the checkbox is)\n          x = contextMenu.rowRect.left + 24;\n          y = contextMenu.rowRect.top + contextMenu.rowRect.height / 2;\n        } else if (contextMenuRef.current) {\n          const rect = contextMenuRef.current.getBoundingClientRect();\n          x = rect.left + rect.width / 2;\n          y = rect.top + rect.height / 2;\n        }\n\n        setCustomColorPicker({\n          x,\n          y,\n          calendarId: contextMenu.calendarId,\n          initialColor: {\n            hue: h,\n            saturation: sliderValue,\n            lightness: l,\n            alpha: 100,\n            layer: 'outer',\n          },\n          currentColor: calendar.colors.lineColor,\n        });\n      }\n      handleCloseContextMenu();\n    }\n  }, [contextMenu, calendars, handleCloseContextMenu]);\n\n  const handleMergeSelect = useCallback(\n    (targetId: string) => {\n      if (contextMenu) {\n        setMergeState({\n          sourceId: contextMenu.calendarId,\n          targetId,\n        });\n        handleCloseContextMenu();\n      }\n    },\n    [contextMenu, handleCloseContextMenu]\n  );\n\n  const handleMergeConfirm = useCallback(async () => {\n    if (mergeState) {\n      const { sourceId, targetId } = mergeState;\n      await app.mergeCalendars(sourceId, targetId);\n      setMergeState(null);\n    }\n  }, [app, mergeState]);\n\n  const handleConfirmDelete = useCallback(async () => {\n    if (deleteState) {\n      await app.deleteCalendar(deleteState.calendarId);\n      setDeleteState(null);\n    }\n  }, [app, deleteState]);\n\n  const handleDeleteMergeSelect = useCallback(\n    (targetId: string) => {\n      if (deleteState) {\n        setMergeState({\n          sourceId: deleteState.calendarId,\n          targetId,\n        });\n        setDeleteState(null);\n      }\n    },\n    [deleteState]\n  );\n\n  // Import Calendar handler\n  const handleImportClick = useCallback(() => {\n    fileInputRef.current?.click();\n    handleCloseSidebarContextMenu();\n  }, [handleCloseSidebarContextMenu]);\n\n  // Subscribe Calendar handler\n  const handleSubscribeClick = useCallback(() => {\n    setSubscribeDialogOpen(true);\n    handleCloseSidebarContextMenu();\n  }, [handleCloseSidebarContextMenu]);\n\n  const handleSubscribeConfirm = useCallback(\n    async (url: string) => {\n      // 1. Check for duplicates\n      const isDuplicate = calendars.some(c => c.subscription?.url === url);\n      if (isDuplicate) {\n        throw new Error('DUPLICATE_URL');\n      }\n\n      // 2. Load the subscription (fetch + parse) using the new utility\n      const { calendar, events } = await subscribeCalendar(url);\n\n      // 3. Mark as loaded to avoid the useEffect triggering another fetch\n      if (calendar.subscription?.url) {\n        loadedSubscriptionsRef.current.add(calendar.subscription.url);\n      }\n\n      // 4. Delegate to user if callback exists, otherwise use default behavior\n      if (onSubscribeCalendar) {\n        await onSubscribeCalendar(calendar as CalendarType, events);\n      } else {\n        // Default behavior: create calendar in the app\n        app.createCalendar(calendar as CalendarType);\n      }\n\n      // 4. Always add events to the internal external store for IMMEDIATE display\n      app.addExternalEvents(calendar.id!, events);\n\n      // 5. Close dialog\n      setSubscribeDialogOpen(false);\n    },\n    [app, onSubscribeCalendar, calendars]\n  );\n\n  const handleFileChange = useCallback(\n    async (e: JSX.TargetedEvent<HTMLInputElement, globalThis.Event>) => {\n      const file = e.currentTarget.files?.[0];\n      if (!file) return;\n\n      const result = await importICSFile(file);\n\n      // Show dialog if found at least one valid event, even if there were some parsing errors\n      if (result.events.length > 0) {\n        setImportState({\n          events: result.events,\n          filename: file.name.replace(/\\.[^/.]+$/, ''), // Remove extension\n        });\n      }\n\n      // Reset file input\n      if (fileInputRef.current) {\n        fileInputRef.current.value = '';\n      }\n    },\n    []\n  );\n\n  const handleImportConfirm = useCallback(\n    (targetCalendarId: string) => {\n      if (importState) {\n        let finalCalendarId = targetCalendarId;\n\n        if (targetCalendarId === NEW_CALENDAR_ID) {\n          // Create new calendar\n          const colors = [\n            '#3b82f6',\n            '#10b981',\n            '#8b5cf6',\n            '#f59e0b',\n            '#ef4444',\n            '#f97316',\n            '#ec4899',\n            '#14b8a6',\n            '#6366f1',\n            '#6b7280',\n          ];\n          const randomColor = colors[Math.floor(Math.random() * colors.length)];\n          const { colors: calendarColors, darkColors } =\n            getCalendarColorsForHex(randomColor);\n\n          finalCalendarId = generateUniKey();\n          app.createCalendar({\n            id: finalCalendarId,\n            name: importState.filename,\n            isDefault: false,\n            colors: calendarColors,\n            darkColors: darkColors,\n            isVisible: true,\n          });\n        }\n\n        importState.events.forEach(event => {\n          app.addEvent({ ...event, calendarId: finalCalendarId });\n        });\n        setImportState(null);\n      }\n    },\n    [app, importState]\n  );\n\n  // Export Calendar handler\n  const handleExportCalendar = useCallback(() => {\n    if (contextMenu) {\n      const calendar = calendars.find(c => c.id === contextMenu.calendarId);\n      if (calendar) {\n        const events = app\n          .getEvents()\n          .filter(e => e.calendarId === calendar.id);\n        downloadICS(events, {\n          calendarName: calendar.name,\n          filename: calendar.name || 'calendar',\n        });\n      }\n      handleCloseContextMenu();\n    }\n  }, [contextMenu, calendars, app, handleCloseContextMenu]);\n\n  const handleReorder = useCallback(\n    (fromIndex: number, toIndex: number) => {\n      app.reorderCalendars(fromIndex, toIndex);\n      if (onReorder) {\n        onReorder(app.getCalendars());\n      }\n    },\n    [app, onReorder]\n  );\n\n  const sourceCalendar = mergeState\n    ? calendars.find(c => c.id === mergeState.sourceId)\n    : null;\n  const targetCalendar = mergeState\n    ? calendars.find(c => c.id === mergeState.targetId)\n    : null;\n  const sourceCalendarName = sourceCalendar?.name || 'Unknown';\n  const targetCalendarName = targetCalendar?.name || 'Unknown';\n  const sourceCalendarColor = sourceCalendar?.colors.lineColor || '#6b7280';\n  const targetCalendarColor = targetCalendar?.colors.lineColor || '#6b7280';\n  const deleteCalendarName = deleteState\n    ? calendars.find(c => c.id === deleteState.calendarId)?.name || 'Unknown'\n    : '';\n\n  const readOnlyConfig = app.getReadOnlyConfig();\n  const isEditable = app.canMutateFromUI();\n  const isDraggable = readOnlyConfig.draggable !== false;\n\n  useEffect(() => {\n    if (isEditable) return;\n\n    setContextMenu(null);\n    setSidebarContextMenu(null);\n    setCustomColorPicker(null);\n    setMergeState(null);\n    setDeleteState(null);\n    setImportState(null);\n    setSubscribeDialogOpen(false);\n    setEditingCalendarId(null);\n  }, [isEditable, setEditingCalendarId]);\n\n  return (\n    <div\n      className='df-sidebar'\n      onContextMenu={isEditable ? handleSidebarContextMenu : undefined}\n    >\n      <ContentSlot\n        generatorName='sidebarHeader'\n        generatorArgs={{\n          isCollapsed,\n          onCollapseToggle: () => setCollapsed(!isCollapsed),\n        }}\n        defaultContent={\n          renderSidebarHeader ? (\n            renderSidebarHeader({\n              isCollapsed,\n              onCollapseToggle: () => setCollapsed(!isCollapsed),\n            })\n          ) : (\n            <SidebarHeader\n              isCollapsed={isCollapsed}\n              onCollapseToggle={() => setCollapsed(!isCollapsed)}\n            />\n          )\n        }\n      />\n\n      {isCollapsed ? (\n        <CalendarList\n          calendars={calendars}\n          onToggleVisibility={toggleCalendarVisibility}\n          onReorder={\n            isDraggable\n              ? handleReorder\n              : () => {\n                  /* noop */\n                }\n          }\n          onRename={\n            isEditable\n              ? (id, newName) => app.updateCalendar(id, { name: newName })\n              : () => {\n                  /* noop */\n                }\n          }\n          onContextMenu={\n            isEditable\n              ? handleContextMenu\n              : () => {\n                  /* noop */\n                }\n          }\n          editingId={editingCalendarId}\n          setEditingId={setEditingCalendarId}\n          activeContextMenuCalendarId={contextMenu?.calendarId}\n          isDraggable={isDraggable}\n          isEditable={isEditable}\n        />\n      ) : (\n        <>\n          {componentsOrder.map(component => {\n            if (component === 'calendarList') {\n              return (\n                <CalendarList\n                  key='calendarList'\n                  calendars={calendars}\n                  onToggleVisibility={toggleCalendarVisibility}\n                  onReorder={\n                    isDraggable\n                      ? handleReorder\n                      : () => {\n                          /* noop */\n                        }\n                  }\n                  onRename={\n                    isEditable\n                      ? (id, newName) =>\n                          app.updateCalendar(id, { name: newName })\n                      : () => {\n                          /* noop */\n                        }\n                  }\n                  onContextMenu={\n                    isEditable\n                      ? handleContextMenu\n                      : () => {\n                          /* noop */\n                        }\n                  }\n                  editingId={editingCalendarId}\n                  setEditingId={setEditingCalendarId}\n                  activeContextMenuCalendarId={contextMenu?.calendarId}\n                  isDraggable={isDraggable}\n                  isEditable={isEditable}\n                />\n              );\n            }\n            if (component === 'miniCalendar') {\n              return (\n                <div key='miniCalendar' className='df-sidebar-mini-calendar'>\n                  <MiniCalendar\n                    visibleMonth={app.getVisibleMonth()}\n                    currentDate={app.getCurrentDate()}\n                    showHeader={true}\n                    onMonthChange={handleMonthChange}\n                    onDateSelect={date => app.setCurrentDate(date)}\n                    events={app.getEvents()}\n                    showEventDots={showEventDots}\n                    calendarRegistry={app.getCalendarRegistry()}\n                    timeZone={\n                      (app.getViewConfig(app.state.currentView)\n                        ?.secondaryTimeZone as string) || undefined\n                    }\n                  />\n                </div>\n              );\n            }\n            return null;\n          })}\n        </>\n      )}\n\n      {contextMenu && app.canMutateFromUI(contextMenu.calendarId) && (\n        <ContextMenu\n          ref={contextMenuRef}\n          x={contextMenu.x}\n          y={contextMenu.y}\n          onClose={handleCloseContextMenu}\n          className='df-sidebar-context-menu df-sidebar-context-menu-calendar'\n        >\n          <ContentSlot\n            generatorName='calendarContextMenu'\n            generatorArgs={{\n              calendar: calendars.find(c => c.id === contextMenu.calendarId)!,\n              onClose: handleCloseContextMenu,\n            }}\n            defaultContent={\n              renderCalendarContextMenu ? (\n                renderCalendarContextMenu(\n                  calendars.find(c => c.id === contextMenu.calendarId)!,\n                  handleCloseContextMenu\n                )\n              ) : (\n                <>\n                  <ContextMenuLabel>{t('calendarOptions')}</ContextMenuLabel>\n                  <MergeMenuItem\n                    calendars={calendars}\n                    currentCalendarId={contextMenu.calendarId}\n                    onMergeSelect={handleMergeSelect}\n                  />\n                  <ContextMenuItem onClick={handleDeleteCalendar}>\n                    {t('delete')}\n                  </ContextMenuItem>\n                  <ContextMenuItem onClick={handleExportCalendar}>\n                    {t('exportCalendar') || 'Export Calendar'}\n                  </ContextMenuItem>\n                  <ContextMenuSeparator />\n                  <ContextMenuColorPicker\n                    selectedColor={\n                      calendars.find(c => c.id === contextMenu.calendarId)\n                        ?.colors.lineColor\n                    }\n                    onSelect={handleColorSelect}\n                    onCustomColor={handleCustomColor}\n                  />\n                </>\n              )\n            }\n          />\n        </ContextMenu>\n      )}\n\n      {isEditable &&\n        sidebarContextMenu &&\n        createPortal(\n          <ContextMenu\n            x={sidebarContextMenu.x}\n            y={sidebarContextMenu.y}\n            onClose={handleCloseSidebarContextMenu}\n            className='df-sidebar-context-menu df-sidebar-context-menu-sidebar'\n          >\n            <ContextMenuItem\n              onClick={() => {\n                onCreateCalendar?.();\n                handleCloseSidebarContextMenu();\n              }}\n            >\n              {t('newCalendar') || 'New Calendar'}\n            </ContextMenuItem>\n            <ContextMenuItem onClick={handleImportClick}>\n              {t('importCalendar') || 'Import Calendar'}\n            </ContextMenuItem>\n            <ContextMenuItem onClick={handleSubscribeClick}>\n              {t('subscribeCalendar') || 'Subscribe to Calendar'}\n            </ContextMenuItem>\n            <ContextMenuItem\n              onClick={() => {\n                app.triggerRender();\n                handleCloseSidebarContextMenu();\n              }}\n            >\n              {t('refreshAll') || 'Refresh All'}\n            </ContextMenuItem>\n          </ContextMenu>,\n          document.body\n        )}\n\n      {/* Hidden file input for ICS import */}\n      <input\n        ref={fileInputRef}\n        type='file'\n        accept='.ics'\n        style={{ display: 'none' }}\n        onChange={handleFileChange}\n      />\n\n      {isEditable &&\n        importState &&\n        createPortal(\n          <ImportCalendarDialog\n            calendars={calendars}\n            filename={importState.filename}\n            onConfirm={handleImportConfirm}\n            onCancel={() => setImportState(null)}\n          />,\n          document.body\n        )}\n\n      {isEditable &&\n        subscribeDialogOpen &&\n        createPortal(\n          <SubscribeCalendarDialog\n            onSubscribe={handleSubscribeConfirm}\n            onCancel={() => setSubscribeDialogOpen(false)}\n          />,\n          document.body\n        )}\n\n      {isEditable &&\n        mergeState &&\n        createPortal(\n          <MergeCalendarDialog\n            sourceName={sourceCalendarName}\n            sourceColor={sourceCalendarColor}\n            targetName={targetCalendarName}\n            targetColor={targetCalendarColor}\n            onConfirm={handleMergeConfirm}\n            onCancel={() => setMergeState(null)}\n          />,\n          document.body\n        )}\n\n      {isEditable &&\n        deleteState &&\n        createPortal(\n          <DeleteCalendarDialog\n            calendarId={deleteState.calendarId}\n            calendarName={deleteCalendarName}\n            calendars={calendars}\n            step={deleteState.step}\n            onStepChange={step =>\n              setDeleteState(prev => (prev ? { ...prev, step } : null))\n            }\n            onConfirmDelete={handleConfirmDelete}\n            onCancel={() => setDeleteState(null)}\n            onMergeSelect={handleDeleteMergeSelect}\n          />,\n          document.body\n        )}\n\n      {isEditable &&\n        customColorPicker &&\n        createPortal(\n          <div\n            className='df-sidebar-color-picker-layer'\n            onMouseDown={() => {\n              app.updateCalendar(customColorPicker.calendarId, {});\n              setCustomColorPicker(null);\n            }}\n          >\n            <div\n              className='df-sidebar-color-picker-anchor'\n              style={{\n                top: customColorPicker.y,\n                left: customColorPicker.x,\n                zIndex: 10002,\n                transform: 'translate(40px, -50%)', // Move it to the right of the checkbox/dot\n              }}\n              onMouseDown={e => e.stopPropagation()}\n            >\n              {hasCustomPicker ? (\n                <ContentSlot\n                  generatorName='colorPicker'\n                  generatorArgs={{\n                    color: customColorPicker.currentColor,\n                    onChange: (color: { hex: string }) => {\n                      setCustomColorPicker(prev =>\n                        prev ? { ...prev, currentColor: color.hex } : null\n                      );\n                      const { colors, darkColors } = getCalendarColorsForHex(\n                        color.hex\n                      );\n                      app.updateCalendar(\n                        customColorPicker.calendarId,\n                        {\n                          colors,\n                          darkColors,\n                        },\n                        true\n                      );\n                    },\n                    onChangeComplete: (color: { hex: string }) => {\n                      const { colors, darkColors } = getCalendarColorsForHex(\n                        color.hex\n                      );\n                      app.updateCalendar(customColorPicker.calendarId, {\n                        colors,\n                        darkColors,\n                      });\n                    },\n                  }}\n                  defaultContent={\n                    <div className='df-sidebar-color-picker-card'>\n                      <DefaultColorPicker\n                        color={customColorPicker.currentColor}\n                        onChange={(color, isPending) => {\n                          setCustomColorPicker(prev =>\n                            prev ? { ...prev, currentColor: color.hex } : null\n                          );\n                          const { colors, darkColors } =\n                            getCalendarColorsForHex(color.hex);\n                          app.updateCalendar(\n                            customColorPicker.calendarId,\n                            {\n                              colors,\n                              darkColors,\n                            },\n                            isPending\n                          );\n                        }}\n                        onClose={() => {\n                          app.updateCalendar(customColorPicker.calendarId, {});\n                          setCustomColorPicker(null);\n                        }}\n                      />\n                    </div>\n                  }\n                />\n              ) : (\n                <BlossomColorPicker\n                  defaultValue={customColorPicker.initialColor}\n                  coreSize={28}\n                  petalSize={28}\n                  initialExpanded={true}\n                  adaptivePositioning={true}\n                  openOnHover={false}\n                  onChange={color => {\n                    const { colors, darkColors } = getCalendarColorsForHex(\n                      color.hex\n                    );\n                    app.updateCalendar(\n                      customColorPicker.calendarId,\n                      {\n                        colors,\n                        darkColors,\n                      },\n                      true\n                    );\n                  }}\n                  onCollapse={() => {\n                    app.updateCalendar(customColorPicker.calendarId, {});\n                    setCustomColorPicker(null);\n                  }}\n                />\n              )}\n            </div>\n          </div>,\n          document.body\n        )}\n    </div>\n  );\n};\n\nexport default DefaultCalendarSidebar;\n"
  },
  {
    "path": "packages/plugins/sidebar/src/components/CalendarChip.tsx",
    "content": "export interface CalendarChipProps {\n  name: string;\n  /** lineColor hex, e.g. \"#3b82f6\" */\n  color: string;\n}\n\nexport const CalendarChip = ({ name, color }: CalendarChipProps) => (\n  <span\n    className='df-sidebar-chip'\n    style={{ backgroundColor: `${color}26`, color }}\n  >\n    {name}\n  </span>\n);\n"
  },
  {
    "path": "packages/plugins/sidebar/src/components/CalendarList.tsx",
    "content": "import {\n  CalendarType,\n  AudioLines,\n  ChevronDown,\n  AlertCircle,\n} from '@dayflow/core';\nimport { JSX } from 'preact';\nimport { useState, useCallback, useRef, useEffect } from 'preact/hooks';\n\ninterface CalendarListProps {\n  calendars: CalendarType[];\n  onToggleVisibility: (id: string, visible: boolean) => void;\n  onReorder: (fromIndex: number, toIndex: number) => void | Promise<void>;\n  onRename: (id: string, newName: string) => void;\n  onContextMenu: (e: JSX.TargetedMouseEvent<HTMLElement>, id: string) => void;\n  editingId: string | null;\n  setEditingId: (id: string | null) => void;\n  activeContextMenuCalendarId?: string | null;\n  isDraggable?: boolean;\n  isEditable?: boolean;\n}\n\nconst getCalendarInitials = (calendar: CalendarType): string => {\n  if (calendar.icon) {\n    return calendar.icon;\n  }\n  const name = calendar.name || calendar.id;\n  return name.charAt(0).toUpperCase();\n};\n\ninterface CalendarItemProps {\n  calendar: CalendarType;\n  isDraggable: boolean;\n  isEditable: boolean;\n  editingId: string | null;\n  editingName: string;\n  setEditingName: (name: string) => void;\n  editInputRef: preact.RefObject<HTMLInputElement>;\n  isProcessedRef: preact.RefObject<boolean>;\n  draggedCalendarId: string | null;\n  dropTarget: { id: string; position: 'top' | 'bottom' } | null;\n  activeContextMenuCalendarId?: string | null;\n  onDragStart: (\n    calendar: CalendarType,\n    e: JSX.TargetedDragEvent<HTMLElement>\n  ) => void;\n  onDragEnd: () => void;\n  onDragOver: (e: JSX.TargetedDragEvent<HTMLElement>, targetId: string) => void;\n  onDragLeave: () => void;\n  onDrop: (targetCalendar: CalendarType) => void;\n  onContextMenu: (e: JSX.TargetedMouseEvent<HTMLElement>, id: string) => void;\n  onToggleVisibility: (id: string, visible: boolean) => void;\n  onRenameStart: (calendar: CalendarType) => void;\n  onRenameSave: () => void;\n  onRenameKeyDown: (e: JSX.TargetedKeyboardEvent<HTMLInputElement>) => void;\n  setEditingId: (id: string | null) => void;\n}\n\nconst CalendarItem = ({\n  calendar,\n  isDraggable,\n  isEditable: _isEditable,\n  editingId,\n  editingName,\n  setEditingName,\n  editInputRef,\n  draggedCalendarId,\n  dropTarget,\n  activeContextMenuCalendarId,\n  onDragStart,\n  onDragEnd,\n  onDragOver,\n  onDragLeave,\n  onDrop,\n  onContextMenu,\n  onToggleVisibility,\n  onRenameStart,\n  onRenameSave,\n  onRenameKeyDown,\n}: CalendarItemProps) => {\n  const isVisible = calendar.isVisible !== false;\n  const calendarColor = calendar.colors?.lineColor || '#3b82f6';\n  const showIcon = Boolean(calendar.icon);\n  const isDropTarget = dropTarget?.id === calendar.id;\n  const isActive =\n    activeContextMenuCalendarId === calendar.id || editingId === calendar.id;\n\n  return (\n    <li\n      key={calendar.id}\n      className='df-sidebar-list-item'\n      onDragOver={e => onDragOver(e, calendar.id)}\n      onDragLeave={onDragLeave}\n      onDrop={() => onDrop(calendar)}\n      onContextMenu={e => onContextMenu(e, calendar.id)}\n    >\n      {isDropTarget && dropTarget.position === 'top' && (\n        <div className='df-sidebar-drop-indicator' data-position='top' />\n      )}\n      <div\n        draggable={\n          isDraggable &&\n          !editingId &&\n          !calendar.readOnly &&\n          !calendar.subscription\n        }\n        onDragStart={e => onDragStart(calendar, e)}\n        onDragEnd={onDragEnd}\n        className='df-sidebar-drag-shell'\n        data-dragging={draggedCalendarId === calendar.id ? 'true' : undefined}\n        data-draggable={isDraggable ? 'true' : 'false'}\n      >\n        <div\n          className='df-sidebar-row'\n          data-active={isActive ? 'true' : undefined}\n          title={calendar.name}\n        >\n          <input\n            type='checkbox'\n            className='df-calendar-checkbox df-sidebar-checkbox'\n            style={\n              {\n                '--checkbox-color': calendarColor,\n              } as Record<string, string | number>\n            }\n            checked={isVisible}\n            onChange={event =>\n              onToggleVisibility(\n                calendar.id,\n                (event.target as HTMLInputElement).checked\n              )\n            }\n          />\n          {showIcon && (\n            <span\n              className='df-sidebar-icon-badge'\n              style={{ backgroundColor: calendarColor }}\n              aria-hidden='true'\n            >\n              {getCalendarInitials(calendar)}\n            </span>\n          )}\n          {editingId === calendar.id ? (\n            <input\n              ref={editInputRef}\n              type='text'\n              value={editingName}\n              onChange={e =>\n                setEditingName((e.target as HTMLInputElement).value)\n              }\n              onBlur={onRenameSave}\n              onKeyDown={onRenameKeyDown}\n              className='df-sidebar-rename-input'\n              onClick={e => e.stopPropagation()}\n            />\n          ) : (\n            <>\n              <span\n                className='df-sidebar-name'\n                onDblClick={() => onRenameStart(calendar)}\n              >\n                {calendar.name || calendar.id}\n              </span>\n              {calendar.subscription?.status === 'error' && (\n                <AlertCircle\n                  width={13}\n                  height={13}\n                  className='df-sidebar-status-icon df-sidebar-status-icon-error'\n                  title='Failed to load subscription'\n                />\n              )}\n              {calendar.subscription &&\n                calendar.subscription.status === 'ready' && (\n                  <AudioLines\n                    width={13}\n                    height={13}\n                    className='df-sidebar-status-icon df-sidebar-status-icon-subscription'\n                  />\n                )}\n            </>\n          )}\n        </div>\n      </div>\n      {isDropTarget && dropTarget.position === 'bottom' && (\n        <div className='df-sidebar-drop-indicator' data-position='bottom' />\n      )}\n    </li>\n  );\n};\n\nexport const CalendarList = ({\n  calendars,\n  onToggleVisibility,\n  onReorder,\n  onRename,\n  onContextMenu,\n  editingId,\n  setEditingId,\n  activeContextMenuCalendarId,\n  isDraggable = true,\n  isEditable = true,\n}: CalendarListProps) => {\n  const [editingName, setEditingName] = useState('');\n  const editInputRef = useRef<HTMLInputElement>(null);\n  const isProcessedRef = useRef(false);\n\n  // Drag state\n  const [draggedCalendarId, setDraggedCalendarId] = useState<string | null>(\n    null\n  );\n  const [dropTarget, setDropTarget] = useState<{\n    id: string;\n    position: 'top' | 'bottom';\n  } | null>(null);\n\n  // Collapsed sources state\n  const [collapsedSources, setCollapsedSources] = useState<\n    Record<string, boolean>\n  >({});\n\n  const handleDragStart = useCallback(\n    (calendar: CalendarType, e: JSX.TargetedDragEvent<HTMLElement>) => {\n      if (editingId || !isDraggable) {\n        e.preventDefault();\n        return;\n      }\n      setDraggedCalendarId(calendar.id);\n\n      const dragData = {\n        calendarId: calendar.id,\n        calendarName: calendar.name,\n        calendarColors: calendar.colors,\n        calendarIcon: calendar.icon,\n      };\n      if (e.dataTransfer) {\n        e.dataTransfer.setData(\n          'application/x-dayflow-calendar',\n          JSON.stringify(dragData)\n        );\n        e.dataTransfer.effectAllowed = 'copy';\n      }\n    },\n    [editingId, isDraggable]\n  );\n\n  const handleDragEnd = useCallback(() => {\n    setDraggedCalendarId(null);\n    setDropTarget(null);\n  }, []);\n\n  const handleDragOver = useCallback(\n    (e: JSX.TargetedDragEvent<HTMLElement>, targetId: string) => {\n      e.preventDefault();\n      if (draggedCalendarId === targetId) {\n        setDropTarget(null);\n        return;\n      }\n\n      const targetIndex = calendars.findIndex(c => c.id === targetId);\n      const isLast = targetIndex === calendars.length - 1;\n\n      const rect = e.currentTarget.getBoundingClientRect();\n      const isTopHalf = e.clientY < rect.top + rect.height / 2;\n\n      if (isLast) {\n        setDropTarget({\n          id: targetId,\n          position: isTopHalf ? 'top' : 'bottom',\n        });\n      } else {\n        setDropTarget({\n          id: targetId,\n          position: 'top',\n        });\n      }\n    },\n    [draggedCalendarId, calendars]\n  );\n\n  const handleDragLeave = useCallback(() => {\n    setDropTarget(null);\n  }, []);\n\n  const handleDrop = useCallback(\n    (targetCalendar: CalendarType) => {\n      if (!draggedCalendarId || !dropTarget) return;\n      if (draggedCalendarId === targetCalendar.id) return;\n\n      const fromIndex = calendars.findIndex(c => c.id === draggedCalendarId);\n      let toIndex = calendars.findIndex(c => c.id === targetCalendar.id);\n\n      if (dropTarget.position === 'bottom') {\n        toIndex += 1;\n      }\n\n      if (toIndex > fromIndex) {\n        toIndex -= 1;\n      }\n\n      if (fromIndex !== -1 && toIndex !== -1) {\n        onReorder(fromIndex, toIndex);\n      }\n      setDropTarget(null);\n    },\n    [draggedCalendarId, dropTarget, calendars, onReorder]\n  );\n\n  const handleRenameStart = useCallback(\n    (calendar: CalendarType) => {\n      if (!isEditable) return;\n      isProcessedRef.current = false;\n      setEditingId(calendar.id);\n      setEditingName(calendar.name);\n    },\n    [setEditingId, isEditable]\n  );\n\n  const handleRenameSave = useCallback(() => {\n    if (isProcessedRef.current) return;\n    isProcessedRef.current = true;\n\n    if (editingId && editingName.trim()) {\n      const calendar = calendars.find(c => c.id === editingId);\n      if (calendar && calendar.name !== editingName.trim()) {\n        onRename(editingId, editingName.trim());\n      }\n    }\n    setEditingId(null);\n    setEditingName('');\n  }, [editingId, editingName, calendars, onRename, setEditingId]);\n\n  const handleRenameCancel = useCallback(() => {\n    if (isProcessedRef.current) return;\n    isProcessedRef.current = true;\n\n    setEditingId(null);\n    setEditingName('');\n  }, [setEditingId]);\n\n  const handleRenameKeyDown = useCallback(\n    (e: JSX.TargetedKeyboardEvent<HTMLInputElement>) => {\n      if (e.key === 'Enter') {\n        handleRenameSave();\n      } else if (e.key === 'Escape') {\n        handleRenameCancel();\n      }\n    },\n    [handleRenameSave, handleRenameCancel]\n  );\n\n  useEffect(() => {\n    if (editingId && editInputRef.current) {\n      editInputRef.current.focus();\n      editInputRef.current.select();\n    }\n  }, [editingId]);\n\n  useEffect(() => {\n    if (editingId) {\n      const calendar = calendars.find(c => c.id === editingId);\n      if (calendar) {\n        setEditingName(calendar.name);\n      }\n    }\n  }, [editingId, calendars]);\n\n  const toggleSource = useCallback((source: string) => {\n    setCollapsedSources(prev => ({ ...prev, [source]: !prev[source] }));\n  }, []);\n\n  // Shared item props (excludes per-item fields)\n  const sharedItemProps = {\n    isDraggable,\n    isEditable,\n    editingId,\n    editingName,\n    setEditingName,\n    editInputRef,\n    isProcessedRef,\n    draggedCalendarId,\n    dropTarget,\n    activeContextMenuCalendarId,\n    onDragStart: handleDragStart,\n    onDragEnd: handleDragEnd,\n    onDragOver: handleDragOver,\n    onDragLeave: handleDragLeave,\n    onDrop: handleDrop,\n    onContextMenu,\n    onToggleVisibility,\n    onRenameStart: handleRenameStart,\n    onRenameSave: handleRenameSave,\n    onRenameKeyDown: handleRenameKeyDown,\n    setEditingId,\n  };\n\n  // Check if any calendar has a source\n  const hasSources = calendars.some(c => c.source);\n\n  if (!hasSources) {\n    // Flat list (original behaviour)\n    return (\n      <div className='df-sidebar-list-shell'>\n        <ul className='df-sidebar-list'>\n          {calendars.map(calendar => (\n            <CalendarItem\n              key={calendar.id}\n              calendar={calendar}\n              {...sharedItemProps}\n            />\n          ))}\n        </ul>\n      </div>\n    );\n  }\n\n  // Group calendars by source; calendars without a source go into a null group\n  const groups = new Map<string | null, CalendarType[]>();\n  for (const calendar of calendars) {\n    const key = calendar.source ?? null;\n    if (!groups.has(key)) groups.set(key, []);\n    groups.get(key)!.push(calendar);\n  }\n\n  return (\n    <div className='df-sidebar-list-shell'>\n      {/* Unsourced calendars first, no header */}\n      {groups.has(null) && (\n        <ul className='df-sidebar-list'>\n          {groups.get(null)!.map(calendar => (\n            <CalendarItem\n              key={calendar.id}\n              calendar={calendar}\n              {...sharedItemProps}\n            />\n          ))}\n        </ul>\n      )}\n\n      {/* Sourced calendar groups */}\n      {Array.from(groups.entries())\n        .filter(([source]) => source !== null)\n        .map(([source, groupCalendars]) => {\n          const isCollapsed = collapsedSources[source!];\n          return (\n            <div key={source} className='df-sidebar-source-group'>\n              <button\n                type='button'\n                className='df-sidebar-source-toggle'\n                onClick={() => toggleSource(source!)}\n              >\n                <span className='df-sidebar-source-label'>{source}</span>\n                <ChevronDown\n                  width={13}\n                  height={13}\n                  className='df-sidebar-source-chevron'\n                  data-collapsed={isCollapsed ? 'true' : 'false'}\n                />\n              </button>\n              <div\n                className='df-sidebar-source-panel'\n                data-collapsed={isCollapsed ? 'true' : 'false'}\n              >\n                <div className='df-sidebar-source-panel-inner'>\n                  <ul className='df-sidebar-list'>\n                    {groupCalendars.map(calendar => (\n                      <CalendarItem\n                        key={calendar.id}\n                        calendar={calendar}\n                        {...sharedItemProps}\n                      />\n                    ))}\n                  </ul>\n                </div>\n              </div>\n            </div>\n          );\n        })}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/sidebar/src/components/DeleteCalendarDialog.tsx",
    "content": "import {\n  createPortal,\n  CalendarType,\n  useLocale,\n  LoadingButton,\n} from '@dayflow/core';\nimport { useState } from 'preact/hooks';\n\nimport { CalendarChip } from './CalendarChip';\n\nconst CAL_SENTINEL = '\\u0001C\\u0001';\n\nfunction renderWithChip(template: string, name: string, color: string) {\n  return template\n    .split(CAL_SENTINEL)\n    .flatMap((part, i) =>\n      i === 0\n        ? [part]\n        : [<CalendarChip key={i} name={name} color={color} />, part]\n    );\n}\n\ninterface DeleteCalendarDialogProps {\n  calendarId: string;\n  calendarName: string;\n  calendars: CalendarType[];\n  step: 'initial' | 'confirm_delete';\n  onStepChange: (step: 'initial' | 'confirm_delete') => void;\n  onConfirmDelete: () => void | Promise<void>;\n  onCancel: () => void;\n  onMergeSelect: (targetId: string) => void | Promise<void>;\n}\n\nexport const DeleteCalendarDialog = ({\n  calendarId,\n  calendarName,\n  calendars,\n  step,\n  onStepChange,\n  onConfirmDelete,\n  onCancel,\n  onMergeSelect,\n}: DeleteCalendarDialogProps) => {\n  const [showMergeDropdown, setShowMergeDropdown] = useState(false);\n  const [isLoading, setIsLoading] = useState(false);\n  const { t } = useLocale();\n  const calendarColor =\n    calendars.find(c => c.id === calendarId)?.colors.lineColor ?? '#6b7280';\n\n  const handleMergeSelect = async (id: string) => {\n    if (isLoading) return;\n    setIsLoading(true);\n    try {\n      await onMergeSelect(id);\n      setShowMergeDropdown(false);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleConfirmDelete = async () => {\n    if (isLoading) return;\n    setIsLoading(true);\n    try {\n      await onConfirmDelete();\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  return createPortal(\n    <div className='df-sidebar-overlay'>\n      <div className='df-sidebar-dialog'>\n        {step === 'initial' ? (\n          <>\n            <h2 className='df-sidebar-dialog-title'>\n              {t('deleteCalendar', { calendarName })}\n            </h2>\n            <p className='df-sidebar-dialog-text df-sidebar-dialog-line'>\n              {renderWithChip(\n                t('deleteCalendarMessage', { calendarName: CAL_SENTINEL }),\n                calendarName,\n                calendarColor\n              )}\n            </p>\n            <div className='df-sidebar-dialog-split-actions'>\n              <div className='df-sidebar-field'>\n                <button\n                  type='button'\n                  disabled={isLoading}\n                  onClick={() => setShowMergeDropdown(!showMergeDropdown)}\n                  className='df-sidebar-button df-sidebar-button-secondary'\n                >\n                  {t('merge')}\n                </button>\n                {showMergeDropdown && (\n                  <div className='df-sidebar-dropdown'>\n                    {calendars\n                      .filter(c => c.id !== calendarId)\n                      .map(calendar => (\n                        <div\n                          key={calendar.id}\n                          className='df-sidebar-dropdown-item'\n                          onClick={() => {\n                            handleMergeSelect(calendar.id);\n                          }}\n                        >\n                          <div\n                            className='df-sidebar-swatch'\n                            style={{\n                              backgroundColor: calendar.colors.lineColor,\n                            }}\n                          />\n                          <span className='df-sidebar-dropdown-label'>\n                            {calendar.name || calendar.id}\n                          </span>\n                        </div>\n                      ))}\n                  </div>\n                )}\n              </div>\n              <div className='df-sidebar-button-row'>\n                <button\n                  type='button'\n                  onClick={onCancel}\n                  disabled={isLoading}\n                  className='df-sidebar-button df-sidebar-button-secondary'\n                >\n                  {t('cancel')}\n                </button>\n                <button\n                  type='button'\n                  onClick={() => onStepChange('confirm_delete')}\n                  disabled={isLoading}\n                  className='df-sidebar-button df-sidebar-button-destructive'\n                >\n                  {t('delete')}\n                </button>\n              </div>\n            </div>\n          </>\n        ) : (\n          <>\n            <h2 className='df-sidebar-dialog-title'>\n              {t('confirmDeleteTitle', { calendarName })}\n            </h2>\n            <p className='df-sidebar-dialog-text'>\n              {t('confirmDeleteMessage')}\n            </p>\n            <div className='df-sidebar-dialog-actions'>\n              <button\n                type='button'\n                onClick={onCancel}\n                disabled={isLoading}\n                className='df-sidebar-button df-sidebar-button-secondary'\n              >\n                {t('cancel')}\n              </button>\n              <LoadingButton\n                type='button'\n                onClick={handleConfirmDelete}\n                loading={isLoading}\n                className='df-sidebar-button df-sidebar-button-destructive'\n              >\n                {t('delete')}\n              </LoadingButton>\n            </div>\n          </>\n        )}\n      </div>\n    </div>,\n    document.body\n  );\n};\n"
  },
  {
    "path": "packages/plugins/sidebar/src/components/ImportCalendarDialog.tsx",
    "content": "import {\n  createPortal,\n  CalendarType,\n  useLocale,\n  Check,\n  ChevronsUpDown,\n  LoadingButton,\n} from '@dayflow/core';\nimport { useState, useRef, useEffect } from 'preact/hooks';\n\ninterface ImportCalendarDialogProps {\n  calendars: CalendarType[];\n  filename: string;\n  onConfirm: (targetCalendarId: string) => void | Promise<void>;\n  onCancel: () => void;\n}\n\nexport const NEW_CALENDAR_ID = 'new-calendar';\n\nexport const ImportCalendarDialog = ({\n  calendars,\n  filename,\n  onConfirm,\n  onCancel,\n}: ImportCalendarDialogProps) => {\n  const { t } = useLocale();\n  const [selectedCalendarId, setSelectedCalendarId] = useState<string>(\n    calendars[0]?.id || NEW_CALENDAR_ID\n  );\n  const [isOpen, setIsOpen] = useState(false);\n  const [shouldRender, setShouldRender] = useState(false);\n  const [isLoading, setIsLoading] = useState(false);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n\n  useEffect(() => {\n    if (isOpen) {\n      setShouldRender(true);\n    } else {\n      const timer = setTimeout(() => setShouldRender(false), 200);\n      return () => clearTimeout(timer);\n    }\n  }, [isOpen]);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        dropdownRef.current &&\n        !dropdownRef.current.contains(event.target as Node) &&\n        !triggerRef.current?.contains(event.target as Node)\n      ) {\n        setIsOpen(false);\n      }\n    };\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => document.removeEventListener('mousedown', handleClickOutside);\n  }, []);\n\n  const selectedCalendar = calendars.find(c => c.id === selectedCalendarId);\n  const isNewSelected = selectedCalendarId === NEW_CALENDAR_ID;\n\n  const handleSelect = (id: string) => {\n    setSelectedCalendarId(id);\n    setIsOpen(false);\n  };\n\n  const handleConfirm = async () => {\n    if (isLoading) return;\n    setIsLoading(true);\n    try {\n      await onConfirm(selectedCalendarId);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const renderDropdown = () => {\n    if (!shouldRender) return null;\n\n    const rect = triggerRef.current?.getBoundingClientRect();\n    if (!rect) return null;\n\n    return createPortal(\n      <div\n        ref={dropdownRef}\n        className='df-sidebar-dropdown'\n        style={{\n          top: rect.bottom,\n          left: rect.left,\n          width: rect.width,\n          overscrollBehavior: 'none',\n        }}\n      >\n        <div>\n          {calendars.map(calendar => (\n            <div\n              key={calendar.id}\n              className='df-sidebar-dropdown-item'\n              data-selected={\n                selectedCalendarId === calendar.id ? 'true' : undefined\n              }\n              onClick={() => handleSelect(calendar.id)}\n            >\n              <div\n                className='df-sidebar-swatch'\n                style={{ backgroundColor: calendar.colors.lineColor }}\n              />\n              <span className='df-sidebar-dropdown-label'>\n                {calendar.name || calendar.id}\n              </span>\n              {selectedCalendarId === calendar.id && (\n                <Check className='df-sidebar-dropdown-check' />\n              )}\n            </div>\n          ))}\n          <div className='df-sidebar-dropdown-divider' />\n          <div\n            className='df-sidebar-dropdown-item'\n            data-selected={isNewSelected ? 'true' : undefined}\n            onClick={() => handleSelect(NEW_CALENDAR_ID)}\n          >\n            <span className='df-sidebar-dropdown-label'>\n              {t('newCalendar') || 'New Calendar'}: {filename}\n            </span>\n            {isNewSelected && <Check className='df-sidebar-dropdown-check' />}\n          </div>\n        </div>\n      </div>,\n      document.body\n    );\n  };\n\n  return (\n    <div className='df-sidebar-overlay'>\n      <div className='df-sidebar-dialog'>\n        <h2 className='df-sidebar-dialog-title'>\n          {t('addSchedule') || 'Add Schedule'}\n        </h2>\n        <p className='df-sidebar-dialog-text'>\n          {t('importCalendarMessage') ||\n            'This calendar contains new events. Please select a target calendar.'}\n        </p>\n\n        <div className='df-sidebar-field'>\n          <button\n            ref={triggerRef}\n            type='button'\n            disabled={isLoading}\n            className='df-sidebar-select-trigger'\n            onClick={() => setIsOpen(!isOpen)}\n          >\n            {!isNewSelected && selectedCalendar && (\n              <div\n                className='df-sidebar-swatch'\n                style={{ backgroundColor: selectedCalendar.colors.lineColor }}\n              />\n            )}\n            <span className='df-sidebar-select-value'>\n              {isNewSelected\n                ? `${t('newCalendar')}: ${filename}`\n                : selectedCalendar?.name || selectedCalendar?.id}\n            </span>\n            <ChevronsUpDown className='df-sidebar-select-icon' />\n          </button>\n          {renderDropdown()}\n        </div>\n\n        <div className='df-sidebar-dialog-actions'>\n          <button\n            type='button'\n            onClick={onCancel}\n            disabled={isLoading}\n            className='df-sidebar-button df-sidebar-button-secondary'\n          >\n            {t('cancel') || 'Cancel'}\n          </button>\n          <LoadingButton\n            type='button'\n            onClick={handleConfirm}\n            loading={isLoading}\n            className='df-sidebar-button df-sidebar-button-primary'\n          >\n            {t('ok') || 'OK'}\n          </LoadingButton>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/sidebar/src/components/MergeCalendarDialog.tsx",
    "content": "import { useLocale, LoadingButton } from '@dayflow/core';\nimport { useState } from 'preact/hooks';\n\nimport { CalendarChip } from './CalendarChip';\n\nconst SOURCE_SENTINEL = '\\u0001S\\u0001';\nconst TARGET_SENTINEL = '\\u0001T\\u0001';\n\nfunction renderLine(\n  line: string,\n  source: { name: string; color: string },\n  target: { name: string; color: string }\n) {\n  return line\n    .split(new RegExp(`(${SOURCE_SENTINEL}|${TARGET_SENTINEL})`))\n    .map((part, i) => {\n      if (part === SOURCE_SENTINEL)\n        return <CalendarChip key={i} name={source.name} color={source.color} />;\n      if (part === TARGET_SENTINEL)\n        return <CalendarChip key={i} name={target.name} color={target.color} />;\n      return part;\n    });\n}\n\ninterface MergeCalendarDialogProps {\n  sourceName: string;\n  sourceColor: string;\n  targetName: string;\n  targetColor: string;\n  onConfirm: () => void | Promise<void>;\n  onCancel: () => void;\n}\n\nexport const MergeCalendarDialog = ({\n  sourceName,\n  sourceColor,\n  targetName,\n  targetColor,\n  onConfirm,\n  onCancel,\n}: MergeCalendarDialogProps) => {\n  const { t } = useLocale();\n  const [isLoading, setIsLoading] = useState(false);\n  const source = { name: sourceName, color: sourceColor };\n  const target = { name: targetName, color: targetColor };\n\n  const messageTemplate = t('mergeConfirmMessage', {\n    sourceName: SOURCE_SENTINEL,\n    targetName: TARGET_SENTINEL,\n  });\n  const messageLines = messageTemplate.split('\\n');\n\n  const handleConfirm = async () => {\n    if (isLoading) return;\n    setIsLoading(true);\n    try {\n      await onConfirm();\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  return (\n    <div className='df-sidebar-overlay'>\n      <div className='df-sidebar-dialog'>\n        <h2 className='df-sidebar-dialog-title'>\n          {t('mergeConfirmTitle', { sourceName, targetName })}\n        </h2>\n        <div className='df-sidebar-dialog-lines'>\n          {messageLines.map((line, i) => (\n            <p key={i} className='df-sidebar-dialog-line'>\n              {renderLine(line, source, target)}\n            </p>\n          ))}\n        </div>\n        <div className='df-sidebar-dialog-actions'>\n          <button\n            type='button'\n            onClick={onCancel}\n            disabled={isLoading}\n            className='df-sidebar-button df-sidebar-button-secondary'\n          >\n            {t('cancel')}\n          </button>\n          <LoadingButton\n            type='button'\n            onClick={handleConfirm}\n            loading={isLoading}\n            className='df-sidebar-button df-sidebar-button-destructive'\n          >\n            {t('merge')}\n          </LoadingButton>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/sidebar/src/components/MergeMenuItem.tsx",
    "content": "import {\n  createPortal,\n  ChevronRight,\n  CalendarType,\n  useLocale,\n} from '@dayflow/core';\nimport { useState, useRef, useEffect } from 'preact/hooks';\n\ninterface MergeMenuItemProps {\n  calendars: CalendarType[];\n  currentCalendarId: string;\n  onMergeSelect: (targetId: string) => void;\n}\n\nconst stopPropagation = (e: MouseEvent) => e.stopPropagation();\n\nexport const MergeMenuItem = ({\n  calendars,\n  currentCalendarId,\n  onMergeSelect,\n}: MergeMenuItemProps) => {\n  const { t } = useLocale();\n  const [isHovered, setIsHovered] = useState(false);\n  const itemRef = useRef<HTMLDivElement>(null);\n  const submenuRef = useRef<HTMLDivElement>(null);\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const timeoutRef = useRef<ReturnType<typeof setTimeout>>();\n\n  const handleMouseEnter = () => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    if (itemRef.current) {\n      const rect = itemRef.current.getBoundingClientRect();\n      setPosition({ x: rect.right, y: rect.top });\n    }\n    setIsHovered(true);\n  };\n\n  const handleMouseLeave = () => {\n    timeoutRef.current = setTimeout(() => {\n      setIsHovered(false);\n    }, 100);\n  };\n\n  useEffect(() => {\n    const el = submenuRef.current;\n    if (el) {\n      el.addEventListener('mousedown', stopPropagation);\n      return () => {\n        el.removeEventListener('mousedown', stopPropagation);\n      };\n    }\n  }, [isHovered]);\n\n  const availableCalendars = calendars.filter(c => c.id !== currentCalendarId);\n\n  if (availableCalendars.length === 0) return null;\n\n  return (\n    <>\n      <div\n        ref={itemRef}\n        className='df-sidebar-submenu-trigger'\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n      >\n        <span>{t('merge')}</span>\n        <ChevronRight className='df-sidebar-submenu-chevron' />\n      </div>\n      {isHovered &&\n        createPortal(\n          <div\n            ref={submenuRef}\n            data-submenu-content='true'\n            className='df-portal df-sidebar-submenu-content df-context-menu-sub-content'\n            style={{ top: position.y, left: position.x }}\n            onMouseEnter={handleMouseEnter}\n            onMouseLeave={handleMouseLeave}\n            onMouseDown={e => e.stopPropagation()}\n          >\n            {availableCalendars.map(calendar => (\n              <div\n                key={calendar.id}\n                className='df-sidebar-submenu-item'\n                onClick={e => {\n                  e.stopPropagation();\n                  onMergeSelect(calendar.id);\n                }}\n              >\n                <div\n                  className='df-sidebar-swatch'\n                  style={{ backgroundColor: calendar.colors.lineColor }}\n                />\n                <span className='df-sidebar-dropdown-label'>\n                  {calendar.name || calendar.id}\n                </span>\n              </div>\n            ))}\n          </div>,\n          document.body\n        )}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/sidebar/src/components/SidebarHeader.tsx",
    "content": "import { PanelRightClose, PanelRightOpen, useLocale } from '@dayflow/core';\n\ninterface SidebarHeaderProps {\n  isCollapsed: boolean;\n  onCollapseToggle: () => void;\n}\n\nexport const SidebarHeader = ({\n  isCollapsed,\n  onCollapseToggle,\n}: SidebarHeaderProps) => {\n  const { t } = useLocale();\n  return (\n    <div className='df-sidebar-header'>\n      <button\n        type='button'\n        aria-label={isCollapsed ? t('expandSidebar') : t('collapseSidebar')}\n        className='df-sidebar-toggle'\n        onClick={onCollapseToggle}\n      >\n        {isCollapsed ? (\n          <PanelRightClose className='df-sidebar-toggle-icon' />\n        ) : (\n          <PanelRightOpen className='df-sidebar-toggle-icon' />\n        )}\n      </button>\n      {!isCollapsed && (\n        <div className='df-sidebar-header-main'>\n          <span className='df-sidebar-header-title'>{t('calendars')}</span>\n        </div>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/sidebar/src/components/SubscribeCalendarDialog.tsx",
    "content": "import { useLocale } from '@dayflow/core';\nimport { useState } from 'preact/hooks';\n\ninterface SubscribeCalendarDialogProps {\n  onSubscribe: (url: string) => Promise<void>;\n  onCancel: () => void;\n}\n\nexport const SubscribeCalendarDialog = ({\n  onSubscribe,\n  onCancel,\n}: SubscribeCalendarDialogProps) => {\n  const { t } = useLocale();\n  const [url, setUrl] = useState('');\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n\n  const handleSubmit = async () => {\n    const trimmed = url.trim();\n    if (!trimmed) return;\n    setError(null);\n    setLoading(true);\n    try {\n      await onSubscribe(trimmed);\n    } catch (err: unknown) {\n      if ((err as Error).message === 'DUPLICATE_URL') {\n        setError(\n          t('calendarAlreadySubscribed') || 'This URL is already subscribed'\n        );\n      } else {\n        setError(t('subscribeError') || 'Failed to subscribe to calendar');\n      }\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleKeyDown = (e: KeyboardEvent) => {\n    if (e.key === 'Enter') handleSubmit();\n    if (e.key === 'Escape') onCancel();\n  };\n\n  return (\n    <div className='df-sidebar-overlay'>\n      <div className='df-sidebar-dialog df-sidebar-dialog-wide'>\n        <h2 className='df-sidebar-dialog-title'>\n          {t('subscribeCalendarTitle')}\n        </h2>\n\n        <div>\n          <div className='df-sidebar-subscribe-row'>\n            <label className='df-sidebar-subscribe-label'>\n              {t('calendarUrl')}\n            </label>\n            <input\n              type='url'\n              value={url}\n              onInput={e => setUrl((e.target as HTMLInputElement).value)}\n              onKeyDown={handleKeyDown}\n              placeholder={t('calendarUrlPlaceholder')}\n              disabled={loading}\n              autoFocus\n              className='df-sidebar-subscribe-input'\n            />\n          </div>\n          {error && <p className='df-sidebar-error'>{error}</p>}\n        </div>\n\n        <div className='df-sidebar-dialog-actions'>\n          <button\n            type='button'\n            onClick={onCancel}\n            disabled={loading}\n            className='df-sidebar-button df-sidebar-button-secondary'\n          >\n            {t('cancel')}\n          </button>\n          <button\n            type='button'\n            onClick={handleSubmit}\n            disabled={loading || !url.trim()}\n            className='df-sidebar-button df-sidebar-button-primary'\n          >\n            {loading ? t('fetchingCalendar') : t('subscribe')}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/sidebar/src/index.ts",
    "content": "export * from './plugin';\n"
  },
  {
    "path": "packages/plugins/sidebar/src/plugin.ts",
    "content": "import {\n  CalendarPlugin,\n  ICalendarApp,\n  CalendarType,\n  Event,\n  TNode,\n  CreateCalendarDialogProps,\n  SidebarHeaderSlotArgs,\n  registerSidebarImplementation,\n  SidebarBridgeReturn,\n  normalizeCssWidth,\n  getCalendarColorsForHex,\n  generateUniKey,\n  useLocale,\n  ContentSlot,\n  CreateCalendarDialog,\n} from '@dayflow/core';\nimport { h } from 'preact';\nimport { useState, useCallback, useMemo, useEffect } from 'preact/hooks';\n\nimport DefaultCalendarSidebar from './DefaultCalendarSidebar';\n\nconst DEFAULT_SIDEBAR_WIDTH = '240px';\n\nconst COLORS = [\n  '#ea426b',\n  '#f19a38',\n  '#f7cf46',\n  '#83d754',\n  '#51aaf2',\n  '#b672d0',\n  '#957e5e',\n];\n\nexport type { SidebarHeaderSlotArgs };\n\nexport interface CalendarSidebarRenderProps {\n  app: ICalendarApp;\n  calendars: CalendarType[];\n  toggleCalendarVisibility: (calendarId: string, visible: boolean) => void;\n  toggleAll: (visible: boolean) => void;\n  isCollapsed: boolean;\n  setCollapsed: (collapsed: boolean) => void;\n  showEventDots?: boolean;\n  renderCalendarContextMenu?: (\n    calendar: CalendarType,\n    onClose: () => void\n  ) => TNode;\n  renderSidebarHeader?: (args: SidebarHeaderSlotArgs) => TNode;\n  createCalendarMode?: 'inline' | 'modal';\n  renderCreateCalendarDialog?: (props: CreateCalendarDialogProps) => TNode;\n  editingCalendarId?: string | null;\n  setEditingCalendarId?: (id: string | null) => void;\n  onCreateCalendar?: () => void;\n  onSubscribeCalendar?: (\n    calendar: CalendarType,\n    events: Event[]\n  ) => Promise<void>;\n  onLoadSubscription?: (calendar: CalendarType) => Promise<void>;\n  onReorder?: (calendars: CalendarType[]) => void | Promise<void>;\n  componentsOrder?: ('calendarList' | 'miniCalendar')[];\n}\nexport interface SidebarPluginConfig {\n  width?: number | string;\n  miniWidth?: string;\n  initialCollapsed?: boolean;\n  showEventDots?: boolean;\n  createCalendarMode?: 'inline' | 'modal';\n  render?: (props: CalendarSidebarRenderProps) => TNode;\n  renderCalendarContextMenu?: (\n    calendar: CalendarType,\n    onClose: () => void\n  ) => TNode;\n  renderSidebarHeader?: (args: SidebarHeaderSlotArgs) => TNode;\n  renderCreateCalendarDialog?: (props: CreateCalendarDialogProps) => TNode;\n  onSubscribeCalendar?: (\n    calendar: CalendarType,\n    events: Event[]\n  ) => Promise<void>;\n  onLoadSubscription?: (calendar: CalendarType) => Promise<void>;\n  onReorder?: (calendars: CalendarType[]) => void | Promise<void>;\n  componentsOrder?: ('calendarList' | 'miniCalendar')[];\n}\n\n/** Control handle written by the Preact bridge on every render cycle. */\ninterface SidebarControlRef {\n  setCollapsed: ((collapsed: boolean) => void) | null;\n  currentCollapsed: boolean;\n}\n\n/**\n * Programmatic API returned by `app.getPlugin<SidebarService>('sidebar')`.\n *\n * @example\n * const sidebar = app.getPlugin<SidebarService>('sidebar');\n * sidebar?.collapse();\n */\nexport interface SidebarService {\n  /** Collapse the sidebar. No-ops if the sidebar has not yet rendered. */\n  collapse: () => void;\n  /** Expand the sidebar. No-ops if the sidebar has not yet rendered. */\n  expand: () => void;\n  /** Set collapsed state explicitly. */\n  setCollapsed: (collapsed: boolean) => void;\n  /** Returns the current collapsed state. */\n  isCollapsed: () => boolean;\n}\n\nexport function createSidebarPlugin(\n  config: SidebarPluginConfig = {}\n): CalendarPlugin {\n  const controlRef: SidebarControlRef = {\n    setCollapsed: null,\n    currentCollapsed: config.initialCollapsed ?? false,\n  };\n\n  const sidebarService: SidebarService = {\n    collapse: () => controlRef.setCollapsed?.(true),\n    expand: () => controlRef.setCollapsed?.(false),\n    setCollapsed: (collapsed: boolean) => controlRef.setCollapsed?.(collapsed),\n    isCollapsed: () => controlRef.currentCollapsed,\n  };\n\n  return {\n    name: 'sidebar',\n    config,\n    api: sidebarService,\n    install(_app: ICalendarApp) {\n      const sidebarWidth = normalizeCssWidth(\n        config.width,\n        DEFAULT_SIDEBAR_WIDTH\n      );\n\n      registerSidebarImplementation(\n        (app: ICalendarApp): SidebarBridgeReturn => {\n          const { t } = useLocale();\n\n          const [isCollapsed, setIsCollapsed] = useState(\n            config.initialCollapsed ?? false\n          );\n\n          // Keep the programmatic API in sync with the current render state.\n          controlRef.setCollapsed = setIsCollapsed;\n          controlRef.currentCollapsed = isCollapsed;\n\n          const [sidebarVersion, setSidebarVersion] = useState(0);\n          const [editingCalendarId, setEditingCalendarId] = useState<\n            string | null\n          >(null);\n          const [showCreateDialog, setShowCreateDialog] = useState(false);\n          const isEditable = app.canMutateFromUI();\n\n          const refreshSidebar = useCallback(() => {\n            setSidebarVersion(prev => prev + 1);\n          }, []);\n\n          useEffect(\n            () =>\n              app.subscribe(() => {\n                refreshSidebar();\n              }),\n            [app, refreshSidebar]\n          );\n\n          const calendars = useMemo(\n            () => app.getCalendars(),\n            [app, sidebarVersion]\n          );\n\n          const handleToggleCalendarVisibility = useCallback(\n            (calendarId: string, visible: boolean) => {\n              app.setCalendarVisibility(calendarId, visible);\n              refreshSidebar();\n            },\n            [app, refreshSidebar]\n          );\n\n          const handleToggleAllCalendars = useCallback(\n            (visible: boolean) => {\n              app.setAllCalendarsVisibility(visible);\n              refreshSidebar();\n            },\n            [app, refreshSidebar]\n          );\n\n          const handleCreateCalendar = useCallback(() => {\n            if (!isEditable) return;\n\n            const createMode = config.createCalendarMode || 'inline';\n\n            if (createMode === 'modal') {\n              setShowCreateDialog(true);\n              return;\n            }\n\n            const randomColor =\n              COLORS[Math.floor(Math.random() * COLORS.length)];\n            const { colors, darkColors } = getCalendarColorsForHex(randomColor);\n            const newId = generateUniKey();\n\n            const newCalendar: CalendarType = {\n              id: newId,\n              name: t('untitled'),\n              colors,\n              darkColors,\n              isVisible: true,\n              isDefault: false,\n            };\n\n            app.createCalendar(newCalendar);\n            setEditingCalendarId(newId);\n            refreshSidebar();\n          }, [app, isEditable, t, refreshSidebar]);\n\n          useEffect(() => {\n            if (isEditable) return;\n\n            setShowCreateDialog(false);\n            setEditingCalendarId(null);\n          }, [isEditable]);\n\n          const sidebarProps: CalendarSidebarRenderProps = useMemo(\n            () => ({\n              app,\n              calendars,\n              toggleCalendarVisibility: handleToggleCalendarVisibility,\n              toggleAll: handleToggleAllCalendars,\n              isCollapsed,\n              setCollapsed: setIsCollapsed,\n              showEventDots: config.showEventDots,\n              renderCalendarContextMenu: config.renderCalendarContextMenu,\n              renderSidebarHeader: config.renderSidebarHeader,\n              createCalendarMode: config.createCalendarMode,\n              renderCreateCalendarDialog: config.renderCreateCalendarDialog,\n              editingCalendarId,\n              setEditingCalendarId,\n              onCreateCalendar: handleCreateCalendar,\n              onSubscribeCalendar: config.onSubscribeCalendar,\n              onLoadSubscription: config.onLoadSubscription,\n              onReorder: config.onReorder,\n              componentsOrder: config.componentsOrder,\n            }),\n            [\n              app,\n              calendars,\n              handleToggleCalendarVisibility,\n              handleToggleAllCalendars,\n              isCollapsed,\n              handleCreateCalendar,\n              editingCalendarId,\n              config.showEventDots,\n              config.renderCalendarContextMenu,\n              config.renderSidebarHeader,\n              config.createCalendarMode,\n              config.renderCreateCalendarDialog,\n              config.onSubscribeCalendar,\n              config.onLoadSubscription,\n              config.onReorder,\n              config.componentsOrder,\n            ]\n          );\n\n          const renderContent = () => {\n            if (config.render) {\n              return h(ContentSlot, {\n                generatorName: 'sidebar',\n                generatorArgs: sidebarProps,\n              });\n            }\n            return h(DefaultCalendarSidebar, {\n              ...sidebarProps,\n            });\n          };\n\n          const renderExtraContent = () => {\n            if (!isEditable || !showCreateDialog) return null;\n\n            const onClose = () => setShowCreateDialog(false);\n            const onCreate = async (newCalendar: unknown) => {\n              await app.createCalendar(newCalendar as CalendarType);\n              setShowCreateDialog(false);\n              refreshSidebar();\n            };\n\n            const generatorArgs = {\n              onClose,\n              onCreate,\n              app,\n            };\n\n            return h(ContentSlot, {\n              generatorName: 'createCalendarDialog',\n              generatorArgs,\n              defaultContent: h(CreateCalendarDialog, {\n                onClose,\n                onCreate,\n                app,\n              }),\n            });\n          };\n\n          return {\n            enabled: true,\n            width: sidebarWidth,\n            isCollapsed,\n            toggleCollapsed: () => setIsCollapsed(prev => !prev),\n            miniWidth: config.miniWidth ?? '50px',\n            content: renderContent(),\n            extraContent: renderExtraContent(),\n            safeAreaLeft: 0,\n          };\n        }\n      );\n    },\n  };\n}\n"
  },
  {
    "path": "packages/plugins/sidebar/src/styles/sidebar.css",
    "content": "@layer components {\n  .df-sidebar {\n    display: flex;\n    min-width: 0;\n    height: 100%;\n    flex-direction: column;\n    border-right: 1px solid var(--df-color-border);\n    background-color: var(--df-color-background);\n  }\n\n  .df-sidebar-header {\n    display: flex;\n    min-height: 2.5rem;\n    align-items: center;\n    padding: 0.25rem 0.5rem;\n  }\n\n  .df-sidebar-toggle {\n    display: inline-flex;\n    height: 2rem;\n    width: 2rem;\n    align-items: center;\n    justify-content: center;\n    border: 0;\n    border-radius: 0.5rem;\n    background: transparent;\n    color: var(--df-color-muted-foreground);\n    transition:\n      background-color 120ms ease,\n      color 120ms ease;\n  }\n\n  .df-sidebar-toggle:hover {\n    background: var(--df-color-hover);\n    color: var(--df-color-foreground);\n  }\n\n  .df-sidebar-toggle-icon {\n    height: 1rem;\n    width: 1rem;\n  }\n\n  .df-sidebar-header-main {\n    display: flex;\n    min-width: 0;\n    flex: 1 1 auto;\n    align-items: center;\n    justify-content: space-between;\n    margin-left: 0.75rem;\n  }\n\n  .df-sidebar-header-title {\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 600;\n    color: var(--df-color-foreground);\n  }\n\n  .df-sidebar-list-shell {\n    flex: 1 1 auto;\n    overflow-y: auto;\n    padding: 0 0.5rem 0.75rem;\n  }\n\n  .df-sidebar-list {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    gap: 0.25rem;\n  }\n\n  .df-sidebar-list-item {\n    position: relative;\n  }\n\n  .df-sidebar-drop-indicator {\n    pointer-events: none;\n    position: absolute;\n    left: 0;\n    right: 0;\n    z-index: 10;\n    height: 2px;\n    background: var(--df-color-primary);\n  }\n\n  .df-sidebar-drop-indicator[data-position='top'] {\n    top: 0;\n  }\n\n  .df-sidebar-drop-indicator[data-position='bottom'] {\n    bottom: 0;\n  }\n\n  .df-sidebar-drag-shell {\n    border-radius: 0.5rem;\n    transition:\n      opacity 120ms ease,\n      transform 120ms ease;\n  }\n\n  .df-sidebar-drag-shell[data-dragging='true'] {\n    opacity: 0.5;\n  }\n\n  .df-sidebar-drag-shell[data-draggable='true'] {\n    cursor: grab;\n  }\n\n  .df-sidebar-drag-shell[data-draggable='false'] {\n    cursor: default;\n  }\n\n  .df-sidebar-row {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    border-radius: 0.5rem;\n    padding: 0.5rem;\n    transition:\n      background-color 120ms ease,\n      color 120ms ease;\n  }\n\n  .df-sidebar-row:hover,\n  .df-sidebar-row[data-active='true'] {\n    background: var(--df-color-hover);\n  }\n\n  .df-sidebar-checkbox {\n    flex-shrink: 0;\n  }\n\n  .df-sidebar-icon-badge {\n    display: inline-flex;\n    height: 1.25rem;\n    width: 1.25rem;\n    flex-shrink: 0;\n    align-items: center;\n    justify-content: center;\n    border-radius: 9999px;\n    font-size: 0.75rem;\n    line-height: 1rem;\n    font-weight: 600;\n    color: #fff;\n  }\n\n  .df-sidebar-rename-input,\n  .df-sidebar-name {\n    min-width: 0;\n    flex: 1 1 auto;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n  }\n\n  .df-sidebar-rename-input {\n    height: 1.25rem;\n    border: 0;\n    border-radius: 0.375rem;\n    background: transparent;\n    color: var(--df-color-foreground);\n    padding: 0;\n    outline: none;\n  }\n\n  .df-sidebar-name {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    color: color-mix(in srgb, var(--df-color-foreground) 88%, transparent);\n    padding-left: 0.25rem;\n    transition: color 120ms ease;\n  }\n\n  .df-sidebar-row:hover .df-sidebar-name {\n    color: var(--df-color-foreground);\n  }\n\n  .df-sidebar-status-icon {\n    flex-shrink: 0;\n    margin-left: 0.25rem;\n  }\n\n  .df-sidebar-status-icon-error {\n    color: var(--df-color-destructive);\n  }\n\n  .df-sidebar-status-icon-subscription {\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-sidebar-source-group {\n    margin-top: 0.25rem;\n  }\n\n  .df-sidebar-source-toggle {\n    display: flex;\n    width: 100%;\n    align-items: center;\n    justify-content: space-between;\n    border: 0;\n    border-radius: 0.5rem;\n    background: transparent;\n    padding: 0.375rem 0.5rem;\n    font-size: 0.75rem;\n    line-height: 1rem;\n    font-weight: 500;\n    color: var(--df-color-muted-foreground);\n    transition:\n      background-color 120ms ease,\n      color 120ms ease;\n  }\n\n  .df-sidebar-source-toggle:hover {\n    background: var(--df-color-hover);\n  }\n\n  .df-sidebar-source-label {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .df-sidebar-source-chevron {\n    margin-left: 0.25rem;\n    flex-shrink: 0;\n    transition: transform 200ms ease;\n  }\n\n  .df-sidebar-source-chevron[data-collapsed='true'] {\n    transform: rotate(-90deg);\n  }\n\n  .df-sidebar-source-panel {\n    display: grid;\n    transition: grid-template-rows 200ms ease;\n  }\n\n  .df-sidebar-source-panel[data-collapsed='true'] {\n    grid-template-rows: 0fr;\n  }\n\n  .df-sidebar-source-panel[data-collapsed='false'] {\n    grid-template-rows: 1fr;\n  }\n\n  .df-sidebar-source-panel-inner {\n    overflow: hidden;\n  }\n\n  .df-sidebar-mini-calendar {\n    border-top: 1px solid var(--df-color-border);\n  }\n\n  .df-sidebar-chip {\n    display: inline-flex;\n    align-items: center;\n    border-radius: 9999px;\n    margin-inline: 0.125rem;\n    padding: 0.125rem 0.5rem;\n    font-size: 0.75rem;\n    line-height: 1rem;\n    font-weight: 500;\n  }\n\n  .df-sidebar-overlay {\n    position: fixed;\n    inset: 0;\n    z-index: 9999;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: rgb(0 0 0 / 0.5);\n  }\n\n  .df-sidebar-dialog {\n    width: min(100%, 32rem);\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.75rem;\n    background: var(--df-color-card);\n    color: var(--df-color-card-foreground);\n    box-shadow:\n      0 20px 25px -5px rgb(0 0 0 / 0.15),\n      0 10px 10px -5px rgb(0 0 0 / 0.08);\n    padding: 1.5rem;\n  }\n\n  .df-sidebar-dialog-wide {\n    width: min(100%, 36rem);\n  }\n\n  .df-sidebar-dialog-title {\n    color: var(--df-color-foreground);\n    font-size: 1.125rem;\n    line-height: 1.75rem;\n    font-weight: 600;\n  }\n\n  .df-sidebar-dialog-text,\n  .df-sidebar-dialog-lines {\n    margin-top: 0.75rem;\n    color: color-mix(in srgb, var(--df-color-foreground) 75%, transparent);\n    font-size: 0.875rem;\n    line-height: 1.35rem;\n  }\n\n  .df-sidebar-dialog-lines {\n    display: flex;\n    flex-direction: column;\n    gap: 0.25rem;\n  }\n\n  .df-sidebar-dialog-line {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    gap: 0.125rem;\n  }\n\n  .df-sidebar-dialog-actions,\n  .df-sidebar-dialog-split-actions {\n    margin-top: 1.5rem;\n    display: flex;\n    align-items: center;\n    gap: 0.75rem;\n  }\n\n  .df-sidebar-dialog-actions {\n    justify-content: flex-end;\n  }\n\n  .df-sidebar-dialog-split-actions {\n    justify-content: space-between;\n  }\n\n  .df-sidebar-button-row {\n    display: flex;\n    align-items: center;\n    gap: 0.75rem;\n  }\n\n  .df-sidebar-button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    border: 1px solid transparent;\n    border-radius: 0.5rem;\n    padding: 0.5rem 0.875rem;\n    font-size: 0.75rem;\n    line-height: 1rem;\n    font-weight: 500;\n    transition:\n      background-color 120ms ease,\n      border-color 120ms ease,\n      color 120ms ease,\n      opacity 120ms ease;\n  }\n\n  .df-sidebar-button-secondary {\n    border-color: var(--df-color-border);\n    background: var(--df-color-background);\n    color: color-mix(in srgb, var(--df-color-foreground) 85%, transparent);\n  }\n\n  .df-sidebar-button-secondary:hover {\n    background: var(--df-color-hover);\n  }\n\n  .df-sidebar-button-primary {\n    background: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n  }\n\n  .df-sidebar-button-primary:hover {\n    background: color-mix(in srgb, var(--df-color-primary) 88%, black 12%);\n  }\n\n  .df-sidebar-button-destructive {\n    background: var(--df-color-destructive);\n    color: var(--df-color-destructive-foreground);\n  }\n\n  .df-sidebar-button-destructive:hover {\n    background: color-mix(in srgb, var(--df-color-destructive) 88%, black 12%);\n  }\n\n  .df-sidebar-button:disabled {\n    cursor: not-allowed;\n    opacity: 0.5;\n  }\n\n  .df-sidebar-dropdown {\n    position: absolute;\n    top: calc(100% + 0.25rem);\n    left: 0;\n    z-index: 10;\n    min-width: 100%;\n    max-height: 15rem;\n    overflow-y: auto;\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.5rem;\n    background: var(--df-color-card);\n    box-shadow: 0 10px 20px rgb(0 0 0 / 0.12);\n  }\n\n  .df-sidebar-dropdown-item {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    padding: 0.5rem 0.875rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    color: color-mix(in srgb, var(--df-color-foreground) 88%, transparent);\n    transition:\n      background-color 120ms ease,\n      color 120ms ease;\n  }\n\n  .df-sidebar-dropdown-item:hover,\n  .df-sidebar-dropdown-item[data-selected='true'] {\n    background: color-mix(in srgb, var(--df-color-primary) 10%, transparent);\n  }\n\n  .df-sidebar-dropdown-divider {\n    margin: 0.25rem 0;\n    border-top: 1px solid var(--df-color-border);\n  }\n\n  .df-sidebar-swatch {\n    height: 0.75rem;\n    width: 0.75rem;\n    flex-shrink: 0;\n    border-radius: 0.25rem;\n  }\n\n  .df-sidebar-dropdown-label {\n    min-width: 0;\n    flex: 1 1 auto;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .df-sidebar-dropdown-check {\n    margin-left: 0.25rem;\n    height: 1rem;\n    width: 1rem;\n    flex-shrink: 0;\n    color: var(--df-color-primary);\n  }\n\n  .df-sidebar-field {\n    position: relative;\n    margin-top: 0.75rem;\n  }\n\n  .df-sidebar-select-trigger {\n    display: flex;\n    width: 100%;\n    align-items: center;\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.5rem;\n    background: var(--df-color-background);\n    padding: 0.5rem 0.75rem;\n    box-shadow: 0 1px 2px rgb(15 23 42 / 0.06);\n    transition:\n      background-color 120ms ease,\n      border-color 120ms ease;\n  }\n\n  .df-sidebar-select-trigger:hover {\n    background: var(--df-color-hover);\n  }\n\n  .df-sidebar-select-value {\n    min-width: 0;\n    flex: 1 1 auto;\n    overflow: hidden;\n    text-align: left;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    color: color-mix(in srgb, var(--df-color-foreground) 88%, transparent);\n  }\n\n  .df-sidebar-select-icon {\n    margin-left: 0.5rem;\n    height: 1rem;\n    width: 1rem;\n    flex-shrink: 0;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-sidebar-subscribe-row {\n    margin-top: 1rem;\n    display: flex;\n    align-items: center;\n    gap: 0.75rem;\n  }\n\n  .df-sidebar-subscribe-label {\n    flex-shrink: 0;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    color: color-mix(in srgb, var(--df-color-foreground) 88%, transparent);\n  }\n\n  .df-sidebar-subscribe-input {\n    flex: 1 1 auto;\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.5rem;\n    background: var(--df-color-background);\n    padding: 0.5rem 0.75rem;\n    color: var(--df-color-foreground);\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    outline: none;\n    transition:\n      border-color 120ms ease,\n      box-shadow 120ms ease,\n      background-color 120ms ease;\n  }\n\n  .df-sidebar-subscribe-input:focus {\n    border-color: var(--df-color-primary);\n    box-shadow:\n      0 0 0 0px var(--df-color-background),\n      0 0 0 2px var(--df-color-primary);\n  }\n\n  .df-sidebar-subscribe-input:disabled {\n    opacity: 0.5;\n  }\n\n  .df-sidebar-error {\n    margin-top: 0.5rem;\n    font-size: 0.75rem;\n    line-height: 1rem;\n    color: var(--df-color-destructive);\n  }\n\n  .df-sidebar-submenu-trigger,\n  .df-sidebar-submenu-item {\n    display: flex;\n    align-items: center;\n    width: 100%;\n    border-radius: 0.25rem;\n    font-size: 0.75rem;\n    line-height: 1rem;\n    color: var(--df-color-foreground);\n    transition:\n      background-color 120ms ease,\n      color 120ms ease;\n  }\n\n  .df-sidebar-submenu-trigger {\n    justify-content: space-between;\n    padding: 0.125rem 0.75rem;\n  }\n\n  .df-sidebar-submenu-item {\n    cursor: pointer;\n    gap: 0.5rem;\n    padding: 0.25rem 0.75rem;\n    user-select: none;\n  }\n\n  .df-sidebar-submenu-trigger:hover,\n  .df-sidebar-submenu-item:hover {\n    background: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n  }\n\n  .df-sidebar-submenu-content {\n    min-width: 12rem;\n    transform: translateX(0.5rem);\n  }\n\n  .df-sidebar-submenu-chevron {\n    height: 1rem;\n    width: 1rem;\n    flex-shrink: 0;\n    margin-left: 0.5rem;\n  }\n\n  .df-sidebar-context-menu-calendar {\n    min-width: 16rem;\n  }\n\n  .df-sidebar-context-menu-sidebar {\n    min-width: max-content;\n  }\n\n  .df-sidebar-color-picker-layer {\n    position: fixed;\n    inset: 0;\n    z-index: 50;\n  }\n\n  .df-sidebar-color-picker-anchor {\n    position: absolute;\n  }\n\n  .df-sidebar-color-picker-card {\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.75rem;\n    background: var(--df-color-card);\n    padding: 0.75rem;\n    box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.15);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/sidebar/src/styles/tailwind-components.css",
    "content": "/* styles.components.css — DayFlow sidebar plugin component styles only. */\n@layer theme, base, df-theme, components, utilities;\n\n@import 'tailwindcss/theme' layer(theme);\n@import './sidebar.css';\n"
  },
  {
    "path": "packages/plugins/sidebar/src/styles/tailwind.css",
    "content": "/* styles.css — standalone DayFlow sidebar plugin styles\n *\n * This file ships semantic plugin CSS only and avoids emitting atomic\n * utility classes into downstream applications.\n */\n@layer theme, base, df-theme, components, utilities;\n\n@import 'tailwindcss/theme' layer(theme);\n@import './sidebar.css';\n"
  },
  {
    "path": "packages/plugins/sidebar/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"declarationDir\": \"dist/types\",\n    \"declarationMap\": false,\n    \"sourceMap\": false,\n    \"module\": \"ESNext\",\n    \"target\": \"ES2015\",\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": false\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"**/*.test.ts\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.ts\",\n    \"**/*.spec.tsx\"\n  ]\n}\n"
  },
  {
    "path": "packages/plugins/sidebar/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"declaration\": true,\n    \"declarationMap\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/react/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/react/README.md",
    "content": "# DayFlow React\n\nA flexible and feature-rich React calendar component library with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/react?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/react)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/dayflow/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/dayflow)](https://github.com/dayflow-js/dayflow/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/dayflow/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n"
  },
  {
    "path": "packages/react/package.json",
    "content": "{\n  \"name\": \"@dayflow/react\",\n  \"version\": \"3.6.2\",\n  \"description\": \"React adapter for DayFlow calendar\",\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"rollup -c\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"prebuild\": \"rimraf dist\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@dayflow/core\": \"workspace:*\",\n    \"@rollup/plugin-commonjs\": \"catalog:\",\n    \"@rollup/plugin-node-resolve\": \"catalog:\",\n    \"@rollup/plugin-terser\": \"catalog:\",\n    \"@rollup/plugin-typescript\": \"catalog:\",\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-peer-deps-external\": \"catalog:\",\n    \"typescript\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"@dayflow/core\": \"workspace:*\",\n    \"react\": \">=18.0.0\",\n    \"react-dom\": \">=18.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react/rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport terser from '@rollup/plugin-terser';\nimport typescript from '@rollup/plugin-typescript';\nimport peerDepsExternal from 'rollup-plugin-peer-deps-external';\n\nexport default {\n  input: 'src/index.ts',\n  output: [\n    {\n      file: 'dist/index.js',\n      format: 'esm',\n      sourcemap: false,\n      exports: 'named',\n    },\n  ],\n  plugins: [\n    peerDepsExternal(),\n    resolve(),\n    commonjs(),\n    typescript({\n      tsconfig: './tsconfig.json',\n      declaration: true,\n      declarationDir: 'dist',\n    }),\n    terser(),\n  ],\n  external: [\n    'react',\n    'react-dom',\n    '@dayflow/core',\n    'preact',\n    'preact/hooks',\n    'preact/compat',\n  ],\n};\n"
  },
  {
    "path": "packages/react/src/DayFlowCalendar.tsx",
    "content": "import { CalendarRenderer } from '@dayflow/core';\nimport type {\n  ICalendarApp,\n  CustomRendering,\n  UseCalendarAppReturn,\n  EventContentSlotArgs,\n  EventContextMenuSlotArgs,\n  GridContextMenuSlotArgs,\n  EventDetailContentProps,\n  EventDetailDialogProps,\n  CreateCalendarDialogProps,\n  TitleBarSlotProps,\n  ColorPickerProps,\n  CreateCalendarDialogColorPickerProps,\n  CalendarHeaderProps,\n  CalendarSearchProps,\n  MobileEventProps,\n} from '@dayflow/core';\nimport { useRef, useEffect, useLayoutEffect, useState, useMemo } from 'react';\n\n// useLayoutEffect on the client fires before paint; fall back to useEffect on\n// the server to avoid SSR hydration warnings (the calendar is client-only anyway).\nconst useIsomorphicLayoutEffect =\n  typeof window === 'undefined' ? useEffect : useLayoutEffect;\nimport type { ReactNode, FC } from 'react';\nimport { createPortal } from 'react-dom';\n\nexport interface DayFlowCalendarProps {\n  calendar: ICalendarApp | UseCalendarAppReturn;\n  /** Custom event content renderer for Day view timed events */\n  eventContentDay?: (args: EventContentSlotArgs) => ReactNode;\n  /** Custom event content renderer for Week view timed events */\n  eventContentWeek?: (args: EventContentSlotArgs) => ReactNode;\n  /** Custom event content renderer for Month view timed events */\n  eventContentMonth?: (args: EventContentSlotArgs) => ReactNode;\n  /** Custom event content renderer for Year view events */\n  eventContentYear?: (args: EventContentSlotArgs) => ReactNode;\n  /** Custom event content renderer for all-day events in Day view */\n  eventContentAllDayDay?: (args: EventContentSlotArgs) => ReactNode;\n  /** Custom event content renderer for all-day events in Week view */\n  eventContentAllDayWeek?: (args: EventContentSlotArgs) => ReactNode;\n  /** Custom event content renderer for all-day events in Month view */\n  eventContentAllDayMonth?: (args: EventContentSlotArgs) => ReactNode;\n  /** Custom event content renderer for all-day events in Year view */\n  eventContentAllDayYear?: (args: EventContentSlotArgs) => ReactNode;\n  /** Custom event detail panel content (React) */\n  eventDetailContent?: (args: EventDetailContentProps) => ReactNode;\n  /** Custom event detail dialog (React) */\n  eventDetailDialog?: (args: EventDetailDialogProps) => ReactNode;\n  /** Custom create calendar dialog (React) */\n  createCalendarDialog?: (args: CreateCalendarDialogProps) => ReactNode;\n  /** Optional left padding to account for safe area insets (e.g. on Mac) */\n  collapsedSafeAreaLeft?: number;\n  /** Title bar slot (React) */\n  titleBarSlot?: (context: TitleBarSlotProps) => ReactNode;\n  /** Custom color picker renderer (React) */\n  colorPicker?: (args: ColorPickerProps) => ReactNode;\n  /** Custom create calendar dialog color picker renderer (React) */\n  createCalendarDialogColorPicker?: (\n    args: CreateCalendarDialogColorPickerProps\n  ) => ReactNode;\n  /** Custom calendar header renderer (React) */\n  calendarHeader?: (args: CalendarHeaderProps) => ReactNode;\n  /** Custom event right-click context menu renderer (React) */\n  eventContextMenu?: (args: EventContextMenuSlotArgs) => ReactNode;\n  /** Custom grid/cell right-click context menu renderer (React) */\n  gridContextMenu?: (args: GridContextMenuSlotArgs) => ReactNode;\n  /** Custom mobile event detail drawer (React) */\n  mobileEventDetail?: (args: MobileEventProps) => ReactNode;\n  /** Search configuration */\n  search?: CalendarSearchProps;\n}\n\n/** Compute active override names from props and installed plugins. */\nfunction computeActiveOverrides(\n  app: ICalendarApp,\n  renderProps: Omit<DayFlowCalendarProps, 'calendar'>\n): string[] {\n  const fromProps = Object.keys(renderProps).filter(\n    key => (renderProps as Record<string, unknown>)[key] !== undefined\n  );\n\n  const fromPlugins: string[] = [];\n  if (app?.state?.plugins) {\n    app.state.plugins.forEach(plugin => {\n      if (plugin.name === 'sidebar' && plugin.config) {\n        const config = plugin.config as Record<string, unknown>;\n        if (config['render']) {\n          fromPlugins.push('sidebar');\n        }\n        if (config['renderCreateCalendarDialog']) {\n          fromPlugins.push('createCalendarDialog');\n        }\n        if (config['renderCalendarContextMenu']) {\n          fromPlugins.push('calendarContextMenu');\n        }\n        if (config['renderSidebarHeader']) {\n          fromPlugins.push('sidebarHeader');\n        }\n      }\n    });\n  }\n\n  return [...new Set([...fromProps, ...fromPlugins])];\n}\n\n/** Shallow ordered comparison — avoids JSON.stringify allocation. */\nfunction overridesChanged(prev: string[], next: string[]): boolean {\n  return prev.length !== next.length || prev.some((v, i) => v !== next[i]);\n}\n\nexport const DayFlowCalendar: FC<DayFlowCalendarProps> = ({\n  calendar,\n  ...renderProps\n}: DayFlowCalendarProps) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const rendererRef = useRef<CalendarRenderer | null>(null);\n  const [customRenderings, setCustomRenderings] = useState<\n    Map<string, CustomRendering>\n  >(new Map());\n  const [isMounted, setIsMounted] = useState(false);\n\n  // Extract the underlying app instance\n  const app =\n    'app' in calendar && calendar.app\n      ? calendar.app\n      : (calendar as ICalendarApp);\n  const renderPropsKeysRef = useRef<string[]>([]);\n\n  useIsomorphicLayoutEffect(() => {\n    setIsMounted(true);\n    if (!containerRef.current || !app) {\n      return;\n    }\n\n    // Compute overrides synchronously so the very first Preact render already\n    // knows which slots are handled by this adapter — no race window.\n    const initialOverrides = computeActiveOverrides(app, renderProps);\n    renderPropsKeysRef.current = initialOverrides;\n\n    const renderer = new CalendarRenderer(app, initialOverrides);\n    rendererRef.current = renderer;\n    renderer.setProps(renderProps);\n\n    const store = renderer.getCustomRenderingStore();\n\n    // Subscribe BEFORE mount so every slot registration that happens during\n    // the synchronous Preact render is captured in the same React batch.\n    // This eliminates the gap between \"old containers detached\" and \"new\n    // portals created\" that caused titleBarSlot (and other slots) to flicker.\n    const unsubscribeStore = store.subscribe(\n      (\n        renderings:\n          | Iterable<readonly [string, CustomRendering]>\n          | null\n          | undefined\n      ) => {\n        setCustomRenderings(new Map(renderings));\n      }\n    );\n\n    renderer.mount(containerRef.current);\n\n    return () => {\n      // Unsubscribe FIRST so that when renderer.unmount() triggers\n      // store.unregister() calls, React state is NOT cleared to an empty map.\n      // The old portals stay in React state (invisible, containers detached)\n      // until the next effect's subscribe + mount batch replaces them atomically.\n      unsubscribeStore();\n      store.setOverrides([]);\n      renderer.unmount();\n      rendererRef.current = null;\n    };\n  }, [app]);\n\n  // Keep overrides and props in sync when they change after the initial mount.\n  useEffect(() => {\n    if (!rendererRef.current) {\n      return;\n    }\n    const store = rendererRef.current.getCustomRenderingStore();\n\n    const allOverrides = computeActiveOverrides(app, renderProps);\n    if (overridesChanged(renderPropsKeysRef.current, allOverrides)) {\n      store.setOverrides(allOverrides);\n      app.setOverrides(allOverrides);\n      renderPropsKeysRef.current = allOverrides;\n    }\n\n    rendererRef.current.setProps(renderProps);\n  }, [renderProps, app]);\n\n  // Portals for custom content\n  const portals = useMemo(() => {\n    if (!isMounted) {\n      return [];\n    }\n    return [...customRenderings.values()].map((rendering: CustomRendering) => {\n      const { id, containerEl, generatorName, generatorArgs } = rendering;\n\n      // 1. Look up the generator in props\n      let generator = (renderProps as Record<string, unknown>)[generatorName];\n\n      // 2. If not in props, look up in plugins\n      if (!generator && app && app.state && app.state.plugins) {\n        app.state.plugins.forEach(plugin => {\n          if (plugin.name === 'sidebar' && plugin.config) {\n            const config = plugin.config as Record<string, unknown>;\n            switch (generatorName) {\n              case 'sidebar':\n                generator = config['render'];\n                break;\n              case 'createCalendarDialog':\n                generator = config['renderCreateCalendarDialog'];\n                break;\n              case 'calendarContextMenu':\n                generator = config['renderCalendarContextMenu'];\n                break;\n              case 'sidebarHeader':\n                generator = config['renderSidebarHeader'];\n                break;\n              default:\n                break;\n            }\n          }\n        });\n      }\n\n      if (!generator) {\n        return null;\n      }\n\n      const content =\n        typeof generator === 'function' ? generator(generatorArgs) : generator;\n\n      return createPortal(content, containerEl, id);\n    });\n  }, [customRenderings, renderProps, isMounted, app]);\n\n  return (\n    <>\n      <div ref={containerRef} className='df-calendar-wrapper' />\n      {isMounted && portals}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/react/src/hooks/useCalendarApp.ts",
    "content": "import type { CalendarAppConfig, UseCalendarAppReturn } from '@dayflow/core';\nimport {\n  CalendarApp,\n  createConfigSyncSnapshot,\n  createNormalizedCalendarAppConfigGetter,\n  syncCalendarAppConfig,\n} from '@dayflow/core';\nimport { useState, useEffect, useMemo, useRef } from 'react';\n\nexport function useCalendarApp(\n  config: CalendarAppConfig,\n  version?: string | number\n): UseCalendarAppReturn {\n  const configRef = useRef(config);\n  configRef.current = config;\n  const getNormalizedConfig = useMemo(\n    () => createNormalizedCalendarAppConfigGetter(() => configRef.current),\n    []\n  );\n  const normalizedConfig = useMemo(\n    () => getNormalizedConfig(),\n    [config, getNormalizedConfig]\n  );\n\n  // `version` lets callers recreate the app (e.g. when plugins change) without\n  // unmounting the React component. Changing `version` creates a new CalendarApp\n  // with the current normalizedConfig; subsequent config-only diffs are synced\n  // via app.updateConfig() in the effect below.\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const app = useMemo(() => new CalendarApp(normalizedConfig), [version]);\n  const [, setTick] = useState(0);\n  const syncSnapshotRef = useRef(createConfigSyncSnapshot(normalizedConfig));\n\n  useEffect(() => {\n    if (!app) {\n      return;\n    }\n    // Subscribe to state changes to trigger React re-renders\n    const unsubscribe = app.subscribe(() => {\n      setTick((tick: number) => tick + 1);\n    });\n\n    return () => {\n      unsubscribe();\n    };\n  }, [app]);\n\n  // Reset sync baselines whenever we recreate the app via `version`.\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useEffect(() => {\n    syncSnapshotRef.current = createConfigSyncSnapshot(normalizedConfig);\n  }, [app]);\n\n  // Sync callbacks and live-updatable config in one pass.\n  useEffect(() => {\n    if (!app) {\n      return;\n    }\n\n    syncSnapshotRef.current = syncCalendarAppConfig(\n      app,\n      syncSnapshotRef.current,\n      normalizedConfig\n    );\n  }, [app, normalizedConfig]);\n\n  // Map app to the UseCalendarAppReturn interface\n  // (In a real implementation, we might want a more comprehensive mapping)\n  return {\n    app,\n    currentView: app.state.currentView,\n    currentDate: app.state.currentDate,\n    events: app.getEvents(),\n    applyEventsChanges: app.applyEventsChanges,\n    changeView: app.changeView,\n    setCurrentDate: app.setCurrentDate,\n    addEvent: app.addEvent,\n    updateEvent: app.updateEvent,\n    deleteEvent: app.deleteEvent,\n    undo: app.undo,\n    goToToday: app.goToToday,\n    goToPrevious: app.goToPrevious,\n    goToNext: app.goToNext,\n    selectDate: app.selectDate,\n    getCalendars: app.getCalendars,\n    createCalendar: app.createCalendar,\n    mergeCalendars: app.mergeCalendars,\n    setCalendarVisibility: app.setCalendarVisibility,\n    setAllCalendarsVisibility: app.setAllCalendarsVisibility,\n    getAllEvents: app.getAllEvents,\n    highlightEvent: app.highlightEvent,\n    setVisibleMonth: app.setVisibleMonth,\n    getVisibleMonth: app.getVisibleMonth,\n    emitVisibleRange: app.emitVisibleRange,\n    canMutateFromUI: app.canMutateFromUI,\n    readOnlyConfig: app.getReadOnlyConfig(),\n  };\n}\n"
  },
  {
    "path": "packages/react/src/index.ts",
    "content": "export * from './DayFlowCalendar';\nexport * from './hooks/useCalendarApp';\n// Re-export core parts for convenience\nexport {\n  CalendarApp,\n  CalendarRegistry,\n  createEventsPlugin,\n  createDayView,\n  createWeekView,\n  createMonthView,\n  createAgendaView,\n  createYearView,\n  ViewType,\n} from '@dayflow/core';\nexport * from '@dayflow/core';\n"
  },
  {
    "path": "packages/react/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"jsx\": \"react-jsx\",\n    \"allowImportingTsExtensions\": false,\n    \"paths\": {\n      \"@dayflow/core\": [\"../../packages/core/dist/index.d.ts\"]\n    }\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/svelte/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/svelte/README.md",
    "content": "# DayFlow Svelte\n\nA flexible and feature-rich Svelte calendar component library with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/svelte?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/svelte)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/dayflow/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/dayflow)](https://github.com/dayflow-js/dayflow/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/dayflow/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n"
  },
  {
    "path": "packages/svelte/index.d.ts",
    "content": "declare module '@dayflow/svelte' {\n  import type { CalendarAppConfig, UseCalendarAppReturn } from '@dayflow/core';\n  import {\n    CalendarApp,\n    CalendarRegistry,\n    createEventsPlugin,\n    createDayView,\n    createWeekView,\n    createMonthView,\n    createAgendaView,\n    createYearView,\n    ViewType,\n  } from '@dayflow/core';\n  import { SvelteComponent } from 'svelte';\n\n  /**\n   * Svelte store based hook for DayFlow\n   */\n  export function useCalendarApp(\n    config: CalendarAppConfig\n  ): UseCalendarAppReturn;\n\n  /**\n   * DayFlow Calendar Svelte Component\n   */\n  export class DayFlowCalendar extends SvelteComponent<\n    unknown,\n    unknown,\n    unknown\n  > {}\n\n  export {\n    CalendarApp,\n    CalendarRegistry,\n    createEventsPlugin,\n    createDayView,\n    createWeekView,\n    createMonthView,\n    createAgendaView,\n    createYearView,\n    ViewType,\n  };\n\n  export default DayFlowCalendar;\n}\n"
  },
  {
    "path": "packages/svelte/package.json",
    "content": "{\n  \"name\": \"@dayflow/svelte\",\n  \"version\": \"3.6.2\",\n  \"description\": \"Svelte adapter for DayFlow calendar\",\n  \"files\": [\n    \"dist\",\n    \"src\",\n    \"index.d.ts\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.js\",\n  \"types\": \"index.d.ts\",\n  \"svelte\": \"src/index.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./index.d.ts\",\n      \"svelte\": \"./src/index.ts\",\n      \"browser\": \"./dist/index.js\",\n      \"ssr\": \"./dist/index.ssr.js\",\n      \"import\": {\n        \"browser\": \"./dist/index.js\",\n        \"default\": \"./dist/index.ssr.js\"\n      },\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"rollup -c\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"typecheck\": \"svelte-check\"\n  },\n  \"devDependencies\": {\n    \"@dayflow/core\": \"workspace:*\",\n    \"@rollup/plugin-commonjs\": \"catalog:\",\n    \"@rollup/plugin-node-resolve\": \"catalog:\",\n    \"@rollup/plugin-terser\": \"catalog:\",\n    \"@rollup/plugin-typescript\": \"catalog:\",\n    \"@tsconfig/svelte\": \"^5.0.4\",\n    \"esbuild\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-esbuild\": \"catalog:\",\n    \"rollup-plugin-svelte\": \"catalog:\",\n    \"svelte\": \"^5.0.0\",\n    \"svelte-check\": \"^4.4.3\",\n    \"svelte-preprocess\": \"^6.0.3\",\n    \"typescript\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"@dayflow/core\": \"workspace:*\",\n    \"svelte\": \"^4.0.0 || ^5.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/svelte/rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport esbuild from 'rollup-plugin-esbuild';\nimport svelte from 'rollup-plugin-svelte';\nimport sveltePreprocess from 'svelte-preprocess';\n\nconst createConfig = ssr => ({\n  input: 'src/index.ts',\n  output: [\n    {\n      file: ssr ? 'dist/index.ssr.js' : 'dist/index.js',\n      format: 'es',\n      sourcemap: true,\n    },\n  ],\n  plugins: [\n    svelte({\n      preprocess: sveltePreprocess(),\n      emitCss: false,\n      compilerOptions: {\n        generate: ssr ? 'server' : 'client',\n      },\n    }),\n    resolve({\n      browser: !ssr,\n      dedupe: ['svelte'],\n      extensions: ['.mjs', '.js', '.ts', '.svelte'],\n    }),\n    esbuild({\n      sourceMap: true,\n      minify: false,\n      target: 'esnext',\n      loaders: {\n        '.ts': 'ts',\n      },\n    }),\n    commonjs(),\n  ],\n  external: id =>\n    id === 'svelte' || id.startsWith('svelte/') || id === '@dayflow/core',\n  onwarn(warning, warn) {\n    if (\n      warning.code === 'CIRCULAR_DEPENDENCY' &&\n      warning.ids &&\n      warning.ids[0].includes('node_modules')\n    ) {\n      return;\n    }\n    warn(warning);\n  },\n});\n\nexport default [createConfig(false), createConfig(true)];\n"
  },
  {
    "path": "packages/svelte/src/DayFlowCalendar.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount, onDestroy, tick } from \"svelte\";\n  import type { Component } from \"svelte\";\n  import { CalendarRenderer } from \"@dayflow/core\";\n  import type {\n    ICalendarApp,\n    UseCalendarAppReturn,\n    CustomRendering,\n    EventDetailContentProps,\n    EventDetailDialogProps,\n    CreateCalendarDialogProps,\n    TitleBarSlotProps,\n    EventContentSlotArgs,\n    ColorPickerProps,\n    CreateCalendarDialogColorPickerProps,\n    CalendarHeaderProps,\n    EventContextMenuSlotArgs,\n    GridContextMenuSlotArgs,\n    CalendarSearchProps,\n    MobileEventProps,\n  } from \"@dayflow/core\";\n\n  const {\n    calendar,\n    eventContentDay = null,\n    eventContentWeek = null,\n    eventContentMonth = null,\n    eventContentYear = null,\n    eventContentAllDayDay = null,\n    eventContentAllDayWeek = null,\n    eventContentAllDayMonth = null,\n    eventContentAllDayYear = null,\n    eventDetailContent = null,\n    eventDetailDialog = null,\n    createCalendarDialog = null,\n    titleBarSlot = null,\n    colorPicker = null,\n    createCalendarDialogColorPicker = null,\n    calendarHeader = null,\n    eventContextMenu = null,\n    gridContextMenu = null,\n    mobileEventDetail = null,\n    collapsedSafeAreaLeft = null,\n    search = null,\n    } = $props<{\n    calendar: ICalendarApp | UseCalendarAppReturn;\n    eventContentDay?: Component<EventContentSlotArgs>;\n    eventContentWeek?: Component<EventContentSlotArgs>;\n    eventContentMonth?: Component<EventContentSlotArgs>;\n    eventContentYear?: Component<EventContentSlotArgs>;\n    eventContentAllDayDay?: Component<EventContentSlotArgs>;\n    eventContentAllDayWeek?: Component<EventContentSlotArgs>;\n    eventContentAllDayMonth?: Component<EventContentSlotArgs>;\n    eventContentAllDayYear?: Component<EventContentSlotArgs>;\n    eventDetailContent?: Component<EventDetailContentProps>;\n    eventDetailDialog?: Component<EventDetailDialogProps>;\n    createCalendarDialog?: Component<CreateCalendarDialogProps>;\n    titleBarSlot?: Component<TitleBarSlotProps>;\n    colorPicker?: Component<ColorPickerProps>;\n    createCalendarDialogColorPicker?: Component<CreateCalendarDialogColorPickerProps>;\n    calendarHeader?: Component<CalendarHeaderProps>;\n    eventContextMenu?: Component<EventContextMenuSlotArgs>;\n    gridContextMenu?: Component<GridContextMenuSlotArgs>;\n    mobileEventDetail?: Component<MobileEventProps>;\n    collapsedSafeAreaLeft?: number | null;\n    search?: CalendarSearchProps | null;\n    }>();\n\n  let container: HTMLElement | undefined = $state();\n  let renderer: CalendarRenderer | undefined;\n  let customRenderings: CustomRendering[] = $state([]);\n  let unsubscribe: (() => void) | undefined;\n  let mounted = $state(false);\n\n  // Guard for browser environment\n  const isBrowser = typeof window !== \"undefined\";\n\n  const app = $derived(\n    (\"app\" in calendar ? calendar.app : calendar) as ICalendarApp,\n  );\n\n  const renderProps = $derived({\n    eventContentDay,\n    eventContentWeek,\n    eventContentMonth,\n    eventContentYear,\n    eventContentAllDayDay,\n    eventContentAllDayWeek,\n    eventContentAllDayMonth,\n    eventContentAllDayYear,\n    eventDetailContent,\n    eventDetailDialog,\n    createCalendarDialog,\n    titleBarSlot,\n    colorPicker,\n    createCalendarDialogColorPicker,\n    calendarHeader,\n    eventContextMenu,\n    gridContextMenu,\n    mobileEventDetail,\n    collapsedSafeAreaLeft,\n    search,\n  } as Record<string, unknown>);\n\n  onMount(async () => {\n    if (!container) {\n      return;\n    }\n\n    await tick();\n\n    const activeOverrides = Object.keys(renderProps).filter(\n      (key) => renderProps[key] !== null,\n    );\n    renderer = new CalendarRenderer(app, activeOverrides);\n    renderer.setProps(renderProps);\n    renderer.mount(container);\n\n    unsubscribe = renderer.getCustomRenderingStore().subscribe((renderings) => {\n      customRenderings = [...renderings.values()];\n    });\n\n    mounted = true;\n  });\n\n  onDestroy(() => {\n    if (unsubscribe) {\n      unsubscribe();\n    }\n    if (renderer) {\n      renderer.unmount();\n    }\n  });\n\n  // Reactively forward prop changes to the renderer after mount.\n  // `mounted` is $state so this effect re-runs when it becomes true, and\n  // again whenever unknown renderProp value changes.\n  $effect(() => {\n    if (!mounted || !renderer) {\n      return;\n    }\n    renderer.setProps(renderProps);\n    const activeOverrides = Object.keys(renderProps).filter(\n      (key) => renderProps[key] !== null,\n    );\n    renderer.getCustomRenderingStore().setOverrides(activeOverrides);\n    app.setOverrides(activeOverrides);\n  });\n\n  function portal(node: HTMLElement, target: HTMLElement) {\n    if (!target || !node || !isBrowser) {\n      return;\n    }\n    target.append(node);\n    return {\n      destroy() {\n        if (node.parentNode === target) {\n          node.remove();\n        }\n      },\n    };\n  }\n</script>\n\n{#if isBrowser}\n  <div bind:this={container} class=\"df-calendar-wrapper\"></div>\n\n  {#if mounted}\n    {#each customRenderings as rendering (rendering.id)}\n      {@const DynamicComponent = renderProps[rendering.generatorName] as any}\n      {#if DynamicComponent && rendering.containerEl}\n        <div use:portal={rendering.containerEl}>\n          <DynamicComponent {...(rendering.generatorArgs as any)} />\n        </div>\n      {/if}\n    {/each}\n  {/if}\n{:else}\n  <!-- SSR Placeholder -->\n  <div class=\"df-calendar-wrapper df-calendar-ssr-placeholder\"></div>\n{/if}\n"
  },
  {
    "path": "packages/svelte/src/index.ts",
    "content": "import DayFlowCalendar from './DayFlowCalendar.svelte';\nexport { DayFlowCalendar };\nexport { useCalendarApp } from './useCalendarApp';\nexport default DayFlowCalendar;\n\nexport {\n  CalendarApp,\n  CalendarRegistry,\n  createEventsPlugin,\n  createDayView,\n  createWeekView,\n  createMonthView,\n  createAgendaView,\n  createYearView,\n  ViewType,\n} from '@dayflow/core';\n\nexport * from '@dayflow/core';\n"
  },
  {
    "path": "packages/svelte/src/svelte-shims.d.ts",
    "content": "declare module '*.svelte' {\n  import type { Component } from 'svelte';\n  const component: Component<Record<string, unknown>>;\n  export default component;\n}\n"
  },
  {
    "path": "packages/svelte/src/useCalendarApp.ts",
    "content": "import {\n  CalendarApp,\n  createNormalizedCalendarAppConfigGetter,\n} from '@dayflow/core';\nimport type { CalendarAppConfig, UseCalendarAppReturn } from '@dayflow/core';\nimport { onDestroy } from 'svelte';\nimport { get, writable } from 'svelte/store';\n\nexport function useCalendarApp(\n  config: CalendarAppConfig\n): UseCalendarAppReturn {\n  const getNormalizedConfig = createNormalizedCalendarAppConfigGetter(\n    () => config\n  );\n\n  const app = new CalendarApp(getNormalizedConfig());\n\n  // Create a store for the parts of state we want to be reactive in Svelte\n  const stateStore = writable({\n    currentView: app.state.currentView,\n    currentDate: app.state.currentDate,\n    events: app.getEvents(),\n  });\n\n  const syncState = () => {\n    stateStore.set({\n      currentView: app.state.currentView,\n      currentDate: app.state.currentDate,\n      events: app.getEvents(),\n    });\n  };\n\n  const unsubscribe = app.subscribe(() => {\n    syncState();\n  });\n\n  onDestroy(() => {\n    unsubscribe();\n  });\n\n  // Proxy the state properties\n  const result = {\n    get app() {\n      return app;\n    },\n    get currentView() {\n      return get(stateStore).currentView;\n    },\n    get currentDate() {\n      return get(stateStore).currentDate;\n    },\n    get events() {\n      return get(stateStore).events;\n    },\n\n    applyEventsChanges: (...args: Parameters<typeof app.applyEventsChanges>) =>\n      app.applyEventsChanges(...args),\n    changeView: (...args: Parameters<typeof app.changeView>) =>\n      app.changeView(...args),\n    setCurrentDate: (...args: Parameters<typeof app.setCurrentDate>) =>\n      app.setCurrentDate(...args),\n    addEvent: (...args: Parameters<typeof app.addEvent>) =>\n      app.addEvent(...args),\n    updateEvent: (...args: Parameters<typeof app.updateEvent>) =>\n      app.updateEvent(...args),\n    deleteEvent: (...args: Parameters<typeof app.deleteEvent>) =>\n      app.deleteEvent(...args),\n    undo: () => app.undo(),\n    goToToday: () => app.goToToday(),\n    goToPrevious: () => app.goToPrevious(),\n    goToNext: () => app.goToNext(),\n    selectDate: (...args: Parameters<typeof app.selectDate>) =>\n      app.selectDate(...args),\n    getCalendars: () => app.getCalendars(),\n    createCalendar: (...args: Parameters<typeof app.createCalendar>) =>\n      app.createCalendar(...args),\n    mergeCalendars: (...args: Parameters<typeof app.mergeCalendars>) =>\n      app.mergeCalendars(...args),\n    setCalendarVisibility: (\n      ...args: Parameters<typeof app.setCalendarVisibility>\n    ) => app.setCalendarVisibility(...args),\n    setAllCalendarsVisibility: (\n      ...args: Parameters<typeof app.setAllCalendarsVisibility>\n    ) => app.setAllCalendarsVisibility(...args),\n    getAllEvents: () => app.getAllEvents(),\n    highlightEvent: (...args: Parameters<typeof app.highlightEvent>) =>\n      app.highlightEvent(...args),\n    setVisibleMonth: (...args: Parameters<typeof app.setVisibleMonth>) =>\n      app.setVisibleMonth(...args),\n    getVisibleMonth: () => app.getVisibleMonth(),\n    emitVisibleRange: (...args: Parameters<typeof app.emitVisibleRange>) =>\n      app.emitVisibleRange(...args),\n    canMutateFromUI: (...args: Parameters<typeof app.canMutateFromUI>) =>\n      app.canMutateFromUI(...args),\n    get readOnlyConfig() {\n      return app.getReadOnlyConfig();\n    },\n  } as unknown as UseCalendarAppReturn;\n\n  return result;\n}\n"
  },
  {
    "path": "packages/svelte/svelte.config.js",
    "content": "import { sveltePreprocess } from 'svelte-preprocess';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n  preprocess: sveltePreprocess(),\n  compilerOptions: {\n    runes: true,\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "packages/svelte/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"strict\": true,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"noEmit\": false,\n    \"outDir\": \"dist\",\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"verbatimModuleSyntax\": true,\n    \"types\": [\"svelte\"],\n    \"skipLibCheck\": true,\n    \"paths\": {\n      \"@dayflow/core\": [\"../core/dist/index.d.ts\"],\n      \"@/*\": [\"../core/src/*\"]\n    }\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.svelte\", \"src/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/ui/context-menu/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/ui/context-menu/README.md",
    "content": "# DayFlow\n\n**English** | [中文](README.zh.md) | [日本語](README.ja.md) | [Getting Started & Contributing](CONTRIBUTING.md)\n\nA flexible and feature-rich calendar component library for **React, Vue, Angular, and Svelte** with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/core?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/core)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/calendar/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/calendar)](https://github.com/dayflow-js/calendar/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/calendar/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n\n---\n"
  },
  {
    "path": "packages/ui/context-menu/package.json",
    "content": "{\n  \"name\": \"@dayflow/ui-context-menu\",\n  \"version\": \"1.1.2\",\n  \"description\": \"Standalone context menu primitives from the DayFlow UI ecosystem\",\n  \"keywords\": [\n    \"context-menu\",\n    \"dayflow\",\n    \"preact\",\n    \"ui\"\n  ],\n  \"license\": \"MIT\",\n  \"author\": \"Jayce Li\",\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"*.css\",\n    \"**/*.css\"\n  ],\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\"\n    },\n    \"./dist/styles.css\": \"./dist/styles.css\",\n    \"./dist/styles.components.css\": \"./dist/styles.components.css\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && rollup -c && pnpm run build:css && pnpm run check:css\",\n    \"build:css\": \"node ./scripts/build-css.mjs\",\n    \"check:css\": \"pnpm run check:css:source && pnpm run check:css:dist\",\n    \"check:css:dist\": \"node ../../core/scripts/check-dist-styling.mjs --package-root .\",\n    \"check:css:source\": \"node ../../core/scripts/check-semantic-css.mjs\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"postbuild\": \"rimraf dist/build dist/types\",\n    \"prebuild\": \"rimraf dist\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"catalog:\",\n    \"@rollup/plugin-node-resolve\": \"catalog:\",\n    \"@tailwindcss/postcss\": \"catalog:\",\n    \"autoprefixer\": \"catalog:\",\n    \"postcss\": \"catalog:\",\n    \"postcss-import\": \"catalog:\",\n    \"preact\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-dts\": \"catalog:\",\n    \"tailwindcss\": \"catalog:\",\n    \"tsc-alias\": \"^1.8.16\",\n    \"typescript\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"preact\": \">=10\"\n  }\n}\n"
  },
  {
    "path": "packages/ui/context-menu/rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport { dts } from 'rollup-plugin-dts';\n\nexport default [\n  {\n    input: 'dist/build/index.js',\n    output: [\n      {\n        file: 'dist/index.js',\n        format: 'esm',\n        sourcemap: false,\n        exports: 'named',\n      },\n    ],\n    plugins: [resolve({ extensions: ['.js', '.jsx'] }), commonjs()],\n    external: ['preact', 'preact/hooks', 'preact/compat'],\n  },\n  {\n    input: 'dist/types/index.d.ts',\n    output: [{ file: 'dist/index.d.ts', format: 'es' }],\n    plugins: [dts()],\n  },\n];\n"
  },
  {
    "path": "packages/ui/context-menu/scripts/build-css.mjs",
    "content": "import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport tailwindcss from '@tailwindcss/postcss';\nimport autoprefixer from 'autoprefixer';\nimport postcss from 'postcss';\nimport postcssImport from 'postcss-import';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst root = path.resolve(__dirname, '..');\n\nasync function buildCss(inputFile, outputFile) {\n  const input = path.join(root, inputFile);\n  const output = path.join(root, outputFile);\n\n  const css = await fs.readFile(input, 'utf8');\n\n  const result = await postcss([\n    postcssImport(),\n    tailwindcss,\n    autoprefixer,\n  ]).process(css, { from: input, to: output });\n\n  await fs.mkdir(path.dirname(output), { recursive: true });\n  await fs.writeFile(output, result.css);\n\n  if (result.map) {\n    await fs.writeFile(`${output}.map`, result.map.toString());\n  }\n\n  console.log('CSS built successfully →', path.relative(root, output));\n}\n\nawait buildCss('src/styles/tailwind.css', 'dist/styles.css');\nawait buildCss(\n  'src/styles/tailwind-components.css',\n  'dist/styles.components.css'\n);\n"
  },
  {
    "path": "packages/ui/context-menu/src/ContextMenu.tsx",
    "content": "import { cloneElement, isValidElement, ComponentChildren } from 'preact';\nimport { createPortal, forwardRef } from 'preact/compat';\nimport { useEffect, useRef, useState } from 'preact/hooks';\n\n// Inline icon\ninterface IconProps {\n  className?: string;\n  width?: number;\n  height?: number;\n}\n\nconst ChevronRight = ({ className, width = 24, height = 24 }: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m9 18 6-6-6-6' />\n  </svg>\n);\n\n// ContextMenu\ninterface ContextMenuProps {\n  x: number;\n  y: number;\n  onClose: () => void;\n  children: ComponentChildren;\n  className?: string;\n}\n\nexport const ContextMenu = forwardRef<HTMLDivElement, ContextMenuProps>(\n  ({ x, y, onClose, children, className }, ref) => {\n    const internalRef = useRef<HTMLDivElement>(null);\n\n    const setRefs = (node: HTMLDivElement | null) => {\n      internalRef.current = node;\n      if (typeof ref === 'function') {\n        ref(node);\n      } else if (ref && 'current' in ref) {\n        ref.current = node;\n      }\n    };\n\n    useEffect(() => {\n      const handleCloseAll = () => onClose();\n      const handleClickOutside = (event: MouseEvent) => {\n        if (\n          internalRef.current &&\n          !internalRef.current.contains(event.target as Node)\n        ) {\n          const target = event.target as HTMLElement;\n          if (target.closest('[data-submenu-content]')) return;\n          onClose();\n        }\n      };\n\n      window.dispatchEvent(new CustomEvent('dayflow-close-all-menus'));\n      window.addEventListener('dayflow-close-all-menus', handleCloseAll);\n      document.body.addEventListener('mousedown', handleClickOutside, {\n        capture: true,\n      });\n      document.body.addEventListener('contextmenu', handleClickOutside, {\n        capture: true,\n      });\n\n      const handleKeyDown = (event: KeyboardEvent) => {\n        if (event.key === 'Escape') onClose();\n      };\n      window.addEventListener('keydown', handleKeyDown);\n\n      const handleScrollOrResize = () => onClose();\n      window.addEventListener('scroll', handleScrollOrResize, true);\n      window.addEventListener('resize', handleScrollOrResize);\n\n      return () => {\n        window.removeEventListener('dayflow-close-all-menus', handleCloseAll);\n        document.body.removeEventListener('mousedown', handleClickOutside, {\n          capture: true,\n        });\n        document.body.removeEventListener('contextmenu', handleClickOutside, {\n          capture: true,\n        });\n        window.removeEventListener('keydown', handleKeyDown);\n        window.removeEventListener('scroll', handleScrollOrResize, true);\n        window.removeEventListener('resize', handleScrollOrResize);\n      };\n    }, [onClose]);\n\n    const style: Record<string, number | string> = { top: y, left: x };\n\n    return createPortal(\n      <div\n        ref={setRefs}\n        className={`df-portal df-context-menu df-context-menu-content${className ? ` ${className}` : ''}`}\n        style={style}\n        onContextMenu={e => e.preventDefault()}\n        data-context-menu-root='true'\n        role='menu'\n      >\n        {children}\n      </div>,\n      document.body\n    );\n  }\n);\n\nContextMenu.displayName = 'ContextMenu';\n\n// ContextMenuItem\nexport const ContextMenuItem = ({\n  onClick,\n  children,\n  icon,\n  danger,\n  disabled,\n}: {\n  onClick: () => void;\n  children: ComponentChildren;\n  icon?: ComponentChildren;\n  danger?: boolean;\n  disabled?: boolean;\n}) => (\n  <div\n    className='df-context-menu-item'\n    onClick={e => {\n      e.stopPropagation();\n      if (!disabled) onClick();\n    }}\n    data-disabled={disabled}\n    data-danger={danger}\n    role='menuitem'\n    tabIndex={disabled ? -1 : 0}\n  >\n    {icon && <span className='df-context-menu-item-icon'>{icon}</span>}\n    {children}\n  </div>\n);\n\n// ContextMenuSeparator\nexport const ContextMenuSeparator = () => (\n  <div className='df-context-menu-separator' role='separator' />\n);\n\n// ContextMenuLabel\nexport const ContextMenuLabel = ({\n  children,\n}: {\n  children: ComponentChildren;\n}) => <div className='df-context-menu-label'>{children}</div>;\n\n// ContextMenuSub\nexport const ContextMenuSub = ({\n  children,\n}: {\n  children: ComponentChildren;\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const handleMouseEnter = () => {\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current);\n      timeoutRef.current = null;\n    }\n    setIsOpen(true);\n  };\n\n  const handleMouseLeave = () => {\n    timeoutRef.current = setTimeout(() => setIsOpen(false), 100);\n  };\n\n  useEffect(\n    () => () => {\n      if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    },\n    []\n  );\n\n  return (\n    <div\n      className='df-context-menu-sub'\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n    >\n      {(Array.isArray(children) ? children : [children]).map(child => {\n        if (isValidElement(child)) return cloneElement(child, { isOpen });\n        return child;\n      })}\n    </div>\n  );\n};\n\n// ContextMenuSubTrigger\n\nexport const ContextMenuSubTrigger = ({\n  children,\n  icon,\n  isOpen,\n}: {\n  children: ComponentChildren;\n  icon?: ComponentChildren;\n  isOpen?: boolean;\n}) => (\n  <div\n    className='df-context-menu-sub-trigger'\n    data-open={isOpen}\n    role='menuitem'\n    tabIndex={0}\n  >\n    {icon && <span className='df-context-menu-sub-icon'>{icon}</span>}\n    <span className='df-context-menu-sub-label'>{children}</span>\n    <ChevronRight className='df-context-menu-sub-chevron' />\n  </div>\n);\n\n// ContextMenuSubContent\n\nexport const ContextMenuSubContent = ({\n  children,\n  isOpen,\n}: {\n  children: ComponentChildren;\n  isOpen?: boolean;\n}) => {\n  const ref = useRef<HTMLDivElement>(null);\n  const [position, setPosition] = useState<'right' | 'left'>('right');\n\n  useEffect(() => {\n    if (isOpen && ref.current) {\n      const rect = ref.current.getBoundingClientRect();\n      const parentRect = ref.current.parentElement?.getBoundingClientRect();\n      if (parentRect) {\n        setPosition(\n          parentRect.right + rect.width > window.innerWidth ? 'left' : 'right'\n        );\n      }\n    }\n  }, [isOpen]);\n\n  if (!isOpen) return null;\n\n  return (\n    <div\n      ref={ref}\n      className='df-portal df-context-menu df-context-menu-sub-content'\n      data-position={position}\n      data-submenu-content='true'\n      role='menu'\n    >\n      {children}\n    </div>\n  );\n};\n\n// ContextMenuColorPicker\n\nconst COLORS = [\n  '#ea426b',\n  '#f19a38',\n  '#f7cf46',\n  '#83d754',\n  '#51aaf2',\n  '#b672d0',\n  '#957e5e',\n];\n\nexport const ContextMenuColorPicker = ({\n  selectedColor,\n  onSelect,\n  onCustomColor,\n  customColorLabel = 'Custom Color',\n}: {\n  selectedColor?: string;\n  onSelect: (color: string) => void;\n  onCustomColor?: () => void;\n  customColorLabel?: string;\n}) => (\n  <div className='df-context-menu-color-picker'>\n    <div className='df-context-menu-color-grid'>\n      {COLORS.map(color => (\n        <button\n          key={color}\n          type='button'\n          className='df-context-menu-color-swatch'\n          data-selected={\n            selectedColor?.toLowerCase() === color.toLowerCase()\n              ? 'true'\n              : undefined\n          }\n          style={{ backgroundColor: color }}\n          onClick={e => {\n            e.stopPropagation();\n            onSelect(color);\n          }}\n          title={color}\n        />\n      ))}\n    </div>\n    {onCustomColor && (\n      <button\n        type='button'\n        className='df-context-menu-custom-color'\n        onClick={e => {\n          e.stopPropagation();\n          onCustomColor();\n        }}\n      >\n        {customColorLabel}\n      </button>\n    )}\n  </div>\n);\n"
  },
  {
    "path": "packages/ui/context-menu/src/index.ts",
    "content": "export {\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuLabel,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n  ContextMenuColorPicker,\n} from './ContextMenu';\n"
  },
  {
    "path": "packages/ui/context-menu/src/styles/context-menu.css",
    "content": "/* ── DayFlow UI – Context Menu ──────────────────────────────────────────── */\n\n@layer components {\n  .df-portal,\n  .df-context-menu {\n    --color-primary: var(--df-color-primary);\n    --color-primary-foreground: var(--df-color-primary-foreground);\n    --color-background: var(--df-color-background);\n    --color-border: var(--df-color-border);\n    --color-foreground: var(--df-color-foreground);\n    --color-destructive: var(--df-color-destructive);\n    --color-destructive-foreground: var(--df-color-destructive-foreground);\n  }\n\n  .df-context-menu {\n    --df-context-menu-background: rgb(255 255 255);\n    --df-context-menu-border: rgb(226 232 240);\n    --df-context-menu-foreground: rgb(46 46 46);\n    --df-context-menu-primary: rgb(46 46 46);\n    --df-context-menu-primary-foreground: rgb(255 255 255);\n    --df-context-menu-destructive: rgb(212 36 34);\n    --df-context-menu-destructive-foreground: rgb(255 255 255);\n    --df-context-menu-swatch-border: rgb(229 231 235);\n    --df-context-menu-swatch-ring: rgb(255 255 255 / 0.9);\n    --df-color-background: var(--df-context-menu-background);\n    --df-color-border: var(--df-context-menu-border);\n    --df-color-foreground: var(--df-context-menu-foreground);\n    --df-color-primary: var(--df-context-menu-primary);\n    --df-color-primary-foreground: var(--df-context-menu-primary-foreground);\n    --df-color-destructive: var(--df-context-menu-destructive);\n    --df-color-destructive-foreground: var(\n      --df-context-menu-destructive-foreground\n    );\n  }\n\n  .dark .df-context-menu {\n    --df-context-menu-background: rgb(2 6 23);\n    --df-context-menu-border: rgb(30 41 59);\n    --df-context-menu-foreground: rgb(229 229 229);\n    --df-context-menu-primary: rgb(229 229 229);\n    --df-context-menu-primary-foreground: rgb(15 23 42);\n    --df-context-menu-destructive: rgb(147 70 69);\n    --df-context-menu-destructive-foreground: rgb(254 242 242);\n    --df-context-menu-swatch-border: rgb(75 85 99);\n    --df-context-menu-swatch-ring: rgb(15 23 42 / 0.9);\n  }\n\n  @media (prefers-color-scheme: dark) {\n    :root:not(.light):not(.dark) .df-context-menu {\n      --df-context-menu-background: rgb(2 6 23);\n      --df-context-menu-border: rgb(30 41 59);\n      --df-context-menu-foreground: rgb(229 229 229);\n      --df-context-menu-primary: rgb(229 229 229);\n      --df-context-menu-primary-foreground: rgb(23 23 23);\n      --df-context-menu-destructive: rgb(147 70 69);\n      --df-context-menu-destructive-foreground: rgb(254 242 242);\n      --df-context-menu-swatch-border: rgb(75 85 99);\n      --df-context-menu-swatch-ring: rgb(15 23 42 / 0.9);\n    }\n  }\n\n  .df-context-menu-content,\n  .df-context-menu-sub-content {\n    min-width: 8rem;\n    padding: 0.25rem;\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.375rem;\n    background-color: var(--df-color-background);\n    color: var(--df-color-foreground);\n    box-shadow:\n      0 10px 15px -3px rgb(0 0 0 / 0.1),\n      0 4px 6px -4px rgb(0 0 0 / 0.1);\n    animation:\n      df-context-menu-fade-in 120ms cubic-bezier(0, 0, 0.2, 1) forwards,\n      df-context-menu-zoom-in 120ms cubic-bezier(0, 0, 0.2, 1) forwards;\n  }\n\n  .df-context-menu-content {\n    position: fixed;\n    z-index: 50;\n    overflow: visible;\n  }\n\n  .df-context-menu-sub {\n    position: relative;\n  }\n\n  .df-context-menu-sub-content {\n    position: absolute;\n    top: 0;\n    z-index: 50;\n    overflow: hidden;\n    white-space: nowrap;\n  }\n\n  .df-context-menu-sub-content[data-position='right'] {\n    left: calc(100% + 0.25rem);\n  }\n\n  .df-context-menu-sub-content[data-position='left'] {\n    right: calc(100% + 0.25rem);\n  }\n\n  .df-context-menu-item,\n  .df-context-menu-sub-trigger,\n  .df-context-menu-custom-color {\n    display: flex;\n    align-items: center;\n    width: 100%;\n    border: 0;\n    border-radius: 0.25rem;\n    background: transparent;\n    padding: 0.125rem 0.75rem;\n    font-size: 12px;\n    line-height: 1.35;\n    color: var(--df-color-foreground);\n    text-align: left;\n    transition:\n      background-color 120ms ease,\n      color 120ms ease,\n      opacity 120ms ease,\n      transform 120ms ease;\n    outline: none;\n    user-select: none;\n  }\n\n  .df-context-menu-item,\n  .df-context-menu-sub-trigger {\n    cursor: default;\n  }\n\n  .df-context-menu-custom-color {\n    cursor: pointer;\n    margin-top: 0.25rem;\n  }\n\n  .df-context-menu-item:is(:hover, :focus-visible):not([data-disabled='true']),\n  .df-context-menu-sub-trigger:is(:hover, :focus-visible),\n  .df-context-menu-custom-color:is(:hover, :focus-visible),\n  .df-context-menu-sub-trigger[data-open='true'] {\n    background-color: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n  }\n\n  .df-context-menu-item[data-danger='true'] {\n    color: var(--df-color-destructive);\n  }\n\n  .df-context-menu-item[data-danger='true']:is(:hover, :focus-visible):not(\n      [data-disabled='true']\n    ) {\n    background-color: var(--df-color-destructive);\n    color: var(--df-color-destructive-foreground);\n  }\n\n  .df-context-menu-item[data-disabled='true'] {\n    pointer-events: none;\n    opacity: 0.5;\n  }\n\n  .df-context-menu-item-icon,\n  .df-context-menu-sub-icon {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    width: 1rem;\n    height: 1rem;\n    margin-right: 0.5rem;\n    flex-shrink: 0;\n  }\n\n  .df-context-menu-sub-label {\n    flex: 1;\n    min-width: 0;\n    text-align: left;\n  }\n\n  .df-context-menu-sub-chevron {\n    width: 1rem;\n    height: 1rem;\n    margin-left: auto;\n    opacity: 0.6;\n    transition:\n      opacity 120ms ease,\n      color 120ms ease;\n  }\n\n  .df-context-menu-sub-trigger[data-open='true'] .df-context-menu-sub-chevron {\n    opacity: 1;\n  }\n\n  .df-context-menu-separator {\n    height: 1px;\n    margin: 0.25rem -0.25rem;\n    background-color: var(--df-color-border);\n  }\n\n  .df-context-menu-label {\n    padding: 0.125rem 0.75rem;\n    font-size: 12px;\n    line-height: 1.35;\n    font-weight: 600;\n    color: inherit;\n  }\n\n  .df-context-menu-calendar-item {\n    display: flex;\n    width: 100%;\n    align-items: center;\n  }\n\n  .df-context-menu-calendar-check-wrap {\n    width: 1rem;\n    flex-shrink: 0;\n  }\n\n  .df-context-menu-calendar-check {\n    height: 0.75rem;\n    width: 0.75rem;\n  }\n\n  .df-context-menu-calendar-info {\n    display: flex;\n    min-width: 0;\n    align-items: center;\n    gap: 0.375rem;\n  }\n\n  .df-context-menu-calendar-dot {\n    height: 0.75rem;\n    width: 0.75rem;\n    flex-shrink: 0;\n    border-radius: 0.125rem;\n  }\n\n  .df-context-menu-calendar-label {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .df-context-menu-calendar-label[data-selected='true'] {\n    font-weight: 600;\n  }\n\n  .df-context-menu-color-picker {\n    width: 100%;\n  }\n\n  .df-context-menu-color-grid {\n    display: grid;\n    grid-template-columns: repeat(7, minmax(0, 1fr));\n    gap: 0.5rem;\n    padding: 0.25rem 0.75rem;\n  }\n\n  .df-context-menu-color-swatch {\n    width: 1.25rem;\n    height: 1.25rem;\n    border: 1px solid var(--df-context-menu-swatch-border);\n    border-radius: 9999px;\n    cursor: pointer;\n    transition:\n      transform 120ms ease,\n      box-shadow 120ms ease,\n      border-color 120ms ease;\n    outline: none;\n  }\n  .df-context-menu-color-swatch:is(:hover, :focus-visible) {\n    transform: scale(1.08);\n    box-shadow: 0 0 0 2px var(--df-context-menu-swatch-ring);\n  }\n\n  .df-context-menu-color-swatch[data-selected='true'] {\n    border-color: var(--df-color-primary);\n    box-shadow:\n      0 0 0 1px var(--df-color-primary),\n      0 0 0 3px var(--df-context-menu-swatch-ring);\n  }\n}\n\n@keyframes df-context-menu-fade-in {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes df-context-menu-zoom-in {\n  from {\n    transform: scale(0.95);\n  }\n  to {\n    transform: scale(1);\n  }\n}\n"
  },
  {
    "path": "packages/ui/context-menu/src/styles/tailwind-components.css",
    "content": "/* styles.components.css — for projects that already have Tailwind CSS\n *\n * This file includes DayFlow context-menu component styles only.\n * It does NOT emit Tailwind utility classes.\n */\n@layer theme, base, df-theme, components, utilities;\n\n@import 'tailwindcss/theme' layer(theme);\n@import './context-menu.css';\n\n@variant dark (.dark &);\n"
  },
  {
    "path": "packages/ui/context-menu/src/styles/tailwind.css",
    "content": "/* styles.css — standalone DayFlow context-menu styles\n *\n * This file now ships semantic component CSS only.\n * It intentionally avoids emitting atomic utility classes.\n */\n@layer theme, base, df-theme, components, utilities;\n\n@import 'tailwindcss/theme' layer(theme);\n\n@import './context-menu.css';\n"
  },
  {
    "path": "packages/ui/context-menu/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist/build\",\n    \"rootDir\": \"src\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": false,\n    \"declarationDir\": \"dist/types\",\n    \"declarationMap\": false,\n    \"sourceMap\": false,\n    \"module\": \"ESNext\",\n    \"target\": \"ES2015\",\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": false,\n    \"ignoreDeprecations\": \"5.0\"\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"**/*.test.ts\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.ts\",\n    \"**/*.spec.tsx\"\n  ]\n}\n"
  },
  {
    "path": "packages/ui/context-menu/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"declaration\": true,\n    \"declarationMap\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/ui/range-picker/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/ui/range-picker/README.md",
    "content": "# DayFlow\n\n**English** | [中文](README.zh.md) | [日本語](README.ja.md) | [Getting Started & Contributing](CONTRIBUTING.md)\n\nA flexible and feature-rich calendar component library for **React, Vue, Angular, and Svelte** with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/core?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/core)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/calendar/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/calendar)](https://github.com/dayflow-js/calendar/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/calendar/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n\n---\n"
  },
  {
    "path": "packages/ui/range-picker/package.json",
    "content": "{\n  \"name\": \"@dayflow/ui-range-picker\",\n  \"version\": \"1.1.2\",\n  \"description\": \"Standalone date/time range picker from the DayFlow UI ecosystem\",\n  \"keywords\": [\n    \"date-picker\",\n    \"dayflow\",\n    \"preact\",\n    \"range-picker\",\n    \"temporal\",\n    \"ui\"\n  ],\n  \"license\": \"MIT\",\n  \"author\": \"Jayce Li\",\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": [\n    \"*.css\",\n    \"**/*.css\"\n  ],\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\"\n    },\n    \"./dist/styles.css\": \"./dist/styles.css\",\n    \"./dist/styles.components.css\": \"./dist/styles.components.css\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && rollup -c && pnpm run build:css && pnpm run check:css\",\n    \"build:css\": \"node ./scripts/build-css.mjs\",\n    \"check:css\": \"pnpm run check:css:source && pnpm run check:css:dist\",\n    \"check:css:dist\": \"node ../../core/scripts/check-dist-styling.mjs --package-root .\",\n    \"check:css:source\": \"node ../../core/scripts/check-semantic-css.mjs\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"postbuild\": \"rimraf dist/build dist/types\",\n    \"prebuild\": \"rimraf dist\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"catalog:\",\n    \"@rollup/plugin-node-resolve\": \"catalog:\",\n    \"@tailwindcss/postcss\": \"catalog:\",\n    \"autoprefixer\": \"catalog:\",\n    \"postcss\": \"catalog:\",\n    \"postcss-import\": \"catalog:\",\n    \"preact\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-dts\": \"catalog:\",\n    \"tailwindcss\": \"catalog:\",\n    \"temporal-polyfill\": \"catalog:\",\n    \"tsc-alias\": \"^1.8.16\",\n    \"typescript\": \"catalog:\"\n  },\n  \"peerDependencies\": {\n    \"preact\": \">=10\",\n    \"temporal-polyfill\": \">=0.2\"\n  }\n}\n"
  },
  {
    "path": "packages/ui/range-picker/rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport { dts } from 'rollup-plugin-dts';\n\nexport default [\n  {\n    input: 'dist/build/index.js',\n    output: [\n      {\n        file: 'dist/index.js',\n        format: 'esm',\n        sourcemap: false,\n        exports: 'named',\n      },\n    ],\n    plugins: [resolve({ extensions: ['.js', '.jsx'] }), commonjs()],\n    external: ['preact', 'preact/hooks', 'preact/compat', 'temporal-polyfill'],\n  },\n  {\n    input: 'dist/types/index.d.ts',\n    output: [{ file: 'dist/index.d.ts', format: 'es' }],\n    plugins: [dts()],\n    external: ['temporal-polyfill'],\n  },\n];\n"
  },
  {
    "path": "packages/ui/range-picker/scripts/build-css.mjs",
    "content": "import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport tailwindcss from '@tailwindcss/postcss';\nimport autoprefixer from 'autoprefixer';\nimport postcss from 'postcss';\nimport postcssImport from 'postcss-import';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst root = path.resolve(__dirname, '..');\n\nasync function buildCss(inputFile, outputFile) {\n  const input = path.join(root, inputFile);\n  const output = path.join(root, outputFile);\n\n  const css = await fs.readFile(input, 'utf8');\n\n  const result = await postcss([\n    postcssImport(),\n    tailwindcss,\n    autoprefixer,\n  ]).process(css, { from: input, to: output });\n\n  await fs.mkdir(path.dirname(output), { recursive: true });\n  await fs.writeFile(output, result.css);\n\n  if (result.map) {\n    await fs.writeFile(`${output}.map`, result.map.toString());\n  }\n\n  console.log('CSS built successfully →', path.relative(root, output));\n}\n\nawait buildCss('src/styles/tailwind.css', 'dist/styles.css');\nawait buildCss(\n  'src/styles/tailwind-components.css',\n  'dist/styles.components.css'\n);\n"
  },
  {
    "path": "packages/ui/range-picker/src/RangePicker.tsx",
    "content": "import { JSX } from 'preact';\nimport { createPortal } from 'preact/compat';\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'preact/hooks';\nimport { Temporal } from 'temporal-polyfill';\n\nimport RangePickerPanel from './components/RangePickerPanel';\nimport { DEFAULT_FORMAT, DEFAULT_TIME_FORMAT } from './constants';\nimport { MoveRight } from './icons';\nimport { RangePickerProps, ZonedRange } from './types';\nimport { getMonthLabels, getWeekDaysLabels } from './utils/locale';\nimport {\n  mergeFormatTemplate,\n  buildParseRegExp,\n  parseTemporalString,\n  getZoneId,\n  normalizeToZoned,\n  formatTemporal,\n} from './utils/rangePicker';\nimport { isPlainDate } from './utils/temporal';\n\nconst RangePicker = ({\n  value,\n  format = DEFAULT_FORMAT,\n  showTimeFormat = DEFAULT_TIME_FORMAT,\n  showTime = true,\n  onChange,\n  onOk,\n  timeZone = Temporal.Now.timeZoneId(),\n  startOfWeek = 1,\n  disabled = false,\n  placement = 'bottomLeft',\n  autoAdjustOverflow = true,\n  getPopupContainer,\n  matchTriggerWidth = false,\n  locale = 'en-US',\n}: RangePickerProps) => {\n  const localeCode = useMemo(\n    () => (typeof locale === 'string' ? locale : locale?.code || 'en-US'),\n    [locale]\n  );\n\n  const isTimeEnabled = useMemo(() => {\n    if (showTime === undefined) return true;\n    if (typeof showTime === 'object') return true;\n    return Boolean(showTime);\n  }, [showTime]);\n\n  const monthLabels = useMemo(\n    () => getMonthLabels(localeCode, 'short'),\n    [localeCode]\n  );\n\n  const weekDayLabels = useMemo(\n    () => getWeekDaysLabels(localeCode, 'narrow', startOfWeek),\n    [localeCode, startOfWeek]\n  );\n\n  const effectiveTimeFormat = useMemo(() => {\n    if (!isTimeEnabled) return '';\n    if (typeof showTime === 'object' && showTime?.format)\n      return showTime.format;\n    return showTimeFormat;\n  }, [isTimeEnabled, showTime, showTimeFormat]);\n\n  const formatTemplate = useMemo(\n    () => mergeFormatTemplate(format, effectiveTimeFormat),\n    [format, effectiveTimeFormat]\n  );\n\n  const parseRegExp = useMemo(\n    () => buildParseRegExp(formatTemplate),\n    [formatTemplate]\n  );\n\n  const normalizedValue = useMemo<ZonedRange>(() => {\n    const zone =\n      timeZone ??\n      (isPlainDate(value[0])\n        ? isPlainDate(value[1])\n          ? Temporal.Now.timeZoneId()\n          : getZoneId(value[1] as Temporal.ZonedDateTime)\n        : getZoneId(value[0] as Temporal.ZonedDateTime));\n\n    const start = normalizeToZoned(value[0], zone).withTimeZone(zone);\n    const end = normalizeToZoned(value[1], zone, start).withTimeZone(zone);\n    return [start, end];\n  }, [value, timeZone]);\n\n  const [draftRange, setDraftRange] = useState<ZonedRange>(normalizedValue);\n  const lastNormalizedRef = useRef<ZonedRange>(normalizedValue);\n  const [focusedField, setFocusedField] = useState<'start' | 'end'>('start');\n  const [inputValues, setInputValues] = useState<[string, string]>([\n    formatTemporal(normalizedValue[0], format, effectiveTimeFormat),\n    formatTemporal(normalizedValue[1], format, effectiveTimeFormat),\n  ]);\n  const inputValuesRef = useRef<[string, string]>([\n    formatTemporal(normalizedValue[0], format, effectiveTimeFormat),\n    formatTemporal(normalizedValue[1], format, effectiveTimeFormat),\n  ]);\n  const draftRangeRef = useRef<ZonedRange>(normalizedValue);\n  const [visibleMonth, setVisibleMonth] = useState<Temporal.PlainDate>(\n    normalizedValue[0].toPlainDate().with({ day: 1 })\n  );\n  const [isOpen, setIsOpenInternal] = useState(false);\n\n  const setIsOpen = useCallback((val: boolean) => {\n    setIsOpenInternal(val);\n  }, []);\n  const [_, setPopupPlacement] = useState(placement);\n  const popupPlacementRef = useRef(placement);\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const popupRef = useRef<HTMLDivElement | null>(null);\n  const timeListRefs = useRef<{\n    start: { hour: HTMLDivElement | null; minute: HTMLDivElement | null };\n    end: { hour: HTMLDivElement | null; minute: HTMLDivElement | null };\n  }>({\n    start: { hour: null, minute: null },\n    end: { hour: null, minute: null },\n  });\n  const committedRef = useRef(false);\n  const isEditingRef = useRef(false);\n\n  useEffect(() => {\n    inputValuesRef.current = inputValues;\n  }, [inputValues]);\n\n  useEffect(() => {\n    draftRangeRef.current = draftRange;\n  }, [draftRange]);\n\n  useEffect(() => {\n    const previous = lastNormalizedRef.current;\n    const startChanged =\n      Temporal.ZonedDateTime.compare(previous[0], normalizedValue[0]) !== 0 ||\n      previous[0].timeZoneId !== normalizedValue[0].timeZoneId;\n    const endChanged =\n      Temporal.ZonedDateTime.compare(previous[1], normalizedValue[1]) !== 0 ||\n      previous[1].timeZoneId !== normalizedValue[1].timeZoneId;\n\n    if (startChanged || endChanged) {\n      setDraftRange(normalizedValue);\n    }\n\n    lastNormalizedRef.current = normalizedValue;\n  }, [normalizedValue]);\n\n  useEffect(() => {\n    setVisibleMonth(normalizedValue[0].toPlainDate().with({ day: 1 }));\n  }, [normalizedValue[0]]);\n\n  const alignActiveToTop = useCallback(\n    (\n      container: HTMLElement | null,\n      activeItem: HTMLElement | null,\n      topPadding = 0\n    ) => {\n      if (!container || !activeItem) return;\n\n      const containerRect = container.getBoundingClientRect();\n      const itemRect = activeItem.getBoundingClientRect();\n      const delta =\n        itemRect.top - containerRect.top + container.scrollTop - topPadding;\n\n      const prefersReducedMotion = window.matchMedia?.(\n        '(prefers-reduced-motion: reduce)'\n      ).matches;\n      const behavior: ScrollBehavior = prefersReducedMotion ? 'auto' : 'smooth';\n\n      if (Math.abs(container.scrollTop - delta) > 1) {\n        container.scrollTo({ top: delta, behavior });\n      }\n    },\n    []\n  );\n\n  const scrollToActiveTime = useCallback(\n    (field: 'start' | 'end') => {\n      requestAnimationFrame(() => {\n        requestAnimationFrame(() => {\n          const refs = timeListRefs.current[field];\n          (['hour', 'minute'] as const).forEach(type => {\n            const container = refs[type];\n            if (!container) return;\n            const active = container.querySelector<HTMLElement>(\n              '[data-active=\"true\"]'\n            );\n            if (active) alignActiveToTop(container, active, 0);\n          });\n        });\n      });\n    },\n    [alignActiveToTop]\n  );\n\n  useEffect(() => {\n    if (!isOpen || !isTimeEnabled) return;\n    scrollToActiveTime(focusedField);\n  }, [focusedField, isOpen, scrollToActiveTime, isTimeEnabled]);\n\n  const draftStartEpoch = draftRange[0].epochMilliseconds;\n  const draftStartOffset = draftRange[0].offsetNanoseconds;\n  const draftEndEpoch = draftRange[1].epochMilliseconds;\n  const draftEndOffset = draftRange[1].offsetNanoseconds;\n\n  useEffect(() => {\n    if (isEditingRef.current) return;\n\n    const [currentStart, currentEnd] = draftRangeRef.current;\n    const nextStart = formatTemporal(currentStart, format, effectiveTimeFormat);\n    const nextEnd = formatTemporal(currentEnd, format, effectiveTimeFormat);\n    const [prevStart, prevEnd] = inputValuesRef.current;\n\n    if (prevStart === nextStart && prevEnd === nextEnd) return;\n\n    inputValuesRef.current = [nextStart, nextEnd];\n    setInputValues([nextStart, nextEnd]);\n  }, [\n    draftStartEpoch,\n    draftStartOffset,\n    draftEndEpoch,\n    draftEndOffset,\n    format,\n    effectiveTimeFormat,\n  ]);\n\n  useEffect(() => {\n    if (!isOpen) return;\n\n    const handleClickOutside = (event: PointerEvent) => {\n      const target = event.target as HTMLElement;\n      if (containerRef.current?.contains(target)) return;\n      if (popupRef.current?.contains(target)) return;\n      if (target.closest('[data-range-picker-popup]')) return;\n      setIsOpen(false);\n    };\n\n    document.addEventListener('pointerdown', handleClickOutside, true);\n    return () => {\n      document.removeEventListener('pointerdown', handleClickOutside, true);\n    };\n  }, [isOpen]);\n\n  useEffect(() => {\n    if (isOpen) return;\n    setFocusedField('start');\n    if (!committedRef.current) {\n      setDraftRange(normalizedValue);\n    }\n    committedRef.current = false;\n  }, [isOpen, normalizedValue]);\n\n  const emitChange = useCallback(\n    (range: ZonedRange) => {\n      if (!onChange) return;\n      onChange(range, [\n        formatTemporal(range[0], format, effectiveTimeFormat),\n        formatTemporal(range[1], format, effectiveTimeFormat),\n      ]);\n    },\n    [effectiveTimeFormat, format, onChange]\n  );\n\n  const emitOk = useCallback(\n    (range: ZonedRange) => {\n      if (!onOk) return;\n      onOk(range, [\n        formatTemporal(range[0], format, effectiveTimeFormat),\n        formatTemporal(range[1], format, effectiveTimeFormat),\n      ]);\n    },\n    [effectiveTimeFormat, format, onOk]\n  );\n\n  const updateRange = useCallback(\n    (field: 'start' | 'end', nextValue: Temporal.ZonedDateTime) => {\n      setDraftRange(prev => {\n        const current: ZonedRange = [...prev] as ZonedRange;\n        if (field === 'start') {\n          const safeEnd = normalizeToZoned(\n            current[1],\n            getZoneId(nextValue),\n            nextValue\n          );\n          const adjustedEnd =\n            Temporal.ZonedDateTime.compare(nextValue, safeEnd) > 0\n              ? nextValue\n              : safeEnd;\n          return [nextValue, adjustedEnd];\n        }\n\n        const safeStart = normalizeToZoned(\n          current[0],\n          getZoneId(nextValue),\n          nextValue\n        );\n        const adjustedStart =\n          Temporal.ZonedDateTime.compare(safeStart, nextValue) > 0\n            ? nextValue\n            : safeStart;\n        return [adjustedStart, nextValue];\n      });\n    },\n    []\n  );\n\n  const handleDaySelect = (day: Temporal.PlainDate) => {\n    if (disabled) return;\n\n    const buildValue = (\n      base: Temporal.ZonedDateTime,\n      source: Temporal.PlainDate\n    ): Temporal.ZonedDateTime => {\n      const zoneId = getZoneId(base);\n      return Temporal.ZonedDateTime.from({\n        timeZone: zoneId,\n        year: source.year,\n        month: source.month,\n        day: source.day,\n        hour: base.hour,\n        minute: base.minute,\n        second: base.second ?? 0,\n        millisecond: base.millisecond ?? 0,\n        microsecond: base.microsecond ?? 0,\n        nanosecond: base.nanosecond ?? 0,\n      });\n    };\n\n    if (focusedField === 'start') {\n      const nextStart = buildValue(draftRange[0], day);\n      const durationMs =\n        draftRange[1].epochMilliseconds - draftRange[0].epochMilliseconds;\n      const adjustedEnd = nextStart.add({ milliseconds: durationMs });\n      setDraftRange([nextStart, adjustedEnd]);\n      return;\n    }\n\n    const nextEndCandidate = buildValue(draftRange[1], day);\n    const durationMs =\n      draftRange[1].epochMilliseconds - draftRange[0].epochMilliseconds;\n\n    if (Temporal.ZonedDateTime.compare(nextEndCandidate, draftRange[0]) < 0) {\n      const newStart = buildValue(draftRange[0], day);\n      const newEnd = newStart.add({ milliseconds: durationMs });\n      setDraftRange([newStart, newEnd]);\n      return;\n    }\n\n    setDraftRange([draftRange[0], nextEndCandidate]);\n    setVisibleMonth(nextEndCandidate.toPlainDate().with({ day: 1 }));\n  };\n\n  const handleHourSelect = useCallback(\n    (field: 'start' | 'end', hour: number) => {\n      if (disabled) return;\n      const index = field === 'start' ? 0 : 1;\n      setDraftRange(prev => {\n        const current = prev[index];\n        const nextValue = current.with({\n          hour,\n          minute: current.minute,\n          second: 0,\n          millisecond: 0,\n          microsecond: 0,\n          nanosecond: 0,\n        });\n\n        if (field === 'start') {\n          const safeEnd = normalizeToZoned(\n            prev[1],\n            getZoneId(nextValue),\n            nextValue\n          );\n          const adjustedEnd =\n            Temporal.ZonedDateTime.compare(nextValue, safeEnd) > 0\n              ? nextValue\n              : safeEnd;\n          return [nextValue, adjustedEnd];\n        }\n\n        const safeStart = normalizeToZoned(\n          prev[0],\n          getZoneId(nextValue),\n          nextValue\n        );\n        const adjustedStart =\n          Temporal.ZonedDateTime.compare(safeStart, nextValue) > 0\n            ? nextValue\n            : safeStart;\n        return [adjustedStart, nextValue];\n      });\n\n      requestAnimationFrame(() => {\n        requestAnimationFrame(() => {\n          const container = timeListRefs.current[field].hour;\n          if (!container) return;\n          const active = container.querySelector<HTMLElement>(\n            '[data-active=\"true\"]'\n          );\n          if (active) alignActiveToTop(container, active, 0);\n        });\n      });\n    },\n    [disabled, alignActiveToTop]\n  );\n\n  const handleMinuteSelect = useCallback(\n    (field: 'start' | 'end', minute: number) => {\n      if (disabled) return;\n\n      const index = field === 'start' ? 0 : 1;\n      setDraftRange(prev => {\n        const current = prev[index];\n        const nextValue = current.with({\n          minute,\n          second: 0,\n          millisecond: 0,\n          microsecond: 0,\n          nanosecond: 0,\n        });\n\n        if (field === 'start') {\n          const safeEnd = normalizeToZoned(\n            prev[1],\n            getZoneId(nextValue),\n            nextValue\n          );\n          const adjustedEnd =\n            Temporal.ZonedDateTime.compare(nextValue, safeEnd) > 0\n              ? nextValue\n              : safeEnd;\n          return [nextValue, adjustedEnd];\n        }\n\n        const safeStart = normalizeToZoned(\n          prev[0],\n          getZoneId(nextValue),\n          nextValue\n        );\n        const adjustedStart =\n          Temporal.ZonedDateTime.compare(safeStart, nextValue) > 0\n            ? nextValue\n            : safeStart;\n        return [adjustedStart, nextValue];\n      });\n\n      requestAnimationFrame(() => {\n        requestAnimationFrame(() => {\n          const container = timeListRefs.current[field].minute;\n          if (!container) return;\n          const active = container.querySelector<HTMLElement>(\n            '[data-active=\"true\"]'\n          );\n          if (active) alignActiveToTop(container, active, 0);\n        });\n      });\n    },\n    [disabled, alignActiveToTop]\n  );\n\n  const updateInputValue = useCallback(\n    (field: 'start' | 'end', next: string) => {\n      const index = field === 'start' ? 0 : 1;\n      setInputValues(prev => {\n        const candidate: [string, string] = [...prev] as [string, string];\n        candidate[index] = next;\n        return candidate;\n      });\n    },\n    []\n  );\n\n  const commitInputValue = useCallback(\n    (field: 'start' | 'end', rawValue: string) => {\n      const index = field === 'start' ? 0 : 1;\n      const reference = draftRange[index];\n      const zoneId = getZoneId(reference);\n      const parsed = parseTemporalString(\n        rawValue,\n        parseRegExp,\n        reference,\n        zoneId\n      );\n\n      if (parsed) {\n        updateRange(field, parsed);\n        const month = parsed.toPlainDate().with({ day: 1 });\n        setVisibleMonth(month);\n        if (field === 'start') setFocusedField('end');\n        return true;\n      }\n\n      setInputValues(prev => {\n        const next: [string, string] = [...prev] as [string, string];\n        next[index] = formatTemporal(\n          draftRange[index],\n          format,\n          effectiveTimeFormat\n        );\n        return next;\n      });\n      return false;\n    },\n    [draftRange, effectiveTimeFormat, format, parseRegExp, updateRange]\n  );\n\n  const handleInputChange = useCallback(\n    (field: 'start' | 'end') =>\n      (event: JSX.TargetedEvent<HTMLInputElement, globalThis.Event>) => {\n        const newValue = event.currentTarget.value;\n        isEditingRef.current = true;\n        updateInputValue(field, newValue);\n\n        const index = field === 'start' ? 0 : 1;\n        const reference = draftRangeRef.current[index];\n        const zoneId = getZoneId(reference);\n        const parsed = parseTemporalString(\n          newValue,\n          parseRegExp,\n          reference,\n          zoneId\n        );\n        if (parsed) {\n          updateRange(field, parsed);\n          const month = parsed.toPlainDate().with({ day: 1 });\n          setVisibleMonth(month);\n          scrollToActiveTime(field);\n        }\n      },\n    [updateInputValue, parseRegExp, updateRange, scrollToActiveTime]\n  );\n\n  const handleInputBlur = useCallback(\n    (field: 'start' | 'end') =>\n      (event: JSX.TargetedFocusEvent<HTMLInputElement>) => {\n        if (disabled) return;\n        isEditingRef.current = false;\n\n        if (isOpen) {\n          const index = field === 'start' ? 0 : 1;\n          const formatted = formatTemporal(\n            draftRangeRef.current[index],\n            format,\n            effectiveTimeFormat\n          );\n          setInputValues(prev => {\n            const next: [string, string] = [...prev] as [string, string];\n            next[index] = formatted;\n            return next;\n          });\n          return;\n        }\n\n        const relatedTarget = event.relatedTarget as HTMLElement;\n        if (!relatedTarget || !containerRef.current?.contains(relatedTarget)) {\n          commitInputValue(field, event.currentTarget.value);\n        }\n      },\n    [commitInputValue, disabled, isOpen, format, effectiveTimeFormat]\n  );\n\n  const handleInputKeyDown = useCallback(\n    (field: 'start' | 'end') =>\n      (event: JSX.TargetedKeyboardEvent<HTMLInputElement>) => {\n        if (event.key === 'Enter') {\n          event.preventDefault();\n          isEditingRef.current = false;\n          commitInputValue(field, event.currentTarget.value);\n        }\n        if (event.key === 'Escape') event.currentTarget.blur();\n      },\n    [commitInputValue]\n  );\n\n  const handleOk = () => {\n    committedRef.current = true;\n    emitChange(draftRange);\n    emitOk(draftRange);\n    setIsOpen(false);\n  };\n\n  const changeMonth = (months: number) => {\n    setVisibleMonth(prev => prev.add({ months }).with({ day: 1 }));\n  };\n\n  const changeYear = (years: number) => {\n    setVisibleMonth(prev => prev.add({ years }).with({ day: 1 }));\n  };\n\n  const calendarDays = useMemo(() => {\n    const startOfMonth = visibleMonth;\n    // Temporal dayOfWeek: 1=Mon...7=Sun. Convert startOfWeek (0=Sun,1=Mon) to Temporal convention.\n    const temporalStartDay = startOfWeek === 0 ? 7 : startOfWeek;\n    const offset = (startOfMonth.dayOfWeek - temporalStartDay + 7) % 7;\n    const gridStart = startOfMonth.subtract({ days: offset });\n    return Array.from({ length: 42 }, (__, index) =>\n      gridStart.add({ days: index })\n    );\n  }, [visibleMonth, startOfWeek]);\n\n  const calculateOptimalPlacement = useCallback(\n    (basePlacement: typeof placement = placement): typeof placement => {\n      if (!autoAdjustOverflow || !containerRef.current) return basePlacement;\n\n      const triggerRect = containerRef.current.getBoundingClientRect();\n      const popupHeight = 500;\n      const popupWidth = matchTriggerWidth ? triggerRect.width : 480;\n\n      const spaceBelow = window.innerHeight - triggerRect.bottom;\n      const spaceAbove = triggerRect.top;\n      const spaceRight = window.innerWidth - triggerRect.left;\n      const spaceLeft = triggerRect.right;\n\n      let finalPlacement = basePlacement;\n\n      if (\n        finalPlacement.startsWith('bottom') &&\n        spaceBelow < popupHeight &&\n        spaceAbove > spaceBelow\n      ) {\n        finalPlacement = finalPlacement.replace(\n          'bottom',\n          'top'\n        ) as typeof placement;\n      } else if (\n        finalPlacement.startsWith('top') &&\n        spaceAbove < popupHeight &&\n        spaceBelow > spaceAbove\n      ) {\n        finalPlacement = finalPlacement.replace(\n          'top',\n          'bottom'\n        ) as typeof placement;\n      }\n\n      if (\n        finalPlacement.endsWith('Left') &&\n        spaceRight < popupWidth &&\n        spaceLeft > spaceRight\n      ) {\n        finalPlacement = finalPlacement.replace(\n          'Left',\n          'Right'\n        ) as typeof placement;\n      } else if (\n        finalPlacement.endsWith('Right') &&\n        spaceLeft < popupWidth &&\n        spaceRight > spaceLeft\n      ) {\n        finalPlacement = finalPlacement.replace(\n          'Right',\n          'Left'\n        ) as typeof placement;\n      }\n\n      return finalPlacement;\n    },\n    [autoAdjustOverflow, matchTriggerWidth, placement]\n  );\n\n  const adjustPopupPlacement = useCallback(() => {\n    const finalPlacement = calculateOptimalPlacement();\n    if (popupPlacementRef.current !== finalPlacement) {\n      popupPlacementRef.current = finalPlacement;\n      setPopupPlacement(finalPlacement);\n    }\n  }, [calculateOptimalPlacement]);\n\n  const openPanelForField = (field: 'start' | 'end') => {\n    if (disabled) return;\n    setFocusedField(field);\n    const index = field === 'start' ? 0 : 1;\n    const targetMonth = draftRange[index].toPlainDate().with({ day: 1 });\n    setVisibleMonth(targetMonth);\n    const initialPlacement = calculateOptimalPlacement();\n    if (popupPlacementRef.current !== initialPlacement) {\n      popupPlacementRef.current = initialPlacement;\n      setPopupPlacement(initialPlacement);\n    }\n    setIsOpen(true);\n  };\n\n  useEffect(() => {\n    if (!isOpen) return;\n    adjustPopupPlacement();\n\n    const handleResize = () => adjustPopupPlacement();\n    window.addEventListener('resize', handleResize);\n    window.addEventListener('scroll', handleResize, true);\n    return () => {\n      window.removeEventListener('resize', handleResize);\n      window.removeEventListener('scroll', handleResize, true);\n    };\n  }, [isOpen, adjustPopupPlacement]);\n\n  const getPopupStyle = (): JSX.CSSProperties => {\n    if (!containerRef.current) return {};\n\n    const triggerRect = containerRef.current.getBoundingClientRect();\n    const placementDom = popupPlacementRef.current;\n    const style: JSX.CSSProperties = { position: 'fixed', zIndex: 9999 };\n\n    if (placementDom.startsWith('bottom')) {\n      style.top = triggerRect.bottom + 8;\n    } else {\n      style.bottom = window.innerHeight - triggerRect.top + 8;\n    }\n\n    if (placement.endsWith('Left')) {\n      style.left = triggerRect.left;\n    } else {\n      style.right = window.innerWidth - triggerRect.right;\n    }\n\n    if (matchTriggerWidth) {\n      style.width = `${triggerRect.width}px`;\n    }\n\n    return style;\n  };\n\n  return (\n    <div className='df-range-picker df-range-picker-root' ref={containerRef}>\n      <div\n        className='df-range-picker-trigger'\n        data-disabled={disabled}\n        data-open={isOpen}\n      >\n        <div className='df-range-picker-field-group'>\n          <input\n            type='text'\n            name='range-start'\n            value={inputValues[0]}\n            onChange={handleInputChange('start')}\n            onFocus={() => openPanelForField('start')}\n            onClick={() => openPanelForField('start')}\n            onBlur={handleInputBlur('start')}\n            onKeyDown={handleInputKeyDown('start')}\n            className='df-range-picker-input'\n            data-disabled={disabled}\n            data-focused={focusedField === 'start' && isOpen}\n            placeholder={formatTemplate}\n            autoComplete='off'\n            disabled={disabled}\n          />\n        </div>\n\n        <MoveRight className='df-range-picker-separator-icon' />\n\n        <div className='df-range-picker-field-group'>\n          <input\n            type='text'\n            name='range-end'\n            value={inputValues[1]}\n            onChange={handleInputChange('end')}\n            onFocus={() => openPanelForField('end')}\n            onClick={() => openPanelForField('end')}\n            onBlur={handleInputBlur('end')}\n            onKeyDown={handleInputKeyDown('end')}\n            className='df-range-picker-input'\n            data-disabled={disabled}\n            data-focused={focusedField === 'end' && isOpen}\n            placeholder={formatTemplate}\n            autoComplete='off'\n            disabled={disabled}\n          />\n        </div>\n      </div>\n\n      {isOpen &&\n        (getPopupContainer\n          ? createPortal(\n              <RangePickerPanel\n                visibleMonth={visibleMonth}\n                monthLabels={monthLabels}\n                weekDayLabels={weekDayLabels}\n                calendarDays={calendarDays}\n                draftRange={draftRange}\n                focusedField={focusedField}\n                isTimeEnabled={!!isTimeEnabled}\n                disabled={disabled}\n                matchTriggerWidth={matchTriggerWidth}\n                popupRef={popupRef}\n                timeListRefs={timeListRefs}\n                onMonthChange={changeMonth}\n                onYearChange={changeYear}\n                onDaySelect={handleDaySelect}\n                onHourSelect={handleHourSelect}\n                onMinuteSelect={handleMinuteSelect}\n                onOk={handleOk}\n                getPopupStyle={getPopupStyle}\n              />,\n              getPopupContainer()\n            )\n          : createPortal(\n              <RangePickerPanel\n                visibleMonth={visibleMonth}\n                monthLabels={monthLabels}\n                weekDayLabels={weekDayLabels}\n                calendarDays={calendarDays}\n                draftRange={draftRange}\n                focusedField={focusedField}\n                isTimeEnabled={!!isTimeEnabled}\n                disabled={disabled}\n                matchTriggerWidth={matchTriggerWidth}\n                popupRef={popupRef}\n                timeListRefs={timeListRefs}\n                onMonthChange={changeMonth}\n                onYearChange={changeYear}\n                onDaySelect={handleDaySelect}\n                onHourSelect={handleHourSelect}\n                onMinuteSelect={handleMinuteSelect}\n                onOk={handleOk}\n                getPopupStyle={getPopupStyle}\n              />,\n              document.body\n            ))}\n    </div>\n  );\n};\n\nexport default RangePicker;\n"
  },
  {
    "path": "packages/ui/range-picker/src/components/CalendarGrid.tsx",
    "content": "import { Temporal } from 'temporal-polyfill';\n\ninterface CalendarGridProps {\n  calendarDays: Temporal.PlainDate[];\n  visibleMonth: Temporal.PlainDate;\n  startDate: Temporal.PlainDate;\n  endDate: Temporal.PlainDate;\n  weekDayLabels: string[];\n  disabled?: boolean;\n  onDaySelect: (day: Temporal.PlainDate) => void;\n}\n\nconst compareDates = (a: Temporal.PlainDate, b: Temporal.PlainDate): number =>\n  Temporal.PlainDate.compare(a, b);\n\nconst CalendarGrid = ({\n  calendarDays,\n  visibleMonth,\n  startDate,\n  endDate,\n  weekDayLabels,\n  disabled,\n  onDaySelect,\n}: CalendarGridProps) => {\n  const renderDayCell = (day: Temporal.PlainDate) => {\n    const isOutsideMonth = day.month !== visibleMonth.month;\n    const isStart = compareDates(day, startDate) === 0;\n    const isEnd = compareDates(day, endDate) === 0;\n    const isInRange =\n      compareDates(day, startDate) >= 0 && compareDates(day, endDate) <= 0;\n\n    return (\n      <button\n        key={day.toString()}\n        type='button'\n        disabled={disabled}\n        onClick={() => onDaySelect(day)}\n        className='df-range-picker-day-cell'\n        data-outside={isOutsideMonth}\n        data-range-edge={isStart ? 'start' : isEnd ? 'end' : undefined}\n        data-in-range={isInRange && !isStart && !isEnd}\n      >\n        {day.day}\n      </button>\n    );\n  };\n\n  return (\n    <>\n      <div className='df-range-picker-weekday-row'>\n        {weekDayLabels.map((day: string, index: number) => (\n          <span key={index} className='df-range-picker-weekday-label'>\n            {day}\n          </span>\n        ))}\n      </div>\n      <div className='df-range-picker-day-grid'>\n        {calendarDays.map(renderDayCell)}\n      </div>\n    </>\n  );\n};\n\nexport default CalendarGrid;\n"
  },
  {
    "path": "packages/ui/range-picker/src/components/CalendarHeader.tsx",
    "content": "import {\n  ChevronLeft,\n  ChevronRight,\n  ChevronsLeft,\n  ChevronsRight,\n} from '@ui-range-picker/icons';\nimport { Temporal } from 'temporal-polyfill';\n\ninterface CalendarHeaderProps {\n  visibleMonth: Temporal.PlainDate;\n  monthLabels: string[];\n  disabled?: boolean;\n  onMonthChange: (months: number) => void;\n  onYearChange: (years: number) => void;\n}\n\nconst CalendarHeader = ({\n  visibleMonth,\n  monthLabels,\n  disabled,\n  onMonthChange,\n  onYearChange,\n}: CalendarHeaderProps) => (\n  <div className='df-range-picker-calendar-header'>\n    <div className='df-range-picker-calendar-nav-group'>\n      <button\n        type='button'\n        disabled={disabled}\n        onClick={() => onYearChange(-1)}\n        className='df-range-picker-calendar-nav-button'\n      >\n        <ChevronsLeft width={14} height={12} />\n      </button>\n      <button\n        type='button'\n        disabled={disabled}\n        onClick={() => onMonthChange(-1)}\n        className='df-range-picker-calendar-nav-button'\n      >\n        <ChevronLeft width={14} height={12} />\n      </button>\n    </div>\n    <div className='df-range-picker-calendar-title'>\n      {monthLabels[visibleMonth.month - 1]} {visibleMonth.year}\n    </div>\n    <div className='df-range-picker-calendar-nav-group'>\n      <button\n        type='button'\n        disabled={disabled}\n        onClick={() => onMonthChange(1)}\n        className='df-range-picker-calendar-nav-button'\n      >\n        <ChevronRight width={14} height={12} />\n      </button>\n      <button\n        type='button'\n        disabled={disabled}\n        onClick={() => onYearChange(1)}\n        className='df-range-picker-calendar-nav-button'\n      >\n        <ChevronsRight width={14} height={12} />\n      </button>\n    </div>\n  </div>\n);\n\nexport default CalendarHeader;\n"
  },
  {
    "path": "packages/ui/range-picker/src/components/RangePickerPanel.tsx",
    "content": "import { ZonedRange } from '@ui-range-picker/types';\nimport { RefObject, JSX } from 'preact';\nimport { Temporal } from 'temporal-polyfill';\n\nimport CalendarGrid from './CalendarGrid';\nimport CalendarHeader from './CalendarHeader';\nimport TimeSelector from './TimeSelector';\n\ninterface RangePickerPanelProps {\n  visibleMonth: Temporal.PlainDate;\n  monthLabels: string[];\n  weekDayLabels: string[];\n  calendarDays: Temporal.PlainDate[];\n  draftRange: ZonedRange;\n  focusedField: 'start' | 'end';\n  isTimeEnabled: boolean;\n  disabled?: boolean;\n  matchTriggerWidth?: boolean;\n  popupRef: RefObject<HTMLDivElement>;\n  timeListRefs: RefObject<{\n    start: { hour: HTMLDivElement | null; minute: HTMLDivElement | null };\n    end: { hour: HTMLDivElement | null; minute: HTMLDivElement | null };\n  }>;\n  onMonthChange: (months: number) => void;\n  onYearChange: (years: number) => void;\n  onDaySelect: (day: Temporal.PlainDate) => void;\n  onHourSelect: (field: 'start' | 'end', hour: number) => void;\n  onMinuteSelect: (field: 'start' | 'end', minute: number) => void;\n  onOk: () => void;\n  getPopupStyle: () => JSX.CSSProperties;\n}\n\nconst RangePickerPanel = ({\n  visibleMonth,\n  monthLabels,\n  weekDayLabels,\n  calendarDays,\n  draftRange,\n  focusedField,\n  isTimeEnabled,\n  disabled,\n  matchTriggerWidth,\n  popupRef,\n  timeListRefs,\n  onMonthChange,\n  onYearChange,\n  onDaySelect,\n  onHourSelect,\n  onMinuteSelect,\n  onOk,\n  getPopupStyle,\n}: RangePickerPanelProps) => {\n  const startDate = draftRange[0].toPlainDate();\n  const endDate = draftRange[1].toPlainDate();\n\n  return (\n    <div\n      ref={popupRef}\n      className='df-range-picker df-range-picker-popup'\n      style={getPopupStyle()}\n      data-range-picker-popup='true'\n    >\n      <div\n        className='df-range-picker-panel'\n        style={{\n          boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',\n          width: matchTriggerWidth ? '100%' : undefined,\n        }}\n      >\n        <div className='df-range-picker-panel-body'>\n          <div className='df-range-picker-calendar-pane'>\n            <CalendarHeader\n              visibleMonth={visibleMonth}\n              monthLabels={monthLabels}\n              disabled={disabled}\n              onMonthChange={onMonthChange}\n              onYearChange={onYearChange}\n            />\n            <CalendarGrid\n              calendarDays={calendarDays}\n              visibleMonth={visibleMonth}\n              startDate={startDate}\n              endDate={endDate}\n              weekDayLabels={weekDayLabels}\n              disabled={disabled}\n              onDaySelect={onDaySelect}\n            />\n          </div>\n\n          {isTimeEnabled && (\n            <div className='df-range-picker-time-pane'>\n              <TimeSelector\n                focusedField={focusedField}\n                draftRange={draftRange}\n                disabled={disabled}\n                onHourSelect={onHourSelect}\n                onMinuteSelect={onMinuteSelect}\n                timeListRefs={timeListRefs}\n              />\n            </div>\n          )}\n        </div>\n\n        <div className='df-range-picker-footer'>\n          <button\n            type='button'\n            onClick={onOk}\n            disabled={disabled}\n            className='df-range-picker-confirm-button'\n          >\n            OK\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default RangePickerPanel;\n"
  },
  {
    "path": "packages/ui/range-picker/src/components/TimeSelector.tsx",
    "content": "import { HOURS, MINUTES } from '@ui-range-picker/constants';\nimport { ZonedRange } from '@ui-range-picker/types';\nimport { pad } from '@ui-range-picker/utils/rangePicker';\nimport { h, RefObject } from 'preact';\n\nconst scrollbarHide = 'df-scrollbar-hide';\n\ninterface TimeSelectorProps {\n  focusedField: 'start' | 'end';\n  draftRange: ZonedRange;\n  disabled?: boolean;\n  onHourSelect: (field: 'start' | 'end', hour: number) => void;\n  onMinuteSelect: (field: 'start' | 'end', minute: number) => void;\n  timeListRefs: RefObject<{\n    start: { hour: HTMLDivElement | null; minute: HTMLDivElement | null };\n    end: { hour: HTMLDivElement | null; minute: HTMLDivElement | null };\n  }>;\n}\n\nconst TimeSelector = ({\n  focusedField,\n  draftRange,\n  disabled,\n  onHourSelect,\n  onMinuteSelect,\n  timeListRefs,\n}: TimeSelectorProps) => {\n  const field = focusedField;\n  const index = field === 'start' ? 0 : 1;\n  const current = draftRange[index];\n  const currentMinute = current.minute;\n  const minuteOptions = MINUTES.includes(currentMinute)\n    ? MINUTES\n    : [...MINUTES, currentMinute].toSorted((a, b) => a - b);\n\n  return (\n    <div className='df-range-picker-time-selector'>\n      <div className='df-range-picker-time-selector-header'>\n        <div className='df-range-picker-time-selector-value'>\n          {current.hour.toString().padStart(2, '0')}:\n          {current.minute.toString().padStart(2, '0')}\n        </div>\n      </div>\n\n      <div className='df-range-picker-time-selector-body'>\n        <div className='df-range-picker-time-selector-column'>\n          <div\n            className={`df-range-picker-time-list ${scrollbarHide}`}\n            role='listbox'\n            aria-label='Hour'\n            ref={element => {\n              if (timeListRefs.current && timeListRefs.current[field]) {\n                timeListRefs.current[field].hour = element;\n              }\n            }}\n          >\n            {HOURS.map(hour => {\n              const isActive = hour === current.hour;\n              return (\n                <button\n                  key={hour}\n                  type='button'\n                  role='option'\n                  aria-selected={isActive}\n                  disabled={disabled}\n                  onClick={() => onHourSelect(field, hour)}\n                  className='df-range-picker-time-option'\n                  data-active={isActive ? 'true' : undefined}\n                >\n                  {pad(hour)}\n                </button>\n              );\n            })}\n          </div>\n        </div>\n        <div className='df-range-picker-time-selector-column'>\n          <div\n            className={`df-range-picker-time-list ${scrollbarHide}`}\n            role='listbox'\n            aria-label='Minute'\n            ref={element => {\n              if (timeListRefs.current && timeListRefs.current[field]) {\n                timeListRefs.current[field].minute = element;\n              }\n            }}\n          >\n            {minuteOptions.map(minute => {\n              const isActive = minute === currentMinute;\n              return (\n                <button\n                  key={minute}\n                  type='button'\n                  role='option'\n                  aria-selected={isActive}\n                  disabled={disabled}\n                  onClick={() => onMinuteSelect(field, minute)}\n                  className='df-range-picker-time-option'\n                  data-active={isActive ? 'true' : undefined}\n                >\n                  {pad(minute)}\n                </button>\n              );\n            })}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default TimeSelector;\n"
  },
  {
    "path": "packages/ui/range-picker/src/constants.ts",
    "content": "export const DEFAULT_FORMAT = 'YYYY-MM-DD';\nexport const DEFAULT_TIME_FORMAT = 'HH:mm';\nexport const HOURS = Array.from({ length: 24 }, (_, index) => index);\nexport const MINUTES = Array.from({ length: 60 }, (_, index) => index);\n"
  },
  {
    "path": "packages/ui/range-picker/src/icons.tsx",
    "content": "interface IconProps {\n  className?: string;\n  width?: number;\n  height?: number;\n}\n\nexport const ChevronLeft = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m15 18-6-6 6-6' />\n  </svg>\n);\n\nexport const ChevronRight = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m9 18 6-6-6-6' />\n  </svg>\n);\n\nexport const ChevronsLeft = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m11 17-5-5 5-5' />\n    <path d='m18 17-5-5 5-5' />\n  </svg>\n);\n\nexport const ChevronsRight = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='m6 17 5-5-5-5' />\n    <path d='m13 17 5-5-5-5' />\n  </svg>\n);\n\nexport const MoveRight = ({\n  className,\n  width = 24,\n  height = 24,\n}: IconProps) => (\n  <svg\n    xmlns='http://www.w3.org/2000/svg'\n    width={width}\n    height={height}\n    viewBox='0 0 24 24'\n    fill='none'\n    stroke='currentColor'\n    stroke-width='2'\n    stroke-linecap='round'\n    stroke-linejoin='round'\n    className={className}\n  >\n    <path d='M18 8L22 12L18 16' />\n    <path d='M2 12H22' />\n  </svg>\n);\n"
  },
  {
    "path": "packages/ui/range-picker/src/index.ts",
    "content": "export { default as RangePicker } from './RangePicker';\nexport type { RangePickerProps, ZonedRange, Locale } from './types';\n"
  },
  {
    "path": "packages/ui/range-picker/src/styles/range-picker.css",
    "content": "/* ── DayFlow UI – Range Picker ──────────────────────────────────────────── */\n\n@layer components {\n  .df-range-picker {\n    --df-range-picker-background: rgb(255 255 255);\n    --df-range-picker-border: rgb(226 232 240);\n    --df-range-picker-foreground: rgb(46 46 46);\n    --df-range-picker-muted: rgb(148 163 184);\n    --df-range-picker-primary: rgb(46 46 46);\n    --df-range-picker-primary-foreground: rgb(255 255 255);\n    --df-range-picker-shadow-sm: 0 1px 2px rgb(15 23 42 / 0.08);\n    --df-range-picker-shadow-lg: 0 6px 18px rgb(15 23 42 / 0.12);\n    --df-range-picker-button-shadow: 0 1px 2px rgb(15 23 42 / 0.12);\n    --df-color-background: var(--df-range-picker-background);\n    --df-color-border: var(--df-range-picker-border);\n    --df-color-foreground: var(--df-range-picker-foreground);\n    --df-color-muted-foreground: var(--df-range-picker-muted);\n    --df-color-primary: var(--df-range-picker-primary);\n    --df-color-primary-foreground: var(--df-range-picker-primary-foreground);\n    --color-background: var(--df-color-background);\n    --color-border: var(--df-color-border);\n    --color-foreground: var(--df-color-foreground);\n    --color-primary: var(--df-color-primary);\n    --color-primary-foreground: var(--df-color-primary-foreground);\n  }\n\n  .dark .df-range-picker {\n    --df-range-picker-background: rgb(31 41 55);\n    --df-range-picker-border: rgb(75 85 99);\n    --df-range-picker-foreground: rgb(229 229 229);\n    --df-range-picker-muted: rgb(107 114 128);\n    --df-range-picker-primary: rgb(229 229 229);\n    --df-range-picker-primary-foreground: rgb(15 23 42);\n  }\n\n  @media (prefers-color-scheme: dark) {\n    :root:not(.light):not(.dark) .df-range-picker {\n      --df-range-picker-background: rgb(31 41 55);\n      --df-range-picker-border: rgb(75 85 99);\n      --df-range-picker-foreground: rgb(229 229 229);\n      --df-range-picker-muted: rgb(107 114 128);\n      --df-range-picker-primary: rgb(229 229 229);\n      --df-range-picker-primary-foreground: rgb(23 23 23);\n    }\n  }\n\n  .df-range-picker-root {\n    position: relative;\n    width: 100%;\n    min-width: 0;\n  }\n\n  .df-range-picker-trigger {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.5rem;\n    background-color: var(--df-color-background);\n    color: var(--df-color-foreground);\n    box-shadow: var(--df-range-picker-shadow-sm);\n    transition:\n      border-color 120ms ease,\n      box-shadow 120ms ease,\n      background-color 120ms ease,\n      color 120ms ease;\n  }\n\n  .df-range-picker-trigger[data-open='true'] {\n    border-color: var(--df-color-primary);\n    box-shadow: var(--df-range-picker-shadow-lg);\n  }\n\n  .df-range-picker-trigger[data-disabled='true'] {\n    cursor: not-allowed;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-range-picker-field-group {\n    display: flex;\n    flex: 1 1 0;\n    min-width: 0;\n    flex-direction: column;\n    gap: 0.25rem;\n  }\n\n  .df-range-picker-input {\n    width: 100%;\n    border: 1px solid transparent;\n    border-radius: 0.375rem;\n    background: transparent;\n    padding: 0.375rem 0.5rem;\n    color: var(--df-color-foreground);\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    transition:\n      color 120ms ease,\n      background-color 120ms ease,\n      box-shadow 120ms ease;\n    outline: none;\n  }\n\n  .df-range-picker-input[data-focused='true'] {\n    background-color: var(--df-color-background);\n    color: var(--df-color-primary);\n  }\n\n  .df-range-picker-input[data-disabled='true'] {\n    cursor: not-allowed;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-range-picker-input:focus-visible {\n    box-shadow:\n      0 0 0 0px var(--df-color-background),\n      0 0 0 2px var(--df-color-primary);\n  }\n\n  .df-range-picker-separator-icon {\n    flex-shrink: 0;\n    color: var(--df-color-muted-foreground);\n  }\n\n  .df-range-picker-popup {\n    z-index: 9999;\n  }\n\n  .df-range-picker-panel {\n    display: flex;\n    flex-direction: column;\n    gap: 0.75rem;\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.75rem;\n    background-color: var(--df-color-background);\n    padding: 0.75rem;\n  }\n\n  .df-range-picker-panel-body {\n    display: flex;\n    gap: 0.25rem;\n  }\n\n  .df-range-picker-calendar-pane {\n    min-width: 0;\n    flex: 3 1 0;\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.75rem;\n    background-color: var(--df-color-background);\n    box-shadow: var(--df-range-picker-shadow-sm);\n  }\n\n  .df-range-picker-time-pane {\n    display: flex;\n    flex: 1 1 0;\n    justify-content: flex-end;\n  }\n\n  @media (min-width: 640px) {\n    .df-range-picker-time-pane {\n      width: 8rem;\n    }\n  }\n\n  .df-range-picker-footer {\n    display: flex;\n    justify-content: flex-end;\n  }\n\n  .df-range-picker-confirm-button {\n    display: inline-flex;\n    align-items: center;\n    border: 0;\n    border-radius: 9999px;\n    background-color: var(--df-color-primary);\n    padding: 0.375rem 1rem;\n    color: var(--df-color-primary-foreground);\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 600;\n    box-shadow: var(--df-range-picker-button-shadow);\n    transition:\n      opacity 120ms ease,\n      transform 120ms ease,\n      background-color 120ms ease;\n  }\n\n  .df-range-picker-confirm-button:hover:not(:disabled) {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 90%,\n      transparent\n    );\n  }\n\n  .df-range-picker-confirm-button:disabled {\n    cursor: not-allowed;\n    opacity: 0.5;\n  }\n\n  .df-range-picker-calendar-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    border-bottom: 1px solid\n      color-mix(in srgb, var(--df-color-border) 80%, transparent);\n    padding: 0.5rem 0.75rem;\n    color: var(--df-color-foreground);\n  }\n\n  .df-range-picker-calendar-nav-group {\n    display: flex;\n    align-items: center;\n    gap: 0.25rem;\n  }\n\n  .df-range-picker-calendar-nav-button {\n    border: 0;\n    border-radius: 0.375rem;\n    background: transparent;\n    padding: 0.25rem 0.5rem;\n    color: var(--df-color-muted-foreground);\n    transition:\n      color 120ms ease,\n      background-color 120ms ease,\n      opacity 120ms ease;\n  }\n\n  .df-range-picker-calendar-nav-button:hover:not(:disabled) {\n    color: var(--df-color-foreground);\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 10%,\n      transparent\n    );\n  }\n\n  .df-range-picker-calendar-nav-button:disabled {\n    opacity: 0.4;\n  }\n\n  .df-range-picker-calendar-title {\n    color: var(--df-color-foreground);\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 600;\n  }\n\n  .df-range-picker-weekday-row,\n  .df-range-picker-day-grid {\n    display: grid;\n    grid-template-columns: repeat(7, minmax(0, 1fr));\n    gap: 0.25rem;\n    padding-inline: 0.25rem;\n  }\n\n  .df-range-picker-weekday-row {\n    padding-top: 0.5rem;\n    padding-bottom: 0.75rem;\n  }\n\n  .df-range-picker-weekday-label {\n    text-align: center;\n    color: var(--df-color-muted-foreground);\n    font-size: 12px;\n    line-height: 1rem;\n    letter-spacing: 0.08em;\n    text-transform: uppercase;\n  }\n\n  .df-range-picker-day-cell {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    width: 2.25rem;\n    height: 2.25rem;\n    border: 0;\n    border-radius: 0.375rem;\n    background: transparent;\n    color: var(--df-color-foreground);\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    transition:\n      background-color 120ms ease,\n      color 120ms ease,\n      opacity 120ms ease;\n  }\n\n  .df-range-picker-day-cell:hover:not(:disabled):not([data-range-edge]) {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 10%,\n      transparent\n    );\n    color: var(--df-color-primary);\n  }\n\n  .df-range-picker-day-cell[data-outside='true'] {\n    color: color-mix(\n      in srgb,\n      var(--df-color-muted-foreground) 75%,\n      transparent\n    );\n  }\n\n  .df-range-picker-day-cell[data-in-range='true'] {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 10%,\n      transparent\n    );\n    color: var(--df-color-primary);\n  }\n\n  .df-range-picker-day-cell[data-range-edge='start'],\n  .df-range-picker-day-cell[data-range-edge='end'] {\n    background-color: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n    font-weight: 600;\n  }\n\n  .df-range-picker-time-selector {\n    display: flex;\n    flex-direction: column;\n    border: 1px solid var(--df-color-border);\n    border-radius: 0.75rem;\n    background-color: var(--df-color-background);\n    box-shadow: var(--df-range-picker-shadow-sm);\n  }\n\n  @media (min-width: 640px) {\n    .df-range-picker-time-selector {\n      width: 7rem;\n    }\n  }\n\n  .df-range-picker-time-selector-header {\n    display: flex;\n    justify-content: center;\n    border-bottom: 1px solid\n      color-mix(in srgb, var(--df-color-border) 80%, transparent);\n  }\n\n  .df-range-picker-time-selector-value {\n    padding-block: 0.375rem;\n    color: var(--df-color-foreground);\n    font-size: 1rem;\n    line-height: 1.5rem;\n  }\n\n  .df-range-picker-time-selector-body {\n    display: flex;\n    padding: 0.25rem;\n  }\n\n  .df-range-picker-time-selector-column {\n    width: 3.5rem;\n  }\n\n  .df-range-picker-time-list {\n    height: 18rem;\n    overflow-y: auto;\n    border: 1px solid\n      color-mix(in srgb, var(--df-color-border) 80%, transparent);\n    border-radius: 0.375rem;\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-background) 92%,\n      white 8%\n    );\n  }\n\n  .df-range-picker-time-option {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 100%;\n    height: 2rem;\n    border: 0;\n    background: transparent;\n    color: var(--df-color-foreground);\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    transition:\n      background-color 120ms ease,\n      color 120ms ease,\n      opacity 120ms ease;\n  }\n\n  .df-range-picker-time-option:hover:not(:disabled):not([data-active='true']) {\n    background-color: color-mix(\n      in srgb,\n      var(--df-color-primary) 10%,\n      transparent\n    );\n    color: var(--df-color-primary);\n  }\n\n  .df-range-picker-time-option[data-active='true'] {\n    background-color: var(--df-color-primary);\n    color: var(--df-color-primary-foreground);\n    font-weight: 600;\n  }\n}\n\n.df-scrollbar-hide::-webkit-scrollbar {\n  display: none;\n}\n\n.df-scrollbar-hide {\n  -ms-overflow-style: none;\n  scrollbar-width: none;\n}\n"
  },
  {
    "path": "packages/ui/range-picker/src/styles/tailwind-components.css",
    "content": "/* styles.components.css — for projects that already have Tailwind CSS\n *\n * This file includes DayFlow range-picker component styles only.\n * It does NOT emit Tailwind utility classes.\n */\n@layer theme, base, df-theme, components, utilities;\n\n@import 'tailwindcss/theme' layer(theme);\n@import './range-picker.css';\n\n@variant dark (.dark &);\n"
  },
  {
    "path": "packages/ui/range-picker/src/styles/tailwind.css",
    "content": "/* styles.css — standalone DayFlow range-picker styles\n *\n * This file now ships semantic component CSS only.\n * It intentionally avoids emitting atomic utility classes.\n */\n@layer theme, base, df-theme, components, utilities;\n\n@import 'tailwindcss/theme' layer(theme);\n\n@import './range-picker.css';\n"
  },
  {
    "path": "packages/ui/range-picker/src/types.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nexport type ZonedRange = [Temporal.ZonedDateTime, Temporal.ZonedDateTime];\n\nexport interface Locale {\n  code: string;\n  messages?: Record<string, string>;\n}\n\nexport interface RangePickerProps {\n  value: [\n    Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime,\n    Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime,\n  ];\n  format?: string;\n  showTimeFormat?: string;\n  showTime?: boolean | { format?: string };\n  onChange?: (value: ZonedRange, dateString: [string, string]) => void;\n  onOk?: (value: ZonedRange, dateString: [string, string]) => void;\n  timeZone?: string;\n  startOfWeek?: number;\n  disabled?: boolean;\n  placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight';\n  autoAdjustOverflow?: boolean;\n  getPopupContainer?: () => HTMLElement;\n  matchTriggerWidth?: boolean;\n  locale?: string | Locale;\n}\n"
  },
  {
    "path": "packages/ui/range-picker/src/utils/locale.ts",
    "content": "/**\n * Get localized weekday labels starting from startOfWeek (0: Sun, 1: Mon, etc.)\n */\nexport const getWeekDaysLabels = (\n  locale: string,\n  format: 'long' | 'short' | 'narrow' = 'short',\n  startOfWeek = 1\n): string[] => {\n  const labels: string[] = [];\n  // Use a known date (2024-01-07 was a Sunday)\n  const baseDate = new Date(2024, 0, 7);\n  for (let i = 0; i < 7; i++) {\n    const date = new Date(baseDate);\n    date.setDate(baseDate.getDate() + startOfWeek + i);\n    try {\n      labels.push(date.toLocaleDateString(locale, { weekday: format }));\n    } catch {\n      labels.push(date.toLocaleDateString('en-US', { weekday: format }));\n    }\n  }\n  return labels;\n};\n\n/**\n * Get localized month labels\n */\nexport const getMonthLabels = (\n  locale: string,\n  format: 'long' | 'short' | 'narrow' | 'numeric' | '2-digit' = 'long'\n): string[] => {\n  const labels: string[] = [];\n  for (let i = 0; i < 12; i++) {\n    const date = new Date(2024, i, 1);\n    try {\n      labels.push(date.toLocaleDateString(locale, { month: format }));\n    } catch {\n      labels.push(date.toLocaleDateString('en-US', { month: format }));\n    }\n  }\n  return labels;\n};\n"
  },
  {
    "path": "packages/ui/range-picker/src/utils/rangePicker.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nimport { isPlainDate } from './temporal';\n\n// Temporal polyfill implementations expose the timezone in different shapes.\n// These interfaces describe the properties we probe at runtime.\ninterface TemporalZoneShape {\n  timeZoneId?: string;\n  timeZone?: string | { id?: string };\n}\n\ninterface TemporalLike extends TemporalZoneShape {\n  year?: number;\n  month?: number;\n  day?: number;\n  hour?: number;\n  minute?: number;\n  second?: number;\n  millisecond?: number;\n  microsecond?: number;\n  nanosecond?: number;\n  toZonedDateTime?: (zone: string) => Temporal.ZonedDateTime;\n}\n\nconst TOKEN_REGEX = /(YYYY|YY|MM|DD|HH|mm)/g;\n\nexport const pad = (input: number) => input.toString().padStart(2, '0');\n\nconst sanitizeFormatTemplate = (template: string): string =>\n  template.replaceAll(/(H{1,2}):MM/g, (_match, hours) => `${hours}:mm`);\n\nexport const mergeFormatTemplate = (\n  dateFormat: string,\n  timeFormat: string\n): string => {\n  const trimmedTime = (timeFormat ?? '').trim();\n  const hasTimeTokens = /[Hhms]/.test(dateFormat);\n  const combined =\n    hasTimeTokens || !trimmedTime\n      ? dateFormat\n      : `${dateFormat} ${trimmedTime}`.trim();\n  return sanitizeFormatTemplate(combined);\n};\n\nconst escapeRegExp = (value: string): string =>\n  value.replaceAll(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&');\n\nexport const buildParseRegExp = (template: string): RegExp => {\n  let lastIndex = 0;\n  let pattern = '^';\n  let match: RegExpExecArray | null;\n\n  while ((match = TOKEN_REGEX.exec(template)) !== null) {\n    pattern += escapeRegExp(template.slice(lastIndex, match.index));\n    const token = match[0];\n    const length = token === 'YYYY' ? 4 : 2;\n    pattern += `(?<${token}>\\\\d{${length}})`;\n    lastIndex = match.index + token.length;\n  }\n\n  pattern += escapeRegExp(template.slice(lastIndex)) + '$';\n  return new RegExp(pattern);\n};\n\nexport const parseTemporalString = (\n  input: string,\n  regExp: RegExp,\n  reference: Temporal.ZonedDateTime,\n  zoneId: string\n): Temporal.ZonedDateTime | null => {\n  const trimmed = input.trim();\n  if (!trimmed) return null;\n\n  const match = trimmed.match(regExp);\n  const groups = match?.groups as\n    | Record<string, string | undefined>\n    | undefined;\n  if (!groups) return null;\n\n  const resolvedYear = groups.YYYY\n    ? Number(groups.YYYY)\n    : groups.YY\n      ? Number(groups.YY) + 2000\n      : reference.year;\n\n  const resolvedMonth = groups.MM ? Number(groups.MM) : reference.month;\n  const resolvedDay = groups.DD ? Number(groups.DD) : reference.day;\n  const resolvedHour = groups.HH ? Number(groups.HH) : reference.hour;\n  const resolvedMinute = groups.mm ? Number(groups.mm) : reference.minute;\n\n  try {\n    return Temporal.ZonedDateTime.from({\n      timeZone: zoneId,\n      year: resolvedYear,\n      month: resolvedMonth,\n      day: resolvedDay,\n      hour: resolvedHour,\n      minute: resolvedMinute,\n      second: reference.second,\n      millisecond: reference.millisecond,\n      microsecond: reference.microsecond,\n      nanosecond: reference.nanosecond,\n    });\n  } catch {\n    return null;\n  }\n};\n\nexport const getZoneId = (value: Temporal.ZonedDateTime): string => {\n  const v = value as unknown as TemporalZoneShape;\n  if (typeof v.timeZoneId === 'string') return v.timeZoneId;\n  if (typeof v.timeZone === 'string') return v.timeZone;\n  if (typeof (v.timeZone as { id?: string } | undefined)?.id === 'string')\n    return (v.timeZone as { id: string }).id;\n  return Temporal.Now.timeZoneId();\n};\n\nexport const normalizeToZoned = (\n  input: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime,\n  fallbackZone?: string,\n  fallbackTemporal?: Temporal.ZonedDateTime\n): Temporal.ZonedDateTime => {\n  if (!input) {\n    const zoneId =\n      fallbackZone ??\n      (fallbackTemporal\n        ? getZoneId(fallbackTemporal)\n        : Temporal.Now.timeZoneId());\n    return fallbackTemporal ?? Temporal.Now.zonedDateTimeISO(zoneId);\n  }\n\n  if (isPlainDate(input)) {\n    const zoneId = fallbackZone ?? Temporal.Now.timeZoneId();\n    const isoString = `${input.year}-${pad(input.month)}-${pad(input.day)}T00:00:00[${zoneId}]`;\n    return Temporal.ZonedDateTime.from(isoString);\n  }\n\n  const candidate = input as unknown as TemporalLike;\n\n  if ('hour' in candidate && !('timeZoneId' in candidate)) {\n    const zoneId = fallbackZone ?? Temporal.Now.timeZoneId();\n    if (typeof candidate.toZonedDateTime === 'function') {\n      try {\n        return candidate.toZonedDateTime(zoneId);\n      } catch {\n        // fall through\n      }\n    }\n    return Temporal.ZonedDateTime.from({\n      timeZone: zoneId,\n      year: candidate.year as number,\n      month: candidate.month as number,\n      day: candidate.day as number,\n      hour: candidate.hour as number,\n      minute: candidate.minute as number,\n      second: candidate.second ?? 0,\n      millisecond: candidate.millisecond ?? 0,\n      microsecond: candidate.microsecond ?? 0,\n      nanosecond: candidate.nanosecond ?? 0,\n    });\n  }\n\n  try {\n    return Temporal.ZonedDateTime.from(\n      input as string | Temporal.ZonedDateTimeLike\n    );\n  } catch {\n    const resolvedZone =\n      (typeof candidate.timeZone === 'string'\n        ? candidate.timeZone\n        : candidate.timeZone?.id) ??\n      candidate.timeZoneId ??\n      fallbackZone ??\n      (fallbackTemporal ? getZoneId(fallbackTemporal) : undefined) ??\n      Temporal.Now.timeZoneId();\n\n    if (typeof candidate.toZonedDateTime === 'function') {\n      try {\n        return candidate.toZonedDateTime(resolvedZone);\n      } catch {\n        // fall through\n      }\n    }\n\n    const reference =\n      fallbackTemporal ?? Temporal.Now.zonedDateTimeISO(resolvedZone);\n\n    return Temporal.ZonedDateTime.from({\n      timeZone: resolvedZone,\n      year: candidate.year ?? reference.year,\n      month: candidate.month ?? reference.month,\n      day: candidate.day ?? reference.day,\n      hour: candidate.hour ?? fallbackTemporal?.hour ?? 0,\n      minute: candidate.minute ?? fallbackTemporal?.minute ?? 0,\n      second: candidate.second ?? fallbackTemporal?.second ?? 0,\n      millisecond: candidate.millisecond ?? fallbackTemporal?.millisecond ?? 0,\n      microsecond: candidate.microsecond ?? fallbackTemporal?.microsecond ?? 0,\n      nanosecond: candidate.nanosecond ?? fallbackTemporal?.nanosecond ?? 0,\n    });\n  }\n};\n\nexport const formatTemporal = (\n  value: Temporal.ZonedDateTime,\n  format: string,\n  timeFormat: string\n): string => {\n  const template = mergeFormatTemplate(format, timeFormat);\n\n  const replacements: Record<string, string> = {\n    YYYY: value.year.toString(),\n    YY: pad(value.year % 100),\n    MM: pad(value.month),\n    DD: pad(value.day),\n    HH: pad(value.hour),\n    mm: pad(value.minute),\n  };\n\n  return template.replace(TOKEN_REGEX, token => replacements[token] ?? token);\n};\n"
  },
  {
    "path": "packages/ui/range-picker/src/utils/temporal.ts",
    "content": "import { Temporal } from 'temporal-polyfill';\n\nexport function isPlainDate(temporal: unknown): temporal is Temporal.PlainDate {\n  return (\n    temporal !== null &&\n    typeof temporal === 'object' &&\n    !('hour' in temporal) &&\n    'year' in temporal &&\n    'month' in temporal &&\n    'day' in temporal &&\n    !(temporal instanceof Date)\n  );\n}\n"
  },
  {
    "path": "packages/ui/range-picker/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist/build\",\n    \"rootDir\": \"src\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": false,\n    \"declarationDir\": \"dist/types\",\n    \"declarationMap\": false,\n    \"sourceMap\": false,\n    \"module\": \"ESNext\",\n    \"target\": \"ES2015\",\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": false,\n    \"ignoreDeprecations\": \"5.0\",\n    \"paths\": {\n      \"@ui-range-picker/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"**/*.test.ts\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.ts\",\n    \"**/*.spec.tsx\"\n  ]\n}\n"
  },
  {
    "path": "packages/ui/range-picker/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"esnext\", \"dom\", \"dom.iterable\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"paths\": {\n      \"@ui-range-picker/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/vue/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Jayce Li\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/vue/README.md",
    "content": "# DayFlow Vue\n\nA flexible and feature-rich Vue calendar component library with drag-and-drop support, multiple views, and plugin architecture.\n\n[![npm](https://img.shields.io/npm/v/@dayflow/vue?logo=npm&color=blue&label=version)](https://www.npmjs.com/package/@dayflow/vue)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?logo=github)](https://github.com/dayflow-js/dayflow/pulls)\n[![License](https://img.shields.io/github/license/dayflow-js/dayflow)](https://github.com/dayflow-js/dayflow/blob/main/LICENSE)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/9vdFZKJqBb)\n\n## Features\n\n### Daily, Weekly, Monthly and Yearly View Types\n\n#### Day View\n\n![Day View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/DayView.png)\n\n#### Week View\n\n![Week View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/WeekView.png)\n\n#### Month View\n\n![Month View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/MonthView.png)\n\n#### Year View(Fixed-Week)\n\n![Year View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Year-Fixed-Week.png)\n\n#### Year View(Year-Canvas)\n\n![Year Canvas View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Year-Canvas.png)\n\n### Mobile View Support\n\n#### Mobile Day & Year View\n\n![Mobile Day and Year View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Mobile-Day-Year.png)\n\n#### Mobile Week & Month View\n\n![Mobile Week and Month View](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/Mobile-Week-Month.png)\n\n### Multiple Event Detail Panel options\n\n#### Detail Popup\n\n![Popup](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/popup.png)\n\n#### Detail Dialog\n\n![Dialog](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/dialog.png)\n\n### Dark Mode Support\n\n![Dark Mode](https://raw.githubusercontent.com/dayflow-js/dayflow/main/assets/images/DarkMode.png)\n\n### Easy to resize and drag\n\nhttps://github.com/user-attachments/assets/726a5232-35a8-4fe3-8e7b-4de07c455353\n\nhttps://github.com/user-attachments/assets/957317e5-02d8-4419-a74b-62b7d191e347\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## Bug Reports\n\nIf you find a bug, please file an issue on [GitHub Issues](https://github.com/dayflow-js/dayflow/issues).\n\n## Support\n\nFor questions and support, please open an issue on GitHub or go to discord.\n"
  },
  {
    "path": "packages/vue/package.json",
    "content": "{\n  \"name\": \"@dayflow/vue\",\n  \"version\": \"3.6.2\",\n  \"description\": \"Vue adapter for DayFlow calendar\",\n  \"files\": [\n    \"dist\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"rollup -c\",\n    \"clean\": \"rimraf dist node_modules\",\n    \"prebuild\": \"rimraf dist\",\n    \"typecheck\": \"vue-tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@dayflow/core\": \"workspace:*\",\n    \"@rollup/plugin-commonjs\": \"catalog:\",\n    \"@rollup/plugin-node-resolve\": \"catalog:\",\n    \"@rollup/plugin-terser\": \"catalog:\",\n    \"@rollup/plugin-typescript\": \"catalog:\",\n    \"rimraf\": \"catalog:\",\n    \"rollup\": \"catalog:\",\n    \"rollup-plugin-peer-deps-external\": \"catalog:\",\n    \"rollup-plugin-vue\": \"catalog:\",\n    \"typescript\": \"catalog:\",\n    \"vue\": \"^3.5.13\",\n    \"vue-tsc\": \"^2.2.0\"\n  },\n  \"peerDependencies\": {\n    \"@dayflow/core\": \"workspace:*\",\n    \"vue\": \">=3.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/vue/rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport terser from '@rollup/plugin-terser';\nimport typescript from '@rollup/plugin-typescript';\nimport peerDepsExternal from 'rollup-plugin-peer-deps-external';\nimport vue from 'rollup-plugin-vue';\n\nexport default {\n  input: 'src/index.ts',\n  output: [\n    {\n      file: 'dist/index.js',\n      format: 'esm',\n      sourcemap: false,\n      exports: 'named',\n    },\n  ],\n  plugins: [\n    peerDepsExternal(),\n    resolve({\n      extensions: ['.js', '.ts', '.vue'],\n    }),\n    vue(),\n    typescript({\n      tsconfig: './tsconfig.json',\n      declaration: true,\n      declarationDir: 'dist',\n    }),\n    commonjs(),\n    terser(),\n  ],\n  external: ['vue', '@dayflow/core'],\n};\n"
  },
  {
    "path": "packages/vue/src/DayFlowCalendar.ts",
    "content": "import type {\n  ICalendarApp,\n  CustomRendering,\n  UseCalendarAppReturn,\n  CalendarSearchProps,\n} from '@dayflow/core';\nimport { CalendarRenderer } from '@dayflow/core';\nimport type { PropType } from 'vue';\nimport {\n  defineComponent,\n  h,\n  ref,\n  onMounted,\n  onUnmounted,\n  shallowRef,\n  Teleport,\n  computed,\n  watch,\n} from 'vue';\n\nexport const DayFlowCalendar = defineComponent({\n  name: 'DayFlowCalendar',\n  props: {\n    calendar: {\n      type: Object as PropType<ICalendarApp | UseCalendarAppReturn>,\n      required: true,\n    },\n    collapsedSafeAreaLeft: {\n      type: Number as PropType<number>,\n      default: undefined,\n    },\n    search: {\n      type: Object as PropType<CalendarSearchProps>,\n      default: undefined,\n    },\n  },\n  setup(props, { slots }) {\n    const container = ref<HTMLElement | null>(null);\n    const renderer = shallowRef<CalendarRenderer | null>(null);\n    const customRenderings = ref<CustomRendering[]>([]);\n\n    // Store subscription cleanup so it can be replaced when the app changes.\n    let storeUnsubscribe: (() => void) | null = null;\n\n    // Extract underlying app instance\n    const app = computed<ICalendarApp>(\n      () =>\n        (props.calendar as UseCalendarAppReturn).app ||\n        (props.calendar as ICalendarApp)\n    );\n\n    // All renderer-level props in one object so a single watcher handles them all.\n    const extraProps = computed(() => ({\n      collapsedSafeAreaLeft: props.collapsedSafeAreaLeft,\n      search: props.search,\n    }));\n\n    function initRenderer(appInstance: ICalendarApp) {\n      if (!container.value) {\n        return;\n      }\n\n      // Tear down the previous renderer if the app instance was replaced.\n      storeUnsubscribe?.();\n      renderer.value?.unmount();\n\n      const r = new CalendarRenderer(appInstance, Object.keys(slots));\n      renderer.value = r;\n      r.setProps(extraProps.value);\n      r.mount(container.value);\n\n      storeUnsubscribe = r.getCustomRenderingStore().subscribe(renderings => {\n        customRenderings.value = [...renderings.values()];\n      });\n    }\n\n    watch(\n      extraProps,\n      val => {\n        renderer.value?.setProps(val);\n      },\n      { deep: true }\n    );\n\n    // Recreate the renderer when the calendar prop is replaced (e.g. after\n    // client-side navigation to a different calendar instance).\n    watch(app, newApp => {\n      if (container.value) {\n        initRenderer(newApp);\n      }\n    });\n\n    onMounted(() => {\n      initRenderer(app.value);\n\n      onUnmounted(() => {\n        storeUnsubscribe?.();\n        renderer.value?.unmount();\n        renderer.value = null;\n      });\n    });\n\n    return () => [\n      h('div', { ref: container, class: 'df-calendar-wrapper' }),\n      ...customRenderings.value.map(rendering =>\n        h(\n          Teleport,\n          { to: rendering.containerEl },\n          {\n            default: () =>\n              slots[rendering.generatorName]?.(rendering.generatorArgs),\n          }\n        )\n      ),\n    ];\n  },\n});\n\nexport default DayFlowCalendar;\n"
  },
  {
    "path": "packages/vue/src/composables/useCalendarApp.ts",
    "content": "import {\n  CalendarApp,\n  createConfigSyncSnapshot,\n  createNormalizedCalendarAppConfigGetter,\n  syncCalendarAppConfig,\n} from '@dayflow/core';\nimport type { CalendarAppConfig, UseCalendarAppReturn } from '@dayflow/core';\nimport { onUnmounted, reactive, watchEffect } from 'vue';\n\nexport function useCalendarApp(\n  config: CalendarAppConfig\n): UseCalendarAppReturn {\n  const getNormalizedConfig = createNormalizedCalendarAppConfigGetter(\n    () => config\n  );\n\n  const app = new CalendarApp(getNormalizedConfig());\n  let syncSnapshot = createConfigSyncSnapshot(getNormalizedConfig());\n\n  // Use reactive state to trigger Vue re-renders\n  const state = reactive({\n    currentView: app.state.currentView,\n    currentDate: app.state.currentDate,\n    events: app.getEvents(),\n  });\n\n  const unsubscribe = app.subscribe(updatedApp => {\n    state.currentView = updatedApp.state.currentView;\n    state.currentDate = updatedApp.state.currentDate;\n    state.events = updatedApp.getEvents();\n  });\n\n  watchEffect(() => {\n    syncSnapshot = syncCalendarAppConfig(\n      app,\n      syncSnapshot,\n      getNormalizedConfig()\n    );\n  });\n\n  onUnmounted(() => {\n    unsubscribe();\n  });\n\n  return {\n    app,\n    // Use computed or simple reactive access\n    get currentView() {\n      return state.currentView;\n    },\n    get currentDate() {\n      return state.currentDate;\n    },\n    get events() {\n      return state.events;\n    },\n\n    // Bind methods to app\n    applyEventsChanges: app.applyEventsChanges.bind(app),\n    changeView: app.changeView.bind(app),\n    setCurrentDate: app.setCurrentDate.bind(app),\n    addEvent: app.addEvent.bind(app),\n    updateEvent: app.updateEvent.bind(app),\n    deleteEvent: app.deleteEvent.bind(app),\n    undo: app.undo.bind(app),\n    goToToday: app.goToToday.bind(app),\n    goToPrevious: app.goToPrevious.bind(app),\n    goToNext: app.goToNext.bind(app),\n    selectDate: app.selectDate.bind(app),\n    getCalendars: app.getCalendars.bind(app),\n    createCalendar: app.createCalendar.bind(app),\n    mergeCalendars: app.mergeCalendars.bind(app),\n    setCalendarVisibility: app.setCalendarVisibility.bind(app),\n    setAllCalendarsVisibility: app.setAllCalendarsVisibility.bind(app),\n    getAllEvents: app.getAllEvents.bind(app),\n    highlightEvent: app.highlightEvent.bind(app),\n    setVisibleMonth: app.setVisibleMonth.bind(app),\n    getVisibleMonth: app.getVisibleMonth.bind(app),\n    emitVisibleRange: app.emitVisibleRange.bind(app),\n    canMutateFromUI: app.canMutateFromUI.bind(app),\n    get readOnlyConfig() {\n      return app.getReadOnlyConfig();\n    },\n  } as UseCalendarAppReturn;\n}\n"
  },
  {
    "path": "packages/vue/src/index.ts",
    "content": "import DayFlowCalendarComponent from './DayFlowCalendar';\nexport { DayFlowCalendarComponent as DayFlowCalendar };\nexport { useCalendarApp } from './composables/useCalendarApp';\nexport default DayFlowCalendarComponent;\n\n// Re-export core parts for convenience\nexport {\n  CalendarApp,\n  CalendarRegistry,\n  createEventsPlugin,\n  createDayView,\n  createWeekView,\n  createMonthView,\n  createAgendaView,\n  createYearView,\n  ViewType,\n} from '@dayflow/core';\n\nexport * from '@dayflow/core';\n"
  },
  {
    "path": "packages/vue/src/vue-shims.d.ts",
    "content": "declare module '*.vue' {\n  import type { DefineComponent } from 'vue';\n  const component: DefineComponent<\n    Record<string, unknown>,\n    Record<string, unknown>,\n    unknown\n  >;\n  export default component;\n}\n"
  },
  {
    "path": "packages/vue/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"jsx\": \"preserve\",\n    \"moduleResolution\": \"node\",\n    \"allowImportingTsExtensions\": false,\n    \"paths\": {\n      \"@dayflow/core\": [\"../../packages/core/dist/index.d.ts\"]\n    }\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.d.ts\", \"src/**/*.tsx\", \"src/**/*.vue\"]\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - 'packages/*'\n  - 'packages/plugins/*'\n  - 'packages/ui/*'\n  # - 'website'\n  - 'examples/*'\n\ncatalog:\n  '@dayflow/blossom-color-picker': ^2.0.1\n  '@preact/preset-vite': ^2.10.3\n  '@rollup/plugin-commonjs': ^28.0.2\n  '@rollup/plugin-node-resolve': ^16.0.0\n  '@rollup/plugin-terser': ^0.4.4\n  '@rollup/plugin-typescript': ^12.1.2\n  '@tailwindcss/postcss': ^4.2.1\n  '@testing-library/jest-dom': ^6.9.1\n  '@testing-library/preact': ^3.2.4\n  '@testing-library/user-event': ^14.6.1\n  '@types/jest': ^29.5.14\n  '@types/lodash': ^4.17.13\n  '@types/node': ^22.10.2\n  '@types/react': ^18.3.12\n  '@types/react-dom': ^18.3.1\n  autoprefixer: ^10.4.24\n  cssnano: ^7.0.6\n  esbuild: ^0.27.3\n  jest: ^29.7.0\n  jest-environment-jsdom: ^29.7.0\n  lucide-react: 0.575.0\n  postcss: ^8.5.10\n  postcss-import: ^16.1.1\n  preact: ^10.28.3\n  react: ^18.3.1\n  react-dom: ^18.3.1\n  rimraf: ^6.1.2\n  rollup: ^4.29.1\n  rollup-plugin-dts: ^6.3.0\n  rollup-plugin-esbuild: ^6.2.1\n  rollup-plugin-peer-deps-external: ^2.2.4\n  rollup-plugin-postcss: ^4.0.2\n  rollup-plugin-svelte: ^7.2.2\n  rollup-plugin-visualizer: ^6.0.5\n  rollup-plugin-vue: ^6.0.0\n  tailwindcss: ^4.2.1\n  temporal-polyfill: ^0.3.0\n  ts-jest: ^29.4.5\n  tslib: ^2.8.1\n  typescript: ^5.9.3\n  vite: ^5.4.20\n\ncatalogMode: prefer\n"
  },
  {
    "path": "postcss.config.mjs",
    "content": "export default {\n  plugins: {\n    '@tailwindcss/postcss': {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "scripts/git-tag.sh",
    "content": "#!/bin/bash\n\n# Exit on error\nset -e\n\n# Path to the core package.json\nCORE_PKG_JSON=\"packages/core/package.json\"\n\nif [ ! -f \"$CORE_PKG_JSON\" ]; then\n  echo \"Error: $CORE_PKG_JSON not found.\"\n  exit 1\nfi\n\n# Extract version using Node.js\nVERSION=$(node -p \"require('./$CORE_PKG_JSON').version\")\n\nif [ -z \"$VERSION\" ]; then\n  echo \"Error: Could not extract version from $CORE_PKG_JSON.\"\n  exit 1\nfi\n\nTAG=\"v$VERSION\"\n\n# Check if tag already exists locally\nif git rev-parse \"$TAG\" >/dev/null 2>&1; then\n  echo \"Tag $TAG already exists locally.\"\nelse\n  echo \"Creating tag $TAG...\"\n  git tag \"$TAG\"\nfi\n\n# Check if tag exists on remote\nREMOTE_TAG_EXISTS=$(git ls-remote --tags origin \"$TAG\")\n\nif [ -n \"$REMOTE_TAG_EXISTS\" ]; then\n  echo \"Tag $TAG already exists on remote 'origin'.\"\nelse\n  echo \"Pushing tag $TAG to origin...\"\n  git push origin \"$TAG\"\n  echo \"Successfully pushed $TAG to origin.\"\nfi\n"
  },
  {
    "path": "scripts/publish.sh",
    "content": "#!/bin/bash\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"$0\")/..\" && pwd)\"\n\n# Colors\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nCYAN='\\033[0;36m'\nBOLD='\\033[1m'\nNC='\\033[0m'\n\n# ---------- Usage ----------\nusage() {\n    echo -e \"${BOLD}Usage:${NC}\"\n    echo \"  ./scripts/publish.sh all          Publish all packages (core, adapters, plugins, ui, cli)\"\n    echo \"  ./scripts/publish.sh main         Publish core + react + vue + svelte\"\n    echo \"  ./scripts/publish.sh plugins      Publish all plugins\"\n    echo \"  ./scripts/publish.sh ui           Publish all UI components\"\n    echo \"  ./scripts/publish.sh angular      Publish angular only\"\n    echo \"  ./scripts/publish.sh cli          Publish create-dayflow CLI\"\n    echo \"\"\n    echo \"Options:\"\n    echo \"  --dry-run      Run npm publish with --dry-run (no actual publish)\"\n    echo \"  --skip-build   Skip the build step\"\n    exit 0\n}\n\n# ---------- Parse args ----------\nMODE=\"all\"\nDRY_RUN=\"\"\nSKIP_BUILD=false\n\nfor arg in \"$@\"; do\n    case \"$arg\" in\n        main) MODE=\"main\" ;;\n        plugins) MODE=\"plugins\" ;;\n        ui) MODE=\"ui\" ;;\n        angular) MODE=\"angular\" ;;\n        cli) MODE=\"cli\" ;;\n        all) MODE=\"all\" ;;\n        --dry-run) DRY_RUN=\"--dry-run\" ;;\n        --skip-build) SKIP_BUILD=true ;;\n        -h|--help) usage ;;\n    esac\ndone\n\nstep() { echo -e \"\\n${CYAN}${BOLD}[$STEP/$TOTAL] $1${NC}\"; STEP=$((STEP + 1)); }\nok() { echo -e \"${GREEN} ✓ $1${NC}\"; }\nerr() { echo -e \"${RED} ✗ $1${NC}\"; exit 1; }\nwarn() { echo -e \"${YELLOW} ⚠ $1${NC}\"; }\n\n# Publish tracking\nPUBLISHED_OK=0\nFAILED_PKGS=()\nPUBLISH_TOTAL=0\n\n# ---------- Pre-flight checks ----------\necho -e \"${BOLD}Pre-flight checks${NC}\"\n\n# Check npm login\nif ! npm whoami &>/dev/null; then\n    err \"Not logged in to npm. Run 'npm login' first.\"\nfi\nNPM_USER=$(npm whoami)\nok \"Logged in as ${BOLD}$NPM_USER${NC}\"\n\n# Check git status\nif [ -z \"$DRY_RUN\" ] && [ -n \"$(git status --porcelain)\" ]; then\n    warn \"Working tree is not clean. Consider committing changes first.\"\n    read -p \"Continue anyway? (y/N) \" -n 1 -r\n    echo\n    if [[ ! $REPLY =~ ^[Yy]$ ]]; then err \"Publish aborted.\"; fi\nfi\n\n# ---------- Define Packages ----------\nMAIN_PKGS=(core react vue svelte)\nPLUGIN_DIRS=(drag keyboard-shortcuts localization sidebar)\nUI_DIRS=(context-menu range-picker)\n\n# Function to map directory names to package names\nget_plugin_package_name() {\n    case \"$1\" in\n        \"drag\") echo \"plugin-drag\" ;;\n        \"keyboard-shortcuts\") echo \"plugin-keyboard-shortcuts\" ;;\n        \"localization\") echo \"plugin-localization\" ;;\n        \"sidebar\") echo \"plugin-sidebar\" ;;\n        *) echo \"plugin-$1\" ;;\n    esac\n}\n\nget_ui_package_name() {\n    case \"$1\" in\n        \"context-menu\") echo \"ui-context-menu\" ;;\n        \"range-picker\") echo \"ui-range-picker\" ;;\n        *) echo \"ui-$1\" ;;\n    esac\n}\n\n# ---------- Calculation ----------\nSTEP=1\ncase \"$MODE\" in\n    main)    TOTAL=$(( ${#MAIN_PKGS[@]} * 2 )) ;;\n    angular) TOTAL=2 ;;\n    plugins) TOTAL=$(( ${#PLUGIN_DIRS[@]} * 2 )) ;;\n    ui)      TOTAL=$(( ${#UI_DIRS[@]} * 2 )) ;;\n    cli)     TOTAL=2 ;;\n    all)     TOTAL=$(( ${#MAIN_PKGS[@]} * 2 + ${#PLUGIN_DIRS[@]} * 2 + ${#UI_DIRS[@]} * 2 + 2 + 2 )) ;;\nesac\n\n# ---------- Build Functions ----------\nbuild_pkg() {\n    local name=$1 # This should be the @dayflow/ package name part\n    local display_path=$2\n    local dir=\"$ROOT/$display_path\"\n\n    step \"Building $display_path\"\n\n    # Ensure LICENSE exists\n    if [ ! -f \"$dir/LICENSE\" ]; then\n        cp \"$ROOT/LICENSE\" \"$dir/LICENSE\"\n    fi\n\n    # Do not overwrite README.md if it already exists,\n    # as they are manually managed and framework-specific.\n    if [ ! -f \"$dir/README.md\" ]; then\n        # Transform README.md: Replace relative image paths with absolute GitHub URLs\n        # Example: ./assets/images/ -> https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/\n        sed 's|(\\./assets/images/|(https://raw.githubusercontent.com/dayflow-js/calendar/main/assets/images/|g' \"$ROOT/README.md\" > \"$dir/README.md\"\n    fi\n\n    if ! pnpm --filter \"@dayflow/$name\" run build > /dev/null; then\n        err \"Build failed for $name\"\n    fi\n    ok \"$display_path built\"\n}\n\npublish_cli() {\n    local dir=\"$ROOT/packages/create-dayflow\"\n    step \"Building create-dayflow CLI\"\n\n    if [ ! -f \"$dir/LICENSE\" ]; then\n        cp \"$ROOT/LICENSE\" \"$dir/LICENSE\"\n    fi\n    if ! (cd \"$dir\" && pnpm run build > /dev/null); then\n        err \"Build failed for create-dayflow\"\n    fi\n    ok \"create-dayflow built\"\n\n    step \"Publishing create-dayflow\"\n    local version=$(node -e \"console.log(require('$dir/package.json').version)\")\n    echo -e \"  version: ${BOLD}$version${NC}\"\n\n    if npm view \"create-dayflow@$version\" version &>/dev/null; then\n        warn \"create-dayflow@$version already exists on npm — skipping\"\n        PUBLISHED_OK=$((PUBLISHED_OK + 1))\n        return 0\n    fi\n\n    if ! (cd \"$dir\" && pnpm publish --access public --no-git-checks $DRY_RUN); then\n        echo -e \"${RED} ✗ Failed to publish create-dayflow${NC}\"\n        FAILED_PKGS+=(\"create-dayflow\")\n        return 1\n    fi\n    ok \"create-dayflow published\"\n    PUBLISHED_OK=$((PUBLISHED_OK + 1))\n}\n\npublish_pkg() {\n    local name=$1 # This should be the @dayflow/ package name part\n    local dir=$2\n    step \"Publishing @dayflow/$name\"\n\n    local version=$(node -e \"console.log(require('$dir/package.json').version)\")\n    echo -e \"  version: ${BOLD}$version${NC}\"\n\n    # Check if already published\n    if npm view \"@dayflow/$name@$version\" version &>/dev/null; then\n        warn \"@dayflow/$name@$version already exists on npm — skipping\"\n        PUBLISHED_OK=$((PUBLISHED_OK + 1))\n        return 0\n    fi\n\n    # SPECIAL HANDLING FOR ANGULAR DIST\n    # ng-packagr doesn't resolve workspace:* protocols and may produce incorrect\n    # export conditions. We patch dist/package.json before publishing.\n    if [[ \"$dir\" == *\"packages/angular/dist\" ]]; then\n        local core_version=$(node -e \"console.log(require('$ROOT/packages/core/package.json').version)\")\n        echo \"  Patching Angular dist/package.json (core: $core_version)...\"\n\n        node -e \"\n          const fs = require('fs');\n          const pkg = JSON.parse(fs.readFileSync('$dir/package.json', 'utf8'));\n\n          // Replace workspace:* with actual core version\n          for (const section of ['peerDependencies', 'dependencies', 'optionalDependencies']) {\n            if (pkg[section]) {\n              for (const dep of Object.keys(pkg[section])) {\n                if (pkg[section][dep] === 'workspace:*') pkg[section][dep] = '$core_version';\n              }\n            }\n          }\n\n          // Add 'import' condition if missing (required by Vite/esbuild to resolve ESM entry)\n          const dot = pkg.exports && pkg.exports['.'];\n          if (dot && !dot.import && (dot.default || dot.esm2022)) {\n            dot.import = dot.default || dot.esm2022;\n          }\n\n          fs.writeFileSync('$dir/package.json', JSON.stringify(pkg, null, 2) + '\\n');\n        \"\n\n        echo \"  Detected Angular dist - using npm publish...\"\n        if ! (cd \"$dir\" && npm publish --access public $DRY_RUN); then\n            echo -e \"${RED} ✗ Failed to publish @dayflow/$name${NC}\"\n            FAILED_PKGS+=(\"@dayflow/$name\")\n            return 1\n        fi\n    else\n        if ! (cd \"$dir\" && pnpm publish --access public --no-git-checks $DRY_RUN); then\n            echo -e \"${RED} ✗ Failed to publish @dayflow/$name${NC}\"\n            FAILED_PKGS+=(\"@dayflow/$name\")\n            return 1\n        fi\n    fi\n    ok \"@dayflow/$name published\"\n    PUBLISHED_OK=$((PUBLISHED_OK + 1))\n}\n\n# ---------- Publish Total ----------\ncase \"$MODE\" in\n    main)    PUBLISH_TOTAL=${#MAIN_PKGS[@]} ;;\n    angular) PUBLISH_TOTAL=1 ;;\n    plugins) PUBLISH_TOTAL=${#PLUGIN_DIRS[@]} ;;\n    ui)      PUBLISH_TOTAL=${#UI_DIRS[@]} ;;\n    cli)     PUBLISH_TOTAL=1 ;;\n    all)     PUBLISH_TOTAL=$(( ${#MAIN_PKGS[@]} + ${#PLUGIN_DIRS[@]} + ${#UI_DIRS[@]} + 1 + 1 )) ;;\nesac\n\n# ---------- Summary ----------\nprint_summary() {\n    echo \"\"\n    if [ ${#FAILED_PKGS[@]} -eq 0 ]; then\n        echo -e \"${GREEN}${BOLD}publish finished：$PUBLISHED_OK/$PUBLISH_TOTAL${NC}\"\n    else\n        echo -e \"${YELLOW}${BOLD}publish finished：$PUBLISHED_OK/$PUBLISH_TOTAL${NC}\"\n        echo -e \"${RED}publish failed：${NC}\"\n        for pkg in \"${FAILED_PKGS[@]}\"; do\n            echo -e \"${RED}  ✗ $pkg${NC}\"\n        done\n    fi\n    if [ -n \"$DRY_RUN\" ]; then warn \"This was a dry run. No actual publish occurred.\"; fi\n    if [ ${#FAILED_PKGS[@]} -ne 0 ]; then exit 1; fi\n}\n\n# ---------- Execution ----------\n\n# 1. Build + Publish (cli handles its own build/publish together)\nif [[ \"$MODE\" == \"cli\" ]]; then\n    publish_cli || true\n    print_summary\n    exit 0\nfi\n\n# 2. Build Phase\nif [ \"$SKIP_BUILD\" = false ]; then\n    if [[ \"$MODE\" == \"all\" || \"$MODE\" == \"main\" ]]; then\n        for pkg in \"${MAIN_PKGS[@]}\"; do build_pkg \"$pkg\" \"packages/$pkg\"; done\n    fi\n    if [[ \"$MODE\" == \"all\" || \"$MODE\" == \"plugins\" ]]; then\n        for dir in \"${PLUGIN_DIRS[@]}\"; do\n            pkg_name=$(get_plugin_package_name \"$dir\")\n            build_pkg \"$pkg_name\" \"packages/plugins/$dir\"\n        done\n    fi\n    if [[ \"$MODE\" == \"all\" || \"$MODE\" == \"ui\" ]]; then\n        for dir in \"${UI_DIRS[@]}\"; do\n            pkg_name=$(get_ui_package_name \"$dir\")\n            build_pkg \"$pkg_name\" \"packages/ui/$dir\"\n        done\n    fi\n    if [[ \"$MODE\" == \"all\" || \"$MODE\" == \"angular\" ]]; then\n        build_pkg \"angular\" \"packages/angular\"\n    fi\nelse\n    warn \"Skipping build (--skip-build)\"\n    # Fast forward step counter\n    case \"$MODE\" in\n        main)    STEP=$(( ${#MAIN_PKGS[@]} + 1 )) ;;\n        angular) STEP=2 ;;\n        plugins) STEP=$(( ${#PLUGIN_DIRS[@]} + 1 )) ;;\n        ui)      STEP=$(( ${#UI_DIRS[@]} + 1 )) ;;\n        all)     STEP=$(( ${#MAIN_PKGS[@]} + ${#PLUGIN_DIRS[@]} + ${#UI_DIRS[@]} + 2 )) ;;\n    esac\nfi\n\n# 3. Publish Phase\nif [[ \"$MODE\" == \"all\" || \"$MODE\" == \"main\" ]]; then\n    for pkg in \"${MAIN_PKGS[@]}\"; do\n        publish_pkg \"$pkg\" \"$ROOT/packages/$pkg\" || true\n    done\nfi\n\nif [[ \"$MODE\" == \"all\" || \"$MODE\" == \"plugins\" ]]; then\n    for dir in \"${PLUGIN_DIRS[@]}\"; do\n        pkg_name=$(get_plugin_package_name \"$dir\")\n        publish_pkg \"$pkg_name\" \"$ROOT/packages/plugins/$dir\" || true\n    done\nfi\n\nif [[ \"$MODE\" == \"all\" || \"$MODE\" == \"ui\" ]]; then\n    for dir in \"${UI_DIRS[@]}\"; do\n        pkg_name=$(get_ui_package_name \"$dir\")\n        publish_pkg \"$pkg_name\" \"$ROOT/packages/ui/$dir\" || true\n    done\nfi\n\nif [[ \"$MODE\" == \"all\" || \"$MODE\" == \"angular\" ]]; then\n    publish_pkg \"angular\" \"$ROOT/packages/angular/dist\" || true\nfi\n\nif [[ \"$MODE\" == \"all\" ]]; then\n    publish_cli || true\nfi\n\nprint_summary\n"
  },
  {
    "path": "scripts/setup-website.sh",
    "content": "#!/bin/bash\n\n# Exit on error\nset -e\n\n# Get root directory\nROOT_DIR=$(pwd)\n\n# Function for interactive menu\nselect_option() {\n  local options=(\"$@\")\n  local cursor=0\n  local count=${#options[@]}\n  local ESC=$(printf \"\\033\")\n\n  # Hide cursor\n  printf \"\\033[?25l\" >&2\n\n  while true; do\n    # Render menu to stderr\n    for i in \"${!options[@]}\"; do\n      if [ $i -eq $cursor ]; then\n        printf \"  \\033[32m>\\033[0m %s\\n\" \"${options[$i]}\" >&2\n      else\n        printf \"    %s\\n\" \"${options[$i]}\" >&2\n      fi\n    done\n\n    # Read key\n    IFS= read -rsn1 key < /dev/tty\n    if [[ \"$key\" == \"$ESC\" ]]; then\n      read -rsn2 key < /dev/tty\n      if [[ \"$key\" == \"[A\" ]]; then # Up\n        ((cursor--))\n        [ $cursor -lt 0 ] && cursor=$((count - 1))\n      elif [[ \"$key\" == \"[B\" ]]; then # Down\n        ((cursor++))\n        [ $cursor -ge $count ] && cursor=0\n      fi\n    elif [[ \"$key\" == \"\" ]]; then # Enter\n      break\n    fi\n\n    # Move cursor back up to redraw (to stderr)\n    printf \"\\033[%dA\" \"$count\" >&2\n  done\n\n  # Show cursor\n  printf \"\\033[?25h\" >&2\n\n  # Return result to stdout\n  echo \"$cursor\"\n}\n\necho \"------------------------------------------\"\necho \"Please choose the installation option (Use arrow keys):\"\n\nOPTIONS=(\"Install Core, React only\" \"Install All\")\nchoice_index=$(select_option \"${OPTIONS[@]}\")\nchoice=$(( choice_index + 1 ))\n\necho \"Selected choice: $choice\"\necho \"------------------------------------------\"\n\nPACK_DIRS=()\nBUILD_FILTER_STAGE1=\"--filter @dayflow/ui-context-menu --filter @dayflow/ui-range-picker --filter @dayflow/core\"\nBUILD_FILTER_STAGE2=\"\"\n\ncase $choice in\n  1)\n    echo \"Selected: Core, React only\"\n    BUILD_FILTER_STAGE2=\"--filter @dayflow/react\"\n    PACK_DIRS=(\n      \"packages/ui/context-menu\"\n      \"packages/ui/range-picker\"\n      \"packages/core\"\n      \"packages/react\"\n    )\n    ;;\n  2)\n    echo \"Selected: Install All\"\n    BUILD_FILTER_STAGE2=\"--filter @dayflow/react --filter @dayflow/angular --filter @dayflow/svelte --filter @dayflow/vue --filter @dayflow/plugin-drag --filter @dayflow/plugin-keyboard-shortcuts --filter @dayflow/plugin-localization --filter @dayflow/plugin-sidebar\"\n    PACK_DIRS=(\n      \"packages/ui/context-menu\"\n      \"packages/ui/range-picker\"\n      \"packages/core\"\n      \"packages/react\"\n      \"packages/angular\"\n      \"packages/svelte\"\n      \"packages/vue\"\n      \"packages/plugins/drag\"\n      \"packages/plugins/keyboard-shortcuts\"\n      \"packages/plugins/localization\"\n      \"packages/plugins/sidebar\"\n    )\n    ;;\n  *)\n    echo \"Invalid choice. Exiting.\"\n    exit 1\n    ;;\nesac\n\necho \"🚀 Building packages with turbo (parallel & cached)...\"\npnpm turbo build $BUILD_FILTER_STAGE1 $BUILD_FILTER_STAGE2\n\necho \"📦 Packing selected packages in parallel...\"\nPACKS_DIR=\"$ROOT_DIR/temp/packs\"\nrm -rf \"$PACKS_DIR\"\nmkdir -p \"$PACKS_DIR\"\n\nfor dir in \"${PACK_DIRS[@]}\"; do\n  (\n    if [ -d \"$dir\" ]; then\n      echo \"Packing $dir...\"\n      cd \"$ROOT_DIR/$dir\"\n      pnpm pack --silent\n      mv *.tgz \"$PACKS_DIR/\"\n    else\n      echo \"⚠️ Warning: Directory $dir not found\"\n    fi\n  ) &\ndone\nwait\n\necho \"🧹 Cleaning up website directory...\"\ncd \"$ROOT_DIR/website\"\nrm -rf node_modules package-lock.json\n\n# Function to find the exact .tgz path (resolving wildcards, picking the latest version)\nfind_tgz() {\n  local pattern=$1\n  # Use ls -v for natural version sorting if available, or sort -V\n  local tgz=$(ls \"$PACKS_DIR\"/$pattern 2>/dev/null | sort -V | tail -n 1)\n  if [ -n \"$tgz\" ]; then\n    echo \"$tgz\"\n  fi\n}\n\nadd_to_install_list() {\n  local path=$(find_tgz \"$1\")\n  if [ -n \"$path\" ]; then\n    INSTALL_LIST+=(\"$path\")\n  else\n    echo \"⚠️ Warning: No package found matching $1\"\n  fi\n}\n\nINSTALL_LIST=()\ncase $choice in\n  1)\n    # UI packages must be installed before core (core depends on them)\n    add_to_install_list \"dayflow-ui-context-menu-*.tgz\"\n    add_to_install_list \"dayflow-ui-range-picker-*.tgz\"\n    add_to_install_list \"dayflow-core-*.tgz\"\n    add_to_install_list \"dayflow-react-*.tgz\"\n    ;;\n  2)\n    # UI packages must be installed before core (core depends on them)\n    add_to_install_list \"dayflow-ui-context-menu-*.tgz\"\n    add_to_install_list \"dayflow-ui-range-picker-*.tgz\"\n    # Core and Frameworks\n    add_to_install_list \"dayflow-core-*.tgz\"\n    add_to_install_list \"dayflow-react-*.tgz\"\n    add_to_install_list \"dayflow-angular-*.tgz\"\n    add_to_install_list \"dayflow-svelte-*.tgz\"\n    add_to_install_list \"dayflow-vue-*.tgz\"\n\n    # Plugins\n    add_to_install_list \"dayflow-plugin-drag-*.tgz\"\n    add_to_install_list \"dayflow-plugin-keyboard-shortcuts-*.tgz\"\n    add_to_install_list \"dayflow-plugin-localization-*.tgz\"\n    add_to_install_list \"dayflow-plugin-sidebar-*.tgz\"\n    ;;\nesac\n\n# Clean up empty values\nCLEAN_INSTALL_LIST=()\nfor item in \"${INSTALL_LIST[@]}\"; do\n  if [ -n \"$item\" ]; then\n    CLEAN_INSTALL_LIST+=(\"$item\")\n  fi\ndone\n\necho \"📥 Installing website dependencies (using npm)...\"\n# The website uses npm (not part of the pnpm workspace). Using npm install\n# ensures a flat node_modules where all transitive deps (e.g. zod required by\n# fumadocs-mdx) are directly resolvable — pnpm's non-flat layout breaks this.\nnpm install\n\n# Install the local .tgz packages on top\nif [ ${#CLEAN_INSTALL_LIST[@]} -gt 0 ]; then\n  echo \"📥 Installing local packages: ${CLEAN_INSTALL_LIST[*]}\"\n  npm install \"${CLEAN_INSTALL_LIST[@]}\"\nelse\n  echo \"⚠️ Warning: No local packages to install\"\nfi\n\necho \"✅ Setup complete! You can now run 'npm run dev' inside the website directory.\"\n"
  },
  {
    "path": "scripts/update-versions.sh",
    "content": "#!/bin/bash\n\n# Find all package.json files inside packages/, excluding node_modules\nPACKAGE_FILES=$(find packages -name \"package.json\" -not -path \"*/node_modules/*\")\n\nif [ -z \"$PACKAGE_FILES\" ]; then\n  echo \"No package.json files found in packages/.\"\n  exit 0\nfi\n\n# Use Node.js for the interactive menu (Arrow keys support)\n# We use stderr for the UI so we can capture the selection from stdout\nBUMP_TYPE=$(node -e '\nconst readline = require(\"readline\");\nconst { stdin, stderr } = process;\n\nconst options = [\"PATCH\", \"MINOR\", \"MAJOR\"];\nlet index = 0;\n\n// Prepare UI area\nstderr.write(\"\\n\".repeat(options.length + 1));\n\nfunction render() {\n  readline.moveCursor(stderr, 0, -(options.length + 1));\n  readline.clearScreenDown(stderr);\n  \n  stderr.write(\"Select version bump type (Use arrow keys, Enter to select):\\n\");\n  options.forEach((opt, i) => {\n    if (i === index) {\n      stderr.write(`\\x1b[36m> ${opt}\\x1b[0m\\n`); // Cyan color for selected\n    } else {\n      stderr.write(`  ${opt}\\n`);\n    }\n  });\n}\n\nreadline.emitKeypressEvents(stdin);\nif (stdin.isTTY) stdin.setRawMode(true);\n\nrender();\n\nstdin.on(\"keypress\", (str, key) => {\n  if (!key) return;\n  \n  if (key.ctrl && key.name === \"c\") {\n    process.exit(1); // Exit with error\n  } else if (key.name === \"up\") {\n    index = (index - 1 + options.length) % options.length;\n    render();\n  } else if (key.name === \"down\") {\n    index = (index + 1) % options.length;\n    render();\n  } else if (key.name === \"return\") {\n    if (stdin.isTTY) stdin.setRawMode(false);\n    console.log(options[index]); // Print selection to stdout\n    process.exit(0);\n  }\n});\n')\n\n# Check if user cancelled\nif [ $? -ne 0 ]; then\n  echo \"Operation cancelled.\"\n  exit 1\nfi\n\n# Trim any whitespace\nBUMP_TYPE=$(echo \"$BUMP_TYPE\" | xargs)\n\n# Selection for scope\nSCOPE=$(node -e '\nconst readline = require(\"readline\");\nconst { stdin, stderr } = process;\n\nconst options = [\"all\", \"partial\"];\nlet index = 0;\n\n// Prepare UI area\nstderr.write(\"\\n\".repeat(options.length + 1));\n\nfunction render() {\n  readline.moveCursor(stderr, 0, -(options.length + 1));\n  readline.clearScreenDown(stderr);\n  \n  stderr.write(\"Select update scope (Partial excludes create-dayflow):\\n\");\n  options.forEach((opt, i) => {\n    if (i === index) {\n      stderr.write(`\\x1b[36m> ${opt.toUpperCase()}\\x1b[0m\\n`); // Cyan color for selected\n    } else {\n      stderr.write(`  ${opt.toUpperCase()}\\n`);\n    }\n  });\n}\n\nreadline.emitKeypressEvents(stdin);\nif (stdin.isTTY) stdin.setRawMode(true);\n\nrender();\n\nstdin.on(\"keypress\", (str, key) => {\n  if (!key) return;\n  \n  if (key.ctrl && key.name === \"c\") {\n    process.exit(1); // Exit with error\n  } else if (key.name === \"up\" || key.name === \"down\") {\n    index = (index + 1) % options.length;\n    render();\n  } else if (key.name === \"return\") {\n    if (stdin.isTTY) stdin.setRawMode(false);\n    console.log(options[index]); // Print selection to stdout\n    process.exit(0);\n  }\n});\n')\n\n# Check if user cancelled\nif [ $? -ne 0 ]; then\n  echo \"Operation cancelled.\"\n  exit 1\nfi\n\nSCOPE=$(echo \"$SCOPE\" | xargs)\n\necho \"Updating versions to $BUMP_TYPE (Scope: $SCOPE)...\"\n\n# Iterate and update using a safe node script invocation\nfor FILE in $PACKAGE_FILES; do\n  node -e '\n    const fs = require(\"fs\");\n    const file = process.argv[1];\n    const type = process.argv[2];\n    const scope = process.argv[3];\n    \n    try {\n      const content = fs.readFileSync(file, \"utf8\");\n      const pkg = JSON.parse(content);\n      \n      if (!pkg.version) {\n        // Silently skip or log if needed\n        process.exit(0);\n      }\n\n      if (scope === \"partial\" && pkg.name === \"create-dayflow\") {\n        console.log(`Skipping ${pkg.name} (partial mode)`);\n        process.exit(0);\n      }\n      \n      const oldVer = pkg.version;\n      const parts = oldVer.split(\".\").map(Number);\n      \n      if (parts.length !== 3 || parts.some(isNaN)) {\n        console.log(`Skipping ${pkg.name}: Invalid version format ${oldVer}`);\n        process.exit(0);\n      }\n      \n      if (type === \"PATCH\") {\n        parts[2]++;\n      } else if (type === \"MINOR\") {\n        parts[1]++;\n        parts[2] = 0;\n      } else if (type === \"MAJOR\") {\n        parts[0]++;\n        parts[1] = 0;\n        parts[2] = 0;\n      }\n      \n      const newVer = parts.join(\".\");\n      pkg.version = newVer;\n      \n      // Write with newline at end\n      fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + \"\\n\");\n      console.log(`Updated ${pkg.name || file}: ${oldVer} -> ${newVer}`);\n    } catch (e) {\n      console.error(`Error updating ${file}: ${e.message}`);\n      process.exit(1);\n    }\n  ' \"$FILE\" \"$BUMP_TYPE\" \"$SCOPE\"\ndone\n\necho \"All packages updated successfully.\"\n"
  },
  {
    "path": "tailwind.config.mjs",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\n    './packages/core/src/**/*.{js,jsx,ts,tsx}',\n    './packages/plugins/*/src/**/*.{js,jsx,ts,tsx}',\n    './examples/**/*.{js,jsx,ts,tsx}',\n    './packages/core/index.html',\n  ],\n  theme: {\n    extend: {\n      colors: {\n        primary: {\n          DEFAULT: 'var(--color-primary)',\n          foreground: 'var(--color-primary-foreground)',\n        },\n        secondary: {\n          DEFAULT: 'var(--color-secondary)',\n          foreground: 'var(--color-secondary-foreground)',\n        },\n        destructive: {\n          DEFAULT: 'var(--color-destructive)',\n          foreground: 'var(--color-destructive-foreground)',\n        },\n        muted: {\n          DEFAULT: 'var(--color-muted)',\n          foreground: 'var(--color-muted-foreground)',\n        },\n        card: {\n          DEFAULT: 'var(--color-card)',\n          foreground: 'var(--color-card-foreground)',\n        },\n      },\n    },\n  },\n  plugins: [],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    \"paths\": {\n      \"@dayflow/core\": [\"./packages/core/src/index.ts\"],\n      \"@dayflow/react\": [\"./packages/react/src/index.ts\"],\n      \"@dayflow/vue\": [\"./packages/vue/src/index.ts\"],\n      \"@dayflow/angular\": [\"./packages/angular/src/public-api.ts\"],\n      \"@dayflow/svelte\": [\"./packages/svelte/src/index.ts\"],\n      \"@dayflow/plugin-localization\": [\n        \"./packages/plugins/localization/src/index.ts\"\n      ],\n      \"@dayflow/plugin-keyboard-shortcuts\": [\n        \"./packages/plugins/keyboard-shortcuts/src/index.ts\"\n      ],\n      \"@dayflow/plugin-sidebar\": [\"./packages/plugins/sidebar/src/index.ts\"],\n      \"@dayflow/plugin-drag\": [\"./packages/plugins/drag/src/index.ts\"],\n      \"@dayflow/ui-range-picker\": [\"./packages/ui/range-picker/src/index.ts\"],\n      \"@dayflow/resource-grid\": [\"./packages/resource-grid/src/index.ts\"],\n      \"@drag/*\": [\"./packages/plugins/drag/src/*\"],\n      \"@sidebar/*\": [\"./packages/plugins/sidebar/src/*\"],\n      \"@keyboard-shortcuts/*\": [\"./packages/plugins/keyboard-shortcuts/src/*\"],\n      \"@localization/*\": [\"./packages/plugins/localization/src/*\"],\n      \"@ui-range-picker/*\": [\"./packages/ui/range-picker/src/*\"],\n      \"@examples/*\": [\"./examples/*\"],\n      \"@website/*\": [\"./website/*\"],\n      \"@/*\": [\"./packages/core/src/*\"]\n    }\n  },\n  \"include\": [\"packages/*/src\", \"examples\", \"website\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"tasks\": {\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"dist/**\"]\n    },\n    \"clean\": {\n      \"cache\": false\n    },\n    \"dev\": {\n      \"cache\": false,\n      \"persistent\": true\n    },\n    \"test\": {\n      \"dependsOn\": [\"build\"]\n    },\n    \"typecheck\": {\n      \"dependsOn\": [\"build\"]\n    }\n  }\n}\n"
  },
  {
    "path": "website/.gitignore",
    "content": "# deps\n/node_modules\n\n# generated content\n.source\n\n# test & build\n/coverage\n/.next/\n/out/\n/build\n*.tsbuildinfo\n\n# misc\n.DS_Store\n*.pem\n/.pnp\n.pnp.js\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# others\n.env*.local\n.vercel\nnext-env.d.ts"
  },
  {
    "path": "website/.prettierrc",
    "content": "{\n  \"printWidth\": 80,\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"trailingComma\": \"es5\",\n  \"bracketSpacing\": true,\n  \"bracketSameLine\": false,\n  \"arrowParens\": \"avoid\",\n  \"endOfLine\": \"lf\",\n  \"plugins\": [\"prettier-plugin-embed\"]\n}\n"
  },
  {
    "path": "website/README.md",
    "content": "# website-new\n\nThis is a Next.js application generated with\n[Create Fumadocs](https://github.com/fuma-nama/fumadocs).\n\nRun development server:\n\n```bash\nnpm run dev\n# or\npnpm dev\n# or\nyarn dev\n```\n\nOpen http://localhost:3000 with your browser to see the result.\n\n## Explore\n\nIn the project, you can see:\n\n- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.\n- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.\n\n| Route                     | Description                                            |\n| ------------------------- | ------------------------------------------------------ |\n| `app/(home)`              | The route group for your landing page and other pages. |\n| `app/docs`                | The documentation layout and pages.                    |\n| `app/api/search/route.ts` | The Route Handler for search.                          |\n\n### Fumadocs MDX\n\nA `source.config.ts` config file has been included, you can customise different options like frontmatter schema.\n\nRead the [Introduction](https://fumadocs.dev/docs/mdx) for further details.\n\n## Learn More\n\nTo learn more about Next.js and Fumadocs, take a look at the following\nresources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js\n  features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs\n"
  },
  {
    "path": "website/app/(home)/layout.tsx",
    "content": "import { HomeLayout } from 'fumadocs-ui/layouts/home';\n\nimport { homeOptions } from '@/lib/layout.shared';\n\nexport default function Layout({ children }: LayoutProps<'/'>) {\n  return <HomeLayout {...homeOptions()}>{children}</HomeLayout>;\n}\n"
  },
  {
    "path": "website/app/(home)/page.tsx",
    "content": "import type { Metadata } from 'next';\n\nimport { LiveDemo } from '@/components/showcase/LiveDemo';\n\nexport const metadata: Metadata = {\n  title: 'DayFlow - Lightweight Calendar Component',\n  description:\n    'A lightweight and elegant full calendar component for React, Vue, Angular, and Svelte. Supports day, week, month, and year views with drag-and-drop, localization, and dark mode.',\n  openGraph: {\n    title: 'DayFlow - Lightweight Calendar Component',\n    description:\n      'A lightweight and elegant full calendar component for React, Vue, Angular, and Svelte. Supports day, week, month, and year views with drag-and-drop, localization, and dark mode.',\n  },\n};\n\nexport default function HomePage() {\n  return <LiveDemo />;\n}\n"
  },
  {
    "path": "website/app/api/search/route.ts",
    "content": "import { defineI18n } from 'fumadocs-core/i18n';\nimport { flexsearchI18n } from 'fumadocs-core/search/flexsearch';\n\nimport { source, sourceJa, sourceZh } from '@/lib/source';\n\nexport const dynamic = 'force-static';\n\nconst docsI18n = defineI18n({\n  languages: ['en', 'zh', 'ja'],\n  defaultLanguage: 'en',\n});\n\nconst sourcesByLocale = {\n  en: source,\n  zh: sourceZh,\n  ja: sourceJa,\n} as const;\n\ntype SearchSource = (typeof sourcesByLocale)[keyof typeof sourcesByLocale];\ntype SearchPage = ReturnType<SearchSource['getPages']>[number];\n\ntype TreeNode = {\n  type?: string;\n  name?: unknown;\n  url?: string;\n  children?: TreeNode[];\n};\n\nfunction findPagePath(\n  nodes: TreeNode[] | undefined,\n  url: string,\n  parents: TreeNode[] = []\n): TreeNode[] | undefined {\n  if (!nodes) return;\n\n  for (const node of nodes) {\n    const nextParents = [...parents, node];\n\n    if (node.type === 'page' && node.url === url) {\n      return nextParents;\n    }\n\n    const nested = findPagePath(node.children, url, nextParents);\n    if (nested) return nested;\n  }\n}\n\nfunction buildBreadcrumbs(docSource: SearchSource, page: SearchPage) {\n  const tree = docSource.getPageTree() as TreeNode;\n  const path = findPagePath(tree.children, page.url);\n\n  if (!path) return;\n\n  const breadcrumbs: string[] = [];\n\n  if (typeof tree.name === 'string' && tree.name.length > 0) {\n    breadcrumbs.push(tree.name);\n  }\n\n  for (const node of path.slice(0, -1)) {\n    if (typeof node.name === 'string' && node.name.length > 0) {\n      breadcrumbs.push(node.name);\n    }\n  }\n\n  return breadcrumbs.length > 0 ? breadcrumbs : undefined;\n}\n\nasync function getStructuredData(page: SearchPage) {\n  const pageData = page.data as {\n    structuredData?: unknown | (() => unknown | Promise<unknown>);\n    load?: () => Promise<{ structuredData?: unknown }>;\n  };\n\n  if (typeof pageData.structuredData === 'function') {\n    return pageData.structuredData();\n  }\n\n  if (pageData.structuredData) {\n    return pageData.structuredData;\n  }\n\n  if (typeof pageData.load === 'function') {\n    return (await pageData.load()).structuredData;\n  }\n}\n\nasync function buildSearchIndex(docSource: SearchSource, page: SearchPage) {\n  const structuredData = await getStructuredData(page);\n\n  if (!structuredData) {\n    throw new Error(`Cannot build search index for ${page.url}`);\n  }\n\n  return {\n    id: page.url,\n    title: page.data.title ?? page.slugs.at(-1) ?? page.path,\n    description: page.data.description,\n    url: page.url,\n    structuredData,\n    breadcrumbs: buildBreadcrumbs(docSource, page),\n  };\n}\n\nconst search = flexsearchI18n({\n  i18n: docsI18n,\n  localeMap: {\n    zh: 'cjk',\n    ja: 'cjk',\n  },\n  indexes() {\n    const indexes = Object.entries(sourcesByLocale).flatMap(\n      ([locale, docSource]) =>\n        docSource.getPages().map(async page => ({\n          locale,\n          ...(await buildSearchIndex(docSource, page)),\n        }))\n    );\n\n    return Promise.all(indexes);\n  },\n});\n\nexport const GET = search.staticGET;\n"
  },
  {
    "path": "website/app/blog/[...slug]/page.tsx",
    "content": "import { blog } from 'fumadocs-mdx:collections/server';\nimport type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\n\nimport { getMDXComponents } from '@/mdx-components';\n\nfunction findPost(slug: string[]) {\n  const target = slug.join('/');\n  return blog.find(p => p.info.path.replace(/\\.mdx$/, '') === target);\n}\n\nexport default async function BlogPost(props: {\n  params: Promise<{ slug: string[] }>;\n}) {\n  const params = await props.params;\n  const page = findPost(params.slug);\n  if (!page) notFound();\n\n  const MDX = page.body;\n\n  return (\n    <div className='mx-auto max-w-4xl px-6 py-12'>\n      <div className='mb-8'>\n        {page.date && (\n          <time\n            dateTime={page.date}\n            className='text-fd-muted-foreground text-sm'\n          >\n            {new Date(page.date).toLocaleDateString('en-US', {\n              year: 'numeric',\n              month: 'long',\n              day: 'numeric',\n            })}\n          </time>\n        )}\n        <h1 className='mt-4 text-4xl font-bold tracking-tight'>{page.title}</h1>\n        {page.description && (\n          <p className='text-fd-muted-foreground mt-4 text-lg'>\n            {page.description}\n          </p>\n        )}\n      </div>\n      <div className='prose dark:prose-invert max-w-none'>\n        <MDX components={getMDXComponents()} />\n      </div>\n    </div>\n  );\n}\n\nexport function generateStaticParams() {\n  return blog.map(page => ({\n    slug: page.info.path.replace(/\\.mdx$/, '').split('/'),\n  }));\n}\n\nexport async function generateMetadata(props: {\n  params: Promise<{ slug: string[] }>;\n}): Promise<Metadata> {\n  const params = await props.params;\n  const page = findPost(params.slug);\n  if (!page) notFound();\n\n  return {\n    title: page.title,\n    description: page.description,\n  };\n}\n"
  },
  {
    "path": "website/app/blog/layout.tsx",
    "content": "import { HomeLayout } from 'fumadocs-ui/layouts/home';\n\nimport { baseOptions } from '@/lib/layout.shared';\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;\n}\n"
  },
  {
    "path": "website/app/blog/page.tsx",
    "content": "import { blog } from 'fumadocs-mdx:collections/server';\nimport type { Metadata } from 'next';\nimport Link from 'next/link';\n\nexport const metadata: Metadata = {\n  title: 'Blog',\n  description: 'The latest news and technical articles from the DayFlow team.',\n};\n\nexport default function BlogPage() {\n  const posts = [...blog].toSorted((a, b) => {\n    const dateA = a.date ? new Date(a.date).getTime() : 0;\n    const dateB = b.date ? new Date(b.date).getTime() : 0;\n    return dateB - dateA;\n  });\n\n  return (\n    <div className='mx-auto max-w-4xl px-6 py-12'>\n      <h1 className='text-4xl font-bold tracking-tight'>Blog</h1>\n      <p className='text-fd-muted-foreground mt-4 text-lg'>\n        The latest news and technical articles from the DayFlow team.\n      </p>\n      <div className='mt-10 space-y-12'>\n        {posts.map(post => {\n          const slug = post.info.path.replace(/\\.mdx$/, '');\n          const url = `/blog/${slug}`;\n          return (\n            <article\n              key={url}\n              className='flex flex-col items-start justify-between'\n            >\n              <div className='flex items-center gap-x-4 text-xs'>\n                {post.date && (\n                  <time\n                    dateTime={post.date}\n                    className='text-fd-muted-foreground'\n                  >\n                    {new Date(post.date).toLocaleDateString('en-US', {\n                      year: 'numeric',\n                      month: 'long',\n                      day: 'numeric',\n                    })}\n                  </time>\n                )}\n              </div>\n              <div className='group relative'>\n                <h3 className='group-hover:text-fd-primary mt-3 text-2xl leading-6 font-semibold transition-colors'>\n                  <Link href={url}>\n                    <span className='absolute inset-0' />\n                    {post.title}\n                  </Link>\n                </h3>\n                {post.description && (\n                  <p className='text-fd-muted-foreground mt-5 line-clamp-3 text-sm leading-6'>\n                    {post.description}\n                  </p>\n                )}\n              </div>\n              <div className='mt-6'>\n                <Link\n                  href={url}\n                  className='text-fd-primary text-sm font-medium transition-opacity hover:opacity-80'\n                >\n                  Read more →\n                </Link>\n              </div>\n            </article>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/app/docs/[[...slug]]/page.tsx",
    "content": "import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page';\nimport { createRelativeLink } from 'fumadocs-ui/mdx';\nimport type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\n\nimport { getPageImage, source } from '@/lib/source';\nimport { getMDXComponents } from '@/mdx-components';\n\nexport default async function Page(props: PageProps<'/docs/[[...slug]]'>) {\n  const params = await props.params;\n  const page = source.getPage(params.slug);\n  if (!page) notFound();\n\n  const MDX = page.data.body;\n\n  const full = page.data.full ?? params.slug?.[0] === 'features';\n\n  return (\n    <DocsPage\n      toc={page.data.toc}\n      full={full}\n      breadcrumb={{ enabled: false }}\n      tableOfContent={{ style: 'clerk' }}\n    >\n      <DocsBody>\n        <MDX\n          components={getMDXComponents({\n            // this allows you to link to other pages with relative file paths\n            a: createRelativeLink(source, page),\n          })}\n        />\n      </DocsBody>\n    </DocsPage>\n  );\n}\n\nexport function generateStaticParams() {\n  return source.generateParams();\n}\n\nexport async function generateMetadata(\n  props: PageProps<'/docs/[[...slug]]'>\n): Promise<Metadata> {\n  const params = await props.params;\n  const page = source.getPage(params.slug);\n  if (!page) notFound();\n\n  return {\n    title: page.data.title,\n    description: page.data.description,\n    openGraph: {\n      images: getPageImage(page).url,\n    },\n  };\n}\n"
  },
  {
    "path": "website/app/docs/layout.tsx",
    "content": "import { DocsLayout } from 'fumadocs-ui/layouts/docs';\nimport React from 'react';\n\nimport { DocsHeader } from '@/components/DocsHeader';\nimport { baseOptions, gitConfig, sidebarTabs } from '@/lib/layout.shared';\nimport { source } from '@/lib/source';\n\nexport default function Layout({ children }: LayoutProps<'/docs'>) {\n  return (\n    <DocsLayout\n      tree={source.getPageTree()}\n      {...baseOptions()}\n      links={[]}\n      nav={{\n        component: (\n          <DocsHeader\n            githubUrl={`https://github.com/${gitConfig.user}/${gitConfig.repo}`}\n          />\n        ),\n      }}\n      sidebar={{\n        collapsible: false,\n        tabs: sidebarTabs,\n      }}\n      containerProps={{\n        style: {\n          '--fd-banner-height': '56px',\n          gridTemplate: `\"banner banner banner banner banner\" 56px \"sidebar sidebar header toc toc\" \"sidebar sidebar toc-popover toc toc\" \"sidebar sidebar main toc toc\" 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-width) - var(--fd-toc-width))) var(--fd-toc-width) minmax(min-content, 1fr)`,\n        } as React.CSSProperties,\n      }}\n    >\n      {children}\n    </DocsLayout>\n  );\n}\n"
  },
  {
    "path": "website/app/docs-ja/[[...slug]]/page.tsx",
    "content": "import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page';\nimport { createRelativeLink } from 'fumadocs-ui/mdx';\nimport type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\n\nimport { sourceJa } from '@/lib/source';\nimport { getMDXComponents } from '@/mdx-components';\n\nexport default async function Page(props: PageProps<'/docs-ja/[[...slug]]'>) {\n  const params = await props.params;\n  const page = sourceJa.getPage(params.slug);\n  if (!page) notFound();\n\n  const MDX = page.data.body;\n\n  const full = page.data.full ?? params.slug?.[0] === 'features';\n\n  return (\n    <DocsPage\n      toc={page.data.toc}\n      full={full}\n      breadcrumb={{ enabled: false }}\n      tableOfContent={{ style: 'clerk' }}\n    >\n      <DocsBody>\n        <MDX\n          components={getMDXComponents({\n            a: createRelativeLink(sourceJa, page),\n          })}\n        />\n      </DocsBody>\n    </DocsPage>\n  );\n}\n\nexport function generateStaticParams() {\n  return sourceJa.generateParams();\n}\n\nexport async function generateMetadata(\n  props: PageProps<'/docs-ja/[[...slug]]'>\n): Promise<Metadata> {\n  const params = await props.params;\n  const page = sourceJa.getPage(params.slug);\n  if (!page) notFound();\n\n  return {\n    title: page.data.title,\n    description: page.data.description,\n  };\n}\n"
  },
  {
    "path": "website/app/docs-ja/layout.tsx",
    "content": "import { DocsLayout } from 'fumadocs-ui/layouts/docs';\nimport React from 'react';\n\nimport { DocsHeader } from '@/components/DocsHeader';\nimport { baseOptions, gitConfig, sidebarTabs } from '@/lib/layout.shared';\nimport { sourceJa } from '@/lib/source';\n\nexport default function Layout({ children }: LayoutProps<'/docs-ja'>) {\n  const tabs = sidebarTabs.map(tab =>\n    tab.title === 'Calendar' ? { ...tab, url: '/docs-ja' } : tab\n  );\n\n  return (\n    <DocsLayout\n      tree={sourceJa.getPageTree()}\n      {...baseOptions()}\n      links={[]}\n      nav={{\n        component: (\n          <DocsHeader\n            githubUrl={`https://github.com/${gitConfig.user}/${gitConfig.repo}`}\n          />\n        ),\n      }}\n      sidebar={{\n        collapsible: false,\n        tabs,\n      }}\n      containerProps={{\n        style: {\n          '--fd-banner-height': '56px',\n          gridTemplate: `\"banner banner banner banner banner\" 56px \"sidebar sidebar header toc toc\" \"sidebar sidebar toc-popover toc toc\" \"sidebar sidebar main toc toc\" 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-width) - var(--fd-toc-width))) var(--fd-toc-width) minmax(min-content, 1fr)`,\n        } as React.CSSProperties,\n      }}\n    >\n      {children}\n    </DocsLayout>\n  );\n}\n"
  },
  {
    "path": "website/app/docs-zh/[[...slug]]/page.tsx",
    "content": "import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page';\nimport { createRelativeLink } from 'fumadocs-ui/mdx';\nimport type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\n\nimport { sourceZh } from '@/lib/source';\nimport { getMDXComponents } from '@/mdx-components';\n\nexport default async function Page(props: PageProps<'/docs-zh/[[...slug]]'>) {\n  const params = await props.params;\n  const page = sourceZh.getPage(params.slug);\n  if (!page) notFound();\n\n  const MDX = page.data.body;\n\n  const full = page.data.full ?? params.slug?.[0] === 'features';\n\n  return (\n    <DocsPage\n      toc={page.data.toc}\n      full={full}\n      breadcrumb={{ enabled: false }}\n      tableOfContent={{ style: 'clerk' }}\n    >\n      <DocsBody>\n        <MDX\n          components={getMDXComponents({\n            a: createRelativeLink(sourceZh, page),\n          })}\n        />\n      </DocsBody>\n    </DocsPage>\n  );\n}\n\nexport function generateStaticParams() {\n  return sourceZh.generateParams();\n}\n\nexport async function generateMetadata(\n  props: PageProps<'/docs-zh/[[...slug]]'>\n): Promise<Metadata> {\n  const params = await props.params;\n  const page = sourceZh.getPage(params.slug);\n  if (!page) notFound();\n\n  return {\n    title: page.data.title,\n    description: page.data.description,\n  };\n}\n"
  },
  {
    "path": "website/app/docs-zh/layout.tsx",
    "content": "import { DocsLayout } from 'fumadocs-ui/layouts/docs';\nimport React from 'react';\n\nimport { DocsHeader } from '@/components/DocsHeader';\nimport { baseOptions, gitConfig, sidebarTabs } from '@/lib/layout.shared';\nimport { sourceZh } from '@/lib/source';\n\nexport default function Layout({ children }: LayoutProps<'/docs-zh'>) {\n  const tabs = sidebarTabs.map(tab =>\n    tab.title === 'Calendar' ? { ...tab, url: '/docs-zh' } : tab\n  );\n\n  return (\n    <DocsLayout\n      tree={sourceZh.getPageTree()}\n      {...baseOptions()}\n      links={[]}\n      nav={{\n        component: (\n          <DocsHeader\n            githubUrl={`https://github.com/${gitConfig.user}/${gitConfig.repo}`}\n          />\n        ),\n      }}\n      sidebar={{\n        collapsible: false,\n        tabs,\n      }}\n      containerProps={{\n        style: {\n          '--fd-banner-height': '56px',\n          gridTemplate: `\"banner banner banner banner banner\" 56px \"sidebar sidebar header toc toc\" \"sidebar sidebar toc-popover toc toc\" \"sidebar sidebar main toc toc\" 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-width) - var(--fd-toc-width))) var(--fd-toc-width) minmax(min-content, 1fr)`,\n        } as React.CSSProperties,\n      }}\n    >\n      {children}\n    </DocsLayout>\n  );\n}\n"
  },
  {
    "path": "website/app/global.css",
    "content": "/* DayFlow Tailwind Setup */\n@import '@dayflow/core/dist/styles.components.css';\n@import '@dayflow/blossom-color-picker/styles.css';\n@import 'tailwindcss';\n@import 'fumadocs-ui/css/neutral.css';\n@import 'fumadocs-ui/css/preset.css';\n\n/* DayFlow: class-based dark mode so theme.mode works correctly */\n@variant dark (.dark &);\n\n@source '../node_modules/fumadocs-ui/dist/**/*.js';\n\n/* Clerk TOC: classes not in fumadocs generated CSS, needed for Tailwind v4 scanning */\n@source inline(\"top-(--fd-top)\");\n@source inline(\"h-(--fd-height)\");\n@source inline(\"transition-[top,height]\");\n@source inline(\"stroke-fd-foreground/10\");\n@source inline(\"-top-1.5\");\n@source inline(\"bg-fd-foreground/10\");\n@source inline(\"w-px\");\n@source inline(\"top-1.5\");\n@source inline(\"bottom-1.5\");\n\n/* fumadocs Tabs: data-[state=active] variants not picked up by Turbopack @source scanning */\n@source inline(\"data-[state=active]:border-fd-primary\");\n@source inline(\"data-[state=active]:text-fd-primary\");\n@source inline(\"data-[state=inactive]:hidden\");\n@source inline(\"group-data-[state=active]:bg-fd-primary\");\n\n/* fumadocs Tabs: fallback direct CSS for active tab underline (Turbopack @source inline limitation) */\n[role='tab'][data-state='active'] {\n  border-color: var(--color-fd-primary);\n  color: var(--color-fd-primary);\n}\n[role='tabpanel'][data-state='inactive'] {\n  display: none;\n}\n\n:root {\n  --color-fd-background: #ffffff;\n  --color-fd-card: #ffffff;\n  --color-fd-secondary: #ffffff;\n}\n\n.dark {\n  --color-fd-background: hsl(0, 0%, 7.04%);\n  --color-fd-card: hsl(0, 0%, 9.8%);\n  --color-fd-secondary: hsl(0, 0%, 12.9%);\n}\n\n/* Clerk TOC: Tailwind v4 uses logical properties (padding-block, inset-block) for\n   multi-axis utilities (py-*, inset-y-*), causing conflicts with physical properties\n   (padding-top, top) from single-axis classes. These rules are outside @layer so\n   they unconditionally win, with !important to override any remaining conflicts.\n   clerk.js reads getComputedStyle().paddingTop (physical), so we must set BOTH\n   physical and logical properties to guarantee the correct resolved value. */\n.first\\:pt-0:first-child {\n  padding-top: 0 !important;\n  padding-block-start: 0 !important;\n}\n.last\\:pb-0:last-child {\n  padding-bottom: 0 !important;\n  padding-block-end: 0 !important;\n}\n/* inset-y-0 generates inset-block:0 (logical), top-1.5/bottom-1.5 use physical top/bottom.\n   Set both physical and logical to guarantee correct layout. */\n.inset-y-0.top-1\\.5 {\n  top: 0.375rem !important;\n  inset-block-start: 0.375rem !important;\n}\n.inset-y-0.bottom-1\\.5 {\n  bottom: 0.375rem !important;\n  inset-block-end: 0.375rem !important;\n}\n\n.shiki {\n  background-color: #f3f7fd !important;\n}\n\n.dark .shiki {\n  background-color: #141414 !important;\n}\n\n/* Docs layout: make the injected HomeLayout header span the full viewport width */\n#nd-docs-layout #nd-nav {\n  position: fixed;\n  left: 0;\n  right: 0;\n  width: 100%;\n  /* Fix: compensate for scrollbar removal by Radix/Shadcn */\n  padding-right: var(--removed-body-scroll-bar-size, 0);\n}\n\n/* Override fumadocs' xl:pt-14 (3.5rem) — the grid \"banner\" row already\n   offsets content below the fixed header, so a smaller value is sufficient. */\n#nd-docs-layout [class*='grid-area:main'] {\n  padding-top: 1.5rem !important;\n}\n\n/* Prevent prose (typography) styles from leaking into the calendar components */\n.df-calendar-wrapper img {\n  margin-top: 0;\n  margin-bottom: 0;\n}\n.df-calendar-wrapper {\n  line-height: normal;\n}\n\n/* Fix: month-view single-day events (allDay or timed) have no explicit height on the\n   CalendarEvent wrapper, so the ContentSlot's h-full would resolve against the fixed-height\n   cell ancestor instead of the auto-height event wrapper — override to auto. */\n.df-month-day-cell .df-event .df-content-slot {\n  height: auto;\n}\n\n/* Home page landing section padding */\n.live-demo-container {\n  width: 100%;\n  padding-inline: 0.5rem;\n}\n\n/* Ensure the calendar wrapper has a minimum height so ResizeObserver\n   gets a stable non-zero measurement on first mount.\n   min-h-150 (Tailwind) may not be generated under Turbopack. */\n.live-demo-container .calendar-wrapper {\n  min-height: 37.5rem; /* equivalent to min-h-150 */\n}\n@media (min-width: 640px) {\n  .live-demo-container {\n    padding-inline: 8rem;\n  }\n}\n@media (min-width: 1024px) {\n  .live-demo-container {\n    padding-inline: 8rem;\n  }\n}\n\n/* Sidebar: active menu item */\n#nd-sidebar a[data-active='true'] {\n  background-color: #dee4f2;\n  color: #307efe;\n}\n#nd-sidebar a[data-active='true']::before {\n  background-color: #307efe;\n}\n.dark #nd-sidebar a[data-active='true'] {\n  background-color: #272b2f;\n  color: #aaceff;\n}\n.dark #nd-sidebar a[data-active='true']::before {\n  background-color: #aaceff;\n}\n\n/* TOC (right outline, clerk style): active link + indicator bar color, no background */\n#nd-toc {\n  --color-fd-primary: #307efe;\n}\n.dark #nd-toc {\n  --color-fd-primary: #aaceff;\n}\n\n/* fumadocs responsive utilities not auto-generated by Tailwind v4 */\n@media not (min-width: 640px) {\n  [class~='max-sm:hidden'] {\n    display: none !important;\n  }\n}\n@media (min-width: 768px) {\n  [class~='md:hidden'] {\n    display: none !important;\n  }\n}\n@media not (min-width: 768px) {\n  [class~='max-md:hidden'] {\n    display: none !important;\n  }\n}\n@media (min-width: 1024px) {\n  [class~='lg:hidden'] {\n    display: none !important;\n  }\n}\n@media not (min-width: 1024px) {\n  [class~='max-lg:hidden'] {\n    display: none !important;\n  }\n}\n@media (min-width: 1280px) {\n  [class~='xl:hidden'] {\n    display: none !important;\n  }\n}\n@media not (min-width: 1280px) {\n  [class~='max-xl:hidden'] {\n    display: none !important;\n  }\n}\n\n.df-calendar-container {\n  --df-calendar-height: 750px !important;\n  height: var(--df-calendar-height, 750px) !important;\n}\n\n@media (max-width: 768px) {\n  .df-calendar-container .df-header-left,\n  .df-calendar-container .df-header-right {\n    display: none !important;\n  }\n\n  .df-calendar-container .df-header {\n    justify-content: center;\n    padding-right: 0;\n  }\n}\n\n.df-calendar-container .df-header button,\n.df-calendar-container .df-header input,\n.df-calendar-container .df-header select,\n.df-calendar-container .df-header a {\n  -webkit-app-region: no-drag;\n}\n\n/* Active only when collapsedSafeAreaLeft / Mac title bar feature is enabled */\n.mac-title-bar-active .df-calendar-container .calendar-title-bar {\n  -webkit-app-region: no-drag;\n}\n\n.mac-title-bar-active .df-calendar-container .df-sidebar-header {\n  display: none;\n}\n\n.mac-title-bar-active .df-calendar-container .df-sidebar {\n  margin-top: 35px;\n}\n"
  },
  {
    "path": "website/app/layout.tsx",
    "content": "import type { Metadata } from 'next';\n\nimport './global.css';\nimport { Inter } from 'next/font/google';\nimport Script from 'next/script';\n\nimport { AppProvider } from '@/components/AppProvider';\nimport { BASE_PATH, SITE_METADATA_BASE } from '@/lib/site';\n\nconst inter = Inter({\n  subsets: ['latin'],\n});\n\nexport const metadata: Metadata = {\n  metadataBase: SITE_METADATA_BASE,\n  title: {\n    template: '%s | DayFlow',\n    default: 'DayFlow - Lightweight Calendar Component',\n  },\n  description:\n    'A lightweight and elegant full calendar component for React, Vue, Angular, and Svelte. Supports day, week, month, and year views with drag-and-drop, localization, and dark mode.',\n  openGraph: {\n    type: 'website',\n    siteName: 'DayFlow',\n    title: {\n      template: '%s | DayFlow',\n      default: 'DayFlow - Lightweight Calendar Component',\n    },\n    description:\n      'A lightweight and elegant full calendar component for React, Vue, Angular, and Svelte.',\n    images: [\n      {\n        url: `${BASE_PATH}/logo.png`,\n        width: 512,\n        height: 512,\n        alt: 'DayFlow Logo',\n      },\n    ],\n  },\n  twitter: {\n    card: 'summary_large_image',\n    title: {\n      template: '%s | DayFlow',\n      default: 'DayFlow - Lightweight Calendar Component',\n    },\n    description:\n      'A lightweight and elegant full calendar component for React, Vue, Angular, and Svelte.',\n  },\n};\n\nexport default function Layout({ children }: LayoutProps<'/'>) {\n  return (\n    <html lang='en' className={inter.className} suppressHydrationWarning>\n      <head>\n        {process.env.NODE_ENV === 'production' && (\n          <>\n            <Script\n              src='https://www.googletagmanager.com/gtag/js?id=G-QEXJYTSEME'\n              strategy='afterInteractive'\n            />\n            <Script id='google-analytics' strategy='afterInteractive'>\n              {`\n                window.dataLayer = window.dataLayer || [];\n                function gtag(){dataLayer.push(arguments);}\n                gtag('js', new Date());\n                gtag('config', 'G-QEXJYTSEME');\n              `}\n            </Script>\n          </>\n        )}\n      </head>\n      <body className='flex min-h-screen flex-col'>\n        <AppProvider>{children}</AppProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "website/app/llms-full.txt/route.ts",
    "content": "import { getLLMText, source } from '@/lib/source';\n\nexport const revalidate = false;\n\nexport async function GET() {\n  const scan = source.getPages().map(getLLMText);\n  const scanned = await Promise.all(scan);\n\n  return new Response(scanned.join('\\n\\n'));\n}\n"
  },
  {
    "path": "website/app/llms.txt/route.ts",
    "content": "import { source } from '@/lib/source';\n\nexport const revalidate = false;\n\nexport function GET() {\n  const lines: string[] = ['# Documentation', ''];\n  for (const page of source.getPages()) {\n    lines.push(`- [${page.data.title}](${page.url}): ${page.data.description}`);\n  }\n  return new Response(lines.join('\\n'));\n}\n"
  },
  {
    "path": "website/app/og/docs/[...slug]/route.tsx",
    "content": "import { generate as DefaultImage } from 'fumadocs-ui/og';\nimport { notFound } from 'next/navigation';\nimport { ImageResponse } from 'next/og';\n\nimport { getPageImage, source } from '@/lib/source';\n\nexport const dynamic = 'force-static';\nexport const revalidate = false;\n\nexport async function GET(\n  _req: Request,\n  { params }: RouteContext<'/og/docs/[...slug]'>\n) {\n  const { slug } = await params;\n  const page = source.getPage(slug.slice(0, -1));\n  if (!page) notFound();\n\n  return new ImageResponse(\n    <DefaultImage\n      title={page.data.title}\n      description={page.data.description}\n      site='DayFlow'\n    />,\n    {\n      width: 1200,\n      height: 630,\n    }\n  );\n}\n\nexport function generateStaticParams() {\n  return source.getPages().map(page => ({\n    lang: page.locale,\n    slug: getPageImage(page).segments,\n  }));\n}\n"
  },
  {
    "path": "website/app/robots.ts",
    "content": "import type { MetadataRoute } from 'next';\n\nexport const dynamic = 'force-static';\n\nimport { SITE_URL } from '@/lib/site';\n\nexport default function robots(): MetadataRoute.Robots {\n  return {\n    rules: {\n      userAgent: '*',\n      allow: '/',\n    },\n    sitemap: `${SITE_URL}/sitemap.xml`,\n  };\n}\n"
  },
  {
    "path": "website/app/showcase/mobile-event-detail/page.tsx",
    "content": "import type { Metadata } from 'next';\n\nimport { MobileEventDetailSimulator } from '@/components/showcase/mobile-event-detail/MobileEventDetailSimulator';\n\nexport const metadata: Metadata = {\n  title: 'Mobile Event Detail Simulator',\n  robots: {\n    index: false,\n    follow: false,\n  },\n};\n\nexport default function MobileEventDetailSimulatorPage() {\n  return <MobileEventDetailSimulator />;\n}\n"
  },
  {
    "path": "website/app/sitemap.ts",
    "content": "import type { MetadataRoute } from 'next';\n\nexport const dynamic = 'force-static';\n\nimport { blog } from 'fumadocs-mdx:collections/server';\n\nimport { SITE_URL } from '@/lib/site';\nimport { source, sourceJa, sourceZh } from '@/lib/source';\n\nexport default function sitemap(): MetadataRoute.Sitemap {\n  const now = new Date();\n\n  const docPages = source.getPages().map(page => ({\n    url: `${SITE_URL}${page.url}`,\n    lastModified: now,\n    changeFrequency: 'weekly' as const,\n    priority: page.url === '/docs' ? 0.9 : 0.8,\n  }));\n\n  const docJaPages = sourceJa.getPages().map(page => ({\n    url: `${SITE_URL}${page.url}`,\n    lastModified: now,\n    changeFrequency: 'weekly' as const,\n    priority: 0.7,\n  }));\n\n  const docZhPages = sourceZh.getPages().map(page => ({\n    url: `${SITE_URL}${page.url}`,\n    lastModified: now,\n    changeFrequency: 'weekly' as const,\n    priority: 0.7,\n  }));\n\n  const blogPages = blog.map(post => {\n    const slug = post.info.path.replace(/\\.mdx$/, '');\n    return {\n      url: `${SITE_URL}/blog/${slug}`,\n      lastModified: post.date ? new Date(post.date) : now,\n      changeFrequency: 'monthly' as const,\n      priority: 0.6,\n    };\n  });\n\n  return [\n    {\n      url: SITE_URL,\n      lastModified: now,\n      changeFrequency: 'weekly',\n      priority: 1.0,\n    },\n    {\n      url: `${SITE_URL}/blog`,\n      lastModified: now,\n      changeFrequency: 'weekly',\n      priority: 0.7,\n    },\n    ...docPages,\n    ...docJaPages,\n    ...docZhPages,\n    ...blogPages,\n  ];\n}\n"
  },
  {
    "path": "website/components/AppProvider.tsx",
    "content": "'use client';\n\nimport { RootProvider } from 'fumadocs-ui/provider/next';\nimport { usePathname } from 'next/navigation';\nimport type { ReactNode } from 'react';\n\nimport { DocsSearchDialog } from '@/components/DocsSearchDialog';\nimport { getLanguageCodeFromPathname, localeItems } from '@/lib/i18n';\nimport { BASE_PATH } from '@/lib/site';\n\nexport function AppProvider({ children }: { children: ReactNode }) {\n  const pathname = usePathname();\n  const locale = getLanguageCodeFromPathname(pathname);\n\n  return (\n    <RootProvider\n      search={{\n        SearchDialog: DocsSearchDialog,\n        options: { api: `${BASE_PATH}/api/search` },\n      }}\n      i18n={{ locale, locales: localeItems }}\n    >\n      {children}\n    </RootProvider>\n  );\n}\n"
  },
  {
    "path": "website/components/CliPreview.tsx",
    "content": "'use client';\n\nimport { useState, useEffect } from 'react';\n\n// Color tokens (light / dark)\n// border-pipe:  text-zinc-400 dark:text-zinc-600\n// secondary:    text-zinc-500 dark:text-zinc-500\n// tertiary:     text-zinc-500 dark:text-zinc-400\n// body:         text-zinc-700 dark:text-zinc-300\n// primary:      text-zinc-900 dark:text-white\n// accent:       text-cyan-600 dark:text-cyan-400\n// success:      text-green-600 dark:text-green-400\n\nexport function CliPreview() {\n  const [command, setCommand] = useState('');\n  const [showContent, setShowContent] = useState(false);\n  const fullCommand = 'npm create dayflow@latest';\n\n  useEffect(() => {\n    let timeout: NodeJS.Timeout;\n    if (command.length < fullCommand.length) {\n      timeout = setTimeout(() => {\n        setCommand(fullCommand.slice(0, command.length + 1));\n      }, 60);\n    } else {\n      timeout = setTimeout(() => {\n        setShowContent(true);\n      }, 600);\n    }\n    return () => clearTimeout(timeout);\n  }, [command]);\n\n  return (\n    <div className='my-6 overflow-hidden rounded-xl border border-zinc-300 bg-[#f3f7fe] font-mono text-sm leading-6 dark:border-zinc-700 dark:bg-[#0d0d0d]'>\n      {/* Window chrome */}\n      <div className='text-fd-muted-foreground flex flex-row items-center gap-2 border-b p-2'>\n        <svg\n          xmlns='http://www.w3.org/2000/svg'\n          width='24'\n          height='24'\n          viewBox='0 0 24 24'\n          fill='none'\n          stroke='currentColor'\n          strokeWidth='2'\n          strokeLinecap='round'\n          strokeLinejoin='round'\n          className='lucide lucide-terminal size-4'\n          aria-hidden='true'\n        >\n          <path d='M12 19h8'></path>\n          <path d='m4 17 6-6-6-6'></path>\n        </svg>\n        <span className='text-xs font-medium'>Terminal</span>\n        <div className='ms-auto me-2 size-2 rounded-full bg-red-400'></div>\n      </div>\n\n      {/* Terminal content */}\n      <div className='overflow-x-auto p-5 text-[13px] leading-[1.7]'>\n        <div className='mb-4 flex gap-2'>\n          <span className='text-cyan-600 dark:text-cyan-400'>~</span>\n          <span className='text-zinc-900 dark:text-white'>\n            {command}\n            {!showContent && (\n              <span className='ml-0.5 inline-block h-4 w-1.5 animate-pulse bg-zinc-500 align-middle' />\n            )}\n          </span>\n        </div>\n\n        {showContent && (\n          <div className='animate-in fade-in slide-in-from-top-1 duration-500'>\n            {/* Intro */}\n            <p>\n              <span className='text-zinc-400 dark:text-zinc-600'>┌ </span>\n              <span className='rounded bg-cyan-500 px-1.5 py-0.5 font-semibold text-black'>\n                DayFlow\n              </span>\n              <span className='text-zinc-500'> — Calendar component setup</span>\n            </p>\n            {/* Framework selection */}\n            <p>\n              <span className='text-cyan-600 dark:text-cyan-400'>◆ </span>\n              <span className='font-medium text-zinc-900 dark:text-white'>\n                Which framework are you using?\n              </span>\n            </p>\n            <p>\n              <span className='text-zinc-400 dark:text-zinc-600'>│ </span>\n              <span className='text-cyan-600 dark:text-cyan-400'>● </span>\n              <span className='text-zinc-900 dark:text-white'>React </span>\n              <span className='text-zinc-500'>(@dayflow/react)</span>\n            </p>\n            <p>\n              <span className='text-zinc-400 dark:text-zinc-600'>│ </span>\n              <span className='text-zinc-500'>○ Vue</span>\n            </p>\n            <p>\n              <span className='text-zinc-400 dark:text-zinc-600'>│ </span>\n              <span className='text-zinc-500'>○ Svelte</span>\n            </p>\n            <p>\n              <span className='text-zinc-400 dark:text-zinc-600'>│ </span>\n              <span className='text-zinc-500'>○ Angular</span>\n            </p>\n\n            <p>\n              <span className='text-zinc-900 dark:text-white'>...</span>\n            </p>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/components/ColorPalette.tsx",
    "content": "'use client';\n\nimport { usePathname } from 'next/navigation';\nimport React from 'react';\n\ninterface ColorSwatchProps {\n  name: string;\n  lightColor: string;\n  darkColor: string;\n  lightLabel: string;\n  darkLabel: string;\n}\n\nconst ColorSwatch: React.FC<ColorSwatchProps> = ({\n  name,\n  lightColor,\n  darkColor,\n  lightLabel,\n  darkLabel,\n}) => (\n  <div className='flex flex-col gap-2'>\n    <div className='text-sm font-medium text-gray-900 dark:text-gray-100'>\n      {name}\n    </div>\n    <div className='grid grid-cols-2 gap-2'>\n      {/* Light mode color */}\n      <div className='flex flex-col gap-1'>\n        <div\n          className='h-16 w-full rounded-lg border border-gray-200 shadow-sm dark:border-gray-700'\n          style={{ backgroundColor: lightColor }}\n        />\n        <div className='font-mono text-xs text-gray-600 dark:text-gray-400'>\n          {lightColor}\n        </div>\n        <div className='text-xs text-gray-500 dark:text-gray-500'>\n          {lightLabel}\n        </div>\n      </div>\n      {/* Dark mode color */}\n      <div className='flex flex-col gap-1'>\n        <div\n          className='h-16 w-full rounded-lg border border-gray-200 shadow-sm dark:border-gray-700'\n          style={{ backgroundColor: darkColor }}\n        />\n        <div className='font-mono text-xs text-gray-600 dark:text-gray-400'>\n          {darkColor}\n        </div>\n        <div className='text-xs text-gray-500 dark:text-gray-500'>\n          {darkLabel}\n        </div>\n      </div>\n    </div>\n  </div>\n);\n\nconst translations = {\n  en: {\n    blue: 'Blue',\n    green: 'Green',\n    purple: 'Purple',\n    yellow: 'Yellow',\n    red: 'Red',\n    orange: 'Orange',\n    pink: 'Pink',\n    teal: 'Teal',\n    indigo: 'Indigo',\n    gray: 'Gray',\n    light: 'Light',\n    dark: 'Dark',\n    tip: 'Tip:',\n    wcagNote:\n      'All colors meet WCAG AA contrast requirements for both light and dark backgrounds, ensuring good readability.',\n  },\n  zh: {\n    blue: '蓝色',\n    green: '绿色',\n    purple: '紫色',\n    yellow: '黄色',\n    red: '红色',\n    orange: '橙色',\n    pink: '粉色',\n    teal: '青色',\n    indigo: '靛蓝',\n    gray: '灰色',\n    light: '浅色',\n    dark: '深色',\n    tip: '提示：',\n    wcagNote:\n      '所有颜色都符合 WCAG AA 对比度标准，确保在浅色和深色背景下都具有良好的可读性。',\n  },\n  ja: {\n    blue: 'ブルー',\n    green: 'グリーン',\n    purple: 'パープル',\n    yellow: 'イエロー',\n    red: 'レッド',\n    orange: 'オレンジ',\n    pink: 'ピンク',\n    teal: 'ティール',\n    indigo: 'インディゴ',\n    gray: 'グレー',\n    light: 'ライト',\n    dark: 'ダーク',\n    tip: 'ヒント：',\n    wcagNote:\n      'すべての色はWCAG AAコントラスト基準を満たしており、明暗どちらの背景でも読みやすくなっています。',\n  },\n};\n\nexport const DefaultColorPalette: React.FC = () => {\n  const pathname = usePathname();\n\n  // Detect language from pathname\n  const lang = pathname?.startsWith('/docs-zh')\n    ? 'zh'\n    : pathname?.startsWith('/docs-ja')\n      ? 'ja'\n      : 'en';\n\n  const t = translations[lang];\n\n  const colors = [\n    { key: 'blue', light: '#3b82f6', dark: '#60a5fa' },\n    { key: 'green', light: '#22c55e', dark: '#4ade80' },\n    { key: 'purple', light: '#a855f7', dark: '#c084fc' },\n    { key: 'yellow', light: '#eab308', dark: '#facc15' },\n    { key: 'red', light: '#ef4444', dark: '#f87171' },\n    { key: 'orange', light: '#f97316', dark: '#fb923c' },\n    { key: 'pink', light: '#ec4899', dark: '#f472b6' },\n    { key: 'teal', light: '#14b8a6', dark: '#2dd4bf' },\n    { key: 'indigo', light: '#6366f1', dark: '#818cf8' },\n    { key: 'gray', light: '#6b7280', dark: '#9ca3af' },\n  ];\n\n  return (\n    <div className='not-prose my-8'>\n      <div className='grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3'>\n        {colors.map(color => (\n          <ColorSwatch\n            key={color.key}\n            name={t[color.key as keyof typeof t] as string}\n            lightColor={color.light}\n            darkColor={color.dark}\n            lightLabel={t.light}\n            darkLabel={t.dark}\n          />\n        ))}\n      </div>\n      <div className='mt-6 rounded-lg border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200'>\n        <strong>{t.tip}</strong> {t.wcagNote}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "website/components/DocsHeader.tsx",
    "content": "'use client';\n\nimport { SidebarTrigger } from 'fumadocs-ui/components/sidebar/base';\nimport { buttonVariants } from 'fumadocs-ui/components/ui/button';\nimport { Sidebar } from 'lucide-react';\nimport Image from 'next/image';\nimport Link from 'next/link';\n\nimport { Badge } from '@/components/ui/badge';\nimport { cn } from '@/lib/utils';\n\nimport { LanguageSwitcher } from './LanguageSwitcher';\n\nconst BASE = process.env.NEXT_PUBLIC_BASE_PATH || '';\nconst PRO_URL = 'https://pro.dayflow.studio';\n\nfunction DiscordIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      role='img'\n      viewBox='0 0 24 24'\n      xmlns='http://www.w3.org/2000/svg'\n      fill='currentColor'\n      className={className}\n    >\n      <path d='M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057c.002.022.015.043.03.056a19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z' />\n    </svg>\n  );\n}\n\nfunction GithubIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      fill='currentColor'\n      viewBox='3 3 18 18'\n      height='24'\n      aria-label='Project repository'\n      className={className}\n    >\n      <path d='M12 3C7.0275 3 3 7.12937 3 12.2276C3 16.3109 5.57625 19.7597 9.15374 20.9824C9.60374 21.0631 9.77249 20.7863 9.77249 20.5441C9.77249 20.3249 9.76125 19.5982 9.76125 18.8254C7.5 19.2522 6.915 18.2602 6.735 17.7412C6.63375 17.4759 6.19499 16.6569 5.8125 16.4378C5.4975 16.2647 5.0475 15.838 5.80124 15.8264C6.51 15.8149 7.01625 16.4954 7.18499 16.7723C7.99499 18.1679 9.28875 17.7758 9.80625 17.5335C9.885 16.9337 10.1212 16.53 10.38 16.2993C8.3775 16.0687 6.285 15.2728 6.285 11.7432C6.285 10.7397 6.63375 9.9092 7.20749 9.26326C7.1175 9.03257 6.8025 8.08674 7.2975 6.81794C7.2975 6.81794 8.05125 6.57571 9.77249 7.76377C10.4925 7.55615 11.2575 7.45234 12.0225 7.45234C12.7875 7.45234 13.5525 7.55615 14.2725 7.76377C15.9937 6.56418 16.7475 6.81794 16.7475 6.81794C17.2424 8.08674 16.9275 9.03257 16.8375 9.26326C17.4113 9.9092 17.76 10.7281 17.76 11.7432C17.76 15.2843 15.6563 16.0687 13.6537 16.2993C13.98 16.5877 14.2613 17.1414 14.2613 18.0065C14.2613 19.2407 14.25 20.2326 14.25 20.5441C14.25 20.7863 14.4188 21.0746 14.8688 20.9824C16.6554 20.364 18.2079 19.1866 19.3078 17.6162C20.4077 16.0457 20.9995 14.1611 21 12.2276C21 7.12937 16.9725 3 12 3Z' />\n    </svg>\n  );\n}\n\nconst DISCORD_URL = 'https://discord.gg/9vdFZKJqBb';\n\ninterface DocsHeaderProps {\n  githubUrl?: string;\n}\n\n/**\n * Full-width sticky header for /docs pages that mirrors the HomeLayout nav.\n * Rendered as nav.component inside DocsLayout so it has SidebarProvider context\n * (needed for SidebarTrigger). A CSS rule in global.css makes #nd-nav fixed\n * so it escapes the grid and spans the full viewport width.\n */\nexport function DocsHeader({ githubUrl }: DocsHeaderProps) {\n  return (\n    <div style={{ gridArea: 'banner', position: 'relative' }}>\n      <header id='nd-nav' className='sticky top-0 z-40 h-14'>\n        <div className='bg-fd-background/80 border-b backdrop-blur-lg'>\n          <nav className='mx-auto flex h-14 w-full max-w-(--fd-layout-width,1400px) items-center px-4'>\n            {/* Logo */}\n            <Link\n              href='/'\n              className='me-6 inline-flex items-center gap-2 text-lg font-semibold text-slate-900 dark:text-white'\n            >\n              <Image\n                src={`${BASE}/logo.png`}\n                alt='DayFlow logo'\n                width={28}\n                height={28}\n                className='h-7 w-auto'\n              />\n              DayFlow\n            </Link>\n\n            {/* Spacer */}\n            <div className='flex-1 max-lg:hidden' />\n\n            {/* Desktop right: language + discord + github */}\n            <div className='flex flex-row items-center gap-1.5 max-lg:hidden'>\n              <a\n                href={PRO_URL}\n                target='_blank'\n                rel='noopener noreferrer'\n                className={cn(\n                  buttonVariants({ color: 'ghost' }),\n                  'gap-2 px-2 text-sm font-medium text-slate-600 dark:text-slate-400'\n                )}\n              >\n                <Image\n                  src={`${BASE}/pro-logo.png`}\n                  alt='DayFlow Pro logo'\n                  width={66}\n                  height={16}\n                  className='h-6 w-auto'\n                />\n                <Badge\n                  variant='outline'\n                  className='border-amber-200 bg-amber-50 px-1.5 py-0 text-[10px] font-bold tracking-[0.16em] text-amber-700 uppercase dark:border-amber-400/30 dark:bg-amber-400/10 dark:text-amber-200'\n                >\n                  Pro\n                </Badge>\n              </a>\n              <a\n                href='https://blossom.dayflow.studio'\n                target='_blank'\n                rel='noopener noreferrer'\n                className={cn(\n                  buttonVariants({ color: 'ghost' }),\n                  'text-sm font-medium text-slate-600 dark:text-slate-400'\n                )}\n              >\n                🌸 Blossom Color Picker\n              </a>\n              <LanguageSwitcher />\n              <a\n                href={DISCORD_URL}\n                target='_blank'\n                rel='noopener noreferrer'\n                aria-label='Discord'\n                className={buttonVariants({ size: 'icon', color: 'ghost' })}\n              >\n                <DiscordIcon className='size-4' />\n              </a>\n              {githubUrl && (\n                <a\n                  href={githubUrl}\n                  target='_blank'\n                  rel='noopener noreferrer'\n                  aria-label='GitHub'\n                  className={buttonVariants({ size: 'icon', color: 'ghost' })}\n                >\n                  <GithubIcon className='size-4' />\n                </a>\n              )}\n            </div>\n\n            {/* Mobile right: language + sidebar trigger */}\n            <div className='ms-auto flex flex-row items-center gap-1 lg:hidden'>\n              <a\n                href='https://blossom.dayflow.studio'\n                target='_blank'\n                rel='noopener noreferrer'\n                className={cn(\n                  buttonVariants({ color: 'ghost', size: 'sm' }),\n                  'text-xs font-medium text-slate-600 dark:text-slate-400'\n                )}\n              >\n                🌸\n              </a>\n              <a\n                href={PRO_URL}\n                target='_blank'\n                rel='noopener noreferrer'\n                className={cn(\n                  buttonVariants({ color: 'ghost', size: 'sm' }),\n                  'gap-1 px-2 text-slate-600 dark:text-slate-400'\n                )}\n              >\n                <Image\n                  src={`${BASE}/pro-logo.png`}\n                  alt='DayFlow Pro logo'\n                  width={50}\n                  height={12}\n                  className='h-3 w-auto'\n                />\n                <Badge\n                  variant='outline'\n                  className='border-amber-200 bg-amber-50 px-1 py-0 text-[9px] font-bold tracking-[0.14em] text-amber-700 uppercase dark:border-amber-400/30 dark:bg-amber-400/10 dark:text-amber-200'\n                >\n                  Pro\n                </Badge>\n              </a>\n              <LanguageSwitcher />\n              <a\n                href={DISCORD_URL}\n                target='_blank'\n                rel='noopener noreferrer'\n                aria-label='Discord'\n                className={buttonVariants({ size: 'icon-sm', color: 'ghost' })}\n              >\n                <DiscordIcon className='size-4' />\n              </a>\n              {githubUrl && (\n                <a\n                  href={githubUrl}\n                  target='_blank'\n                  rel='noopener noreferrer'\n                  aria-label='GitHub'\n                  // className='inline-flex size-7 items-center justify-center rounded-full bg-[#24292e] text-white transition-opacity hover:opacity-80 dark:bg-white dark:text-[#24292e]'\n                >\n                  <GithubIcon className='size-3.5' />\n                </a>\n              )}\n              <SidebarTrigger\n                className={buttonVariants({ size: 'icon-sm', color: 'ghost' })}\n                aria-label='Toggle sidebar'\n              >\n                <Sidebar className='size-4' />\n              </SidebarTrigger>\n            </div>\n          </nav>\n        </div>\n      </header>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/components/DocsSearchDialog.tsx",
    "content": "'use client';\n\nimport { Charset, Document } from 'flexsearch';\nimport { useOnChange } from 'fumadocs-core/utils/use-on-change';\nimport {\n  SearchDialog,\n  SearchDialogClose,\n  SearchDialogContent,\n  SearchDialogFooter,\n  SearchDialogHeader,\n  SearchDialogIcon,\n  SearchDialogInput,\n  SearchDialogList,\n  SearchDialogOverlay,\n  TagsList,\n  TagsListItem,\n} from 'fumadocs-ui/components/dialog/search';\nimport { useI18n } from 'fumadocs-ui/contexts/i18n';\nimport { useEffect, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\n\ntype SearchLink = [name: string, href: string];\n\ntype TagItem = {\n  name: string;\n  value: string;\n};\n\ntype SearchResultItem = {\n  type: 'page' | 'heading' | 'text';\n  id: string;\n  content: string;\n  url: string;\n  breadcrumbs?: string[];\n};\n\ntype SearchDocument = SearchResultItem & {\n  page_id: string;\n  tags: string[];\n};\n\ninterface DocsSearchDialogProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  links?: SearchLink[];\n  defaultTag?: string;\n  tags?: TagItem[];\n  api?: string;\n  delayMs?: number;\n  footer?: ReactNode;\n  allowClear?: boolean;\n}\n\nfunction isCjkLocale(locale: string) {\n  return locale === 'zh' || locale === 'ja';\n}\n\nfunction createSearchDocument(locale: string) {\n  return new Document<SearchDocument>({\n    tokenize: 'full',\n    ...(isCjkLocale(locale) ? { encoder: Charset.CJK } : {}),\n    document: {\n      id: 'id',\n      index: ['content'],\n      tag: ['tags'],\n      store: true,\n    },\n  });\n}\n\ntype SearchIndexDocument = ReturnType<typeof createSearchDocument>;\n\nconst searchCache = new Map<\n  string,\n  Promise<Map<string, SearchIndexDocument>>\n>();\n\nfunction loadSearchDatabases(from = '/api/search') {\n  const cached = searchCache.get(from);\n  if (cached) return cached;\n\n  const promise = (async () => {\n    const response = await fetch(from);\n\n    if (!response.ok) {\n      throw new Error(`failed to fetch exported search indexes from ${from}`);\n    }\n\n    const data = await response.json();\n    const databases = new Map<string, SearchIndexDocument>();\n\n    if (data.type === 'i18n') {\n      for (const [locale, raw] of Object.entries(\n        data.raw as Record<string, Record<string, string>>\n      )) {\n        const document = createSearchDocument(locale);\n\n        for (const [key, value] of Object.entries(raw)) {\n          document.import(key, value);\n        }\n\n        databases.set(locale, document);\n      }\n\n      return databases;\n    }\n\n    const document = createSearchDocument('');\n\n    for (const [key, value] of Object.entries(\n      data.raw as Record<string, string>\n    )) {\n      document.import(key, value);\n    }\n\n    databases.set('', document);\n    return databases;\n  })();\n\n  searchCache.set(from, promise);\n  return promise;\n}\n\nasync function searchStaticDocs({\n  from,\n  locale,\n  query,\n  tag,\n}: {\n  from?: string;\n  locale?: string;\n  query: string;\n  tag?: string;\n}) {\n  const database = (await loadSearchDatabases(from)).get(locale ?? '');\n  if (!database) return [];\n\n  const matches = await database.searchAsync(query, {\n    index: 'content',\n    limit: 60,\n    tag: tag ? { tags: tag } : undefined,\n  });\n\n  if (matches.length === 0) return [];\n\n  const out: SearchResultItem[] = [];\n  const results = matches[0]?.result ?? [];\n  const grouped = new Map<string, SearchDocument[]>();\n\n  for (const id of results) {\n    const document = database.get(id);\n    if (!document) continue;\n\n    let list = grouped.get(document.page_id);\n    if (!list) {\n      list = [];\n      grouped.set(document.page_id, list);\n    }\n\n    if (document.type !== 'page') {\n      list.push(document);\n    }\n  }\n\n  for (const [pageId, documents] of grouped) {\n    const page = database.get(pageId);\n    if (!page) continue;\n\n    out.push({\n      id: pageId,\n      type: 'page',\n      content: page.content,\n      breadcrumbs: page.breadcrumbs,\n      url: page.url,\n    });\n\n    for (const document of documents) {\n      out.push({\n        id: document.id,\n        type: document.type,\n        content: document.content,\n        breadcrumbs: document.breadcrumbs,\n        url: document.url,\n      });\n    }\n  }\n\n  return out;\n}\n\nexport function DocsSearchDialog({\n  defaultTag,\n  tags = [],\n  api,\n  delayMs,\n  allowClear = false,\n  links = [],\n  footer,\n  ...props\n}: DocsSearchDialogProps) {\n  const { locale } = useI18n();\n  const [tag, setTag] = useState(defaultTag);\n  const [search, setSearch] = useState('');\n  const [results, setResults] = useState<SearchResultItem[] | 'empty'>('empty');\n  const [isLoading, setIsLoading] = useState(false);\n\n  const defaultItems = useMemo(() => {\n    if (links.length === 0) return null;\n\n    return links.map(([name, link]) => ({\n      type: 'page' as const,\n      id: name,\n      content: name,\n      url: link,\n    }));\n  }, [links]);\n\n  useOnChange(defaultTag, value => {\n    setTag(value);\n  });\n\n  useEffect(() => {\n    let active = true;\n    const timeoutId = window.setTimeout(async () => {\n      if (search.length === 0) {\n        if (active) {\n          setResults('empty');\n          setIsLoading(false);\n        }\n        return;\n      }\n\n      setIsLoading(true);\n\n      try {\n        const nextResults = await searchStaticDocs({\n          from: api,\n          locale,\n          query: search,\n          tag,\n        });\n\n        if (active) {\n          setResults(nextResults);\n        }\n      } finally {\n        if (active) {\n          setIsLoading(false);\n        }\n      }\n    }, delayMs ?? 100);\n\n    return () => {\n      active = false;\n      window.clearTimeout(timeoutId);\n    };\n  }, [api, delayMs, locale, search, tag]);\n\n  return (\n    <SearchDialog\n      search={search}\n      onSearchChange={setSearch}\n      isLoading={isLoading}\n      {...props}\n    >\n      <SearchDialogOverlay />\n      <SearchDialogContent>\n        <SearchDialogHeader>\n          <SearchDialogIcon />\n          <SearchDialogInput />\n          <SearchDialogClose />\n        </SearchDialogHeader>\n        <SearchDialogList\n          items={results === 'empty' ? defaultItems : results}\n        />\n      </SearchDialogContent>\n      <SearchDialogFooter>\n        {tags.length > 0 && (\n          <TagsList tag={tag} onTagChange={setTag} allowClear={allowClear}>\n            {tags.map(item => (\n              <TagsListItem key={item.value} value={item.value}>\n                {item.name}\n              </TagsListItem>\n            ))}\n          </TagsList>\n        )}\n        {footer}\n      </SearchDialogFooter>\n    </SearchDialog>\n  );\n}\n"
  },
  {
    "path": "website/components/FrameworkInstall.tsx",
    "content": "'use client';\n\nimport { Tab, Tabs } from 'fumadocs-ui/components/tabs';\nimport React, { useState } from 'react';\n\nconst frameworks = [\n  {\n    id: 'react',\n    name: 'React',\n    package: '@dayflow/react',\n    color: '#61DAFB',\n    icon: (\n      <svg viewBox='-11.5 -10.23177 23 20.46354' className='h-5 w-5'>\n        <circle cx='0' cy='0' r='2.05' fill='#61DAFB' />\n        <g stroke='#61DAFB' strokeWidth='1' fill='none'>\n          <ellipse rx='11' ry='4.2' />\n          <ellipse rx='11' ry='4.2' transform='rotate(60)' />\n          <ellipse rx='11' ry='4.2' transform='rotate(120)' />\n        </g>\n      </svg>\n    ),\n  },\n  {\n    id: 'vue',\n    name: 'Vue',\n    package: '@dayflow/vue',\n    color: '#41B883',\n    icon: (\n      <svg viewBox='0 0 256 221' className='h-5 w-5'>\n        <path\n          fill='#41B883'\n          d='M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z'\n        />\n        <path\n          fill='#35495E'\n          d='M0 0l128 220.8L256 0h-51.2L128 132.48L54.4 0H0Z'\n        />\n        <path fill='#41B883' d='M97.92 0L128 51.2L158.08 0h-60.16Z' />\n      </svg>\n    ),\n  },\n  {\n    id: 'angular',\n    name: 'Angular',\n    package: '@dayflow/angular',\n    color: '#DD0031',\n    icon: (\n      <svg viewBox='0 0 250 250' className='h-5 w-5'>\n        <path\n          fill='#DD0031'\n          d='M125 30L31.9 63.2l14.2 123.1L125 230l78.9-43.7 14.2-123.1z'\n        />\n        <path\n          fill='#C3002F'\n          d='M125 30v22.2l76 2.7l-13.9 120.4L125 218.4v11.6l78.9-43.7 14.2-123.1z'\n        />\n        <path\n          fill='#FFF'\n          d='M125 52.1L66.8 182.6h21.7l11.7-29.2h49.4l11.7 29.2h21.8L125 52.1zm24.6 101.2h-49.2L125 94.2l24.6 59.1z'\n        />\n      </svg>\n    ),\n  },\n  {\n    id: 'svelte',\n    name: 'Svelte',\n    package: '@dayflow/svelte',\n    color: '#FF3E00',\n    icon: (\n      <svg viewBox='0 0 98.1 118' className='h-5 w-5'>\n        <path\n          fill='#FF3E00'\n          d='M91.8 15.6C80.9-.1 59.2-4.7 43.6 5.2L16.1 22.8C8.6 27.5 3.4 35.2 1.9 43.9c-1.3 7.3-.2 14.8 3.3 21.3-2.4 3.6-4 7.6-4.7 11.8-1.6 8.9.5 18.1 5.7 25.4 11 15.7 32.6 20.3 48.2 10.4l27.5-17.5c7.5-4.7 12.7-12.4 14.2-21.1 1.3-7.3.2-14.8-3.3-21.3 2.4-3.6 4-7.6 4.7-11.8 1.7-8.9-.4-18.1-5.7-25.5'\n        />\n        <path\n          fill='#FFF'\n          d='M40.9 103.9c-8.9 2.3-18.2-1.2-23.4-8.7-3.2-4.4-4.4-9.9-3.5-15.3.2-.9.4-1.7.6-2.6l.5-1.6 1.4 1c3.3 2.4 6.9 4.2 10.8 5.4l1 .3-.1 1c-.1 1.4.3 2.9 1.1 4.1 1.6 2.3 4.4 3.4 7.1 2.7.6-.2 1.2-.4 1.7-.7L65.5 72c1.4-.9 2.3-2.2 2.6-3.8.3-1.6-.1-3.3-1-4.6-1.6-2.3-4.4-3.3-7.1-2.6-.6.2-1.2.4-1.7.7l-10.5 6.7c-1.7 1.1-3.6 1.9-5.6 2.4-8.9 2.3-18.2-1.2-23.4-8.7-3.1-4.4-4.4-9.9-3.4-15.3.9-5.2 4.1-9.9 8.6-12.7l27.5-17.5c1.7-1.1 3.6-1.9 5.6-2.5 8.9-2.3 18.2 1.2 23.4 8.7 3.2 4.4 4.4 9.9 3.5 15.3-.2.9-.4 1.7-.7 2.6l-.5 1.6-1.4-1c-3.3-2.4-6.9-4.2-10.8-5.4l-1-.3.1-1c.1-1.4-.3-2.9-1.1-4.1-1.6-2.3-4.4-3.3-7.1-2.6-.6.2-1.2.4-1.7.7L32.4 46.1c-1.4.9-2.3 2.2-2.6 3.8s.1 3.3 1 4.6c1.6 2.3 4.4 3.3 7.1 2.6.6-.2 1.2-.4 1.7-.7l10.5-6.7c1.7-1.1 3.6-1.9 5.6-2.5 8.9-2.3 18.2 1.2 23.4 8.7 3.2 4.4 4.4 9.9 3.5 15.3-.9 5.2-4.1 9.9-8.6 12.7L47.2 101.5c-1.8 1.1-3.7 1.9-5.7 2.5h-.6z'\n        />\n      </svg>\n    ),\n  },\n];\n\nconst InstallCommand = ({ cmd, pkg }: { cmd: string; pkg: string }) => {\n  const [copied, setCopied] = useState(false);\n  const command = `${cmd} ${pkg} @dayflow/core`;\n\n  const copy = () => {\n    navigator.clipboard.writeText(command);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  };\n\n  return (\n    <div className='group relative mt-4'>\n      <div className='overflow-x-auto rounded-lg bg-[#f3f7fe] p-4 text-sm font-medium whitespace-pre dark:bg-zinc-800'>\n        {command}\n      </div>\n      <button\n        type='button'\n        onClick={copy}\n        className='absolute top-2 right-2 rounded-md border border-border bg-white p-2 opacity-0 transition-opacity group-hover:opacity-100 dark:bg-zinc-700'\n        aria-label='Copy to clipboard'\n      >\n        {copied ? (\n          <svg\n            viewBox='0 0 24 24'\n            className='h-4 w-4 text-green-500'\n            fill='none'\n            stroke='currentColor'\n            strokeWidth='2'\n          >\n            <polyline points='20 6 9 17 4 12' />\n          </svg>\n        ) : (\n          <svg\n            viewBox='0 0 24 24'\n            className='h-4 w-4 text-muted-foreground'\n            fill='none'\n            stroke='currentColor'\n            strokeWidth='2'\n          >\n            <rect x='9' y='9' width='13' height='13' rx='2' ry='2' />\n            <path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' />\n          </svg>\n        )}\n      </button>\n    </div>\n  );\n};\n\nconst SimpleCommand = ({ cmd, pkg }: { cmd: string; pkg: string }) => {\n  const [copied, setCopied] = useState(false);\n  const command = `${cmd} ${pkg}`;\n\n  const copy = () => {\n    navigator.clipboard.writeText(command);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  };\n\n  return (\n    <div className='group relative mt-4'>\n      <div className='overflow-x-auto rounded-lg bg-[#f3f7fe] p-4 text-sm font-medium whitespace-pre dark:bg-zinc-800'>\n        {command}\n      </div>\n      <button\n        type='button'\n        onClick={copy}\n        className='absolute top-2 right-2 rounded-md border border-border bg-white p-2 opacity-0 transition-opacity group-hover:opacity-100 dark:bg-zinc-700'\n        aria-label='Copy to clipboard'\n      >\n        {copied ? (\n          <svg\n            viewBox='0 0 24 24'\n            className='h-4 w-4 text-green-500'\n            fill='none'\n            stroke='currentColor'\n            strokeWidth='2'\n          >\n            <polyline points='20 6 9 17 4 12' />\n          </svg>\n        ) : (\n          <svg\n            viewBox='0 0 24 24'\n            className='h-4 w-4 text-muted-foreground'\n            fill='none'\n            stroke='currentColor'\n            strokeWidth='2'\n          >\n            <rect x='9' y='9' width='13' height='13' rx='2' ry='2' />\n            <path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' />\n          </svg>\n        )}\n      </button>\n    </div>\n  );\n};\n\nconst CREATE_COMMANDS: Record<string, string> = {\n  npm: 'npm create dayflow@latest',\n  pnpm: 'pnpm create dayflow@latest',\n  yarn: 'yarn create dayflow',\n  bun: 'bun create dayflow@latest',\n};\n\nconst CreateCommand = ({ command }: { command: string }) => {\n  const [copied, setCopied] = useState(false);\n\n  const copy = () => {\n    navigator.clipboard.writeText(command);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  };\n\n  return (\n    <div className='group relative mt-4'>\n      <div className='overflow-x-auto rounded-lg bg-[#f3f7fe] p-4 text-sm font-medium whitespace-pre dark:bg-zinc-800'>\n        {command}\n      </div>\n      <button\n        type='button'\n        onClick={copy}\n        className='absolute top-2 right-2 rounded-md border border-border bg-white p-2 opacity-0 transition-opacity group-hover:opacity-100 dark:bg-zinc-700'\n        aria-label='Copy to clipboard'\n      >\n        {copied ? (\n          <svg\n            viewBox='0 0 24 24'\n            className='h-4 w-4 text-green-500'\n            fill='none'\n            stroke='currentColor'\n            strokeWidth='2'\n          >\n            <polyline points='20 6 9 17 4 12' />\n          </svg>\n        ) : (\n          <svg\n            viewBox='0 0 24 24'\n            className='h-4 w-4 text-muted-foreground'\n            fill='none'\n            stroke='currentColor'\n            strokeWidth='2'\n          >\n            <rect x='9' y='9' width='13' height='13' rx='2' ry='2' />\n            <path d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' />\n          </svg>\n        )}\n      </button>\n    </div>\n  );\n};\n\nexport function CreateDayflowTabs() {\n  return (\n    <div className='my-4'>\n      <Tabs items={['npm', 'pnpm', 'yarn', 'bun']}>\n        {(['npm', 'pnpm', 'yarn', 'bun'] as const).map(pm => (\n          <Tab key={pm} value={pm}>\n            <CreateCommand command={CREATE_COMMANDS[pm]} />\n          </Tab>\n        ))}\n      </Tabs>\n    </div>\n  );\n}\n\nexport function PackageTabs({ pkg }: { pkg: string }) {\n  return (\n    <div className='my-4'>\n      <Tabs items={['npm', 'pnpm', 'yarn', 'bun']}>\n        <Tab value='npm'>\n          <SimpleCommand cmd='npm install' pkg={pkg} />\n        </Tab>\n        <Tab value='pnpm'>\n          <SimpleCommand cmd='pnpm add' pkg={pkg} />\n        </Tab>\n        <Tab value='yarn'>\n          <SimpleCommand cmd='yarn add' pkg={pkg} />\n        </Tab>\n        <Tab value='bun'>\n          <SimpleCommand cmd='bun add' pkg={pkg} />\n        </Tab>\n      </Tabs>\n    </div>\n  );\n}\n\nexport function FrameworkInstall() {\n  const [activeFramework, setActiveFramework] = useState(frameworks[0]);\n\n  return (\n    <div className='my-6 overflow-hidden rounded-xl border border-border bg-background'>\n      <div className='flex flex-wrap gap-2 border-b border-border bg-muted/20 p-4'>\n        {frameworks.map(fw => (\n          <button\n            type='button'\n            key={fw.id}\n            onClick={() => setActiveFramework(fw)}\n            className={`flex items-center gap-2 rounded-lg border px-4 py-2 transition-all ${\n              activeFramework.id === fw.id\n                ? 'border-[#bfdbfe] bg-[#e5effe] shadow-sm dark:border-[#1e3a5f] dark:bg-[#0d2137]'\n                : 'border-transparent bg-transparent hover:bg-muted dark:hover:bg-zinc-800'\n            }`}\n          >\n            {fw.icon}\n            <span className='text-sm font-bold text-black dark:text-white'>\n              {fw.name}\n            </span>\n          </button>\n        ))}\n      </div>\n      <div className='p-4'>\n        <Tabs items={['npm', 'pnpm', 'yarn', 'bun']}>\n          <Tab value='npm'>\n            <InstallCommand cmd='npm install' pkg={activeFramework.package} />\n          </Tab>\n          <Tab value='pnpm'>\n            <InstallCommand cmd='pnpm add' pkg={activeFramework.package} />\n          </Tab>\n          <Tab value='yarn'>\n            <InstallCommand cmd='yarn add' pkg={activeFramework.package} />\n          </Tab>\n          <Tab value='bun'>\n            <InstallCommand cmd='bun add' pkg={activeFramework.package} />\n          </Tab>\n        </Tabs>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/components/FrameworkTabs.tsx",
    "content": "'use client';\n\nimport {\n  Tabs,\n  TabsList,\n  TabsTrigger,\n  TabsContent,\n} from 'fumadocs-ui/components/tabs';\nimport React from 'react';\n\nconst frameworks = [\n  {\n    id: 'react',\n    name: 'React',\n    icon: (\n      <svg viewBox='-11.5 -10.23177 23 20.46354' className='h-4 w-4'>\n        <circle cx='0' cy='0' r='2.05' fill='#61DAFB' />\n        <g stroke='#61DAFB' strokeWidth='1' fill='none'>\n          <ellipse rx='11' ry='4.2' />\n          <ellipse rx='11' ry='4.2' transform='rotate(60)' />\n          <ellipse rx='11' ry='4.2' transform='rotate(120)' />\n        </g>\n      </svg>\n    ),\n  },\n  {\n    id: 'vue',\n    name: 'Vue',\n    icon: (\n      <svg viewBox='0 0 256 221' className='h-4 w-4'>\n        <path\n          fill='#41B883'\n          d='M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z'\n        />\n        <path\n          fill='#35495E'\n          d='M0 0l128 220.8L256 0h-51.2L128 132.48L54.4 0H0Z'\n        />\n        <path fill='#41B883' d='M97.92 0L128 51.2L158.08 0h-60.16Z' />\n      </svg>\n    ),\n  },\n  {\n    id: 'angular',\n    name: 'Angular',\n    icon: (\n      <svg viewBox='0 0 250 250' className='h-4 w-4'>\n        <path\n          fill='#DD0031'\n          d='M125 30L31.9 63.2l14.2 123.1L125 230l78.9-43.7 14.2-123.1z'\n        />\n        <path\n          fill='#C3002F'\n          d='M125 30v22.2l76 2.7l-13.9 120.4L125 218.4v11.6l78.9-43.7 14.2-123.1z'\n        />\n        <path\n          fill='#FFF'\n          d='M125 52.1L66.8 182.6h21.7l11.7-29.2h49.4l11.7 29.2h21.8L125 52.1zm24.6 101.2h-49.2L125 94.2l24.6 59.1z'\n        />\n      </svg>\n    ),\n  },\n  {\n    id: 'svelte',\n    name: 'Svelte',\n    icon: (\n      <svg viewBox='0 0 98.1 118' className='h-4 w-4'>\n        <path\n          fill='#FF3E00'\n          d='M91.8 15.6C80.9-.1 59.2-4.7 43.6 5.2L16.1 22.8C8.6 27.5 3.4 35.2 1.9 43.9c-1.3 7.3-.2 14.8 3.3 21.3-2.4 3.6-4 7.6-4.7 11.8-1.6 8.9.5 18.1 5.7 25.4 11 15.7 32.6 20.3 48.2 10.4l27.5-17.5c7.5-4.7 12.7-12.4 14.2-21.1 1.3-7.3.2-14.8-3.3-21.3 2.4-3.6 4-7.6 4.7-11.8 1.7-8.9-.4-18.1-5.7-25.5'\n        />\n        <path\n          fill='#FFF'\n          d='M40.9 103.9c-8.9 2.3-18.2-1.2-23.4-8.7-3.2-4.4-4.4-9.9-3.5-15.3.2-.9.4-1.7.6-2.6l.5-1.6 1.4 1c3.3 2.4 6.9 4.2 10.8 5.4l1 .3-.1 1c-.1 1.4.3 2.9 1.1 4.1 1.6 2.3 4.4 3.4 7.1 2.7.6-.2 1.2-.4 1.7-.7L65.5 72c1.4-.9 2.3-2.2 2.6-3.8.3-1.6-.1-3.3-1-4.6-1.6-2.3-4.4-3.3-7.1-2.6-.6.2-1.2.4-1.7.7l-10.5 6.7c-1.7 1.1-3.6 1.9-5.6 2.4-8.9 2.3-18.2-1.2-23.4-8.7-3.1-4.4-4.4-9.9-3.4-15.3.9-5.2 4.1-9.9 8.6-12.7l27.5-17.5c1.7-1.1 3.6-1.9 5.6-2.5 8.9-2.3 18.2 1.2 23.4 8.7 3.2 4.4 4.4 9.9 3.5 15.3-.2.9-.4 1.7-.7 2.6l-.5 1.6-1.4-1c-3.3-2.4-6.9-4.2-10.8-5.4l-1-.3.1-1c.1-1.4-.3-2.9-1.1-4.1-1.6-2.3-4.4-3.3-7.1-2.6-.6.2-1.2.4-1.7.7L32.4 46.1c-1.4.9-2.3 2.2-2.6 3.8s.1 3.3 1 4.6c1.6 2.3 4.4 3.3 7.1 2.6.6-.2 1.2-.4 1.7-.7l10.5-6.7c1.7-1.1 3.6-1.9 5.6-2.5 8.9-2.3 18.2 1.2 23.4 8.7 3.2 4.4 4.4 9.9 3.5 15.3-.9 5.2-4.1 9.9-8.6 12.7L47.2 101.5c-1.8 1.1-3.7 1.9-5.7 2.5h-.6z'\n        />\n      </svg>\n    ),\n  },\n];\n\nexport function FrameworkTabs({ children }: { children: React.ReactNode }) {\n  const childrenArray = React.Children.toArray(children);\n\n  return (\n    <Tabs defaultValue='React'>\n      <TabsList>\n        {frameworks.map(fw => (\n          <TabsTrigger\n            key={fw.id}\n            value={fw.name}\n            className='text-black dark:text-white'\n          >\n            {fw.icon}\n            {fw.name}\n          </TabsTrigger>\n        ))}\n      </TabsList>\n      {frameworks.map((fw, i) => (\n        <TabsContent key={fw.id} value={fw.name}>\n          {childrenArray[i]}\n        </TabsContent>\n      ))}\n    </Tabs>\n  );\n}\n\n// Simple Tab wrapper used in MDX content\nexport function Tab({\n  children,\n}: {\n  children: React.ReactNode;\n}): React.ReactNode {\n  return children;\n}\n"
  },
  {
    "path": "website/components/LanguageSwitcher.tsx",
    "content": "'use client';\n\nimport { buttonVariants } from 'fumadocs-ui/components/ui/button';\nimport { Languages } from 'lucide-react';\nimport { usePathname } from 'next/navigation';\nimport { useEffect, useRef, useState } from 'react';\n\nimport { getLanguageCodeFromPathname, languages } from '@/lib/i18n';\nimport type { LanguageCode } from '@/lib/i18n';\nimport { BASE_PATH } from '@/lib/site';\n\nfunction switchTo(newLocale: LanguageCode, currentPath: string) {\n  const current = languages.find(\n    language => language.code === getLanguageCodeFromPathname(currentPath)\n  );\n  const next = languages.find(language => language.code === newLocale);\n  if (!next) return;\n\n  const isDocsPath = languages.some(\n    language =>\n      currentPath === language.prefix ||\n      currentPath.startsWith(`${language.prefix}/`)\n  );\n\n  if (!isDocsPath) {\n    localStorage.setItem('dayflow-locale', newLocale);\n    window.location.href = BASE_PATH + `${next.prefix}/introduction`;\n    return;\n  }\n\n  // strip current locale prefix\n  let contentPath = currentPath;\n  if (current && currentPath.startsWith(current.prefix)) {\n    contentPath = currentPath.slice(current.prefix.length) || '/';\n  }\n  if (!contentPath.startsWith('/')) contentPath = '/' + contentPath;\n\n  let newPath = next.prefix + contentPath;\n  // avoid trailing slash for root\n  if (contentPath === '/') newPath = next.prefix;\n\n  localStorage.setItem('dayflow-locale', newLocale);\n  window.location.href = BASE_PATH + newPath;\n}\n\nexport function LanguageSwitcher() {\n  const pathname = usePathname();\n  const [open, setOpen] = useState(false);\n  const ref = useRef<HTMLDivElement>(null);\n  const currentLocale = getLanguageCodeFromPathname(pathname);\n\n  // close on outside click\n  useEffect(() => {\n    function handler(e: MouseEvent) {\n      if (ref.current && !ref.current.contains(e.target as Node))\n        setOpen(false);\n    }\n    if (open) document.addEventListener('mousedown', handler);\n    return () => document.removeEventListener('mousedown', handler);\n  }, [open]);\n\n  return (\n    <div ref={ref} className='relative'>\n      <button\n        type='button'\n        onClick={() => setOpen(v => !v)}\n        aria-label='Switch language'\n        aria-expanded={open}\n        className={buttonVariants({ size: 'icon-sm', color: 'ghost' })}\n      >\n        <Languages className='size-4' />\n      </button>\n\n      {open && (\n        <div className='bg-fd-background absolute inset-e-0 top-full z-50 mt-1 w-36 rounded-lg border p-1 shadow-md'>\n          {languages.map(language => (\n            <button\n              key={language.code}\n              type='button'\n              onClick={() => {\n                setOpen(false);\n                switchTo(language.code, pathname);\n              }}\n              className={`hover:bg-fd-accent hover:text-fd-accent-foreground flex w-full items-center justify-between rounded px-2 py-1 text-sm transition-colors ${\n                currentLocale === language.code\n                  ? 'text-fd-primary font-medium'\n                  : 'text-fd-muted-foreground'\n              }`}\n            >\n              {language.name}\n              {currentLocale === language.code && (\n                <svg\n                  className='size-3.5'\n                  fill='currentColor'\n                  viewBox='0 0 20 20'\n                >\n                  <path\n                    fillRule='evenodd'\n                    d='M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z'\n                    clipRule='evenodd'\n                  />\n                </svg>\n              )}\n            </button>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/components/ai/page-actions.tsx",
    "content": "'use client';\nimport { buttonVariants } from 'fumadocs-ui/components/ui/button';\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from 'fumadocs-ui/components/ui/popover';\nimport { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';\nimport { Check, ChevronDown, Copy, ExternalLinkIcon } from 'lucide-react';\nimport { useMemo, useState } from 'react';\n\nimport { cn } from '@/lib/cn';\n\nconst cache = new Map<string, string>();\n\nexport function LLMCopyButton({\n  /**\n   * A URL to fetch the raw Markdown/MDX content of page\n   */\n  markdownUrl,\n}: {\n  markdownUrl: string;\n}) {\n  const [isLoading, setLoading] = useState(false);\n  const [checked, onClick] = useCopyButton(async () => {\n    const cached = cache.get(markdownUrl);\n    if (cached) return navigator.clipboard.writeText(cached);\n\n    setLoading(true);\n\n    try {\n      await navigator.clipboard.write([\n        new ClipboardItem({\n          'text/plain': fetch(markdownUrl).then(async res => {\n            const content = await res.text();\n            cache.set(markdownUrl, content);\n\n            return content;\n          }),\n        }),\n      ]);\n    } finally {\n      setLoading(false);\n    }\n  });\n\n  return (\n    <button\n      type='button'\n      disabled={isLoading}\n      className={cn(\n        buttonVariants({\n          color: 'secondary',\n          size: 'sm',\n          className: 'gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground',\n        })\n      )}\n      onClick={onClick}\n    >\n      {checked ? <Check /> : <Copy />}\n      Copy Markdown\n    </button>\n  );\n}\n\nexport function ViewOptions({\n  markdownUrl,\n  githubUrl,\n}: {\n  /**\n   * A URL to the raw Markdown/MDX content of page\n   */\n  markdownUrl: string;\n\n  /**\n   * Source file URL on GitHub\n   */\n  githubUrl: string;\n}) {\n  const items = useMemo(() => {\n    const fullMarkdownUrl =\n      typeof window === 'undefined'\n        ? 'loading'\n        : new URL(markdownUrl, window.location.origin);\n    const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`;\n\n    return [\n      {\n        title: 'Open in GitHub',\n        href: githubUrl,\n        icon: (\n          <svg fill='currentColor' role='img' viewBox='0 0 24 24'>\n            <title>GitHub</title>\n            <path d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12' />\n          </svg>\n        ),\n      },\n      {\n        title: 'Open in Scira AI',\n        href: `https://scira.ai/?${new URLSearchParams({\n          q,\n        })}`,\n        icon: (\n          <svg\n            width='910'\n            height='934'\n            viewBox='0 0 910 934'\n            fill='none'\n            xmlns='http://www.w3.org/2000/svg'\n          >\n            <title>Scira AI</title>\n            <path\n              d='M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z'\n              fill='currentColor'\n              stroke='currentColor'\n              strokeWidth='8'\n              strokeLinejoin='round'\n            />\n            <path\n              d='M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z'\n              fill='currentColor'\n              stroke='currentColor'\n              strokeWidth='8'\n              strokeLinejoin='round'\n            />\n            <path\n              d='M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z'\n              stroke='currentColor'\n              strokeWidth='20'\n              strokeLinejoin='round'\n            />\n            <path\n              d='M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z'\n              stroke='currentColor'\n              strokeWidth='20'\n              strokeLinejoin='round'\n            />\n            <path\n              d='M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z'\n              fill='currentColor'\n              stroke='currentColor'\n              strokeWidth='8'\n              strokeLinejoin='round'\n            />\n            <path\n              d='M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z'\n              fill='currentColor'\n              stroke='currentColor'\n              strokeWidth='8'\n              strokeLinejoin='round'\n            />\n            <path\n              d='M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442'\n              stroke='currentColor'\n              strokeWidth='30'\n              strokeLinecap='round'\n              strokeLinejoin='round'\n            />\n          </svg>\n        ),\n      },\n      {\n        title: 'Open in ChatGPT',\n        href: `https://chatgpt.com/?${new URLSearchParams({\n          hints: 'search',\n          q,\n        })}`,\n        icon: (\n          <svg\n            role='img'\n            viewBox='0 0 24 24'\n            fill='currentColor'\n            xmlns='http://www.w3.org/2000/svg'\n          >\n            <title>OpenAI</title>\n            <path d='M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z' />\n          </svg>\n        ),\n      },\n      {\n        title: 'Open in Claude',\n        href: `https://claude.ai/new?${new URLSearchParams({\n          q,\n        })}`,\n        icon: (\n          <svg\n            fill='currentColor'\n            role='img'\n            viewBox='0 0 24 24'\n            xmlns='http://www.w3.org/2000/svg'\n          >\n            <title>Anthropic</title>\n            <path d='M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z' />\n          </svg>\n        ),\n      },\n      {\n        title: 'Open in Cursor',\n        icon: (\n          <svg\n            fill='currentColor'\n            role='img'\n            viewBox='0 0 24 24'\n            xmlns='http://www.w3.org/2000/svg'\n          >\n            <title>Cursor</title>\n            <path d='M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23' />\n          </svg>\n        ),\n        href: `https://cursor.com/link/prompt?${new URLSearchParams({\n          text: q,\n        })}`,\n      },\n    ];\n  }, [githubUrl, markdownUrl]);\n\n  return (\n    <Popover>\n      <PopoverTrigger\n        className={cn(\n          buttonVariants({\n            color: 'secondary',\n            size: 'sm',\n            className: 'gap-2',\n          })\n        )}\n      >\n        Open\n        <ChevronDown className='text-fd-muted-foreground size-3.5' />\n      </PopoverTrigger>\n      <PopoverContent className='flex flex-col'>\n        {items.map(item => (\n          <a\n            key={item.href}\n            href={item.href}\n            rel='noreferrer noopener'\n            target='_blank'\n            className='hover:text-fd-accent-foreground hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg p-2 text-sm [&_svg]:size-4'\n          >\n            {item.icon}\n            {item.title}\n            <ExternalLinkIcon className='text-fd-muted-foreground ms-auto size-3.5' />\n          </a>\n        ))}\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "website/components/showcase/ColorPickerShowcase.tsx",
    "content": "'use client';\n\nimport {\n  ColorPickerProps,\n  CreateCalendarDialogColorPickerProps,\n} from '@dayflow/core';\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react';\nimport React, { useMemo } from 'react';\nimport { SketchPicker, PhotoshopPicker } from 'react-color';\n\nimport { getWebsiteCalendars } from '@/utils/palette';\nimport { generateMinimalSampleEvents } from '@/utils/sampleData';\n\nexport function ColorPickerShowcase() {\n  const events = useMemo(() => generateMinimalSampleEvents(), []);\n  const calendars = useMemo(() => getWebsiteCalendars(), []);\n\n  const calendar = useCalendarApp({\n    views: [createMonthView(), createWeekView()],\n    plugins: [\n      createSidebarPlugin({\n        createCalendarMode: 'modal',\n      }),\n    ],\n    defaultView: ViewType.WEEK,\n    events,\n    calendars,\n    initialDate: new Date(),\n  });\n\n  return (\n    <div className='not-prose w-full overflow-hidden rounded-lg p-1'>\n      <DayFlowCalendar\n        calendar={calendar}\n        colorPicker={(args: ColorPickerProps) => (\n          <div className='relative'>\n            <div className='absolute top-0 left-0 z-9999'>\n              <SketchPicker\n                color={args.color}\n                onChange={color => args.onChange({ hex: color.hex })}\n                width='220px'\n              />\n            </div>\n          </div>\n        )}\n        createCalendarDialogColorPicker={(\n          args: CreateCalendarDialogColorPickerProps\n        ) => (\n          <div className='flex items-center justify-center p-4'>\n            <PhotoshopPicker\n              color={args.color}\n              onChange={color => args.onChange({ hex: color.hex })}\n              onAccept={() => args.onAccept?.()}\n              onCancel={() => args.onCancel?.()}\n            />\n          </div>\n        )}\n      />\n    </div>\n  );\n}\n\nexport default ColorPickerShowcase;\n"
  },
  {
    "path": "website/components/showcase/ContextMenuShowcase.tsx",
    "content": "'use client';\n\nimport '@dayflow/core/dist/styles.components.css';\nimport {\n  EventContextMenuSlotArgs,\n  GridContextMenuSlotArgs,\n  createDayView,\n  createWeekView,\n  createMonthView,\n  createYearView,\n} from '@dayflow/core';\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport { useCalendarApp, DayFlowCalendar, ViewType } from '@dayflow/react';\nimport { useTheme } from 'next-themes';\nimport React, { useCallback, useMemo, useState } from 'react';\n\nimport { getWebsiteCalendars } from '@/utils/palette';\nimport { generateMinimalSampleEvents } from '@/utils/sampleData';\n\n/* ── tiny reusable menu primitives ──────────────────────────── */\n\nconst MenuRoot = ({ children }: { children: React.ReactNode }) => (\n  <div className='flex min-w-40 flex-col bg-white py-1 dark:bg-slate-900'>\n    {children}\n  </div>\n);\n\nconst MenuItem = ({\n  label,\n  danger,\n  disabled,\n  onClick,\n}: {\n  label: string;\n  danger?: boolean;\n  disabled?: boolean;\n  onClick?: () => void;\n}) => (\n  <button\n    type='button'\n    disabled={disabled}\n    onClick={onClick}\n    className={`flex w-full items-center rounded px-3 py-1.5 text-left text-sm transition-colors ${danger ? 'text-red-500 hover:bg-red-50 dark:hover:bg-red-900/30' : 'text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800'} ${disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'}`}\n  >\n    {label}\n  </button>\n);\n\nconst MenuSeparator = () => (\n  <div className='my-1 border-t border-slate-100 dark:border-slate-800' />\n);\n\n/* ── Showcase ───────────────────────────────────────────────── */\n\nexport const ContextMenuShowcase: React.FC = () => {\n  const { resolvedTheme } = useTheme();\n  const [events] = useState(() => generateMinimalSampleEvents());\n  const [log, setLog] = useState<string | null>(null);\n\n  const themeMode = useMemo(() => {\n    if (resolvedTheme === 'dark') return 'dark';\n    if (resolvedTheme === 'light') return 'light';\n    return 'auto';\n  }, [resolvedTheme]);\n\n  const calendar = useCalendarApp({\n    views: [\n      createDayView(),\n      createWeekView(),\n      createMonthView(),\n      createYearView(),\n    ],\n    plugins: [createDragPlugin()],\n    events,\n    calendars: getWebsiteCalendars(),\n    defaultCalendar: 'work',\n    defaultView: ViewType.WEEK,\n    theme: { mode: themeMode },\n  });\n\n  /* custom event right-click menu */\n  const eventContextMenu = useCallback(\n    ({ event, onClose }: EventContextMenuSlotArgs) => {\n      const notify = (action: string) => {\n        setLog(`${action}: \"${event.title}\"`);\n        onClose();\n      };\n\n      return (\n        <MenuRoot>\n          <MenuItem label='Edit' onClick={() => notify('Edit')} />\n          <MenuItem label='Duplicate' onClick={() => notify('Duplicate')} />\n          <MenuItem label='Copy link' onClick={() => notify('Copy link')} />\n          <MenuSeparator />\n          <MenuItem\n            label='Delete'\n            danger\n            onClick={() => {\n              calendar.deleteEvent(event.id);\n              onClose();\n            }}\n          />\n        </MenuRoot>\n      );\n    },\n    [calendar]\n  );\n\n  /* custom grid/cell right-click menu */\n  const gridContextMenu = useCallback(\n    ({ date, onClose }: GridContextMenuSlotArgs) => {\n      const notify = (action: string) => {\n        setLog(\n          `${action} at ${date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })}`\n        );\n        onClose();\n      };\n\n      return (\n        <MenuRoot>\n          <MenuItem\n            label='New event here'\n            onClick={() => notify('New event')}\n          />\n          <MenuItem label='Set reminder' onClick={() => notify('Reminder')} />\n          <MenuSeparator />\n          <MenuItem label='Mark as busy' onClick={() => notify('Busy')} />\n        </MenuRoot>\n      );\n    },\n    []\n  );\n\n  return (\n    <div className='not-prose space-y-2 p-1'>\n      {log && (\n        <div className='rounded-md border border-indigo-200 bg-indigo-50 px-3 py-2 text-sm text-indigo-700 dark:border-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300'>\n          Action: {log}\n        </div>\n      )}\n      <DayFlowCalendar\n        calendar={calendar}\n        eventContextMenu={eventContextMenu}\n        gridContextMenu={gridContextMenu}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "website/components/showcase/CustomDetailDialogShowcase.tsx",
    "content": "'use client';\n\nimport {\n  Event,\n  CalendarType,\n  EventDetailDialogProps,\n  createYearView,\n} from '@dayflow/core';\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  createDayView,\n  ViewType,\n} from '@dayflow/react';\nimport {\n  CalendarDays,\n  Clock3,\n  MapPin,\n  Star,\n  StarOff,\n  User,\n  X,\n} from 'lucide-react';\nimport { useTheme } from 'next-themes';\nimport React, { useMemo, useCallback } from 'react';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { CALENDAR_SIDE_PANEL, getWebsiteCalendars } from '@/utils/palette';\nimport { generateMinimalSampleEvents } from '@/utils/sampleData';\n\nimport '@dayflow/core/dist/styles.components.css';\n\nconst cloneCalendarTypes = (): CalendarType[] => getWebsiteCalendars();\n\nconst COLOR_PRESETS = CALENDAR_SIDE_PANEL.map(item => ({\n  id: item.id,\n  color: item.color,\n  label: item.name,\n}));\n\nconst META_PRESETS = [\n  {\n    owner: 'Alice',\n    location: 'HQ West · Room 301',\n    attendees: ['Brian', 'Chiara', 'Diego'],\n    favorite: true,\n  },\n  {\n    owner: 'Brian',\n    location: 'Zoom',\n    attendees: ['Alice', 'Product Design'],\n    favorite: false,\n  },\n  {\n    owner: 'Chiara',\n    location: 'Local Bistro',\n    attendees: ['Ops', 'Marketing'],\n    favorite: false,\n  },\n  {\n    owner: 'Diego',\n    location: 'Online broadcast',\n    attendees: ['Stakeholders', 'Engineering'],\n    favorite: false,\n  },\n  {\n    owner: 'Mina',\n    location: 'HQ East · Innovation Lab',\n    attendees: ['Research', 'Growth'],\n    favorite: true,\n  },\n  {\n    owner: 'Noah',\n    location: 'Client HQ',\n    attendees: ['Customer team', 'Support'],\n    favorite: false,\n  },\n];\n\nconst enrichEventsWithMeta = (sourceEvents: Event[]): Event[] =>\n  sourceEvents.map((event, index) => {\n    const preset = META_PRESETS[index % META_PRESETS.length];\n    return {\n      ...event,\n      meta: {\n        ...event.meta,\n        ...preset,\n        favorite:\n          typeof preset.favorite === 'boolean'\n            ? preset.favorite\n            : index % 4 === 0,\n      },\n    };\n  });\n\nconst useDemoCalendar = ({\n  useEventDetailDialog = false,\n}: {\n  useEventDetailDialog?: boolean;\n}) => {\n  const { resolvedTheme } = useTheme();\n\n  const memoizedEvents = useMemo(\n    () => enrichEventsWithMeta(generateMinimalSampleEvents()),\n    []\n  );\n\n  const views = useMemo(\n    () => [\n      createDayView(),\n      createWeekView(),\n      createMonthView(),\n      createYearView({ mode: 'fixed-week' }),\n    ],\n    []\n  );\n  const dragPlugin = useMemo(\n    () =>\n      createDragPlugin({\n        enableDrag: true,\n        enableResize: true,\n        enableCreate: true,\n      }),\n    []\n  );\n  const calendars = useMemo(() => cloneCalendarTypes(), []);\n\n  const themeMode = useMemo(() => {\n    if (resolvedTheme === 'dark') return 'dark';\n    if (resolvedTheme === 'light') return 'light';\n    return 'auto';\n  }, [resolvedTheme]);\n\n  return useCalendarApp({\n    views,\n    plugins: [dragPlugin],\n    events: memoizedEvents,\n    calendars,\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    switcherMode: 'buttons',\n    theme: { mode: themeMode },\n    useEventDetailDialog,\n  });\n};\n\nexport const CustomDetailDialogShowcase: React.FC = () => {\n  const calendar = useDemoCalendar({ useEventDetailDialog: true });\n\n  const customDialog = useCallback(\n    ({\n      event,\n      isOpen,\n      onClose,\n      onEventDelete,\n      onEventUpdate,\n    }: EventDetailDialogProps) => {\n      if (!isOpen) return null;\n\n      const meta = event.meta ?? {};\n      const colorPresets = COLOR_PRESETS;\n      const accentColor =\n        colorPresets.find(preset => preset.id === event.calendarId)?.color ||\n        (typeof event.calendarId === 'string' &&\n        event.calendarId.startsWith('#')\n          ? event.calendarId\n          : '#6366f1');\n\n      const toJsDate = (value: Event['start'] | Event['end']): Date | null => {\n        if (!value) return null;\n\n        let result: Date | null = null;\n\n        if (value instanceof Temporal.ZonedDateTime) {\n          result = new Date(Number(value.epochMilliseconds));\n        } else if (value instanceof Temporal.PlainDate) {\n          result = new Date(Date.UTC(value.year, value.month - 1, value.day));\n        } else if (value instanceof Date) {\n          result = value;\n        } else if (typeof value === 'string' || typeof value === 'number') {\n          result = new Date(value);\n        } else if (\n          typeof (value as unknown as { toString?: () => string })?.toString ===\n          'function'\n        ) {\n          result = new Date(String(value));\n        }\n\n        if (!result || Number.isNaN(result.getTime())) {\n          return null;\n        }\n\n        return result;\n      };\n\n      const rawStartDate = toJsDate(event.start);\n      const rawEndDate = toJsDate(event.end ?? event.start);\n      const fallbackDate = new Date();\n\n      const startDate = rawStartDate ?? fallbackDate;\n      const endDate = rawEndDate ?? startDate;\n      const hasValidStart = Boolean(rawStartDate);\n      const hasValidEnd = Boolean(rawEndDate);\n      const isAllDayEvent = Boolean(event.allDay);\n      const sameDay =\n        hasValidStart &&\n        hasValidEnd &&\n        startDate.toDateString() === endDate.toDateString();\n\n      const dateFormatter = new Intl.DateTimeFormat('en-US', {\n        weekday: 'short',\n        month: 'short',\n        day: 'numeric',\n      });\n      const timeFormatter = new Intl.DateTimeFormat('en-US', {\n        hour: 'numeric',\n        minute: '2-digit',\n      });\n\n      const startDayLabel = hasValidStart\n        ? dateFormatter.format(startDate)\n        : 'Date TBD';\n      const endDayLabel = hasValidEnd\n        ? dateFormatter.format(endDate)\n        : 'Date TBD';\n\n      const dayLabel =\n        hasValidStart && hasValidEnd\n          ? sameDay\n            ? startDayLabel\n            : `${startDayLabel} → ${endDayLabel}`\n          : 'Date to be scheduled';\n\n      const timeRangeLabel = isAllDayEvent\n        ? 'All day'\n        : hasValidStart && hasValidEnd\n          ? sameDay\n            ? `${timeFormatter.format(startDate)} – ${timeFormatter.format(\n                endDate\n              )}`\n            : `${startDayLabel} ${timeFormatter.format(\n                startDate\n              )} → ${endDayLabel} ${timeFormatter.format(endDate)}`\n          : 'Time to be scheduled';\n\n      const diffMinutes =\n        hasValidStart && hasValidEnd\n          ? Math.max(\n              0,\n              Math.round((endDate.getTime() - startDate.getTime()) / 60000)\n            )\n          : 0;\n      let durationLabel = '';\n      if (isAllDayEvent) {\n        const diffDays = Math.max(1, Math.round(diffMinutes / (60 * 24)));\n        durationLabel = diffDays === 1 ? 'All day' : `${diffDays} days`;\n      } else if (diffMinutes > 0) {\n        const hours = Math.floor(diffMinutes / 60);\n        const minutes = diffMinutes % 60;\n        if (hours && minutes) {\n          durationLabel = `${hours}h ${minutes}m`;\n        } else if (hours) {\n          durationLabel = `${hours}h`;\n        } else {\n          durationLabel = `${minutes}m`;\n        }\n      }\n\n      const timeZoneLabel =\n        !isAllDayEvent &&\n        hasValidStart &&\n        event.start instanceof Temporal.ZonedDateTime &&\n        typeof event.start.timeZoneId === 'string'\n          ? event.start.timeZoneId\n          : hasValidStart && !isAllDayEvent\n            ? 'Local time'\n            : null;\n\n      const participants = Array.from(\n        new Set(\n          [\n            meta.owner,\n            ...(Array.isArray(meta.attendees) ? meta.attendees : []),\n            'Product Design',\n            'Stakeholders',\n            'Engineering',\n          ].filter(Boolean) as string[]\n        )\n      ).slice(0, 4);\n\n      const isFavorite = Boolean(meta.favorite);\n      const location = meta.location ?? 'To be announced';\n\n      const headerChips = [\n        {\n          icon: CalendarDays,\n          label: dayLabel,\n        },\n        {\n          icon: Clock3,\n          label: timeRangeLabel,\n        },\n        durationLabel\n          ? {\n              icon: Clock3,\n              label: `Duration · ${durationLabel}`,\n            }\n          : null,\n      ].filter(Boolean) as Array<{\n        icon: React.ComponentType<{ className?: string; size?: number }>;\n        label: string;\n      }>;\n\n      const handleToggleFavorite = () => {\n        onEventUpdate({\n          ...event,\n          meta: {\n            ...meta,\n            favorite: !isFavorite,\n          },\n        });\n      };\n\n      const handleUpdateColor = (calendarId: string) => {\n        onEventUpdate({\n          ...event,\n          calendarId,\n        });\n        onClose();\n      };\n\n      const handleDelete = () => {\n        onEventDelete(event.id);\n        onClose();\n      };\n\n      return (\n        <div\n          className='fixed inset-0 z-60 flex items-center justify-center bg-slate-950/60 px-4 py-6 backdrop-blur-md sm:py-10'\n          data-event-detail-dialog='true'\n        >\n          <div className='relative flex max-h-[90vh] w-full max-w-3xl flex-col overflow-hidden rounded-3xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-900'>\n            <div\n              className='absolute inset-x-0 top-0 h-40'\n              style={{\n                background: `linear-gradient(135deg, ${accentColor}, #0f172a)`,\n              }}\n            />\n            <button\n              type='button'\n              onClick={onClose}\n              className='absolute top-4 right-4 z-10 inline-flex h-10 w-10 items-center justify-center rounded-full bg-white text-slate-900 transition hover:bg-white focus-visible:ring-2 focus-visible:ring-white/80 focus-visible:outline-none sm:top-6 sm:right-6 dark:bg-gray-900/20 dark:text-white'\n              aria-label='Close dialog'\n            >\n              <X className='h-5 w-5' />\n            </button>\n\n            <div className='relative px-7 pt-9 pb-4 text-white sm:px-9'>\n              <div className='flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between'>\n                <div className='space-y-3'>\n                  <span className='inline-flex items-center text-xs font-semibold tracking-[0.25em] text-white/70 uppercase'>\n                    {meta.owner ? `Hosted by ${meta.owner}` : 'Team ritual'}\n                  </span>\n                  <div className='flex items-center gap-3'>\n                    <h2 className='text-2xl font-semibold sm:text-3xl'>\n                      {event.title}\n                    </h2>\n                    <button\n                      type='button'\n                      onClick={handleToggleFavorite}\n                      className={`inline-flex h-8 w-8 items-center justify-center rounded-full border border-white/20 transition focus-visible:ring-2 focus-visible:ring-white/80 focus-visible:outline-none ${\n                        isFavorite\n                          ? 'bg-white text-slate-900 backdrop-blur dark:bg-gray-900/20 dark:text-white'\n                          : 'text-white hover:bg-white hover:text-slate-900 dark:hover:bg-slate-900/15 dark:hover:text-white'\n                      }`}\n                      aria-label={\n                        isFavorite\n                          ? 'Remove from focus list'\n                          : 'Add to focus list'\n                      }\n                    >\n                      {isFavorite ? (\n                        <Star className='h-4 w-4 fill-current' />\n                      ) : (\n                        <StarOff className='h-4 w-4' />\n                      )}\n                    </button>\n                  </div>\n                  {event.description && (\n                    <p className='max-w-xl text-sm text-white/80'>\n                      {event.description}\n                    </p>\n                  )}\n                  <div className='flex flex-wrap gap-2 pt-1'>\n                    {headerChips.map(({ icon: Icon, label }) => (\n                      <span\n                        key={label}\n                        className='inline-flex items-center gap-2 rounded-full bg-white px-3 py-1 text-xs font-medium text-slate-900 shadow-sm backdrop-blur dark:bg-gray-900/10 dark:text-white'\n                      >\n                        <Icon className='h-4 w-4' />\n                        {label}\n                      </span>\n                    ))}\n                  </div>\n                </div>\n              </div>\n            </div>\n\n            <div className='relative flex-1 overflow-y-auto px-7 pb-7 sm:px-9 sm:pb-9'>\n              <div className='grid gap-6 lg:grid-cols-[1.6fr,1fr]'>\n                <div className='space-y-5'>\n                  <div className='rounded-2xl border border-slate-100 bg-white p-5 shadow-sm backdrop-blur dark:border-slate-800 dark:bg-gray-900/60'>\n                    <h3 className='text-xs font-semibold tracking-wide text-slate-500 uppercase dark:text-slate-400'>\n                      Schedule\n                    </h3>\n                    <div className='mt-4 space-y-4'>\n                      {[\n                        {\n                          title: 'Starts',\n                          date: startDayLabel,\n                          time: isAllDayEvent\n                            ? 'All day'\n                            : hasValidStart\n                              ? timeFormatter.format(startDate)\n                              : 'Time TBD',\n                        },\n                        {\n                          title: 'Wraps',\n                          date: endDayLabel,\n                          time: isAllDayEvent\n                            ? sameDay && hasValidEnd\n                              ? 'Same day'\n                              : endDayLabel\n                            : hasValidEnd\n                              ? timeFormatter.format(endDate)\n                              : 'Time TBD',\n                        },\n                      ].map(({ title, date, time }) => (\n                        <div key={title} className='flex items-start gap-3'>\n                          <span\n                            className='mt-1 h-2.5 w-2.5 rounded-full'\n                            style={{ backgroundColor: accentColor }}\n                          />\n                          <div>\n                            <p className='text-sm font-medium text-slate-900 dark:text-slate-100'>\n                              {title}\n                            </p>\n                            <p className='text-xs text-slate-500 dark:text-slate-400'>\n                              {date}\n                              {time ? ` · ${time}` : ''}\n                            </p>\n                          </div>\n                        </div>\n                      ))}\n                    </div>\n                    <div className='mt-4 flex flex-wrap gap-2'>\n                      {durationLabel && (\n                        <span className='inline-flex items-center gap-2 rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-600 dark:bg-blue-900/30 dark:text-blue-200'>\n                          <Clock3 className='h-4 w-4' />\n                          {durationLabel}\n                        </span>\n                      )}\n                      {timeZoneLabel && (\n                        <span className='inline-flex items-center gap-2 rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-600 dark:bg-slate-800 dark:text-slate-200'>\n                          <Clock3 className='h-4 w-4' />\n                          {timeZoneLabel}\n                        </span>\n                      )}\n                    </div>\n                  </div>\n\n                  <div className='rounded-2xl border border-slate-100 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-gray-900'>\n                    <h3 className='text-xs font-semibold tracking-wide text-slate-500 uppercase dark:text-slate-400'>\n                      Logistics\n                    </h3>\n                    <div className='mt-4 grid gap-4 sm:grid-cols-2'>\n                      <div className='flex items-start gap-3'>\n                        <span className='flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-200'>\n                          <User className='h-5 w-5' />\n                        </span>\n                        <div>\n                          <p className='text-xs font-semibold tracking-wide text-slate-500 uppercase dark:text-slate-400'>\n                            Host\n                          </p>\n                          <p className='text-sm text-slate-900 dark:text-slate-100'>\n                            {(meta.owner as string) ?? 'Unassigned'}\n                          </p>\n                        </div>\n                      </div>\n                      <div className='flex items-start gap-3'>\n                        <span className='flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-200'>\n                          <MapPin className='h-5 w-5' />\n                        </span>\n                        <div>\n                          <p className='text-xs font-semibold tracking-wide text-slate-500 uppercase dark:text-slate-400'>\n                            Location\n                          </p>\n                          <p className='text-sm text-slate-900 dark:text-slate-100'>\n                            {`${location}`}\n                          </p>\n                        </div>\n                      </div>\n                    </div>\n\n                    <div className='mt-4'>\n                      <p className='text-xs font-semibold tracking-wide text-slate-500 uppercase dark:text-slate-400'>\n                        Participants\n                      </p>\n                      <div className='mt-3 flex flex-wrap gap-2'>\n                        {participants.map(participant => (\n                          <span\n                            key={participant}\n                            className='inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-700 shadow-sm dark:border-slate-700 dark:bg-gray-900 dark:text-slate-200'\n                          >\n                            <span className='inline-block h-2 w-2 rounded-full bg-emerald-500' />\n                            {participant}\n                          </span>\n                        ))}\n                      </div>\n                    </div>\n                  </div>\n                </div>\n\n                <div className='space-y-5'>\n                  <div className='rounded-2xl border border-slate-100 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-gray-900'>\n                    <h3 className='text-xs font-semibold tracking-wide text-slate-500 uppercase dark:text-slate-400'>\n                      Personalize\n                    </h3>\n                    <p className='mt-2 text-xs text-slate-500 dark:text-slate-400'>\n                      Update highlight colors to match your brand palette.\n                    </p>\n                    <div className='mt-4 flex flex-wrap gap-2'>\n                      {colorPresets.map(({ id, color }) => {\n                        const isActive = event.calendarId === id;\n                        return (\n                          <button\n                            key={id}\n                            type='button'\n                            onClick={() => handleUpdateColor(id)}\n                            className={`h-9 w-9 rounded-full border-2 border-white shadow-sm transition-transform hover:-translate-y-0.5 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none ${\n                              isActive ? 'ring-2 ring-offset-white' : ''\n                            }`}\n                            style={{ backgroundColor: color }}\n                            aria-label={`Change color to ${id}`}\n                          />\n                        );\n                      })}\n                    </div>\n                  </div>\n\n                  <div className='rounded-2xl border border-slate-100 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-gray-900'>\n                    <h3 className='text-xs font-semibold tracking-wide text-slate-500 uppercase dark:text-slate-400'>\n                      Quick actions\n                    </h3>\n                    <div className='mt-4 space-y-3'>\n                      <button\n                        type='button'\n                        onClick={handleToggleFavorite}\n                        className={`flex w-full items-center justify-between rounded-xl border px-4 py-3 text-sm font-medium transition ${\n                          isFavorite\n                            ? 'border-amber-200 bg-amber-50 text-amber-700 dark:bg-amber-900/30'\n                            : 'border-slate-200 bg-white text-slate-700 hover:border-slate-300 dark:border-slate-700 dark:bg-gray-900 dark:text-slate-200'\n                        }`}\n                      >\n                        <span>\n                          {isFavorite\n                            ? 'Added to focus list'\n                            : 'Add to focus list'}\n                        </span>\n                        <Star className='h-4 w-4' />\n                      </button>\n                      <button\n                        type='button'\n                        onClick={handleDelete}\n                        className='flex w-full items-center justify-between rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm font-medium text-red-600 transition hover:bg-red-100 dark:border-red-700 dark:bg-red-900/30 dark:text-red-200'\n                      >\n                        <span>Delete event</span>\n                        <X className='h-4 w-4' />\n                      </button>\n                    </div>\n                  </div>\n                </div>\n              </div>\n              <div className='mt-6 rounded-2xl border border-slate-100 bg-slate-50 px-4 py-3 text-xs text-slate-500 dark:border-slate-800 dark:bg-slate-800/60 dark:text-slate-400'>\n                Need more? Swap in your design system dialog.\n              </div>\n            </div>\n          </div>\n        </div>\n      );\n    },\n    []\n  );\n\n  return (\n    <div className='not-prose rounded-xl bg-white dark:border-slate-700 dark:bg-gray-900'>\n      <DayFlowCalendar calendar={calendar} eventDetailDialog={customDialog} />\n    </div>\n  );\n};\n\nexport default CustomDetailDialogShowcase;\n"
  },
  {
    "path": "website/components/showcase/CustomDetailPanelShowcase.tsx",
    "content": "'use client';\n\nimport {\n  Event,\n  CalendarType,\n  EventDetailContentProps,\n  createYearView,\n} from '@dayflow/core';\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport type { DayFlowCalendarProps } from '@dayflow/react';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  createDayView,\n  ViewType,\n} from '@dayflow/react';\nimport { useTheme } from 'next-themes';\nimport { useMemo, useCallback } from 'react';\nimport type { FC } from 'react';\n\nimport { getWebsiteCalendars } from '@/utils/palette';\nimport { generateMinimalSampleEvents } from '@/utils/sampleData';\n\nimport '@dayflow/core/dist/styles.components.css';\n\ntype SwitcherMode = 'buttons' | 'select';\n\ninterface DemoCalendarProps {\n  switcherMode?: SwitcherMode;\n  customDetailPanelContent?: DayFlowCalendarProps['eventDetailContent'];\n  customEventDetailDialog?: DayFlowCalendarProps['eventDetailDialog'];\n  useEventDetailDialog?: boolean;\n  className?: string;\n}\n\nconst cloneCalendarTypes = (): CalendarType[] => getWebsiteCalendars();\n\nconst META_PRESETS = [\n  {\n    owner: 'Alice',\n    location: 'HQ West · Room 301',\n    attendees: ['Brian', 'Chiara', 'Diego'],\n    favorite: true,\n  },\n  {\n    owner: 'Brian',\n    location: 'Zoom',\n    attendees: ['Alice', 'Product Design'],\n    favorite: false,\n  },\n  {\n    owner: 'Chiara',\n    location: 'Local Bistro',\n    attendees: ['Ops', 'Marketing'],\n    favorite: false,\n  },\n  {\n    owner: 'Diego',\n    location: 'Online broadcast',\n    attendees: ['Stakeholders', 'Engineering'],\n    favorite: false,\n  },\n  {\n    owner: 'Mina',\n    location: 'HQ East · Innovation Lab',\n    attendees: ['Research', 'Growth'],\n    favorite: true,\n  },\n  {\n    owner: 'Noah',\n    location: 'Client HQ',\n    attendees: ['Customer team', 'Support'],\n    favorite: false,\n  },\n];\n\nconst enrichEventsWithMeta = (sourceEvents: Event[]): Event[] =>\n  sourceEvents.map((event, index) => {\n    const preset = META_PRESETS[index % META_PRESETS.length];\n    return {\n      ...event,\n      meta: {\n        ...event.meta,\n        ...preset,\n        favorite:\n          typeof preset.favorite === 'boolean'\n            ? preset.favorite\n            : index % 4 === 0,\n      },\n    };\n  });\n\nconst formatTemporal = (value: Event['start']) => {\n  try {\n    return value.toString();\n  } catch {\n    return String(value);\n  }\n};\n\nconst useDemoCalendar = ({\n  switcherMode,\n  events,\n  useEventDetailDialog = false,\n}: {\n  switcherMode?: SwitcherMode;\n  events?: Event[];\n  useEventDetailDialog?: boolean;\n}) => {\n  const { resolvedTheme } = useTheme();\n\n  const memoizedEvents = useMemo(() => {\n    if (events) {\n      return events;\n    }\n    return enrichEventsWithMeta(generateMinimalSampleEvents());\n  }, [events]);\n\n  const views = useMemo(\n    () => [\n      createDayView(),\n      createWeekView(),\n      createMonthView(),\n      createYearView({ mode: 'fixed-week' }),\n    ],\n    []\n  );\n  const dragPlugin = useMemo(\n    () =>\n      createDragPlugin({\n        enableDrag: true,\n        enableResize: true,\n        enableCreate: true,\n      }),\n    []\n  );\n  const calendars = useMemo(() => cloneCalendarTypes(), []);\n\n  const themeMode = useMemo(() => {\n    if (resolvedTheme === 'dark') return 'dark';\n    if (resolvedTheme === 'light') return 'light';\n    return 'auto';\n  }, [resolvedTheme]);\n\n  return useCalendarApp({\n    views,\n    plugins: [dragPlugin],\n    events: memoizedEvents,\n    calendars,\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    switcherMode: switcherMode ?? 'buttons',\n    theme: { mode: themeMode },\n    useEventDetailDialog,\n  });\n};\n\nconst DemoCalendar: FC<DemoCalendarProps> = ({\n  switcherMode,\n  customDetailPanelContent,\n  // customEventDetailDialog,\n  useEventDetailDialog = false,\n}) => {\n  const calendar = useDemoCalendar({ switcherMode, useEventDetailDialog });\n\n  return (\n    <div className='rounded-xl bg-white dark:border-slate-700 dark:bg-gray-900'>\n      <DayFlowCalendar\n        calendar={calendar}\n        eventDetailContent={customDetailPanelContent}\n      />\n    </div>\n  );\n};\n\nexport const CustomDetailPanelShowcase: FC = () => {\n  const detailPanel = useCallback(\n    ({\n      event,\n      onEventDelete,\n      onEventUpdate,\n      onClose,\n    }: EventDetailContentProps) => {\n      const meta = event.meta ?? {};\n      const isFavorite = Boolean(meta.favorite);\n\n      const handleToggleFavorite = () => {\n        const updatedEvent: Event = {\n          ...event,\n          meta: {\n            ...meta,\n            favorite: !isFavorite,\n          },\n        };\n        onEventUpdate(updatedEvent);\n      };\n\n      const handleDelete = () => {\n        onEventDelete(event.id);\n        onClose?.();\n      };\n\n      return (\n        <div className='space-y-3'>\n          <div className='flex items-start justify-between'>\n            <div>\n              <h5 className='text-base font-semibold text-slate-900 dark:text-slate-100'>\n                {event.title}\n              </h5>\n              <p className='mt-0.5 text-xs text-slate-500 dark:text-slate-400'>\n                {formatTemporal(event.start)} → {formatTemporal(event.end)}\n              </p>\n            </div>\n            {isFavorite && (\n              <span className='text-sm font-semibold text-amber-500'>\n                ★ Favorite\n              </span>\n            )}\n          </div>\n\n          {event.description && (\n            <p className='text-sm leading-relaxed text-slate-600 dark:text-slate-300'>\n              {event.description}\n            </p>\n          )}\n\n          <div className='grid grid-cols-2 gap-3 text-xs text-slate-500 dark:text-slate-400'>\n            <div>\n              <span className='font-medium text-slate-700 dark:text-slate-200'>\n                Owner:\n              </span>\n              <span>{`${meta.owner ?? 'Unassigned'}`}</span>\n            </div>\n            <div>\n              <span className='font-medium text-slate-700 dark:text-slate-200'>\n                Location:\n              </span>\n              <span>{`${meta.location ?? 'TBD'}`}</span>\n            </div>\n          </div>\n\n          <div className='flex justify-end gap-2 pt-2'>\n            <button\n              type='button'\n              onClick={handleToggleFavorite}\n              className='rounded-md border border-amber-300 px-3 py-1.5 text-xs font-medium text-amber-600 transition-colors hover:bg-amber-50 dark:border-amber-600 dark:bg-amber-900/30 dark:text-amber-200'\n            >\n              {isFavorite ? 'Remove favorite' : 'Add to favorites'}\n            </button>\n            <button\n              type='button'\n              onClick={handleDelete}\n              className='rounded-md border border-red-300 px-3 py-1.5 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 dark:bg-red-900/30 dark:text-red-200'\n            >\n              Delete event\n            </button>\n          </div>\n        </div>\n      );\n    },\n    []\n  );\n\n  return (\n    <div className='not-prose p-1'>\n      <DemoCalendar\n        customDetailPanelContent={detailPanel}\n        className='h-[750px]'\n      />\n    </div>\n  );\n};\n\nexport default CustomDetailPanelShowcase;\n"
  },
  {
    "path": "website/components/showcase/EventContentShowcase.tsx",
    "content": "/* eslint-disable @next/next/no-img-element */\n'use client';\n\nimport '@dayflow/core/dist/styles.components.css';\nimport { Event, getLineColor } from '@dayflow/core';\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  createDayView,\n  createYearView,\n  ViewType,\n} from '@dayflow/react';\nimport { useTheme } from 'next-themes';\nimport React, { useState, useMemo } from 'react';\n\nimport { getWebsiteCalendars } from '@/utils/palette';\nimport { generateSampleEvents } from '@/utils/sampleData';\n\nconst BASE = process.env.NEXT_PUBLIC_BASE_PATH || '';\n\nconst EventIcon = ({\n  calendarId,\n  defaultIcon,\n}: {\n  calendarId?: string;\n  defaultIcon: string;\n}) => {\n  if (calendarId === 'team') {\n    return (\n      <img\n        src={`${BASE}/images/avatar/avatar1.png`}\n        className='h-4 w-4 shrink-0 rounded-full object-cover'\n        alt='Product Team'\n      />\n    );\n  }\n  if (calendarId === 'personal') {\n    return (\n      <img\n        src={`${BASE}/images/avatar/avatar2.png`}\n        className='h-4 w-4 shrink-0 rounded-full object-cover'\n        alt='Personal'\n      />\n    );\n  }\n  return <span className='shrink-0 text-[10px]'>{defaultIcon}</span>;\n};\n\nexport const EventContentShowcase: React.FC = () => {\n  const { resolvedTheme } = useTheme();\n  const [events] = useState<Event[]>(generateSampleEvents());\n\n  const themeMode = useMemo(() => {\n    if (resolvedTheme === 'dark') return 'dark';\n    if (resolvedTheme === 'light') return 'light';\n    return 'auto';\n  }, [resolvedTheme]);\n\n  const calendar = useCalendarApp({\n    views: [\n      createDayView(),\n      createWeekView(),\n      createMonthView({\n        showWeekNumbers: true,\n        showMonthIndicator: false,\n      }),\n      createYearView({\n        showTimedEventsInYearView: true,\n      }),\n    ],\n    plugins: [createDragPlugin()],\n    events: events,\n    calendars: getWebsiteCalendars(),\n    defaultCalendar: 'work',\n    defaultView: ViewType.MONTH,\n    theme: { mode: themeMode },\n  });\n\n  return (\n    <div className='not-prose p-1'>\n      <DayFlowCalendar\n        calendar={calendar}\n        eventContentDay={({ event, isSelected }) => (\n          <div\n            className='flex h-full flex-col justify-start overflow-hidden border-l-4'\n            style={{\n              borderTopLeftRadius: '4px',\n              borderBottomLeftRadius: '4px',\n              borderColor: getLineColor(event.calendarId || 'blue'),\n            }}\n          >\n            <div className='flex items-center gap-1 px-0.5'>\n              <span className='shrink-0 text-[10px]'>📅</span>\n              <span\n                className={`truncate text-xs font-semibold ${isSelected ? 'text-white' : ''}`}\n              >\n                {event.title}\n              </span>\n            </div>\n            <div className='flex flex-col px-1'>\n              <span\n                className={`truncate text-[10px] opacity-70 ${isSelected ? 'text-white' : ''}`}\n              >\n                📍 {`${event.meta?.location || 'No location'}`}\n              </span>\n              <span\n                className={`text-[10px] opacity-60 ${isSelected ? 'text-white' : ''}`}\n              >\n                {event.description}\n              </span>\n            </div>\n          </div>\n        )}\n        eventContentWeek={({ event, isSelected }) => (\n          <div\n            className='flex h-full flex-col justify-start overflow-hidden border-l-4 border-black/20 pr-0.5 pl-1'\n            style={{\n              borderTopLeftRadius: '4px',\n              borderBottomLeftRadius: '4px',\n              borderColor: getLineColor(event.calendarId || 'blue'),\n            }}\n          >\n            <div className='flex items-center gap-1 px-0.5'>\n              <EventIcon calendarId={event.calendarId} defaultIcon='🗓' />\n              <span\n                className={`text-xs font-semibold ${isSelected ? 'text-white' : ''}`}\n              >\n                {event.title}\n              </span>\n            </div>\n            <div className='flex flex-col'>\n              <span\n                className={`text-[9px] opacity-70 ${isSelected ? 'text-white' : ''}`}\n              >\n                📍 {`${event.meta?.location || 'No location'}`}\n              </span>\n              <span\n                className={`text-[9px] opacity-60 ${isSelected ? 'text-white' : ''}`}\n              >\n                {event.description}\n              </span>\n            </div>\n          </div>\n        )}\n        eventContentMonth={({ event }) => (\n          <div className='flex items-center gap-1 overflow-hidden px-0.5'>\n            <EventIcon calendarId={event.calendarId} defaultIcon='🗃' />\n            <span className='truncate text-xs font-medium'>{event.title}</span>\n          </div>\n        )}\n        eventContentYear={({ event }) => (\n          <div className='flex items-center gap-1 overflow-hidden px-0.5'>\n            <EventIcon calendarId={event.calendarId} defaultIcon='🗃' />\n            <span className='truncate text-xs font-medium'>{event.title}</span>\n          </div>\n        )}\n        eventContentAllDayDay={({ event }) => (\n          <div className='flex h-full items-center gap-1 overflow-hidden px-0.5'>\n            <EventIcon calendarId={event.calendarId} defaultIcon='☀️' />\n            <span className='truncate text-xs font-medium'>{event.title}</span>\n          </div>\n        )}\n        eventContentAllDayWeek={({ event }) => (\n          <div className='flex h-full items-center gap-1 overflow-hidden px-0.5'>\n            <EventIcon calendarId={event.calendarId} defaultIcon='☀️' />\n            <span className='truncate text-xs font-medium'>{event.title}</span>\n          </div>\n        )}\n        eventContentAllDayMonth={({ event }) => (\n          <div className='flex h-full items-center gap-1 overflow-hidden px-0.5'>\n            <EventIcon calendarId={event.calendarId} defaultIcon='☀️' />\n            <span className='truncate text-xs font-medium'>{event.title}</span>\n          </div>\n        )}\n        eventContentAllDayYear={({ event }) => (\n          <div className='flex h-full items-center gap-1 overflow-hidden px-0.5'>\n            <EventIcon calendarId={event.calendarId} defaultIcon='☀️' />\n            <span className='truncate text-xs font-medium'>{event.title}</span>\n          </div>\n        )}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "website/components/showcase/FeatureShowcase.tsx",
    "content": "'use client';\n\nimport { CalendarType, createYearView } from '@dayflow/core';\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  createDayView,\n  ViewType,\n} from '@dayflow/react';\nimport { useTheme } from 'next-themes';\nimport React, { useMemo } from 'react';\n\nimport { Card, CardContent } from '@/components/ui/card';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport { TooltipProvider } from '@/components/ui/tooltip';\nimport { getWebsiteCalendars } from '@/utils/palette';\nimport { generateMinimalSampleEvents } from '@/utils/sampleData';\n\nimport { CustomDetailDialogShowcase } from './CustomDetailDialogShowcase';\n\nimport '@dayflow/core/dist/styles.components.css';\n\ntype SwitcherMode = 'buttons' | 'select';\n\ninterface DemoCalendarProps {\n  switcherMode?: SwitcherMode;\n  useEventDetailDialog?: boolean;\n  className?: string;\n}\n\ninterface FeatureCardProps {\n  title: string;\n  description: string;\n  children: React.ReactNode;\n}\n\nconst cloneCalendarTypes = (): CalendarType[] => getWebsiteCalendars();\n\nconst FeatureCard: React.FC<FeatureCardProps> = ({\n  title,\n  description,\n  children,\n}) => (\n  <div className='overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-700 dark:bg-gray-900'>\n    <div className='border-b border-slate-100 bg-slate-50 px-6 py-5 dark:border-slate-800 dark:bg-slate-800/60'>\n      <h3 className='text-xl font-semibold text-slate-900 dark:text-slate-100'>\n        {title}\n      </h3>\n      <p className='mt-2 text-sm text-slate-600 dark:text-slate-400'>\n        {description}\n      </p>\n    </div>\n    <div className='bg-white p-6 dark:bg-gray-900'>{children}</div>\n  </div>\n);\n\nconst useDemoCalendar = ({\n  switcherMode,\n  useEventDetailDialog = false,\n}: {\n  switcherMode?: SwitcherMode;\n  useEventDetailDialog?: boolean;\n}) => {\n  const { resolvedTheme } = useTheme();\n\n  const memoizedEvents = useMemo(() => generateMinimalSampleEvents(), []);\n\n  const views = useMemo(\n    () => [\n      createDayView(),\n      createWeekView(),\n      createMonthView(),\n      createYearView({ mode: 'fixed-week' }),\n    ],\n    []\n  );\n  const dragPlugin = useMemo(\n    () =>\n      createDragPlugin({\n        enableDrag: true,\n        enableResize: true,\n        enableCreate: true,\n      }),\n    []\n  );\n  const calendars = useMemo(() => cloneCalendarTypes(), []);\n\n  const themeMode = useMemo(() => {\n    if (resolvedTheme === 'dark') return 'dark';\n    if (resolvedTheme === 'light') return 'light';\n    return 'auto';\n  }, [resolvedTheme]);\n\n  return useCalendarApp({\n    views,\n    plugins: [dragPlugin],\n    events: memoizedEvents,\n    calendars,\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    switcherMode: switcherMode ?? 'buttons',\n    theme: { mode: themeMode },\n    useEventDetailDialog,\n  });\n};\n\nconst DemoCalendar: React.FC<DemoCalendarProps> = ({\n  switcherMode,\n  useEventDetailDialog = false,\n  className,\n}) => {\n  const calendar = useDemoCalendar({ switcherMode, useEventDetailDialog });\n\n  return (\n    <div\n      className={`not-prose rounded-xl bg-white dark:border-slate-700 dark:bg-gray-900 ${className ?? ''}`}\n    >\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n};\n\nexport const SwitcherModeShowcase: React.FC = () => {\n  const [mode, setMode] = React.useState<SwitcherMode>('buttons');\n  const [mounted, setMounted] = React.useState(false);\n\n  React.useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  if (!mounted) {\n    return (\n      <div className='calendar-wrapper w-full' style={{ minHeight: '600px' }} />\n    );\n  }\n\n  return (\n    <TooltipProvider delayDuration={0}>\n      <div className='flex w-full flex-col gap-6'>\n        <Card className='border-slate-200 bg-slate-50/50 shadow-none dark:border-slate-800 dark:bg-gray-900/50'>\n          <CardContent className='flex flex-col gap-3 px-4 py-2'>\n            <div className='flex items-start justify-between gap-8'>\n              <div className='space-y-1'>\n                <h3 className='text-xs font-semibold tracking-tight text-slate-900 uppercase dark:text-slate-100'>\n                  Switcher Mode\n                </h3>\n                <Select\n                  value={mode}\n                  onValueChange={value => setMode(value as SwitcherMode)}\n                >\n                  <SelectTrigger className='h-7 w-35 text-xs'>\n                    <SelectValue placeholder='Select mode' />\n                  </SelectTrigger>\n                  <SelectContent className='max-h-80 w-35 overflow-hidden p-0'>\n                    <SelectItem\n                      value='buttons'\n                      className='cursor-pointer text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                    >\n                      Button\n                    </SelectItem>\n                    <SelectItem\n                      value='select'\n                      className='cursor-pointer text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                    >\n                      Select\n                    </SelectItem>\n                  </SelectContent>\n                </Select>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n        <DemoCalendar switcherMode={mode} className='h-[750px]' />\n      </div>\n    </TooltipProvider>\n  );\n};\n\nexport const EventDialogShowcase: React.FC = () => (\n  <DemoCalendar className='h-[750px]' useEventDetailDialog={true} />\n);\n\nexport const FeatureShowcase: React.FC = () => (\n  <div className='space-y-10'>\n    <FeatureCard\n      title='View Switcher Modes'\n      description='Switch between button and select based headers with the switcherMode prop.'\n    >\n      <SwitcherModeShowcase />\n    </FeatureCard>\n\n    <FeatureCard\n      title='Custom Event Detail Dialog'\n      description='Open a fully custom dialog when an event is selected, keeping parity with your modal system.'\n    >\n      <CustomDetailDialogShowcase />\n    </FeatureCard>\n  </div>\n);\n\nexport default FeatureShowcase;\n"
  },
  {
    "path": "website/components/showcase/InteractiveCalendar.tsx",
    "content": "'use client';\n\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport { createKeyboardShortcutsPlugin } from '@dayflow/plugin-keyboard-shortcuts';\nimport {\n  createLocalizationPlugin,\n  zh,\n  ja,\n  ko,\n  fr,\n  de,\n  es,\n} from '@dayflow/plugin-localization';\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\nimport {\n  CalendarAppConfig,\n  createAgendaView,\n  createDayView,\n  createWeekView,\n  createMonthView,\n  ViewType,\n  createYearView,\n  UseCalendarAppReturn,\n} from '@dayflow/react';\nimport { Inbox, PanelRightClose, PanelRightOpen } from 'lucide-react';\nimport { useTheme } from 'next-themes';\nimport React, {\n  useMemo,\n  useState,\n  useEffect,\n  useRef,\n  useTransition,\n  useCallback,\n} from 'react';\n\nimport { TooltipProvider } from '@/components/ui/tooltip';\nimport { getWebsiteCalendars } from '@/utils/palette';\nimport { generateSampleEvents } from '@/utils/sampleData';\n\nimport { CalendarViewer } from './livedemo/CalendarViewer';\nimport { ControlPanel } from './livedemo/ControlPanel';\nimport {\n  CalendarFeatures,\n  CalendarSelections,\n  DEFAULT_THEME_COLOR,\n} from './livedemo/types';\n\nconst calendarTypes = getWebsiteCalendars();\n\nconst LOCALES_OPTIONS = [\n  { label: 'English', value: 'en', data: null },\n  { label: 'Chinese', value: 'zh', data: zh },\n  { label: 'Japanese', value: 'ja', data: ja },\n  { label: 'Korean', value: 'ko', data: ko },\n  { label: 'French', value: 'fr', data: fr },\n  { label: 'German', value: 'de', data: de },\n  { label: 'Spanish', value: 'es', data: es },\n];\n\nconst mergeChangedState = <T extends object>(\n  prev: T,\n  updates: Partial<T>\n): T => {\n  const keys = Object.keys(updates) as Array<keyof T>;\n  if (keys.every(key => prev[key] === updates[key])) return prev;\n\n  return { ...prev, ...updates };\n};\n\nexport function InteractiveCalendar() {\n  const { resolvedTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n  const [, startTransition] = useTransition();\n\n  const [showControls, setShowControls] = useState(false);\n  const calendarRef = useRef<UseCalendarAppReturn | null>(null);\n  const calendarWrapperRef = useRef<HTMLDivElement>(null);\n\n  // Consolidated State\n  const [features, setFeatures] = useState<CalendarFeatures>({\n    showSidebar: false,\n    showHeader: true,\n    enableDrag: true,\n    enableShortcuts: true,\n    showEventDots: true,\n    showCalendarGroups: true,\n    showMultiCalendar: false,\n    readOnly: false,\n    collapsedSafeAreaLeft: false,\n    sidebarOrder: ['calendarList', 'miniCalendar'],\n  });\n\n  const [selections, setSelections] = useState<CalendarSelections>({\n    locale: 'en',\n    selectedViews: [\n      ViewType.DAY,\n      ViewType.WEEK,\n      ViewType.MONTH,\n      ViewType.YEAR,\n      ViewType.AGENDA,\n    ],\n    activeView: ViewType.MONTH,\n    yearMode: 'fixed-week',\n    switcherMode: 'buttons',\n  });\n\n  useEffect(() => {\n    setMounted(true);\n    if (typeof window !== 'undefined' && window.innerWidth >= 1024) {\n      setFeatures(prev => ({ ...prev, showSidebar: true }));\n      setShowControls(true);\n    }\n  }, []);\n\n  const updateFeatures = useCallback(\n    (updates: Partial<CalendarFeatures>) => {\n      startTransition(() => {\n        setFeatures(prev => mergeChangedState(prev, updates));\n      });\n    },\n    [startTransition]\n  );\n\n  const updateSelections = useCallback(\n    (updates: Partial<CalendarSelections>) => {\n      startTransition(() => {\n        setSelections(prev => mergeChangedState(prev, updates));\n      });\n    },\n    [startTransition]\n  );\n\n  const previewThemeColor = useCallback((color: string) => {\n    calendarWrapperRef.current?.style.setProperty('--df-color-primary', color);\n  }, []);\n\n  const titleBarSlot = useMemo(\n    () =>\n      features.collapsedSafeAreaLeft && features.showSidebar\n        ? ({\n            isCollapsed,\n            toggleCollapsed,\n          }: {\n            isCollapsed: boolean;\n            toggleCollapsed: () => void;\n          }) => (\n            <>\n              <div\n                style={{\n                  position: 'absolute',\n                  top: '10px',\n                  left: '12px',\n                  display: 'flex',\n                  alignItems: 'center',\n                  gap: '6px',\n                  zIndex: 50,\n                }}\n              >\n                <div\n                  style={{\n                    width: 12,\n                    height: 12,\n                    borderRadius: '50%',\n                    background: '#ff5f57',\n                    border: '0.5px solid rgba(0,0,0,0.12)',\n                  }}\n                />\n                <div\n                  style={{\n                    width: 12,\n                    height: 12,\n                    borderRadius: '50%',\n                    background: '#febc2e',\n                    border: '0.5px solid rgba(0,0,0,0.12)',\n                  }}\n                />\n                <div\n                  style={{\n                    width: 12,\n                    height: 12,\n                    borderRadius: '50%',\n                    background: '#28c840',\n                    border: '0.5px solid rgba(0,0,0,0.12)',\n                  }}\n                />\n              </div>\n              <div\n                className='calendar-title-bar absolute z-50 flex items-center gap-1'\n                style={{ top: '10px', left: '72px' }}\n              >\n                <button type='button' onClick={toggleCollapsed}>\n                  {isCollapsed ? (\n                    <PanelRightClose size={16} className='mx-2 text-gray-600' />\n                  ) : (\n                    <PanelRightOpen size={16} className='mx-2 text-gray-600' />\n                  )}\n                </button>\n                <button type='button' onClick={toggleCollapsed}>\n                  <Inbox size={16} className='text-gray-600' />\n                </button>\n              </div>\n            </>\n          )\n        : undefined,\n    [features.collapsedSafeAreaLeft, features.showSidebar]\n  );\n\n  const allEvents = useMemo(() => generateSampleEvents(), []);\n  const events = useMemo(() => {\n    if (features.showMultiCalendar) return allEvents;\n    return allEvents.filter(e => !e.calendarIds);\n  }, [allEvents, features.showMultiCalendar]);\n\n  const calendarsWithGroups = useMemo(() => {\n    const googleIds = new Set(['team', 'personal', 'learning', 'travel']);\n    const icloudIds = new Set(['wellness', 'marketing', 'support']);\n    return calendarTypes.map(cal => ({\n      ...cal,\n      source: googleIds.has(cal.id)\n        ? 'Google'\n        : icloudIds.has(cal.id)\n          ? 'iCloud'\n          : undefined,\n    }));\n  }, []);\n\n  const themeMode = useMemo(() => {\n    if (resolvedTheme === 'dark') return 'dark';\n    if (resolvedTheme === 'light') return 'light';\n    return 'auto';\n  }, [resolvedTheme]);\n\n  const config = useMemo(() => {\n    const p = [];\n    if (features.enableDrag) p.push(createDragPlugin());\n    if (features.showSidebar) {\n      p.push(\n        createSidebarPlugin({\n          createCalendarMode: 'modal',\n          showEventDots: features.showEventDots,\n          componentsOrder: features.sidebarOrder,\n        })\n      );\n    }\n    if (features.enableShortcuts) p.push(createKeyboardShortcutsPlugin());\n\n    const selectedLocaleData = LOCALES_OPTIONS.find(\n      l => l.value === selections.locale\n    )?.data;\n    if (selectedLocaleData) {\n      p.push(createLocalizationPlugin({ locales: [selectedLocaleData] }));\n    }\n\n    const v = [];\n    if (selections.selectedViews.includes(ViewType.DAY)) {\n      v.push(\n        createDayView({\n          secondaryTimeZone: selections.secondaryTimeZone as never,\n          scrollToCurrentTime: true,\n          showEventDots: features.showEventDots,\n        })\n      );\n    }\n    if (selections.selectedViews.includes(ViewType.WEEK)) {\n      v.push(\n        createWeekView({\n          secondaryTimeZone: selections.secondaryTimeZone as never,\n          scrollToCurrentTime: true,\n          showEventDots: features.showEventDots,\n        })\n      );\n    }\n    if (selections.selectedViews.includes(ViewType.MONTH)) {\n      v.push(\n        createMonthView({\n          showMonthIndicator: false,\n          showEventDots: features.showEventDots,\n        })\n      );\n    }\n    if (selections.selectedViews.includes(ViewType.YEAR)) {\n      v.push(\n        createYearView({\n          mode: selections.yearMode as never,\n          showTimedEventsInYearView: true,\n          showEventDots: features.showEventDots,\n        })\n      );\n    }\n    if (selections.selectedViews.includes(ViewType.AGENDA)) {\n      v.push(\n        createAgendaView({\n          daysToShow: 14,\n          gridDateDoubleClick: 'day-view',\n        })\n      );\n    }\n\n    const currentView = selections.selectedViews.includes(selections.activeView)\n      ? selections.activeView\n      : selections.selectedViews.includes(ViewType.MONTH)\n        ? ViewType.MONTH\n        : (selections.selectedViews[0] as ViewType);\n\n    return {\n      views: v,\n      plugins: p,\n      initialDate: new Date(),\n      defaultView: currentView,\n      callbacks: {\n        onViewChange: (view: string) =>\n          updateSelections({ activeView: view as ViewType }),\n        onMoreEventsClick: (date: Date) => {\n          calendarRef.current?.selectDate(date);\n          calendarRef.current?.changeView(ViewType.DAY);\n        },\n      },\n      events,\n      timeZone:\n        selections.timeZone ||\n        (mounted\n          ? Intl.DateTimeFormat().resolvedOptions().timeZone\n          : undefined),\n      locale: selections.locale,\n      calendars: features.showCalendarGroups\n        ? calendarsWithGroups\n        : calendarTypes,\n      useCalendarHeader: features.showHeader,\n      switcherMode: selections.switcherMode,\n      theme: { mode: themeMode as 'light' | 'dark' | 'auto' },\n      readOnly: features.readOnly\n        ? {\n            viewable: true,\n            draggable: !features.readOnly,\n          }\n        : false,\n    };\n  }, [\n    features.enableDrag,\n    features.showSidebar,\n    features.enableShortcuts,\n    features.showCalendarGroups,\n    features.showHeader,\n    features.readOnly,\n    features.showEventDots,\n    features.sidebarOrder,\n    selections.selectedViews,\n    selections.activeView,\n    selections.timeZone,\n    selections.locale,\n    selections.switcherMode,\n    selections.secondaryTimeZone,\n    selections.yearMode,\n    events,\n    mounted,\n    calendarsWithGroups,\n    themeMode,\n    updateSelections,\n  ]) as unknown as CalendarAppConfig;\n\n  if (!mounted) {\n    return (\n      <div className='calendar-wrapper w-full' style={{ minHeight: '600px' }} />\n    );\n  }\n\n  return (\n    <TooltipProvider delayDuration={0}>\n      <div className='flex w-full flex-col gap-6'>\n        <ControlPanel\n          features={features}\n          selections={selections}\n          onUpdateFeatures={updateFeatures}\n          onUpdateSelections={updateSelections}\n          onPreviewThemeColor={previewThemeColor}\n          localesOptions={LOCALES_OPTIONS}\n          showControls={showControls}\n        />\n\n        <div\n          ref={calendarWrapperRef}\n          className={`calendar-wrapper w-full${features.collapsedSafeAreaLeft && features.showSidebar ? ' mac-title-bar-active' : ''}`}\n          style={\n            {\n              '--df-color-primary':\n                selections.themeColor || DEFAULT_THEME_COLOR,\n              '--df-color-primary-foreground': '#ffffff',\n            } as React.CSSProperties\n          }\n        >\n          <CalendarViewer\n            key={`${selections.locale}-${selections.selectedViews.join(',')}-${selections.yearMode}-${selections.switcherMode}`}\n            version={`${features.showSidebar}-${features.enableDrag}-${features.enableShortcuts}-${features.showEventDots}-${features.showMultiCalendar}-${features.sidebarOrder?.join(',')}-${features.collapsedSafeAreaLeft}`}\n            config={config}\n            calendarRef={calendarRef}\n            collapsedSafeAreaLeft={\n              features.collapsedSafeAreaLeft && features.showSidebar\n                ? 130\n                : undefined\n            }\n            titleBarSlot={titleBarSlot}\n          />\n        </div>\n      </div>\n    </TooltipProvider>\n  );\n}\n\nexport default InteractiveCalendar;\n"
  },
  {
    "path": "website/components/showcase/LiveDemo.tsx",
    "content": "'use client';\n\nimport Link from 'next/link';\n\nimport InteractiveCalendarComponent from './InteractiveCalendar';\n\nexport function LiveDemo() {\n  return (\n    <div className='live-demo-container mx-auto'>\n      <section className='space-y-12 py-8'>\n        <div className='mx-auto text-center'>\n          <span className='inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-xs font-medium tracking-wide text-blue-600 uppercase dark:bg-blue-500/10 dark:text-blue-300'>\n            Calendar toolkit for product teams\n          </span>\n          <h1 className='mt-6 px-6 text-3xl leading-tight font-semibold sm:text-5xl'>\n            A lightweight and elegant full calendar component\n          </h1>\n          <p className='mt-4 text-base text-slate-600 sm:text-lg dark:text-slate-400'>\n            DayFlow provides production-ready calendar views, drag-and-drop, and\n            a modular architecture so you can focus on your product, not date\n            math.\n          </p>\n          <div className='mt-8 flex flex-wrap items-center justify-center gap-4'>\n            <Link\n              href='/docs/introduction'\n              className='inline-flex items-center justify-center rounded-full bg-blue-500 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-400'\n            >\n              Get started\n            </Link>\n            <Link\n              href='https://github.com/dayflow-js/dayflow'\n              className='inline-flex items-center justify-center rounded-full border border-slate-200 px-5 py-2.5 text-sm font-semibold text-slate-700 transition hover:border-blue-200 hover:text-blue-500 dark:border-slate-700 dark:text-slate-200 dark:hover:border-blue-500/60 dark:hover:text-blue-300'\n            >\n              View on GitHub\n            </Link>\n          </div>\n        </div>\n      </section>\n\n      <section className='space-y-6'>\n        <div className='overflow-hidden'>\n          <InteractiveCalendarComponent />\n        </div>\n      </section>\n\n      <footer className='py-8 text-center text-sm text-slate-500 dark:text-slate-400'>\n        MIT {new Date().getFullYear()} © DayFlow.\n      </footer>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/components/showcase/MobileEventDetailShowcase.tsx",
    "content": "'use client';\n\nimport { BASE_PATH } from '@/lib/site';\n\nconst demoSrc = `${BASE_PATH}/showcase/mobile-event-detail`;\n\nexport const MobileEventDetailShowcase = () => (\n  <div className='overflow-hidden rounded-[2rem] border border-slate-200 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.16),_transparent_42%),linear-gradient(180deg,_#f8fbff_0%,_#eef4ff_100%)] p-4 shadow-[0_20px_60px_rgba(15,23,42,0.08)] dark:border-slate-800 dark:bg-[radial-gradient(circle_at_top,_rgba(96,165,250,0.18),_transparent_38%),linear-gradient(180deg,_rgba(15,23,42,0.96)_0%,_rgba(2,6,23,1)_100%)]'>\n    <div className='mx-auto w-full max-w-[390px] rounded-[2.4rem] bg-slate-950 p-2.5 shadow-[0_24px_80px_rgba(15,23,42,0.45)] ring-1 ring-white/10'>\n      <div className='mb-2 flex items-center justify-center'>\n        <div className='h-6 w-32 rounded-full bg-slate-800' />\n      </div>\n\n      <div className='overflow-hidden rounded-[1.9rem] bg-white ring-1 ring-slate-200/70 dark:bg-slate-950 dark:ring-slate-700/80'>\n        <iframe\n          title='Mobile Event Detail Simulator'\n          src={demoSrc}\n          className='block h-[720px] w-full border-0 bg-white'\n          sandbox='allow-scripts'\n        />\n      </div>\n    </div>\n\n    <p className='mt-4 text-center text-xs text-slate-600 dark:text-slate-400'>\n      Tap an event to open the mobile drawer. This showcase runs in an iframe so\n      DayFlow reads a real mobile viewport instead of the desktop window.\n    </p>\n  </div>\n);\n\nexport default MobileEventDetailShowcase;\n"
  },
  {
    "path": "website/components/showcase/MultiCalendarEventShowcase.tsx",
    "content": "'use client';\n\nimport '@dayflow/core/dist/styles.components.css';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react';\nimport { useTheme } from 'next-themes';\nimport React, { useMemo } from 'react';\n\nimport { getWebsiteCalendars } from '@/utils/palette';\nimport { generateMultiCalendarEvents } from '@/utils/sampleData';\n\nexport const MultiCalendarEventShowcase: React.FC = () => {\n  const { resolvedTheme } = useTheme();\n  const events = useMemo(() => generateMultiCalendarEvents(), []);\n\n  const themeMode = useMemo(() => {\n    if (resolvedTheme === 'dark') return 'dark';\n    if (resolvedTheme === 'light') return 'light';\n    return 'auto';\n  }, [resolvedTheme]);\n\n  const calendar = useCalendarApp({\n    views: [\n      createWeekView(),\n      createMonthView({\n        showWeekNumbers: true,\n        showMonthIndicator: false,\n      }),\n    ],\n    events: events,\n    calendars: getWebsiteCalendars(),\n    defaultCalendar: 'team',\n    defaultView: ViewType.WEEK,\n    theme: { mode: themeMode },\n  });\n\n  return (\n    <div className='not-prose p-1'>\n      <div className='mb-4 rounded-lg bg-blue-50 p-4 text-sm text-blue-800 dark:bg-blue-900/30 dark:text-blue-200'>\n        <strong>Multi-calendar events</strong> are rendered with a diagonal\n        stripe pattern background (one stripe per calendar color) and a\n        multi-color gradient left-side bar.\n      </div>\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "website/components/showcase/SidebarShowcases.tsx",
    "content": "'use client';\n\nimport { CalendarType, Event, temporalToDate } from '@dayflow/core';\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport {\n  createSidebarPlugin,\n  CalendarSidebarRenderProps,\n} from '@dayflow/plugin-sidebar';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  ViewType,\n} from '@dayflow/react';\nimport { ChevronLeft, ChevronRight, Eye, EyeOff } from 'lucide-react';\nimport { useTheme } from 'next-themes';\nimport React, { useMemo, useCallback } from 'react';\nimport { Temporal } from 'temporal-polyfill';\n\nimport { getWebsiteCalendars } from '@/utils/palette';\n\nconst SIDEBAR_CALENDAR_IDS = new Set([\n  'team',\n  'personal',\n  'learning',\n  'travel',\n  'wellness',\n]);\n\nconst BASE_CALENDARS: CalendarType[] = getWebsiteCalendars().filter(calendar =>\n  SIDEBAR_CALENDAR_IDS.has(calendar.id)\n);\n\nconst cloneCalendars = (): CalendarType[] =>\n  BASE_CALENDARS.map(calendar => ({\n    ...calendar,\n    colors: { ...calendar.colors },\n    darkColors: calendar.darkColors ? { ...calendar.darkColors } : undefined,\n  }));\n\nconst createSidebarEvents = (): Event[] => {\n  const now = Temporal.Now.zonedDateTimeISO();\n  const startOfDay = now.startOfDay();\n\n  return [\n    {\n      id: 'sidebar-1',\n      title: 'Sprint Planning',\n      start: startOfDay.add({ hours: 10 }),\n      end: startOfDay.add({ hours: 11, minutes: 30 }),\n      calendarId: 'team',\n      description: 'Outline weekly priorities with the core product pod.',\n    },\n    {\n      id: 'sidebar-2',\n      title: 'Customer Journey Workshop',\n      start: startOfDay.add({ days: 1, hours: 14 }),\n      end: startOfDay.add({ days: 1, hours: 16 }),\n      calendarId: 'team',\n    },\n    {\n      id: 'sidebar-3',\n      title: 'Pilates Class',\n      start: startOfDay.add({ hours: 7, minutes: 30 }),\n      end: startOfDay.add({ hours: 8, minutes: 30 }),\n      calendarId: 'wellness',\n    },\n    {\n      id: 'sidebar-4',\n      title: 'Notion System Review',\n      start: startOfDay.add({ days: 2, hours: 18 }),\n      end: startOfDay.add({ days: 2, hours: 19 }),\n      calendarId: 'personal',\n    },\n    {\n      id: 'sidebar-5',\n      title: 'Advanced TypeScript Study',\n      start: startOfDay.add({ days: 3, hours: 20 }),\n      end: startOfDay.add({ days: 3, hours: 21, minutes: 30 }),\n      calendarId: 'learning',\n    },\n    {\n      id: 'sidebar-6',\n      title: 'Weekend City Break',\n      start: startOfDay.add({ days: 5 }),\n      end: startOfDay.add({ days: 7 }),\n      calendarId: 'travel',\n      allDay: true,\n    },\n  ];\n};\n\nconst useSidebarCalendar = (\n  sidebarPlugin: ReturnType<typeof createSidebarPlugin>\n) => {\n  const { resolvedTheme } = useTheme();\n  const views = useMemo(() => [createMonthView()], []);\n  const dragPlugin = useMemo(\n    () =>\n      createDragPlugin({\n        enableDrag: true,\n        enableResize: true,\n        enableCreate: true,\n      }),\n    []\n  );\n  const events = useMemo(() => createSidebarEvents(), []);\n  const calendars = useMemo(() => cloneCalendars(), []);\n\n  const themeMode = useMemo(() => {\n    if (resolvedTheme === 'dark') return 'dark';\n    if (resolvedTheme === 'light') return 'light';\n    return 'auto';\n  }, [resolvedTheme]);\n\n  return useCalendarApp({\n    views,\n    plugins: [dragPlugin, sidebarPlugin],\n    events,\n    calendars,\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    switcherMode: 'buttons',\n    theme: { mode: themeMode },\n  });\n};\n\nconst CustomSidebarPanel: React.FC<CalendarSidebarRenderProps> = ({\n  app,\n  calendars,\n  toggleCalendarVisibility,\n  toggleAll,\n  isCollapsed,\n  setCollapsed,\n}) => {\n  const upcoming = useMemo(() => {\n    const now = Date.now();\n    return app\n      .getEvents()\n      .map(event => {\n        const matchingCalendar = calendars.find(\n          calendar => calendar.id === event.calendarId\n        );\n        const startDate = temporalToDate(event.start);\n        return {\n          event,\n          calendar: matchingCalendar,\n          startDate,\n        };\n      })\n      .filter(item => item.startDate.getTime() >= now)\n      .toSorted((a, b) => a.startDate.getTime() - b.startDate.getTime())\n      .slice(0, 4);\n  }, [app, calendars]);\n\n  const hiddenCount = calendars.filter(calendar => !calendar.isVisible).length;\n\n  if (isCollapsed) {\n    return (\n      <div className='h-full bg-slate-900 py-4 pl-2 text-slate-100'>\n        <button\n          type='button'\n          onClick={() => setCollapsed(false)}\n          className='rounded-full bg-slate-800 p-2 text-slate-300 transition hover:bg-slate-700'\n          aria-label='Expand sidebar'\n        >\n          <ChevronRight className='h-5 w-5' />\n        </button>\n      </div>\n    );\n  }\n\n  return (\n    <div className='flex h-full flex-col bg-slate-950 text-slate-100'>\n      <div className='flex items-center justify-between border-b border-white/10 px-4 py-3'>\n        <div>\n          <p className='text-xs font-semibold tracking-wide text-slate-500 uppercase'>\n            Workspace\n          </p>\n          <p className='text-base font-semibold text-white'>DayFlow Team</p>\n        </div>\n        <button\n          type='button'\n          onClick={() => setCollapsed(true)}\n          className='rounded-full bg-slate-900 p-2 text-slate-400 transition hover:text-white'\n          aria-label='Collapse sidebar'\n        >\n          <ChevronLeft className='h-5 w-5' />\n        </button>\n      </div>\n\n      <div className='flex-1 overflow-y-auto px-4 py-4'>\n        <div className='flex items-center justify-between text-xs tracking-wide text-slate-500 uppercase'>\n          <span>Calendars</span>\n          <div className='flex items-center gap-2'>\n            <button\n              type='button'\n              onClick={() => toggleAll(true)}\n              className='inline-flex items-center gap-1 text-emerald-400 transition hover:text-emerald-300'\n            >\n              <Eye className='h-3.5 w-3.5' />\n              Show all\n            </button>\n            <button\n              type='button'\n              onClick={() => toggleAll(false)}\n              className='inline-flex items-center gap-1 text-slate-400 transition hover:text-slate-300'\n            >\n              <EyeOff className='h-3.5 w-3.5' />\n              Hide all\n            </button>\n          </div>\n        </div>\n\n        <div className='mt-3 space-y-2'>\n          {calendars.map(calendar => (\n            <button\n              key={calendar.id}\n              type='button'\n              onClick={() =>\n                toggleCalendarVisibility(calendar.id, !calendar.isVisible)\n              }\n              className={`flex w-full items-center justify-between rounded-lg border px-3 py-2 text-left text-sm shadow-sm transition ${\n                calendar.isVisible\n                  ? 'border-slate-700 bg-slate-900/70 hover:border-slate-600'\n                  : 'border-slate-800 bg-slate-950 text-slate-500 hover:border-slate-800/80'\n              }`}\n            >\n              <span className='inline-flex items-center gap-2'>\n                <span className='text-lg leading-none'>\n                  {calendar.icon ?? ''}\n                </span>\n                <span>{calendar.name}</span>\n              </span>\n              <span\n                className='h-2.5 w-2.5 rounded-full'\n                style={{\n                  backgroundColor: calendar.isVisible\n                    ? (calendar.colors?.lineColor ?? '#22c55e')\n                    : 'rgba(148, 163, 184, 0.45)',\n                }}\n              />\n            </button>\n          ))}\n        </div>\n\n        <div className='mt-6 rounded-xl border border-slate-800 bg-slate-900/70 p-4'>\n          <p className='text-xs font-semibold tracking-wide text-slate-400 uppercase'>\n            Upcoming\n          </p>\n          <ul className='mt-3 space-y-3 text-sm'>\n            {upcoming.length ? (\n              upcoming.map(item => (\n                <li key={item.event.id} className='space-y-1'>\n                  <p className='font-medium text-white'>{item.event.title}</p>\n                  <p className='text-xs text-slate-400'>\n                    {item.startDate.toLocaleString(undefined, {\n                      month: 'short',\n                      day: 'numeric',\n                      hour: '2-digit',\n                      minute: '2-digit',\n                    })}\n                  </p>\n                  {item.calendar && (\n                    <p className='text-xs text-slate-500'>\n                      {item.calendar.icon} {item.calendar.name}\n                    </p>\n                  )}\n                </li>\n              ))\n            ) : (\n              <li className='text-xs text-slate-500'>\n                No upcoming events scheduled.\n              </li>\n            )}\n          </ul>\n        </div>\n      </div>\n\n      <div className='border-t border-white/10 px-4 py-3 text-xs text-slate-500'>\n        {hiddenCount\n          ? `${hiddenCount} calendar${hiddenCount > 1 ? 's' : ''} hidden`\n          : 'All calendars visible'}\n      </div>\n    </div>\n  );\n};\n\nconst ShowcaseWrapper: React.FC<{ children: React.ReactNode }> = ({\n  children,\n}) => <div className='mt-6'>{children}</div>;\n\nexport const SidebarCustomShowcase: React.FC = () => {\n  const renderSidebar = useCallback(\n    (props: CalendarSidebarRenderProps) => <CustomSidebarPanel {...props} />,\n    []\n  );\n\n  const sidebarPlugin = useMemo(\n    () =>\n      createSidebarPlugin({\n        createCalendarMode: 'modal',\n        width: 260,\n        render: renderSidebar,\n      }),\n    [renderSidebar]\n  );\n  const calendar = useSidebarCalendar(sidebarPlugin);\n\n  return (\n    <ShowcaseWrapper>\n      <DayFlowCalendar calendar={calendar} />\n    </ShowcaseWrapper>\n  );\n};\n"
  },
  {
    "path": "website/components/showcase/livedemo/CalendarViewer.tsx",
    "content": "'use client';\n\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  DayFlowCalendarProps,\n  UseCalendarAppReturn,\n  CalendarAppConfig,\n  CalendarSearchProps,\n} from '@dayflow/react';\nimport React, { useEffect } from 'react';\n\ninterface CalendarViewerProps {\n  config: CalendarAppConfig;\n  calendarRef: React.MutableRefObject<UseCalendarAppReturn | null>;\n  version: string;\n  search?: CalendarSearchProps;\n  collapsedSafeAreaLeft?: number;\n  titleBarSlot?: DayFlowCalendarProps['titleBarSlot'];\n}\n\nexport function CalendarViewer({\n  config,\n  calendarRef,\n  version,\n  search,\n  collapsedSafeAreaLeft,\n  titleBarSlot,\n}: CalendarViewerProps) {\n  const calendar = useCalendarApp(config, version);\n  useEffect(() => {\n    calendarRef.current = calendar;\n  }, [calendar, calendarRef]);\n\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      search={search}\n      collapsedSafeAreaLeft={collapsedSafeAreaLeft}\n      titleBarSlot={titleBarSlot}\n    />\n  );\n}\n"
  },
  {
    "path": "website/components/showcase/livedemo/ControlPanel.tsx",
    "content": "'use client';\n\nimport { TimeZone } from '@dayflow/core';\nimport { ViewType } from '@dayflow/react';\nimport { CircleAlert, Inbox, PanelRightClose, RefreshCw } from 'lucide-react';\nimport { useMemo, useState } from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { Label } from '@/components/ui/label';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip';\nimport { cn } from '@/lib/utils';\n\nimport { MiniDotsFeature } from './MiniDotsFeature';\nimport { MultiCalFeature } from './MultiCalFeature';\nimport { ThemeColorColumn } from './ThemeColorColumn';\nimport {\n  CalendarFeatures,\n  CalendarSelections,\n  DEFAULT_THEME_COLOR,\n  YearMode,\n  SwitcherMode,\n} from './types';\n\ninterface ControlPanelProps {\n  features: CalendarFeatures;\n  selections: CalendarSelections;\n  onUpdateFeatures: (updates: Partial<CalendarFeatures>) => void;\n  onUpdateSelections: (updates: Partial<CalendarSelections>) => void;\n  onPreviewThemeColor: (color: string) => void;\n  localesOptions: Array<{ label: string; value: string }>;\n  showControls: boolean;\n}\n\nconst VIEW_OPTIONS = [\n  { label: 'Day', value: ViewType.DAY },\n  { label: 'Week', value: ViewType.WEEK },\n  { label: 'Month', value: ViewType.MONTH },\n  { label: 'Year', value: ViewType.YEAR },\n  { label: 'Agenda', value: ViewType.AGENDA },\n];\n\nconst TIME_ZONE_OPTIONS = Object.entries(TimeZone).toSorted((a, b) =>\n  a[1].localeCompare(b[1])\n);\n\nconst formatTimeZoneLabel = (timeZone: string) =>\n  timeZone\n    .split('/')\n    .map(part => part.replaceAll('_', ' '))\n    .join(' / ');\n\nfunction FeatureCheckbox({\n  id,\n  label,\n  checked,\n  onCheckedChange,\n}: {\n  id: string;\n  label: string;\n  checked: boolean;\n  onCheckedChange: (checked: boolean) => void;\n}) {\n  return (\n    <div className='flex items-center space-x-2'>\n      <Checkbox\n        id={id}\n        checked={checked}\n        onCheckedChange={newChecked => onCheckedChange(newChecked === true)}\n        className='data-[state=checked]:border-black data-[state=checked]:bg-black data-[state=checked]:text-white dark:data-[state=checked]:border-white dark:data-[state=checked]:bg-white dark:data-[state=checked]:text-black'\n      />\n      <Label\n        htmlFor={id}\n        className='cursor-pointer text-xs font-normal text-slate-600 dark:text-slate-400'\n      >\n        {label}\n      </Label>\n    </div>\n  );\n}\n\nexport function ControlPanel({\n  features,\n  selections,\n  onUpdateFeatures,\n  onUpdateSelections,\n  onPreviewThemeColor,\n  localesOptions,\n  showControls,\n}: ControlPanelProps) {\n  const [searchTimeZone, setSearchTimeZone] = useState('');\n  const [searchSecondaryTz, setSearchSecondaryTz] = useState('');\n\n  const filterTimeZones = (search: string) => {\n    if (!search) return TIME_ZONE_OPTIONS;\n    const lowerSearch = search.toLowerCase();\n    return TIME_ZONE_OPTIONS.filter(\n      ([key, value]) =>\n        key.toLowerCase().includes(lowerSearch) ||\n        value.toLowerCase().includes(lowerSearch) ||\n        formatTimeZoneLabel(value).toLowerCase().includes(lowerSearch)\n    );\n  };\n\n  const filteredPrimaryTimeZones = useMemo(\n    () => filterTimeZones(searchTimeZone),\n    [searchTimeZone]\n  );\n\n  const filteredSecondaryTimeZones = useMemo(\n    () => filterTimeZones(searchSecondaryTz),\n    [searchSecondaryTz]\n  );\n  const themeColor = selections.themeColor || DEFAULT_THEME_COLOR;\n\n  const toggleView = (view: string) => {\n    const next = selections.selectedViews.includes(view)\n      ? selections.selectedViews.filter(v => v !== view)\n      : [...selections.selectedViews, view];\n    onUpdateSelections({ selectedViews: next.length === 0 ? [view] : next });\n  };\n\n  return (\n    <Card\n      className={cn(\n        'relative z-30 border-slate-200 bg-slate-50/50 shadow-none dark:border-slate-800 dark:bg-gray-900/50',\n        showControls ? 'block' : 'hidden'\n      )}\n    >\n      <CardContent className='flex flex-col gap-3 overflow-visible p-4 pt-4'>\n        {/* Row 1: Features and Views */}\n        <div className='flex items-start justify-between gap-8'>\n          {/* Features Column */}\n          <div className='flex-1 space-y-3'>\n            <h3 className='text-xs font-semibold tracking-tight text-slate-900 uppercase dark:text-slate-100'>\n              Features\n            </h3>\n            <div className='flex flex-wrap gap-x-4 gap-y-2'>\n              <FeatureCheckbox\n                id='sidebar'\n                label='Sidebar'\n                checked={features.showSidebar}\n                onCheckedChange={checked =>\n                  onUpdateFeatures({ showSidebar: checked })\n                }\n              />\n              <FeatureCheckbox\n                id='header'\n                label='Header'\n                checked={features.showHeader}\n                onCheckedChange={checked =>\n                  onUpdateFeatures({ showHeader: checked })\n                }\n              />\n              <FeatureCheckbox\n                id='drag'\n                label='Drag'\n                checked={features.enableDrag}\n                onCheckedChange={checked =>\n                  onUpdateFeatures({ enableDrag: checked })\n                }\n              />\n              <div className='flex items-center space-x-2'>\n                <Checkbox\n                  id='shortcuts'\n                  checked={features.enableShortcuts}\n                  onCheckedChange={checked =>\n                    onUpdateFeatures({ enableShortcuts: checked === true })\n                  }\n                  className='data-[state=checked]:border-black data-[state=checked]:bg-black data-[state=checked]:text-white dark:data-[state=checked]:border-white dark:data-[state=checked]:bg-white dark:data-[state=checked]:text-black'\n                />\n                <div className='flex items-center gap-1'>\n                  <Label\n                    htmlFor='shortcuts'\n                    className='cursor-pointer text-xs font-normal text-slate-600 dark:text-slate-400'\n                  >\n                    Keys\n                  </Label>\n                  <Tooltip>\n                    <TooltipTrigger asChild>\n                      <div className='inline-flex cursor-help items-center'>\n                        <CircleAlert className='h-3 w-3 text-slate-400' />\n                      </div>\n                    </TooltipTrigger>\n                    <TooltipContent side='top' className='w-56 p-3'>\n                      <p className='mb-2 text-sm font-semibold'>Shortcuts</p>\n                      <ul className='space-y-1.5 text-xs'>\n                        <li className='flex justify-between gap-4'>\n                          <span>Search</span>{' '}\n                          <kbd className='font-sans opacity-70'>⌘F</kbd>\n                        </li>\n                        <li className='flex justify-between gap-4'>\n                          <span>Today</span>{' '}\n                          <kbd className='font-sans opacity-70'>⌘T</kbd>\n                        </li>\n                        <li className='flex justify-between gap-4'>\n                          <span>New Event</span>{' '}\n                          <kbd className='font-sans opacity-70'>⌘N</kbd>\n                        </li>\n                        <li className='flex justify-between gap-4'>\n                          <span>Undo</span>{' '}\n                          <kbd className='font-sans opacity-70'>⌘Z</kbd>\n                        </li>\n                        <li className='flex justify-between gap-4'>\n                          <span>Event Switch</span>{' '}\n                          <kbd className='font-sans opacity-70'>⌘Tab</kbd>\n                        </li>\n                        <li className='flex justify-between gap-4'>\n                          <span>Prev/Next</span>{' '}\n                          <kbd className='font-sans opacity-70'>← / →</kbd>\n                        </li>\n                        <li className='flex justify-between gap-4'>\n                          <span>Copy Event</span>{' '}\n                          <kbd className='font-sans opacity-70'>⌘C</kbd>\n                        </li>\n                        <li className='flex justify-between gap-4'>\n                          <span>Paste Event</span>{' '}\n                          <kbd className='font-sans opacity-70'>⌘V</kbd>\n                        </li>\n                        <li className='flex justify-between gap-4'>\n                          <span>Cut Event</span>{' '}\n                          <kbd className='font-sans opacity-70'>⌘X</kbd>\n                        </li>\n                        <li className='flex justify-between gap-4'>\n                          <span>Delete</span>{' '}\n                          <kbd className='font-sans opacity-70'>⌫</kbd>\n                        </li>\n                      </ul>\n                    </TooltipContent>\n                  </Tooltip>\n                </div>\n              </div>\n\n              <MiniDotsFeature\n                checked={features.showEventDots}\n                onUpdateFeatures={onUpdateFeatures}\n              />\n\n              <div className='flex items-center space-x-2'>\n                <Checkbox\n                  id='calendar-groups'\n                  checked={features.showCalendarGroups}\n                  onCheckedChange={checked =>\n                    onUpdateFeatures({ showCalendarGroups: checked === true })\n                  }\n                  className='data-[state=checked]:border-black data-[state=checked]:bg-black data-[state=checked]:text-white dark:data-[state=checked]:border-white dark:data-[state=checked]:bg-white dark:data-[state=checked]:text-black'\n                />\n                <div className='flex items-center gap-1'>\n                  <Label\n                    htmlFor='calendar-groups'\n                    className='cursor-pointer text-xs font-normal text-slate-600 dark:text-slate-400'\n                  >\n                    Cal Groups\n                  </Label>\n                  <Tooltip>\n                    <TooltipTrigger asChild>\n                      <div className='inline-flex cursor-help items-center'>\n                        <CircleAlert className='h-3 w-3 text-slate-400' />\n                      </div>\n                    </TooltipTrigger>\n                    <TooltipContent side='top' className='w-64 p-3'>\n                      <p className='mb-2 text-sm font-semibold'>\n                        Calendar Groups\n                      </p>\n                      <p className='mb-2 text-xs text-slate-500 dark:text-slate-400'>\n                        Groups calendars by source in the sidebar. Each source\n                        is shown as a collapsible section header.\n                      </p>\n                    </TooltipContent>\n                  </Tooltip>\n                </div>\n              </div>\n\n              <FeatureCheckbox\n                id='read-only'\n                label='Read Only'\n                checked={features.readOnly}\n                onCheckedChange={checked =>\n                  onUpdateFeatures({ readOnly: checked })\n                }\n              />\n\n              {features.showSidebar && (\n                <div className='flex items-center space-x-2'>\n                  <Checkbox\n                    id='collapsed-safe-area'\n                    checked={features.collapsedSafeAreaLeft}\n                    onCheckedChange={checked =>\n                      onUpdateFeatures({\n                        collapsedSafeAreaLeft: checked === true,\n                      })\n                    }\n                    className='data-[state=checked]:border-black data-[state=checked]:bg-black data-[state=checked]:text-white dark:data-[state=checked]:border-white dark:data-[state=checked]:bg-white dark:data-[state=checked]:text-black'\n                  />\n                  <div className='flex items-center gap-1'>\n                    <Label\n                      htmlFor='collapsed-safe-area'\n                      className='cursor-pointer text-xs font-normal text-slate-600 dark:text-slate-400'\n                    >\n                      Title Bar Slot\n                    </Label>\n                    <Tooltip>\n                      <TooltipTrigger asChild>\n                        <div className='inline-flex cursor-help items-center'>\n                          <CircleAlert className='h-3 w-3 text-slate-400' />\n                        </div>\n                      </TooltipTrigger>\n                      <TooltipContent side='top' className='w-64 p-3'>\n                        <p className='mb-1 text-sm font-semibold'>\n                          Title Bar Slot\n                        </p>\n                        <p className='mb-2 text-xs text-slate-500 dark:text-slate-400'>\n                          <code className='text-slate-700 dark:text-slate-300'>\n                            titleBarSlot\n                          </code>{' '}\n                          renders custom content in the top-left corner, while{' '}\n                          <code className='text-slate-700 dark:text-slate-300'>\n                            collapsedSafeAreaLeft\n                          </code>{' '}\n                          reserves that pixel width so the calendar never slides\n                          under it when the sidebar collapses. Use both together\n                          in frameless Mac apps.\n                        </p>\n                        <div className='overflow-hidden rounded-md border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900'>\n                          <div className='flex items-center gap-1 px-3 py-2'>\n                            <div className='flex items-center gap-1.5'>\n                              <div\n                                className='h-3 w-3 rounded-full bg-[#ff5f57]'\n                                style={{\n                                  border: '0.5px solid rgba(0,0,0,0.12)',\n                                }}\n                              />\n                              <div\n                                className='h-3 w-3 rounded-full bg-[#febc2e]'\n                                style={{\n                                  border: '0.5px solid rgba(0,0,0,0.12)',\n                                }}\n                              />\n                              <div\n                                className='h-3 w-3 rounded-full bg-[#28c840]'\n                                style={{\n                                  border: '0.5px solid rgba(0,0,0,0.12)',\n                                }}\n                              />\n                            </div>\n                            <PanelRightClose\n                              size={14}\n                              className='ml-3 text-gray-500'\n                            />\n                            <Inbox size={14} className='text-gray-500' />\n                            <RefreshCw\n                              size={14}\n                              className='ml-1 text-gray-500'\n                            />\n                          </div>\n                        </div>\n                      </TooltipContent>\n                    </Tooltip>\n                  </div>\n                </div>\n              )}\n\n              <MultiCalFeature\n                checked={features.showMultiCalendar}\n                onUpdateFeatures={onUpdateFeatures}\n              />\n            </div>\n          </div>\n\n          {/* Views Column */}\n          <div className='space-y-3'>\n            <h3 className='text-xs font-semibold tracking-tight text-slate-900 uppercase dark:text-slate-100'>\n              Views\n            </h3>\n            <div className='flex flex-wrap gap-1.5'>\n              {VIEW_OPTIONS.map(opt => {\n                const isSelected = selections.selectedViews.includes(opt.value);\n                return (\n                  <Button\n                    key={opt.value}\n                    size='sm'\n                    variant={isSelected ? 'default' : 'ghost'}\n                    className={cn(\n                      'h-7 rounded-full px-2.5 text-[11px] transition-all',\n                      isSelected\n                        ? 'bg-black text-white hover:bg-black/90 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-200'\n                        : 'bg-transparent text-black hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800'\n                    )}\n                    onClick={() => toggleView(opt.value)}\n                  >\n                    {opt.label}\n                  </Button>\n                );\n              })}\n            </div>\n          </div>\n        </div>\n\n        {/* Row 2: Localization, Timezone, Secondary TZ, Switcher Mode */}\n        <div className='flex items-start gap-8 border-t border-slate-200 pt-4 dark:border-slate-800'>\n          {/* Localization Column */}\n          <div className='space-y-1'>\n            <h3 className='text-xs font-semibold tracking-tight text-slate-900 uppercase dark:text-slate-100'>\n              Language\n            </h3>\n            <Select\n              value={selections.locale}\n              onValueChange={val => onUpdateSelections({ locale: val })}\n            >\n              <SelectTrigger className='h-7 w-35 text-xs'>\n                <SelectValue placeholder='Select Locale' />\n              </SelectTrigger>\n              <SelectContent>\n                {localesOptions.map(opt => (\n                  <SelectItem\n                    key={opt.value}\n                    value={opt.value}\n                    className='cursor-pointer text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                  >\n                    {opt.label}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n\n          {/* Timezone Column */}\n          <div className='space-y-1'>\n            <div className='flex items-center gap-1'>\n              <h3 className='text-xs font-semibold tracking-tight text-slate-900 uppercase dark:text-slate-100'>\n                Timezone\n              </h3>\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <div className='inline-flex cursor-help items-center'>\n                    <CircleAlert className='h-3 w-3 text-slate-400' />\n                  </div>\n                </TooltipTrigger>\n                <TooltipContent side='top' className='w-60 p-3'>\n                  <p className='mb-1 text-sm font-semibold'>\n                    Calendar Timezone\n                  </p>\n                  <p className='text-xs text-slate-500 dark:text-slate-400'>\n                    Controls the primary display and editing timezone across\n                    views.\n                  </p>\n                </TooltipContent>\n              </Tooltip>\n            </div>\n            <Select\n              value={selections.timeZone || 'device-local'}\n              onValueChange={val => {\n                onUpdateSelections({\n                  timeZone: val === 'device-local' ? undefined : val,\n                });\n                setSearchTimeZone('');\n              }}\n              onOpenChange={open => !open && setSearchTimeZone('')}\n            >\n              <SelectTrigger className='h-7 w-35 text-xs'>\n                <SelectValue placeholder='Select TZ' />\n              </SelectTrigger>\n              <SelectContent className='max-h-80 w-56 overflow-hidden p-0'>\n                <div className='flex items-center border-b border-slate-100 px-2 dark:border-slate-800'>\n                  <input\n                    placeholder='Search timezone...'\n                    className='h-9 w-full bg-transparent py-2 text-xs outline-none'\n                    value={searchTimeZone}\n                    onChange={e => setSearchTimeZone(e.target.value)}\n                    onKeyDown={e => e.key === ' ' && e.stopPropagation()}\n                  />\n                </div>\n                <div className='max-h-60 overflow-y-auto p-1'>\n                  <SelectItem\n                    value='device-local'\n                    className='text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                  >\n                    Device local\n                  </SelectItem>\n                  {filteredPrimaryTimeZones.map(([key, value]) => (\n                    <SelectItem\n                      key={key}\n                      value={value}\n                      className='text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                    >\n                      <span className='truncate'>\n                        {formatTimeZoneLabel(value)}\n                      </span>\n                    </SelectItem>\n                  ))}\n                </div>\n              </SelectContent>\n            </Select>\n          </div>\n\n          <div className='space-y-1'>\n            <div className='flex items-center gap-1'>\n              <h3 className='text-xs font-semibold tracking-tight text-slate-900 uppercase dark:text-slate-100'>\n                Secondary TZ\n              </h3>\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <div className='inline-flex cursor-help items-center'>\n                    <CircleAlert className='h-3 w-3 text-slate-400' />\n                  </div>\n                </TooltipTrigger>\n                <TooltipContent side='top' className='w-60 p-3'>\n                  <p className='mb-1 text-sm font-semibold'>\n                    Secondary Timeline\n                  </p>\n                  <p className='text-xs text-slate-500 dark:text-slate-400'>\n                    Adds a second reference timeline in Day and Week views.\n                  </p>\n                </TooltipContent>\n              </Tooltip>\n            </div>\n            <Select\n              value={selections.secondaryTimeZone || 'none'}\n              onValueChange={val => {\n                onUpdateSelections({\n                  secondaryTimeZone: val === 'none' ? undefined : val,\n                });\n                setSearchSecondaryTz('');\n              }}\n              onOpenChange={open => !open && setSearchSecondaryTz('')}\n            >\n              <SelectTrigger className='h-7 w-35 text-xs'>\n                <SelectValue placeholder='Select TZ' />\n              </SelectTrigger>\n              <SelectContent className='max-h-80 w-56 overflow-hidden p-0'>\n                <div className='flex items-center border-b border-slate-100 px-2 dark:border-slate-800'>\n                  <input\n                    placeholder='Search timezone...'\n                    className='h-9 w-full bg-transparent py-2 text-xs outline-none'\n                    value={searchSecondaryTz}\n                    onChange={e => setSearchSecondaryTz(e.target.value)}\n                    onKeyDown={e => e.key === ' ' && e.stopPropagation()}\n                  />\n                </div>\n                <div className='max-h-60 overflow-y-auto p-1'>\n                  <SelectItem\n                    value='none'\n                    className='text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                  >\n                    None\n                  </SelectItem>\n                  {filteredSecondaryTimeZones.map(([key, value]) => (\n                    <SelectItem\n                      key={key}\n                      value={value}\n                      className='text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                    >\n                      <span className='truncate'>\n                        {formatTimeZoneLabel(value)}\n                      </span>\n                    </SelectItem>\n                  ))}\n                </div>\n              </SelectContent>\n            </Select>\n          </div>\n\n          {/* New Switcher Mode Control */}\n          <div className='space-y-1'>\n            <div className='flex items-center gap-1'>\n              <h3 className='text-xs font-semibold tracking-tight text-slate-900 uppercase dark:text-slate-100'>\n                Switcher Mode\n              </h3>\n              <Tooltip>\n                <TooltipTrigger asChild>\n                  <div className='inline-flex cursor-help items-center'>\n                    <CircleAlert className='h-3 w-3 text-slate-400' />\n                  </div>\n                </TooltipTrigger>\n                <TooltipContent side='top' className='w-60 p-3'>\n                  <p className='mb-1 text-sm font-semibold'>\n                    View Switcher Style\n                  </p>\n                  <p className='text-xs text-slate-500 dark:text-slate-400'>\n                    Changes how view switching is presented in the header: as\n                    individual buttons or a dropdown menu.\n                  </p>\n                </TooltipContent>\n              </Tooltip>\n            </div>\n            <Select\n              value={selections.switcherMode}\n              onValueChange={val =>\n                onUpdateSelections({ switcherMode: val as SwitcherMode })\n              }\n            >\n              <SelectTrigger className='h-7 w-35 text-xs'>\n                <SelectValue placeholder='Select Mode' />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem\n                  value='buttons'\n                  className='cursor-pointer text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                >\n                  Buttons\n                </SelectItem>\n                <SelectItem\n                  value='select'\n                  className='cursor-pointer text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                >\n                  Select\n                </SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n\n          {/* Sidebar Order Column */}\n          {features.showSidebar && (\n            <div className='space-y-1'>\n              <div className='flex items-center gap-1'>\n                <h3 className='text-xs font-semibold tracking-tight text-slate-900 uppercase dark:text-slate-100'>\n                  Sidebar Order\n                </h3>\n                <Tooltip>\n                  <TooltipTrigger asChild>\n                    <div className='inline-flex cursor-help items-center'>\n                      <CircleAlert className='h-3 w-3 text-slate-400' />\n                    </div>\n                  </TooltipTrigger>\n                  <TooltipContent side='top' className='w-60 p-3'>\n                    <p className='mb-1 text-sm font-semibold'>Sidebar Order</p>\n                    <p className='text-xs text-slate-500 dark:text-slate-400'>\n                      Controls the vertical order of the two sidebar sections:\n                      the calendar list and the mini calendar. Use{' '}\n                      <code className='text-slate-700 dark:text-slate-300'>\n                        componentsOrder\n                      </code>{' '}\n                      in{' '}\n                      <code className='text-slate-700 dark:text-slate-300'>\n                        createSidebarPlugin\n                      </code>{' '}\n                      to configure this.\n                    </p>\n                  </TooltipContent>\n                </Tooltip>\n              </div>\n              <Select\n                value={\n                  features.sidebarOrder?.[0] === 'calendarList'\n                    ? 'list-first'\n                    : 'mini-first'\n                }\n                onValueChange={val =>\n                  onUpdateFeatures({\n                    sidebarOrder:\n                      val === 'list-first'\n                        ? ['calendarList', 'miniCalendar']\n                        : ['miniCalendar', 'calendarList'],\n                  })\n                }\n              >\n                <SelectTrigger className='h-7 w-35 text-xs'>\n                  <SelectValue />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem\n                    value='list-first'\n                    className='cursor-pointer text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                  >\n                    List → Mini\n                  </SelectItem>\n                  <SelectItem\n                    value='mini-first'\n                    className='cursor-pointer text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                  >\n                    Mini → List\n                  </SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n          )}\n\n          {/* Year View Mode — always shown in Row 2 */}\n          <div className='space-y-1'>\n            <h3 className='text-xs font-semibold tracking-tight text-slate-900 uppercase dark:text-slate-100'>\n              Year View Mode\n            </h3>\n            <Select\n              value={selections.yearMode}\n              onValueChange={val =>\n                onUpdateSelections({ yearMode: val as YearMode })\n              }\n            >\n              <SelectTrigger className='h-7 w-35 px-2 text-xs'>\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem\n                  value='fixed-week'\n                  className='cursor-pointer text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                >\n                  Fixed Week\n                </SelectItem>\n                <SelectItem\n                  value='canvas'\n                  className='cursor-pointer text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                >\n                  Canvas\n                </SelectItem>\n                <SelectItem\n                  value='grid'\n                  className='cursor-pointer text-xs focus:bg-slate-100 dark:focus:bg-slate-800'\n                >\n                  Grid Year\n                </SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n\n          <ThemeColorColumn\n            themeColor={themeColor}\n            onPreviewThemeColor={onPreviewThemeColor}\n            onUpdateSelections={onUpdateSelections}\n          />\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "website/components/showcase/livedemo/MiniDotsFeature.tsx",
    "content": "'use client';\n\nimport { CircleAlert } from 'lucide-react';\n\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { Label } from '@/components/ui/label';\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip';\n\nimport { CalendarFeatures } from './types';\n\ninterface MiniDotsFeatureProps {\n  checked: boolean;\n  onUpdateFeatures: (updates: Partial<CalendarFeatures>) => void;\n}\n\nexport function MiniDotsFeature({\n  checked,\n  onUpdateFeatures,\n}: MiniDotsFeatureProps) {\n  return (\n    <div className='flex items-center space-x-2'>\n      <Checkbox\n        id='event-dots'\n        checked={checked}\n        onCheckedChange={newChecked =>\n          onUpdateFeatures({ showEventDots: newChecked === true })\n        }\n        className='data-[state=checked]:border-black data-[state=checked]:bg-black data-[state=checked]:text-white dark:data-[state=checked]:border-white dark:data-[state=checked]:bg-white dark:data-[state=checked]:text-black'\n      />\n      <div className='flex items-center gap-1'>\n        <Label\n          htmlFor='event-dots'\n          className='cursor-pointer text-xs font-normal text-slate-600 dark:text-slate-400'\n        >\n          Mini Dots\n        </Label>\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <div className='inline-flex cursor-help items-center'>\n              <CircleAlert className='h-3 w-3 text-slate-400' />\n            </div>\n          </TooltipTrigger>\n          <TooltipContent\n            side='top'\n            className='w-80 overflow-hidden border-slate-200 p-0 shadow-xl dark:border-slate-800'\n          >\n            <div className='border-b border-slate-100 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-900'>\n              <p className='mb-1 text-sm font-semibold'>Mini Calendar Dots</p>\n              <p className='text-[11px] leading-relaxed text-slate-500 dark:text-slate-400'>\n                Shows colored dots below dates in the sidebar mini calendar.\n                Each dot represents a unique calendar color with events on that\n                day.\n              </p>\n            </div>\n            <div className='bg-white p-3 dark:bg-slate-950'>\n              <div className='space-y-1.5'>\n                <p className='text-[10px] font-bold tracking-wider text-slate-400 uppercase'>\n                  Mini Calendar Preview\n                </p>\n                <div className='relative h-14 w-full rounded-md border border-slate-100 bg-slate-50/50 p-1 dark:border-slate-800 dark:bg-slate-900/50'>\n                  <div className='grid h-full grid-cols-4 border border-slate-200/50 dark:border-slate-800/50'>\n                    {/* Day 1 */}\n                    <div className='relative flex flex-col items-center border-r border-slate-200/50 p-1 dark:border-slate-800/50'>\n                      <span className='text-[10px] font-medium opacity-60'>\n                        14\n                      </span>\n                      <div className='mt-0.5 flex gap-0.5'>\n                        <div className='h-1 w-1 rounded-full bg-blue-500' />\n                      </div>\n                    </div>\n                    {/* Day 2 (Today) */}\n                    <div className='relative flex flex-col items-center border-r border-slate-200/50 bg-white p-1 dark:border-slate-800/50 dark:bg-slate-950'>\n                      <span className='flex h-4 w-4 items-center justify-center rounded-full bg-blue-600 text-[10px] font-bold text-white'>\n                        15\n                      </span>\n                      <div className='mt-0.5 flex gap-0.5'>\n                        <div className='h-1 w-1 rounded-full bg-blue-500' />\n                        <div className='h-1 w-1 rounded-full bg-emerald-500' />\n                        <div className='h-1 w-1 rounded-full bg-rose-500' />\n                      </div>\n                    </div>\n                    {/* Day 3 */}\n                    <div className='relative flex flex-col items-center border-r border-slate-200/50 p-1 dark:border-slate-800/50'>\n                      <span className='text-[10px] font-medium opacity-60'>\n                        16\n                      </span>\n                      <div className='mt-0.5 flex gap-0.5'>\n                        <div className='h-1 w-1 rounded-full bg-amber-500' />\n                        <div className='h-1 w-1 rounded-full bg-purple-500' />\n                      </div>\n                    </div>\n                    {/* Day 4 */}\n                    <div className='relative flex flex-col items-center p-1'>\n                      <span className='text-[10px] font-medium opacity-60'>\n                        17\n                      </span>\n                      <div className='mt-0.5 flex gap-0.5'></div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/components/showcase/livedemo/MultiCalFeature.tsx",
    "content": "'use client';\n\nimport { CircleAlert } from 'lucide-react';\n\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { Label } from '@/components/ui/label';\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip';\n\nimport { CalendarFeatures } from './types';\n\ninterface MultiCalFeatureProps {\n  checked: boolean;\n  onUpdateFeatures: (updates: Partial<CalendarFeatures>) => void;\n}\n\nexport function MultiCalFeature({\n  checked,\n  onUpdateFeatures,\n}: MultiCalFeatureProps) {\n  return (\n    <div className='flex items-center space-x-2'>\n      <Checkbox\n        id='multi-calendar'\n        checked={checked}\n        onCheckedChange={newChecked =>\n          onUpdateFeatures({ showMultiCalendar: newChecked === true })\n        }\n        className='data-[state=checked]:border-black data-[state=checked]:bg-black data-[state=checked]:text-white dark:data-[state=checked]:border-white dark:data-[state=checked]:bg-white dark:data-[state=checked]:text-black'\n      />\n      <div className='flex items-center gap-1'>\n        <Label\n          htmlFor='multi-calendar'\n          className='cursor-pointer text-xs font-normal text-slate-600 dark:text-slate-400'\n        >\n          Multi Cal\n        </Label>\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <div className='inline-flex cursor-help items-center'>\n              <CircleAlert className='h-3 w-3 text-slate-400' />\n            </div>\n          </TooltipTrigger>\n          <TooltipContent\n            side='top'\n            className='w-80 overflow-hidden border-slate-200 p-0 shadow-xl dark:border-slate-800'\n          >\n            <div className='border-b border-slate-100 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-900'>\n              <p className='mb-1 text-sm font-semibold'>\n                Multi-calendar Events\n              </p>\n              <p className='text-[11px] leading-relaxed text-slate-500 dark:text-slate-400'>\n                Display a single event across multiple calendars. Perfect for\n                shared team activities or cross-functional blocks.\n              </p>\n            </div>\n            <div className='space-y-4 bg-white p-3 dark:bg-slate-950'>\n              <div className='space-y-1.5'>\n                <p className='text-[10px] font-bold tracking-wider text-slate-400 uppercase'>\n                  Week View Preview (Personal, Wellness)\n                </p>\n                <div\n                  className='relative flex h-10 w-full flex-col justify-center rounded-md border border-slate-100 bg-slate-50/50 p-1.5 dark:border-slate-800 dark:bg-slate-900/50'\n                  style={{\n                    background:\n                      'repeating-linear-gradient(-45deg, rgba(37, 99, 235, 0.08) 0px, rgba(37, 99, 235, 0.08) 6px, rgba(16, 185, 129, 0.08) 6px, rgba(16, 185, 129, 0.08) 12px)',\n                  }}\n                >\n                  <div className='flex h-full items-center gap-2'>\n                    <div\n                      className='pointer-events-none absolute inset-0'\n                      style={{\n                        background:\n                          'repeating-linear-gradient(-45deg, #2563eb 0px, #2563eb 6px, #10b981 6px, #10b981 12px)',\n                        clipPath:\n                          'inset(4px calc(100% - 4px - 3px) 4px 4px round 9999px)',\n                      }}\n                    />\n                    <div className='flex min-w-0 flex-col gap-0.5 pl-3'>\n                      <div className='truncate text-[10px] font-bold'>\n                        Team Sync & Wellness\n                      </div>\n                      <div className='text-[8px] text-slate-400'>\n                        10:00 - 11:30\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n\n              {/* Month View Preview */}\n              <div className='space-y-1.5'>\n                <p className='text-[10px] font-bold tracking-wider text-slate-400 uppercase'>\n                  Month View Preview (Team, Travel, Learning)\n                </p>\n                <div className='relative h-14 w-full rounded-md border border-slate-100 bg-slate-50/50 p-1 dark:border-slate-800 dark:bg-slate-900/50'>\n                  <div className='grid h-full grid-cols-3 border border-slate-200/50 dark:border-slate-800/50'>\n                    <div className='border-r border-slate-200/50 p-0.5 dark:border-slate-800/50'>\n                      <span className='text-[8px] opacity-30'>12</span>\n                    </div>\n                    <div className='border-r border-slate-200/50 bg-white p-0.5 dark:border-slate-800/50 dark:bg-slate-950'>\n                      <span className='text-[8px] font-bold'>13</span>\n                      <div\n                        className='relative mt-0.5 flex h-3 items-center overflow-hidden rounded-[2px] px-1 pl-2'\n                        style={{\n                          background:\n                            'repeating-linear-gradient(-45deg, rgba(37, 99, 235, 0.1) 0px, rgba(37, 99, 235, 0.1) 6px, rgba(236, 72, 153, 0.1) 6px, rgba(236, 72, 153, 0.1) 12px, rgba(236, 72, 153, 0.1) 12px, rgba(20, 184, 166, 0.1) 12px, rgba(20, 184, 166, 0.1) 18px)',\n                        }}\n                      >\n                        {/* Vertical segmented color bar */}\n                        <div\n                          className='absolute top-0 bottom-0 left-0.5 w-[3px] rounded'\n                          style={{\n                            background:\n                              'linear-gradient(to bottom, #2563eb 0%, #2563eb 33.33%, #ec4899 33.33%, #ec4899 66.66%, #14b8a6 66.66%, #14b8a6 100%)',\n                          }}\n                        />\n                        <span className='truncate text-[8px] font-medium'>\n                          Company Off-site\n                        </span>\n                      </div>\n                    </div>\n                    <div className='p-0.5'>\n                      <span className='text-[8px] opacity-30'>14</span>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/components/showcase/livedemo/ThemeColorColumn.tsx",
    "content": "'use client';\n\nimport {\n  BlossomColorPicker,\n  hexToHsl,\n  lightnessToSliderValue,\n} from '@dayflow/blossom-color-picker-react';\nimport type {\n  BlossomColorPickerColor,\n  BlossomColorPickerValue,\n} from '@dayflow/blossom-color-picker-react';\nimport { CircleAlert } from 'lucide-react';\nimport React, { useCallback, useEffect, useMemo, useRef } from 'react';\n\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from '@/components/ui/tooltip';\n\nimport { CalendarSelections } from './types';\n\ninterface ThemeColorColumnProps {\n  themeColor: string;\n  onPreviewThemeColor: (color: string) => void;\n  onUpdateSelections: (updates: Partial<CalendarSelections>) => void;\n}\n\nconst getBlossomValueFromHex = (hex: string): BlossomColorPickerValue => {\n  const { h, s, l } = hexToHsl(hex);\n\n  return {\n    hue: h,\n    saturation: lightnessToSliderValue(l),\n    lightness: l,\n    originalSaturation: s,\n    alpha: 100,\n    layer: l >= 75 ? 'inner' : 'outer',\n  };\n};\n\nexport function ThemeColorColumn({\n  themeColor,\n  onPreviewThemeColor,\n  onUpdateSelections,\n}: ThemeColorColumnProps) {\n  const themePreviewRef = useRef<HTMLDivElement>(null);\n  const blossomThemeValue = useMemo(\n    () => getBlossomValueFromHex(themeColor),\n    [themeColor]\n  );\n\n  useEffect(() => {\n    themePreviewRef.current?.style.setProperty(\n      '--df-live-demo-theme-color',\n      themeColor\n    );\n  }, [themeColor]);\n\n  const handleThemePreview = useCallback(\n    (color: BlossomColorPickerColor) => {\n      themePreviewRef.current?.style.setProperty(\n        '--df-live-demo-theme-color',\n        color.hex\n      );\n      onPreviewThemeColor(color.hex);\n    },\n    [onPreviewThemeColor]\n  );\n\n  const handleThemeCommit = useCallback(\n    (color: BlossomColorPickerColor) => {\n      handleThemePreview(color);\n      onUpdateSelections({ themeColor: color.hex });\n    },\n    [handleThemePreview, onUpdateSelections]\n  );\n\n  return (\n    <div\n      ref={themePreviewRef}\n      className='relative z-50 space-y-1'\n      style={\n        {\n          '--df-live-demo-theme-color': themeColor,\n          '--df-color-primary': themeColor,\n        } as React.CSSProperties\n      }\n    >\n      <div className='flex items-center gap-1'>\n        <h3 className='text-xs font-semibold tracking-tight text-slate-900 uppercase dark:text-slate-100'>\n          Theme\n        </h3>\n        <Tooltip delayDuration={0}>\n          <TooltipTrigger asChild>\n            <div className='inline-flex cursor-help items-center'>\n              <CircleAlert className='h-3 w-3 text-slate-400' />\n            </div>\n          </TooltipTrigger>\n          <TooltipContent\n            side='top'\n            className='w-80 overflow-hidden border-slate-200 p-0 shadow-xl dark:border-slate-800'\n          >\n            <div className='border-b border-slate-100 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-900'>\n              <p className='mb-1 text-sm font-semibold text-slate-900 dark:text-slate-100'>\n                Theme Preview\n              </p>\n              <p className='text-[11px] leading-relaxed text-slate-500 dark:text-slate-400'>\n                Controls the primary accent color for the calendar, including\n                today&apos;s highlight and timeline indicators.\n              </p>\n            </div>\n            <div className='space-y-4 bg-white p-3 dark:bg-slate-950'>\n              <div className='space-y-1.5'>\n                <p className='text-[10px] font-bold tracking-wider text-slate-400 uppercase'>\n                  Today Highlight\n                </p>\n                <div className='relative h-26 overflow-hidden rounded-md border border-slate-100 bg-white p-2 dark:border-slate-800 dark:bg-slate-950'>\n                  <div\n                    className='absolute top-2 right-4 flex h-6 w-6 items-center justify-center rounded-full text-sm font-semibold text-white'\n                    style={{\n                      backgroundColor: themeColor,\n                    }}\n                  >\n                    22\n                  </div>{' '}\n                  <div className='absolute right-2 bottom-3 left-2 space-y-1'>\n                    <div className='flex h-6 items-center gap-2 rounded-r-md bg-blue-50 pr-2 pl-1 text-blue-600 dark:bg-blue-950/40 dark:text-blue-300'>\n                      <span className='h-5 w-1 shrink-0 rounded-full bg-blue-500' />\n                      <span className='min-w-0 flex-1 truncate text-sm font-medium'>\n                        Product Sync\n                      </span>\n                      <span className='shrink-0 text-sm font-medium'>\n                        08:30\n                      </span>\n                    </div>\n                    <div className='flex h-6 items-center gap-2 rounded-r-md bg-orange-50 pr-2 pl-1 text-orange-600 dark:bg-orange-950/40 dark:text-orange-300'>\n                      <span className='h-5 w-1 shrink-0 rounded-full bg-orange-500' />\n                      <span className='min-w-0 flex-1 truncate text-sm font-medium'>\n                        Design Review\n                      </span>\n                      <span className='shrink-0 text-sm font-medium'>\n                        10:30\n                      </span>\n                    </div>\n                  </div>\n                </div>\n              </div>\n\n              <div className='space-y-1.5'>\n                <p className='text-[10px] font-bold tracking-wider text-slate-400 uppercase'>\n                  Current Timeline\n                </p>\n                <div className='relative h-24 overflow-hidden rounded-md border border-slate-100 bg-white dark:border-slate-800 dark:bg-slate-950'>\n                  <div className='absolute inset-y-0 left-0 w-16 bg-white dark:bg-slate-950'>\n                    <span className='absolute top-2 right-2 text-xs font-medium text-slate-500 dark:text-slate-400'>\n                      09:00\n                    </span>\n                    <span className='absolute right-2 bottom-2 text-xs font-medium text-slate-500 dark:text-slate-400'>\n                      10:00\n                    </span>\n                  </div>\n\n                  <div className='absolute top-3 right-0 left-16 h-px bg-slate-200 dark:bg-slate-800' />\n                  <div className='absolute right-0 bottom-3 left-16 h-px bg-slate-200 dark:bg-slate-800' />\n                  <div className='absolute top-0 bottom-0 left-[calc(4rem+25%)] w-px bg-slate-100 dark:bg-slate-800' />\n                  <div className='absolute top-0 bottom-0 left-[calc(4rem+50%)] w-px bg-slate-100 dark:bg-slate-800' />\n                  <div className='absolute top-0 bottom-0 left-[calc(4rem+75%)] w-px bg-slate-100 dark:bg-slate-800' />\n\n                  <div className='absolute top-1/2 right-0 left-4 flex -translate-y-1/2 items-center'>\n                    <div\n                      className='mr-0 rounded px-1.5 py-0.5 text-[11px] font-bold text-white'\n                      style={{\n                        backgroundColor: themeColor,\n                      }}\n                    >\n                      09:30\n                    </div>\n                    <div className='flex flex-1 items-center'>\n                      {[0, 1, 2, 3].map(idx => (\n                        <div key={idx} className='flex flex-1 items-center'>\n                          <div\n                            className='relative h-0.5 w-full'\n                            style={{\n                              backgroundColor: `color-mix(in srgb, ${themeColor} 32%, transparent)`,\n                            }}\n                          >\n                            {idx === 0 && (\n                              <span\n                                className='absolute top-1/2 -left-1 h-2.5 w-2.5 -translate-y-1/2 rounded-full'\n                                style={{\n                                  backgroundColor: themeColor,\n                                }}\n                              />\n                            )}\n                          </div>\n                        </div>\n                      ))}\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </TooltipContent>\n        </Tooltip>\n      </div>\n      <div className='flex h-8 w-8 items-center justify-center'>\n        <BlossomColorPicker\n          key={themeColor}\n          defaultValue={blossomThemeValue}\n          coreSize={28}\n          petalSize={26}\n          showAlphaSlider={true}\n          onCollapse={handleThemeCommit}\n          onChange={handleThemePreview}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "website/components/showcase/livedemo/types.ts",
    "content": "import { ViewType } from '@dayflow/react';\n\nexport const DEFAULT_THEME_COLOR = '#6786e6';\n\nexport type YearMode = 'fixed-week' | 'canvas' | 'grid';\nexport type SwitcherMode = 'buttons' | 'select';\n\nexport interface CalendarFeatures {\n  showSidebar: boolean;\n  showHeader: boolean;\n  enableDrag: boolean;\n  enableShortcuts: boolean;\n  showEventDots: boolean;\n  showCalendarGroups: boolean;\n  showMultiCalendar: boolean;\n  readOnly: boolean;\n  collapsedSafeAreaLeft: boolean;\n  sidebarOrder?: ('calendarList' | 'miniCalendar')[];\n}\n\nexport interface CalendarSelections {\n  locale: string;\n  timeZone?: string;\n  secondaryTimeZone?: string;\n  selectedViews: string[];\n  activeView: ViewType;\n  yearMode: YearMode;\n  switcherMode: SwitcherMode;\n  themeColor?: string;\n}\n"
  },
  {
    "path": "website/components/showcase/mobile-event-detail/MobileEventDetailSimulator.tsx",
    "content": "'use client';\n\nimport { createAgendaView, createYearView } from '@dayflow/core';\nimport type { Event, MobileEventProps } from '@dayflow/core';\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport {\n  DayFlowCalendar,\n  ViewType,\n  createDayView,\n  createMonthView,\n  createWeekView,\n  useCalendarApp,\n} from '@dayflow/react';\nimport { CalendarDays, Check, ChevronLeft, Clock3, Trash2 } from 'lucide-react';\nimport { useTheme } from 'next-themes';\nimport { useEffect, useMemo, useState } from 'react';\nimport type { CSSProperties } from 'react';\n\nimport { getWebsiteCalendars } from '@/utils/palette';\nimport { generateSampleEvents } from '@/utils/sampleData';\n\ntype DraftEvent = NonNullable<MobileEventProps['draftEvent']>;\n\nconst toJsDate = (value: DraftEvent['start'] | DraftEvent['end']) => {\n  if (!value) return new Date();\n\n  if (value instanceof Date) {\n    return value;\n  }\n\n  const asString =\n    typeof value === 'object' && value !== null && 'toString' in value\n      ? value.toString()\n      : String(value);\n\n  const parsed = new Date(asString);\n  return Number.isNaN(parsed.getTime()) ? new Date() : parsed;\n};\n\nconst useDemoCalendar = () => {\n  const { resolvedTheme } = useTheme();\n\n  const memoizedEvents = useMemo(() => generateSampleEvents(), []);\n\n  const views = useMemo(\n    () => [\n      createDayView({\n        scrollToCurrentTime: true,\n      }),\n      createWeekView({\n        scrollToCurrentTime: true,\n      }),\n      createMonthView(),\n      createYearView({ mode: 'fixed-week' }),\n      createAgendaView(),\n    ],\n    []\n  );\n\n  const dragPlugin = useMemo(\n    () =>\n      createDragPlugin({\n        enableDrag: true,\n        enableResize: true,\n        enableCreate: true,\n      }),\n    []\n  );\n\n  const calendars = useMemo(() => getWebsiteCalendars(), []);\n\n  const themeMode = useMemo(() => {\n    if (resolvedTheme === 'dark') return 'dark';\n    if (resolvedTheme === 'light') return 'light';\n    return 'auto';\n  }, [resolvedTheme]);\n\n  return useCalendarApp({\n    views,\n    plugins: [dragPlugin],\n    events: memoizedEvents,\n    calendars,\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    switcherMode: 'buttons',\n    theme: { mode: themeMode },\n  });\n};\n\nconst phoneViewportStyle = {\n  '--df-calendar-height': '100dvh',\n} as CSSProperties;\n\nconst DemoMobileEventDrawer = ({\n  isOpen,\n  onClose,\n  onSave,\n  onEventDelete,\n  draftEvent,\n  accentColor,\n}: MobileEventProps & {\n  accentColor: string;\n}) => {\n  const [title, setTitle] = useState('');\n  const [notes, setNotes] = useState('');\n\n  useEffect(() => {\n    if (!isOpen || !draftEvent) return;\n    setTitle(draftEvent.title ?? '');\n    setNotes(draftEvent.description ?? '');\n  }, [draftEvent, isOpen]);\n\n  if (!isOpen || !draftEvent) return null;\n\n  const start = toJsDate(draftEvent.start);\n  const end = toJsDate(draftEvent.end);\n\n  const handleSave = () => {\n    onSave({\n      ...draftEvent,\n      title: title.trim() || 'Untitled event',\n      description: notes.trim(),\n    } as Event);\n  };\n\n  return (\n    <div className='fixed inset-0 z-[100] flex flex-col bg-white dark:bg-slate-950'>\n      <header className='flex items-center justify-between border-b border-slate-200 bg-white px-4 py-3 dark:border-slate-800 dark:bg-slate-950'>\n        <button\n          type='button'\n          onClick={onClose}\n          className='rounded-full p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-100'\n          aria-label='Close mobile event detail'\n        >\n          <ChevronLeft size={22} />\n        </button>\n\n        <p className='text-sm font-semibold text-slate-900 dark:text-slate-100'>\n          {draftEvent.id ? 'Event details' : 'New event'}\n        </p>\n\n        <button\n          type='button'\n          onClick={handleSave}\n          className='inline-flex h-9 w-9 items-center justify-center rounded-full bg-blue-600 text-white transition hover:bg-blue-500'\n          aria-label='Save event'\n        >\n          <Check size={18} />\n        </button>\n      </header>\n\n      <div className='flex-1 overflow-y-auto bg-slate-50 px-4 py-5 dark:bg-slate-950'>\n        <div className='space-y-4'>\n          <section className='overflow-hidden rounded bg-white shadow-sm ring-1 ring-slate-200/70 dark:bg-slate-900 dark:ring-slate-800'>\n            <div className='flex'>\n              <div\n                className='w-1.5 shrink-0 self-stretch'\n                style={{ backgroundColor: accentColor }}\n              />\n              <div className='min-w-0 flex-1 space-y-4 p-4'>\n                <input\n                  type='text'\n                  value={title}\n                  onChange={event => setTitle(event.target.value)}\n                  placeholder='Add title'\n                  className='w-full border-0 bg-transparent text-xl font-semibold text-slate-950 outline-none placeholder:text-slate-400 dark:text-slate-50'\n                  autoFocus\n                />\n\n                <textarea\n                  value={notes}\n                  onChange={event => setNotes(event.target.value)}\n                  placeholder='Notes'\n                  rows={3}\n                  className='w-full resize-none border-0 bg-transparent text-sm leading-6 text-slate-600 outline-none placeholder:text-slate-400 dark:text-slate-300'\n                />\n              </div>\n            </div>\n          </section>\n\n          <section className='overflow-hidden rounded bg-white shadow-sm ring-1 ring-slate-200/70 dark:bg-slate-900 dark:ring-slate-800'>\n            <div className='flex items-start gap-3 border-b border-slate-100 px-4 py-4 dark:border-slate-800'>\n              <CalendarDays\n                size={18}\n                className='mt-1 text-slate-400 dark:text-slate-500'\n              />\n              <div className='min-w-0'>\n                <p className='text-sm font-medium text-slate-900 dark:text-slate-100'>\n                  {start.toLocaleDateString(undefined, {\n                    weekday: 'long',\n                    month: 'long',\n                    day: 'numeric',\n                  })}\n                </p>\n                <p className='mt-1 text-xs text-slate-500 dark:text-slate-400'>\n                  Starts{' '}\n                  {start.toLocaleTimeString([], {\n                    hour: '2-digit',\n                    minute: '2-digit',\n                  })}\n                </p>\n              </div>\n            </div>\n\n            <div className='flex items-start gap-3 px-4 py-4'>\n              <Clock3\n                size={18}\n                className='mt-1 text-slate-400 dark:text-slate-500'\n              />\n              <div className='min-w-0'>\n                <p className='text-sm font-medium text-slate-900 dark:text-slate-100'>\n                  {end.toLocaleDateString(undefined, {\n                    weekday: 'long',\n                    month: 'long',\n                    day: 'numeric',\n                  })}\n                </p>\n                <p className='mt-1 text-xs text-slate-500 dark:text-slate-400'>\n                  Ends{' '}\n                  {end.toLocaleTimeString([], {\n                    hour: '2-digit',\n                    minute: '2-digit',\n                  })}\n                </p>\n              </div>\n            </div>\n          </section>\n\n          {draftEvent.id && onEventDelete && (\n            <button\n              type='button'\n              onClick={() => onEventDelete(draftEvent.id)}\n              className='flex w-full items-center justify-center gap-2 rounded-[1.2rem] bg-red-50 px-4 py-3 text-sm font-semibold text-red-600 ring-1 ring-red-100 transition hover:bg-red-100 dark:bg-red-950/40 dark:text-red-300 dark:ring-red-900/70 dark:hover:bg-red-950/60'\n            >\n              <Trash2 size={16} />\n              Delete event\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport const MobileEventDetailSimulator = () => {\n  const calendar = useDemoCalendar();\n\n  return (\n    <div\n      className='h-[100dvh] overflow-hidden bg-white dark:bg-slate-950'\n      style={phoneViewportStyle}\n    >\n      <DayFlowCalendar\n        calendar={calendar}\n        mobileEventDetail={(args: MobileEventProps) => {\n          const accentColor =\n            calendar.app\n              .getCalendars()\n              .find(item => item.id === args.draftEvent?.calendarId)?.colors\n              ?.lineColor ?? '#3b82f6';\n\n          return <DemoMobileEventDrawer {...args} accentColor={accentColor} />;\n        }}\n      />\n    </div>\n  );\n};\n\nexport default MobileEventDetailSimulator;\n"
  },
  {
    "path": "website/components/ui/alert.tsx",
    "content": "import { cva } from 'class-variance-authority';\nimport type { VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst alertVariants = cva(\n  'relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:top-4 [&>svg]:left-4 [&>svg]:text-foreground [&>svg+div]:translate-y-[-3px] [&>svg~*]:pl-7',\n  {\n    variants: {\n      variant: {\n        default: 'bg-background text-foreground',\n        destructive:\n          'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',\n        info: 'border-blue-200 bg-blue-50/50 text-blue-800 dark:border-blue-900/30 dark:bg-blue-900/10 dark:text-blue-300 [&>svg]:text-blue-800 dark:[&>svg]:text-blue-300',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  }\n);\n\nconst Alert = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n  <div\n    ref={ref}\n    role='alert'\n    className={cn(alertVariants({ variant }), className)}\n    {...props}\n  />\n));\nAlert.displayName = 'Alert';\n\nconst AlertTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h5\n    ref={ref}\n    className={cn('mb-1 leading-none font-medium tracking-tight', className)}\n    {...props}\n  >\n    {props.children}\n  </h5>\n));\nAlertTitle.displayName = 'AlertTitle';\n\nconst AlertDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn('text-sm [&_p]:leading-relaxed', className)}\n    {...props}\n  />\n));\nAlertDescription.displayName = 'AlertDescription';\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "website/components/ui/badge.tsx",
    "content": "import { cva } from 'class-variance-authority';\nimport type { VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst badgeVariants = cva(\n  'focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-none',\n  {\n    variants: {\n      variant: {\n        default:\n          'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',\n        secondary:\n          'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',\n        destructive:\n          'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',\n        outline: 'text-foreground',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  }\n);\n\nexport interface BadgeProps\n  extends\n    React.HTMLAttributes<HTMLSpanElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <span className={cn(badgeVariants({ variant }), className)} {...props} />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "website/components/ui/button.tsx",
    "content": "import { Slot } from '@radix-ui/react-slot';\nimport { cva } from 'class-variance-authority';\nimport type { VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst buttonVariants = cva(\n  'focus-visible:ring-ring inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap ring-offset-background transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n  {\n    variants: {\n      variant: {\n        default: 'bg-primary text-primary-foreground hover:bg-primary/90',\n        destructive:\n          'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n        outline:\n          'border-input hover:bg-accent hover:text-accent-foreground border bg-background',\n        secondary:\n          'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        link: 'text-primary underline-offset-4 hover:underline',\n      },\n      size: {\n        default: 'h-10 px-4 py-2',\n        sm: 'h-9 rounded-md px-3',\n        lg: 'h-11 rounded-md px-8',\n        icon: 'h-10 w-10',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  }\n);\n\nexport interface ButtonProps\n  extends\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : 'button';\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  }\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "website/components/ui/card.tsx",
    "content": "import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      'rounded-lg border bg-card text-card-foreground shadow-sm',\n      className\n    )}\n    {...props}\n  />\n));\nCard.displayName = 'Card';\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn('flex flex-col space-y-1.5 p-6', className)}\n    {...props}\n  />\n));\nCardHeader.displayName = 'CardHeader';\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\n      'text-2xl leading-none font-semibold tracking-tight',\n      className\n    )}\n    {...props}\n  >\n    {props.children}\n  </h3>\n));\nCardTitle.displayName = 'CardTitle';\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nCardDescription.displayName = 'CardDescription';\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />\n));\nCardContent.displayName = 'CardContent';\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn('flex items-center p-6 pt-0', className)}\n    {...props}\n  />\n));\nCardFooter.displayName = 'CardFooter';\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "website/components/ui/checkbox.tsx",
    "content": "'use client';\n\nimport * as CheckboxPrimitive from '@radix-ui/react-checkbox';\nimport { Check } from 'lucide-react';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <CheckboxPrimitive.Root\n    ref={ref}\n    className={cn(\n      'peer focus-visible:ring-ring h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',\n      className\n    )}\n    {...props}\n  >\n    <CheckboxPrimitive.Indicator\n      className={cn('flex items-center justify-center text-current')}\n    >\n      <Check className='h-4 w-4' />\n    </CheckboxPrimitive.Indicator>\n  </CheckboxPrimitive.Root>\n));\nCheckbox.displayName = CheckboxPrimitive.Root.displayName;\n\nexport { Checkbox };\n"
  },
  {
    "path": "website/components/ui/label.tsx",
    "content": "'use client';\n\nimport * as LabelPrimitive from '@radix-ui/react-label';\nimport { cva } from 'class-variance-authority';\nimport type { VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst labelVariants = cva(\n  'text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70'\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "website/components/ui/select.tsx",
    "content": "'use client';\n\nimport * as SelectPrimitive from '@radix-ui/react-select';\nimport { Check, ChevronDown, ChevronUp } from 'lucide-react';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      'border-input flex h-10 w-full items-center justify-between rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:ring-0 focus:ring-offset-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className='h-4 w-4 opacity-50' />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      'flex cursor-default items-center justify-center py-1',\n      className\n    )}\n    {...props}\n  >\n    <ChevronUp className='h-4 w-4' />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      'flex cursor-default items-center justify-center py-1',\n      className\n    )}\n    {...props}\n  >\n    <ChevronDown className='h-4 w-4' />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = 'popper', ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        'text-popover-foreground relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white shadow-md dark:border-slate-800 dark:bg-slate-950',\n        position === 'popper' &&\n          'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',\n        className\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          'p-1',\n          position === 'popper' &&\n            'h-[var(--radix-select-content-available-height)] w-full min-w-[var(--radix-select-trigger-width)]'\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn('py-1.5 pr-2 pl-8 text-sm font-semibold', className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className\n    )}\n    {...props}\n  >\n    <span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>\n      <SelectPrimitive.ItemIndicator>\n        <Check className='h-4 w-4' />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "website/components/ui/separator.tsx",
    "content": "'use client';\n\nimport * as SeparatorPrimitive from '@radix-ui/react-separator';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = 'horizontal', decorative = true, ...props },\n    ref\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        'shrink-0 bg-border',\n        orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',\n        className\n      )}\n      {...props}\n    />\n  )\n);\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "website/components/ui/tooltip.tsx",
    "content": "'use client';\n\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst Tooltip = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Portal>\n    <TooltipPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-[100] overflow-hidden rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-950 shadow-md dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50',\n        className\n      )}\n      {...props}\n    />\n  </TooltipPrimitive.Portal>\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "website/content/blog/theme-customization.mdx",
    "content": "---\ntitle: 'V1.5-1.6: Theme Customization'\ndescription: Learn how to customize the look and feel of your calendar with CSS variables.\ndate: '2026-01-03'\n---\n\n# Default Black theme\n\n<img\n  width='1458'\n  height='728'\n  alt='image'\n  src='https://github.com/user-attachments/assets/484795ba-d751-4ad3-b370-a7f10240e5f0'\n/>\n\n```css\n:root {\n  /* Light mode colors */\n  --color-background: rgb(255 255 255);\n  --color-foreground: rgb(46 46 46);\n  --color-hover: rgb(245 245 245);\n  --color-border: rgb(229 229 229);\n  --color-card: rgb(255 255 255);\n  --color-card-foreground: rgb(46 46 46);\n  --color-muted: rgb(243 244 246);\n  --color-muted-foreground: rgb(107 114 128);\n\n  /* Theme Colors */\n  --color-primary: rgb(46 46 46);\n  --color-primary-foreground: rgb(255 255 255);\n  --color-secondary: rgb(100 116 139);\n  --color-secondary-foreground: rgb(255 255 255);\n\n  /* Destructive */\n  --color-destructive: rgb(212 36 34);\n  --color-destructive-foreground: rgb(255 255 255);\n}\n\n.dark {\n  /* Dark mode colors */\n  --color-background: rgb(21 21 21);\n  --color-hover: rgb(28 28 28);\n  --color-foreground: rgb(229 229 229);\n  --color-card: rgb(31 41 55);\n  --color-card-foreground: rgb(229 229 229);\n  --color-border: rgb(56 56 56);\n  --color-muted: rgb(55 65 81);\n  --color-muted-foreground: rgb(156 163 175);\n\n  --color-primary: rgb(229 229 229);\n  --color-primary-foreground: rgb(23 23 23);\n  --color-secondary: rgb(156 163 175);\n  --color-secondary-foreground: rgb(23 23 23);\n\n  /* Destructive */\n  --color-destructive: rgb(147 70 69);\n  --color-destructive-foreground: rgb(254 242 242);\n}\n```\n\n# Mac Calendar theme\n\n<img\n  width='1461'\n  height='737'\n  alt='image'\n  src='https://github.com/user-attachments/assets/31815665-649d-47b5-a895-03f5b0a71df0'\n/>\n\n```css\n/* Light mode - Mac Calendar style */\n:root {\n  --color-background: rgb(255 255 255);\n  --color-foreground: rgb(46 46 46);\n  --color-hover: rgb(255 229 229);\n  --color-border: rgb(240 240 240);\n  --color-card: rgb(255 255 255);\n  --color-card-foreground: rgb(46 46 46);\n  --color-muted: rgb(249 242 242);\n  --color-muted-foreground: rgb(169 68 66);\n\n  /* Theme Colors */\n  --color-primary: rgb(212 36 34);\n  --color-primary-foreground: rgb(255 255 255);\n  --color-secondary: rgb(242 139 130);\n  --color-secondary-foreground: rgb(23 23 23);\n\n  /* Destructive */\n  --color-destructive: rgb(255 255 255);\n  --color-destructive-foreground: rgb(23 23 23);\n}\n\n/* Dark mode */\n.dark {\n  --color-background: rgb(28 28 28);\n  --color-hover: rgb(46 28 28);\n  --color-foreground: rgb(245 245 245);\n  --color-card: rgb(42 42 42);\n  --color-card-foreground: rgb(245 245 245);\n  --color-border: rgb(58 58 58);\n  --color-muted: rgb(60 28 28);\n  --color-muted-foreground: rgb(242 139 130);\n\n  --color-primary: rgb(255 82 82);\n  --color-primary-foreground: rgb(23 23 23);\n  --color-secondary: rgb(255 138 128);\n  --color-secondary-foreground: rgb(23 23 23);\n\n  /* Destructive */\n  --color-destructive: rgb(255 255 255);\n  --color-destructive-foreground: rgb(23 23 23);\n}\n```\n\n# Ant Design Theme\n\n<img\n  width='1448'\n  height='725'\n  alt='image'\n  src='https://github.com/user-attachments/assets/1b8e80ad-6a3a-4e5d-b4b4-6d3a2b9d1f76'\n/>\n\n```css\n/* Light mode - Ant Design style */\n:root {\n  --color-background: rgb(255 255 255);\n  --color-foreground: rgb(31 31 31);\n  --color-hover: rgb(230 247 255); /* light blue hover */\n  --color-border: rgb(217 217 217);\n  --color-card: rgb(255 255 255);\n  --color-card-foreground: rgb(31 31 31);\n  --color-muted: rgb(245 245 245);\n  --color-muted-foreground: rgb(140 140 140);\n\n  /* Theme Colors */\n  --color-primary: rgb(24 144 255); /* Ant Design blue-6 */\n  --color-primary-foreground: rgb(255 255 255);\n  --color-secondary: rgb(64 169 255); /* lighter blue */\n  --color-secondary-foreground: rgb(255 255 255);\n\n  /* Destructive */\n  --color-destructive: rgb(255 77 79); /* Ant Design red-6 */\n  --color-destructive-foreground: rgb(255 255 255);\n}\n\n/* Dark mode */\n.dark {\n  --color-background: rgb(20 20 20);\n  --color-hover: rgb(10 61 102); /* darker blue hover */\n  --color-foreground: rgb(229 229 229);\n  --color-card: rgb(31 31 31);\n  --color-card-foreground: rgb(229 229 229);\n  --color-border: rgb(48 48 48);\n  --color-muted: rgb(38 38 38);\n  --color-muted-foreground: rgb(140 140 140);\n\n  --color-primary: rgb(64 169 255);\n  --color-primary-foreground: rgb(23 23 23);\n  --color-secondary: rgb(105 192 255);\n  --color-secondary-foreground: rgb(23 23 23);\n\n  --color-destructive: rgb(255 77 79);\n  --color-destructive-foreground: rgb(254 242 242);\n}\n```\n\n# Material Design Theme\n\n<img\n  width='1462'\n  height='733'\n  alt='image'\n  src='https://github.com/user-attachments/assets/2d219a54-6b45-4a3b-a856-2055757b9668'\n/>\n\n```css\n/* Light mode - Material Design style */\n:root {\n  --color-background: rgb(255 255 255);\n  --color-foreground: rgb(33 33 33);\n  --color-hover: rgb(224 247 250); /* light cyan hover */\n  --color-border: rgb(224 224 224);\n  --color-card: rgb(255 255 255);\n  --color-card-foreground: rgb(33 33 33);\n  --color-muted: rgb(240 240 240);\n  --color-muted-foreground: rgb(117 117 117);\n\n  /* Theme Colors */\n  --color-primary: rgb(76 175 80); /* green-500 */\n  --color-primary-foreground: rgb(255 255 255);\n  --color-secondary: rgb(255 152 0); /* orange-500 */\n  --color-secondary-foreground: rgb(255 255 255);\n\n  /* Destructive */\n  --color-destructive: rgb(244 67 54); /* red-500 */\n  --color-destructive-foreground: rgb(255 255 255);\n}\n\n/* Dark mode */\n.dark {\n  --color-background: rgb(18 18 18);\n  --color-hover: rgb(27 94 32); /* dark green hover */\n  --color-foreground: rgb(224 224 224);\n  --color-card: rgb(30 30 30);\n  --color-card-foreground: rgb(224 224 224);\n  --color-border: rgb(51 51 51);\n  --color-muted: rgb(44 44 44);\n  --color-muted-foreground: rgb(176 176 176);\n\n  --color-primary: rgb(129 199 132);\n  --color-primary-foreground: rgb(23 23 23);\n  --color-secondary: rgb(255 183 77);\n  --color-secondary-foreground: rgb(23 23 23);\n\n  --color-destructive: rgb(229 115 115);\n  --color-destructive-foreground: rgb(254 242 242);\n}\n```\n\n# Customizing Colors\n\nYou can override default colors by defining CSS variables in a `:root` block in your CSS file. This must be loaded **after** DayFlow's CSS to override the default values.\n\n**Important:**\n\n- Prefer the `--df-color-*` variables exposed by DayFlow\n- Colors must use `rgb()` format with space-separated values\n- Import DayFlow CSS first, then your custom CSS\n\n**Example (Tailwind project):**\n\n```css\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n@import './globals.css';\n```\n\n**Example (non-Tailwind project):**\n\n```tsx\nimport '@dayflow/core/dist/styles.css';\nimport './globals.css'; // Your custom styles after DayFlow\n```\n\n**Example (in your globals.css):**\n\n```css\n:root {\n  /* Theme Colors - use rgb() format */\n  --df-color-primary: rgb(26 115 232); /* blue */\n  --df-color-primary-foreground: rgb(255 255 255);\n  --df-color-secondary: rgb(255 152 0); /* orange */\n  --df-color-secondary-foreground: rgb(23 23 23);\n  --df-color-destructive: rgb(229 57 53); /* red */\n  --df-color-destructive-foreground: rgb(255 255 255);\n\n  /* Other colors */\n  --df-color-background: rgb(255 255 255);\n  --df-color-foreground: rgb(33 33 33);\n  --df-color-border: rgb(229 229 229);\n  --df-color-card: rgb(255 255 255);\n  --df-color-card-foreground: rgb(33 33 33);\n  --df-color-muted: rgb(243 244 246);\n  --df-color-muted-foreground: rgb(107 114 128);\n  --df-color-hover: rgb(245 245 245);\n}\n\n/* For dark mode: */\n.dark {\n  --df-color-primary: rgb(96 165 250); /* lighter blue */\n  --df-color-primary-foreground: rgb(23 23 23);\n  --df-color-background: rgb(18 18 18);\n  --df-color-foreground: rgb(229 229 229);\n  /* ... other dark mode colors */\n}\n```\n\n> **Note:** DayFlow uses `:where()` selectors for its default CSS variables, giving them zero specificity. This means your `:root` definitions will automatically override the defaults without needing `!important`.\n\n**Converting Hex to RGB:**\n\n- Hex `#1a73e8` → `rgb(26 115 232)`\n- Hex `#ff9800` → `rgb(255 152 0)`\n- Hex `#e53935` → `rgb(229 57 53)`\n\nYou can use online converters or browser dev tools to convert hex colors to RGB values.\n"
  },
  {
    "path": "website/content/blog/v1.4.mdx",
    "content": "---\ntitle: 'V1.4: Building a Customizable Calendar Sidebar in React'\ndescription: Learn how to build a powerful calendar sidebar with context menus, inline renaming, custom colours, and flexible calendar creation modes using a React calendar API.\ndate: '2026-01-01'\n---\n\n**1. Context Menu Actions on Calendars**\n\nCalendars in the sidebar support a right-click context menu, allowing quick access to common actions.\n\nAvailable actions include:\n\n- Delete calendar\n- Merge calendar\n- Change calendar colour\n\n<img\n  width='1024'\n  height='724'\n  alt='image'\n  src='https://github.com/user-attachments/assets/e28d97ca-4ef3-41e7-ad00-3798233d7aa2'\n/>\n\nWhen selecting Custom Colour, a colour picker will be displayed, enabling users to choose any colour from a palette and apply it to the calendar immediately.\n\n<img\n  width='337'\n  height='306'\n  alt='Image'\n  src='https://github.com/user-attachments/assets/852d3f97-b689-4dc6-8d46-04fce9a98a9e'\n/>\n\n**2. Rename Calendar via Double-Click**\n\nCalendars can be renamed directly in the sidebar. Double-click on a calendar name to enter edit mode.\n\n<img\n  width='210'\n  height='284'\n  alt='Image'\n  src='https://github.com/user-attachments/assets/ae9c0c46-e422-42c3-859f-558451dc70ac'\n/>\n\n**3. Add a New Calendar**\n\nNew calendars can be created by clicking the “＋” (Add) button in the sidebar.\n\nTwo creation modes are supported:\n\n```ts\n  createCalendarMode?: 'inline' | 'modal';\n```\n\n**Inline Mode (Default)**\n\nThe new calendar is created immediately in the sidebar.\n\nThe default name is **Untitled**.\n\nSuitable for fast creation and minimal interruption.\n\n<img\n  width='207'\n  height='211'\n  alt='Image'\n  src='https://github.com/user-attachments/assets/05b2ee0d-03e6-4158-a6af-f0b3e56343b9'\n/>\n\n**Modal Mode**\n\nThe calendar is created through a modal dialog.\n\nAllows additional configuration before confirming creation.\n\nUseful when more setup options are required.\n\n<img\n  width='554'\n  height='525'\n  alt='image'\n  src='https://github.com/user-attachments/assets/bbcaa543-a595-4082-8fd0-2d3cb941c584'\n/>\n\n**4. How to use**\n\n```ts\nconst CalendarWithSidebar: React.FC<{}> = () => {\n  const [events] = useState<Event[]>(getSampleEvents());\n  const appRef = useRef<CalendarApp | null>(null);\n\n  const dragPlugin = createDragPlugin({\n    enableDrag: true,\n    enableResize: true,\n    enableCreate: true,\n  });\n\n  const sidebarConfig = {\n    createCalendarMode: 'modal' as const,\n    renderCreateCalendarDialog: (props: any) => (\n      <CustomCreateCalendarDialog // <----  you can custom create calendar dialog\n        onClose={props.onClose}\n        onCreate={props.onCreate}\n      />\n    ),\n    renderCalendarContextMenu: (calendar: CalendarType, onClose: () => void) => (\n      appRef.current ? (\n        <CustomContextMenu // <--- you can custom right click context menu\n          calendar={calendar}\n          onClose={onClose}\n          app={appRef.current}\n        />\n      ) : null\n    )\n  }\n\n  const calendar = useCalendarApp({\n    views: [createDayView(), createWeekView(), createMonthView()],\n    events: events,\n    calendars: customCalendarTypes,\n    defaultCalendar: 'work',\n    plugins: [dragPlugin],\n    theme: { mode: 'auto' },\n    useSidebar: sidebarConfig,\n    callbacks: {\n      onCalendarUpdate: async (calendar) => { // <-- get update callback\n        console.log('update calendar:', calendar);\n      },\n      onCalendarDelete: async (calendar) => {\n        console.log('delete calendar:', calendar);\n      },\n      onCalendarCreate: async (calendar) => {\n        // call server api using promise function\n        // await ...\n        console.log('create calendar:', calendar);\n      },\n      onCalendarMerge: async (sourceId, targetId) => {\n        console.log('merge calendar:', sourceId, targetId);\n      },\n    }\n  });\n\n  // Assign app ref after initialization\n  appRef.current = calendar.app as any;\n\n  return (\n    <div >\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n};\n```\n"
  },
  {
    "path": "website/content/blog/v1.7.mdx",
    "content": "---\ntitle: 'V1.7: Internationalization Support & i18n'\ndescription: DayFlow now supports multiple languages natively, including English, Japanese, Chinese, German, French, Spanish, and Korean.\ndate: '2026-01-15'\n---\n\nThe release of V1.7, which brings comprehensive Internationalization (i18n) support to DayFlow. This update allows you to build calendar applications that speak your users' languages.\n\n**1. Native Multi-language Support**\n\nDayFlow now includes built-in translations for the following languages:\n\n- **Chinese** (zh)\n- **English** (en)\n- **French** (fr)\n- **German** (de)\n- **Japanese** (ja)\n- **Korean** (ko)\n- **Spanish** (es)\n\nAll UI elements, including buttons, dialogs, and labels, are automatically translated based on the configured locale.\n\n**2. How to Set the Locale**\n\nYou can set the locale when initializing your calendar application using the `useCalendarApp` hook. You can provide a language code string, a built-in `Locale` object, or a custom `Locale` object.\n\n**Option A: Using a language code string**\nThis is the simplest way. DayFlow will look up the built-in translation.\n\n```ts\nconst calendar = useCalendarApp({\n  // ... other config\n  locale: 'ja', // Set locale to Japanese\n});\n```\n\n**Option B: Using built-in Locale objects**\nYou can import locale objects directly from the package. This is useful if you want to ensure type safety or extend an existing locale.\n\n```ts\nimport { ja, zh } from '@dayflow/core';\n\nconst calendar = useCalendarApp({\n  // ... other config\n  locale: ja, // Pass the Japanese locale object directly\n});\n```\n\n**Option C: Using a custom Locale object**\nIf you want to support a language not included in DayFlow or provide a full set of translations, you can pass a custom `Locale` object:\n\n```ts\nconst customLocale = {\n  code: 'it', // Italian\n  messages: {\n    today: 'Oggi',\n    day: 'Giorno',\n    week: 'Settimana',\n    month: 'Mese',\n    // ... all other translation keys\n  },\n};\n\nconst calendar = useCalendarApp({\n  // ... other config\n  locale: customLocale,\n});\n```\n\n**3. Custom i18n Messages**\n\nIf you just want to override specific terms in the current locale (whether built-in or custom), you can provide `customMessages` directly to the `DayFlowCalendar` component.\n\n```tsx\nconst customMessages = {\n  today: 'Today!',\n  newEvent: 'New Appointment',\n  // ... override other keys\n};\n\nreturn <DayFlowCalendar calendar={calendar} customMessages={customMessages} />;\n```\n\n4. examples for week view(default English):\n\n**Chinese:**\n\n<img\n  width='1256'\n  height='752'\n  alt='image'\n  src='https://github.com/user-attachments/assets/7c5e28e2-9d8a-4edb-b39a-4119537cdc12'\n/>\n\n**Japanese:**\n\n<img\n  width='1261'\n  height='755'\n  alt='image'\n  src='https://github.com/user-attachments/assets/4b7c15bd-d8b5-481c-b5e1-d55ad148c20a'\n/>\n\n**German:**\n\n<img\n  width='1254'\n  height='747'\n  alt='image'\n  src='https://github.com/user-attachments/assets/6d789a35-5aa2-493c-9490-ae99520bb23c'\n/>\n\n**French:**\n\n<img\n  width='1258'\n  height='754'\n  alt='image'\n  src='https://github.com/user-attachments/assets/e40f108b-793a-41df-b564-d1555813dec4'\n/>\n\n**Korean:**\n\n<img\n  width='1253'\n  height='753'\n  alt='image'\n  src='https://github.com/user-attachments/assets/f0d16e2f-2c49-4587-ae18-7ce68893e579'\n/>\n\n**Spanish:**\n\n<img\n  width='1256'\n  height='760'\n  alt='image'\n  src='https://github.com/user-attachments/assets/bdeec281-d2b7-477f-8fc4-186dcf77d17d'\n/>\n\n**What's Next?**\n\nWe are committed to making DayFlow the most flexible calendar toolkit. In future updates, we plan to add support for more languages and improve Right-to-Left (RTL) layout support.\n\nStay tuned for more updates!\n"
  },
  {
    "path": "website/content/blog/v1.8.mdx",
    "content": "---\ntitle: 'V1.8: Powerful Search Capabilities'\ndescription: Introducing the new search feature for Desktop, enabling users to instantly find and navigate to events.\ndate: '2026-01-26'\n---\n\n## Desktop Search\n\nEasily find your events by keyword. The search bar is intuitively located in the header, making it easier than ever to manage your schedule.\n\n### Key Features\n\n- **Real-time filtering**: Results appear instantly as you type.\n- **Quick Navigation**: Clicking a search result instantly navigates the calendar to the event's date.\n- **Visual Highlight**: The selected event is automatically highlighted in the view for immediate visibility.\n\n### Demo\n\nCheck out the search functionality in action:\n\n<video\n  src='https://github.com/user-attachments/assets/f6c7f77a-88b0-4bc1-97ec-376f0a152cc0'\n  controls='controls'\n  style={{ maxWidth: '100%', borderRadius: '8px', marginTop: '1rem' }}\n/>\n"
  },
  {
    "path": "website/content/blog/v2.0.3.mdx",
    "content": "---\ntitle: 'V2.0.3: Year View Reimagined'\ndescription: Introducing Fixed Week Year View and Year Canvas for comprehensive annual planning.\ndate: '2026-02-04'\n---\n\n## Year View Reimagined in DayFlow v2.0.3\n\n### Year Canvas\n\nThe year-canvas mode provides the ability to '**Year as one canvas**' and see the distribution of events for an entire year at once.\n\n<img\n  width='1258'\n  height='768'\n  alt='image'\n  src='https://github.com/user-attachments/assets/d5811c4e-b5b5-4a28-b42b-605137969ef7'\n/>\n\n### Fixed Week Year View\n\nWe have completely reimagined the Year View (`FixedWeekYearView`) to provide a more powerful and continuous planning experience.\n\n<img\n  width='1483'\n  height='804'\n  alt='image'\n  src='https://github.com/user-attachments/assets/b1b160c2-52ac-4e76-9c55-8559a556f02e'\n/>\n\n- **Continuous Grid Layout**: Instead of 12 separate month grids, the year is now displayed as a continuous, scrollable grid of weeks. This \"Fixed Week\" layout aligns weeks vertically, making it perfect for long-term planning.\n- **Multi-Day Event Support**: Events spanning multiple days, weeks, or even months are now rendered as continuous bars, providing a clear visual representation of duration and overlap.\n- **Drag & Drop**: Full support for interactive event management.\n  - **Move**: Drag events to reschedule them.\n  - **Resize**: Drag the edges of an event to change its duration.\n  - **Create**: Double-click any day to quickly create a new event.\n- **Timed Events Display**: A new configuration option `showTimedEventsInYearView` allows you to choose whether to display dot indicators for timed events alongside all-day event bars.\n\n### Customization\n\n- **Weekend Styling**: Added global CSS classes `df-year-view-weekend-header` and `df-year-view-weekend-cell` to easily customize the background color and appearance of weekends in the Year View.\n\n### Bug Fixes\n\n- **Week View**: Fixed an issue where clicking on the Week View could incorrectly trigger the drag indicator display.\n- **Event Rendering**: Resolved issues with all-day event rendering limits in Month View and Day View, ensuring events are correctly stacked and hidden/shown based on available space.\n- **Scroll Synchronisation**: Improved the synchronisation between the week header, month sidebar, and content grid in the Year View.\n\n### Upgrading\n\nTo use the new Year View features, ensure you are using the latest version of `@dayflow/core`.\n\n```bash\nnpm install @dayflow/core@latest\n```\n\nThe new Year View is enabled by default when using `createYearView()`.\n\n```tsx\nimport { createYearView } from '@dayflow/core';\n\n// ... in your useCalendarApp config\nviews: [\n  createYearView({\n    mode: 'fixed-week', // default is year-canvas\n    showTimedEventsInYearView: true, // Enable dots for timed events\n  }),\n];\n```\n"
  },
  {
    "path": "website/content/blog/v3.0.mdx",
    "content": "---\ntitle: 'V3.0: Multi-Framework Support'\ndescription: Introducing the new multi-framework support for DayFlow, enabling developers to use DayFlow with React, Vue, Svelte, and Angular.\ndate: '2026-02-15'\n---\n\n# Announcing DayFlow v3.0: The Multi-Framework Future\n\n### Major Architectural Overhaul: Multi-Framework Support\n\nThis version marks a complete rewrite of the DayFlow internal architecture, moving from a React-only library to a **framework-agnostic monorepo structure**.\n\n#### New Package Structure\n\n- **`@dayflow/core`**: The new heart of DayFlow. Powered by **Preact**, it handles all state management, layout algorithms, and the core rendering engine (~3KB gzipped).\n- **`@dayflow/react`**: High-performance React adapter.\n- **`@dayflow/vue`**: Brand new adapter for Vue 3.\n- **`@dayflow/svelte`**: Brand new adapter for Svelte 5 (with full SSR support).\n- **`@dayflow/angular`**: Brand new adapter for Angular (v14+).\n\n### New Features\n\n- **Framework Agnostic**: Core logic and UI are now decoupled from specific frameworks.\n- **Improved Content Injection**: New **Content Slots** system allowing users to inject native framework components (React/Vue/Svelte/Angular) into the Preact-driven calendar.\n- **SSR Ready**:\n  - **Svelte**: Provided dedicated SSR bundles (`dist/index.ssr.js`) to avoid DOM reference errors during server-side rendering.\n  - **React/Vue**: Enhanced hydration safety.\n\n### Fixed & Improved\n\n- Optimized mobile responsiveness for all framework adapters.\n- Improved build process using Rollup and Turborepo for faster and smaller bundles.\n\n### Breaking Changes\n\n- **Package Names**: If you were using the old `dayflow` package, you should now migrate to framework-specific packages (e.g., `@dayflow/react`).\n- **Import Paths**:\n  - Components and hooks are now exported from `@dayflow/[framework]`.\n  - Core types and utilities are exported from `@dayflow/core`.\n- **External Dependencies**: To maintain framework-agnosticism, the built-in color picker (`react-color`) has been removed. Users should now provide their own color picker via Content Slots.\n\n---\n\n_Ready to try it? Head over to the [Getting Started](/docs/introduction) guide._\n"
  },
  {
    "path": "website/content/docs/features/calendar-header.mdx",
    "content": "# Calendar Header\n\nThe calendar header provides navigation controls, the view switcher, and search functionality. You can customize its behavior or replace it entirely.\n\n<Callout title='Docs Site Navigation' type='info'>\n  The documentation site's top navigation now includes a separate DayFlow Pro\n  entry alongside the main DayFlow logo and Blossom Picker. That Pro entry uses\n  the `pro-logo.png + Pro badge` structure and is part of the website chrome,\n  not the calendar component API described on this page.\n</Callout>\n\n## Hiding the Header\n\nIf you want to build your own navigation or just don't need the default header, you can disable it.\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  useCalendarHeader: false, // Hides the default header\n});\n```\n\n## Custom Header\n\n<Callout title='Breaking change from v3.4.1' type='warn'>\n  Passing a function to `useCalendarHeader` is no longer supported. Move your\n  custom header to the `calendarHeader` slot on `DayFlowCalendar` as shown\n  below.\n</Callout>\n\nYou can replace the default header with your own component using the `calendarHeader` content slot. Your renderer receives the calendar's state and helper methods so you can drive navigation and search from your own UI.\n\n### React\n\nPass a `calendarHeader` render prop to `DayFlowCalendar`:\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n});\n\n<DayFlowCalendar\n  calendar={calendar}\n  calendarHeader={() => (\n    <div className='custom-header'>\n      <button onClick={() => calendar.goToPrevious()}>‹</button>\n      <button onClick={() => calendar.goToToday()}>Today</button>\n      <button onClick={() => calendar.goToNext()}>›</button>\n    </div>\n  )}\n/>;\n```\n\n### Vue\n\nUse the named `calendarHeader` slot:\n\n```vue\n<DayFlowCalendar :calendar=\"calendar\">\n  <template #calendarHeader=\"{ calendar, switcherMode, onSearchChange }\">\n    <div class=\"custom-header\">\n      <button @click=\"calendar.goToPrevious()\">‹</button>\n      <button @click=\"calendar.goToToday()\">Today</button>\n      <button @click=\"calendar.goToNext()\">›</button>\n    </div>\n  </template>\n</DayFlowCalendar>\n```\n\n### Slot Args\n\nThe slot renderer receives the following arguments:\n\n| Arg              | Description                                                          |\n| :--------------- | :------------------------------------------------------------------- |\n| `calendar`       | The `CalendarApp` instance.                                          |\n| `switcherMode`   | The current view switcher mode (`buttons` or `select`).              |\n| `onAddCalendar`  | Handler for adding a new calendar.                                   |\n| `onSearchChange` | Handler for updating the search query.                               |\n| `onSearchClick`  | Handler for when the search icon is clicked.                         |\n| `searchValue`    | The current search string.                                           |\n| `isSearchOpen`   | Whether the search UI is currently open.                             |\n| `isEditable`     | Whether the calendar is currently in an editable state.              |\n| `safeAreaLeft`   | Left padding (px) to avoid overlapping traffic lights in macOS mode. |\n"
  },
  {
    "path": "website/content/docs/features/content-slots.mdx",
    "content": "import { EventContentShowcase } from '@/components/showcase/EventContentShowcase';\nimport { CustomDetailPanelShowcase } from '@/components/showcase/CustomDetailPanelShowcase';\nimport { ColorPickerShowcase } from '@/components/showcase/ColorPickerShowcase';\nimport { CustomDetailDialogShowcase } from '@/components/showcase/CustomDetailDialogShowcase';\nimport { ContextMenuShowcase } from '@/components/showcase/ContextMenuShowcase';\nimport { MobileEventDetailShowcase } from '@/components/showcase/MobileEventDetailShowcase';\n\n# Content Slots\n\nDayFlow uses a slot-based architecture that allows you to inject custom UI components from your preferred framework (React, Vue, etc.) directly into the core calendar engine.\n\nThis is powered by the `ContentSlot` mechanism. While the core engine provides default implementations for most UI elements (using Preact), you can override them to match your application's design or to use specific libraries.\n\n## Table of Contents\n\n- [How it Works](#how-it-works)\n- [Available Slots](#available-slots)\n- [Event Content](#event-content)\n- [Event Detail Content](#event-detail-content)\n- [Event Detail Dialog](#event-detail-dialog)\n- [Mobile Event Detail](#mobile-event-detail)\n- [Injecting a Custom Color Picker](#example-injecting-a-custom-color-picker)\n- [Context Menu](#context-menu-slots)\n\n## How it Works\n\n1. **Core Defines a Slot**: Inside `@dayflow/core`, certain UI areas are wrapped in a `ContentSlot`. Each slot has a `generatorName` and `generatorArgs`.\n2. **You Provide the Implementation**: You pass a component or a render function to the `DayFlowCalendar` component. The adapter then portals your component into the exact location defined by the core.\n\n## Available Slots\n\n| Generator Name                    | Description                                                                                              | Arguments                                                                 |\n| :-------------------------------- | :------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------ |\n| `colorPicker`                     | A small color picker used for event colors.                                                              | `{ color, onChange, onChangeComplete }`                                   |\n| `createCalendarDialogColorPicker` | A full-featured color picker used in dialogs.                                                            | `{ color, onChange, onAccept, onCancel, styles }`                         |\n| `eventContent*`                   | Custom rendering for event cards (e.g. `eventContentDay`).                                               | `{ event, viewType, isAllDay, isMobile, isSelected, isDragging, layout }` |\n| `eventContextMenu`                | Custom right-click context menu for events.                                                              | `{ event, onClose }`                                                      |\n| `eventDetailContent`              | The content shown in the popover/panel when an event is clicked.                                         | `{ event, isAllDay, onEventDelete, onEventUpdate, onClose, app }`         |\n| `eventDetailDialog`               | Custom dialog shown when an event is clicked. Requires `useEventDetailDialog: true` in `useCalendarApp`. | `{ event, isOpen, isAllDay, onEventDelete, onEventUpdate, onClose, app }` |\n| `gridContextMenu`                 | Custom right-click context menu for calendar cells/grid.                                                 | `{ date, viewType, onClose }`                                             |\n| `mobileEventDetail`               | Custom mobile event detail drawer/dialog.                                                                | `{ isOpen, onClose, onSave, onEventDelete, draftEvent, app, timeFormat }` |\n| `sidebarCalendarColorPicker`      | A color picker used for calendar colors in the sidebar.                                                  | `{ color, onChange, onChangeComplete }`                                   |\n| `titleBarSlot`                    | Extra content in the sidebar title bar.                                                                  | `{ isCollapsed, toggleCollapsed }`                                        |\n\n## Event Content\n\nThe `eventContent` slots allow you to completely customize how events are rendered within the calendar. Unlike other slots, event content **must** be specified per view to ensure the layout remains consistent with each view's specific event structure.\n\n### View-Specific Slots\n\n| Slot Name                 | Description                             |\n| :------------------------ | :-------------------------------------- |\n| `eventContentDay`         | Overrides timed events in Day view.     |\n| `eventContentWeek`        | Overrides timed events in Week view.    |\n| `eventContentMonth`       | Overrides events in Month view.         |\n| `eventContentYear`        | Overrides events in Year view.          |\n| `eventContentAllDayDay`   | Overrides all-day events in Day view.   |\n| `eventContentAllDayWeek`  | Overrides all-day events in Week view.  |\n| `eventContentAllDayMonth` | Overrides all-day events in Month view. |\n| `eventContentAllDayYear`  | Overrides all-day events in Year view.  |\n\n<EventContentShowcase />\n\n<details>\n<summary>View Source Code</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nexport const EventContentShowcase = () => {\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      // Customize event rendering for Day View\n      eventContentDay={({ event, isSelected }) => (\n        <div className='custom-event-card'>\n          <span>{event.title}</span>\n          {/* ... add your custom icons or layout */}\n        </div>\n      )}\n      // Customize event rendering for Month View\n      eventContentMonth={({ event }) => (\n        <div className='flex items-center gap-1'>\n          <span>🗓️</span>\n          <span className='truncate'>{event.title}</span>\n        </div>\n      )}\n      // Omit other view overrides for brevity...\n    />\n  );\n};\n```\n\n</details>\n\n## Event Detail Content\n\nThe `eventDetailContent` slot lets you customize the content displayed in the event detail popover or panel when an event is clicked.\n\n<CustomDetailPanelShowcase />\n\n<details>\n<summary>View Source Code</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { useCallback } from 'react';\n\nexport const CustomDetailPanelShowcase = () => {\n  const detailPanel = useCallback(\n    ({ event, onEventDelete, onEventUpdate, onClose }) => {\n      return (\n        <div className='p-4 space-y-3'>\n          <h5 className='font-bold'>{event.title}</h5>\n          <p>{event.description}</p>\n\n          <div className='flex gap-2'>\n            <button\n              onClick={() => onEventUpdate({ ...event, title: 'Updated' })}\n            >\n              Update\n            </button>\n            <button onClick={() => onEventDelete(event.id)}>Delete</button>\n          </div>\n        </div>\n      );\n    },\n    []\n  );\n\n  return (\n    <DayFlowCalendar calendar={calendar} eventDetailContent={detailPanel} />\n  );\n};\n```\n\n</details>\n\n## Event Detail Dialog\n\nIf you prefer a modal/dialog interface for viewing and editing event details, you can use the `eventDetailDialog` slot. This is particularly useful for mobile-first applications or when you need more space for complex event data.\n\n> **Prerequisite**: set `useEventDetailDialog: true` in `useCalendarApp` to activate dialog mode. Without it the slot is never triggered.\n\n> **Opt out instead**: if you want to suppress the built-in floating panel without replacing it with your own dialog, set `useEventDetailPanel: false` in your `useCalendarApp` configuration instead of using a slot.\n\n<CustomDetailDialogShowcase />\n\n<details>\n<summary>View Source Code</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { useCallback } from 'react';\n\nexport const CustomDetailDialogShowcase = () => {\n  const customDialog = useCallback(({ event, isOpen, onClose }) => {\n    if (!isOpen) return null;\n\n    return (\n      <div className='fixed inset-0 z-50 flex items-center justify-center bg-black/50'>\n        <div className='bg-white p-6 rounded-lg shadow-xl'>\n          <h2>{event.title}</h2>\n          {/* ... build your custom dialog UI here */}\n          <button onClick={onClose}>Close</button>\n        </div>\n      </div>\n    );\n  }, []);\n\n  return (\n    <DayFlowCalendar calendar={calendar} eventDetailDialog={customDialog} />\n  );\n};\n```\n\n</details>\n\n## Mobile Event Detail\n\nWhen DayFlow is viewed on mobile devices (or small screens), it uses a dedicated `mobileEventDetail` slot to handle event creation and editing. By default, this is a full-screen drawer.\n\n<MobileEventDetailShowcase />\n\n<details>\n<summary>View Source Code</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react';\nimport { useCallback } from 'react';\n\nexport const MobileEventDetailShowcase = () => {\n  const customMobileDrawer = useCallback(\n    ({ isOpen, onClose, onSave, onEventDelete, draftEvent }) => {\n      if (!isOpen || !draftEvent) return null;\n\n      return (\n        <div className='fixed inset-0 z-50 flex flex-col bg-white'>\n          <header className='flex items-center justify-between p-4 border-b'>\n            <button onClick={onClose}>Back</button>\n            <h2>{draftEvent.id ? 'Edit' : 'New'} Event</h2>\n            <button onClick={() => onSave(draftEvent)}>Save</button>\n          </header>\n          <div className='p-4'>\n            <input\n              defaultValue={draftEvent.title}\n              onChange={e => (draftEvent.title = e.target.value)}\n            />\n            {/* ... build your mobile-optimized UI */}\n          </div>\n        </div>\n      );\n    },\n    []\n  );\n\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      mobileEventDetail={customMobileDrawer}\n    />\n  );\n};\n```\n\n</details>\n\n## Example: Injecting a Custom Color Picker\n\nBy default, DayFlow uses a built-in `BlossomColorPicker`. If you prefer to use a library like `react-color` or `vue-color`, you can inject it using the `colorPicker` slots.\n\n<DocImg src='/images/docs/colorPicker.png' alt='Color Picker' />\n\n### React Example\n\nInstall `react-color`:\n\n<PackageTabs pkg='react-color' />\n\nInject it into `DayFlowCalendar`:\n\n<ColorPickerShowcase />\n\n<details>\n<summary>View Source Code</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { SketchPicker, PhotoshopPicker } from 'react-color';\n\nfunction MyCalendar() {\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      colorPicker={args => (\n        <SketchPicker\n          color={args.color}\n          onChange={color => args.onChange({ hex: color.hex })}\n        />\n      )}\n      createCalendarDialogColorPicker={args => (\n        <PhotoshopPicker\n          color={args.color}\n          onChange={color => args.onChange({ hex: color.hex })}\n          onAccept={args.onAccept}\n          onCancel={args.onCancel}\n        />\n      )}\n    />\n  );\n}\n```\n\n</details>\n\n## Context Menu Slots\n\nThe `eventContextMenu` and `gridContextMenu` slots let you replace the default right-click menus with fully custom React components. Both slots receive an `onClose` callback to dismiss the menu.\n\n- **`eventContextMenu`** — triggered when the user right-clicks an event. Receives `{ event, onClose }`.\n- **`gridContextMenu`** — triggered when the user right-clicks an empty area of the calendar grid. Receives `{ date, viewType, onClose }`.\n\nRight-click on any event or empty cell to see the custom menus in action:\n\n<ContextMenuShowcase />\n\n<details>\n<summary>View Source Code</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react';\nimport type {\n  EventContextMenuSlotArgs,\n  GridContextMenuSlotArgs,\n} from '@dayflow/core';\nimport { useCallback } from 'react';\n\nfunction MyCalendar() {\n  const eventContextMenu = useCallback(\n    ({ event, onClose }: EventContextMenuSlotArgs) => (\n      <div className='custom-menu'>\n        <button\n          onClick={() => {\n            /* duplicate */ onClose();\n          }}\n        >\n          Duplicate\n        </button>\n        <button\n          onClick={() => {\n            /* share */ onClose();\n          }}\n        >\n          Share\n        </button>\n        <button\n          onClick={() => {\n            calendar.deleteEvent(event.id);\n            onClose();\n          }}\n        >\n          Delete\n        </button>\n      </div>\n    ),\n    []\n  );\n\n  const gridContextMenu = useCallback(\n    ({ date, onClose }: GridContextMenuSlotArgs) => (\n      <div className='custom-menu'>\n        <button\n          onClick={() => {\n            /* create event at date */ onClose();\n          }}\n        >\n          New Event\n        </button>\n        <button\n          onClick={() => {\n            /* set reminder */ onClose();\n          }}\n        >\n          Remind me\n        </button>\n      </div>\n    ),\n    []\n  );\n\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      eventContextMenu={eventContextMenu}\n      gridContextMenu={gridContextMenu}\n    />\n  );\n}\n```\n\n</details>\n"
  },
  {
    "path": "website/content/docs/features/dark-mode.mdx",
    "content": "import { DefaultColorPalette } from '@/components/ColorPalette';\n\n# Dark Mode\n\nDayFlow Calendar includes comprehensive dark mode support out of the box. The calendar automatically adapts its appearance based on your theme preference, including all UI components, event colors, and interactive elements.\n\n## Features\n\n- **Three Theme Modes**: Light, Dark, and Auto (system preference)\n- **Automatic Color Adjustment**: Event colors optimized for both light and dark backgrounds\n- **Seamless Switching**: Instant theme changes without flickering\n- **System Sync**: Auto mode follows your operating system's theme preference\n- **Custom Colors**: Define different colors for light and dark modes\n\n## Quick Start\n\n### Basic Setup\n\nEnable dark mode by setting the `theme` configuration:\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    theme: {\n      mode: 'dark', // 'light' | 'dark' | 'auto'\n    },\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## Theme Modes\n\n### Light Mode\n\nDefault theme with light backgrounds and dark text.\n\n```tsx\nconst calendar = useCalendarApp({\n  theme: {\n    mode: 'light',\n  },\n});\n```\n\n### Dark Mode\n\nDark theme with dark backgrounds and light text.\n\n```tsx\nconst calendar = useCalendarApp({\n  theme: {\n    mode: 'dark',\n  },\n});\n```\n\n### Auto Mode\n\nAutomatically follows system theme preference and updates when system theme changes.\n\n```tsx\nconst calendar = useCalendarApp({\n  theme: {\n    mode: 'auto',\n  },\n});\n```\n\n## Theme Switching\n\n### Programmatic Theme Changes\n\nUse the calendar API to change themes dynamically:\n\n```tsx\nimport { useCalendarApp } from '@dayflow/react';\n\nfunction ThemeToggle() {\n  const calendar = useCalendarApp({\n    theme: { mode: 'light' },\n  });\n\n  const toggleTheme = () => {\n    const currentTheme = calendar.app.getTheme();\n    const nextTheme = currentTheme === 'light' ? 'dark' : 'light';\n    calendar.app.setTheme(nextTheme);\n  };\n\n  return <button onClick={toggleTheme}>Toggle Theme</button>;\n}\n```\n\n### Subscribe to Theme Changes\n\nListen to theme changes to update your UI:\n\n```tsx\nimport { useEffect, useState } from 'react';\nimport type { ThemeMode } from '@dayflow/core';\n\nfunction MyComponent({ calendar }) {\n  const [theme, setTheme] = useState<ThemeMode>('light');\n\n  useEffect(() => {\n    const handleThemeChange = (newTheme: ThemeMode) => {\n      setTheme(newTheme);\n    };\n\n    // Subscribe to theme changes\n    calendar.app.subscribeThemeChange(handleThemeChange);\n\n    // Cleanup\n    return () => {\n      calendar.app.unsubscribeThemeChange(handleThemeChange);\n    };\n  }, [calendar.app]);\n\n  return <div>Current theme: {theme}</div>;\n}\n```\n\n## Custom Theme Colors\n\nDefine different colors for light and dark modes for your calendar types:\n\n```tsx\nconst calendar = useCalendarApp({\n  theme: {\n    mode: 'auto',\n  },\n  calendars: [\n    {\n      id: 'work',\n      name: 'Work',\n      colors: {\n        // Light mode colors\n        lineColor: '#0066cc',\n        eventColor: '#e6f2ff',\n        eventSelectedColor: '#cce4ff',\n        textColor: '#003d7a',\n      },\n      darkColors: {\n        // Dark mode colors\n        lineColor: '#4da6ff',\n        eventColor: '#1a3d5c',\n        eventSelectedColor: '#2a5a8a',\n        textColor: '#b3d9ff',\n      },\n    },\n  ],\n});\n```\n\n### Color Recommendations\n\nFor optimal readability and accessibility:\n\n**Light Mode:**\n\n- Line colors: Vibrant, saturated colors (#0066cc, #16a34a)\n- Event colors: Light tints (#e6f2ff, #dcfce7)\n- Text colors: Dark shades for contrast (#003d7a, #14532d)\n\n**Dark Mode:**\n\n- Line colors: Lighter, more luminous variants (#4da6ff, #4ade80)\n- Event colors: Dark, desaturated backgrounds (#1a3d5c, #1e4d2b)\n- Text colors: Light shades for readability (#b3d9ff, #bbf7d0)\n\n## Default Calendar Type Colors\n\nDayFlow includes 10 default calendar types with pre-configured dark mode colors:\n\n<DefaultColorPalette />\n\n## API Reference\n\n### Theme Configuration\n\n```typescript\ninterface ThemeConfig {\n  mode: 'light' | 'dark' | 'auto';\n}\n```\n\n### Calendar App Methods\n\n```typescript\n// Get current theme mode\napp.getTheme(): ThemeMode\n\n// Set theme mode\napp.setTheme(mode: ThemeMode): void\n\n// Subscribe to theme changes\napp.subscribeThemeChange(callback: (theme: ThemeMode) => void): void\n\n// Unsubscribe from theme changes\napp.unsubscribeThemeChange(callback: (theme: ThemeMode) => void): void\n```\n\n### Calendar Type Colors\n\n```typescript\ninterface CalendarTypeColors {\n  lineColor: string; // Border and accent color\n  eventColor: string; // Event background color\n  eventSelectedColor: string; // Selected event background color\n  textColor: string; // Text color\n}\n\ninterface CalendarType {\n  id: string;\n  name: string;\n  colors: CalendarTypeColors; // Light mode colors\n  darkColors?: CalendarTypeColors; // Dark mode colors (optional)\n}\n```\n\n## Examples\n\n### Simple Theme Toggle\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { Sun, Moon } from 'lucide-react';\n\nfunction SimpleThemeToggle() {\n  const calendar = useCalendarApp({\n    theme: { mode: 'light' },\n  });\n\n  const [isDark, setIsDark] = useState(false);\n\n  const toggleTheme = () => {\n    const nextTheme = isDark ? 'light' : 'dark';\n    calendar.app.setTheme(nextTheme);\n    setIsDark(!isDark);\n  };\n\n  return (\n    <div>\n      <button onClick={toggleTheme}>{isDark ? <Sun /> : <Moon />}</button>\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n}\n```\n\n## Related Documentation\n\n- [Use Calendar App](/docs/introduction/use-calendar-app) - Core calendar configuration\n- [Calendar Types](/docs/introduction/events#calendar-types) - Event categorization and colors\n- [Theme Customization Guide](/docs/guides/theme-customization) - Advanced theming\n"
  },
  {
    "path": "website/content/docs/features/event-dialog.mdx",
    "content": "import { EventDialogShowcase } from '@/components/showcase/FeatureShowcase';\n\n# Event Dialog\n\nDayFlow comes with a powerful built-in event dialog that provides a complete event management experience out of the box. The dialog supports creating, editing, and deleting events with an intuitive interface.\n\n<EventDialogShowcase />\n\n## Usage\n\nDayFlow renders the built-in floating detail panel by default. To use the built-in modal dialog instead, enable `useEventDetailDialog` in `useCalendarApp`, then render `DayFlowCalendar` as usual.\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  views: [...],\n  useEventDetailDialog: true,\n});\n\n<DayFlowCalendar calendar={calendar} />;\n```\n\n## Customization\n\n### Replace the dialog UI\n\nUse the `eventDetailDialog` slot to swap in your own dialog component while keeping DayFlow's open/close state management. This requires `useEventDetailDialog: true` in your `useCalendarApp` config.\n\n```tsx\n// 1. Enable dialog mode in the calendar config\nconst calendar = useCalendarApp({\n  views: [...],\n  useEventDetailDialog: true,\n});\n\n// 2. Provide your own dialog via the slot\n<DayFlowCalendar\n  calendar={calendar}\n  eventDetailDialog={({ event, isOpen, onClose }) => (\n    <MyDialog isOpen={isOpen} onClose={onClose}>\n      <EventForm event={event} />\n    </MyDialog>\n  )}\n/>\n```\n\n### Opt out of the built-in panel\n\nIf your application manages event detail in its own modals (driven by callbacks or global state), you can suppress the built-in floating panel entirely by setting `useEventDetailPanel: false` in your `useCalendarApp` configuration:\n\n```tsx\nconst calendar = useCalendarApp({\n  // ...\n  useEventDetailPanel: false,\n});\n\n<DayFlowCalendar calendar={calendar} />;\n```\n\nThis works across React, Vue, Svelte, and Angular adapters.\n"
  },
  {
    "path": "website/content/docs/features/meta.json",
    "content": "{\n  \"title\": \"Features\",\n  \"pages\": [\n    \"calendar-header\",\n    \"content-slots\",\n    \"dark-mode\",\n    \"event-dialog\",\n    \"multi-calendar-event\",\n    \"read-only\",\n    \"switcher-mode\"\n  ]\n}\n"
  },
  {
    "path": "website/content/docs/features/multi-calendar-event.mdx",
    "content": "import { MultiCalendarEventShowcase } from '@/components/showcase/MultiCalendarEventShowcase';\n\n# Multi-calendar Event\n\nDayFlow supports multi-calendar events, which allow a single event to be associated with multiple calendars. This is particularly useful for cross-team meetings, shared family events, or any situation where an event belongs to more than one category.\n\n## Configuration\n\nTo create a multi-calendar event, you need to provide an array of calendar IDs in the `calendarIds` property of the event object.\n\n```tsx\nconst events = [\n  {\n    id: 'multi-cal-1',\n    title: 'Cross-team Planning',\n    start: '2026-04-20T10:00:00',\n    end: '2026-04-20T11:30:00',\n    // The primary calendar ID (used for default styling if needed)\n    calendarId: 'team-a',\n    // Associating the event with multiple calendars\n    calendarIds: ['team-a', 'team-b', 'marketing'],\n  },\n];\n```\n\nWhen an event has multiple `calendarIds`, DayFlow renders it with a distinctive diagonal stripe pattern background, using the colors assigned to each associated calendar.\n\n## Showcase\n\nThe following showcase demonstrates how multi-calendar events are rendered in the calendar.\n\n<MultiCalendarEventShowcase />\n\n<Callout title='Recommendation'>\n  By default, the built-in `eventDetailPanel`/`Dialog` doesn’t support selecting\n  multiple calendars. Since this requirement is not very common, I recommend\n  customizing the implementation of `eventDetailContent` or `eventDetailDialog`\n  instead. You can find more details in the documentation: [Content\n  Slots](./content-slots).\n</Callout>\n"
  },
  {
    "path": "website/content/docs/features/read-only.mdx",
    "content": "# Read-only Mode\n\nYou can easily make the calendar read-only to prevent user modifications. This is useful for public calendars or display-only dashboards.\n\nRead-only mode only disables DayFlow's built-in mutation UI. Programmatic APIs like `calendar.addEvent()`, `calendar.updateEvent()`, `calendar.deleteEvent()`, and `calendar.applyEventsChanges()` still work.\n\n## Basic Usage\n\nTo enable read-only mode, pass `readOnly: true` to the calendar configuration.\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  readOnly: true, // Disables built-in mutation UI (dragging, creating, editing)\n});\n```\n\n## Fine-grained Control\n\nYou can also provide a `ReadOnlyConfig` object to selectively disable certain features:\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  readOnly: {\n    draggable: false, // Disables drag-and-drop\n    viewable: true, // Allows clicking to view event details (read-only mode)\n  },\n});\n```\n\n### Configuration Options\n\n| Option      | Type      | Description                                                          |\n| :---------- | :-------- | :------------------------------------------------------------------- |\n| `draggable` | `boolean` | Whether events can be moved or resized via drag-and-drop.            |\n| `viewable`  | `boolean` | Whether event details can be viewed (opens the detail panel/dialog). |\n\nWhen `readOnly` is active, the calendar will also hide \"Create\" buttons and other UI elements that would normally trigger changes.\n\n## Custom UI\n\nIf you render your own buttons, menus, or dialogs, use `calendar.canMutateFromUI()` to decide whether mutation controls should be shown:\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  readOnly: true,\n});\n\nconst canEdit = calendar.canMutateFromUI();\n```\n\n- `true`: your custom UI may show create/edit/delete controls.\n- `false`: your custom UI should hide or disable mutation controls.\n"
  },
  {
    "path": "website/content/docs/features/switcher-mode.mdx",
    "content": "import { SwitcherModeShowcase } from '@/components/showcase/FeatureShowcase';\n\n# View Switcher Modes\n\nThe `switcherMode` option controls how the header view switcher is rendered. The library ships with two modes tailored for different layouts:\n\n- **`buttons`** – the default desktop friendly layout with a centered button group.\n- **`select`** – condenses the switcher into a dropdown for mobile or compact toolbars.\n\nUse the interactive sample below to compare both modes with identical data and configuration.\n\n<SwitcherModeShowcase />\n\n## Quick Start\n\n```tsx {9}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n} from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  switcherMode: 'select', // default switcherMode: 'buttons'\n});\n\n<DayFlowCalendar calendar={calendar} />;\n```\n\nFlip `switcherMode` between `'buttons'` and `'select'` to preview the alternate experiences instantly.\n"
  },
  {
    "path": "website/content/docs/guides/global-css.mdx",
    "content": "# Global CSS Classes\n\nDayFlow provides a set of CSS classes with the `df-` prefix that allow you to customize the appearance of calendar components. These classes are designed for easy styling customization without modifying the core library.\n\n## Choosing the Right CSS Entry\n\nChoose the stylesheet based on whether your app already uses Tailwind CSS:\n\n| Entry                   | Includes                                 | Use when                   |\n| ----------------------- | ---------------------------------------- | -------------------------- |\n| `styles.css`            | Full bundle including Tailwind preflight | You are not using Tailwind |\n| `styles.components.css` | Component styles only, without CSS reset | You already use Tailwind   |\n\n```css\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\nFor theme overrides, prefer `--df-color-*` variables or the stable `df-*` semantic classes documented below.\n\n## Table of Contents\n\n- [Common Classes](#common)\n- [Semantic Utility Classes](#semantic-utility-classes)\n- [Layout & Motion Helpers](#layout--motion-helpers)\n- [Day View Classes](#day-view)\n- [Week View Classes](#week-view)\n- [Month View Classes](#month-view)\n- [Year View Classes](#year-view)\n- [Mini Calendar Classes](#mini-calendar)\n- [Sidebar Classes](#sidebar)\n- [Navigation Classes](#navigation)\n- [Event Detail Classes](#event-detail)\n- [Content Slot Classes](#content-slots)\n- [Event State Attributes](#event-state-attributes)\n\n---\n\n## Common\n\nCommon CSS classes used across all calendar views.\n\n| Class Name                 | Description                                                                         |\n| -------------------------- | ----------------------------------------------------------------------------------- |\n| `df-calendar-container`    | Outer root container for the Sidebar and Calendar; supports `--df-calendar-height`. |\n| `df-calendar`              | Main calendar container (includes header and view content)                          |\n| `df-header`                | Calendar header section (includes title, today button, and view switcher)           |\n| `df-header-left`           | Left section of the header (title and navigation buttons)                           |\n| `df-header-mid`            | Middle section of the header (current date range title)                             |\n| `df-header-right`          | Right section of the header (view switcher and search)                              |\n| `df-navigation`            | Navigation controls container                                                       |\n| `df-view-header-container` | Shared container used by the secondary view header area                             |\n| `df-view-header-title`     | Shared title text class used in view-level headers                                  |\n| `df-view-header-subtitle`  | Shared subtitle text class used in view-level headers                               |\n| `df-event`                 | Base class for all events                                                           |\n| `df-event-title`           | Event title text                                                                    |\n| `df-event-time`            | Event time text                                                                     |\n| `df-event-color-bar`       | Colored bar on the left side of events (Day/Week view)                              |\n| `df-month-event-color-bar` | Colored indicator for regular events (Month view)                                   |\n| `df-all-day-row`           | All-day event row container                                                         |\n| `df-all-day-label`         | All-day label text                                                                  |\n| `df-all-day-content`       | All-day event content area                                                          |\n| `df-all-day-cell`          | Individual all-day event cell                                                       |\n| `df-date-number`           | Date number display                                                                 |\n| `df-current-time-line`     | Current time indicator line container                                               |\n| `df-current-time-label`    | Current time label text                                                             |\n| `df-current-time-bar`      | Current time horizontal line                                                        |\n| `df-calendar-checkbox`     | Shared checkbox style used by calendar lists and related controls                   |\n| `df-portal`                | Root scope class used by floating overlays rendered with portals                    |\n| `df-range-picker`          | Root scope class used by the range picker popup and trigger wrapper                 |\n\n---\n\n## Semantic Utility Classes\n\nDayFlow now exposes a stable set of `df-*` semantic classes for internal states such as selected buttons, highlighted dates, destructive actions, and focus rings. These are defined inside the shared foundation CSS and are safe to target from your own stylesheet.\n\nUse these classes when you want to align custom content, plugin UI, or content-slot output with DayFlow's theme tokens:\n\n| Class Name                | Purpose                                                        |\n| ------------------------- | -------------------------------------------------------------- |\n| `df-fill-primary`         | Primary filled background + matching foreground text           |\n| `df-fill-secondary`       | Secondary filled background + matching foreground text         |\n| `df-fill-destructive`     | Destructive filled background + matching foreground text       |\n| `df-tint-primary`         | Primary 10% tint + primary text                                |\n| `df-tint-primary-md`      | Primary 20% tint + primary text                                |\n| `df-tint-primary-lg`      | Primary 30% tint + primary text                                |\n| `df-hover-primary`        | Primary-tinted hover state                                     |\n| `df-hover-primary-md`     | Stronger primary-tinted hover state                            |\n| `df-hover-primary-solid`  | Solid primary hover state for filled buttons                   |\n| `df-hover-base`           | Default hover background using DayFlow's hover token           |\n| `df-hover-muted`          | Muted hover background                                         |\n| `df-hover-destructive`    | Destructive hover state                                        |\n| `df-text-primary`         | Primary text color                                             |\n| `df-text-muted`           | Muted text color                                               |\n| `df-text-primary-fg`      | Text color for primary-filled backgrounds                      |\n| `df-text-secondary-fg`    | Text color for secondary-filled backgrounds                    |\n| `df-text-destructive`     | Destructive text color                                         |\n| `df-text-destructive-fg`  | Text color for destructive-filled backgrounds                  |\n| `df-bg-base`              | Base surface background                                        |\n| `df-bg-card`              | Card surface background                                        |\n| `df-bg-sidebar`           | Sidebar-style muted background                                 |\n| `df-bg-secondary`         | Secondary muted background                                     |\n| `df-bg-tertiary`          | Tertiary background / divider surface                          |\n| `df-border-base`          | Base border                                                    |\n| `df-border-light`         | Lighter / softer border                                        |\n| `df-border-strong`        | Strong border using the primary token                          |\n| `df-border-primary`       | Primary border color                                           |\n| `df-border-primary-soft`  | Softer primary border color                                    |\n| `df-ring-primary`         | Primary focus/selection ring color                             |\n| `df-ring-primary-solid`   | Solid primary ring color                                       |\n| `df-shadow-sm`            | Small elevation shadow                                         |\n| `df-shadow-md`            | Medium elevation shadow                                        |\n| `df-shadow-primary`       | Primary shadow color token                                     |\n| `df-focus-ring`           | Combined primary `border-color` + `ring-color` on `:focus`     |\n| `df-focus-ring-only`      | Primary `ring-color` on `:focus` without changing border color |\n| `df-focus-border-primary` | Primary `border-color` on `:focus` only                        |\n\nPrefer these classes or the `--df-color-*` variables for theming. Avoid targeting old internal combinations such as `bg-primary`, `text-primary`, or `hover:bg-primary/90` on DayFlow internals, because those Tailwind semantic names are no longer part of the public styling contract.\n\n---\n\n## Layout & Motion Helpers\n\nThese classes are useful when you want custom slot content or plugin UI to inherit DayFlow's layout and animation behavior.\n\n| Class Name                | Purpose                                                         |\n| ------------------------- | --------------------------------------------------------------- |\n| `df-content-slot-stacked` | Makes a content slot fill available height and stack vertically |\n| `df-scrollbar-hide`       | Hides scrollbars while preserving scroll behavior               |\n| `df-animate-in`           | Base animation timing helper for entry transitions              |\n| `df-fade-in`              | Fade-in animation helper                                        |\n| `df-zoom-in-95`           | Slight zoom-in animation helper                                 |\n| `df-animate-slide-up`     | Slide-up entry animation                                        |\n| `df-animate-slide-down`   | Slide-down exit animation                                       |\n\n---\n\n## Day View\n\nCSS classes specific to the Day View.\n\n| Class Name          | Description                                          |\n| ------------------- | ---------------------------------------------------- |\n| `df-day-view`       | Day view container                                   |\n| `df-day-event`      | Individual event in day view                         |\n| `df-day-time-grid`  | Time grid container                                  |\n| `df-time-column`    | Time column (left sidebar with hours)                |\n| `df-time-slot`      | Individual time slot container (left column)         |\n| `df-time-label`     | Time label text (e.g., \"1 PM\")                       |\n| `df-time-grid-row`  | Horizontal row in the time grid                      |\n| `df-time-grid-cell` | Individual cell in the time grid                     |\n| `df-right-panel`    | Right panel in day view (mini calendar + event list) |\n\n---\n\n## Week View\n\nCSS classes specific to the Week View.\n\n| Class Name           | Description                                  |\n| -------------------- | -------------------------------------------- |\n| `df-week-view`       | Week view container                          |\n| `df-week-event`      | Individual event in week view                |\n| `df-week-header`     | Week header container with day names         |\n| `df-week-header-row` | The sticky header row containing day names   |\n| `df-week-day-cell`   | Individual weekday cell in header            |\n| `df-time-column`     | Time column (left sidebar with hours)        |\n| `df-time-slot`       | Individual time slot container (left column) |\n| `df-time-label`      | Time label text                              |\n| `df-time-grid-row`   | Horizontal row in the time grid              |\n| `df-time-grid-cell`  | Individual cell in the time grid             |\n\n---\n\n## Month View\n\nCSS classes specific to the Month View.\n\n| Class Name                       | Description                                |\n| -------------------------------- | ------------------------------------------ |\n| `df-month-view`                  | Month view container                       |\n| `df-month-grid`                  | The main grid container for the month view |\n| `df-month-day-cell`              | Individual day cell in month view          |\n| `df-month-date-number-container` | Container for date number area             |\n| `df-month-date-number`           | Date number in month view                  |\n| `df-month-more-events`           | \"+ x more\" indicator for hidden events     |\n| `df-month-title`                 | Floating month title during scroll         |\n\n---\n\n## Year View\n\nCSS classes specific to the Year View. These are particularly useful for customizing event appearance in the year overview.\n\n| Class Name                | Description                                                          |\n| ------------------------- | -------------------------------------------------------------------- |\n| `df-year-event`           | Event outer container - wraps the entire event element               |\n| `df-event-year-content`   | Event content container - contains icon, title, and other content    |\n| `df-event-year-title`     | Event title text - use this to customize title alignment, font, etc. |\n| `df-event-icon-svg`       | Event icon SVG used across event renderers, including Year view      |\n| `df-event-year-indicator` | Timed event color indicator - the colored dot/bar for timed events   |\n| `df-year-grid-month`      | Month card container in grid-based Year view                         |\n| `df-year-fixed-day-cell`  | Day cell in fixed-week Year view                                     |\n\n---\n\n## Mini Calendar\n\nCSS classes for the Mini Calendar component (usually found in the sidebar or day view).\n\n| Class Name                     | Description                                |\n| ------------------------------ | ------------------------------------------ |\n| `df-mini-calendar`             | Mini calendar container                    |\n| `df-mini-calendar-body`        | Mini calendar body wrapper                 |\n| `df-mini-calendar-header-nav`  | Month navigation row                       |\n| `df-mini-calendar-nav-btn`     | Previous / next month button               |\n| `df-mini-calendar-month-label` | Current month label                        |\n| `df-mini-calendar-grid`        | The grid container for days                |\n| `df-mini-calendar-header`      | Weekday header cell (e.g., \"Mo\", \"Tu\")     |\n| `df-mini-calendar-day`         | Base class for a mini calendar date button |\n| `df-mini-calendar-day-cell`    | Styled date cell surface                   |\n| `df-mini-calendar-day-number`  | Numeric day label                          |\n| `df-mini-calendar-dots`        | Event-dot container inside a day cell      |\n| `df-mini-calendar-dot`         | Individual event dot                       |\n\n---\n\n## Sidebar\n\nCSS classes for the sidebar component.\n\n| Class Name                | Description                             |\n| ------------------------- | --------------------------------------- |\n| `df-sidebar`              | Sidebar container                       |\n| `df-sidebar-header`       | Sidebar header container                |\n| `df-sidebar-toggle`       | Sidebar collapse/expand toggle button   |\n| `df-sidebar-header-title` | Sidebar header title (\"Calendars\")      |\n| `df-sidebar-list-shell`   | Scrollable wrapper around sidebar lists |\n| `df-sidebar-list`         | Calendar list container                 |\n| `df-sidebar-list-item`    | Individual calendar item in list        |\n| `df-sidebar-row`          | Interactive row inside each list item   |\n| `df-sidebar-chip`         | Small sidebar label / chip              |\n| `df-sidebar-dialog`       | Sidebar modal/dialog container          |\n| `df-sidebar-button`       | Shared sidebar button class             |\n\n---\n\n## Navigation\n\nCSS classes for navigation buttons.\n\n| Class Name                 | Description                                    |\n| -------------------------- | ---------------------------------------------- |\n| `df-nav-button`            | Base navigation arrow button class (prev/next) |\n| `df-calendar-nav-button`   | Styled calendar navigation arrow button        |\n| `df-today-button`          | Base \"Today\" button class                      |\n| `df-calendar-today-button` | Styled calendar \"Today\" button                 |\n\n---\n\n## Event Detail\n\nCSS classes for event detail panels and dialogs.\n\n| Class Name                 | Description                      |\n| -------------------------- | -------------------------------- |\n| `df-event-detail-panel`    | Floating event detail panel      |\n| `df-event-dialog-overlay`  | Event detail dialog overlay root |\n| `df-event-dialog-backdrop` | Event detail dialog backdrop     |\n| `df-dialog-container`      | Dialog content container         |\n| `df-event-dialog-close`    | Event detail dialog close button |\n| `df-portal`                | Shared portal root class         |\n\n---\n\n## Content Slots\n\nCSS classes used for the Content Slot rendering system.\n\n| Class Name        | Description                                                                            |\n| ----------------- | -------------------------------------------------------------------------------------- |\n| `df-content-slot` | Container for a content slot                                                           |\n| `df-slot-[id]`    | Dynamic class assigned to a specific slot instance where `[id]` is a unique identifier |\n\n---\n\n## Event State Attributes\n\nDayFlow exposes more than just event state flags. Across events, grids, overlays, drawers, and internal layout helpers, the current project uses the following `data-*` attributes. You can target many of them in CSS directly, while a few are primarily structural hooks for overlay logic and click-outside handling.\n\n### Event Elements\n\n| Attribute              | Values                                         | Description                                                             |\n| ---------------------- | ---------------------------------------------- | ----------------------------------------------------------------------- |\n| `data-view`            | `\"day\"` `\"week\"` `\"month\"` `\"year\"` `\"agenda\"` | Which view is currently rendering the event or event fragment           |\n| `data-all-day`         | `\"true\"` `\"false\"`                             | Whether the event is an all-day event                                   |\n| `data-selected`        | `\"true\"` `\"false\"`                             | Whether the event is currently selected                                 |\n| `data-dragging`        | `\"true\"` `\"false\"`                             | Whether the event is currently being dragged                            |\n| `data-resizing`        | `\"true\"` `\"false\"`                             | Whether the event is currently being resized                            |\n| `data-popping`         | `\"true\"` `\"false\"`                             | Whether the event is visually popped / elevated during interaction      |\n| `data-editable`        | `\"true\"` `\"false\"`                             | Whether the event is editable from built-in UI                          |\n| `data-viewable`        | `\"true\"` `\"false\"`                             | Whether the event detail UI may be opened                               |\n| `data-draggable`       | `\"true\"` `\"false\"`                             | Whether dragging is enabled for that event                              |\n| `data-multi-day`       | `\"true\"` `\"false\"`                             | Whether the event spans multiple days                                   |\n| `data-month-stack`     | `\"true\"` `\"false\"`                             | Whether the event is rendered inside the Month view stacked layout      |\n| `data-touch-handles`   | `\"true\"` `\"false\"`                             | Whether touch resize handles should be visible / reserved               |\n| `data-axis`            | View-specific keywords                         | Orientation hint used by compact/timed event renderers                  |\n| `data-density`         | View-specific keywords                         | Density hint used by compact event content                              |\n| `data-position`        | View-specific keywords                         | Positional hint for segmented or compact event renderers                |\n| `data-segment-shape`   | `\"full\"` `\"start\"` `\"middle\"` `\"end\"`          | Rounded-corner shape for multi-segment events                           |\n| `data-segment-days`    | Numeric string                                 | Number of days represented by a rendered month-view event segment       |\n| `data-event-id`        | Event id string                                | Raw event identifier attached to interactive event / panel anchor nodes |\n| `data-continued-left`  | `\"true\"` `\"false\"`                             | Agenda all-day badge continues from a previous day                      |\n| `data-continued-right` | `\"true\"` `\"false\"`                             | Agenda all-day badge continues into a following day                     |\n\n### Date, Grid, and Layout State\n\n| Attribute                | Values                 | Description                                                                |\n| ------------------------ | ---------------------- | -------------------------------------------------------------------------- |\n| `data-today`             | `\"true\"` `\"false\"`     | Marks a date, section, or header cell representing today                   |\n| `data-other-month`       | `\"true\"` `\"false\"`     | Marks dates that belong to the previous / next month                       |\n| `data-current-month`     | `\"true\"` `\"false\"`     | Marks dates rendered inside the current month range                        |\n| `data-date`              | Date string            | Concrete date payload attached to grid / year cells                        |\n| `data-first-day`         | `\"true\"` `\"false\"`     | Marks the first visible day in a grouped year or month structure           |\n| `data-weekend`           | `\"true\"` `\"false\"`     | Marks weekend cells / labels                                               |\n| `data-has-events`        | `\"true\"` `\"false\"`     | Marks grid cells that contain one or more events                           |\n| `data-heat-level`        | Numeric string         | Heatmap intensity level used by grid-based Year view                       |\n| `data-visible`           | `\"true\"` `\"false\"`     | Generic visibility flag used by virtual / conditional layouts              |\n| `data-layout`            | Layout keyword         | Internal layout mode marker used by month / virtual scroll rendering       |\n| `data-layout-ready`      | `\"true\"` `\"false\"`     | Whether a virtualized layout has completed its first measurement pass      |\n| `data-trailing-border`   | `\"true\"` `\"false\"`     | Whether a month cell should render the trailing border                     |\n| `data-scrollbar-space`   | `\"true\"` `\"false\"`     | Whether a view reserves room for a scrollbar                               |\n| `data-secondary-tz`      | `\"true\"` `\"false\"`     | Whether a secondary timezone axis is active                                |\n| `data-show-all-day`      | `\"true\"` `\"false\"`     | Whether the all-day row is currently enabled                               |\n| `data-sliding-view`      | `\"true\"` `\"false\"`     | Whether a view is in a sliding / transition state                          |\n| `data-mobile`            | `\"true\"` `\"false\"`     | Mobile-mode rendering hint used by some compact view fragments             |\n| `data-switcher-mode`     | `\"buttons\"` `\"select\"` | Which built-in view switcher mode is active                                |\n| `data-inside-pill`       | `\"true\"` `\"false\"`     | Whether compact header content is currently rendered inside the today pill |\n| `data-sidebar-collapsed` | `\"true\"` `\"false\"`     | Whether the sidebar is collapsed                                           |\n| `data-sidebar-enabled`   | `\"true\"` `\"false\"`     | Whether the calendar shell is currently mounted with the sidebar plugin    |\n| `data-df-theme`          | Theme keyword          | Theme marker emitted by themed layout wrappers                             |\n\n### Controls, Inputs, and Search\n\n| Attribute                | Values                             | Description                                                         |\n| ------------------------ | ---------------------------------- | ------------------------------------------------------------------- |\n| `data-active`            | `\"true\"` `\"false\"`                 | Generic active-state marker for switches, tabs, and picker controls |\n| `data-open`              | `\"true\"` `\"false\"`                 | Whether a dropdown, drawer, or search surface is currently open     |\n| `data-bordered`          | `\"true\"` `\"false\"`                 | Whether the header should render its bottom border                  |\n| `data-loading`           | `\"true\"` `\"false\"`                 | Loading state for async buttons / actions                           |\n| `data-ready`             | `\"true\"` `\"false\"`                 | Whether a quick-create overlay has completed initial positioning    |\n| `data-placement`         | Placement keyword                  | Placement hint for popup / quick-create alignment                   |\n| `data-checked`           | `\"true\"` `\"false\"`                 | Checked state for custom switches                                   |\n| `data-disabled`          | `\"true\"` `\"false\"`                 | Disabled state for custom switches / inputs                         |\n| `data-kind`              | Component-specific keyword         | Section / field kind marker used by the mobile event drawer         |\n| `data-expanded`          | `\"true\"` `\"false\"`                 | Expanded / collapsed state for drawer sections                      |\n| `data-closing`           | `\"true\"` `\"false\"`                 | Marks the mobile drawer during its closing transition               |\n| `data-tone`              | `\"default\"` `\"today\"` `\"upcoming\"` | Semantic tone used by grouped search result date headers            |\n| `data-mini-calendar-dot` | Present attribute                  | Marker attached to mini-calendar event dots                         |\n\n### Sidebar Plugin State\n\n| Attribute              | Values             | Description                                                                 |\n| ---------------------- | ------------------ | --------------------------------------------------------------------------- |\n| `data-active`          | `\"true\"` `\"false\"` | Highlights the active calendar row in the sidebar list                      |\n| `data-collapsed`       | `\"true\"` `\"false\"` | Whether a sidebar source group or chevron-controlled panel is collapsed     |\n| `data-draggable`       | `\"true\"` `\"false\"` | Whether a sidebar calendar item can be dragged                              |\n| `data-dragging`        | `\"true\"` `\"false\"` | Whether a sidebar calendar item is currently being dragged                  |\n| `data-position`        | `\"top\"` `\"bottom\"` | Drop-indicator position used while dragging calendars within the sidebar    |\n| `data-selected`        | `\"true\"` `\"false\"` | Selected option state inside sidebar dropdowns / import dialogs             |\n| `data-submenu-content` | Present attribute  | Marks sidebar submenu portal content (for merge menus and related overlays) |\n\n### Overlays and Interaction Hooks\n\n| Attribute                       | Values            | Description                                       |\n| ------------------------------- | ----------------- | ------------------------------------------------- |\n| `data-event-detail-panel`       | Present attribute | Marks the floating event detail panel root        |\n| `data-event-detail-dialog`      | Present attribute | Marks the modal event detail dialog root          |\n| `data-range-picker-popup`       | Present attribute | Marks the range picker popup root                 |\n| `data-calendar-picker-dropdown` | Present attribute | Marks the calendar picker dropdown root           |\n| `data-grid-day-cell`            | Present attribute | Marks clickable day cells in grid-based Year view |\n| `data-grid-day-popup`           | Present attribute | Marks the Year view day popup root                |\n\n### Example\n\n```css\n/* Selected events */\n.df-event[data-selected='true'] {\n  outline: 2px solid var(--df-color-primary);\n}\n\n/* Events while being dragged */\n.df-event[data-dragging='true'] {\n  opacity: 0.6;\n}\n\n/* Only target month-view events */\n.df-event[data-view='month'] {\n  font-size: 11px;\n}\n\n/* All-day events only */\n.df-event[data-all-day='true'] {\n  font-weight: 600;\n}\n\n/* Highlight today's agenda section or compact grid cell */\n[data-today='true'] {\n  background-color: color-mix(in srgb, var(--df-color-primary) 8%, transparent);\n}\n```\n\n---\n\n## How to Customize\n\nTo customize the appearance, target these classes or override the `--df-color-*` variables in your global CSS. Most theme-level changes should not need `!important`; use it only when you intentionally want to beat a highly specific local rule.\n\n### Example\n\n```css\n/* Change the background of all events */\n.df-event {\n  border-radius: 8px !important;\n}\n\n/* Customize the current time indicator */\n.df-current-time-line {\n  z-index: 100;\n}\n.df-current-time-bar {\n  height: 3px !important;\n  background-color: #ef4444 !important;\n}\n\n/* Customize specific view */\n.df-day-view .df-event {\n  border-left: 4px solid #3b82f6;\n}\n\n/* Style the sidebar */\n.df-sidebar {\n  background-color: #f8fafc !important;\n}\n\n/* Style mini calendar */\n.df-mini-calendar-day:hover {\n  background-color: #e2e8f0;\n}\n\n/* Reuse DayFlow semantic helpers in your own slot or plugin markup */\n.my-custom-primary-chip {\n  background-color: var(--df-color-primary);\n  color: var(--df-color-primary-foreground);\n}\n\n/* Dark mode overrides */\n.dark .df-calendar {\n  background-color: #111827;\n}\n\n/* Style event detail panel */\n.df-event-detail-panel {\n  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !important;\n}\n```\n\n## Usage Tips\n\n1. **Specificity**: If your styles don't apply, increase specificity by wrapping the calendar in a custom class:\n\n   ```css\n   .my-custom-calendar .df-event { ... }\n   ```\n\n2. **Responsive Design**: Use media queries to adjust styles for mobile devices:\n\n   ```css\n   @media (max-width: 768px) {\n     .df-event-title {\n       font-size: 10px;\n     }\n     .df-time-label {\n       font-size: 10px;\n     }\n   }\n   ```\n\n3. **Dark Mode**: Use the `.dark` selector for dark mode specific styles:\n\n   ```css\n   .dark .df-month-day-cell {\n     border-color: #374151;\n   }\n   ```\n\n4. **CSS Variables**: DayFlow uses `--df-color-*` custom properties for colors. Override them on `.df-calendar-container` (and `.df-portal` for floating overlays) for scoped color changes:\n   ```css\n   .df-calendar-container,\n   .df-portal {\n     --df-color-primary: #3b82f6;\n     --df-color-primary-foreground: #ffffff;\n   }\n   ```\n\n## Upgrading from Earlier Versions\n\n<Callout title=\"Breaking change: calendar-event class removed\" type=\"warn\">\n  The `calendar-event` class has been removed from event elements. If your stylesheet targets `.calendar-event { ... }`, update it to `.df-event { ... }`.\n\nInternal Tailwind utility classes (`flex`, `rounded-md`, `shadow-sm`, etc.) that were previously present on DayFlow DOM elements have also been removed. These were never part of the public API. Use the documented `df-*` classes and `data-*` attributes for all CSS customization.\n\n</Callout>\n\n## Related Documentation\n\n- [Theme Customization](/docs/guides/theme-customization) - Advanced theming options\n- [Dark Mode](/docs/features/dark-mode) - Dark mode configuration\n"
  },
  {
    "path": "website/content/docs/guides/meta.json",
    "content": "{\n  \"title\": \"Guides\",\n  \"pages\": [\"global-css\", \"theme-customization\", \"timezones\"]\n}\n"
  },
  {
    "path": "website/content/docs/guides/theme-customization.mdx",
    "content": "# Theme Customization Guide\n\nDayFlow Calendar's appearance is controlled by a set of namespaced CSS custom properties (`--df-color-*`). All tokens are defined inside `@layer df-theme`, so host applications can override them without `!important` — regardless of whether they use Tailwind or plain CSS.\n\nDayFlow's shared foundation CSS is the single source of truth for theme tokens, component primitives, and semantic helper classes such as `df-fill-primary` and `df-focus-ring`.\n\n## Table of Contents\n\n- [Custom Calendar Type Colors](#custom-calendar-type-colors)\n- [CSS Variable Reference](#css-variable-reference)\n- [Override Methods](#override-methods)\n- [Tailwind v4 Integration](#tailwind-v4-integration)\n- [Creating a Custom Theme](#creating-a-custom-theme)\n\n## Custom Calendar Type Colors\n\n### Basic Custom Colors\n\nDefine unique colors for each calendar type with separate light and dark variants:\n\n```tsx\nconst calendar = useCalendarApp({\n  calendars: [\n    {\n      id: 'personal',\n      name: 'Personal',\n      colors: {\n        lineColor: '#0891b2', // cyan-600\n        eventColor: '#cffafe', // cyan-100\n        eventSelectedColor: '#a5f3fc', // cyan-200\n        textColor: '#164e63', // cyan-900\n      },\n      darkColors: {\n        lineColor: '#22d3ee', // cyan-400\n        eventColor: '#164e63', // cyan-900\n        eventSelectedColor: '#083344', // cyan-950\n        textColor: '#cffafe', // cyan-100\n      },\n    },\n  ],\n});\n```\n\n### Color Properties Explained\n\n- **lineColor**: Border and accent color (left bar on events)\n- **eventColor**: Event background fill\n- **eventSelectedColor**: Background fill when an event is selected\n- **textColor**: Text color for event title and time\n\n### Brand Color Integration\n\n```tsx\n{\n  id: 'brand',\n  name: 'Brand Events',\n  colors: {\n    lineColor: '#6366f1',       // Brand indigo\n    eventColor: '#e0e7ff', // Indigo-100\n    eventSelectedColor: '#c7d2fe', // Indigo-200\n    textColor: '#312e81',       // Indigo-900\n  },\n  darkColors: {\n    lineColor: '#a5b4fc',       // Indigo-300\n    eventColor: '#312e81', // Indigo-900\n    eventSelectedColor: '#1e1b4b', // Indigo-950\n    textColor: '#e0e7ff',       // Indigo-100\n  },\n}\n```\n\n### Color Generation Helper\n\n```tsx\n// utils/colorGenerator.ts\ninterface ColorSet {\n  lineColor: string;\n  eventColor: string;\n  eventSelectedColor: string;\n  textColor: string;\n}\n\nexport function generateLightColors(baseColor: string): ColorSet {\n  return {\n    lineColor: baseColor,\n    eventColor: lighten(baseColor, 0.9),\n    eventSelectedColor: lighten(baseColor, 0.8),\n    textColor: darken(baseColor, 0.4),\n  };\n}\n\nexport function generateDarkColors(baseColor: string): ColorSet {\n  return {\n    lineColor: lighten(baseColor, 0.3),\n    eventColor: darken(baseColor, 0.6),\n    eventSelectedColor: darken(baseColor, 0.7),\n    textColor: lighten(baseColor, 0.8),\n  };\n}\n```\n\n## CSS Variable Reference\n\nAll DayFlow theme tokens use the `--df-color-` prefix to avoid collisions with any host application variables.\n\n| Variable                            | Purpose                                   |\n| ----------------------------------- | ----------------------------------------- |\n| `--df-color-background`             | Calendar background                       |\n| `--df-color-foreground`             | Primary text color                        |\n| `--df-color-hover`                  | Hover state background                    |\n| `--df-color-border`                 | Borders and dividers                      |\n| `--df-color-card`                   | Card / panel background                   |\n| `--df-color-card-foreground`        | Card text color                           |\n| `--df-color-muted`                  | Subtle background areas                   |\n| `--df-color-muted-foreground`       | Muted / secondary text                    |\n| `--df-color-primary`                | Primary accent (buttons, selected states) |\n| `--df-color-primary-foreground`     | Text on primary-colored backgrounds       |\n| `--df-color-secondary`              | Secondary accent                          |\n| `--df-color-secondary-foreground`   | Text on secondary-colored backgrounds     |\n| `--df-color-destructive`            | Destructive actions                       |\n| `--df-color-destructive-foreground` | Text on destructive backgrounds           |\n\n## Semantic Helper Classes\n\nIn addition to CSS variables, DayFlow now exposes theme-aware semantic helper classes. These are useful when you render custom content inside slots, plugin UI, or wrappers that should visually match the built-in controls.\n\n| Class Name               | Meaning                                                   |\n| ------------------------ | --------------------------------------------------------- |\n| `df-fill-primary`        | Primary filled surface with automatic foreground text     |\n| `df-fill-secondary`      | Secondary filled surface with automatic foreground text   |\n| `df-fill-destructive`    | Destructive filled surface with automatic foreground text |\n| `df-tint-primary`        | Subtle primary selection tint                             |\n| `df-hover-primary`       | Primary-themed hover state                                |\n| `df-hover-primary-solid` | Hover state for filled primary buttons                    |\n| `df-text-primary`        | Primary text color                                        |\n| `df-border-primary`      | Primary border color                                      |\n| `df-ring-primary`        | Primary ring color token                                  |\n| `df-focus-ring`          | Focus helper that applies both primary border and ring    |\n\nPrefer these `df-*` helpers or `--df-color-*` overrides when styling DayFlow. Avoid depending on old internal Tailwind semantic names such as `bg-primary`, `text-primary`, or `hover:bg-primary/90`.\n\n## Override Methods\n\n### Method 1 — Container-level override (works everywhere)\n\nSet variables directly on `.df-calendar-container`. Because this rule is outside any `@layer`, it always wins over the library's defaults regardless of Tailwind version or CSS setup.\n\nIn practice, you will often want to target both `.df-calendar-container` and `.df-portal`:\n\n- `.df-calendar-container` is the root container for the visible calendar surface\n- `.df-portal` is the root class DayFlow uses for floating UI rendered via portals into `document.body`, such as dialogs, dropdowns, and some picker panels\n\nIf you only override `.df-calendar-container`, the main calendar can pick up your theme while portal-based overlays may still use the default tokens.\n\n```css\n/* styles/globals.css */\n.df-calendar-container,\n.df-portal {\n  --df-color-primary: #6366f1;\n  --df-color-background: #f9fafb;\n  --df-color-border: #e0e7ff;\n}\n\n/* Dark mode — target the container when the .dark class is on an ancestor */\n.dark .df-calendar-container,\n.dark .df-portal {\n  --df-color-primary: #a5b4fc;\n  --df-color-background: #1e1e2e;\n  --df-color-border: #312e81;\n}\n```\n\nThis is the recommended approach for most projects. The variables are scoped to the calendar and do not affect anything outside it.\n\n## Tailwind v4 Integration\n\nTailwind v4 is configured entirely through CSS — there is no `tailwind.config.js`. DayFlow's distributed CSS already includes the shared foundation layer, so theme tokens and semantic helper classes are available immediately after import.\n\n### Choosing the right CSS file\n\nDayFlow ships two CSS bundles:\n\n| File                    | Contents                                             | Use when               |\n| ----------------------- | ---------------------------------------------------- | ---------------------- |\n| `styles.css`            | Full bundle including Tailwind preflight (CSS reset) | Not using Tailwind     |\n| `styles.components.css` | Component styles only, **no CSS reset**              | Already using Tailwind |\n\n### Minimal setup\n\n```css\n/* app.css — for Tailwind projects */\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\nIf your project does **not** use Tailwind, import the full bundle instead:\n\n```css\n/* app.css — for non-Tailwind projects */\n@import '@dayflow/core/dist/styles.css';\n```\n\n### Dark mode\n\nDayFlow respects the `.dark` class on any ancestor element (including `<html>`). Apply it with JavaScript:\n\n```ts\ndocument.documentElement.classList.toggle('dark', isDark);\n```\n\nSystem-preference dark mode (no explicit class) is also supported automatically.\n\n**Using `theme.mode` with Tailwind v4**\n\nDayFlow's distributed CSS already includes the `.dark` variant it needs for `theme.mode`. In Tailwind v4 projects, importing `@dayflow/core/dist/styles.components.css` is enough for DayFlow itself to respond to the `.dark` class on `<html>`.\n\nIf you also want your app's own Tailwind `dark:` utilities to follow the same class toggle, add the class-based variant in your CSS entry:\n\n```css\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n\n/* Optional: only needed for your app's own Tailwind dark: utilities */\n@variant dark (.dark &);\n```\n\nWithout this extra line, DayFlow still switches correctly via `theme.mode`; only your own Tailwind `dark:` utilities stay on Tailwind's default media-query behavior.\n\n### Practical recommendation\n\nFor most apps, use this order of precedence:\n\n1. Override `--df-color-*` tokens on `.df-calendar-container`, `.df-portal`, or your own wrapper.\n2. Reuse DayFlow's semantic helpers such as `df-fill-primary` and `df-focus-ring` in custom slot/plugin markup.\n3. Only reach for low-level utility overrides when you are changing layout rather than theme.\n\n### Applying overrides with `@theme`\n\n```css\n@import '@dayflow/core/dist/styles.css' layer(dayflow);\n@import 'tailwindcss';\n\n@theme {\n  --df-color-primary: #6366f1;\n  --df-color-primary-foreground: #ffffff;\n  --df-color-secondary: #8b5cf6;\n  --df-color-secondary-foreground: #ffffff;\n}\n```\n\n### Wrapping the calendar with Tailwind utilities\n\n```tsx\nfunction ThemedCalendar({ calendar }) {\n  return (\n    <div className='rounded-2xl shadow-xl ring-1 ring-gray-200 dark:ring-gray-700'>\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n}\n```\n\n## Creating a Custom Theme\n\n```tsx\n// themes/oceanTheme.ts\nexport const oceanTheme = {\n  mode: 'light' as const,\n  calendars: [\n    {\n      id: 'deep-ocean',\n      name: 'Deep Ocean',\n      colors: {\n        lineColor: '#0369a1',\n        eventColor: '#e0f2fe',\n        eventSelectedColor: '#bae6fd',\n        textColor: '#0c4a6e',\n      },\n      darkColors: {\n        lineColor: '#7dd3fc',\n        eventColor: '#0c4a6e',\n        eventSelectedColor: '#083344',\n        textColor: '#e0f2fe',\n      },\n    },\n    {\n      id: 'coral-reef',\n      name: 'Coral Reef',\n      colors: {\n        lineColor: '#ea580c',\n        eventColor: '#ffedd5',\n        eventSelectedColor: '#fed7aa',\n        textColor: '#7c2d12',\n      },\n      darkColors: {\n        lineColor: '#fb923c',\n        eventColor: '#7c2d12',\n        eventSelectedColor: '#431407',\n        textColor: '#ffedd5',\n      },\n    },\n  ],\n};\n```\n\nPair it with CSS variable overrides for full visual consistency:\n\n```css\n/* Ocean theme CSS tokens */\n.df-calendar-container {\n  --df-color-primary: #0369a1;\n  --df-color-background: #f0f9ff;\n  --df-color-border: #bae6fd;\n}\n\n.dark .df-calendar-container {\n  --df-color-primary: #7dd3fc;\n  --df-color-background: #0c1220;\n  --df-color-border: #0c4a6e;\n}\n```\n\n## Resources\n\n### Tools\n\n- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) - Test color contrast\n- [Coolors](https://coolors.co/) - Generate color palettes\n- [Color Contrast Analyzer](https://www.tpgi.com/color-contrast-checker/) - Desktop tool\n\n### Libraries\n\n- [chroma-js](https://gka.github.io/chroma.js/) - Color manipulation\n- [tinycolor2](https://github.com/bgrins/TinyColor) - Color utilities\n- [color](https://github.com/Qix-/color) - Color conversion\n\n### Tailwind Resources\n\n- [Tailwind v4 Upgrade Guide](https://tailwindcss.com/docs/upgrade-guide) - v3 → v4 migration\n- [Tailwind CSS Layers](https://tailwindcss.com/docs/adding-custom-styles#using-css-and-layer) - Layer documentation\n\n## Related Documentation\n\n- [Dark Mode](/docs/features/dark-mode) - Dark mode overview and API\n- [Calendar Types](/docs/introduction/events#calendar-types) - Event categorization\n- [Use Calendar App](/docs/introduction/use-calendar-app) - Core configuration\n"
  },
  {
    "path": "website/content/docs/guides/timezones.mdx",
    "content": "# Time Zones\n\nDayFlow provides a `TimeZone` enum so you can configure both the app-level `timeZone` and the Day/Week-only `secondaryTimeZone` without memorizing raw IANA strings.\n\n## Timezone Model\n\n- `timeZone`: the primary display and editing timezone for the whole calendar\n- `secondaryTimeZone`: an additional reference timeline shown only in Day and Week views\n\nChanging `timeZone` reprojects all views. Changing `secondaryTimeZone` only changes the extra timeline axis in Day/Week.\n\n## Callback Semantics\n\n- `timeZone` changes how events are displayed and edited in the UI.\n- Switching `timeZone` never triggers `onEventUpdate`, `onEventDrop`, or `onEventResize` by itself.\n- Update callbacks return the canonical event after the edit is applied.\n- In other words, if a Sydney event is viewed in Shanghai and resized there, the edit is interpreted using Shanghai wall time, but the callback payload is converted back to the event's canonical time representation before you persist it.\n\n## Usage\n\n```tsx\nimport { TimeZone } from '@dayflow/core';\nimport {\n  useCalendarApp,\n  createDayView,\n  createWeekView,\n  createMonthView,\n} from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  timeZone: TimeZone.SHANGHAI,\n  views: [\n    createDayView({\n      secondaryTimeZone: TimeZone.NEW_YORK,\n    }),\n    createWeekView({\n      secondaryTimeZone: TimeZone.NEW_YORK,\n    }),\n    createMonthView(),\n  ],\n});\n```\n\n## Available Time Zones\n\n| Enum Key                 | IANA String                      |\n| :----------------------- | :------------------------------- |\n| **UTC/GMT**              |                                  |\n| `TimeZone.UTC`           | `UTC`                            |\n| **North America**        |                                  |\n| `TimeZone.NEW_YORK`      | `America/New_York`               |\n| `TimeZone.CHICAGO`       | `America/Chicago`                |\n| `TimeZone.DENVER`        | `America/Denver`                 |\n| `TimeZone.LOS_ANGELES`   | `America/Los_Angeles`            |\n| `TimeZone.TORONTO`       | `America/Toronto`                |\n| `TimeZone.VANCOUVER`     | `America/Vancouver`              |\n| `TimeZone.PHOENIX`       | `America/Phoenix`                |\n| `TimeZone.ANCHORAGE`     | `America/Anchorage`              |\n| `TimeZone.HONOLULU`      | `Pacific/Honolulu`               |\n| `TimeZone.MEXICO_CITY`   | `America/Mexico_City`            |\n| `TimeZone.WINNIPEG`      | `America/Winnipeg`               |\n| `TimeZone.HALIFAX`       | `America/Halifax`                |\n| `TimeZone.ST_JOHNS`      | `America/St_Johns`               |\n| `TimeZone.DETROIT`       | `America/Detroit`                |\n| `TimeZone.MIAMI`         | `America/Miami`                  |\n| **Europe**               |                                  |\n| `TimeZone.LONDON`        | `Europe/London`                  |\n| `TimeZone.PARIS`         | `Europe/Paris`                   |\n| `TimeZone.BERLIN`        | `Europe/Berlin`                  |\n| `TimeZone.MADRID`        | `Europe/Madrid`                  |\n| `TimeZone.ROME`          | `Europe/Rome`                    |\n| `TimeZone.AMSTERDAM`     | `Europe/Amsterdam`               |\n| `TimeZone.ZURICH`        | `Europe/Zurich`                  |\n| `TimeZone.STOCKHOLM`     | `Europe/Stockholm`               |\n| `TimeZone.OSLO`          | `Europe/Oslo`                    |\n| `TimeZone.COPENHAGEN`    | `Europe/Copenhagen`              |\n| `TimeZone.MOSCOW`        | `Europe/Moscow`                  |\n| `TimeZone.ISTANBUL`      | `Europe/Istanbul`                |\n| `TimeZone.DUBLIN`        | `Europe/Dublin`                  |\n| `TimeZone.LISBON`        | `Europe/Lisbon`                  |\n| `TimeZone.PRAGUE`        | `Europe/Prague`                  |\n| `TimeZone.VIENNA`        | `Europe/Vienna`                  |\n| `TimeZone.WARSAW`        | `Europe/Warsaw`                  |\n| `TimeZone.BRUSSELS`      | `Europe/Brussels`                |\n| `TimeZone.ATHENS`        | `Europe/Athens`                  |\n| `TimeZone.BUCHAREST`     | `Europe/Bucharest`               |\n| `TimeZone.HELSINKI`      | `Europe/Helsinki`                |\n| `TimeZone.KYIV`          | `Europe/Kyiv`                    |\n| `TimeZone.BUDAPEST`      | `Europe/Budapest`                |\n| `TimeZone.BELGRADE`      | `Europe/Belgrade`                |\n| `TimeZone.LUXEMBOURG`    | `Europe/Luxembourg`              |\n| `TimeZone.MONACO`        | `Europe/Monaco`                  |\n| `TimeZone.REYKJAVIK`     | `Atlantic/Reykjavik`             |\n| **Asia**                 |                                  |\n| `TimeZone.TOKYO`         | `Asia/Tokyo`                     |\n| `TimeZone.SHANGHAI`      | `Asia/Shanghai`                  |\n| `TimeZone.HONG_KONG`     | `Asia/Hong_Kong`                 |\n| `TimeZone.TAIPEI`        | `Asia/Taipei`                    |\n| `TimeZone.SEOUL`         | `Asia/Seoul`                     |\n| `TimeZone.SINGAPORE`     | `Asia/Singapore`                 |\n| `TimeZone.HANOI`         | `Asia/Ho_Chi_Minh`               |\n| `TimeZone.BANGKOK`       | `Asia/Bangkok`                   |\n| `TimeZone.JAKARTA`       | `Asia/Jakarta`                   |\n| `TimeZone.KUALA_LUMPUR`  | `Asia/Kuala_Lumpur`              |\n| `TimeZone.MANILA`        | `Asia/Manila`                    |\n| `TimeZone.DUBAI`         | `Asia/Dubai`                     |\n| `TimeZone.KOLKATA`       | `Asia/Kolkata`                   |\n| `TimeZone.RIYADH`        | `Asia/Riyadh`                    |\n| `TimeZone.TEHRAN`        | `Asia/Tehran`                    |\n| `TimeZone.JERUSALEM`     | `Asia/Jerusalem`                 |\n| `TimeZone.TEL_AVIV`      | `Asia/Tel_Aviv`                  |\n| `TimeZone.BAGHDAD`       | `Asia/Baghdad`                   |\n| `TimeZone.DHAKA`         | `Asia/Dhaka`                     |\n| `TimeZone.KARACHI`       | `Asia/Karachi`                   |\n| `TimeZone.KABUL`         | `Asia/Kabul`                     |\n| `TimeZone.KATHMANDU`     | `Asia/Kathmandu`                 |\n| `TimeZone.COLOMBO`       | `Asia/Colombo`                   |\n| `TimeZone.TASHKENT`      | `Asia/Tashkent`                  |\n| `TimeZone.ALMATY`        | `Asia/Almaty`                    |\n| `TimeZone.PHNOM_PENH`    | `Asia/Phnom_Penh`                |\n| `TimeZone.VIENTIANE`     | `Asia/Vientiane`                 |\n| `TimeZone.MUSCAT`        | `Asia/Muscat`                    |\n| **Oceania**              |                                  |\n| `TimeZone.SYDNEY`        | `Australia/Sydney`               |\n| `TimeZone.MELBOURNE`     | `Australia/Melbourne`            |\n| `TimeZone.BRISBANE`      | `Australia/Brisbane`             |\n| `TimeZone.PERTH`         | `Australia/Perth`                |\n| `TimeZone.ADELAIDE`      | `Australia/Adelaide`             |\n| `TimeZone.DARWIN`        | `Australia/Darwin`               |\n| `TimeZone.HOBART`        | `Australia/Hobart`               |\n| `TimeZone.AUCKLAND`      | `Pacific/Auckland`               |\n| `TimeZone.FIJI`          | `Pacific/Fiji`                   |\n| `TimeZone.GUAM`          | `Pacific/Guam`                   |\n| `TimeZone.NOUMEA`        | `Pacific/Noumea`                 |\n| `TimeZone.PAGO_PAGO`     | `Pacific/Pago_Pago`              |\n| `TimeZone.PORT_MORESBY`  | `Pacific/Port_Moresby`           |\n| **South America**        |                                  |\n| `TimeZone.SAO_PAULO`     | `America/Sao_Paulo`              |\n| `TimeZone.BUENOS_AIRES`  | `America/Argentina/Buenos_Aires` |\n| `TimeZone.SANTIAGO`      | `America/Santiago`               |\n| `TimeZone.LIMA`          | `America/Lima`                   |\n| `TimeZone.BOGOTA`        | `America/Bogota`                 |\n| `TimeZone.CARACAS`       | `America/Caracas`                |\n| `TimeZone.LA_PAZ`        | `America/La_Paz`                 |\n| `TimeZone.MONTEVIDEO`    | `America/Montevideo`             |\n| `TimeZone.QUITO`         | `America/Quito`                  |\n| `TimeZone.ASUNCION`      | `America/Asuncion`               |\n| `TimeZone.GEORGETOWN`    | `America/Guyana`                 |\n| **Africa**               |                                  |\n| `TimeZone.CAIRO`         | `Africa/Cairo`                   |\n| `TimeZone.JOHANNESBURG`  | `Africa/Johannesburg`            |\n| `TimeZone.LAGOS`         | `Africa/Lagos`                   |\n| `TimeZone.NAIROBI`       | `Africa/Nairobi`                 |\n| `TimeZone.CASABLANCA`    | `Africa/Casablanca`              |\n| `TimeZone.ALGIERS`       | `Africa/Algiers`                 |\n| `TimeZone.TUNIS`         | `Africa/Tunis`                   |\n| `TimeZone.ADDIS_ABABA`   | `Africa/Addis_Ababa`             |\n| `TimeZone.ACCRA`         | `Africa/Accra`                   |\n| `TimeZone.DAKAR`         | `Africa/Dakar`                   |\n| `TimeZone.LUANDA`        | `Africa/Luanda`                  |\n| `TimeZone.ANTANANARIVO`  | `Indian/Antananarivo`            |\n| `TimeZone.KINSHASA`      | `Africa/Kinshasa`                |\n| `TimeZone.DAR_ES_SALAAM` | `Africa/Dar_es_Salaam`           |\n| **Antarctica**           |                                  |\n| `TimeZone.MCMURDO`       | `Antarctica/McMurdo`             |\n| `TimeZone.CASEY`         | `Antarctica/Casey`               |\n"
  },
  {
    "path": "website/content/docs/introduction/dayflow-calendar.mdx",
    "content": "---\ntitle: DayFlowCalendar\n---\n\n# DayFlowCalendar Component\n\n`DayFlowCalendar` is the high-level component that renders the currently selected view, handles layout, and wires optional UI like the sidebar or event detail dialog. Pair it with `useCalendarApp` and the view factory helpers to get a fully functioning calendar with minimal code.\n\n## Basic Usage\n\n```tsx\nimport {\n  DayFlowCalendar,\n  useCalendarApp,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\nimport '@dayflow/core/dist/styles.css';\n\nexport function CalendarDemo() {\n  const calendar = useCalendarApp({\n    views: [createWeekView()],\n    defaultView: ViewType.WEEK,\n    initialDate: new Date(),\n    events: [],\n    plugins: [createSidebarPlugin()],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\nThe component receives the `calendar` object from `useCalendarApp`, reads the active view, and renders the corresponding view component. Any toolbar or custom UI can live beside `DayFlowCalendar`, but it already includes:\n\n- An optional calendar sidebar (added through `createSidebarPlugin`).\n- Event detail dialog support (driven by `useEventDetailDialog` or a custom renderer).\n- Automatic layout updates when calendars are toggled or views change.\n\n## Props\n\n| Prop                    | Type                   | Required | Description                                                                                         |\n| ----------------------- | ---------------------- | -------- | --------------------------------------------------------------------------------------------------- |\n| `calendar`              | `UseCalendarAppReturn` | Required | Result of `useCalendarApp`. Provides state, registered views, sidebar config, and calendar actions. |\n| `collapsedSafeAreaLeft` | `number`               | Optional | Left padding (px) when the sidebar is collapsed, e.g. to account for a Mac traffic-light area.      |\n| `search`                | `CalendarSearchProps`  | Optional | Configuration for the built-in search functionality.                                                |\n\n> **Slot-based customisation** (event content, detail panel content, detail dialog, color picker, etc.) is handled through framework-native slots rather than props. See [Content Slots](/docs/features/content-slots) for the full list.\n\n## Search Configuration\n\nThe `search` prop allows you to customize the built-in search behavior. By default, it searches event titles and descriptions locally.\n\n```tsx\n<DayFlowCalendar\n  calendar={calendar}\n  search={{\n    debounceDelay: 500,\n    emptyText: 'No events found',\n    onSearch: async keyword => {\n      // Custom async search (e.g., from an API)\n      return fetch(`/api/search?q=${keyword}`).then(res => res.json());\n    },\n  }}\n/>\n```\n\n### Search Props\n\n| Option          | Description                                                                                                                                                                                                                                                                                                                      |\n| :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `debounceDelay` | Time in milliseconds to wait before triggering a search.                                                                                                                                                                                                                                                                         |\n| `onSearch`      | Async function to fetch results based on a keyword.                                                                                                                                                                                                                                                                              |\n| `customSearch`  | Synchronous function to filter events locally with custom logic.                                                                                                                                                                                                                                                                 |\n| `onResultClick` | Optional callback triggered when a search result is clicked. Includes a `defaultAction` callback to invoke the built-in navigation logic (navigating to the event date, highlighting the event, and closing the search UI on mobile). If you don't call `defaultAction()`, you must handle the navigation and UI state yourself. |\n| `emptyText`     | Text to display when no results are found.                                                                                                                                                                                                                                                                                       |\n\n## Sidebar Behavior\n\nThe sidebar is added by installing the `@dayflow/plugin-sidebar` plugin:\n\n```tsx\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\n\nconst calendar = useCalendarApp({\n  views: [createWeekView()],\n  plugins: [\n    createSidebarPlugin({\n      width: 280,\n      initialCollapsed: false,\n      createCalendarMode: 'modal', // 'inline' or 'modal'\n    }),\n  ],\n});\n\nreturn <DayFlowCalendar calendar={calendar} />;\n```\n\n- When `createSidebarPlugin()` is added to plugins, the default sidebar (`DefaultCalendarSidebar`) is rendered with calendar filters and toggle-all controls.\n- You can customize width, default collapsed state, creation mode (`createCalendarMode`), or supply your own renderer via `render` or `renderSidebarHeader`. Your renderer receives `CalendarSidebarRenderProps` (for `render`) or `SidebarHeaderSlotArgs` (for `renderSidebarHeader`), so you can reuse its helper callbacks and data.\n\n## Event Detail Experiences\n\nDayFlow ships with three options for event details:\n\n1. **Default panel mode** – when `useEventDetailDialog` is `false` (the default), clicking an event opens the built-in floating `DefaultEventDetailPanel`. Use the `eventDetailContent` slot to replace the panel body while keeping the default chrome.\n2. **Dialog mode** – set `useEventDetailDialog: true` in `useCalendarApp` to enable the built-in centered modal. Use the `eventDetailDialog` slot to replace it with your own dialog UI.\n3. **Opt out entirely** – set `useEventDetailPanel: false` in `useCalendarApp` to suppress the floating panel when you have your own event modals driven by app callbacks.\n\n```tsx\n// Suppress the built-in panel — use your own modal triggered elsewhere\nconst calendar = useCalendarApp({\n  ...\n  useEventDetailPanel: false,\n});\n\nreturn <DayFlowCalendar calendar={calendar} />;\n```\n\n```tsx\n// Custom panel content (keeps the default panel chrome)\n<DayFlowCalendar\n  calendar={calendar}\n  eventDetailContent={({ event, onClose }) => (\n    <div className='p-4'>\n      <h2>{event.title}</h2>\n      <button onClick={onClose}>Close</button>\n    </div>\n  )}\n/>\n```\n\n```tsx\n// Custom dialog (requires useEventDetailDialog: true in useCalendarApp)\n<DayFlowCalendar\n  calendar={calendar}\n  eventDetailDialog={({ event, isOpen, onClose }) => (\n    <MyDialog isOpen={isOpen} onClose={onClose}>\n      <EventForm event={event} />\n    </MyDialog>\n  )}\n/>\n```\n\n---\n\nIn short, `DayFlowCalendar` handles the orchestration layer—layout, sidebar wiring, and event detail plumbing—so you can focus on configuring `useCalendarApp` and building the surrounding product experience.\n"
  },
  {
    "path": "website/content/docs/introduction/events.mdx",
    "content": "---\ntitle: Events\n---\n\n# Working with Events\n\nEvents are the core data structure in Day Flow. Learn how to create, update, delete, and manage calendar events.\n\n## Event Interface\n\nThe library uses the Temporal API for all date/time handling. Events support three Temporal types:\n\n- **PlainDate**: For all-day events (no time)\n- **PlainDateTime**: For local events (date + time, no timezone) Recommended for most use cases\n- **ZonedDateTime**: For timezone-aware events (international meetings, flights, etc.)\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n```\n\n| Property      | Type                                                                     | Description                                                                                                                                                                                           | Required |\n| ------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |\n| `id`          | `string`                                                                 | Unique identifier for the event                                                                                                                                                                       | Required |\n| `title`       | `string`                                                                 | Event title displayed in the calendar                                                                                                                                                                 | Required |\n| `start`       | `Temporal.PlainDate \\| Temporal.PlainDateTime \\| Temporal.ZonedDateTime` | Event start date/time. Use `PlainDate` for all-day, `PlainDateTime` for local, `ZonedDateTime` for timezone-aware                                                                                     | Required |\n| `end`         | `Temporal.PlainDate \\| Temporal.PlainDateTime \\| Temporal.ZonedDateTime` | Event end date/time                                                                                                                                                                                   | Required |\n| `description` | `string`                                                                 | Optional event description or notes                                                                                                                                                                   | Optional |\n| `allDay`      | `boolean`                                                                | Whether this is an all-day event (default: `false`)                                                                                                                                                   | Optional |\n| `icon`        | `boolean \\| Node`                                                        | Custom icon for the event. `true` (default): show default, `false`: hide, `Node`: custom icon                                                                                                         | Optional |\n| `calendarId`  | `string`                                                                 | Calendar type reference for single-calendar assignment                                                                                                                                                | Optional |\n| `calendarIds` | `string[]`                                                               | List of calendar IDs this event belongs to. When set, takes precedence over `calendarId`. Event is visible if **any** listed calendar is visible. Renders with a multi-color diagonal stripe pattern. | Optional |\n| `meta`        | `Record<string, any>`                                                    | Additional custom metadata (location, attendees, custom fields, etc.)                                                                                                                                 | Optional |\n\n## Creating Events\n\n### Simple Event Creation (Recommended)\n\nUse the `createEvent()` and `createAllDayEvent()` helpers for most use cases:\n\n```tsx\nimport { createEvent, createAllDayEvent } from '@dayflow/core';\nimport '@dayflow/core/dist/styles.css';\n\n// Local timed event (no timezone complexity)\nconst meeting = createEvent({\n  id: '1',\n  title: 'Team Meeting',\n  start: new Date(2024, 9, 15, 10, 0), // October 15, 2024, 10:00 AM\n  end: new Date(2024, 9, 15, 11, 0), // October 15, 2024, 11:00 AM\n  calendarId: 'work',\n});\n\n// All-day event\nconst holiday = createAllDayEvent({\n  id: '2',\n  title: 'Conference',\n  start: new Date(2024, 9, 20),\n  calendarId: 'work',\n});\n```\n\n### Advanced: Using Temporal API Directly\n\nFor more control, use Temporal API directly:\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n\n// Local event with PlainDateTime (recommended)\nconst localEvent: Event = {\n  id: '1',\n  title: 'Team Meeting',\n  start: Temporal.PlainDateTime.from({\n    year: 2024,\n    month: 10,\n    day: 15,\n    hour: 10,\n    minute: 0,\n  }),\n  end: Temporal.PlainDateTime.from({\n    year: 2024,\n    month: 10,\n    day: 15,\n    hour: 11,\n    minute: 0,\n  }),\n};\n\n// All-day event with PlainDate\nconst allDayEvent: Event = {\n  id: '2',\n  title: 'Conference',\n  start: Temporal.PlainDate.from('2024-10-20'),\n  end: Temporal.PlainDate.from('2024-10-22'),\n  allDay: true,\n};\n\n// Timezone-aware event with ZonedDateTime\nconst timezoneEvent: Event = {\n  id: '3',\n  title: 'International Call',\n  start: Temporal.ZonedDateTime.from('2024-10-16T14:00:00[America/New_York]'),\n  end: Temporal.ZonedDateTime.from('2024-10-16T15:00:00[America/New_York]'),\n};\n```\n\n### Event with Metadata\n\n```tsx\nimport { createEvent } from '@dayflow/core';\n\nconst event = createEvent({\n  id: '3',\n  title: 'Client Call',\n  description: 'Discuss Q4 roadmap',\n  start: new Date(2024, 9, 16, 14, 0),\n  end: new Date(2024, 9, 16, 15, 0),\n  calendarId: 'work',\n  meta: {\n    location: 'Zoom',\n    attendees: ['john@example.com', 'jane@example.com'],\n    recurring: false,\n  },\n});\n```\n\n## Managing Events\n\n### Adding Events\n\n```tsx\n// Add a single event\ncalendar.addEvent(event);\n\n// Add multiple events during initialization\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  calendars: [\n    {\n      id: 'work',\n      name: 'Work',\n      colors: {\n        lineColor: '#2563eb',\n        eventColor: '#dbeafe',\n        eventSelectedColor: '#bfdbfe',\n        textColor: '#1e3a8a',\n      },\n    },\n  ],\n  events: [event1, event2, event3],\n});\n```\n\n### Updating Events\n\n```tsx\n// Update an event\ncalendar.updateEvent('event-id', {\n  title: 'Updated Meeting Title',\n  start: new Date(2024, 9, 15, 11, 0),\n  end: new Date(2024, 9, 15, 12, 0),\n});\n\n// Update with pending state (for resize operations)\ncalendar.updateEvent('event-id', updatedEvent, true);\n```\n\n### Deleting Events\n\n```tsx\n// Delete an event by ID\ncalendar.deleteEvent('event-id');\n```\n\n### Getting Events\n\n```tsx\n// Get all events\nconst events = calendar.getEvents();\n\n// Get current events from state\nconst { events } = calendar;\n```\n\n## Event Callbacks\n\nDay Flow provides callbacks to handle event lifecycle:\n\n```tsx\nimport { useCalendarApp, createMonthView, Event } from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  events: initialEvents,\n  callbacks: {\n    onEventCreate: (event: Event) => {\n      console.log('New event created:', event);\n      // Sync with backend\n      api.createEvent(event);\n    },\n    onEventUpdate: (event: Event) => {\n      console.log('Event updated:', event);\n      // Sync with backend\n      api.updateEvent(event);\n    },\n    onEventDelete: (eventId: string) => {\n      console.log('Event deleted:', eventId);\n      // Sync with backend\n      api.deleteEvent(eventId);\n    },\n    onEventDoubleClick: (event: Event, e: MouseEvent) => {\n      console.log('Event double-clicked:', event);\n      // Use e.currentTarget as an anchor for a custom popover\n    },\n  },\n});\n```\n\n## Event State Management\n\n### Basic Usage\n\nYou can use the native state management of your framework (React, Vue, Svelte, Angular) to manage events.\n\n```tsx\nimport { useState } from 'react';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  Event,\n} from '@dayflow/react';\n\nfunction MyCalendar() {\n  const [events, setEvents] = useState<Event[]>([]);\n\n  const calendar = useCalendarApp({\n    views: [createMonthView()],\n    events,\n    callbacks: {\n      onEventCreate: (event: Event) => {\n        setEvents(prev => [...prev, event]);\n      },\n      onEventUpdate: (event: Event) => {\n        setEvents(prev => prev.map(e => (e.id === event.id ? event : e)));\n      },\n      onEventDelete: (eventId: string) => {\n        setEvents(prev => prev.filter(e => e.id !== eventId));\n      },\n    },\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### Syncing with Backend\n\n```tsx\nimport { useCalendarApp, createMonthView, Event } from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  events,\n  callbacks: {\n    onEventCreate: async (event: Event) => {\n      try {\n        // Create event in backend\n        const savedEvent = await api.createEvent(event);\n\n        // Update local state with backend response\n        setEvents(prev => [...prev, savedEvent]);\n      } catch (error) {\n        console.error('Failed to create event:', error);\n        // Optionally remove the optimistic update\n      }\n    },\n    onEventUpdate: async (event: Event) => {\n      try {\n        await api.updateEvent(event);\n        setEvents(prev => prev.map(e => (e.id === event.id ? event : e)));\n      } catch (error) {\n        console.error('Failed to update event:', error);\n      }\n    },\n    onEventDelete: async (eventId: string) => {\n      try {\n        await api.deleteEvent(eventId);\n        setEvents(prev => prev.filter(e => e.id !== eventId));\n      } catch (error) {\n        console.error('Failed to delete event:', error);\n      }\n    },\n  },\n});\n```\n\n## Event Styling with Calendar Types\n\nCustomize event appearance by assigning events to different calendar types using `calendarId`. Each calendar type can have its own color scheme and styling:\n\n```tsx\nimport { createEvent } from '@dayflow/core';\n\n// Work event\nconst workEvent = createEvent({\n  id: '1',\n  title: 'Design Review',\n  start: new Date(2024, 9, 15, 14, 0),\n  end: new Date(2024, 9, 15, 15, 0),\n  calendarId: 'work', // Links to work calendar styling\n});\n\n// Personal event\nconst personalEvent = createEvent({\n  id: '2',\n  title: 'Dentist Appointment',\n  start: new Date(2024, 9, 16, 10, 0),\n  end: new Date(2024, 9, 16, 11, 0),\n  calendarId: 'personal', // Links to personal calendar styling\n});\n\n// Configure calendar types with colors\nconst calendars = [\n  {\n    id: 'work',\n    name: 'Work',\n    colors: {\n      eventColor: '#3b82f6',\n      eventSelectedColor: '#2563eb',\n      lineColor: '#3b82f6',\n      textColor: '#ffffff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'personal',\n    name: 'Personal',\n    colors: {\n      eventColor: '#10b981',\n      eventSelectedColor: '#059669',\n      lineColor: '#10b981',\n      textColor: '#ffffff',\n    },\n    isVisible: true,\n  },\n];\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  events: [workEvent, personalEvent],\n  calendars,\n});\n```\n\n## Multi-Calendar Events\n\n### How it works\n\n- `calendarIds` takes precedence over `calendarId` when both are set.\n- **Visibility**: the event is shown as long as at least one listed calendar is visible.\n- **Visual indicator**: events with multiple calendars render with a **diagonal stripe pattern** background (one stripe per calendar color) and a **multi-color gradient** left-side bar — making them visually distinct from single-calendar events.\n- **Selected state**: reverts to the primary calendar's solid color.\n\n### Example\n\n```tsx\nimport { createEvent, useCalendarApp, createWeekView } from '@dayflow/react';\n\n// This event belongs to both \"team\" and \"marketing\" calendars\nconst sharedEvent = createEvent({\n  id: 'shared-1',\n  title: 'Company All-Hands',\n  start: new Date(2024, 9, 15, 10, 0),\n  end: new Date(2024, 9, 15, 11, 30),\n  calendarIds: ['team', 'marketing'], // multi-calendar\n});\n\n// All-day event spanning three calendars\nconst crossCalendarDay = {\n  id: 'shared-2',\n  title: 'Team Offsite',\n  start: Temporal.PlainDate.from('2024-10-20'),\n  end: Temporal.PlainDate.from('2024-10-22'),\n  allDay: true,\n  calendarIds: ['team', 'personal', 'travel'],\n};\n\nconst calendars = [\n  {\n    id: 'team',\n    name: 'Team',\n    colors: {\n      eventColor: '#3b82f6',\n      lineColor: '#3b82f6',\n      eventSelectedColor: '#2563eb',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'marketing',\n    name: 'Marketing',\n    colors: {\n      eventColor: '#f59e0b',\n      lineColor: '#f59e0b',\n      eventSelectedColor: '#d97706',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'personal',\n    name: 'Personal',\n    colors: {\n      eventColor: '#10b981',\n      lineColor: '#10b981',\n      eventSelectedColor: '#059669',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'travel',\n    name: 'Travel',\n    colors: {\n      eventColor: '#8b5cf6',\n      lineColor: '#8b5cf6',\n      eventSelectedColor: '#7c3aed',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n];\n\nconst calendar = useCalendarApp({\n  views: [createWeekView()],\n  events: [sharedEvent, crossCalendarDay],\n  calendars,\n});\n```\n\n## Multi-Day Events\n\nEvents can span multiple days:\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n\n// Conference spanning 3 days (all-day event)\nconst multiDayEvent: Event = {\n  id: '1',\n  title: 'Tech Conference 2024',\n  start: Temporal.PlainDate.from('2024-10-20'),\n  end: Temporal.PlainDate.from('2024-10-22'),\n  allDay: true,\n  calendarId: 'conferences',\n};\n\n// Meeting spanning across midnight (timed event)\nconst crossMidnightEvent: Event = {\n  id: '2',\n  title: 'Night Shift',\n  start: Temporal.ZonedDateTime.from('2024-10-15T22:00:00[America/New_York]'), // 10 PM\n  end: Temporal.ZonedDateTime.from('2024-10-16T06:00:00[America/New_York]'), // 6 AM next day\n  calendarId: 'shifts',\n};\n```\n\n## Event Metadata\n\nStore additional information in the `meta` field:\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n\nconst event: Event = {\n  id: '1',\n  title: 'Project Kickoff',\n  start: Temporal.ZonedDateTime.from('2024-10-15T10:00:00[America/New_York]'),\n  end: Temporal.ZonedDateTime.from('2024-10-15T11:00:00[America/New_York]'),\n  meta: {\n    // Meeting details\n    location: 'Conference Room A',\n    meetingUrl: 'https://zoom.us/j/123456',\n\n    // Attendees\n    organizer: 'john@example.com',\n    attendees: ['jane@example.com', 'bob@example.com'],\n\n    // Custom fields\n    project: 'Project X',\n    priority: 'high',\n    tags: ['planning', 'kickoff'],\n\n    // Recurring info\n    recurring: true,\n    recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO',\n\n    // Any other data\n    customField: 'custom value',\n  },\n};\n```\n\n## Best Practices\n\n1. **Always provide unique IDs** - Use UUID or database IDs (string format)\n2. **Use helper functions** - Prefer `createEvent()` over manual Temporal construction (90% of use cases)\n3. **Choose the right type**:\n   - Use `PlainDate` for all-day events (birthdays, holidays)\n   - Use `PlainDateTime` for local events (meetings, appointments) **Recommended default**\n   - Use `ZonedDateTime` only for timezone-aware events (international calls, flights)\n4. **Validate time values** - The helper functions validate hour (0-23) and minute (0-59) automatically\n5. **Use calendar types for styling** - Assign `calendarId` to categorize and style events consistently; use `calendarIds` when an event spans multiple calendars\n6. **Leverage the `meta` field** - Store custom data without modifying the Event interface\n7. **Validate event data** - Ensure start < end for timed events\n8. **Optimize event updates** - Batch updates when possible\n\n## Type Reference\n\nFor detailed information about date/time handling, see:\n\n- Event interface: `/src/types/event.ts`\n\n## Related Documentation\n\n- [Views](/docs/introduction/views) - Understanding calendar views\n- [Plugins](/docs/plugins/overview) - Event management with plugins\n- [Getting Started](/docs/introduction/introduction) - Basic usage examples\n"
  },
  {
    "path": "website/content/docs/introduction/index.mdx",
    "content": "---\ntitle: Getting Started\ndescription: Learn how to install and integrate DayFlow into your React, Vue, Svelte, or Angular application. Quick start guide for the universal calendar component.\n---\n\n# Getting Started\n\nWelcome to Day Flow! This guide will help you get up and running quickly.\n\n## Installation\n\n### CLI (Recommended)\n\nRun the interactive setup wizard — it detects your package manager, lets you pick a framework and plugins, then installs everything in one step.\n\n<CreateDayflowTabs />\n\n<CliPreview />\n\n### Manual Installation\n\nPrefer installing packages yourself? Select your framework and package manager:\n\n<FrameworkInstall />\n\n### Import DayFlow Styles\n\n**Important:** You must import the DayFlow styles in your application. Add this import to your main App component or layout file:\n\n```tsx\n// In App.tsx or layout.tsx\nimport '@dayflow/core/dist/styles.css';\n```\n\nAlternatively, you can import it in your CSS file:\n\n```css\n/* src/index.css */\n@import '@dayflow/core/dist/styles.css';\n```\n\n<Callout title=\"Using Tailwind CSS? \" type=\"warn\">\n  Import `styles.components.css` instead to avoid CSS reset conflicts with your existing styles:\n\n```css\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\nSee [Tailwind V4 integration](https://calendar.dayflow.studio/docs/guides/theme-customization#tailwind-v4-integration) for more details.\n\n> **Tip:** If you use the `create-dayflow` setup wizard and choose \"Yes\" for Tailwind CSS, it will add this import for you automatically.\n\n</Callout>\n\n<Callout title=\"Using Next.js Pages Router or Node.js require()?\" type=\"warn\">\n  `@dayflow/core` is **ESM-only** and does not ship a CommonJS build. This means:\n\n- **`require('@dayflow/core')`** will throw an error. Use `import` instead.\n- **Next.js Pages Router** does not transpile `node_modules` ESM by default. Add the package to `transpilePackages` in your `next.config.js`:\n\n```js\n// next.config.js\nmodule.exports = {\n  transpilePackages: ['@dayflow/core'],\n};\n```\n\n- **Next.js App Router** (with `\"use client\"` / Server Components) works out of the box — no extra config needed.\n- **SSR frameworks** (Nuxt, SvelteKit, etc.) generally handle ESM natively. If you encounter import errors, check that your bundler's `ssr.noExternal` or equivalent option includes `@dayflow/core`.\n\n</Callout>\n\n## Basic Usage\n\nSelect your framework to see how to integrate DayFlow into your application.\n\n<FrameworkTabs>\n  <Tab>\n    ```tsx\n    import {\n      useCalendarApp,\n      DayFlowCalendar,\n      createDayView,\n      createWeekView,\n      createMonthView,\n      createEventsPlugin,\n    } from '@dayflow/react';\n    import { createDragPlugin } from '@dayflow/plugin-drag';\n    import { createEvent, createAllDayEvent } from '@dayflow/core';\n    import '@dayflow/core/dist/styles.css';\n\n    function App() {\n      const calendar = useCalendarApp({\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: 'Work',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: 'Team Meeting',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: 'Conference',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      });\n\n      return <DayFlowCalendar calendar={calendar} />;\n    }\n    ```\n\n  </Tab>\n  <Tab>\n    ```vue\n    <template>\n      <DayFlowCalendar :calendar=\"calendar\" />\n    </template>\n\n    <script setup>\n      import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';\n      import {\n        createDayView,\n        createWeekView,\n        createMonthView,\n        createEventsPlugin,\n        createEvent,\n        createAllDayEvent\n      } from '@dayflow/core';\n      import { createDragPlugin } from '@dayflow/plugin-drag';\n      import '@dayflow/core/dist/styles.css';\n\n      const calendar = useCalendarApp({\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: 'Work',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: 'Team Meeting',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: 'Conference',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      });\n    </script>\n    ```\n\n  </Tab>\n  <Tab>\n    ```typescript\n    import { Component } from '@angular/core';\n    import {\n      createDayView,\n      createWeekView,\n      createMonthView,\n      createEventsPlugin,\n      createEvent,\n      createAllDayEvent\n    } from '@dayflow/core';\n    import { createDragPlugin } from '@dayflow/plugin-drag';\n    import { DayFlowCalendarModule } from '@dayflow/angular';\n    import '@dayflow/core/dist/styles.css';\n\n    @Component({\n      selector: 'app-root',\n      standalone: true,\n      imports: [DayFlowCalendarModule],\n      template: `\n        <dayflow-calendar [calendar]=\"calendar\"></dayflow-calendar>\n      `\n    })\n    export class AppComponent {\n      calendar = {\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: 'Work',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: 'Team Meeting',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: 'Conference',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      };\n    }\n    ```\n\n  </Tab>\n  <Tab>\n    ```svelte\n    <script>\n      import { DayFlowCalendar, useCalendarApp } from '@dayflow/svelte';\n      import {\n        createDayView,\n        createWeekView,\n        createMonthView,\n        createEventsPlugin,\n        createEvent,\n        createAllDayEvent\n      } from '@dayflow/core';\n      import { createDragPlugin } from '@dayflow/plugin-drag';\n      import '@dayflow/core/dist/styles.css';\n\n      const calendar = useCalendarApp({\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: 'Work',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: 'Team Meeting',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: 'Conference',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      });\n    </script>\n\n    <DayFlowCalendar {calendar} />\n    ```\n\n  </Tab>\n</FrameworkTabs>\n\n## Customizing Styles\n\nBy default, the calendar takes up the full width of its container. You can customize the height and other styles using CSS. For example, to set a responsive height:\n\n```css\n.df-calendar-container {\n  --df-calendar-height: 760px !important;\n  height: var(--df-calendar-height, 760px) !important;\n}\n\n@media (max-width: 768px) {\n  .df-calendar-container {\n    --df-calendar-height: 550px !important;\n  }\n}\n```\n\n**Note:** The library uses the modern [Temporal API](/docs/temporal-migration) for date/time handling. The helper functions (`createEvent`) make it easy to create events without dealing with Temporal directly.\n"
  },
  {
    "path": "website/content/docs/introduction/meta.json",
    "content": "{\n  \"title\": \"Introduction\",\n  \"defaultOpen\": true,\n  \"pages\": [\n    \"index\",\n    \"events\",\n    \"views\",\n    \"dayflow-calendar\",\n    \"use-calendar-app\",\n    \"resource-grid\",\n    \"resource-timeline\"\n  ]\n}\n"
  },
  {
    "path": "website/content/docs/introduction/pro-installation.mdx",
    "content": "---\ntitle: Pro Installation\ndescription: Install DayFlow Pro packages, configure private registry access, and activate your DayFlow Pro license key.\nstatus: pro\n---\n\n# Pro Installation\n\nDayFlow Pro uses two separate credentials:\n\n- A private npm registry token for installing `@dayflow-pro/*` packages\n- A signed DayFlow Pro license key used to activate Pro features\n\n## 1. Configure private registry access\n\nAdd your DayFlow Pro npm token to `.npmrc` before installing private packages:\n\n```ini\n@dayflow-pro:registry=https://gitlab.com/api/v4/projects/81880038/packages/npm/\n//gitlab.com/api/v4/projects/81880038/packages/npm/:_authToken=${DAYFLOW_PRO_NPM_TOKEN}\n```\n\nAfter that, install the packages you purchased as usual:\n\n```bash\npnpm add @dayflow-pro/license @dayflow-pro/resource-grid\n```\n\n## 2. Register your license key once\n\nRegister your DayFlow Pro license key during app startup:\n\n```tsx\nimport { registerDayflowProLicense } from '@dayflow-pro/license';\n\nregisterDayflowProLicense({\n  token: process.env.NEXT_PUBLIC_DAYFLOW_PRO_LICENSE_TOKEN!,\n});\n```\n\nOnce this is registered, your Pro views and plugins can read the token automatically.\n\n## 3. Use Pro packages normally\n\nAfter installation and registration, you can use Pro packages without repeating the license config in every call:\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { createResourceGridView } from '@dayflow-pro/resource-grid';\n\nfunction App() {\n  const calendar = useCalendarApp({\n    views: [\n      createResourceGridView({\n        mode: 'resourceView',\n        resources: [\n          { id: 'room-a', title: 'Room A' },\n          { id: 'room-b', title: 'Room B' },\n        ],\n      }),\n    ],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n"
  },
  {
    "path": "website/content/docs/introduction/resource-grid.mdx",
    "content": "---\ntitle: Resource Grid\ndescription: DayFlow Pro vertical resource grid view with per-resource columns, date grouping modes, and drag-based scheduling interactions.\nstatus: pro\n---\n\n# Resource Grid\n\n`@dayflow-pro/resource-grid` provides a vertical scheduling grid where time runs top to bottom and resources are rendered as columns. It works well for booking rooms, equipment, studios, desks, or staff shifts.\n\nTry the [live demo](https://pro.dayflow.studio)\n\n## Installation\n\n<PackageTabs pkg='@dayflow-pro/resource-grid' />\n\nRefer to the [Pro Installation](/docs/introduction/pro-installation) guide for installation steps.\n\n## Basic usage\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { createResourceGridView } from '@dayflow-pro/resource-grid';\n\nfunction App() {\n  const calendar = useCalendarApp({\n    views: [\n      createResourceGridView({\n        mode: 'resourceView',\n        resources: [\n          { id: 'room-a', title: 'Room A' },\n          { id: 'room-b', title: 'Room B' },\n        ],\n        visibleDays: 3,\n        hourHeight: 72,\n        firstHour: 8,\n        lastHour: 20,\n      }),\n    ],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## View modes\n\n| Mode              | Description                                                                  |\n| :---------------- | :--------------------------------------------------------------------------- |\n| `resourceView`    | Resources are the primary columns, with dates repeated inside each resource. |\n| `resourcesByDate` | Dates are the primary columns, with resources repeated inside each date.     |\n\nThe package default config is:\n\n```ts\n{\n  visibleDays: 7,\n  showWeekend: true,\n  onlyShowEventDays: false,\n  dayWidth: 120,\n  hourHeight: 72,\n  firstHour: 0,\n  lastHour: 24,\n  stickyHeaders: true,\n}\n```\n\n## Resource and event model\n\nResources are intentionally simple:\n\n```ts\ntype Resource = {\n  id: string;\n  title: string;\n  avatar?: string;\n  color?: string;\n  meta?: Record<string, unknown>;\n};\n```\n\nEvents can either store `resourceId` directly or resolve it through a callback:\n\n```tsx\nconst event = {\n  id: 'booking-1',\n  title: 'Studio recording',\n  start: new Date(2026, 4, 8, 10, 0),\n  end: new Date(2026, 4, 8, 12, 0),\n  resourceId: 'room-a',\n};\n```\n\nResolution order is:\n\n1. `getResourceId(event)` if provided\n2. `event.resourceId`\n3. `event.calendarId`\n\n## Filtering and visibility\n\nThe grid can filter both resources and days:\n\n| Property                              | Type                                | Notes                                              |\n| :------------------------------------ | :---------------------------------- | :------------------------------------------------- |\n| `visibleResourceIds`                  | `string[]`                          | Restricts the grid to a specific resource subset.  |\n| `isResourceVisible`                   | `(resource) => boolean`             | Custom predicate to hide resources entirely.       |\n| `syncResourceVisibilityWithCalendars` | `boolean`                           | Follows calendar visibility automatically.         |\n| `getCalendarIdForResource`            | `(resource) => string \\| undefined` | Overrides resource-to-calendar mapping.            |\n| `showWeekend`                         | `boolean`                           | Includes or excludes weekend days.                 |\n| `onlyShowEventDays`                   | `boolean`                           | Removes visible days that have no matching events. |\n\nWhen calendar syncing is enabled, the fallback mapping is `resource.meta?.calendarId ?? resource.id`.\n\nIf `onlyShowEventDays` removes every day from the current range, the view falls back to the normal date range instead of rendering an empty grid.\n\n## Layout configuration\n\n| Property             | Type                              | Notes                                                      |\n| :------------------- | :-------------------------------- | :--------------------------------------------------------- |\n| `license`            | `PackageLicenseConfig`            | Optional override when you do not use global registration. |\n| `visibleDays`        | `number`                          | Number of days generated from the current date.            |\n| `hourHeight`         | `number`                          | Height of one hour row in pixels.                          |\n| `firstHour`          | `number`                          | First visible hour.                                        |\n| `lastHour`           | `number`                          | Last visible hour.                                         |\n| `dayWidth`           | `number`                          | Base width used for each column.                           |\n| `stickyHeaders`      | `boolean`                         | Keeps headers pinned while scrolling.                      |\n| `initialScrollState` | `{ left?: number; top?: number }` | Sets the initial scroll position.                          |\n\nThe implementation also stretches columns automatically when the viewport is wider than the configured content and the current mode allows it.\n"
  },
  {
    "path": "website/content/docs/introduction/resource-timeline.mdx",
    "content": "---\ntitle: Resource Timeline\ndescription: DayFlow Pro horizontal resource timeline view with grouped resources, milestone rendering, view switching, and scheduler-style interactions.\nstatus: pro\n---\n\n# Resource Timeline\n\n`@dayflow-pro/resource-timeline` adds a scheduler-style horizontal timeline to DayFlow. Time flows left to right, each resource gets its own row, and the view is designed for planning work across people, rooms, projects, or equipment.\n\nTry the [live demo](https://pro.dayflow.studio)\n\nCompared with the built-in calendar views, Resource Timeline is optimized for:\n\n- Long-running tasks and project plans\n- Resource allocation across multiple lanes\n- Milestones and progress tracking\n- Hierarchical or grouped resource lists\n\n## Installation\n\n<PackageTabs pkg='@dayflow-pro/resource-timeline' />\n\nRefer to the [Pro Installation](/docs/introduction/pro-installation) guide for installation steps.\n\n## Basic usage\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { createResourceTimelineView } from '@dayflow-pro/resource-timeline';\n\nfunction App() {\n  const calendar = useCalendarApp({\n    views: [\n      createResourceTimelineView({\n        resources: [\n          {\n            id: 'eng-alice',\n            name: 'Alice Chen',\n            groupId: 'engineering',\n            groupName: 'Engineering',\n            subtitle: 'Frontend',\n          },\n          {\n            id: 'eng-bob',\n            name: 'Bob Torres',\n            groupId: 'engineering',\n            groupName: 'Engineering',\n            subtitle: 'Platform',\n          },\n        ],\n        defaultView: 'week',\n        height: 640,\n        getResourceId: event =>\n          (event as { resourceId?: string }).resourceId ?? event.calendarId,\n      }),\n    ],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## Timeline modes\n\n| Mode      | Description                             |\n| :-------- | :-------------------------------------- |\n| `day`     | One day with hour-based columns.        |\n| `week`    | One week with day-based columns.        |\n| `month`   | One month with day-based columns.       |\n| `quarter` | Three months with week/month groupings. |\n| `year`    | A full year for roadmap-style planning. |\n\nUse `defaultView` to choose the initial mode. You can also control how switching is rendered through `viewSwitcherMode`, which supports `buttons`, `select`, and `filter`.\n\n## Resource model\n\nResources are plain objects. The source code supports more than just `id` and `name`:\n\n```ts\ntype Resource = {\n  id: string;\n  name: string;\n  parentId?: string;\n  collapsed?: boolean;\n  groupId?: string;\n  groupName?: string;\n  order?: number;\n  subtitle?: string;\n  avatar?: string;\n  color?: string;\n  style?: {\n    colors: { color: string; textColor?: string };\n    darkColors?: { color: string; textColor?: string };\n    opacity?: number;\n    borderRadius?: number;\n  };\n  meta?: Record<string, unknown>;\n};\n```\n\nUseful fields:\n\n- `groupId` and `groupName` let you organize resources into grouped sections.\n- `parentId` and `collapsed` support hierarchical resource trees.\n- `subtitle`, `avatar`, `color`, and `style` let you enrich the sidebar presentation.\n- `meta` is often used to store an external identifier such as `calendarId`.\n\n## Event-to-resource binding\n\nThe view resolves each event's row in this order:\n\n1. `getResourceId(event)` if provided\n2. `event.resourceId`\n3. `event.meta?.resourceId`\n4. `event.calendarId`\n\nThat means you can either store `resourceId` directly on your event objects or map existing DayFlow events into resources with a callback.\n\n```tsx\nconst event = {\n  id: 'task-1',\n  title: 'Build export flow',\n  start: new Date(2026, 4, 10, 9, 0),\n  end: new Date(2026, 4, 12, 18, 0),\n  resourceId: 'eng-alice',\n  progress: 65,\n};\n```\n\n## Milestones and event styling\n\nEvents whose `start` and `end` are equal are rendered as milestones automatically.\n\n```tsx\nconst milestone = {\n  id: 'release-gate',\n  title: 'Release approval',\n  start: new Date(2026, 4, 14, 10, 0),\n  end: new Date(2026, 4, 14, 10, 0),\n  resourceId: 'eng-bob',\n};\n```\n\n| Property                | Type                             | Notes                                                                         |\n| :---------------------- | :------------------------------- | :---------------------------------------------------------------------------- |\n| `milestoneLabelDisplay` | `'hover' \\| 'always'`            | Controls whether milestone labels appear only on hover or stay visible.       |\n| `milestoneLaneBehavior` | `'smart' \\| 'overlay'`           | Controls whether milestones share normal lanes when possible or overlay them. |\n| `getEventType`          | `(event) => ResourceEventType`   | Maps events to timeline-specific event types.                                 |\n| `getEventTooltip`       | `(event) => string \\| undefined` | Provides tooltip text for an event.                                           |\n| `styleRegistry`         | `Record<string, EventStyle>`     | Registers custom style presets.                                               |\n| `getEventStyleId`       | `(event) => string \\| undefined` | Selects a style preset for an event.                                          |\n\n## ResourceTimelineViewConfig\n\n### Core\n\n| Property           | Type                                      | Notes                                                      |\n| :----------------- | :---------------------------------------- | :--------------------------------------------------------- |\n| `resources`        | `Resource[]`                              | Required resource list for the timeline.                   |\n| `license`          | `PackageLicenseConfig`                    | Optional override when you do not use global registration. |\n| `defaultView`      | `day \\| week \\| month \\| quarter \\| year` | Initial timeline scale.                                    |\n| `views`            | `SchedulerTimelineView[]`                 | Override the available switchable timeline modes.          |\n| `viewType`         | `ViewType \\| string`                      | Custom view type when integrating with advanced hosts.     |\n| `viewSwitcherMode` | `'buttons' \\| 'select' \\| 'filter'`       | Controls the mode switcher UI.                             |\n| `height`           | `number \\| string`                        | Height of the rendered view.                               |\n\n### Layout\n\n| Property                | Type     | Notes                                              |\n| :---------------------- | :------- | :------------------------------------------------- |\n| `resourcePanelWidth`    | `number` | Preferred sidebar width.                           |\n| `resourcePanelMinWidth` | `number` | Minimum sidebar width.                             |\n| `resourcePanelMaxWidth` | `number` | Maximum sidebar width.                             |\n| `rowHeight`             | `number` | Height of each resource row.                       |\n| `headerHeight`          | `number` | Timeline header height.                            |\n| `hourWidth`             | `number` | Width used by the day mode hour scale.             |\n| `weekDayWidth`          | `number` | Width per day in week mode.                        |\n| `dayWidth`              | `number` | Width per day in month mode.                       |\n| `monthDayWidth`         | `number` | Width per day when rendering dense monthly ranges. |\n| `quarterMonthWidth`     | `number` | Width per month in quarter mode.                   |\n| `yearCellWidth`         | `number` | Width per unit in year mode.                       |\n| `yearRangePast`         | `number` | Extra year range before the current year.          |\n| `yearRangeFuture`       | `number` | Extra year range after the current year.           |\n| `overscanRows`          | `number` | Extra rows rendered outside the viewport.          |\n| `bufferCells`           | `number` | Extra time cells rendered for smoother scrolling.  |\n\n### Resource visibility and syncing\n\n| Property                              | Type                                | Notes                                                      |\n| :------------------------------------ | :---------------------------------- | :--------------------------------------------------------- |\n| `resourceSorter`                      | `(left, right) => number`           | Custom row ordering.                                       |\n| `syncResourceVisibilityWithCalendars` | `boolean`                           | Hides resource rows when their mapped calendar is hidden.  |\n| `getCalendarIdForResource`            | `(resource) => string \\| undefined` | Overrides how a resource maps to calendar visibility.      |\n| `onResourcesUpdate`                   | `(resources) => void`               | Fired when resources are reordered or collapsed in the UI. |\n\nWhen calendar syncing is enabled, the fallback resource-to-calendar mapping is `resource.meta?.calendarId ?? resource.id`.\n\n## Focus, sidebar, and integrations\n\nThe timeline exposes a few integration points that are easy to miss:\n\n- `focusRequest` lets host apps programmatically move the timeline to a specific event and date.\n- `sidebar` passes through scheduler sidebar configuration such as context-menu actions.\n- `customDetailPanelContent` and `customEventDetailDialog` can be passed through the view props when you need custom scheduler detail UIs.\n- `timeFormat` supports `24h` and `12h`.\n- `resizeTimeFormat` customizes the label shown while resizing events.\n"
  },
  {
    "path": "website/content/docs/introduction/use-calendar-app.mdx",
    "content": "---\ntitle: useCalendarApp\n---\n\n# useCalendarApp Reference\n\n`useCalendarApp(config: CalendarAppConfig)` is the hook that connects the DayFlow core to your application. Pass a single configuration object to register views, feed initial events, toggle optional UI, and connect lifecycle callbacks. This guide breaks down every available option.\n\n## Quick Start\n\n```tsx {11-16}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport '@dayflow/core/dist/styles.css';\n\nexport function TeamCalendar() {\n  const calendar = useCalendarApp({\n    views: [createMonthView(), createWeekView()],\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    events: [],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## Configuration Overview\n\n| Option                 | Type                        | Required | Default                                                  | Description                                                                                                                           |\n| ---------------------- | --------------------------- | -------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |\n| `views`                | `CalendarView[]`            | Required | —                                                        | Registers the view definitions (e.g., `createMonthView()`). At least one view is required.                                            |\n| `plugins`              | `CalendarPlugin[]`          | Optional | `[]`                                                     | Installs optional plugins (drag helpers, shortcuts, etc.). Each plugin receives the app instance during `install`.                    |\n| `events`               | `Event[]`                   | Optional | `[]`                                                     | Initial event payload. Use `addEvent`/`updateEvent` afterward to mutate state.                                                        |\n| `callbacks`            | `CalendarCallbacks`         | Optional | `{}`                                                     | Lifecycle hooks that fire when views, dates, or events change—perfect for syncing with APIs.                                          |\n| `defaultView`          | `ViewType`                  | Optional | `ViewType.WEEK`                                          | View that loads first; must exist in `views`.                                                                                         |\n| `initialDate`          | `Date`                      | Optional | `new Date()`                                             | Starting focus date (also seeds visible month calculations).                                                                          |\n| `timeZone`             | `string`                    | Optional | User's system timezone                                   | Global display and editing timezone for all views.                                                                                    |\n| `switcherMode`         | `'buttons' \\| 'select'`     | Optional | `'buttons'`                                              | Controls how the built-in view switcher renders in headers.                                                                           |\n| `calendars`            | `CalendarType[]`            | Optional | `[]`                                                     | Registers calendar categories (Work, Personal, etc.) with colors and visibility.                                                      |\n| `defaultCalendar`      | `string`                    | Optional | First visible calendar                                   | ID used when new events are created.                                                                                                  |\n| `theme`                | `ThemeConfig`               | Optional | `{ mode: 'light' }`                                      | Sets global theme mode and optional token overrides.                                                                                  |\n| `locale`               | `string \\| Locale`          | Optional | `'en-US'`                                                | Sets the locale for internationalization (i18n). Supports language codes (e.g., 'ja') or Locale objects.                              |\n| `useEventDetailDialog` | `boolean`                   | Optional | `false`                                                  | Enables the default modal detail dialog instead of inline panels.                                                                     |\n| `useCalendarHeader`    | `boolean`                   | Optional | `true`                                                   | Enables the default header (`true`) or hides it (`false`). For a custom header, use the `calendarHeader` slot on `DayFlowCalendar`.   |\n| `readOnly`             | `boolean \\| ReadOnlyConfig` | Optional | `false`                                                  | Disables built-in mutation UI. Can be a boolean or a config for fine-grained control (drag/view). Programmatic event APIs still work. |\n| `allDaySortComparator` | `AllDaySortComparator`      | Optional | Calendar-group order with multi-day all-day events first | Custom comparator controlling the row order of all-day events across all views. When provided, it fully overrides the default order.  |\n\n## Key Options\n\n### Views (required)\n\n- Each view is a `CalendarView` object `{ type, component, config }`.\n- DayFlow ships with factories (`createDayView`, `createWeekView`, `createMonthView`, `createAgendaView`, `createYearView`) that return ready-made definitions.\n- `defaultView` must match one of the registered view types; otherwise the app throws during initialization.\n\n### Events\n\n- The provided array becomes the in-memory `CalendarApp.state.events` list.\n- `useCalendarApp` watches app mutations, so calling `calendar.addEvent()` or `calendar.updateEvent()` automatically syncs application state.\n- Make sure events adhere to the `Event` interface (`start`/`end` accept `PlainDate`, `PlainDateTime`, or `ZonedDateTime`).\n\n### Plugins\n\n- A plugin has `{ name, install(app), config }` and is executed once during construction.\n- Use plugins to register drag handlers, keyboard shortcuts, analytics observers, or expose custom APIs via `app.getPlugin(name)`.\n\n### Callbacks\n\n`callbacks` keep state in sync with your backend or analytics layer:\n\n- `onViewChange(view)` fires after a successful view switch.\n- `onDateChange(date)` fires whenever the focused date changes (navigation or selection).\n- `onVisibleRangeChange(start, end, reason)` fires whenever visible date-range shifts. Helps in more precise fetches without additional calculations.\n- `onEventCreate(event)`, `onEventUpdate(event)`, `onEventDelete(id)` mirror CRUD operations—ideal for syncing APIs.\n- `onEventDoubleClick(event, e)` fires when an event is double-clicked. Use `e.currentTarget` as an anchor for external popovers, and return `false` to suppress DayFlow's default detail panel/dialog.\n- `onMoreEventsClick(date)` fires when the \"+ X more\" link is clicked in Month view.\n- `onCalendarCreate(calendar)`, `onCalendarUpdate(calendar)`, `onCalendarDelete(id)` mirror calendar CRUD operations.\n- `onCalendarMerge(sourceId, targetId)` fires when merging two calendars (e.g., merging \"Work\" into \"Personal\").\n- `onRender()` executes when the calendar finishes a render pass (useful for instrumentation).\n\n### defaultView & initialDate\n\n- `defaultView` sets the initial `state.currentView`. If omitted, DayFlow falls back to `ViewType.WEEK`, so explicitly set it for single-view calendars.\n- `initialDate` seeds both `state.currentDate` and the internal `visibleMonth`. Override it if you pull the date from routing or user settings.\n\n### timeZone\n\n- `timeZone` defines the primary display and editing timezone for Day, Week, Month, Agenda, and Year views.\n- If omitted, DayFlow resolves it from the user's system timezone.\n- Changing `timeZone` reprojects the UI, but does not itself mutate event data.\n- Edit callbacks still return canonical event data after the change is applied. DayFlow does not persist the temporary UI projection timezone as the stored event shape.\n- Day/Week `secondaryTimeZone` remains a display-only reference axis and does not replace the primary app timezone.\n\n### switcherMode\n\n- `'buttons'`: renders a horizontal button group, great for desktop.\n- `'select'`: renders a dropdown, perfect when space is limited or you expose many view types.\n\n### calendars & defaultCalendar\n\n- `calendars` defines each calendar type (`id`, `name`, `colors`, visibility). The Calendar Registry automatically picks light/dark colors based on `theme.mode` (`'light' | 'dark' | 'auto'`).\n- `defaultCalendar` determines which calendar new events inherit; if omitted, the first visible calendar wins.\n\n**`CalendarType` properties:**\n\n| Property       | Type                     | Required | Description                                                     |\n| -------------- | ------------------------ | -------- | --------------------------------------------------------------- |\n| `id`           | `string`                 | Yes      | Unique identifier (e.g., `'work'`, `'personal'`).               |\n| `name`         | `string`                 | Yes      | Display name shown in the UI.                                   |\n| `colors`       | `CalendarColors`         | Yes      | Light mode color set (see below).                               |\n| `darkColors`   | `CalendarColors`         | No       | Dark mode color set; falls back to `colors` if omitted.         |\n| `description`  | `string`                 | No       | Optional human-readable description.                            |\n| `icon`         | `string`                 | No       | Emoji or icon name displayed alongside the calendar name.       |\n| `isVisible`    | `boolean`                | No       | Whether events of this calendar are shown. Defaults to `true`.  |\n| `isDefault`    | `boolean`                | No       | Marks this as a system default calendar.                        |\n| `readOnly`     | `boolean`                | No       | Disables drag, resize, and edit UI for events in this calendar. |\n| `source`       | `string`                 | No       | Origin label (e.g., `'Google Calendar'`, `'iCloud'`).           |\n| `subscription` | `{ url, status, meta? }` | No       | Subscription metadata for ICS/remote calendars.                 |\n\n**`CalendarColors` properties:**\n\n| Property             | Type     | Description                                   |\n| -------------------- | -------- | --------------------------------------------- |\n| `eventColor`         | `string` | Event background color (usually translucent). |\n| `eventSelectedColor` | `string` | Event background when selected.               |\n| `lineColor`          | `string` | Accent / border color.                        |\n| `textColor`          | `string` | Event text color.                             |\n\n### theme\n\nThe `theme` configuration controls the visual appearance of the entire calendar:\n\n```tsx\nconst calendar = useCalendarApp({\n  theme: {\n    mode: 'dark', // 'light' | 'dark' | 'auto'\n  },\n});\n```\n\n**Theme Modes:**\n\n- `'light'`: Light mode with light backgrounds and dark text (default)\n- `'dark'`: Dark mode with dark backgrounds and light text\n- `'auto'`: Automatically follows system theme preference\n\n**Programmatic Theme Changes:**\n\n```tsx\n// Get current theme\nconst currentTheme = calendar.app.getTheme();\n\n// Set theme\ncalendar.app.setTheme('dark');\n\n// Subscribe to theme changes\ncalendar.app.subscribeThemeChange(theme => {\n  console.log('Theme changed to:', theme);\n});\n```\n\n**Custom Dark Mode Colors:**\n\nDefine different colors for light and dark modes for each calendar type:\n\n```tsx\nconst calendars = [\n  {\n    id: 'work',\n    name: 'Work',\n    colors: {\n      // Light mode colors\n      lineColor: '#0066cc',\n      eventColor: '#e6f2ff',\n      eventSelectedColor: '#cce4ff',\n      textColor: '#003d7a',\n    },\n    darkColors: {\n      // Dark mode colors\n      lineColor: '#4da6ff',\n      eventColor: '#1a3d5c',\n      eventSelectedColor: '#2a5a8a',\n      textColor: '#b3d9ff',\n    },\n  },\n];\n```\n\nSee [Dark Mode](/docs/features/dark-mode) for comprehensive theming documentation.\n\n### locale\n\nThe `locale` option sets the language and regional settings for the calendar.\n\n- **String:** Use a language code like `'en-US'`, `'ja'`, `'zh'`, `'de'`, `'fr'`, `'es'`, `'ko'`.\n- **Locale Object:** Pass an imported `Locale` object for type safety or a custom object for unsupported languages.\n\n```tsx\n// String code\nconst calendar = useCalendarApp({\n  locale: 'ja',\n});\n\n// Additional Locale object from the localization plugin\nimport { ja } from '@dayflow/plugin-localization';\nconst calendar = useCalendarApp({\n  locale: ja,\n});\n\n// Custom Locale object\nconst customLocale = {\n  code: 'it',\n  messages: { today: 'Oggi', ... }\n};\nconst calendar = useCalendarApp({\n  locale: customLocale,\n});\n```\n\n### useEventDetailDialog\n\n- `true` turns on the default modal dialog (`DefaultEventDetailDialog`).\n- Combine with `eventDetailContent` or `eventDetailDialog` on `DayFlowCalendar` to swap the UI while keeping the core state machine.\n\n### useCalendarHeader\n\n- `true` (default): Renders the built-in calendar header.\n- `false`: Hides the header entirely.\n\nTo render a custom header, use the `calendarHeader` slot on `DayFlowCalendar`. See [Calendar Header](/docs/features/calendar-header) for details.\n\n### readOnly\n\n- `true`: Disables built-in mutation UI (dragging, creating, editing).\n- `ReadOnlyConfig`: Fine-grained control.\n  - `draggable`: Whether dragging is allowed.\n  - `viewable`: Whether opening event details is allowed.\n- Programmatic APIs such as `calendar.addEvent()`, `calendar.updateEvent()`, `calendar.deleteEvent()`, and `calendar.applyEventsChanges()` still work in read-only mode.\n- Use `calendar.canMutateFromUI()` in custom UI to decide whether create/edit/delete controls should be shown.\n- See [Read-only Mode](/docs/features/read-only) for details.\n\n### allDaySortComparator\n\nControls the row order of all-day events in every view (day, week, month, agenda, year).\n\nBy default, DayFlow:\n\n- groups all-day events by `calendarId` in first-seen order\n- keeps multi-day all-day events above single-day all-day events\n- keeps same-calendar all-day events visually grouped together\n\nPass `allDaySortComparator` only when you want to take full control of the final order. Once provided, the comparator result is used directly.\n\nPass a comparator to take full control — it receives two `Event` objects and works like JavaScript's `Array.sort`:\n\n```tsx\nconst calendar = useCalendarApp({\n  allDaySortComparator: (a, b) => a.title.localeCompare(b.title),\n});\n```\n\n**Available exported helper** from `@dayflow/core`:\n\n| Helper              | Behavior                                                                                   |\n| :------------------ | :----------------------------------------------------------------------------------------- |\n| `sortAllDayByTitle` | Sorts all-day events alphabetically by title and fully overrides the default grouping flow |\n\n```tsx\nimport { sortAllDayByTitle } from '@dayflow/core';\n\nconst calendar = useCalendarApp({\n  allDaySortComparator: sortAllDayByTitle,\n});\n```\n\nUse `updateConfig` to change the sort at runtime:\n\n```tsx\ncalendar.app.updateConfig({ allDaySortComparator: sortAllDayByTitle });\n\n// Restore the default calendar-grouped ordering\ncalendar.app.updateConfig({ allDaySortComparator: undefined });\n```\n\n## Advanced Configuration Example\n\n```tsx\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\nimport '@dayflow/core/dist/styles.css';\n\nconst calendars = [\n  {\n    id: 'work',\n    name: 'Work',\n    colors: {\n      eventColor: '#2563eb',\n      eventSelectedColor: '#1d4ed8',\n      lineColor: '#1e40af',\n      textColor: '#ffffff',\n    },\n  },\n  {\n    id: 'personal',\n    name: 'Personal',\n    colors: {\n      eventColor: '#f97316',\n      eventSelectedColor: '#ea580c',\n      lineColor: '#c2410c',\n      textColor: '#ffffff',\n    },\n  },\n];\n\nexport function AdvancedCalendar() {\n  const calendar = useCalendarApp({\n    views: [createMonthView(), createWeekView()],\n    defaultView: ViewType.WEEK,\n    initialDate: new Date('2024-10-01'),\n    events: [],\n    calendars,\n    defaultCalendar: 'work',\n    switcherMode: 'select',\n    callbacks: {\n      onEventUpdate: event => api.events.update(event),\n      onVisibleRangeChange: (start, end, reason) =>\n        preloadVisibleRange(start, end, reason),\n    },\n    plugins: [\n      createSidebarPlugin({\n        width: 280,\n        initialCollapsed: false,\n      }),\n    ],\n    useEventDetailDialog: true,\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n> Tip: The object returned by `useCalendarApp` exposes both state (`currentView`, `currentDate`, `events`) and actions. Share the same instance with `DayFlowCalendar`, your own toolbar, and side panels to keep everything synchronized.\n"
  },
  {
    "path": "website/content/docs/introduction/views.mdx",
    "content": "---\ntitle: Views\n---\n\n# Calendar Views\n\nDay Flow supports five different view types, each optimized for different use cases and user needs. You can switch between views programmatically or provide a UI for users to switch views.\n\n## Available Views\n\n### Day View\n\nThe day view focuses on a single day with detailed time slots, perfect for managing daily schedules.\n\n#### Configuration\n\n| Property              | Type             | Default     | Description                                            |\n| :-------------------- | :--------------- | :---------- | :----------------------------------------------------- |\n| `showAllDay`          | `boolean`        | `true`      | Whether to show the all-day events row.                |\n| `scrollToCurrentTime` | `boolean`        | `true`      | Whether to scroll to the current time on initial load. |\n| `timeFormat`          | `'12h' \\| '24h'` | `'24h'`     | The time format for the time axis.                     |\n| `secondaryTimeZone`   | `string`         | `undefined` | Secondary reference timeline shown in Day view only.   |\n| `hourHeight`          | `number`         | `72`        | Height in pixels of each hour row in the time grid.    |\n\n```tsx {10}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createDayView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [createDayView()],\n    defaultView: ViewType.DAY,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### Week View\n\nThe week view displays a 7-day grid with time slots, showing events with their exact start and end times.\n\n#### Configuration\n\n| Property              | Type                                                 | Default          | Description                                                                                                                        |\n| :-------------------- | :--------------------------------------------------- | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------- |\n| `showWeekends`        | `boolean`                                            | `true`           | Whether to show Saturday and Sunday.                                                                                               |\n| `showAllDay`          | `boolean`                                            | `true`           | Whether to show the all-day events row.                                                                                            |\n| `startOfWeek`         | `number`                                             | `1`              | The start day of the week (0 for Sunday, 1 for Monday, etc.).                                                                      |\n| `scrollToCurrentTime` | `boolean`                                            | `true`           | Whether to scroll to the current time on initial load.                                                                             |\n| `timeFormat`          | `'12h' \\| '24h'`                                     | `'24h'`          | The time format for the time axis.                                                                                                 |\n| `secondaryTimeZone`   | `string`                                             | `undefined`      | Secondary reference timeline shown in Week view only.                                                                              |\n| `gridDateClick`       | `'day-view' \\| 'none' \\| function`                   | `undefined`      | Action when a date cell is clicked.                                                                                                |\n| `gridDateDoubleClick` | `'create-event' \\| 'day-view' \\| 'none' \\| function` | `'create-event'` | Action when a date cell is double-clicked. `'create-event'` (default) creates a 1-hour timed event at the clicked position (hour). |\n| `hourHeight`          | `number`                                             | `72`             | Height in pixels of each hour row in the time grid.                                                                                |\n\n```tsx {10}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [createWeekView()],\n    defaultView: ViewType.WEEK,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### Month View\n\nThe month view displays a traditional calendar grid showing all days in a month.\n\n#### Configuration\n\n| Property              | Type                                                                | Default          | Description                                                                                                                         |\n| :-------------------- | :------------------------------------------------------------------ | :--------------- | :---------------------------------------------------------------------------------------------------------------------------------- |\n| `showWeekNumbers`     | `boolean`                                                           | `false`          | Whether to show week numbers.                                                                                                       |\n| `showMonthIndicator`  | `boolean`                                                           | `true`           | Whether to show the month indicator title when scrolling.                                                                           |\n| `startOfWeek`         | `number`                                                            | `1`              | The start day of the week (0 for Sunday, 1 for Monday, etc.).                                                                       |\n| `snapToMonth`         | `boolean`                                                           | `false`          | When enabled, the view snaps to the start of the dominant month after the user stops scrolling.                                     |\n| `gridDateClick`       | `'week-view' \\| 'day-view' \\| 'none' \\| function`                   | `undefined`      | Action when a date cell is clicked.                                                                                                 |\n| `gridDateDoubleClick` | `'create-event' \\| 'week-view' \\| 'day-view' \\| 'none' \\| function` | `'create-event'` | Action when a date cell is double-clicked. `'create-event'` (default) creates a timed event from 9:00 to 10:00 on the clicked date. |\n| `eventHeight`         | `number`                                                            | `16`             | Height in pixels of each event row in the month grid.                                                                               |\n| `scroll`              | `MonthScrollConfig`                                                 | —                | Controls scroll locking and month-switch animation. See below.                                                                      |\n\n#### MonthScrollConfig\n\n| Property     | Type                    | Default     | Description                                                                                                                                                                                                                      |\n| :----------- | :---------------------- | :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `disabled`   | `boolean`               | `false`     | Disables continuous scrolling. Months can only be changed via the Prev / Next navigation buttons.                                                                                                                                |\n| `transition` | `'fade'` \\| `undefined` | `undefined` | Animation when switching months (only applies when `disabled: true`). `'fade'` fades the current month out with a horizontal slide and fades the next month in. When omitted, the original smooth-scroll animation is preserved. |\n\n```tsx\ncreateMonthView({\n  scroll: {\n    disabled: true, // lock to one month at a time, Prev/Next only\n    transition: 'fade', // optional fade animation (omit to keep smooth scroll)\n  },\n});\n```\n\n```tsx {10}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [createMonthView()],\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### Agenda View\n\nThe agenda view displays events as a chronological list grouped by day. It works especially well for mobile layouts, long-range planning, and feeds where users want to scan upcoming events quickly.\n\n#### Configuration\n\n| Property              | Type                               | Default      | Description                                              |\n| :-------------------- | :--------------------------------- | :----------- | :------------------------------------------------------- |\n| `daysToShow`          | `number`                           | `14`         | Number of consecutive days to render in the agenda list. |\n| `showEmptyDays`       | `boolean`                          | `true`       | Whether to keep days with no events visible in the list. |\n| `timeFormat`          | `'12h' \\| '24h'`                   | `'24h'`      | Time format used for timed event rows.                   |\n| `gridDateClick`       | `'day-view' \\| 'none' \\| function` | `undefined`  | Action when a day section is clicked.                    |\n| `gridDateDoubleClick` | `'day-view' \\| 'none' \\| function` | `'day-view'` | Action when a day section is double-clicked.             |\n\n```tsx {10-15}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createAgendaView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      createAgendaView({\n        daysToShow: 14,\n        timeFormat: '12h',\n        gridDateDoubleClick: 'day-view',\n      }),\n    ],\n    defaultView: ViewType.AGENDA,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### Year View\n\nThe year view provides a comprehensive annual overview. DayFlow's Year View supports three modes: `year-canvas` (continuous grid), `fixed-week` (fixed 52-week columns), and `grid` (a 4x3 month grid perfect for heatmaps).\n\n#### Configuration\n\n| Property                    | Type                                                 | Default          | Description                                                                                                                 |\n| :-------------------------- | :--------------------------------------------------- | :--------------- | :-------------------------------------------------------------------------------------------------------------------------- |\n| `mode`                      | `'year-canvas' \\| 'fixed-week' \\| 'grid'`            | `'year-canvas'`  | The display mode for the year view.                                                                                         |\n| `startOfWeek`               | `number`                                             | `1`              | The start day of the week (0 for Sunday, 1 for Monday, etc.). Used in `fixed-week` and `grid` modes.                        |\n| `showTimedEventsInYearView` | `boolean`                                            | `false`          | Whether to show timed events (dots/indicators/intensity) in the year view.                                                  |\n| `gridDateClick`             | `'popup' \\| 'day-view' \\| 'none' \\| function`        | `'popup'`        | (Grid mode) Action when a date cell is clicked.                                                                             |\n| `gridDateDoubleClick`       | `'create-event' \\| 'day-view' \\| 'none' \\| function` | `'create-event'` | (Grid mode) Action when a date cell is double-clicked. `'create-event'` (default) creates a timed event from 9:00 to 10:00. |\n| `gridPopupContent`          | `function`                                           | `undefined`      | (Grid mode) Custom renderer for the day popup content.                                                                      |\n| `gridHeatmapLevels`         | `number`                                             | `5`              | (Grid mode) Number of heatmap intensity levels to use.                                                                      |\n\n#### Heatmap Customization (Grid Mode)\n\nWhen using `mode: 'grid'`, the calendar acts as a heatmap where the intensity (color) of each day is determined by the number of events. You can customize the colors by overriding the following CSS variables in your global CSS:\n\n```css\n/* Light Mode Heatmap */\n.df-year-grid-month {\n  --heat-1: #ebf5ff;\n  --heat-2: #cfe8ff;\n  --heat-3: #91d5ff;\n  --heat-4: #60a5fa;\n  --heat-5: #3b82f6;\n}\n\n/* Dark Mode Heatmap */\n.dark .df-year-grid-month {\n  --heat-1: #1e3a5f;\n  --heat-2: #2563eb;\n  --heat-3: #1e40af;\n  --heat-4: #3b82f6;\n  --heat-5: #93c5fd;\n}\n```\n\nIf you change `gridHeatmapLevels` to a different number (e.g., `3`), you should provide variables up to that number (e.g., `--heat-1` to `--heat-3`).\n\n```tsx {10-15}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createYearView,\n  ViewType,\n} from '@dayflow/react';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      createYearView({\n        mode: 'grid',\n        showTimedEventsInYearView: true,\n        gridHeatmapLevels: 5,\n      }),\n    ],\n    defaultView: ViewType.YEAR,\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## Using Multiple Views\n\nYou can register multiple views and allow users to switch between them:\n\n```tsx {13-18}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createAgendaView,\n  createDayView,\n  createWeekView,\n  createMonthView,\n  createYearView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MultiViewCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      createAgendaView({ daysToShow: 10 }),\n      createDayView(),\n      createWeekView(),\n      createMonthView(),\n      createYearView(),\n    ],\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    callbacks: {\n      onViewChange: view => {\n        console.log('View changed to:', view);\n      },\n    },\n  });\n\n  return (\n    <div className='h-screen'>\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n}\n```\n\n## Programmatic View Control\n\n### Changing Views\n\n```tsx\n// Change to a specific view\ncalendar.changeView(ViewType.WEEK);\n\n// Get current view\nconst currentView = calendar.app.getCurrentView();\nconsole.log(currentView.type); // 'week'\n```\n\n### Navigation\n\nAll views support the same navigation methods:\n\n```tsx\n// Go to today\ncalendar.goToToday();\n\n// Go to previous period (day/week/month/agenda/year depending on current view)\ncalendar.goToPrevious();\n\n// Go to next period\ncalendar.goToNext();\n\n// Go to a specific date\ncalendar.selectDate(new Date(2024, 9, 15));\n```\n\n## ViewType Enum\n\n```tsx\nenum ViewType {\n  DAY = 'day',\n  WEEK = 'week',\n  MONTH = 'month',\n  AGENDA = 'agenda',\n  YEAR = 'year',\n}\n```\n"
  },
  {
    "path": "website/content/docs/meta.json",
    "content": "{\n  \"title\": \"Documentation\",\n  \"pages\": [\"introduction\", \"plugins\", \"features\", \"guides\", \"ui\"]\n}\n"
  },
  {
    "path": "website/content/docs/plugins/drag.mdx",
    "content": "# Drag & Drop Plugin\n\nThe Drag & Drop plugin enables interactive event management, allowing users to move, resize, and create events directly on the calendar grid.\n\n## Installation\n\nInstall the plugin using your preferred package manager:\n\n<PackageTabs pkg='@dayflow/plugin-drag' />\n\n```tsx\nimport { createDragPlugin } from '@dayflow/plugin-drag';\n```\n\n## Usage\n\n```tsx\nimport { useCalendarApp, ViewType } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createDragPlugin } from '@dayflow/plugin-drag';\n\nfunction MyCalendar() {\n  const dragPlugin = createDragPlugin({\n    enableDrag: true,\n    enableResize: true,\n    enableCreate: true,\n    onEventDrop: (updated, original) => {\n      console.log('Event moved:', updated);\n    },\n    onEventResize: (updated, original) => {\n      console.log('Event resized:', updated);\n    },\n  });\n\n  const calendar = useCalendarApp({\n    // ... views\n    plugins: [dragPlugin],\n  });\n\n  // ...\n}\n```\n\n## Configuration\n\n| Property             | Type         | Default                    | Description                                                   |\n| :------------------- | :----------- | :------------------------- | :------------------------------------------------------------ |\n| `enableDrag`         | `boolean`    | `true`                     | Allow moving events by dragging.                              |\n| `enableResize`       | `boolean`    | `true`                     | Allow changing event duration by resizing.                    |\n| `enableCreate`       | `boolean`    | `true`                     | Allow creating events by double-clicking the grid.            |\n| `enableAllDayCreate` | `boolean`    | `true`                     | Allow creating all-day events by dragging in the all-day row. |\n| `supportedViews`     | `ViewType[]` | `[DAY, WEEK, MONTH, YEAR]` | Views where drag functionality should be enabled.             |\n| `onEventDrop`        | `Function`   | —                          | Callback when an event is moved (drag-and-drop).              |\n| `onEventResize`      | `Function`   | —                          | Callback when an event is resized.                            |\n\n## Plugin API\n\nYou can access the drag service to update configuration dynamically:\n\n```tsx\nconst dragService = calendar.app.getPlugin<DragService>('drag');\n\n// Disable resizing dynamically\ndragService.updateConfig({ enableResize: false });\n```\n"
  },
  {
    "path": "website/content/docs/plugins/events.mdx",
    "content": "# Events Service Plugin\n\nThe Events Service plugin provides advanced event management capabilities, including validation, filtering, and complex querying.\n\n## Installation\n\nThe Events Service plugin is currently part of the `@dayflow/core` package.\n\n```tsx\nimport { createEventsPlugin } from '@dayflow/core';\n```\n\n## Usage\n\n```tsx\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createEventsPlugin,\n} from '@dayflow/react';\n\nfunction MyCalendar() {\n  const eventsPlugin = createEventsPlugin({\n    enableValidation: true,\n    maxEventsPerDay: 50,\n  });\n\n  const calendar = useCalendarApp({\n    // ... views\n    plugins: [eventsPlugin],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## Configuration\n\n| Property                | Type      | Default | Description                                                 |\n| :---------------------- | :-------- | :------ | :---------------------------------------------------------- |\n| `enableAutoRecalculate` | `boolean` | `true`  | Automatically recalculate event segments when dates change. |\n| `enableValidation`      | `boolean` | `true`  | Validate event objects before adding or updating.           |\n| `maxEventsPerDay`       | `number`  | `50`    | Maximum number of events allowed per day.                   |\n\n## Plugin API\n\nAccess the `EventsService` to perform advanced queries:\n\n```tsx\nconst eventsService = calendar.app.getPlugin<EventsService>('events');\n\n// Get events for a specific day\nconst events = eventsService.getByDate(new Date());\n\n// Filter events\nconst workEvents = eventsService.filterEvents(\n  allEvents,\n  e => e.calendarId === 'work'\n);\n```\n\n### Available Methods\n\n- `getAll()`: Get all events.\n- `getById(id)`: Find a specific event.\n- `getByDate(date)`: Get events occurring on a specific date.\n- `getByDateRange(start, end)`: Get events within a range.\n- `validateEvent(event)`: Returns an array of validation errors.\n"
  },
  {
    "path": "website/content/docs/plugins/keyboard-shortcuts.mdx",
    "content": "# Keyboard Shortcuts Plugin\n\nThe Keyboard Shortcuts plugin enables global keyboard controls for navigation, event management, and clipboard operations.\n\n## Installation\n\nInstall the plugin package:\n\n<PackageTabs pkg='@dayflow/plugin-keyboard-shortcuts' />\n\n## Usage\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react';\nimport { createKeyboardShortcutsPlugin } from '@dayflow/plugin-keyboard-shortcuts';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    // ...\n    plugins: [\n      createKeyboardShortcutsPlugin({\n        // Optional configuration\n      }),\n    ],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## Default Shortcuts\n\n| Action                   | Key (Mac)                     | Key (Windows/Linux)        |\n| :----------------------- | :---------------------------- | :------------------------- |\n| **Go to Today**          | `Cmd + T`                     | `Ctrl + T`                 |\n| **Search**               | `Cmd + F`                     | `Ctrl + F`                 |\n| **New Event**            | `Cmd + N`                     | `Ctrl + N`                 |\n| **Navigation**           | `ArrowLeft` / `ArrowRight`    | `ArrowLeft` / `ArrowRight` |\n| **Tab Through Events**   | `Tab` / `Shift + Tab`         | `Tab` / `Shift + Tab`      |\n| **Undo**                 | `Cmd + Z`                     | `Ctrl + Z`                 |\n| **Redo**                 | `Cmd + Shift + Z` / `Cmd + Y` | `Ctrl + Y`                 |\n| **Copy Event**           | `Cmd + C`                     | `Ctrl + C`                 |\n| **Cut Event**            | `Cmd + X`                     | `Ctrl + X`                 |\n| **Paste Event**          | `Cmd + V`                     | `Ctrl + V`                 |\n| **Delete Event**         | `Backspace` / `Delete`        | `Backspace` / `Delete`     |\n| **Close Dialogs/Panels** | `Esc`                         | `Esc`                      |\n\n## Configuration\n\nYou can customize the key mappings and provide custom callbacks for each action:\n\n```tsx\ncreateKeyboardShortcutsPlugin({\n  keyMap: {\n    today: 't',\n    search: 'f',\n    prev: 'ArrowLeft',\n    next: 'ArrowRight',\n    undo: 'z',\n    redo: 'y',\n    delete: 'Delete',\n    newEvent: 'n',\n  },\n  callbacks: {\n    undo: app => {\n      console.log('Custom undo logic');\n      app.undo();\n    },\n    redo: app => {\n      console.log('Custom redo logic');\n      if (app.redo) app.redo();\n    },\n    delete: app => {\n      if (confirm('Are you sure?')) {\n        const selectedId = app.state.selectedEventId;\n        if (selectedId) app.deleteEvent(selectedId);\n      }\n    },\n  },\n});\n```\n\n### Available Callbacks\n\nThe following callbacks are supported in the `callbacks` object:\n\n- `undo`, `redo`, `paste` (receives `app`)\n- `copy`, `cut`, `delete` (receives `app` and `event?: Event`)\n- `today`, `search`, `prev`, `next`, `newEvent`, `dismiss` (receives `app`)\n- `tab` (receives `app` and `reverse: boolean`)\n\n## Plugin API\n\nAccess the plugin handle to control shortcut handling at runtime:\n\n```tsx\nimport { type KeyboardShortcutsService } from '@dayflow/plugin-keyboard-shortcuts';\n\nconst kb = app.getPlugin<KeyboardShortcutsService>('keyboard-shortcuts');\n```\n\n### KeyboardShortcutsService\n\n| Method        | Returns   | Description                                         |\n| :------------ | :-------- | :-------------------------------------------------- |\n| `enable()`    | `void`    | Re-enables shortcut handling                        |\n| `disable()`   | `void`    | Suppresses all shortcuts until `enable()` is called |\n| `isEnabled()` | `boolean` | Returns whether shortcuts are currently active      |\n\nA common use case is suppressing calendar shortcuts while a custom modal or rich text editor is open:\n\n```tsx\nfunction MyModal() {\n  const kb = app.getPlugin<KeyboardShortcutsService>('keyboard-shortcuts');\n\n  useEffect(() => {\n    kb?.disable();\n    return () => kb?.enable(); // restore on unmount\n  }, []);\n\n  // ...\n}\n```\n\nYou can also programmatically dismiss any open UI:\n\n```tsx\napp.dismissUI();\n```\n"
  },
  {
    "path": "website/content/docs/plugins/localization.mdx",
    "content": "# i18n Plugin\n\nThe i18n plugin provides support for multiple languages and locales. By default, Day Flow core only includes the English (`en-US`) locale to keep the bundle size as small as possible.\n\n## Installation\n\nInstall the i18n plugin package:\n\n<PackageTabs pkg='@dayflow/plugin-localization' />\n\n## Usage\n\nTo enable support for additional languages, register the plugin and provide the desired locale objects.\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react';\nimport {\n  createLocalizationPlugin,\n  zh,\n  ja,\n  fr,\n} from '@dayflow/plugin-localization';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      /* your views */\n    ],\n    plugins: [\n      createLocalizationPlugin({\n        locales: [zh, ja, fr], // Register the languages you need\n      }),\n    ],\n    locale: 'zh-CN', // Set the current locale\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## Configuration\n\nThe plugin accepts a list of `Locale` objects to register in the global locale registry.\n\n```tsx\ninterface LocalizationConfig {\n  locales: Locale[];\n}\n```\n\n## Available Locales\n\nThe following locales are currently available in the `@dayflow/plugin-localization` package:\n\n| Export | Language | Locale Code                     |\n| :----- | :------- | :------------------------------ |\n| `zh`   | Chinese  | `zh-CN`                         |\n| `ja`   | Japanese | `ja-JP`                         |\n| `ko`   | Korean   | `ko-KR`                         |\n| `fr`   | French   | `fr-FR`                         |\n| `de`   | German   | `de-DE`                         |\n| `es`   | Spanish  | `es-ES`                         |\n| `en`   | English  | `en-US` (Also built-in to core) |\n\n## Custom Locales\n\nYou can also register your own custom locales without using the plugin, or by passing them to the plugin:\n\n```tsx\nconst myCustomLocale = {\n  code: 'pt-BR',\n  messages: {\n    today: 'Hoje',\n    day: 'Dia',\n    week: 'Semana',\n    month: 'Mês',\n    // ... all other translation keys\n  },\n};\n\nconst calendar = useCalendarApp({\n  plugins: [\n    createLocalizationPlugin({\n      locales: [myCustomLocale],\n    }),\n  ],\n  locale: 'pt-BR',\n});\n```\n"
  },
  {
    "path": "website/content/docs/plugins/meta.json",
    "content": "{\n  \"title\": \"Plugins\",\n  \"defaultOpen\": true,\n  \"pages\": [\n    \"overview\",\n    \"localization\",\n    \"drag\",\n    \"print\",\n    \"events\",\n    \"keyboard-shortcuts\",\n    \"sidebar\"\n  ]\n}\n"
  },
  {
    "path": "website/content/docs/plugins/overview.mdx",
    "content": "# Plugins Overview\n\nDay Flow uses a plugin architecture to extend functionality. Plugins provide modular features that can be enabled, disabled, or configured based on your needs.\n\n## Why Plugins?\n\nThe plugin system allows you to:\n\n- **Enable/disable features** - Only load what you need (e.g., only English by default to save bundle size)\n- **Configure behavior** - Customize plugin settings for specific use cases\n- **Extend functionality** - Create custom plugins for your unique requirements\n- **Access plugin APIs** - Use plugin services directly in your business logic\n\n## Available Plugins\n\nDay Flow provides several built-in and official plugins:\n\n### i18n\n\nSupport for multiple languages. Extracting non-English locales into a plugin allows for a smaller core bundle for English-only applications. [Learn more](../localization)\n\n### Drag & Drop\n\nEnables interactive event management including moving, resizing, and double-click to create events. [Learn more](../drag)\n\n### Events Service\n\nProvides advanced event management capabilities including validation, filtering, and date-range querying. [Learn more](../events)\n\n---\n\n## Plugin Lifecycle\n\n1. **Plugin Creation** - Call the factory function (e.g., `createDragPlugin()`) with your configuration.\n2. **Plugin Registration** - Add the plugin instance to the `plugins` array when initializing your calendar.\n3. **Installation** - The `install()` function is automatically called with the `CalendarApp` instance.\n4. **Usage** - Access plugin APIs via `app.getPlugin('plugin-name')`.\n\n## Creating Custom Plugins\n\nYou can create your own plugins to extend calendar functionality by implementing the `CalendarPlugin` interface:\n\n```tsx\nimport { CalendarPlugin, ICalendarApp } from '@dayflow/core';\n\nexport const myPlugin: CalendarPlugin = {\n  name: 'my-custom-plugin',\n  install(app: ICalendarApp) {\n    console.log('Plugin installed!');\n    // Extend app functionality here\n  },\n};\n```\n\nRefer to [Creating Custom Plugins](#creating-custom-plugins) for more details on custom plugin development.\n"
  },
  {
    "path": "website/content/docs/plugins/print.mdx",
    "content": "---\ntitle: Print Plugin\ndescription: DayFlow Pro print and export workflow with preview, paper sizing, calendar filtering, and multiple printable layouts.\nstatus: pro\n---\n\n# Print Plugin\n\n`@dayflow-pro/plugin-print` adds a built-in print dialog and preview flow for DayFlow. It supports day, week, month, and two year layouts, lets users choose paper size and orientation, and can print either the currently visible event set or all matching events depending on context.\n\nTry the [live demo](https://pro.dayflow.studio)\n\n## Installation\n\n<PackageTabs pkg='@dayflow-pro/plugin-print' />\n\nRefer to the [Pro Installation](/docs/introduction/pro-installation) guide for installation steps.\n\n## Basic usage\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { createPrintPlugin } from '@dayflow-pro/plugin-print';\n\nfunction App() {\n  const printPlugin = createPrintPlugin({\n    defaultOptions: {\n      miniCalendar: true,\n      calendarKeys: true,\n      textSize: 'medium',\n    },\n  });\n\n  const calendar = useCalendarApp({\n    plugins: [printPlugin],\n  });\n\n  return (\n    <>\n      <button onClick={() => printPlugin.api.open()}>Print</button>\n      <DayFlowCalendar calendar={calendar} />\n    </>\n  );\n}\n```\n\nThe plugin also installs two helpers directly onto the calendar app:\n\n```tsx\nawait calendar.openPrintDialog();\nawait calendar.print();\n```\n\n## What the plugin provides\n\n- A print preview dialog rendered into `document.body`\n- Native browser printing through `window.print()`\n- View selection for day, week, month, and yearly layouts\n- Calendar-level filtering through `calendarIds`\n- Paper size and orientation controls\n- Cmd/Ctrl + P shortcut support while the plugin is installed\n\n## Supported print views\n\n| View              | Notes                                                         |\n| :---------------- | :------------------------------------------------------------ |\n| `day`             | Prints one or more selected days.                             |\n| `week`            | Prints a week-style layout.                                   |\n| `month`           | Prints a month grid.                                          |\n| `year-fixed-week` | Year overview using fixed weekly rows. Defaults to landscape. |\n| `year-canvas`     | Dense year overview optimized for portrait output.            |\n\nWhen the dialog opens, it derives its initial view from the current DayFlow view:\n\n- current month view -> `month`\n- current week view -> `week`\n- current day view -> `day`\n- current year view -> `year-fixed-week`\n\n## Plugin configuration\n\n| Property         | Type                            | Default | Notes                                                      |\n| :--------------- | :------------------------------ | :------ | :--------------------------------------------------------- |\n| `enabled`        | `boolean`                       | `true`  | Disables installation entirely when set to `false`.        |\n| `defaultOptions` | `Partial<CalendarPrintOptions>` | —       | Preloads the dialog with custom print options.             |\n| `license`        | `PackageLicenseConfig`          | —       | Optional override when you do not use global registration. |\n\n## CalendarPrintOptions\n\n| Property        | Type                             | Default  | Notes                                          |\n| :-------------- | :------------------------------- | :------- | :--------------------------------------------- |\n| `allDayEvents`  | `boolean`                        | `true`   | Include all-day events.                        |\n| `timedEvents`   | `boolean`                        | `true`   | Include timed events.                          |\n| `miniCalendar`  | `boolean`                        | `true`   | Show the mini calendar in the print header.    |\n| `calendarKeys`  | `boolean`                        | `true`   | Show the calendar legend.                      |\n| `blackAndWhite` | `boolean`                        | `false`  | Render without calendar colors.                |\n| `textSize`      | `'small' \\| 'medium' \\| 'large'` | `medium` | Controls preview and print typography density. |\n\n## CalendarPrintConfig\n\nYou can pre-seed or override the print dialog with a partial config:\n\n```ts\ntype CalendarPrintConfig = {\n  view: 'month' | 'week' | 'day' | 'year-fixed-week' | 'year-canvas';\n  paper: 'A4' | 'Letter';\n  orientation?: 'portrait' | 'landscape';\n  startDate: Date;\n  endDate?: Date;\n  calendarIds: string[];\n  options: CalendarPrintOptions;\n};\n```\n\nImportant behavior from the source:\n\n- `year-fixed-week` forces `landscape`.\n- `year-canvas` forces `portrait`.\n- The dialog injects an `@page` rule before opening the browser's native print UI.\n- The preview supports zooming and panning before print.\n\n## Printing scope\n\nThe dialog does not always pull from the same event source:\n\n- If the selected print view matches the current calendar view and `startDate` matches the app's current date, preview rendering uses `app.getEvents()` for the visible event set.\n- Otherwise it uses `app.getAllEvents()` so printing can cover dates outside the currently rendered screen.\n\nThis is exposed internally as `sourceScope: 'visible' | 'all'`.\n\n## API\n\n`createPrintPlugin()` returns a normal DayFlow plugin with an `api` object:\n\n```ts\ntype PrintPluginApi = {\n  open: () => void;\n  close: () => void;\n  print: (config?: Partial<CalendarPrintConfig>) => void;\n};\n```\n\nNotes:\n\n- `api.open()` opens the print dialog.\n- `api.close()` closes the dialog if it is mounted.\n- `api.print(config)` currently opens the dialog as well; it does not bypass preview yet.\n- The plugin listens for `Cmd/Ctrl + P` globally once installed.\n"
  },
  {
    "path": "website/content/docs/plugins/sidebar.mdx",
    "content": "import { SidebarCustomShowcase } from '@/components/showcase/SidebarShowcases';\n\n# Sidebar Plugin\n\nThe Sidebar plugin adds a calendar management sidebar to your DayFlow calendar. It includes a mini calendar for date navigation, a calendar list with visibility toggles, and full calendar CRUD operations (create, rename, recolor, merge, delete, import/export).\n\n## Installation\n\nInstall the plugin package:\n\n<PackageTabs pkg='@dayflow/plugin-sidebar' />\n\n## Usage\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\n\nfunction MyCalendar() {\n  const sidebarPlugin = createSidebarPlugin({\n    width: 280,\n    createCalendarMode: 'modal',\n  });\n\n  const calendar = useCalendarApp({\n    views: [\n      /* your views */\n    ],\n    plugins: [sidebarPlugin],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## Configuration\n\n| Property                     | Type                                                   | Default                            | Description                                     |\n| :--------------------------- | :----------------------------------------------------- | :--------------------------------- | :---------------------------------------------- |\n| `width`                      | `number \\| string`                                     | `'240px'`                          | Width of the sidebar (e.g. `280` or `'20rem'`)  |\n| `miniWidth`                  | `string`                                               | `'50px'`                           | Width of the sidebar when collapsed             |\n| `initialCollapsed`           | `boolean`                                              | `false`                            | Whether the sidebar starts collapsed            |\n| `createCalendarMode`         | `'inline' \\| 'modal'`                                  | `'inline'`                         | How the \"create calendar\" form is displayed     |\n| `colorPickerMode`            | `'blossom' \\| 'default'`                               | `'default'`                        | Which color picker component to use             |\n| `onSubscribeCalendar`        | `(calendar, events) => Promise<void>`                  | `undefined`                        | Callback triggered after a new subscription     |\n| `onLoadSubscription`         | `(calendar) => Promise<void>`                          | `undefined`                        | Custom loader for existing subscriptions        |\n| `onReorder`                  | `(calendars: CalendarType[]) => void \\| Promise<void>` | `undefined`                        | Callback triggered after calendar reordering    |\n| `componentsOrder`            | `('calendarList' \\| 'miniCalendar')[]`                 | `['calendarList', 'miniCalendar']` | Display order of sidebar components             |\n| `render`                     | `(props: CalendarSidebarRenderProps) => TNode`         | `undefined`                        | Full override for the sidebar UI                |\n| `renderSidebarHeader`        | `(args: SidebarHeaderSlotArgs) => TNode`               | `undefined`                        | Custom sidebar header renderer                  |\n| `renderCalendarContextMenu`  | `(calendar, onClose) => TNode`                         | `undefined`                        | Custom context menu renderer for calendar items |\n| `renderCreateCalendarDialog` | `(props) => TNode`                                     | `undefined`                        | Custom create-calendar dialog renderer          |\n\n## Programmatic API\n\nAfter the calendar has rendered, access `SidebarService` via `app.getPlugin`:\n\n```tsx\nimport { type SidebarService } from '@dayflow/plugin-sidebar';\n\nconst sidebar = app.getPlugin<SidebarService>('sidebar');\n\nsidebar?.collapse(); // Collapse the sidebar\nsidebar?.expand(); // Expand the sidebar\nsidebar?.setCollapsed(true); // Set state explicitly\nconsole.log(sidebar?.isCollapsed()); // Read current state\n```\n\n### SidebarService\n\n| Method                             | Returns   | Description                         |\n| :--------------------------------- | :-------- | :---------------------------------- |\n| `collapse()`                       | `void`    | Collapses the sidebar               |\n| `expand()`                         | `void`    | Expands the sidebar                 |\n| `setCollapsed(collapsed: boolean)` | `void`    | Sets collapsed state explicitly     |\n| `isCollapsed()`                    | `boolean` | Returns the current collapsed state |\n\n> **Note:** These methods are no-ops before the first render. All real interactions happen after the calendar mounts, so this is rarely a concern in practice.\n\n### Binding to a keyboard shortcut\n\n```tsx\nwindow.addEventListener('keydown', e => {\n  if (e.metaKey && e.key === '[') {\n    const sidebar = app.getPlugin<SidebarService>('sidebar');\n    sidebar?.setCollapsed(!sidebar.isCollapsed());\n  }\n});\n```\n\n## Features\n\nThe sidebar provides a ready-made layout with visibility toggles, calendar color swatches, and collapse controls. It also supports calendar creation and context menus, allowing users to add, edit colors, and delete calendars.\n\nBuilt-in features include:\n\n- **Mini Calendar** - A compact month view for quick date navigation\n- **Calendar List** - Toggle visibility of individual calendars with color-coded checkboxes\n- **Create Calendar** - Add new calendars with custom name and color\n- **Rename / Recolor** - Right-click a calendar to rename or change its color\n- **Calendar Reordering** - Drag and drop calendars in the list to reorder them. Use the `onReorder` callback to persist the new order to your backend.\n- **Merge Calendars** - Move all events from one calendar into another\n- **Delete Calendar** - Remove a calendar with a confirmation step (or merge first)\n- **Import / Export** - Import `.ics` files or export calendars to `.ics`\n- **Subscribe to Calendar** - Subscribe to remote `.ics` feeds by URL; subscribed calendars display a badge in the sidebar list\n\n### Live Showcase\n\nExplore the built-in sidebar UI and a custom framework-native implementation below.\n\n<SidebarCustomShowcase />\n\n#### What to explore\n\n- **Visibility Toggles**: Hide and show calendars to see the views update instantly.\n- **Drag & Sync**: Drag events between views; the sidebar keeps upcoming highlights in sync.\n- **Collapse**: Toggle the sidebar to reclaim space when focusing on dense week views.\n\n## Subscribe to Calendar\n\nThe sidebar includes a built-in subscribe feature that lets users add remote ICS calendar feeds. To trigger it, right-click any calendar or the empty area in the sidebar to open the context menu and select **Subscribe to Calendar**, then enter any publicly accessible `.ics` URL.\n\nDayFlow fetches the file, parses the events, and creates a new calendar automatically. Subscribed calendars display a small badge icon in the sidebar list to distinguish them from locally created calendars.\n\n### Automatic Loading & Deduplication\n\nWhen you provide a calendar with a `subscription.url` in the initial configuration, the Sidebar plugin automatically fetches the latest events on mount.\n\nTo prevent visual duplicates when combining locally cached events with fresh subscription data, DayFlow implements **ID-based deduplication**:\n\n- If an event from a subscription has the same `id` as an existing event in the core store, the subscription version takes precedence.\n- This ensures your UI always shows the most up-to-date information without \"doubling up\" on events after a page reload.\n\n### Persisting Subscribed Calendars\n\nDayFlow identifies subscribed calendars by the presence of a `subscription` object on the `CalendarType`.\n\n**Default Behavior for Subscriptions:**\n\n- **Read-only**: UI mutations (editing title, changing calendar, deleting) are disabled via `canMutateFromUI()`.\n- **Non-draggable**: Drag-and-drop actions (moving, resizing) are disabled.\n- **Hidden Notes**: If an event has no description, the \"Note\" field is hidden in the detail panel to keep the UI clean.\n\nTo allow users to modify subscribed events or move them around, you can explicitly override these protections by setting `readOnly: false` on the calendar object:\n\n```tsx\nconst calendar = useCalendarApp({\n  calendars: [\n    {\n      id: 'team-ics',\n      name: 'Team Calendar',\n      subscription: {\n        url: 'https://example.com/calendar.ics',\n        status: 'ready',\n      },\n      // Override default protections:\n      readOnly: false, // Enables both UI mutations and drag-and-drop\n      colors: {\n        /* ... */\n      },\n    },\n  ],\n});\n```\n\nWhen saving calendar state to a backend, preserve the `subscription` field and restore it on the next load.\n\n> **Note:** DayFlow does not auto-refresh subscribed feeds. To keep events up to date, implement periodic fetching in your app and update events via `app.addEvent()` / `app.removeEvent()`.\n\n## Fully Custom Sidebar\n\nYou can replace the sidebar with your own component by passing a `render` function to `createSidebarPlugin`. It receives real-time calendar state and helper actions.\n\n```tsx\nimport {\n  createSidebarPlugin,\n  type CalendarSidebarRenderProps,\n} from '@dayflow/plugin-sidebar';\n\nconst CustomSidebar = ({\n  app,\n  calendars,\n  toggleCalendarVisibility,\n  toggleAll,\n  isCollapsed,\n  setCollapsed,\n}: CalendarSidebarRenderProps) => {\n  if (isCollapsed) {\n    return (\n      <div className='p-2'>\n        <button onClick={() => setCollapsed(false)}>→</button>\n      </div>\n    );\n  }\n\n  return (\n    <aside className='flex h-full flex-col gap-4 p-4 bg-slate-50 border-r'>\n      <header className='flex items-center justify-between'>\n        <h3 className='font-semibold'>My Workspace</h3>\n        <button onClick={() => setCollapsed(true)}>←</button>\n      </header>\n\n      <nav className='space-y-1'>\n        {calendars.map(calendar => (\n          <label\n            key={calendar.id}\n            className='flex items-center gap-2 cursor-pointer'\n          >\n            <input\n              type='checkbox'\n              checked={calendar.isVisible}\n              onChange={e =>\n                toggleCalendarVisibility(calendar.id, e.target.checked)\n              }\n            />\n            <span\n              className='w-3 h-3 rounded-full'\n              style={{ backgroundColor: calendar.colors.lineColor }}\n            />\n            {calendar.name}\n          </label>\n        ))}\n      </nav>\n\n      <div className='mt-auto pt-4 border-t text-xs text-slate-500'>\n        Total Events: {app.getEvents().length}\n      </div>\n    </aside>\n  );\n};\n\nconst sidebarPlugin = createSidebarPlugin({\n  render: props => <CustomSidebar {...props} />,\n});\n```\n\n## Custom Context Menu\n\nYou can replace the default right-click menu for calendar items:\n\n```tsx\ncreateSidebarPlugin({\n  renderCalendarContextMenu: (calendar, onClose) => (\n    <div className='bg-white shadow-lg border rounded p-2'>\n      <button\n        onClick={() => {\n          console.log('Custom action');\n          onClose();\n        }}\n      >\n        Custom Action for {calendar.name}\n      </button>\n    </div>\n  ),\n});\n```\n\n## Custom Create Calendar Dialog\n\nYou can replace the default create-calendar dialog:\n\n```tsx\ncreateSidebarPlugin({\n  renderCreateCalendarDialog: ({ onCreate, onClose }) => (\n    <MyCustomDialog onSave={onCreate} onCancel={onClose} />\n  ),\n});\n```\n\n## Custom Sidebar Header\n\nYou can replace the default sidebar header (the area with the \"Calendars\" title and collapse toggle):\n\n```tsx\ncreateSidebarPlugin({\n  renderSidebarHeader: ({ isCollapsed, onCollapseToggle }) => (\n    <div className='flex items-center justify-between p-4 border-b'>\n      {!isCollapsed && <span className='font-bold text-lg'>My Calendars</span>}\n      <button\n        onClick={onCollapseToggle}\n        className='p-1 hover:bg-slate-100 rounded'\n      >\n        {isCollapsed ? '→' : '←'}\n      </button>\n    </div>\n  ),\n});\n```\n"
  },
  {
    "path": "website/content/docs/ui/context-menu.mdx",
    "content": "---\ntitle: Context Menu\n---\n\n# Context Menu\n\nA composable, portal-based context menu for DayFlow UI. Supports nested submenus, labels, separators, and a built-in color picker. Automatically closes on outside click, scroll, resize, or `Escape`.\n\n## Installation\n\n<PackageTabs pkg='@dayflow/ui-context-menu' />\n\nFor projects that already use Tailwind CSS, import the component-only bundle:\n\n```css\n@import '@dayflow/ui-context-menu/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\nFor projects that do not use Tailwind CSS, import the full stylesheet instead:\n\n```tsx\nimport {\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuLabel,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n  ContextMenuColorPicker,\n} from '@dayflow/ui-context-menu';\nimport '@dayflow/ui-context-menu/dist/styles.css';\n```\n\n## Basic Usage\n\nRender `ContextMenu` at the cursor position captured from a `contextmenu` event.\n\n```tsx\nimport { useState } from 'react';\nimport {\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n} from '@dayflow/ui-context-menu';\n\nfunction MyComponent() {\n  const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);\n\n  const handleContextMenu = (e: React.MouseEvent) => {\n    e.preventDefault();\n    setMenu({ x: e.clientX, y: e.clientY });\n  };\n\n  return (\n    <div onContextMenu={handleContextMenu}>\n      Right-click here\n      {menu && (\n        <ContextMenu x={menu.x} y={menu.y} onClose={() => setMenu(null)}>\n          <ContextMenuItem onClick={() => console.log('Edit')}>\n            Edit\n          </ContextMenuItem>\n          <ContextMenuItem onClick={() => console.log('Copy')}>\n            Copy\n          </ContextMenuItem>\n          <ContextMenuSeparator />\n          <ContextMenuItem onClick={() => console.log('Delete')} danger>\n            Delete\n          </ContextMenuItem>\n        </ContextMenu>\n      )}\n    </div>\n  );\n}\n```\n\n## With Labels and Icons\n\n```tsx\n<ContextMenu x={x} y={y} onClose={onClose}>\n  <ContextMenuLabel>Actions</ContextMenuLabel>\n  <ContextMenuItem icon={<PencilIcon />} onClick={() => onEdit()}>\n    Edit Event\n  </ContextMenuItem>\n  <ContextMenuItem icon={<CopyIcon />} onClick={() => onDuplicate()}>\n    Duplicate\n  </ContextMenuItem>\n  <ContextMenuSeparator />\n  <ContextMenuItem icon={<TrashIcon />} onClick={() => onDelete()} danger>\n    Delete\n  </ContextMenuItem>\n</ContextMenu>\n```\n\n## Nested Submenus\n\nWrap items with `ContextMenuSub` to create hover-triggered submenus. The submenu automatically repositions itself to avoid viewport overflow.\n\n```tsx\n<ContextMenu x={x} y={y} onClose={onClose}>\n  <ContextMenuItem onClick={() => onEdit()}>Edit</ContextMenuItem>\n  <ContextMenuSub>\n    <ContextMenuSubTrigger>Move to</ContextMenuSubTrigger>\n    <ContextMenuSubContent>\n      <ContextMenuItem onClick={() => onMove('work')}>Work</ContextMenuItem>\n      <ContextMenuItem onClick={() => onMove('personal')}>\n        Personal\n      </ContextMenuItem>\n    </ContextMenuSubContent>\n  </ContextMenuSub>\n  <ContextMenuSeparator />\n  <ContextMenuItem danger onClick={() => onDelete()}>\n    Delete\n  </ContextMenuItem>\n</ContextMenu>\n```\n\n## Color Picker\n\n`ContextMenuColorPicker` renders a row of preset swatches and an optional \"Custom Color\" action.\n\n```tsx\n<ContextMenu x={x} y={y} onClose={onClose}>\n  <ContextMenuLabel>Calendar Color</ContextMenuLabel>\n  <ContextMenuColorPicker\n    selectedColor={currentColor}\n    onSelect={color => {\n      updateCalendarColor(color);\n      onClose();\n    }}\n    onCustomColor={() => openColorDialog()}\n    customColorLabel='Custom color...'\n  />\n</ContextMenu>\n```\n\n## Disabled Items\n\n```tsx\n<ContextMenuItem onClick={() => onPaste()} disabled={!hasClipboard}>\n  Paste\n</ContextMenuItem>\n```\n\n## API Reference\n\n### `ContextMenu`\n\n| Prop        | Type                | Description                                |\n| :---------- | :------------------ | :----------------------------------------- |\n| `x`         | `number`            | Horizontal position (e.g. `event.clientX`) |\n| `y`         | `number`            | Vertical position (e.g. `event.clientY`)   |\n| `onClose`   | `() => void`        | Called when the menu should close          |\n| `children`  | `ComponentChildren` | Menu items                                 |\n| `className` | `string`            | Extra CSS classes for the menu container   |\n\n### `ContextMenuItem`\n\n| Prop       | Type                | Default | Description                            |\n| :--------- | :------------------ | :------ | :------------------------------------- |\n| `onClick`  | `() => void`        | —       | Click handler                          |\n| `children` | `ComponentChildren` | —       | Item label                             |\n| `icon`     | `ComponentChildren` | —       | Icon rendered to the left of the label |\n| `danger`   | `boolean`           | `false` | Renders the item in destructive red    |\n| `disabled` | `boolean`           | `false` | Makes the item non-interactive         |\n\n### `ContextMenuColorPicker`\n\n| Prop               | Type                      | Default          | Description                                 |\n| :----------------- | :------------------------ | :--------------- | :------------------------------------------ |\n| `selectedColor`    | `string`                  | —                | Currently active color (hex)                |\n| `onSelect`         | `(color: string) => void` | —                | Called when a swatch is clicked             |\n| `onCustomColor`    | `() => void`              | —                | Called when the custom color row is clicked |\n| `customColorLabel` | `string`                  | `\"Custom Color\"` | Label for the custom color action           |\n\n### Other exports\n\n| Component               | Description                                   |\n| :---------------------- | :-------------------------------------------- |\n| `ContextMenuSeparator`  | Horizontal divider line                       |\n| `ContextMenuLabel`      | Non-interactive section heading               |\n| `ContextMenuSub`        | Wrapper that manages open state for a submenu |\n| `ContextMenuSubTrigger` | Row that reveals the submenu on hover         |\n| `ContextMenuSubContent` | The floating submenu panel                    |\n"
  },
  {
    "path": "website/content/docs/ui/meta.json",
    "content": "{\n  \"title\": \"UI\",\n  \"pages\": [\"context-menu\", \"range-picker\"]\n}\n"
  },
  {
    "path": "website/content/docs/ui/range-picker.mdx",
    "content": "---\ntitle: Range Picker\n---\n\n# Range Picker\n\nA date/time range picker built on the Temporal API. Supports date-only and date+time modes, timezone awareness, keyboard input, and flexible popup placement.\n\n## Installation\n\n<PackageTabs pkg='@dayflow/ui-range-picker' />\n\nFor projects that already use Tailwind CSS, import the component-only bundle:\n\n```css\n@import '@dayflow/ui-range-picker/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\nFor projects that do not use Tailwind CSS, import the full stylesheet instead:\n\n```tsx\nimport { RangePicker } from '@dayflow/ui-range-picker';\nimport '@dayflow/ui-range-picker/dist/styles.css';\n```\n\n## Basic Usage\n\n```tsx\nimport { useState } from 'react';\nimport { Temporal } from 'temporal-polyfill';\nimport { RangePicker } from '@dayflow/ui-range-picker';\nimport type { ZonedRange } from '@dayflow/ui-range-picker';\n\nfunction MyComponent() {\n  const [range, setRange] = useState<ZonedRange>([\n    Temporal.Now.zonedDateTimeISO(),\n    Temporal.Now.zonedDateTimeISO().add({ hours: 1 }),\n  ]);\n\n  return <RangePicker value={range} onChange={value => setRange(value)} />;\n}\n```\n\n## Date-Only Mode\n\nPass `showTime={false}` to hide the time selector.\n\n```tsx\n<RangePicker\n  value={range}\n  showTime={false}\n  format='YYYY-MM-DD'\n  onChange={value => setRange(value)}\n/>\n```\n\n## With Timezone\n\n```tsx\n<RangePicker\n  value={range}\n  timeZone='America/New_York'\n  onChange={(value, dateStrings) => {\n    console.log('range:', value);\n    console.log('formatted:', dateStrings); // ['2024-10-15 10:00', '2024-10-15 11:00']\n  }}\n/>\n```\n\n## Custom Time Format\n\n```tsx\n<RangePicker\n  value={range}\n  format='MM/DD/YYYY'\n  showTime={{ format: 'hh:mm A' }}\n  onChange={value => setRange(value)}\n  onOk={value => saveToBackend(value)}\n/>\n```\n\n## Popup Placement\n\nThe popup defaults to `bottomLeft` and automatically adjusts to avoid viewport overflow.\n\n```tsx\n<RangePicker\n  value={range}\n  placement='topRight'\n  autoAdjustOverflow={true}\n  onChange={value => setRange(value)}\n/>\n```\n\n## Locale\n\nPass a BCP 47 locale string to localize month names and weekday labels.\n\n```tsx\n<RangePicker value={range} locale='zh-CN' onChange={value => setRange(value)} />\n```\n\n## Match Trigger Width\n\n```tsx\n<RangePicker\n  value={range}\n  matchTriggerWidth\n  onChange={value => setRange(value)}\n/>\n```\n\n## API Reference\n\n### `RangePicker`\n\n| Prop                 | Type                                                            | Default        | Description                                                                |\n| :------------------- | :-------------------------------------------------------------- | :------------- | :------------------------------------------------------------------------- |\n| `value`              | `[Temporal.PlainDate \\| PlainDateTime \\| ZonedDateTime, ...]`   | —              | Controlled range value. Accepts any mix of Temporal types.                 |\n| `format`             | `string`                                                        | `\"YYYY-MM-DD\"` | Display and parse format for date part                                     |\n| `showTime`           | `boolean \\| { format?: string }`                                | `true`         | Enable time selection. Pass an object to set a custom time format.         |\n| `showTimeFormat`     | `string`                                                        | `\"HH:mm\"`      | Default time format when `showTime` is `true`                              |\n| `onChange`           | `(value: ZonedRange, dateString: [string, string]) => void`     | —              | Fires on every selection change                                            |\n| `onOk`               | `(value: ZonedRange, dateString: [string, string]) => void`     | —              | Fires when the user confirms the selection with the OK button              |\n| `timeZone`           | `string`                                                        | —              | IANA timezone string (e.g. `\"America/New_York\"`). Defaults to system zone. |\n| `disabled`           | `boolean`                                                       | `false`        | Disables all interactions                                                  |\n| `placement`          | `'bottomLeft' \\| 'bottomRight' \\| 'topLeft' \\| 'topRight'`      | `'bottomLeft'` | Preferred popup position                                                   |\n| `autoAdjustOverflow` | `boolean`                                                       | `true`         | Flip placement automatically when the popup would overflow the viewport    |\n| `getPopupContainer`  | `() => HTMLElement`                                             | —              | Mount the popup inside a custom container instead of `document.body`       |\n| `matchTriggerWidth`  | `boolean`                                                       | `false`        | Set the popup width to match the trigger input width                       |\n| `locale`             | `string \\| { code: string; messages?: Record<string, string> }` | `'en-US'`      | BCP 47 locale code for month/weekday labels                                |\n\n### `ZonedRange`\n\n```ts\ntype ZonedRange = [Temporal.ZonedDateTime, Temporal.ZonedDateTime];\n```\n\nThe `onChange` and `onOk` callbacks always receive a `ZonedRange` regardless of the input value type, normalized to the active timezone.\n"
  },
  {
    "path": "website/content/docs-ja/features/calendar-header.mdx",
    "content": "# カレンダーヘッダー\n\nカレンダーヘッダーは、ナビゲーションコントロール、表示切り替え、検索機能を提供します。その動作をカスタマイズしたり、完全に置き換えたりすることができます。\n\n<Callout title='ドキュメントサイトのナビゲーション更新' type='info'>\n  現在のドキュメントサイトのトップナビゲーションには、メインの DayFlow ロゴと\n  Blossom Picker に加えて、独立した DayFlow Pro 入口が追加されています。 この\n  Pro 入口は `pro-logo.png + Pro badge` の構成で表示され、ここで説明している\n  カレンダーコンポーネントの header API\n  そのものではなく、サイト側のナビゲーション要素です。\n</Callout>\n\n## ヘッダーを非表示にする\n\n独自のナビゲーションを構築したい場合や、デフォルトのヘッダーが不要な場合は、無効にすることができます。\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  useCalendarHeader: false, // デフォルトのヘッダーを非表示にする\n});\n```\n\n## カスタムヘッダー\n\n<Callout title='Breaking change from v3.4.1' type='warn'>\n  `useCalendarHeader`\n  への関数の渡しは非対応になりました。以下のように、カスタムヘッダーを\n  `DayFlowCalendar` の `calendarHeader` スロットに移行してください。\n</Callout>\n\n`calendarHeader` コンテンツスロットを使用して、デフォルトのヘッダーを独自のコンポーネントに置き換えることができます。レンダラーはカレンダーの状態とヘルパーメソッドを受け取るため、独自の UI からナビゲーションや検索を制御できます。\n\n### React\n\n`DayFlowCalendar` に `calendarHeader` レンダープロップを渡します：\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n});\n\n<DayFlowCalendar\n  calendar={calendar}\n  calendarHeader={() => (\n    <div className='custom-header'>\n      <button onClick={() => calendar.goToPrevious()}>‹</button>\n      <button onClick={() => calendar.goToToday()}>今日</button>\n      <button onClick={() => calendar.goToNext()}>›</button>\n    </div>\n  )}\n/>;\n```\n\n### Vue\n\n名前付きスロット `calendarHeader` を使用します：\n\n```vue\n<DayFlowCalendar :calendar=\"calendar\">\n  <template #calendarHeader=\"{ calendar, switcherMode, onSearchChange }\">\n    <div class=\"custom-header\">\n      <button @click=\"calendar.goToPrevious()\">‹</button>\n      <button @click=\"calendar.goToToday()\">今日</button>\n      <button @click=\"calendar.goToNext()\">›</button>\n    </div>\n  </template>\n</DayFlowCalendar>\n```\n\n### スロット引数\n\nスロットレンダラーは以下の引数を受け取ります：\n\n| 引数             | 説明                                                                   |\n| :--------------- | :--------------------------------------------------------------------- |\n| `calendar`       | `CalendarApp` インスタンス。                                           |\n| `switcherMode`   | 現在の表示切り替えモード（`buttons` または `select`）。                |\n| `onAddCalendar`  | 新しいカレンダーを追加するためのハンドラー。                           |\n| `onSearchChange` | 検索クエリを更新するためのハンドラー。                                 |\n| `onSearchClick`  | 検索アイコンがクリックされた時のハンドラー。                           |\n| `searchValue`    | 現在の検索文字列。                                                     |\n| `isSearchOpen`   | 検索 UI が現在開いているかどうか。                                     |\n| `isEditable`     | カレンダーが現在編集可能な状態かどうか。                               |\n| `safeAreaLeft`   | macOS モードでトラフィックライトボタンと重ならないための左余白（px）。 |\n"
  },
  {
    "path": "website/content/docs-ja/features/content-slots.mdx",
    "content": "import { EventContentShowcase } from '@/components/showcase/EventContentShowcase';\nimport { CustomDetailPanelShowcase } from '@/components/showcase/CustomDetailPanelShowcase';\nimport { ColorPickerShowcase } from '@/components/showcase/ColorPickerShowcase';\nimport { CustomDetailDialogShowcase } from '@/components/showcase/CustomDetailDialogShowcase';\nimport { ContextMenuShowcase } from '@/components/showcase/ContextMenuShowcase';\nimport { MobileEventDetailShowcase } from '@/components/showcase/MobileEventDetailShowcase';\n\n# コンテンツスロット\n\nDayFlow はスロットベースのアーキテクチャを採用しており、使用しているフレームワーク（React、Vue など）からカスタム UI コンポーネントをコア日暦エンジンに直接注入できます。\n\nこれは `ContentSlot` メカニズムによって実現されています。コアエンジンはほとんどの UI 要素に対して（Preact を使用した）デフォルトの実装を提供していますが、アプリケーションのデザインに合わせたり、特定のライブラリを使用したりするために、これらを上書きすることができます。\n\n## Table of Contents\n\n- [仕組み](#仕組み)\n- [利用可能なスロット](#利用可能なスロット)\n- [イベントコンテンツ](#イベントコンテンツ)\n- [イベント詳細コンテンツ](#イベント詳細コンテンツ)\n- [イベント詳細ダイアログ](#イベント詳細ダイアログ)\n- [モバイルイベント詳細](#モバイルイベント詳細)\n- [カスタムカラーピッカーの注入](#カスタムカラーピッカー의-注入)\n- [コンテキストメニュー](#コンテキストメニュー)\n\n## 仕組み\n\n1. **コアがスロットを定義**: `@dayflow/core` の内部で、特定の UI 領域が `ContentSlot` でラップされています。各スロットには `generatorName` と `generatorArgs` があります。\n2. **実装を提供**: `DayFlowCalendar` コンポーネントにコンポーネントまたはレンダー関数を渡します。アダプターは、コアで定義された正確な場所にコンポーネントをポータル（転送）します。\n\n## 利用可能なスロット\n\n| ジェネレーター名                  | 説明                                                                                                                  | 引数                                                                      |\n| :-------------------------------- | :-------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------ |\n| `colorPicker`                     | イベントの色に使用される小さなカラーピッカー。                                                                        | `{ color, onChange, onChangeComplete }`                                   |\n| `createCalendarDialogColorPicker` | ダイアログで使用されるフル機能のカラーピッカー。                                                                      | `{ color, onChange, onAccept, onCancel, styles }`                         |\n| `eventContent*`                   | イベントカードのカスタムレンダリング（例：`eventContentDay`）。                                                       | `{ event, viewType, isAllDay, isMobile, isSelected, isDragging, layout }` |\n| `eventContextMenu`                | イベントの右クリックコンテキストメニューのカスタム実装。                                                              | `{ event, onClose }`                                                      |\n| `eventDetailContent`              | イベントがクリックされたときにポップオーバー/パネルに表示される内容。                                                 | `{ event, isAllDay, onEventDelete, onEventUpdate, onClose, app }`         |\n| `eventDetailDialog`               | イベントがクリックされたときに表示されるカスタムダイアログ。`useCalendarApp` で `useEventDetailDialog: true` が必要。 | `{ event, isOpen, isAllDay, onEventDelete, onEventUpdate, onClose, app }` |\n| `gridContextMenu`                 | カレンダーのセル / グリッドのカスタム右クリックコンテキストメニュー。                                                 | `{ date, viewType, onClose }`                                             |\n| `mobileEventDetail`               | モバイル用のカスタムイベント詳細ドロワー / ダイアログ。                                                               | `{ isOpen, onClose, onSave, onEventDelete, draftEvent, app, timeFormat }` |\n| `sidebarCalendarColorPicker`      | サイドバーのカレンダー色用のカラーピッカー。                                                                          | `{ color, onChange, onChangeComplete }`                                   |\n| `titleBarSlot`                    | サイドバーのタイトルバー内の追加コンテンツ。                                                                          | `{ isCollapsed, toggleCollapsed }`                                        |\n\n## イベントコンテンツ\n\n`eventContent` スロットを使用すると、カレンダー内でのイベントのレンダリング方法を完全にカスタマイズできます。他のスロットとは異なり、イベントコンテンツはビューごとに指定する**必要があります**。これは、各ビューの特定のイベント構造とレイアウトの整合性を保つためです。\n\n### ビュー固有のスロット\n\n| スロット名                | 説明                             |\n| :------------------------ | :------------------------------- |\n| `eventContentDay`         | 日ビューの定时イベントを上書き。 |\n| `eventContentWeek`        | 週ビューの定时イベントを上書き。 |\n| `eventContentMonth`       | 月ビューのイベントを上書き。     |\n| `eventContentYear`        | 年ビューのイベントを上書き。     |\n| `eventContentAllDayDay`   | 日ビューの終日イベントを上書き。 |\n| `eventContentAllDayWeek`  | 週ビューの終日イベントを上書き。 |\n| `eventContentAllDayMonth` | 月ビューの終日イベントを上書き。 |\n| `eventContentAllDayYear`  | 年ビューの終日イベントを上書き。 |\n\n<EventContentShowcase />\n\n<details>\n<summary>ソースコードを表示</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nexport const EventContentShowcase = () => {\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      // 日ビューのイベントレンダリングをカスタマイズ\n      eventContentDay={({ event, isSelected }) => (\n        <div className='custom-event-card'>\n          <span>{event.title}</span>\n          {/* ... カスタムアイコンやレイアウトを追加 */}\n        </div>\n      )}\n      // 月ビューのイベントレンダリングをカスタマイズ\n      eventContentMonth={({ event }) => (\n        <div className='flex items-center gap-1'>\n          <span>🗓️</span>\n          <span className='truncate'>{event.title}</span>\n        </div>\n      )}\n      // 簡潔にするため、他のビューの上書きは省略...\n    />\n  );\n};\n```\n\n</details>\n\n## イベント詳細コンテンツ\n\n`eventDetailContent` スロットを使用すると、イベントをクリックしたときに表示される詳細ポップオーバーまたはパネルのコンテンツをカスタマイズできます。\n\n<CustomDetailPanelShowcase />\n\n<details>\n<summary>ソースコードを表示</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { useCallback } from 'react';\n\nexport const CustomDetailPanelShowcase = () => {\n  const detailPanel = useCallback(\n    ({ event, onEventDelete, onEventUpdate, onClose }) => {\n      return (\n        <div className='p-4 space-y-3'>\n          <h5 className='font-bold'>{event.title}</h5>\n          <p>{event.description}</p>\n\n          <div className='flex gap-2'>\n            <button\n              onClick={() => onEventUpdate({ ...event, title: '更新済み' })}\n            >\n              更新\n            </button>\n            <button onClick={() => onEventDelete(event.id)}>削除</button>\n          </div>\n        </div>\n      );\n    },\n    []\n  );\n\n  return (\n    <DayFlowCalendar calendar={calendar} eventDetailContent={detailPanel} />\n  );\n};\n```\n\n</details>\n\n## イベント詳細ダイアログ\n\nイベント詳細の表示や編集にモーダル/ダイアログインターフェースを使用したい場合は、`eventDetailDialog` スロットを使用できます。これはモバイルファーストのアプリケーションや、複雑なイベントデータのために広いスペースが必要な場合に特に便利です。\n\n> **前提条件**：ダイアログモードを有効にするには、`useCalendarApp` で `useEventDetailDialog: true` を設定してください。この設定がなければスロットは呼び出されません。\n\n> **代わりに無効化する場合**：独自のダイアログを提供せずに組み込みのフローティングパネルを非表示にしたい場合は、スロットを使用するのではなく、`useCalendarApp` の設定で `useEventDetailPanel: false` を指定してください。\n\n<CustomDetailDialogShowcase />\n\n<details>\n<summary>ソースコードを表示</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { useCallback } from 'react';\n\nexport const CustomDetailDialogShowcase = () => {\n  const customDialog = useCallback(({ event, isOpen, onClose }) => {\n    if (!isOpen) return null;\n\n    return (\n      <div className='fixed inset-0 z-50 flex items-center justify-center bg-black/50'>\n        <div className='bg-white p-6 rounded-lg shadow-xl'>\n          <h2>{event.title}</h2>\n          {/* ... ここでカスタムダイアログ UI を構築 */}\n          <button onClick={onClose}>閉じる</button>\n        </div>\n      </div>\n    );\n  }, []);\n\n  return (\n    <DayFlowCalendar calendar={calendar} eventDetailDialog={customDialog} />\n  );\n};\n```\n\n</details>\n\n## モバイルイベント詳細\n\nモバイルデバイス（または小さな画面）で DayFlow を表示する場合、イベントの作成と編集を処理するために専用の `mobileEventDetail` スロットを使用します。デフォルトでは、これはフルスクリーンのドロワーです。\n\n<MobileEventDetailShowcase />\n\n<details>\n<summary>ソースコードを表示</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react';\nimport { useCallback } from 'react';\n\nexport const MobileEventDetailShowcase = () => {\n  const customMobileDrawer = useCallback(\n    ({ isOpen, onClose, onSave, onEventDelete, draftEvent }) => {\n      if (!isOpen || !draftEvent) return null;\n\n      return (\n        <div className='fixed inset-0 z-50 flex flex-col bg-white'>\n          <header className='flex items-center justify-between p-4 border-b'>\n            <button onClick={onClose}>戻る</button>\n            <h2>{draftEvent.id ? '編集' : '新規'}イベント</h2>\n            <button onClick={() => onSave(draftEvent)}>保存</button>\n          </header>\n          <div className='p-4'>\n            <input\n              defaultValue={draftEvent.title}\n              onChange={e => (draftEvent.title = e.target.value)}\n            />\n            {/* ... モバイルに最適化された UI を構築 */}\n          </div>\n        </div>\n      );\n    },\n    []\n  );\n\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      mobileEventDetail={customMobileDrawer}\n    />\n  );\n};\n```\n\n</details>\n\n## カスタムカラーピッカーの注入\n\nデフォルトでは、DayFlow は組み込みの `BlossomColorPicker` を使用します。`react-color` や `vue-color` などのライブラリを使用したい場合は、`colorPicker` スロットを使用して注入できます。\n\n<DocImg src='/images/docs/colorPicker.png' alt='カラーピッカー' />\n\n### React の例\n\n`react-color` をインストールします:\n\n<PackageTabs pkg='react-color' />\n\n`DayFlowCalendar` に注入します:\n\n<ColorPickerShowcase />\n\n<details>\n<summary>ソースコードを表示</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { SketchPicker, PhotoshopPicker } from 'react-color';\n\nfunction MyCalendar() {\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      colorPicker={args => (\n        <SketchPicker\n          color={args.color}\n          onChange={color => args.onChange({ hex: color.hex })}\n        />\n      )}\n      createCalendarDialogColorPicker={args => (\n        <PhotoshopPicker\n          color={args.color}\n          onChange={color => args.onChange({ hex: color.hex })}\n          onAccept={args.onAccept}\n          onCancel={args.onCancel}\n        />\n      )}\n    />\n  );\n}\n```\n\n</details>\n\n## コンテキストメニュースロット\n\n`eventContextMenu` と `gridContextMenu` スロットを使用すると、デフォルトの右クリックメニューを完全なカスタム React コンポーネントで置き換えられます。どちらのスロットも、メニューを閉じるための `onClose` コールバックを受け取ります。\n\n- **`eventContextMenu`** — ユーザーがイベントを右クリックしたときに発火します。`{ event, onClose }` を受け取ります。\n- **`gridContextMenu`** — ユーザーがカレンダーグリッドの空白領域を右クリックしたときに発火します。`{ date, viewType, onClose }` を受け取ります。\n\n任意のイベントまたは空のセルを右クリックして、カスタムメニューを確認してください：\n\n<ContextMenuShowcase />\n\n<details>\n<summary>ソースコードを表示</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react';\nimport type {\n  EventContextMenuSlotArgs,\n  GridContextMenuSlotArgs,\n} from '@dayflow/core';\nimport { useCallback } from 'react';\n\nfunction MyCalendar() {\n  const eventContextMenu = useCallback(\n    ({ event, onClose }: EventContextMenuSlotArgs) => (\n      <div className='custom-menu'>\n        <button\n          onClick={() => {\n            /* 複製 */ onClose();\n          }}\n        >\n          複製\n        </button>\n        <button\n          onClick={() => {\n            /* 共有 */ onClose();\n          }}\n        >\n          共有\n        </button>\n        <button\n          onClick={() => {\n            calendar.deleteEvent(event.id);\n            onClose();\n          }}\n        >\n          削除\n        </button>\n      </div>\n    ),\n    []\n  );\n\n  const gridContextMenu = useCallback(\n    ({ date, onClose }: GridContextMenuSlotArgs) => (\n      <div className='custom-menu'>\n        <button\n          onClick={() => {\n            /* ここにイベントを作成 */ onClose();\n          }}\n        >\n          新しいイベント\n        </button>\n        <button\n          onClick={() => {\n            /* リマインダーを設定 */ onClose();\n          }}\n        >\n          リマインダー\n        </button>\n      </div>\n    ),\n    []\n  );\n\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      eventContextMenu={eventContextMenu}\n      gridContextMenu={gridContextMenu}\n    />\n  );\n}\n```\n\n</details>\n"
  },
  {
    "path": "website/content/docs-ja/features/dark-mode.mdx",
    "content": "import { DefaultColorPalette } from '@/components/ColorPalette';\n\n# ダークモード\n\nDayFlow にはライト/ダーク/自動の 3 つのテーマモードが標準搭載されており、ビューやサイドバー、イベントカード、ダイアログまで一貫して切り替わります。システム設定に合わせたり、アプリ独自のトグルから制御するのも簡単です。\n\n## 特徴\n\n- **3 モード対応**：`light` / `dark` / `auto`\n- **シームレスな切替**：ちらつきやレイアウト崩れなし\n- **システム連動**：`auto` で OS のテーマと同期\n- **イベントカラー最適化**：背景に応じたコントラスト調整\n- **API 完備**：`getTheme` / `setTheme` / `subscribeThemeChange`\n- **カレンダー単位の色替え**：各 `calendarId` にライト/ダーク用の色を設定可能\n\n## クイックスタート\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    theme: { mode: 'dark' }, // 'light' | 'dark' | 'auto'\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\nモードを切り替えるときは `mode` を変更するだけです。\n\n```tsx\nuseCalendarApp({ theme: { mode: 'light' } }); // ライト\nuseCalendarApp({ theme: { mode: 'dark' } }); // ダーク\nuseCalendarApp({ theme: { mode: 'auto' } }); // OS に追従\n```\n\n## プログラムからの切り替え\n\n```tsx\nconst calendar = useCalendarApp({ theme: { mode: 'light' } });\n\nconst toggleTheme = () => {\n  const current = calendar.app.getTheme();\n  calendar.app.setTheme(current === 'light' ? 'dark' : 'light');\n};\n```\n\n購読 API を使えば、アプリ全体でテーマを共有できます。\n\n```tsx\nuseEffect(() => {\n  const handler = (mode: ThemeMode) => setTheme(mode);\n  calendar.app.subscribeThemeChange(handler);\n  return () => calendar.app.unsubscribeThemeChange(handler);\n}, [calendar.app]);\n```\n\n## カレンダータイプごとの色を調整\n\n```tsx\nconst calendars = [\n  {\n    id: 'work',\n    name: '仕事',\n    colors: {\n      lineColor: '#0066cc',\n      eventColor: '#e6f2ff',\n      eventSelectedColor: '#cce4ff',\n      textColor: '#003d7a',\n    },\n    darkColors: {\n      lineColor: '#4da6ff',\n      eventColor: '#1a3d5c',\n      eventSelectedColor: '#2a5a8a',\n      textColor: '#b3d9ff',\n    },\n  },\n];\n\nconst calendar = useCalendarApp({\n  theme: { mode: 'auto' },\n  calendars,\n});\n```\n\n`darkColors` を省略するとライトモードの色がそのまま使われます。暗い背景でも読みやすいよう、必ずトーンを調整しましょう。\n\n## デフォルト色セット\n\n<DefaultColorPalette />\n\n## API リファレンス\n\n```ts\ninterface ThemeConfig {\n  mode: 'light' | 'dark' | 'auto';\n}\n\napp.getTheme(): ThemeMode;\napp.setTheme(mode: ThemeMode): void;\napp.subscribeThemeChange((mode: ThemeMode) => void): void;\napp.unsubscribeThemeChange((mode: ThemeMode) => void): void;\n```\n\n## Tailwind / CSS 変数との併用\n\n- Tailwind で `darkMode: 'class'` にしている場合、ページのルートに付与している `dark` クラスと DayFlow の `theme.mode` を同期させると整合性が取れます。\n- `--dayflow-bg-primary` などの CSS カスタムプロパティを上書きすれば、より細かいトークンレベルでブランドカラーを適用できます（[テーマカスタマイズガイド](/docs-ja/guides/theme-customization) 参照）。\n\n## テストのヒント\n\n1. ライト/ダークの両方でイベント文字のコントラストが十分か確認する（4.5:1 以上が目安）。\n2. ドラッグ&ドロップ、ポップアップ、サイドバーなど動的 UI がモード切り替え時に崩れないかテストする。\n3. `auto` モードを使う場合は、OS のテーマ変更イベントを実機で試し、即時反映されるかチェックする。\n\nこの仕組みを使えば、DayFlow を既存アプリのテーマ戦略にスムーズに統合できます。\n\n## カラー推奨事項\n\n読みやすさとアクセシビリティを最適化するために：\n\n**ライトモード：**\n\n- 線の色：鮮やかで彩度の高い色 (#0066cc、#16a34a)\n- イベント色：明るいティント (#e6f2ff、#dcfce7)\n- テキスト色：コントラストのための暗い色調 (#003d7a、#14532d)\n\n**ダークモード：**\n\n- 線の色：より明るく発光感のある色 (#4da6ff、#4ade80)\n- イベント色：暗く彩度を落とした背景 (#1a3d5c、#1e4d2b)\n- テキスト色：読みやすい明るい色調 (#b3d9ff、#bbf7d0)\n\n## 使用例\n\n### シンプルなテーマ切り替え\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { Sun, Moon } from 'lucide-react';\n\nfunction SimpleThemeToggle() {\n  const calendar = useCalendarApp({\n    theme: { mode: 'light' },\n  });\n\n  const [isDark, setIsDark] = useState(false);\n\n  const toggleTheme = () => {\n    const nextTheme = isDark ? 'light' : 'dark';\n    calendar.app.setTheme(nextTheme);\n    setIsDark(!isDark);\n  };\n\n  return (\n    <div>\n      <button onClick={toggleTheme}>{isDark ? <Sun /> : <Moon />}</button>\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n}\n```\n\n## 関連ドキュメント\n\n- [useCalendarApp](/docs-ja/introduction/use-calendar-app) - コアカレンダー設定\n- [カレンダータイプ](/docs-ja/introduction/events#calendar-types) - イベントのカテゴリと色\n- [テーマカスタマイズガイド](/docs-ja/guides/theme-customization) - 高度なテーマ設定\n"
  },
  {
    "path": "website/content/docs-ja/features/event-dialog.mdx",
    "content": "import { EventDialogShowcase } from '@/components/showcase/FeatureShowcase';\n\n# イベントダイアログ\n\nDayFlow には、イベントの作成・編集・削除をまとめて行えるダイアログが標準で組み込まれています。モーダルはポータル経由で描画されるため、他の UI と干渉せずに使えます。\n\n<EventDialogShowcase />\n\n## 使い方\n\nDayFlow のデフォルト動作では、イベントをクリックすると組み込みのフローティング詳細パネルが開きます。組み込みモーダルダイアログを使いたい場合は、`useCalendarApp` で `useEventDetailDialog` を有効にしてから `DayFlowCalendar` を描画します。\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  views: [...],\n  useEventDetailDialog: true,\n});\n\n<DayFlowCalendar calendar={calendar} />;\n```\n\n## カスタマイズ\n\n### ダイアログ UI を置き換える\n\n`eventDetailDialog` スロットを使用すると、DayFlow の開閉状態管理はそのままに、独自のダイアログコンポーネントに差し替えられます。この機能を使用するには、`useCalendarApp` の設定で `useEventDetailDialog: true` を設定する必要があります。\n\n```tsx\n// 1. カレンダー設定でダイアログモードを有効にする\nconst calendar = useCalendarApp({\n  views: [...],\n  useEventDetailDialog: true,\n});\n\n// 2. スロット経由で独自のダイアログを提供する\n<DayFlowCalendar\n  calendar={calendar}\n  eventDetailDialog={({ event, isOpen, onClose }) => (\n    <MyDialog isOpen={isOpen} onClose={onClose}>\n      <EventForm event={event} />\n    </MyDialog>\n  )}\n/>\n```\n\n### 組み込みパネルを無効にする\n\nアプリケーション側でコールバックやグローバルな状態からイベント詳細を管理している場合は、`useCalendarApp` の設定で `useEventDetailPanel: false` を設定することで、組み込みのフローティングパネルを完全に非表示にできます。\n\n```tsx\nconst calendar = useCalendarApp({\n  // ...\n  useEventDetailPanel: false,\n});\n\n<DayFlowCalendar calendar={calendar} />;\n```\n\nReact、Vue、Svelte、Angular の各アダプターで利用できます。\n"
  },
  {
    "path": "website/content/docs-ja/features/meta.json",
    "content": "{\n  \"title\": \"機能\",\n  \"pages\": [\n    \"calendar-header\",\n    \"content-slots\",\n    \"dark-mode\",\n    \"event-dialog\",\n    \"multi-calendar-event\",\n    \"read-only\",\n    \"switcher-mode\"\n  ]\n}\n"
  },
  {
    "path": "website/content/docs-ja/features/multi-calendar-event.mdx",
    "content": "import { MultiCalendarEventShowcase } from '@/components/showcase/MultiCalendarEventShowcase';\n\n# マルチカレンダーイベント\n\nDayFlow はマルチカレンダーイベントをサポートしており、1つのイベントを複数のカレンダーに関連付けることができます。これは、チームをまたぐ会議や共有の家族イベント、またはイベントが複数のカテゴリに属するあらゆる状況で特に便利です。\n\n## 設定\n\nマルチカレンダーイベントを作成するには、イベントオブジェクトの `calendarIds` プロパティにカレンダー ID の配列を指定します。\n\n```tsx\nconst events = [\n  {\n    id: 'multi-cal-1',\n    title: 'チーム横断プランニング',\n    start: '2026-04-20T10:00:00',\n    end: '2026-04-20T11:30:00',\n    // プライバリのカレンダー ID（必要に応じてデフォルトのスタイリングに使用されます）\n    calendarId: 'team-a',\n    // イベントを複数のカレンダーに関連付けます\n    calendarIds: ['team-a', 'team-b', 'marketing'],\n  },\n];\n```\n\nイベントに複数の `calendarIds` がある場合、DayFlow は関連付けられた各カレンダーに割り当てられた色を使用して、特徴的な斜めストライプの背景パターンでレンダリングします。\n\n## 展示\n\n以下の展示は、マルチカレンダーイベントがカレンダー内でどのようにレンダリングされるかを示しています。\n\n<MultiCalendarEventShowcase />\n\n<Callout title='推奨事項'>\n  デフォルトでは、組み込みの `eventDetailPanel`/`Dialog`\n  は複数カレンダーの選択をサポートしていません。この要件はあまり一般的ではないため、代わりに\n  `eventDetailContent` または `eventDetailDialog`\n  の実装をカスタマイズすることをお勧めします。詳細はドキュメント：[コンテンツスロット](./content-slots)\n  を参照してください。\n</Callout>\n"
  },
  {
    "path": "website/content/docs-ja/features/read-only.mdx",
    "content": "# 読み取り専用モード\n\nカレンダーを簡単に読み取り専用にして、ユーザーによる変更を防ぐことができます。これは、公開カレンダーや表示専用のダッシュボードに役立ちます。\n\n読み取り専用モードは DayFlow 組み込みの変更 UI だけを無効にします。`calendar.addEvent()`、`calendar.updateEvent()`、`calendar.deleteEvent()`、`calendar.applyEventsChanges()` のようなプログラムからの API は引き続き利用できます。\n\n## 基本的な使い方\n\n読み取り専用モードを有効にするには、カレンダーの設定に `readOnly: true` を渡します。\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  readOnly: true, // 組み込みの変更 UI（ドラッグ、作成、編集）を無効にします\n});\n```\n\n## きめ細かな制御\n\n`ReadOnlyConfig` オブジェクトを提供して、特定の機能を個別に無効にすることもできます：\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  readOnly: {\n    draggable: false, // ドラッグ＆ドロップを無効にする\n    viewable: true, // クリックしてイベント詳細を表示することを許可する（読み取り専用モード）\n  },\n});\n```\n\n### 設定オプション\n\n| オプション  | 型        | 説明                                                              |\n| :---------- | :-------- | :---------------------------------------------------------------- |\n| `draggable` | `boolean` | ドラッグ＆ドロップでイベントを移動またはリサイズできるかどうか。  |\n| `viewable`  | `boolean` | イベント詳細を表示できるかどうか（詳細パネル/ダイアログを開く）。 |\n\n`readOnly` が有効な場合、カレンダーは「作成」ボタンや、通常変更をトリガーするその他の UI 要素も非表示にします。\n\n## カスタム UI\n\n独自のボタン、メニュー、ダイアログを描画する場合は、`calendar.canMutateFromUI()` を使って変更系 UI を表示してよいか判定してください。\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  readOnly: true,\n});\n\nconst canEdit = calendar.canMutateFromUI();\n```\n\n- `true`: 独自 UI で作成、編集、削除の操作を表示できます。\n- `false`: 独自 UI では変更系の操作を隠すか無効化してください。\n"
  },
  {
    "path": "website/content/docs-ja/features/switcher-mode.mdx",
    "content": "import { SwitcherModeShowcase } from '@/components/showcase/FeatureShowcase';\n\n# ビュースイッチャーモード\n\nヘッダーに表示されるビュー切り替え UI は `switcherMode` で制御できます。DayFlow には以下の 2 パターンが用意されています。\n\n- `buttons`：デスクトップに適したボタングループ。初期値はこちら。\n- `select`：ドロップダウン型で、省スペースかつモバイル向け。\n\n同じ設定で見た目の違いを比較したい場合は下のデモを触ってみてください。\n\n<SwitcherModeShowcase />\n\n## 使い方\n\n```tsx {9}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n} from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  switcherMode: 'select', // 初期値 switcherMode: 'buttons'\n});\n\n<DayFlowCalendar calendar={calendar} />;\n```\n\n`switcherMode` を `'buttons'` ↔ `'select'` に切り替えるだけで、即座に別の UI を試せます。\n\n## 選択の目安\n\n- **ボタンモード**：3〜4 個のビューを並べて常に表示したいとき。ショートカットキーとも相性が良い。\n- **セレクトモード**：ヘッダースペースが限られている、またはモバイル UI を重視したいとき。\n\nカスタムヘッダーを作りたい場合は独自コンポーネントから `calendar.changeView()` を呼び出すだけで同じ体験を構築できます。\n"
  },
  {
    "path": "website/content/docs-ja/guides/global-css.mdx",
    "content": "# グローバル CSS クラス\n\nDayFlow は、カレンダーコンポーネントの外観をカスタマイズできる `df-` プレフィックス付きの CSS クラスセットを提供しています。これらのクラスは、コアライブラリを変更せずに簡単にスタイルをカスタマイズできるように設計されています。\n\n## 適切な CSS エントリの選び方\n\nTailwind CSS をすでに使っているかどうかで、読み込むスタイルシートを選んでください。\n\n| エントリ                | 含まれる内容                                  | 使う場面                         |\n| ----------------------- | --------------------------------------------- | -------------------------------- |\n| `styles.css`            | Tailwind preflight / reset を含む完全バンドル | Tailwind を使っていない場合      |\n| `styles.components.css` | コンポーネントスタイルのみ、CSS reset なし    | すでに Tailwind を使っている場合 |\n\n```css\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\nテーマ上書きでは、`--df-color-*` 変数または以下に記載した安定した `df-*` セマンティッククラスを優先してください。\n\n## 目次\n\n- [共通クラス](#共通)\n- [セマンティック補助クラス](#セマンティック補助クラス)\n- [レイアウトとモーションの補助クラス](#レイアウトとモーションの補助クラス)\n- [日ビュークラス](#日ビュー)\n- [週ビュークラス](#週ビュー)\n- [月ビュークラス](#月ビュー)\n- [年ビュークラス](#年ビュー)\n- [ミニカレンダークラス](#ミニカレンダー)\n- [サイドバークラス](#サイドバー)\n- [ナビゲーションクラス](#ナビゲーション)\n- [イベント詳細クラス](#イベント詳細)\n- [コンテンツスロットクラス](#content-slots)\n- [イベント状態属性](#イベント状態属性)\n\n---\n\n## 共通\n\nすべてのカレンダービューで使用される共通 CSS クラス。\n\n| クラス名                   | 説明                                                                                      |\n| -------------------------- | ----------------------------------------------------------------------------------------- |\n| `df-calendar-container`    | サイドバーとカレンダーを包む最外層のルートコンテナ。`--df-calendar-height` に対応します。 |\n| `df-calendar`              | カレンダーメインコンテナ（ヘッダーとビューコンテンツを含む）                              |\n| `df-header`                | カレンダーヘッダーセクション（タイトル、今日ボタン、表示切り替えを含む）                  |\n| `df-header-left`           | ヘッダーの左側セクション（タイトルとナビゲーションボタン）                                |\n| `df-header-mid`            | ヘッダーの中央セクション（現在の表示期間タイトル）                                        |\n| `df-header-right`          | ヘッダーの右側セクション（表示切り替えと検索）                                            |\n| `df-navigation`            | ナビゲーションコントロールコンテナ                                                        |\n| `df-view-header-container` | セカンダリなビューヘッダー領域で使われる共有コンテナ                                      |\n| `df-view-header-title`     | ビュー単位のタイトルに使う共有テキストクラス                                              |\n| `df-view-header-subtitle`  | ビュー単位のサブタイトルに使う共有テキストクラス                                          |\n| `df-event`                 | すべてのイベントの基本クラス                                                              |\n| `df-event-title`           | イベントタイトルテキスト                                                                  |\n| `df-event-time`            | イベント時間テキスト                                                                      |\n| `df-event-color-bar`       | イベント左側のカラーバー（日/週ビュー）                                                   |\n| `df-month-event-color-bar` | 通常イベントのカラーインジケーター（月ビュー）                                            |\n| `df-all-day-row`           | 終日イベント行コンテナ                                                                    |\n| `df-all-day-label`         | 終日ラベルテキスト                                                                        |\n| `df-all-day-content`       | 終日イベントコンテンツエリア                                                              |\n| `df-all-day-cell`          | 個別の終日イベントセル                                                                    |\n| `df-date-number`           | 日付番号の表示                                                                            |\n| `df-current-time-line`     | 現在時刻インジケーターラインコンテナ                                                      |\n| `df-current-time-label`    | 現在時刻ラベルテキスト                                                                    |\n| `df-current-time-bar`      | 現在時刻の水平線                                                                          |\n| `df-calendar-checkbox`     | カレンダー一覧や関連コントロールで再利用される共有チェックボックススタイル                |\n| `df-portal`                | Portal で描画されるフローティング UI のルートスコープクラス                               |\n| `df-range-picker`          | Range Picker のトリガーとポップアップに付くルートスコープクラス                           |\n\n---\n\n## セマンティック補助クラス\n\n今回のスタイル再構成により、DayFlow はボタン状態や選択状態、破壊的アクション、フォーカスリングなどを表す安定した `df-*` セマンティッククラスを公開しています。これらは共有基盤 CSS で定義されており、カスタム slot やプラグイン UI から安全に再利用できます。\n\n| クラス名                  | 用途                                          |\n| ------------------------- | --------------------------------------------- |\n| `df-fill-primary`         | プライマリ塗りつぶし背景 + 自動前景文字色     |\n| `df-fill-secondary`       | セカンダリ塗りつぶし背景 + 自動前景文字色     |\n| `df-fill-destructive`     | 破壊的アクション背景 + 自動前景文字色         |\n| `df-tint-primary`         | プライマリ 10% ティント                       |\n| `df-tint-primary-md`      | プライマリ 20% ティント                       |\n| `df-tint-primary-lg`      | プライマリ 30% ティント                       |\n| `df-hover-primary`        | プライマリ系ホバー状態                        |\n| `df-hover-primary-md`     | より強めのプライマリ系ホバー状態              |\n| `df-hover-primary-solid`  | プライマリ塗りつぶしボタンのホバー状態        |\n| `df-hover-base`           | DayFlow の hover トークンを使う標準ホバー背景 |\n| `df-hover-muted`          | muted 系ホバー背景                            |\n| `df-hover-destructive`    | 破壊的アクションのホバー状態                  |\n| `df-text-primary`         | プライマリ文字色                              |\n| `df-text-muted`           | 弱めのテキスト色                              |\n| `df-text-primary-fg`      | プライマリ背景上の文字色                      |\n| `df-text-secondary-fg`    | セカンダリ背景上の文字色                      |\n| `df-text-destructive`     | 破壊的アクション文字色                        |\n| `df-text-destructive-fg`  | 破壊的背景上の文字色                          |\n| `df-bg-base`              | ベースサーフェス背景                          |\n| `df-bg-card`              | カード背景                                    |\n| `df-bg-sidebar`           | サイドバー向けの muted 背景                   |\n| `df-bg-secondary`         | セカンダリ muted 背景                         |\n| `df-bg-tertiary`          | 第3レベル背景 / 区切り面                      |\n| `df-border-base`          | ベース境界線                                  |\n| `df-border-light`         | より軽い境界線                                |\n| `df-border-strong`        | プライマリ token を使う強い境界線             |\n| `df-border-primary`       | プライマリ境界線色                            |\n| `df-border-primary-soft`  | 柔らかいプライマリ境界線色                    |\n| `df-ring-primary`         | プライマリ ring トークン                      |\n| `df-ring-primary-solid`   | ソリッドなプライマリ ring トークン            |\n| `df-shadow-sm`            | 小さめの影                                    |\n| `df-shadow-md`            | 中程度の影                                    |\n| `df-shadow-primary`       | プライマリ shadow トークン                    |\n| `df-focus-ring`           | `:focus` 時にプライマリ border と ring を適用 |\n| `df-focus-ring-only`      | `:focus` 時に ring のみ適用                   |\n| `df-focus-border-primary` | `:focus` 時に border のみプライマリ化         |\n\nテーマ調整では、これらの `df-*` クラスまたは `--df-color-*` 変数を優先してください。`bg-primary`、`text-primary`、`hover:bg-primary/90` のような旧来の内部 Tailwind セマンティック名に依存しないでください。\n\n---\n\n## レイアウトとモーションの補助クラス\n\nこれらのクラスは、カスタム slot コンテンツやプラグイン UI で DayFlow のレイアウトや入場アニメーションを再利用したいときに便利です。\n\n| クラス名                  | 用途                                                 |\n| ------------------------- | ---------------------------------------------------- |\n| `df-content-slot-stacked` | コンテンツスロットを縦積みし、利用可能な高さを埋める |\n| `df-scrollbar-hide`       | スクロール動作を維持したままスクロールバーを隠す     |\n| `df-animate-in`           | 入場アニメーションの基本タイミングヘルパー           |\n| `df-fade-in`              | フェードイン補助クラス                               |\n| `df-zoom-in-95`           | わずかなズームイン補助クラス                         |\n| `df-animate-slide-up`     | 上方向スライドの入場アニメーション                   |\n| `df-animate-slide-down`   | 下方向スライドの退場アニメーション                   |\n\n---\n\n## 日ビュー\n\n日ビュー専用の CSS クラス。\n\n| クラス名            | 説明                                                  |\n| ------------------- | ----------------------------------------------------- |\n| `df-day-view`       | 日ビューコンテナ                                      |\n| `df-day-event`      | 日ビューの個別イベント                                |\n| `df-day-time-grid`  | タイムグリッドコンテナ                                |\n| `df-time-column`    | 時間列（時間を表示する左サイドバー）                  |\n| `df-time-slot`      | 個別のタイムスロットコンテナ（左列）                  |\n| `df-time-label`     | 時間ラベルテキスト（例：「13:00」）                   |\n| `df-time-grid-row`  | タイムグリッドの水平行                                |\n| `df-time-grid-cell` | タイムグリッドの個別セル                              |\n| `df-right-panel`    | 日ビューの右パネル（ミニカレンダー + イベントリスト） |\n\n---\n\n## 週ビュー\n\n週ビュー専用の CSS クラス。\n\n| クラス名             | 説明                                 |\n| -------------------- | ------------------------------------ |\n| `df-week-view`       | 週ビューコンテナ                     |\n| `df-week-event`      | 週ビューの個別イベント               |\n| `df-week-header`     | 曜日名付きの週ヘッダーコンテナ       |\n| `df-week-header-row` | 曜日名を含む固定ヘッダー行           |\n| `df-week-day-cell`   | ヘッダーの個別曜日セル               |\n| `df-time-column`     | 時間列（時間を表示する左サイドバー） |\n| `df-time-slot`       | 個別のタイムスロットコンテナ（左列） |\n| `df-time-label`      | 時間ラベルテキスト                   |\n| `df-time-grid-row`   | タイムグリッドの水平行               |\n| `df-time-grid-cell`  | タイムグリッドの個別セル             |\n\n---\n\n## 月ビュー\n\n月ビュー専用の CSS クラス。\n\n| クラス名                         | 説明                                    |\n| -------------------------------- | --------------------------------------- |\n| `df-month-view`                  | 月ビューコンテナ                        |\n| `df-month-grid`                  | 月ビューのメイングリッドコンテナ        |\n| `df-month-day-cell`              | 月ビューの個別日付セル                  |\n| `df-month-date-number-container` | 日付番号エリアのコンテナ                |\n| `df-month-date-number`           | 月ビューの日付番号                      |\n| `df-month-more-events`           | 非表示イベントの「+ x件」インジケーター |\n| `df-month-title`                 | スクロール時のフローティング月タイトル  |\n\n---\n\n## 年ビュー\n\n年ビュー専用の CSS クラス。これらは年間概要でのイベント外観のカスタマイズに特に便利です。\n\n| クラス名                  | 説明                                                                |\n| ------------------------- | ------------------------------------------------------------------- |\n| `df-year-event`           | イベント外側コンテナ - イベント要素全体をラップ                     |\n| `df-event-year-content`   | 年ビューイベントのコンテンツコンテナ - アイコン、タイトルなどを含む |\n| `df-event-year-title`     | 年ビューイベントのタイトルテキスト - 配置やフォントの調整に使用     |\n| `df-event-icon-svg`       | イベント描画で共通利用される SVG アイコンクラス。年ビューでも使用   |\n| `df-event-year-indicator` | 年ビューの時間指定イベント用カラーインジケーター                    |\n| `df-year-grid-month`      | Grid Year View の月カードコンテナ                                   |\n| `df-year-fixed-day-cell`  | Fixed Week Year View の日セル                                       |\n\n---\n\n## ミニカレンダー\n\nミニカレンダーコンポーネント（通常サイドバー或者日ビューに配置）用の CSS クラス。\n\n| クラス名                       | 説明                                   |\n| ------------------------------ | -------------------------------------- |\n| `df-mini-calendar`             | ミニカレンダーコンテナ                 |\n| `df-mini-calendar-body`        | ミニカレンダー本体ラッパー             |\n| `df-mini-calendar-header-nav`  | 月ナビゲーション行                     |\n| `df-mini-calendar-nav-btn`     | 前月 / 次月ボタン                      |\n| `df-mini-calendar-month-label` | 現在月ラベル                           |\n| `df-mini-calendar-grid`        | 日付グリッドコンテナ                   |\n| `df-mini-calendar-header`      | 曜日ヘッダーセル（例：「月」、「火」） |\n| `df-mini-calendar-day`         | ミニカレンダー日付ボタンのベースクラス |\n| `df-mini-calendar-day-cell`    | 日付セルの見た目を担うサーフェス       |\n| `df-mini-calendar-day-number`  | 日付数字ラベル                         |\n| `df-mini-calendar-dots`        | 日セル内のイベントドットコンテナ       |\n| `df-mini-calendar-dot`         | 個別のイベントドット                   |\n\n---\n\n## サイドバー\n\nサイドバーコンポーネント用の CSS クラス。\n\n| クラス名                  | 説明                                         |\n| ------------------------- | -------------------------------------------- |\n| `df-sidebar`              | サイドバーコンテナ                           |\n| `df-sidebar-header`       | サイドバーヘッダーコンテナ                   |\n| `df-sidebar-toggle`       | サイドバー折りたたみ/展開切り替えボタン      |\n| `df-sidebar-header-title` | サイドバーヘッダータイトル（「カレンダー」） |\n| `df-sidebar-list-shell`   | サイドバーリストのスクロールラッパー         |\n| `df-sidebar-list`         | カレンダーリストコンテナ                     |\n| `df-sidebar-list-item`    | リスト内の個別カレンダー項目                 |\n| `df-sidebar-row`          | 各項目内のインタラクティブな行               |\n| `df-sidebar-chip`         | 小型ラベル / chip                            |\n| `df-sidebar-dialog`       | サイドバー用ダイアログコンテナ               |\n| `df-sidebar-button`       | サイドバー共通ボタン                         |\n\n---\n\n## ナビゲーション\n\nナビゲーションボタン用の CSS クラス。\n\n| クラス名                   | 説明                                                |\n| -------------------------- | --------------------------------------------------- |\n| `df-nav-button`            | ナビゲーション矢印ボタンのベースクラス（前へ/次へ） |\n| `df-calendar-nav-button`   | カレンダーナビゲーション矢印ボタンのスタイルクラス  |\n| `df-today-button`          | 「今日」ボタンのベースクラス                        |\n| `df-calendar-today-button` | カレンダー用「今日」ボタンのスタイルクラス          |\n\n---\n\n## イベント詳細\n\nイベント詳細パネルとダイアログ用の CSS クラス。\n\n| クラス名                   | 説明                                       |\n| -------------------------- | ------------------------------------------ |\n| `df-event-detail-panel`    | フローティングイベント詳細パネル           |\n| `df-event-dialog-overlay`  | イベント詳細ダイアログのオーバーレイルート |\n| `df-event-dialog-backdrop` | イベント詳細ダイアログの背景               |\n| `df-dialog-container`      | ダイアログコンテンツコンテナ               |\n| `df-event-dialog-close`    | イベント詳細ダイアログの閉じるボタン       |\n| `df-portal`                | フローティング UI の共通ルート             |\n\n---\n\n## コンテンツスロット\n\nコンテンツスロットレンダリングシステムに使用される CSS クラス。\n\n| クラス名          | 説明                                                                              |\n| ----------------- | --------------------------------------------------------------------------------- |\n| `df-content-slot` | コンテンツスロットのコンテナ                                                      |\n| `df-slot-[id]`    | 特定のスロットインスタンスに割り当てられる動的クラス。`[id]` は一意の識別子です。 |\n\n---\n\n## イベント状態属性\n\nDayFlow はイベント状態だけでなく、グリッド、オーバーレイ、ドロワー、内部レイアウト補助にも `data-*` 属性を使っています。以下は現在のプロジェクトで使われている属性の一覧です。多くは CSS から直接ターゲットでき、一部はオーバーレイ制御や click-outside 判定用の構造フックです。\n\n### イベント要素\n\n| 属性                   | 値                                             | 説明                                                                |\n| ---------------------- | ---------------------------------------------- | ------------------------------------------------------------------- |\n| `data-view`            | `\"day\"` `\"week\"` `\"month\"` `\"year\"` `\"agenda\"` | そのイベントや断片を描画しているビュー                              |\n| `data-all-day`         | `\"true\"` `\"false\"`                             | 終日イベントかどうか                                                |\n| `data-selected`        | `\"true\"` `\"false\"`                             | 現在選択中かどうか                                                  |\n| `data-dragging`        | `\"true\"` `\"false\"`                             | 現在ドラッグ中かどうか                                              |\n| `data-resizing`        | `\"true\"` `\"false\"`                             | 現在リサイズ中かどうか                                              |\n| `data-popping`         | `\"true\"` `\"false\"`                             | インタラクション中に視覚的に浮き上がった状態かどうか                |\n| `data-editable`        | `\"true\"` `\"false\"`                             | ビルトイン UI から編集可能かどうか                                  |\n| `data-viewable`        | `\"true\"` `\"false\"`                             | 詳細 UI を開けるかどうか                                            |\n| `data-draggable`       | `\"true\"` `\"false\"`                             | そのイベントでドラッグが有効かどうか                                |\n| `data-multi-day`       | `\"true\"` `\"false\"`                             | 複数日にまたがるかどうか                                            |\n| `data-month-stack`     | `\"true\"` `\"false\"`                             | 月ビューのスタック表示内で描画されているか                          |\n| `data-touch-handles`   | `\"true\"` `\"false\"`                             | タッチ用リサイズハンドルを表示 / 確保すべきか                       |\n| `data-axis`            | ビュー依存のキーワード                         | コンパクト / timed イベント描画の向きヒント                         |\n| `data-density`         | ビュー依存のキーワード                         | コンパクトイベント内容の密度ヒント                                  |\n| `data-position`        | ビュー依存のキーワード                         | セグメントやコンパクトイベントの位置ヒント                          |\n| `data-segment-shape`   | `\"full\"` `\"start\"` `\"middle\"` `\"end\"`          | 複数セグメントイベントの角丸の形状                                  |\n| `data-segment-days`    | 数値文字列                                     | 月ビューのイベントセグメントが表す日数                              |\n| `data-event-id`        | イベント ID 文字列                             | インタラクティブなイベント / パネルアンカーに付く生のイベント識別子 |\n| `data-continued-left`  | `\"true\"` `\"false\"`                             | Agenda の終日バッジが前日から続いているか                           |\n| `data-continued-right` | `\"true\"` `\"false\"`                             | Agenda の終日バッジが翌日へ続くか                                   |\n\n### 日付・グリッド・レイアウト状態\n\n| 属性                     | 値                     | 説明                                                         |\n| ------------------------ | ---------------------- | ------------------------------------------------------------ |\n| `data-today`             | `\"true\"` `\"false\"`     | 今日に対応する日付 / セクション / ヘッダーセル               |\n| `data-other-month`       | `\"true\"` `\"false\"`     | 前月または翌月に属する日付                                   |\n| `data-current-month`     | `\"true\"` `\"false\"`     | 現在の月レンジ内の日付                                       |\n| `data-date`              | 日付文字列             | グリッド / 年ビューセルに付く具体的な日付ペイロード          |\n| `data-first-day`         | `\"true\"` `\"false\"`     | グループ化された年 / 月構造内の最初の可視日                  |\n| `data-weekend`           | `\"true\"` `\"false\"`     | 週末セル / ラベルかどうか                                    |\n| `data-has-events`        | `\"true\"` `\"false\"`     | 1 件以上のイベントを含むセル                                 |\n| `data-heat-level`        | 数値文字列             | グリッド年ビューのヒートマップ強度レベル                     |\n| `data-visible`           | `\"true\"` `\"false\"`     | 仮想化や条件付きレイアウトで使う表示状態フラグ               |\n| `data-layout`            | レイアウトキーワード   | 月ビュー / 仮想スクロール描画で使う内部レイアウトモード      |\n| `data-layout-ready`      | `\"true\"` `\"false\"`     | 仮想化レイアウトが初回計測を完了したか                       |\n| `data-trailing-border`   | `\"true\"` `\"false\"`     | 月セルの末尾ボーダーを描画するか                             |\n| `data-scrollbar-space`   | `\"true\"` `\"false\"`     | スクロールバー分の余白を予約するか                           |\n| `data-secondary-tz`      | `\"true\"` `\"false\"`     | 補助タイムゾーン軸が有効か                                   |\n| `data-show-all-day`      | `\"true\"` `\"false\"`     | 終日行が有効か                                               |\n| `data-sliding-view`      | `\"true\"` `\"false\"`     | ビューがスライド / 遷移状態にあるか                          |\n| `data-mobile`            | `\"true\"` `\"false\"`     | モバイル向けコンパクト描画ヒント                             |\n| `data-switcher-mode`     | `\"buttons\"` `\"select\"` | ビルトインビュー切り替え UI のモード                         |\n| `data-inside-pill`       | `\"true\"` `\"false\"`     | Compact header の内容が today pill 内に入っているか          |\n| `data-sidebar-collapsed` | `\"true\"` `\"false\"`     | サイドバーが折りたたまれているか                             |\n| `data-sidebar-enabled`   | `\"true\"` `\"false\"`     | カレンダー shell が sidebar プラグイン付きで描画されているか |\n| `data-df-theme`          | テーマキーワード       | テーマ付きレイアウトラッパーが出すテーママーカー             |\n\n### コントロール・入力・検索\n\n| 属性                     | 値                                 | 説明                                                |\n| ------------------------ | ---------------------------------- | --------------------------------------------------- |\n| `data-active`            | `\"true\"` `\"false\"`                 | スイッチ、タブ、ピッカーなどの汎用 active 状態      |\n| `data-open`              | `\"true\"` `\"false\"`                 | ドロップダウン、ドロワー、検索 UI が開いているか    |\n| `data-bordered`          | `\"true\"` `\"false\"`                 | ヘッダー下ボーダーを描画するか                      |\n| `data-loading`           | `\"true\"` `\"false\"`                 | 非同期ボタン / アクションのローディング状態         |\n| `data-ready`             | `\"true\"` `\"false\"`                 | quick-create オーバーレイの初期配置が完了したか     |\n| `data-placement`         | 配置キーワード                     | ポップアップ / quick-create の位置ヒント            |\n| `data-checked`           | `\"true\"` `\"false\"`                 | カスタムスイッチの checked 状態                     |\n| `data-disabled`          | `\"true\"` `\"false\"`                 | カスタムスイッチ / 入力の disabled 状態             |\n| `data-kind`              | コンポーネント依存のキーワード     | mobile event drawer 内のセクション / フィールド種別 |\n| `data-expanded`          | `\"true\"` `\"false\"`                 | ドロワーセクションが展開されているか                |\n| `data-closing`           | `\"true\"` `\"false\"`                 | mobile drawer の closing transition 中か            |\n| `data-tone`              | `\"default\"` `\"today\"` `\"upcoming\"` | グループ化された検索結果日付ヘッダーのトーン        |\n| `data-mini-calendar-dot` | 存在属性                           | mini calendar のイベントドットを示すマーカー        |\n\n### Sidebar プラグイン状態\n\n| 属性                   | 値                 | 説明                                                           |\n| ---------------------- | ------------------ | -------------------------------------------------------------- |\n| `data-active`          | `\"true\"` `\"false\"` | サイドバーのカレンダー行が active 状態か                       |\n| `data-collapsed`       | `\"true\"` `\"false\"` | サイドバーの source group やパネルが折りたたまれているか       |\n| `data-draggable`       | `\"true\"` `\"false\"` | サイドバー内のカレンダー項目をドラッグできるか                 |\n| `data-dragging`        | `\"true\"` `\"false\"` | サイドバー内のカレンダー項目が現在ドラッグ中か                 |\n| `data-position`        | `\"top\"` `\"bottom\"` | サイドバー内でカレンダーを並べ替える際の drop indicator の位置 |\n| `data-selected`        | `\"true\"` `\"false\"` | サイドバーのドロップダウン / import ダイアログ内の選択状態     |\n| `data-submenu-content` | 存在属性           | サイドバーのサブメニュー portal 内容を示すマーカー             |\n\n### オーバーレイとインタラクションフック\n\n| 属性                            | 値       | 説明                                     |\n| ------------------------------- | -------- | ---------------------------------------- |\n| `data-event-detail-panel`       | 存在属性 | フローティングイベント詳細パネルのルート |\n| `data-event-detail-dialog`      | 存在属性 | モーダルイベント詳細ダイアログのルート   |\n| `data-range-picker-popup`       | 存在属性 | range picker ポップアップのルート        |\n| `data-calendar-picker-dropdown` | 存在属性 | calendar picker ドロップダウンのルート   |\n| `data-grid-day-cell`            | 存在属性 | グリッド型年ビューの日セル               |\n| `data-grid-day-popup`           | 存在属性 | 年ビューの日ポップアップルート           |\n\n### 例\n\n```css\n/* 選択中のイベント */\n.df-event[data-selected='true'] {\n  outline: 2px solid var(--df-color-primary);\n}\n\n/* ドラッグ中のイベント */\n.df-event[data-dragging='true'] {\n  opacity: 0.6;\n}\n\n/* 月ビューのイベントのみ */\n.df-event[data-view='month'] {\n  font-size: 11px;\n}\n\n/* 終日イベントのみ */\n.df-event[data-all-day='true'] {\n  font-weight: 600;\n}\n\n/* 今日の Agenda セクションやコンパクトグリッドセルを強調 */\n[data-today='true'] {\n  background-color: color-mix(in srgb, var(--df-color-primary) 8%, transparent);\n}\n```\n\n---\n\n## カスタマイズ方法\n\n外観をカスタマイズするには、これらのクラスや `--df-color-*` 変数をグローバル CSS から上書きします。ほとんどのテーマ調整では `!important` は不要で、高い詳細度の局所ルールを意図的に上書きしたい場合だけ使ってください。\n\n### 例\n\n```css\n/* すべてのイベントの角丸を変更 */\n.df-event {\n  border-radius: 8px !important;\n}\n\n/* 現在時刻インジケーターをカスタマイズ */\n.df-current-time-line {\n  z-index: 100;\n}\n.df-current-time-bar {\n  height: 3px !important;\n  background-color: #ef4444 !important;\n}\n\n/* 特定のビューをカスタマイズ */\n.df-day-view .df-event {\n  border-left: 4px solid #3b82f6;\n}\n\n/* サイドバーのスタイル設定 */\n.df-sidebar {\n  background-color: #f8fafc !important;\n}\n\n/* ミニカレンダーのスタイル設定 */\n.df-mini-calendar-day:hover {\n  background-color: #e2e8f0;\n}\n\n/* カスタム slot / プラグイン UI で DayFlow のセマンティック色を再利用 */\n.my-custom-primary-chip {\n  background-color: var(--df-color-primary);\n  color: var(--df-color-primary-foreground);\n}\n\n/* ダークモードの上書き */\n.dark .df-calendar {\n  background-color: #111827;\n}\n\n/* イベント詳細パネルのスタイル設定 */\n.df-event-detail-panel {\n  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !important;\n}\n```\n\n## 使用のヒント\n\n1. **詳細度**: スタイルが適用されない場合は、カレンダーをカスタムクラスでラップして詳細度を上げてください：\n\n   ```css\n   .my-custom-calendar .df-event { ... }\n   ```\n\n2. **レスポンシブデザイン**: メディアクエリを使用して、モバイルデバイス向けにスタイルを調整します：\n\n   ```css\n   @media (max-width: 768px) {\n     .df-event-title {\n       font-size: 10px;\n     }\n     .df-time-label {\n       font-size: 10px;\n     }\n   }\n   ```\n\n3. **ダークモード**: `.dark` セレクターを使用してダークモード専用のスタイルを設定します：\n\n   ```css\n   .dark .df-month-day-cell {\n     border-color: #374151;\n   }\n   ```\n\n4. **CSS 変数**: DayFlow は `--df-color-*` カスタムプロパティで色を管理しています。`.df-calendar-container`（フローティング UI には `.df-portal` も）で上書きすると、スコープを絞った色変更ができます：\n   ```css\n   .df-calendar-container,\n   .df-portal {\n     --df-color-primary: #3b82f6;\n     --df-color-primary-foreground: #ffffff;\n   }\n   ```\n\n## 旧バージョンからのアップグレード\n\n<Callout title=\"破壊的変更：calendar-event クラスの削除\" type=\"warn\">\n  イベント要素から `calendar-event` クラスが削除されました。スタイルシートで `.calendar-event { ... }` を使用している場合は、`.df-event { ... }` に変更してください。\n\n以前 DayFlow の DOM 要素に付いていた Tailwind utility クラス（`flex`、`rounded-md`、`shadow-sm` など）もすべて削除されました。これらはパブリック API の一部ではありませんでした。すべての CSS カスタマイズには、ドキュメントに記載された `df-*` クラスと `data-*` 属性を使用してください。\n\n</Callout>\n\n## 関連ドキュメント\n\n- [テーマカスタマイズ](/docs-ja/guides/theme-customization) - 高度なテーマオプション\n- [ダークモード](/docs-ja/features/dark-mode) - ダークモード設定\n"
  },
  {
    "path": "website/content/docs-ja/guides/meta.json",
    "content": "{\n  \"title\": \"ガイド\",\n  \"pages\": [\"global-css\", \"theme-customization\", \"timezones\"]\n}\n"
  },
  {
    "path": "website/content/docs-ja/guides/theme-customization.mdx",
    "content": "# テーマカスタマイズガイド\n\nDayFlow カレンダーの見た目は、`--df-color-*` という名前空間付き CSS カスタムプロパティで管理されています。すべてのトークンは `@layer df-theme` の内側で定義されているため、ホストアプリケーションは `!important` を使わずに上書きできます。Tailwind を使っていない環境でも同様です。\n\nDayFlow の共有基盤 CSS は、トークン、コンポーネント基礎スタイル、そして `df-fill-primary` や `df-focus-ring` などの `df-*` セマンティック補助クラスの単一ソースです。\n\n## 目次\n\n- [カレンダータイプのカスタムカラー](#カレンダータイプのカスタムカラー)\n- [CSS 変数リファレンス](#css-変数リファレンス)\n- [上書き方法](#上書き方法)\n- [Tailwind v4 との連携](#tailwind-v4-との連携)\n- [カスタムテーマの組み立て](#カスタムテーマの組み立て)\n\n## カレンダータイプのカスタムカラー\n\n### 基本設定\n\n1 つの `calendarId` ごとにライト／ダークの配色を定義できます。\n\n```tsx\nconst calendar = useCalendarApp({\n  calendars: [\n    {\n      id: 'personal',\n      name: '個人',\n      colors: {\n        lineColor: '#0891b2',\n        eventColor: '#cffafe',\n        eventSelectedColor: '#a5f3fc',\n        textColor: '#164e63',\n      },\n      darkColors: {\n        lineColor: '#22d3ee',\n        eventColor: '#164e63',\n        eventSelectedColor: '#083344',\n        textColor: '#cffafe',\n      },\n    },\n  ],\n});\n```\n\n### 各色の役割\n\n- `lineColor`：イベントカード左側のアクセントやボーダー\n- `eventColor`：イベント本体の背景\n- `eventSelectedColor`：選択されたイベントの背景\n- `textColor`：タイトルや時刻テキスト\n\n### ブランドカラーを当てはめる\n\n```tsx\nconst brandPalette = {\n  lineColor: '#6366f1',\n  eventColor: '#e0e7ff',\n  eventSelectedColor: '#c7d2fe',\n  textColor: '#312e81',\n};\n\nconst brandDarkPalette = {\n  lineColor: '#a5b4fc',\n  eventColor: '#312e81',\n  eventSelectedColor: '#1e1b4b',\n  textColor: '#e0e7ff',\n};\n```\n\n### ヘルパーで色生成\n\n```ts\ninterface ColorSet {\n  lineColor: string;\n  eventColor: string;\n  eventSelectedColor: string;\n  textColor: string;\n}\n\nexport const buildLightColors = (base: string): ColorSet => ({\n  lineColor: base,\n  eventColor: lighten(base, 0.88),\n  eventSelectedColor: lighten(base, 0.8),\n  textColor: darken(base, 0.45),\n});\n\nexport const buildDarkColors = (base: string): ColorSet => ({\n  lineColor: lighten(base, 0.25),\n  eventColor: darken(base, 0.55),\n  eventSelectedColor: darken(base, 0.65),\n  textColor: lighten(base, 0.8),\n});\n```\n\n## CSS 変数リファレンス\n\nDayFlow のすべてのテーマトークンは `--df-color-` プレフィックスを持ち、ホストアプリの変数との衝突を防ぎます。\n\n| 変数                                | 用途                                 |\n| ----------------------------------- | ------------------------------------ |\n| `--df-color-background`             | カレンダーの背景                     |\n| `--df-color-foreground`             | 主テキスト色                         |\n| `--df-color-hover`                  | ホバー時の背景                       |\n| `--df-color-border`                 | ボーダーと区切り線                   |\n| `--df-color-card`                   | カード・パネルの背景                 |\n| `--df-color-card-foreground`        | カード内テキスト色                   |\n| `--df-color-muted`                  | 控えめな背景エリア                   |\n| `--df-color-muted-foreground`       | 補助テキスト色                       |\n| `--df-color-primary`                | 主アクセント（ボタン、選択状態など） |\n| `--df-color-primary-foreground`     | プライマリ背景上のテキスト           |\n| `--df-color-secondary`              | セカンダリアクセント                 |\n| `--df-color-secondary-foreground`   | セカンダリ背景上のテキスト           |\n| `--df-color-destructive`            | 削除など破壊的アクション             |\n| `--df-color-destructive-foreground` | デストラクティブ背景上のテキスト     |\n\n## セマンティック補助クラス\n\nCSS 変数に加えて、DayFlow はテーマ連動の `df-*` セマンティック補助クラスも公開しています。slot 内の独自 UI やプラグインコンポーネントを、内蔵 UI と同じ見た目に揃えたいときに便利です。\n\n| クラス名                 | 意味                                          |\n| ------------------------ | --------------------------------------------- |\n| `df-fill-primary`        | プライマリ塗りつぶし面 + 自動前景文字色       |\n| `df-fill-secondary`      | セカンダリ塗りつぶし面 + 自動前景文字色       |\n| `df-fill-destructive`    | 破壊的アクション面 + 自動前景文字色           |\n| `df-tint-primary`        | 控えめなプライマリ選択ティント                |\n| `df-hover-primary`       | プライマリ系ホバー状態                        |\n| `df-hover-primary-solid` | 塗りつぶしプライマリボタン用ホバー状態        |\n| `df-text-primary`        | プライマリ文字色                              |\n| `df-border-primary`      | プライマリ border 色                          |\n| `df-ring-primary`        | プライマリ ring トークン                      |\n| `df-focus-ring`          | `:focus` 時にプライマリ border と ring を適用 |\n\nテーマ調整では、これらの `df-*` クラスまたは `--df-color-*` を優先し、`bg-primary`、`text-primary`、`hover:bg-primary/90` などの旧来の内部 Tailwind セマンティック名には依存しないでください。\n\n## 上書き方法\n\n### 方法 1 — コンテナへの直接指定（全環境対応）\n\n`.df-calendar-container` に変数を直接設定します。この規則はどの `@layer` にも属さないため、ライブラリのデフォルト値に対して常に優先されます。\n\n実際には、`.df-calendar-container` と `.df-portal` を一緒に対象にすることが多くなります。\n\n- `.df-calendar-container` はカレンダー本体のルートコンテナです\n- `.df-portal` は DayFlow が `document.body` 配下に portal 描画する浮動 UI のルートクラスで、ダイアログ、ドロップダウン、一部の picker パネルなどに使われます\n\n`.df-calendar-container` だけを上書きすると、本体は新しいテーマになっても portal ベースの浮動 UI はデフォルトのトークンを使い続けることがあります。\n\n```css\n/* styles/globals.css */\n.df-calendar-container,\n.df-portal {\n  --df-color-primary: #6366f1;\n  --df-color-background: #f9fafb;\n  --df-color-border: #e0e7ff;\n}\n\n/* ダークモード — 祖先要素に .dark クラスがあるとき */\n.dark .df-calendar-container,\n.dark .df-portal {\n  --df-color-primary: #a5b4fc;\n  --df-color-background: #1e1e2e;\n  --df-color-border: #312e81;\n}\n```\n\nほとんどのプロジェクトにはこの方法を推奨します。変数はカレンダー内部にのみ影響し、外部には漏れません。\n\n## Tailwind v4 との連携\n\nTailwind v4 はすべて CSS で設定します。`tailwind.config.js` は不要です。DayFlow が配布する CSS には共有基盤スタイルが含まれているため、テーマトークンと `df-*` セマンティッククラスはインポート直後から利用できます。\n\n### CSS ファイルの選択\n\nDayFlow は 2 種類の CSS バンドルを提供します。\n\n| ファイル                | 内容                                                 | 使用場面                           |\n| ----------------------- | ---------------------------------------------------- | ---------------------------------- |\n| `styles.css`            | Tailwind preflight（CSS リセット）を含む完全バンドル | Tailwind を使わない場合            |\n| `styles.components.css` | コンポーネントスタイルのみ、**CSS リセットなし**     | すでに Tailwind を使用している場合 |\n\n### 最小構成\n\n```css\n/* app.css — Tailwind プロジェクト向け */\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\nTailwind を使用していない場合は、完全バンドルをインポートしてください。\n\n```css\n/* app.css — Tailwind を使わないプロジェクト向け */\n@import '@dayflow/core/dist/styles.css';\n```\n\n### ダークモード\n\nDayFlow は祖先要素（`<html>` を含む）の `.dark` クラスを参照します。JavaScript で切り替えてください。\n\n```ts\ndocument.documentElement.classList.toggle('dark', isDark);\n```\n\nクラスなしでもシステム設定のダークモードが自動的に適用されます。\n\n**Tailwind v4 で `theme.mode` を使う場合**\n\nDayFlow が配布する CSS には、`theme.mode` に必要な `.dark` variant がすでに含まれています。Tailwind v4 プロジェクトでは、`@dayflow/core/dist/styles.components.css` を読み込むだけで、DayFlow 自身は `<html>` 上の `.dark` クラス切り替えに追従します。\n\nアプリ側で自分の Tailwind `dark:` ユーティリティも同じクラス切り替えに合わせたい場合だけ、CSS エントリーファイルに次を追加してください：\n\n```css\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n\n/* 任意: アプリ側の Tailwind dark: ユーティリティにも必要な場合のみ */\n@variant dark (.dark &);\n```\n\nこの設定がなくても DayFlow は `theme.mode` で正しく切り替わります。影響を受けるのは、アプリ側で書いた Tailwind `dark:` ユーティリティが Tailwind 既定のメディアクエリ挙動のままになる点だけです。\n\n### 実践ガイド\n\n通常は次の優先順位でテーマを調整するのがおすすめです。\n\n1. `.df-calendar-container`、`.df-portal`、または自前のラッパーで `--df-color-*` を上書きする\n2. カスタム slot / プラグイン UI で `df-fill-primary` や `df-focus-ring` などの `df-*` 補助クラスを再利用する\n3. テーマではなくレイアウトを変えたい場合だけ、低レベルの utility を直接上書きする\n\n### Tailwind ユーティリティでラップする\n\n```tsx\nfunction ThemedCalendar({ calendar }) {\n  return (\n    <div className='rounded-2xl shadow-xl ring-1 ring-gray-200 dark:ring-gray-700'>\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n}\n```\n\n## カスタムテーマの組み立て\n\n```tsx\nexport const oceanTheme = {\n  mode: 'auto' as const,\n  calendars: [\n    {\n      id: 'deep-ocean',\n      name: 'Deep Ocean',\n      colors: {\n        lineColor: '#0284c7',\n        eventColor: '#e0f2fe',\n        eventSelectedColor: '#bae6fd',\n        textColor: '#0f172a',\n      },\n      darkColors: {\n        lineColor: '#38bdf8',\n        eventColor: '#0f172a',\n        eventSelectedColor: '#020617',\n        textColor: '#e0f2fe',\n      },\n    },\n  ],\n};\n\nconst calendar = useCalendarApp({ theme: oceanTheme });\n```\n\nCSS 変数の上書きと組み合わせると、見た目を完全に統一できます。\n\n```css\n/* Ocean テーマ CSS トークン */\n.df-calendar-container {\n  --df-color-primary: #0369a1;\n  --df-color-background: #f0f9ff;\n  --df-color-border: #bae6fd;\n}\n\n.dark .df-calendar-container {\n  --df-color-primary: #7dd3fc;\n  --df-color-background: #0c1220;\n  --df-color-border: #0c4a6e;\n}\n```\n\n## リソース\n\n### ツール\n\n- [WebAIM コントラストチェッカー](https://webaim.org/resources/contrastchecker/) - 色のコントラストを確認\n- [Coolors](https://coolors.co/) - カラーパレット生成\n- [カラーコントラストアナライザー](https://www.tpgi.com/color-contrast-checker/) - デスクトップツール\n\n### ライブラリ\n\n- [chroma-js](https://gka.github.io/chroma.js/) - 色操作\n- [tinycolor2](https://github.com/bgrins/TinyColor) - 色ユーティリティ\n- [color](https://github.com/Qix-/color) - 色変換\n\n### Tailwind リソース\n\n- [Tailwind v4 アップグレードガイド](https://tailwindcss.com/docs/upgrade-guide) - v3 → v4 移行\n- [Tailwind CSS レイヤー](https://tailwindcss.com/docs/adding-custom-styles#using-css-and-layer) - レイヤードキュメント\n\n## 関連ドキュメント\n\n- [ダークモード](/docs-ja/features/dark-mode) - ダークモードの概要と API\n- [カレンダータイプ](/docs-ja/introduction/events#calendar-types) - イベントの分類\n- [カレンダーアプリの使用](/docs-ja/introduction/use-calendar-app) - コア設定\n"
  },
  {
    "path": "website/content/docs-ja/guides/timezones.mdx",
    "content": "# タイムゾーン一覧\n\nDayFlow は、素の IANA 文字列を覚えなくても、アプリ全体の `timeZone` と Day / Week 専用の `secondaryTimeZone` を設定しやすいように `TimeZone` 列挙型を提供しています。\n\n## タイムゾーンモデル\n\n- `timeZone`: カレンダー全体の主表示 / 編集タイムゾーン\n- `secondaryTimeZone`: Day / Week ビューだけに表示される補助タイムライン\n\n`timeZone` を変更すると全ビューの表示が切り替わります。`secondaryTimeZone` を変更しても、Day / Week の追加タイムラインだけが変わります。\n\n## コールバックの意味\n\n- `timeZone` は UI 上の表示方法と、編集時に使う壁時計ベースの時間座標を変更します。\n- `timeZone` を切り替えるだけでは `onEventUpdate`、`onEventDrop`、`onEventResize` は発火しません。\n- 更新系コールバックが返すのは、編集後の canonical event です。現在表示中の一時的な投影タイムゾーン値そのものではありません。\n- つまり、Sydney のイベントを Shanghai 表示でリサイズした場合、操作自体は Shanghai の壁時計時間で解釈されますが、保存前にコールバック payload はイベントの canonical な時間表現へ戻されます。\n\n## 使用方法\n\n```tsx\nimport { TimeZone } from '@dayflow/core';\nimport {\n  useCalendarApp,\n  createDayView,\n  createWeekView,\n  createMonthView,\n} from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  timeZone: TimeZone.SHANGHAI,\n  views: [\n    createDayView({\n      secondaryTimeZone: TimeZone.NEW_YORK,\n    }),\n    createWeekView({\n      secondaryTimeZone: TimeZone.NEW_YORK,\n    }),\n    createMonthView(),\n  ],\n});\n```\n\n## 利用可能なタイムゾーン\n\n| 列挙型のキー             | IANA 文字列                      |\n| :----------------------- | :------------------------------- |\n| **UTC/GMT**              |                                  |\n| `TimeZone.UTC`           | `UTC`                            |\n| **北米**                 |                                  |\n| `TimeZone.NEW_YORK`      | `America/New_York`               |\n| `TimeZone.CHICAGO`       | `America/Chicago`                |\n| `TimeZone.DENVER`        | `America/Denver`                 |\n| `TimeZone.LOS_ANGELES`   | `America/Los_Angeles`            |\n| `TimeZone.TORONTO`       | `America/Toronto`                |\n| `TimeZone.VANCOUVER`     | `America/Vancouver`              |\n| `TimeZone.PHOENIX`       | `America/Phoenix`                |\n| `TimeZone.ANCHORAGE`     | `America/Anchorage`              |\n| `TimeZone.HONOLULU`      | `Pacific/Honolulu`               |\n| `TimeZone.MEXICO_CITY`   | `America/Mexico_City`            |\n| `TimeZone.WINNIPEG`      | `America/Winnipeg`               |\n| `TimeZone.HALIFAX`       | `America/Halifax`                |\n| `TimeZone.ST_JOHNS`      | `America/St_Johns`               |\n| `TimeZone.DETROIT`       | `America/Detroit`                |\n| `TimeZone.MIAMI`         | `America/Miami`                  |\n| **欧州**                 |                                  |\n| `TimeZone.LONDON`        | `Europe/London`                  |\n| `TimeZone.PARIS`         | `Europe/Paris`                   |\n| `TimeZone.BERLIN`        | `Europe/Berlin`                  |\n| `TimeZone.MADRID`        | `Europe/Madrid`                  |\n| `TimeZone.ROME`          | `Europe/Rome`                    |\n| `TimeZone.AMSTERDAM`     | `Europe/Amsterdam`               |\n| `TimeZone.ZURICH`        | `Europe/Zurich`                  |\n| `TimeZone.STOCKHOLM`     | `Europe/Stockholm`               |\n| `TimeZone.OSLO`          | `Europe/Oslo`                    |\n| `TimeZone.COPENHAGEN`    | `Europe/Copenhagen`              |\n| `TimeZone.MOSCOW`        | `Europe/Moscow`                  |\n| `TimeZone.ISTANBUL`      | `Europe/Istanbul`                |\n| `TimeZone.DUBLIN`        | `Europe/Dublin`                  |\n| `TimeZone.LISBON`        | `Europe/Lisbon`                  |\n| `TimeZone.PRAGUE`        | `Europe/Prague`                  |\n| `TimeZone.VIENNA`        | `Europe/Vienna`                  |\n| `TimeZone.WARSAW`        | `Europe/Warsaw`                  |\n| `TimeZone.BRUSSELS`      | `Europe/Brussels`                |\n| `TimeZone.ATHENS`        | `Europe/Athens`                  |\n| `TimeZone.BUCHAREST`     | `Europe/Bucharest`               |\n| `TimeZone.HELSINKI`      | `Europe/Helsinki`                |\n| `TimeZone.KYIV`          | `Europe/Kyiv`                    |\n| `TimeZone.BUDAPEST`      | `Europe/Budapest`                |\n| `TimeZone.BELGRADE`      | `Europe/Belgrade`                |\n| `TimeZone.LUXEMBOURG`    | `Europe/Luxembourg`              |\n| `TimeZone.MONACO`        | `Europe/Monaco`                  |\n| `TimeZone.REYKJAVIK`     | `Atlantic/Reykjavik`             |\n| **アジア**               |                                  |\n| `TimeZone.TOKYO`         | `Asia/Tokyo`                     |\n| `TimeZone.SHANGHAI`      | `Asia/Shanghai`                  |\n| `TimeZone.HONG_KONG`     | `Asia/Hong_Kong`                 |\n| `TimeZone.TAIPEI`        | `Asia/Taipei`                    |\n| `TimeZone.SEOUL`         | `Asia/Seoul`                     |\n| `TimeZone.SINGAPORE`     | `Asia/Singapore`                 |\n| `TimeZone.HANOI`         | `Asia/Ho_Chi_Minh`               |\n| `TimeZone.BANGKOK`       | `Asia/Bangkok`                   |\n| `TimeZone.JAKARTA`       | `Asia/Jakarta`                   |\n| `TimeZone.KUALA_LUMPUR`  | `Asia/Kuala_Lumpur`              |\n| `TimeZone.MANILA`        | `Asia/Manila`                    |\n| `TimeZone.DUBAI`         | `Asia/Dubai`                     |\n| `TimeZone.KOLKATA`       | `Asia/Kolkata`                   |\n| `TimeZone.RIYADH`        | `Asia/Riyadh`                    |\n| `TimeZone.TEHRAN`        | `Asia/Tehran`                    |\n| `TimeZone.JERUSALEM`     | `Asia/Jerusalem`                 |\n| `TimeZone.TEL_AVIV`      | `Asia/Tel_Aviv`                  |\n| `TimeZone.BAGHDAD`       | `Asia/Baghdad`                   |\n| `TimeZone.DHAKA`         | `Asia/Dhaka`                     |\n| `TimeZone.KARACHI`       | `Asia/Karachi`                   |\n| `TimeZone.KABUL`         | `Asia/Kabul`                     |\n| `TimeZone.KATHMANDU`     | `Asia/Kathmandu`                 |\n| `TimeZone.COLOMBO`       | `Asia/Colombo`                   |\n| `TimeZone.TASHKENT`      | `Asia/Tashkent`                  |\n| `TimeZone.ALMATY`        | `Asia/Almaty`                    |\n| `TimeZone.PHNOM_PENH`    | `Asia/Phnom_Penh`                |\n| `TimeZone.VIENTIANE`     | `Asia/Vientiane`                 |\n| `TimeZone.MUSCAT`        | `Asia/Muscat`                    |\n| **オセアニア**           |                                  |\n| `TimeZone.SYDNEY`        | `Australia/Sydney`               |\n| `TimeZone.MELBOURNE`     | `Australia/Melbourne`            |\n| `TimeZone.BRISBANE`      | `Australia/Brisbane`             |\n| `TimeZone.PERTH`         | `Australia/Perth`                |\n| `TimeZone.ADELAIDE`      | `Australia/Adelaide`             |\n| `TimeZone.DARWIN`        | `Australia/Darwin`               |\n| `TimeZone.HOBART`        | `Australia/Hobart`               |\n| `TimeZone.AUCKLAND`      | `Pacific/Auckland`               |\n| `TimeZone.FIJI`          | `Pacific/Fiji`                   |\n| `TimeZone.GUAM`          | `Pacific/Guam`                   |\n| `TimeZone.NOUMEA`        | `Pacific/Noumea`                 |\n| `TimeZone.PAGO_PAGO`     | `Pacific/Pago_Pago`              |\n| `TimeZone.PORT_MORESBY`  | `Pacific/Port_Moresby`           |\n| **南米**                 |                                  |\n| `TimeZone.SAO_PAULO`     | `America/Sao_Paulo`              |\n| `TimeZone.BUENOS_AIRES`  | `America/Argentina/Buenos_Aires` |\n| `TimeZone.SANTIAGO`      | `America/Santiago`               |\n| `TimeZone.LIMA`          | `America/Lima`                   |\n| `TimeZone.BOGOTA`        | `America/Bogota`                 |\n| `TimeZone.CARACAS`       | `America/Caracas`                |\n| `TimeZone.LA_PAZ`        | `America/La_Paz`                 |\n| `TimeZone.MONTEVIDEO`    | `America/Montevideo`             |\n| `TimeZone.QUITO`         | `America/Quito`                  |\n| `TimeZone.ASUNCION`      | `America/Asuncion`               |\n| `TimeZone.GEORGETOWN`    | `America/Guyana`                 |\n| **アフリカ**             |                                  |\n| `TimeZone.CAIRO`         | `Africa/Cairo`                   |\n| `TimeZone.JOHANNESBURG`  | `Africa/Johannesburg`            |\n| `TimeZone.LAGOS`         | `Africa/Lagos`                   |\n| `TimeZone.NAIROBI`       | `Africa/Nairobi`                 |\n| `TimeZone.CASABLANCA`    | `Africa/Casablanca`              |\n| `TimeZone.ALGIERS`       | `Africa/Algiers`                 |\n| `TimeZone.TUNIS`         | `Africa/Tunis`                   |\n| `TimeZone.ADDIS_ABABA`   | `Africa/Addis_Ababa`             |\n| `TimeZone.ACCRA`         | `Africa/Accra`                   |\n| `TimeZone.DAKAR`         | `Africa/Dakar`                   |\n| `TimeZone.LUANDA`        | `Africa/Luanda`                  |\n| `TimeZone.ANTANANARIVO`  | `Indian/Antananarivo`            |\n| `TimeZone.KINSHASA`      | `Africa/Kinshasa`                |\n| `TimeZone.DAR_ES_SALAAM` | `Africa/Dar_es_Salaam`           |\n| **南極**                 |                                  |\n| `TimeZone.MCMURDO`       | `Antarctica/McMurdo`             |\n| `TimeZone.CASEY`         | `Antarctica/Casey`               |\n"
  },
  {
    "path": "website/content/docs-ja/introduction/dayflow-calendar.mdx",
    "content": "# DayFlowCalendar\n\n`DayFlowCalendar` は現在のビューを描画し、サイドバーやイベント詳細ダイアログなどの UI をひとまとめに扱うコンポーネントです。`useCalendarApp` から返される `calendar` オブジェクトを渡すだけで、選択中のビューが自動的に切り替わります。\n\n## 基本的な使い方\n\n```tsx\nimport {\n  DayFlowCalendar,\n  useCalendarApp,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\nimport '@dayflow/core/dist/styles.css';\n\nexport function CalendarDemo() {\n  const calendar = useCalendarApp({\n    views: [createWeekView()],\n    defaultView: ViewType.WEEK,\n    initialDate: new Date(),\n    events: [],\n    plugins: [createSidebarPlugin()],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\nこのコンポーネントが担うのは以下のような処理です。\n\n- サイドバーの表示・折りたたみ（`createSidebarPlugin` を追加した場合）\n- イベント詳細パネル/ダイアログの切り替え\n- ビュー変更やカレンダーの可視状態に合わせたレイアウト更新\n\n## Props\n\n| Prop                    | 型                     | 必須 | 説明                                                                                   |\n| ----------------------- | ---------------------- | ---- | -------------------------------------------------------------------------------------- |\n| `calendar`              | `UseCalendarAppReturn` | 必須 | `useCalendarApp` の戻り値。状態やビュー、アクションを内包。                            |\n| `collapsedSafeAreaLeft` | `number`               | 任意 | サイドバー折りたたみ時の左パディング（px）。Mac のトラフィックライト領域の考慮に使用。 |\n| `search`                | `CalendarSearchProps`  | 任意 | 組み込み検索機能の設定。                                                               |\n\n## 検索設定\n\n`search` プロップを使用すると、組み込みの検索動作をカスタマイズできます。デフォルトでは、イベントのタイトルと説明をローカルで検索します。\n\n```tsx\n<DayFlowCalendar\n  calendar={calendar}\n  search={{\n    debounceDelay: 500,\n    emptyText: 'イベントが見つかりませんでした',\n    onSearch: async keyword => {\n      // カスタム非同期検索（例：APIから取得）\n      return fetch(`/api/search?q=${keyword}`).then(res => res.json());\n    },\n  }}\n/>\n```\n\n### 検索の設定 (Search Props)\n\n| オプション      | 説明                                                                                                                                                                                                                                                                                                          |\n| :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `debounceDelay` | 検索を実行するまでのデバウンス時間 (ミリ秒)。                                                                                                                                                                                                                                                                 |\n| `onSearch`      | キーワードに基づいて結果を取得する非同期関数。                                                                                                                                                                                                                                                                |\n| `customSearch`  | カスタムロジックを使用してローカルでイベントをフィルタリングする同期関数。                                                                                                                                                                                                                                    |\n| `onResultClick` | 検索結果をクリックした際の設定可能なコールバック。組み込みの遷移ロジック（イベントの日付への移動、イベントのハイライト、モバイル版での検索UIのクローズ）を実行するための `defaultAction` コールバックが含まれます。`defaultAction()` を呼び出さない場合は、遷移やUIの状態管理を独自に実装する必要があります。 |\n| `emptyText`     | 結果が見つからない場合に表示されるテキスト。                                                                                                                                                                                                                                                                  |\n\n## サイドバー連携\n\nサイドバーを表示するには、`@dayflow/plugin-sidebar` プラグインをインストールして `plugins` に追加します。\n\n```tsx\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\n\nconst calendar = useCalendarApp({\n  views: [createWeekView()],\n  plugins: [\n    createSidebarPlugin({\n      width: 280,\n      initialCollapsed: false,\n      createCalendarMode: 'modal', // 'inline' または 'modal'\n    }),\n  ],\n});\n\nreturn <DayFlowCalendar calendar={calendar} />;\n```\n\n- `createSidebarPlugin()` を追加すると、デフォルトのサイドバーが描画されます。\n- 幅や折りたたみ状態のほか、`render` または `renderSidebarHeader` による UI のカスタマイズも可能です。`renderSidebarHeader` を使用すると、サイドバー全体のレイアウトを維持したままヘッダー部分（タイトルや折りたたみボタン）のみを置き換えることができます。\n\n## イベント詳細 UI\n\nDayFlow には 3 つのイベント詳細表示オプションがあります。\n\n1. **デフォルトパネルモード** — `useEventDetailDialog` が `false`（デフォルト）の場合、イベントをクリックすると組み込みのフローティングパネル `DefaultEventDetailPanel` が開きます。`eventDetailContent` スロットを使用すると、パネルの外枠を維持しながら中身だけを置き換えられます。\n2. **ダイアログモード** — `useCalendarApp` で `useEventDetailDialog: true` を設定すると組み込みのモーダルが有効になります。`eventDetailDialog` スロットで独自のダイアログ UI に置き換えられます。\n3. **完全に無効化** — アプリ側のコールバックで独自のモーダルを管理している場合は、`useCalendarApp` で `useEventDetailPanel: false` を設定するとフローティングパネルを完全に非表示にできます。\n\n```tsx\n// 組み込みパネルを非表示にする — 別の場所でモーダルを管理\nconst calendar = useCalendarApp({\n  ...\n  useEventDetailPanel: false,\n});\n\nreturn <DayFlowCalendar calendar={calendar} />;\n```\n\n```tsx\n// カスタムパネルコンテンツ（デフォルトのパネル外枠を維持）\n<DayFlowCalendar\n  calendar={calendar}\n  eventDetailContent={({ event, onClose }) => (\n    <div className='p-4'>\n      <h2>{event.title}</h2>\n      <button onClick={onClose}>閉じる</button>\n    </div>\n  )}\n/>\n```\n\n```tsx\n// カスタムダイアログ（useCalendarApp で useEventDetailDialog: true が必要）\n<DayFlowCalendar\n  calendar={calendar}\n  eventDetailDialog={({ event, isOpen, onClose }) => (\n    <MyDialog isOpen={isOpen} onClose={onClose}>\n      <EventForm event={event} />\n    </MyDialog>\n  )}\n/>\n```\n\n---\n\nつまり `DayFlowCalendar` は「レイアウトと補助 UI の制御」を一手に引き受け、開発者は `useCalendarApp` の設定や周辺 UI に集中できるようになっています。\n\n## 関連ドキュメント\n\n- [useCalendarApp](/docs-ja/introduction/use-calendar-app) - カレンダーアプリ Hook\n- [クイックスタート](/docs-ja/introduction) - セットアップガイド\n- [ビュー](/docs-ja/introduction/views) - 利用可能なビュー\n"
  },
  {
    "path": "website/content/docs-ja/introduction/events.mdx",
    "content": "# イベントの扱い\n\nDayFlow におけるイベントは、ビューやプラグインすべての土台になるデータ構造です。ここではイベント定義の考え方、作成・更新・削除の方法、そしてバックエンド連携までをまとめて紹介します。\n\n## イベントインターフェース\n\n日付/時刻はすべて [Temporal API](https://tc39.es/proposal-temporal/) をベースにしています。必要に応じて以下 3 種類を使い分けます。\n\n- **PlainDate**：終日イベント（開始・終了日のみ欲しい場合）\n- **PlainDateTime**：タイムゾーンを意識しないローカルの予定 ➜ **標準はこちらを推奨**\n- **ZonedDateTime**：タイムゾーン込みの予定（海外会議・移動など）\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n```\n\n| プロパティ    | 型                                                                       | 説明                                                                                                                                                               | 必須 |\n| ------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---- |\n| `id`          | `string`                                                                 | イベントを一意に識別する ID                                                                                                                                        | 必須 |\n| `title`       | `string`                                                                 | カレンダーに表示するタイトル                                                                                                                                       | 必須 |\n| `start`       | `Temporal.PlainDate \\| Temporal.PlainDateTime \\| Temporal.ZonedDateTime` | 開始日時。イベント種別に合わせて型を選択                                                                                                                           | 必須 |\n| `end`         | `Temporal.PlainDate \\| Temporal.PlainDateTime \\| Temporal.ZonedDateTime` | 終了日時。`start` と同じ型を使う                                                                                                                                   | 必須 |\n| `description` | `string`                                                                 | メモや詳細。表示方法は自由                                                                                                                                         | 任意 |\n| `allDay`      | `boolean`                                                                | true なら終日イベントとして描画                                                                                                                                    | 任意 |\n| `icon`        | `boolean \\| Node`                                                        | イベント用カスタムアイコン。`true`（デフォルト）：デフォルト表示、`false`：非表示、`Node`：カスタムアイコン                                                        | 任意 |\n| `calendarId`  | `string`                                                                 | 単一カレンダー帰属のカテゴリ ID                                                                                                                                    | 任意 |\n| `calendarIds` | `string[]`                                                               | 複数カレンダーに同時帰属させる ID のリスト。設定時は `calendarId` より優先。リスト内のいずれかが表示中なら予定も表示される。複数色の斜線パターン背景で描画される。 | 任意 |\n| `meta`        | `Record<string, any>`                                                    | 任意の追加データ（場所・参加者など）                                                                                                                               | 任意 |\n\n## イベントの作り方\n\n### ヘルパー関数（最も簡単）\n\n`createEvent` や `createAllDayEvent` などのヘルパーは `Date` を渡すだけで内部で Temporal に変換してくれます。\n\n```tsx\nimport { createEvent, createAllDayEvent } from '@dayflow/core';\nimport '@dayflow/core/dist/styles.css';\n\nconst meeting = createEvent({\n  id: '1',\n  title: 'チームミーティング',\n  start: new Date(2024, 9, 15, 10, 0),\n  end: new Date(2024, 9, 15, 11, 0),\n  calendarId: 'work',\n});\n\nconst holiday = createAllDayEvent({\n  id: '2',\n  title: 'カンファレンス',\n  start: new Date(2024, 9, 20),\n  calendarId: 'work',\n});\n```\n\n### Temporal を直接扱う\n\n日時の粒度を厳密に制御したい場合はインターフェースを直接実装します。\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n\nconst localEvent: Event = {\n  id: '1',\n  title: 'ローカル会議',\n  start: Temporal.PlainDateTime.from({\n    year: 2024,\n    month: 10,\n    day: 15,\n    hour: 10,\n  }),\n  end: Temporal.PlainDateTime.from({\n    year: 2024,\n    month: 10,\n    day: 15,\n    hour: 11,\n  }),\n};\n\nconst allDayEvent: Event = {\n  id: '2',\n  title: 'カンファレンス',\n  start: Temporal.PlainDate.from('2024-10-20'),\n  end: Temporal.PlainDate.from('2024-10-22'),\n  allDay: true,\n};\n\nconst timezoneEvent: Event = {\n  id: '3',\n  title: '国際ミーティング',\n  start: Temporal.ZonedDateTime.from('2024-10-16T14:00:00[America/New_York]'),\n  end: Temporal.ZonedDateTime.from('2024-10-16T15:00:00[America/New_York]'),\n};\n```\n\n### メタデータ付きのイベント\n\n`meta` は UI に露出しない任意の情報を詰め込めます。\n\n```tsx\nimport { createEvent } from '@dayflow/core';\n\nconst event = createEvent({\n  id: '3',\n  title: '顧客打ち合わせ',\n  description: 'Q4 のロードマップ確認',\n  start: new Date(2024, 9, 16, 14, 0),\n  end: new Date(2024, 9, 16, 15, 0),\n  calendarId: 'work',\n  meta: {\n    location: 'Zoom',\n    attendees: ['john@example.com', 'jane@example.com'],\n    recurring: false,\n  },\n});\n```\n\n## イベントの管理\n\nDayFlow では `useCalendarApp` から返る `calendar` インスタンスに対して CRUD を行います。\n\n### 追加\n\n```tsx\ncalendar.addEvent(event);\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  calendars: [\n    {\n      id: 'work',\n      name: '仕事',\n      colors: {\n        lineColor: '#2563eb',\n        eventColor: '#dbeafe',\n        eventSelectedColor: '#bfdbfe',\n        textColor: '#1e3a8a',\n      },\n    },\n  ],\n  events: [event1, event2],\n});\n```\n\n### 更新\n\n```tsx\ncalendar.updateEvent('event-id', {\n  title: 'タイトルを更新',\n  start: new Date(2024, 9, 15, 11, 0),\n  end: new Date(2024, 9, 15, 12, 0),\n});\n\n// 3 つ目の引数を true にするとドラッグ中などの一時状態として扱える\ncalendar.updateEvent('event-id', updatedEvent, true);\n```\n\n### 削除・取得\n\n```tsx\ncalendar.deleteEvent('event-id');\n\nconst allEvents = calendar.getEvents();\nconst { events } = calendar; // 現在の状態をそのまま参照\n```\n\n## コールバックと状態管理\n\n使用しているフレームワーク（React, Vue, Svelte, Angular）の状態管理と組み合わせると次のようになります。\n\n```tsx\nimport { useState } from 'react';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  Event,\n} from '@dayflow/react';\n\nfunction MyCalendar() {\n  const [events, setEvents] = useState<Event[]>([]);\n\n  const calendar = useCalendarApp({\n    views: [createMonthView()],\n    events,\n    callbacks: {\n      onEventCreate: event => setEvents(prev => [...prev, event]),\n      onEventUpdate: event =>\n        setEvents(prev => prev.map(e => (e.id === event.id ? event : e))),\n      onEventDelete: eventId =>\n        setEvents(prev => prev.filter(e => e.id !== eventId)),\n      onEventDoubleClick: (event, e) => {\n        console.log('イベントがダブルクリックされました:', event);\n        // e.currentTarget をカスタムポップオーバーのアンカーとして使用\n      },\n    },\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\nバックエンドと同期する場合は非同期処理をコールバックに直接書いて問題ありません。\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  events,\n  callbacks: {\n    onEventCreate: async event => {\n      const saved = await api.createEvent(event);\n      setEvents(prev => [...prev, saved]);\n    },\n    onEventUpdate: async event => {\n      await api.updateEvent(event);\n      setEvents(prev => prev.map(e => (e.id === event.id ? event : e)));\n    },\n    onEventDelete: async eventId => {\n      await api.deleteEvent(eventId);\n      setEvents(prev => prev.filter(e => e.id !== eventId));\n    },\n  },\n});\n```\n\n## カレンダータイプと装飾\n\n`calendarId` と `calendars` 設定を組み合わせると、予定ごとに色やスタイルを切り替えられます。\n\n```tsx\nimport { createEvent, useCalendarApp, createMonthView } from '@dayflow/react';\n\nconst workEvent = createEvent({\n  id: '1',\n  title: 'デザインレビュー',\n  start: new Date(2024, 9, 15, 14, 0),\n  end: new Date(2024, 9, 15, 15, 0),\n  calendarId: 'work',\n});\n\nconst personalEvent = createEvent({\n  id: '2',\n  title: '歯医者',\n  start: new Date(2024, 9, 16, 10, 0),\n  end: new Date(2024, 9, 16, 11, 0),\n  calendarId: 'personal',\n});\n\nconst calendars = [\n  {\n    id: 'work',\n    name: '仕事',\n    colors: {\n      eventColor: '#3b82f6',\n      eventSelectedColor: '#2563eb',\n      lineColor: '#3b82f6',\n      textColor: '#ffffff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'personal',\n    name: 'プライベート',\n    colors: {\n      eventColor: '#10b981',\n      eventSelectedColor: '#059669',\n      lineColor: '#10b981',\n      textColor: '#ffffff',\n    },\n    isVisible: true,\n  },\n];\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  events: [workEvent, personalEvent],\n  calendars,\n});\n```\n\n## 複数カレンダーへの同時帰属\n\n### 動作仕様\n\n- `calendarIds` が設定されている場合、`calendarId` より優先されます。\n- **表示ルール**：リスト内のカレンダーがひとつでも表示中であれば予定も表示されます。\n- **ビジュアル**：複数カレンダーの予定は **45° 斜線パターン**背景（カレンダーごとの色が交互に入ります）と、左端の**多色グラデーションバー**で一目でわかるようになります。\n- **選択中**：プライマリカレンダー（`calendarIds[0]`）のソリッドカラーに戻ります。\n\n### 使用例\n\n```tsx\nimport { createEvent, useCalendarApp, createWeekView } from '@dayflow/react';\n\n// \"team\" と \"marketing\" の両カレンダーに帰属させる\nconst sharedEvent = createEvent({\n  id: 'shared-1',\n  title: '全社ミーティング',\n  start: new Date(2024, 9, 15, 10, 0),\n  end: new Date(2024, 9, 15, 11, 30),\n  calendarIds: ['team', 'marketing'], // 複数カレンダー\n});\n\n// 3 カレンダーにまたがる終日予定\nconst crossCalendarDay = {\n  id: 'shared-2',\n  title: 'チーム合宿',\n  start: Temporal.PlainDate.from('2024-10-20'),\n  end: Temporal.PlainDate.from('2024-10-22'),\n  allDay: true,\n  calendarIds: ['team', 'personal', 'travel'],\n};\n\nconst calendars = [\n  {\n    id: 'team',\n    name: 'チーム',\n    colors: {\n      eventColor: '#3b82f6',\n      lineColor: '#3b82f6',\n      eventSelectedColor: '#2563eb',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'marketing',\n    name: 'マーケティング',\n    colors: {\n      eventColor: '#f59e0b',\n      lineColor: '#f59e0b',\n      eventSelectedColor: '#d97706',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'personal',\n    name: 'プライベート',\n    colors: {\n      eventColor: '#10b981',\n      lineColor: '#10b981',\n      eventSelectedColor: '#059669',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'travel',\n    name: '旅行',\n    colors: {\n      eventColor: '#8b5cf6',\n      lineColor: '#8b5cf6',\n      eventSelectedColor: '#7c3aed',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n];\n\nconst calendar = useCalendarApp({\n  views: [createWeekView()],\n  events: [sharedEvent, crossCalendarDay],\n  calendars,\n});\n```\n\n## 複数日にまたがる予定\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n\nconst conference: Event = {\n  id: '1',\n  title: 'Tech Conference 2024',\n  start: Temporal.PlainDate.from('2024-10-20'),\n  end: Temporal.PlainDate.from('2024-10-22'),\n  allDay: true,\n};\n\nconst nightShift: Event = {\n  id: '2',\n  title: '夜勤シフト',\n  start: Temporal.ZonedDateTime.from('2024-10-15T22:00:00[America/New_York]'),\n  end: Temporal.ZonedDateTime.from('2024-10-16T06:00:00[America/New_York]'),\n};\n```\n\n## イベントメタデータ\n\n`meta` フィールドにはあらゆる追加情報を格納できます。\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n\nconst event: Event = {\n  id: '1',\n  title: 'プロジェクトキックオフ',\n  start: Temporal.ZonedDateTime.from('2024-10-15T10:00:00[America/New_York]'),\n  end: Temporal.ZonedDateTime.from('2024-10-15T11:00:00[America/New_York]'),\n  meta: {\n    // ミーティング詳細\n    location: '会議室 A',\n    meetingUrl: 'https://zoom.us/j/123456',\n\n    // 参加者情報\n    organizer: 'john@example.com',\n    attendees: ['jane@example.com', 'bob@example.com'],\n\n    // カスタムフィールド\n    project: 'プロジェクト X',\n    priority: 'high',\n    tags: ['planning', 'kickoff'],\n\n    // 繰り返し情報\n    recurring: true,\n    recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO',\n\n    // その他任意のデータ\n    customField: 'custom value',\n  },\n};\n```\n\n## ベストプラクティス\n\n1. **ID は必ず一意に**：UUID もしくはバックエンドの ID をそのまま使う\n2. **ヘルパーを優先**：`createEvent` 系で 9 割のユースケースを賄える\n3. **型の選択を意識**：終日 = PlainDate、通常 = PlainDateTime、時差対応 = ZonedDateTime\n4. **`calendarId` / `calendarIds` で分類**：単一カレンダーは `calendarId`、複数カレンダー帰属は `calendarIds` を使う\n5. **`meta` を活用**：場所や出席者など UI に依存しない情報をここへ\n6. **開始と終了の整合性をチェック**：`start < end` を守るとバグを防げる\n7. **バッチ更新を検討**：大量更新はまとめて `updateEvents` 相当の処理に寄せる\n\n## 型リファレンス\n\n日付と時刻の処理の詳細については以下を参照してください。\n\n- イベントインターフェース：`src/types/event.ts`\n\n## 関連ドキュメント\n\n- [ビュー](/docs-ja/introduction/views) - カレンダービューについて\n- [プラグイン](/docs-ja/plugins/overview) - プラグインによるイベント管理\n- [はじめに](/docs-ja/introduction) - 基本的な使い方\n"
  },
  {
    "path": "website/content/docs-ja/introduction/index.mdx",
    "content": "# はじめに\n\nDayFlow は「すぐに触れるカレンダー体験」を目指して設計された、マルチフレームワーク対応（React, Vue, Svelte, Angular）のコンポーネントスイートです。このページでは、最小構成で導入するための流れと、次に読むべきドキュメントをまとめました。\n\n## インストール\n\n### CLI を使う（推奨）\n\nインタラクティブなセットアップウィザードを実行してください。パッケージマネージャーを自動検出し、フレームワークとプラグインを選択して、一度にすべてをインストールします。\n\n<CreateDayflowTabs />\n\n<CliPreview />\n\n### 手動インストール\n\nパッケージを手動でインストールする場合は、フレームワークとパッケージマネージャーを選択してください。\n\n<FrameworkInstall />\n\n### DayFlow のスタイルを読み込む\n\n**重要:** DayFlow のレイアウトを正しく表示するには、スタイルシートのインポートが必要です。アプリのエントリーポイント（App.tsx や layout.tsx など）でスタイルシートを 1 行追加してください。\n\n```tsx\n// App.tsx または layout.tsx\nimport '@dayflow/core/dist/styles.css';\n```\n\nCSS ファイルで読み込みたい場合は次のように記述できます。\n\n```css\n/* src/index.css */\n@import '@dayflow/core/dist/styles.css';\n```\n\n<Callout title=\"Tailwind CSS を使用していますか？\" type=\"warn\">\n  既存のスタイルとの CSS リセットの競合を避けるために、代わりに `styles.components.css` をインポートしてください：\n\n```css\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\n詳細については、[Tailwind V4 integration](https://calendar.dayflow.studio/docs/guides/theme-customization#tailwind-v4-integration) を参照してください。\n\n> **ヒント:** `create-dayflow` セットアップウィザードで Tailwind CSS に「はい」を選ぶと、この import はルート CSS ファイルへ自動追加されます。\n\n</Callout>\n\n<Callout title=\"Next.js Pages Router または Node.js の require() を使っていますか？\" type=\"warn\">\n  `@dayflow/core` は **ESM のみ** を提供しており、CommonJS ビルドは含まれていません。\n\n- **`require('@dayflow/core')`** はエラーになります。`import` を使用してください。\n- **Next.js Pages Router** はデフォルトで `node_modules` の ESM をトランスパイルしません。`next.config.js` の `transpilePackages` にパッケージを追加してください：\n\n```js\n// next.config.js\nmodule.exports = {\n  transpilePackages: ['@dayflow/core'],\n};\n```\n\n- **Next.js App Router**（`\"use client\"` / Server Components）は追加設定なしで動作します。\n- **Nuxt・SvelteKit などの SSR フレームワーク**は通常 ESM をネイティブに扱えます。インポートエラーが発生する場合は、`ssr.noExternal` などのオプションに `@dayflow/core` を追加してください。\n\n</Callout>\n\n## 基本的な使い方\n\n使用しているフレームワークを選択して、DayFlow をプロジェクトに統合する方法を確認してください。\n\n<FrameworkTabs>\n  <Tab>\n    ```tsx\n    import {\n      useCalendarApp,\n      DayFlowCalendar,\n      createDayView,\n      createWeekView,\n      createMonthView,\n      createEventsPlugin,\n    } from '@dayflow/react';\n    import { createDragPlugin } from '@dayflow/plugin-drag';\n    import { createEvent, createAllDayEvent } from '@dayflow/core';\n    import '@dayflow/core/dist/styles.css';\n\n    function App() {\n      const calendar = useCalendarApp({\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: '仕事',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: 'チームミーティング',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: '終日会議',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      });\n\n      return <DayFlowCalendar calendar={calendar} />;\n    }\n    ```\n\n  </Tab>\n  <Tab>\n    ```vue\n    <template>\n      <DayFlowCalendar :calendar=\"calendar\" />\n    </template>\n\n    <script setup>\n      import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';\n      import {\n        createDayView,\n        createWeekView,\n        createMonthView,\n        createEventsPlugin,\n        createEvent,\n        createAllDayEvent\n      } from '@dayflow/core';\n      import { createDragPlugin } from '@dayflow/plugin-drag';\n      import '@dayflow/core/dist/styles.css';\n\n      const calendar = useCalendarApp({\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: '仕事',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: 'チームミーティング',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: '終日会議',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      });\n    </script>\n    ```\n\n  </Tab>\n  <Tab>\n    ```typescript\n    import { Component } from '@angular/core';\n    import {\n      createDayView,\n      createWeekView,\n      createMonthView,\n      createEventsPlugin,\n      createEvent,\n      createAllDayEvent\n    } from '@dayflow/core';\n    import { createDragPlugin } from '@dayflow/plugin-drag';\n    import { DayFlowCalendarModule } from '@dayflow/angular';\n    import '@dayflow/core/dist/styles.css';\n\n    @Component({\n      selector: 'app-root',\n      standalone: true,\n      imports: [DayFlowCalendarModule],\n      template: `\n        <dayflow-calendar [calendar]=\"calendar\"></dayflow-calendar>\n      `\n    })\n    export class AppComponent {\n      calendar = {\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: '仕事',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: 'チームミーティング',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: '終日会議',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      };\n    }\n    ```\n\n  </Tab>\n  <Tab>\n    ```svelte\n    <script>\n      import { DayFlowCalendar, useCalendarApp } from '@dayflow/svelte';\n      import {\n        createDayView,\n        createWeekView,\n        createMonthView,\n        createEventsPlugin,\n        createEvent,\n        createAllDayEvent\n      } from '@dayflow/core';\n      import { createDragPlugin } from '@dayflow/plugin-drag';\n      import '@dayflow/core/dist/styles.css';\n\n      const calendar = useCalendarApp({\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: '仕事',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: 'チームミーティング',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: '終日会議',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      });\n    </script>\n\n    <DayFlowCalendar {calendar} />\n    ```\n\n  </Tab>\n</FrameworkTabs>\n\n## スタイルのカスタマイズ\n\nデフォルトでは、カレンダーはコンテナの幅全体を占有します。CSS を使用して、高さやその他のスタイルをカスタマイズできます。例えば、レスポンシブな高さを設定するには次のようにします。\n\n```css\n.df-calendar-container {\n  --df-calendar-height: 760px !important;\n  height: var(--df-calendar-height, 760px) !important;\n}\n\n@media (max-width: 768px) {\n  .df-calendar-container {\n    --df-calendar-height: 550px !important;\n  }\n}\n```\n\n> 注: DayFlow は、日付/時刻の処理にモダンな [Temporal API](/docs-ja/temporal-migration) を内部で使用しています。ヘルパー関数 (`createEvent`) を使用することで、Temporal を直接意識することなくイベントを簡単に作成できます。\n\n## 次に読むとよいページ\n\n## 次に読むとよいページ\n\n- [イベント](/docs-ja/introduction/events)：予定作成・更新・削除の基本\n- [ビュー](/docs-ja/introduction/views)：日・週・月・Agenda・年ビューの挙動\n- [サイドバー](/docs-ja/introduction/sidebar)：カレンダーリストやフィルターの構成\n- [機能一覧](/docs-ja/features)：DayFlow が提供する追加機能を俯瞰\n"
  },
  {
    "path": "website/content/docs-ja/introduction/meta.json",
    "content": "{\n  \"title\": \"はじめに\",\n  \"defaultOpen\": true,\n  \"pages\": [\n    \"index\",\n    \"events\",\n    \"views\",\n    \"resource-grid\",\n    \"resource-timeline\",\n    \"dayflow-calendar\",\n    \"use-calendar-app\"\n  ]\n}\n"
  },
  {
    "path": "website/content/docs-ja/introduction/resource-grid.mdx",
    "content": "---\ntitle: リソースグリッド\ndescription: リソースごとの列、日付グループ化モード、およびドラッグベースのスケジューリングインタラクションを備えた DayFlow Pro 垂直リソースグリッドビュー。\nstatus: pro\n---\n\n# リソースグリッド (Resource Grid)\n\n`@dayflow-pro/resource-grid` は、時間が上から下に流れ、リソースが列としてレンダリングされる垂直スケジューリンググリッドを提供します。これは、会議室、設備、スタジオ、デスクの予約、またはスタッフのシフト管理に適しています。\n\n[ライブデモ](https://pro.dayflow.studio)を試す\n\n## インストール\n\n<PackageTabs pkg='@dayflow-pro/resource-grid' />\n\nDayFlow Pro のインストール手順については、[Pro のインストール](/docs/introduction/pro-installation)を参照してください。\n\n## 基本的な使い方\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { createResourceGridView } from '@dayflow-pro/resource-grid';\n\nfunction App() {\n  const calendar = useCalendarApp({\n    views: [\n      createResourceGridView({\n        mode: 'resourceView',\n        resources: [\n          { id: 'room-a', title: 'Room A' },\n          { id: 'room-b', title: 'Room B' },\n        ],\n        visibleDays: 3,\n        hourHeight: 72,\n        firstHour: 8,\n        lastHour: 20,\n      }),\n    ],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## ビューモード\n\n| モード            | 説明                                                           |\n| :---------------- | :------------------------------------------------------------- |\n| `resourceView`    | リソースが主要な列となり、各リソース内で日付が繰り返されます。 |\n| `resourcesByDate` | 日付が主要な列となり、各日付内でリソースが繰り返されます。     |\n\nパッケージのデフォルト設定は以下の通りです。\n\n```ts\n{\n  visibleDays: 7,\n  showWeekend: true,\n  onlyShowEventDays: false,\n  dayWidth: 120,\n  hourHeight: 72,\n  firstHour: 0,\n  lastHour: 24,\n  stickyHeaders: true,\n}\n```\n\n## リソースとイベントモデル\n\nリソースは意図的にシンプルに設計されています。\n\n```ts\ntype Resource = {\n  id: string;\n  title: string;\n  avatar?: string;\n  color?: string;\n  meta?: Record<string, unknown>;\n};\n```\n\nイベントは `resourceId` を直接保持するか、コールバックを通じて解決できます。\n\n```tsx\nconst event = {\n  id: 'booking-1',\n  title: 'Studio recording',\n  start: new Date(2026, 4, 8, 10, 0),\n  end: new Date(2026, 4, 8, 12, 0),\n  resourceId: 'room-a',\n};\n```\n\n解決順序は以下の通りです。\n\n1. 提供されている場合は `getResourceId(event)`\n2. `event.resourceId`\n3. `event.calendarId`\n\n## フィルタリングと表示設定\n\nグリッドはリソースと日付の両方をフィルタリングできます。\n\n| プロパティ                            | 型                                  | 備考                                                 |\n| :------------------------------------ | :---------------------------------- | :--------------------------------------------------- |\n| `visibleResourceIds`                  | `string[]`                          | グリッドを特定のリソースサブセットに制限します。     |\n| `isResourceVisible`                   | `(resource) => boolean`             | リソースを完全に非表示にするためのカスタム述語。     |\n| `syncResourceVisibilityWithCalendars` | `boolean`                           | カレンダーの表示状態に自動的に従います。             |\n| `getCalendarIdForResource`            | `(resource) => string \\| undefined` | リソースからカレンダーへのマッピングを上書きします。 |\n| `showWeekend`                         | `boolean`                           | 週末を含めるか除外するかを指定します。               |\n| `onlyShowEventDays`                   | `boolean`                           | 一致するイベントがない表示日を削除します。           |\n\nカレンダーの同期が有効な場合、フォールバックのマッピングは `resource.meta?.calendarId ?? resource.id` です。\n\n`onlyShowEventDays` によって現在の範囲からすべての日付が削除された場合、ビューは空のグリッドを表示する代わりに通常の日付範囲にフォールバックします。\n\n## レイアウト設定\n\n| プロパティ           | 型                                | 備考                                                 |\n| :------------------- | :-------------------------------- | :--------------------------------------------------- |\n| `license`            | `PackageLicenseConfig`            | グローバル登録を使用しない場合のオプションの上書き。 |\n| `visibleDays`        | `number`                          | 現在の日付から生成される日数。                       |\n| `hourHeight`         | `number`                          | 1 時間の行の高さ（ピクセル単位）。                   |\n| `firstHour`          | `number`                          | 最初に表示される時間。                               |\n| `lastHour`           | `number`                          | 最後に表示される時間。                               |\n| `dayWidth`           | `number`                          | 各列に使用される基本の幅。                           |\n| `stickyHeaders`      | `boolean`                         | スクロー中、ヘッダーを固定します。                   |\n| `initialScrollState` | `{ left?: number; top?: number }` | 初期のスクロール位置を設定します。                   |\n\nまた、ビューポートが設定されたコンテンツよりも広く、現在のモードが許可している場合、実装により列が自動的に引き伸ばされます。\n"
  },
  {
    "path": "website/content/docs-ja/introduction/resource-timeline.mdx",
    "content": "---\ntitle: リソースタイムライン\ndescription: グループ化されたリソース、マイルストーンのレンダリング、表示の切り替え、およびスケジューラ形式のインタラクションを備えた DayFlow Pro 水平リソースタイムラインビュー。\nstatus: pro\n---\n\n# リソースタイムライン (Resource Timeline)\n\n`@dayflow-pro/resource-timeline` は、DayFlow にスケジューラ形式の水平タイムラインを追加します。時間は左から右に流れ、各リソースは独自の行を持ちます。このビューは、人、会議室、プロジェクト、または機器にわたる作業の計画に最適化されています。\n\n[ライブデモ](https://pro.dayflow.studio)を試す\n\n組み込みのカレンダービューと比較して、リソースタイムラインは以下の用途に最適化されています。\n\n- 長期にわたるタスクとプロジェクト計画\n- 複数のレーンにわたるリソース割り当て\n- マイルストーンと進捗管理\n- 階層化またはグループ化されたリソースリスト\n\n## インストール\n\n<PackageTabs pkg='@dayflow-pro/resource-timeline' />\n\nDayFlow Pro のインストール手順については、[Pro のインストール](/docs/introduction/pro-installation)を参照してください。\n\n## 基本的な使い方\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { createResourceTimelineView } from '@dayflow-pro/resource-timeline';\n\nfunction App() {\n  const calendar = useCalendarApp({\n    views: [\n      createResourceTimelineView({\n        resources: [\n          {\n            id: 'eng-alice',\n            name: 'Alice Chen',\n            groupId: 'engineering',\n            groupName: 'Engineering',\n            subtitle: 'Frontend',\n          },\n          {\n            id: 'eng-bob',\n            name: 'Bob Torres',\n            groupId: 'engineering',\n            groupName: 'Engineering',\n            subtitle: 'Platform',\n          },\n        ],\n        defaultView: 'week',\n        height: 640,\n        getResourceId: event =>\n          (event as { resourceId?: string }).resourceId ?? event.calendarId,\n      }),\n    ],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## タイムラインモード\n\n| モード    | 説明                                       |\n| :-------- | :----------------------------------------- |\n| `day`     | 時間ベースの列を持つ 1 日。                |\n| `week`    | 日ベースの列を持つ 1 週間。                |\n| `month`   | 日ベースの列を持つ 1 か月。                |\n| `quarter` | 週/月でグループ化された 3 か月。           |\n| `year`    | ロードマップ形式の計画のためのフルイヤー。 |\n\n`defaultView` を使用して初期モードを選択します。また、`viewSwitcherMode` を通じて切り替えのレンダリング方法を制御でき、`buttons`、`select`、`filter` をサポートしています。\n\n## リソースモデル\n\nリソースはプレーンなオブジェクトです。ソースコードでは `id` と `name` 以外にも多くのフィールドをサポートしています。\n\n```ts\ntype Resource = {\n  id: string;\n  name: string;\n  parentId?: string;\n  collapsed?: boolean;\n  groupId?: string;\n  groupName?: string;\n  order?: number;\n  subtitle?: string;\n  avatar?: string;\n  color?: string;\n  style?: {\n    colors: { color: string; textColor?: string };\n    darkColors?: { color: string; textColor?: string };\n    opacity?: number;\n    borderRadius?: number;\n  };\n  meta?: Record<string, unknown>;\n};\n```\n\n便利なフィールド:\n\n- `groupId` と `groupName`: リソースをグループ化されたセクションに整理できます。\n- `parentId` と `collapsed`: 階層的なリソースツリーをサポートします。\n- `subtitle`、`avatar`、`color`、`style`: サイドバーの表示を豊かにします。\n- `meta`: `calendarId` などの外部識別子を保存するために頻繁に使用されます。\n\n## イベントとリソースの紐付け\n\nこのビューは、各イベントの行を次の順序で解決します。\n\n1. 提供されている場合は `getResourceId(event)`\n2. `event.resourceId`\n3. `event.meta?.resourceId`\n4. `event.calendarId`\n\nつまり、イベントオブジェクトに直接 `resourceId` を保存するか、コールバックを使用して既存の DayFlow イベントをリソースにマップすることができます。\n\n```tsx\nconst event = {\n  id: 'task-1',\n  title: 'Build export flow',\n  start: new Date(2026, 4, 10, 9, 0),\n  end: new Date(2026, 4, 12, 18, 0),\n  resourceId: 'eng-alice',\n  progress: 65,\n};\n```\n\n## マイルストーンとイベントのスタイリング\n\n`start` と `end` が等しいイベントは、自動的にマイルストーンとしてレンダリングされます。\n\n```tsx\nconst milestone = {\n  id: 'release-gate',\n  title: 'Release approval',\n  start: new Date(2026, 4, 14, 10, 0),\n  end: new Date(2026, 4, 14, 10, 0),\n  resourceId: 'eng-bob',\n};\n```\n\n| プロパティ              | 型                               | 備考                                                                                   |\n| :---------------------- | :------------------------------- | :------------------------------------------------------------------------------------- |\n| `milestoneLabelDisplay` | `'hover' \\| 'always'`            | マイルストーンのラベルをホバー時にのみ表示するか、常に表示するかを制御します。         |\n| `milestoneLaneBehavior` | `'smart' \\| 'overlay'`           | マイルストーンが可能であれば通常のレーンを共有するか、オーバーレイするかを制御します。 |\n| `getEventType`          | `(event) => ResourceEventType`   | イベントをタイムライン固有のイベントタイプにマップします。                             |\n| `getEventTooltip`       | `(event) => string \\| undefined` | イベントのツールチップテキストを提供します。                                           |\n| `styleRegistry`         | `Record<string, EventStyle>`     | カスタムスタイルのプリセットを登録します。                                             |\n| `getEventStyleId`       | `(event) => string \\| undefined` | イベントのスタイルプリセットを選択します。                                             |\n\n## ResourceTimelineViewConfig\n\n### コア (Core)\n\n| プロパティ         | 型                                        | 備考                                                       |\n| :----------------- | :---------------------------------------- | :--------------------------------------------------------- |\n| `resources`        | `Resource[]`                              | タイムラインに必要なリソースリスト。                       |\n| `license`          | `PackageLicenseConfig`                    | グローバル登録を使用しない場合のオプションの上書き。       |\n| `defaultView`      | `day \\| week \\| month \\| quarter \\| year` | 初期のタイムラインスケール。                               |\n| `views`            | `SchedulerTimelineView[]`                 | 利用可能な切り替え可能なタイムラインモードを上書きします。 |\n| `viewType`         | `ViewType \\| string`                      | 高度なホストと統合する場合のカスタムビュータイプ。         |\n| `viewSwitcherMode` | `'buttons' \\| 'select' \\| 'filter'`       | モード切り替え UI を制御します。                           |\n| `height`           | `number \\| string`                        | レンダリングされるビューの高さ。                           |\n\n### レイアウト (Layout)\n\n| プロパティ              | 型       | 備考                                                           |\n| :---------------------- | :------- | :------------------------------------------------------------- |\n| `resourcePanelWidth`    | `number` | 推奨されるサイドバーの幅。                                     |\n| `resourcePanelMinWidth` | `number` | 最小サイドバー幅。                                             |\n| `resourcePanelMaxWidth` | `number` | 最大サイドバー幅。                                             |\n| `rowHeight`             | `number` | 各リソース行の高さ。                                           |\n| `headerHeight`          | `number` | タイムラインヘッダーの高さ。                                   |\n| `hourWidth`             | `number` | day モードの時間スケールで使用される幅。                       |\n| `weekDayWidth`          | `number` | week モードの 1 日あたりの幅。                                 |\n| `dayWidth`              | `number` | month モードの 1 日あたりの幅。                                |\n| `monthDayWidth`         | `number` | 高密度な月間範囲をレンダリングする際の 1 日あたりの幅。        |\n| `quarterMonthWidth`     | `number` | quarter モードの 1 か月あたりの幅。                            |\n| `yearCellWidth`         | `number` | year モードのユニットあたりの幅。                              |\n| `yearRangePast`         | `number` | 現在の年の前の追加の年範囲。                                   |\n| `yearRangeFuture`       | `number` | 現在の年の後の追加の年範囲。                                   |\n| `overscanRows`          | `number` | ビューポート外にレンダリングされる追加の行数。                 |\n| `bufferCells`           | `number` | スムーズなスクロールのためにレンダリングされる追加の時間セル。 |\n\n### リソースの表示と同期\n\n| プロパティ                            | 型                                  | 備考                                                                    |\n| :------------------------------------ | :---------------------------------- | :---------------------------------------------------------------------- |\n| `resourceSorter`                      | `(left, right) => number`           | カスタムの行の並び替え。                                                |\n| `syncResourceVisibilityWithCalendars` | `boolean`                           | マップされたカレンダーが非表示のときにリソース行を非表示にします。      |\n| `getCalendarIdForResource`            | `(resource) => string \\| undefined` | リソースがカレンダーの表示にどのようにマップされるかを上書きします。    |\n| `onResourcesUpdate`                   | `(resources) => void`               | UI でリソースが並べ替えられたり、折りたたまれたりしたときに発生します。 |\n\nカレンダーの同期が有効な場合、フォールバックのリソースからカレンダーへのマッピングは `resource.meta?.calendarId ?? resource.id` です。\n\n## フォーカス、サイドバー、および統合\n\nタイムラインには、見落としやすい統合ポイントがいくつかあります。\n\n- `focusRequest`: ホストアプリがプログラムでタイムラインを特定のイベントや日付に移動できるようにします。\n- `sidebar`: コンテキストメニューアクションなどのスケジューラサイドバー設定を渡します。\n- `customDetailPanelContent` と `customEventDetailDialog`: カスタムのスケジューラ詳細 UI が必要な場合にビューの props を通じて渡すことができます。\n- `timeFormat`: `24h` と `12h` をサポートします。\n- `resizeTimeFormat`: イベントのサイズ変更中に表示されるラベルをカスタマイズします。\n"
  },
  {
    "path": "website/content/docs-ja/introduction/use-calendar-app.mdx",
    "content": "# useCalendarApp\n\n`useCalendarApp(config: CalendarAppConfig)` は DayFlow の中枢フックです。ビューの登録、イベントやカレンダータイプの投入、テーマ設定、コールバック登録などを 1 つのオブジェクトで完結させます。\n\n## クイックスタート\n\n```tsx {11-16}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport '@dayflow/core/dist/styles.css';\n\nexport function TeamCalendar() {\n  const calendar = useCalendarApp({\n    views: [createMonthView(), createWeekView()],\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    events: [],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 主な設定項目\n\n| オプション             | 型                          | 必須 | デフォルト                               | 説明                                                                                                                                         |\n| ---------------------- | --------------------------- | ---- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |\n| `views`                | `CalendarView[]`            | 必須 | —                                        | 表示するビューを登録。`createMonthView()` などのファクトリを使用。                                                                           |\n| `plugins`              | `CalendarPlugin[]`          | 任意 | `[]`                                     | ドラッグ操作やショートカットなどのプラグインをインストール。                                                                                 |\n| `events`               | `Event[]`                   | 任意 | `[]`                                     | 初期イベント配列。以後は `calendar.addEvent` などで更新。                                                                                    |\n| `callbacks`            | `CalendarCallbacks`         | 任意 | `{}`                                     | ビュー変更やイベント CRUD をフックするライフサイクルコールバック。                                                                           |\n| `defaultView`          | `ViewType`                  | 任意 | `ViewType.WEEK`                          | 最初に表示するビュー。`views` に含まれている必要あり。                                                                                       |\n| `initialDate`          | `Date`                      | 任意 | `new Date()`                             | フォーカスする基準日。ルーティングの状態などから差し替え可能。                                                                               |\n| `timeZone`             | `string`                    | 任意 | ユーザーのシステムタイムゾーン           | 全ビュー共通の表示 / 編集タイムゾーン。                                                                                                      |\n| `switcherMode`         | `'buttons' \\| 'select'`     | 任意 | `'buttons'`                              | ビルトインビュースイッチャーの表示方法。                                                                                                     |\n| `calendars`            | `CalendarType[]`            | 任意 | `[]`                                     | カテゴリや色の定義。`calendarId` と連携。                                                                                                    |\n| `defaultCalendar`      | `string`                    | 任意 | 最初の可視カレンダー                     | 新規イベント作成時の既定カレンダー ID。                                                                                                      |\n| `theme`                | `ThemeConfig`               | 任意 | `{ mode: 'light' }`                      | テーマモード（`light`/`dark`/`auto`）やトークン上書き。                                                                                      |\n| `locale`               | `string \\| Locale`          | 任意 | `'en-US'`                                | 国際化（i18n）のためのロケール設定。言語コード（'ja'など）またはLocaleオブジェクト。                                                         |\n| `useEventDetailDialog` | `boolean`                   | 任意 | `false`                                  | 詳細表示をモーダルダイアログに切り替える。                                                                                                   |\n| `useCalendarHeader`    | `boolean`                   | 任意 | `true`                                   | デフォルトヘッダーの有効化（`true`）または非表示（`false`）。カスタムヘッダーは `DayFlowCalendar` の `calendarHeader` スロットを使用します。 |\n| `readOnly`             | `boolean \\| ReadOnlyConfig` | 任意 | `false`                                  | 組み込みの変更 UI を無効化。真偽値、または詳細制御（ドラッグ/表示）用の設定オブジェクト。プログラム API は引き続き利用可能。                 |\n| `allDaySortComparator` | `AllDaySortComparator`      | 任意 | カレンダーグループ順（複数日終日は先頭） | 全ビューでの終日イベントの行順を制御するカスタム比較関数。指定するとデフォルト順を完全に上書きします。                                       |\n\n## それぞれのポイント\n\n### Views\n\n- 少なくとも 1 つのビューが必要です。`defaultView` が未指定の場合は週ビューが使われます。\n- ファクトリの戻り値は `{ type, component, config }` から成る `CalendarView`。自作ビューを追加することも可能です。\n\n### Events\n\n- 渡した配列がそのままメモリ上のイベントストアになります。\n- CRUD は `calendar.addEvent`, `calendar.updateEvent`, `calendar.deleteEvent` を使うと即座に UI に反映されます。\n- `Event` インターフェースは Temporal の `PlainDate` / `PlainDateTime` / `ZonedDateTime` を受け付けます。\n\n### Plugins\n\n- プラグインは `{ name, install(app) }` を持ち、初期化時に 1 度だけ実行されます。\n- ドラッグ&ドロップやキーボード操作、外部 API との橋渡しなどをここで拡張できます。\n- 複数のプラグインを同時に登録でき、それぞれ独立して動作します。\n\n```tsx\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport { createEventsPlugin } from '@dayflow/core';\n\nconst dragPlugin = createDragPlugin({\n  enableDrag: true,\n  enableResize: true,\n  enableCreate: true,\n});\n\nconst eventsPlugin = createEventsPlugin({\n  enableValidation: true,\n  maxEventsPerDay: 100,\n});\n\nconst calendar = useCalendarApp({\n  views: [createWeekView(), createMonthView()],\n  plugins: [dragPlugin, eventsPlugin],\n});\n```\n\n### Callbacks\n\n| コールバック                                             | 役割                                                                                                                                                                   |\n| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `onViewChange(view)`                                     | ビュー切り替え後に発火。アナリティクスや URL 同期に便利。                                                                                                              |\n| `onDateChange(date)`                                     | 選択日が変わったときに発火。日付連動のサイドバーなどに。                                                                                                               |\n| `onVisibleRangeChange(start, end, reason)`               | 可視日付範囲が変わるたびに発火。追加計算なしで、より正確なデータ取得に役立ちます。                                                                                     |\n| `onEventCreate / onEventUpdate / onEventDelete`          | イベント CRUD を捕捉。バックエンドと同期。                                                                                                                             |\n| `onEventDoubleClick(event, e)`                           | イベントがダブルクリックされたときに発火。`e.currentTarget` を外部ポップオーバーのアンカーに使え、`false` を返すと DayFlow の既定詳細パネル/ダイアログを抑止できます。 |\n| `onMoreEventsClick(date)`                                | 月ビューで「+ X 件」リンクがクリックされたときに発火。                                                                                                                 |\n| `onCalendarCreate / onCalendarUpdate / onCalendarDelete` | カレンダーの追加・更新・削除を捕捉。                                                                                                                                   |\n| `onCalendarMerge(sourceId, targetId)`                    | カレンダーのマージ操作を捕捉（例：「仕事」を「個人」に統合）。                                                                                                         |\n| `onRender()`                                             | レンダリング完了時に 1 度だけ発火。計測用。                                                                                                                            |\n\n### defaultView と initialDate\n\n- 単一ビューのカレンダーでは必ず `defaultView` を明示的に設定してください。\n- `initialDate` はルーティング、ユーザー設定、またはサーバーデータから取得してマルチデバイス間の一貫性を保つことができます。\n\n### timeZone\n\n- `timeZone` は Day / Week / Month / Agenda / Year 全体の主表示 / 編集タイムゾーンを定義します。\n- 未指定の場合、DayFlow はユーザーのシステムタイムゾーンを使います。\n- `timeZone` の変更は UI の再投影だけを行い、タイムゾーン変更そのものではイベントデータを書き換えません。\n- 編集系コールバックが返すのは canonical event のままです。UI 上で一時的に投影されたタイムゾーン値を、そのまま保存用データとして返すことはありません。\n- Day / Week の `secondaryTimeZone` は補助タイムライン専用で、アプリ全体の主タイムゾーンを置き換えるものではありません。\n\n### switcherMode\n\n- `'buttons'`: デスクトップ向けの水平ボタン群。\n- `'select'`: モバイルや画面幅が狭い場合に便利なセレクトボックス。\n\n### calendars & defaultCalendar\n\n- `calendars` に `id`, `name`, `colors`, `darkColors`, `isVisible` などを定義すると、イベントが `calendarId` を通じてスタイルを引き継ぎます。\n- `defaultCalendar` を設定しない場合は最初に可視状態のカレンダーが使われます。\n\n**`CalendarType` のプロパティ：**\n\n| プロパティ     | 型                       | 必須   | 説明                                                             |\n| -------------- | ------------------------ | ------ | ---------------------------------------------------------------- |\n| `id`           | `string`                 | はい   | 一意の識別子（例：`'work'`、`'personal'`）。                     |\n| `name`         | `string`                 | はい   | UI に表示される名前。                                            |\n| `colors`       | `CalendarColors`         | はい   | ライトモードのカラーセット（下表参照）。                         |\n| `darkColors`   | `CalendarColors`         | いいえ | ダークモードのカラーセット；省略時は `colors` にフォールバック。 |\n| `description`  | `string`                 | いいえ | 任意の説明文。                                                   |\n| `icon`         | `string`                 | いいえ | カレンダー名の横に表示する絵文字またはアイコン名。               |\n| `isVisible`    | `boolean`                | いいえ | このカレンダーのイベントを表示するか。デフォルト `true`。        |\n| `isDefault`    | `boolean`                | いいえ | システムデフォルトカレンダーとしてマークする。                   |\n| `readOnly`     | `boolean`                | いいえ | ドラッグ・リサイズ・編集 UI を無効化する。                       |\n| `source`       | `string`                 | いいえ | 取得元ラベル（例：`'Google Calendar'`、`'iCloud'`）。            |\n| `subscription` | `{ url, status, meta? }` | いいえ | ICS / リモートカレンダーのサブスクリプションメタデータ。         |\n\n**`CalendarColors` のプロパティ：**\n\n| プロパティ           | 型       | 説明                               |\n| -------------------- | -------- | ---------------------------------- |\n| `eventColor`         | `string` | イベントの背景色（通常は半透明）。 |\n| `eventSelectedColor` | `string` | 選択時のイベント背景色。           |\n| `lineColor`          | `string` | アクセント / ボーダーカラー。      |\n| `textColor`          | `string` | イベントのテキストカラー。         |\n\n### theme\n\n```tsx\nconst calendar = useCalendarApp({\n  theme: {\n    mode: 'dark', // 'light' | 'dark' | 'auto'\n  },\n});\n\n// 現在のテーマを取得\nconst currentTheme = calendar.app.getTheme();\ncalendar.app.setTheme('light');\ncalendar.app.subscribeThemeChange(theme => console.log(theme));\n```\n\n各カレンダータイプには `colors` と `darkColors` を用意しておくと、ライト/ダークの切替時も自然な配色になります。詳細は [ダークモードガイド](/docs-ja/features/dark-mode) へ。\n\n### locale\n\n`locale` オプションでカレンダーの言語と地域設定を行います。\n\n- **文字列:** `'en-US'`, `'ja'`, `'zh'`, `'de'`, `'fr'`, `'es'`, `'ko'` などの言語コードを指定します。\n- **Locale オブジェクト:** インポートした `Locale` オブジェクト（型安全性のため）またはカスタムオブジェクト（未サポート言語用）を渡します。\n\n```tsx\n// 言語コード\nconst calendar = useCalendarApp({\n  locale: 'ja',\n});\n\n// ローカライズプラグインが提供する Locale オブジェクト\nimport { ja } from '@dayflow/plugin-localization';\nconst calendar = useCalendarApp({\n  locale: ja,\n});\n\n// カスタム Locale オブジェクト\nconst customLocale = {\n  code: 'it',\n  messages: { today: 'Oggi', ... }\n};\nconst calendar = useCalendarApp({\n  locale: customLocale,\n});\n```\n\n### useEventDetailDialog\n\n- `true`：デフォルトのモーダルダイアログ（`DefaultEventDetailDialog`）を有効にします。\n- `DayFlowCalendar` の `eventDetailContent` または `eventDetailDialog` と組み合わせると、内部のステートマシンを保ちながら UI を差し替えられます。\n\n### useCalendarHeader\n\n- `true`（デフォルト）：組み込みカレンダーヘッダーを表示します。\n- `false`：ヘッダーを完全に非表示にします。\n\nカスタムヘッダーをレンダリングするには、`DayFlowCalendar` の `calendarHeader` スロットを使用してください。詳細は [Calendar Header](/docs-ja/features/calendar-header) を参照してください。\n\n### readOnly\n\n- `true`：組み込みの変更 UI（ドラッグ、作成、編集）を無効にします。\n- `ReadOnlyConfig`：詳細制御：\n  - `draggable`：ドラッグ操作を許可するかどうか。\n  - `viewable`：イベント詳細の表示を許可するかどうか。\n- `calendar.addEvent()`、`calendar.updateEvent()`、`calendar.deleteEvent()`、`calendar.applyEventsChanges()` などのプログラム API は、読み取り専用モードでも利用できます。\n- カスタム UI では `calendar.canMutateFromUI()` を使って、作成・編集・削除操作を表示してよいか判定してください。\n- 詳細は [読み取り専用モード](/docs-ja/features/read-only) を参照してください。\n\n### allDaySortComparator\n\n全ビュー（日・週・月・Agenda・年）での終日イベントの行順を制御します。\n\nデフォルトでは DayFlow が次のルールで並べます：\n\n- `calendarId` の初出現順でグループ化する\n- 複数日にまたがる終日イベントを単日終日イベントより上に配置する\n- 同じカレンダーの終日イベントはできるだけ隣接した順序を保つ\n\n`allDaySortComparator` は、最終的な順序を完全に自分で決めたい場合にだけ渡してください。指定した場合は、その比較結果がそのまま最終順序として使われます。\n\n比較関数を渡すと完全に制御できます — 2 つの `Event` オブジェクトを受け取り、JavaScript の `Array.sort` と同じ動作です：\n\n```tsx\nconst calendar = useCalendarApp({\n  allDaySortComparator: (a, b) => a.title.localeCompare(b.title),\n});\n```\n\n**そのまま使えるエクスポート済みヘルパー**（`@dayflow/core` からエクスポート）：\n\n| ヘルパー            | 動作                                                                                     |\n| :------------------ | :--------------------------------------------------------------------------------------- |\n| `sortAllDayByTitle` | タイトルのアルファベット順で終日イベントを並べる。デフォルトのグループ順は適用されません |\n\n```tsx\nimport { sortAllDayByTitle } from '@dayflow/core';\n\nconst calendar = useCalendarApp({\n  allDaySortComparator: sortAllDayByTitle,\n});\n```\n\n`updateConfig` を使うとランタイムで変更できます：\n\n```tsx\ncalendar.app.updateConfig({ allDaySortComparator: sortAllDayByTitle });\n\n// デフォルトのカレンダーグループ順に戻す\ncalendar.app.updateConfig({ allDaySortComparator: undefined });\n```\n\n## 応用的な設定例\n\n```tsx\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\nimport '@dayflow/core/dist/styles.css';\n\nconst calendars = [\n  {\n    id: 'work',\n    name: '仕事',\n    colors: {\n      eventColor: '#2563eb',\n      eventSelectedColor: '#1d4ed8',\n      lineColor: '#1e40af',\n      textColor: '#ffffff',\n    },\n  },\n  {\n    id: 'personal',\n    name: 'プライベート',\n    colors: {\n      eventColor: '#f97316',\n      eventSelectedColor: '#ea580c',\n      lineColor: '#c2410c',\n      textColor: '#ffffff',\n    },\n  },\n];\n\nexport function AdvancedCalendar() {\n  const calendar = useCalendarApp({\n    views: [createMonthView(), createWeekView()],\n    defaultView: ViewType.WEEK,\n    initialDate: new Date('2024-10-01'),\n    events: [],\n    calendars,\n    defaultCalendar: 'work',\n    switcherMode: 'select',\n    callbacks: {\n      onEventUpdate: event => api.events.update(event),\n      onVisibleRangeChange: (start, end, reason) =>\n        preloadVisibleRange(start, end, reason),\n    },\n    plugins: [\n      createSidebarPlugin({\n        width: 280,\n        initialCollapsed: false,\n      }),\n    ],\n    useEventDetailDialog: true,\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n> ヒント：`useCalendarApp` が返すインスタンスをカレンダー本体・ヘッダー・サイドバーなど複数のコンポーネントで共有すると、状態が常に同期されたまま UI を構築できます。\n\n## 関連ドキュメント\n\n- [DayFlowCalendar](/docs-ja/introduction/dayflow-calendar) — メイン UI コンポーネント\n- [クイックスタート](/docs-ja/introduction) — セットアップガイド\n- [イベント](/docs-ja/introduction/events) — イベント管理\n- [ビュー](/docs-ja/introduction/views) — 利用可能なビュー\n"
  },
  {
    "path": "website/content/docs-ja/introduction/views.mdx",
    "content": "# ビュー\n\nDay Flow は 5 つの異なるビュータイプをサポートしており、それぞれが異なるユースケースやユーザーのニーズに合わせて最適化されています。プログラムでビューを切り替えたり、ユーザーがビューを切り替えるための UI を提供したりすることができます。\n\n## 利用可能なビュー\n\n### 日ビュー\n\n日ビューは単一の日に焦点を当て、詳細な時間枠を表示します。毎日のスケジュールの管理に最適です。\n\n#### 設定\n\n| プロパティ            | 型               | デフォルト  | 説明                                               |\n| :-------------------- | :--------------- | :---------- | :------------------------------------------------- |\n| `showAllDay`          | `boolean`        | `true`      | 終日イベント行を表示するかどうか。                 |\n| `scrollToCurrentTime` | `boolean`        | `true`      | 初回ロード時に現在時刻までスクロールするかどうか。 |\n| `timeFormat`          | `'12h' \\| '24h'` | `'24h'`     | 時間軸の時間形式。                                 |\n| `secondaryTimeZone`   | `string`         | `undefined` | 日ビューだけに表示される補助タイムライン。         |\n| `hourHeight`          | `number`         | `72`        | 時間グリッドの 1 時間あたりの行の高さ（px）。      |\n\n```tsx {10}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createDayView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [createDayView()],\n    defaultView: ViewType.DAY,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### 週ビュー\n\n週ビューは、時間枠を持つ 7 日間のグリッドを表示し、イベントを正確な開始時間と終了時間で表示します。\n\n#### 設定\n\n| プロパティ            | 型                                                   | デフォルト       | 説明                                                                                                                                                   |\n| :-------------------- | :--------------------------------------------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `showWeekends`        | `boolean`                                            | `true`           | 土日を表示するかどうか。                                                                                                                               |\n| `showAllDay`          | `boolean`                                            | `true`           | 終日イベント行を表示するかどうか。                                                                                                                     |\n| `startOfWeek`         | `number`                                             | `1`              | 週の開始日（0 は日曜日、1 は月曜日など）。                                                                                                             |\n| `scrollToCurrentTime` | `boolean`                                            | `true`           | 初回ロード時に現在時刻までスクロールするかどうか。                                                                                                     |\n| `timeFormat`          | `'12h' \\| '24h'`                                     | `'24h'`          | 時間軸の時間形式。                                                                                                                                     |\n| `secondaryTimeZone`   | `string`                                             | `undefined`      | 週ビューだけに表示される補助タイムライン。                                                                                                             |\n| `gridDateClick`       | `'day-view' \\| 'none' \\| function`                   | `undefined`      | 日付セルをクリックした際のアクション。                                                                                                                 |\n| `gridDateDoubleClick` | `'create-event' \\| 'day-view' \\| 'none' \\| function` | `'create-event'` | 日付セルをダブルクリックした際のアクション。`'create-event'` (デフォルト) を指定すると、クリックした位置（時間）に 1 時間の Timed Event を作成します。 |\n| `hourHeight`          | `number`                                             | `72`             | 時間グリッドの 1 時間あたりの行の高さ（px）。                                                                                                          |\n\n```tsx {10}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [createWeekView()],\n    defaultView: ViewType.WEEK,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### 月ビュー\n\n月ビューは、1 か月のすべての金を表示する伝統的なカレンダーグリッドを表示します。\n\n#### 設定\n\n| プロパティ            | 型                                                                  | デフォルト       | 説明                                                                                                                                                     |\n| :-------------------- | :------------------------------------------------------------------ | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `showWeekNumbers`     | `boolean`                                                           | `false`          | 週番号を表示するかどうか。                                                                                                                               |\n| `showMonthIndicator`  | `boolean`                                                           | `true`           | スクロール時に月を示すタイトルを表示するかどうか。                                                                                                       |\n| `startOfWeek`         | `number`                                                            | `1`              | 週の開始曜日（0 は日曜日、1 は月曜日など）。                                                                                                             |\n| `snapToMonth`         | `boolean`                                                           | `false`          | 有効にすると、スクロールが止まった後、表示中のメイン月の先頭に自動的にスナップします。                                                                   |\n| `gridDateClick`       | `'week-view' \\| 'day-view' \\| 'none' \\| function`                   | `undefined`      | 日付セルをクリックした際のアクション。                                                                                                                   |\n| `gridDateDoubleClick` | `'create-event' \\| 'week-view' \\| 'day-view' \\| 'none' \\| function` | `'create-event'` | 日付セルをダブルクリックした際のアクション。`'create-event'` (デフォルト) を指定すると、クリックした日付に 9:00 から 10:00 の Timed Event を作成します。 |\n| `eventHeight`         | `number`                                                            | `16`             | 月ビューグリッド内の各イベント行の高さ（px）。                                                                                                           |\n| `scroll`              | `MonthScrollConfig`                                                 | —                | スクロールのロックと月切替アニメーションを制御します。以下を参照してください。                                                                           |\n\n#### MonthScrollConfig\n\n| プロパティ   | 型                      | デフォルト  | 説明                                                                                                                                                                                                                           |\n| :----------- | :---------------------- | :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `disabled`   | `boolean`               | `false`     | 連続スクロールを無効化します。月の切替は Prev / Next ボタンのみで行えます。                                                                                                                                                    |\n| `transition` | `'fade'` \\| `undefined` | `undefined` | 月切替時のアニメーション（`disabled: true` の場合のみ適用）。`'fade'` を指定すると、現在の月がフェードアウトしながら横にスライドし、次の月がフェードインします。省略すると元のスムーズスクロールアニメーションが維持されます。 |\n\n```tsx\ncreateMonthView({\n  scroll: {\n    disabled: true, // 1ヶ月単位に固定、Prev/Next ボタンのみ\n    transition: 'fade', // フェードアニメーション（省略するとスムーズスクロール）\n  },\n});\n```\n\n```tsx {10}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [createMonthView()],\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### Agenda ビュー\n\nAgenda ビューは、イベントを日ごとにグルーピングした時系列リストとして表示します。モバイルレイアウト、少し先までの予定確認、今後のイベントを素早く流し見したいケースに向いています。\n\n#### 設定\n\n| プロパティ            | 型                                 | デフォルト   | 説明                                               |\n| :-------------------- | :--------------------------------- | :----------- | :------------------------------------------------- |\n| `daysToShow`          | `number`                           | `14`         | Agenda リストに表示する連続日数。                  |\n| `showEmptyDays`       | `boolean`                          | `true`       | イベントがない日もリストに残すかどうか。           |\n| `timeFormat`          | `'12h' \\| '24h'`                   | `'24h'`      | 時間指定イベント行に使う時刻表示形式。             |\n| `gridDateClick`       | `'day-view' \\| 'none' \\| function` | `undefined`  | 日セクションをクリックしたときのアクション。       |\n| `gridDateDoubleClick` | `'day-view' \\| 'none' \\| function` | `'day-view'` | 日セクションをダブルクリックしたときのアクション。 |\n\n```tsx {10-15}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createAgendaView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      createAgendaView({\n        daysToShow: 14,\n        timeFormat: '12h',\n        gridDateDoubleClick: 'day-view',\n      }),\n    ],\n    defaultView: ViewType.AGENDA,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### 年ビュー\n\n年ビューは、包括的な年間概要を提供します。DayFlow の年ビューは、`year-canvas`（連続グリッド）、`fixed-week`（固定 52 週間の列）、および `grid`（ヒートマップに最適な 4x3 の月間グリッド）の 3 つのモードをサポートしています。\n\n#### 設定\n\n| プロパティ                  | 型                                                   | デフォルト       | 説明                                                                                                                                                                   |\n| :-------------------------- | :--------------------------------------------------- | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `mode`                      | `'year-canvas' \\| 'fixed-week' \\| 'grid'`            | `'year-canvas'`  | 年ビューの表示モード。                                                                                                                                                 |\n| `startOfWeek`               | `number`                                             | `1`              | 週の開始曜日（0 は日曜日、1 は月曜日など）。`fixed-week` および `grid` モードで使用されます。                                                                          |\n| `showTimedEventsInYearView` | `boolean`                                            | `false`          | 年ビューに時間指定イベント（ドット/インジケーター/ヒートマップ強度）を表示するかどうか。                                                                               |\n| `gridDateClick`             | `'popup' \\| 'day-view' \\| 'none' \\| function`        | `'popup'`        | (Grid モード) 日付セルをクリックした際のアクション。                                                                                                                   |\n| `gridDateDoubleClick`       | `'create-event' \\| 'day-view' \\| 'none' \\| function` | `'create-event'` | (Grid モード) 日付セルをダブルクリックした際のアクション。`'create-event'` (デフォルト) を指定すると、クリックした日付に 9:00 から 10:00 の Timed Event を作成します。 |\n| `gridPopupContent`          | `function`                                           | `undefined`      | (Grid モード) 日付ポップアップコンテンツ用のカスタムレンダラー。                                                                                                       |\n| `gridHeatmapLevels`         | `number`                                             | `5`              | (Grid モード) 使用するヒートマップの強度の段階数。                                                                                                                     |\n\n#### ヒートマップのカスタマイズ (Grid モード)\n\n`mode: 'grid'` を使用する場合、カレンダーはイベントの数によって各日の強度（色）が決定されるヒートマップとして機能します。グローバル CSS で以下の CSS 変数を上書きすることで、色をカスタマイズできます。\n\n```css\n/* ライトモードのヒートマップ */\n.df-year-grid-month {\n  --heat-1: #ebf5ff;\n  --heat-2: #cfe8ff;\n  --heat-3: #91d5ff;\n  --heat-4: #60a5fa;\n  --heat-5: #3b82f6;\n}\n\n/* ダークモードのヒートマップ */\n.dark .df-year-grid-month {\n  --heat-1: #1e3a5f;\n  --heat-2: #2563eb;\n  --heat-3: #1e40af;\n  --heat-4: #3b82f6;\n  --heat-5: #93c5fd;\n}\n```\n\n`gridHeatmapLevels` を別の数値（例: `3`）に変更した場合は、その数値までの変数（例: `--heat-1` から `--heat-3`）を提供する必要があります。\n\n```tsx {10-15}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createYearView,\n  ViewType,\n} from '@dayflow/react';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      createYearView({\n        mode: 'grid',\n        showTimedEventsInYearView: true,\n        gridHeatmapLevels: 5,\n      }),\n    ],\n    defaultView: ViewType.YEAR,\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 複数ビューの使用\n\n複数のビューを登録して、ユーザーがそれらを切り替えられるようにすることができます。\n\n```tsx {13-18}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createAgendaView,\n  createDayView,\n  createWeekView,\n  createMonthView,\n  createYearView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MultiViewCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      createAgendaView({ daysToShow: 10 }),\n      createDayView(),\n      createWeekView(),\n      createMonthView(),\n      createYearView(),\n    ],\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    callbacks: {\n      onViewChange: view => {\n        console.log('View changed to:', view);\n      },\n    },\n  });\n\n  return (\n    <div className='h-screen'>\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n}\n```\n\n## API によるビュー制御\n\n### ビューの変更\n\n```tsx\n// 特定のビューに変更\ncalendar.changeView(ViewType.WEEK);\n\n// 現在のビューを取得\nconst currentView = calendar.app.getCurrentView();\nconsole.log(currentView.type); // 'week'\n```\n\n### ナビゲーション\n\nすべてのビューは同じナビゲーションメソッドをサポートしています。\n\n```tsx\n// 今日へ移動\ncalendar.goToToday();\n\n// 前の期間へ移動（現在のビューに応じて日/週/月/agenda/年）\ncalendar.goToPrevious();\n\n// 次の期間へ移動\ncalendar.goToNext();\n\n// 特定の日付を選択\ncalendar.selectDate(new Date(2024, 9, 15));\n```\n\n## ViewType 列挙体\n\n```tsx\nenum ViewType {\n  DAY = 'day',\n  WEEK = 'week',\n  MONTH = 'month',\n  AGENDA = 'agenda',\n  YEAR = 'year',\n}\n```\n"
  },
  {
    "path": "website/content/docs-ja/meta.json",
    "content": "{\n  \"title\": \"ドキュメント\",\n  \"pages\": [\"introduction\", \"plugins\", \"features\", \"guides\", \"ui\"]\n}\n"
  },
  {
    "path": "website/content/docs-ja/plugins/drag.mdx",
    "content": "# ドラッグ＆ドロップ プラグイン\n\nドラッグ＆ドロッププラグインは、カレンダーグリッド上で直接イベントを移動、リサイズ、作成できるインタラクティブなイベント管理機能を提供します。\n\n## インストール\n\nお好みのパッケージマネージャーを使用してプラグインをインストールします：\n\n<PackageTabs pkg='@dayflow/plugin-drag' />\n\n```tsx\nimport { createDragPlugin } from '@dayflow/plugin-drag';\n```\n\n## 使い方\n\n```tsx\nimport { useCalendarApp, ViewType } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createDragPlugin } from '@dayflow/plugin-drag';\n\nfunction MyCalendar() {\n  const dragPlugin = createDragPlugin({\n    enableDrag: true,\n    enableResize: true,\n    enableCreate: true,\n    onEventDrop: (updated, original) => {\n      console.log('Event moved:', updated);\n    },\n    onEventResize: (updated, original) => {\n      console.log('Event resized:', updated);\n    },\n  });\n\n  const calendar = useCalendarApp({\n    // ... ビューの設定\n    plugins: [dragPlugin],\n  });\n\n  // ...\n}\n```\n\n## 設定項目\n\n| プロパティ           | 型           | デフォルト値               | 説明                                                           |\n| :------------------- | :----------- | :------------------------- | :------------------------------------------------------------- |\n| `enableDrag`         | `boolean`    | `true`                     | ドラッグによるイベントの移動を許可するかどうか。               |\n| `enableResize`       | `boolean`    | `true`                     | ドラッグによるイベント期間の変更を許可するかどうか。           |\n| `enableCreate`       | `boolean`    | `true`                     | グリッドのダブルクリックによるイベント作成を許可するかどうか。 |\n| `enableAllDayCreate` | `boolean`    | `true`                     | 終日行でのドラッグによる終日イベントの作成を許可するかどうか。 |\n| `supportedViews`     | `ViewType[]` | `[DAY, WEEK, MONTH, YEAR]` | ドラッグ機能が有効になるビューのリスト。                       |\n| `onEventDrop`        | `Function`   | -                          | イベントが移動（ドラッグ＆ドロップ）された時のコールバック。   |\n| `onEventResize`      | `Function`   | -                          | イベントがリサイズされた時のコールバック。                     |\n\n## プラグイン API\n\nドラッグサービスにアクセスして、設定を動的に更新できます：\n\n```tsx\nconst dragService = calendar.app.getPlugin<DragService>('drag');\n\n// 動的にリサイズ機能を無効にする\ndragService.updateConfig({ enableResize: false });\n```\n"
  },
  {
    "path": "website/content/docs-ja/plugins/events.mdx",
    "content": "# イベントサービス プラグイン\n\nイベントサービスプラグインは、バリデーション、フィルタリング、複雑なクエリなど、高度なイベント管理機能を提供します。\n\n## インストール\n\nイベントサービスプラグインは現在、`@dayflow/core` パッケージに含まれています。\n\n```tsx\nimport { createEventsPlugin } from '@dayflow/core';\n```\n\n## 使い方\n\n```tsx\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createEventsPlugin,\n} from '@dayflow/react';\n\nfunction MyCalendar() {\n  const eventsPlugin = createEventsPlugin({\n    enableValidation: true,\n    maxEventsPerDay: 50,\n  });\n\n  const calendar = useCalendarApp({\n    // ... ビューの設定\n    plugins: [eventsPlugin],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 設定項目\n\n| プロパティ              | 型        | デフォルト値 | 説明                                                               |\n| :---------------------- | :-------- | :----------- | :----------------------------------------------------------------- |\n| `enableAutoRecalculate` | `boolean` | `true`       | 日付が変更されたときにイベントのセグメントを自動的に再計算します。 |\n| `enableValidation`      | `boolean` | `true`       | 追加または更新前にイベントオブジェクトをバリデーションします。     |\n| `maxEventsPerDay`       | `number`  | `50`         | 1日あたりの最大イベント数。                                        |\n\n## プラグイン API\n\n`EventsService` にアクセスして、高度なクエリを実行します：\n\n```tsx\nconst eventsService = calendar.app.getPlugin<EventsService>('events');\n\n// 特定の日付のイベントを取得\nconst events = eventsService.getByDate(new Date());\n\n// イベントをフィルタリング\nconst workEvents = eventsService.filterEvents(\n  allEvents,\n  e => e.calendarId === 'work'\n);\n```\n\n### 利用可能なメソッド\n\n- `getAll()`: すべてのイベントを取得します。\n- `getById(id)`: 特定のイベントを検索します。\n- `getByDate(date)`: 特定の日に発生するイベントを取得します。\n- `getByDateRange(start, end)`: 指定した範囲内のイベントを取得します。\n- `validateEvent(event)`: バリデーションエラーの配列を返します。\n"
  },
  {
    "path": "website/content/docs-ja/plugins/keyboard-shortcuts.mdx",
    "content": "# キーボードショートカットプラグイン\n\nキーボードショートカット (Keyboard Shortcuts) プラグインは、ナビゲーション、イベント管理、およびクリップボード操作のためのグローバルなキーボードコントロールを可能にします。\n\n## インストール\n\nプラグインパッケージをインストールします：\n\n<PackageTabs pkg='@dayflow/plugin-keyboard-shortcuts' />\n\n## 使い方\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react';\nimport { createKeyboardShortcutsPlugin } from '@dayflow/plugin-keyboard-shortcuts';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    // ...\n    plugins: [\n      createKeyboardShortcutsPlugin({\n        // オプション設定\n      }),\n    ],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## デフォルトのショートカット\n\n| 操作                          | キー (Mac)                    | キー (Windows/Linux)       |\n| :---------------------------- | :---------------------------- | :------------------------- |\n| **今日に移動**                | `Cmd + T`                     | `Ctrl + T`                 |\n| **検索**                      | `Cmd + F`                     | `Ctrl + F`                 |\n| **新規イベント**              | `Cmd + N`                     | `Ctrl + N`                 |\n| **ナビゲーション**            | `ArrowLeft` / `ArrowRight`    | `ArrowLeft` / `ArrowRight` |\n| **イベントのタブ移動**        | `Tab` / `Shift + Tab`         | `Tab` / `Shift + Tab`      |\n| **元に戻す**                  | `Cmd + Z`                     | `Ctrl + Z`                 |\n| **やり直し**                  | `Cmd + Shift + Z` / `Cmd + Y` | `Ctrl + Y`                 |\n| **イベントをコピー**          | `Cmd + C`                     | `Ctrl + C`                 |\n| **イベントを切り取り**        | `Cmd + X`                     | `Ctrl + X`                 |\n| **イベントを貼り付け**        | `Cmd + V`                     | `Ctrl + V`                 |\n| **イベントを削除**            | `Backspace` / `Delete`        | `Backspace` / `Delete`     |\n| **ダイアログ/パネルを閉じる** | `Esc`                         | `Esc`                      |\n\n## 設定\n\nキーマッピングをカスタマイズしたり、各アクションにカスタムコールバックを提供したりできます：\n\n```tsx\ncreateKeyboardShortcutsPlugin({\n  keyMap: {\n    today: 't',\n    search: 'f',\n    prev: 'ArrowLeft',\n    next: 'ArrowRight',\n    undo: 'z',\n    redo: 'y',\n    delete: 'Delete',\n    newEvent: 'n',\n  },\n  callbacks: {\n    undo: app => {\n      console.log('カスタムの元に戻すロジック');\n      app.undo();\n    },\n    redo: app => {\n      console.log('カスタムのやり直しロジック');\n      if ((app as any).redo) (app as any).redo();\n    },\n    delete: app => {\n      if (confirm('本当に削除しますか？')) {\n        const selectedId = app.state.selectedEventId;\n        if (selectedId) app.deleteEvent(selectedId);\n      }\n    },\n  },\n});\n```\n\n### 利用可能なコールバック\n\n`callbacks` オブジェクトでは以下のコールバックがサポートされています：\n\n- `undo`, `redo`, `paste` (`app` を受け取ります)\n- `copy`, `cut`, `delete` (`app` と `event?: Event` を受け取ります)\n- `today`, `search`, `prev`, `next`, `newEvent`, `dismiss` (`app` を受け取ります)\n- `tab` (`app` と `reverse: boolean` を受け取ります)\n\n## プラグイン API\n\nプラグインハンドルを取得して、実行時にショートカットの動作を制御できます：\n\n```tsx\nimport { type KeyboardShortcutsService } from '@dayflow/plugin-keyboard-shortcuts';\n\nconst kb = app.getPlugin<KeyboardShortcutsService>('keyboard-shortcuts');\n```\n\n### KeyboardShortcutsService\n\n| メソッド      | 返り値    | 説明                                                |\n| :------------ | :-------- | :-------------------------------------------------- |\n| `enable()`    | `void`    | ショートカット処理を再有効化する                    |\n| `disable()`   | `void`    | `enable()` が呼ばれるまで全ショートカットを抑制する |\n| `isEnabled()` | `boolean` | ショートカットが現在有効かどうかを返す              |\n\nよくある使用例として、カスタムモーダルやリッチテキストエディタが開いている間、カレンダーのショートカットを一時的に抑制する場合があります：\n\n```tsx\nfunction MyModal() {\n  const kb = app.getPlugin<KeyboardShortcutsService>('keyboard-shortcuts');\n\n  useEffect(() => {\n    kb?.disable();\n    return () => kb?.enable(); // アンマウント時に復元\n  }, []);\n\n  // ...\n}\n```\n\nプログラムから開いている UI を閉じることもできます：\n\n```tsx\napp.dismissUI();\n```\n"
  },
  {
    "path": "website/content/docs-ja/plugins/localization.mdx",
    "content": "# i18n プラグイン\n\ni18n プラグインは、多言語およびローカライズのサポートを提供します。デフォルトでは、Day Flow のコアライブラリはバンドルサイズを最小限に抑えるため、英語 (`en-US`) ののみが含まれています。\n\n## インストール\n\nローカライズプラグインパッケージをインストールします：\n\n<PackageTabs pkg='@dayflow/plugin-localization' />\n\n## 使い方\n\n他の言語のサポートを有効にするには、プラグインを登録し、必要なロケールオブジェクトを提供します。\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react';\nimport {\n  createLocalizationPlugin,\n  zh,\n  ja,\n  fr,\n} from '@dayflow/plugin-localization';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      /* ビューの設定 */\n    ],\n    plugins: [\n      createLocalizationPlugin({\n        locales: [zh, ja, fr], // 必要な言語を登録\n      }),\n    ],\n    locale: 'ja-JP', // 現在のロケールを設定\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 設定\n\nプラグインは、グローバルロケールレジストリに登録する `Locale` オブジェクトのリストを受け取ります。\n\n```tsx\ninterface LocalizationConfig {\n  locales: Locale[];\n}\n```\n\n## 利用可能なロケール\n\n現在、`@dayflow/plugin-localization` パッケージでは以下のロケールが利用可能です：\n\n| エクスポート | 言語       | ロケールコード           |\n| :----------- | :--------- | :----------------------- |\n| `zh`         | 中国語     | `zh-CN`                  |\n| `ja`         | 日本語     | `ja-JP`                  |\n| `ko`         | 韓国語     | `ko-KR`                  |\n| `fr`         | フランス語 | `fr-FR`                  |\n| `de`         | ドイツ語   | `de-DE`                  |\n| `es`         | スペイン語 | `es-ES`                  |\n| `en`         | 英語       | `en-US` (コアに内蔵済み) |\n\n## カスタムロケール\n\nプラグインを使用せずに、またはプラグインに渡すことで、独自のカスタムロケールを登録することもできます：\n\n```tsx\nconst myCustomLocale = {\n  code: 'pt-BR',\n  messages: {\n    today: 'Hoje',\n    day: 'Dia',\n    week: 'Semana',\n    month: 'Mês',\n    // ... 他のすべての翻訳キー\n  },\n};\n\nconst calendar = useCalendarApp({\n  plugins: [\n    createLocalizationPlugin({\n      locales: [myCustomLocale],\n    }),\n  ],\n  locale: 'pt-BR',\n});\n```\n"
  },
  {
    "path": "website/content/docs-ja/plugins/meta.json",
    "content": "{\n  \"title\": \"プラグイン\",\n  \"defaultOpen\": true,\n  \"pages\": [\n    \"overview\",\n    \"localization\",\n    \"drag\",\n    \"print\",\n    \"events\",\n    \"keyboard-shortcuts\",\n    \"sidebar\"\n  ]\n}\n"
  },
  {
    "path": "website/content/docs-ja/plugins/overview.mdx",
    "content": "# プラグインの概要\n\nDay Flow はプラグインアーキテクチャを採用しており、必要に応じて機能を拡張できます。プラグインはモジュール化されており、有効化、無効化、または詳細な設定が可能です。\n\n## なぜプラグインを使うのか？\n\nプラグインシステムには以下のメリットがあります：\n\n- **オンデマンド読み込み** - 必要な機能だけをロードできます（例：デフォルトでは英語のみを含めることでバンドルサイズを削減）。\n- **動作のカスタマイズ** - 特定のユースケースに合わせてプラグインの設定を調整できます。\n- **機能拡張** - 独自のニーズに合わせてカスタムプラグインを作成できます。\n- **プラグイン API へのアクセス** - 業務ロジック内でプラグインのサービスを直接利用できます。\n\n## 利用可能なプラグイン\n\nDay Flow では、以下の公式プラグインを提供しています：\n\n### i18n\n\n多言語サポートを提供します。英語以外のロケールをプラグインに切り出すことで、英語のみを使用するアプリケーションのコアバンドルサイズを小さく保つことができます。[詳細はこちら](../localization)\n\n### ドラッグ＆ドロップ (Drag & Drop)\n\nイベントの移動、リサイズ、ダブルクリックによる新規作成など、インタラクティブなイベント管理を可能にします。[詳細はこちら](../drag)\n\n### イベントサービス (Events Service)\n\nバリデーション、フィルタリング、日付範囲クエリなど、高度なイベント管理機能を提供します。[詳細はこちら](../events)\n\n---\n\n## プラグインのライフサイクル\n\n1. **プラグインの作成** - 工場関数（例：`createDragPlugin()`）を呼び出して設定を行います。\n2. **プラグインの登録** - カレンダーの初期化時に、インスタンスを `plugins` 配列に追加します。\n3. **インストール** - `install()` 関数が自動的に呼び出され、`CalendarApp` インスタンスが渡されます。\n4. **利用** - `app.getPlugin('plugin-name')` を介してプラグイン API にアクセスします。\n\n## カスタムプラグインの作成\n\n`CalendarPlugin` インターフェースを実装することで、独自のプラグインを作成できます：\n\n```tsx\nimport { CalendarPlugin, ICalendarApp } from '@dayflow/core';\n\nexport const myPlugin: CalendarPlugin = {\n  name: 'my-custom-plugin',\n  install(app: ICalendarApp) {\n    console.log('プラグインがインストールされました！');\n    // ここでアプリの機能を拡張します\n  },\n};\n```\n"
  },
  {
    "path": "website/content/docs-ja/plugins/print.mdx",
    "content": "---\ntitle: 印刷プラグイン\ndescription: プレビュー、用紙サイズ設定、カレンダーフィルタリング、および複数の印刷用レイアウトを備えた DayFlow Pro の印刷およびエクスポートワークフロー。\nstatus: pro\n---\n\n# 印刷プラグイン (Print Plugin)\n\n`@dayflow-pro/plugin-print` は、DayFlow に組み込みの印刷ダイアログとプレビューフローを追加します。日、週、月、および 2 つの年間レイアウトをサポートし、ユーザーは用紙サイズや向きを選択できます。コンテキストに応じて、現在表示されているイベントセットまたは一致するすべてのイベントを印刷できます。\n\n[ライブデモ](https://pro.dayflow.studio)を試す\n\n## インストール\n\n<PackageTabs pkg='@dayflow-pro/plugin-print' />\n\nDayFlow Pro のインストール手順については、[Pro のインストール](/docs/introduction/pro-installation)を参照してください。\n\n## 基本的な使い方\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { createPrintPlugin } from '@dayflow-pro/plugin-print';\n\nfunction App() {\n  const printPlugin = createPrintPlugin({\n    defaultOptions: {\n      miniCalendar: true,\n      calendarKeys: true,\n      textSize: 'medium',\n    },\n  });\n\n  const calendar = useCalendarApp({\n    plugins: [printPlugin],\n  });\n\n  return (\n    <>\n      <button onClick={() => printPlugin.api.open()}>印刷</button>\n      <DayFlowCalendar calendar={calendar} />\n    </>\n  );\n}\n```\n\nこのプラグインは、カレンダーアプリに 2 つのヘルパーを直接インストールします。\n\n```tsx\nawait calendar.openPrintDialog();\nawait calendar.print();\n```\n\n## プラグインが提供するもの\n\n- `document.body` にレンダリングされる印刷プレビューダイアログ\n- `window.print()` を介したブラウザ標準の印刷\n- 日、週、月、および年間レイアウトのビュー選択\n- `calendarIds` によるカレンダーレベルのフィルタリング\n- 用紙サイズと向きの制御\n- プラグインがインストールされている間の Cmd/Ctrl + P ショートカットのサポート\n\n## サポートされている印刷ビュー\n\n| ビュー            | 備考                                                         |\n| :---------------- | :----------------------------------------------------------- |\n| `day`             | 選択した 1 日または複数日を印刷します。                      |\n| `week`            | 週間スタイルのレイアウトを印刷します。                       |\n| `month`           | 月間グリッドを印刷します。                                   |\n| `year-fixed-week` | 固定された週ごとの行を使用した年間概要。デフォルトは横向き。 |\n| `year-canvas`     | 縦向き出力に最適化された高密度な年間概要。                   |\n\nダイアログが開くと、現在の DayFlow ビューから初期ビューが派生します。\n\n- 現在が月表示 -> `month`\n- 現在が週表示 -> `week`\n- 現在が日表示 -> `day`\n- 現在が年表示 -> `year-fixed-week`\n\n## プラグインの設定\n\n| プロパティ       | 型                              | デフォルト | 備考                                                     |\n| :--------------- | :------------------------------ | :--------- | :------------------------------------------------------- |\n| `enabled`        | `boolean`                       | `true`     | `false` に設定すると、インストールを完全に無効にします。 |\n| `defaultOptions` | `Partial<CalendarPrintOptions>` | —          | カスタム印刷オプションでダイアログをプリロードします。   |\n| `license`        | `PackageLicenseConfig`          | —          | グローバル登録を使用しない場合のオプションの上書き。     |\n\n## CalendarPrintOptions\n\n| プロパティ      | 型                               | デフォルト | 備考                                                 |\n| :-------------- | :------------------------------- | :--------- | :--------------------------------------------------- |\n| `allDayEvents`  | `boolean`                        | `true`     | 終日イベントを含めます。                             |\n| `timedEvents`   | `boolean`                        | `true`     | 時間指定イベントを含めます。                         |\n| `miniCalendar`  | `boolean`                        | `true`     | 印刷ヘッダーにミニカレンダーを表示します。           |\n| `calendarKeys`  | `boolean`                        | `true`     | カレンダーの凡例を表示します。                       |\n| `blackAndWhite` | `boolean`                        | `false`    | カレンダーの色を使用せずにレンダリングします。       |\n| `textSize`      | `'small' \\| 'medium' \\| 'large'` | `medium`   | プレビューと印刷のタイポグラフィの密度を制御します。 |\n\n## CalendarPrintConfig\n\n印刷ダイアログを部分的な設定で事前シードまたは上書きできます。\n\n```ts\ntype CalendarPrintConfig = {\n  view: 'month' | 'week' | 'day' | 'year-fixed-week' | 'year-canvas';\n  paper: 'A4' | 'Letter';\n  orientation?: 'portrait' | 'landscape';\n  startDate: Date;\n  endDate?: Date;\n  calendarIds: string[];\n  options: CalendarPrintOptions;\n};\n```\n\nソースコードからの重要な動作:\n\n- `year-fixed-week` は強制的に `landscape`（横向き）になります。\n- `year-canvas` は強制的に `portrait`（縦向き）になります。\n- ダイアログは、ブラウザのネイティブ印刷 UI を開く前に `@page` ルールを注入します。\n- プレビューでは、印刷前にズームとパンがサポートされます。\n\n## 印刷スコープ\n\nダイアログは常に同じイベントソースからプルするとは限りません。\n\n- 選択した印刷ビューが現在のカレンダービューと一致し、`startDate` がアプリの現在の日付と一致する場合、プレビューレンダリングは表示されているイベントセットに `app.getEvents()` を使用します。\n- それ以外の場合は `app.getAllEvents()` を使用し、現在画面にレンダリングされている範囲外の日付も印刷対象に含めることができます。\n\nこれは内部的に `sourceScope: 'visible' | 'all'` として公開されています。\n\n## API\n\n`createPrintPlugin()` は、`api` オブジェクトを持つ通常の DayFlow プラグインを返します。\n\n```ts\ntype PrintPluginApi = {\n  open: () => void;\n  close: () => void;\n  print: (config?: Partial<CalendarPrintConfig>) => void;\n};\n```\n\n備考:\n\n- `api.open()`: 印刷ダイアログを開きます。\n- `api.close()`: ダイアログがマウントされている場合に閉じます。\n- `api.print(config)`: 現在、これもダイアログを開きます。まだプレビューをバイパスしません。\n- プラグインは、インストールされると `Cmd/Ctrl + P` をグローバルにリッスンします。\n"
  },
  {
    "path": "website/content/docs-ja/plugins/sidebar.mdx",
    "content": "import { SidebarCustomShowcase } from '@/components/showcase/SidebarShowcases';\n\n# サイドバープラグイン\n\nサイドバープラグインは、DayFlow カレンダーにカレンダー管理サイドバーを追加します。日付ナビゲーション用のミニカレンダー、表示切替付きカレンダーリスト、カレンダーの CRUD 操作（作成、名前変更、色変更、統合、削除、インポート/エクスポート）を含みます。\n\n## インストール\n\nプラグインパッケージをインストールします：\n\n<PackageTabs pkg='@dayflow/plugin-sidebar' />\n\n## 使い方\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\n\nfunction MyCalendar() {\n  const sidebarPlugin = createSidebarPlugin({\n    width: 280,\n    createCalendarMode: 'modal',\n  });\n\n  const calendar = useCalendarApp({\n    views: [\n      /* ビュー */\n    ],\n    plugins: [sidebarPlugin],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 設定リファレンス\n\n| プロパティ                   | 型                                                     | デフォルト                         | 説明                                                   |\n| :--------------------------- | :----------------------------------------------------- | :--------------------------------- | :----------------------------------------------------- |\n| `width`                      | `number \\| string`                                     | `'240px'`                          | サイドバーの幅（例: `280` または `'20rem'`）           |\n| `miniWidth`                  | `string`                                               | `'50px'`                           | 折りたたみ時のサイドバーの幅                           |\n| `initialCollapsed`           | `boolean`                                              | `false`                            | サイドバーを折りたたんだ状態で開始するかどうか         |\n| `createCalendarMode`         | `'inline' \\| 'modal'`                                  | `'inline'`                         | カレンダー作成フォームの表示方法                       |\n| `colorPickerMode`            | `'blossom' \\| 'default'`                               | `'default'`                        | 使用するカラーピッカーコンポーネント                   |\n| `onSubscribeCalendar`        | `(calendar, events) => Promise<void>`                  | `undefined`                        | 新しい購読が追加された後のコールバック                 |\n| `onLoadSubscription`         | `(calendar) => Promise<void>`                          | `undefined`                        | 既存の購読用のカスタムローダー                         |\n| `onReorder`                  | `(calendars: CalendarType[]) => void \\| Promise<void>` | `undefined`                        | カレンダーの並べ替え後のコールバック                   |\n| `componentsOrder`            | `('calendarList' \\| 'miniCalendar')[]`                 | `['calendarList', 'miniCalendar']` | サイドバーコンポーネントの表示順序                     |\n| `render`                     | `(props: CalendarSidebarRenderProps) => TNode`         | `undefined`                        | サイドバー UI を完全にオーバーライドします             |\n| `renderSidebarHeader`        | `(args: SidebarHeaderSlotArgs) => TNode`               | `undefined`                        | カスタムサイドバーヘッダーレンダラー                   |\n| `renderCalendarContextMenu`  | `(calendar, onClose) => TNode`                         | `undefined`                        | カレンダー項目のカスタムコンテキストメニューレンダラー |\n| `renderCreateCalendarDialog` | `(props) => TNode`                                     | `undefined`                        | カスタムカレンダー作成ダイアログレンダラー             |\n\n## プログラマティック API\n\nカレンダーのレンダリング後、`app.getPlugin` で `SidebarService` を取得できます：\n\n```tsx\nimport { type SidebarService } from '@dayflow/plugin-sidebar';\n\nconst sidebar = app.getPlugin<SidebarService>('sidebar');\n\nsidebar?.collapse(); // サイドバーを折りたたむ\nsidebar?.expand(); // サイドバーを展開する\nsidebar?.setCollapsed(true); // 状態を明示的に設定する\nconsole.log(sidebar?.isCollapsed()); // 現在の状態を取得する\n```\n\n### SidebarService\n\n| メソッド                           | 返り値    | 説明                             |\n| :--------------------------------- | :-------- | :------------------------------- |\n| `collapse()`                       | `void`    | サイドバーを折りたたむ           |\n| `expand()`                         | `void`    | サイドバーを展開する             |\n| `setCollapsed(collapsed: boolean)` | `void`    | 折りたたみ状態を明示的に設定する |\n| `isCollapsed()`                    | `boolean` | 現在の折りたたみ状態を返す       |\n\n> **注意:** これらのメソッドは最初のレンダリング前に呼び出すと no-op になります。実際のユーザー操作やライフサイクルコールバックはレンダリング後に発生するため、実用上ほとんど問題にはなりません。\n\n### キーボードショートカットへのバインド\n\n```tsx\nwindow.addEventListener('keydown', e => {\n  if (e.metaKey && e.key === '[') {\n    const sidebar = app.getPlugin<SidebarService>('sidebar');\n    sidebar?.setCollapsed(!sidebar.isCollapsed());\n  }\n});\n```\n\n## 機能\n\nサイドバープラグインは、可視状態のトグル、カレンダーの配色表示、折りたたみボタンを備えた既製のレイアウトを提供します。また、カレンダーの作成とコンテキストメニューもサポートしており、ユーザーがカレンダーの追加、色の編集、削除を自由に行うことができます。\n\n組み込み機能：\n\n- **ミニカレンダー** - 日付の素早いナビゲーション用のコンパクトな月表示\n- **カレンダーリスト** - 色分けされたチェックボックスで個々のカレンダーの表示を切替\n- **カレンダー作成** - カスタム名と色で新しいカレンダーを追加\n- **名前変更 / 色変更** - カレンダーを右クリックして名前や色を変更\n- **カレンダーの並べ替え** - リスト内でカレンダーをドラッグ＆ドロップして並べ替えることができます。`onReorder` コールバックを使用して、新しい順序をバックエンドに保存できます。\n- **カレンダー統合** - あるカレンダーの全イベントを別のカレンダーに移動\n- **カレンダー削除** - 確認ステップ付きでカレンダーを削除（事前に統合も可能）\n- **インポート / エクスポート** - `.ics` ファイルのインポートまたはカレンダーを `.ics` にエクスポート\n- **カレンダーを登録** - ICS URL でリモートの `.ics` フィードを登録でき、登録済みカレンダーにはサイドバーリストにバッジアイコンが表示されます\n\n### デモ\n\nDayFlow のサイドバー体験を 2 つのデモで紹介します。標準 UI のまま使うパターンと、完全カスタムに置き換えるパターンを比較しながら確認してください。\n\n<SidebarCustomShowcase />\n\n#### 見どころ\n\n- **表示の同期**: 可視状態を切り替えるとカレンダー側もリアルタイムに反映\n- **ドラッグ & 同期**: ビューを切り替えてもカスタムサイドバーに表示する「次の予定」や KPI を同期可能\n- **折りたたみ**: 集中したいときは折りたたんで表示領域を広げられる\n\n## カレンダーを登録\n\nサイドバーにはリモート ICS カレンダーフィードを登録する機能が組み込まれています。サイドバーのカレンダーリストまたは空き領域を右クリックしてコンテキストメニューを開き、**カレンダーを登録**を選択し、公開されている `.ics` URL を入力するだけで利用できます。\n\nDayFlow はファイルを取得してイベントを解析し、新しいカレンダーを自動的に作成します。登録済みカレンダーはサイドバーリストに小さなバッジアイコンが表示され、ローカルで作成したカレンダーと区別できます。\n\n### 自動読み込みと重複排除\n\n初期設定で `subscription.url` を持つカレンダーを提供すると、サイドバープラグインはマウント時に自動的に最新のイベントを取得します。\n\nローカルにキャッシュされたイベントと、新しく取得した購読データを組み合わせる際の二重表示を防ぐため、DayFlow は**IDベースの重複排除**を実装しています：\n\n- 購読から取得したイベントが、コアストア内の既存イベントと同じ `id` を持っている場合、購読側のバージョンが優先されます。\n- これにより、ページをリロードした後にイベントが「二重に」表示されることなく、常に最新の情報を UI に表示できます。\n\n### 購読カレンダーの永続化\n\nDayFlow は `CalendarType` に `subscription` オブジェクトが存在するかどうかで購読カレンダーを識別します。\n\n**購読カレンダーのデフォルト動作：**\n\n- **読み取り専用**：タイトル編集、カレンダー変更、削除などの UI 操作が `canMutateFromUI()` を介して無効化されます。\n- **ドラッグ不可**：ドラッグ＆ドロップ操作（移動、リサイズ）が無効化されます。\n- **備考の非表示**：イベントに説明がない場合、詳細パネルの「備考」フィールドは非表示になり、UI が整理されます。\n\nユーザーに購読イベントの編集や移動を許可したい場合は、カレンダーオブジェクトに `readOnly: false` を設定することで、これらのデフォルトの保護を明示的に上書きできます：\n\n```tsx\nconst calendar = useCalendarApp({\n  calendars: [\n    {\n      id: 'team-ics',\n      name: 'チームカレンダー',\n      subscription: {\n        url: 'https://example.com/calendar.ics',\n        status: 'ready',\n      },\n      // デフォルトの保護を上書き：\n      readOnly: false, // UI 操作とドラッグ＆ドロップの両方を有効化\n      colors: {\n        /* ... */\n      },\n    },\n  ],\n});\n```\n\nカレンダーの状態をバックエンドに保存する際は `subscription` フィールドを保持し、次回の読み込み時に復元してください。\n},\n],\n});\n\n````\n\n> **注意:** DayFlow は登録済みフィードを自動更新しません。イベントを最新の状態に保つには、アプリ側で定期的な取得を実装し、`app.addEvent()` / `app.removeEvent()` でイベントを更新してください。\n\n## 完全なカスタマイズ\n\n独自のサイドバーコンポーネントを使用することも可能です。`createSidebarPlugin` の `render` にレンダリング関数を渡すだけで、リアルタイムのカレンダー状態とヘルパーアクションを受け取ることができます。\n\n```tsx\nimport {\n  createSidebarPlugin,\n  type CalendarSidebarRenderProps,\n} from '@dayflow/plugin-sidebar';\n\nconst CustomSidebar = ({\n  app,\n  calendars,\n  toggleCalendarVisibility,\n  toggleAll,\n  isCollapsed,\n  setCollapsed,\n}: CalendarSidebarRenderProps) => {\n  if (isCollapsed) {\n    return (\n      <div className='p-2'>\n        <button onClick={() => setCollapsed(false)}>→</button>\n      </div>\n    );\n  }\n\n  return (\n    <aside className='flex h-full flex-col gap-4 p-4 bg-slate-50 border-r'>\n      <header className='flex items-center justify-between'>\n        <h3 className='font-semibold'>マイワークスペース</h3>\n        <button onClick={() => setCollapsed(true)}>←</button>\n      </header>\n\n      <nav className='space-y-1'>\n        {calendars.map(calendar => (\n          <label\n            key={calendar.id}\n            className='flex items-center gap-2 cursor-pointer'\n          >\n            <input\n              type='checkbox'\n              checked={calendar.isVisible}\n              onChange={e =>\n                toggleCalendarVisibility(calendar.id, e.target.checked)\n              }\n            />\n            <span\n              className='w-3 h-3 rounded-full'\n              style={{ backgroundColor: calendar.colors.lineColor }}\n            />\n            {calendar.name}\n          </label>\n        ))}\n      </nav>\n\n      <div className='mt-auto pt-4 border-t text-xs text-slate-500'>\n        選択日: {app.getCurrentDate().toLocaleDateString()}\n      </div>\n    </aside>\n  );\n};\n\nconst sidebarPlugin = createSidebarPlugin({\n  render: props => <CustomSidebar {...props} />,\n});\n````\n\n## カスタムコンテキストメニュー\n\nカレンダー項目のデフォルトの右クリックメニューを置き換えることができます：\n\n```tsx\ncreateSidebarPlugin({\n  renderCalendarContextMenu: (calendar, onClose) => (\n    <div className='bg-white shadow-lg border rounded p-2'>\n      <button\n        onClick={() => {\n          console.log('カスタムアクション');\n          onClose();\n        }}\n      >\n        {calendar.name} のカスタムアクション\n      </button>\n    </div>\n  ),\n});\n```\n\n## カスタムカレンダー作成ダイアログ\n\nデフォルトのカレンダー作成ダイアログを置き換えることができます：\n\n```tsx\ncreateSidebarPlugin({\n  renderCreateCalendarDialog: ({ onCreate, onClose }) => (\n    <MyCustomDialog onSave={onCreate} onCancel={onClose} />\n  ),\n});\n```\n\n## カスタムサイドバーヘッダー\n\nデフォルトのサイドバーヘッダー（「カレンダー」というタイトルと折りたたみボタンがある領域）を置き換えることができます：\n\n```tsx\ncreateSidebarPlugin({\n  renderSidebarHeader: ({ isCollapsed, onCollapseToggle }) => (\n    <div className='flex items-center justify-between p-4 border-b'>\n      {!isCollapsed && (\n        <span className='font-bold text-lg'>マイカレンダー</span>\n      )}\n      <button\n        onClick={onCollapseToggle}\n        className='p-1 hover:bg-slate-100 rounded'\n      >\n        {isCollapsed ? '→' : '←'}\n      </button>\n    </div>\n  ),\n});\n```\n"
  },
  {
    "path": "website/content/docs-ja/ui/context-menu.mdx",
    "content": "---\ntitle: コンテキストメニュー\n---\n\n# コンテキストメニュー\n\nPortal ベースの組み合わせ可能なコンテキストメニューコンポーネント。ネストされたサブメニュー、ラベル、セパレーター、内蔵カラーピッカーをサポート。外側クリック・スクロール・ウィンドウリサイズ・`Escape` キーで自動的に閉じます。\n\n## インストール\n\n<PackageTabs pkg='@dayflow/ui-context-menu' />\n\nすでに Tailwind CSS を使っている場合は、コンポーネント専用バンドルを読み込んでください。\n\n```css\n@import '@dayflow/ui-context-menu/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\nTailwind を使っていない場合は、従来どおり完全なスタイルシートを読み込みます。\n\n```tsx\nimport {\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuLabel,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n  ContextMenuColorPicker,\n} from '@dayflow/ui-context-menu';\nimport '@dayflow/ui-context-menu/dist/styles.css';\n```\n\n## 基本的な使い方\n\n`contextmenu` イベントからカーソル位置を取得して `ContextMenu` に渡します。\n\n```tsx\nimport { useState } from 'react';\nimport {\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n} from '@dayflow/ui-context-menu';\n\nfunction MyComponent() {\n  const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);\n\n  const handleContextMenu = (e: React.MouseEvent) => {\n    e.preventDefault();\n    setMenu({ x: e.clientX, y: e.clientY });\n  };\n\n  return (\n    <div onContextMenu={handleContextMenu}>\n      ここを右クリック\n      {menu && (\n        <ContextMenu x={menu.x} y={menu.y} onClose={() => setMenu(null)}>\n          <ContextMenuItem onClick={() => console.log('編集')}>\n            編集\n          </ContextMenuItem>\n          <ContextMenuItem onClick={() => console.log('コピー')}>\n            コピー\n          </ContextMenuItem>\n          <ContextMenuSeparator />\n          <ContextMenuItem onClick={() => console.log('削除')} danger>\n            削除\n          </ContextMenuItem>\n        </ContextMenu>\n      )}\n    </div>\n  );\n}\n```\n\n## ラベルとアイコン\n\n```tsx\n<ContextMenu x={x} y={y} onClose={onClose}>\n  <ContextMenuLabel>操作</ContextMenuLabel>\n  <ContextMenuItem icon={<PencilIcon />} onClick={() => onEdit()}>\n    イベントを編集\n  </ContextMenuItem>\n  <ContextMenuItem icon={<CopyIcon />} onClick={() => onDuplicate()}>\n    複製\n  </ContextMenuItem>\n  <ContextMenuSeparator />\n  <ContextMenuItem icon={<TrashIcon />} onClick={() => onDelete()} danger>\n    削除\n  </ContextMenuItem>\n</ContextMenu>\n```\n\n## ネストされたサブメニュー\n\n`ContextMenuSub` でアイテムをラップすると、ホバーで開くサブメニューを作成できます。サブメニューはビューポートからはみ出さないよう自動的に位置を調整します。\n\n```tsx\n<ContextMenu x={x} y={y} onClose={onClose}>\n  <ContextMenuItem onClick={() => onEdit()}>編集</ContextMenuItem>\n  <ContextMenuSub>\n    <ContextMenuSubTrigger>移動先</ContextMenuSubTrigger>\n    <ContextMenuSubContent>\n      <ContextMenuItem onClick={() => onMove('work')}>仕事</ContextMenuItem>\n      <ContextMenuItem onClick={() => onMove('personal')}>\n        プライベート\n      </ContextMenuItem>\n    </ContextMenuSubContent>\n  </ContextMenuSub>\n  <ContextMenuSeparator />\n  <ContextMenuItem danger onClick={() => onDelete()}>\n    削除\n  </ContextMenuItem>\n</ContextMenu>\n```\n\n## カラーピッカー\n\n`ContextMenuColorPicker` はプリセットカラーのスウォッチ一覧と、オプションの「カスタムカラー」アクションを表示します。\n\n```tsx\n<ContextMenu x={x} y={y} onClose={onClose}>\n  <ContextMenuLabel>カレンダーの色</ContextMenuLabel>\n  <ContextMenuColorPicker\n    selectedColor={currentColor}\n    onSelect={color => {\n      updateCalendarColor(color);\n      onClose();\n    }}\n    onCustomColor={() => openColorDialog()}\n    customColorLabel='カスタムカラー...'\n  />\n</ContextMenu>\n```\n\n## 無効なアイテム\n\n```tsx\n<ContextMenuItem onClick={() => onPaste()} disabled={!hasClipboard}>\n  貼り付け\n</ContextMenuItem>\n```\n\n## API リファレンス\n\n### `ContextMenu`\n\n| プロパティ  | 型                  | 説明                                  |\n| :---------- | :------------------ | :------------------------------------ |\n| `x`         | `number`            | 水平位置（例：`event.clientX`）       |\n| `y`         | `number`            | 垂直位置（例：`event.clientY`）       |\n| `onClose`   | `() => void`        | メニューを閉じるべきときに呼ばれる    |\n| `children`  | `ComponentChildren` | メニューの内容                        |\n| `className` | `string`            | メニューコンテナに追加する CSS クラス |\n\n### `ContextMenuItem`\n\n| プロパティ | 型                  | デフォルト | 説明                           |\n| :--------- | :------------------ | :--------- | :----------------------------- |\n| `onClick`  | `() => void`        | —          | クリックハンドラー             |\n| `children` | `ComponentChildren` | —          | アイテムのラベル               |\n| `icon`     | `ComponentChildren` | —          | ラベルの左に表示するアイコン   |\n| `danger`   | `boolean`           | `false`    | 削除系の赤いスタイルで表示する |\n| `disabled` | `boolean`           | `false`    | インタラクションを無効にする   |\n\n### `ContextMenuColorPicker`\n\n| プロパティ         | 型                        | デフォルト       | 説明                                         |\n| :----------------- | :------------------------ | :--------------- | :------------------------------------------- |\n| `selectedColor`    | `string`                  | —                | 現在選択中の色（hex）                        |\n| `onSelect`         | `(color: string) => void` | —                | スウォッチをクリックしたときに呼ばれる       |\n| `onCustomColor`    | `() => void`              | —                | カスタムカラー行をクリックしたときに呼ばれる |\n| `customColorLabel` | `string`                  | `\"Custom Color\"` | カスタムカラーアクションのラベル             |\n\n### その他のエクスポート\n\n| コンポーネント          | 説明                                     |\n| :---------------------- | :--------------------------------------- |\n| `ContextMenuSeparator`  | 水平の区切り線                           |\n| `ContextMenuLabel`      | インタラクションなしのセクション見出し   |\n| `ContextMenuSub`        | サブメニューの開閉状態を管理するラッパー |\n| `ContextMenuSubTrigger` | ホバーでサブメニューを開くトリガー行     |\n| `ContextMenuSubContent` | フローティングサブメニューパネル         |\n"
  },
  {
    "path": "website/content/docs-ja/ui/meta.json",
    "content": "{\n  \"title\": \"UI\",\n  \"pages\": [\"context-menu\", \"range-picker\"]\n}\n"
  },
  {
    "path": "website/content/docs-ja/ui/range-picker.mdx",
    "content": "---\ntitle: 日時範囲ピッカー\n---\n\n# 日時範囲ピッカー\n\nTemporal API をベースにした日付/時刻の範囲選択コンポーネント。日付のみ・日付+時刻の両モード、タイムゾーン対応、キーボード入力、柔軟なポップアップ配置をサポートします。\n\n## インストール\n\n<PackageTabs pkg='@dayflow/ui-range-picker' />\n\nすでに Tailwind CSS を使っている場合は、コンポーネント専用バンドルを読み込んでください。\n\n```css\n@import '@dayflow/ui-range-picker/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\nTailwind を使っていない場合は、従来どおり完全なスタイルシートを読み込みます。\n\n```tsx\nimport { RangePicker } from '@dayflow/ui-range-picker';\nimport '@dayflow/ui-range-picker/dist/styles.css';\n```\n\n## 基本的な使い方\n\n```tsx\nimport { useState } from 'react';\nimport { Temporal } from 'temporal-polyfill';\nimport { RangePicker } from '@dayflow/ui-range-picker';\nimport type { ZonedRange } from '@dayflow/ui-range-picker';\n\nfunction MyComponent() {\n  const [range, setRange] = useState<ZonedRange>([\n    Temporal.Now.zonedDateTimeISO(),\n    Temporal.Now.zonedDateTimeISO().add({ hours: 1 }),\n  ]);\n\n  return <RangePicker value={range} onChange={value => setRange(value)} />;\n}\n```\n\n## 日付のみモード\n\n`showTime={false}` を渡すと時刻セレクターを非表示にできます。\n\n```tsx\n<RangePicker\n  value={range}\n  showTime={false}\n  format='YYYY-MM-DD'\n  onChange={value => setRange(value)}\n/>\n```\n\n## タイムゾーン指定\n\n```tsx\n<RangePicker\n  value={range}\n  timeZone='Asia/Tokyo'\n  onChange={(value, dateStrings) => {\n    console.log('範囲:', value);\n    console.log('フォーマット済み:', dateStrings); // ['2024-10-15 10:00', '2024-10-15 11:00']\n  }}\n/>\n```\n\n## カスタム時刻フォーマット\n\n```tsx\n<RangePicker\n  value={range}\n  format='YYYY/MM/DD'\n  showTime={{ format: 'HH:mm' }}\n  onChange={value => setRange(value)}\n  onOk={value => saveToBackend(value)}\n/>\n```\n\n## ポップアップの配置\n\nポップアップはデフォルトで左下に表示され、ビューポートからはみ出す場合は自動的に位置を調整します。\n\n```tsx\n<RangePicker\n  value={range}\n  placement='topRight'\n  autoAdjustOverflow={true}\n  onChange={value => setRange(value)}\n/>\n```\n\n## ロケール\n\nBCP 47 のロケールコードを渡すと月名・曜日名がローカライズされます。\n\n```tsx\n<RangePicker value={range} locale='ja-JP' onChange={value => setRange(value)} />\n```\n\n## トリガー幅に合わせる\n\n```tsx\n<RangePicker\n  value={range}\n  matchTriggerWidth\n  onChange={value => setRange(value)}\n/>\n```\n\n## API リファレンス\n\n### `RangePicker`\n\n| プロパティ           | 型                                                              | デフォルト     | 説明                                                                        |\n| :------------------- | :-------------------------------------------------------------- | :------------- | :-------------------------------------------------------------------------- |\n| `value`              | `[Temporal.PlainDate \\| PlainDateTime \\| ZonedDateTime, ...]`   | —              | 制御された範囲値。任意の Temporal 型の組み合わせを受け付ける                |\n| `format`             | `string`                                                        | `\"YYYY-MM-DD\"` | 日付部分の表示・パースフォーマット                                          |\n| `showTime`           | `boolean \\| { format?: string }`                                | `true`         | 時刻選択を有効にする。オブジェクトで時刻フォーマットを指定できる            |\n| `showTimeFormat`     | `string`                                                        | `\"HH:mm\"`      | `showTime` が `true` のときのデフォルト時刻フォーマット                     |\n| `onChange`           | `(value: ZonedRange, dateString: [string, string]) => void`     | —              | 選択が変わるたびに発火                                                      |\n| `onOk`               | `(value: ZonedRange, dateString: [string, string]) => void`     | —              | ユーザーが OK ボタンで確定したときに発火                                    |\n| `timeZone`           | `string`                                                        | —              | IANA タイムゾーン文字列（例：`\"Asia/Tokyo\"`）。省略時はシステムタイムゾーン |\n| `disabled`           | `boolean`                                                       | `false`        | すべての操作を無効にする                                                    |\n| `placement`          | `'bottomLeft' \\| 'bottomRight' \\| 'topLeft' \\| 'topRight'`      | `'bottomLeft'` | ポップアップの優先表示位置                                                  |\n| `autoAdjustOverflow` | `boolean`                                                       | `true`         | ビューポートからはみ出す場合にポップアップ位置を自動反転する                |\n| `getPopupContainer`  | `() => HTMLElement`                                             | —              | `document.body` の代わりにカスタムコンテナにポップアップをマウントする      |\n| `matchTriggerWidth`  | `boolean`                                                       | `false`        | ポップアップの幅をトリガー入力欄の幅に合わせる                              |\n| `locale`             | `string \\| { code: string; messages?: Record<string, string> }` | `'en-US'`      | 月名・曜日名のローカライズに使用する BCP 47 ロケールコード                  |\n\n### `ZonedRange`\n\n```ts\ntype ZonedRange = [Temporal.ZonedDateTime, Temporal.ZonedDateTime];\n```\n\n`onChange` と `onOk` のコールバックは、入力値の型にかかわらず、常に現在のタイムゾーンに正規化された `ZonedRange` を返します。\n"
  },
  {
    "path": "website/content/docs-zh/features/calendar-header.mdx",
    "content": "# 日历页眉\n\n日历页眉提供导航控制、视图切换器和搜索功能。您可以自定义其行为或完全替换它。\n\n<Callout title='文档站导航更新' type='info'>\n  当前文档站的顶栏导航已经新增了一个独立的 DayFlow Pro 入口，它和主 DayFlow logo\n  以及 Blossom Picker 并列显示。这个 Pro 入口使用 `pro-logo.png + Pro badge`\n  结构，属于网站导航本身，不属于本页介绍的日历组件 header API。\n</Callout>\n\n## 隐藏页眉\n\n如果您想构建自己的导航，或者只是不需要默认页眉，可以禁用它。\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  useCalendarHeader: false, // 隐藏默认页眉\n});\n```\n\n## 自定义页眉\n\n<Callout title='Breaking change from v3.4.1' type='warn'>\n  不再支持向 `useCalendarHeader` 传入函数。请将自定义页眉迁移到\n  `DayFlowCalendar` 的 `calendarHeader` 插槽，如下所示。\n</Callout>\n\n通过 `calendarHeader` 内容插槽，您可以用自己的组件替换默认页眉。渲染器接收日历的状态和辅助方法，让您可以从自己的 UI 控制导航和搜索。\n\n### React\n\n向 `DayFlowCalendar` 传递 `calendarHeader` 渲染属性：\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n});\n\n<DayFlowCalendar\n  calendar={calendar}\n  calendarHeader={() => (\n    <div className='custom-header'>\n      <button onClick={() => calendar.goToPrevious()}>‹</button>\n      <button onClick={() => calendar.goToToday()}>今天</button>\n      <button onClick={() => calendar.goToNext()}>›</button>\n    </div>\n  )}\n/>;\n```\n\n### Vue\n\n使用具名插槽 `calendarHeader`：\n\n```vue\n<DayFlowCalendar :calendar=\"calendar\">\n  <template #calendarHeader=\"{ calendar, switcherMode, onSearchChange }\">\n    <div class=\"custom-header\">\n      <button @click=\"calendar.goToPrevious()\">‹</button>\n      <button @click=\"calendar.goToToday()\">今天</button>\n      <button @click=\"calendar.goToNext()\">›</button>\n    </div>\n  </template>\n</DayFlowCalendar>\n```\n\n### 插槽参数\n\n插槽渲染器接收以下参数：\n\n| 参数             | 描述                                                        |\n| :--------------- | :---------------------------------------------------------- |\n| `calendar`       | `CalendarApp` 实例。                                        |\n| `switcherMode`   | 当前视图切换器模式（`buttons` 或 `select`）。               |\n| `onAddCalendar`  | 添加新日历的处理函数。                                      |\n| `onSearchChange` | 更新搜索查询的处理函数。                                    |\n| `onSearchClick`  | 点击搜索图标时的处理函数。                                  |\n| `searchValue`    | 当前搜索字符串。                                            |\n| `isSearchOpen`   | 搜索 UI 当前是否打开。                                      |\n| `isEditable`     | 日历当前是否处于可编辑状态。                                |\n| `safeAreaLeft`   | 左侧内边距（px），用于在 macOS 模式下避免与交通灯按钮重叠。 |\n"
  },
  {
    "path": "website/content/docs-zh/features/content-slots.mdx",
    "content": "import { EventContentShowcase } from '@/components/showcase/EventContentShowcase';\nimport { CustomDetailPanelShowcase } from '@/components/showcase/CustomDetailPanelShowcase';\nimport { CustomDetailDialogShowcase } from '@/components/showcase/CustomDetailDialogShowcase';\nimport { ColorPickerShowcase } from '@/components/showcase/ColorPickerShowcase';\nimport { ContextMenuShowcase } from '@/components/showcase/ContextMenuShowcase';\nimport { MobileEventDetailShowcase } from '@/components/showcase/MobileEventDetailShowcase';\n\n# 内容插槽\n\nDayFlow 采用了基于插槽的架构，允许您直接从所使用的框架（React、Vue 等）中注入自定义 UI 组件到核心日历引擎中。\n\n这是通过 `ContentSlot` 机制实现的。虽然核心引擎为大多数 UI 元素提供了默认实现（使用 Preact），但您可以覆盖它们以符合您的应用设计或使用特定的第三方库。\n\n## Table of Contents\n\n- [工作原理](#工作原理)\n- [可用插槽](#可用插槽)\n- [事件内容](#事件内容)\n- [事件详情内容](#事件详情内容)\n- [事件详情对话框](#事件详情对话框)\n- [移动端事件详情](#移动端事件详情)\n- [注入自定义颜色选择器](#注入自定义颜色选择器)\n- [右键菜单](#右键菜单)\n\n## 工作原理\n\n1. **核心定义插槽**：在 `@dayflow/core` 内部，某些 UI 区域被包装 in a `ContentSlot` 中。每个插槽都有一个 `generatorName`（生成器名称）和 `generatorArgs`（生成器参数）。\n2. **您提供实现**：您将组件或渲染函数传递给 `DayFlowCalendar` 组件。适配器随后会将您的组件以 Portal（传送门）的形式渲染到核心定义的精确位置。\n\n## 可用插槽\n\n| 生成器名称                        | 描述                                                                                        | 参数                                                                      |\n| :-------------------------------- | :------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------ |\n| `colorPicker`                     | 用于事件颜色的迷你颜色选择器。                                                              | `{ color, onChange, onChangeComplete }`                                   |\n| `createCalendarDialogColorPicker` | 对话框中使用功能完整的颜色选择器。                                                          | `{ color, onChange, onAccept, onCancel, styles }`                         |\n| `eventContent*`                   | 事件卡片的自定义渲染（例如 `eventContentDay`）。                                            | `{ event, viewType, isAllDay, isMobile, isSelected, isDragging, layout }` |\n| `eventContextMenu`                | 自定义事件右键菜单。                                                                        | `{ event, onClose }`                                                      |\n| `eventDetailContent`              | 点击事件时在弹出框/面板中显示的内容。                                                       | `{ event, isAllDay, onEventDelete, onEventUpdate, onClose, app }`         |\n| `eventDetailDialog`               | 点击事件时显示的自定义对话框。需要在 `useCalendarApp` 中设置 `useEventDetailDialog: true`。 | `{ event, isOpen, isAllDay, onEventDelete, onEventUpdate, onClose, app }` |\n| `gridContextMenu`                 | 日历单元格 / 网格的自定义右键上下文菜单。                                                   | `{ date, viewType, onClose }`                                             |\n| `mobileEventDetail`               | 自定义移动端事件详情抽屉 / 对话框。                                                         | `{ isOpen, onClose, onSave, onEventDelete, draftEvent, app, timeFormat }` |\n| `sidebarCalendarColorPicker`      | 侧边栏中用于日历颜色的颜色选择器。                                                          | `{ color, onChange, onChangeComplete }`                                   |\n| `titleBarSlot`                    | 侧边栏标题栏中的额外内容。                                                                  | `{ isCollapsed, toggleCollapsed }`                                        |\n\n## 事件内容\n\n`eventContent` 插槽允许您完全自定义事件在日历中的渲染方式。与其他插槽不同，事件内容**必须**针对每个视图分别指定，以确保布局与每个视图特定的事件结构保持一致。\n\n### 视图特定插槽\n\n| 插槽名称                  | 描述                                   |\n| :------------------------ | :------------------------------------- |\n| `eventContentDay`         | 重写天视图（Day View）中的定时事件。   |\n| `eventContentWeek`        | 重写周视图（Week View）中的定时事件。  |\n| `eventContentMonth`       | 重写月视图（Month View）中的事件。     |\n| `eventContentYear`        | 重写年视图（Year View）中的事件。      |\n| `eventContentAllDayDay`   | 重写天视图（Day View）中的全天事件。   |\n| `eventContentAllDayWeek`  | 重写周视图（Week View）中的全天事件。  |\n| `eventContentAllDayMonth` | 重写月视图（Month View）中的全天事件。 |\n| `eventContentAllDayYear`  | 重写年视图（Year View）中的全天事件。  |\n\n<EventContentShowcase />\n\n<details>\n<summary>查看源代码</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nexport const EventContentShowcase = () => {\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      // 自定义天视图的事件渲染\n      eventContentDay={({ event, isSelected }) => (\n        <div className='custom-event-card'>\n          <span>{event.title}</span>\n          {/* ... 自定义图标或布局 */}\n        </div>\n      )}\n      // 自定义月视图的事件渲染\n      eventContentMonth={({ event }) => (\n        <div className='flex items-center gap-1'>\n          <span>🗓️</span>\n          <span className='truncate'>{event.title}</span>\n        </div>\n      )}\n      // 省略其他视图的重写以保持简洁...\n    />\n  );\n};\n```\n\n</details>\n\n## 事件详情内容\n\n`eventDetailContent` 插槽允许您自定义点击事件时在详情弹出层或面板中显示的内容。\n\n<CustomDetailPanelShowcase />\n\n<details>\n<summary>查看源代码</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { useCallback } from 'react';\n\nexport const CustomDetailPanelShowcase = () => {\n  const detailPanel = useCallback(\n    ({ event, onEventDelete, onEventUpdate, onClose }) => {\n      return (\n        <div className='p-4 space-y-3'>\n          <h5 className='font-bold'>{event.title}</h5>\n          <p>{event.description}</p>\n\n          <div className='flex gap-2'>\n            <button\n              onClick={() => onEventUpdate({ ...event, title: '已更新' })}\n            >\n              更新\n            </button>\n            <button onClick={() => onEventDelete(event.id)}>删除</button>\n          </div>\n        </div>\n      );\n    },\n    []\n  );\n\n  return (\n    <DayFlowCalendar calendar={calendar} eventDetailContent={detailPanel} />\n  );\n};\n```\n\n</details>\n\n## 事件详情对话框\n\n如果您更喜欢使用对话框/模态框来查看和编辑事件详情，可以使用 `eventDetailDialog` 插槽。这对于移动优先的应用或需要更多空间展示复杂事件数据的情况特别有用。\n\n> **前提条件**：在 `useCalendarApp` 中设置 `useEventDetailDialog: true` 以激活对话框模式。若未设置，插槽将不会被触发。\n\n> **仅关闭面板**：若您想在不提供自定义对话框的情况下隐藏内置浮动面板，请在 `useCalendarApp` 中设置 `useEventDetailPanel: false`，而非使用插槽。\n\n<CustomDetailDialogShowcase />\n\n<details>\n<summary>查看源代码</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { useCallback } from 'react';\n\nexport const CustomDetailDialogShowcase = () => {\n  const customDialog = useCallback(({ event, isOpen, onClose }) => {\n    if (!isOpen) return null;\n\n    return (\n      <div className='fixed inset-0 z-50 flex items-center justify-center bg-black/50'>\n        <div className='bg-white p-6 rounded-lg shadow-xl'>\n          <h2>{event.title}</h2>\n          {/* ... 在此构建您的自定义对话框 UI */}\n          <button onClick={onClose}>关闭</button>\n        </div>\n      </div>\n    );\n  }, []);\n\n  return (\n    <DayFlowCalendar calendar={calendar} eventDetailDialog={customDialog} />\n  );\n};\n```\n\n</details>\n\n## 移动端事件详情\n\n当在移动设备（或小屏幕）上查看 DayFlow 时，它使用专门的 `mobileEventDetail` 插槽来处理事件的创建和编辑。默认情况下，这是一个全屏抽屉。\n\n<MobileEventDetailShowcase />\n\n<details>\n<summary>查看源代码</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react';\nimport { useCallback } from 'react';\n\nexport const MobileEventDetailShowcase = () => {\n  const customMobileDrawer = useCallback(\n    ({ isOpen, onClose, onSave, onEventDelete, draftEvent }) => {\n      if (!isOpen || !draftEvent) return null;\n\n      return (\n        <div className='fixed inset-0 z-50 flex flex-col bg-white'>\n          <header className='flex items-center justify-between p-4 border-b'>\n            <button onClick={onClose}>返回</button>\n            <h2>{draftEvent.id ? '编辑' : '新建'}事件</h2>\n            <button onClick={() => onSave(draftEvent)}>保存</button>\n          </header>\n          <div className='p-4'>\n            <input\n              defaultValue={draftEvent.title}\n              onChange={e => (draftEvent.title = e.target.value)}\n            />\n            {/* ... 构建您的移动端优化 UI */}\n          </div>\n        </div>\n      );\n    },\n    []\n  );\n\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      mobileEventDetail={customMobileDrawer}\n    />\n  );\n};\n```\n\n</details>\n\n## 注入自定义颜色选择器\n\n默认情况下，DayFlow 使用内置的 `BlossomColorPicker`。如果您更喜欢使用 `react-color` 或 `vue-color` 等库，可以使用 `colorPicker` 插槽进行注入。\n\n<DocImg src='/images/docs/colorPicker.png' alt='颜色选择器' />\n\n### React 示例\n\n安装 `react-color`：\n\n<PackageTabs pkg='react-color' />\n\n将其注入到 `DayFlowCalendar`：\n\n<ColorPickerShowcase />\n\n<details>\n<summary>查看源代码</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { SketchPicker, PhotoshopPicker } from 'react-color';\n\nfunction MyCalendar() {\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      colorPicker={args => (\n        <SketchPicker\n          color={args.color}\n          onChange={color => args.onChange({ hex: color.hex })}\n        />\n      )}\n      createCalendarDialogColorPicker={args => (\n        <PhotoshopPicker\n          color={args.color}\n          onChange={color => args.onChange({ hex: color.hex })}\n          onAccept={args.onAccept}\n          onCancel={args.onCancel}\n        />\n      )}\n    />\n  );\n}\n```\n\n</details>\n\n## 右键菜单插槽\n\n`eventContextMenu` 和 `gridContextMenu` 插槽允许您用完全自定义的 React 组件替换默认的右键菜单。两个插槽都接收 `onClose` 回调以关闭菜单。\n\n- **`eventContextMenu`** — 当用户右键点击事件时触发，接收 `{ event, onClose }`。\n- **`gridContextMenu`** — 当用户右键点击日历网格空白区域时触发，接收 `{ date, viewType, onClose }`。\n\n右键点击任意事件或空白格子，查看自定义菜单效果：\n\n<ContextMenuShowcase />\n\n<details>\n<summary>查看源代码</summary>\n\n```tsx\nimport { DayFlowCalendar } from '@dayflow/react';\nimport type {\n  EventContextMenuSlotArgs,\n  GridContextMenuSlotArgs,\n} from '@dayflow/core';\nimport { useCallback } from 'react';\n\nfunction MyCalendar() {\n  const eventContextMenu = useCallback(\n    ({ event, onClose }: EventContextMenuSlotArgs) => (\n      <div className='custom-menu'>\n        <button\n          onClick={() => {\n            /* 复制 */ onClose();\n          }}\n        >\n          复制\n        </button>\n        <button\n          onClick={() => {\n            /* 分享 */ onClose();\n          }}\n        >\n          分享\n        </button>\n        <button\n          onClick={() => {\n            calendar.deleteEvent(event.id);\n            onClose();\n          }}\n        >\n          删除\n        </button>\n      </div>\n    ),\n    []\n  );\n\n  const gridContextMenu = useCallback(\n    ({ date, onClose }: GridContextMenuSlotArgs) => (\n      <div className='custom-menu'>\n        <button\n          onClick={() => {\n            /* 在此创建事件 */ onClose();\n          }}\n        >\n          新建事件\n        </button>\n        <button\n          onClick={() => {\n            /* 设置提醒 */ onClose();\n          }}\n        >\n          设置提醒\n        </button>\n      </div>\n    ),\n    []\n  );\n\n  return (\n    <DayFlowCalendar\n      calendar={calendar}\n      eventContextMenu={eventContextMenu}\n      gridContextMenu={gridContextMenu}\n    />\n  );\n}\n```\n\n</details>\n"
  },
  {
    "path": "website/content/docs-zh/features/dark-mode.mdx",
    "content": "import { DefaultColorPalette } from '@/components/ColorPalette';\n\n# 深色模式\n\nDayFlow Calendar 原生支持完整的深色模式能力：从视图、侧边栏、事件卡片到对话框，都会在浅色/深色/自动模式之间无缝切换。无论是桌面仪表盘还是移动端排程，都可以与系统偏好保持一致。\n\n## 功能综述\n\n- **三种主题模式**：`light`、`dark`、`auto`\n- **自动颜色调整**：事件颜色会针对不同背景自动优化对比度\n- **瞬时切换**：无白屏、无闪烁，状态保持不变\n- **系统同步**：`auto` 跟随操作系统主题变化\n- **自定义调色**：可为每个日历类型定义独立的浅/深色配色\n\n## 快速上手\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    theme: {\n      mode: 'dark', // 'light' | 'dark' | 'auto'\n    },\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n切换模式也只需修改 `mode`：\n\n```tsx\nuseCalendarApp({ theme: { mode: 'light' } });\nuseCalendarApp({ theme: { mode: 'dark' } });\nuseCalendarApp({ theme: { mode: 'auto' } });\n```\n\n## 编程式主题切换\n\n```tsx\nconst calendar = useCalendarApp({ theme: { mode: 'light' } });\n\nconst toggleTheme = () => {\n  const current = calendar.app.getTheme();\n  calendar.app.setTheme(current === 'light' ? 'dark' : 'light');\n};\n```\n\n如果需要让其他组件知道主题变化，可订阅事件：\n\n```tsx\nuseEffect(() => {\n  const handler = (mode: ThemeMode) => setTheme(mode);\n  calendar.app.subscribeThemeChange(handler);\n  return () => calendar.app.unsubscribeThemeChange(handler);\n}, [calendar.app]);\n```\n\n## 为日历类型配置浅/深色\n\n```tsx\nconst calendars = [\n  {\n    id: 'work',\n    name: '工作',\n    colors: {\n      lineColor: '#0066cc',\n      eventColor: '#e6f2ff',\n      eventSelectedColor: '#cce4ff',\n      textColor: '#003d7a',\n    },\n    darkColors: {\n      lineColor: '#4da6ff',\n      eventColor: '#1a3d5c',\n      eventSelectedColor: '#2a5a8a',\n      textColor: '#b3d9ff',\n    },\n  },\n];\n\nconst calendar = useCalendarApp({\n  theme: { mode: 'auto' },\n  calendars,\n});\n```\n\n> 建议：浅色模式选择亮背景 + 深文本，深色模式选择低饱和背景 + 偏发光的线条色，以保证对比度。\n\n## 默认色板\n\nDayFlow 预设了 10 种日历类型，浅色与深色都满足 WCAG AA 对比度，可直接使用：\n\n<DefaultColorPalette />\n\n## API 速查\n\n```ts\ninterface ThemeConfig {\n  mode: 'light' | 'dark' | 'auto';\n}\n\n// CalendarApp\napp.getTheme(): ThemeMode;\napp.setTheme(mode: ThemeMode): void;\napp.subscribeThemeChange(callback: (theme: ThemeMode) => void): void;\napp.unsubscribeThemeChange(callback: (theme: ThemeMode) => void): void;\n```\n\n## 与 Tailwind / CSS 变量搭配\n\n- Tailwind 项目中可以设置 `darkMode: 'class'`，让页面根节点负责切换，再把当前模式传给 DayFlow。\n- 也可以通过 CSS 变量（如 `--dayflow-bg-primary`）覆盖默认颜色，形成统一的 Design Token。可参考[主题自定义指南](/docs-zh/guides/theme-customization)。\n\n## 测试建议\n\n1. 在浅色和深色模式下分别验证事件颜色与文字对比度（≥4.5:1）。\n2. 检查拖拽、事件对话框、侧边栏等交互在模式切换时是否仍清晰可见。\n3. 若使用 `auto` 模式，记得在系统主题切换时手动测试一次，确保没有闪烁。\n\n通过这些配置，DayFlow 可以轻松融入任何品牌配色与暗色策略中。\n\n## 示例\n\n### 简单主题切换\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { Sun, Moon } from 'lucide-react';\n\nfunction SimpleThemeToggle() {\n  const calendar = useCalendarApp({\n    theme: { mode: 'light' },\n  });\n\n  const [isDark, setIsDark] = useState(false);\n\n  const toggleTheme = () => {\n    const nextTheme = isDark ? 'light' : 'dark';\n    calendar.app.setTheme(nextTheme);\n    setIsDark(!isDark);\n  };\n\n  return (\n    <div>\n      <button onClick={toggleTheme}>{isDark ? <Sun /> : <Moon />}</button>\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n}\n```\n\n## 相关文档\n\n- [useCalendarApp](/docs-zh/introduction/use-calendar-app) - 核心日历配置\n- [日历类型](/docs-zh/introduction/events#calendar-types) - 事件分类与颜色\n- [主题自定义指南](/docs-zh/guides/theme-customization) - 高级主题设置\n"
  },
  {
    "path": "website/content/docs-zh/features/event-dialog.mdx",
    "content": "import { EventDialogShowcase } from '@/components/showcase/FeatureShowcase';\n\n# 事件对话框\n\nDayFlow 自带的事件对话框涵盖了大多数团队需要的 CRUD 流程：创建/编辑/删除、全天与定时切换、日历颜色选择、备注输入等，都在同一个模态中完成。\n\n<EventDialogShowcase />\n\n## 使用方法\n\nDayFlow 默认打开的是内置浮动详情面板。如果您想使用内置模态对话框，需要先在 `useCalendarApp` 中开启 `useEventDetailDialog`，然后正常渲染 `DayFlowCalendar`。\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  views: [...],\n  useEventDetailDialog: true,\n});\n\n<DayFlowCalendar calendar={calendar} />;\n```\n\n## 自定义\n\n### 替换对话框 UI\n\n使用 `eventDetailDialog` 插槽，可以在保留 DayFlow 开关状态管理的同时，换入您自定义的对话框组件。需要在 `useCalendarApp` 配置中设置 `useEventDetailDialog: true` 才能激活此功能。\n\n```tsx\n// 1. 在日历配置中启用对话框模式\nconst calendar = useCalendarApp({\n  views: [...],\n  useEventDetailDialog: true,\n});\n\n// 2. 通过插槽提供您自己的对话框\n<DayFlowCalendar\n  calendar={calendar}\n  eventDetailDialog={({ event, isOpen, onClose }) => (\n    <MyDialog isOpen={isOpen} onClose={onClose}>\n      <EventForm event={event} />\n    </MyDialog>\n  )}\n/>\n```\n\n### 完全关闭内置面板\n\n如果您的应用通过回调或全局状态管理事件详情，可以通过在 `useCalendarApp` 中设置 `useEventDetailPanel: false` 来完全隐藏内置浮动面板，无需提供替代组件：\n\n```tsx\nconst calendar = useCalendarApp({\n  // ...\n  useEventDetailPanel: false,\n});\n\n<DayFlowCalendar calendar={calendar} />;\n```\n\n适用于 React、Vue、Svelte 和 Angular 适配器。\n"
  },
  {
    "path": "website/content/docs-zh/features/meta.json",
    "content": "{\n  \"title\": \"功能特性\",\n  \"pages\": [\n    \"calendar-header\",\n    \"content-slots\",\n    \"dark-mode\",\n    \"event-dialog\",\n    \"multi-calendar-event\",\n    \"read-only\",\n    \"switcher-mode\"\n  ]\n}\n"
  },
  {
    "path": "website/content/docs-zh/features/multi-calendar-event.mdx",
    "content": "import { MultiCalendarEventShowcase } from '@/components/showcase/MultiCalendarEventShowcase';\n\n# 多日历事件\n\nDayFlow 支持多日历事件，允许单个事件与多个日历相关联。这对于跨团队会议、共享家庭活动或任何事件属于多个类别的场景特别有用。\n\n## 配置\n\n要创建多日历事件，您需要在事件对象的 `calendarIds` 属性中提供一个日历 ID 数组。\n\n```tsx\nconst events = [\n  {\n    id: 'multi-cal-1',\n    title: '跨团队规划',\n    start: '2026-04-20T10:00:00',\n    end: '2026-04-20T11:30:00',\n    // 主日历 ID（在需要时用于默认样式）\n    calendarId: 'team-a',\n    // 将事件与多个日历关联\n    calendarIds: ['team-a', 'team-b', 'marketing'],\n  },\n];\n```\n\n当事件具有多个 `calendarIds` 时，DayFlow 会使用分配给每个关联日历的颜色，以独特的对角条纹图案背景进行渲染。\n\n## 展示\n\n以下展示了多日历事件在日历中的渲染效果。\n\n<MultiCalendarEventShowcase />\n\n<Callout title='建议'>\n  默认情况下，内置的 `eventDetailPanel`/`Dialog`\n  不支持选择多个日历。由于此需求并不常见，建议您通过自定义 `eventDetailContent`\n  或 `eventDetailDialog`\n  的实现来满足此类需求。更多详情请参阅文档：[内容插槽](./content-slots)。\n</Callout>\n"
  },
  {
    "path": "website/content/docs-zh/features/read-only.mdx",
    "content": "# 只读模式\n\n您可以轻松地将日历设置为只读模式，以防止用户修改。这对于公共日历或仅用于展示的仪表板非常有用。\n\n只读模式只会禁用 DayFlow 内置的修改 UI。像 `calendar.addEvent()`、`calendar.updateEvent()`、`calendar.deleteEvent()`、`calendar.applyEventsChanges()` 这样的程序化 API 仍然可以正常使用。\n\n## 基本用法\n\n要启用只读模式，请在日历配置中传递 `readOnly: true`。\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  readOnly: true, // 禁用内置修改 UI（拖拽、创建、编辑）\n});\n```\n\n## 细粒度控制\n\n您还可以提供一个 `ReadOnlyConfig` 对象来选择性地禁用某些功能：\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  readOnly: {\n    draggable: false, // 禁用拖拽\n    viewable: true, // 允许点击查看事件详情（只读模式）\n  },\n});\n```\n\n### 配置选项\n\n| 选项        | 类型      | 描述                                          |\n| :---------- | :-------- | :-------------------------------------------- |\n| `draggable` | `boolean` | 是否可以通过拖拽移动事件或调整其大小。        |\n| `viewable`  | `boolean` | 是否可以查看事件详情（打开详情面板/对话框）。 |\n\n当 `readOnly` 激活时，日历还会隐藏\"创建\"按钮和其他通常会触发更改的 UI 元素。\n\n## 自定义 UI\n\n如果您渲染了自己的按钮、菜单或弹窗，建议使用 `calendar.canMutateFromUI()` 来决定是否显示修改入口：\n\n```tsx\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  readOnly: true,\n});\n\nconst canEdit = calendar.canMutateFromUI();\n```\n\n- `true`：自定义 UI 可以显示创建、编辑、删除等入口。\n- `false`：自定义 UI 应隐藏或禁用这些修改入口。\n"
  },
  {
    "path": "website/content/docs-zh/features/switcher-mode.mdx",
    "content": "import { SwitcherModeShowcase } from '@/components/showcase/FeatureShowcase';\n\n# 视图切换器模式\n\n`switcherMode` 决定了顶部视图切换器长什么样。DayFlow 内置两种表现形式：\n\n- `buttons`：水平按钮组，默认启用，适合桌面端\n- `select`：下拉菜单，节省空间，移动端更友好\n\n下面的示例使用同一套数据，方便你直观比较两种模式。\n\n<SwitcherModeShowcase />\n\n## 快速上手\n\n```tsx {9}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n} from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  switcherMode: 'select', // 默认 switcherMode: 'buttons'\n});\n\n<DayFlowCalendar calendar={calendar} />;\n```\n\n在 `'buttons'` 与 `'select'` 之间切换即可即时预览差异。如果还需要完全自定义的切换器，可以直接构建自己的控件并调用 `calendar.changeView`.\n"
  },
  {
    "path": "website/content/docs-zh/guides/global-css.mdx",
    "content": "# 全局 CSS 类\n\nDayFlow 提供了一组带有 `df-` 前缀的 CSS 类，允许您自定义日历组件的外观。这些类旨在方便地进行样式定制，无需修改核心库。\n\n## 选择正确的 CSS 入口\n\n请根据项目是否已经使用 Tailwind CSS 来选择样式入口：\n\n| 入口                    | 包含内容                                | 适用场景               |\n| ----------------------- | --------------------------------------- | ---------------------- |\n| `styles.css`            | 完整包，包含 Tailwind preflight / reset | 不使用 Tailwind 的项目 |\n| `styles.components.css` | 仅组件样式，不包含 CSS Reset            | 已使用 Tailwind 的项目 |\n\n```css\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\n做主题覆盖时，推荐优先使用 `--df-color-*` 变量或下面列出的稳定 `df-*` 语义类。\n\n## 目录\n\n- [通用类](#通用)\n- [语义辅助类](#语义辅助类)\n- [布局与动效辅助类](#布局与动效辅助类)\n- [日视图类](#日视图)\n- [周视图类](#周视图)\n- [月视图类](#月视图)\n- [年视图类](#年视图)\n- [迷你日历类](#迷你日历)\n- [侧边栏类](#侧边栏)\n- [导航类](#导航)\n- [事件详情类](#事件详情)\n- [内容插槽类](#内容插槽)\n- [事件状态属性](#事件状态属性)\n\n---\n\n## 通用\n\n所有日历视图中使用的通用 CSS 类。\n\n| 类名                       | 描述                                                          |\n| -------------------------- | ------------------------------------------------------------- |\n| `df-calendar-container`    | 包裹侧边栏和日历的最外层根容器，支持 `--df-calendar-height`。 |\n| `df-calendar`              | 日历主容器（包含头部和视图内容）                              |\n| `df-header`                | 日历头部区域（包含标题、今天按钮和视图切换器）                |\n| `df-header-left`           | 头部左侧区域（标题和导航按钮）                                |\n| `df-header-mid`            | 头部中间区域（当前日期范围标题）                              |\n| `df-header-right`          | 头部右侧区域（视图切换器和搜索）                              |\n| `df-navigation`            | 导航控件容器                                                  |\n| `df-view-header-container` | 次级视图头部区域的共享容器                                    |\n| `df-view-header-title`     | 视图级标题文本的共享类                                        |\n| `df-view-header-subtitle`  | 视图级副标题文本的共享类                                      |\n| `df-event`                 | 所有事件的基础类                                              |\n| `df-event-title`           | 事件标题文本                                                  |\n| `df-event-time`            | 事件时间文本                                                  |\n| `df-event-color-bar`       | 事件左侧的颜色条（日/周视图）                                 |\n| `df-month-event-color-bar` | 普通事件的颜色指示器（月视图）                                |\n| `df-all-day-row`           | 全天事件行容器                                                |\n| `df-all-day-label`         | 全天标签文本                                                  |\n| `df-all-day-content`       | 全天事件内容区域                                              |\n| `df-all-day-cell`          | 单个全天事件单元格                                            |\n| `df-date-number`           | 日期数字显示                                                  |\n| `df-current-time-line`     | 当前时间指示线容器                                            |\n| `df-current-time-label`    | 当前时间标签文本                                              |\n| `df-current-time-bar`      | 当前时间水平线                                                |\n| `df-calendar-checkbox`     | 日历列表及相关控件中复用的共享复选框样式                      |\n| `df-portal`                | 通过 portal 渲染的浮层根作用域类                              |\n| `df-range-picker`          | Range Picker 触发器与弹层共用的根作用域类                     |\n\n---\n\n## 语义辅助类\n\n这次样式重构后，DayFlow 对外公开了一组稳定的 `df-*` 语义类，用来表达按钮填充态、选中态、危险操作态、焦点环等主题语义。它们定义在共享基础样式中，适合在自定义 slot、插件 UI 或包裹层里复用。\n\n| 类名                      | 用途                                    |\n| ------------------------- | --------------------------------------- |\n| `df-fill-primary`         | 主色填充背景 + 自动前景文字             |\n| `df-fill-secondary`       | 次色填充背景 + 自动前景文字             |\n| `df-fill-destructive`     | 危险态填充背景 + 自动前景文字           |\n| `df-tint-primary`         | 主色 10% 轻染背景                       |\n| `df-tint-primary-md`      | 主色 20% 轻染背景                       |\n| `df-tint-primary-lg`      | 主色 30% 轻染背景                       |\n| `df-hover-primary`        | 主色语义悬停态                          |\n| `df-hover-primary-md`     | 更明显的主色悬停态                      |\n| `df-hover-primary-solid`  | 主色实心按钮的悬停态                    |\n| `df-hover-base`           | 使用 DayFlow hover token 的默认悬停背景 |\n| `df-hover-muted`          | muted 风格悬停背景                      |\n| `df-hover-destructive`    | 危险态悬停效果                          |\n| `df-text-primary`         | 主色文字                                |\n| `df-text-muted`           | 弱化文字                                |\n| `df-text-primary-fg`      | 主色填充背景上的文字                    |\n| `df-text-secondary-fg`    | 次色填充背景上的文字                    |\n| `df-text-destructive`     | 危险态文字                              |\n| `df-text-destructive-fg`  | 危险态填充背景上的文字                  |\n| `df-bg-base`              | 基础表面背景                            |\n| `df-bg-card`              | 卡片背景                                |\n| `df-bg-sidebar`           | 侧边栏风格的 muted 背景                 |\n| `df-bg-secondary`         | 次级 muted 背景                         |\n| `df-bg-tertiary`          | 第三级背景 / 分隔面                     |\n| `df-border-base`          | 基础边框                                |\n| `df-border-light`         | 更轻的柔和边框                          |\n| `df-border-strong`        | 使用主色 token 的强调边框               |\n| `df-border-primary`       | 主色边框                                |\n| `df-border-primary-soft`  | 柔和主色边框                            |\n| `df-ring-primary`         | 主色 ring 颜色 token                    |\n| `df-ring-primary-solid`   | 纯主色 ring 颜色                        |\n| `df-shadow-sm`            | 小层级阴影                              |\n| `df-shadow-md`            | 中层级阴影                              |\n| `df-shadow-primary`       | 主色阴影 token                          |\n| `df-focus-ring`           | `:focus` 时同时设置主色边框与 ring      |\n| `df-focus-ring-only`      | `:focus` 时仅应用主色 ring              |\n| `df-focus-border-primary` | `:focus` 时仅应用主色边框               |\n\n推荐优先使用这些 `df-*` 类或 `--df-color-*` 变量来做主题定制，不要再依赖 `bg-primary`、`text-primary`、`hover:bg-primary/90` 这类旧的内部 Tailwind 语义类名。\n\n---\n\n## 布局与动效辅助类\n\n这些类适合在自定义 slot 内容或插件 UI 中复用 DayFlow 现有的布局和入场动画行为。\n\n| 类名                      | 用途                             |\n| ------------------------- | -------------------------------- |\n| `df-content-slot-stacked` | 让内容插槽纵向堆叠并填满可用高度 |\n| `df-scrollbar-hide`       | 隐藏滚动条但保留滚动行为         |\n| `df-animate-in`           | 入场动画的基础时间参数类         |\n| `df-fade-in`              | 淡入动画辅助类                   |\n| `df-zoom-in-95`           | 轻微缩放进入动画辅助类           |\n| `df-animate-slide-up`     | 向上滑入动画                     |\n| `df-animate-slide-down`   | 向下滑出动画                     |\n\n---\n\n## 日视图\n\n日视图特有的 CSS 类。\n\n| 类名                | 描述                                  |\n| ------------------- | ------------------------------------- |\n| `df-day-view`       | 日视图容器                            |\n| `df-day-event`      | 日视图中的单个事件                    |\n| `df-day-time-grid`  | 时间网格容器                          |\n| `df-time-column`    | 时间列（左侧显示小时的侧边栏）        |\n| `df-time-slot`      | 单个时间槽容器（左侧列）              |\n| `df-time-label`     | 时间标签文本（例如 \"1 PM\"）           |\n| `df-time-grid-row`  | 时间网格中的水平行                    |\n| `df-time-grid-cell` | 时间网格中的单个单元格                |\n| `df-right-panel`    | 日视图右侧面板（迷你日历 + 事件列表） |\n\n---\n\n## 周视图\n\n周视图特有的 CSS 类。\n\n| 类名                 | 描述                           |\n| -------------------- | ------------------------------ |\n| `df-week-view`       | 周视图容器                     |\n| `df-week-event`      | 周视图中的单个事件             |\n| `df-week-header`     | 带有星期名称的周头部容器       |\n| `df-week-header-row` | 包含星期名称的固定标题行       |\n| `df-week-day-cell`   | 头部中的单个星期单元格         |\n| `df-time-column`     | 时间列（左侧显示小时的侧边栏） |\n| `df-time-slot`       | 单个时间槽容器（左侧列）       |\n| `df-time-label`      | 时间标签文本                   |\n| `df-time-grid-row`   | 时间网格中的水平行             |\n| `df-time-grid-cell`  | 时间网格中的单个单元格         |\n\n---\n\n## 月视图\n\n月视图特有的 CSS 类。\n\n| 类名                             | 描述                         |\n| -------------------------------- | ---------------------------- |\n| `df-month-view`                  | 月视图容器                   |\n| `df-month-grid`                  | 月视图的主要网格容器         |\n| `df-month-day-cell`              | 月视图中的单个日期单元格     |\n| `df-month-date-number-container` | 日期数字区域的容器           |\n| `df-month-date-number`           | 月视图中的日期数字           |\n| `df-month-more-events`           | 隐藏事件的 \"+ x 更多\" 指示器 |\n| `df-month-title`                 | 滚动时的浮动月份标题         |\n\n---\n\n## 年视图\n\n年视图特有的 CSS 类。这些类特别适用于自定义年度概览中的事件外观。\n\n| 类名                      | 描述                                            |\n| ------------------------- | ----------------------------------------------- |\n| `df-year-event`           | 事件外层容器 - 包裹整个事件元素                 |\n| `df-event-year-content`   | 年视图事件内容容器 - 包含图标、标题和其他内容   |\n| `df-event-year-title`     | 年视图事件标题文本 - 用于自定义标题对齐、字体等 |\n| `df-event-icon-svg`       | 事件渲染中通用的 SVG 图标类，年视图也会复用     |\n| `df-event-year-indicator` | 年视图定时事件颜色指示器 - 颜色点/条            |\n| `df-year-grid-month`      | Grid Year View 中的月份卡片容器                 |\n| `df-year-fixed-day-cell`  | Fixed Week Year View 中的日期单元格             |\n\n---\n\n## 迷你日历\n\n迷你日历组件（通常位于侧边栏或日视图）的 CSS 类。\n\n| 类名                           | 描述                              |\n| ------------------------------ | --------------------------------- |\n| `df-mini-calendar`             | 迷你日历容器                      |\n| `df-mini-calendar-body`        | 迷你日历主体包裹层                |\n| `df-mini-calendar-header-nav`  | 月份导航行                        |\n| `df-mini-calendar-nav-btn`     | 上一月 / 下一月按钮               |\n| `df-mini-calendar-month-label` | 当前月份标签                      |\n| `df-mini-calendar-grid`        | 日期网格容器                      |\n| `df-mini-calendar-header`      | 星期标题单元格（例如 \"一\", \"二\"） |\n| `df-mini-calendar-day`         | 迷你日历日期按钮的基础类          |\n| `df-mini-calendar-day-cell`    | 日期单元格的视觉表面              |\n| `df-mini-calendar-day-number`  | 日期数字标签                      |\n| `df-mini-calendar-dots`        | 日期单元格中的事件圆点容器        |\n| `df-mini-calendar-dot`         | 单个事件圆点                      |\n\n---\n\n## 侧边栏\n\n侧边栏组件的 CSS 类。\n\n| 类名                      | 描述                      |\n| ------------------------- | ------------------------- |\n| `df-sidebar`              | 侧边栏容器                |\n| `df-sidebar-header`       | 侧边栏头部容器            |\n| `df-sidebar-toggle`       | 侧边栏折叠/展开切换按钮   |\n| `df-sidebar-header-title` | 侧边栏头部标题（\"日历\"）  |\n| `df-sidebar-list-shell`   | 侧边栏列表的可滚动包裹层  |\n| `df-sidebar-list`         | 日历列表容器              |\n| `df-sidebar-list-item`    | 列表中的单个日历项        |\n| `df-sidebar-row`          | 每个列表项里的交互行      |\n| `df-sidebar-chip`         | 小型标签 / chip           |\n| `df-sidebar-dialog`       | 侧边栏模态框 / 对话框容器 |\n| `df-sidebar-button`       | 侧边栏按钮的共享类        |\n\n---\n\n## 导航\n\n导航按钮的 CSS 类。\n\n| 类名                       | 描述                                  |\n| -------------------------- | ------------------------------------- |\n| `df-nav-button`            | 导航箭头按钮的基础类（上一个/下一个） |\n| `df-calendar-nav-button`   | 日历导航箭头按钮的样式类              |\n| `df-today-button`          | “今天”按钮的基础类                    |\n| `df-calendar-today-button` | 日历“今天”按钮的样式类                |\n\n---\n\n## 事件详情\n\n事件详情面板和对话框的 CSS 类。\n\n| 类名                       | 描述                            |\n| -------------------------- | ------------------------------- |\n| `df-event-detail-panel`    | 浮动事件详情面板                |\n| `df-event-dialog-overlay`  | 事件详情对话框的 overlay 根节点 |\n| `df-event-dialog-backdrop` | 事件详情对话框的背景遮罩        |\n| `df-dialog-container`      | 对话框内容容器                  |\n| `df-event-dialog-close`    | 事件详情对话框的关闭按钮        |\n| `df-portal`                | 浮层根节点作用域                |\n\n---\n\n## 内容插槽\n\n用于内容插槽渲染系统的 CSS 类。\n\n| 类名              | 描述                                                 |\n| ----------------- | ---------------------------------------------------- |\n| `df-content-slot` | 内容插槽的容器                                       |\n| `df-slot-[id]`    | 分配给特定插槽实例的动态类，其中 `[id]` 是唯一标识符 |\n\n---\n\n## 事件状态属性\n\nDayFlow 暴露的不只是事件状态标记。当前项目里，事件、网格、浮层、抽屉以及部分内部布局辅助节点都会使用 `data-*` 属性。下面这份表覆盖了当前代码里实际存在的属性。其中大多数可以直接拿来写 CSS，少数则主要用于浮层控制或 click-outside 这类结构化逻辑。\n\n### 事件元素\n\n| 属性                   | 可选值                                         | 描述                                            |\n| ---------------------- | ---------------------------------------------- | ----------------------------------------------- |\n| `data-view`            | `\"day\"` `\"week\"` `\"month\"` `\"year\"` `\"agenda\"` | 当前正在渲染该事件或事件片段的视图              |\n| `data-all-day`         | `\"true\"` `\"false\"`                             | 是否为全天事件                                  |\n| `data-selected`        | `\"true\"` `\"false\"`                             | 事件是否处于选中状态                            |\n| `data-dragging`        | `\"true\"` `\"false\"`                             | 事件是否正在被拖拽                              |\n| `data-resizing`        | `\"true\"` `\"false\"`                             | 事件是否正在被调整大小                          |\n| `data-popping`         | `\"true\"` `\"false\"`                             | 交互时事件是否处于弹起 / 提升的视觉状态         |\n| `data-editable`        | `\"true\"` `\"false\"`                             | 是否允许通过内置 UI 编辑                        |\n| `data-viewable`        | `\"true\"` `\"false\"`                             | 是否允许打开事件详情 UI                         |\n| `data-draggable`       | `\"true\"` `\"false\"`                             | 该事件是否启用拖拽                              |\n| `data-multi-day`       | `\"true\"` `\"false\"`                             | 是否为跨天事件                                  |\n| `data-month-stack`     | `\"true\"` `\"false\"`                             | 是否处于月视图的堆叠布局中                      |\n| `data-touch-handles`   | `\"true\"` `\"false\"`                             | 是否需要显示 / 预留触控调整手柄                 |\n| `data-axis`            | 视图相关关键字                                 | 紧凑 / timed 事件内容的方向提示                 |\n| `data-density`         | 视图相关关键字                                 | 紧凑事件内容的密度提示                          |\n| `data-position`        | 视图相关关键字                                 | 分段或紧凑事件渲染中的位置提示                  |\n| `data-segment-shape`   | `\"full\"` `\"start\"` `\"middle\"` `\"end\"`          | 多段事件的圆角形状                              |\n| `data-segment-days`    | 数字字符串                                     | 月视图片段所覆盖的天数                          |\n| `data-event-id`        | 事件 id 字符串                                 | 挂在交互事件节点 / 面板锚点节点上的原始事件标识 |\n| `data-continued-left`  | `\"true\"` `\"false\"`                             | Agenda 全天 badge 是否从前一天延续而来          |\n| `data-continued-right` | `\"true\"` `\"false\"`                             | Agenda 全天 badge 是否延续到后一天              |\n\n### 日期、网格与布局状态\n\n| 属性                     | 可选值                 | 描述                                              |\n| ------------------------ | ---------------------- | ------------------------------------------------- |\n| `data-today`             | `\"true\"` `\"false\"`     | 标记今天对应的日期、section 或头部单元格          |\n| `data-other-month`       | `\"true\"` `\"false\"`     | 标记属于上个月或下个月的日期                      |\n| `data-current-month`     | `\"true\"` `\"false\"`     | 标记当前月份范围内的日期                          |\n| `data-date`              | 日期字符串             | 挂在网格 / 年视图单元格上的具体日期值             |\n| `data-first-day`         | `\"true\"` `\"false\"`     | 标记分组年/月结构中的第一个可见日期               |\n| `data-weekend`           | `\"true\"` `\"false\"`     | 是否为周末单元格 / 标签                           |\n| `data-has-events`        | `\"true\"` `\"false\"`     | 单元格内是否包含至少一个事件                      |\n| `data-heat-level`        | 数字字符串             | 网格年视图热力等级                                |\n| `data-visible`           | `\"true\"` `\"false\"`     | 用于虚拟化或条件渲染的通用可见状态标记            |\n| `data-layout`            | 布局关键字             | 月视图 / 虚拟滚动渲染使用的内部布局模式           |\n| `data-layout-ready`      | `\"true\"` `\"false\"`     | 虚拟布局是否已完成首轮测量                        |\n| `data-trailing-border`   | `\"true\"` `\"false\"`     | 月视图单元格是否应绘制尾部边框                    |\n| `data-scrollbar-space`   | `\"true\"` `\"false\"`     | 当前视图是否为滚动条预留空间                      |\n| `data-secondary-tz`      | `\"true\"` `\"false\"`     | 是否启用了辅助时区轴                              |\n| `data-show-all-day`      | `\"true\"` `\"false\"`     | 是否显示全天行                                    |\n| `data-sliding-view`      | `\"true\"` `\"false\"`     | 视图是否处于滑动 / 过渡状态                       |\n| `data-mobile`            | `\"true\"` `\"false\"`     | 某些紧凑片段使用的移动端渲染提示                  |\n| `data-switcher-mode`     | `\"buttons\"` `\"select\"` | 内置视图切换器当前使用的模式                      |\n| `data-inside-pill`       | `\"true\"` `\"false\"`     | Compact header 内容是否被渲染进 today pill        |\n| `data-sidebar-collapsed` | `\"true\"` `\"false\"`     | 侧边栏是否折叠                                    |\n| `data-sidebar-enabled`   | `\"true\"` `\"false\"`     | 当前 calendar shell 是否带着 sidebar 插件一起挂载 |\n| `data-df-theme`          | 主题关键字             | 主题化布局包装节点发出的主题标记                  |\n\n### 控件、输入与搜索\n\n| 属性                     | 可选值                             | 描述                                           |\n| ------------------------ | ---------------------------------- | ---------------------------------------------- |\n| `data-active`            | `\"true\"` `\"false\"`                 | 通用 active 状态标记，用于开关、tab、picker 等 |\n| `data-open`              | `\"true\"` `\"false\"`                 | 下拉、抽屉或搜索区域是否处于打开状态           |\n| `data-bordered`          | `\"true\"` `\"false\"`                 | header 是否绘制底部边框                        |\n| `data-loading`           | `\"true\"` `\"false\"`                 | 异步按钮 / 操作的加载状态                      |\n| `data-ready`             | `\"true\"` `\"false\"`                 | quick-create 浮层是否完成初始定位              |\n| `data-placement`         | 位置关键字                         | popup / quick-create 的定位提示                |\n| `data-checked`           | `\"true\"` `\"false\"`                 | 自定义开关的选中状态                           |\n| `data-disabled`          | `\"true\"` `\"false\"`                 | 自定义开关 / 输入的禁用状态                    |\n| `data-kind`              | 组件相关关键字                     | mobile event drawer 中的区块 / 字段类型        |\n| `data-expanded`          | `\"true\"` `\"false\"`                 | 抽屉区块是否展开                               |\n| `data-closing`           | `\"true\"` `\"false\"`                 | mobile drawer 是否处于关闭过渡阶段             |\n| `data-tone`              | `\"default\"` `\"today\"` `\"upcoming\"` | 搜索结果日期分组标题使用的语义色调             |\n| `data-mini-calendar-dot` | 存在属性                           | mini calendar 事件圆点的标记                   |\n\n### Sidebar 插件状态\n\n| 属性                   | 可选值             | 描述                                           |\n| ---------------------- | ------------------ | ---------------------------------------------- |\n| `data-active`          | `\"true\"` `\"false\"` | 侧边栏中的日历行是否处于 active 状态           |\n| `data-collapsed`       | `\"true\"` `\"false\"` | 侧边栏 source group 或折叠面板是否处于收起状态 |\n| `data-draggable`       | `\"true\"` `\"false\"` | 侧边栏日历项是否允许拖拽                       |\n| `data-dragging`        | `\"true\"` `\"false\"` | 侧边栏日历项当前是否正在拖拽                   |\n| `data-position`        | `\"top\"` `\"bottom\"` | 侧边栏内拖拽排序时 drop indicator 所在的位置   |\n| `data-selected`        | `\"true\"` `\"false\"` | 侧边栏下拉菜单 / import 对话框中的选中状态     |\n| `data-submenu-content` | 存在属性           | 标记 sidebar 子菜单 portal 内容节点            |\n\n### 浮层与交互钩子\n\n| 属性                            | 可选值   | 描述                             |\n| ------------------------------- | -------- | -------------------------------- |\n| `data-event-detail-panel`       | 存在属性 | 浮动事件详情面板根节点           |\n| `data-event-detail-dialog`      | 存在属性 | 模态事件详情对话框根节点         |\n| `data-range-picker-popup`       | 存在属性 | range picker 弹层根节点          |\n| `data-calendar-picker-dropdown` | 存在属性 | calendar picker 下拉层根节点     |\n| `data-grid-day-cell`            | 存在属性 | 网格型年视图中的可点击日期单元格 |\n| `data-grid-day-popup`           | 存在属性 | 年视图日期 popup 根节点          |\n\n### 示例\n\n```css\n/* 选中状态的事件 */\n.df-event[data-selected='true'] {\n  outline: 2px solid var(--df-color-primary);\n}\n\n/* 拖拽中的事件 */\n.df-event[data-dragging='true'] {\n  opacity: 0.6;\n}\n\n/* 仅针对月视图的事件 */\n.df-event[data-view='month'] {\n  font-size: 11px;\n}\n\n/* 仅针对全天事件 */\n.df-event[data-all-day='true'] {\n  font-weight: 600;\n}\n\n/* 强调今天对应的 Agenda section 或紧凑网格单元格 */\n[data-today='true'] {\n  background-color: color-mix(in srgb, var(--df-color-primary) 8%, transparent);\n}\n```\n\n---\n\n## 如何自定义\n\n要自定义外观，建议优先覆盖这些类或 `--df-color-*` 变量。大多数主题层面的调整不需要 `!important`；只有在你明确要压过某条高特异性局部规则时才需要它。\n\n### 示例\n\n```css\n/* 更改所有事件的圆角 */\n.df-event {\n  border-radius: 8px !important;\n}\n\n/* 自定义当前时间指示器 */\n.df-current-time-line {\n  z-index: 100;\n}\n.df-current-time-bar {\n  height: 3px !important;\n  background-color: #ef4444 !important;\n}\n\n/* 自定义特定视图 */\n.df-day-view .df-event {\n  border-left: 4px solid #3b82f6;\n}\n\n/* 设置侧边栏样式 */\n.df-sidebar {\n  background-color: #f8fafc !important;\n}\n\n/* 设置迷你日历样式 */\n.df-mini-calendar-day:hover {\n  background-color: #e2e8f0;\n}\n\n/* 在自定义 slot / 插件 UI 中复用 DayFlow 的语义类 */\n.my-custom-primary-chip {\n  background-color: var(--df-color-primary);\n  color: var(--df-color-primary-foreground);\n}\n\n/* 深色模式覆盖 */\n.dark .df-calendar {\n  background-color: #111827;\n}\n\n/* 设置事件详情面板样式 */\n.df-event-detail-panel {\n  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15) !important;\n}\n```\n\n## 使用技巧\n\n1. **特异性**: 如果样式未生效，可以通过在日历外层包裹一个自定义类来增加特异性：\n\n   ```css\n   .my-custom-calendar .df-event { ... }\n   ```\n\n2. **响应式设计**: 使用媒体查询为移动设备调整样式：\n\n   ```css\n   @media (max-width: 768px) {\n     .df-event-title {\n       font-size: 10px;\n     }\n     .df-time-label {\n       font-size: 10px;\n     }\n   }\n   ```\n\n3. **深色模式**: 使用 `.dark` 选择器为深色模式设置特定样式：\n\n   ```css\n   .dark .df-month-day-cell {\n     border-color: #374151;\n   }\n   ```\n\n4. **CSS 变量**: DayFlow 使用 `--df-color-*` 自定义属性来设置颜色。在 `.df-calendar-container`（和 `.df-portal` 用于浮层）上覆盖它们，可以实现作用域隔离的颜色修改：\n   ```css\n   .df-calendar-container,\n   .df-portal {\n     --df-color-primary: #3b82f6;\n     --df-color-primary-foreground: #ffffff;\n   }\n   ```\n\n## 从旧版本升级\n\n<Callout title=\"破坏性变更：calendar-event 类已移除\" type=\"warn\">\n  事件元素上的 `calendar-event` 类已被移除。如果你的样式表中有 `.calendar-event { ... }`，请改为 `.df-event { ... }`。\n\n之前出现在 DayFlow DOM 元素上的 Tailwind utility 类（`flex`、`rounded-md`、`shadow-sm` 等）也已被全部移除。这些类从未属于公开 API。请使用文档中列出的 `df-*` 类和 `data-*` 属性进行所有 CSS 自定义。\n\n</Callout>\n\n## 相关文档\n\n- [主题自定义](/docs-zh/guides/theme-customization) - 高级主题选项\n- [深色模式](/docs-zh/features/dark-mode) - 深色模式配置\n"
  },
  {
    "path": "website/content/docs-zh/guides/meta.json",
    "content": "{\n  \"title\": \"指南\",\n  \"pages\": [\"global-css\", \"theme-customization\", \"timezones\"]\n}\n"
  },
  {
    "path": "website/content/docs-zh/guides/theme-customization.mdx",
    "content": "# 主题定制指南\n\nDayFlow Calendar 的外观由一组带命名空间的 CSS 自定义属性（`--df-color-*`）控制。所有 token 都定义在 `@layer df-theme` 内部，因此宿主应用无需使用 `!important` 即可覆盖它们——无论是否使用 Tailwind 都适用。\n\nDayFlow 的共享基础样式是 token、组件基础规则和 `df-*` 语义辅助类的统一来源，例如 `df-fill-primary`、`df-tint-primary`、`df-focus-ring` 等。\n\n## 目录\n\n- [日历类型自定义颜色](#日历类型自定义颜色)\n- [CSS 变量参考](#css-变量参考)\n- [覆盖方法](#覆盖方法)\n- [Tailwind v4 集成](#tailwind-v4-集成)\n- [创建自定义主题](#创建自定义主题)\n\n## 日历类型自定义颜色\n\n### 基础自定义颜色\n\n为每个日历类型定义独立的浅色和深色配色方案：\n\n```tsx\nconst calendar = useCalendarApp({\n  calendars: [\n    {\n      id: 'personal',\n      name: '个人日程',\n      colors: {\n        lineColor: '#0891b2', // cyan-600\n        eventColor: '#cffafe', // cyan-100\n        eventSelectedColor: '#a5f3fc', // cyan-200\n        textColor: '#164e63', // cyan-900\n      },\n      darkColors: {\n        lineColor: '#22d3ee', // cyan-400\n        eventColor: '#164e63', // cyan-900\n        eventSelectedColor: '#083344', // cyan-950\n        textColor: '#cffafe', // cyan-100\n      },\n    },\n  ],\n});\n```\n\n### 颜色属性说明\n\n- **lineColor**：边框和强调色（事件左侧竖条）\n- **eventColor**：事件背景填充色\n- **eventSelectedColor**：选中事件时的背景填充色\n- **textColor**：事件标题和时间的文字颜色\n\n### 品牌颜色集成\n\n```tsx\n{\n  id: 'brand',\n  name: '品牌活动',\n  colors: {\n    lineColor: '#6366f1',       // 品牌靛蓝色\n    eventColor: '#e0e7ff', // Indigo-100\n    eventSelectedColor: '#c7d2fe', // Indigo-200\n    textColor: '#312e81',       // Indigo-900\n  },\n  darkColors: {\n    lineColor: '#a5b4fc',       // Indigo-300\n    eventColor: '#312e81', // Indigo-900\n    eventSelectedColor: '#1e1b4b', // Indigo-950\n    textColor: '#e0e7ff',       // Indigo-100\n  },\n}\n```\n\n### 颜色生成工具函数\n\n```tsx\n// utils/colorGenerator.ts\ninterface ColorSet {\n  lineColor: string;\n  eventColor: string;\n  eventSelectedColor: string;\n  textColor: string;\n}\n\nexport function generateLightColors(baseColor: string): ColorSet {\n  return {\n    lineColor: baseColor,\n    eventColor: lighten(baseColor, 0.9),\n    eventSelectedColor: lighten(baseColor, 0.8),\n    textColor: darken(baseColor, 0.4),\n  };\n}\n\nexport function generateDarkColors(baseColor: string): ColorSet {\n  return {\n    lineColor: lighten(baseColor, 0.3),\n    eventColor: darken(baseColor, 0.6),\n    eventSelectedColor: darken(baseColor, 0.7),\n    textColor: lighten(baseColor, 0.8),\n  };\n}\n```\n\n## CSS 变量参考\n\n所有 DayFlow 主题 token 均使用 `--df-color-` 前缀，以避免与宿主应用的变量命名冲突。\n\n| 变量                                | 用途                         |\n| ----------------------------------- | ---------------------------- |\n| `--df-color-background`             | 日历背景色                   |\n| `--df-color-foreground`             | 主文本颜色                   |\n| `--df-color-hover`                  | 悬停状态背景色               |\n| `--df-color-border`                 | 边框和分割线                 |\n| `--df-color-card`                   | 卡片 / 面板背景色            |\n| `--df-color-card-foreground`        | 卡片内文本颜色               |\n| `--df-color-muted`                  | 低调背景区域                 |\n| `--df-color-muted-foreground`       | 辅助 / 次要文本颜色          |\n| `--df-color-primary`                | 主强调色（按钮、选中状态等） |\n| `--df-color-primary-foreground`     | 主色背景上的文本             |\n| `--df-color-secondary`              | 次要强调色                   |\n| `--df-color-secondary-foreground`   | 次要色背景上的文本           |\n| `--df-color-destructive`            | 破坏性操作（删除等）         |\n| `--df-color-destructive-foreground` | 破坏性背景上的文本           |\n\n## 语义辅助类\n\n除了 CSS 变量之外，DayFlow 现在还暴露了一套主题感知的 `df-*` 语义辅助类，适合在 slot、自定义弹层、插件 UI 中复用，使外观与内置控件保持一致。\n\n| 类名                     | 含义                          |\n| ------------------------ | ----------------------------- |\n| `df-fill-primary`        | 主色实心背景 + 自动前景文字   |\n| `df-fill-secondary`      | 次色实心背景 + 自动前景文字   |\n| `df-fill-destructive`    | 危险态实心背景 + 自动前景文字 |\n| `df-tint-primary`        | 主色轻染选中态                |\n| `df-hover-primary`       | 主色语义悬停态                |\n| `df-hover-primary-solid` | 主色实心按钮悬停态            |\n| `df-text-primary`        | 主色文字                      |\n| `df-border-primary`      | 主色边框                      |\n| `df-ring-primary`        | 主色 ring token               |\n| `df-focus-ring`          | 焦点时同时设置主色边框与 ring |\n\n推荐优先使用这些 `df-*` 类或 `--df-color-*` 变量，不要再依赖 `bg-primary`、`text-primary`、`hover:bg-primary/90` 等旧的内部 Tailwind 语义类名。\n\n## 覆盖方法\n\n### 方法 1 — 容器级覆盖（适用于所有场景）\n\n直接在 `.df-calendar-container` 上设置变量。由于该规则不属于任何 `@layer`，它始终优先于库的默认值，与 Tailwind 版本无关。\n\n```css\n/* styles/globals.css */\n.df-calendar-container {\n  --df-color-primary: #6366f1;\n  --df-color-background: #f9fafb;\n  --df-color-border: #e0e7ff;\n}\n\n/* 深色模式 — 当祖先元素带有 .dark 类时生效 */\n.dark .df-calendar-container {\n  --df-color-primary: #a5b4fc;\n  --df-color-background: #1e1e2e;\n  --df-color-border: #312e81;\n}\n```\n\n大多数项目推荐使用此方法。变量仅作用于日历组件内部，不会影响外部元素。\n\n## Tailwind v4 集成\n\nTailwind v4 完全通过 CSS 配置，不再需要 `tailwind.config.js`。DayFlow 分发的 CSS 已经包含共享基础样式，因此主题 token 和 `df-*` 语义类在导入后即可使用。\n\n### 选择正确的 CSS 文件\n\nDayFlow 提供两个 CSS 产物：\n\n| 文件                    | 内容                                       | 适用场景               |\n| ----------------------- | ------------------------------------------ | ---------------------- |\n| `styles.css`            | 完整包，含 Tailwind preflight（CSS Reset） | 不使用 Tailwind 的项目 |\n| `styles.components.css` | 仅含组件样式，**不含 CSS Reset**           | 已使用 Tailwind 的项目 |\n\n### 最小配置\n\n```css\n/* app.css — Tailwind 项目 */\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\n如果项目**不使用** Tailwind，改为导入完整包：\n\n```css\n/* app.css — 非 Tailwind 项目 */\n@import '@dayflow/core/dist/styles.css';\n```\n\n### 深色模式\n\nDayFlow 监听任意祖先元素（包括 `<html>`）上的 `.dark` 类，通过 JavaScript 进行切换：\n\n```ts\ndocument.documentElement.classList.toggle('dark', isDark);\n```\n\n无需显式设置类名时，也会自动响应系统级深色模式偏好。\n\n**在 Tailwind v4 中使用 `theme.mode`**\n\nDayFlow 发布的 CSS 已经内置了 `theme.mode` 所需的 `.dark` variant。在 Tailwind v4 项目里，只要导入 `@dayflow/core/dist/styles.components.css`，DayFlow 自身就会响应 `<html>` 上的 `.dark` 类切换。\n\n如果你还希望项目里你自己写的 Tailwind `dark:` 工具类也跟随这个类名切换，再在 CSS 入口里补上 class-based 配置：\n\n```css\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n\n/* 可选：只有你自己的 Tailwind dark: 工具类需要跟随 .dark 时才需要 */\n@variant dark (.dark &);\n```\n\n如果不加这行，DayFlow 仍会通过 `theme.mode` 正常切换；只是你项目自己的 Tailwind `dark:` 工具类仍然保持 Tailwind 默认的媒体查询行为。\n\n### 实践建议\n\n大多数项目可以按下面的优先级来定制主题：\n\n1. 在 `.df-calendar-container`、`.df-portal` 或你的包裹层上覆盖 `--df-color-*`。\n2. 在自定义 slot / 插件 UI 中复用 `df-fill-primary`、`df-focus-ring` 等 `df-*` 语义类。\n3. 只有在你要改布局而不是主题时，才去覆盖更底层的 utility 行为。\n\n下面是 3 个对应的常见示例：\n\n#### 示例 1：优先覆盖 `--df-color-*` token\n\n适合品牌色替换、浅色/深色主题统一、整套日历换肤。\n\n这里同时覆盖 `.df-calendar-container` 和 `.df-portal`：\n\n- `.df-calendar-container` 是日历主体根容器\n- `.df-portal` 是 DayFlow 通过 portal 渲染到 `document.body` 下的浮层根类，例如详情弹窗、下拉层、部分选择器面板\n\n如果你只覆盖 `.df-calendar-container`，主体区域会换肤，但这类浮层可能仍然使用默认 token。\n\n```css\n/* .my-calendar-shell 是你自己项目里的包裹层类名，不是 DayFlow 内置类 */\n.my-calendar-shell .df-calendar-container,\n.my-calendar-shell .df-portal {\n  --df-color-primary: #0f766e;\n  --df-color-primary-foreground: #ffffff;\n  --df-color-background: #f8fafc;\n  --df-color-border: #cbd5e1;\n}\n\n.dark .my-calendar-shell .df-calendar-container,\n.dark .my-calendar-shell .df-portal {\n  --df-color-primary: #5eead4;\n  --df-color-primary-foreground: #042f2e;\n  --df-color-background: #0f172a;\n  --df-color-border: #334155;\n}\n```\n\n```tsx\n<div className='my-calendar-shell'>\n  <DayFlowCalendar calendar={calendar} />\n</div>\n```\n\n#### 示例 2：在自定义 UI 中复用 `df-*` 语义类\n\n适合 content slot、自定义弹层、插件按钮，让它们和 DayFlow 内置按钮保持一致。\n\n```tsx\nfunction CustomToolbar() {\n  return (\n    <div className='flex items-center gap-2'>\n      <button className='df-fill-primary rounded-md px-3 py-1.5 text-sm font-medium df-hover-primary-solid'>\n        新建事件\n      </button>\n      <button className='rounded-md border px-3 py-1.5 text-sm df-focus-ring'>\n        聚焦搜索\n      </button>\n      <span className='df-tint-primary rounded-full px-2 py-0.5 text-xs font-medium'>\n        已连接\n      </span>\n    </div>\n  );\n}\n```\n\n#### 示例 3：只改布局时再覆盖低层样式\n\n适合调节间距、圆角、尺寸，不改变主题语义本身。\n\n```css\n/* .my-compact-calendar 同样是你自己定义的包裹层类名 */\n.my-compact-calendar .df-header {\n  padding-block: 0.25rem;\n}\n\n.my-compact-calendar .df-event {\n  border-radius: 6px;\n}\n\n.my-compact-calendar .df-mini-calendar-day {\n  width: 1.75rem;\n  height: 1.75rem;\n}\n```\n\n如果你的目标是“换色”，优先用示例 1；如果是“让自定义内容看起来像 DayFlow 内置控件”，优先用示例 2；如果只是“调整尺寸和间距”，再使用示例 3。\n\n### 使用 Tailwind 工具类包裹日历\n\n```tsx\nfunction ThemedCalendar({ calendar }) {\n  return (\n    <div className='rounded-2xl shadow-xl ring-1 ring-gray-200 dark:ring-gray-700'>\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n}\n```\n\n## 创建自定义主题\n\n```tsx\n// themes/oceanTheme.ts\nexport const oceanTheme = {\n  mode: 'light' as const,\n  calendars: [\n    {\n      id: 'deep-ocean',\n      name: '深海蓝',\n      colors: {\n        lineColor: '#0369a1',\n        eventColor: '#e0f2fe',\n        eventSelectedColor: '#bae6fd',\n        textColor: '#0c4a6e',\n      },\n      darkColors: {\n        lineColor: '#7dd3fc',\n        eventColor: '#0c4a6e',\n        eventSelectedColor: '#083344',\n        textColor: '#e0f2fe',\n      },\n    },\n    {\n      id: 'coral-reef',\n      name: '珊瑚礁',\n      colors: {\n        lineColor: '#ea580c',\n        eventColor: '#ffedd5',\n        eventSelectedColor: '#fed7aa',\n        textColor: '#7c2d12',\n      },\n      darkColors: {\n        lineColor: '#fb923c',\n        eventColor: '#7c2d12',\n        eventSelectedColor: '#431407',\n        textColor: '#ffedd5',\n      },\n    },\n  ],\n};\n```\n\n配合 CSS 变量覆盖使用，可实现完整的视觉一致性：\n\n```css\n/* Ocean 主题 CSS token */\n.df-calendar-container {\n  --df-color-primary: #0369a1;\n  --df-color-background: #f0f9ff;\n  --df-color-border: #bae6fd;\n}\n\n.dark .df-calendar-container {\n  --df-color-primary: #7dd3fc;\n  --df-color-background: #0c1220;\n  --df-color-border: #0c4a6e;\n}\n```\n\n## 资源\n\n### 工具\n\n- [WebAIM 对比度检查器](https://webaim.org/resources/contrastchecker/) - 检验颜色对比度\n- [Coolors](https://coolors.co/) - 生成配色方案\n- [颜色对比度分析器](https://www.tpgi.com/color-contrast-checker/) - 桌面工具\n\n### 库\n\n- [chroma-js](https://gka.github.io/chroma.js/) - 颜色操作\n- [tinycolor2](https://github.com/bgrins/TinyColor) - 颜色工具\n- [color](https://github.com/Qix-/color) - 颜色转换\n\n### Tailwind 资源\n\n- [Tailwind v4 升级指南](https://tailwindcss.com/docs/upgrade-guide) - v3 → v4 迁移\n- [Tailwind CSS 层](https://tailwindcss.com/docs/adding-custom-styles#using-css-and-layer) - 层级文档\n\n## 相关文档\n\n- [深色模式](/docs-zh/features/dark-mode) - 深色模式概览与 API\n- [日历类型](/docs-zh/introduction/events#calendar-types) - 事件分类\n- [使用日历应用](/docs-zh/introduction/use-calendar-app) - 核心配置\n"
  },
  {
    "path": "website/content/docs-zh/guides/timezones.mdx",
    "content": "# 时区列表\n\nDayFlow 提供了一个 `TimeZone` 枚举，方便您同时配置全局 `timeZone` 和仅用于 Day/Week 的 `secondaryTimeZone`，无需记忆原始 IANA 时区字符串。\n\n## 时区模型\n\n- `timeZone`：整个日历的主显示/编辑时区\n- `secondaryTimeZone`：仅在 Day / Week 视图中显示的辅助参考时间轴\n\n切换 `timeZone` 会影响所有视图；切换 `secondaryTimeZone` 只会影响 Day / Week 中的第二条时间轴。\n\n## 回调语义\n\n- `timeZone` 会改变事件在 UI 中的显示方式，以及用户编辑时使用的墙上时间坐标。\n- 仅仅切换 `timeZone` 本身不会触发 `onEventUpdate`、`onEventDrop` 或 `onEventResize`。\n- 更新类回调返回的是编辑完成后的 canonical event，而不是当前界面投影时区下的临时显示值。\n- 也就是说，如果一个 Sydney 事件在 Shanghai 视图里被调整大小，编辑动作会按 Shanghai 的墙上时间解释，但回调参数会在持久化前转换回事件的 canonical 时间表达。\n\n## 使用方法\n\n```tsx\nimport { TimeZone } from '@dayflow/core';\nimport {\n  useCalendarApp,\n  createDayView,\n  createWeekView,\n  createMonthView,\n} from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  timeZone: TimeZone.SHANGHAI,\n  views: [\n    createDayView({\n      secondaryTimeZone: TimeZone.NEW_YORK,\n    }),\n    createWeekView({\n      secondaryTimeZone: TimeZone.NEW_YORK,\n    }),\n    createMonthView(),\n  ],\n});\n```\n\n## 可用时区\n\n| 枚举键                   | IANA 字符串                      |\n| :----------------------- | :------------------------------- |\n| **UTC/GMT**              |                                  |\n| `TimeZone.UTC`           | `UTC`                            |\n| **北美洲**               |                                  |\n| `TimeZone.NEW_YORK`      | `America/New_York`               |\n| `TimeZone.CHICAGO`       | `America/Chicago`                |\n| `TimeZone.DENVER`        | `America/Denver`                 |\n| `TimeZone.LOS_ANGELES`   | `America/Los_Angeles`            |\n| `TimeZone.TORONTO`       | `America/Toronto`                |\n| `TimeZone.VANCOUVER`     | `America/Vancouver`              |\n| `TimeZone.PHOENIX`       | `America/Phoenix`                |\n| `TimeZone.ANCHORAGE`     | `America/Anchorage`              |\n| `TimeZone.HONOLULU`      | `Pacific/Honolulu`               |\n| `TimeZone.MEXICO_CITY`   | `America/Mexico_City`            |\n| `TimeZone.WINNIPEG`      | `America/Winnipeg`               |\n| `TimeZone.HALIFAX`       | `America/Halifax`                |\n| `TimeZone.ST_JOHNS`      | `America/St_Johns`               |\n| `TimeZone.DETROIT`       | `America/Detroit`                |\n| `TimeZone.MIAMI`         | `America/Miami`                  |\n| **欧洲**                 |                                  |\n| `TimeZone.LONDON`        | `Europe/London`                  |\n| `TimeZone.PARIS`         | `Europe/Paris`                   |\n| `TimeZone.BERLIN`        | `Europe/Berlin`                  |\n| `TimeZone.MADRID`        | `Europe/Madrid`                  |\n| `TimeZone.ROME`          | `Europe/Rome`                    |\n| `TimeZone.AMSTERDAM`     | `Europe/Amsterdam`               |\n| `TimeZone.ZURICH`        | `Europe/Zurich`                  |\n| `TimeZone.STOCKHOLM`     | `Europe/Stockholm`               |\n| `TimeZone.OSLO`          | `Europe/Oslo`                    |\n| `TimeZone.COPENHAGEN`    | `Europe/Copenhagen`              |\n| `TimeZone.MOSCOW`        | `Europe/Moscow`                  |\n| `TimeZone.ISTANBUL`      | `Europe/Istanbul`                |\n| `TimeZone.DUBLIN`        | `Europe/Dublin`                  |\n| `TimeZone.LISBON`        | `Europe/Lisbon`                  |\n| `TimeZone.PRAGUE`        | `Europe/Prague`                  |\n| `TimeZone.VIENNA`        | `Europe/Vienna`                  |\n| `TimeZone.WARSAW`        | `Europe/Warsaw`                  |\n| `TimeZone.BRUSSELS`      | `Europe/Brussels`                |\n| `TimeZone.ATHENS`        | `Europe/Athens`                  |\n| `TimeZone.BUCHAREST`     | `Europe/Bucharest`               |\n| `TimeZone.HELSINKI`      | `Europe/Helsinki`                |\n| `TimeZone.KYIV`          | `Europe/Kyiv`                    |\n| `TimeZone.BUDAPEST`      | `Europe/Budapest`                |\n| `TimeZone.BELGRADE`      | `Europe/Belgrade`                |\n| `TimeZone.LUXEMBOURG`    | `Europe/Luxembourg`              |\n| `TimeZone.MONACO`        | `Europe/Monaco`                  |\n| `TimeZone.REYKJAVIK`     | `Atlantic/Reykjavik`             |\n| **亚洲**                 |                                  |\n| `TimeZone.TOKYO`         | `Asia/Tokyo`                     |\n| `TimeZone.SHANGHAI`      | `Asia/Shanghai`                  |\n| `TimeZone.HONG_KONG`     | `Asia/Hong_Kong`                 |\n| `TimeZone.TAIPEI`        | `Asia/Taipei`                    |\n| `TimeZone.SEOUL`         | `Asia/Seoul`                     |\n| `TimeZone.SINGAPORE`     | `Asia/Singapore`                 |\n| `TimeZone.HANOI`         | `Asia/Ho_Chi_Minh`               |\n| `TimeZone.BANGKOK`       | `Asia/Bangkok`                   |\n| `TimeZone.JAKARTA`       | `Asia/Jakarta`                   |\n| `TimeZone.KUALA_LUMPUR`  | `Asia/Kuala_Lumpur`              |\n| `TimeZone.MANILA`        | `Asia/Manila`                    |\n| `TimeZone.DUBAI`         | `Asia/Dubai`                     |\n| `TimeZone.KOLKATA`       | `Asia/Kolkata`                   |\n| `TimeZone.RIYADH`        | `Asia/Riyadh`                    |\n| `TimeZone.TEHRAN`        | `Asia/Tehran`                    |\n| `TimeZone.JERUSALEM`     | `Asia/Jerusalem`                 |\n| `TimeZone.TEL_AVIV`      | `Asia/Tel_Aviv`                  |\n| `TimeZone.BAGHDAD`       | `Asia/Baghdad`                   |\n| `TimeZone.DHAKA`         | `Asia/Dhaka`                     |\n| `TimeZone.KARACHI`       | `Asia/Karachi`                   |\n| `TimeZone.KABUL`         | `Asia/Kabul`                     |\n| `TimeZone.KATHMANDU`     | `Asia/Kathmandu`                 |\n| `TimeZone.COLOMBO`       | `Asia/Colombo`                   |\n| `TimeZone.TASHKENT`      | `Asia/Tashkent`                  |\n| `TimeZone.ALMATY`        | `Asia/Almaty`                    |\n| `TimeZone.PHNOM_PENH`    | `Asia/Phnom_Penh`                |\n| `TimeZone.VIENTIANE`     | `Asia/Vientiane`                 |\n| `TimeZone.MUSCAT`        | `Asia/Muscat`                    |\n| **大洋洲**               |                                  |\n| `TimeZone.SYDNEY`        | `Australia/Sydney`               |\n| `TimeZone.MELBOURNE`     | `Australia/Melbourne`            |\n| `TimeZone.BRISBANE`      | `Australia/Brisbane`             |\n| `TimeZone.PERTH`         | `Australia/Perth`                |\n| `TimeZone.ADELAIDE`      | `Australia/Adelaide`             |\n| `TimeZone.DARWIN`        | `Australia/Darwin`               |\n| `TimeZone.HOBART`        | `Australia/Hobart`               |\n| `TimeZone.AUCKLAND`      | `Pacific/Auckland`               |\n| `TimeZone.FIJI`          | `Pacific/Fiji`                   |\n| `TimeZone.GUAM`          | `Pacific/Guam`                   |\n| `TimeZone.NOUMEA`        | `Pacific/Noumea`                 |\n| `TimeZone.PAGO_PAGO`     | `Pacific/Pago_Pago`              |\n| `TimeZone.PORT_MORESBY`  | `Pacific/Port_Moresby`           |\n| **南美洲**               |                                  |\n| `TimeZone.SAO_PAULO`     | `America/Sao_Paulo`              |\n| `TimeZone.BUENOS_AIRES`  | `America/Argentina/Buenos_Aires` |\n| `TimeZone.SANTIAGO`      | `America/Santiago`               |\n| `TimeZone.LIMA`          | `America/Lima`                   |\n| `TimeZone.BOGOTA`        | `America/Bogota`                 |\n| `TimeZone.CARACAS`       | `America/Caracas`                |\n| `TimeZone.LA_PAZ`        | `America/La_Paz`                 |\n| `TimeZone.MONTEVIDEO`    | `America/Montevideo`             |\n| `TimeZone.QUITO`         | `America/Quito`                  |\n| `TimeZone.ASUNCION`      | `America/Asuncion`               |\n| `TimeZone.GEORGETOWN`    | `America/Guyana`                 |\n| **非洲**                 |                                  |\n| `TimeZone.CAIRO`         | `Africa/Cairo`                   |\n| `TimeZone.JOHANNESBURG`  | `Africa/Johannesburg`            |\n| `TimeZone.LAGOS`         | `Africa/Lagos`                   |\n| `TimeZone.NAIROBI`       | `Africa/Nairobi`                 |\n| `TimeZone.CASABLANCA`    | `Africa/Casablanca`              |\n| `TimeZone.ALGIERS`       | `Africa/Algiers`                 |\n| `TimeZone.TUNIS`         | `Africa/Tunis`                   |\n| `TimeZone.ADDIS_ABABA`   | `Africa/Addis_Ababa`             |\n| `TimeZone.ACCRA`         | `Africa/Accra`                   |\n| `TimeZone.DAKAR`         | `Africa/Dakar`                   |\n| `TimeZone.LUANDA`        | `Africa/Luanda`                  |\n| `TimeZone.ANTANANARIVO`  | `Indian/Antananarivo`            |\n| `TimeZone.KINSHASA`      | `Africa/Kinshasa`                |\n| `TimeZone.DAR_ES_SALAAM` | `Africa/Dar_es_Salaam`           |\n| **南极洲**               |                                  |\n| `TimeZone.MCMURDO`       | `Antarctica/McMurdo`             |\n| `TimeZone.CASEY`         | `Antarctica/Casey`               |\n"
  },
  {
    "path": "website/content/docs-zh/introduction/dayflow-calendar.mdx",
    "content": "# **DayFlowCalendar**\n\n`DayFlowCalendar` 是日历组件的核心容器，负责渲染当前选中的视图、处理整体布局，并集成可选 UI 元素（如侧边栏和事件详情弹窗）。配合 `useCalendarApp` 和视图工厂函数，您可以用最少的代码快速构建功能完整的日历应用。\n\n## **基础用法**\n\n```tsx\nimport {\n  DayFlowCalendar,\n  useCalendarApp,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\nimport '@dayflow/core/dist/styles.css';\n\nexport function CalendarDemo() {\n  const calendar = useCalendarApp({\n    views: [createWeekView()],\n    defaultView: ViewType.WEEK,\n    initialDate: new Date(),\n    events: [],\n    plugins: [createSidebarPlugin()],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n该组件接收来自 `useCalendarApp` 的 `calendar` 对象，读取当前激活的视图，并渲染对应的视图组件。可以在 `DayFlowCalendar` 外部放置工具栏或其他自定义 UI，该组件本身已内置：\n\n- 可选日历侧边栏（通过 `createSidebarPlugin` 插件添加）\n- 事件详情弹窗支持（可通过 `useEventDetailDialog` 或自定义渲染器驱动）\n- 日历切换或视图变更时的自动布局更新\n\n## **属性说明**\n\n| **属性名**              | **类型**               | **说明**                                                              | **必填** |\n| ----------------------- | ---------------------- | --------------------------------------------------------------------- | -------- |\n| `calendar`              | `UseCalendarAppReturn` | `useCalendarApp` 的返回结果，提供状态管理、视图注册、侧边栏配置等能力 | 必填     |\n| `collapsedSafeAreaLeft` | `number`               | 侧边栏折叠时的左内边距（px），用于兼容 Mac 红绿灯按钮区域             | 可选     |\n| `search`                | `CalendarSearchProps`  | 内置搜索功能的配置项                                                  | 可选     |\n\n## 搜索配置\n\n`search` 属性允许您自定义内置搜索的行为。默认情况下，它会在本地搜索事件的标题和描述。\n\n```tsx\n<DayFlowCalendar\n  calendar={calendar}\n  search={{\n    debounceDelay: 500,\n    emptyText: '未找到相关事件',\n    onSearch: async keyword => {\n      // 自定义异步搜索（例如从 API 获取）\n      return fetch(`/api/search?q=${keyword}`).then(res => res.json());\n    },\n  }}\n/>\n```\n\n### Search Props\n\n| 选项            | 描述                                                                                                                                                                                                      |\n| :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `debounceDelay` | 触发搜索前的防抖延迟时间（毫秒）。                                                                                                                                                                        |\n| `onSearch`      | 基于关键词获取结果的异步函数。                                                                                                                                                                            |\n| `customSearch`  | 使用自定义逻辑在本地过滤事件的同步函数。                                                                                                                                                                  |\n| `onResultClick` | 点击搜索结果时的回调。包含一个 `defaultAction` 回调用于触发内置的跳转逻辑（跳转到事件日期、高亮该事件并在移动端自动关闭搜索界面）。如果不调用 `defaultAction()`，则需要自行处理所有的跳转和 UI 状态切换。 |\n| `emptyText`     | 未找到结果时显示的文本。                                                                                                                                                                                  |\n\n## **侧边栏配置**\n\n侧边栏通过安装并添加 `@dayflow/plugin-sidebar` 插件来启用：\n\n```tsx\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\n\nconst calendar = useCalendarApp({\n  views: [createWeekView()],\n  plugins: [\n    createSidebarPlugin({\n      width: 280,\n      initialCollapsed: false,\n      createCalendarMode: 'modal', // 'inline' 或 'modal'\n    }),\n  ],\n});\n\nreturn <DayFlowCalendar calendar={calendar} />;\n```\n\n- 当添加 `createSidebarPlugin()` 插件后，会渲染默认侧边栏，包含日历筛选器和全选控制功能\n- 可以自定义宽度、默认折叠状态，或通过 `render` 或 `renderSidebarHeader` 提供自定义渲染器。`renderSidebarHeader` 允许您在保留侧边栏整体布局的同时，仅替换头部区域（标题和折叠按钮）\n\n## **事件详情展示**\n\nDayFlow 提供三种事件详情展示方式：\n\n1. **默认面板模式** — `useEventDetailDialog` 为 `false`（默认值）时，点击事件会打开内置的浮动面板 `DefaultEventDetailPanel`。使用 `eventDetailContent` 插槽可在保留默认面板框架的同时替换面板内容。\n2. **弹窗模式** — 在 `useCalendarApp` 中设置 `useEventDetailDialog: true` 可启用内置居中模态框。使用 `eventDetailDialog` 插槽可替换为您自定义的弹窗 UI。\n3. **完全关闭** — 若您的应用通过回调或全局状态管理事件详情，可在 `useCalendarApp` 中设置 `useEventDetailPanel: false` 来完全隐藏浮动面板。\n\n```tsx\n// 隐藏内置面板 — 在其他地方使用自定义模态框\nconst calendar = useCalendarApp({\n  ...\n  useEventDetailPanel: false,\n});\n\nreturn <DayFlowCalendar calendar={calendar} />;\n```\n\n```tsx\n// 自定义面板内容（保留默认面板框架）\n<DayFlowCalendar\n  calendar={calendar}\n  eventDetailContent={({ event, onClose }) => (\n    <div className='p-4'>\n      <h2>{event.title}</h2>\n      <button onClick={onClose}>关闭</button>\n    </div>\n  )}\n/>\n```\n\n```tsx\n// 自定义弹窗（需要在 useCalendarApp 中设置 useEventDetailDialog: true）\n<DayFlowCalendar\n  calendar={calendar}\n  eventDetailDialog={({ event, isOpen, onClose }) => (\n    <MyDialog isOpen={isOpen} onClose={onClose}>\n      <EventForm event={event} />\n    </MyDialog>\n  )}\n/>\n```\n\n---\n\n总而言之，`DayFlowCalendar` 承担了协调层的职责——处理整体布局、侧边栏集成和事件详情逻辑，专注于配置 `useCalendarApp` 和构建周边的产品体验。\n\n## 相关文档\n\n- [useCalendarApp](/docs-zh/introduction/use-calendar-app) - 日历应用 hook\n- [快速开始](/docs-zh/introduction) - 设置指南\n- [视图](/docs-zh/introduction/views) - 可用视图\n"
  },
  {
    "path": "website/content/docs-zh/introduction/events.mdx",
    "content": "# **事件管理指南**\n\n事件是 Day Flow 日历的核心数据结构。本文将介绍如何创建、更新、删除和管理日历事件。\n\n## **事件接口定义**\n\n本库使用 Temporal API 处理所有日期/时间相关操作。事件支持三种 Temporal 类型：\n\n- **PlainDate**：适用于全天事件（不包含具体时间）\n- **PlainDateTime**：适用于本地事件（日期+时间，无时区） **推荐在大多数场景使用**\n- **ZonedDateTime**：适用于时区敏感事件（国际会议、航班等）\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n```\n\n| 属性          | 类型                                                                     | 描述                                                                                                                    | 必需 |\n| ------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ---- |\n| `id`          | `string`                                                                 | 事件的唯一标识符                                                                                                        | 必填 |\n| `title`       | `string`                                                                 | 在日历中显示的事件标题                                                                                                  | 必填 |\n| `start`       | `Temporal.PlainDate \\| Temporal.PlainDateTime \\| Temporal.ZonedDateTime` | 事件开始日期/时间。使用 `PlainDate` 表示全天，`PlainDateTime` 表示本地时间                                              | 必填 |\n| `end`         | `Temporal.PlainDate \\| Temporal.PlainDateTime \\| Temporal.ZonedDateTime` | 事件结束日期/时间                                                                                                       | 必填 |\n| `description` | `string`                                                                 | 可选的事件描述或备注                                                                                                    | 可选 |\n| `allDay`      | `boolean`                                                                | 是否为全天事件（默认：`false`）                                                                                         | 可选 |\n| `icon`        | `boolean \\| Node`                                                        | 事件的自定义图标。`true`（默认）：显示默认，`false`：隐藏，`Node`：自定义图标                                           | 可选 |\n| `calendarId`  | `string`                                                                 | 单日历归属的日历类型引用                                                                                                | 可选 |\n| `calendarIds` | `string[]`                                                               | 事件所属的日历 ID 列表。设置后优先于 `calendarId`。只要其中任意一个日历可见，事件就会显示。渲染时使用多色斜纹图案背景。 | 可选 |\n| `meta`        | `Record<string, any>`                                                    | 额外的自定义元数据（位置、参与者、自定义字段等）                                                                        | 可选 |\n\n## **创建事件**\n\n### **简单事件创建（推荐）**\n\n在大多数场景下，使用 `createEvent()` 和 `createAllDayEvent()` 辅助函数：\n\n```tsx\nimport { createEvent, createAllDayEvent } from '@dayflow/core';\nimport '@dayflow/core/dist/styles.css';\n\n// 本地定时事件（无时区复杂性）\nconst meeting = createEvent({\n  id: '1',\n  title: '团队会议',\n  start: new Date(2024, 9, 15, 10, 0), // 2024年10月15日 10:00\n  end: new Date(2024, 9, 15, 11, 0), // 2024年10月15日 11:00\n  calendarId: 'work',\n});\n\n// 全天事件\nconst holiday = createAllDayEvent({\n  id: '2',\n  title: '技术大会',\n  start: new Date(2024, 9, 20),\n  calendarId: 'work',\n});\n```\n\n### **高级用法：直接使用 Temporal API**\n\n如需更精细的控制，可直接使用 Temporal API：\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n\n// 使用 PlainDateTime 的本地事件（推荐）\nconst localEvent: Event = {\n  id: '1',\n  title: '团队会议',\n  start: Temporal.PlainDateTime.from({\n    year: 2024,\n    month: 10,\n    day: 15,\n    hour: 10,\n    minute: 0,\n  }),\n  end: Temporal.PlainDateTime.from({\n    year: 2024,\n    month: 10,\n    day: 15,\n    hour: 11,\n    minute: 0,\n  }),\n};\n\n// 使用 PlainDate 的全天事件\nconst allDayEvent: Event = {\n  id: '2',\n  title: '技术大会',\n  start: Temporal.PlainDate.from('2024-10-20'),\n  end: Temporal.PlainDate.from('2024-10-22'),\n  allDay: true,\n};\n\n// 使用时区感知的 ZonedDateTime 事件\nconst timezoneEvent: Event = {\n  id: '3',\n  title: '国际电话会议',\n  start: Temporal.ZonedDateTime.from('2024-10-16T14:00:00[America/New_York]'),\n  end: Temporal.ZonedDateTime.from('2024-10-16T15:00:00[America/New_York]'),\n};\n```\n\n### **包含元数据的事件**\n\n```tsx\nimport { createEvent } from '@dayflow/core';\n\nconst event = createEvent({\n  id: '3',\n  title: '客户电话',\n  description: '讨论第四季度路线图',\n  start: new Date(2024, 9, 16, 14, 0),\n  end: new Date(2024, 9, 16, 15, 0),\n  calendarId: 'work',\n  meta: {\n    location: 'Zoom 会议',\n    attendees: ['zhangsan@example.com', 'lisi@example.com'],\n    recurring: false,\n  },\n});\n```\n\n## **事件管理**\n\n### **添加事件**\n\n```tsx\n// 添加单个事件\ncalendar.addEvent(event);\n\n// 初始化时添加多个事件\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  calendars: [\n    {\n      id: 'work',\n      name: '工作',\n      colors: {\n        lineColor: '#2563eb',\n        eventColor: '#dbeafe',\n        eventSelectedColor: '#bfdbfe',\n        textColor: '#1e3a8a',\n      },\n    },\n  ],\n  events: [event1, event2, event3],\n});\n```\n\n### **更新事件**\n\n```tsx\n// 更新事件\ncalendar.updateEvent('event-id', {\n  title: '更新后的会议标题',\n  start: new Date(2024, 9, 15, 11, 0),\n  end: new Date(2024, 9, 15, 12, 0),\n});\n\n// 使用待处理状态更新（适用于调整大小操作）\ncalendar.updateEvent('event-id', updatedEvent, true);\n```\n\n### **删除事件**\n\n```tsx\n// 根据 ID 删除事件\ncalendar.deleteEvent('event-id');\n```\n\n### **获取事件**\n\n```tsx\n// 获取所有事件const events = calendar.getEvents();\n\n// 从状态中获取当前事件const { events } = calendar;\n```\n\n## **事件回调函数**\n\nDay Flow 提供回调函数来处理事件生命周期：\n\n```tsx\nimport { useCalendarApp, createMonthView, Event } from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  events: initialEvents,\n  callbacks: {\n    onEventCreate: (event: Event) => {\n      console.log('新事件已创建:', event);\n      // 与后端同步\n      api.createEvent(event);\n    },\n    onEventUpdate: (event: Event) => {\n      console.log('事件已更新:', event);\n      // 与后端同步\n      api.updateEvent(event);\n    },\n    onEventDelete: (eventId: string) => {\n      console.log('事件已删除:', eventId);\n      // 同步后端\n      api.deleteEvent(eventId);\n    },\n    onEventDoubleClick: (event: Event, e: MouseEvent) => {\n      console.log('事件被双击:', event);\n      // 使用 e.currentTarget 作为自定义弹出层的锚点\n    },\n  },\n});\n```\n\n## **事件状态管理**\n\n### **使用框架状态**\n\n您可以结合所使用框架（React, Vue, Svelte, Angular）的原生状态管理来维护事件。\n\n```tsx\nimport { useState } from 'react';\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  Event,\n} from '@dayflow/react';\n\nfunction MyCalendar() {\n  const [events, setEvents] = useState<Event[]>([]);\n\n  const calendar = useCalendarApp({\n    views: [createMonthView()],\n    events,\n    callbacks: {\n      onEventCreate: (event: Event) => {\n        setEvents(prev => [...prev, event]);\n      },\n      onEventUpdate: (event: Event) => {\n        setEvents(prev => prev.map(e => (e.id === event.id ? event : e)));\n      },\n      onEventDelete: (eventId: string) => {\n        setEvents(prev => prev.filter(e => e.id !== eventId));\n      },\n    },\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### **与后端同步**\n\n```tsx\nimport { useCalendarApp, createMonthView, Event } from '@dayflow/react';\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  events,\n  callbacks: {\n    onEventCreate: async (event: Event) => {\n      try {\n        // 在后端创建事件\n        const savedEvent = await api.createEvent(event);\n\n        // 使用后端响应更新本地状态\n        setEvents(prev => [...prev, savedEvent]);\n      } catch (error) {\n        console.error('创建事件失败:', error);\n        // 可选：回滚乐观更新\n      }\n    },\n    onEventUpdate: async (event: Event) => {\n      try {\n        await api.updateEvent(event);\n        setEvents(prev => prev.map(e => (e.id === event.id ? event : e)));\n      } catch (error) {\n        console.error('更新事件失败:', error);\n      }\n    },\n    onEventDelete: async (eventId: string) => {\n      try {\n        await api.deleteEvent(eventId);\n        setEvents(prev => prev.filter(e => e.id !== eventId));\n      } catch (error) {\n        console.error('删除事件失败:', error);\n      }\n    },\n  },\n});\n```\n\n## **通过日历类型设置事件样式**\n\n通过 `calendarId` 将事件分配到不同的日历类型来自定义外观。每个日历类型都可以有自己的配色方案和样式：\n\n```tsx\nimport { createEvent } from '@dayflow/core';\n\n// 工作事件\nconst workEvent = createEvent({\n  id: '1',\n  title: '设计评审',\n  start: new Date(2024, 9, 15, 14, 0),\n  end: new Date(2024, 9, 15, 15, 0),\n  calendarId: 'work',// 关联工作日历样式});\n\n// 个人事件\nconst personalEvent = createEvent({\n  id: '2',\n  title: '牙医预约',\n  start: new Date(2024, 9, 16, 10, 0),\n  end: new Date(2024, 9, 16, 11, 0),\n  calendarId: 'personal',// 关联个人日历样式});\n\n// 配置带颜色的日历类型\nconst calendars = [\n  {\n    id: 'work',\n    name: '工作',\n    colors: {\n      eventColor: '#3b82f6',\n      eventSelectedColor: '#2563eb',\n      lineColor: '#3b82f6',\n      textColor: '#ffffff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'personal',\n    name: '个人',\n    colors: {\n      eventColor: '#10b981',\n      eventSelectedColor: '#059669',\n      lineColor: '#10b981',\n      textColor: '#ffffff',\n    },\n    isVisible: true,\n  },\n];\n\nconst calendar = useCalendarApp({\n  views: [createMonthView()],\n  events: [workEvent, personalEvent],\n  calendars,\n});\n```\n\n## **多日历事件**\n\n### 工作原理\n\n- `calendarIds` 设置后将优先于 `calendarId`。\n- **可见性**：只要列表中任意一个日历可见，事件就会显示。\n- **视觉区分**：多日历事件使用**45° 斜纹图案**背景（每种颜色对应一个日历），左侧色条显示**多色渐变**，与单日历事件一眼区分。\n- **选中状态**：回退为主日历（`calendarIds[0]`）的纯色背景。\n\n### 示例\n\n```tsx\nimport { createEvent, useCalendarApp, createWeekView } from '@dayflow/react';\n\n// 同时归属于 \"team\" 和 \"marketing\" 两个日历\nconst sharedEvent = createEvent({\n  id: 'shared-1',\n  title: '公司全员会议',\n  start: new Date(2024, 9, 15, 10, 0),\n  end: new Date(2024, 9, 15, 11, 30),\n  calendarIds: ['team', 'marketing'], // 多日历\n});\n\n// 跨三个日历的全天事件\nconst crossCalendarDay = {\n  id: 'shared-2',\n  title: '团队外出',\n  start: Temporal.PlainDate.from('2024-10-20'),\n  end: Temporal.PlainDate.from('2024-10-22'),\n  allDay: true,\n  calendarIds: ['team', 'personal', 'travel'],\n};\n\nconst calendars = [\n  {\n    id: 'team',\n    name: '团队',\n    colors: {\n      eventColor: '#3b82f6',\n      lineColor: '#3b82f6',\n      eventSelectedColor: '#2563eb',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'marketing',\n    name: '市场',\n    colors: {\n      eventColor: '#f59e0b',\n      lineColor: '#f59e0b',\n      eventSelectedColor: '#d97706',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'personal',\n    name: '个人',\n    colors: {\n      eventColor: '#10b981',\n      lineColor: '#10b981',\n      eventSelectedColor: '#059669',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n  {\n    id: 'travel',\n    name: '旅行',\n    colors: {\n      eventColor: '#8b5cf6',\n      lineColor: '#8b5cf6',\n      eventSelectedColor: '#7c3aed',\n      textColor: '#fff',\n    },\n    isVisible: true,\n  },\n];\n\nconst calendar = useCalendarApp({\n  views: [createWeekView()],\n  events: [sharedEvent, crossCalendarDay],\n  calendars,\n});\n```\n\n## **跨多天事件**\n\n事件可以跨越多个日期：\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n\n// 持续3天的会议（全天事件）\nconst multiDayEvent: Event = {\n  id: '1',\n  title: '2024技术大会',\n  start: Temporal.PlainDate.from('2024-10-20'),\n  end: Temporal.PlainDate.from('2024-10-22'),\n  allDay: true,\n  calendarId: 'conferences',\n};\n\n// 跨越午夜的事件（定时事件）\nconst crossMidnightEvent: Event = {\n  id: '2',\n  title: '夜班',\n  start: Temporal.ZonedDateTime.from('2024-10-15T22:00:00[America/New_York]'), // 晚上10点\n  end: Temporal.ZonedDateTime.from('2024-10-16T06:00:00[America/New_York]'), // 次日早上6点\n  calendarId: 'shifts',\n};\n```\n\n## **事件元数据**\n\n在 `meta` 字段中存储额外信息：\n\n```tsx\nimport { Temporal } from 'temporal-polyfill';\nimport { Event } from '@dayflow/core';\n\nconst event: Event = {\n  id: '1',\n  title: '项目启动会',\n  start: Temporal.ZonedDateTime.from('2024-10-15T10:00:00[America/New_York]'),\n  end: Temporal.ZonedDateTime.from('2024-10-15T11:00:00[America/New_York]'),\n  meta: {\n    // 会议详情\n    location: 'A会议室',\n    meetingUrl: 'https://zoom.us/j/123456',\n\n    // 参与人\n    organizer: 'zhangsan@example.com',\n    attendees: ['lisi@example.com', 'wangwu@example.com'],\n\n    // 自定义字段\n    project: 'X项目',\n    priority: 'high',\n    tags: ['规划', '启动会'],\n\n    // 重复信息\n    recurring: true,\n    recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO',\n\n    // 其他任意数据\n    customField: '自定义值',\n  },\n};\n```\n\n## **最佳实践**\n\n1. **始终提供唯一ID** - 使用 UUID 或数据库 ID（字符串格式）\n2. **使用辅助函数** - 优先使用 `createEvent()` 而非手动构建 Temporal 对象（覆盖90%的使用场景）\n3. **选择正确的类型**：\n   - 全天事件（生日、节假日）使用 `PlainDate`\n   - 本地事件（会议、预约）使用 `PlainDateTime` **推荐默认选项**\n   - 仅时区敏感事件（国际电话、航班）使用 `ZonedDateTime`\n4. **验证时间值** - 辅助函数会自动验证小时（0-23）和分钟（0-59）\n5. **使用日历类型进行样式设置** - 分配 `calendarId` 来分类事件并保持样式一致性；当事件跨多个日历时使用 `calendarIds`\n6. **善用 `meta` 字段** - 存储自定义数据而无需修改 Event 接口\n7. **验证事件数据** - 确保定时事件的开始时间早于结束时间\n8. **优化事件更新** - 尽可能批量更新\n\n## **类型参考**\n\n有关日期/时间处理的详细信息，请参阅：\n\n- 事件接口：`/src/types/event.ts`\n\n## **相关文档**\n\n- [视图](/docs-zh/introduction/views) - 理解日历视图\n- [插件](/docs-zh/plugins/overview) - 使用插件管理事件\n- [快速开始](/docs-zh/introduction) - 基础使用示例\n"
  },
  {
    "path": "website/content/docs-zh/introduction/index.mdx",
    "content": "---\ntitle: 快速上手\ndescription: 学习如何安装并将 DayFlow 集成到您的 React、Vue、Svelte 或 Angular 应用中。通用日历组件的快速入门指南。\n---\n\n# 快速上手\n\n欢迎使用 DayFlow！本指南将帮助您快速开始。\n\n## 安装\n\n### 使用 CLI（推荐）\n\n运行交互式安装向导 —— 自动检测包管理器，选择框架与插件，一步完成安装。\n\n<CreateDayflowTabs />\n\n<CliPreview />\n\n### 手动安装\n\n也可以手动选择框架和包管理器进行安装：\n\n<FrameworkInstall />\n\n### 引入 DayFlow 样式\n\n**重要：** 您必须在应用中引入 DayFlow 的样式。请将以下代码添加到您的主 App 组件或布局文件中：\n\n```tsx\n// 在 App.tsx 或 layout.tsx 中\nimport '@dayflow/core/dist/styles.css';\n```\n\n也可以在 CSS 文件中导入：\n\n```css\n/* src/index.css */\n@import '@dayflow/core/dist/styles.css';\n```\n\n<Callout title=\"正在使用 Tailwind CSS？\" type=\"warn\">\n  请改为导入 `styles.components.css`，以避免 CSS Reset 与您现有样式的冲突：\n\n```css\n@import '@dayflow/core/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\n查看 [Tailwind V4 集成指南](https://calendar.dayflow.studio/docs-zh/guides/theme-customization#tailwind-v4-integration) 了解更多详情。\n\n> **提示：** 如果您使用 `create-dayflow` 安装向导并在 Tailwind CSS 选项中选择“是”，它会自动为您添加这条导入。\n\n</Callout>\n\n<Callout title=\"正在使用 Next.js Pages Router 或 Node.js require()？\" type=\"warn\">\n  `@dayflow/core` 是 **ESM-only** 的，不提供 CommonJS 构建版本。这意味着：\n\n- **`require('@dayflow/core')`** 会报错。请改用 `import`。\n- **Next.js Pages Router** 默认不会转译 `node_modules` 中的 ESM。请在 `next.config.js` 的 `transpilePackages` 中添加该包：\n\n```js\n// next.config.js\nmodule.exports = {\n  transpilePackages: ['@dayflow/core'],\n};\n```\n\n- **Next.js App Router** (带有 `\"use client\"` / Server Components) 无需额外配置，开箱即用。\n- **SSR 框架** (Nuxt, SvelteKit 等) 通常原生支持 ESM。如果遇到导入错误，请检查您的构建器选项（如 `ssr.noExternal`）中是否包含 `@dayflow/core`。\n\n</Callout>\n\n## 基础用法\n\n选择您的框架，查看如何将 DayFlow 集成到您的应用中。\n\n<FrameworkTabs>\n  <Tab>\n    ```tsx\n    import {\n      useCalendarApp,\n      DayFlowCalendar,\n      createDayView,\n      createWeekView,\n      createMonthView,\n      createEventsPlugin,\n    } from '@dayflow/react';\n    import { createDragPlugin } from '@dayflow/plugin-drag';\n    import { createEvent, createAllDayEvent } from '@dayflow/core';\n    import '@dayflow/core/dist/styles.css';\n\n    function App() {\n      const calendar = useCalendarApp({\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: '工作',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: '团队会议',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: '全天会议',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      });\n\n      return <DayFlowCalendar calendar={calendar} />;\n    }\n    ```\n\n  </Tab>\n  <Tab>\n    ```vue\n    <template>\n      <DayFlowCalendar :calendar=\"calendar\" />\n    </template>\n\n    <script setup>\n      import { DayFlowCalendar, useCalendarApp } from '@dayflow/vue';\n      import {\n        createDayView,\n        createWeekView,\n        createMonthView,\n        createEventsPlugin,\n        createEvent,\n        createAllDayEvent\n      } from '@dayflow/core';\n      import { createDragPlugin } from '@dayflow/plugin-drag';\n      import '@dayflow/core/dist/styles.css';\n\n      const calendar = useCalendarApp({\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: '工作',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: '团队会议',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: '全天会议',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      });\n    </script>\n    ```\n\n  </Tab>\n  <Tab>\n    ```typescript\n    import { Component } from '@angular/core';\n    import {\n      createDayView,\n      createWeekView,\n      createMonthView,\n      createEventsPlugin,\n      createEvent,\n      createAllDayEvent\n    } from '@dayflow/core';\n    import { createDragPlugin } from '@dayflow/plugin-drag';\n    import { DayFlowCalendarModule } from '@dayflow/angular';\n    import '@dayflow/core/dist/styles.css';\n\n    @Component({\n      selector: 'app-root',\n      standalone: true,\n      imports: [DayFlowCalendarModule],\n      template: `\n        <dayflow-calendar [calendar]=\"calendar\"></dayflow-calendar>\n      `\n    })\n    export class AppComponent {\n      calendar = {\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: '工作',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: '团队会议',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: '全天会议',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      };\n    }\n    ```\n\n  </Tab>\n  <Tab>\n    ```svelte\n    <script>\n      import { DayFlowCalendar, useCalendarApp } from '@dayflow/svelte';\n      import {\n        createDayView,\n        createWeekView,\n        createMonthView,\n        createEventsPlugin,\n        createEvent,\n        createAllDayEvent\n      } from '@dayflow/core';\n      import { createDragPlugin } from '@dayflow/plugin-drag';\n      import '@dayflow/core/dist/styles.css';\n\n      const calendar = useCalendarApp({\n        views: [createDayView(), createWeekView(), createMonthView()],\n        plugins: [createDragPlugin(), createEventsPlugin()],\n        calendars: [\n          {\n            id: 'work',\n            name: '工作',\n            colors: {\n              lineColor: '#2563eb',\n              eventColor: '#dbeafe',\n              eventSelectedColor: '#bfdbfe',\n              textColor: '#1e3a8a',\n            },\n          },\n        ],\n        events: [\n          createEvent({\n            id: '1',\n            title: '团队会议',\n            start: new Date(2025, 10, 15, 10, 0),\n            end: new Date(2025, 10, 15, 11, 0),\n            calendarId: 'work',\n          }),\n          createAllDayEvent({\n            id: '2',\n            title: '全天会议',\n            start: new Date(2025, 10, 20),\n            calendarId: 'work',\n          }),\n        ],\n        initialDate: new Date(),\n      });\n    </script>\n\n    <DayFlowCalendar {calendar} />\n    ```\n\n  </Tab>\n</FrameworkTabs>\n\n## 自定义样式\n\n默认情况下，日历会自动撑满其容器的宽度。您可以通过 CSS 自定义日历的高度或其他样式。例如，设置响应式高度：\n\n```css\n.df-calendar-container {\n  --df-calendar-height: 760px !important;\n  height: var(--df-calendar-height, 760px) !important;\n}\n\n@media (max-width: 768px) {\n  .df-calendar-container {\n    --df-calendar-height: 550px !important;\n  }\n}\n```\n\n**注意：** 本库使用现代的 [Temporal API](/docs-zh/temporal-migration) 处理日期/时间。通过辅助函数 (`createEvent`)，您可以轻松创建事件，而无需直接处理 Temporal 对象。\n"
  },
  {
    "path": "website/content/docs-zh/introduction/meta.json",
    "content": "{\n  \"title\": \"介绍\",\n  \"defaultOpen\": true,\n  \"pages\": [\n    \"index\",\n    \"events\",\n    \"views\",\n    \"resource-grid\",\n    \"resource-timeline\",\n    \"dayflow-calendar\",\n    \"use-calendar-app\"\n  ]\n}\n"
  },
  {
    "path": "website/content/docs-zh/introduction/resource-grid.mdx",
    "content": "---\ntitle: 资源网格\ndescription: DayFlow Pro 垂直资源网格视图，支持按资源分列、日期分组模式和基于拖拽的调度交互。\nstatus: pro\n---\n\n# 资源网格 (Resource Grid)\n\n`@dayflow-pro/resource-grid` 提供了一个垂直调度网格，其中时间从上向下流动，资源被渲染为列。它非常适合预订房间、设备、工作室、办公位或管理员工排班。\n\n试用[在线演示](https://pro.dayflow.studio)\n\n## 安装\n\n<PackageTabs pkg='@dayflow-pro/resource-grid' />\n\n有关 DayFlow Pro 的安装步骤，请参阅 [Pro 安装](/docs/introduction/pro-installation)。\n\n## 基本用法\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { createResourceGridView } from '@dayflow-pro/resource-grid';\n\nfunction App() {\n  const calendar = useCalendarApp({\n    views: [\n      createResourceGridView({\n        mode: 'resourceView',\n        resources: [\n          { id: 'room-a', title: 'Room A' },\n          { id: 'room-b', title: 'Room B' },\n        ],\n        visibleDays: 3,\n        hourHeight: 72,\n        firstHour: 8,\n        lastHour: 20,\n      }),\n    ],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 视图模式\n\n| 模式              | 描述                                   |\n| :---------------- | :------------------------------------- |\n| `resourceView`    | 资源是主要列，日期在每个资源内部重复。 |\n| `resourcesByDate` | 日期是主要列，资源在每个日期内部重复。 |\n\n软件包默认配置为：\n\n```ts\n{\n  visibleDays: 7,\n  showWeekend: true,\n  onlyShowEventDays: false,\n  dayWidth: 120,\n  hourHeight: 72,\n  firstHour: 0,\n  lastHour: 24,\n  stickyHeaders: true,\n}\n```\n\n## 资源与事件模型\n\n资源模型设计得很简单：\n\n```ts\ntype Resource = {\n  id: string;\n  title: string;\n  avatar?: string;\n  color?: string;\n  meta?: Record<string, unknown>;\n};\n```\n\n事件可以直接存储 `resourceId`，也可以通过回调解析：\n\n```tsx\nconst event = {\n  id: 'booking-1',\n  title: 'Studio recording',\n  start: new Date(2026, 4, 8, 10, 0),\n  end: new Date(2026, 4, 8, 12, 0),\n  resourceId: 'room-a',\n};\n```\n\n解析顺序为：\n\n1. 如果提供了 `getResourceId(event)`，则使用它\n2. `event.resourceId`\n3. `event.calendarId`\n\n## 过滤与可见性\n\n网格可以过滤资源和日期：\n\n| 属性                                  | 类型                                | 备注                           |\n| :------------------------------------ | :---------------------------------- | :----------------------------- |\n| `visibleResourceIds`                  | `string[]`                          | 将网格限制在特定的资源子集内。 |\n| `isResourceVisible`                   | `(resource) => boolean`             | 自定义谓词，用于完全隐藏资源。 |\n| `syncResourceVisibilityWithCalendars` | `boolean`                           | 自动跟随日历可见性。           |\n| `getCalendarIdForResource`            | `(resource) => string \\| undefined` | 覆盖资源到日历的映射。         |\n| `showWeekend`                         | `boolean`                           | 包含或排除周末。               |\n| `onlyShowEventDays`                   | `boolean`                           | 移除没有匹配事件的可见日期。   |\n\n启用日历同步时，回退映射为 `resource.meta?.calendarId ?? resource.id`。\n\n如果 `onlyShowEventDays` 移除了当前范围内的所有日期，视图将回退到普通日期范围，而不是渲染一个空网格。\n\n## 布局配置\n\n| 属性                 | 类型                              | 备注                               |\n| :------------------- | :-------------------------------- | :--------------------------------- |\n| `license`            | `PackageLicenseConfig`            | 当你不使用全局注册时的可选覆盖项。 |\n| `visibleDays`        | `number`                          | 从当前日期起生成的显示天数。       |\n| `hourHeight`         | `number`                          | 每小时行的高度（像素）。           |\n| `firstHour`          | `number`                          | 第一个可见小时。                   |\n| `lastHour`           | `number`                          | 最后一个可见小时。                 |\n| `dayWidth`           | `number`                          | 每列使用的基础宽度。               |\n| `stickyHeaders`      | `boolean`                         | 滚动时保持标题固定。               |\n| `initialScrollState` | `{ left?: number; top?: number }` | 设置初始滚动位置。                 |\n\n当视口宽度大于配置的内容宽度且当前模式允许时，实现也会自动拉伸列。\n"
  },
  {
    "path": "website/content/docs-zh/introduction/resource-timeline.mdx",
    "content": "---\ntitle: 资源时间线\ndescription: DayFlow Pro 水平资源时间线视图，支持资源分组、里程碑渲染、视图切换和调度器风格的交互。\nstatus: pro\n---\n\n# 资源时间线 (Resource Timeline)\n\n`@dayflow-pro/resource-timeline` 为 DayFlow 增加了一个调度器风格的水平时间线。时间从左向右流动，每个资源拥有自己的行。该视图专门为人员、房间、项目或设备的跨资源工作规划而优化。\n\n试用[在线演示](https://pro.dayflow.studio)\n\n与内置的日历视图相比，资源时间线针对以下场景进行了优化：\n\n- 长期运行的任务和项目计划\n- 跨多个车道（Lane）的资源分配\n- 里程碑和进度跟踪\n- 分层或分组的资源列表\n\n## 安装\n\n<PackageTabs pkg='@dayflow-pro/resource-timeline' />\n\n有关 DayFlow Pro 的安装步骤，请参阅 [Pro 安装](/docs/introduction/pro-installation)。\n\n## 基本用法\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { createResourceTimelineView } from '@dayflow-pro/resource-timeline';\n\nfunction App() {\n  const calendar = useCalendarApp({\n    views: [\n      createResourceTimelineView({\n        resources: [\n          {\n            id: 'eng-alice',\n            name: 'Alice Chen',\n            groupId: 'engineering',\n            groupName: 'Engineering',\n            subtitle: 'Frontend',\n          },\n          {\n            id: 'eng-bob',\n            name: 'Bob Torres',\n            groupId: 'engineering',\n            groupName: 'Engineering',\n            subtitle: 'Platform',\n          },\n        ],\n        defaultView: 'week',\n        height: 640,\n        getResourceId: event =>\n          (event as { resourceId?: string }).resourceId ?? event.calendarId,\n      }),\n    ],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 时间线模式\n\n| 模式      | 描述                            |\n| :-------- | :------------------------------ |\n| `day`     | 以小时为列的一天。              |\n| `week`    | 以天为列的一周。                |\n| `month`   | 以天为列的一个月。              |\n| `quarter` | 包含周/月分组的三个月（季度）。 |\n| `year`    | 用于路线图风格规划的全年视图。  |\n\n使用 `defaultView` 选择初始模式。你还可以通过 `viewSwitcherMode` 控制切换器的渲染方式，支持 `buttons`、`select` 和 `filter`。\n\n## 资源模型\n\n资源是普通的 JavaScript 对象。源代码支持的字段不仅仅是 `id` 和 `name`：\n\n```ts\ntype Resource = {\n  id: string;\n  name: string;\n  parentId?: string;\n  collapsed?: boolean;\n  groupId?: string;\n  groupName?: string;\n  order?: number;\n  subtitle?: string;\n  avatar?: string;\n  color?: string;\n  style?: {\n    colors: { color: string; textColor?: string };\n    darkColors?: { color: string; textColor?: string };\n    opacity?: number;\n    borderRadius?: number;\n  };\n  meta?: Record<string, unknown>;\n};\n```\n\n有用字段：\n\n- `groupId` 和 `groupName`：让你将资源整理到分组部分。\n- `parentId` 和 `collapsed`：支持分层资源树。\n- `subtitle`、`avatar`、`color` 和 `style`：丰富侧边栏的展示效果。\n- `meta`：通常用于存储外部标识符，如 `calendarId`。\n\n## 事件与资源绑定\n\n该视图按以下顺序解析每个事件所在的行：\n\n1. 如果提供了 `getResourceId(event)`，则使用它\n2. `event.resourceId`\n3. `event.meta?.resourceId`\n4. `event.calendarId`\n\n这意味着你既可以直接在事件对象上存储 `resourceId`，也可以通过回调将现有的 DayFlow 事件映射到资源。\n\n```tsx\nconst event = {\n  id: 'task-1',\n  title: 'Build export flow',\n  start: new Date(2026, 4, 10, 9, 0),\n  end: new Date(2026, 4, 12, 18, 0),\n  resourceId: 'eng-alice',\n  progress: 65,\n};\n```\n\n## 里程碑和事件样式\n\n`start` 和 `end` 相等的事件会被自动渲染为里程碑。\n\n```tsx\nconst milestone = {\n  id: 'release-gate',\n  title: 'Release approval',\n  start: new Date(2026, 4, 14, 10, 0),\n  end: new Date(2026, 4, 14, 10, 0),\n  resourceId: 'eng-bob',\n};\n```\n\n| 属性                    | 类型                             | 备注                                         |\n| :---------------------- | :------------------------------- | :------------------------------------------- |\n| `milestoneLabelDisplay` | `'hover' \\| 'always'`            | 控制里程碑标签是仅在悬停时出现还是始终可见。 |\n| `milestoneLaneBehavior` | `'smart' \\| 'overlay'`           | 控制里程碑是尽可能共享普通车道还是覆盖它们。 |\n| `getEventType`          | `(event) => ResourceEventType`   | 将事件映射到时间线特定的事件类型。           |\n| `getEventTooltip`       | `(event) => string \\| undefined` | 提供事件的工具提示文本。                     |\n| `styleRegistry`         | `Record<string, EventStyle>`     | 注册自定义样式预設。                         |\n| `getEventStyleId`       | `(event) => string \\| undefined` | 为事件选择样式预設。                         |\n\n## ResourceTimelineViewConfig\n\n### 核心 (Core)\n\n| 属性               | 类型                                      | 备注                               |\n| :----------------- | :---------------------------------------- | :--------------------------------- |\n| `resources`        | `Resource[]`                              | 时间线所需的资源列表。             |\n| `license`          | `PackageLicenseConfig`                    | 当你不使用全局注册时的可选覆盖项。 |\n| `defaultView`      | `day \\| week \\| month \\| quarter \\| year` | 初始时间线刻度。                   |\n| `views`            | `SchedulerTimelineView[]`                 | 覆盖可用的可切换时间线模式。       |\n| `viewType`         | `ViewType \\| string`                      | 与高级宿主集成时的自定义视图类型。 |\n| `viewSwitcherMode` | `'buttons' \\| 'select' \\| 'filter'`       | 控制模式切换器 UI。                |\n| `height`           | `number \\| string`                        | 渲染视图的高度。                   |\n\n### 布局 (Layout)\n\n| 属性                    | 类型     | 备注                                   |\n| :---------------------- | :------- | :------------------------------------- |\n| `resourcePanelWidth`    | `number` | 首选侧边栏宽度。                       |\n| `resourcePanelMinWidth` | `number` | 最小侧边栏宽度。                       |\n| `resourcePanelMaxWidth` | `number` | 最大侧边栏宽度。                       |\n| `rowHeight`             | `number` | 每个资源行的高度。                     |\n| `headerHeight`          | `number` | 时间线标题高度。                       |\n| `hourWidth`             | `number` | 天模式小时刻度使用的宽度。             |\n| `weekDayWidth`          | `number` | 周模式下每天的宽度。                   |\n| `dayWidth`              | `number` | 月模式下每天的宽度。                   |\n| `monthDayWidth`         | `number` | 渲染密集月度范围时每天的宽度。         |\n| `quarterMonthWidth`     | `number` | 季度模式下每月的宽度。                 |\n| `yearCellWidth`         | `number` | 年模式下每个单元格的宽度。             |\n| `yearRangePast`         | `number` | 当前年份之前的额外年份范围。           |\n| `yearRangeFuture`       | `number` | 当前年份之后的额外年份范围。           |\n| `overscanRows`          | `number` | 在视口外渲染的额外行数。               |\n| `bufferCells`           | `number` | 为使滚动更顺畅而渲染的额外时间单元格。 |\n\n### 资源可见性与同步\n\n| 属性                                  | 类型                                | 备注                               |\n| :------------------------------------ | :---------------------------------- | :--------------------------------- |\n| `resourceSorter`                      | `(left, right) => number`           | 自定义行排序。                     |\n| `syncResourceVisibilityWithCalendars` | `boolean`                           | 当映射的日历隐藏时，隐藏资源行。   |\n| `getCalendarIdForResource`            | `(resource) => string \\| undefined` | 覆盖资源映射到日历可见性的方式。   |\n| `onResourcesUpdate`                   | `(resources) => void`               | 当资源在 UI 中被重排或折叠时触发。 |\n\n启用日历同步时，回退的资源到日历映射为 `resource.meta?.calendarId ?? resource.id`。\n\n## 焦点、侧边栏与集成\n\n时间线公开了一些容易被忽视的集成点：\n\n- `focusRequest`：让宿主应用能够以编程方式将时间线移动到特定的事件和日期。\n- `sidebar`：透传调度器侧边栏配置，如右键菜单操作。\n- `customDetailPanelContent` 和 `customEventDetailDialog`：当你需要自定义调度器详情 UI 时，可以通过视图属性透传。\n- `timeFormat`：支持 `24h` 和 `12h`。\n- `resizeTimeFormat`：自定义调整事件大小时显示的标签。\n"
  },
  {
    "path": "website/content/docs-zh/introduction/use-calendar-app.mdx",
    "content": "# useCalendarApp\n\n`useCalendarApp(config: CalendarAppConfig)` 是 DayFlow 的\"中控台\"。通过一个配置对象即可完成视图注册、事件注入、主题/侧边栏/对话框开关，以及与外部系统同步的回调设置。下面依次拆解常用选项与实践技巧。\n\n## 快速开始\n\n```tsx {11-16}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport '@dayflow/core/dist/styles.css';\n\nexport function TeamCalendar() {\n  const calendar = useCalendarApp({\n    views: [createMonthView(), createWeekView()],\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    events: [],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 配置概览\n\n| 选项                   | 类型                        | 必需 | 默认值                         | 描述                                                                                                       |\n| ---------------------- | --------------------------- | ---- | ------------------------------ | ---------------------------------------------------------------------------------------------------------- |\n| `views`                | `CalendarView[]`            | 必填 | —                              | 注册视图定义（例如 `createMonthView()`）。至少需要一个视图                                                 |\n| `plugins`              | `CalendarPlugin[]`          | 可选 | `[]`                           | 安装可选插件（拖拽辅助、快捷键等）。每个插件在安装期间接收 app 实例                                        |\n| `events`               | `Event[]`                   | 可选 | `[]`                           | 初始事件数据。之后使用 `addEvent`/`updateEvent` 来修改状态                                                 |\n| `callbacks`            | `CalendarCallbacks`         | 可选 | `{}`                           | 在视图、日期或事件更改时触发的生命周期钩子——非常适合与 API 同步                                            |\n| `defaultView`          | `ViewType`                  | 可选 | `ViewType.WEEK`                | 首先加载的视图；必须存在于 `views` 中                                                                      |\n| `initialDate`          | `Date`                      | 可选 | `new Date()`                   | 起始焦点日期（也初始化可见月份计算）                                                                       |\n| `timeZone`             | `string`                    | 可选 | 用户系统时区                   | 所有视图共享的全局显示/编辑时区                                                                            |\n| `switcherMode`         | `'buttons' \\| 'select'`     | 可选 | `'buttons'`                    | 控制内置视图切换器在标题中的渲染方式                                                                       |\n| `calendars`            | `CalendarType[]`            | 可选 | `[]`                           | 注册日历类别（工作、个人等）及其颜色和可见性                                                               |\n| `defaultCalendar`      | `string`                    | 可选 | 第一个可见日历                 | 创建新事件时使用的 ID                                                                                      |\n| `theme`                | `ThemeConfig`               | 可选 | `{ mode: 'light' }`            | 设置全局主题模式和可选的令牌覆盖                                                                           |\n| `locale`               | `string \\| Locale`          | 可选 | `'en-US'`                      | 设置国际化（i18n）的语言环境。支持语言代码（如 'zh'）或 Locale 对象。                                      |\n| `useEventDetailDialog` | `boolean`                   | 可选 | `false`                        | 启用默认模态详情对话框而不是内联面板                                                                       |\n| `useCalendarHeader`    | `boolean`                   | 可选 | `true`                         | 启用默认页眉（`true`）或隐藏页眉（`false`）。自定义页眉请使用 `DayFlowCalendar` 上的 `calendarHeader` 插槽 |\n| `readOnly`             | `boolean \\| ReadOnlyConfig` | 可选 | `false`                        | 禁用内置修改 UI。可以是布尔值，也可以是用于细粒度控制（拖拽/查看）的配置对象。程序化事件 API 仍然可用      |\n| `allDaySortComparator` | `AllDaySortComparator`      | 可选 | 按日历分组，且跨天全天事件优先 | 控制所有视图中全天事件行顺序的自定义比较函数。传入后会完全覆盖默认顺序                                     |\n\n## 核心选项说明\n\n### 视图（必选）\n\n- 每个视图都是一个包含 `type`、`component`、`config` 的 `CalendarView`\n- 推荐使用内置工厂 `createDayView`/`createWeekView`/`createMonthView`/`createAgendaView`/`createYearView`\n- `defaultView` 必须存在于 `views` 列表，否则初始化会报错\n\n### 事件\n\n- `events` 数组会注入到内部状态，之后通过 `calendar.addEvent` / `updateEvent` / `deleteEvent` 来维护\n- 事件的 `start`/`end` 支持 `PlainDate`、`PlainDateTime`、`ZonedDateTime`，helper 已经帮你转换\n\n### 插件\n\n- 插件在 `install(app)` 阶段获取到日历实例，可注册拖拽、快捷键、统计等能力\n- 通过 `calendar.app.getPlugin('drag')` 等方式可以在别处访问插件 API\n- 可以同时安装多个插件，它们彼此独立\n\n```tsx\nimport { createDragPlugin } from '@dayflow/plugin-drag';\nimport { createEventsPlugin } from '@dayflow/core';\n\nconst dragPlugin = createDragPlugin({\n  enableDrag: true,\n  enableResize: true,\n  enableCreate: true,\n});\n\nconst eventsPlugin = createEventsPlugin({\n  enableValidation: true,\n  maxEventsPerDay: 100,\n});\n\nconst calendar = useCalendarApp({\n  views: [createWeekView(), createMonthView()],\n  plugins: [dragPlugin, eventsPlugin],\n});\n```\n\n### 回调\n\n`callbacks` 是连接后端或其他状态管理的桥梁，例如：\n\n- `onViewChange(view)`：视图切换后触发，可用于埋点或同步 URL\n- `onDateChange(date)`：当前聚焦日期变化\n- `onVisibleRangeChange(start, end, reason)`：可见日期范围每次变化时触发，无需额外计算即可进行更精准的数据拉取\n- `onEventCreate/Update/Delete`：与服务端 CRUD 对接\n- `onEventDoubleClick(event, e)`：当事件被双击时触发，常用于自定义弹出层锚点；可使用 `e.currentTarget` 作为锚点，返回 `false` 可阻止 DayFlow 默认详情面板/对话框打开\n- `onMoreEventsClick(date)`：当月视图中的\"+ X 更多\"链接被点击时触发\n- `onCalendarCreate/onCalendarUpdate/onCalendarDelete`：日历增删改的回调，用于同步日历列表\n- `onCalendarMerge(sourceId, targetId)`：合并两个日历时触发（例如将\"工作\"合并到\"个人\"）\n- `onRender`：一次渲染完成后触发，适合性能监控\n\n### 默认视图 & 初始日期\n\n- 单视图日历务必显式设置 `defaultView`\n- `initialDate` 可从路由、用户偏好或服务器数据中读取，以保持多端一致\n\n### timeZone\n\n- `timeZone` 用来定义 Day、Week、Month、Agenda、Year 共用的主显示/编辑时区。\n- 不传时，DayFlow 会默认使用当前用户设备的系统时区。\n- 切换 `timeZone` 只会重新投影视图，不会因为切换时区本身触发事件数据写回。\n- 编辑类回调返回的仍然是 canonical event；DayFlow 不会把当前 UI 投影时区下的临时显示值直接当成最终持久化数据。\n- Day / Week 的 `secondaryTimeZone` 仍然只是辅助显示时间轴，不会替代全局主时区。\n\n### switcherMode\n\n- `'buttons'`：横向按钮，桌面端常用\n- `'select'`：下拉选择，占用空间小，适合移动端或视图较多的场景\n\n### calendars 与 defaultCalendar\n\n- 用 `calendars` 声明不同的日历类别（工作/个人…）及颜色\n- 不指定 `defaultCalendar` 时会使用第一个可见日历\n\n**`CalendarType` 属性：**\n\n| 属性           | 类型                     | 必填 | 说明                                             |\n| -------------- | ------------------------ | ---- | ------------------------------------------------ |\n| `id`           | `string`                 | 是   | 唯一标识符（如 `'work'`、`'personal'`）。        |\n| `name`         | `string`                 | 是   | 界面中显示的名称。                               |\n| `colors`       | `CalendarColors`         | 是   | 浅色模式颜色配置（见下表）。                     |\n| `darkColors`   | `CalendarColors`         | 否   | 深色模式颜色配置；未提供时回退到 `colors`。      |\n| `description`  | `string`                 | 否   | 可选的描述文字。                                 |\n| `icon`         | `string`                 | 否   | 显示在日历名称旁的 Emoji 或图标名。              |\n| `isVisible`    | `boolean`                | 否   | 是否显示该日历的事件，默认为 `true`。            |\n| `isDefault`    | `boolean`                | 否   | 标记为系统默认日历。                             |\n| `readOnly`     | `boolean`                | 否   | 禁用该日历事件的拖拽、调整大小和编辑操作。       |\n| `source`       | `string`                 | 否   | 来源标签（如 `'Google Calendar'`、`'iCloud'`）。 |\n| `subscription` | `{ url, status, meta? }` | 否   | ICS / 远程日历的订阅元数据。                     |\n\n**`CalendarColors` 属性：**\n\n| 属性                 | 类型     | 说明                         |\n| -------------------- | -------- | ---------------------------- |\n| `eventColor`         | `string` | 事件背景色（通常为半透明）。 |\n| `eventSelectedColor` | `string` | 事件选中时的背景色。         |\n| `lineColor`          | `string` | 强调色 / 边框颜色。          |\n| `textColor`          | `string` | 事件文字颜色。               |\n\n### theme\n\n`theme` 控制全局配色与明暗模式：\n\n```tsx\nconst calendar = useCalendarApp({\n  theme: {\n    mode: 'dark', // 'light' | 'dark' | 'auto'\n  },\n});\n```\n\n**主题模式：**\n\n- `'light'`：浅色模式，浅色背景和深色文本（默认）\n- `'dark'`：深色模式，深色背景和浅色文本\n- `'auto'`：自动跟随系统主题偏好\n\n**编程式主题更改：**\n\n```tsx\n// 获取当前主题\nconst currentTheme = calendar.app.getTheme();\n\n// 设置主题\ncalendar.app.setTheme('dark');\n\n// 订阅主题更改\ncalendar.app.subscribeThemeChange(theme => {\n  console.log('主题已更改为:', theme);\n});\n```\n\n**自定义深色模式颜色：**\n\n为每个日历类型定义浅色和深色模式的不同颜色：\n\n```tsx\nconst calendars = [\n  {\n    id: 'work',\n    name: '工作',\n    colors: {\n      // 浅色模式颜色\n      lineColor: '#0066cc',\n      eventColor: '#e6f2ff',\n      eventSelectedColor: '#cce4ff',\n      textColor: '#003d7a',\n    },\n    darkColors: {\n      // 深色模式颜色\n      lineColor: '#4da6ff',\n      eventColor: '#1a3d5c',\n      eventSelectedColor: '#2a5a8a',\n      textColor: '#b3d9ff',\n    },\n  },\n];\n```\n\n更多细节见 [深色模式](/docs-zh/features/dark-mode)。\n\n### locale\n\n`locale` 选项用于设置日历的语言和区域设置。\n\n- **字符串代码：** 使用 `'en-US'`、`'ja'`、`'zh'`、`'de'`、`'fr'`、`'es'`、`'ko'` 等语言代码。\n- **Locale 对象：** 传递导入的 `Locale` 对象（以获得类型安全）或自定义对象（用于未支持的语言）。\n\n```tsx\n// 使用字符串代码\nconst calendar = useCalendarApp({\n  locale: 'zh',\n});\n\n// 使用多语言插件提供的 Locale 对象\nimport { zh } from '@dayflow/plugin-localization';\nconst calendar = useCalendarApp({\n  locale: zh,\n});\n\n// 使用自定义 Locale 对象\nconst customLocale = {\n  code: 'it',\n  messages: { today: 'Oggi', ... }\n};\nconst calendar = useCalendarApp({\n  locale: customLocale,\n});\n```\n\n### useEventDetailDialog\n\n- `true`：启用默认模态对话框（`DefaultEventDetailDialog`）。\n- 与 `DayFlowCalendar` 上的 `eventDetailContent` 或 `eventDetailDialog` 配合使用，可替换 UI 同时保留内部状态机。\n\n### useCalendarHeader\n\n- `true`（默认）：渲染内置日历头部。\n- `false`：完全隐藏头部。\n\n如需渲染自定义头部，请使用 `DayFlowCalendar` 上的 `calendarHeader` 插槽。详见 [Calendar Header](/docs-zh/features/calendar-header)。\n\n### readOnly\n\n- `true`：禁用内置修改 UI（拖拽、创建、编辑）。\n- `ReadOnlyConfig`：细粒度控制：\n  - `draggable`：是否允许拖拽。\n  - `viewable`：是否允许打开事件详情。\n- `calendar.addEvent()`、`calendar.updateEvent()`、`calendar.deleteEvent()`、`calendar.applyEventsChanges()` 等程序化 API 在只读模式下仍然可用。\n- 如果您有自定义 UI，建议使用 `calendar.canMutateFromUI()` 来决定是否显示创建、编辑、删除入口。\n- 详见 [只读模式](/docs-zh/features/read-only)。\n\n### allDaySortComparator\n\n控制所有视图（日、周、月、Agenda、年）中全天事件的行排列顺序。\n\n默认情况下，DayFlow 会：\n\n- 按 `calendarId` 的首次出现顺序对全天事件分组\n- 让跨天全天事件排在单天全天事件之前\n- 尽量让同一日历下的全天事件保持相邻\n\n只有当你确实想完全接管最终顺序时，才需要传入 `allDaySortComparator`。一旦传入，该比较函数的结果会直接作为最终排序结果。\n\n传入比较函数可完全自定义排序——接收两个 `Event` 对象，与 JavaScript 的 `Array.sort` 用法相同：\n\n```tsx\nconst calendar = useCalendarApp({\n  allDaySortComparator: (a, b) => a.title.localeCompare(b.title),\n});\n```\n\n**可直接使用的导出辅助函数**（从 `@dayflow/core` 导出）：\n\n| 辅助函数            | 行为                                               |\n| :------------------ | :------------------------------------------------- |\n| `sortAllDayByTitle` | 按标题字母顺序排序全天事件，并完全覆盖默认分组逻辑 |\n\n```tsx\nimport { sortAllDayByTitle } from '@dayflow/core';\n\nconst calendar = useCalendarApp({\n  allDaySortComparator: sortAllDayByTitle,\n});\n```\n\n使用 `updateConfig` 在运行时更改排序：\n\n```tsx\ncalendar.app.updateConfig({ allDaySortComparator: sortAllDayByTitle });\n\n// 恢复默认的按日历分组排序\ncalendar.app.updateConfig({ allDaySortComparator: undefined });\n```\n\n## 高级配置示例\n\n```tsx\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\nimport '@dayflow/core/dist/styles.css';\n\nconst calendars = [\n  {\n    id: 'work',\n    name: '工作',\n    colors: {\n      eventColor: '#2563eb',\n      eventSelectedColor: '#1d4ed8',\n      lineColor: '#1e40af',\n      textColor: '#ffffff',\n    },\n  },\n  {\n    id: 'personal',\n    name: '个人',\n    colors: {\n      eventColor: '#f97316',\n      eventSelectedColor: '#ea580c',\n      lineColor: '#c2410c',\n      textColor: '#ffffff',\n    },\n  },\n];\n\nexport function AdvancedCalendar() {\n  const calendar = useCalendarApp({\n    views: [createMonthView(), createWeekView()],\n    defaultView: ViewType.WEEK,\n    initialDate: new Date('2024-10-01'),\n    events: [],\n    calendars,\n    defaultCalendar: 'work',\n    switcherMode: 'select',\n    callbacks: {\n      onEventUpdate: event => api.events.update(event),\n      onVisibleRangeChange: (start, end, reason) =>\n        preloadVisibleRange(start, end, reason),\n    },\n    plugins: [\n      createSidebarPlugin({\n        width: 280,\n        initialCollapsed: false,\n      }),\n    ],\n    useEventDetailDialog: true,\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n> 提示：`useCalendarApp` 返回的对象同时公开状态（`currentView`、`currentDate`、`events`）和操作。与 `DayFlowCalendar`、您自己的工具栏和侧面板共享相同的实例以保持所有内容同步。\n\n## 相关文档\n\n- [DayFlowCalendar](/docs-zh/introduction/dayflow-calendar) - 主要 UI 组件\n- [快速开始](/docs-zh/introduction) - 设置指南\n- [事件](/docs-zh/introduction/events) - 事件管理\n- [视图](/docs-zh/introduction/views) - 可用视图\n"
  },
  {
    "path": "website/content/docs-zh/introduction/views.mdx",
    "content": "# 日历视图\n\nDay Flow 支持五种不同的视图类型，每种类型都针对不同的用例和用户需求进行了优化。您可以以编程方式切换视图，或者提供 UI 供用户切换视图。\n\n## 可用视图\n\n### 日视图\n\n日视图专注于单个日期，并带有详细的时间槽，非常适合管理每日日程。\n\n#### 配置\n\n| 属性                  | 类型             | 默认值      | 描述                               |\n| :-------------------- | :--------------- | :---------- | :--------------------------------- |\n| `showAllDay`          | `boolean`        | `true`      | 是否显示全天事件行。               |\n| `scrollToCurrentTime` | `boolean`        | `true`      | 初始加载时是否自动滚动到当前时间。 |\n| `timeFormat`          | `'12h' \\| '24h'` | `'24h'`     | 时间轴的时间格式。                 |\n| `secondaryTimeZone`   | `string`         | `undefined` | 仅在日视图中显示的辅助参考时间轴。 |\n| `hourHeight`          | `number`         | `72`        | 时间网格中每小时行的高度（px）。   |\n\n```tsx {10}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createDayView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [createDayView()],\n    defaultView: ViewType.DAY,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### 周视图\n\n周视图显示一个带有时间槽的 7 天网格，显示具有准确开始和结束时间的事件。\n\n#### 配置\n\n| 属性                  | 类型                                                 | 默认值           | 描述                                                                                                  |\n| :-------------------- | :--------------------------------------------------- | :--------------- | :---------------------------------------------------------------------------------------------------- |\n| `showWeekends`        | `boolean`                                            | `true`           | 是否显示周六和周日。                                                                                  |\n| `showAllDay`          | `boolean`                                            | `true`           | 是否显示全天事件行。                                                                                  |\n| `startOfWeek`         | `number`                                             | `1`              | 一周的开始日期（0 为周日，1 为周一等）。                                                              |\n| `scrollToCurrentTime` | `boolean`                                            | `true`           | 初始加载时是否自动滚动到当前时间。                                                                    |\n| `timeFormat`          | `'12h' \\| '24h'`                                     | `'24h'`          | 时间轴的时间格式。                                                                                    |\n| `secondaryTimeZone`   | `string`                                             | `undefined`      | 仅在周视图中显示的辅助参考时间轴。                                                                    |\n| `gridDateClick`       | `'day-view' \\| 'none' \\| function`                   | `undefined`      | 点击日期单元格时的操作。                                                                              |\n| `gridDateDoubleClick` | `'create-event' \\| 'day-view' \\| 'none' \\| function` | `'create-event'` | 双击日期单元格时的操作。`'create-event'` (默认) 会在点击的位置 (小时) 创建一个 1 小时的 Timed Event。 |\n| `hourHeight`          | `number`                                             | `72`             | 时间网格中每小时行的高度（px）。                                                                      |\n\n```tsx {10}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createWeekView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [createWeekView()],\n    defaultView: ViewType.WEEK,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### 月视图\n\n月视图显示传统的日历网格，显示一个月中的所有日期。\n\n#### 配置\n\n| 属性                  | 类型                                                                | 默认值           | 描述                                                                                                  |\n| :-------------------- | :------------------------------------------------------------------ | :--------------- | :---------------------------------------------------------------------------------------------------- |\n| `showWeekNumbers`     | `boolean`                                                           | `false`          | 是否显示周数。                                                                                        |\n| `showMonthIndicator`  | `boolean`                                                           | `true`           | 是否在滚动时显示月份指示标题。                                                                        |\n| `startOfWeek`         | `number`                                                            | `1`              | 一周的开始日期（0 为周日，1 为周一等）。                                                              |\n| `snapToMonth`         | `boolean`                                                           | `false`          | 启用后，用户停止滚动时视图会自动对齐到当前主要月份的起始位置。                                        |\n| `gridDateClick`       | `'week-view' \\| 'day-view' \\| 'none' \\| function`                   | `undefined`      | 点击日期单元格时的操作。                                                                              |\n| `gridDateDoubleClick` | `'create-event' \\| 'week-view' \\| 'day-view' \\| 'none' \\| function` | `'create-event'` | 双击日期单元格时的操作。`'create-event'` (默认) 会在点击的日期创建一个 9:00 至 10:00 的 Timed Event。 |\n| `eventHeight`         | `number`                                                            | `16`             | 月视图网格中每个事件行的高度（px）。                                                                  |\n| `scroll`              | `MonthScrollConfig`                                                 | —                | 控制滚动锁定与月份切换动画，详见下方说明。                                                            |\n\n#### MonthScrollConfig\n\n| 属性         | 类型                    | 默认值      | 描述                                                                                                                                             |\n| :----------- | :---------------------- | :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |\n| `disabled`   | `boolean`               | `false`     | 禁用连续滚动，只能通过 Prev / Next 导航按钮切换月份。                                                                                            |\n| `transition` | `'fade'` \\| `undefined` | `undefined` | 月份切换时的动画效果（仅在 `disabled: true` 时生效）。设为 `'fade'` 时，当前月份向左淡出，下一月份从右侧淡入；省略则保留原有的平滑滚动动画效果。 |\n\n```tsx\ncreateMonthView({\n  scroll: {\n    disabled: true, // 锁定为单月模式，仅允许 Prev/Next 切换\n    transition: 'fade', // 可选淡入淡出动画（省略则保持平滑滚动）\n  },\n});\n```\n\n```tsx {10}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createMonthView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [createMonthView()],\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### Agenda 列表视图\n\nAgenda 视图会按天分组，以时间顺序列表的方式展示事件。它特别适合移动端布局、查看较长时间范围，以及快速浏览接下来要发生的事项。\n\n#### 配置\n\n| 属性                  | 类型                               | 默认值       | 描述                              |\n| :-------------------- | :--------------------------------- | :----------- | :-------------------------------- |\n| `daysToShow`          | `number`                           | `14`         | Agenda 列表中要渲染的连续天数。   |\n| `showEmptyDays`       | `boolean`                          | `true`       | 是否保留没有事件的日期分组。      |\n| `timeFormat`          | `'12h' \\| '24h'`                   | `'24h'`      | 定时事件行使用的时间显示格式。    |\n| `gridDateClick`       | `'day-view' \\| 'none' \\| function` | `undefined`  | 点击某一天 section 时触发的操作。 |\n| `gridDateDoubleClick` | `'day-view' \\| 'none' \\| function` | `'day-view'` | 双击某一天 section 时触发的操作。 |\n\n```tsx {10-15}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createAgendaView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      createAgendaView({\n        daysToShow: 14,\n        timeFormat: '12h',\n        gridDateDoubleClick: 'day-view',\n      }),\n    ],\n    defaultView: ViewType.AGENDA,\n    initialDate: new Date(),\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n### 年视图\n\n年视图提供全面的年度概览。DayFlow 的年视图支持三种模式：`year-canvas`（连续网格）、`fixed-week`（固定 52 周列）和 `grid`（非常适合热力图的 4x3 月份网格）。\n\n#### 配置\n\n| 属性                        | 类型                                                 | 默认值           | 描述                                                                                                              |\n| :-------------------------- | :--------------------------------------------------- | :--------------- | :---------------------------------------------------------------------------------------------------------------- |\n| `mode`                      | `'year-canvas' \\| 'fixed-week' \\| 'grid'`            | `'year-canvas'`  | 年视图的显示模式。                                                                                                |\n| `startOfWeek`               | `number`                                             | `1`              | 设置周起始日（0 为周日，1 为周一等）。用于 `fixed-week` 和 `grid` 模式。                                          |\n| `showTimedEventsInYearView` | `boolean`                                            | `false`          | 是否在年视图中显示定时事件（圆点/指示器/热力强度）。                                                              |\n| `gridDateClick`             | `'popup' \\| 'day-view' \\| 'none' \\| function`        | `'popup'`        | (Grid 模式) 点击日期单元格时的操作。                                                                              |\n| `gridDateDoubleClick`       | `'create-event' \\| 'day-view' \\| 'none' \\| function` | `'create-event'` | (Grid 模式) 双击日期单元格时的操作。`'create-event'` (默认) 会在点击的日期创建一个 9:00 至 10:00 的 Timed Event。 |\n| `gridPopupContent`          | `function`                                           | `undefined`      | (Grid 模式) 自定义日期悬浮弹窗内容的渲染函数。                                                                    |\n| `gridHeatmapLevels`         | `number`                                             | `5`              | (Grid 模式) 热力图的强度层级数量。                                                                                |\n\n#### 热力图自定义 (Grid 模式)\n\n当使用 `mode: 'grid'` 时，日历会根据事件数量显示热力图强度。您可以通过在全局 CSS 中覆盖以下 CSS 变量来自定义颜色：\n\n```css\n/* 浅色模式热力图 */\n.df-year-grid-month {\n  --heat-1: #ebf5ff;\n  --heat-2: #cfe8ff;\n  --heat-3: #91d5ff;\n  --heat-4: #60a5fa;\n  --heat-5: #3b82f6;\n}\n\n/* 深色模式热力图 */\n.dark .df-year-grid-month {\n  --heat-1: #1e3a5f;\n  --heat-2: #2563eb;\n  --heat-3: #1e40af;\n  --heat-4: #3b82f6;\n  --heat-5: #93c5fd;\n}\n```\n\n如果您将 `gridHeatmapLevels` 更改为不同的数字（例如 `3`），则应提供对应数量的变量（例如 `--heat-1` 到 `--heat-3`）。\n\n```tsx {10-15}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createYearView,\n  ViewType,\n} from '@dayflow/react';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      createYearView({\n        mode: 'grid',\n        showTimedEventsInYearView: true,\n        gridHeatmapLevels: 5,\n      }),\n    ],\n    defaultView: ViewType.YEAR,\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 使用多个视图\n\n您可以注册多个视图并允许用户在它们之间切换：\n\n```tsx {13-18}\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createAgendaView,\n  createDayView,\n  createWeekView,\n  createMonthView,\n  createYearView,\n  ViewType,\n} from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\n\nfunction MultiViewCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      createAgendaView({ daysToShow: 10 }),\n      createDayView(),\n      createWeekView(),\n      createMonthView(),\n      createYearView(),\n    ],\n    defaultView: ViewType.MONTH,\n    initialDate: new Date(),\n    callbacks: {\n      onViewChange: view => {\n        console.log('View changed to:', view);\n      },\n    },\n  });\n\n  return (\n    <div className='h-screen'>\n      <DayFlowCalendar calendar={calendar} />\n    </div>\n  );\n}\n```\n\n## 编程控制视图\n\n### 切换视图\n\n```tsx\n// 切换到特定视图\ncalendar.changeView(ViewType.WEEK);\n\n// 获取当前视图\nconst currentView = calendar.app.getCurrentView();\nconsole.log(currentView.type); // 'week'\n```\n\n### 导航\n\n所有视图都支持相同的导航方法：\n\n```tsx\n// 跳转到今天\ncalendar.goToToday();\n\n// 跳转到上一个周期（取决于当前视图，可能是日/周/月/agenda/年）\ncalendar.goToPrevious();\n\n// 跳转到下一个周期\ncalendar.goToNext();\n\n// 选择特定日期\ncalendar.selectDate(new Date(2024, 9, 15));\n```\n\n## ViewType 枚举\n\n```tsx\nenum ViewType {\n  DAY = 'day',\n  WEEK = 'week',\n  MONTH = 'month',\n  AGENDA = 'agenda',\n  YEAR = 'year',\n}\n```\n"
  },
  {
    "path": "website/content/docs-zh/meta.json",
    "content": "{\n  \"title\": \"文档\",\n  \"pages\": [\"introduction\", \"plugins\", \"features\", \"guides\", \"ui\"]\n}\n"
  },
  {
    "path": "website/content/docs-zh/plugins/drag.mdx",
    "content": "# 拖拽插件\n\n拖拽插件启用了交互式的事件管理，允许用户直接在日历网格上移动、调整大小和创建事件。\n\n## 安装\n\n使用您喜欢的包管理器安装插件：\n\n<PackageTabs pkg='@dayflow/plugin-drag' />\n\n```tsx\nimport { createDragPlugin } from '@dayflow/plugin-drag';\n```\n\n## 使用方法\n\n```tsx\nimport { useCalendarApp, ViewType } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createDragPlugin } from '@dayflow/plugin-drag';\n\nfunction MyCalendar() {\n  const dragPlugin = createDragPlugin({\n    enableDrag: true,\n    enableResize: true,\n    enableCreate: true,\n    onEventDrop: (updated, original) => {\n      console.log('Event moved:', updated);\n    },\n    onEventResize: (updated, original) => {\n      console.log('Event resized:', updated);\n    },\n  });\n\n  const calendar = useCalendarApp({\n    // ... 视图配置\n    plugins: [dragPlugin],\n  });\n\n  // ...\n}\n```\n\n## 配置项\n\n| 属性                 | 类型         | 默认值                     | 描述                                   |\n| :------------------- | :----------- | :------------------------- | :------------------------------------- |\n| `enableDrag`         | `boolean`    | `true`                     | 是否允许通过拖拽移动事件。             |\n| `enableResize`       | `boolean`    | `true`                     | 是否允许通过拖拽调整事件持续时间。     |\n| `enableCreate`       | `boolean`    | `true`                     | 是否允许通过双击网格创建事件。         |\n| `enableAllDayCreate` | `boolean`    | `true`                     | 是否允许在全天行通过拖拽创建全天事件。 |\n| `supportedViews`     | `ViewType[]` | `[DAY, WEEK, MONTH, YEAR]` | 启用拖拽功能的视图类型。               |\n| `onEventDrop`        | `Function`   | -                          | 事件移动（拖放）时的回调。             |\n| `onEventResize`      | `Function`   | -                          | 事件调整大小时的回调。                 |\n\n## 插件 API\n\n您可以访问拖拽服务来动态更新配置：\n\n```tsx\nconst dragService = calendar.app.getPlugin<DragService>('drag');\n\n// 动态禁用调整大小功能\ndragService.updateConfig({ enableResize: false });\n```\n"
  },
  {
    "path": "website/content/docs-zh/plugins/events.mdx",
    "content": "# 事件服务插件\n\n事件服务插件提供高级的事件管理功能，包括验证、过滤和复杂的查询。\n\n## 安装\n\n事件服务插件目前是 `@dayflow/core` 包的一部分。\n\n```tsx\nimport { createEventsPlugin } from '@dayflow/core';\n```\n\n## 使用方法\n\n```tsx\nimport {\n  useCalendarApp,\n  DayFlowCalendar,\n  createEventsPlugin,\n} from '@dayflow/react';\n\nfunction MyCalendar() {\n  const eventsPlugin = createEventsPlugin({\n    enableValidation: true,\n    maxEventsPerDay: 50,\n  });\n\n  const calendar = useCalendarApp({\n    // ... 视图配置\n    plugins: [eventsPlugin],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 配置项\n\n| 属性                    | 类型      | 默认值 | 描述                               |\n| :---------------------- | :-------- | :----- | :--------------------------------- |\n| `enableAutoRecalculate` | `boolean` | `true` | 当日期变化时自动重新计算事件片段。 |\n| `enableValidation`      | `boolean` | `true` | 在添加或更新事件前验证事件对象。   |\n| `maxEventsPerDay`       | `number`  | `50`   | 每天允许的最大事件数量。           |\n\n## 插件 API\n\n访问 `EventsService` 以执行高级查询：\n\n```tsx\nconst eventsService = calendar.app.getPlugin<EventsService>('events');\n\n// 获取特定日期的事件\nconst events = eventsService.getByDate(new Date());\n\n// 过滤事件\nconst workEvents = eventsService.filterEvents(\n  allEvents,\n  e => e.calendarId === 'work'\n);\n```\n\n### 可用方法\n\n- `getAll()`: 获取所有事件。\n- `getById(id)`: 查找特定事件。\n- `getByDate(date)`: 获取发生在特定日期的事件。\n- `getByDateRange(start, end)`: 获取日期范围内的事件。\n- `validateEvent(event)`: 返回验证错误数组。\n"
  },
  {
    "path": "website/content/docs-zh/plugins/keyboard-shortcuts.mdx",
    "content": "# 键盘快捷键插件\n\n键盘快捷键 (Keyboard Shortcuts) 插件为日历提供了全局键盘控制功能，支持导航、事件管理和剪贴板操作。\n\n## 安装\n\n安装插件包：\n\n<PackageTabs pkg='@dayflow/plugin-keyboard-shortcuts' />\n\n## 使用方法\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react';\nimport { createKeyboardShortcutsPlugin } from '@dayflow/plugin-keyboard-shortcuts';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    // ...\n    plugins: [\n      createKeyboardShortcutsPlugin({\n        // 可选配置\n      }),\n    ],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 默认快捷键\n\n| 操作              | 快捷键 (Mac)                  | 快捷键 (Windows/Linux) |\n| :---------------- | :---------------------------- | :--------------------- |\n| **跳转到今天**    | `Cmd + T`                     | `Ctrl + T`             |\n| **搜索**          | `Cmd + F`                     | `Ctrl + F`             |\n| **新建事件**      | `Cmd + N`                     | `Ctrl + N`             |\n| **视图导航**      | `左箭头` / `右箭头`           | `左箭头` / `右箭头`    |\n| **循环选择事件**  | `Tab` / `Shift + Tab`         | `Tab` / `Shift + Tab`  |\n| **撤销**          | `Cmd + Z`                     | `Ctrl + Z`             |\n| **重做**          | `Cmd + Shift + Z` / `Cmd + Y` | `Ctrl + Y`             |\n| **复制事件**      | `Cmd + C`                     | `Ctrl + C`             |\n| **剪切事件**      | `Cmd + X`                     | `Ctrl + X`             |\n| **粘贴事件**      | `Cmd + V`                     | `Ctrl + V`             |\n| **删除事件**      | `Backspace` / `Delete`        | `Backspace` / `Delete` |\n| **关闭弹窗/面板** | `Esc`                         | `Esc`                  |\n\n## 配置项\n\n您可以自定义按键映射并为每个操作提供自定义回调函数：\n\n```tsx\ncreateKeyboardShortcutsPlugin({\n  keyMap: {\n    today: 't',\n    search: 'f',\n    prev: 'ArrowLeft',\n    next: 'ArrowRight',\n    undo: 'z',\n    redo: 'y',\n    delete: 'Delete',\n    newEvent: 'n',\n  },\n  callbacks: {\n    undo: app => {\n      console.log('自定义撤销逻辑');\n      app.undo();\n    },\n    redo: app => {\n      console.log('自定义重放逻辑');\n      if ((app as any).redo) (app as any).redo();\n    },\n    delete: app => {\n      if (confirm('您确定要删除吗？')) {\n        const selectedId = app.state.selectedEventId;\n        if (selectedId) app.deleteEvent(selectedId);\n      }\n    },\n  },\n});\n```\n\n### 可选回调函数\n\n`callbacks` 对象支持以下回调：\n\n- `undo`, `redo`, `paste` (接收 `app` 参数)\n- `copy`, `cut`, `delete` (接收 `app` 和 `event?: Event` 参数)\n- `today`, `search`, `prev`, `next`, `newEvent`, `dismiss` (接收 `app` 参数)\n- `tab` (接收 `app` 和 `reverse: boolean` 参数)\n\n## 插件 API\n\n通过以下方式获取插件句柄，以便在运行时控制快捷键行为：\n\n```tsx\nimport { type KeyboardShortcutsService } from '@dayflow/plugin-keyboard-shortcuts';\n\nconst kb = app.getPlugin<KeyboardShortcutsService>('keyboard-shortcuts');\n```\n\n### KeyboardShortcutsService\n\n| 方法          | 返回值    | 说明                                |\n| :------------ | :-------- | :---------------------------------- |\n| `enable()`    | `void`    | 重新启用快捷键处理                  |\n| `disable()`   | `void`    | 暂停所有快捷键，直到调用 `enable()` |\n| `isEnabled()` | `boolean` | 返回快捷键当前是否处于激活状态      |\n\n一个典型的使用场景是：当自定义弹窗或富文本编辑器打开时，临时屏蔽日历快捷键：\n\n```tsx\nfunction MyModal() {\n  const kb = app.getPlugin<KeyboardShortcutsService>('keyboard-shortcuts');\n\n  useEffect(() => {\n    kb?.disable();\n    return () => kb?.enable(); // 卸载时恢复\n  }, []);\n\n  // ...\n}\n```\n\n也可以通过编程方式关闭当前打开的 UI：\n\n```tsx\napp.dismissUI();\n```\n"
  },
  {
    "path": "website/content/docs-zh/plugins/localization.mdx",
    "content": "# i18n 插件\n\ni18n 插件提供多语言和本地化支持。默认情况下，Day Flow 核心库仅包含英文 (`en-US`) 语言包，以保持最小的包体积。\n\n## 安装\n\n安装多语言插件包：\n\n<PackageTabs pkg='@dayflow/plugin-localization' />\n\n## 使用方法\n\n要启用其他语言支持，请注册插件并提供所需的语言对象。\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react';\nimport {\n  createLocalizationPlugin,\n  zh,\n  ja,\n  fr,\n} from '@dayflow/plugin-localization';\n\nfunction MyCalendar() {\n  const calendar = useCalendarApp({\n    views: [\n      /* 视图配置 */\n    ],\n    plugins: [\n      createLocalizationPlugin({\n        locales: [zh, ja, fr], // 注册你需要的语言包\n      }),\n    ],\n    locale: 'zh-CN', // 设置当前语言\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 配置项\n\n该插件接受一个 `Locale` 对象列表，用于注册到全局语言注册表中。\n\n```tsx\ninterface LocalizationConfig {\n  locales: Locale[];\n}\n```\n\n## 可用的语言包\n\n`@dayflow/plugin-localization` 包含以下语言包：\n\n| 导出名称 | 语言     | 语言代码               |\n| :------- | :------- | :--------------------- |\n| `zh`     | 中文     | `zh-CN`                |\n| `ja`     | 日语     | `ja-JP`                |\n| `ko`     | 韩语     | `ko-KR`                |\n| `fr`     | 法语     | `fr-FR`                |\n| `de`     | 德语     | `de-DE`                |\n| `es`     | 西班牙语 | `es-ES`                |\n| `en`     | 英语     | `en-US` (核心库已内置) |\n\n## 自定义语言包\n\n你也可以不使用插件直接注册自定义语言包，或者通过插件传入它们：\n\n```tsx\nconst myCustomLocale = {\n  code: 'pt-BR',\n  messages: {\n    today: 'Hoje',\n    day: 'Dia',\n    week: 'Semana',\n    month: 'Mês',\n    // ... 其他所有翻译键值\n  },\n};\n\nconst calendar = useCalendarApp({\n  plugins: [\n    createLocalizationPlugin({\n      locales: [myCustomLocale],\n    }),\n  ],\n  locale: 'pt-BR',\n});\n```\n"
  },
  {
    "path": "website/content/docs-zh/plugins/meta.json",
    "content": "{\n  \"title\": \"插件\",\n  \"defaultOpen\": true,\n  \"pages\": [\n    \"overview\",\n    \"localization\",\n    \"drag\",\n    \"print\",\n    \"events\",\n    \"keyboard-shortcuts\",\n    \"sidebar\"\n  ]\n}\n"
  },
  {
    "path": "website/content/docs-zh/plugins/overview.mdx",
    "content": "# 插件概览\n\nDay Flow 使用插件架构来扩展功能。插件提供模块化的功能特性，可以根据您的需要启用、禁用或配置。\n\n## 为什么使用插件？\n\n插件系统允许您：\n\n- **按需加载** - 仅加载您需要的功能（例如：默认仅包含英文以减小包体积）。\n- **配置行为** - 为特定用例定制插件设置。\n- **扩展功能** - 为您的独特需求创建自定义插件。\n- **访问插件 API** - 直接在业务逻辑中使用插件服务。\n\n## 可用插件\n\nDay Flow 提供以下官方插件：\n\n### i18n\n\n支持多种语言。将非英文语言包提取到插件中，可以让仅使用英文的应用拥有更小的核心包体积。[了解更多](../localization)\n\n### 拖拽 (Drag & Drop)\n\n启用交互式事件管理，包括移动、调整大小以及双击创建事件。[了解更多](../drag)\n\n### 事件服务 (Events Service)\n\n提供高级事件管理功能，包括验证、过滤和日期范围查询。[了解更多](../events)\n\n---\n\n## 插件生命周期\n\n1. **创建插件** - 调用工厂函数（如 `createDragPlugin()`）并传入配置。\n2. **注册插件** - 在初始化日历时，将插件实例添加到 `plugins` 数组中。\n3. **安装** - `install()` 函数会自动调用，并传入 `CalendarApp` 实例。\n4. **使用** - 通过 `app.getPlugin('plugin-name')` 访问插件 API。\n\n## 创建自定义插件\n\n您可以通过实现 `CalendarPlugin` 接口来创建自己的插件：\n\n```tsx\nimport { CalendarPlugin, ICalendarApp } from '@dayflow/core';\n\nexport const myPlugin: CalendarPlugin = {\n  name: 'my-custom-plugin',\n  install(app: ICalendarApp) {\n    console.log('插件已安装！');\n    // 在此处扩展应用功能\n  },\n};\n```\n"
  },
  {
    "path": "website/content/docs-zh/plugins/print.mdx",
    "content": "---\ntitle: 打印插件\ndescription: DayFlow Pro 打印和导出工作流，支持预览、纸张尺寸、日历过滤和多种打印布局。\nstatus: pro\n---\n\n# 打印插件 (Print Plugin)\n\n`@dayflow-pro/plugin-print` 为 DayFlow 增加了内置的打印对话框和预览流程。它支持日、周、月以及两种年度布局，允许用户选择纸张尺寸和方向，并能根据上下文打印当前可见的事件集或所有匹配的事件。\n\n试用[在线演示](https://pro.dayflow.studio)\n\n## 安装\n\n<PackageTabs pkg='@dayflow-pro/plugin-print' />\n\n有关 DayFlow Pro 的安装步骤，请参阅 [Pro 安装](/docs/introduction/pro-installation)。\n\n## 基本用法\n\n```tsx\nimport { DayFlowCalendar, useCalendarApp } from '@dayflow/react';\nimport { createPrintPlugin } from '@dayflow-pro/plugin-print';\n\nfunction App() {\n  const printPlugin = createPrintPlugin({\n    defaultOptions: {\n      miniCalendar: true,\n      calendarKeys: true,\n      textSize: 'medium',\n    },\n  });\n\n  const calendar = useCalendarApp({\n    plugins: [printPlugin],\n  });\n\n  return (\n    <>\n      <button onClick={() => printPlugin.api.open()}>打印</button>\n      <DayFlowCalendar calendar={calendar} />\n    </>\n  );\n}\n```\n\n该插件还会在日历实例上直接安装两个辅助方法：\n\n```tsx\nawait calendar.openPrintDialog();\nawait calendar.print();\n```\n\n## 插件提供的功能\n\n- 渲染到 `document.body` 的打印预览对话框\n- 通过 `window.print()` 调用原生浏览器打印\n- 提供日、周、月和年度布局的视图选择\n- 通过 `calendarIds` 进行日历级别的过滤\n- 纸张尺寸和方向控制\n- 安装插件后支持 Cmd/Ctrl + P 快捷键\n\n## 支持的打印视图\n\n| 视图              | 备注                                       |\n| :---------------- | :----------------------------------------- |\n| `day`             | 打印一个或多个选定的日期。                 |\n| `week`            | 打印周样式的布局。                         |\n| `month`           | 打印月网格。                               |\n| `year-fixed-week` | 使用固定的每周行进行年度概览。默认为横向。 |\n| `year-canvas`     | 针对纵向输出优化的密集年度概览。           |\n\n对话框打开时，会根据当前的 DayFlow 视图派生其初始视图：\n\n- 当前为月视图 -> `month`\n- 当前为周视图 -> `week`\n- 当前为日视图 -> `day`\n- 当前为年视图 -> `year-fixed-week`\n\n## 插件配置\n\n| 属性             | 类型                            | 默认值 | 备注                               |\n| :--------------- | :------------------------------ | :----- | :--------------------------------- |\n| `enabled`        | `boolean`                       | `true` | 设置为 `false` 时完全禁用安装。    |\n| `defaultOptions` | `Partial<CalendarPrintOptions>` | —      | 预加载自定义打印选项。             |\n| `license`        | `PackageLicenseConfig`          | —      | 当你不使用全局注册时的可选覆盖项。 |\n\n## CalendarPrintOptions\n\n| 属性            | 类型                             | 默认值   | 备注                       |\n| :-------------- | :------------------------------- | :------- | :------------------------- |\n| `allDayEvents`  | `boolean`                        | `true`   | 包含全天事件。             |\n| `timedEvents`   | `boolean`                        | `true`   | 包含带时间事件。           |\n| `miniCalendar`  | `boolean`                        | `true`   | 在打印页眉中显示迷你日历。 |\n| `calendarKeys`  | `boolean`                        | `true`   | 显示日历图例。             |\n| `blackAndWhite` | `boolean`                        | `false`  | 渲染时不带日历颜色。       |\n| `textSize`      | `'small' \\| 'medium' \\| 'large'` | `medium` | 控制预览和打印的排版密度。 |\n\n## CalendarPrintConfig\n\n你可以使用部分配置预设或覆盖打印对话框：\n\n```ts\ntype CalendarPrintConfig = {\n  view: 'month' | 'week' | 'day' | 'year-fixed-week' | 'year-canvas';\n  paper: 'A4' | 'Letter';\n  orientation?: 'portrait' | 'landscape';\n  startDate: Date;\n  endDate?: Date;\n  calendarIds: string[];\n  options: CalendarPrintOptions;\n};\n```\n\n源码中的重要行为：\n\n- `year-fixed-week` 强制使用 `landscape`（横向）。\n- `year-canvas` 强制使用 `portrait`（纵向）。\n- 对话框在打开浏览器原生打印 UI 之前会注入 `@page` 规则。\n- 预览支持在打印前进行缩放和移动。\n\n## 打印范围\n\n对话框并不总是从同一个事件源拉取数据：\n\n- 如果选择的打印视图与当前日历视图匹配，且 `startDate` 与应用当前日期匹配，预览渲染将使用 `app.getEvents()` 获取可见事件集。\n- 否则，它将使用 `app.getAllEvents()`，以便打印可以覆盖当前渲染屏幕之外的日期。\n\n这在内部公开为 `sourceScope: 'visible' | 'all'`。\n\n## API\n\n`createPrintPlugin()` 返回一个带有 `api` 对象的普通 DayFlow 插件：\n\n```ts\ntype PrintPluginApi = {\n  open: () => void;\n  close: () => void;\n  print: (config?: Partial<CalendarPrintConfig>) => void;\n};\n```\n\n备注：\n\n- `api.open()` 打开打印对话框。\n- `api.close()` 关闭已挂载的对话框。\n- `api.print(config)` 目前也会打开对话框；暂不支持绕过预览直接打印。\n- 插件安装后会全局监听 `Cmd/Ctrl + P`。\n"
  },
  {
    "path": "website/content/docs-zh/plugins/sidebar.mdx",
    "content": "import { SidebarCustomShowcase } from '@/components/showcase/SidebarShowcases';\n\n# 侧边栏插件\n\n侧边栏插件为 DayFlow 日历添加日历管理侧边栏。包含用于日期导航的迷你日历、带可见性切换的日历列表，以及完整的日历 CRUD 操作（创建、重命名、更改颜色、合并、删除、导入/导出）。\n\n## 安装\n\n安装插件包：\n\n<PackageTabs pkg='@dayflow/plugin-sidebar' />\n\n## 使用\n\n```tsx\nimport { useCalendarApp, DayFlowCalendar } from '@dayflow/react'; // or '@dayflow/vue', '@dayflow/svelte', '@dayflow/angular'\nimport { createSidebarPlugin } from '@dayflow/plugin-sidebar';\n\nfunction MyCalendar() {\n  const sidebarPlugin = createSidebarPlugin({\n    width: 280,\n    createCalendarMode: 'modal',\n  });\n\n  const calendar = useCalendarApp({\n    views: [\n      /* 视图 */\n    ],\n    plugins: [sidebarPlugin],\n  });\n\n  return <DayFlowCalendar calendar={calendar} />;\n}\n```\n\n## 属性参数\n\n| 属性                         | 类型                                                   | 默认值                             | 描述                                |\n| :--------------------------- | :----------------------------------------------------- | :--------------------------------- | :---------------------------------- |\n| `width`                      | `number \\| string`                                     | `'240px'`                          | 侧边栏宽度（如 `280` 或 `'20rem'`） |\n| `miniWidth`                  | `string`                                               | `'50px'`                           | 折叠状态下的侧边栏宽度              |\n| `initialCollapsed`           | `boolean`                                              | `false`                            | 是否以折叠状态启动侧边栏            |\n| `createCalendarMode`         | `'inline' \\| 'modal'`                                  | `'inline'`                         | 创建日历表单的显示方式              |\n| `colorPickerMode`            | `'blossom' \\| 'default'`                               | `'default'`                        | 使用的颜色选择器组件                |\n| `onSubscribeCalendar`        | `(calendar, events) => Promise<void>`                  | `undefined`                        | 成功订阅新日历后的回调              |\n| `onLoadSubscription`         | `(calendar) => Promise<void>`                          | `undefined`                        | 针对现有订阅的自定义加载逻辑        |\n| `onReorder`                  | `(calendars: CalendarType[]) => void \\| Promise<void>` | `undefined`                        | 日历排序变更后的回调                |\n| `componentsOrder`            | `('calendarList' \\| 'miniCalendar')[]`                 | `['calendarList', 'miniCalendar']` | 侧边栏组件显示顺序                  |\n| `render`                     | `(props: CalendarSidebarRenderProps) => TNode`         | `undefined`                        | 完全自定义侧边栏 UI                 |\n| `renderSidebarHeader`        | `(args: SidebarHeaderSlotArgs) => TNode`               | `undefined`                        | 自定义侧边栏头部渲染器              |\n| `renderCalendarContextMenu`  | `(calendar, onClose) => TNode`                         | `undefined`                        | 日历项的自定义右键菜单渲染器        |\n| `renderCreateCalendarDialog` | `(props) => TNode`                                     | `undefined`                        | 自定义创建日历对话框渲染器          |\n\n## 编程式 API\n\n日历渲染完成后，通过 `app.getPlugin` 获取 `SidebarService`：\n\n```tsx\nimport { type SidebarService } from '@dayflow/plugin-sidebar';\n\nconst sidebar = app.getPlugin<SidebarService>('sidebar');\n\nsidebar?.collapse(); // 折叠侧边栏\nsidebar?.expand(); // 展开侧边栏\nsidebar?.setCollapsed(true); // 显式设置状态\nconsole.log(sidebar?.isCollapsed()); // 读取当前状态\n```\n\n### SidebarService\n\n| 方法                               | 返回值    | 说明               |\n| :--------------------------------- | :-------- | :----------------- |\n| `collapse()`                       | `void`    | 折叠侧边栏         |\n| `expand()`                         | `void`    | 展开侧边栏         |\n| `setCollapsed(collapsed: boolean)` | `void`    | 显式设置折叠状态   |\n| `isCollapsed()`                    | `boolean` | 返回当前的折叠状态 |\n\n> **注意：** 这些方法在首次渲染之前调用会静默 no-op。实际的用户交互和生命周期回调均发生在渲染之后，因此在实践中几乎不需要担心这个问题。\n\n### 绑定到键盘快捷键\n\n```tsx\nwindow.addEventListener('keydown', e => {\n  if (e.metaKey && e.key === '[') {\n    const sidebar = app.getPlugin<SidebarService>('sidebar');\n    sidebar?.setCollapsed(!sidebar.isCollapsed());\n  }\n});\n```\n\n## 功能\n\n侧边栏插件提供包含颜色点、可见性开关和折叠按钮的侧栏布局。它会自动与日历状态同步，无需额外代码。同时内置了日历管理与右键菜单功能。\n\n内置功能包括：\n\n- **迷你日历** - 用于快速日期导航的紧凑月视图\n- **日历列表** - 通过彩色复选框切换各个日历的可见性\n- **创建日历** - 使用自定义名称和颜色添加新日历\n- **重命名 / 更改颜色** - 右键点击日历以重命名或更改颜色\n- **日历排序** - 通过在列表中拖拽日历来调整排序。使用 `onReorder` 回调将新顺序持久化到后端。\n- **合并日历** - 将一个日历的所有事件移动到另一个日历\n- **删除日历** - 带确认步骤的日历删除（也可先合并）\n- **导入 / 导出** - 导入 `.ics` 文件或将日历导出为 `.ics`\n- **订阅日历** - 通过 ICS URL 订阅远程日历，已订阅的日历在侧边栏列表中会显示标识图标\n\n### 在线示例\n\n这一页用两个示例展示侧边栏的不同形态：一个是开箱即用的默认布局，另一个是使用框架（如 React）完全定制的版本。\n\n<SidebarCustomShowcase />\n\n#### 可以尝试\n\n- **可见性切换**：在两个示例中切换日历可见性，观察 UI 如何瞬时同步\n- **拖拽同步**：拖拽或创建事件，侧栏的\"即将到来\"卡片会实时更新\n- **收起侧栏**：折叠侧栏，看看在周视图等高密度布局里如何回收更多空间\n\n## 订阅日历\n\n侧边栏内置了订阅远程 ICS 日历源的功能。在侧边栏的日历列表或空白区域右键点击打开菜单，选择**订阅日历**，然后输入任意公开的 `.ics` 地址即可。\n\nDayFlow 会自动获取该文件、解析事件并创建一个新日历。已订阅的日历在侧边栏列表中会显示一个小图标，以便与本地创建的日历区分。\n\n### 自动加载与去重\n\n如果你在初始配置中提供了带有 `subscription.url` 的日历，侧边栏插件会在挂载时自动获取最新事件。\n\n为了防止将本地缓存的事件与新鲜的订阅数据结合时出现视觉重复，DayFlow 实现了**基于 ID 的去重机制**：\n\n- 如果来自订阅的事件与核心存储中已有的事件具有相同的 `id`，订阅版本的事件将优先显示。\n- 这确保了你的 UI 始终显示最新信息，而不会在页面刷新后出现“双重”事件。\n\n### 持久化订阅日历\n\nDayFlow 通过 `CalendarType` 对象上是否存在 `subscription` 字段来识别订阅日历。\n\n**订阅日历的默认行为：**\n\n- **只读**：UI 修改操作（编辑标题、更换日历、删除）会通过 `canMutateFromUI()` 禁用。\n- **不可拖拽**：禁用拖放操作（移动、调整大小）。\n- **隐藏备注**：如果事件没有描述，详情面板中的“备注”字段将被隐藏，以保持界面整洁。\n\n如果您希望允许用户修改订阅事件或移动它们，可以通过在日历对象上设置 `readOnly: false` 来显式覆盖这些默认保护：\n\n```tsx\nconst calendar = useCalendarApp({\n  calendars: [\n    {\n      id: 'team-ics',\n      name: '团队日历',\n      subscription: {\n        url: 'https://example.com/calendar.ics',\n        status: 'ready',\n      },\n      // 覆盖默认保护：\n      readOnly: false, // 同时启用 UI 修改和拖放操作\n      colors: {\n        /* ... */\n      },\n    },\n  ],\n});\n```\n\n将日历状态保存到后端时，请保留 `subscription` 字段，并在下次加载时恢复。\n},\n],\n});\n\n````\n\n> **注意：** DayFlow 不会自动刷新已订阅的日历源。如需保持事件最新，请在应用中自行实现定期拉取逻辑，并通过 `app.addEvent()` / `app.removeEvent()` 更新事件。\n\n## 完全自定义侧边栏\n\n你可以通过向 `createSidebarPlugin` 传入 `render` 函数来完全替换侧边栏。它会接收实时的日历状态和辅助操作。\n\n```tsx\nimport {\n  createSidebarPlugin,\n  type CalendarSidebarRenderProps,\n} from '@dayflow/plugin-sidebar';\n\nconst CustomSidebar = ({\n  app,\n  calendars,\n  toggleCalendarVisibility,\n  toggleAll,\n  isCollapsed,\n  setCollapsed,\n}: CalendarSidebarRenderProps) => {\n  if (isCollapsed) {\n    return (\n      <div className='p-2'>\n        <button onClick={() => setCollapsed(false)}>展开 →</button>\n      </div>\n    );\n  }\n\n  return (\n    <aside className='flex h-full flex-col gap-4 p-4 bg-slate-50 border-r'>\n      <header className='flex items-center justify-between'>\n        <h3 className='font-semibold'>我的工作区</h3>\n        <button onClick={() => setCollapsed(true)}>收起 ←</button>\n      </header>\n\n      <nav className='space-y-1'>\n        {calendars.map(calendar => (\n          <label\n            key={calendar.id}\n            className='flex items-center gap-2 cursor-pointer'\n          >\n            <input\n              type='checkbox'\n              checked={calendar.isVisible}\n              onChange={e =>\n                toggleCalendarVisibility(calendar.id, e.target.checked)\n              }\n            />\n            <span\n              className='w-3 h-3 rounded-full'\n              style={{ backgroundColor: calendar.colors.lineColor }}\n            />\n            {calendar.name}\n          </label>\n        ))}\n      </nav>\n\n      <div className='mt-auto pt-4 border-t text-xs text-slate-500'>\n        当前日期：{app.getCurrentDate().toLocaleDateString()}\n      </div>\n    </aside>\n  );\n};\n\nconst sidebarPlugin = createSidebarPlugin({\n  render: props => <CustomSidebar {...props} />,\n});\n````\n\n## 自定义右键菜单\n\n可以替换日历项的默认右键菜单：\n\n```tsx\ncreateSidebarPlugin({\n  renderCalendarContextMenu: (calendar, onClose) => (\n    <div className='bg-white shadow-lg border rounded p-2'>\n      <button\n        onClick={() => {\n          console.log('自定义操作');\n          onClose();\n        }}\n      >\n        针对 {calendar.name} 的自定义操作\n      </button>\n    </div>\n  ),\n});\n```\n\n## 自定义创建日历对话框\n\n可以替换默认的创建日历对话框：\n\n```tsx\ncreateSidebarPlugin({\n  renderCreateCalendarDialog: ({ onCreate, onClose }) => (\n    <MyCustomDialog onSave={onCreate} onCancel={onClose} />\n  ),\n});\n```\n\n## 自定义侧边栏头部\n\n可以替换默认的侧边栏头部（包含“日历”标题和折叠按钮的区域）：\n\n```tsx\ncreateSidebarPlugin({\n  renderSidebarHeader: ({ isCollapsed, onCollapseToggle }) => (\n    <div className='flex items-center justify-between p-4 border-b'>\n      {!isCollapsed && <span className='font-bold text-lg'>我的日历</span>}\n      <button\n        onClick={onCollapseToggle}\n        className='p-1 hover:bg-slate-100 rounded'\n      >\n        {isCollapsed ? '→' : '←'}\n      </button>\n    </div>\n  ),\n});\n```\n"
  },
  {
    "path": "website/content/docs-zh/ui/context-menu.mdx",
    "content": "---\ntitle: 右键菜单\n---\n\n# 右键菜单\n\n基于 Portal 的可组合右键菜单组件。支持嵌套子菜单、标签、分隔线和内置颜色选择器。点击外部、滚动、窗口缩放或按下 `Escape` 时自动关闭。\n\n## 安装\n\n<PackageTabs pkg='@dayflow/ui-context-menu' />\n\n如果你的项目已经使用 Tailwind CSS，请导入仅包含组件样式的入口：\n\n```css\n@import '@dayflow/ui-context-menu/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\n如果你的项目不使用 Tailwind CSS，则继续导入完整样式：\n\n```tsx\nimport {\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuLabel,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n  ContextMenuColorPicker,\n} from '@dayflow/ui-context-menu';\nimport '@dayflow/ui-context-menu/dist/styles.css';\n```\n\n## 基础用法\n\n在 `contextmenu` 事件中捕获鼠标位置，将其传给 `ContextMenu`。\n\n```tsx\nimport { useState } from 'react';\nimport {\n  ContextMenu,\n  ContextMenuItem,\n  ContextMenuSeparator,\n} from '@dayflow/ui-context-menu';\n\nfunction MyComponent() {\n  const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);\n\n  const handleContextMenu = (e: React.MouseEvent) => {\n    e.preventDefault();\n    setMenu({ x: e.clientX, y: e.clientY });\n  };\n\n  return (\n    <div onContextMenu={handleContextMenu}>\n      在此处右键单击\n      {menu && (\n        <ContextMenu x={menu.x} y={menu.y} onClose={() => setMenu(null)}>\n          <ContextMenuItem onClick={() => console.log('编辑')}>\n            编辑\n          </ContextMenuItem>\n          <ContextMenuItem onClick={() => console.log('复制')}>\n            复制\n          </ContextMenuItem>\n          <ContextMenuSeparator />\n          <ContextMenuItem onClick={() => console.log('删除')} danger>\n            删除\n          </ContextMenuItem>\n        </ContextMenu>\n      )}\n    </div>\n  );\n}\n```\n\n## 标签与图标\n\n```tsx\n<ContextMenu x={x} y={y} onClose={onClose}>\n  <ContextMenuLabel>操作</ContextMenuLabel>\n  <ContextMenuItem icon={<PencilIcon />} onClick={() => onEdit()}>\n    编辑事件\n  </ContextMenuItem>\n  <ContextMenuItem icon={<CopyIcon />} onClick={() => onDuplicate()}>\n    复制\n  </ContextMenuItem>\n  <ContextMenuSeparator />\n  <ContextMenuItem icon={<TrashIcon />} onClick={() => onDelete()} danger>\n    删除\n  </ContextMenuItem>\n</ContextMenu>\n```\n\n## 嵌套子菜单\n\n使用 `ContextMenuSub` 包裹菜单项以创建悬停触发的子菜单，子菜单会自动调整位置以避免超出视口。\n\n```tsx\n<ContextMenu x={x} y={y} onClose={onClose}>\n  <ContextMenuItem onClick={() => onEdit()}>编辑</ContextMenuItem>\n  <ContextMenuSub>\n    <ContextMenuSubTrigger>移动到</ContextMenuSubTrigger>\n    <ContextMenuSubContent>\n      <ContextMenuItem onClick={() => onMove('work')}>工作</ContextMenuItem>\n      <ContextMenuItem onClick={() => onMove('personal')}>个人</ContextMenuItem>\n    </ContextMenuSubContent>\n  </ContextMenuSub>\n  <ContextMenuSeparator />\n  <ContextMenuItem danger onClick={() => onDelete()}>\n    删除\n  </ContextMenuItem>\n</ContextMenu>\n```\n\n## 颜色选择器\n\n`ContextMenuColorPicker` 渲染一排预设色块，并可选地提供\"自定义颜色\"操作。\n\n```tsx\n<ContextMenu x={x} y={y} onClose={onClose}>\n  <ContextMenuLabel>日历颜色</ContextMenuLabel>\n  <ContextMenuColorPicker\n    selectedColor={currentColor}\n    onSelect={color => {\n      updateCalendarColor(color);\n      onClose();\n    }}\n    onCustomColor={() => openColorDialog()}\n    customColorLabel='自定义颜色...'\n  />\n</ContextMenu>\n```\n\n## 禁用菜单项\n\n```tsx\n<ContextMenuItem onClick={() => onPaste()} disabled={!hasClipboard}>\n  粘贴\n</ContextMenuItem>\n```\n\n## API 参考\n\n### `ContextMenu`\n\n| 属性        | 类型                | 说明                           |\n| :---------- | :------------------ | :----------------------------- |\n| `x`         | `number`            | 水平位置（如 `event.clientX`） |\n| `y`         | `number`            | 垂直位置（如 `event.clientY`） |\n| `onClose`   | `() => void`        | 菜单需要关闭时调用             |\n| `children`  | `ComponentChildren` | 菜单内容                       |\n| `className` | `string`            | 菜单容器的额外 CSS 类名        |\n\n### `ContextMenuItem`\n\n| 属性       | 类型                | 默认值  | 说明                 |\n| :--------- | :------------------ | :------ | :------------------- |\n| `onClick`  | `() => void`        | —       | 点击处理函数         |\n| `children` | `ComponentChildren` | —       | 菜单项文本           |\n| `icon`     | `ComponentChildren` | —       | 显示在文本左侧的图标 |\n| `danger`   | `boolean`           | `false` | 以危险红色样式渲染   |\n| `disabled` | `boolean`           | `false` | 禁用交互             |\n\n### `ContextMenuColorPicker`\n\n| 属性               | 类型                      | 默认值           | 说明                     |\n| :----------------- | :------------------------ | :--------------- | :----------------------- |\n| `selectedColor`    | `string`                  | —                | 当前选中颜色（十六进制） |\n| `onSelect`         | `(color: string) => void` | —                | 点击色块时调用           |\n| `onCustomColor`    | `() => void`              | —                | 点击自定义颜色行时调用   |\n| `customColorLabel` | `string`                  | `\"Custom Color\"` | 自定义颜色操作的文案     |\n\n### 其他导出\n\n| 组件                    | 说明                         |\n| :---------------------- | :--------------------------- |\n| `ContextMenuSeparator`  | 水平分隔线                   |\n| `ContextMenuLabel`      | 非交互式分组标题             |\n| `ContextMenuSub`        | 管理子菜单展开状态的包裹组件 |\n| `ContextMenuSubTrigger` | 悬停时展开子菜单的触发行     |\n| `ContextMenuSubContent` | 浮动子菜单面板               |\n"
  },
  {
    "path": "website/content/docs-zh/ui/meta.json",
    "content": "{\n  \"title\": \"UI\",\n  \"pages\": [\"context-menu\", \"range-picker\"]\n}\n"
  },
  {
    "path": "website/content/docs-zh/ui/range-picker.mdx",
    "content": "---\ntitle: 日期范围选择器\n---\n\n# 日期范围选择器\n\n基于 Temporal API 构建的日期/时间范围选择器。支持仅日期和日期+时间两种模式、时区感知、键盘输入以及灵活的弹出层定位。\n\n## 安装\n\n<PackageTabs pkg='@dayflow/ui-range-picker' />\n\n如果你的项目已经使用 Tailwind CSS，请导入仅包含组件样式的入口：\n\n```css\n@import '@dayflow/ui-range-picker/dist/styles.components.css';\n@import 'tailwindcss';\n```\n\n如果你的项目不使用 Tailwind CSS，则继续导入完整样式：\n\n```tsx\nimport { RangePicker } from '@dayflow/ui-range-picker';\nimport '@dayflow/ui-range-picker/dist/styles.css';\n```\n\n## 基础用法\n\n```tsx\nimport { useState } from 'react';\nimport { Temporal } from 'temporal-polyfill';\nimport { RangePicker } from '@dayflow/ui-range-picker';\nimport type { ZonedRange } from '@dayflow/ui-range-picker';\n\nfunction MyComponent() {\n  const [range, setRange] = useState<ZonedRange>([\n    Temporal.Now.zonedDateTimeISO(),\n    Temporal.Now.zonedDateTimeISO().add({ hours: 1 }),\n  ]);\n\n  return <RangePicker value={range} onChange={value => setRange(value)} />;\n}\n```\n\n## 仅日期模式\n\n传入 `showTime={false}` 可隐藏时间选择器。\n\n```tsx\n<RangePicker\n  value={range}\n  showTime={false}\n  format='YYYY-MM-DD'\n  onChange={value => setRange(value)}\n/>\n```\n\n## 指定时区\n\n```tsx\n<RangePicker\n  value={range}\n  timeZone='Asia/Shanghai'\n  onChange={(value, dateStrings) => {\n    console.log('范围:', value);\n    console.log('格式化:', dateStrings); // ['2024-10-15 10:00', '2024-10-15 11:00']\n  }}\n/>\n```\n\n## 自定义时间格式\n\n```tsx\n<RangePicker\n  value={range}\n  format='YYYY/MM/DD'\n  showTime={{ format: 'HH:mm' }}\n  onChange={value => setRange(value)}\n  onOk={value => saveToBackend(value)}\n/>\n```\n\n## 弹出层定位\n\n弹出层默认显示在左下方，并会自动调整以避免超出视口。\n\n```tsx\n<RangePicker\n  value={range}\n  placement='topRight'\n  autoAdjustOverflow={true}\n  onChange={value => setRange(value)}\n/>\n```\n\n## 国际化\n\n传入 BCP 47 语言标签以本地化月份和星期名称。\n\n```tsx\n<RangePicker value={range} locale='zh-CN' onChange={value => setRange(value)} />\n```\n\n## 与触发器同宽\n\n```tsx\n<RangePicker\n  value={range}\n  matchTriggerWidth\n  onChange={value => setRange(value)}\n/>\n```\n\n## API 参考\n\n### `RangePicker`\n\n| 属性                 | 类型                                                            | 默认值         | 说明                                                      |\n| :------------------- | :-------------------------------------------------------------- | :------------- | :-------------------------------------------------------- |\n| `value`              | `[Temporal.PlainDate \\| PlainDateTime \\| ZonedDateTime, ...]`   | —              | 受控的范围值，支持任意 Temporal 类型组合                  |\n| `format`             | `string`                                                        | `\"YYYY-MM-DD\"` | 日期部分的显示和解析格式                                  |\n| `showTime`           | `boolean \\| { format?: string }`                                | `true`         | 启用时间选择。传入对象可自定义时间格式                    |\n| `showTimeFormat`     | `string`                                                        | `\"HH:mm\"`      | `showTime` 为 `true` 时的默认时间格式                     |\n| `onChange`           | `(value: ZonedRange, dateString: [string, string]) => void`     | —              | 每次选择变化时触发                                        |\n| `onOk`               | `(value: ZonedRange, dateString: [string, string]) => void`     | —              | 用户点击确认按钮时触发                                    |\n| `timeZone`           | `string`                                                        | —              | IANA 时区字符串（如 `\"Asia/Shanghai\"`），默认使用系统时区 |\n| `disabled`           | `boolean`                                                       | `false`        | 禁用所有交互                                              |\n| `placement`          | `'bottomLeft' \\| 'bottomRight' \\| 'topLeft' \\| 'topRight'`      | `'bottomLeft'` | 弹出层的首选位置                                          |\n| `autoAdjustOverflow` | `boolean`                                                       | `true`         | 超出视口时自动翻转弹出层位置                              |\n| `getPopupContainer`  | `() => HTMLElement`                                             | —              | 将弹出层挂载到自定义容器，而非 `document.body`            |\n| `matchTriggerWidth`  | `boolean`                                                       | `false`        | 将弹出层宽度设置为与触发器输入框宽度一致                  |\n| `locale`             | `string \\| { code: string; messages?: Record<string, string> }` | `'en-US'`      | BCP 47 语言标签，用于本地化月份和星期名称                 |\n\n### `ZonedRange`\n\n```ts\ntype ZonedRange = [Temporal.ZonedDateTime, Temporal.ZonedDateTime];\n```\n\n`onChange` 和 `onOk` 的回调始终返回 `ZonedRange`，无论输入值类型如何，都会被规范化为当前时区。\n"
  },
  {
    "path": "website/eslint.config.mjs",
    "content": "import nextVitals from 'eslint-config-next/core-web-vitals';\nimport { defineConfig, globalIgnores } from 'eslint/config';\n\nconst nextVitalsWithoutReactRules = nextVitals.map(config => {\n  if (!config.rules) {\n    return config;\n  }\n\n  const filteredRules = Object.fromEntries(\n    Object.entries(config.rules).filter(\n      ([ruleName]) => !ruleName.startsWith('react/')\n    )\n  );\n\n  return {\n    ...config,\n    rules: filteredRules,\n  };\n});\n\nconst eslintConfig = defineConfig([\n  ...nextVitalsWithoutReactRules,\n  globalIgnores([\n    '.next/**',\n    'out/**',\n    'build/**',\n    'next-env.d.ts',\n    '.source/**',\n  ]),\n]);\n\nexport default eslintConfig;\n"
  },
  {
    "path": "website/lib/cn.ts",
    "content": "export { twMerge as cn } from 'tailwind-merge';\n"
  },
  {
    "path": "website/lib/i18n.ts",
    "content": "export const languages = [\n  { code: 'en', name: 'English', prefix: '/docs' },\n  { code: 'zh', name: '中文', prefix: '/docs-zh' },\n  { code: 'ja', name: '日本語', prefix: '/docs-ja' },\n] as const;\n\nexport type LanguageCode = (typeof languages)[number]['code'];\n\nexport const defaultLanguage: LanguageCode = 'en';\n\nexport const localeItems = languages.map(language => ({\n  locale: language.code,\n  name: language.name,\n}));\n\nexport function getLanguageFromPathname(pathname: string) {\n  return (\n    [...languages]\n      .toSorted((a, b) => b.prefix.length - a.prefix.length)\n      .find(\n        language =>\n          pathname === language.prefix ||\n          pathname.startsWith(`${language.prefix}/`)\n      ) ?? languages[0]\n  );\n}\n\nexport function getLanguageCodeFromPathname(pathname: string): LanguageCode {\n  return getLanguageFromPathname(pathname).code;\n}\n"
  },
  {
    "path": "website/lib/layout.shared.tsx",
    "content": "import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';\nimport Image from 'next/image';\n\nimport { LanguageSwitcher } from '@/components/LanguageSwitcher';\nimport { Badge } from '@/components/ui/badge';\n\nexport const gitConfig = {\n  user: 'dayflow-js',\n  repo: 'dayflow',\n  branch: 'main',\n};\n\nconst BASE = process.env.NEXT_PUBLIC_BASE_PATH || '';\nconst PRO_URL = 'https://pro.dayflow.studio';\n\nexport const sidebarTabs = [\n  {\n    title: 'Calendar',\n    url: '/docs',\n    icon: (\n      <Image\n        src={`${BASE}/logo.png`}\n        alt='DayFlow logo'\n        width={16}\n        height={16}\n        className='size-4'\n      />\n    ),\n  },\n  {\n    title: 'Calendar Pro',\n    url: PRO_URL,\n    icon: (\n      <Image\n        src={`${BASE}/pro-logo.png`}\n        alt='DayFlow Pro logo'\n        width={16}\n        height={16}\n        className='size-4'\n      />\n    ),\n  },\n  {\n    title: 'Blossom Color Picker',\n    url: 'https://blossom.dayflow.studio/',\n    icon: <span className='flex size-4 items-center justify-center'>🌸</span>,\n  },\n];\n\nfunction DiscordIcon() {\n  return (\n    <svg\n      role='img'\n      viewBox='0 0 24 24'\n      xmlns='http://www.w3.org/2000/svg'\n      fill='currentColor'\n      className='size-4'\n    >\n      <path d='M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057c.002.022.015.043.03.056a19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z' />\n    </svg>\n  );\n}\n\nfunction GithubButton() {\n  return (\n    <svg\n      fill='currentColor'\n      viewBox='3 3 18 18'\n      height='24'\n      aria-label='Project repository'\n      className='size-4'\n    >\n      <path d='M12 3C7.0275 3 3 7.12937 3 12.2276C3 16.3109 5.57625 19.7597 9.15374 20.9824C9.60374 21.0631 9.77249 20.7863 9.77249 20.5441C9.77249 20.3249 9.76125 19.5982 9.76125 18.8254C7.5 19.2522 6.915 18.2602 6.735 17.7412C6.63375 17.4759 6.19499 16.6569 5.8125 16.4378C5.4975 16.2647 5.0475 15.838 5.80124 15.8264C6.51 15.8149 7.01625 16.4954 7.18499 16.7723C7.99499 18.1679 9.28875 17.7758 9.80625 17.5335C9.885 16.9337 10.1212 16.53 10.38 16.2993C8.3775 16.0687 6.285 15.2728 6.285 11.7432C6.285 10.7397 6.63375 9.9092 7.20749 9.26326C7.1175 9.03257 6.8025 8.08674 7.2975 6.81794C7.2975 6.81794 8.05125 6.57571 9.77249 7.76377C10.4925 7.55615 11.2575 7.45234 12.0225 7.45234C12.7875 7.45234 13.5525 7.55615 14.2725 7.76377C15.9937 6.56418 16.7475 6.81794 16.7475 6.81794C17.2424 8.08674 16.9275 9.03257 16.8375 9.26326C17.4113 9.9092 17.76 10.7281 17.76 11.7432C17.76 15.2843 15.6563 16.0687 13.6537 16.2993C13.98 16.5877 14.2613 17.1414 14.2613 18.0065C14.2613 19.2407 14.25 20.2326 14.25 20.5441C14.25 20.7863 14.4188 21.0746 14.8688 20.9824C16.6554 20.364 18.2079 19.1866 19.3078 17.6162C20.4077 16.0457 20.9995 14.1611 21 12.2276C21 7.12937 16.9725 3 12 3Z' />\n    </svg>\n  );\n}\n\nconst NavTitle = (\n  <span className='flex items-center gap-2 text-lg font-semibold text-slate-900 dark:text-white'>\n    <Image\n      src={`${BASE}/logo.png`}\n      alt='DayFlow logo'\n      width={28}\n      height={28}\n      priority\n      className='h-7 w-auto'\n    />\n    DayFlow\n  </span>\n);\n\nconst ProLink = (\n  <a\n    href={PRO_URL}\n    target='_blank'\n    rel='noopener noreferrer'\n    className='inline-flex items-center gap-2 text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 dark:text-slate-400 dark:hover:text-white'\n  >\n    <Image\n      src={`${BASE}/pro-logo.png`}\n      alt='DayFlow Pro logo'\n      width={66}\n      height={66}\n      className='h-6 w-auto'\n    />\n    <Badge\n      variant='outline'\n      className='border-amber-200 bg-amber-50 px-1.5 py-0 text-[10px] font-bold tracking-[0.16em] text-amber-700 uppercase dark:border-amber-400/30 dark:bg-amber-400/10 dark:text-amber-200'\n    >\n      Pro\n    </Badge>\n  </a>\n);\n\nconst githubUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;\n\nexport function baseOptions(): BaseLayoutProps {\n  return {\n    nav: {\n      title: NavTitle,\n    },\n    links: [\n      {\n        text: '🌸 Blossom Color Picker',\n        url: 'https://blossom.dayflow.studio',\n        external: true,\n      },\n      {\n        type: 'custom',\n        children: ProLink,\n      },\n      {\n        type: 'icon',\n        text: 'Discord',\n        label: 'Discord',\n        icon: <DiscordIcon />,\n        url: 'https://discord.gg/9vdFZKJqBb',\n        external: true,\n      },\n      {\n        type: 'icon',\n        text: 'GitHub',\n        label: 'GitHub',\n        icon: <GithubButton />,\n        url: githubUrl,\n        external: true,\n      },\n    ],\n  };\n}\n\nexport function homeOptions(): BaseLayoutProps {\n  const base = baseOptions();\n  return {\n    ...base,\n    links: [\n      ...(base.links ?? []),\n      {\n        type: 'custom',\n        children: <LanguageSwitcher />,\n        secondary: true,\n      },\n    ],\n  };\n}\n"
  },
  {
    "path": "website/lib/site.ts",
    "content": "const rawBasePath = process.env.NEXT_PUBLIC_BASE_PATH || '';\n\nexport const BASE_PATH =\n  rawBasePath.length > 1 && rawBasePath.endsWith('/')\n    ? rawBasePath.slice(0, -1)\n    : rawBasePath;\n\nconst rawSiteUrl =\n  process.env.NEXT_PUBLIC_SITE_URL ||\n  (BASE_PATH\n    ? `https://dayflow-js.github.io${BASE_PATH}`\n    : 'http://localhost:3000');\n\nexport const SITE_URL = rawSiteUrl.endsWith('/')\n  ? rawSiteUrl.slice(0, -1)\n  : rawSiteUrl;\n\nexport const SITE_METADATA_BASE = new URL(SITE_URL);\n"
  },
  {
    "path": "website/lib/source.tsx",
    "content": "import { loader } from 'fumadocs-core/source';\nimport type { InferPageType } from 'fumadocs-core/source';\nimport { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';\nimport { statusBadgesPlugin } from 'fumadocs-core/source/status-badges';\nimport { docs, docsJa, docsZh } from 'fumadocs-mdx:collections/server';\nimport React from 'react';\n\nimport { Badge } from '@/components/ui/badge';\n\nconst renderProBadge = (status: string) => {\n  if (status === 'pro') {\n    return (\n      <Badge\n        variant='outline'\n        className='ml-auto rounded-full bg-[#fee699] px-1.5 py-0.5 text-[11px] leading-none font-bold whitespace-nowrap text-[#231b08]'\n      >\n        PRO\n      </Badge>\n    );\n  }\n  return null;\n};\n\nexport const source = loader({\n  baseUrl: '/docs',\n  source: docs.toFumadocsSource(),\n  plugins: [\n    lucideIconsPlugin(),\n    statusBadgesPlugin({\n      renderBadge: renderProBadge,\n    }),\n  ],\n});\n\nexport const sourceJa = loader({\n  baseUrl: '/docs-ja',\n  source: docsJa.toFumadocsSource(),\n  plugins: [\n    lucideIconsPlugin(),\n    statusBadgesPlugin({\n      renderBadge: renderProBadge,\n    }),\n  ],\n});\n\nexport const sourceZh = loader({\n  baseUrl: '/docs-zh',\n  source: docsZh.toFumadocsSource(),\n  plugins: [\n    lucideIconsPlugin(),\n    statusBadgesPlugin({\n      renderBadge: renderProBadge,\n    }),\n  ],\n});\n\nexport function getPageImage(page: InferPageType<typeof source>) {\n  const segments = [...page.slugs, 'image.png'];\n\n  return {\n    segments,\n    url: `/og/docs/${segments.join('/')}`,\n  };\n}\n\nexport async function getLLMText(page: InferPageType<typeof source>) {\n  const processed = await page.data.getText('processed');\n\n  return `# ${page.data.title}\n\n${processed}`;\n}\n"
  },
  {
    "path": "website/lib/utils.ts",
    "content": "import { clsx } from 'clsx';\nimport type { ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "website/mdx-components.tsx",
    "content": "import defaultMdxComponents from 'fumadocs-ui/mdx';\nimport type { MDXComponents } from 'mdx/types';\n\nimport { CliPreview } from '@/components/CliPreview';\nimport { DefaultColorPalette } from '@/components/ColorPalette';\nimport {\n  CreateDayflowTabs,\n  FrameworkInstall,\n  PackageTabs,\n} from '@/components/FrameworkInstall';\nimport { FrameworkTabs, Tab } from '@/components/FrameworkTabs';\n\nconst BASE = process.env.NEXT_PUBLIC_BASE_PATH || '';\n\nfunction DocImg({\n  src,\n  alt,\n  ...props\n}: {\n  src: string;\n  alt?: string;\n  [key: string]: unknown;\n}) {\n  return (\n    // eslint-disable-next-line @next/next/no-img-element\n    <img\n      {...(props as React.ImgHTMLAttributes<HTMLImageElement>)}\n      src={`${BASE}${src}`}\n      alt={alt || ''}\n      style={{ maxWidth: '100%', height: 'auto' }}\n    />\n  );\n}\n\nexport function getMDXComponents(components?: MDXComponents): MDXComponents {\n  return {\n    ...defaultMdxComponents,\n    FrameworkTabs,\n    FrameworkInstall,\n    PackageTabs,\n    CreateDayflowTabs,\n    CliPreview,\n    Tab,\n    DefaultColorPalette,\n    DocImg,\n    ...components,\n  };\n}\n"
  },
  {
    "path": "website/next.config.mjs",
    "content": "import path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { createMDX } from 'fumadocs-mdx/next';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst websiteNodeModules = path.resolve(__dirname, 'node_modules');\n\nconst withMDX = createMDX();\n\n/** @type {import('next').NextConfig} */\nconst config = {\n  turbopack: {\n    root: path.resolve(__dirname, '../'),\n    resolveAlias: {\n      'fumadocs-ui': path.resolve(websiteNodeModules, 'fumadocs-ui'),\n      'fumadocs-core': path.resolve(websiteNodeModules, 'fumadocs-core'),\n    },\n  },\n  output: 'export',\n  reactStrictMode: true,\n  basePath: process.env.BASE_PATH || '',\n  images: {\n    unoptimized: true,\n  },\n  transpilePackages: [\n    '@dayflow/core',\n    '@dayflow/react',\n    '@dayflow/plugin-drag',\n    '@dayflow/plugin-keyboard-shortcuts',\n    '@dayflow/plugin-localization',\n    '@dayflow/plugin-sidebar',\n    '@dayflow/blossom-color-picker',\n    '@dayflow/blossom-color-picker-react',\n  ],\n};\n\n// Apply webpack fix after withMDX so it isn't overridden\nconst mdxConfig = withMDX(config);\nconst originalWebpack = mdxConfig.webpack;\nmdxConfig.webpack = (webpackConfig, options) => {\n  const result = originalWebpack\n    ? originalWebpack(webpackConfig, options)\n    : webpackConfig;\n  result.resolve.modules = [\n    websiteNodeModules,\n    ...(result.resolve.modules ?? ['node_modules']),\n  ];\n  return result;\n};\n\nexport default mdxConfig;\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"name\": \"website\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"next build\",\n    \"dev\": \"next dev\",\n    \"format\": \"prettier --write \\\"**/*.{ts,tsx,mdx,css}\\\"\",\n    \"lint\": \"oxlint .\",\n    \"postinstall\": \"fumadocs-mdx\",\n    \"start\": \"next start\",\n    \"types:check\": \"fumadocs-mdx && next typegen && tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@dayflow/blossom-color-picker-react\": \"^1.2.0\",\n    \"@dayflow/core\": \"latest\",\n    \"@dayflow/plugin-drag\": \"latest\",\n    \"@dayflow/plugin-keyboard-shortcuts\": \"latest\",\n    \"@dayflow/plugin-localization\": \"latest\",\n    \"@dayflow/plugin-sidebar\": \"latest\",\n    \"@dayflow/react\": \"latest\",\n    \"@radix-ui/react-checkbox\": \"^1.3.3\",\n    \"@radix-ui/react-label\": \"^2.1.8\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.10\",\n    \"@radix-ui/react-select\": \"^2.2.6\",\n    \"@radix-ui/react-separator\": \"^1.1.8\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@radix-ui/react-tooltip\": \"^1.2.8\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"flexsearch\": \"0.8.212\",\n    \"fumadocs-core\": \"16.6.17\",\n    \"fumadocs-mdx\": \"14.2.10\",\n    \"fumadocs-ui\": \"16.6.17\",\n    \"lucide-react\": \"^0.570.0\",\n    \"next\": \"16.1.6\",\n    \"next-themes\": \"^0.4.6\",\n    \"react\": \"^19.2.4\",\n    \"react-color\": \"^2.19.3\",\n    \"react-dom\": \"^19.2.4\",\n    \"tailwind-merge\": \"^3.4.1\",\n    \"temporal-polyfill\": \"^0.3.0\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4.1.18\",\n    \"@types/mdx\": \"^2.0.13\",\n    \"@types/node\": \"^25.2.3\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-color\": \"^3.0.13\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"eslint\": \"^10.0.0\",\n    \"eslint-config-next\": \"16.1.6\",\n    \"postcss\": \"^8.5.6\",\n    \"prettier\": \"^3.5.0\",\n    \"prettier-plugin-embed\": \"^0.5.1\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"typescript\": \"^5.9.3\"\n  }\n}\n"
  },
  {
    "path": "website/postcss.config.mjs",
    "content": "const config = {\n  plugins: {\n    '@tailwindcss/postcss': {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "website/source.config.ts",
    "content": "import { metaSchema } from 'fumadocs-core/source/schema';\nimport {\n  defineConfig,\n  defineDocs,\n  defineCollections,\n  frontmatterSchema,\n} from 'fumadocs-mdx/config';\nimport { z } from 'zod';\n\nconst customPageSchema = frontmatterSchema.extend({\n  title: z.string().optional(),\n  status: z.string().optional(),\n});\n\nconst docsOptions = {\n  docs: {\n    schema: customPageSchema,\n    postprocess: {\n      includeProcessedMarkdown: true,\n    },\n  },\n  meta: {\n    schema: metaSchema.extend({\n      status: z.string().optional(),\n    }),\n  },\n};\n\nexport const docs = defineDocs({ dir: 'content/docs', ...docsOptions });\nexport const docsJa = defineDocs({ dir: 'content/docs-ja', ...docsOptions });\nexport const docsZh = defineDocs({ dir: 'content/docs-zh', ...docsOptions });\n\nexport const blog = defineCollections({\n  type: 'doc',\n  dir: 'content/blog',\n  schema: frontmatterSchema.extend({\n    date: z.string().optional(),\n  }),\n});\n\nexport default defineConfig({\n  mdxOptions: {},\n});\n"
  },
  {
    "path": "website/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"paths\": {\n      \"@/*\": [\"./*\"],\n      \"fumadocs-mdx:collections/*\": [\"./.source/*\"]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "website/utils/palette.ts",
    "content": "import { CalendarType, CalendarColors } from '@dayflow/core';\n\ninterface PaletteCalendar extends Pick<CalendarType, 'id' | 'name' | 'icon'> {\n  color: string;\n  colors: CalendarColors;\n  darkColors: CalendarColors;\n}\n\nexport const CALENDAR_SIDE_PANEL: PaletteCalendar[] = [\n  {\n    id: 'team',\n    name: 'Product Team',\n    color: '#2563eb',\n    // icon: '👩‍💻',\n    colors: {\n      eventColor: 'rgba(37, 99, 235, 0.12)',\n      eventSelectedColor: '#2563eb',\n      lineColor: '#2563eb',\n      textColor: '#1d4ed8',\n    },\n    darkColors: {\n      eventColor: 'rgba(59, 130, 246, 0.25)',\n      eventSelectedColor: '#3b82f6',\n      lineColor: '#60a5fa',\n      textColor: '#dbeafe',\n    },\n  },\n  {\n    id: 'personal',\n    name: 'Personal',\n    color: '#0ea5e9',\n    // icon: '❤️',\n    colors: {\n      eventColor: 'rgba(14, 165, 233, 0.12)',\n      eventSelectedColor: '#0ea5e9',\n      lineColor: '#0ea5e9',\n      textColor: '#0369a1',\n    },\n    darkColors: {\n      eventColor: 'rgba(14, 165, 233, 0.24)',\n      eventSelectedColor: '#38bdf8',\n      lineColor: '#7dd3fc',\n      textColor: '#e0f2fe',\n    },\n  },\n  {\n    id: 'learning',\n    name: 'Learning',\n    color: '#8b5cf6',\n    // icon: '📚',\n    colors: {\n      eventColor: 'rgba(139, 92, 246, 0.15)',\n      eventSelectedColor: '#8b5cf6',\n      lineColor: '#8b5cf6',\n      textColor: '#5b21b6',\n    },\n    darkColors: {\n      eventColor: 'rgba(167, 139, 250, 0.28)',\n      eventSelectedColor: '#a855f7',\n      lineColor: '#c084fc',\n      textColor: '#ede9fe',\n    },\n  },\n  {\n    id: 'travel',\n    name: 'Travel',\n    color: '#f97316',\n    // icon: '✈️',\n    colors: {\n      eventColor: 'rgba(249, 115, 22, 0.15)',\n      eventSelectedColor: '#f97316',\n      lineColor: '#f97316',\n      textColor: '#7c2d12',\n    },\n    darkColors: {\n      eventColor: 'rgba(251, 146, 60, 0.3)',\n      eventSelectedColor: '#fb923c',\n      lineColor: '#fdba74',\n      textColor: '#ffedd5',\n    },\n  },\n  {\n    id: 'wellness',\n    name: 'Wellness',\n    color: '#10b981',\n    // icon: '🧘',\n    colors: {\n      eventColor: 'rgba(16, 185, 129, 0.15)',\n      eventSelectedColor: '#10b981',\n      lineColor: '#10b981',\n      textColor: '#065f46',\n    },\n    darkColors: {\n      eventColor: 'rgba(52, 211, 153, 0.25)',\n      eventSelectedColor: '#34d399',\n      lineColor: '#6ee7b7',\n      textColor: '#ecfdf5',\n    },\n  },\n  {\n    id: 'marketing',\n    name: 'Marketing',\n    color: '#ec4899',\n    // icon: '📣',\n    colors: {\n      eventColor: 'rgba(236, 72, 153, 0.15)',\n      eventSelectedColor: '#ec4899',\n      lineColor: '#ec4899',\n      textColor: '#831843',\n    },\n    darkColors: {\n      eventColor: 'rgba(244, 114, 182, 0.28)',\n      eventSelectedColor: '#f472b6',\n      lineColor: '#f9a8d4',\n      textColor: '#fce7f3',\n    },\n  },\n  {\n    id: 'support',\n    name: 'Support',\n    color: '#14b8a6',\n    // icon: '🎧',\n    colors: {\n      eventColor: 'rgba(20, 184, 166, 0.15)',\n      eventSelectedColor: '#14b8a6',\n      lineColor: '#14b8a6',\n      textColor: '#115e59',\n    },\n    darkColors: {\n      eventColor: 'rgba(45, 212, 191, 0.25)',\n      eventSelectedColor: '#5eead4',\n      lineColor: '#99f6e4',\n      textColor: '#ccfbf1',\n    },\n  },\n];\n\nexport const getWebsiteCalendars = (): CalendarType[] =>\n  CALENDAR_SIDE_PANEL.map(item => ({\n    id: item.id,\n    name: item.name,\n    icon: item.icon,\n    colors: {\n      eventColor: `${item.color}30`,\n      eventSelectedColor: `${item.color}`,\n      lineColor: item.color,\n      textColor: item.color,\n    },\n    isVisible: true,\n  }));\n"
  },
  {
    "path": "website/utils/sampleData.ts",
    "content": "import { Event } from '@dayflow/core';\nimport { Temporal } from 'temporal-polyfill';\n\nconst calendarIds = [\n  'team',\n  'personal',\n  'learning',\n  'travel',\n  'wellness',\n  'marketing',\n  'support',\n];\n\nconst titles = [\n  'Product Sync',\n  'Design Review',\n  'Customer Call',\n  'Weekly Planning',\n  'Deep Work',\n  'Code Review',\n  'Brainstorm',\n  'Usability Test',\n  'Team Retro',\n  'Partner Demo',\n  'Lunch & Learn',\n  'Yoga Break',\n  'Travel Block',\n  'Hiring Interview',\n  'Content Planning',\n];\n\nconst locations = [\n  'Conference Room A',\n  'Meeting Room 302',\n  'Zoom Meeting',\n  'Main Office, 4th Floor',\n  'Starbucks Coffee',\n  'Community Center',\n  'Innovation Hub',\n  'Building 5, Lab 2',\n];\n\nconst eventDetails: Record<string, { description: string; location?: string }> =\n  {\n    'Product Sync': {\n      description: 'Sync up on the latest product roadmap and milestones.',\n      location: 'Room 101',\n    },\n    'Design Review': {\n      description:\n        'Review the new UI/UX designs for the upcoming mobile app release.',\n      location: 'Design Studio',\n    },\n    'Customer Call': {\n      description:\n        'Discussion with key clients regarding feature requests and feedback.',\n      location: 'Virtual',\n    },\n    'Weekly Planning': {\n      description: 'Plan tasks and priorities for the upcoming week.',\n      location: 'Main Hall',\n    },\n    'Deep Work': {\n      description: 'Focus time for intense development and problem solving.',\n      location: 'Quiet Zone',\n    },\n    'Code Review': {\n      description: 'Review pull requests and ensure code quality standards.',\n      location: 'Dev Corner',\n    },\n    Brainstorm: {\n      description: 'Ideation session for the next big feature.',\n      location: 'Whiteboard Room',\n    },\n    'Usability Test': {\n      description: 'Observe users interacting with the latest prototype.',\n      location: 'User Lab',\n    },\n    'Team Retro': {\n      description: 'Reflect on the past sprint and discuss improvements.',\n      location: 'Common Area',\n    },\n    'Partner Demo': {\n      description: 'Demonstrate our latest capabilities to potential partners.',\n      location: 'Executive Suite',\n    },\n    'Lunch & Learn': {\n      description: 'Educational session over lunch about new technologies.',\n      location: 'Cafeteria',\n    },\n    'Yoga Break': {\n      description: 'Stretch and relax with a quick yoga session.',\n      location: 'Wellness Room',\n    },\n    'Travel Block': {\n      description: 'Time allocated for travel and logistics.',\n      location: 'Airport Terminal',\n    },\n    'Hiring Interview': {\n      description: 'Interviewing candidates for the Senior Engineer position.',\n      location: 'HR Office',\n    },\n    'Content Planning': {\n      description: 'Plan the editorial calendar and upcoming blog posts.',\n      location: 'Marketing Hub',\n    },\n  };\n\n// Simple deterministic random number generator\nconst createRandom = (seed: number) => {\n  let s = seed;\n  return () => {\n    const x = Math.sin(s++) * 10000;\n    return x - Math.floor(x);\n  };\n};\n\nconst createRandomInt = (random: () => number) => (min: number, max: number) =>\n  Math.floor(random() * (max - min + 1)) + min;\n\nconst DEFAULT_TIME_ZONE = Temporal.Now.timeZoneId();\n\nconst createTimedEvent = (\n  baseDate: Temporal.PlainDate,\n  index: number,\n  randomInt: (min: number, max: number) => number\n): Event => {\n  const title = titles[index % titles.length];\n  const details = eventDetails[title] || {\n    description: 'General event details.',\n    location: locations[index % locations.length],\n  };\n\n  // Keep sample events concentrated in local working hours for easier testing.\n  const startHour = randomInt(8, 15);\n  const maxDuration = Math.max(1, 17 - startHour);\n  const duration = Math.max(1, randomInt(1, Math.min(3, maxDuration)));\n\n  const startPlain = baseDate.toPlainDateTime({\n    hour: startHour,\n    minute: randomInt(0, 1) ? 30 : 0,\n  });\n\n  const start = Temporal.ZonedDateTime.from({\n    timeZone: DEFAULT_TIME_ZONE,\n    year: startPlain.year,\n    month: startPlain.month,\n    day: startPlain.day,\n    hour: startPlain.hour,\n    minute: startPlain.minute,\n  });\n\n  const end = start.add({ hours: duration });\n\n  return {\n    id: `event-${index}`,\n    title: title,\n    description: details.description,\n    start,\n    end,\n    calendarId: calendarIds[index % calendarIds.length],\n    meta: {\n      location: details.location || locations[index % locations.length],\n    },\n  };\n};\n\nconst createAllDayEvent = (\n  start: Temporal.PlainDate,\n  span: number,\n  index: number,\n  calendarId: string,\n  title: string\n): Event => {\n  const details = eventDetails[title] || {\n    description: 'All day event details.',\n    location: 'Various',\n  };\n  return {\n    id: `all-day-${index}`,\n    title,\n    description: details.description,\n    start,\n    end: start.add({ days: span }),\n    allDay: true,\n    calendarId,\n    icon: true,\n    meta: {\n      location: details.location || 'Multiple Locations',\n    },\n  };\n};\n\nconst baseAllDayDefinitions: Array<{\n  offset: number;\n  span: number;\n  calendarId: string;\n  title: string;\n}> = [\n  { offset: -6, span: 2, calendarId: 'team', title: 'Sprint Offsite' },\n  { offset: -2, span: 0, calendarId: 'personal', title: 'Family Day' },\n  { offset: 3, span: 1, calendarId: 'travel', title: 'Client Visit' },\n  { offset: 7, span: 2, calendarId: 'marketing', title: 'Campaign Launch' },\n  { offset: 12, span: 0, calendarId: 'learning', title: 'Conference' },\n  { offset: 16, span: 3, calendarId: 'wellness', title: 'Wellness Retreat' },\n  { offset: 20, span: 1, calendarId: 'support', title: 'Support Rotation' },\n];\n\n/** Multi-calendar sample events that demonstrate calendarIds support. */\nexport const generateMultiCalendarEvents = (): Event[] => {\n  const today = Temporal.Now.plainDateISO();\n  const tz = Temporal.Now.timeZoneId();\n\n  const makeZoned = (\n    date: Temporal.PlainDate,\n    hour: number,\n    minute = 0\n  ): Temporal.ZonedDateTime =>\n    Temporal.ZonedDateTime.from({\n      timeZone: tz,\n      year: date.year,\n      month: date.month,\n      day: date.day,\n      hour,\n      minute,\n    });\n\n  return [\n    // Two-calendar timed event — today\n    {\n      id: 'multi-cal-1',\n      title: 'Family Dance Workshop',\n      description: 'Shared across the whole family calendar.',\n      start: makeZoned(today, 10),\n      end: makeZoned(today, 11, 30),\n      calendarIds: ['personal', 'wellness'],\n      meta: { location: 'Community Center' },\n    },\n    // Three-calendar timed event — tomorrow\n    {\n      id: 'multi-cal-2',\n      title: 'All-Hands Sprint Planning',\n      description: 'Cross-team planning session.',\n      start: makeZoned(today.add({ days: 1 }), 14),\n      end: makeZoned(today.add({ days: 1 }), 16),\n      calendarIds: ['team', 'marketing', 'support'],\n      meta: { location: 'Main Hall' },\n    },\n    // Four-calendar all-day event — spans today + 2 days\n    {\n      id: 'multi-cal-3',\n      title: 'Company Off-site',\n      description: 'Belongs to every team calendar.',\n      start: today.add({ days: 3 }),\n      end: today.add({ days: 5 }),\n      allDay: true,\n      icon: true,\n      calendarIds: ['team', 'personal', 'travel', 'learning'],\n    },\n    // Two-calendar timed event — yesterday (tests backward visibility)\n    {\n      id: 'multi-cal-4',\n      title: 'Retrospective + Wellness Check',\n      description: 'Weekly retro combined with wellness review.',\n      start: makeZoned(today.subtract({ days: 1 }), 15),\n      end: makeZoned(today.subtract({ days: 1 }), 16),\n      calendarIds: ['team', 'wellness'],\n      meta: { location: 'Zoom' },\n    },\n  ];\n};\n\nexport const generateSampleEvents = (): Event[] => {\n  const today = Temporal.Now.plainDateISO();\n  const windowStart = today.subtract({ days: 24 });\n  const events: Event[] = [];\n\n  // Initialize deterministic random generator\n  const random = createRandom(12345);\n  const randomInt = createRandomInt(random);\n\n  for (let offset = 0; offset < 56; offset += 1) {\n    const date = windowStart.add({ days: offset });\n    const dayEvents = randomInt(2, 4);\n    for (let i = 0; i < dayEvents; i += 1) {\n      events.push(createTimedEvent(date, offset * 10 + i, randomInt));\n    }\n  }\n  baseAllDayDefinitions.forEach((definition, index) => {\n    const start = today.add({ days: definition.offset });\n    const span = Math.max(0, definition.span);\n    events.push(\n      createAllDayEvent(\n        start,\n        span,\n        index,\n        definition.calendarId,\n        definition.title\n      )\n    );\n  });\n\n  // Append multi-calendar demo events\n  events.push(...generateMultiCalendarEvents());\n\n  // Annual events for Year View demonstration\n  const currentYear = today.year;\n  const annualEvents = [\n    // Jan: New Year & Kickoff\n    {\n      month: 1,\n      day: 1,\n      span: 3,\n      calendarId: 'personal',\n      title: 'New Year Holiday',\n    },\n    {\n      month: 1,\n      day: 15,\n      span: 5,\n      calendarId: 'team',\n      title: 'Annual Kickoff Week',\n    },\n    {\n      month: 1,\n      day: 25,\n      span: 3,\n      calendarId: 'learning',\n      title: 'Goal Setting Workshop',\n    },\n\n    // Feb-Mar: Work Focus\n    {\n      month: 2,\n      day: 5,\n      span: 4,\n      calendarId: 'team',\n      title: 'Q1 Strategy Offsite',\n    },\n    {\n      month: 2,\n      day: 14,\n      span: 3,\n      calendarId: 'personal',\n      title: \"Valentine's Trip\",\n    },\n    {\n      month: 2,\n      day: 26,\n      span: 4,\n      calendarId: 'learning',\n      title: 'Tech Conference',\n    },\n    {\n      month: 3,\n      day: 10,\n      span: 4,\n      calendarId: 'team',\n      title: 'Design Sprint Week',\n    },\n    {\n      month: 3,\n      day: 24,\n      span: 4,\n      calendarId: 'marketing',\n      title: 'Product Launch Week',\n    },\n\n    // Apr-May: Conferences & Holidays\n    {\n      month: 4,\n      day: 12,\n      span: 5,\n      calendarId: 'travel',\n      title: 'Spring Team Building',\n    },\n    {\n      month: 4,\n      day: 25,\n      span: 3,\n      calendarId: 'personal',\n      title: 'Anniversary Trip',\n    },\n    {\n      month: 5,\n      day: 1,\n      span: 3,\n      calendarId: 'personal',\n      title: 'Labour Day Holiday',\n    },\n    {\n      month: 5,\n      day: 15,\n      span: 4,\n      calendarId: 'learning',\n      title: 'Developer Summit',\n    },\n    {\n      month: 5,\n      day: 28,\n      span: 3,\n      calendarId: 'marketing',\n      title: 'Brand Workshop',\n    },\n\n    // Jun-Jul: Travel & Vacation\n    {\n      month: 6,\n      day: 10,\n      span: 4,\n      calendarId: 'support',\n      title: 'Quarterly Review',\n    },\n    {\n      month: 6,\n      day: 15,\n      span: 14,\n      calendarId: 'travel',\n      title: 'Summer Vacation (Europe)',\n    },\n    {\n      month: 7,\n      day: 8,\n      span: 4,\n      calendarId: 'team',\n      title: 'Mid-Year Review Week',\n    },\n    {\n      month: 7,\n      day: 20,\n      span: 5,\n      calendarId: 'wellness',\n      title: 'Hiking Trip',\n    },\n\n    // Aug-Sep: Projects & Learning\n    { month: 8, day: 12, span: 6, calendarId: 'team', title: 'Hackathon Week' },\n    {\n      month: 8,\n      day: 25,\n      span: 3,\n      calendarId: 'wellness',\n      title: 'Wellness Retreat',\n    },\n    {\n      month: 9,\n      day: 5,\n      span: 3,\n      calendarId: 'learning',\n      title: 'Leadership Training',\n    },\n    {\n      month: 9,\n      day: 18,\n      span: 4,\n      calendarId: 'travel',\n      title: 'Client Roadshow',\n    },\n\n    // Oct-Nov: Q4 Push\n    {\n      month: 10,\n      day: 10,\n      span: 5,\n      calendarId: 'team',\n      title: 'Q4 Planning Week',\n    },\n    {\n      month: 10,\n      day: 31,\n      span: 3,\n      calendarId: 'personal',\n      title: 'Halloween Weekend',\n    },\n    {\n      month: 11,\n      day: 15,\n      span: 5,\n      calendarId: 'marketing',\n      title: 'Black Friday Prep',\n    },\n    {\n      month: 11,\n      day: 24,\n      span: 3,\n      calendarId: 'personal',\n      title: 'Thanksgiving Holiday',\n    },\n\n    // Dec: Holidays\n    {\n      month: 12,\n      day: 10,\n      span: 3,\n      calendarId: 'team',\n      title: 'Year End Party Trip',\n    },\n    {\n      month: 12,\n      day: 24,\n      span: 5,\n      calendarId: 'personal',\n      title: 'Christmas Holiday',\n    },\n    {\n      month: 12,\n      day: 29,\n      span: 4,\n      calendarId: 'travel',\n      title: 'New Year Ski Trip',\n    },\n  ];\n\n  annualEvents.forEach((def, index) => {\n    try {\n      const start = Temporal.PlainDate.from({\n        year: currentYear,\n        month: def.month,\n        day: def.day,\n      });\n      events.push(\n        createAllDayEvent(\n          start,\n          def.span,\n          2000 + index, // Use a high base index to avoid collisions\n          def.calendarId,\n          def.title\n        )\n      );\n    } catch {\n      // Ignore invalid dates (e.g. leap years edge cases in simple config)\n    }\n  });\n\n  return events;\n};\n\nexport const generateMinimalSampleEvents = (): Event[] => {\n  const today = Temporal.Now.plainDateISO();\n  const windowStart = today.subtract({ days: 3 });\n  const events: Event[] = [];\n\n  const random = createRandom(54321);\n  const randomInt = createRandomInt(random);\n\n  for (let offset = 0; offset < 7; offset += 1) {\n    const date = windowStart.add({ days: offset });\n    const dayEvents = randomInt(1, 2);\n    for (let i = 0; i < dayEvents; i += 1) {\n      events.push(createTimedEvent(date, offset * 10 + i, randomInt));\n    }\n  }\n\n  // Add just a couple of all-day events\n  events.push(\n    createAllDayEvent(\n      today.subtract({ days: 1 }),\n      2,\n      999,\n      'team',\n      'Minimal Team Event'\n    )\n  );\n\n  return events;\n};\n"
  }
]