Commit 48f2819d authored by Andy-bubu's avatar Andy-bubu

feat: migrate model analysis to Xinyuan HeroUI

parent 963b3a6e
...@@ -11,16 +11,23 @@ const ROUTE_PATHS = { ...@@ -11,16 +11,23 @@ const ROUTE_PATHS = {
models: '/models', models: '/models',
modelForwards: '/models/forwards', modelForwards: '/models/forwards',
} as const; } as const;
const APP_BASE_PATH = '/model_analysis/';
export default defineConfig({ export default defineConfig({
layout: false, layout: false,
fastRefresh: true, fastRefresh: true,
base: APP_BASE_PATH,
publicPath: APP_BASE_PATH,
links: [
{
rel: 'stylesheet',
href: `${APP_BASE_PATH}fonts/inter.css`,
},
],
alias: { alias: {
react: resolvePackageRoot('react'), react: resolvePackageRoot('react'),
'react-dom': resolvePackageRoot('react-dom'), 'react-dom': resolvePackageRoot('react-dom'),
}, },
base: '/model_analysis/',
publicPath: '/model_analysis/',
routes: [ routes: [
{ {
path: ROUTE_PATHS.login, path: ROUTE_PATHS.login,
...@@ -115,6 +122,9 @@ export default defineConfig({ ...@@ -115,6 +122,9 @@ export default defineConfig({
} }
}, },
esbuildMinifyIIFE: true, esbuildMinifyIIFE: true,
jsMinifierOptions: {
target: ['chrome80', 'es2020'],
},
define: { define: {
STREAMS_API_URL: '/api', STREAMS_API_URL: '/api',
STREAMS_PARSER_URL: '/parser', STREAMS_PARSER_URL: '/parser',
......
# UI 架构
## 层次
1. `.umirc.ts` + `public/fonts/inter.css`:常驻 Inter 400/500/600 拉丁字符字体入口,不参与 `umi.css` 热替换。
2. `src/tailwind.css`:Tailwind、HeroUI、HeroUI Pro CSS 入口;使用 `source(none)` 和显式源码目录,排除 Umi 生成目录。
3. `src/design-system/theme.css`:不可修改的权威 HeroUI 主题。
4. `src/design-system/tokens.css`:Skill 批准的鑫元扩展 Token。
5. `src/design-system/xinyuan.css`:登录与 Shell 几何、应用切换宽高、KPI 布局。
6. 页面 JSX:仅业务组合和布局,不包含私有组件外观。
## 字体稳定性
- Inter 字体文件固定在 `public/fonts/`,由 Umi HTML 中的常驻 `<link>` 加载;开发态替换 `umi.css` 时不会卸载字体注册。
- `src/tailwind.css` 禁止重新导入 `@fontsource/inter/*.css`,并禁止扫描 `src/.umi`,避免生成文件反向触发 CSS 编译循环。
## 组件边界
- `Layout.jsx`:HeroUI Pro Sidebar/Navbar、主题切换、应用切换、退出。
- `ApplicationSwitcherDialog.jsx`:HeroUI Modal/Card/Surface/Avatar。
- `XinyuanModal.jsx`:HeroUI `Modal.Root > Backdrop > Container > Dialog` 的业务无关组合,集中实现 320–1216px 设计系统宽度梯度和 28px 标准内容 inset;扩展尺寸由官方 `lg` Container 加共享最大宽度实现,不向 HeroUI v3 传递无效的 `xl/4xl` size。圆角、背景、阴影和交互状态仍由 HeroUI 官方 Modal 样式负责。
- `ModelMonitorShared.jsx`:业务中性的 SearchField、DataGrid、Pagination、ActionBar、状态 Chip、动态字段组合。
- `XinyuanAutocomplete.jsx`:HeroUI Autocomplete/SearchField/ListBox 组合,暴露当前 HeroUI 值契约,不兼容旧 Select props。
- 页面:直接组合 HeroUI/HeroUI Pro,不通过 Catalyst、Headless UI 或旧 primitive wrapper。
## 数据流
页面状态和 service 调用保持迁移前的业务边界。组件只把 HeroUI 的 `onPress`、`onChange`、`onSelectionChange` 和 `selectedKeys` 映射回已有 handler;API 路径、metadata、Record/Subject 字段和审批状态不在 UI 层重定义。
## 删除边界
`src/catalyst-ui-kit`、不可达资源预约模板及 ECharts 统计模板已从仓库移除。它们不在 `.umirc.ts` 的活动路由中,备份为 `/tmp/model-analysis-legacy-ui.iaSi6Y`。
# UI 迁移验收清单
## 已完成
- [x] 官方 HeroUI Pro 安装成功,完整包和六个要求子路径可导入
- [x] 单一 `pnpm-lock.yaml`,无 `file:`、vendor、本机绝对路径或跨项目依赖
- [x] 权威 `theme.css` 与 `tokens.css` 逐字节一致
- [x] 登录、Sidebar、Navbar、应用切换和退出结构迁移
- [x] SearchField、Autocomplete、DatePicker、Checkbox、Tabs 使用 HeroUI compound API
- [x] 模型、批次、批次数据、转发审批列表迁移为 DataGrid
- [x] DataGrid 原生选择、真实溢出固定列、横向滚动、单一空态
- [x] Pagination 固定 10 条、最多五页窗口、无页大小选择
- [x] ActionBar 固定定位并对齐活动内容区
- [x] 弹窗使用 HeroUI Modal/AlertDialog;全部业务 Modal 接入共享 320–1216px 尺寸梯度,审批详情使用单一 secondary Surface
- [x] 批量同步弹窗使用 HeroUI Pro Stepper/ListView、HeroUI ScrollShadow 和正确 Checkbox anatomy;移除手写灰色模型/批次分组及重复 Surface
- [x] KPI 使用 HeroUI Pro compound API 与 16px grid gap
- [x] Catalyst、Headless UI、ECharts、原生可见 select/date/file 和旧兼容包装仓库扫描为零
- [x] 活动页面私有颜色/背景/描边/圆角/阴影类扫描为零
- [x] 业务 API、参数、校验、回调、状态流和请求时机保持不变
## 最终命令
- [x] `npm run build` / `pnpm build`
- [x] `git diff --check`
- [x] 鑫元 `audit_project.sh`
- [x] 依赖树与 Pro 子路径检查
- [x] 旧组件、私有视觉 CSS、本机依赖路径扫描
## 浏览器
- [x] 1440x900 登录页与问题整改双应用对比
- [x] 1280x720 登录页浅色/深色
- [x] 768x1024 登录页浅色/深色
- [x] 390x844 登录页浅色/深色
- [ ] 鉴权后 Shell、退出、应用切换、Dropdown、Tabs、DataGrid、Pagination、ActionBar、Modal、KPI 实数据验收
鉴权后验收必须使用真实登录凭据或既有有效 session;不得伪造 token、用户或业务数据。2026-08-21 检查现有 Playwright 会话,未发现模型分析的有效登录态,因此该项当前被真实凭据阻断。
# 模型分析鑫元设计系统
本项目的唯一视觉与交互基准为已确认的“问题整改”应用,业务行为以 `record-api.md`、`subjectsDefinition.md` 和 `model-monitor.business-flow.md` 为准。
## 基础约束
- 页面和共享组件直接使用 `@heroui/react`、`@heroui-pro/react` 及官方子路径。
- 权威 `theme.css` 原样加载,SHA-256 为 `a0bf6f243aaf2c94cc8b577145d33d765f8ecab863b3a3b4a358bad4d1a05901`;`tokens.css` 仅提供 Skill 批准的扩展 Token。
- Inter 400/500/600 拉丁子集由 `.umirc.ts` 生成的常驻 `public/fonts/inter.css` link 加载;不得在 `tailwind.css` 中导入字体 CSS,避免 Umi HMR 替换 `umi.css` 时造成英文、数字和符号字体回退抖动。
- 页面 `className` 只承担 flex、grid、尺寸、间距、对齐、截断和响应式布局,不定义组件颜色、边框、圆角、阴影或状态;次要文字使用 HeroUI `Description`,必填星号使用 `Label isRequired`。
- 文字命令按钮不加装饰图标;真实图标操作使用 HeroUI Button、Tooltip 与可访问名称。
## Shell 与登录
- Sidebar 为 HeroUI Pro offcanvas,宽 240px,折叠为零宽;单菜单组不显示 GroupLabel。
- Sidebar Footer 使用直接 `Dropdown.Trigger`、HeroUI Avatar 和真实退出回调。
- Navbar 为透明 64px HeroUI Pro Navbar;Trigger 使用与基准一致的实心 16px Sidebar 图标。
- Navbar 与页面共用 48/32/16px 对称 gutter。
- 登录页使用居中 28rem Card、36px Logo、40px 字段、24px 字段节奏和 46px 提交按钮;验证码及原鉴权协议保留。
- 应用切换为 HeroUI Modal,宽 46rem、高 42rem,移动一列、`sm` 起两列,36px identity well。
## 业务组件
- 筛选:HeroUI Surface、SearchField、Autocomplete、DatePicker、Disclosure。
- 列表:HeroUI Pro DataGrid,原生选择/排序/固定列/横向滚动;空态使用 `renderEmptyState`。
- 分页:HeroUI compound Pagination,固定 10 条,最多五个连续页码,无页大小选择器。
- 批量操作:HeroUI Pro ActionBar 固定于视口并居中于活动内容区。
- 弹窗:HeroUI Modal/AlertDialog;统一通过 HeroUI 原生 `XinyuanModal` compound 组合解析尺寸梯度:`xs` 320px、`sm` 384px、`md` 480px、`lg` 640px、`xl` 736px、`2xl` 840px、`3xl` 960px、`4xl` 1088px、`5xl` 1216px。确认弹窗使用 `md`,短单列表单使用 `lg`,模型归档双列表单使用 `2xl`,内容型详情/转发/批量同步使用 `4xl`;查看态仅一个 secondary Surface。禁止将 HeroUI v3 不支持的 `xl` 至 `5xl` 直接传给 `Modal.Container`。
- 普通 Modal 保持 HeroUI 官方 24px 圆角和 Overlay shadow,统一使用 28px panel inset;Modal 内承担真实 L2 边界的 `Surface secondary` 使用基准项目一致的 `rounded-2xl`(16px),不得保留大面积直角灰色块。关闭按钮直接使用无子节点的 `Modal.CloseTrigger`,由 HeroUI 渲染原生 16px CloseIcon,禁止替换为受字体基线影响的第三方图标;关闭按钮、标题、正文和 Footer 对齐同一内容边界,标题到正文 12px、正文到 Footer 24px。批量同步使用紧凑 HeroUI Pro Stepper,年份工具区使用正确 Checkbox compound anatomy,批次选择使用单一 HeroUI Pro ListView `primary` 原生多选和 HeroUI ScrollShadow,不使用页面自制灰色分组块或重复 Surface。
- KPI:HeroUI Pro KPI compound API;四项使用 `.rc-kpi-grid` 16px gap,标题不附加说明图标。
- 当前活动业务没有图表;不得为视觉验收制造图表或测试业务数据。未来增加时只使用 HeroUI Pro chart 与 ChartTooltip。
## 数据保护
- Reference 写入仍只保存 ID 或现有页面协议对象;枚举来自 `settings.options`。
- DatePicker 保持既有 service 转 ISO 流程;动态 Subject 继续使用现有 `loadSubject`/subject definition。
- UI 迁移不得改变 API、路由、权限、校验、保存回调、请求时机、分页口径或审批状态。
# 模型分析 UI 审计
审计日期:2026-08-21
实现基准:已确认的“问题整改”应用。
## 安装硬门槛
- 执行目录:`/Users/liuchenlong/Desktop/job/model_analysis`
- 官方 `hpsetup` 命令:成功,退出码 0;授权值未写入仓库或本文档。
- `@heroui-pro/react`:`1.0.0-beta.8`
- 实际解析:`node_modules/.pnpm/@heroui-pro+react@1.0.0-beta.8/node_modules/@heroui-pro/react/package.json`
- 完整包文件:`dist/index.js`、`dist/css/index.css` 均存在。
- 已验证子路径:`sidebar`、`navbar`、`data-grid`、`drop-zone`、`action-bar`、`empty-state`。
- 依赖变化:更新 `package.json` 和 `pnpm-lock.yaml`,新增 `pnpm-workspace.yaml`;删除旧 `package-lock.json` 与本机 vendor TGZ。唯一锁文件为 `pnpm-lock.yaml`。
- 依赖中不存在 `file:`、相邻项目 `node_modules`、软链接或 `/Users/...` 路径。
## 业务与路由
| 路由 | 页面 | UI 状态 |
| --- | --- | --- |
| `/login` | 登录 | HeroUI Card/TextField/Input/Button,验证码和原登录协议保留 |
| `/models/overview` | 模型总览 | HeroUI Pro KPI、Widget、ListView、ItemCard |
| `/models` | 全部模型 | HeroUI Surface、SearchField、DataGrid、Modal/AlertDialog |
| `/models/:modelId/batches` | 模型批次 | HeroUI Pro DataGrid、Modal、ProgressBar |
| `/models/:modelId/batches/:batchId/data` | 批次数据 | DataGrid 原生选择、固定列、Pagination、ActionBar、Modal |
| `/models/forwards` | 转发审批 | Tabs、DataGrid、单一 secondary Surface 详情、4xl Modal |
历史 `/analysis/*` 与 `/resource/*` 仅重定向到上述活动路由。不可达的资源预约/统计模板以及 `src/catalyst-ui-kit` 已从仓库移除,备份位于 `/tmp/model-analysis-legacy-ui.iaSi6Y`。
## 鉴权与业务保护
- 登录继续调用 `getCaptcha()`、`userLogin()`,保持 AES 密码、`captcha/token/type`、localStorage 键和 redirect。
- Auth wrapper 继续通过 `getUserInfo()` 合并用户;退出继续调用 `userLogout(username)`,最终清理登录态并进入 `/login`。
- 模型同步、批次同步、动态 Subject、隐藏记录、转发、填写和两级审批的 service、参数、校验、调用顺序与状态条件未修改。
- 批次数据每页固定 10 条;Pagination 最多显示五个连续页码,无页大小选择器。
- 没有生成测试数字、浏览器 fixture 或伪造业务数据。
## 组件与样式审计
- 活动页面直接使用 `@heroui/react`、`@heroui-pro/react` 及 Pro 子路径,没有旧 props 兼容包装。
- `XinyuanAutocomplete` 是 HeroUI 原生业务中性组合,远程用户选择保留现有对象协议。
- `XinyuanModal` 是 HeroUI 原生业务中性 compound 组合;已清除向 `Modal.Container` 直接传递不受支持的 `xl/4xl` 值,应用切换、分组、模型编辑、同步、隐藏管理、转发、审批详情和填写弹窗均按共享尺寸梯度约束。
- 批量同步弹窗此前将 Checkbox 标签放在 `Checkbox.Content` 外部,并用 `Surface tertiary + Checkbox` 手写模型分组列表,造成方框/文案错位和陌生灰色块。现已改为正确 Checkbox compound 层级、紧凑 HeroUI Pro Stepper、单一 ListView `primary` 原生多选与 ScrollShadow;来源模型信息进入批次行说明,保留原年份筛选、跨年份选择、已同步禁用和提交回调。
- 核心列表均使用 DataGrid;空态由 `renderEmptyState` 单一渲染。
- 页面视觉色、背景、描边、圆角、阴影及 HeroUI 内部状态覆盖扫描为零;次要文字直接使用 HeroUI `Description`,必填状态由 `Label isRequired` 管理。
- `xinyuan.css` 仅保留批准的登录基准、Shell 几何、应用切换宽高和 KPI 布局;组件外观由权威主题及官方 variant 管理。
- 当前业务需求没有活动图表;旧 ECharts 页面已移除,因此没有伪造图表或业务数据。
## 主题
- `src/design-system/theme.css` 与 Skill 权威文件逐字节一致。
- SHA-256:`a0bf6f243aaf2c94cc8b577145d33d765f8ecab863b3a3b4a358bad4d1a05901`
- `tokens.css` 与 Skill 副本逐字节一致。
- Shell gutter:桌面 48px、平板 32px、移动 16px,Navbar 与页面对称共用。
## 浏览器证据
- 目标:`http://localhost:8004/model_analysis/`
- 基准:`http://localhost:8002/problem_rectification_new/`
- 同一 Playwright 会话内完成登录页 1440x900 基准比较;关键卡片、字段、验证码和提交按钮几何一致。
- 最终多视口、浅/深色与鉴权页面验收结果见 `docs/ui-migration-checklist.md`。
This diff is collapsed.
...@@ -5,28 +5,58 @@ ...@@ -5,28 +5,58 @@
"dev": "max dev", "dev": "max dev",
"postinstall": "max setup", "postinstall": "max setup",
"setup": "max setup", "setup": "max setup",
"start": "npm run dev" "start": "npm run dev",
"verify:ui-runtime": "node scripts/verify-ui-runtime.mjs"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.10", "@ant-design/icons": "^6.3.2",
"@heroicons/react": "^2.2.0", "@fontsource/inter": "5.3.0",
"@tailwindcss/postcss": "^4.2.0", "@heroui-pro/react": "1.0.0-beta.8",
"@umijs/max": "^4.5.3", "@heroui/react": "3.2.2",
"@heroui/styles": "3.2.2",
"@internationalized/date": "3.12.3",
"@number-flow/react": "0.6.2",
"@react-aria/utils": "3.34.1",
"@tailwindcss/postcss": "4.2.4",
"@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.6.51",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"echarts": "^6.1.0", "embla-carousel": "^8.6.0",
"echarts-for-react": "^3.0.6", "embla-carousel-react": "^8.6.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"react": "^18.3.1", "maplibre-gl": "^6.4.1",
"react-dom": "^18.3.1", "marked": "^18.0.10",
"tailwindcss": "^4.2.0", "motion": "12.35.1",
"react": "19.2.7",
"react-aria": "3.51.0",
"react-aria-components": "1.20.0",
"react-dom": "19.2.7",
"react-markdown": "^10.1.0",
"react-resizable-panels": "4.12.2",
"react-stately": "3.49.0",
"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.2.2",
"tailwindcss": "4.2.4",
"umi-request": "^1.4.0", "umi-request": "^1.4.0",
"umi-request-progress": "^1.1.0" "umi-request-progress": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.33", "@types/react": "^19.2.17",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^19.2.3",
"typescript": "^5.0.3" "typescript": "^5.0.3"
} }
} }
This diff is collapsed.
onlyBuiltDependencies:
- '@heroui-pro/react'
- '@zowe/secrets-for-zowe-sdk'
@font-face {
font-family: "Inter";
font-style: normal;
font-display: swap;
font-weight: 400;
src: url("./inter-latin-400-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: swap;
font-weight: 500;
src: url("./inter-latin-500-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: swap;
font-weight: 600;
src: url("./inter-latin-600-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
import { readFileSync, realpathSync } from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const require = createRequire(import.meta.url);
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const expectedVersions = {
'@heroui-pro/react': '1.0.0-beta.8',
'@heroui/react': '3.2.2',
'@heroui/styles': '3.2.2',
'@react-aria/utils': '3.34.1',
react: '19.2.7',
'react-aria-components': '1.20.0',
'react-dom': '19.2.7',
};
function packageMetadata(packageName) {
const packageJson = require.resolve(`${packageName}/package.json`);
return {
packageJson: realpathSync(packageJson),
value: JSON.parse(readFileSync(packageJson, 'utf8')),
};
}
let heroUiProRoot = '';
for (const [packageName, expectedVersion] of Object.entries(expectedVersions)) {
const { packageJson, value } = packageMetadata(packageName);
const expectedRoot = path.join(projectRoot, 'node_modules');
if (!packageJson.startsWith(expectedRoot + path.sep)) {
throw new Error(`${packageName} resolved outside this project: ${packageJson}`);
}
if (value.version !== expectedVersion) {
throw new Error(`${packageName} expected ${expectedVersion}, received ${value.version}`);
}
if (packageName === '@heroui-pro/react') {
heroUiProRoot = path.dirname(packageJson);
}
console.log(`${packageName}@${value.version}: ${packageJson}`);
}
const cssPath = realpathSync(fileURLToPath(import.meta.resolve('@heroui-pro/react/css')));
const expectedCssPath = realpathSync(path.join(heroUiProRoot, 'dist/css/index.css'));
if (cssPath !== expectedCssPath) {
throw new Error(`Unexpected HeroUI Pro CSS path: ${cssPath}`);
}
console.log(`@heroui-pro/react/css: ${cssPath}`);
for (const specifier of [
'@heroui-pro/react/sidebar',
'@heroui-pro/react/navbar',
'@heroui-pro/react/data-grid',
'@heroui-pro/react/drop-zone',
'@heroui-pro/react/action-bar',
'@heroui-pro/react/empty-state',
]) {
await import(specifier);
console.log(`${specifier}: import ok`);
}
import '@/global.less'; import '@/global.less';
import '@/tailwind.css'; import '@/tailwind.css';
import { applyRootTheme } from '@/constants/app';
import { defineApp } from 'umi'; import { defineApp } from 'umi';
applyRootTheme();
export async function getInitialState() { export async function getInitialState() {
return { return {
name: 'project_manager_xy', name: 'project_manager_xy',
......
# 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')} />
}
import clsx from 'clsx'
import { Button } from './button'
export function Pagination({ 'aria-label': ariaLabel = 'Page navigation', className, ...props }) {
return <nav aria-label={ariaLabel} {...props} className={clsx(className, 'flex gap-x-2')} />
}
export function PaginationPrevious({ href = null, className, children = 'Previous' }) {
return (
<span className={clsx(className, 'grow basis-0')}>
<Button {...(href === null ? { disabled: true } : { href })} plain aria-label="Previous page">
<svg className="stroke-current" data-slot="icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M2.75 8H13.25M2.75 8L5.25 5.5M2.75 8L5.25 10.5"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{children}
</Button>
</span>
)
}
export function PaginationNext({ href = null, className, children = 'Next' }) {
return (
<span className={clsx(className, 'flex grow basis-0 justify-end')}>
<Button {...(href === null ? { disabled: true } : { href })} plain aria-label="Next page">
{children}
<svg className="stroke-current" data-slot="icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M13.25 8L2.75 8M13.25 8L10.75 10.5M13.25 8L10.75 5.5"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Button>
</span>
)
}
export function PaginationList({ className, ...props }) {
return <span {...props} className={clsx(className, 'hidden items-baseline gap-x-2 sm:flex')} />
}
export function PaginationPage({ href, className, current = false, children }) {
return (
<Button
href={href}
plain
aria-label={`Page ${children}`}
aria-current={current ? 'page' : undefined}
className={clsx(
className,
'min-w-9 before:absolute before:-inset-px before:rounded-lg',
current && 'before:bg-zinc-950/5 dark:before:bg-white/10'
)}
>
<span className="-mx-0.5">{children}</span>
</Button>
)
}
export function PaginationGap({ className, children = <>&hellip;</>, ...props }) {
return (
<span
aria-hidden="true"
{...props}
className={clsx(className, 'w-9 text-center text-sm/6 font-semibold text-zinc-950 select-none dark:text-white')}
>
{children}
</span>
)
}
This diff is collapsed.
@import 'tailwindcss';
@theme {
--font-sans: Inter, sans-serif;
--font-sans--font-feature-settings: 'cv11';
}
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-img-element": "off"
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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