From 056cd4970f2f0c0507303740e6eef02ab8cb321b Mon Sep 17 00:00:00 2001 From: oneao Date: Tue, 23 Jun 2026 21:29:18 +0800 Subject: [PATCH] 20260623212917 --- code/app/.workbuddy/memory/2026-06-23.md | 109 +++++++ .../skills/allapp-conventions/SKILL.md | 49 +++- .../skills/heroui-native/LICENSE.txt | 199 +++++++++++++ .../.workbuddy/skills/heroui-native/SKILL.md | 229 +++++++++++++++ .../scripts/get_component_docs.mjs | 157 +++++++++++ .../skills/heroui-native/scripts/get_docs.mjs | 154 ++++++++++ .../heroui-native/scripts/get_theme.mjs | 222 +++++++++++++++ .../heroui-native/scripts/list_components.mjs | 134 +++++++++ code/app/app-rn/package.json | 1 + code/app/app-rn/pnpm-lock.yaml | 51 ++++ .../src/app/{ => (main)}/(tabs)/_layout.tsx | 0 .../src/app/(main)/(tabs)/chat/index.tsx | 104 +++++++ .../src/app/(main)/(tabs)/home/index.tsx | 266 ++++++++++++++++++ .../src/app/{ => (main)}/(tabs)/my/index.tsx | 0 .../src/app/(main)/activities/index.tsx | 117 ++++++++ .../src/app/{ => (main)}/auth/login.tsx | 2 +- .../src/app/{ => (main)}/family/entry.tsx | 9 +- .../app/{ => (main)}/family/member/[id].tsx | 0 .../src/app/{ => (main)}/family/settings.tsx | 0 .../src/app/{ => (main)}/family/user.tsx | 29 ++ .../src/app/{ => (main)}/tools/index.tsx | 145 +++++----- code/app/app-rn/src/app/(tabs)/chat/index.tsx | 83 ------ code/app/app-rn/src/app/(tabs)/home/index.tsx | 207 -------------- code/app/app-rn/src/app/(tabs)/star/index.tsx | 13 - code/app/app-rn/src/app/auth/login.txt | 119 -------- code/app/app-rn/src/app/index.tsx | 2 +- code/app/app-rn/src/components/DateTime.tsx | 71 +++-- .../app-rn/src/components/DateTimeRange.tsx | 169 +++++------ code/app/app-rn/src/helpers/feed.ts | 86 ++++++ code/app/app-rn/src/layouts/PageLayout.tsx | 2 +- 30 files changed, 2093 insertions(+), 636 deletions(-) create mode 100644 code/app/.workbuddy/memory/2026-06-23.md create mode 100644 code/app/.workbuddy/skills/heroui-native/LICENSE.txt create mode 100644 code/app/.workbuddy/skills/heroui-native/SKILL.md create mode 100644 code/app/.workbuddy/skills/heroui-native/scripts/get_component_docs.mjs create mode 100644 code/app/.workbuddy/skills/heroui-native/scripts/get_docs.mjs create mode 100644 code/app/.workbuddy/skills/heroui-native/scripts/get_theme.mjs create mode 100644 code/app/.workbuddy/skills/heroui-native/scripts/list_components.mjs rename code/app/app-rn/src/app/{ => (main)}/(tabs)/_layout.tsx (100%) create mode 100644 code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx create mode 100644 code/app/app-rn/src/app/(main)/(tabs)/home/index.tsx rename code/app/app-rn/src/app/{ => (main)}/(tabs)/my/index.tsx (100%) create mode 100644 code/app/app-rn/src/app/(main)/activities/index.tsx rename code/app/app-rn/src/app/{ => (main)}/auth/login.tsx (99%) rename code/app/app-rn/src/app/{ => (main)}/family/entry.tsx (97%) rename code/app/app-rn/src/app/{ => (main)}/family/member/[id].tsx (100%) rename code/app/app-rn/src/app/{ => (main)}/family/settings.tsx (100%) rename code/app/app-rn/src/app/{ => (main)}/family/user.tsx (95%) rename code/app/app-rn/src/app/{ => (main)}/tools/index.tsx (58%) delete mode 100644 code/app/app-rn/src/app/(tabs)/chat/index.tsx delete mode 100644 code/app/app-rn/src/app/(tabs)/home/index.tsx delete mode 100644 code/app/app-rn/src/app/(tabs)/star/index.tsx delete mode 100644 code/app/app-rn/src/app/auth/login.txt create mode 100644 code/app/app-rn/src/helpers/feed.ts diff --git a/code/app/.workbuddy/memory/2026-06-23.md b/code/app/.workbuddy/memory/2026-06-23.md new file mode 100644 index 00000000..d9f291a0 --- /dev/null +++ b/code/app/.workbuddy/memory/2026-06-23.md @@ -0,0 +1,109 @@ +# 2026-06-23 工作日志 + +## Skills 配置 +- 将 `heroui-native` skill 从临时目录安装到用户级 (`~/.workbuddy/skills/`) 和项目级 (`项目/.workbuddy/skills/`) +- 项目中仅使用两个 skills:`heroui-native` 和 `allapp-conventions` +- heroui-native 来源:通过 `npx skills` 安装的 `heroui-inc/heroui` 仓库,原始路径在临时目录 `C:\Users\admin\AppData\Local\Temp\skills-oUbMIJ\` + +## 前端页面优化 + +### tools/index.tsx — 多轮迭代 +- 去掉卡片背景(`bg-white shadow-sm`),极简化 +- 图标保持原生色,不加彩色圆底 +- 收藏:用 `Heart`(非 `Star`)图标,18px 右上角独立触摸,teal 实心/灰色空心 +- 图标 26→32px,文字 11→13px,整体放大更利于触摸 + +### home/index.tsx — 重构 +- 宫格布局 → 横向功能卡片(图标圆底 + 标题 + 描述 + 右箭头) +- 计时数字层级化:天 `text-3xl` 主力,时分降级 +- 去掉卡片背景,与 tools 页风格统一 +- 更多工具独立卡片,用 `Grid3X3` 图标 + +### 项目目录结构重组 +- 主应用所有页面收进 `(main)/` route group:`(tabs)/`, `auth/`, `family/`, `tools/` +- 子应用 `finance/` 保持独立在顶层 +- 删除未用的 `star/` 占位页 +- 路由引用 `/(tabs)/home` → `/home`(4 处更新) +- 最终目录:`app/(main)/*` + `app/finance/*`,主副清晰分离 + +### 最近动态功能 — 完整实现 + +**数据库:** +- 建表 `b_feed`(动态),字段:id, family_id, user_id, type, content, meta(VARCHAR), create_time, update_time, create_by, update_by +- 插入 7 条模拟数据(加入家庭/修改昵称/记账/收藏工具) + +**前端新增:** +- `src/helpers/feed.ts` — `recordFeed()` 写入、`loadFeeds()` 查询,全程用通用接口 +- `src/app/(main)/activities/index.tsx` — 完整动态列表页,时间轴布局(图标圆底 + 竖线 + 内容) +- 首页 `/activities` 路由,CloseButton 关闭 + +**埋点位置(4 处):** +- `tools/index.tsx` — 收藏工具时 → `favorite_app` +- `family/entry.tsx` — 创建家庭 → `join_family`(xxx 创建了家庭) +- `family/entry.tsx` — 加入家庭 → `join_family`(xxx 加入了家庭) +- `family/user.tsx` — 修改昵称 → `change_nickname`(旧名 修改了昵称为「新名」) + +**首页改造:** +- 最近动态区从 Demo 数据 → 真实 `loadFeeds` 加载,展示最新 3 条 +- 右上角「更多 →」入口跳转 `/activities` +- 时间格式化:刚刚 / N分钟前 / N小时前 / N天前 / 日期 + +### 动态功能优化 — 表名 + 昵称修复 + +**表名重命名:** +- `b_feed` → `b_family_feed`(统一 `b_family_*` 风格) +- `feed.ts` 中 `table_name` / `view_name` 同步更新 + +**昵称来源修正:** +- 新增 `getFamilyMemberNickname(familyId, userId)` — 从 `b_family_member.nickname` 查询 +- 所有 `recordFeed()` 调用处改用此函数,不再从 `userStore.userInfo.nickname` 取 +- `family/user.tsx` 改名时同步更新 `b_family_member.nickname`(之前只更新 `b_user.name`) + +**Skill 更新:** +- `allapp-conventions` 新增「家庭动态记录规范」章节,含 5 种触发场景、昵称来源规则、代码示例 +- 目录结构修正为 `(main)/` route group + +### 动态 type 简化 + content 占位符化 + +**type 简码:** +- `join_family` → `join`,`change_nickname` → `rename`,`add_finance` → `bill`,`favorite_app` → `fav` +- 数据库存量数据已批量 UPDATE,前端 `TYPE_ICONS` 全部更新 + +**content 占位符:** +- content 存储模板 `{nickname} 加入了家庭`,不再拼接具体昵称 +- `loadFeeds()` 内部通过 `getFamilyMemberNickname()` 动态替换 `{nickname}` 为 `b_family_member.nickname` 最新值 +- 改名后历史动态自动显示新昵称,彻底解决写死问题 +- `recordFeed()` 调用处不再需要 `getFamilyMemberNickname`,只需传模板字符串 +- `entry.tsx` / `user.tsx` / `tools/index.tsx` 的 `getFamilyMemberNickname` import 已移除 + +### 动态 meta 字段清理 + +**原则变更**:content 自包含,不依赖 meta。`recordFeed()` 不再接收 meta 参数,所有展示信息直接写入 content。 + +**改动**: +- `helpers/feed.ts` — `recordFeed` 签名去掉 `meta?` 参数,inserts 不再写 meta 字段 +- `tools/index.tsx` — fav 调用去掉 `{ app_name: subItem.title }` 冗余 meta +- `family/user.tsx` — rename content 改为 `{nickname} 修改了昵称为「{新昵称}」` +- `activities/index.tsx` — 去掉 meta JSON.parse + amount 显示逻辑 +- 数据库:7 条存量记录 meta 全部置 NULL,bill 记录金额已嵌入 content(如「午餐」¥35) +- Skill 更新:content 模板表同步,新增「content 自包含原则」说明 + +### tools 卡片样式 +- item 加 `bg-white rounded-xl shadow-sm`,去底部分割线 +- 左右间距 `mx-3`;标题 + 描述双行布局,`items-center` 垂直居中 + +### DateTime/DateTimeRange 组件重构 +- **替换依赖**:`@yz1311/react-native-wheel-picker` → `react-native-dates-picker`(纯 JS、样式可控) +- `DateTime.tsx` — `mode="single"` + `initialView` 映射(year/month/day)+ `timePicker` +- `DateTimeRange.tsx` — `mode="range"` 原生支持起止日期选择,代码大幅简化(去掉 active/toggle 逻辑) +- 选中色 `selectedItemColor="#0d9488"` 与 teal 主题统一 +- BottomSheet 包裹保留 +- `chat/index.tsx` 写入完整测试代码(5 种 DateTime 模式 + DateTimeRange) + +### 记账页面重构 +- 页面分为上下两部分:上 — 大字号金额(40px bold,点击可输入)+ 日期行(文字 + ChevronDown 图标);下 — 类别网格(一行 5 个,flex-wrap) +- 金额使用 `TextInput` 内联,点击切换显示/输入态,`inputMode="decimal"` +- 日期不包在 Input 里,改为 `bg-gray-50 rounded-xl` 行 + 右侧 `ChevronDown` +- 类别区域顶部加本地搜索框(`useMemo` 过滤 `name.includes`),无 API +- 去掉横向滚动的 `LegendList`,改为 `flex-row flex-wrap` 的 `View` +- BottomSheet "更多"面板保留,同样含搜索框 + 5 列网格 diff --git a/code/app/.workbuddy/skills/allapp-conventions/SKILL.md b/code/app/.workbuddy/skills/allapp-conventions/SKILL.md index 118a213f..d2c39e1f 100644 --- a/code/app/.workbuddy/skills/allapp-conventions/SKILL.md +++ b/code/app/.workbuddy/skills/allapp-conventions/SKILL.md @@ -44,10 +44,13 @@ app-rn/ # React Native 前端 ├── src/ │ ├── app/ # Expo Router 页面 │ │ ├── _layout.tsx # 根布局 -│ │ ├── (tabs)/ # 主 Tab(home / space / tools / chat / mine) -│ │ ├── auth/ # 登录页 -│ │ ├── tools/ # 工具广场 -│ │ └── finance/ # 记账子模块 +│ │ ├── (main)/ # 主应用(route group,不影响 URL) +│ │ │ ├── (tabs)/ # 主 Tab(home / chat / my) +│ │ │ ├── auth/ # 登录页 +│ │ │ ├── family/ # 家庭管理 +│ │ │ ├── tools/ # 工具广场 +│ │ │ └── activities/ # 动态列表 +│ │ └── finance/ # 记账子模块(独立于 main) │ ├── components/ # 通用组件(layout/view / AZIndex 等) │ ├── configs/ # pages.ts(子应用列表配置) │ ├── layouts/ # 布局(ModuleLayout 等) @@ -188,11 +191,47 @@ b_finance - 记账记录 b_finance_category - 记账分类 b_finance_category_default - 默认分类模板 b_app_favorite - 子应用收藏 +b_family_feed - 家庭动态 ``` 视图:`v_family_member`(join b_family_member + b_user) -## 常见操作示例 +## 家庭动态(b_family_feed)记录规范 + +以下场景**必须**调用 `recordFeed()` 写入动态。 + +**type 简码对照**:`join`(加入/创建)、`rename`(改名)、`bill`(记账)、`fav`(收藏) + +| 触发场景 | type | content 模板 | 触发位置 | +|---------|------|-------------|---------| +| 创建家庭 | `join` | `{nickname} 创建了家庭「{名称}」` | `family/entry.tsx` handleCreate | +| 加入家庭 | `join` | `{nickname} 加入了家庭` | `family/entry.tsx` handleJoin | +| 修改昵称 | `rename` | `{nickname} 修改了昵称为「{新昵称}」` | `family/user.tsx` handleRename | +| 记账 | `bill` | `{nickname} 记了一笔「{备注}」¥{金额}` | `finance/add/` 保存成功 | +| 收藏应用 | `fav` | `{nickname} 收藏了「{应用名}」` | `tools/index.tsx` 收藏操作 | + +**⚠️ content 自包含原则**:所有展示信息直接写在 content 字符串中,不依赖 meta 字段。`recordFeed()` 不接收 meta 参数,`loadFeeds()` 只做 `{nickname}` 替换。meta 列保留但始终为 NULL。 + +**content 占位符规则**:content 存储模板字符串,用户昵称用 `{nickname}` 占位,**不在写入时拼接**。`loadFeeds()` 会自动将 `{nickname}` 替换为 `b_family_member.nickname` 的最新值,确保改名后历史动态也显示最新昵称。 + +```ts +import { recordFeed } from "@/helpers/feed"; + +// 正确写法 — 用 {nickname} 占位符,不拼接昵称 +recordFeed(familyId, userId, "join", "{nickname} 加入了家庭"); + +// 错误写法 — 禁止拼接昵称写死 +recordFeed(familyId, userId, "join", `${nick} 加入了家庭`); // ❌ +``` + +**修改昵称时同步更新**:`family/user.tsx` 的 `handleRename` 除了更新 `b_user.name`,还必须同步更新 `b_family_member.nickname`: + +```ts +saveDataApi([ + { table_name: "b_user", updates: [{ id: uid, name: newName }] }, + { table_name: "b_family_member", updates: [{ id: memberId, nickname: newName }] }, +]); +``` ### 前端:拉取数据 ```ts diff --git a/code/app/.workbuddy/skills/heroui-native/LICENSE.txt b/code/app/.workbuddy/skills/heroui-native/LICENSE.txt new file mode 100644 index 00000000..bf4dfe2e --- /dev/null +++ b/code/app/.workbuddy/skills/heroui-native/LICENSE.txt @@ -0,0 +1,199 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please also get an approval + from the project maintainers before using the Apache License. + + Copyright 2025 NextUI Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/code/app/.workbuddy/skills/heroui-native/SKILL.md b/code/app/.workbuddy/skills/heroui-native/SKILL.md new file mode 100644 index 00000000..33ace072 --- /dev/null +++ b/code/app/.workbuddy/skills/heroui-native/SKILL.md @@ -0,0 +1,229 @@ +--- +name: heroui-native +description: "HeroUI Native component library for React Native (Tailwind v4 via Uniwind). Use when building mobile UIs with HeroUI Native — creating Buttons, Cards, TextFields, Dialogs; installing heroui-native; configuring dark/light themes; or fetching component docs. Keywords: HeroUI Native, heroui-native, React Native UI, Uniwind, mobile components." +metadata: + author: heroui + version: "2.0.1" +--- + +# HeroUI Native Development Guide + +HeroUI Native is a component library built on **Uniwind (Tailwind CSS for React Native)** and **React Native**, providing accessible, customizable UI components for mobile applications. + +--- + +## Installation + +```bash +curl -fsSL https://heroui.com/install | bash -s heroui-native +``` + +--- + +## CRITICAL: Native Only - Do Not Use Web Patterns + +**This guide is for HeroUI Native ONLY.** Do NOT apply HeroUI React (web) patterns — the package, styling engine, and color format all differ: + +| Feature | React (Web) | Native (Mobile) | +| ------------ | -------------------- | ----------------------------------- | +| **Styling** | Tailwind CSS v4 | Uniwind (Tailwind for React Native) | +| **Colors** | oklch format | HSL format | +| **Package** | `@heroui/react` | `heroui-native` | +| **Platform** | Web browsers | iOS & Android | + +```tsx +// CORRECT — Native pattern +import { Button } from "heroui-native"; + +; +``` + +**Always fetch Native docs before implementing.** + +--- + +## Core Principles + +- Semantic variants (`primary`, `secondary`, `tertiary`) over visual descriptions +- Composition over configuration (compound components) +- Theme variables with HSL color format +- React Native StyleSheet patterns with Uniwind utilities + +--- + +## Accessing Documentation & Component Information + +**For component details, examples, props, and implementation patterns, always fetch documentation:** + +### Using Scripts + +```bash +# List all available components +node scripts/list_components.mjs + +# Get component documentation (MDX) +node scripts/get_component_docs.mjs Button +node scripts/get_component_docs.mjs Button Card TextField + +# Get theme variables +node scripts/get_theme.mjs + +# Get non-component docs (guides, releases) +node scripts/get_docs.mjs /docs/native/getting-started/theming +``` + +### Direct MDX URLs + +Component docs: `https://heroui.com/docs/native/components/{component-name}.mdx` + +Examples: + +- Button: `https://heroui.com/docs/native/components/button.mdx` +- Dialog: `https://heroui.com/docs/native/components/dialog.mdx` +- TextField: `https://heroui.com/docs/native/components/text-field.mdx` + +Getting started guides: `https://heroui.com/docs/native/getting-started/{topic}.mdx` + +**Important:** Always fetch component docs before implementing. The MDX docs include complete examples, props, anatomy, and API references. + +--- + +## Installation Essentials + +### Quick Install + +```bash +npm i heroui-native react-native-reanimated react-native-gesture-handler react-native-safe-area-context @gorhom/bottom-sheet react-native-svg react-native-worklets tailwind-merge tailwind-variants +``` + +### Framework Setup (Expo - Recommended) + +1. **Install dependencies:** + +```bash +npx create-expo-app MyApp +cd MyApp +npm i heroui-native uniwind tailwindcss +npm i react-native-reanimated react-native-gesture-handler react-native-safe-area-context @gorhom/bottom-sheet react-native-svg react-native-worklets tailwind-merge tailwind-variants +``` + +2. **Create `global.css`:** + +```css +@import "tailwindcss"; +@import "uniwind"; +@import "heroui-native/styles"; + +@source "./node_modules/heroui-native/lib"; +``` + +3. **Wrap app with providers:** + +```tsx +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { HeroUINativeProvider } from "heroui-native"; +import "./global.css"; + +export default function Layout() { + return ( + + + + + + ); +} +``` + +### Critical Setup Requirements + +1. **Uniwind is Required** - HeroUI Native uses Uniwind (Tailwind CSS for React Native) +2. **HeroUINativeProvider Required** - Wrap your app with `HeroUINativeProvider` +3. **GestureHandlerRootView Required** - Wrap with `GestureHandlerRootView` from react-native-gesture-handler +4. **Use Compound Components** - Components use compound structure (e.g., `Card.Header`, `Card.Body`) +5. **Use onPress, not onClick** - React Native uses `onPress` event handlers +6. **Platform-Specific Code** - Use `Platform.OS` for iOS/Android differences + +--- + +## Component Patterns + +HeroUI Native uses **compound component patterns**. Each component has subcomponents accessed via dot notation. + +**Example - Card:** + +```tsx + + {/* Icons, badges */} + + Title + Description + + {/* Actions */} + +``` + +**Key Points:** + +- Always use compound structure - don't flatten to props +- Subcomponents are accessed via dot notation (e.g., `Card.Header`) +- Native Card uses `Card.Body` (not `Card.Content`); Title and Description go inside Body +- **Fetch component docs for complete anatomy and examples** + +--- + +## Semantic Variants + +HeroUI uses semantic naming to communicate functional intent: + +| Variant | Purpose | Usage | +| ------------- | --------------------------------- | -------------- | +| `primary` | Main action to move forward | 1 per context | +| `secondary` | Alternative actions | Multiple | +| `tertiary` | Dismissive actions (cancel, skip) | Sparingly | +| `danger` | Destructive actions | When needed | +| `danger-soft` | Soft destructive actions | Less prominent | +| `ghost` | Low-emphasis actions | Minimal weight | +| `outline` | Secondary actions | Bordered style | + +**Don't use raw colors** - semantic variants adapt to themes and accessibility. + +--- + +## Theming + +HeroUI Native uses CSS variables via Tailwind/Uniwind for theming. Theme colors are defined in `global.css`: + +```css +@theme { + --color-accent: hsl(260, 100%, 70%); + --color-accent-foreground: hsl(0, 0%, 100%); +} +``` + +**Get current theme variables:** + +```bash +node scripts/get_theme.mjs +``` + +**Access theme colors programmatically:** + +```tsx +import { useThemeColor } from "heroui-native"; + +const accentColor = useThemeColor("accent"); +``` + +**Theme switching (Light/Dark Mode):** + +```tsx +import { Uniwind, useUniwind } from "uniwind"; + +const { theme } = useUniwind(); +Uniwind.setTheme(theme === "light" ? "dark" : "light"); +``` + +For detailed theming, fetch: `https://heroui.com/docs/native/getting-started/theming.mdx` diff --git a/code/app/.workbuddy/skills/heroui-native/scripts/get_component_docs.mjs b/code/app/.workbuddy/skills/heroui-native/scripts/get_component_docs.mjs new file mode 100644 index 00000000..1cec3258 --- /dev/null +++ b/code/app/.workbuddy/skills/heroui-native/scripts/get_component_docs.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env node +/** + * Get complete component documentation (MDX) for HeroUI Native components. + * + * Usage: + * node get_component_docs.mjs Button + * node get_component_docs.mjs Button Card TextField + * + * Output: + * MDX documentation including imports, usage, variants, props, examples + */ + +const API_BASE = process.env.HEROUI_NATIVE_API_BASE || "https://native-mcp-api.heroui.com"; +const FALLBACK_BASE = "https://heroui.com"; +const APP_PARAM = "app=native-skills"; + +/** + * Convert PascalCase to kebab-case. + */ +function toKebabCase(name) { + return name + .replace(/([a-z])([A-Z])/g, "$1-$2") + .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2") + .toLowerCase(); +} + +/** + * Fetch data from HeroUI Native API with app parameter for analytics. + */ +async function fetchApi(endpoint, method = "GET", body = null) { + const separator = endpoint.includes("?") ? "&" : "?"; + const url = `${API_BASE}${endpoint}${separator}${APP_PARAM}`; + + try { + const options = { + headers: { + "Content-Type": "application/json", + "User-Agent": "HeroUI-Native-Skill/1.0", + }, + method, + signal: AbortSignal.timeout(30000), + }; + + if (body) { + options.body = JSON.stringify(body); + } + + const response = await fetch(url, options); + + if (!response.ok) { + return null; + } + + return await response.json(); + } catch { + return null; + } +} + +/** + * Fetch MDX directly from v3.heroui.com as fallback. + */ +async function fetchFallback(component) { + const kebabName = toKebabCase(component); + const url = `${FALLBACK_BASE}/docs/native/components/${kebabName}.mdx`; + + try { + const response = await fetch(url, { + headers: {"User-Agent": "HeroUI-Native-Skill/1.0"}, + signal: AbortSignal.timeout(30000), + }); + + if (!response.ok) { + return {component, error: `Failed to fetch docs for ${component}`}; + } + + const content = await response.text(); + + return { + component, + content, + contentType: "mdx", + source: "fallback", + url, + }; + } catch { + return {component, error: `Failed to fetch docs for ${component}`}; + } +} + +/** + * Main function to get component documentation. + */ +async function main() { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.error("Usage: node get_component_docs.mjs [Component2] ..."); + console.error("Example: node get_component_docs.mjs Button Card"); + process.exit(1); + } + + const components = args; + + // Try API first - use POST /v1/components/docs for batch requests + console.error(`# Fetching Native docs for: ${components.join(", ")}...`); + const data = await fetchApi("/v1/components/docs", "POST", {components}); + + if (data && data.results) { + // Output results + if (data.results.length === 1) { + // Single component - output content directly for easier reading + const result = data.results[0]; + + if (result.content) { + console.log(result.content); + } else if (result.error) { + console.error(`# Error for ${result.component}: ${result.error}`); + console.log(JSON.stringify(result, null, 2)); + } else { + console.log(JSON.stringify(result, null, 2)); + } + } else { + // Multiple components - output as JSON array + console.log(JSON.stringify(data, null, 2)); + } + + return; + } + + // Fallback to individual component fetches + console.error("# API failed, using fallback..."); + const results = []; + + for (const component of components) { + const result = await fetchFallback(component); + + results.push(result); + } + + // Output results + if (results.length === 1) { + // Single component - output content directly for easier reading + const result = results[0]; + + if (result.content) { + console.log(result.content); + } else { + console.log(JSON.stringify(result, null, 2)); + } + } else { + // Multiple components - output as JSON array + console.log(JSON.stringify(results, null, 2)); + } +} + +main(); diff --git a/code/app/.workbuddy/skills/heroui-native/scripts/get_docs.mjs b/code/app/.workbuddy/skills/heroui-native/scripts/get_docs.mjs new file mode 100644 index 00000000..cf3a47ed --- /dev/null +++ b/code/app/.workbuddy/skills/heroui-native/scripts/get_docs.mjs @@ -0,0 +1,154 @@ +#!/usr/bin/env node +/** + * Get non-component HeroUI Native documentation (guides, theming, releases). + * + * Usage: + * node get_docs.mjs /docs/native/getting-started/theming + * node get_docs.mjs /docs/native/releases/beta-12 + * + * Output: + * MDX documentation content + * + * Note: For component docs, use get_component_docs.mjs instead. + */ + +const API_BASE = process.env.HEROUI_NATIVE_API_BASE || "https://native-mcp-api.heroui.com"; +const FALLBACK_BASE = "https://heroui.com"; +const APP_PARAM = "app=native-skills"; + +/** + * Fetch documentation from HeroUI Native API. + * Uses v1 endpoint pattern: /v1/docs/:path + */ +async function fetchApi(path) { + // The v1 API expects path without /docs/ prefix + // Input: /docs/native/getting-started/theming + // API expects: native/getting-started/theming (route is /v1/docs/:path(*)) + let apiPath = path.startsWith("/docs/") + ? path.slice(6) // Remove /docs/ prefix + : path.startsWith("/") + ? path.slice(1) // Remove leading / + : path; + + const separator = "?"; + const url = `${API_BASE}/v1/docs/${apiPath}${separator}${APP_PARAM}`; + + try { + const response = await fetch(url, { + headers: {"User-Agent": "HeroUI-Native-Skill/1.0"}, + signal: AbortSignal.timeout(30000), + }); + + if (!response.ok) { + console.error(`# API Error: HTTP ${response.status}`); + + return null; + } + + return await response.json(); + } catch (error) { + console.error(`# API Error: ${error.message}`); + + return null; + } +} + +/** + * Fetch MDX directly from v3.heroui.com as fallback. + */ +async function fetchFallback(path) { + // Ensure path starts with /docs and ends with .mdx + let cleanPath = path.replace(/^\//, ""); + + if (!cleanPath.endsWith(".mdx")) { + cleanPath = `${cleanPath}.mdx`; + } + + const url = `${FALLBACK_BASE}/${cleanPath}`; + + try { + const response = await fetch(url, { + headers: {"User-Agent": "HeroUI-Native-Skill/1.0"}, + signal: AbortSignal.timeout(30000), + }); + + if (!response.ok) { + return {error: `HTTP ${response.status}: ${response.statusText}`, path}; + } + + const content = await response.text(); + + return { + content, + contentType: "mdx", + path, + source: "fallback", + url, + }; + } catch (error) { + return {error: `Fetch Error: ${error.message}`, path}; + } +} + +/** + * Main function to get documentation for specified path. + */ +async function main() { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.error("Usage: node get_docs.mjs "); + console.error("Example: node get_docs.mjs /docs/native/getting-started/theming"); + console.error(); + console.error("Available paths include:"); + console.error(" /docs/native/getting-started/theming"); + console.error(" /docs/native/getting-started/colors"); + console.error(" /docs/native/getting-started/styling"); + console.error(" /docs/native/releases/beta-12"); + console.error(); + console.error("Note: For component docs, use get_component_docs.mjs instead."); + process.exit(1); + } + + const path = args[0]; + + // Check if user is trying to get component docs + if (path.includes("/components/")) { + console.error("# Warning: Use get_component_docs.mjs for component documentation."); + const componentName = path.split("/").pop().replace(".mdx", ""); + const titleCase = componentName.charAt(0).toUpperCase() + componentName.slice(1); + + console.error(`# Example: node get_component_docs.mjs ${titleCase}`); + } + + // Validate Native path + if (!path.startsWith("/docs/native/")) { + console.error("# Warning: Native documentation paths should start with /docs/native/"); + console.error(`# Provided path: ${path}`); + } + + console.error(`# Fetching Native documentation for ${path}...`); + + // Try API first + const data = await fetchApi(path); + + if (data && data.content) { + data.source = "api"; + console.log(data.content); + + return; + } + + // Fallback to direct fetch + console.error("# API failed, using fallback..."); + const fallbackData = await fetchFallback(path); + + if (fallbackData.content) { + console.log(fallbackData.content); + } else { + console.log(JSON.stringify(fallbackData, null, 2)); + process.exit(1); + } +} + +main(); diff --git a/code/app/.workbuddy/skills/heroui-native/scripts/get_theme.mjs b/code/app/.workbuddy/skills/heroui-native/scripts/get_theme.mjs new file mode 100644 index 00000000..88faf950 --- /dev/null +++ b/code/app/.workbuddy/skills/heroui-native/scripts/get_theme.mjs @@ -0,0 +1,222 @@ +#!/usr/bin/env node +/** + * Get theme variables and design tokens for HeroUI Native. + * + * Usage: + * node get_theme.mjs + * + * Output: + * Theme variables organized by light/dark with HSL color format + */ + +const API_BASE = process.env.HEROUI_NATIVE_API_BASE || "https://native-mcp-api.heroui.com"; +const APP_PARAM = "app=native-skills"; + +// Fallback theme reference when API is unavailable +const FALLBACK_THEME = { + borderRadius: { + full: 9999, + lg: 12, + md: 8, + sm: 6, + }, + dark: { + colors: [ + { + category: "base", + name: "--color-background", + value: "hsl(0, 0%, 14.5%)", + }, + { + category: "semantic", + name: "--color-foreground", + value: "hsl(0, 0%, 98.4%)", + }, + { + category: "semantic", + name: "--color-accent", + value: "hsl(264.1, 100%, 55.1%)", + }, + { + category: "status", + name: "--color-danger", + value: "hsl(25.3, 100%, 63.7%)", + }, + { + category: "status", + name: "--color-success", + value: "hsl(163.2, 100%, 76.5%)", + }, + { + category: "status", + name: "--color-warning", + value: "hsl(86.0, 100%, 79.5%)", + }, + ], + }, + latestVersion: "beta", + light: { + colors: [ + { + category: "base", + name: "--color-background", + value: "hsl(0, 0%, 100%)", + }, + { + category: "semantic", + name: "--color-foreground", + value: "hsl(285.89, 5.9%, 21.03%)", + }, + { + category: "semantic", + name: "--color-accent", + value: "hsl(253.83, 100%, 62.04%)", + }, + { + category: "status", + name: "--color-danger", + value: "hsl(25.74, 100%, 65.32%)", + }, + { + category: "status", + name: "--color-success", + value: "hsl(150.81, 100%, 73.29%)", + }, + { + category: "status", + name: "--color-warning", + value: "hsl(72.33, 100%, 78.19%)", + }, + ], + }, + note: "This is a fallback. For complete theme variables, ensure the API is accessible.", + opacity: { + disabled: 0.4, + hover: 0.8, + pressed: 0.6, + }, + source: "fallback", + theme: "default", +}; + +/** + * Fetch data from HeroUI Native API with app parameter for analytics. + */ +async function fetchApi(endpoint) { + const separator = endpoint.includes("?") ? "&" : "?"; + const url = `${API_BASE}${endpoint}${separator}${APP_PARAM}`; + + try { + const response = await fetch(url, { + headers: {"User-Agent": "HeroUI-Native-Skill/1.0"}, + signal: AbortSignal.timeout(30000), + }); + + if (!response.ok) { + console.error(`# API Error: HTTP ${response.status}`); + + return null; + } + + return await response.json(); + } catch (error) { + console.error(`# API Error: ${error.message}`); + + return null; + } +} + +/** + * Format colors grouped by category. + */ +function formatColors(colors) { + const grouped = {}; + + for (const color of colors) { + const category = color.category || "semantic"; + + if (!grouped[category]) { + grouped[category] = []; + } + grouped[category].push(color); + } + + const lines = []; + + for (const [category, tokens] of Object.entries(grouped)) { + lines.push(` /* ${category.charAt(0).toUpperCase() + category.slice(1)} Colors */`); + for (const token of tokens) { + const name = token.name || ""; + const value = token.value || ""; + + lines.push(` ${name}: ${value};`); + } + lines.push(""); + } + + return lines.join("\n"); +} + +/** + * Main function to get theme variables. + */ +async function main() { + console.error("# Fetching Native theme variables..."); + + const rawData = await fetchApi("/v1/themes/variables?theme=default"); + + let data; + let version; + + if (!rawData) { + console.error("# API failed, using fallback theme reference..."); + data = FALLBACK_THEME; + version = FALLBACK_THEME.latestVersion || "unknown"; + } else { + // Handle API response format + data = rawData; + version = rawData.latestVersion || "unknown"; + } + + // Output as formatted structure for readability + console.log("/* HeroUI Native Theme Variables */"); + console.log(`/* Theme: ${data.theme || "default"} */`); + console.log(`/* Version: ${version} */`); + console.log(); + + // Light mode colors + if (data.light && data.light.colors) { + console.log("/* Light Mode Colors */"); + console.log(formatColors(data.light.colors)); + } + + // Dark mode colors + if (data.dark && data.dark.colors) { + console.log("/* Dark Mode Colors */"); + console.log(formatColors(data.dark.colors)); + } + + // Border radius + if (data.borderRadius) { + console.log("/* Border Radius */"); + for (const [key, value] of Object.entries(data.borderRadius)) { + console.log(` --radius-${key}: ${value};`); + } + console.log(); + } + + // Opacity + if (data.opacity) { + console.log("/* Opacity */"); + for (const [key, value] of Object.entries(data.opacity)) { + console.log(` --opacity-${key}: ${value};`); + } + console.log(); + } + + // Also output raw JSON to stderr for programmatic use + console.error("\n# Raw JSON output:"); + console.error(JSON.stringify(rawData || data, null, 2)); +} + +main(); diff --git a/code/app/.workbuddy/skills/heroui-native/scripts/list_components.mjs b/code/app/.workbuddy/skills/heroui-native/scripts/list_components.mjs new file mode 100644 index 00000000..5210eb41 --- /dev/null +++ b/code/app/.workbuddy/skills/heroui-native/scripts/list_components.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node +/** + * List all available HeroUI Native components. + * + * Usage: + * node list_components.mjs + * + * Output: + * JSON with components array, latestVersion, and count + */ + +const API_BASE = process.env.HEROUI_NATIVE_API_BASE || "https://native-mcp-api.heroui.com"; +const APP_PARAM = "app=native-skills"; +const LLMS_TXT_URL = "https://heroui.com/native/llms.txt"; + +/** + * Fetch data from HeroUI Native API with app parameter for analytics. + */ +async function fetchApi(endpoint) { + const separator = endpoint.includes("?") ? "&" : "?"; + const url = `${API_BASE}${endpoint}${separator}${APP_PARAM}`; + + try { + const response = await fetch(url, { + headers: {"User-Agent": "HeroUI-Native-Skill/1.0"}, + signal: AbortSignal.timeout(30000), + }); + + if (!response.ok) { + console.error(`HTTP Error ${response.status}: ${response.statusText}`); + + return null; + } + + return await response.json(); + } catch (error) { + console.error(`API Error: ${error.message}`); + + return null; + } +} + +/** + * Fetch component list from llms.txt fallback URL. + */ +async function fetchFallback() { + try { + const response = await fetch(LLMS_TXT_URL, { + headers: {"User-Agent": "HeroUI-Native-Skill/1.0"}, + signal: AbortSignal.timeout(30000), + }); + + if (!response.ok) { + return null; + } + + const content = await response.text(); + + // Parse markdown to extract component names from pattern: - [ComponentName](url) + // Look for links under the Components section (### Components) + const components = []; + let inComponentsSection = false; + + for (const line of content.split("\n")) { + // Check if we're entering the Components section (uses ### header) + if (line.trim() === "### Components") { + inComponentsSection = true; + continue; + } + + // Check if we're leaving the Components section (another ### header) + if (inComponentsSection && line.trim().startsWith("### ")) { + break; + } + + // Extract component name from markdown link pattern + // Match: - [ComponentName](https://www.heroui.com/docs/native/components/component-name) + // Skip "All Components" which links to /components without a specific component + if (inComponentsSection) { + const match = line.match( + /^\s*-\s*\[([^\]]+)\]\(https:\/\/www\.heroui\.com\/docs\/native\/components\/[a-z]/, + ); + + if (match) { + components.push(match[1]); + } + } + } + + if (components.length > 0) { + console.error(`# Using fallback: ${LLMS_TXT_URL}`); + + return { + components: components.sort(), + count: components.length, + latestVersion: "unknown", + }; + } + + return null; + } catch (error) { + console.error(`Fallback Error: ${error.message}`); + + return null; + } +} + +/** + * Main function to list all available HeroUI Native components. + */ +async function main() { + let data = await fetchApi("/v1/components"); + + // Check if API returned valid data with components + if (!data || !data.components || data.components.length === 0) { + console.error("# API returned no components, trying fallback..."); + data = await fetchFallback(); + } + + if (!data || !data.components || data.components.length === 0) { + console.error("Error: Failed to fetch component list from API and fallback"); + process.exit(1); + } + + // Output formatted JSON + console.log(JSON.stringify(data, null, 2)); + + // Print summary to stderr for human readability + console.error( + `\n# Found ${data.components.length} Native components (${data.latestVersion || "unknown"})`, + ); +} + +main(); diff --git a/code/app/app-rn/package.json b/code/app/app-rn/package.json index f5247456..284611e2 100644 --- a/code/app/app-rn/package.json +++ b/code/app/app-rn/package.json @@ -49,6 +49,7 @@ "react": "19.2.0", "react-dom": "19.2.0", "react-native": "0.83.4", + "react-native-dates-picker": "^0.2.4", "react-native-gesture-handler": "~2.30.0", "react-native-image-crop-picker": "^0.51.1", "react-native-keyboard-controller": "1.20.7", diff --git a/code/app/app-rn/pnpm-lock.yaml b/code/app/app-rn/pnpm-lock.yaml index 6c864ce8..21dcb0d6 100644 --- a/code/app/app-rn/pnpm-lock.yaml +++ b/code/app/app-rn/pnpm-lock.yaml @@ -116,6 +116,9 @@ importers: react-native: specifier: 0.83.4 version: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + react-native-dates-picker: + specifier: ^0.2.4 + version: 0.2.4(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react-native-gesture-handler: specifier: ~2.30.0 version: 2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -1028,48 +1031,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.44.0': resolution: {integrity: sha512-AlObQIXyVRZ96LbtVljtFq0JqH5B92NU+BQeDFrXWBUWlCKAM0wF5GLfIhCLT5kQ3Sl+U0YjRJ7Alqj5hGQaCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.44.0': resolution: {integrity: sha512-YcFE8/q/BbrCiIiM5piwbkA6GwJc5QqhMQp2yDrqQ2fuVkZ7CInb1aIijZ/k8EXc72qXMSwKpVlBv1w/MsGO/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.44.0': resolution: {integrity: sha512-eOdzs6RqkRzuqNHUX5C8ISN5xfGh4xDww8OEd9YAmc3OWN8oAe5bmlIqQ+rrHLpv58/0BuU48bxkhnIGjA/ATQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.44.0': resolution: {integrity: sha512-YBgNTxntD/QvlFUfgvh8bEdwOhXiquX8gaofZJAwYa/Xp1S1DQrFVZEeck7GFktr24DztsSp8N8WtWCBwxs0Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.44.0': resolution: {integrity: sha512-GLIh1R6WHWshl/i4QQDNgj0WtT25aRO4HNUWEoitxiywyRdhTFmFEYT2rXlcl9U6/26vhmOqG5cRlMLG3ocaIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.44.0': resolution: {integrity: sha512-gZOpgTlOsLcLfAF9qgpTr7FIIFSKnQN3hDf/0JvQ4CIwMY7h+eilNjxq/CorqvYcEOu+LRt1W4ZS7KccEHLOdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.44.0': resolution: {integrity: sha512-1CyS9JTB+pCUFYFI6pkQGGZaT/AY5gnhHVrQQLhFba6idP9AzVYm1xbdWfywoldTYvjxQJV6x4SuduCIfP3W+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxfmt/binding-openharmony-arm64@0.44.0': resolution: {integrity: sha512-bmEv70Ak6jLr1xotCbF5TxIKjsmQaiX+jFRtnGtfA03tJPf6VG3cKh96S21boAt3JZc+Vjx8PYcDuLj39vM2Pw==} @@ -1142,48 +1153,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.60.0': resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.60.0': resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.60.0': resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.60.0': resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.60.0': resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.60.0': resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-musl@1.60.0': resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/binding-openharmony-arm64@1.60.0': resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==} @@ -1605,24 +1624,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -1696,10 +1719,12 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@xmldom/xmldom@0.8.12': resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@yz1311/react-native-wheel-picker@0.4.5': resolution: {integrity: sha512-07ga/fYdgHYI3JTAd+ntaXcJ1D3VxbgaqCPxABjxigehmFYD3VpBbMMVRhxmZC3oVnpedc+SKJXHTNwGtLriCQ==} @@ -2907,72 +2932,84 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.31.1: resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -3518,6 +3555,13 @@ packages: react-native-animatable@1.4.0: resolution: {integrity: sha512-DZwaDVWm2NBvBxf7I0wXKXLKb/TxDnkV53sWhCvei1pRyTX3MVFpkvdYBknNBqPrxYuAIlPxEp7gJOidIauUkw==} + react-native-dates-picker@0.2.4: + resolution: {integrity: sha512-zKNri33Ci8ozvWuZW9ErlYcrs66CUy3ZyVvLqqaYz9quE+sRyu2AMouLl1LQrwq4Fsk0hUjoSDuK2PPc0zUE/g==} + engines: {node: '>= 16.0.0'} + peerDependencies: + react: '>=17.0.0' + react-native: '*' + react-native-gesture-handler@2.30.1: resolution: {integrity: sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA==} peerDependencies: @@ -4076,6 +4120,7 @@ packages: uuid@7.0.3: resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true validate-npm-package-name@5.0.1: @@ -8017,6 +8062,12 @@ snapshots: dependencies: prop-types: 15.8.1 + react-native-dates-picker@0.2.4(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + dependencies: + dayjs: 1.11.20 + react: 19.2.0 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + react-native-gesture-handler@2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: '@egjs/hammerjs': 2.0.17 diff --git a/code/app/app-rn/src/app/(tabs)/_layout.tsx b/code/app/app-rn/src/app/(main)/(tabs)/_layout.tsx similarity index 100% rename from code/app/app-rn/src/app/(tabs)/_layout.tsx rename to code/app/app-rn/src/app/(main)/(tabs)/_layout.tsx diff --git a/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx b/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx new file mode 100644 index 00000000..112575b8 --- /dev/null +++ b/code/app/app-rn/src/app/(main)/(tabs)/chat/index.tsx @@ -0,0 +1,104 @@ +import React, { useState } from "react"; +import { View, Text, Button, ScrollView } from "react-native"; +import LayoutView from "@/components/layout/view"; +import DateTime from "@/components/DateTime"; +import DateTimeRange from "@/components/DateTimeRange"; + +export default function ChatTest() { + // --- DateTime (单选) --- + const [showDate, setShowDate] = useState(false); + const [dateValue, setDateValue] = useState("2026-06-23"); + + const [showTime, setShowTime] = useState(false); + const [timeValue, setTimeValue] = useState("20:00:00"); + + const [showDatetime, setShowDatetime] = useState(false); + const [datetimeValue, setDatetimeValue] = useState("2026-06-23 20:00"); + + const [showMonth, setShowMonth] = useState(false); + const [monthValue, setMonthValue] = useState("2026-06"); + + const [showYear, setShowYear] = useState(false); + const [yearValue, setYearValue] = useState("2026"); + + // --- DateTimeRange (范围) --- + const [showRange, setShowRange] = useState(false); + const [rangeStart, setRangeStart] = useState(""); + const [rangeEnd, setRangeEnd] = useState(""); + + return ( + + + + + {/* DateTime */} + DateTime(单选) + + - - */} - - - - - - - - - - 登录即表示你同意我们的 - - 用户协议 - - 和 - - 隐私政策 - - - - - - ); -} diff --git a/code/app/app-rn/src/app/index.tsx b/code/app/app-rn/src/app/index.tsx index 3911d2b9..eb270184 100644 --- a/code/app/app-rn/src/app/index.tsx +++ b/code/app/app-rn/src/app/index.tsx @@ -63,7 +63,7 @@ export default function Index() { family_id: res.data[0].family_id, }); - router.replace("/(tabs)/home"); + router.replace("/home"); } catch (e) { console.error(e); toast.error("初始化失败,请稍后重试"); diff --git a/code/app/app-rn/src/components/DateTime.tsx b/code/app/app-rn/src/components/DateTime.tsx index b0e7aa5d..807fdd88 100644 --- a/code/app/app-rn/src/components/DateTime.tsx +++ b/code/app/app-rn/src/components/DateTime.tsx @@ -1,15 +1,19 @@ -import { DatePicker as YzDatePicker } from "@yz1311/react-native-wheel-picker"; +import DateTimePicker from "react-native-dates-picker"; import dayjs from "dayjs"; import { BottomSheet, Button, Spinner } from "heroui-native"; -import { useState, useEffect } from "react"; -import { toDate } from "@/utils/time"; +import { useState, useEffect, useCallback, useRef } from "react"; import { View } from "react-native"; import { CheckIcon } from "lucide-react-native"; import { FadeIn } from "react-native-reanimated"; export type DatePickerMode = "year" | "month" | "date" | "time" | "datetime"; -export type DateFormatType = "YYYY" | "YYYY-MM" | "YYYY-MM-DD" | "HH:mm:ss" | "YYYY-MM-DD HH:mm"; +export type DateFormatType = + | "YYYY" + | "YYYY-MM" + | "YYYY-MM-DD" + | "HH:mm:ss" + | "YYYY-MM-DD HH:mm"; interface DatePickerProps { value: Date | string; @@ -28,6 +32,14 @@ const defaultFormatMap: Record = { datetime: "YYYY-MM-DD HH:mm", }; +const modeViewMap: Record = { + year: "year", + month: "month", + date: "day", + time: "day", + datetime: "day", +}; + export default function DateTime({ value, show = false, @@ -38,51 +50,60 @@ export default function DateTime({ }: DatePickerProps) { const finalFormat = format ?? defaultFormatMap[mode]; - const [tempDate, setTempDate] = useState(toDate(value) ?? new Date()); + const [open, setOpen] = useState(false); + const [tempDate, setTempDate] = useState(dayjs()); const [loading, setLoading] = useState(false); + const onCloseRef = useRef(onClose); + onCloseRef.current = onClose; + + // 打开 useEffect(() => { if (show) { - setTempDate(toDate(value) ?? new Date()); + const d = dayjs(value); + setTempDate(d.isValid() ? d : dayjs()); + setOpen(true); } }, [show, value]); - const handleClose = () => { - if (loading) return; - onClose?.(); - }; + // 关闭:BottomSheet 的 onOpenChange(false) 或 confirm 后 setOpen(false) + // 立刻通知父组件,不延迟 — BottomSheet 自身驱动关闭动画 + useEffect(() => { + if (!open && show) { + onCloseRef.current?.(); + } + }, [open, show]); - const handleConfirm = async () => { - if (!tempDate || loading) return; + const handleConfirm = useCallback(async () => { + if (loading || !tempDate) return; try { setLoading(true); - - const result = dayjs(tempDate).format(finalFormat); - + const result = tempDate.format(finalFormat); await onConfirm?.(result); - - handleClose(); + setOpen(false); } catch (e) { console.log(e); } finally { setLoading(false); } - }; + }, [loading, tempDate, finalFormat, onConfirm]); return ( - !v && handleClose()}> + - - setTempDate(d)} + initialView={modeViewMap[mode]} + timePicker={mode === "time" || mode === "datetime"} + selectedItemColor="#0d9488" + onChange={({ date }) => { + if (date) setTempDate(dayjs(date)); + }} /> diff --git a/code/app/app-rn/src/helpers/feed.ts b/code/app/app-rn/src/helpers/feed.ts new file mode 100644 index 00000000..d8e467af --- /dev/null +++ b/code/app/app-rn/src/helpers/feed.ts @@ -0,0 +1,86 @@ +import { loadDataApi, saveDataApi, getUniqueIdApi } from "@/request/api"; + +export type FeedType = "join" | "rename" | "bill" | "fav"; + +export interface FeedRecord { + id: number; + family_id: number; + user_id: number; + type: FeedType; + content: string; + meta: string | null; + create_time: string; +} + +/** 获取用户在家庭中的昵称(来自 b_family_member,非 b_user) */ +export async function getFamilyMemberNickname( + familyId: number, + userId: number, +): Promise { + const res = await loadDataApi({ + view_name: "b_family_member", + search_condition: "family_id = ? AND user_id = ?", + search_columns: ["nickname"], + order_by: "", + args: [familyId, userId], + }); + if (res.isSuccess && res.data?.length > 0) { + return res.data[0].nickname || "成员"; + } + return "成员"; +} + +/** 写入一条动态 — content 直接包含所有展示信息,用 {nickname} 占位 */ +export async function recordFeed( + familyId: number, + userId: number, + type: FeedType, + content: string, +) { + const idRes = await getUniqueIdApi(1); + const feedId = idRes.data?.[0]; + if (!feedId) return; + + await saveDataApi([ + { + table_name: "b_family_feed", + key_field: "id", + inserts: [ + { + id: feedId, + family_id: familyId, + user_id: userId, + type, + content, + }, + ], + updates: [], + deletes: [], + }, + ]); +} + +/** 加载家庭动态列表 — 自动替换 {nickname} 占位符 */ +export async function loadFeeds(familyId: number): Promise { + const res = await loadDataApi({ + view_name: "b_family_feed", + search_condition: "family_id = ?", + order_by: "create_time DESC", + search_columns: ["id", "family_id", "user_id", "type", "content", "meta", "create_time"], + args: [familyId], + }); + const feeds = (res.data || []) as FeedRecord[]; + + // 批量加载昵称:收集唯一 user_id,逐个查 b_family_member + const uniqueUserIds = [...new Set(feeds.map((f) => f.user_id))]; + const nickMap = new Map(); + for (const uid of uniqueUserIds) { + nickMap.set(uid, await getFamilyMemberNickname(familyId, uid)); + } + + // 替换 {nickname} 占位符,保证每次展示都是最新昵称 + return feeds.map((f) => ({ + ...f, + content: f.content.replace(/\{nickname\}/g, nickMap.get(f.user_id) || "成员"), + })); +} diff --git a/code/app/app-rn/src/layouts/PageLayout.tsx b/code/app/app-rn/src/layouts/PageLayout.tsx index 8388fe54..18dc17d0 100644 --- a/code/app/app-rn/src/layouts/PageLayout.tsx +++ b/code/app/app-rn/src/layouts/PageLayout.tsx @@ -21,7 +21,7 @@ export default function PageLayout({ title, titleIcon, children, renderTitle }: } function handleClose() { - router.navigate("/(tabs)/home"); + router.navigate("/home"); } return (