Commit f7fcabfd authored by Andy-bubu's avatar Andy-bubu

feat: 完成鑫元 HeroUI 界面迁移

parent efc6e02c
...@@ -13,3 +13,4 @@ ...@@ -13,3 +13,4 @@
.swc .swc
.pnpm-store .pnpm-store
.codex-audit .codex-audit
.playwright-cli/
...@@ -50,7 +50,7 @@ export default defineConfig({ ...@@ -50,7 +50,7 @@ export default defineConfig({
], ],
}, },
], ],
npmClient: 'npm', npmClient: 'pnpm',
proxy: { proxy: {
'/api': { '/api': {
target: 'https://xy.streams7.com/api', target: 'https://xy.streams7.com/api',
......
# UI 架构
更新日期:2026-08-20
## 目标边界
部门任务管理使用原生 HeroUI 3 / HeroUI Pro 架构。页面负责业务数据、权限、请求、筛选值、表单 draft 和回调;组件库负责可访问交互、compound structure、状态和外观。
```text
Umi routes
-> Site/Auth wrappers (业务鉴权与站点数据)
-> HeroUI Pro AppShell / Sidebar / Navbar
-> page-owned business state and services
-> direct HeroUI/HeroUI Pro feature surfaces
-> root-theme-aware Portal overlays
```
## 依赖来源
- `@heroui-pro/react@1.0.0-beta.8` 由目标项目根目录的官方授权安装工具获取。
- `@heroui/react@3.2.4`、`@heroui/styles@3.2.4` 由正式 registry 安装。
- 不允许 `file:`、绝对路径、相邻项目 node_modules、vendor 包或跨项目软链接。
- 授权值不进入源码、文档、环境文件、锁文件或 Git。
- 最终以依赖树和 require/import 解析路径证明单一 React/HeroUI/React Aria runtime。
## 直接导入政策
- 页面和共享 UI 组合直接导入 `@heroui/react`、`@heroui-pro/react` 或已验证 Pro 子路径。
- Shared composition 只可拥有业务无关的布局、可访问标签、Tokens 和重复 compound 结构。
- Shared composition 必须暴露当前 HeroUI 的 value/selection/event model。
- 禁止保留 Catalyst/Headless/local primitive API,禁止合成旧 DOM event,禁止解析旧 variant/className。
- AppIcons 仅是 Ant Design icon catalog,不包装 UI primitive。
## 状态与业务所有权
- `projectTaskService.js` 继续拥有 `pm_project_task` 查询、metadata 转换、导入、用户搜索与保存协议。
- `projectStatisticsService.js` 继续拥有统计数据请求和真实数据错误口径。
- `Auth.jsx` / `authService.js` / `DataService.js` 继续拥有用户校验、token、登录与退出调用。
- 页面继续拥有 Tabs、筛选、分页、DataGrid 临时选择、Modal draft、loading/error/success 状态;DataGrid 临时选择不替代当前项目业务状态。
- 迁移只把这些状态绑定到当前 HeroUI props,不改业务值、字段名、请求参数或触发时机。
## 主题与样式所有权
- `theme.css`:不可变 HeroUI 核心 Token。
- `tokens.css`:只定义批准的 `--rc-*` 别名、布局、数据可视化和第三方桥 Token。
- `shell.css`:只承担共享 Shell 几何和响应式布局。
- 共享 feature CSS:只允许业务无关布局、第三方不可挂载 React 的渲染桥,以及 Skill 明确批准的共享 contracts。
- 页面 className:仅允许 flex/grid/width/gap/alignment/overflow/responsive。
- 禁止页面选择 HeroUI BEM/slot/pseudo-element 或覆盖组件外观/状态。
## DataGrid 架构
- DataGrid 使用 `selectedKeys`、`onSelectionChange`、`selectionMode`、`showSelectionCheckboxes`。
- 列 schema 保留业务 render/sort/callback,转换为 Pro ColumnSize。
- 首业务列和操作列只在真实横向溢出时固定。
- ActionBar 由受控选择状态驱动;Pagination 保留 `page` 和固定 `page_size=10`。
- 不手写 selection column,不嵌入旧 Checkbox,不维护第二套选择模型。
## 甘特图架构
- HeroUI Pro 当前没有 Gantt 组件;甘特图继续复用项目既有日期窗口、列计算和任务区间算法,不误用垂直事件 `Timeline`。
- 可见结构使用 HeroUI 原生组合:L1 `Card` 承载分析模块,L2 `Surface secondary` 承载时间轴,L3 `Surface tertiary` 承载刻度和任务行。
- 实际进度与时间进度使用 `ProgressBar` compound API;结果状态使用语义 `Chip`,空范围使用 Pro `EmptyState`。
- 共享 CSS 只控制网格、宽度、间距、对齐与横向滚动;组件颜色、圆角、描边和交互状态由 HeroUI variant 与鑫元共享 Tokens 负责。
## 个人日历架构
- 个人日历直接使用 HeroUI Pro `Agenda` 的 month compound API:`Header`、`Navigation`、`Body`、`MonthGrid`、`MonthRow`、`MonthCell`、`MonthSpanningEvent` 与 `MonthEvent`。
- 页面只把 `pm_project_task` 的计划开始/结束日期转换为只读全天事件;Agenda 持有月份矩阵、跨周分段、非本月日期、今日状态和键盘交互。
- `onEventSelect` 继续映射到既有项目编辑回调;不传入创建、移动、缩放或删除回调,日历不会修改业务数据。
- 外层业务模块使用批准的 HeroUI Card contract;窄视口只在 Agenda Body 内横向滚动,不允许形成页面级横向溢出。
## Overlay 架构
- Modal/AlertDialog/Dropdown/Autocomplete/DatePicker/Tooltip 直接使用 HeroUI Portal 结构。
- Autocomplete 统一在共享 HeroUI-native 组合中使用当前 `react-stately` Select 的受控 `value/onChange` 契约:单选值为 ID 或 `null`,多选值为 ID 数组;官方 ClearButton、触发器显示和选项勾选均由该值驱动。远程用户字段只在业务边界输出原有 ID、`''`、ID 数组或 `[]`。`selectedKeys/onSelectionChange` 仅用于 DataGrid 等集合选择,不用于 Autocomplete 根。
- 根 `html` 同时携带 class 与 `data-theme`,Portal 从根继承主题。
- 查看态详情使用单一 L2 Surface;编辑态 controls 直接位于 Overlay 并使用 `secondary`。
- 应用切换保留目标的应用 API、归一化和导航 callback,只替换为批准的 Modal/identity composition。
- 移动导航复用 HeroUI Pro Sidebar Provider 的 `isMobileOpen/setMobileOpen` 状态,并以同包 Sheet compound 建立具名 Dialog,规避当前 Sidebar.Mobile 未向 Dialog 根透传可访问名称的问题。
## 删除顺序
1. 迁移 theme/Shell/Login/ApplicationSwitcher。
2. 迁移统计页 KPI 与图表。
3. 迁移预定页字段、筛选、列表和弹窗。
4. 当引用为零后删除旧 Dialog、DataTable、MetricCard、Checkbox、FormSelect、FilterSelect、FormDatePicker、SystemUserSearchSelect、TableActionButton 等旧边界。
5. 扫描并移除活动页面私有视觉 CSS 与 legacy utility bridge。
# 鑫元 UI 迁移清单
更新日期:2026-08-20
状态说明:`[ ]` 待处理,`[x]` 已完成,`[-]` 不适用或受外部条件阻塞。
## 安装与基线
- [x] 在目标根目录执行用户指定的官方 HeroUI Pro 安装命令。
- [x] Pro、OSS、styles 解析到目标项目自身 node_modules。
- [x] Pro 核心文件与要求子路径可导入。
- [x] package/lock 扫描无 `file:`、本机路径、相邻 node_modules。
- [x] 启动未修改 UI 的目标并记录登录页尺寸与控制台。
- [x] 启动问题整改基准并在相同 1440x900 视口比较登录页。
- [x] 统一仓库包管理器与唯一锁文件。
## 架构与主题
- [x] 阅读 API、Subject、业务流程和全部鑫元迁移合同。
- [x] 创建四份项目迁移文档。
- [x] 复制逐字节一致的权威 theme.css。
- [x] tokens.css 不覆盖 theme 核心变量。
- [x] 正确加载 Inter 与 CSS 顺序。
- [x] 活动范围 Catalyst/Headless/旧包装引用为零。
- [x] 可见原生 select/date/datetime/file 为零。
- [x] 页面私有视觉 CSS 扫描为零。
## 登录与 Shell
- [x] 登录结构、Logo、标题、卡片宽度、字段节奏、验证码和 filled/autofill 与问题整改一致。
- [x] Sidebar 使用 Pro offcanvas,桌面关闭后零宽。
- [x] Navbar 使用 Pro 默认 Sidebar.Trigger。
- [x] 页面与 Navbar 对齐 48/32/16px 对称 gutter。
- [x] 用户入口使用 Dropdown.Trigger,真实退出可用且 base-aware。
- [x] 应用切换使用 HeroUI Modal、固定两列和 36px identity well。
- [x] 主题在刷新和路由切换后保持,全部 Portal 跟随根主题。
## 资源统计
- [x] 顶部概览和核心指标全部改为 Pro KPI compound,不保留手写数字 Surface。
- [x] KPI 外卡 24px、1px token border、共享 shadow、16px board gap。
- [x] ECharts 与私有 tooltip/theme 引用为零。
- [x] 状态环图改为 Pro PieChart。
- [x] 结果/进度/成员负载改为 Pro BarChart/ComposedChart。
- [x] ChartTooltip 指示点与系列 Token 一致。
- [x] 风险列表/空/错/加载保留真实数据口径。
## 资源预定
- [x] 页面级 Tabs 使用直接 HeroUI Tabs。
- [x] 筛选使用直接 Surface + secondary 字段 + Disclosure;无重复刷新/查询按钮,所有条件实时生效。
- [x] 所有枚举/用户字段使用 HeroUI Autocomplete current API。
- [x] Autocomplete 根使用当前 HeroUI `value/onChange` 契约;触发器回显、选项勾选和官方 ClearButton 由同一受控选择值驱动,单选清空为 `''`,多选清空为 `[]`。
- [x] 所有日期字段使用 HeroUI DatePicker/DateField/Calendar。
- [x] 导入使用 Pro DropZone,保留 Excel 解析与提交行为。
- [x] 项目清单使用 Pro DataGrid 原生选择、排序、固定列和横向滚动。
- [x] 项目清单行操作使用 HeroUI tertiary 图标按钮与 Tooltip;预览使用 4xl Modal 和单一 secondary Surface,不触发业务请求。
- [x] Pagination 使用 HeroUI compound,固定 page_size=10,无页容量控件。
- [x] 选中态使用 Pro ActionBar 和右侧关闭清除。
- [x] 弹窗使用直接 Modal/AlertDialog;详情/表单为 4xl。
- [x] 新建、启动、完成、周进度、删除、导入回调与请求时机不变。
- [x] 甘特图使用 HeroUI Card/Surface/ProgressBar/Chip 与 Pro EmptyState,保留原日期区间算法并落实 L1/L2/L3 层级。
- [x] 甘特图周/月/年、前后时间段、今天、任务打开和横向滚动交互保持不变。
- [x] 个人项目日历使用 Pro Agenda 月视图、连续跨日/跨周事件、官方月份导航和模块内滚动;不保留手写日期网格。
## 浏览器验收
- [x] 1440x900 浅色/深色。
- [x] 1280x720 浅色/深色。
- [x] 768x1024 浅色/深色。
- [x] 390x844 浅色/深色。
- [x] 登录、Sidebar 开关、移动 offcanvas、Navbar、用户菜单、退出。
- [x] 应用切换打开态、真实/透明/失败 Logo。
- [x] Tabs、全部 Autocomplete/ListBox/DatePicker 打开态。
- [x] Checkbox checked/indeterminate/disabled、DataGrid、Pagination、ActionBar。
- [x] 项目/周进度/导入/删除 Modal 与键盘焦点。
- [x] KPI、三类图表和 Tooltip。
- [x] 控制台零 runtime/slot/provider/duplicate Context 错误。
## 终检
- [x] `npm run build`。
- [x] `git diff --check`。
- [x] 鑫元 `audit_project.sh`。
- [x] 旧组件引用扫描。
- [x] 页面私有视觉 CSS 扫描。
- [x] `file:` 与本机路径扫描。
- [x] HeroUI/React Aria 依赖树检查。
- [x] HeroUI Pro 子路径检查。
## 验收记录
- 目标与问题整改在四个规定视口、浅色和深色下同时打开比较;两端八组视口均无页面级横向溢出。
- 已打开并操作用户 Dropdown、应用切换 Modal、移动 Sheet、筛选 Disclosure、枚举与远程用户 Autocomplete、DatePicker、项目/周进度/导入 Modal、删除 AlertDialog、DataGrid、Pagination、ActionBar 和图表 Tooltip。
- 选择器专项回归:项目负责人和项目协作人均通过连续双选、重新打开保留勾选、单项取消、触发器即时回显及官方 ClearButton 全部清空;筛选状态/负责人及表单枚举通过选择回显、清除和实时查询。桌面浅色与 390x844 深色均通过,移动弹层无横向溢出。
- 退出菜单实际跳转到 `/project_manager_xy/login`;取消周进度与删除确认不会触发保存/删除请求。
- 最后一次目标页面重载与退出验收均为 0 error、0 warning。
- 浏览器截图与交互快照仅作为本地临时验收产物,统一通过 `.gitignore` 排除,不纳入源代码管理。
- 2026-08-20 甘特图专项复核:代表性项目数据下,1440x900、1280x720、768x1024、390x844 的浅色/深色实渲染通过;L1 Card 为 24px,L2/L3 Surface 为 8px,页面级横向溢出为 0,时间轴在 768px 和 390px 下分别形成 `606 < 2364`、`236 < 2364` 的模块内滚动边界。
- 2026-08-20 甘特图专项复核:周/月/年 Tabs、上一时间段、今天、任务点击编辑、实际/时间 ProgressBar 和周进展 Tooltip 打开态通过;代表性数据控制台 0 error、0 warning。
- [ ] 甘特图最终空态位置修正后的浏览器重拍。修正已通过生产构建和 JSX 结构检查,但本轮环境权限切换后禁止监听本地端口并禁止启动 Chrome(`listen EPERM` / browser launch `SIGABRT`),不得把该项记录为已完成。
# 部门任务管理鑫元设计系统
更新日期:2026-08-20
本项目直接继承鑫元 HeroUI 设计系统,以“问题整改”运行结果为唯一结构、尺寸、间距和交互基准。业务页面不得建立第二套视觉语言。
## 技术边界
- 基础控件直接从 `@heroui/react` 导入。
- Sidebar、Navbar、DataGrid、ActionBar、EmptyState、KPI 和图表直接从 `@heroui-pro/react` 或其子路径导入。
- 使用当前 compound API、`onPress`、`selectedKey(s)`、`onSelectionChange`;不提供旧 props/event 兼容层。
- 不使用 Catalyst、Headless UI、旧 Dialog/Table/Select/Pagination 包装或可见原生 select/date/file 控件。
- 图标统一由 `src/components/AppIcons.jsx` 导出 Ant Design Icons;普通文字按钮不带装饰图标。
## 样式权威与顺序
全局顺序固定为:Tailwind、HeroUI、HeroUI Pro、权威 `theme.css`、鑫元扩展 `tokens.css`、共享 Shell/业务中立布局样式。
- `src/design-system/theme.css` 必须与鑫元 Skill 权威副本逐字节一致。
- `tokens.css` 只能新增或别名 `--rc-*`,不得重定义 theme.css 的核心变量。
- 应用根 `body` 统一关闭合成字重并固定文本栅格策略。开发服务通过 `HMR=none` 禁用 Umi 模块热替换,避免异常热更新循环反复替换 `umi.css` 并让 Inter 字体在已加载字体与回退字体之间切换。
- 页面 `className` 只承担 flex/grid/width/gap/alignment/overflow/responsive。
- 页面不得设置组件背景、颜色、描边、圆角、阴影或交互状态,不得选择 HeroUI 内部 slot/BEM/伪元素。
## Shell 与页面布局
- Sidebar 桌面宽 240px,`collapsible="offcanvas"`,关闭后宽度为零。
- Navbar 高 64px,使用 HeroUI Pro `Sidebar.Trigger` 默认图形。
- Navbar 与页面共用左右 gutter:桌面 48px、平板 32px、手机 16px。
- 页面桌面/平板上下 padding 为 24/32px,手机为 16/24px。
- 一级模块间距 32px,普通模块 24px,紧密列表/工具栏/KPI 间距 16px;手机为 24/16px。
- Navbar 是唯一可见页面标题,内容区不得重复大标题。
## Surface 层级
- L0 page canvas:`background`。
- L1 首层内容:HeroUI `Surface default` / `surface`。
- L2 嵌套内容与 Surface/Card/Modal 内字段:`surface-secondary` / HeroUI `secondary`。
- L3 仅用于 L2 内真实静态子对象:`surface-tertiary`。
- Modal、Popover、Dropdown、Tooltip、Calendar 使用独立 `overlay`,不是 L3。
- 筛选必须用 Surface,列表区域透明,DataGrid 自身拥有唯一数据表面。
## 组件合同
- 登录:约 28rem 卡片、36px Logo、卡内品牌、24px 字段组节奏、40px 字段、46px提交按钮。
- 应用切换:46rem/42rem,手机一列、sm 起固定两列,36px `Surface tertiary + Avatar soft` identity well。
- 筛选:1-2 个字段不加容器;3 个以上使用 `Surface default rounded-3xl p-6`,前三项常用字段,其余放 HeroUI Disclosure。
- 列表工具栏:搜索在左、列分组紧随、业务操作在右、主操作最右;手机搜索先独占一行。
- DataGrid:原生选择/排序/固定列/滚动;20px 外圆角;固定每页 10 条;Pagination 与 DataGrid 间距 8px。
- ActionBar:选中后位于当前主内容视口下方中央,右侧关闭清除选择。
- Modal:标准 24px 圆角;详情/复杂表单 4xl;查看态只有一个 secondary Surface,编辑态移除该 Surface。
- KPI:HeroUI Pro KPI compound,24px 外卡、16px board gap、实际 data-count 布局。
- 图表:HeroUI Pro PieChart、BarChart、ComposedChart 与 ChartTooltip;数据系列只用共享 8 色 Token。
- 个人日历:HeroUI Pro Agenda 月视图;跨日期项目使用 MonthSpanningEvent,单日项目使用 MonthEvent,导航和日期状态使用 Agenda 官方 compound API。
## 响应式与主题
- 视口:1440x900、1280x720、768x1024、390x844。
- 根节点统一 `.light/.dark` 和 `data-theme`,持久化键保持 `xinyuan_color_theme`。
- Portal、Sidebar.Mobile、Modal、Dropdown、Autocomplete、DatePicker、Tooltip、ActionBar 与图表 Tooltip 必须继承根主题。
- 页面不按主题切换 component variant,不使用 `bg-white`、zinc/slate/blue/cyan/rose/pink 私有外观桥。
## 已验收实现
- 桌面 Sidebar 使用 Pro `Sidebar` offcanvas;移动端复用同一 Provider 状态并使用 Pro `Sheet` compound,保证 Dialog 根具备可访问名称。
- 目标和问题整改已在 1440x900、1280x720、768x1024、390x844 的浅色/深色模式逐项并排检查,页面级横向溢出均为 0。
- 登录、Navbar、Sidebar、用户菜单、退出、应用切换、Tabs、筛选、所有打开态选择器、DatePicker、DataGrid、Pagination、ActionBar、Modal、AlertDialog、KPI、图表及 Tooltip 已通过真实浏览器交互。
- 浏览器最终控制台为 0 error、0 warning;本地证据位于 `.playwright-cli/`。
This diff is collapsed.
This diff is collapsed.
...@@ -2,30 +2,50 @@ ...@@ -2,30 +2,50 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build": "max build", "build": "max build",
"dev": "max dev", "dev": "HMR=none max dev",
"postinstall": "max setup", "postinstall": "max setup",
"setup": "max setup", "setup": "max setup",
"start": "npm run dev" "start": "npm run dev"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.3.2", "@ant-design/icons": "^6.3.2",
"@headlessui/react": "^2.2.10", "@fontsource/inter": "^5.3.0",
"@heroicons/react": "^2.2.0", "@heroui-pro/react": "1.0.0-beta.8",
"@heroui-pro/react": "file:../project_manager_new/node_modules/@heroui-pro/react", "@heroui/react": "3.2.4",
"@heroui/react": "file:../project_manager_new/node_modules/@heroui/react", "@heroui/styles": "3.2.4",
"@heroui/styles": "file:../project_manager_new/node_modules/@heroui/styles", "@internationalized/date": "3.12.3",
"@internationalized/number": "3.6.7",
"@number-flow/react": "^0.6.2",
"@react-aria/utils": "^3.34.1",
"@react-stately/utils": "3.12.1",
"@tailwindcss/postcss": "^4.2.0", "@tailwindcss/postcss": "^4.2.0",
"@tiptap/core": "^3.30.2",
"@tiptap/extension-link": "^3.30.2",
"@tiptap/extension-underline": "^3.30.2",
"@tiptap/extensions": "^3.30.2",
"@tiptap/pm": "^3.30.2",
"@tiptap/react": "^3.30.2",
"@tiptap/starter-kit": "^3.30.2",
"@tiptap/suggestion": "^3.30.2",
"@umijs/max": "^4.5.3", "@umijs/max": "^4.5.3",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"echarts": "^6.1.0", "embla-carousel": "^8.6.0",
"echarts-for-react": "^3.0.6", "embla-carousel-react": "^8.6.0",
"maplibre-gl": "^6.4.1",
"marked": "^18.0.10",
"motion": "^12.43.0", "motion": "^12.43.0",
"react": "^19.2.8", "react": "^19.2.8",
"react-aria-components": "^1.20.0", "react-aria-components": "^1.20.0",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.12.3",
"recharts": "^3.10.1", "recharts": "^3.10.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"shiki": "^4.4.3",
"streamdown": "^2.5.0",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.3.1", "tailwind-variants": "^3.3.1",
"tailwindcss": "^4.2.0", "tailwindcss": "^4.2.0",
...@@ -37,5 +57,11 @@ ...@@ -37,5 +57,11 @@
"@types/react": "^19.2.18", "@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4", "@types/react-dom": "^19.2.4",
"typescript": "^5.0.3" "typescript": "^5.0.3"
},
"pnpm": {
"onlyBuiltDependencies": [
"@heroui-pro/react",
"@zowe/secrets-for-zowe-sdk"
]
} }
} }
This diff is collapsed.
import '@/tailwind.css'; import '@/tailwind.css';
import '@/global.less';
import { I18nProvider } from '@heroui/react'; import { I18nProvider } from '@heroui/react';
import { createElement } from 'react'; import { createElement } from 'react';
import { defineApp } from 'umi'; import { defineApp } from 'umi';
......
# Changelog
## 2025-11-18
- Fix sidebar indicator animation when page is scrolled ([#1778](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1778))
## 2025-08-13
- Use `motion/react` instead of `framer-motion`
- Fix inconsistent `Input` error border color ([#1710](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1710))
## 2025-07-29
- Update to React 19 and Next.js 15.4
## 2025-06-05
- Update `DropdownLabel` to extend a plain `<div>` instead of the `Headless.Label` ([#1699](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1699))
## 2025-04-28
- Update template to Tailwind CSS v4.1.4
## 2025-04-22
- Fix focus styles in the `Avatar`, `Badge`, `Button`, `Checkbox`, and `Switch` components ([#1693](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1693))
## 2025-04-17
- Update `@headlessui/react` dependency to `v2.2.2` to fix listbox performance issues ([#1667](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1667))
## 2025-04-04
- Add new `Combobox` component
- Add new `AuthLayout` component, with example login page, registration page, and forgot password page
- Fix centering of checkboxes, radios, and switches in fields with multi-line labels
- Update template to Tailwind CSS v4.1.3
- Update template to Headless UI v2.2.1 (required for new `Combobox` component)
## 2025-03-24
- Adjust avatar ring opacity from 20% to 10%
## 2025-03-22
- Update template to Tailwind CSS v4.0.15
## 2025-02-22
- Fix border radius of avatars in navbar items
## 2025-02-12
- Fix baseline alignment for buttons with icons ([#1668](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1668))
## 2025-02-10
- Update template to Tailwind CSS v4.0.6
## 2025-01-29
- Fix conflicting outline utilities ([#1661](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1661))
## 2025-01-23
- Update template to Tailwind CSS v4.0
## 2024-10-31
- Fix `disabled` prop in `DropdownItem` component ([#1640](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1640))
- Update types in `SidebarItem` component
## 2024-06-09
- Omit `as` prop from types for Headless UI-based components
## 2024-06-26
- Update `@headlessui/react` dependency to `v2.1.1`
- Update `Dialog`, `Alert`, and `Listbox` to new data-attribute-based transition API
## 2024-06-21
- Update `@headlessui/react` dependency to `v2.1.0`
- Use `DialogBackdrop` in `Alert` and `Dialog` components
- Update to new data-attribute-based transition API
## 2024-06-19
- Prevent content from overflowing the mobile sidebar
## 2024-06-18
- Use consistent `let` and `const` declarations
- Use consistent `clsx` import
- Use consistent `forwardRef` instead of `React.forwardRef` import
- Update `tailwindcss`, `prettier` and `prettier-plugin-tailwindcss` dependencies in Next.js demo app
- Fix class order in `Dropdown` component
## 2024-06-10
- Add `dark:[color-scheme:dark]` class to `Input` component ([#1596](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1596))
## 2024-05-31
- Add Next.js demo app ([#1580](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1580))
- Fix `Avatar` sizing and padding ([#1588](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1588))
## 2024-05-27
- Add `pointer-events-none` to `InputGroup` icon ([#1594](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1594))
- Add default text color to `Table` component ([#1581](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1581))
- Fix TypeScript issues when using `as={Link}` ([#1582](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1582))
- Fix `SidebarItem` import ([#1582](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1582))
- Add `isolate` class to `InputGroup`, `SidebarLayout`, and `StackedLayout` components
- Fix comment indentation and use proper DocBlocks
## 2024-05-24
- Add new `SidebarLayout` component
- Add new `StackedLayout` component
- Add new `Navbar` component
- Add new `Sidebar` component
- Add new `DescriptionList` component
- Add new `Heading` component
- Add new `Divider` component
- Add new `framer-motion` dependency
- Update `@headlessui/react` dependency to `v2.0`
- Remove `as={Fragment}` from the transition components
- Improve and fix a number of types
- Made formatting of `className` and `{...prop}` more consistent across all components
- Made formatting of `forwardRef` consistent across all components
- Update `PaginationGap` component to use a `span` instead of a `div`
- Update the `SidebarHeading` to render use an `h3` instead of a `d`iv
- Remove `children` prop from `Switch` component
## 2024-01-08
- Add `resizable` prop to `Textarea` component ([#1543](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1543))
## 2024-01-03
- Change top-level `let` declarations to `const`
- Fix `--btn-hover-overlay` color for dark/white button ([#1538](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1538))
- Add `forwardRef` to `Input`, `Select`, and `Textarea` components ([#1540](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1540))
## 2023-12-20
- Initial release
This diff is collapsed.
# Catalyst UI Kit
Catalyst is a modern application UI kit built with [Tailwind CSS](https://tailwindcss.com) and [React](https://react.dev/), designed and built by the Tailwind CSS team and included as part of [Tailwind Plus](https://tailwindcss.com/plus).
## Getting started
To get started, first copy the component files included in the downloaded ZIP file into wherever you keep components in your own project. The components are provided in both TypeScript and plain JavaScript, pick whichever set you prefer.
Next, install the dependencies used by the components in Catalyst:
```sh
npm install @headlessui/react motion clsx
```
Catalyst is also designed for the latest version of Tailwind CSS, which is currently Tailwind CSS v4.0. To make sure that you are on the latest version of Tailwind, update it via npm:
```sh
npm install tailwindcss@latest
```
Now you're ready to start using the components in your project — just import them from wherever you're keeping your components and start using them like any of your other React components:
```jsx
import { Input } from './components/input'
import { Field, FieldGroup, Label } from './components/fieldset'
import { Button } from './components/button'
export default function SettingsForm() {
return (
<form>
<FieldGroup>
<Field>
<Label>Name</Label>
<Input name="name" />
</Field>
<Field>
<Label>Email</Label>
<Input type="email" name="email" />
</Field>
<Button type="submit">Save changes</Button>
</FieldGroup>
</form>
)
}
```
Additional installation instructions can be found in the Catalyst documentation.
## Documentation
You can find the Catalyst documentation at https://catalyst.tailwindui.com/docs.
## License
This site template is a commercial product and is licensed under the [Tailwind Plus license](https://tailwindcss.com/plus/license).
## Learn more
To learn more about the technologies used in this site template, see the following resources:
- [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation
- [Headless UI](https://headlessui.dev) - the official Headless UI documentation
- [React](https://react.dev) - the official React documentation
- [Framer Motion](https://www.framer.com/docs/) - the official Framer Motion documentation
- [clsx](https://github.com/lukeed/clsx) - the GitHub repo for the `clsx` helper
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-img-element": "off"
}
}
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Tailwind Plus License
## Personal License
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates.
You **can**:
- Use the Components and Templates to create unlimited End Products.
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
You **cannot**:
- Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates.
- Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets.
- Share your access to the Components and Templates with any other individuals.
- Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
### Example usage
Examples of usage **allowed** by the license:
- Creating a personal website by yourself.
- Creating a website or web application for a client that will be owned by that client.
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available.
Examples of usage **not allowed** by the license:
- Creating a repository of your favorite Tailwind Plus components or templates (or derivatives based on Tailwind Plus components or templates) and publishing it publicly.
- Creating a React or Vue version of Tailwind Plus and making it available either for sale or for free.
- Create a Figma or Sketch UI kit based on the Tailwind Plus component designs.
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind Plus.
- Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free.
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
In simple terms, use Tailwind Plus for anything you like as long as it doesn't compete with Tailwind Plus.
### Personal License Definitions
Licensee is the individual who has purchased a Personal License.
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind Plus license.
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
End User is a user of an End Product.
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
## Team License
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates.
You **can**:
- Use the Components and Templates to create unlimited End Products.
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
You **cannot**:
- Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates.
- Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product.
- Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee.
- Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
### Example usage
Examples of usage **allowed** by the license:
- Creating a website for your company.
- Creating a website or web application for a client that will be owned by that client.
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available.
Examples of use **not allowed** by the license:
- Creating a repository of your favorite Tailwind Plus components or template (or derivatives based on Tailwind Plus components or templates) and publishing it publicly.
- Creating a React or Vue version of Tailwind Plus and making it available either for sale or for free.
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind Plus.
- Creating a theme or template using the components or templates and making it available either for sale or for free.
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
- Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind Plus license to build their own websites or side projects.
### Team License Definitions
Licensee is the business entity who has purchased a Team License.
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind Plus license.
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
End User is a user of an End Product.
Employee is a full-time or part-time employee of the Licensee.
Contractor is an individual or business entity contracted to perform services for the Licensee.
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
## Enforcement
If you are found to be in violation of the license, access to your Tailwind Plus account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued.
The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license.
## Liability
Tailwind Labs Inc.’s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates.
This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order.
## Questions?
Unsure which license you need, or unsure if your use case is covered by our licenses?
Email us at [support@tailwindcss.com](mailto:support@tailwindcss.com) with your questions.
# Catalyst Demo
To run the Catalyst demo, first install the npm dependencies:
```bash
npm install
```
Next, run the development server:
```bash
npm run dev
```
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
/** @type {import('next').NextConfig} */
const nextConfig = {}
export default nextConfig
This diff is collapsed.
{
"name": "catalyst-demo",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@headlessui/react": "^2.2.6",
"@heroicons/react": "^2.2.0",
"clsx": "^2.1.1",
"motion": "^12.23.11",
"next": "16.1.6",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.0",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"postcss": "^8",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.2.0",
"prettier-plugin-tailwindcss": "^0.7.2",
"tailwindcss": "^4.2.0",
"typescript": "^5.8.3"
},
"overrides": {
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3"
}
}
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config
/** @type {import("prettier").Config} */
const config = {
semi: false,
singleQuote: true,
printWidth: 120,
trailingComma: 'es5',
tailwindFunctions: ['clsx', 'tw'],
plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],
tailwindStylesheet: './src/styles/tailwind.css',
}
export default config
<svg width="16" height="12" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12" fill="none">
<g clip-path="url(#clip0_270_55097)">
<path fill="#fff" d="M0 0H16V12H0z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 0h8.5v12H4V0z" fill="#F7FCFF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.164 4.201L7.987 3 8 10h-.343l.21-1.732s-2.305.423-2.115.21c.191-.214.3-.606.3-.606L4 6.474s.324-.004.587-.164c.264-.16-.263-1.109-.263-1.109l1.036.154.392-.435.782.836h.352l-.352-1.914.63.36zM8 10V3l.836 1.201.63-.359-.352 1.914h.352l.782-.836.392.435 1.036-.154s-.527.949-.263 1.109c.263.16.587.164.587.164L9.947 7.872s.11.392.3.606c.191.213-2.115-.21-2.115-.21L8.342 10H8zM12 0h4v12h-4V0zM0 0h4v12H0V0z" fill="#E31D1C"/>
</g>
<defs>
<clipPath id="clip0_270_55097">
<path fill="#fff" d="M0 0H16V12H0z"/>
</clipPath>
</defs>
</svg>
<svg width="16" height="15" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12" fill="none">
<g clip-path="url(#clip0_270_55118)">
<path fill="#fff" d="M0 0H16V12H0z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 0h5v12h-5V0z" fill="#D9071E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0h6v12H0V0z" fill="#006923"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 0h6v12H5V0z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.192 3.957s-.343.266-.279.55c.064.284.74 0 .66-.275-.083-.275-.38-.275-.38-.275z" fill="#FCCA3D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.668 4.634c-.257 0-.364-.214-.311-.442.028-.121.102-.276.222-.476l.43.257a2.861 2.861 0 00-.11.198.96.96 0 01.25.113c.234.154.32.428.07.631-.05.04-.128.173-.171.302.12.007.204.025.283.071.221.13.2.37.032.553-.12.13-.278.237-.429.29-.25.091-.516.057-.516-.263v-.002c0-.013-.002-.036.11-.038h.022-.022c-.19-.003-.231-.104-.255-.283a1.853 1.853 0 01-.012-.182l-.002-.06-.002-.06-.001-.013c-.004-.058-.007-.097.123-.097h-.29v-.5h.29c.164 0 .255.055.307.17.03-.052.06-.101.094-.145a.386.386 0 00-.112-.024zm.318.878l.008-.008-.002.004-.006.004zm-.218.149zm0-.189v.008-.015.007z" fill="#A8AC71"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.633 6.201s.463-1.37-.272-2.08c-.736-.71-1.585-.662-1.585-.662s-.245.182 0 .32c.245.14.142.272.142.272s-.41-.416-.682-.173c-.272.243.254.206.199.39-.056.182-.292.995.053 1.405.344.41-.326.321-.19.321.137 0 .62.096.62 0 0-.095.168.371.312.371s.255-.164.255-.164.201.164.324.164.552-.107.552-.107l-.81-.682s.049-.301-.066-.366.92.577 1.002.784c.08.207.146.207.146.207z" fill="#8F4620"/>
<path d="M5.533 5.863s.075-.239.156-.256c.07-.014.216.115.216.115.26 1.445.889 2.01 1.933 2.01 1.055 0 1.695-.43 2.069-1.762 0 0 .198-.203.268-.18.077.024.172.292.172.292-.095 1.559-1.22 2.482-2.492 2.482-1.284 0-2.333-1.035-2.322-2.7z" fill="#9FAB2F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.667 6.828s1.502.319 2.095.319c.593 0-.52.484-.954.484-.435 0-1.14-.803-1.14-.803z" fill="#2FC2DC"/>
<rect x="7.33337" y="7.05" width="1" height="0.535714" rx="0.0833333" fill="#F9AA51"/>
<path d="M6.088 6.068l.41-.287c.532.76 1.328 1.009 2.443.748l.114.487c-1.303.305-2.31-.01-2.967-.948z" fill="#259485"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.296 6.175s-.607.32-.405.32c.202 0 .994.16.84 0-.154-.16-.435-.32-.435-.32zM7.578 5.794s-.115-.333-.355-.333-.172.273-.288.273c-.115 0 .108.193.207.193.1 0 .436-.133.436-.133z" fill="#FCCA3D"/>
</g>
<defs>
<clipPath id="clip0_270_55118">
<path fill="#fff" d="M0 0H16V12H0z"/>
</clipPath>
</defs>
</svg>
<svg width="16" height="12" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12" fill="none">
<g clip-path="url(#clip0_270_55071)">
<path fill="#fff" d="M0 0H16V12H0z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0h16v12H0V0z" fill="#E31D1C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1v1h16V1H0zm0 2v1h16V3H0zm0 3V5h16v1H0zm0 1v1h16V7H0zm0 3V9h16v1H0zm0 2v-1h16v1H0z" fill="#F7FCFF"/>
<path fill="#2E42A5" d="M0 0H9V7H0z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.04 2.174l.53-.37.411.297h-.233l.471.416-.159.584h-.249l-.242-.536-.206.536H.748l.471.416-.179.657.53-.37.411.297h-.233l.471.416-.159.584h-.249l-.242-.536-.206.536H.748l.471.416-.179.657.53-.37.513.37-.16-.657.412-.416h-.19l.425-.296.411.296h-.233l.471.416-.179.657.53-.37.513.37-.16-.657.412-.416h-.19l.425-.296.411.296h-.233l.471.416-.179.657.53-.37.513.37-.16-.657.412-.416h-.19l.425-.296.411.296h-.233l.471.416-.179.657.53-.37.513.37-.16-.657.412-.416h-.524l-.242-.536-.206.536h-.298l-.142-.584.412-.416h-.19l.425-.296.513.369-.16-.657.412-.416h-.524l-.242-.536-.206.536h-.298l-.142-.584.412-.416h-.19l.425-.296.513.369-.16-.657.412-.416h-.524L7.569.565l-.206.536h-.615l.471.416-.159.584h-.249l-.242-.536-.206.536h-.298l-.142-.584.412-.416h-.524L5.569.565l-.206.536h-.615l.471.416-.159.584h-.249l-.242-.536-.206.536h-.298l-.142-.584.412-.416h-.524L3.569.565l-.206.536h-.615l.471.416-.159.584h-.249l-.242-.536-.206.536h-.298l-.142-.584.412-.416h-.524L1.569.565l-.206.536H.748l.471.416-.179.657zM7.06 4.1l.159-.584-.47-.416h.232l-.411-.296-.425.296h.19l-.412.416.142.584h.298l.206-.536.242.536h.249zm-1.079 0l-.411-.296-.425.296h.19l-.412.416.142.584h.298l.206-.536.242.536h.249l.159-.584-.47-.416h.232zm-1.762.416L4.06 5.1h-.249l-.242-.536-.206.536h-.298l-.142-.584.412-.416h-.19l.425-.296.411.296h-.233l.471.416zm.144-.416h-.298l-.142-.584.412-.416h-.19l.425-.296.411.296h-.233l.471.416-.159.584h-.249l-.242-.536-.206.536zm-1.303 0l.159-.584-.47-.416h.232l-.411-.296-.425.296h.19l-.412.416.142.584h.298l.206-.536.242.536h.249zm3.159-1.584L6.06 3.1h-.249l-.242-.536-.206.536h-.298l-.142-.584.412-.416h-.19l.425-.296.411.296h-.233l.471.416zM3.981 2.1l-.411-.296-.425.296h.19l-.412.416.142.584h.298l.206-.536.242.536h.249l.159-.584-.47-.416h.232z" fill="#F7FCFF"/>
</g>
<defs>
<clipPath id="clip0_270_55071">
<path fill="#fff" d="M0 0H16V12H0z"/>
</clipPath>
</defs>
</svg>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="#18181B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5715 7.74275L11.0003 7.5H21.0008L21.3543 8.35355L19.2709 10.437L18.9173 10.5834H12.9422L9.30034 16.3337L12.9422 22.0841H18.9173L19.2709 22.2305L21.3543 24.3139L21.0008 25.1675H11.0003L10.5715 24.9247L5.57129 16.591V16.0765L10.5715 7.74275Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.4121 12.6466L22.4347 10.624L23.217 10.7203L26.4305 16.0764V16.5909L23.217 21.947L22.4347 22.0433L20.4121 20.0207L20.3433 19.3997L22.2848 16.3337L20.3433 13.2677L20.4121 12.6466Z" fill="white"/>
</svg>
'use client'
import { Avatar } from '@/components/avatar'
import {
Dropdown,
DropdownButton,
DropdownDivider,
DropdownItem,
DropdownLabel,
DropdownMenu,
} from '@/components/dropdown'
import { Navbar, NavbarItem, NavbarSection, NavbarSpacer } from '@/components/navbar'
import {
Sidebar,
SidebarBody,
SidebarFooter,
SidebarHeader,
SidebarHeading,
SidebarItem,
SidebarLabel,
SidebarSection,
SidebarSpacer,
} from '@/components/sidebar'
import { SidebarLayout } from '@/components/sidebar-layout'
import {
ArrowRightStartOnRectangleIcon,
ChevronDownIcon,
ChevronUpIcon,
Cog8ToothIcon,
LightBulbIcon,
PlusIcon,
ShieldCheckIcon,
UserCircleIcon,
} from '@heroicons/react/16/solid'
import {
Cog6ToothIcon,
HomeIcon,
QuestionMarkCircleIcon,
SparklesIcon,
Square2StackIcon,
TicketIcon,
} from '@heroicons/react/20/solid'
import { usePathname } from 'next/navigation'
function AccountDropdownMenu({ anchor }) {
return (
<DropdownMenu className="min-w-64" anchor={anchor}>
<DropdownItem href="#">
<UserCircleIcon />
<DropdownLabel>My account</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="#">
<ShieldCheckIcon />
<DropdownLabel>Privacy policy</DropdownLabel>
</DropdownItem>
<DropdownItem href="#">
<LightBulbIcon />
<DropdownLabel>Share feedback</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/login">
<ArrowRightStartOnRectangleIcon />
<DropdownLabel>Sign out</DropdownLabel>
</DropdownItem>
</DropdownMenu>
)
}
export function ApplicationLayout({ events, children }) {
let pathname = usePathname()
return (
<SidebarLayout
navbar={
<Navbar>
<NavbarSpacer />
<NavbarSection>
<Dropdown>
<DropdownButton as={NavbarItem}>
<Avatar src="/users/erica.jpg" square />
</DropdownButton>
<AccountDropdownMenu anchor="bottom end" />
</Dropdown>
</NavbarSection>
</Navbar>
}
sidebar={
<Sidebar>
<SidebarHeader>
<Dropdown>
<DropdownButton as={SidebarItem}>
<Avatar src="/teams/catalyst.svg" />
<SidebarLabel>Catalyst</SidebarLabel>
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu className="min-w-80 lg:min-w-64" anchor="bottom start">
<DropdownItem href="/settings">
<Cog8ToothIcon />
<DropdownLabel>Settings</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="#">
<Avatar slot="icon" src="/teams/catalyst.svg" />
<DropdownLabel>Catalyst</DropdownLabel>
</DropdownItem>
<DropdownItem href="#">
<Avatar slot="icon" initials="BE" className="bg-purple-500 text-white" />
<DropdownLabel>Big Events</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="#">
<PlusIcon />
<DropdownLabel>New team&hellip;</DropdownLabel>
</DropdownItem>
</DropdownMenu>
</Dropdown>
</SidebarHeader>
<SidebarBody>
<SidebarSection>
<SidebarItem href="/" current={pathname === '/'}>
<HomeIcon />
<SidebarLabel>Home</SidebarLabel>
</SidebarItem>
<SidebarItem href="/events" current={pathname.startsWith('/events')}>
<Square2StackIcon />
<SidebarLabel>Events</SidebarLabel>
</SidebarItem>
<SidebarItem href="/orders" current={pathname.startsWith('/orders')}>
<TicketIcon />
<SidebarLabel>Orders</SidebarLabel>
</SidebarItem>
<SidebarItem href="/settings" current={pathname.startsWith('/settings')}>
<Cog6ToothIcon />
<SidebarLabel>Settings</SidebarLabel>
</SidebarItem>
</SidebarSection>
<SidebarSection className="max-lg:hidden">
<SidebarHeading>Upcoming Events</SidebarHeading>
{events.map((event) => (
<SidebarItem key={event.id} href={event.url}>
{event.name}
</SidebarItem>
))}
</SidebarSection>
<SidebarSpacer />
<SidebarSection>
<SidebarItem href="#">
<QuestionMarkCircleIcon />
<SidebarLabel>Support</SidebarLabel>
</SidebarItem>
<SidebarItem href="#">
<SparklesIcon />
<SidebarLabel>Changelog</SidebarLabel>
</SidebarItem>
</SidebarSection>
</SidebarBody>
<SidebarFooter className="max-lg:hidden">
<Dropdown>
<DropdownButton as={SidebarItem}>
<span className="flex min-w-0 items-center gap-3">
<Avatar src="/users/erica.jpg" className="size-10" square alt="" />
<span className="min-w-0">
<span className="block truncate text-sm/5 font-medium text-zinc-950 dark:text-white">Erica</span>
<span className="block truncate text-xs/5 font-normal text-zinc-500 dark:text-zinc-400">
erica@example.com
</span>
</span>
</span>
<ChevronUpIcon />
</DropdownButton>
<AccountDropdownMenu anchor="top start" />
</Dropdown>
</SidebarFooter>
</Sidebar>
}
>
{children}
</SidebarLayout>
)
}
import { Stat } from '@/app/stat'
import { Badge } from '@/components/badge'
import { Button } from '@/components/button'
import { Heading, Subheading } from '@/components/heading'
import { Link } from '@/components/link'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/table'
import { getEvent, getEventOrders } from '@/data'
import { ChevronLeftIcon } from '@heroicons/react/16/solid'
import { notFound } from 'next/navigation'
export async function generateMetadata({ params }) {
let { id } = await params
let event = await getEvent(id)
return {
title: event?.name,
}
}
export default async function Event({ params }) {
let { id } = await params
let event = await getEvent(id)
let orders = await getEventOrders(id)
if (!event) {
notFound()
}
return (
<>
<div className="max-lg:hidden">
<Link href="/events" className="inline-flex items-center gap-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
<ChevronLeftIcon className="size-4 fill-zinc-400 dark:fill-zinc-500" />
Events
</Link>
</div>
<div className="mt-4 flex flex-wrap items-end justify-between gap-4">
<div className="flex flex-wrap items-center gap-6">
<div className="w-32 shrink-0">
<img className="aspect-3/2 rounded-lg shadow-sm" src={event.imgUrl} alt="" />
</div>
<div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
<Heading>{event.name}</Heading>
<Badge color={event.status === 'On Sale' ? 'lime' : 'zinc'}>{event.status}</Badge>
</div>
<div className="mt-2 text-sm/6 text-zinc-500">
{event.date} at {event.time} <span aria-hidden="true">·</span> {event.location}
</div>
</div>
</div>
<div className="flex gap-4">
<Button outline>Edit</Button>
<Button>View</Button>
</div>
</div>
<div className="mt-8 grid gap-8 sm:grid-cols-3">
<Stat title="Total revenue" value={event.totalRevenue} change={event.totalRevenueChange} />
<Stat
title="Tickets sold"
value={`${event.ticketsSold}/${event.ticketsAvailable}`}
change={event.ticketsSoldChange}
/>
<Stat title="Pageviews" value={event.pageViews} change={event.pageViewsChange} />
</div>
<Subheading className="mt-12">Recent orders</Subheading>
<Table className="mt-4 [--gutter:--spacing(6)] lg:[--gutter:--spacing(10)]">
<TableHead>
<TableRow>
<TableHeader>Order number</TableHeader>
<TableHeader>Purchase date</TableHeader>
<TableHeader>Customer</TableHeader>
<TableHeader className="text-right">Amount</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{orders.map((order) => (
<TableRow key={order.id} href={order.url} title={`Order #${order.id}`}>
<TableCell>{order.id}</TableCell>
<TableCell className="text-zinc-500">{order.date}</TableCell>
<TableCell>{order.customer.name}</TableCell>
<TableCell className="text-right">US{order.amount.usd}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
)
}
import { Badge } from '@/components/badge'
import { Button } from '@/components/button'
import { Divider } from '@/components/divider'
import { Dropdown, DropdownButton, DropdownItem, DropdownMenu } from '@/components/dropdown'
import { Heading } from '@/components/heading'
import { Input, InputGroup } from '@/components/input'
import { Link } from '@/components/link'
import { Select } from '@/components/select'
import { getEvents } from '@/data'
import { EllipsisVerticalIcon, MagnifyingGlassIcon } from '@heroicons/react/16/solid'
export const metadata = {
title: 'Events',
}
export default async function Events() {
let events = await getEvents()
return (
<>
<div className="flex flex-wrap items-end justify-between gap-4">
<div className="max-sm:w-full sm:flex-1">
<Heading>Events</Heading>
<div className="mt-4 flex max-w-xl gap-4">
<div className="flex-1">
<InputGroup>
<MagnifyingGlassIcon />
<Input name="search" placeholder="Search events&hellip;" />
</InputGroup>
</div>
<div>
<Select name="sort_by">
<option value="name">Sort by name</option>
<option value="date">Sort by date</option>
<option value="status">Sort by status</option>
</Select>
</div>
</div>
</div>
<Button>Create event</Button>
</div>
<ul className="mt-10">
{events.map((event, index) => (
<li key={event.id}>
<Divider soft={index > 0} />
<div className="flex items-center justify-between">
<div key={event.id} className="flex gap-6 py-6">
<div className="w-32 shrink-0">
<Link href={event.url} aria-hidden="true">
<img className="aspect-3/2 rounded-lg shadow-sm" src={event.imgUrl} alt="" />
</Link>
</div>
<div className="space-y-1.5">
<div className="text-base/6 font-semibold">
<Link href={event.url}>{event.name}</Link>
</div>
<div className="text-xs/6 text-zinc-500">
{event.date} at {event.time} <span aria-hidden="true">·</span> {event.location}
</div>
<div className="text-xs/6 text-zinc-600">
{event.ticketsSold}/{event.ticketsAvailable} tickets sold
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Badge className="max-sm:hidden" color={event.status === 'On Sale' ? 'lime' : 'zinc'}>
{event.status}
</Badge>
<Dropdown>
<DropdownButton plain aria-label="More options">
<EllipsisVerticalIcon />
</DropdownButton>
<DropdownMenu anchor="bottom end">
<DropdownItem href={event.url}>View</DropdownItem>
<DropdownItem>Edit</DropdownItem>
<DropdownItem>Delete</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</div>
</li>
))}
</ul>
</>
)
}
import { getEvents } from '@/data'
import { ApplicationLayout } from './application-layout'
export default async function RootLayout({ children }) {
let events = await getEvents()
return <ApplicationLayout events={events}>{children}</ApplicationLayout>
}
import { Avatar } from '@/components/avatar'
import { Badge } from '@/components/badge'
import { Button } from '@/components/button'
import { DescriptionDetails, DescriptionList, DescriptionTerm } from '@/components/description-list'
import { Divider } from '@/components/divider'
import { Heading, Subheading } from '@/components/heading'
import { Link } from '@/components/link'
import { getOrder } from '@/data'
import { BanknotesIcon, CalendarIcon, ChevronLeftIcon, CreditCardIcon } from '@heroicons/react/16/solid'
import { notFound } from 'next/navigation'
import { RefundOrder } from './refund'
export async function generateMetadata({ params }) {
let { id } = await params
let order = await getOrder(id)
return {
title: order && `Order #${order.id}`,
}
}
export default async function Order({ params }) {
let { id } = await params
let order = await getOrder(id)
if (!order) {
notFound()
}
return (
<>
<div className="max-lg:hidden">
<Link href="/orders" className="inline-flex items-center gap-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
<ChevronLeftIcon className="size-4 fill-zinc-400 dark:fill-zinc-500" />
Orders
</Link>
</div>
<div className="mt-4 lg:mt-8">
<div className="flex items-center gap-4">
<Heading>Order #{order.id}</Heading>
<Badge color="lime">Successful</Badge>
</div>
<div className="isolate mt-2.5 flex flex-wrap justify-between gap-x-6 gap-y-4">
<div className="flex flex-wrap gap-x-10 gap-y-4 py-1.5">
<span className="flex items-center gap-3 text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white">
<BanknotesIcon className="size-4 shrink-0 fill-zinc-400 dark:fill-zinc-500" />
<span>US{order.amount.usd}</span>
</span>
<span className="flex items-center gap-3 text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white">
<CreditCardIcon className="size-4 shrink-0 fill-zinc-400 dark:fill-zinc-500" />
<span className="inline-flex gap-3">
{order.payment.card.type}{' '}
<span>
<span aria-hidden="true">••••</span> {order.payment.card.number}
</span>
</span>
</span>
<span className="flex items-center gap-3 text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white">
<CalendarIcon className="size-4 shrink-0 fill-zinc-400 dark:fill-zinc-500" />
<span>{order.date}</span>
</span>
</div>
<div className="flex gap-4">
<RefundOrder outline amount={order.amount.usd}>
Refund
</RefundOrder>
<Button>Resend Invoice</Button>
</div>
</div>
</div>
<div className="mt-12">
<Subheading>Summary</Subheading>
<Divider className="mt-4" />
<DescriptionList>
<DescriptionTerm>Customer</DescriptionTerm>
<DescriptionDetails>{order.customer.name}</DescriptionDetails>
<DescriptionTerm>Event</DescriptionTerm>
<DescriptionDetails>
<Link href={order.event.url} className="flex items-center gap-2">
<Avatar src={order.event.thumbUrl} className="size-6" />
<span>{order.event.name}</span>
</Link>
</DescriptionDetails>
<DescriptionTerm>Amount</DescriptionTerm>
<DescriptionDetails>US{order.amount.usd}</DescriptionDetails>
<DescriptionTerm>Amount after exchange rate</DescriptionTerm>
<DescriptionDetails>
US{order.amount.usd} &rarr; CA{order.amount.cad}
</DescriptionDetails>
<DescriptionTerm>Fee</DescriptionTerm>
<DescriptionDetails>CA{order.amount.fee}</DescriptionDetails>
<DescriptionTerm>Net</DescriptionTerm>
<DescriptionDetails>CA{order.amount.net}</DescriptionDetails>
</DescriptionList>
</div>
<div className="mt-12">
<Subheading>Payment method</Subheading>
<Divider className="mt-4" />
<DescriptionList>
<DescriptionTerm>Transaction ID</DescriptionTerm>
<DescriptionDetails>{order.payment.transactionId}</DescriptionDetails>
<DescriptionTerm>Card number</DescriptionTerm>
<DescriptionDetails>•••• {order.payment.card.number}</DescriptionDetails>
<DescriptionTerm>Card type</DescriptionTerm>
<DescriptionDetails>{order.payment.card.type}</DescriptionDetails>
<DescriptionTerm>Card expiry</DescriptionTerm>
<DescriptionDetails>{order.payment.card.expiry}</DescriptionDetails>
<DescriptionTerm>Owner</DescriptionTerm>
<DescriptionDetails>{order.customer.name}</DescriptionDetails>
<DescriptionTerm>Email address</DescriptionTerm>
<DescriptionDetails>{order.customer.email}</DescriptionDetails>
<DescriptionTerm>Address</DescriptionTerm>
<DescriptionDetails>{order.customer.address}</DescriptionDetails>
<DescriptionTerm>Country</DescriptionTerm>
<DescriptionDetails>
<span className="inline-flex gap-3">
<img src={order.customer.countryFlagUrl} alt={order.customer.country} />
{order.customer.country}
</span>
</DescriptionDetails>
<DescriptionTerm>CVC</DescriptionTerm>
<DescriptionDetails>
<Badge color="lime">Passed successfully</Badge>
</DescriptionDetails>
</DescriptionList>
</div>
</>
)
}
'use client'
import { Button } from '@/components/button'
import { Checkbox, CheckboxField } from '@/components/checkbox'
import { Dialog, DialogActions, DialogBody, DialogDescription, DialogTitle } from '@/components/dialog'
import { Description, Field, FieldGroup, Label } from '@/components/fieldset'
import { Input } from '@/components/input'
import { Select } from '@/components/select'
import { useState } from 'react'
export function RefundOrder({ amount, ...props }) {
let [isOpen, setIsOpen] = useState(false)
return (
<>
<Button type="button" onClick={() => setIsOpen(true)} {...props} />
<Dialog open={isOpen} onClose={setIsOpen}>
<DialogTitle>Refund payment</DialogTitle>
<DialogDescription>
The refund will be reflected in the customer’s bank account 2 to 3 business days after processing.
</DialogDescription>
<DialogBody>
<FieldGroup>
<Field>
<Label>Amount</Label>
<Input name="amount" defaultValue={amount} placeholder="$0.00" autoFocus />
</Field>
<Field>
<Label>Reason</Label>
<Select name="reason" defaultValue="">
<option value="" disabled>
Select a reason&hellip;
</option>
<option value="duplicate">Duplicate</option>
<option value="fraudulent">Fraudulent</option>
<option value="requested_by_customer">Requested by customer</option>
<option value="other">Other</option>
</Select>
</Field>
<CheckboxField>
<Checkbox name="notify" />
<Label>Notify customer</Label>
<Description>An email notification will be sent to this customer.</Description>
</CheckboxField>
</FieldGroup>
</DialogBody>
<DialogActions>
<Button plain onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button onClick={() => setIsOpen(false)}>Refund</Button>
</DialogActions>
</Dialog>
</>
)
}
import { Avatar } from '@/components/avatar'
import { Button } from '@/components/button'
import { Heading } from '@/components/heading'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/table'
import { getOrders } from '@/data'
export const metadata = {
title: 'Orders',
}
export default async function Orders() {
let orders = await getOrders()
return (
<>
<div className="flex items-end justify-between gap-4">
<Heading>Orders</Heading>
<Button className="-my-0.5">Create order</Button>
</div>
<Table className="mt-8 [--gutter:--spacing(6)] lg:[--gutter:--spacing(10)]">
<TableHead>
<TableRow>
<TableHeader>Order number</TableHeader>
<TableHeader>Purchase date</TableHeader>
<TableHeader>Customer</TableHeader>
<TableHeader>Event</TableHeader>
<TableHeader className="text-right">Amount</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{orders.map((order) => (
<TableRow key={order.id} href={order.url} title={`Order #${order.id}`}>
<TableCell>{order.id}</TableCell>
<TableCell className="text-zinc-500">{order.date}</TableCell>
<TableCell>{order.customer.name}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Avatar src={order.event.thumbUrl} className="size-6" />
<span>{order.event.name}</span>
</div>
</TableCell>
<TableCell className="text-right">US{order.amount.usd}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
)
}
import { Stat } from '@/app/stat'
import { Avatar } from '@/components/avatar'
import { Heading, Subheading } from '@/components/heading'
import { Select } from '@/components/select'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/table'
import { getRecentOrders } from '@/data'
export default async function Home() {
let orders = await getRecentOrders()
return (
<>
<Heading>Good afternoon, Erica</Heading>
<div className="mt-8 flex items-end justify-between">
<Subheading>Overview</Subheading>
<div>
<Select name="period">
<option value="last_week">Last week</option>
<option value="last_two">Last two weeks</option>
<option value="last_month">Last month</option>
<option value="last_quarter">Last quarter</option>
</Select>
</div>
</div>
<div className="mt-4 grid gap-8 sm:grid-cols-2 xl:grid-cols-4">
<Stat title="Total revenue" value="$2.6M" change="+4.5%" />
<Stat title="Average order value" value="$455" change="-0.5%" />
<Stat title="Tickets sold" value="5,888" change="+4.5%" />
<Stat title="Pageviews" value="823,067" change="+21.2%" />
</div>
<Subheading className="mt-14">Recent orders</Subheading>
<Table className="mt-4 [--gutter:--spacing(6)] lg:[--gutter:--spacing(10)]">
<TableHead>
<TableRow>
<TableHeader>Order number</TableHeader>
<TableHeader>Purchase date</TableHeader>
<TableHeader>Customer</TableHeader>
<TableHeader>Event</TableHeader>
<TableHeader className="text-right">Amount</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{orders.map((order) => (
<TableRow key={order.id} href={order.url} title={`Order #${order.id}`}>
<TableCell>{order.id}</TableCell>
<TableCell className="text-zinc-500">{order.date}</TableCell>
<TableCell>{order.customer.name}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Avatar src={order.event.thumbUrl} className="size-6" />
<span>{order.event.name}</span>
</div>
</TableCell>
<TableCell className="text-right">US{order.amount.usd}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
)
}
'use client'
import { Input } from '@/components/input'
import { Listbox, ListboxLabel, ListboxOption } from '@/components/listbox'
import { getCountries } from '@/data'
import { useState } from 'react'
export function Address() {
let countries = getCountries()
let [country, setCountry] = useState(countries[0])
return (
<div className="grid grid-cols-2 gap-6">
<Input
aria-label="Street Address"
name="address"
placeholder="Street Address"
defaultValue="147 Catalyst Ave"
className="col-span-2"
/>
<Input aria-label="City" name="city" placeholder="City" defaultValue="Toronto" className="col-span-2" />
<Listbox aria-label="Region" name="region" placeholder="Region" defaultValue="Ontario">
{country.regions.map((region) => (
<ListboxOption key={region} value={region}>
<ListboxLabel>{region}</ListboxLabel>
</ListboxOption>
))}
</Listbox>
<Input aria-label="Postal code" name="postal_code" placeholder="Postal Code" defaultValue="A1A 1A1" />
<Listbox
aria-label="Country"
name="country"
placeholder="Country"
by="code"
value={country}
onChange={(country) => setCountry(country)}
className="col-span-2"
>
{countries.map((country) => (
<ListboxOption key={country.code} value={country}>
<img className="w-5 sm:w-4" src={country.flagUrl} alt="" />
<ListboxLabel>{country.name}</ListboxLabel>
</ListboxOption>
))}
</Listbox>
</div>
)
}
import { Button } from '@/components/button'
import { Checkbox, CheckboxField } from '@/components/checkbox'
import { Divider } from '@/components/divider'
import { Label } from '@/components/fieldset'
import { Heading, Subheading } from '@/components/heading'
import { Input } from '@/components/input'
import { Select } from '@/components/select'
import { Text } from '@/components/text'
import { Textarea } from '@/components/textarea'
import { Address } from './address'
export const metadata = {
title: 'Settings',
}
export default function Settings() {
return (
<form method="post" className="mx-auto max-w-4xl">
<Heading>Settings</Heading>
<Divider className="my-10 mt-6" />
<section className="grid gap-x-8 gap-y-6 sm:grid-cols-2">
<div className="space-y-1">
<Subheading>Organization Name</Subheading>
<Text>This will be displayed on your public profile.</Text>
</div>
<div>
<Input aria-label="Organization Name" name="name" defaultValue="Catalyst" />
</div>
</section>
<Divider className="my-10" soft />
<section className="grid gap-x-8 gap-y-6 sm:grid-cols-2">
<div className="space-y-1">
<Subheading>Organization Bio</Subheading>
<Text>This will be displayed on your public profile. Maximum 240 characters.</Text>
</div>
<div>
<Textarea aria-label="Organization Bio" name="bio" />
</div>
</section>
<Divider className="my-10" soft />
<section className="grid gap-x-8 gap-y-6 sm:grid-cols-2">
<div className="space-y-1">
<Subheading>Organization Email</Subheading>
<Text>This is how customers can contact you for support.</Text>
</div>
<div className="space-y-4">
<Input type="email" aria-label="Organization Email" name="email" defaultValue="info@example.com" />
<CheckboxField>
<Checkbox name="email_is_public" defaultChecked />
<Label>Show email on public profile</Label>
</CheckboxField>
</div>
</section>
<Divider className="my-10" soft />
<section className="grid gap-x-8 gap-y-6 sm:grid-cols-2">
<div className="space-y-1">
<Subheading>Address</Subheading>
<Text>This is where your organization is registered.</Text>
</div>
<Address />
</section>
<Divider className="my-10" soft />
<section className="grid gap-x-8 gap-y-6 sm:grid-cols-2">
<div className="space-y-1">
<Subheading>Currency</Subheading>
<Text>The currency that your organization will be collecting.</Text>
</div>
<div>
<Select aria-label="Currency" name="currency" defaultValue="cad">
<option value="cad">CAD - Canadian Dollar</option>
<option value="usd">USD - United States Dollar</option>
</Select>
</div>
</section>
<Divider className="my-10" soft />
<div className="flex justify-end gap-4">
<Button type="reset" plain>
Reset
</Button>
<Button type="submit">Save changes</Button>
</div>
</form>
)
}
import { Logo } from '@/app/logo'
import { Button } from '@/components/button'
import { Field, Label } from '@/components/fieldset'
import { Heading } from '@/components/heading'
import { Input } from '@/components/input'
import { Strong, Text, TextLink } from '@/components/text'
export const metadata = {
title: 'Forgot password',
}
export default function Login() {
return (
<form action="" method="POST" className="grid w-full max-w-sm grid-cols-1 gap-8">
<Logo className="h-6 text-zinc-950 dark:text-white forced-colors:text-[CanvasText]" />
<Heading>Reset your password</Heading>
<Text>Enter your email and we’ll send you a link to reset your password.</Text>
<Field>
<Label>Email</Label>
<Input type="email" name="email" />
</Field>
<Button type="submit" className="w-full">
Reset password
</Button>
<Text>
Don’t have an account?{' '}
<TextLink href="/register">
<Strong>Sign up</Strong>
</TextLink>
</Text>
</form>
)
}
import { AuthLayout } from '@/components/auth-layout'
export default async function RootLayout({ children }) {
return <AuthLayout>{children}</AuthLayout>
}
import { Logo } from '@/app/logo'
import { Button } from '@/components/button'
import { Checkbox, CheckboxField } from '@/components/checkbox'
import { Field, Label } from '@/components/fieldset'
import { Heading } from '@/components/heading'
import { Input } from '@/components/input'
import { Strong, Text, TextLink } from '@/components/text'
export const metadata = {
title: 'Login',
}
export default function Login() {
return (
<form action="" method="POST" className="grid w-full max-w-sm grid-cols-1 gap-8">
<Logo className="h-6 text-zinc-950 dark:text-white forced-colors:text-[CanvasText]" />
<Heading>Sign in to your account</Heading>
<Field>
<Label>Email</Label>
<Input type="email" name="email" />
</Field>
<Field>
<Label>Password</Label>
<Input type="password" name="password" />
</Field>
<div className="flex items-center justify-between">
<CheckboxField>
<Checkbox name="remember" />
<Label>Remember me</Label>
</CheckboxField>
<Text>
<TextLink href="/forgot-password">
<Strong>Forgot password?</Strong>
</TextLink>
</Text>
</div>
<Button type="submit" className="w-full">
Login
</Button>
<Text>
Don’t have an account?{' '}
<TextLink href="/register">
<Strong>Sign up</Strong>
</TextLink>
</Text>
</form>
)
}
import { Logo } from '@/app/logo'
import { Button } from '@/components/button'
import { Checkbox, CheckboxField } from '@/components/checkbox'
import { Field, Label } from '@/components/fieldset'
import { Heading } from '@/components/heading'
import { Input } from '@/components/input'
import { Select } from '@/components/select'
import { Strong, Text, TextLink } from '@/components/text'
export const metadata = {
title: 'Register',
}
export default function Login() {
return (
<form action="" method="POST" className="grid w-full max-w-sm grid-cols-1 gap-8">
<Logo className="h-6 text-zinc-950 dark:text-white forced-colors:text-[CanvasText]" />
<Heading>Create your account</Heading>
<Field>
<Label>Email</Label>
<Input type="email" name="email" />
</Field>
<Field>
<Label>Full name</Label>
<Input name="name" />
</Field>
<Field>
<Label>Password</Label>
<Input type="password" name="password" autoComplete="new-password" />
</Field>
<Field>
<Label>Country</Label>
<Select name="country">
<option>Canada</option>
<option>Mexico</option>
<option>United States</option>
</Select>
</Field>
<CheckboxField>
<Checkbox name="remember" />
<Label>Get emails about product updates and news.</Label>
</CheckboxField>
<Button type="submit" className="w-full">
Create account
</Button>
<Text>
Already have an account?{' '}
<TextLink href="/login">
<Strong>Sign in</Strong>
</TextLink>
</Text>
</form>
)
}
import '@/styles/tailwind.css'
export const metadata = {
title: {
template: '%s - Catalyst',
default: 'Catalyst',
},
description: '',
}
export default async function RootLayout({ children }) {
return (
<html
lang="en"
className="text-zinc-950 antialiased lg:bg-zinc-100 dark:bg-zinc-900 dark:text-white dark:lg:bg-zinc-950"
>
<head>
<link rel="preconnect" href="https://rsms.me/" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
</head>
<body>{children}</body>
</html>
)
}
export function Logo({ className, ...props }) {
return (
<svg fill="currentColor" viewBox="0 0 113 22" {...props} className={className}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.99906 0.5L6.57031 0.742752L0.570312 10.7428V11.2572L6.57031 21.2572L6.99906 21.5H18.9991L19.3526 20.6464L16.8526 18.1464L16.4991 18H9.27424L4.8409 11L9.27424 4H16.4991L16.8526 3.85355L19.3526 1.35355L18.9991 0.5H6.99906Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20.7927 4.21875L18.3657 6.64575L18.2969 7.2668L20.6605 10.9993L18.2969 14.7318L18.3657 15.3529L20.7927 17.7799L21.5751 17.6835L25.4311 11.2565V10.7421L21.5751 4.31507L20.7927 4.21875Z"
/>
<path d="M41.0629 18.2529C40.0969 18.2529 39.1729 18.0779 38.2909 17.7279C37.4249 17.3884 36.6385 16.8736 35.9809 16.2159C35.3229 15.5579 34.7909 14.7529 34.3849 13.8009C33.9929 12.8349 33.7969 11.7569 33.7969 10.5669C33.7969 9.39086 33.9999 8.33386 34.4059 7.39586C34.8119 6.44386 35.3509 5.64586 36.0229 5.00186C36.6826 4.3518 37.4688 3.8443 38.3329 3.51086C39.2011 3.16351 40.1277 2.98531 41.0629 2.98586C41.8968 2.98056 42.7262 3.10816 43.5199 3.36386C44.2899 3.60186 44.9619 3.95186 45.5359 4.41386C46.1217 4.85944 46.6141 5.41603 46.9849 6.05186C47.3769 6.68186 47.6289 7.38186 47.7409 8.15186H44.8639C44.6556 7.37614 44.1795 6.69917 43.5199 6.24086C42.8479 5.75086 42.0289 5.50586 41.0629 5.50586C40.4749 5.50586 39.9219 5.62486 39.4039 5.86286C38.8808 6.1047 38.4154 6.45554 38.0389 6.89186C37.6609 7.32586 37.3599 7.85786 37.1359 8.48786C36.9259 9.10386 36.8209 9.79686 36.8209 10.5669C36.8209 11.3369 36.9329 12.0439 37.1569 12.6879C37.3809 13.3319 37.6819 13.8779 38.0599 14.3259C38.4379 14.7739 38.8859 15.1239 39.4039 15.3759C39.9205 15.6275 40.4882 15.7569 41.0629 15.7539C42.0289 15.7539 42.8409 15.5159 43.4989 15.0399C44.1569 14.5639 44.6189 13.9129 44.8849 13.0869H47.7619C47.6318 13.8444 47.3611 14.5709 46.9639 15.2289C46.5931 15.8647 46.1007 16.4213 45.5149 16.8669C44.9013 17.314 44.2209 17.6613 43.4989 17.8959C42.7099 18.1388 41.8884 18.2592 41.0629 18.2529ZM57.0079 18.0009V16.4259C56.6978 16.988 56.2308 17.4477 55.6639 17.7489C55.0759 18.0709 54.3839 18.2319 53.5849 18.2319C52.9173 18.2353 52.255 18.1142 51.6319 17.8749C51.0354 17.6267 50.4985 17.2544 50.0569 16.7829C49.6089 16.2929 49.2519 15.6909 48.9869 14.9769C48.7339 14.2489 48.6079 13.4159 48.6079 12.4779C48.6079 11.5819 48.7409 10.7839 49.0079 10.0839C49.2879 9.38386 49.6509 8.79586 50.0989 8.31986C50.5479 7.85254 51.0922 7.48728 51.6949 7.24886C52.3009 6.99881 52.9503 6.87038 53.6059 6.87086C54.3899 6.87086 55.0619 7.02486 55.6219 7.33286C56.1959 7.62686 56.6369 8.02586 56.9449 8.52986V7.12286H59.6549V18.0009H57.0079ZM54.1939 16.0059C54.9639 16.0059 55.6219 15.6979 56.1679 15.0819C56.7279 14.4519 57.0079 13.5839 57.0079 12.4779C57.0079 11.4279 56.7349 10.6019 56.1889 9.99986C55.6569 9.39786 54.9919 9.09686 54.1939 9.09686C53.3819 9.09686 52.7099 9.39786 52.1779 9.99986C51.6459 10.6019 51.3799 11.4279 51.3799 12.4779C51.3799 13.5839 51.6459 14.4519 52.1779 15.0819C52.4227 15.3777 52.7311 15.6145 53.0802 15.7745C53.4292 15.9345 53.81 16.0136 54.1939 16.0059ZM62.4869 4.45586H65.1539V7.12286H67.6739V9.18086H65.1539V13.6119L62.4869 15.9429V9.18086H60.6599V7.12286H62.4869V4.45586ZM67.6739 18.0009H62.4869V15.9429H67.6739V18.0009ZM76.5019 18.0009V16.4259C76.1918 16.988 75.7248 17.4477 75.1579 17.7489C74.5699 18.0709 73.8779 18.2319 73.0789 18.2319C72.4113 18.2353 71.749 18.1142 71.1259 17.8749C70.5294 17.6267 69.9925 17.2544 69.5509 16.7829C69.1029 16.2929 68.7459 15.6909 68.4809 14.9769C68.2279 14.2489 68.1019 13.4159 68.1019 12.4779C68.1019 11.5819 68.2349 10.7839 68.5019 10.0839C68.7819 9.38386 69.1449 8.79586 69.5929 8.31986C70.0419 7.85254 70.5862 7.48728 71.1889 7.24886C71.7949 6.99881 72.4443 6.87038 73.0999 6.87086C73.8839 6.87086 74.5559 7.02486 75.1159 7.33286C75.6899 7.62686 76.1309 8.02586 76.4389 8.52986V7.12286H79.1489V18.0009H76.5019ZM73.6879 16.0059C74.4579 16.0059 75.1159 15.6979 75.6619 15.0819C76.2219 14.4519 76.5019 13.5839 76.5019 12.4779C76.5019 11.4279 76.2289 10.6019 75.6829 9.99986C75.1509 9.39786 74.4859 9.09686 73.6879 9.09686C72.8759 9.09686 72.2039 9.39786 71.6719 9.99986C71.1399 10.6019 70.8739 11.4279 70.8739 12.4779C70.8739 13.5839 71.1399 14.4519 71.6719 15.0819C71.9166 15.3777 72.2251 15.6145 72.5742 15.7745C72.9232 15.9345 73.304 16.0136 73.6879 16.0059ZM81.3299 18.0009V2.88086H84.0179V18.0009H81.3299ZM92.9789 7.12286H95.7509L89.8709 21.9909H87.0989L92.9789 7.12286ZM84.7469 7.12286H87.6239L90.3119 13.8429H87.6019L84.7469 7.12286ZM100.656 18.2529C100.039 18.2564 99.4251 18.1787 98.8289 18.0219C98.2886 17.8906 97.7764 17.663 97.3169 17.3499C96.8843 17.0429 96.5199 16.6496 96.2469 16.1949C95.9669 15.7329 95.7909 15.1869 95.7209 14.5569H98.2619C98.3599 15.1309 98.6329 15.5579 99.0819 15.8379C99.5289 16.1039 100.068 16.2369 100.698 16.2369C101.216 16.2369 101.678 16.1389 102.084 15.9429C102.504 15.7329 102.714 15.4109 102.714 14.9769C102.714 14.5849 102.525 14.2909 102.147 14.0949C101.783 13.8989 101.244 13.7379 100.53 13.6119L99.4379 13.4229C98.3459 13.2409 97.4849 12.8909 96.8549 12.3729C96.2389 11.8549 95.9309 11.1759 95.9309 10.3359C95.9309 9.78986 96.0429 9.29986 96.2669 8.86586C96.5049 8.43186 96.8269 8.06786 97.2329 7.77386C97.6594 7.46999 98.1355 7.24261 98.6399 7.10186C99.205 6.93106 99.7925 6.84611 100.383 6.84986C100.971 6.84986 101.531 6.92686 102.063 7.08086C102.609 7.22086 103.085 7.43786 103.491 7.73186C103.897 8.02586 104.233 8.38286 104.499 8.80286C104.779 9.22286 104.954 9.70586 105.024 10.2519H102.525C102.399 9.78986 102.14 9.44686 101.748 9.22286C101.37 8.98486 100.894 8.86586 100.32 8.86586C99.8019 8.86586 99.3609 8.97786 98.9969 9.20186C98.6469 9.42586 98.4719 9.71986 98.4719 10.0839C98.4719 10.4339 98.6259 10.6929 98.9339 10.8609C99.2419 11.0289 99.7039 11.1619 100.32 11.2599L101.412 11.4489C102.602 11.6589 103.547 12.0229 104.247 12.5409C104.947 13.0449 105.297 13.7659 105.297 14.7039C105.297 15.2779 105.164 15.7889 104.898 16.2369C104.643 16.674 104.3 17.0531 103.89 17.3499C103.436 17.6593 102.931 17.8867 102.399 18.0219C101.831 18.1769 101.245 18.2546 100.656 18.2529ZM107.387 4.45586H110.054V7.12286H112.574V9.18086H110.054V13.6119L107.387 15.9429V9.18086H105.56V7.12286H107.387V4.45586ZM112.574 18.0009H107.387V15.9429H112.574V18.0009Z" />
</svg>
)
}
import { Badge } from '@/components/badge'
import { Divider } from '@/components/divider'
export function Stat({ title, value, change }) {
return (
<div>
<Divider />
<div className="mt-6 text-lg/6 font-medium sm:text-sm/6">{title}</div>
<div className="mt-3 text-3xl/8 font-semibold sm:text-2xl/8">{value}</div>
<div className="mt-3 text-sm/6 sm:text-xs/6">
<Badge color={change.startsWith('+') ? 'lime' : 'pink'}>{change}</Badge>{' '}
<span className="text-zinc-500">from last week</span>
</div>
</div>
)
}
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { Text } from './text'
const sizes = {
xs: 'sm:max-w-xs',
sm: 'sm:max-w-sm',
md: 'sm:max-w-md',
lg: 'sm:max-w-lg',
xl: 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
'3xl': 'sm:max-w-3xl',
'4xl': 'sm:max-w-4xl',
'5xl': 'sm:max-w-5xl',
}
export function Alert({ size = 'md', className, children, ...props }) {
return (
<Headless.Dialog {...props}>
<Headless.DialogBackdrop
transition
className="fixed inset-0 flex w-screen justify-center overflow-y-auto bg-zinc-950/15 px-2 py-2 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-6 sm:py-8 lg:px-8 lg:py-16 dark:bg-zinc-950/50"
/>
<div className="fixed inset-0 w-screen overflow-y-auto pt-6 sm:pt-0">
<div className="grid min-h-full grid-rows-[1fr_auto_1fr] justify-items-center p-8 sm:grid-rows-[1fr_auto_3fr] sm:p-4">
<Headless.DialogPanel
transition
className={clsx(
className,
sizes[size],
'row-start-2 w-full rounded-2xl bg-white p-8 shadow-lg ring-1 ring-zinc-950/10 sm:rounded-2xl sm:p-6 dark:bg-zinc-900 dark:ring-white/10 forced-colors:outline',
'transition duration-100 will-change-transform data-closed:opacity-0 data-enter:ease-out data-closed:data-enter:scale-95 data-leave:ease-in'
)}
>
{children}
</Headless.DialogPanel>
</div>
</div>
</Headless.Dialog>
)
}
export function AlertTitle({ className, ...props }) {
return (
<Headless.DialogTitle
{...props}
className={clsx(
className,
'text-center text-base/6 font-semibold text-balance text-zinc-950 sm:text-left sm:text-sm/6 sm:text-wrap dark:text-white'
)}
/>
)
}
export function AlertDescription({ className, ...props }) {
return (
<Headless.Description
as={Text}
{...props}
className={clsx(className, 'mt-2 text-center text-pretty sm:text-left')}
/>
)
}
export function AlertBody({ className, ...props }) {
return <div {...props} className={clsx(className, 'mt-4')} />
}
export function AlertActions({ className, ...props }) {
return (
<div
{...props}
className={clsx(
className,
'mt-6 flex flex-col-reverse items-center justify-end gap-3 *:w-full sm:mt-4 sm:flex-row sm:*:w-auto'
)}
/>
)
}
export function AuthLayout({ children }) {
return (
<main className="flex min-h-dvh flex-col p-2">
<div className="flex grow items-center justify-center p-6 lg:rounded-lg lg:bg-white lg:p-10 lg:shadow-xs lg:ring-1 lg:ring-zinc-950/5 dark:lg:bg-zinc-900 dark:lg:ring-white/10">
{children}
</div>
</main>
)
}
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { forwardRef } from 'react'
import { TouchTarget } from './button'
import { Link } from './link'
export function Avatar({ src = null, square = false, initials, alt = '', className, ...props }) {
return (
<span
data-slot="avatar"
{...props}
className={clsx(
className,
// Basic layout
'inline-grid shrink-0 align-middle [--avatar-radius:20%] *:col-start-1 *:row-start-1',
'outline -outline-offset-1 outline-black/10 dark:outline-white/10',
// Border radius
square ? 'rounded-(--avatar-radius) *:rounded-(--avatar-radius)' : 'rounded-full *:rounded-full'
)}
>
{initials && (
<svg
className="size-full fill-current p-[5%] text-[48px] font-medium uppercase select-none"
viewBox="0 0 100 100"
aria-hidden={alt ? undefined : 'true'}
>
{alt && <title>{alt}</title>}
<text x="50%" y="50%" alignmentBaseline="middle" dominantBaseline="middle" textAnchor="middle" dy=".125em">
{initials}
</text>
</svg>
)}
{src && <img className="size-full" src={src} alt={alt} />}
</span>
)
}
export const AvatarButton = forwardRef(function AvatarButton(
{ src, square = false, initials, alt, className, ...props },
ref
) {
let classes = clsx(
className,
square ? 'rounded-[20%]' : 'rounded-full',
'relative inline-grid focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500'
)
return typeof props.href === 'string' ? (
<Link {...props} className={classes} ref={ref}>
<TouchTarget>
<Avatar src={src} square={square} initials={initials} alt={alt} />
</TouchTarget>
</Link>
) : (
<Headless.Button {...props} className={classes} ref={ref}>
<TouchTarget>
<Avatar src={src} square={square} initials={initials} alt={alt} />
</TouchTarget>
</Headless.Button>
)
})
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { forwardRef } from 'react'
import { TouchTarget } from './button'
import { Link } from './link'
const colors = {
red: 'bg-red-500/15 text-red-700 group-data-hover:bg-red-500/25 dark:bg-red-500/10 dark:text-red-400 dark:group-data-hover:bg-red-500/20',
orange:
'bg-orange-500/15 text-orange-700 group-data-hover:bg-orange-500/25 dark:bg-orange-500/10 dark:text-orange-400 dark:group-data-hover:bg-orange-500/20',
amber:
'bg-amber-400/20 text-amber-700 group-data-hover:bg-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400 dark:group-data-hover:bg-amber-400/15',
yellow:
'bg-yellow-400/20 text-yellow-700 group-data-hover:bg-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:group-data-hover:bg-yellow-400/15',
lime: 'bg-lime-400/20 text-lime-700 group-data-hover:bg-lime-400/30 dark:bg-lime-400/10 dark:text-lime-300 dark:group-data-hover:bg-lime-400/15',
green:
'bg-green-500/15 text-green-700 group-data-hover:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:group-data-hover:bg-green-500/20',
emerald:
'bg-emerald-500/15 text-emerald-700 group-data-hover:bg-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:group-data-hover:bg-emerald-500/20',
teal: 'bg-teal-500/15 text-teal-700 group-data-hover:bg-teal-500/25 dark:bg-teal-500/10 dark:text-teal-300 dark:group-data-hover:bg-teal-500/20',
cyan: 'bg-cyan-400/20 text-cyan-700 group-data-hover:bg-cyan-400/30 dark:bg-cyan-400/10 dark:text-cyan-300 dark:group-data-hover:bg-cyan-400/15',
sky: 'bg-sky-500/15 text-sky-700 group-data-hover:bg-sky-500/25 dark:bg-sky-500/10 dark:text-sky-300 dark:group-data-hover:bg-sky-500/20',
blue: 'bg-blue-500/15 text-blue-700 group-data-hover:bg-blue-500/25 dark:text-blue-400 dark:group-data-hover:bg-blue-500/25',
indigo:
'bg-indigo-500/15 text-indigo-700 group-data-hover:bg-indigo-500/25 dark:text-indigo-400 dark:group-data-hover:bg-indigo-500/20',
violet:
'bg-violet-500/15 text-violet-700 group-data-hover:bg-violet-500/25 dark:text-violet-400 dark:group-data-hover:bg-violet-500/20',
purple:
'bg-purple-500/15 text-purple-700 group-data-hover:bg-purple-500/25 dark:text-purple-400 dark:group-data-hover:bg-purple-500/20',
fuchsia:
'bg-fuchsia-400/15 text-fuchsia-700 group-data-hover:bg-fuchsia-400/25 dark:bg-fuchsia-400/10 dark:text-fuchsia-400 dark:group-data-hover:bg-fuchsia-400/20',
pink: 'bg-pink-400/15 text-pink-700 group-data-hover:bg-pink-400/25 dark:bg-pink-400/10 dark:text-pink-400 dark:group-data-hover:bg-pink-400/20',
rose: 'bg-rose-400/15 text-rose-700 group-data-hover:bg-rose-400/25 dark:bg-rose-400/10 dark:text-rose-400 dark:group-data-hover:bg-rose-400/20',
zinc: 'bg-zinc-600/10 text-zinc-700 group-data-hover:bg-zinc-600/20 dark:bg-white/5 dark:text-zinc-400 dark:group-data-hover:bg-white/10',
}
export function Badge({ color = 'zinc', className, ...props }) {
return (
<span
{...props}
className={clsx(
className,
'inline-flex items-center gap-x-1.5 rounded-md px-1.5 py-0.5 text-sm/5 font-medium sm:text-xs/5 forced-colors:outline',
colors[color]
)}
/>
)
}
export const BadgeButton = forwardRef(function BadgeButton(
{ color = 'zinc', className, children, ...props },
ref
) {
let classes = clsx(
className,
'group relative inline-flex rounded-md focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500'
)
return typeof props.href === 'string' ? (
<Link {...props} className={classes} ref={ref}>
<TouchTarget>
<Badge color={color}>{children}</Badge>
</TouchTarget>
</Link>
) : (
<Headless.Button {...props} className={classes} ref={ref}>
<TouchTarget>
<Badge color={color}>{children}</Badge>
</TouchTarget>
</Headless.Button>
)
})
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
export function CheckboxGroup({ className, ...props }) {
return (
<div
data-slot="control"
{...props}
className={clsx(
className,
// Basic groups
'space-y-3',
// With descriptions
'has-data-[slot=description]:space-y-6 has-data-[slot=description]:**:data-[slot=label]:font-medium'
)}
/>
)
}
export function CheckboxField({ className, ...props }) {
return (
<Headless.Field
data-slot="field"
{...props}
className={clsx(
className,
// Base layout
'grid grid-cols-[1.125rem_1fr] gap-x-4 gap-y-1 sm:grid-cols-[1rem_1fr]',
// Control layout
'*:data-[slot=control]:col-start-1 *:data-[slot=control]:row-start-1 *:data-[slot=control]:mt-0.75 sm:*:data-[slot=control]:mt-1',
// Label layout
'*:data-[slot=label]:col-start-2 *:data-[slot=label]:row-start-1',
// Description layout
'*:data-[slot=description]:col-start-2 *:data-[slot=description]:row-start-2',
// With description
'has-data-[slot=description]:**:data-[slot=label]:font-medium'
)}
/>
)
}
const base = [
// Basic layout
'relative isolate flex size-4.5 items-center justify-center rounded-[0.3125rem] sm:size-4',
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(0.3125rem-1px)] before:bg-white before:shadow-sm',
// Background color when checked
'group-data-checked:before:bg-(--checkbox-checked-bg)',
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
'dark:before:hidden',
// Background color applied to control in dark mode
'dark:bg-white/5 dark:group-data-checked:bg-(--checkbox-checked-bg)',
// Border
'border border-zinc-950/15 group-data-checked:border-transparent group-data-hover:group-data-checked:border-transparent group-data-hover:border-zinc-950/30 group-data-checked:bg-(--checkbox-checked-border)',
'dark:border-white/15 dark:group-data-checked:border-white/5 dark:group-data-hover:group-data-checked:border-white/5 dark:group-data-hover:border-white/30',
// Inner highlight shadow
'after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)] after:shadow-[inset_0_1px_--theme(--color-white/15%)]',
'dark:after:-inset-px dark:after:hidden dark:after:rounded-[0.3125rem] dark:group-data-checked:after:block',
// Focus ring
'group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-blue-500',
// Disabled state
'group-data-disabled:opacity-50',
'group-data-disabled:border-zinc-950/25 group-data-disabled:bg-zinc-950/5 group-data-disabled:[--checkbox-check:var(--color-zinc-950)]/50 group-data-disabled:before:bg-transparent',
'dark:group-data-disabled:border-white/20 dark:group-data-disabled:bg-white/2.5 dark:group-data-disabled:[--checkbox-check:var(--color-white)]/50 dark:group-data-checked:group-data-disabled:after:hidden',
// Forced colors mode
'forced-colors:[--checkbox-check:HighlightText] forced-colors:[--checkbox-checked-bg:Highlight] forced-colors:group-data-disabled:[--checkbox-check:Highlight]',
'dark:forced-colors:[--checkbox-check:HighlightText] dark:forced-colors:[--checkbox-checked-bg:Highlight] dark:forced-colors:group-data-disabled:[--checkbox-check:Highlight]',
]
const colors = {
'dark/zinc': [
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
'dark:[--checkbox-checked-bg:var(--color-zinc-600)]',
],
'dark/white': [
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
'dark:[--checkbox-check:var(--color-zinc-900)] dark:[--checkbox-checked-bg:var(--color-white)] dark:[--checkbox-checked-border:var(--color-zinc-950)]/15',
],
white:
'[--checkbox-check:var(--color-zinc-900)] [--checkbox-checked-bg:var(--color-white)] [--checkbox-checked-border:var(--color-zinc-950)]/15',
dark: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
zinc: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-600)] [--checkbox-checked-border:var(--color-zinc-700)]/90',
red: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-red-600)] [--checkbox-checked-border:var(--color-red-700)]/90',
orange:
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-orange-500)] [--checkbox-checked-border:var(--color-orange-600)]/90',
amber:
'[--checkbox-check:var(--color-amber-950)] [--checkbox-checked-bg:var(--color-amber-400)] [--checkbox-checked-border:var(--color-amber-500)]/80',
yellow:
'[--checkbox-check:var(--color-yellow-950)] [--checkbox-checked-bg:var(--color-yellow-300)] [--checkbox-checked-border:var(--color-yellow-400)]/80',
lime: '[--checkbox-check:var(--color-lime-950)] [--checkbox-checked-bg:var(--color-lime-300)] [--checkbox-checked-border:var(--color-lime-400)]/80',
green:
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-green-600)] [--checkbox-checked-border:var(--color-green-700)]/90',
emerald:
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-emerald-600)] [--checkbox-checked-border:var(--color-emerald-700)]/90',
teal: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-teal-600)] [--checkbox-checked-border:var(--color-teal-700)]/90',
cyan: '[--checkbox-check:var(--color-cyan-950)] [--checkbox-checked-bg:var(--color-cyan-300)] [--checkbox-checked-border:var(--color-cyan-400)]/80',
sky: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-sky-500)] [--checkbox-checked-border:var(--color-sky-600)]/80',
blue: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-blue-600)] [--checkbox-checked-border:var(--color-blue-700)]/90',
indigo:
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-indigo-500)] [--checkbox-checked-border:var(--color-indigo-600)]/90',
violet:
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-violet-500)] [--checkbox-checked-border:var(--color-violet-600)]/90',
purple:
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-purple-500)] [--checkbox-checked-border:var(--color-purple-600)]/90',
fuchsia:
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-fuchsia-500)] [--checkbox-checked-border:var(--color-fuchsia-600)]/90',
pink: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-pink-500)] [--checkbox-checked-border:var(--color-pink-600)]/90',
rose: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-rose-500)] [--checkbox-checked-border:var(--color-rose-600)]/90',
}
export function Checkbox({ color = 'dark/zinc', className, ...props }) {
return (
<Headless.Checkbox
data-slot="control"
{...props}
className={clsx(className, 'group inline-flex focus:outline-hidden')}
>
<span className={clsx([base, colors[color]])}>
<svg
className="size-4 stroke-(--checkbox-check) opacity-0 group-data-checked:opacity-100 sm:h-3.5 sm:w-3.5"
viewBox="0 0 14 14"
fill="none"
>
{/* Checkmark icon */}
<path
className="opacity-100 group-data-indeterminate:opacity-0"
d="M3 8L6 11L11 3.5"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Indeterminate icon */}
<path
className="opacity-0 group-data-indeterminate:opacity-100"
d="M3 7H11"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</Headless.Checkbox>
)
}
'use client'
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { useState } from 'react'
export function Combobox({
options,
displayValue,
filter,
anchor = 'bottom',
className,
placeholder,
autoFocus,
'aria-label': ariaLabel,
children,
...props
}) {
const [query, setQuery] = useState('')
const filteredOptions =
query === ''
? options
: options.filter((option) =>
filter ? filter(option, query) : displayValue(option)?.toLowerCase().includes(query.toLowerCase())
)
return (
<Headless.Combobox {...props} multiple={false} virtual={{ options: filteredOptions }} onClose={() => setQuery('')}>
<span
data-slot="control"
className={clsx([
className,
// Basic layout
'relative block w-full',
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)] before:bg-white before:shadow-sm',
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
'dark:before:hidden',
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-blue-500',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
// Invalid state
'has-data-invalid:before:shadow-red-500/10',
])}
>
<Headless.ComboboxInput
autoFocus={autoFocus}
data-slot="control"
aria-label={ariaLabel}
displayValue={(option) => displayValue(option) ?? ''}
onChange={(event) => setQuery(event.target.value)}
placeholder={placeholder}
className={clsx([
className,
// Basic layout
'relative block w-full appearance-none rounded-lg py-[calc(--spacing(2.5)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
// Horizontal padding
'pr-[calc(--spacing(10)-1px)] pl-[calc(--spacing(3.5)-1px)] sm:pr-[calc(--spacing(9)-1px)] sm:pl-[calc(--spacing(3)-1px)]',
// Typography
'text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white',
// Border
'border border-zinc-950/10 data-hover:border-zinc-950/20 dark:border-white/10 dark:data-hover:border-white/20',
// Background color
'bg-transparent dark:bg-white/5',
// Hide default focus styles
'focus:outline-hidden',
// Invalid state
'data-invalid:border-red-500 data-invalid:data-hover:border-red-500 dark:data-invalid:border-red-500 dark:data-invalid:data-hover:border-red-500',
// Disabled state
'data-disabled:border-zinc-950/20 dark:data-disabled:border-white/15 dark:data-disabled:bg-white/2.5 dark:data-hover:data-disabled:border-white/15',
// System icons
'dark:scheme-dark',
])}
/>
<Headless.ComboboxButton className="group absolute inset-y-0 right-0 flex items-center px-2">
<svg
className="size-5 stroke-zinc-500 group-data-disabled:stroke-zinc-600 group-data-hover:stroke-zinc-700 sm:size-4 dark:stroke-zinc-400 dark:group-data-hover:stroke-zinc-300 forced-colors:stroke-[CanvasText]"
viewBox="0 0 16 16"
aria-hidden="true"
fill="none"
>
<path d="M5.75 10.75L8 13L10.25 10.75" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.25 5.25L8 3L5.75 5.25" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Headless.ComboboxButton>
</span>
<Headless.ComboboxOptions
transition
anchor={anchor}
className={clsx(
// Anchor positioning
'[--anchor-gap:--spacing(2)] [--anchor-padding:--spacing(4)] sm:data-[anchor~=start]:[--anchor-offset:-4px]',
// Base styles,
'isolate min-w-[calc(var(--input-width)+8px)] scroll-py-1 rounded-xl p-1 select-none empty:invisible',
// Invisible border that is only visible in `forced-colors` mode for accessibility purposes
'outline outline-transparent focus:outline-hidden',
// Handle scrolling when menu won't fit in viewport
'overflow-y-scroll overscroll-contain',
// Popover background
'bg-white/75 backdrop-blur-xl dark:bg-zinc-800/75',
// Shadows
'shadow-lg ring-1 ring-zinc-950/10 dark:ring-white/10 dark:ring-inset',
// Transitions
'transition-opacity duration-100 ease-in data-closed:data-leave:opacity-0 data-transition:pointer-events-none'
)}
>
{({ option }) => children(option)}
</Headless.ComboboxOptions>
</Headless.Combobox>
)
}
export function ComboboxOption({ children, className, ...props }) {
let sharedClasses = clsx(
// Base
'flex min-w-0 items-center',
// Icons
'*:data-[slot=icon]:size-5 *:data-[slot=icon]:shrink-0 sm:*:data-[slot=icon]:size-4',
'*:data-[slot=icon]:text-zinc-500 group-data-focus/option:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400',
'forced-colors:*:data-[slot=icon]:text-[CanvasText] forced-colors:group-data-focus/option:*:data-[slot=icon]:text-[Canvas]',
// Avatars
'*:data-[slot=avatar]:-mx-0.5 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:size-5'
)
return (
<Headless.ComboboxOption
{...props}
className={clsx(
// Basic layout
'group/option grid w-full cursor-default grid-cols-[1fr_--spacing(5)] items-baseline gap-x-2 rounded-lg py-2.5 pr-2 pl-3.5 sm:grid-cols-[1fr_--spacing(4)] sm:py-1.5 sm:pr-2 sm:pl-3',
// Typography
'text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
// Focus
'outline-hidden data-focus:bg-blue-500 data-focus:text-white',
// Forced colors mode
'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]',
// Disabled
'data-disabled:opacity-50'
)}
>
<span className={clsx(className, sharedClasses)}>{children}</span>
<svg
className="relative col-start-2 hidden size-5 self-center stroke-current group-data-selected/option:inline sm:size-4"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 8.5l3 3L12 4" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Headless.ComboboxOption>
)
}
export function ComboboxLabel({ className, ...props }) {
return <span {...props} className={clsx(className, 'ml-2.5 truncate first:ml-0 sm:ml-2 sm:first:ml-0')} />
}
export function ComboboxDescription({ className, children, ...props }) {
return (
<span
{...props}
className={clsx(
className,
'flex flex-1 overflow-hidden text-zinc-500 group-data-focus/option:text-white before:w-2 before:min-w-0 before:shrink dark:text-zinc-400'
)}
>
<span className="flex-1 truncate">{children}</span>
</span>
)
}
import clsx from 'clsx'
export function DescriptionList({ className, ...props }) {
return (
<dl
{...props}
className={clsx(
className,
'grid grid-cols-1 text-base/6 sm:grid-cols-[min(50%,--spacing(80))_auto] sm:text-sm/6'
)}
/>
)
}
export function DescriptionTerm({ className, ...props }) {
return (
<dt
{...props}
className={clsx(
className,
'col-start-1 border-t border-zinc-950/5 pt-3 text-zinc-500 first:border-none sm:border-t sm:border-zinc-950/5 sm:py-3 dark:border-white/5 dark:text-zinc-400 sm:dark:border-white/5'
)}
/>
)
}
export function DescriptionDetails({ className, ...props }) {
return (
<dd
{...props}
className={clsx(
className,
'pt-1 pb-3 text-zinc-950 sm:border-t sm:border-zinc-950/5 sm:py-3 sm:nth-2:border-none dark:text-white dark:sm:border-white/5'
)}
/>
)
}
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { Text } from './text'
const sizes = {
xs: 'sm:max-w-xs',
sm: 'sm:max-w-sm',
md: 'sm:max-w-md',
lg: 'sm:max-w-lg',
xl: 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
'3xl': 'sm:max-w-3xl',
'4xl': 'sm:max-w-4xl',
'5xl': 'sm:max-w-5xl',
}
export function Dialog({ size = 'lg', className, children, ...props }) {
return (
<Headless.Dialog {...props}>
<Headless.DialogBackdrop
transition
className="fixed inset-0 flex w-screen justify-center overflow-y-auto bg-zinc-950/25 px-2 py-2 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-6 sm:py-8 lg:px-8 lg:py-16 dark:bg-zinc-950/50"
/>
<div className="fixed inset-0 w-screen overflow-y-auto pt-6 sm:pt-0">
<div className="grid min-h-full grid-rows-[1fr_auto] justify-items-center sm:grid-rows-[1fr_auto_3fr] sm:p-4">
<Headless.DialogPanel
transition
className={clsx(
className,
sizes[size],
'row-start-2 w-full min-w-0 rounded-t-3xl bg-white p-(--gutter) shadow-lg ring-1 ring-zinc-950/10 [--gutter:--spacing(8)] sm:mb-auto sm:rounded-2xl dark:bg-zinc-900 dark:ring-white/10 forced-colors:outline',
'transition duration-100 will-change-transform data-closed:translate-y-12 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:data-closed:translate-y-0 sm:data-closed:data-enter:scale-95'
)}
>
{children}
</Headless.DialogPanel>
</div>
</div>
</Headless.Dialog>
)
}
export function DialogTitle({ className, ...props }) {
return (
<Headless.DialogTitle
{...props}
className={clsx(className, 'text-lg/6 font-semibold text-balance text-zinc-950 sm:text-base/6 dark:text-white')}
/>
)
}
export function DialogDescription({ className, ...props }) {
return <Headless.Description as={Text} {...props} className={clsx(className, 'mt-2 text-pretty')} />
}
export function DialogBody({ className, ...props }) {
return <div {...props} className={clsx(className, 'mt-6')} />
}
export function DialogActions({ className, ...props }) {
return (
<div
{...props}
className={clsx(
className,
'mt-8 flex flex-col-reverse items-center justify-end gap-3 *:w-full sm:flex-row sm:*:w-auto'
)}
/>
)
}
import clsx from 'clsx'
export function Divider({ soft = false, className, ...props }) {
return (
<hr
role="presentation"
{...props}
className={clsx(
className,
'w-full border-t',
soft && 'border-zinc-950/5 dark:border-white/5',
!soft && 'border-zinc-950/10 dark:border-white/10'
)}
/>
)
}
'use client'
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { Button } from './button'
import { Link } from './link'
export function Dropdown(props) {
return <Headless.Menu {...props} />
}
export function DropdownButton({ as = Button, ...props }) {
return <Headless.MenuButton as={as} {...props} />
}
export function DropdownMenu({ anchor = 'bottom', className, ...props }) {
return (
<Headless.MenuItems
{...props}
transition
anchor={anchor}
className={clsx(
className,
// Anchor positioning
'[--anchor-gap:--spacing(2)] [--anchor-padding:--spacing(1)] data-[anchor~=end]:[--anchor-offset:6px] data-[anchor~=start]:[--anchor-offset:-6px] sm:data-[anchor~=end]:[--anchor-offset:4px] sm:data-[anchor~=start]:[--anchor-offset:-4px]',
// Base styles
'isolate w-max rounded-xl p-1',
// Invisible border that is only visible in `forced-colors` mode for accessibility purposes
'outline outline-transparent focus:outline-hidden',
// Handle scrolling when menu won't fit in viewport
'overflow-y-auto',
// Popover background
'bg-white/75 backdrop-blur-xl dark:bg-zinc-800/75',
// Shadows
'shadow-lg ring-1 ring-zinc-950/10 dark:ring-white/10 dark:ring-inset',
// Define grid at the menu level if subgrid is supported
'supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]',
// Transitions
'transition data-leave:duration-100 data-leave:ease-in data-closed:data-leave:opacity-0'
)}
/>
)
}
export function DropdownItem({ className, ...props }) {
let classes = clsx(
className,
// Base styles
'group cursor-default rounded-lg px-3.5 py-2.5 focus:outline-hidden sm:px-3 sm:py-1.5',
// Text styles
'text-left text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
// Focus
'data-focus:bg-blue-500 data-focus:text-white',
// Disabled state
'data-disabled:opacity-50',
// Forced colors mode
'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText] forced-colors:data-focus:*:data-[slot=icon]:text-[HighlightText]',
// Use subgrid when available but fallback to an explicit grid layout if not
'col-span-full grid grid-cols-[auto_1fr_1.5rem_0.5rem_auto] items-center supports-[grid-template-columns:subgrid]:grid-cols-subgrid',
// Icons
'*:data-[slot=icon]:col-start-1 *:data-[slot=icon]:row-start-1 *:data-[slot=icon]:mr-2.5 *:data-[slot=icon]:-ml-0.5 *:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:mr-2 sm:*:data-[slot=icon]:size-4',
'*:data-[slot=icon]:text-zinc-500 data-focus:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400 dark:data-focus:*:data-[slot=icon]:text-white',
// Avatar
'*:data-[slot=avatar]:mr-2.5 *:data-[slot=avatar]:-ml-1 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:mr-2 sm:*:data-[slot=avatar]:size-5'
)
return typeof props.href === 'string' ? (
<Headless.MenuItem as={Link} {...props} className={classes} />
) : (
<Headless.MenuItem as="button" type="button" {...props} className={classes} />
)
}
export function DropdownHeader({ className, ...props }) {
return <div {...props} className={clsx(className, 'col-span-5 px-3.5 pt-2.5 pb-1 sm:px-3')} />
}
export function DropdownSection({ className, ...props }) {
return (
<Headless.MenuSection
{...props}
className={clsx(
className,
// Define grid at the section level instead of the item level if subgrid is supported
'col-span-full supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]'
)}
/>
)
}
export function DropdownHeading({ className, ...props }) {
return (
<Headless.MenuHeading
{...props}
className={clsx(
className,
'col-span-full grid grid-cols-[1fr_auto] gap-x-12 px-3.5 pt-2 pb-1 text-sm/5 font-medium text-zinc-500 sm:px-3 sm:text-xs/5 dark:text-zinc-400'
)}
/>
)
}
export function DropdownDivider({ className, ...props }) {
return (
<Headless.MenuSeparator
{...props}
className={clsx(
className,
'col-span-full mx-3.5 my-1 h-px border-0 bg-zinc-950/5 sm:mx-3 dark:bg-white/10 forced-colors:bg-[CanvasText]'
)}
/>
)
}
export function DropdownLabel({ className, ...props }) {
return <div {...props} data-slot="label" className={clsx(className, 'col-start-2 row-start-1')} {...props} />
}
export function DropdownDescription({ className, ...props }) {
return (
<Headless.Description
data-slot="description"
{...props}
className={clsx(
className,
'col-span-2 col-start-2 row-start-2 text-sm/5 text-zinc-500 group-data-focus:text-white sm:text-xs/5 dark:text-zinc-400 forced-colors:group-data-focus:text-[HighlightText]'
)}
/>
)
}
export function DropdownShortcut({ keys, className, ...props }) {
return (
<Headless.Description
as="kbd"
{...props}
className={clsx(className, 'col-start-5 row-start-1 flex justify-self-end')}
>
{(Array.isArray(keys) ? keys : keys.split('')).map((char, index) => (
<kbd
key={index}
className={clsx([
'min-w-[2ch] text-center font-sans text-zinc-400 capitalize group-data-focus:text-white forced-colors:group-data-focus:text-[HighlightText]',
// Make sure key names that are longer than one character (like "Tab") have extra space
index > 0 && char.length > 1 && 'pl-1',
])}
>
{char}
</kbd>
))}
</Headless.Description>
)
}
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
export function Fieldset({ className, ...props }) {
return (
<Headless.Fieldset
{...props}
className={clsx(className, '*:data-[slot=text]:mt-1 [&>*+[data-slot=control]]:mt-6')}
/>
)
}
export function Legend({ className, ...props }) {
return (
<Headless.Legend
data-slot="legend"
{...props}
className={clsx(
className,
'text-base/6 font-semibold text-zinc-950 data-disabled:opacity-50 sm:text-sm/6 dark:text-white'
)}
/>
)
}
export function FieldGroup({ className, ...props }) {
return <div data-slot="control" {...props} className={clsx(className, 'space-y-8')} />
}
export function Field({ className, ...props }) {
return (
<Headless.Field
{...props}
className={clsx(
className,
'[&>[data-slot=label]+[data-slot=control]]:mt-3',
'[&>[data-slot=label]+[data-slot=description]]:mt-1',
'[&>[data-slot=description]+[data-slot=control]]:mt-3',
'[&>[data-slot=control]+[data-slot=description]]:mt-3',
'[&>[data-slot=control]+[data-slot=error]]:mt-3',
'*:data-[slot=label]:font-medium'
)}
/>
)
}
export function Label({ className, ...props }) {
return (
<Headless.Label
data-slot="label"
{...props}
className={clsx(
className,
'text-base/6 text-zinc-950 select-none data-disabled:opacity-50 sm:text-sm/6 dark:text-white'
)}
/>
)
}
export function Description({ className, ...props }) {
return (
<Headless.Description
data-slot="description"
{...props}
className={clsx(className, 'text-base/6 text-zinc-500 data-disabled:opacity-50 sm:text-sm/6 dark:text-zinc-400')}
/>
)
}
export function ErrorMessage({ className, ...props }) {
return (
<Headless.Description
data-slot="error"
{...props}
className={clsx(className, 'text-base/6 text-red-600 data-disabled:opacity-50 sm:text-sm/6 dark:text-red-500')}
/>
)
}
import clsx from 'clsx'
export function Heading({ className, level = 1, ...props }) {
let Element = `h${level}`
return (
<Element
{...props}
className={clsx(className, 'text-2xl/8 font-semibold text-zinc-950 sm:text-xl/8 dark:text-white')}
/>
)
}
export function Subheading({ className, level = 2, ...props }) {
let Element = `h${level}`
return (
<Element
{...props}
className={clsx(className, 'text-base/7 font-semibold text-zinc-950 sm:text-sm/6 dark:text-white')}
/>
)
}
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { forwardRef } from 'react'
export function InputGroup({ children }) {
return (
<span
data-slot="control"
className={clsx(
'relative isolate block',
'has-[[data-slot=icon]:first-child]:[&_input]:pl-10 has-[[data-slot=icon]:last-child]:[&_input]:pr-10 sm:has-[[data-slot=icon]:first-child]:[&_input]:pl-8 sm:has-[[data-slot=icon]:last-child]:[&_input]:pr-8',
'*:data-[slot=icon]:pointer-events-none *:data-[slot=icon]:absolute *:data-[slot=icon]:top-3 *:data-[slot=icon]:z-10 *:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:top-2.5 sm:*:data-[slot=icon]:size-4',
'[&>[data-slot=icon]:first-child]:left-3 sm:[&>[data-slot=icon]:first-child]:left-2.5 [&>[data-slot=icon]:last-child]:right-3 sm:[&>[data-slot=icon]:last-child]:right-2.5',
'*:data-[slot=icon]:text-zinc-500 dark:*:data-[slot=icon]:text-zinc-400'
)}
>
{children}
</span>
)
}
const dateTypes = ['date', 'datetime-local', 'month', 'time', 'week']
export const Input = forwardRef(function Input(
{ className, ...props },
ref
) {
return (
<span
data-slot="control"
className={clsx([
className,
// Basic layout
'relative block w-full',
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)] before:bg-white before:shadow-sm',
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
'dark:before:hidden',
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-blue-500',
// Disabled state
'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
])}
>
<Headless.Input
ref={ref}
{...props}
className={clsx([
// Date classes
props.type &&
dateTypes.includes(props.type) && [
'[&::-webkit-datetime-edit-fields-wrapper]:p-0',
'[&::-webkit-date-and-time-value]:min-h-[1.5em]',
'[&::-webkit-datetime-edit]:inline-flex',
'[&::-webkit-datetime-edit]:p-0',
'[&::-webkit-datetime-edit-year-field]:p-0',
'[&::-webkit-datetime-edit-month-field]:p-0',
'[&::-webkit-datetime-edit-day-field]:p-0',
'[&::-webkit-datetime-edit-hour-field]:p-0',
'[&::-webkit-datetime-edit-minute-field]:p-0',
'[&::-webkit-datetime-edit-second-field]:p-0',
'[&::-webkit-datetime-edit-millisecond-field]:p-0',
'[&::-webkit-datetime-edit-meridiem-field]:p-0',
],
// Basic layout
'relative block w-full appearance-none rounded-lg px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2.5)-1px)] sm:px-[calc(--spacing(3)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
// Typography
'text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white',
// Border
'border border-zinc-950/10 data-hover:border-zinc-950/20 dark:border-white/10 dark:data-hover:border-white/20',
// Background color
'bg-transparent dark:bg-white/5',
// Hide default focus styles
'focus:outline-hidden',
// Invalid state
'data-invalid:border-red-500 data-invalid:data-hover:border-red-500 dark:data-invalid:border-red-600 dark:data-invalid:data-hover:border-red-600',
// Disabled state
'data-disabled:border-zinc-950/20 dark:data-disabled:border-white/15 dark:data-disabled:bg-white/2.5 dark:data-hover:data-disabled:border-white/15',
// System icons
'dark:scheme-dark',
])}
/>
</span>
)
})
import * as Headless from '@headlessui/react'
import NextLink from 'next/link'
import { forwardRef } from 'react'
export const Link = forwardRef(function Link(props, ref) {
return (
<Headless.DataInteractive>
<NextLink {...props} ref={ref} />
</Headless.DataInteractive>
)
})
'use client'
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { Fragment } from 'react'
export function Listbox({ className, placeholder, autoFocus, 'aria-label': ariaLabel, children: options, ...props }) {
return (
<Headless.Listbox {...props} multiple={false}>
<Headless.ListboxButton
autoFocus={autoFocus}
data-slot="control"
aria-label={ariaLabel}
className={clsx([
className,
// Basic layout
'group relative block w-full',
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)] before:bg-white before:shadow-sm',
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
'dark:before:hidden',
// Hide default focus styles
'focus:outline-hidden',
// Focus ring
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset data-focus:after:ring-2 data-focus:after:ring-blue-500',
// Disabled state
'data-disabled:opacity-50 data-disabled:before:bg-zinc-950/5 data-disabled:before:shadow-none',
])}
>
<Headless.ListboxSelectedOption
as="span"
options={options}
placeholder={placeholder && <span className="block truncate text-zinc-500">{placeholder}</span>}
className={clsx([
// Basic layout
'relative block w-full appearance-none rounded-lg py-[calc(--spacing(2.5)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
// Set minimum height for when no value is selected
'min-h-11 sm:min-h-9',
// Horizontal padding
'pr-[calc(--spacing(7)-1px)] pl-[calc(--spacing(3.5)-1px)] sm:pl-[calc(--spacing(3)-1px)]',
// Typography
'text-left text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
// Border
'border border-zinc-950/10 group-data-active:border-zinc-950/20 group-data-hover:border-zinc-950/20 dark:border-white/10 dark:group-data-active:border-white/20 dark:group-data-hover:border-white/20',
// Background color
'bg-transparent dark:bg-white/5',
// Invalid state
'group-data-invalid:border-red-500 group-data-hover:group-data-invalid:border-red-500 dark:group-data-invalid:border-red-600 dark:data-hover:group-data-invalid:border-red-600',
// Disabled state
'group-data-disabled:border-zinc-950/20 group-data-disabled:opacity-100 dark:group-data-disabled:border-white/15 dark:group-data-disabled:bg-white/2.5 dark:group-data-disabled:data-hover:border-white/15',
])}
/>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
className="size-5 stroke-zinc-500 group-data-disabled:stroke-zinc-600 sm:size-4 dark:stroke-zinc-400 forced-colors:stroke-[CanvasText]"
viewBox="0 0 16 16"
aria-hidden="true"
fill="none"
>
<path d="M5.75 10.75L8 13L10.25 10.75" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.25 5.25L8 3L5.75 5.25" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
</Headless.ListboxButton>
<Headless.ListboxOptions
transition
anchor="selection start"
className={clsx(
// Anchor positioning
'[--anchor-offset:-1.625rem] [--anchor-padding:--spacing(4)] sm:[--anchor-offset:-1.375rem]',
// Base styles
'isolate w-max min-w-[calc(var(--button-width)+1.75rem)] scroll-py-1 rounded-xl p-1 select-none',
// Invisible border that is only visible in `forced-colors` mode for accessibility purposes
'outline outline-transparent focus:outline-hidden',
// Handle scrolling when menu won't fit in viewport
'overflow-y-scroll overscroll-contain',
// Popover background
'bg-white/75 backdrop-blur-xl dark:bg-zinc-800/75',
// Shadows
'shadow-lg ring-1 ring-zinc-950/10 dark:ring-white/10 dark:ring-inset',
// Transitions
'transition-opacity duration-100 ease-in data-closed:data-leave:opacity-0 data-transition:pointer-events-none'
)}
>
{options}
</Headless.ListboxOptions>
</Headless.Listbox>
)
}
export function ListboxOption({ children, className, ...props }) {
let sharedClasses = clsx(
// Base
'flex min-w-0 items-center',
// Icons
'*:data-[slot=icon]:size-5 *:data-[slot=icon]:shrink-0 sm:*:data-[slot=icon]:size-4',
'*:data-[slot=icon]:text-zinc-500 group-data-focus/option:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400',
'forced-colors:*:data-[slot=icon]:text-[CanvasText] forced-colors:group-data-focus/option:*:data-[slot=icon]:text-[Canvas]',
// Avatars
'*:data-[slot=avatar]:-mx-0.5 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:size-5'
)
return (
<Headless.ListboxOption as={Fragment} {...props}>
{({ selectedOption }) => {
if (selectedOption) {
return <div className={clsx(className, sharedClasses)}>{children}</div>
}
return (
<div
className={clsx(
// Basic layout
'group/option grid cursor-default grid-cols-[--spacing(5)_1fr] items-baseline gap-x-2 rounded-lg py-2.5 pr-3.5 pl-2 sm:grid-cols-[--spacing(4)_1fr] sm:py-1.5 sm:pr-3 sm:pl-1.5',
// Typography
'text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
// Focus
'outline-hidden data-focus:bg-blue-500 data-focus:text-white',
// Forced colors mode
'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]',
// Disabled
'data-disabled:opacity-50'
)}
>
<svg
className="relative hidden size-5 self-center stroke-current group-data-selected/option:inline sm:size-4"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path d="M4 8.5l3 3L12 4" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span className={clsx(className, sharedClasses, 'col-start-2')}>{children}</span>
</div>
)
}}
</Headless.ListboxOption>
)
}
export function ListboxLabel({ className, ...props }) {
return <span {...props} className={clsx(className, 'ml-2.5 truncate first:ml-0 sm:ml-2 sm:first:ml-0')} />
}
export function ListboxDescription({ className, children, ...props }) {
return (
<span
{...props}
className={clsx(
className,
'flex flex-1 overflow-hidden text-zinc-500 group-data-focus/option:text-white before:w-2 before:min-w-0 before:shrink dark:text-zinc-400'
)}
>
<span className="flex-1 truncate">{children}</span>
</span>
)
}
'use client'
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import { LayoutGroup, motion } from 'motion/react'
import { forwardRef, useId } from 'react'
import { TouchTarget } from './button'
import { Link } from './link'
export function Navbar({ className, ...props }) {
return <nav {...props} className={clsx(className, 'flex flex-1 items-center gap-4 py-2.5')} />
}
export function NavbarDivider({ className, ...props }) {
return <div aria-hidden="true" {...props} className={clsx(className, 'h-6 w-px bg-zinc-950/10 dark:bg-white/10')} />
}
export function NavbarSection({ className, ...props }) {
let id = useId()
return (
<LayoutGroup id={id}>
<div {...props} className={clsx(className, 'flex items-center gap-3')} />
</LayoutGroup>
)
}
export function NavbarSpacer({ className, ...props }) {
return <div aria-hidden="true" {...props} className={clsx(className, '-ml-4 flex-1')} />
}
export const NavbarItem = forwardRef(function NavbarItem(
{ current, className, children, ...props },
ref
) {
let classes = clsx(
// Base
'relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 font-medium text-zinc-950 sm:text-sm/5',
// Leading icon/icon-only
'*:data-[slot=icon]:size-6 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:fill-zinc-500 sm:*:data-[slot=icon]:size-5',
// Trailing icon (down chevron or similar)
'*:not-nth-2:last:data-[slot=icon]:ml-auto *:not-nth-2:last:data-[slot=icon]:size-5 sm:*:not-nth-2:last:data-[slot=icon]:size-4',
// Avatar
'*:data-[slot=avatar]:-m-0.5 *:data-[slot=avatar]:size-7 *:data-[slot=avatar]:[--avatar-radius:var(--radius-md)] sm:*:data-[slot=avatar]:size-6',
// Hover
'data-hover:bg-zinc-950/5 data-hover:*:data-[slot=icon]:fill-zinc-950',
// Active
'data-active:bg-zinc-950/5 data-active:*:data-[slot=icon]:fill-zinc-950',
// Dark mode
'dark:text-white dark:*:data-[slot=icon]:fill-zinc-400',
'dark:data-hover:bg-white/5 dark:data-hover:*:data-[slot=icon]:fill-white',
'dark:data-active:bg-white/5 dark:data-active:*:data-[slot=icon]:fill-white'
)
return (
<span className={clsx(className, 'relative')}>
{current && (
<motion.span
layoutId="current-indicator"
className="absolute inset-x-2 -bottom-2.5 h-0.5 rounded-full bg-zinc-950 dark:bg-white"
/>
)}
{typeof props.href === 'string' ? (
<Link {...props} className={classes} data-current={current ? 'true' : undefined} ref={ref}>
<TouchTarget>{children}</TouchTarget>
</Link>
) : (
<Headless.Button
{...props}
className={clsx('cursor-default', classes)}
data-current={current ? 'true' : undefined}
ref={ref}
>
<TouchTarget>{children}</TouchTarget>
</Headless.Button>
)}
</span>
)
})
export function NavbarLabel({ className, ...props }) {
return <span {...props} className={clsx(className, 'truncate')} />
}
This diff is collapsed.
@import 'tailwindcss';
@theme {
--font-sans: Inter, sans-serif;
--font-sans--font-feature-settings: 'cv11';
}
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-img-element": "off"
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
/** @type {import('next').NextConfig} */
const nextConfig = {}
export default nextConfig
This diff is collapsed.
This diff is collapsed.
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment