Commit 239971a9 authored by Andy-bubu's avatar Andy-bubu

feat: migrate audit project manager to Xinyuan HeroUI

parent 6001bdb3
......@@ -11,5 +11,7 @@
/.umi-test
/dist
/.mfsu
/.playwright-cli
/artifacts
.swc
.pnpm-store
......@@ -20,6 +20,12 @@ export default defineConfig({
fastRefresh: true,
base: APP_BASE_PATH,
publicPath: APP_BASE_PATH,
links: [
{
rel: 'stylesheet',
href: `${APP_BASE_PATH}fonts/inter.css`,
},
],
alias: {
react: resolvePackageRoot('react'),
'react-dom': resolvePackageRoot('react-dom'),
......@@ -62,7 +68,7 @@ export default defineConfig({
],
},
],
npmClient: 'npm',
npmClient: 'pnpm',
proxy: {
'/api': {
target: 'https://xy.streams7.com/api',
......@@ -86,6 +92,9 @@ export default defineConfig({
}
},
esbuildMinifyIIFE: true,
jsMinifierOptions: {
target: ['chrome80', 'es2020'],
},
define: {
STREAMS_API_URL: '/api',
STREAMS_PARSER_URL: '/parser',
......
# UI 架构
## 组件所有权
活动页面和共享 UI 组合直接导入 `@heroui/react`、`@heroui-pro/react` 或验证过的 Pro
子路径。共享代码只组合当前 HeroUI compound 结构、布局、可访问性和鑫元 Token;不复刻
Catalyst props、DOM event、variant、class hook 或旧组件名。活动源码中没有第二套 UI 系统。
页面 className 只处理 flex/grid、宽高、间距、对齐、溢出和响应式。HeroUI/HeroUI Pro、
权威 `theme.css` 与批准的 `tokens.css`/`shell.css` 持有背景、颜色、描边、圆角、阴影和
Hover/Focus/Selected/Disabled。`src/pages` 下没有页面私有 CSS/Less/SCSS。
## 共享组合
- `AppShell`:根主题、Pro Sidebar Provider、Navbar 和 base-aware 页面滚动容器。
- `DashboardSidebar` / `DashboardNavbar`:导航、offcanvas、应用切换、用户菜单与主题动作。
- `DataTable`:Pro DataGrid 原生选择、排序、固定列、内部滚动、EmptyState、ActionBar。
- `XinyuanAutocomplete` / `FilterSelect` / `ReferenceSearchSelect`:当前 HeroUI
`selectedKey(s)`、`onSelectionChange`、`inputValue` 合同。
- `FormDatePicker`:HeroUI DatePicker/DateField/Calendar 与既有字符串协议的页面边界映射。
- `FileUploadField`:Pro DropZone files 到既有业务回调的页面边界映射。
- `XinyuanModal`:HeroUI Modal compound 结构、批准宽度和 Surface 层级。
- `TableActionButton`:HeroUI Button/Dropdown/Tooltip 与统一 Ant Design 图标。
- `DynamicFormValueEditor`:将 Subject `form` 的 `{ form, data, settings }` 协议映射为 Tabs、
Disclosure、Pro DataGrid、Autocomplete、Modal 和 AlertDialog;组件只通过 `onChange` 回传
草稿,不拥有记录更新接口或提交时机。
页面主数据集分类、详情弹窗业务阶段与动态表单 `settings.group` 全部直接使用 HeroUI
`Tabs variant="primary"` 的官方胶囊轨道。三者共用内容宽度
与官方 `Tabs.ListContainer` 滚动合同,应用不拥有 Tabs 的背景、描边、圆角、选中指示器或
交互样式。
详情弹窗的标题、阶段 Tabs 和顶部业务操作共同属于 HeroUI `Modal.Header`;Header 不参与
正文滚动,`Modal.Body` 是唯一纵向滚动容器。正文不得从 Tabs 下方穿过,也不得通过给 Tabs
增加私有背景、描边或阴影来遮挡滚动内容。
`RecordDetailDrawer` 的查看态结构固定为 Modal Overlay/L1 -> 完整字段组 `Surface secondary`/L2
-> `form` 或引用记录 `Surface tertiary`/L3。动态表单内部继续使用 Pro DataGrid 自有的 L2
表头和 L1 数据单元格;不得通过 DataGrid 私有 CSS、边框或阴影制造分隔。编辑态不创建 L3,
所有输入仍直接使用 HeroUI `secondary` 控件。
上述组合均是业务无关的 HeroUI-native 组合,不合成旧 DOM event,也不隐藏页面 API、权限
或数据映射。
`DataTable` 默认透传 HeroUI Pro `allowsColumnResize`,主业务列表继续使用官方可调整列宽模式;
弹窗内的只读关联记录短表显式关闭列宽拖拽,使用官方非 ResizableContainer 布局并按实际
表头和数据行高度收缩,避免弹窗剩余高度被误分配给单条数据行。应用不覆盖 DataGrid Hover
状态或内部 Table slot。
## 依赖与可复现性
HeroUI Pro 在目标仓库根目录通过官方授权 setup 独立安装,manifest 与 `pnpm-lock.yaml`
固定 `@heroui-pro/react@1.0.0-beta.8`。包从项目自身
`node_modules/.pnpm/@heroui-pro+react@1.0.0-beta.8` 解析;普通 pnpm `node_modules` 链接只
指向该项目 virtual store,不指向相邻仓库。交付没有 `file:`、绝对路径、本机 vendor 或
第二份锁文件。
应用 HeroUI/React Aria 图如下:HeroUI Pro `1.0.0-beta.8` -> HeroUI/Styles `3.2.4` ->
React Aria Components `1.20.0` / `@react-aria/utils` `3.34.1` -> React/React DOM `19.2.8`。
定向 `npm ls` 与 `pnpm ls` 均通过,七个 Pro JS 子路径及 CSS export 可解析。
`@heroui/styles@3.2.4` 在 pnpm 中有两个 peer 虚拟目录,分别绑定 `tailwind-merge` 3.4 与
3.6,但实际 HeroUI Styles 版本相同,不形成第二个 HeroUI/React Aria Context。锁文件另含
Umi 构建工具使用的 React 18.3.1 子图;它不进入应用 HeroUI Context。`@number-flow/react`
0.6.2 和 `recharts` 3.10.1 仍声明 React `^18.3.1` peer,因此把 React 一并交给 npm 全量
校验会得到 `ELSPROBLEMS`;pnpm 应用图实际解析 React 19.2.8,生产构建和浏览器运行通过。
## 业务边界
路由、鉴权、权限、API 地址、请求参数、Subject/View 数据结构、值格式化、校验、分页口径、
保存回调、登录退出、请求时机和状态转换仍由原 controller/service 拥有。UI 组合只转换
HeroUI 当前 value/files/selection 表达到既有页面回调,不引入“问题整改”的业务服务。
登录页只复用“问题整改”的已批准 HeroUI 结构,通过本项目 `DataService` 调用
`GET /auth/captcha`,并将验证码文本与服务端 token 交给既有 `POST /auth/login`。
浏览器验收所需代表性响应通过 Playwright 会话 route 注入,不存在于源码或后端。查看转
编辑、取消编辑、AlertDialog 取消均不触发业务请求;真实退出仍调用原 logout API 并执行
原 token/current-user 清理与 base-aware 跳转。
动态表单定义通过既有 `GET /form/:name` 按实例加载,模板列表通过既有 `/form/query` 在编辑
态加载。查看态只消费业务字段定义与 `data` 行,过滤回退列中的内部字段;所有行和分组改动
继续进入 `RecordDetailDrawer` 的 `draftRecord/dirtyFields`,最终复用原 `handleFieldsUpdate`
生成一次记录更新请求。
详情查看态将空实例与编辑占位分开:空实例只参与编辑草稿,不进入查看态计数、分组或
Disclosure;已选择模板但零行时使用紧凑 EmptyState,不挂载 DataGrid。引用字段的展示层
统一解包 `raw`,但引用 ID、Subject、查询参数、返回数据和保存值均保持原协议。
## 结构图
```text
Umi routes / auth wrappers
-> AppShell (root theme + Pro Sidebar/Navbar)
-> route page (layout and business state)
-> HeroUI base compounds
-> HeroUI Pro DataGrid / DropZone / KPI / Charts
-> business-neutral shared compositions
-> existing service callbacks and data protocols
```
Portal 与页面共享 `documentElement` 的 `.light/.dark` 和 `data-theme`;Modal、Dropdown、
Autocomplete、Calendar、Tooltip、ActionBar 与 ChartTooltip 不建立局部主题。
详情查看态的 `rc-detail-view-surface` 是共享结构类,只把权威 `--radius` 映射为设计系统规定的
16px 容器圆角;背景、前景、边框和交互仍由 HeroUI `Surface variant="secondary"` 所有。
页面不允许为 Surface、Tabs、DataGrid、Modal 或 AlertDialog 增加私有外观规则。
Inter 400/500/600 拉丁子集由 `.umirc.ts` 生成的常驻 `public/fonts/inter.css`
stylesheet link 加载,不参与 `umi.css` 热替换。`src/tailwind.css` 不导入字体
CSS,并用显式 `@source` 仅扫描入口、组件、页面和 wrapper;`src/.umi` 与
`src/.umi-production` 不进入 Tailwind 扫描范围,避免生成文件反向触发 CSS 编译循环。
## 变更门槛
未来 UI 改动必须先比较“问题整改”相同视口,再运行构建、diff check、鑫元审计、旧组件与
私有视觉扫描、路径扫描、依赖树和 Pro 子路径检查。任何页面重新引入旧组件、私有外观、
可见原生控件、跨项目依赖或改变业务协议,都不能进入交付范围。
配置化工具栏、批量 ActionBar 和详情操作区在共享层按 HeroUI 实际 variant 做稳定分组:`secondary`、
`tertiary` 等辅助操作保持业务传入顺序,`primary` 操作统一追加到最右侧。该规则只影响
呈现顺序,不改变权限判断、禁用条件、回调引用、请求时机或状态流。
动态表单模板缺失或定义加载失败属于当前 Disclosure 内容的内嵌不可用状态,使用 HeroUI Pro
`EmptyState`,不再叠加带 `surface-shadow` 的 Alert,也不在错误状态下继续渲染 DataGrid。
后端表单标识和英文错误不直接暴露给业务用户;编辑态保留原模板选择控件以便重新配置。
服务端 View 的 DataGrid 列标题由共享 `buildViewColumns` 统一解析:优先使用 Subject 字段
标签,其次使用 View 自带的非技术标题,再使用已确认的兼容字段定义。任何无法解析的
`metadata.*` 路径只保留为内部取值路径,不允许原样成为业务表头。
动态表单缺少字段定义时不从数据对象 key 猜测表头,改为 Pro EmptyState,避免把内部字段
标识或存储结构作为业务界面。
关联记录在缺少 Subject 字段定义时同样只保留标题列,不从 `metadata` key 生成未知表头。
# UI 迁移清单
## 接入与依赖
- [x] 在 `codex/ui-ux-xinyuan-migration` 独立分支工作
- [x] 未修改项目与“问题整改”基准先启动、后审计
- [x] 项目根目录成功执行官方 HeroUI Pro setup,授权值未落盘
- [x] `@heroui-pro/react@1.0.0-beta.8` dist 与要求子路径全部可解析
- [x] 清除跨项目依赖,只保留项目自身 pnpm virtual store
- [x] 删除 `package-lock.json`,仓库只保留 `pnpm-lock.yaml`
- [x] HeroUI/React Aria 子图统一使用 React 19.2.8,无重复 Context
## 架构与主题
- [x] 权威 `theme.css` 字节一致,Tokens 只扩展 `--rc-*`
- [x] 根级浅/深色主题、持久化和 Portal 继承
- [x] Catalyst、Headless UI、Heroicons、ECharts 和旧包装引用归零
- [x] 可见原生 select/date/datetime/file 和浏览器 confirm/alert 归零
- [x] 页面/共享组件私有外观 utility 与页面私有样式文件归零
- [x] `file:`、本机依赖路径和授权字面值归零
## Shell 与登录
- [x] 居中登录 Card、鑫元 Logo、标题、统一字段高度和 24px 节奏
- [x] 登录账号/密码、真实验证码与 token 提交,密码加密、失败刷新、失败反馈和 redirect
- [x] HeroUI Pro Sidebar 真 offcanvas,移动面板与基准同为 80vw
- [x] Pro Navbar/Sidebar.Trigger 与 48/32/16px 对称边距
- [x] HeroUI Modal 应用切换,一列/两列和唯一当前状态
- [x] Dropdown 用户菜单、真实退出、token 清理和 base-aware 跳转
- [x] 浅深色主题切换、刷新持久化与深色 Portal
## 页面与组件
- [x] 驾驶舱:4 个 Pro KPI、PieChart/BarChart/ComposedChart/ChartTooltip
- [x] 项目列表/归档/外部填写:Tabs、Surface、Autocomplete、DatePicker、Disclosure
- [x] 全部业务列表:Pro DataGrid、原生选择/排序/固定列、Pagination、ActionBar、EmptyState
- [x] DataGrid 表头只显示业务标签,内部 `metadata.*` 路径和 snake_case 标识不对用户可见
- [x] 工具栏顺序与移动端搜索独占行
- [x] 工具栏、详情操作区和字段编辑区的 `primary` 主操作统一位于最右侧
- [x] 详情/表单:4xl Modal、查看态单一 L2 Surface、编辑态官方 secondary 控件
- [x] 删除/危险操作:AlertDialog,取消不发请求
- [x] 项目表单与附件:Pro DropZone,无可见原生 file 控件
- [x] form 字段:名称/行数/动态字段 DataGrid,不暴露内部 JSON 或元数据
- [x] form 编辑:Autocomplete、Disclosure、Tabs、Modal、AlertDialog,统一外层保存
- [x] Dashboard 图表和 KPI 不使用测试数字或源码 fixture
- [x] 动态表单模板不可用状态使用内嵌 Pro EmptyState,不产生额外浮起 Surface
## 浏览器验收
- [x] 1440x900 浅色/深色目标与基准对照
- [x] 1280x720 浅色/深色目标与基准对照
- [x] 768x1024 浅色/深色目标与基准对照
- [x] 390x844 浅色/深色目标与基准对照
- [ ] 验证码恢复后的首次加载、点击刷新、失败重载及四视口浅深色对照(本轮浏览器实例不可用,待重验)
- [x] 登录、退出、Sidebar、Navbar、应用切换和用户菜单
- [x] Tabs、筛选展开/收起、Dropdown/ListBox、DatePicker、Tooltip
- [x] 三处活动 Tabs 均显式使用 HeroUI `primary` 胶囊轨道,按文字收缩且窄屏由官方容器滚动
- [ ] 页面数据集、详情阶段与动态表单分组的切换业务回调未变,本轮浏览器对照待补
- [ ] 详情标题、阶段 Tabs 和顶部操作固定在 Modal Header,只有 Body 滚动;本轮浏览器滚动对照待补
- [x] Modal 查看态复杂字段使用官方 L3 Surface,与 L2 DataGrid 表头和 L1 数据行分层
- [x] 结构化筛选使用原生 Surface;关键词只在列表工具栏,单/双字段不再铺满页面背景
- [x] 实时筛选无全局刷新/重置;状态与年度选择通过 Autocomplete 框内清除
- [x] Checkbox 选中/半选、DataGrid 内部滚动/固定列、Pagination、ActionBar
- [x] DataGrid 长文本限制在所属列内,两行预览并通过 HeroUI Tooltip 展示全文
- [ ] 弹窗只读关联记录表按实际行高收缩,不再出现整块巨型 Hover 行;浏览器 Hover 回归待补
- [x] ActionBar 随 Sidebar 展开/折叠在主内容视口内重新居中
- [x] 驾驶舱重复项目清单使用 HeroUI Pro ListView,不使用 Button 模拟列表项
- [x] Modal 查看/编辑/取消、AlertDialog 取消、DropZone
- [x] Modal 24px 圆角,详情 L2 Surface 16px 圆角;Overlay 与 DataGrid 表头计算色阶可区分
- [x] 项目表单/检查底稿/审计需求材料在四视口浅深色下显示真实业务行
- [x] 动态表单行修改先进入草稿,保存前无 PUT,外层保存后一次 PUT
- [x] 详情弹窗空表单实例不计数、不显示空分组,零行不创建空 DataGrid
- [x] 关联记录解包 raw、隐藏全空列和技术字段,纯 ID 使用明确不可用状态
- [x] 查看态标题不在正文重复,占位标题不显示为 Modal 副标题
- [x] KPI、Pie/Bar/Composed Chart 和打开态 ChartTooltip
- [x] 四视口无页面横向溢出,目标控制台 0 errors / 0 warnings
- [ ] 长文本列修复后的四视口浅/深色截图与 Tooltip 复验(本轮浏览器实例不可用)
## 交付检查
- [x] `npm run build` 成功
- [x] `git diff --check` 成功
- [x] 鑫元 `audit_project.sh` 退出 0
- [x] 权威主题 `cmp` 成功
- [x] 旧组件、原生控件、旧颜色、页面私有视觉和路径扫描为零
- [x] 全局路由加载态使用 HeroUI Spinner,页面直接导入图标包为零
- [x] `pnpm ls` 与 HeroUI/React Aria 定向 `npm ls` 通过
- [x] Pro 六个 JS 子路径 import 成功,CSS export resolve 成功
- [x] 四份项目 UI 文档更新为最终事实
- [x] 未提交、未推送、未合并
说明:扩展的全项目 `npm ls ... react react-dom` 会报告两个 Pro peer 包仍声明 React 18
peer,以及 Umi 工具链锁定的 React 18 子图;应用与 HeroUI/React Aria 子图实际统一解析
React 19.2.8,构建和浏览器验收通过。详见 `docs/xinyuan-ui-audit.md`。
# 审计项目管理鑫元设计系统
## 权威边界
本应用以“问题整改”的实际运行结构作为唯一实现基准,并使用原生 HeroUI 3 / HeroUI Pro
compound API。`src/design-system/theme.css` 必须与 `xinyuan-design-system` 权威副本逐字节
一致;`tokens.css` 只增加批准的 `--rc-*` 语义与布局 Token,不重定义 HeroUI 核心主题。
全局加载顺序为 Tailwind、HeroUI、HeroUI Pro、权威 `theme.css`、鑫元 `tokens.css`、共享
Shell 样式。页面 className 只承担布局;组件外观由官方 variant/size/state、权威主题和批准
的共享 Token 持有。禁止页面覆盖 HeroUI slot、BEM、伪元素和交互状态。
Inter 400/500/600 拉丁子集由 `.umirc.ts` 中的常驻
`public/fonts/inter.css` link 加载。禁止在 `tailwind.css` 中重新导入
`@fontsource/inter/*.css`,避免 Umi HMR 替换 `umi.css` 时使英文、数字和符号在
Inter 与系统回退字体之间切换。
## 身份与 Shell
- 统一应用名称为 `审计项目管理`;浅色使用红色鑫元 Logo,深色使用白色 Logo。
- 登录按“问题整改”保留真实验证码:首次加载 `/auth/captcha`,提交 `captcha/token`,失败后
清空并刷新;账号、密码和验证码控件等高,验证码输入与图片使用批准的两列网格。
- HeroUI Pro Sidebar 为桌面 240px,`collapsible="offcanvas"`;移动端使用官方 80vw。
- HeroUI Pro Navbar 高 64px,直接使用 `Sidebar.Trigger`;Navbar 与页面共享桌面/平板/
手机 48/32/16px 对称 gutter。
- Sidebar 用户入口必须使用 `Dropdown.Trigger`,退出调用既有 auth service 和 base-aware 路由。
- 应用切换使用 HeroUI Modal:46rem/42rem 几何,移动一列、`sm` 起始终两列;每个 Logo
使用 36px `Surface tertiary + Avatar soft` identity well,只允许一个当前应用。
- 根主题写入 `xinyuan_color_theme`,所有 Portal、Sidebar.Mobile、ActionBar 和图表 Tooltip
继承同一根主题。
## Surface 与页面节奏
- L0 页面为 `background`;首层结构为 `Surface default` / L1。
- Surface/Card/Modal 内字段一律使用 HeroUI `secondary` / L2;查看态 `form` 与引用记录属于
L2 字段组中的真实静态结构对象,整体使用官方 `Surface tertiary` / L3,使其中的 DataGrid
L2 表头和 L1 数据行保持可辨;普通标量字段不得逐项卡片化。
- Modal、Popover、Dropdown、Tooltip 和 Calendar 使用官方 `overlay`,不属于 L3。
- 页面首层模块、普通模块、紧密相关列表/KPI 分别使用 32/24/16px 间距;移动端为 24/16px。
- 列表标题和工具栏透明无框;搜索在左、列分组紧随其后、业务操作在右、主操作最右;
手机搜索独占第一行。DataGrid 是列表唯一数据 Surface。
## 组件合同
- 基础交互直接从 `@heroui/react` 导入;Sidebar、Navbar、DataGrid、DropZone、ActionBar、
EmptyState、KPI 和 Chart 使用 `@heroui-pro/react` 或已验证子路径。
- 交互使用 `onPress`,compound 组件使用点号结构;图标统一从 `AppIcons` 输出 Ant Design
Icons,图标按钮必须有 Tooltip 和可访问名称。
- 三个及以上结构化筛选使用 HeroUI Surface,前 3 个字段在主行,其余字段进入
Disclosure;枚举/引用为 Autocomplete,日期为 DatePicker,附件为 Pro DropZone。
- 页面级、模块级与详情级 Tabs 均使用 HeroUI compound API、显式 `variant="primary"` 并按
标签内容收缩;页面主数据集、详情业务阶段与动态表单 `settings.group` 分组统一使用官方
胶囊轨道。窄屏
只由 `Tabs.ListContainer` 负责横向滚动,不得用私有 CSS 重画 Tabs。
- 同一操作组中辅助按钮保持原相对顺序,HeroUI `primary` 主操作始终位于最右侧;共享工具栏、
批量 ActionBar 和详情配置化操作区必须统一执行该排序,页面不得用私有定位样式修正按钮顺序。
- DataGrid 使用原生 `selectedKeys`、`onSelectionChange`、排序、固定列、Pagination、
ActionBar 和 EmptyState。固定每页 10 条,不显示页容量控件。
- DataGrid 表头必须显示 Subject 或 View 提供的业务标签;`metadata.*`、snake_case 和其它
技术路径不得作为可见表头。已确认的跨对象兼容字段使用数据定义文档中的标签和类型。
- 普通详情/表单使用 4xl HeroUI Modal;查看态整组字段仅有一个 secondary Surface,编辑态
移除该 Surface,取消恢复原值且不得请求接口。删除使用受控 AlertDialog。
- Subject `form` 字段保持 `{ form, data, settings }` 存储协议,但业务 UI 只显示表单名称、
行数和动态字段 DataGrid。多值表单按 `settings.group` 使用 Tabs 分组,实例使用 Disclosure;
禁止以 JSON、`pre`、Chip 摘要或内部 `_id/status/settings` 代替实际表单内容。
- 动态表单模板使用 HeroUI Autocomplete,数据行使用 4xl Modal 和既有动态字段校验;行级
修改只进入详情草稿,必须由详情外层保存统一写回,不得增加即时更新请求。
- 动态表单查看态只呈现具有真实模板标识的实例;空实例和空分组仅属于编辑草稿。零行模板
使用 HeroUI Pro EmptyState,不渲染空 DataGrid。
- 动态表单模板不可用时使用当前 Disclosure 内的 HeroUI Pro EmptyState,不使用带阴影的
Alert 形成额外 Surface,不继续渲染 DataGrid,也不向业务用户暴露内部模板 ID 或英文错误。
- 引用记录展示必须解包 `raw`,以标题为首列,仅保留有业务值且具有业务标签的字段;只有
ID 而没有可展示明细时使用明确状态,不得以空 DataGrid 冒充内容。
- 数字指标使用 Pro KPI;图表使用 Pro PieChart、BarChart、ComposedChart 与 ChartTooltip,
系列色来自批准的图表 Token,不自制 Tooltip 或图表主题。
## 禁止事项
- Catalyst、Headless UI、Heroicons、ECharts、兼容旧 props 的 UI 包装层。
- 可见原生 select/date/datetime/file、原生 table、window confirm/alert。
- 页面自制背景、颜色、边框、圆角、阴影、Hover/Focus/Selected/Disabled 外观。
- 手写表格、分页、弹窗、KPI、Tooltip、图表主题或 HeroUI 仿制组件。
- `file:`、绝对工作站路径、相邻项目 `node_modules`、本机 vendor、授权值入库。
- 修改 API、参数、数据结构、路由、权限、校验、分页口径、回调、请求时机或状态流。
## 新功能验收
每个新模块必须在目标与“问题整改”相同视口、相同主题下同时打开比较,并覆盖打开态
Portal、错误/空/加载、键盘 focus 和移动响应式。合入前运行 `npm run build`、
`git diff --check`、鑫元审计脚本、旧组件/私有视觉/本机路径扫描、依赖树和 Pro 子路径检查;
不得以构建成功替代浏览器验收。
This diff is collapsed.
This diff is collapsed.
......@@ -8,27 +8,53 @@
"start": "npm run dev"
},
"dependencies": {
"@headlessui/react": "^2.2.10",
"@heroicons/react": "^2.2.0",
"@ant-design/icons": "^6.3.2",
"@fontsource/inter": "^5.3.0",
"@heroui-pro/react": "1.0.0-beta.8",
"@heroui/react": "^3.2.4",
"@heroui/styles": "^3.2.4",
"@internationalized/date": "^3.12.3",
"@number-flow/react": "^0.6.2",
"@react-aria/utils": "^3.34.1",
"@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",
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.20",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.6",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"jszip": "^3.10.1",
"maplibre-gl": "^6.4.1",
"marked": "^18.0.10",
"motion": "^12.23.24",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^19.2.8",
"react-aria-components": "^1.20.0",
"react-dom": "^19.2.8",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.12.3",
"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-variants": "^3.3.1",
"tailwindcss": "^4.2.0",
"umi-request": "^1.4.0",
"umi-request-progress": "^1.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"typescript": "^5.0.3"
}
}
This diff is collapsed.
......@@ -3,3 +3,5 @@ allowBuilds:
core-js-pure: false
es5-ext: false
esbuild: false
"@heroui-pro/react": true
"@zowe/secrets-for-zowe-sdk": true
@font-face {
font-family: "Inter";
font-style: normal;
font-display: swap;
font-weight: 400;
src: url("./inter-latin-400-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: swap;
font-weight: 500;
src: url("./inter-latin-500-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: swap;
font-weight: 600;
src: url("./inter-latin-600-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
import '@/global.less';
import '@/tailwind.css';
import { defineApp } from 'umi';
import { APPLICATION_BASE_PATH, THEME_STORAGE_KEY } from '@/config/application';
if (typeof document !== 'undefined') {
const storedTheme = window.localStorage?.getItem?.(THEME_STORAGE_KEY);
const theme = storedTheme === 'dark' ? 'dark' : 'light';
document.documentElement.classList.toggle('dark', theme === 'dark');
document.documentElement.classList.toggle('light', theme === 'light');
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
}
const LEGACY_LOGIN_PATH = '/login';
const appBasePath = String(
AUDIT_PROJECT_MANAGER_APP_BASE_PATH || APPLICATION_BASE_PATH || '/',
).replace(/\/+$/, '');
if (
typeof window !== 'undefined' &&
appBasePath &&
window.location.pathname === LEGACY_LOGIN_PATH
) {
window.location.replace(
`${appBasePath}${LEGACY_LOGIN_PATH}${window.location.search}${window.location.hash}`,
);
}
export async function getInitialState() {
return {
......
# 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 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>
)
})
This diff is collapsed.
@import 'tailwindcss';
@theme {
--font-sans: Inter, sans-serif;
--font-sans--font-feature-settings: 'cv11';
}
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.
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