Commit 627f09e4 authored by Andy-bubu's avatar Andy-bubu

feat: migrate OA process monitor to Xinyuan HeroUI

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