diff --git a/README.md b/README.md
index 2584819..9c401ed 100644
--- a/README.md
+++ b/README.md
@@ -8,17 +8,22 @@ YMB 公开站点 React + Vite 项目,参考 YouTube 的内容发现信息架
```text
src/
-├── app/ # 路由与全局 Store
-├── layouts/ # AppShell、顶部栏、侧栏、移动端导航
-├── pages/ # Home、Shorts、Subscriptions、Library、History、Search、Watch
-├── components/ # MediaCard、SectionHeading 等复用组件
-├── features/ # 后续业务域扩展入口
-├── services/ # 目录检索与接口边界
-├── data/ # 演示目录数据
-├── i18n/ # ID / EN / zh-CN 语言包
-└── lib/seo.js # 页面 SEO 元数据入口
+├── app/ # 应用装配:路由(懒加载)、Locale/UI Provider
+├── shared/ # 与业务无关的通用能力
+│ ├── api/ # fetch 客户端、拦截器、错误类型、领域无关接口
+│ ├── hooks/ # useRequest / useMutation / useToast / useEngagement
+│ ├── components/ # AsyncBoundary、VideoGrid、MediaCard、骨架屏
+│ ├── lib/ # format、media、pagination、seo、watchProgress
+│ ├── data/ # 接口不可用时的演示数据
+│ └── styles/ # base / enterprise / 组件覆盖样式
+├── entities/ # 领域模型与接口(drama、category)
+├── features/ # 业务域:home / shorts / watch / search / history / listing / about
+├── layouts/ # AppShell + TopBar / SearchBox / Sidebar / BottomNav
+└── i18n/ # ID / EN / zh-CN 语言包
```
+依赖方向:`features → entities → shared`,`layouts → shared`;`shared` 不允许反向依赖业务域。
+
## YouTube 对照的信息架构
- 顶部 AppBar:品牌、全局搜索、语言、通知、用户入口。
@@ -43,4 +48,4 @@ pnpm build
- 多语言:印尼语 `?lang=id`、English `?lang=en`、简体中文 `?lang=zh`。
- SEO:动态 `lang/title/description`、canonical、hreflang、Open Graph、Twitter Card、JSON-LD、robots.txt、sitemap.xml。
-部署前请将 `ymb.example.com` 替换为正式域名,并同步修改 `index.html`、`app.js`、`robots.txt` 与 `sitemap.xml`。
+部署前请将 `ymb.example.com` 替换为正式域名,并同步修改 `index.html`、`public/robots.txt` 与 `public/sitemap.xml`。
diff --git a/app.js b/app.js
deleted file mode 100644
index 75fb844..0000000
--- a/app.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// Author: fruit.com.cn
-(function(){'use strict';const d=window.YMB_DATA;const params=new URLSearchParams(location.search);const s={lang:d.locales[params.get('lang')]?params.get('lang'):'id',genre:'',query:params.get('q')||''};const $=(q)=>document.querySelector(q);const t=()=>d.locales[s.lang];const notify=(m)=>{const toast=$('#toast');toast.textContent=m;toast.classList.add('show');setTimeout(()=>toast.classList.remove('show'),1600)};const setMeta=()=>{const locale=t();document.documentElement.lang=locale.lang;document.title=locale.title;$('#page-title').textContent=locale.title;$('#meta-description').content=locale.description;$('#canonical-link').href=`https://ymb.example.com/?lang=${s.lang}`;$('#og-title').content=locale.title;$('#og-description').content=locale.description;$('#twitter-title').content=locale.title;$('#twitter-description').content=locale.description;$('#structured-data').textContent=JSON.stringify({'@context':'https://schema.org','@type':'WebSite',name:'YMB',inLanguage:locale.lang,url:`https://ymb.example.com/?lang=${s.lang}`,description:locale.description,potentialAction:{'@type':'SearchAction',target:`https://ymb.example.com/?lang=${s.lang}&q={search_term_string}`,'query-input':'required name=search_term_string'}});$('#global-search').placeholder=locale.search;$('#language-select').value=s.lang;document.querySelectorAll('[data-i18n]').forEach((node)=>{const key=node.dataset.i18n;if(locale[key])node.textContent=locale[key]})};const updateUrl=()=>{const query=new URLSearchParams({lang:s.lang});if(s.query)query.set('q',s.query);history.replaceState(null,'',`?${query.toString()}`)};const render=()=>{const locale=t();const genres=locale.genres;const activeGenre=s.genre?d.locales.id.genres[genres.indexOf(s.genre)]:'';$('#genre-nav').innerHTML=genres.slice(1).map((g)=>`${g}`).join('');$('#filter-chips').innerHTML=genres.map((g)=>``).join('');const rows=d.videos.filter((v)=>(!activeGenre||v[2]===activeGenre)&&(!s.query||v.join(' ').toLowerCase().includes(s.query)));$('#video-grid').innerHTML=rows.map((v,i)=>`
${v[4]} `).join('')||`
${locale.empty}
`;$('#shorts-row').innerHTML=d.shorts.map((v)=>`
${v[0]}`).join('');$('#channel-grid').innerHTML=d.channels.map((v)=>`${v[2]}`).join('');setMeta()};document.addEventListener('click',(e)=>{const genre=e.target.closest('[data-genre]');if(genre){e.preventDefault();s.genre=genre.dataset.genre;render()}const play=e.target.closest('[data-play]');if(play)notify(`${t().playing}${play.dataset.play}`);const follow=e.target.closest('[data-follow]');if(follow){const isFollowing=follow.textContent===t().follow;follow.textContent=isFollowing?t().following:t().follow;notify(isFollowing?t().added:t().removed)}if(e.target.closest('#menu-toggle'))$('#sidebar').classList.toggle('open')});$('#search-form').addEventListener('submit',(e)=>{e.preventDefault();s.query=$('#global-search').value.trim().toLowerCase();updateUrl();render()});$('#language-select').addEventListener('change',(e)=>{s.lang=e.target.value;s.genre='';updateUrl();render()});$('#global-search').value=s.query;render()})();
diff --git a/data.js b/data.js
deleted file mode 100644
index 787280d..0000000
--- a/data.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// Author: fruit.com.cn
-window.YMB_DATA={
- locales:{
- id:{lang:'id',title:'YMB · Cerita pendek, emosi besar',description:'YMB — cerita pendek, emosi besar. Temukan drama pendek terbaik dan episode baru setiap hari.',search:'Cari drama, aktor, atau genre',original:'YMB ORIGINAL',heroTitle:'Cerita pendek,',heroAccent:'emosi besar.',heroDescription:'Temukan drama yang bikin kamu terus menonton. Episode baru setiap hari.',watchNow:'▶ Mulai menonton',save:'♡ Simpan',recommendation:'REKOMENDASI UNTUKMU',weeklyPicks:'Drama pilihan minggu ini',seeAll:'Lihat semua →',shortsTitle:'Cerita dalam satu tarikan napas',explore:'Jelajahi →',channels:'CHANNEL UNTUKMU',followStories:'Ikuti cerita favoritmu',manage:'Kelola →',follow:'Ikuti',following:'Mengikuti',added:'Channel ditambahkan',removed:'Channel dihapus',playing:'Memutar: ',empty:'Belum ada drama yang cocok.',genres:['Semua','Romansa','Balas dendam','Keluarga','Rahasia','Kisah kedua']},
- en:{lang:'en',title:'YMB · Short stories, big emotions',description:'YMB — short stories, big emotions. Discover the best short dramas and new episodes every day.',search:'Search dramas, actors, or genres',original:'YMB ORIGINAL',heroTitle:'Short stories,',heroAccent:'big emotions.',heroDescription:'Find dramas that keep you watching. New episodes every day.',watchNow:'▶ Watch now',save:'♡ Save',recommendation:'RECOMMENDED FOR YOU',weeklyPicks:'Drama picks this week',seeAll:'See all →',shortsTitle:'Stories in one breath',explore:'Explore →',channels:'CHANNELS FOR YOU',followStories:'Follow your favorite stories',manage:'Manage →',follow:'Follow',following:'Following',added:'Channel followed',removed:'Channel unfollowed',playing:'Playing: ',empty:'No dramas match your search.',genres:['All','Romance','Revenge','Family','Secrets','Second chance']},
- zh:{lang:'zh-CN',title:'YMB · 短故事,大情绪',description:'YMB——短故事,大情绪。发现精彩短剧,每天更新新剧集。',search:'搜索短剧、演员或题材',original:'YMB 原创',heroTitle:'短故事,',heroAccent:'大情绪。',heroDescription:'发现让你停不下来的短剧,每天都有新集数。',watchNow:'▶ 立即观看',save:'♡ 收藏',recommendation:'为你推荐',weeklyPicks:'本周精选短剧',seeAll:'查看全部 →',shortsTitle:'一口气看完的故事',explore:'去探索 →',channels:'为你推荐的频道',followStories:'关注你喜欢的故事',manage:'管理 →',follow:'关注',following:'已关注',added:'已关注频道',removed:'已取消关注',playing:'正在播放:',empty:'没有找到匹配的短剧。',genres:['全部','爱情','复仇','家庭','秘密','重生']}
- },
- videos:[['Pernikahan yang Tak Terduga','YMB Originals','Romansa','wedding.jpg','12:48'],['Rahasia di Balik Senyuman','Cerita Senja','Rahasia','secret.jpg','09:32'],['Pewaris yang Kembali','Drama Pilihan','Keluarga','heir.jpg','15:20'],['Kesempatan Kedua untuk Kita','YMB Originals','Kisah kedua','second-chance.jpg','11:04'],['Cinta Setelah Balas Dendam','Cerita Senja','Balas dendam','revenge.jpg','13:40'],['Sahabat yang Selalu Ada','Drama Pilihan','Keluarga','friends.jpg','08:16'],['Kota Kecil, Rahasia Besar','YMB Originals','Rahasia','hero.jpg','10:12'],['Satu Janji Terakhir','Drama Pilihan','Romansa','romance.jpg','07:55']],
- shorts:[['Dia kembali setelah 5 tahun','second-chance.jpg'],['Ketika rahasia terbongkar','secret.jpg'],['Satu pesan yang mengubah semuanya','romance.jpg'],['Jangan percaya senyumnya','revenge.jpg'],['Pewaris tanpa nama','heir.jpg'],['Hari pernikahan itu','wedding.jpg']],
- channels:[['YMB Originals','2,4 jt pengikut','Y','#ca4d43'],['Cerita Senja','986 rb pengikut','C','#5c4aa1'],['Drama Pilihan','742 rb pengikut','D','#2e7d71'],['Kisah Hari Ini','428 rb pengikut','K','#a06a39']]
-};
diff --git a/docs/plans/2026-11-12-enterprise-refactor.md b/docs/plans/2026-11-12-enterprise-refactor.md
new file mode 100644
index 0000000..286ca83
--- /dev/null
+++ b/docs/plans/2026-11-12-enterprise-refactor.md
@@ -0,0 +1,211 @@
+# YMB Site 企业级重构方案
+
+> 目标:在不改变现有视觉与交互的前提下,把「演示工程」重构成可按业务域扩展的前端工程。
+> 约束:纯重构,功能与文案不变;每个阶段结束必须 `pnpm build` + `pnpm lint` 通过;剧集 ID 保持字符串(19 位长整型)。
+
+## 一、现状诊断
+
+### 1. 目录没有业务边界
+
+当前 `src/` 按技术类型平铺:`pages/`、`components/`、`services/`、`hooks/`、`lib/`、`data/`。
+- 首页、Shorts、播放页、搜索页共享同一批 `services` 与 `components`,无法知道改一个页面会影响谁。
+- 页面与接口调用直接耦合:`HomePage`、`SearchPage`、`ListingPage` 各自写一遍 `normalizePage(...).list.map(normalizeDrama)`。
+- `data/catalog.js` 是演示兜底数据,和真实 API 层混在同一层级。
+
+### 2. 请求层只有 24 行,没有封装
+
+`src/services/httpClient.js` 现状:
+- header、超时、JSON 解析、业务码判断全部内联在一个 `request()` 里;
+- 错误统一 `throw new Error(payload.message)`,丢失 HTTP 状态、业务码、请求路径,超时/断网/业务失败无法区分;
+- 没有拦截器链,后续加 token、埋点、错误上报只能继续往里堆 if;
+- `appApi.js` 用字符串拼 query(`?pageIndex=1&pageSize=20`),16 个端点挤在一个对象里。
+
+### 3. 数据与状态层薄弱
+
+- `useApi` 只有 `active` 标志,没有 `AbortController`、没有 `refetch`、没有重试;错误只有一个 `message` 字符串。
+- 点赞/收藏在 `ShortsFeedPage` 和 `WatchPage` 各写了一遍(含 try/catch + console.error),没有 mutation 抽象。
+- `store.jsx` 把 locale / 搜索词 / 侧栏状态塞进一个 Context,搜索框每次输入都会让整棵树重渲染。
+- `normalizers.js` 同时承担媒体 URL 解析、分页解包、播放进度模型,职责混杂。
+
+### 4. 组件没有抽离
+
+| 现状 | 问题 |
+| --- | --- |
+| `layouts/AppShell.jsx` 134 行 | 顶栏 + 搜索 + 创建菜单 + 侧栏 + 底部导航 + 上传弹窗状态全在一个组件 |
+| `pages/ShortsFeedPage.jsx` 237 行 | `ShortSlide`、点赞/收藏、分页游标、滚动监听、键盘事件混在一起 |
+| `pages/WatchPage.jsx` 235 行 | 播放源、滑动手势、剧集列表、统计、评论、分享混在一起 |
+| 各页面 | loading / error / empty 三态重复手写 |
+| `components/Skeletons.jsx` 104 行 | 6 个骨架屏塞在一个文件 |
+
+### 5. 样式与路由
+
+- `src/styles.css`(旧版全站样式,含 `@font-face`)、`enterprise.css`、`youtube-overrides.css` 三层叠加,没有 tokens 与层级约定。
+- `src/app/App.jsx` 单行全量 import,无懒加载;构建主 chunk 907KB,Vite 已提示超限。
+
+## 二、目标目录结构
+
+```text
+src/
+├── app/ # 应用装配层
+│ ├── App.jsx # 只做 Provider + Router 组装
+│ ├── router.jsx # 路由表(React.lazy + Suspense)
+│ └── providers/
+│ ├── AppProviders.jsx
+│ ├── LocaleProvider.jsx # 语言 + SEO
+│ └── UiProvider.jsx # 侧栏 / 搜索词 / toast
+├── shared/ # 与业务无关的通用能力
+│ ├── api/
+│ │ ├── config.js # baseURL、超时、平台常量
+│ │ ├── errors.js # ApiError / TimeoutError / NetworkError
+│ │ ├── client.js # fetch 核心 + 拦截器管线
+│ │ ├── interceptors.js # 设备头 / JSON / 业务码 / 错误归一
+│ │ └── index.js
+│ ├── hooks/
+│ │ ├── useRequest.js # 查询:取消、重试、refetch
+│ │ ├── useMutation.js # 写操作:pending、错误、乐观更新
+│ │ └── useToast.js
+│ ├── components/
+│ │ ├── AsyncBoundary.jsx # 统一 loading / error / empty / retry
+│ │ ├── VideoGrid.jsx
+│ │ ├── EmptyState.jsx
+│ │ ├── ErrorState.jsx
+│ │ ├── Toast.jsx
+│ │ └── skeletons/index.jsx
+│ ├── lib/ # format / seo / media
+│ └── styles/ # tokens / base / layout / 组件样式
+├── entities/ # 领域模型与数据转换
+│ └── drama/
+│ ├── model.js # normalizeDrama / normalizeProgress
+│ └── api.js # 剧集、分集、播放、统计
+├── features/ # 业务域(页面 + 域内组件 + 域内 hook)
+│ ├── home/ HomePage.jsx / CategoryBar.jsx / useHomeFeed.js
+│ ├── shorts/ ShortsFeedPage.jsx / ShortSlide.jsx / useShortsFeed.js
+│ ├── watch/ WatchPage.jsx / EpisodeRail.jsx / useEpisodePlayback.js
+│ ├── search/ SearchPage.jsx / useSearchResults.js
+│ ├── history/ HistoryPage.jsx
+│ ├── listing/ ListingPage.jsx
+│ └── about/ AboutPage.jsx + i18n/about.js
+├── layouts/
+│ ├── AppShell.jsx # 只做骨架拼装
+│ └── components/
+│ ├── TopBar.jsx
+│ ├── SearchBox.jsx
+│ ├── CreateMenu.jsx
+│ ├── Sidebar.jsx
+│ └── BottomNav.jsx
+├── i18n/
+│ ├── index.js # 聚合 + getLocale
+│ └── locales/{id,en,zh}.js
+└── main.jsx
+```
+
+依赖方向固定为:`features → entities → shared`,`layouts → shared`;禁止 `shared` 反向引用 `features`,禁止页面直接调用 `fetch`。
+
+## 三、请求层设计(重点)
+
+### 1. 统一错误类型
+
+```js
+// shared/api/errors.js
+export class ApiError extends Error {
+ constructor(message, { status = 0, code = null, path = '', cause } = {}) { ... }
+}
+export class TimeoutError extends ApiError {}
+export class NetworkError extends ApiError {}
+```
+
+页面只判断 `error instanceof TimeoutError` / `error.status`,不再解析字符串。
+
+### 2. 拦截器管线
+
+```js
+// shared/api/client.js
+const client = createHttpClient({ baseURL, timeout: 12000 });
+
+client.useRequest((config) => ({
+ ...config,
+ headers: { 'X-Device-Id': getDeviceId(), 'X-Client-Platform': 'web', ...config.headers },
+}));
+
+client.useResponse((response) => response); // 预留:解包、埋点
+client.useError((error) => { /* 归一化 + 401 处理入口 */ throw error; });
+
+client.get('/app/home/feed');
+client.post('/app/likes/toggle', { targetType, targetId });
+```
+
+- 请求拦截器:设备 ID、平台头、Content-Type、语言头(预留)。
+- 响应拦截器:`code === 0/200` 时解包 `data`;否则抛 `ApiError(code, message)`。
+- 错误拦截器:`AbortError` → 取消(静默);超时 → `TimeoutError`;`fetch` 异常 → `NetworkError`;HTTP 非 2xx → `ApiError(status)`。
+- 所有查询共用 `AbortSignal`,组件卸载即取消,避免切页后 setState。
+
+### 3. 端点按域拆分
+
+```text
+entities/drama/api.js getDrama / getEpisodes / getPlayback / getStats
+features/home/api.js getHomeFeed / getCategories / getCategoryDramas
+features/search/api.js search
+features/history/api.js getRecent / clearHistory / reportProgress
+features/shared? likes / favorites / comments → shared/api/engagement.js
+```
+
+`src/services/appApi.js` 在迁移期保留为兼容门面,阶段 6 删除。
+
+## 四、组件抽离清单
+
+| 抽离项 | 来源 | 去向 |
+| --- | --- | --- |
+| `TopBar` / `SearchBox` / `CreateMenu` / `Sidebar` / `BottomNav` | `AppShell.jsx` | `layouts/components/` |
+| `ShortSlide` | `ShortsFeedPage.jsx` 内的私有组件 | `features/shorts/components/ShortSlide.jsx` |
+| `EpisodeRail` | `WatchPage.jsx` 右侧剧集列表 | `features/watch/components/EpisodeRail.jsx` |
+| `AsyncBoundary` | 4 个页面重复的三态 | `shared/components/AsyncBoundary.jsx` |
+| `VideoGrid` | `HomePage` / `ListingPage` / `SearchPage` 重复网格 | `shared/components/VideoGrid.jsx` |
+| `EmptyState` / `ErrorState` | 各页面 `api-note` / `search-empty` | `shared/components/` |
+| `Toast` | Shorts / Watch 各自实现 | `shared/components/Toast.jsx` + `useToast` |
+| 骨架屏 | `Skeletons.jsx` | `shared/components/skeletons/`(按页面一个文件 + index 聚合) |
+| 点赞/收藏交互 | Shorts 与 Watch 重复 | `shared/hooks/useEngagement.js` |
+
+`AsyncBoundary` 使用方式:
+
+```jsx
+}
+ emptyText={locale.empty}
+ onRetry={request.refetch}
+>
+
+
+```
+
+## 五、分阶段实施
+
+| 阶段 | 内容 | 验收 |
+| --- | --- | --- |
+| P0 | 记录基线:`pnpm build` / `pnpm lint` 结果,页面清单截图 | 基线存档 |
+| P1 | `shared/api`:错误类型、client、拦截器、config;`appApi` 改为基于 client | build + lint,接口行为不变 |
+| P2 | `entities/drama`:模型归一化、分页、媒体 URL 收敛;页面改用 | 首页 / 列表 / 搜索数据一致 |
+| P3 | `useRequest` / `useMutation` / `useToast` / `useEngagement`;替换 `useApi` 与重复逻辑 | 点赞、收藏、评论、上传弹窗回归 |
+| P4 | 拆 `AppShell` 为 TopBar/SearchBox/CreateMenu/Sidebar/BottomNav;路由懒加载 | 侧栏收起、移动端抽屉、搜索跳转回归 |
+| P5 | 页面迁入 `features/`,抽 ShortSlide、EpisodeRail、VideoGrid、AsyncBoundary、骨架屏 | 首页 / Shorts / 播放 / 搜索 / 历史 / 关于全量回归 |
+| P6 | 样式分层(tokens/base/legacy 分离),删除旧兼容层与 `services/appApi.js` | 视觉 diff 无变化,build + lint 通过 |
+
+每个阶段独立提交,提交信息遵循仓库现有 `feat:` / `refactor:` 风格。
+
+## 六、风险与对策
+
+1. **视觉回归**:P4/P5 只调整结构,CSS 类名与覆盖顺序保持不变;阶段结束对照截图。
+2. **Shorts 滑动性能**:`ShortsFeedPage` 的 IntersectionObserver、scroll-snap、键盘/滚轮逻辑整体迁移,不重写。
+3. **ID 精度**:所有 episode/drama ID 继续保持 `String()`,禁止 `Number()`;在 `entities/drama/model.js` 注释固化。
+4. **Context 拆分**:拆 locale 与 ui 后,确认切语言仍能刷新全站文案、搜索输入不再触发页面重渲染。
+5. **接口兼容**:P1/P2 期间 `appApi` 门面保留,页面可逐步迁移,避免一次性大爆炸。
+
+## 七、验收标准
+
+- `pnpm build` 通过,主包因路由懒加载下降(当前 907KB)。
+- `pnpm lint` 0 error。
+- 路由回归:`/`、`/shorts`、`/watch/:id`、`/search`、`/history`、`/subscriptions`、`/library`、`/about`。
+- 交互回归:上下滑切集、Shorts 上下滑、点赞/收藏/评论、语言切换、移动端侧栏、上传弹窗。
+- 目录符合本文第二节,跨层依赖检查无违规。
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..3fefdc5
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,32 @@
+// Author: fruit.com.cn
+import js from '@eslint/js';
+import react from 'eslint-plugin-react';
+import reactHooks from 'eslint-plugin-react-hooks';
+import globals from 'globals';
+
+export default [
+ { ignores: ['dist/**', 'node_modules/**'] },
+ {
+ files: ['**/*.{js,jsx}'],
+ languageOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ globals: { ...globals.browser },
+ parserOptions: { ecmaFeatures: { jsx: true } },
+ },
+ plugins: { react, 'react-hooks': reactHooks },
+ settings: { react: { version: 'detect' } },
+ rules: {
+ ...js.configs.recommended.rules,
+ ...react.configs.recommended.rules,
+ ...react.configs['jsx-runtime'].rules,
+ 'react/prop-types': 'off',
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'warn',
+ },
+ },
+ {
+ files: ['vite.config.js', 'eslint.config.js'],
+ languageOptions: { globals: { ...globals.node } },
+ },
+];
diff --git a/friends.jpg b/friends.jpg
deleted file mode 100644
index c8a06d1..0000000
Binary files a/friends.jpg and /dev/null differ
diff --git a/heir.jpg b/heir.jpg
deleted file mode 100644
index eb9208c..0000000
Binary files a/heir.jpg and /dev/null differ
diff --git a/hero.jpg b/hero.jpg
deleted file mode 100644
index d397b1f..0000000
Binary files a/hero.jpg and /dev/null differ
diff --git a/package.json b/package.json
index 4efaa2e..7b1865e 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "lint": "eslint ."
},
"dependencies": {
"@vitejs/plugin-react": "latest",
@@ -16,5 +17,12 @@
"react-dom": "latest",
"react-router-dom": "latest",
"vite": "latest"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.5",
+ "eslint": "^9.39.5",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "globals": "^17.14.0"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index acea96f..69fa2db 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,9 +29,166 @@ importers:
vite:
specifier: latest
version: 8.3.7
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.39.5
+ version: 9.39.5
+ eslint:
+ specifier: ^9.39.5
+ version: 9.39.5
+ eslint-plugin-react:
+ specifier: ^7.37.5
+ version: 7.37.5(eslint@9.39.5)
+ eslint-plugin-react-hooks:
+ specifier: ^7.1.1
+ version: 7.1.1(eslint@9.39.5)
+ globals:
+ specifier: ^17.14.0
+ version: 17.14.0
packages:
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.13':
+ resolution: {integrity: sha512-j1nvOQCZYSgp7HLmisnCXFTpVvFODiijQ9QHkORfGKWH6YDtskTEeCJS7eV545GZ9UBzrq0e2xsOd5QQ5PjIqw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.14':
+ resolution: {integrity: sha512-qSY56jgW5JnSXzuekLVil6zaOQxOCtOHidr65KkaMBw772IuAY28Qx4ZykAcDn8+FRF15qk80XzKwgSc/RYW6g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.29.13':
+ resolution: {integrity: sha512-8WxJ4/Vmes3ghacj8VOJy3NW8eyHiFY/4thU/LSBuBE470+igjR/j1ZTNn8fV5VdNrLx6iXUwig526UdOvfP0w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.29.14':
+ resolution: {integrity: sha512-2XDvQVd9JESaOgGc4jT2/jquTAO7ppA5noTZQqH+MmZ89yjCzScLwtCPbWtkPZylL+TLWR49IwWmk+QkRR4GsA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-string-parser@7.29.14':
+ resolution: {integrity: sha512-CXvKMFvM80ihI7Vox9jb+kQjnyjESQNPM+vB7U0Zz6TWuZA+Z/FL0QPMH300h33qJtIx+REeBhXqWOPW48TcAQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.14':
+ resolution: {integrity: sha512-6E3Mi1r0B0umq6vmZa4/78q+64fqO2YY0THUdjDPKUCKhNb0yQy5NQIaPHCSD3ryTgiTss//P4JsWS80AsZ6LQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.14':
+ resolution: {integrity: sha512-VL220vXuyO2z3rNIQwY7ftj9UwpqOt+33sH+sBWkOekbsSR2g7EEMnOx24ZlCH0xOA3DM/33SifVFGbnJD8r/w==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.14':
+ resolution: {integrity: sha512-RAdQJrBgmIH1FWvN0X+1hoDxzMzlP4cv/1Zi4dn4tG1H0cTb+rw7CzUxQJhp1f4ANd8j5vH3fy8BAWFGkBDEsA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.14':
+ resolution: {integrity: sha512-UjI/dKmwJwT9pOszJitq2G6EObZPnsZUfdCr92z2TlF3EZK9J3EdfOKUrCzHl6wFthbzwhzl1Soc/UIwxRiq6w==}
+ engines: {node: '>=6.9.0'}
+
+ '@eslint-community/eslint-utils@4.10.1':
+ resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.3':
+ resolution: {integrity: sha512-R76UKbvsqI68DJJVTVyqFKqU98CN4+B+acxNfEA2IFarSzP0SjtKf/3+fFiXzTyv5Fj3PxLkGHe2wnVWWMxobQ==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.21.2':
+ resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.4.2':
+ resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.17.0':
+ resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.7':
+ resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.39.5':
+ resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.7':
+ resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.4.1':
+ resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@humanfs/core@0.19.2':
+ resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.8':
+ resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/types@0.15.0':
+ resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
'@oxc-project/types@0.156.0':
resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==}
@@ -128,6 +285,12 @@ packages:
'@rolldown/pluginutils@1.1.0':
resolution: {integrity: sha512-K+NU2HDCEem4sClgHH4ZZXwdJeNFkJo+hRk479DIwdIviAPN2hFJH1KHHwg8CxHQXvi0M5XY4deTXINfIiSG6w==}
+ '@types/estree@1.0.10':
+ resolution: {integrity: sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
'@vitejs/plugin-react@6.3.0':
resolution: {integrity: sha512-H9E955aVvD2F4zNHCw4eIZbIfF6xKldlh7MNu+hSU7xwQzh65lI/N+CJWqmfhi0PEjDXIt/H7T7J/4wU63KJiw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -144,14 +307,277 @@ packages:
oxc-transform-react:
optional: true
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.19.0:
+ resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.15.0:
+ resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ array-buffer-byte-length@1.0.2:
+ resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
+ engines: {node: '>= 0.4'}
+
+ array-includes@3.2.0:
+ resolution: {integrity: sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.findlast@1.2.5:
+ resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.flat@1.3.3:
+ resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.flatmap@1.3.3:
+ resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.tosorted@1.1.4:
+ resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==}
+ engines: {node: '>= 0.4'}
+
+ arraybuffer.prototype.slice@1.0.4:
+ resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
+ engines: {node: '>= 0.4'}
+
+ async-function@1.0.0:
+ resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
+ engines: {node: '>= 0.4'}
+
+ available-typed-arrays@1.0.7:
+ resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+ engines: {node: '>= 0.4'}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ baseline-browser-mapping@2.11.36:
+ resolution: {integrity: sha512-AbPFGS4K9S3meMRCLTM0Zas+e9X7b5ygUBOboDT5zevCZt9ByKSbMtKhwyVqW+oL8WemPVsbP7X3rdZcIyOa0w==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ brace-expansion@1.1.22:
+ resolution: {integrity: sha512-CKLDTuSsN0+r1ThM0WkzzBI7dnPI6xEGfa/JnVYIK9JNBA1N1NyPwXVHor//NKD/y3sDxp764sZdLhFK7pH8XQ==}
+
+ browserslist@4.29.5:
+ resolution: {integrity: sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bind@1.0.9:
+ resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001826:
+ resolution: {integrity: sha512-wTB+9tIbLro/BCXwLuNV1meHE4L9dS6dArMDwgYfNBPMYScqClHyGunhkCSDtHhe/69Xl0sHlKgBVZ+fNsYtWA==}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ data-view-buffer@1.0.2:
+ resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-length@1.0.2:
+ resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-offset@1.0.1:
+ resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
+ engines: {node: '>= 0.4'}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
+ define-properties@1.2.1:
+ resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+ engines: {node: '>= 0.4'}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
+ doctrine@2.1.0:
+ resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
+ engines: {node: '>=0.10.0'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ electron-to-chromium@1.5.481:
+ resolution: {integrity: sha512-dNUsbAP6E2S1dHIYEuN3cQ0dViThjMtpVRheGdvv5HgHFtzLi4Cu4Mbzms85YggP3Eh2VVZTl5+XxEN8ofCbmQ==}
+
+ es-abstract-get@1.0.0:
+ resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==}
+ engines: {node: '>= 0.4'}
+
+ es-abstract@1.24.2:
+ resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==}
+ engines: {node: '>= 0.4'}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-iterator-helpers@1.4.0:
+ resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.2:
+ resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ es-shim-unscopables@1.1.0:
+ resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==}
+ engines: {node: '>= 0.4'}
+
+ es-to-primitive@1.3.4:
+ resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==}
+ engines: {node: '>= 0.4'}
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-react-hooks@7.1.1:
+ resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0
+
+ eslint-plugin-react@7.37.5:
+ resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
+
+ eslint-scope@8.4.0:
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.1:
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint@9.39.5:
+ resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.4.0:
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -161,14 +587,279 @@ packages:
picomatch:
optional: true
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.4.4:
+ resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
+
+ for-each@0.3.5:
+ resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
+ engines: {node: '>= 0.4'}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ function.prototype.name@1.2.0:
+ resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==}
+ engines: {node: '>= 0.4'}
+
+ functions-have-names@1.2.3:
+ resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+
+ generator-function@2.0.1:
+ resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
+ engines: {node: '>= 0.4'}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-symbol-description@1.1.0:
+ resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
+ engines: {node: '>= 0.4'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globals@17.14.0:
+ resolution: {integrity: sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==}
+ engines: {node: '>=18'}
+
+ globalthis@1.0.4:
+ resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
+ engines: {node: '>= 0.4'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ has-bigints@1.1.0:
+ resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
+ engines: {node: '>= 0.4'}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+ has-proto@1.2.0:
+ resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==}
+ engines: {node: '>= 0.4'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.4:
+ resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
+ engines: {node: '>= 0.4'}
+
+ hermes-estree@0.25.1:
+ resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+
+ hermes-parser@0.25.1:
+ resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+
hls.js@1.7.4:
resolution: {integrity: sha512-3GlxGdPvT2YHvQYOt+yOzT/DEior/qJVNIylHA77kgeoBTdQQqi70GwefNEUmHQeNGyjC8U62+bruqW/Tpm+tg==}
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ internal-slot@1.1.0:
+ resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
+ engines: {node: '>= 0.4'}
+
+ is-array-buffer@3.0.5:
+ resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
+ engines: {node: '>= 0.4'}
+
+ is-async-function@2.1.1:
+ resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
+ engines: {node: '>= 0.4'}
+
+ is-bigint@1.1.0:
+ resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
+ engines: {node: '>= 0.4'}
+
+ is-boolean-object@1.2.2:
+ resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
+ engines: {node: '>= 0.4'}
+
+ is-callable@1.2.7:
+ resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+ engines: {node: '>= 0.4'}
+
+ is-core-module@2.18.0:
+ resolution: {integrity: sha512-qyax9fgaKOEC4qK8WZn7Q4IcoZC9RC1O1fFxRcRTq/oMMkWT/uXpqpXa12kzWR409zAt1drYN9N9BO26UmBCmw==}
+ engines: {node: '>= 0.4'}
+
+ is-data-view@1.0.2:
+ resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==}
+ engines: {node: '>= 0.4'}
+
+ is-date-object@1.1.0:
+ resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
+ engines: {node: '>= 0.4'}
+
+ is-document.all@1.0.0:
+ resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==}
+ engines: {node: '>= 0.4'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-finalizationregistry@1.1.1:
+ resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
+ engines: {node: '>= 0.4'}
+
+ is-generator-function@1.1.2:
+ resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
+ engines: {node: '>= 0.4'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-map@2.0.3:
+ resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
+ engines: {node: '>= 0.4'}
+
+ is-negative-zero@2.0.3:
+ resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
+ engines: {node: '>= 0.4'}
+
+ is-number-object@1.1.1:
+ resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
+ engines: {node: '>= 0.4'}
+
+ is-regex@1.2.1:
+ resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+ engines: {node: '>= 0.4'}
+
+ is-set@2.0.3:
+ resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
+ engines: {node: '>= 0.4'}
+
+ is-shared-array-buffer@1.0.4:
+ resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
+ engines: {node: '>= 0.4'}
+
+ is-string@1.1.1:
+ resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
+ engines: {node: '>= 0.4'}
+
+ is-symbol@1.1.1:
+ resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
+ engines: {node: '>= 0.4'}
+
+ is-typed-array@1.1.15:
+ resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
+ engines: {node: '>= 0.4'}
+
+ is-weakmap@2.0.2:
+ resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
+ engines: {node: '>= 0.4'}
+
+ is-weakref@1.1.1:
+ resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==}
+ engines: {node: '>= 0.4'}
+
+ is-weakset@2.0.4:
+ resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
+ engines: {node: '>= 0.4'}
+
+ isarray@2.0.5:
+ resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ iterator.prototype@1.1.5:
+ resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
+ engines: {node: '>= 0.4'}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.3.2:
+ resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ jsx-ast-utils@3.3.5:
+ resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
+ engines: {node: '>=4.0'}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
lightningcss-android-arm64@1.33.0:
resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
engines: {node: '>= 12.0.0'}
@@ -239,6 +930,20 @@ packages:
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
engines: {node: '>= 12.0.0'}
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
lucide-react@1.71.0:
resolution: {integrity: sha512-8YtvF+bqT/EMqAXU7kWvYdRu+TV9j4kS1eIS7IbN1j1Z1DgupbrTL0nDIygdqgBDTD7AJPP607VdVgzDTar4zA==}
peerDependencies:
@@ -248,11 +953,91 @@ packages:
'@types/react':
optional: true
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ minimatch@3.1.5:
+ resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
nanoid@3.3.20:
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ node-exports-info@1.6.2:
+ resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==}
+ engines: {node: '>= 0.4'}
+
+ node-releases@2.0.63:
+ resolution: {integrity: sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==}
+ engines: {node: '>=18'}
+
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ object-keys@1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
+
+ object.assign@4.1.7:
+ resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
+ engines: {node: '>= 0.4'}
+
+ object.entries@1.1.9:
+ resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==}
+ engines: {node: '>= 0.4'}
+
+ object.fromentries@2.0.8:
+ resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==}
+ engines: {node: '>= 0.4'}
+
+ object.values@1.2.1:
+ resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
+ engines: {node: '>= 0.4'}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ own-keys@1.0.2:
+ resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==}
+ engines: {node: '>= 0.4'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -260,15 +1045,33 @@ packages:
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
engines: {node: '>=12'}
+ possible-typed-array-names@1.1.0:
+ resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
+ engines: {node: '>= 0.4'}
+
postcss@8.5.32:
resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==}
engines: {node: ^10 || ^12 || >=14}
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ prop-types@15.8.1:
+ resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
react-dom@19.3.1:
resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==}
peerDependencies:
react: ^19.3.1
+ react-is@16.13.1:
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
react-router-dom@7.18.5:
resolution: {integrity: sha512-9HcfDMU6JJ/fp6gJZYGwNE6RknVGS4kBjH1HkNw959mCJi70TVwdipQWv2FEol9uGpi+SZTFtVpJPtEQfayeOw==}
engines: {node: '>=20.0.0'}
@@ -290,25 +1093,162 @@ packages:
resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==}
engines: {node: '>=0.10.0'}
+ reflect.getprototypeof@1.0.10:
+ resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
+ engines: {node: '>= 0.4'}
+
+ regexp.prototype.flags@1.5.4:
+ resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
+ engines: {node: '>= 0.4'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve@2.0.0-next.7:
+ resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
rolldown@1.2.18:
resolution: {integrity: sha512-C1uuT3UR60SShuCaPQFAAZyefJCWZlAKB02BV3BuPE2Rs4fevRQltEKoFO2tV5W9/eJhU5RieV6vJj3euulfNQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
+ safe-array-concat@1.1.4:
+ resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==}
+ engines: {node: '>=0.4'}
+
+ safe-push-apply@1.0.0:
+ resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
+ engines: {node: '>= 0.4'}
+
+ safe-regex-test@1.1.0:
+ resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+ engines: {node: '>= 0.4'}
+
scheduler@0.28.0:
resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
+ semver@6.3.2:
+ resolution: {integrity: sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==}
+ hasBin: true
+
set-cookie-parser@2.7.3:
resolution: {integrity: sha512-irZHtTqurLna57KQG/s+fzOwv/J6XSH4cs8M/vsrY3uCaQ3S6+QJguAH0WdN47SXlk82voMZ4naWMQf8fdEoIg==}
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ set-function-name@2.0.2:
+ resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+ engines: {node: '>= 0.4'}
+
+ set-proto@1.0.0:
+ resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
+ engines: {node: '>= 0.4'}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ side-channel-list@1.0.1:
+ resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.1:
+ resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
+ engines: {node: '>= 0.4'}
+
source-map-js@1.2.2:
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
+ stop-iteration-iterator@1.1.0:
+ resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.matchall@4.1.0:
+ resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.repeat@1.0.0:
+ resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==}
+
+ string.prototype.trim@1.2.11:
+ resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimend@1.0.10:
+ resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimstart@1.0.8:
+ resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
+ engines: {node: '>= 0.4'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typed-array-buffer@1.0.3:
+ resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-length@1.0.3:
+ resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-offset@1.0.5:
+ resolution: {integrity: sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-length@1.0.8:
+ resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==}
+ engines: {node: '>= 0.4'}
+
+ unbox-primitive@1.1.0:
+ resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
+ engines: {node: '>= 0.4'}
+
+ update-browserslist-db@1.3.4:
+ resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
vite@8.3.7:
resolution: {integrity: sha512-oxh0dMxrayBr33ScJvvBJ2HBQ2jc1TBrXKR2NyL/MJ9WPuX19mN2DXWUmywKdyw5RFVQBo8hmtb2FMC0LHrnng==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -352,8 +1292,230 @@ packages:
yaml:
optional: true
+ which-boxed-primitive@1.1.1:
+ resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
+ engines: {node: '>= 0.4'}
+
+ which-builtin-type@1.2.1:
+ resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==}
+ engines: {node: '>= 0.4'}
+
+ which-collection@1.0.2:
+ resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
+ engines: {node: '>= 0.4'}
+
+ which-typed-array@1.1.24:
+ resolution: {integrity: sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==}
+ engines: {node: '>= 0.4'}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zod-validation-error@4.0.2:
+ resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ zod: ^3.25.0 || ^4.0.0
+
+ zod@4.6.7:
+ resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==}
+
snapshots:
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.7': {}
+
+ '@babel/core@7.29.13':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.14
+ '@babel/helper-compilation-targets': 7.29.13
+ '@babel/helper-module-transforms': 7.29.14(@babel/core@7.29.13)
+ '@babel/helpers': 7.29.14
+ '@babel/parser': 7.29.14
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.14
+ '@babel/types': 7.29.14
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.14':
+ dependencies:
+ '@babel/parser': 7.29.14
+ '@babel/types': 7.29.14
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.29.13':
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.29.5
+ lru-cache: 5.1.1
+ semver: 6.3.2
+
+ '@babel/helper-globals@7.29.7': {}
+
+ '@babel/helper-module-imports@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.14
+ '@babel/types': 7.29.14
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.29.14(@babel/core@7.29.13)':
+ dependencies:
+ '@babel/core': 7.29.13
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.14
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.29.14': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-option@7.29.7': {}
+
+ '@babel/helpers@7.29.14':
+ dependencies:
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.14
+
+ '@babel/parser@7.29.14':
+ dependencies:
+ '@babel/types': 7.29.14
+
+ '@babel/template@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.14
+ '@babel/types': 7.29.14
+
+ '@babel/traverse@7.29.14':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.14
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.14
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.14
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.14':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.14
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)':
+ dependencies:
+ eslint: 9.39.5
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.3': {}
+
+ '@eslint/config-array@0.21.2':
+ dependencies:
+ '@eslint/object-schema': 2.1.7
+ debug: 4.4.3
+ minimatch: 3.1.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.4.2':
+ dependencies:
+ '@eslint/core': 0.17.0
+
+ '@eslint/core@0.17.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.7':
+ dependencies:
+ ajv: 6.15.0
+ debug: 4.4.3
+ espree: 10.4.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.3.2
+ minimatch: 3.1.5
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.39.5': {}
+
+ '@eslint/object-schema@2.1.7': {}
+
+ '@eslint/plugin-kit@0.4.1':
+ dependencies:
+ '@eslint/core': 0.17.0
+ levn: 0.4.1
+
+ '@humanfs/core@0.19.2':
+ dependencies:
+ '@humanfs/types': 0.15.0
+
+ '@humanfs/node@0.16.8':
+ dependencies:
+ '@humanfs/core': 0.19.2
+ '@humanfs/types': 0.15.0
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanfs/types@0.15.0': {}
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.6.0': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.6.0
+
'@oxc-project/types@0.156.0': {}
'@rolldown/binding-android-arm-eabi@1.2.18':
@@ -403,24 +1565,714 @@ snapshots:
'@rolldown/pluginutils@1.1.0': {}
+ '@types/estree@1.0.10': {}
+
+ '@types/json-schema@7.0.15': {}
+
'@vitejs/plugin-react@6.3.0(vite@8.3.7)':
dependencies:
'@rolldown/pluginutils': 1.1.0
vite: 8.3.7
+ acorn-jsx@5.3.2(acorn@8.19.0):
+ dependencies:
+ acorn: 8.19.0
+
+ acorn@8.19.0: {}
+
+ ajv@6.15.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
+ array-buffer-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ is-array-buffer: 3.0.5
+
+ array-includes@3.2.0:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-object-atoms: 1.1.2
+ es-shim-unscopables: 1.1.0
+ is-string: 1.1.1
+ math-intrinsics: 1.1.0
+
+ array.prototype.findlast@1.2.5:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.flat@1.3.3:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.flatmap@1.3.3:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.tosorted@1.1.4:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-errors: 1.3.0
+ es-shim-unscopables: 1.1.0
+
+ arraybuffer.prototype.slice@1.0.4:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ is-array-buffer: 3.0.5
+
+ async-function@1.0.0: {}
+
+ available-typed-arrays@1.0.7:
+ dependencies:
+ possible-typed-array-names: 1.1.0
+
+ balanced-match@1.0.2: {}
+
+ baseline-browser-mapping@2.11.36: {}
+
+ brace-expansion@1.1.22:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ browserslist@4.29.5:
+ dependencies:
+ baseline-browser-mapping: 2.11.36
+ caniuse-lite: 1.0.30001826
+ electron-to-chromium: 1.5.481
+ node-releases: 2.0.63
+ update-browserslist-db: 1.3.4(browserslist@4.29.5)
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bind@1.0.9:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ get-intrinsic: 1.3.0
+ set-function-length: 1.2.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001826: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ concat-map@0.0.1: {}
+
+ convert-source-map@2.0.0: {}
+
cookie@1.1.1: {}
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ data-view-buffer@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-offset@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ define-properties@1.2.1:
+ dependencies:
+ define-data-property: 1.1.4
+ has-property-descriptors: 1.0.2
+ object-keys: 1.1.1
+
detect-libc@2.1.2: {}
+ doctrine@2.1.0:
+ dependencies:
+ esutils: 2.0.3
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ electron-to-chromium@1.5.481: {}
+
+ es-abstract-get@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+ is-callable: 1.2.7
+ object-inspect: 1.13.4
+
+ es-abstract@1.24.2:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ arraybuffer.prototype.slice: 1.0.4
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ data-view-buffer: 1.0.2
+ data-view-byte-length: 1.0.2
+ data-view-byte-offset: 1.0.1
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+ es-set-tostringtag: 2.1.0
+ es-to-primitive: 1.3.4
+ function.prototype.name: 1.2.0
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ get-symbol-description: 1.1.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.4
+ internal-slot: 1.1.0
+ is-array-buffer: 3.0.5
+ is-callable: 1.2.7
+ is-data-view: 1.0.2
+ is-negative-zero: 2.0.3
+ is-regex: 1.2.1
+ is-set: 2.0.3
+ is-shared-array-buffer: 1.0.4
+ is-string: 1.1.1
+ is-typed-array: 1.1.15
+ is-weakref: 1.1.1
+ math-intrinsics: 1.1.0
+ object-inspect: 1.13.4
+ object-keys: 1.1.1
+ object.assign: 4.1.7
+ own-keys: 1.0.2
+ regexp.prototype.flags: 1.5.4
+ safe-array-concat: 1.1.4
+ safe-push-apply: 1.0.0
+ safe-regex-test: 1.1.0
+ set-proto: 1.0.0
+ stop-iteration-iterator: 1.1.0
+ string.prototype.trim: 1.2.11
+ string.prototype.trimend: 1.0.10
+ string.prototype.trimstart: 1.0.8
+ typed-array-buffer: 1.0.3
+ typed-array-byte-length: 1.0.3
+ typed-array-byte-offset: 1.0.5
+ typed-array-length: 1.0.8
+ unbox-primitive: 1.1.0
+ which-typed-array: 1.1.24
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-iterator-helpers@1.4.0:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-errors: 1.3.0
+ es-set-tostringtag: 2.1.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ internal-slot: 1.1.0
+ iterator.prototype: 1.1.5
+ math-intrinsics: 1.1.0
+
+ es-object-atoms@1.1.2:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.4
+
+ es-shim-unscopables@1.1.0:
+ dependencies:
+ hasown: 2.0.4
+
+ es-to-primitive@1.3.4:
+ dependencies:
+ es-abstract-get: 1.0.0
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ is-callable: 1.2.7
+ is-date-object: 1.1.0
+ is-symbol: 1.1.1
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@7.1.1(eslint@9.39.5):
+ dependencies:
+ '@babel/core': 7.29.13
+ '@babel/parser': 7.29.14
+ eslint: 9.39.5
+ hermes-parser: 0.25.1
+ zod: 4.6.7
+ zod-validation-error: 4.0.2(zod@4.6.7)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-react@7.37.5(eslint@9.39.5):
+ dependencies:
+ array-includes: 3.2.0
+ array.prototype.findlast: 1.2.5
+ array.prototype.flatmap: 1.3.3
+ array.prototype.tosorted: 1.1.4
+ doctrine: 2.1.0
+ es-iterator-helpers: 1.4.0
+ eslint: 9.39.5
+ estraverse: 5.3.0
+ hasown: 2.0.4
+ jsx-ast-utils: 3.3.5
+ minimatch: 3.1.5
+ object.entries: 1.1.9
+ object.fromentries: 2.0.8
+ object.values: 1.2.1
+ prop-types: 15.8.1
+ resolve: 2.0.0-next.7
+ semver: 6.3.2
+ string.prototype.matchall: 4.1.0
+ string.prototype.repeat: 1.0.0
+
+ eslint-scope@8.4.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.1: {}
+
+ eslint@9.39.5:
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5)
+ '@eslint-community/regexpp': 4.12.3
+ '@eslint/config-array': 0.21.2
+ '@eslint/config-helpers': 0.4.2
+ '@eslint/core': 0.17.0
+ '@eslint/eslintrc': 3.3.7
+ '@eslint/js': 9.39.5
+ '@eslint/plugin-kit': 0.4.1
+ '@humanfs/node': 0.16.8
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.10
+ ajv: 6.15.0
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.5
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.4.0:
+ dependencies:
+ acorn: 8.19.0
+ acorn-jsx: 5.3.2(acorn@8.19.0)
+ eslint-visitor-keys: 4.2.1
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ esutils@2.0.3: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
fdir@6.5.0(picomatch@4.0.7):
optionalDependencies:
picomatch: 4.0.7
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.4.4
+ keyv: 4.5.4
+
+ flatted@3.4.4: {}
+
+ for-each@0.3.5:
+ dependencies:
+ is-callable: 1.2.7
+
fsevents@2.3.3:
optional: true
+ function-bind@1.1.2: {}
+
+ function.prototype.name@1.2.0:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.2
+ hasown: 2.0.4
+ is-callable: 1.2.7
+ is-document.all: 1.0.0
+
+ functions-have-names@1.2.3: {}
+
+ generator-function@2.0.1: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.4
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.2
+
+ get-symbol-description@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@14.0.0: {}
+
+ globals@17.14.0: {}
+
+ globalthis@1.0.4:
+ dependencies:
+ define-properties: 1.2.1
+ gopd: 1.2.0
+
+ gopd@1.2.0: {}
+
+ has-bigints@1.1.0: {}
+
+ has-flag@4.0.0: {}
+
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.1
+
+ has-proto@1.2.0:
+ dependencies:
+ dunder-proto: 1.0.1
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ hasown@2.0.4:
+ dependencies:
+ function-bind: 1.1.2
+
+ hermes-estree@0.25.1: {}
+
+ hermes-parser@0.25.1:
+ dependencies:
+ hermes-estree: 0.25.1
+
hls.js@1.7.4: {}
+ ignore@5.3.2: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ internal-slot@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ hasown: 2.0.4
+ side-channel: 1.1.1
+
+ is-array-buffer@3.0.5:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ is-async-function@2.1.1:
+ dependencies:
+ async-function: 1.0.0
+ call-bound: 1.0.4
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-bigint@1.1.0:
+ dependencies:
+ has-bigints: 1.1.0
+
+ is-boolean-object@1.2.2:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-callable@1.2.7: {}
+
+ is-core-module@2.18.0:
+ dependencies:
+ hasown: 2.0.4
+
+ is-data-view@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ is-typed-array: 1.1.15
+
+ is-date-object@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-document.all@1.0.0:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-extglob@2.1.1: {}
+
+ is-finalizationregistry@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-generator-function@1.1.2:
+ dependencies:
+ call-bound: 1.0.4
+ generator-function: 2.0.1
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-map@2.0.3: {}
+
+ is-negative-zero@2.0.3: {}
+
+ is-number-object@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-regex@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.4
+
+ is-set@2.0.3: {}
+
+ is-shared-array-buffer@1.0.4:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-string@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-symbol@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-symbols: 1.1.0
+ safe-regex-test: 1.1.0
+
+ is-typed-array@1.1.15:
+ dependencies:
+ which-typed-array: 1.1.24
+
+ is-weakmap@2.0.2: {}
+
+ is-weakref@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-weakset@2.0.4:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ isarray@2.0.5: {}
+
+ isexe@2.0.0: {}
+
+ iterator.prototype@1.1.5:
+ dependencies:
+ define-data-property: 1.1.4
+ es-object-atoms: 1.1.2
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ has-symbols: 1.1.0
+ set-function-name: 2.0.2
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.3.2:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ jsx-ast-utils@3.3.5:
+ dependencies:
+ array-includes: 3.2.0
+ array.prototype.flat: 1.3.3
+ object.assign: 4.1.7
+ object.values: 1.2.1
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
lightningcss-android-arm64@1.33.0:
optional: true
@@ -470,27 +2322,144 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.33.0
lightningcss-win32-x64-msvc: 1.33.0
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.merge@4.6.2: {}
+
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
lucide-react@1.71.0(react@19.3.1):
dependencies:
react: 19.3.1
+ math-intrinsics@1.1.0: {}
+
+ minimatch@3.1.5:
+ dependencies:
+ brace-expansion: 1.1.22
+
+ ms@2.1.3: {}
+
nanoid@3.3.20: {}
+ natural-compare@1.4.0: {}
+
+ node-exports-info@1.6.2:
+ dependencies:
+ array.prototype.flatmap: 1.3.3
+ es-errors: 1.3.0
+ object.entries: 1.1.9
+ semver: 6.3.2
+
+ node-releases@2.0.63: {}
+
+ object-assign@4.1.1: {}
+
+ object-inspect@1.13.4: {}
+
+ object-keys@1.1.1: {}
+
+ object.assign@4.1.7:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.2
+ has-symbols: 1.1.0
+ object-keys: 1.1.1
+
+ object.entries@1.1.9:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.2
+
+ object.fromentries@2.0.8:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-object-atoms: 1.1.2
+
+ object.values@1.2.1:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.2
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ own-keys@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ object-keys: 1.1.1
+ safe-push-apply: 1.0.0
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ path-parse@1.0.7: {}
+
picocolors@1.1.1: {}
picomatch@4.0.7: {}
+ possible-typed-array-names@1.1.0: {}
+
postcss@8.5.32:
dependencies:
nanoid: 3.3.20
picocolors: 1.1.1
source-map-js: 1.2.2
+ prelude-ls@1.2.1: {}
+
+ prop-types@15.8.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ react-is: 16.13.1
+
+ punycode@2.3.1: {}
+
react-dom@19.3.1(react@19.3.1):
dependencies:
react: 19.3.1
scheduler: 0.28.0
+ react-is@16.13.1: {}
+
react-router-dom@7.18.5(react-dom@19.3.1(react@19.3.1))(react@19.3.1):
dependencies:
react: 19.3.1
@@ -507,6 +2476,37 @@ snapshots:
react@19.3.1: {}
+ reflect.getprototypeof@1.0.10:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ which-builtin-type: 1.2.1
+
+ regexp.prototype.flags@1.5.4:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-errors: 1.3.0
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ set-function-name: 2.0.2
+
+ resolve-from@4.0.0: {}
+
+ resolve@2.0.0-next.7:
+ dependencies:
+ es-errors: 1.3.0
+ is-core-module: 2.18.0
+ node-exports-info: 1.6.2
+ object-keys: 1.1.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
rolldown@1.2.18:
dependencies:
'@oxc-project/types': 0.156.0
@@ -528,17 +2528,205 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc': 1.2.18
'@rolldown/binding-win32-x64-msvc': 1.2.18
+ safe-array-concat@1.1.4:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ has-symbols: 1.1.0
+ isarray: 2.0.5
+
+ safe-push-apply@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ isarray: 2.0.5
+
+ safe-regex-test@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-regex: 1.2.1
+
scheduler@0.28.0: {}
+ semver@6.3.2: {}
+
set-cookie-parser@2.7.3: {}
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+
+ set-function-name@2.0.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.2
+
+ set-proto@1.0.0:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ side-channel-list@1.0.1:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.1
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
source-map-js@1.2.2: {}
+ stop-iteration-iterator@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ internal-slot: 1.1.0
+
+ string.prototype.matchall@4.1.0:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ internal-slot: 1.1.0
+ regexp.prototype.flags: 1.5.4
+ set-function-name: 2.0.2
+ side-channel: 1.1.1
+
+ string.prototype.repeat@1.0.0:
+ dependencies:
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+
+ string.prototype.trim@1.2.11:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-data-property: 1.1.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.2
+ es-object-atoms: 1.1.2
+ has-property-descriptors: 1.0.2
+ safe-regex-test: 1.1.0
+
+ string.prototype.trimend@1.0.10:
+ dependencies:
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.2
+
+ string.prototype.trimstart@1.0.8:
+ dependencies:
+ call-bind: 1.0.9
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.2
+
+ strip-json-comments@3.1.1: {}
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-preserve-symlinks-flag@1.0.0: {}
+
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.7)
picomatch: 4.0.7
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typed-array-buffer@1.0.3:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-length@1.0.3:
+ dependencies:
+ call-bind: 1.0.9
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-offset@1.0.5:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.9
+ for-each: 0.3.5
+ gopd: 1.2.0
+ is-typed-array: 1.1.15
+ reflect.getprototypeof: 1.0.10
+
+ typed-array-length@1.0.8:
+ dependencies:
+ call-bind: 1.0.9
+ for-each: 0.3.5
+ gopd: 1.2.0
+ is-typed-array: 1.1.15
+ possible-typed-array-names: 1.1.0
+ reflect.getprototypeof: 1.0.10
+
+ unbox-primitive@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-bigints: 1.1.0
+ has-symbols: 1.1.0
+ which-boxed-primitive: 1.1.1
+
+ update-browserslist-db@1.3.4(browserslist@4.29.5):
+ dependencies:
+ browserslist: 4.29.5
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
vite@8.3.7:
dependencies:
lightningcss: 1.33.0
@@ -548,3 +2736,60 @@ snapshots:
tinyglobby: 0.2.17
optionalDependencies:
fsevents: 2.3.3
+
+ which-boxed-primitive@1.1.1:
+ dependencies:
+ is-bigint: 1.1.0
+ is-boolean-object: 1.2.2
+ is-number-object: 1.1.1
+ is-string: 1.1.1
+ is-symbol: 1.1.1
+
+ which-builtin-type@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ function.prototype.name: 1.2.0
+ has-tostringtag: 1.0.2
+ is-async-function: 2.1.1
+ is-date-object: 1.1.0
+ is-finalizationregistry: 1.1.1
+ is-generator-function: 1.1.2
+ is-regex: 1.2.1
+ is-weakref: 1.1.1
+ isarray: 2.0.5
+ which-boxed-primitive: 1.1.1
+ which-collection: 1.0.2
+ which-typed-array: 1.1.24
+
+ which-collection@1.0.2:
+ dependencies:
+ is-map: 2.0.3
+ is-set: 2.0.3
+ is-weakmap: 2.0.2
+ is-weakset: 2.0.4
+
+ which-typed-array@1.1.24:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.9
+ call-bound: 1.0.4
+ for-each: 0.3.5
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ word-wrap@1.2.5: {}
+
+ yallist@3.1.1: {}
+
+ yocto-queue@0.1.0: {}
+
+ zod-validation-error@4.0.2(zod@4.6.7):
+ dependencies:
+ zod: 4.6.7
+
+ zod@4.6.7: {}
diff --git a/public/brand/ymb-logo-gold.png b/public/brand/ymb-logo-gold.png
deleted file mode 100644
index 406a788..0000000
Binary files a/public/brand/ymb-logo-gold.png and /dev/null differ
diff --git a/robots.txt b/public/robots.txt
similarity index 100%
rename from robots.txt
rename to public/robots.txt
diff --git a/sitemap.xml b/public/sitemap.xml
similarity index 100%
rename from sitemap.xml
rename to public/sitemap.xml
diff --git a/revenge.jpg b/revenge.jpg
deleted file mode 100644
index e82cb29..0000000
Binary files a/revenge.jpg and /dev/null differ
diff --git a/romance.jpg b/romance.jpg
deleted file mode 100644
index cc39851..0000000
Binary files a/romance.jpg and /dev/null differ
diff --git a/second-chance.jpg b/second-chance.jpg
deleted file mode 100644
index 55683a8..0000000
Binary files a/second-chance.jpg and /dev/null differ
diff --git a/secret.jpg b/secret.jpg
deleted file mode 100644
index ce53b5a..0000000
Binary files a/secret.jpg and /dev/null differ
diff --git a/src/app/App.jsx b/src/app/App.jsx
index d7ce5db..8ab5307 100644
--- a/src/app/App.jsx
+++ b/src/app/App.jsx
@@ -1,13 +1,14 @@
// Author: fruit.com.cn
-import { BrowserRouter, Route, Routes } from 'react-router-dom';
-import { StoreProvider } from './store';
-import { AppShell } from '../layouts/AppShell';
-import { HomePage } from '../pages/HomePage';
-import { ListingPage } from '../pages/ListingPage';
-import { SearchPage } from '../pages/SearchPage';
-import { WatchPage } from '../pages/WatchPage';
-import { ShortsFeedPage } from '../pages/ShortsFeedPage';
-import { HistoryPage } from '../pages/HistoryPage';
-import { AboutPage } from '../pages/AboutPage';
+import { BrowserRouter } from 'react-router-dom';
+import { AppProviders } from './providers/AppProviders';
+import { AppRouter } from './router';
-export function App() { return }>} />} />} />} />} />} />} />} />; }
+export function App() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/app/providers/AppProviders.jsx b/src/app/providers/AppProviders.jsx
new file mode 100644
index 0000000..cdcdeec
--- /dev/null
+++ b/src/app/providers/AppProviders.jsx
@@ -0,0 +1,90 @@
+// Author: fruit.com.cn
+import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
+import { getLocale } from '../../i18n';
+import { detectLocaleFromIp } from '../../shared/lib/geo';
+import { applySeo } from '../../shared/lib/seo';
+
+const LocaleContext = createContext(null);
+const UiContext = createContext(null);
+const LANG_STORAGE_KEY = 'ymb.lang';
+const SUPPORTED_LANGS = ['id', 'en', 'zh'];
+
+function readUrlLang() {
+ const value = new URLSearchParams(window.location.search).get('lang');
+ return SUPPORTED_LANGS.includes(value) ? value : '';
+}
+
+function readStoredLang() {
+ try {
+ const value = window.localStorage.getItem(LANG_STORAGE_KEY);
+ return SUPPORTED_LANGS.includes(value) ? value : '';
+ } catch {
+ return '';
+ }
+}
+
+function readNavigatorLang() {
+ const code = (navigator.language || '').toLowerCase();
+ if (code.startsWith('id')) return 'id';
+ if (code.startsWith('zh')) return 'zh';
+ return 'en';
+}
+
+function detectInitialLang() {
+ return readUrlLang() || readStoredLang() || readNavigatorLang();
+}
+
+export function AppProviders({ children }) {
+ const [lang, setLangState] = useState(detectInitialLang);
+ const [query, setQuery] = useState('');
+ const [sidebarOpen, setSidebarOpen] = useState(() => window.innerWidth > 760);
+ const userSelectedRef = useRef(false);
+ const locale = getLocale(lang);
+
+ // 首次进入且没有明确偏好时,按 IP 归属地自动匹配语言
+ useEffect(() => {
+ if (readUrlLang() || readStoredLang()) return undefined;
+ let active = true;
+ detectLocaleFromIp().then((detected) => {
+ if (active && detected && !userSelectedRef.current) setLangState(detected);
+ });
+ return () => {
+ active = false;
+ };
+ }, []);
+
+ const setLang = useCallback((next) => {
+ userSelectedRef.current = true;
+ setLangState(next);
+ try {
+ window.localStorage.setItem(LANG_STORAGE_KEY, next);
+ } catch {
+ // 隐私模式下写入失败时仍保证本次会话生效
+ }
+ }, []);
+
+ useEffect(() => {
+ applySeo(locale, window.location.pathname);
+ }, [lang, locale]);
+
+ const localeValue = useMemo(() => ({ lang, locale, setLang }), [lang, locale, setLang]);
+ const uiValue = useMemo(() => ({ query, setQuery, sidebarOpen, setSidebarOpen }), [query, sidebarOpen]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useLocale() {
+ const value = useContext(LocaleContext);
+ if (!value) throw new Error('useLocale must be used within AppProviders');
+ return value;
+}
+
+export function useUi() {
+ const value = useContext(UiContext);
+ if (!value) throw new Error('useUi must be used within AppProviders');
+ return value;
+}
diff --git a/src/app/router.jsx b/src/app/router.jsx
new file mode 100644
index 0000000..03b9a1d
--- /dev/null
+++ b/src/app/router.jsx
@@ -0,0 +1,29 @@
+// Author: fruit.com.cn
+import { lazy } from 'react';
+import { Route, Routes } from 'react-router-dom';
+import { AppShell } from '../layouts/AppShell';
+
+const HomePage = lazy(() => import('../features/home/HomePage').then((module) => ({ default: module.HomePage })));
+const ShortsFeedPage = lazy(() => import('../features/shorts/ShortsFeedPage').then((module) => ({ default: module.ShortsFeedPage })));
+const WatchPage = lazy(() => import('../features/watch/WatchPage').then((module) => ({ default: module.WatchPage })));
+const SearchPage = lazy(() => import('../features/search/SearchPage').then((module) => ({ default: module.SearchPage })));
+const HistoryPage = lazy(() => import('../features/history/HistoryPage').then((module) => ({ default: module.HistoryPage })));
+const ListingPage = lazy(() => import('../features/listing/ListingPage').then((module) => ({ default: module.ListingPage })));
+const AboutPage = lazy(() => import('../features/about/AboutPage').then((module) => ({ default: module.AboutPage })));
+
+export function AppRouter() {
+ return (
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
diff --git a/src/app/store.jsx b/src/app/store.jsx
deleted file mode 100644
index a5a94bd..0000000
--- a/src/app/store.jsx
+++ /dev/null
@@ -1,9 +0,0 @@
-// Author: fruit.com.cn
-import { createContext, useContext, useMemo, useState } from 'react';
-import { getLocale } from '../i18n';
-import { applySeo } from '../lib/seo';
-import { useEffect } from 'react';
-
-const StoreContext = createContext(null);
-export function StoreProvider({ children }) { const initial = new URLSearchParams(location.search).get('lang'); const [lang, setLangState] = useState(initial || 'id'); const [query, setQuery] = useState(''); const [sidebarOpen, setSidebarOpen] = useState(() => window.innerWidth > 760); const setLang = (next) => { setLangState(next); const url = new URL(location.href); url.searchParams.set('lang', next); history.replaceState(null, '', url); }; const locale = getLocale(lang); useEffect(() => applySeo(locale, location.pathname), [lang, locale]); const value = useMemo(() => ({ lang, locale, setLang, query, setQuery, sidebarOpen, setSidebarOpen }), [lang, locale, query, sidebarOpen]); return {children}; }
-export function useAppStore() { return useContext(StoreContext); }
diff --git a/src/components/player/VideoPlayer.jsx b/src/components/player/VideoPlayer.jsx
deleted file mode 100644
index 0408066..0000000
--- a/src/components/player/VideoPlayer.jsx
+++ /dev/null
@@ -1,13 +0,0 @@
-// Author: fruit.com.cn
-import { useEffect, useRef, useState } from 'react';
-import Hls from 'hls.js';
-
-export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProgress }) {
- const ref = useRef(null); const [ready, setReady] = useState(false); const [error, setError] = useState(''); const playedRef = useRef(false); const onProgressRef = useRef(onProgress);
- useEffect(() => { onProgressRef.current = onProgress; }, [onProgress]);
- useEffect(() => { const video = ref.current; if (!video) return undefined; const flush = () => { if (playedRef.current) onProgressRef.current?.(video.currentTime, video.duration, true); }; video.addEventListener('pause', flush); window.addEventListener('pagehide', flush); return () => { flush(); video.removeEventListener('pause', flush); window.removeEventListener('pagehide', flush); }; }, []);
- useEffect(() => { const video = ref.current; if (!video || !src) return undefined; video.muted = muted; playedRef.current = false; setReady(false); setError(''); const hls = new Hls({ enableWorker: true }); const start = () => setReady(true); const onError = (_event, data) => { if (data?.fatal) { const message = `${data.details || 'Video stream error'} · ${data.response?.code || ''}`; console.error('YMB HLS ERROR', data); setError(message); } }; const onVideoError = () => { const message = `VIDEO_ERROR: ${video.error?.code || 'unknown'} ${video.error?.message || ''}`; console.error(message, { src, mediaError: video.error }); setError(message); }; hls.on(Hls.Events.MANIFEST_PARSED, start); hls.on(Hls.Events.ERROR, onError); video.addEventListener('error', onVideoError); if (Hls.isSupported()) { hls.attachMedia(video); hls.loadSource(src); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = src; video.addEventListener('loadedmetadata', start, { once: true }); } else setError('This browser does not support HLS'); return () => { video.removeEventListener('error', onVideoError); hls.destroy(); video.pause(); }; }, [src, muted]);
- useEffect(() => { const video = ref.current; if (!video || !ready) return; if (autoPlay) video.play().catch((playError) => { if (playError.name !== 'AbortError') setError(`PLAY_FAILED: ${playError.message}`); }); else video.pause(); }, [autoPlay, ready]);
- const playNow = () => { const video = ref.current; if (video) { video.muted = false; video.play().catch((playError) => setError(playError.message)); } };
- return ;
-}
diff --git a/src/entities/category/api.js b/src/entities/category/api.js
new file mode 100644
index 0000000..83296de
--- /dev/null
+++ b/src/entities/category/api.js
@@ -0,0 +1,8 @@
+// Author: fruit.com.cn
+import { http } from '../../shared/api';
+import { toQuery } from '../../shared/api/query';
+
+export const categoryApi = {
+ list: (options) => http.get('/app/categories', options),
+ dramas: (id, pageIndex = 1, pageSize = 20, options) => http.get(`/app/categories/${encodeURIComponent(id)}/dramas${toQuery({ pageIndex, pageSize })}`, options),
+};
diff --git a/src/entities/drama/api.js b/src/entities/drama/api.js
new file mode 100644
index 0000000..69634c1
--- /dev/null
+++ b/src/entities/drama/api.js
@@ -0,0 +1,11 @@
+// Author: fruit.com.cn
+import { http } from '../../shared/api';
+import { toQuery } from '../../shared/api/query';
+
+export const dramaApi = {
+ get: (id, options) => http.get(`/app/dramas/${encodeURIComponent(id)}`, options),
+ playback: (episodeId, options) => http.get(`/app/episodes/${encodeURIComponent(episodeId)}/play`, options),
+ stats: (episodeId, options) => http.get(`/app/episodes/${encodeURIComponent(episodeId)}/stats`, options),
+ homeFeed: (options) => http.get('/app/home/feed', options),
+ feed: ({ types = '', categoryId = '', cursor, size = 10 } = {}, options) => http.get(`/app/feed${toQuery({ size, types, categoryId, cursor })}`, options),
+};
diff --git a/src/entities/drama/model.js b/src/entities/drama/model.js
new file mode 100644
index 0000000..2c77a0f
--- /dev/null
+++ b/src/entities/drama/model.js
@@ -0,0 +1,52 @@
+// Author: fruit.com.cn
+import { getImage, resolveMedia } from '../../shared/lib/media';
+import { getText } from '../../shared/lib/text';
+
+export function normalizeDrama(item = {}, lang = 'id') {
+ const id = String(item.id ?? item.dramaId ?? item.code ?? '');
+ const episodes = item.episodes || item.episodeList || (item.preview ? [item.preview] : []);
+ const preview = item.preview || episodes[0] || null;
+ return {
+ id,
+ title: getText(item.titleLocales || item.locales || item.title, lang) || item.name || 'YMB Drama',
+ channel: getText(item.channelLocales || item.channel, lang) || item.creatorName || 'YMB Originals',
+ genre: getText(item.categoryLocales || item.genre, lang) || getText(item.types?.[0], lang),
+ image: getImage(item),
+ duration: item.duration || preview?.duration || '',
+ views: item.views || item.viewCount || '',
+ likeCount: Number(item.likeCount) || 0,
+ favoriteCount: Number(item.favoriteCount) || 0,
+ commentCount: Number(item.commentCount) || 0,
+ description: getText(item.synopsisLocales, lang) || '',
+ episodes: episodes.map((episode) => ({ ...episode, playUrl: resolveMedia(episode.playUrl) })),
+ preview: preview ? { ...preview, playUrl: resolveMedia(preview.playUrl) } : null,
+ };
+}
+
+export function normalizeProgress(item = {}, lang = 'id') {
+ const durationMs = Number(item.durationMs) || 0;
+ const positionMs = Number(item.positionMs) || 0;
+ return {
+ id: String(item.dramaId ?? ''),
+ episodeId: String(item.episodeId ?? ''),
+ title: getText(item.titleLocales, lang) || item.dramaTitle || 'YMB Drama',
+ image: resolveMedia(item.dramaCoverLandscape || item.dramaCover || ''),
+ episodeNumber: item.episodeNumber == null ? null : Number(item.episodeNumber),
+ totalEpisodes: item.totalEpisodes == null ? null : Number(item.totalEpisodes),
+ positionMs,
+ durationMs,
+ fraction: durationMs > 0 ? Math.min(1, positionMs / durationMs) : 0,
+ viewCount: item.viewCount,
+ finished: Boolean(item.finished),
+ updatedAt: item.updatedAt || '',
+ };
+}
+
+export function uniqueDramas(items) {
+ const seen = new Set();
+ return items.filter((item) => {
+ if (seen.has(item.id)) return false;
+ seen.add(item.id);
+ return true;
+ });
+}
diff --git a/src/pages/AboutPage.jsx b/src/features/about/AboutPage.jsx
similarity index 96%
rename from src/pages/AboutPage.jsx
rename to src/features/about/AboutPage.jsx
index f97dc1c..a1328e7 100644
--- a/src/pages/AboutPage.jsx
+++ b/src/features/about/AboutPage.jsx
@@ -1,8 +1,8 @@
// Author: fruit.com.cn
import { ArrowRight, Clapperboard, Cpu, Play, Smartphone, Sparkles, Users } from 'lucide-react';
import { Link } from 'react-router-dom';
-import { useAppStore } from '../app/store';
-import { aboutContent } from '../i18n/about';
+import { useLocale } from '../../app/providers/AppProviders';
+import { aboutContent } from './content';
const APP_DOWNLOAD_URL = 'https://cdnetworksdist.okrivo.xyz/wPwBG';
@@ -13,7 +13,7 @@ const PILLARS = [
];
export function AboutPage() {
- const { locale } = useAppStore();
+ const { locale } = useLocale();
const copy = aboutContent[locale.code] || aboutContent.en;
return (
diff --git a/src/i18n/about.js b/src/features/about/content.js
similarity index 100%
rename from src/i18n/about.js
rename to src/features/about/content.js
diff --git a/src/pages/HistoryPage.jsx b/src/features/history/HistoryPage.jsx
similarity index 55%
rename from src/pages/HistoryPage.jsx
rename to src/features/history/HistoryPage.jsx
index d57782d..d544687 100644
--- a/src/pages/HistoryPage.jsx
+++ b/src/features/history/HistoryPage.jsx
@@ -1,10 +1,12 @@
// Author: fruit.com.cn
-import { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
-import { useAppStore } from '../app/store';
-import { appApi } from '../services/appApi';
-import { normalizePage, normalizeProgress } from '../services/normalizers';
-import { HistoryGridSkeleton } from '../components/Skeletons';
+import { useLocale } from '../../app/providers/AppProviders';
+import { HistoryGridSkeleton } from '../../shared/components/skeletons';
+import { useMutation } from '../../shared/hooks/useMutation';
+import { useRequest } from '../../shared/hooks/useRequest';
+import { normalizePage } from '../../shared/lib/pagination';
+import { watchApi } from '../../shared/api/watch';
+import { normalizeProgress } from '../../entities/drama/model';
function episodeLabel(record, localeCode) {
if (!record.episodeNumber) return '';
@@ -12,30 +14,22 @@ function episodeLabel(record, localeCode) {
}
export function HistoryPage() {
- const { locale } = useAppStore();
- const [records, setRecords] = useState(null);
- const [error, setError] = useState('');
-
- const load = useCallback(async () => {
- setError('');
- try {
- const payload = await appApi.watchRecent(1, 20);
- setRecords(normalizePage(payload).list.map((item) => normalizeProgress(item, locale.code)));
- } catch (loadError) {
- setError(loadError.message || 'Request failed');
- setRecords([]);
- }
- }, [locale.code]);
-
- useEffect(() => { load(); }, [load]);
+ const { locale } = useLocale();
+ const history = useRequest(
+ ({ signal }) => watchApi.recent(1, 20, { signal }).then((payload) => normalizePage(payload).list.map((item) => normalizeProgress(item, locale.code))),
+ [locale.code],
+ );
+ const clearHistory = useMutation(() => watchApi.clear());
+ const records = history.data;
+ const errorMessage = history.error?.message || clearHistory.error?.message || '';
const clearAll = async () => {
if (!window.confirm(`${locale.clearHistory}?`)) return;
try {
- await appApi.clearWatchHistory();
- setRecords([]);
- } catch (clearError) {
- setError(clearError.message || 'Request failed');
+ await clearHistory.run();
+ history.refetch();
+ } catch {
+ // 错误由 clearHistory.error 呈现
}
};
@@ -45,9 +39,9 @@ export function HistoryPage() {
{locale.history}
{!!records?.length && }
- {records === null && }
- {error && {error}
}
- {records && !records.length && !error && {locale.historyEmpty}
}
+ {history.loading && records === null && }
+ {errorMessage && {errorMessage}
}
+ {records && !records.length && !errorMessage && {locale.historyEmpty}
}
{!!records?.length && (
{records.map((record) => {
diff --git a/src/pages/HomePage.jsx b/src/features/home/HomePage.jsx
similarity index 51%
rename from src/pages/HomePage.jsx
rename to src/features/home/HomePage.jsx
index 2221e7c..598760c 100644
--- a/src/pages/HomePage.jsx
+++ b/src/features/home/HomePage.jsx
@@ -1,13 +1,18 @@
// Author: fruit.com.cn
import { useMemo, useState } from 'react';
-import { MediaCard } from '../components/MediaCard';
-import { CategoryChipsSkeleton, ShortsShelfSkeleton, VideoGridSkeleton } from '../components/Skeletons';
-import { videos } from '../data/catalog';
-import { useAppStore } from '../app/store';
-import { searchCatalog } from '../services/catalogService';
-import { appApi } from '../services/appApi';
-import { normalizeDrama, normalizePage, uniqueDramas } from '../services/normalizers';
-import { useApi } from '../hooks/useApi';
+import { useLocale } from '../../app/providers/AppProviders';
+import { AsyncBoundary } from '../../shared/components/AsyncBoundary';
+import { MediaCard } from '../../shared/components/MediaCard';
+import { VideoGrid } from '../../shared/components/VideoGrid';
+import { CategoryChipsSkeleton, ShortsShelfSkeleton, VideoGridSkeleton } from '../../shared/components/skeletons';
+import { videos } from '../../shared/data/catalog';
+import { searchCatalog } from '../../shared/data/catalogService';
+import { useRequest } from '../../shared/hooks/useRequest';
+import { normalizePage } from '../../shared/lib/pagination';
+import { dramaApi } from '../../entities/drama/api';
+import { categoryApi } from '../../entities/category/api';
+import { normalizeDrama, uniqueDramas } from '../../entities/drama/model';
+import { CategoryBar } from './components/CategoryBar';
const SHORTS_LIMIT = 5;
const FIRST_SHELF_LIMIT = 6;
@@ -42,24 +47,26 @@ function fallbackGenreIndex(categoryId) {
}
export function HomePage() {
- const { locale } = useAppStore();
+ const { locale } = useLocale();
const [activeCategory, setActiveCategory] = useState(ALL_CATEGORY_ID);
- const homeApi = useApi(
- () => appApi.homeFeed().then((payload) => normalizeHomeItems(payload, locale.code)),
+ const isAllCategory = activeCategory === ALL_CATEGORY_ID;
+
+ const feedRequest = useRequest(
+ ({ signal }) => dramaApi.homeFeed({ signal }).then((payload) => normalizeHomeItems(payload, locale.code)),
[locale.code],
);
- const categoriesApi = useApi(
- () => appApi.categories().then((payload) => normalizeCategories(payload, locale.code)),
+ const categoriesRequest = useRequest(
+ ({ signal }) => categoryApi.list({ signal }).then((payload) => normalizeCategories(payload, locale.code)),
[locale.code],
);
- const categoryApi = useApi(
- () => (activeCategory === ALL_CATEGORY_ID
- ? Promise.resolve(null)
- : appApi.categoryDramas(activeCategory).then((payload) => normalizeCategoryItems(payload, locale.code))),
+ const categoryRequest = useRequest(
+ ({ signal }) => categoryApi.dramas(activeCategory, 1, 20, { signal }).then((payload) => normalizeCategoryItems(payload, locale.code)),
[activeCategory, locale.code],
+ { enabled: !isAllCategory },
);
- const homeItems = homeApi.data || [];
- const serverCategories = categoriesApi.data || [];
+
+ const homeItems = feedRequest.data || [];
+ const serverCategories = useMemo(() => categoriesRequest.data || [], [categoriesRequest.data]);
const fallbackCategories = useMemo(
() => locale.genres.slice(1).map((label, index) => ({ id: `${FALLBACK_GENRE_PREFIX}${index + 1}`, label })),
[locale.genres],
@@ -67,16 +74,17 @@ export function HomePage() {
const categories = useMemo(() => {
const allCategory = { id: ALL_CATEGORY_ID, label: locale.genres[0] };
if (serverCategories.length) return [allCategory, ...serverCategories];
- return categoriesApi.loading ? [allCategory] : [allCategory, ...fallbackCategories];
- }, [categoriesApi.loading, fallbackCategories, locale.genres, serverCategories]);
+ return categoriesRequest.loading ? [allCategory] : [allCategory, ...fallbackCategories];
+ }, [categoriesRequest.loading, fallbackCategories, locale.genres, serverCategories]);
+
const fallbackItems = useMemo(() => {
const genreIndex = fallbackGenreIndex(activeCategory);
return searchCatalog('', locale.genres[genreIndex], genreIndex);
}, [activeCategory, locale.genres]);
- const isAllCategory = activeCategory === ALL_CATEGORY_ID;
- const loading = isAllCategory ? homeApi.loading : categoryApi.loading;
- const error = isAllCategory ? homeApi.error : categoryApi.error;
- const remoteItems = isAllCategory ? homeItems : (categoryApi.data || []);
+
+ const loading = isAllCategory ? feedRequest.loading : categoryRequest.loading;
+ const error = isAllCategory ? feedRequest.error : categoryRequest.error;
+ const remoteItems = isAllCategory ? homeItems : (categoryRequest.data || []);
const items = error ? fallbackItems : remoteItems;
const shortsItems = (homeItems.length ? homeItems : videos).slice(0, SHORTS_LIMIT);
const firstShelf = items.slice(0, FIRST_SHELF_LIMIT);
@@ -84,30 +92,19 @@ export function HomePage() {
return (
<>
- {categoriesApi.loading && !serverCategories.length ?
: (
-
- {categories.map((category) => (
-
- ))}
-
+ {categoriesRequest.loading && !serverCategories.length ?
: (
+
)}
{error && {locale.apiFallback}
}
- {!loading && !error && !items.length && {locale.empty}
}
- {loading && !items.length ? : (
-
- {firstShelf.map((video) => )}
-
- )}
+ }
+ empty={!loading && !error && !items.length}
+ emptyMessage={locale.empty}
+ >
+
+
@@ -115,18 +112,18 @@ export function HomePage() {
▶{locale.shortTitle}
- {homeApi.loading && !homeItems.length ? : (
+ {feedRequest.loading && !homeItems.length ? : (
{shortsItems.map((video) => )}
)}
- {secondShelf.length > 0 &&
-
- {secondShelf.map((video) => )}
-
- }
+ {secondShelf.length > 0 && (
+
+ )}
>
);
}
diff --git a/src/features/home/components/CategoryBar.jsx b/src/features/home/components/CategoryBar.jsx
new file mode 100644
index 0000000..5f7708c
--- /dev/null
+++ b/src/features/home/components/CategoryBar.jsx
@@ -0,0 +1,19 @@
+// Author: fruit.com.cn
+export function CategoryBar({ categories, activeId, onChange }) {
+ return (
+
+ {categories.map((category) => (
+
+ ))}
+
+ );
+}
diff --git a/src/features/listing/ListingPage.jsx b/src/features/listing/ListingPage.jsx
new file mode 100644
index 0000000..f00e8f4
--- /dev/null
+++ b/src/features/listing/ListingPage.jsx
@@ -0,0 +1,32 @@
+// Author: fruit.com.cn
+import { useLocale } from '../../app/providers/AppProviders';
+import { SectionHeading } from '../../shared/components/SectionHeading';
+import { VideoGrid } from '../../shared/components/VideoGrid';
+import { VideoGridSkeleton } from '../../shared/components/skeletons';
+import { videos } from '../../shared/data/catalog';
+import { useRequest } from '../../shared/hooks/useRequest';
+import { normalizePage } from '../../shared/lib/pagination';
+import { normalizeDrama, uniqueDramas } from '../../entities/drama/model';
+import { listingApi } from './api';
+
+export function ListingPage({ type }) {
+ const { locale } = useLocale();
+ const title = type === 'shorts' ? locale.shortTitle : type === 'subscriptions' ? locale.channels : type === 'history' ? locale.history : locale.library;
+ const request = useRequest(
+ ({ signal }) => listingApi.dramas({}, { signal }).then((payload) => uniqueDramas(normalizePage(payload).list.map((item) => normalizeDrama(item, locale.code)))),
+ [locale.code],
+ );
+ const items = request.data?.length ? request.data : videos;
+
+ return (
+ <>
+
+ {request.loading ? : (
+ <>
+ {request.error && {locale.apiFallback}
}
+
+ >
+ )}
+ >
+ );
+}
diff --git a/src/features/listing/api.js b/src/features/listing/api.js
new file mode 100644
index 0000000..0718f3f
--- /dev/null
+++ b/src/features/listing/api.js
@@ -0,0 +1,13 @@
+// Author: fruit.com.cn
+import { http } from '../../shared/api';
+
+export const listingApi = {
+ dramas: (body = {}, options) => http.post('/app/dramas/page', {
+ pageIndex: 1,
+ pageSize: 20,
+ categoryIds: [],
+ classificationIds: [],
+ tagIds: [],
+ ...body,
+ }, options),
+};
diff --git a/src/features/search/SearchPage.jsx b/src/features/search/SearchPage.jsx
new file mode 100644
index 0000000..c3438de
--- /dev/null
+++ b/src/features/search/SearchPage.jsx
@@ -0,0 +1,49 @@
+// Author: fruit.com.cn
+import { Search as SearchIcon } from 'lucide-react';
+import { useSearchParams } from 'react-router-dom';
+import { useLocale } from '../../app/providers/AppProviders';
+import { SectionHeading } from '../../shared/components/SectionHeading';
+import { VideoGrid } from '../../shared/components/VideoGrid';
+import { VideoGridSkeleton } from '../../shared/components/skeletons';
+import { useRequest } from '../../shared/hooks/useRequest';
+import { normalizePage } from '../../shared/lib/pagination';
+import { dramaApi } from '../../entities/drama/api';
+import { normalizeDrama, uniqueDramas } from '../../entities/drama/model';
+import { searchApi } from './api';
+
+async function loadSearchResults(keyword, localeCode, signal) {
+ if (keyword) {
+ const payload = await searchApi.search(keyword, { signal });
+ return normalizePage(payload).list.map((item) => normalizeDrama(item, localeCode));
+ }
+ const payload = await dramaApi.homeFeed({ signal });
+ const items = payload?.sections?.flatMap((section) => section.dramas || []) || [];
+ return uniqueDramas(items.map((item) => normalizeDrama(item, localeCode)));
+}
+
+export function SearchPage() {
+ const { locale } = useLocale();
+ const [params] = useSearchParams();
+ const query = (params.get('q') || '').trim();
+ const request = useRequest(({ signal }) => loadSearchResults(query, locale.code, signal), [query, locale.code]);
+ const results = request.data || [];
+
+ return (
+ <>
+
+ {request.loading ? : (
+ <>
+ {request.error && {locale.apiFallback}
}
+ {!request.error && query && results.length === 0 ? (
+
+
+ {locale.noResults} “{query}”
+
+ ) : (
+
+ )}
+ >
+ )}
+ >
+ );
+}
diff --git a/src/features/search/api.js b/src/features/search/api.js
new file mode 100644
index 0000000..7a7d02f
--- /dev/null
+++ b/src/features/search/api.js
@@ -0,0 +1,7 @@
+// Author: fruit.com.cn
+import { http } from '../../shared/api';
+import { toQuery } from '../../shared/api/query';
+
+export const searchApi = {
+ search: (keyword, options) => http.get(`/app/search${toQuery({ keyword, pageIndex: 1, pageSize: 48 })}`, options),
+};
diff --git a/src/pages/ShortsFeedPage.jsx b/src/features/shorts/ShortsFeedPage.jsx
similarity index 55%
rename from src/pages/ShortsFeedPage.jsx
rename to src/features/shorts/ShortsFeedPage.jsx
index e7e0577..147636f 100644
--- a/src/pages/ShortsFeedPage.jsx
+++ b/src/features/shorts/ShortsFeedPage.jsx
@@ -1,92 +1,22 @@
// Author: fruit.com.cn
import { useCallback, useEffect, useRef, useState } from 'react';
-import { Bookmark, MessageCircle, Share2, ThumbsUp } from 'lucide-react';
import { useSearchParams } from 'react-router-dom';
-import { useAppStore } from '../app/store';
-import { appApi } from '../services/appApi';
-import { normalizeDrama, normalizePage, uniqueDramas } from '../services/normalizers';
-import { VideoPlayer } from '../components/player/VideoPlayer';
-import { CommentsSheet } from '../components/CommentsSheet';
-import { ShortsSkeleton } from '../components/Skeletons';
-import { formatCount } from '../lib/format';
-import { reportWatchProgress } from '../services/watchProgress';
-import { useFollowedChannels } from '../hooks/useFollowedChannels';
+import { useLocale, useUi } from '../../app/providers/AppProviders';
+import { CommentsSheet } from '../../shared/components/CommentsSheet';
+import { ShortsSkeleton } from '../../shared/components/skeletons';
+import { useFollowedChannels } from '../../shared/hooks/useFollowedChannels';
+import { useToast } from '../../shared/hooks/useToast';
+import { normalizePage } from '../../shared/lib/pagination';
+import { dramaApi } from '../../entities/drama/api';
+import { normalizeDrama, uniqueDramas } from '../../entities/drama/model';
+import { ShortSlide } from './components/ShortSlide';
const PAGE_SIZE = 6;
const PRELOAD_THRESHOLD = 2;
-function ShortSlide({ video, active, locale, followed, onToggleFollow, onOpenComments, onShare }) {
- const src = video.preview?.playUrl;
- const [liked, setLiked] = useState(false);
- const [likeCount, setLikeCount] = useState(video.likeCount);
- const [saved, setSaved] = useState(false);
- const [saveCount, setSaveCount] = useState(video.favoriteCount);
-
- const toggleLike = async () => {
- try {
- const result = await appApi.toggleLike(video.preview?.id ? 'EPISODE' : 'DRAMA', video.preview?.id || video.id);
- setLiked(Boolean(result?.liked));
- setLikeCount(Number(result?.count) || 0);
- } catch (error) {
- console.error('YMB LIKE FAILED', error);
- }
- };
-
- const toggleSave = async () => {
- try {
- const next = await appApi.toggleFavorite(video.id);
- setSaved(Boolean(next));
- setSaveCount((count) => Math.max(0, count + (next ? 1 : -1)));
- } catch (error) {
- console.error('YMB SAVE FAILED', error);
- }
- };
-
- return (
-
-
-
-
- {video.channel?.[0] || 'Y'}
- {video.channel}
-
-
-
{video.title}
-
-
-
-
- {src
- ?
reportWatchProgress(video.preview?.id, position, duration, 'shorts', flush)} />
- :
{video.title} }
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
export function ShortsFeedPage() {
- const { locale, sidebarOpen } = useAppStore();
+ const { locale } = useLocale();
+ const { sidebarOpen } = useUi();
const [params] = useSearchParams();
const startId = params.get('start');
const [items, setItems] = useState([]);
@@ -94,12 +24,11 @@ export function ShortsFeedPage() {
const [status, setStatus] = useState({ loading: true, error: '', hasMore: true });
const { channels: followedChannels, toggle: toggleFollow } = useFollowedChannels();
const [commentsVideo, setCommentsVideo] = useState(null);
- const [toast, setToast] = useState('');
+ const toast = useToast();
const feedRef = useRef(null);
const cursorRef = useRef(null);
const loadingRef = useRef(false);
const requestIdRef = useRef(0);
- const toastTimer = useRef(null);
const loadPage = useCallback(async (reset) => {
if (loadingRef.current && !reset) return;
@@ -108,7 +37,7 @@ export function ShortsFeedPage() {
loadingRef.current = true;
setStatus((prev) => ({ ...prev, loading: true, error: '' }));
try {
- const payload = await appApi.feed({ cursor: reset ? null : cursorRef.current, size: PAGE_SIZE });
+ const payload = await dramaApi.feed({ cursor: reset ? null : cursorRef.current, size: PAGE_SIZE });
if (requestId !== requestIdRef.current) return;
const list = uniqueDramas(normalizePage(payload).list.map((item) => normalizeDrama(item, locale.code)));
cursorRef.current = payload?.nextCursor ?? null;
@@ -158,10 +87,6 @@ export function ShortsFeedPage() {
if (index >= items.length - PRELOAD_THRESHOLD) loadPage(false);
}, [activeId, items, loadPage, status.hasMore, status.loading]);
- useEffect(() => () => {
- if (toastTimer.current) window.clearTimeout(toastTimer.current);
- }, []);
-
const activeIndex = Math.max(0, items.findIndex((item) => item.id === activeId));
const move = (delta) => {
const node = feedRef.current;
@@ -170,12 +95,6 @@ export function ShortsFeedPage() {
if (next !== activeIndex) node.scrollTo({ top: next * node.clientHeight, behavior: 'smooth' });
};
- const showToast = (message) => {
- setToast(message);
- if (toastTimer.current) window.clearTimeout(toastTimer.current);
- toastTimer.current = window.setTimeout(() => setToast(''), 2000);
- };
-
const shareVideo = async (video) => {
const url = `${window.location.origin}/watch/${video.id}`;
if (navigator.share) {
@@ -188,9 +107,9 @@ export function ShortsFeedPage() {
}
try {
await navigator.clipboard.writeText(url);
- showToast(locale.linkCopied);
+ toast.show(locale.linkCopied);
} catch {
- showToast(url);
+ toast.show(url);
}
};
@@ -201,6 +120,7 @@ export function ShortsFeedPage() {
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeIndex, items.length]);
return (
@@ -215,7 +135,6 @@ export function ShortsFeedPage() {
key={video.id}
video={video}
active={video.id === activeId}
- locale={locale}
followed={followedChannels.includes(video.channel)}
onToggleFollow={() => toggleFollow(video.channel)}
onOpenComments={() => setCommentsVideo(video)}
@@ -231,7 +150,7 @@ export function ShortsFeedPage() {
)}
{commentsVideo && setCommentsVideo(null)} />}
- {toast && {toast}
}
+ {toast.message && {toast.message}
}
>
);
}
diff --git a/src/features/shorts/components/ShortSlide.jsx b/src/features/shorts/components/ShortSlide.jsx
new file mode 100644
index 0000000..3f8dcc1
--- /dev/null
+++ b/src/features/shorts/components/ShortSlide.jsx
@@ -0,0 +1,62 @@
+// Author: fruit.com.cn
+import { Bookmark, MessageCircle, Share2, ThumbsUp } from 'lucide-react';
+import { useLocale } from '../../../app/providers/AppProviders';
+import { VideoPlayer } from '../../../shared/components/player/VideoPlayer';
+import { useEngagement } from '../../../shared/hooks/useEngagement';
+import { formatCount } from '../../../shared/lib/format';
+import { reportWatchProgress } from '../../../shared/lib/watchProgress';
+
+export function ShortSlide({ video, active, followed, onToggleFollow, onOpenComments, onShare }) {
+ const { locale } = useLocale();
+ const src = video.preview?.playUrl;
+ const likeTargetId = video.preview?.id || video.id;
+ const { liked, likeCount, saved, saveCount, toggleLike, toggleSave } = useEngagement({
+ likeTargetType: video.preview?.id ? 'EPISODE' : 'DRAMA',
+ likeTargetId,
+ dramaId: video.id,
+ initialLikeCount: video.likeCount,
+ initialSaveCount: video.favoriteCount,
+ });
+
+ return (
+
+
+
+
+ {video.channel?.[0] || 'Y'}
+ {video.channel}
+
+
+
{video.title}
+
+
+
+
+ {src
+ ?
reportWatchProgress(video.preview?.id, position, duration, 'shorts', flush)} />
+ :
{video.title} }
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/watch/WatchPage.jsx b/src/features/watch/WatchPage.jsx
new file mode 100644
index 0000000..579af58
--- /dev/null
+++ b/src/features/watch/WatchPage.jsx
@@ -0,0 +1,216 @@
+// Author: fruit.com.cn
+import { useEffect, useRef, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { Bookmark, MessageCircle, Share2, ThumbsUp } from 'lucide-react';
+import { useLocale } from '../../app/providers/AppProviders';
+import { CommentsSheet } from '../../shared/components/CommentsSheet';
+import { VideoPlayer } from '../../shared/components/player/VideoPlayer';
+import { WatchPageSkeleton } from '../../shared/components/skeletons';
+import { useEngagement } from '../../shared/hooks/useEngagement';
+import { useFollowedChannels } from '../../shared/hooks/useFollowedChannels';
+import { useRequest } from '../../shared/hooks/useRequest';
+import { useToast } from '../../shared/hooks/useToast';
+import { formatCount, formatViews } from '../../shared/lib/format';
+import { selectPlaybackUrl } from '../../shared/lib/media';
+import { reportWatchProgress } from '../../shared/lib/watchProgress';
+import { normalizeDrama } from '../../entities/drama/model';
+import { dramaApi } from '../../entities/drama/api';
+import { EpisodeRail, episodeLabel } from './components/EpisodeRail';
+
+function collectEpisodes(video) {
+ if (!video.episodes?.length) return video.preview ? [video.preview] : [];
+ const hasPreview = video.preview && !video.episodes.some((item) => item.id === video.preview.id);
+ return hasPreview ? [video.preview, ...video.episodes] : video.episodes;
+}
+
+function WatchContent({ video }) {
+ const { locale } = useLocale();
+ const episodes = collectEpisodes(video);
+ const [episodeId, setEpisodeId] = useState(video.preview?.id || episodes[0]?.id);
+ const episodeIndex = Math.max(0, episodes.findIndex((item) => item.id === episodeId));
+ const currentEpisode = episodes[episodeIndex] || episodes[0] || null;
+ const nextEpisode = episodes[episodeIndex + 1];
+ const previousEpisode = episodes[episodeIndex - 1];
+ const toast = useToast();
+
+ const playback = useRequest(
+ ({ signal }) => dramaApi.playback(currentEpisode.id, { signal }),
+ [currentEpisode?.id],
+ { enabled: Boolean(currentEpisode?.id) && !currentEpisode?.locked },
+ );
+ const stats = useRequest(
+ ({ signal }) => dramaApi.stats(currentEpisode.id, { signal }),
+ [currentEpisode?.id],
+ { enabled: Boolean(currentEpisode?.id) },
+ );
+ const engagement = useEngagement({
+ likeTargetType: 'EPISODE',
+ likeTargetId: currentEpisode?.id,
+ dramaId: video.id,
+ initialLikeCount: currentEpisode?.likeCount,
+ initialSaveCount: video.favoriteCount,
+ });
+ const { channels, toggle } = useFollowedChannels();
+ const followed = channels.includes(video.channel);
+ const [commentsOpen, setCommentsOpen] = useState(false);
+ const swipeStart = useRef(null);
+ const playerRef = useRef(null);
+ const wheelRef = useRef({ accum: 0, locked: false, lastAt: 0, lastAbs: 0 });
+
+ useEffect(() => {
+ engagement.syncStats(stats.data);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [stats.data]);
+
+ const changeEpisode = (episode, scroll = false) => {
+ if (!episode || episode.id === currentEpisode?.id) return;
+ setEpisodeId(episode.id);
+ if (scroll) playerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ };
+ const goNext = () => changeEpisode(nextEpisode);
+ const goPrevious = () => changeEpisode(previousEpisode);
+
+ useEffect(() => {
+ // 捕获阶段接管画面内的滚轮,任何幅度都不交给页面滚动
+ const onWheel = (event) => {
+ if (!event.deltaY) return;
+ // 用坐标判断是否在播放区内:切集后 video 节点会被替换,事件 target 可能失效
+ const rect = playerRef.current?.getBoundingClientRect();
+ const inside = rect
+ ? event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom
+ : Boolean(event.target?.closest?.('.watch-stage'));
+ if (!inside) return;
+ event.preventDefault();
+ const state = wheelRef.current;
+ const now = performance.now();
+ // 兼容按行滚动的设备(Firefox 等),统一成像素
+ const delta = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
+ const magnitude = Math.abs(delta);
+ // 新手势:停顿超过 60ms,或惯性衰减中突然重新加速(新的一次滑动)
+ if (now - state.lastAt > 60 || magnitude > state.lastAbs * 2.2 + 8) state.locked = false;
+ state.lastAt = now;
+ state.lastAbs = magnitude;
+ if (state.accum && Math.sign(delta) !== Math.sign(state.accum)) state.accum = delta;
+ else state.accum += delta;
+ if (state.locked || Math.abs(state.accum) < 24) return;
+ const forward = state.accum > 0;
+ if (forward ? !nextEpisode : !previousEpisode) return;
+ state.locked = true;
+ state.accum = 0;
+ if (forward) goNext(); else goPrevious();
+ };
+ window.addEventListener('wheel', onWheel, { passive: false, capture: true });
+ return () => window.removeEventListener('wheel', onWheel, { capture: true });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [nextEpisode, previousEpisode]);
+
+ if (!currentEpisode) return {locale.playbackUnavailable}
;
+
+ const src = currentEpisode.locked ? '' : selectPlaybackUrl(playback.data, currentEpisode.playUrl);
+ const views = formatViews(video.views, locale.code);
+ const tag = video.genre ? `#${video.genre.replace(/\s+/g, '')}` : '';
+ const commentCount = Number(stats.data?.commentCount ?? currentEpisode.commentCount) || 0;
+ const label = episodeLabel(currentEpisode, episodeIndex, locale.code);
+ const commentVideo = { ...video, preview: currentEpisode };
+
+ const onTouchStart = (event) => {
+ const touch = event.touches[0];
+ swipeStart.current = { x: touch.clientX, y: touch.clientY };
+ };
+
+ const onTouchEnd = (event) => {
+ const start = swipeStart.current;
+ swipeStart.current = null;
+ if (!start) return;
+ const touch = event.changedTouches[0];
+ const dx = start.x - touch.clientX;
+ const dy = start.y - touch.clientY;
+ if (Math.abs(dy) < 50 || Math.abs(dx) > Math.abs(dy)) return;
+ if (dy > 0) goNext(); else goPrevious();
+ };
+
+ const share = async () => {
+ const url = `${window.location.origin}/watch/${video.id}`;
+ if (navigator.share) {
+ try {
+ await navigator.share({ title: video.title, url });
+ return;
+ } catch (error) {
+ if (error?.name === 'AbortError') return;
+ }
+ }
+ try {
+ await navigator.clipboard.writeText(url);
+ toast.show(locale.linkCopied);
+ } catch {
+ toast.show(url);
+ }
+ };
+
+ return (
+
+
+
+
+ {src
+ ?
reportWatchProgress(currentEpisode.id, position, duration, 'watch', flush)} />
+ :
{currentEpisode.locked ? locale.lockedEpisode : playback.loading ? locale.loading : playback.error?.message || locale.playbackUnavailable} }
+ {locale.episodeSwipeHint}
+
+
+
+
+
+
+
+ {label}
+ {views}{tag ? ` · ${tag}` : ''}
+
+
{video.title}
+
+
+
{video.channel?.[0] || 'Y'}
+
{video.channel}
+
+
+
+
+
+
+
+
+
+ {video.description && (
+
+ )}
+
+
+
changeEpisode(episode, true)} />
+
+ {commentsOpen && setCommentsOpen(false)} />}
+ {toast.message && {toast.message}
}
+
+ );
+}
+
+export function WatchPage() {
+ const { id } = useParams();
+ const { locale } = useLocale();
+ const drama = useRequest(
+ ({ signal }) => dramaApi.get(id, { signal }).then((data) => normalizeDrama(data, locale.code)),
+ [id, locale.code],
+ );
+
+ useEffect(() => {
+ window.scrollTo({ top: 0 });
+ const timer = window.setTimeout(() => window.scrollTo({ top: 0 }), 0);
+ return () => window.clearTimeout(timer);
+ }, [id]);
+
+ if (!drama.data && drama.loading) return ;
+ if (!drama.data) return {drama.error?.message || locale.empty}
;
+
+ return ;
+}
diff --git a/src/features/watch/components/EpisodeRail.jsx b/src/features/watch/components/EpisodeRail.jsx
new file mode 100644
index 0000000..cfc8931
--- /dev/null
+++ b/src/features/watch/components/EpisodeRail.jsx
@@ -0,0 +1,43 @@
+// Author: fruit.com.cn
+import { Lock } from 'lucide-react';
+import { useLocale } from '../../../app/providers/AppProviders';
+
+export function episodeLabel(episode, index, code) {
+ const number = episode?.number ?? index + 1;
+ return code === 'zh' ? `第${number}集` : `Episode ${number}`;
+}
+
+export function EpisodeRail({ episodes, currentId, image, onSelect }) {
+ const { locale } = useLocale();
+ if (!episodes.length) return null;
+ const currentIndex = Math.max(0, episodes.findIndex((episode) => episode.id === currentId));
+
+ return (
+
+ );
+}
diff --git a/src/hooks/useApi.js b/src/hooks/useApi.js
deleted file mode 100644
index 9460a5f..0000000
--- a/src/hooks/useApi.js
+++ /dev/null
@@ -1,13 +0,0 @@
-// Author: fruit.com.cn
-import { useEffect, useState } from 'react';
-
-export function useApi(loader, deps = []) {
- const [state, setState] = useState({ data: null, loading: true, error: '' });
- useEffect(() => {
- let active = true;
- setState({ data: null, loading: true, error: '' });
- loader().then((data) => active && setState({ data, loading: false, error: '' })).catch((error) => active && setState({ data: null, loading: false, error: error.message || 'Request failed' }));
- return () => { active = false; };
- }, deps);
- return state;
-}
diff --git a/src/layouts/AppShell.jsx b/src/layouts/AppShell.jsx
index f7b19aa..6018997 100644
--- a/src/layouts/AppShell.jsx
+++ b/src/layouts/AppShell.jsx
@@ -1,133 +1,33 @@
// Author: fruit.com.cn
-import { Bell, Compass, History, Home, Info, ListVideo, Menu, Plus, Search, Upload, UserRound, X } from 'lucide-react';
-import { useEffect, useState, useRef } from 'react';
-import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
-import { useAppStore } from '../app/store';
-import { LanguageSwitcher } from '../components/LanguageSwitcher';
-import { UploadVideoDialog } from '../components/UploadVideoDialog';
+import { Suspense, useState } from 'react';
+import { Outlet, useLocation } from 'react-router-dom';
+import { useLocale, useUi } from '../app/providers/AppProviders';
+import { BottomNav } from './components/BottomNav';
+import { Sidebar } from './components/Sidebar';
+import { TopBar } from './components/TopBar';
+import { UploadVideoDialog } from './components/UploadVideoDialog';
export function AppShell() {
- const { locale, query, setQuery, sidebarOpen, setSidebarOpen } = useAppStore();
- const navigate = useNavigate();
+ const { locale } = useLocale();
+ const { sidebarOpen, setSidebarOpen } = useUi();
const location = useLocation();
- const [searchOpen, setSearchOpen] = useState(false);
- const [createOpen, setCreateOpen] = useState(false);
const [uploadOpen, setUploadOpen] = useState(false);
- const searchInputRef = useRef(null);
- const createRef = useRef(null);
- const primaryItems = [[Home, locale.home, '/'], [Compass, locale.shorts, '/shorts'], [ListVideo, locale.subscriptions, '/subscriptions']];
- const libraryItems = [[History, locale.history, '/history']];
- const aboutItems = [[Info, locale.about, '/about']];
const immersive = location.pathname === '/shorts';
- // 移动端为抽屉式侧边栏,点击任一菜单项后必须收起
- const closeSidebarOnNavigate = (event) => {
- if (!event.target.closest?.('a')) return;
- if (window.matchMedia('(max-width: 760px)').matches) setSidebarOpen(false);
- };
- useEffect(() => {
- if (!createOpen) return undefined;
- const closeMenu = (event) => {
- if (!createRef.current?.contains(event.target)) setCreateOpen(false);
- };
- document.addEventListener('mousedown', closeMenu);
- return () => document.removeEventListener('mousedown', closeMenu);
- }, [createOpen]);
- const submitSearch = (event) => {
- event.preventDefault();
- setSearchOpen(false);
- const keyword = query.trim();
- navigate(keyword ? `/search?q=${encodeURIComponent(keyword)}` : '/search');
- };
- const navClass = (to) => {
- const [path, search = ''] = to.split('?');
- if (location.pathname !== path) return '';
- const current = new URLSearchParams(location.search);
- return [...new URLSearchParams(search)].every(([key, value]) => current.get(key) === value) ? 'active' : '';
- };
return (
<>
-
-
-
-
- {!immersive && }
-
- {sidebarOpen && }
+ setSidebarOpen(!sidebarOpen)} onUpload={() => setUploadOpen(true)} />
+
+
+ }>
+
+
+ {!immersive && }
+
+
+ {sidebarOpen && (
+