* feat: 更新依赖,解决启动报错

* feat: 依赖升级后的样式修改

* refactor: 首页提取样式至单独的文件

* refactor: 重构首页代码,提取处理函数

* feat: 使用web worker来进行文件导入计算

* refactor: 提取组件,优化代码

* refactor: 人员列表代码结构重组

* feat:  修复问题

* feat: 添加Loading效果

* feat: indexdb存储人员名单,使用dexie

* feat(config): 重构界面配置项布局与样式

将原有的线性表单布局调整为分组的 fieldset 布局,提升视觉层次和可维护性。
新增文本设置、布局设置、主题设置、图案设置和其他设置等分类区域。
优化部分输入控件的结构和样式类名,增强用户体验和代码可读性。

* feat(data): 调整默认图案列表顺序

2025改为2026

feat(ui): 添加配置项注释说明

为 FaceConfig.vue 中的各个设置字段添加了 HTML 注释,便于识别和维护不同功能区域。包括:
- 文本设置(主标题、语言、文字大小)
- 布局设置(列数、卡片宽度、卡片高度)
- 主题设置(主题、背景图片)
- 图案设置
- 其他设置(是否常显奖项列表、是否显示头像)

* feat(PageHeader): 新增 PageHeader 组件并应用于多个配置页面

新增了 PageHeader 通用组件,用于统一页面标题和操作按钮区域的布局。
该组件包含 title 属性以及 buttons 和 alerts 两个具名插槽,便于复用和维护。
已在以下页面中集成使用:
- 图片管理页(ImageConfig.vue)
- 音乐管理页(MusicConfig.vue)
- 人员管理页(PersonAll/index.vue)
- 中奖者管理页(PersonAlready.vue)
- 奖品管理页(PrizeConfig.vue)

* fix(style): 调整 markdown 样式以支持主题变量

移除 media 查询,统一使用 CSS 变量定义颜色;修复组件中 i18n 导入位置问题。

* ```
fix(Config): 注释掉设置卡片颜色的逻辑

将设置卡片颜色的相关代码注释掉,完全使用用户使用的颜色,避免在主题切换时出现异常。
```

* ```
refactor(FaceConfig): 移除未使用的颜色验证工具函数

从 FaceConfig 组件中移除了不再需要的 isHex 和 isRgbOrRgba 工具函数导入,
因为这些函数当前并未在组件中使用。这有助于减少不必要的代码依赖并提高
组件的简洁性。
```

* fix(MusicConfig): 优化本地音乐列表项的显示样式

移除列表项中的 divide-y 类,为音乐名称添加 title 属性以显示完整名称,
并将音乐名称渲染为可点击的链接样式。

* build(workflow): 更新Node.js版本到22.x

将GitHub Actions工作流中的Node.js版本从20.x升级到22.x,
以使用最新的稳定版Node.js环境进行构建和测试。

* feat(router、config): 更新配置页面组件路径

将 FaceConfig、ImageConfig 和 MusicConfig 组件的导入路径从直接引用 `.vue` 文件改为引用 `index.vue`。

* build(eslint): 更新 ESLint 配置以忽略更多文件并添加警告规则

在原有忽略文件列表中新增了 '*.config.js' 文件类型,同时添加了
'no-console' 和 'no-debugger' 规则,并将其设置为警告级别,以提高
代码质量和一致性。

* feat(components): 添加图片上传组件及对话框功能

新增 `ImageUpload` 组件用于文件选择与预览,并集成到 `UploadDialog` 中实现图片上传逻辑。
更新了 `Dialog` 组件以支持可选属性和 model 绑定,增强其灵活性和可用性。
引入 `lucide-vue-next` 图标库支持图标渲染。
重构图片配置页面,移除旧上传逻辑,使用新的弹窗方式进行图片上传操作。

* feat(imgConfig): 优化图片存储结构

新增uuid依赖用于生成唯一ID,改进图片上传功能中的本地存储结构,
使用uuid替代时间戳作为键名以避免冲突,并调整从indexedDB读取数据的方式。

* feat: 音乐管理功能优化

* fix(MusicConfig): 移除上传对话框中的图片上传提示功能

移除了 UploadDialog 组件中不再使用的图片上传状态提示逻辑,
包括相关的响应式变量 imgUploadToast 及其对应的模板代码。
此变更简化了组件结构并移除了未使用的错误提示功能。

* fix(Config): 添加图片上传提示信息

在全局人脸配置页面添加了图片上传提示文本,提醒用户需要先上传图片到图片管理模块。

* feat(layout): 重构页面布局与音乐播放组件

将 PlayMusic 组件迁移至 layout/RightButton 并重构成通用右下角按钮组件,
提取音乐播放逻辑到独立 hook `usePlayMusic`,优化模态框提示逻辑并统一滚动行为。

* feat(prize-config): 引入拖拽功能并优化奖品配置界面

- 添加 `vue-draggable-plus` 和 `lodash-es` 依赖以支持列表拖拽排序
- 使用 `cloneDeep` 深拷贝奖品列表,避免直接修改原始数据
- 移除原有的手动排序逻辑(上下移动按钮),改用可视化拖拽方式
- 调整 UI 布局和样式,增强用户体验与可操作性
- 在 Demo 页面中添加 draggable 示例用于验证功能实现

* feat(Config): 动态显示当前年份并更新版权信息

将页脚中的版权年份从硬编码的 2024 改为动态获取的当前年份,并将作者名称添加跳转到 GitHub 主页的链接。

* fix(ui): 优化图片列表下拉选择器及表单项的样式与布局

* feat(person): 导入人员时添加uuid唯一标识字段 #91

* feat(utils): 添加安全洗牌与随机抽样函数 Feature #91

* test(random): 添加随机元素抽取函数的全面测试用例 Test #91

- 新增 Random.test.ts 文件,对 getRandomElements 函数进行详尽测试
- 包括基础功能、边界情况(count 为 0、负数、超出数组长度)
- 支持空数组、单元素数组、字符串数组、对象数组等多种数据类型
- 增加概率性测试,验证多次调用结果不完全一致
- 20万次循环验证各元素被抽中概率接近理论值,确保算法公平性

* feat(home): 重构抽奖逻辑并优化初始化流程 Feature #91

* feat(Home): 添加初始化完成状态控制头部标题显示 Fix #91

* feat(HeaderTitle): 添加加载状态显示 #91

* feat(store): 调整默认人员列表类型定义

* feat(deps): 添加tauri打包(遗留pnpm build:file打包好的文件跨域无法访问) #94

* feat(icons): 添加tauri多种平台的应用图标资源

* fix(tauri): 更新 Cargo.toml 配置格式 #94

* build(workflow): 更新 GitHub Actions 工作流配置 #94

* build(workflow): 更新发布流程并移除旧的构建文件

* chore(release): 调整 GitHub Actions 工作流中的缩进格式

* build(deps): 固定 three.js 相关依赖版本

* build(workflows): 添加 fast-glob 依赖到 GitHub 工作流

* build(release): 添加发布名称和文件名以包含版本信息

* build(github): 升级下载构件操作到v4版本

* ci(release): 上传web构建产物作为GitHub Actions工件

* feat(Config): 支持多语言 README 文档展示

* feat(Global/FaceConfig): 添加跳转到图片管理页面的链接 #96

* chore: 格式化

* feat(i18n): 更新抽奖配置相关国际化文案 #96

* build: git钩子

* build: pre-psuh

* feat(components): 添加 shadcn-vue 组件配置文件及配套组件库 #96

* feat(font): 添加本地字体选择功能(not done) #96

* feat: 更换字体v,以及页面加载之前就设置字体 #96

* chore: 删除v console.log #96

* build: pre-push

* feat(globalConfig): 设置默认字体为微软雅黑 #96

* feat: 设置标题字体与全局字体同步 #96

* build(.gitignore): 添加 husky 目录到忽略文件

* docs(readme): 格式

* docs(readme): 移除关于build:file命令的过时文档

* build: 🏗️ 使用低版本node会报错,强制限制使用node 22.x版本

可在.npmrc里面修改为不强制使用

* feat:  上传文件时校验是否模板文件,否则提示 #96

* feat(layout): 添加全屏切换功能 #96

* refactor: ♻️ 界面设置页面重构,抽离逻辑 #96

* fix(background): 修复从本地存储获取图片数据的问题 #96

* refactor: ♻️ 重构奖项配置模块 #96

* feat(personalready):  已中奖的员页面修改switch组件 #96

* refactor(personalready): ♻️ 已中奖的界面代码重构 #96

* style: 💄 删除shadcn-vue的样式预设,只使用daisyui的样式 #96

* feat:  标题样式跟随主题,设置文本颜色会覆盖主题的标题颜色 #96

* fix(Global/FaceConfig): 调整主题设置中文本颜色按钮的高度样式 #96

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96 (#119)

卸载路由时清除requestAnimationFrame

* 96 UI optimization (#122)

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96

卸载路由时清除requestAnimationFrame

* feat:  文件存储使用Blob格式

* style: 💄 修改部分类型any为具体类型

* feat:  界面设置中模块使用瀑布流布局 #96

* fix: 🐛 md文档更换文件夹解决控制台警告

* style: 💄 switch按钮改回使用daisyui组件

* refactor: ♻️ 所有人员列表提取tableColumn

* style: 💄 奖项列表中的图片类型修复

* Confilct dev date 12 22 (#131)

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96

卸载路由时清除requestAnimationFrame

* feat:  文件存储使用Blob格式

* style: 💄 修改部分类型any为具体类型

* feat:  界面设置中模块使用瀑布流布局 #96

* fix: 🐛 md文档更换文件夹解决控制台警告

* style: 💄 switch按钮改回使用daisyui组件

* refactor: ♻️ 所有人员列表提取tableColumn

* style: 💄 奖项列表中的图片类型修复

* fix(globalConfig): 修复当前音乐项类型缺失问题

* feat:  single person not done

* feat:  可添加单人 #96

* build(.gitignore): 添加 auto-imports.d.ts 到忽略文件

* fix: 🐛 上传、下载excel文件时修复路径错误

打包成应用和网页端的baseUrl不一样,使用环境变量来表示

* fix: 🐛 导入人员列表时处理有值为空的情况

* style: 💄 改变toaster的组件

* fix: 🐛 上传文件、解析数据与存储/读取数据的处理

、

* 96 UI optimization (#132)

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96

卸载路由时清除requestAnimationFrame

* feat:  文件存储使用Blob格式

* style: 💄 修改部分类型any为具体类型

* feat:  界面设置中模块使用瀑布流布局 #96

* fix: 🐛 md文档更换文件夹解决控制台警告

* style: 💄 switch按钮改回使用daisyui组件

* refactor: ♻️ 所有人员列表提取tableColumn

* style: 💄 奖项列表中的图片类型修复

* fix(globalConfig): 修复当前音乐项类型缺失问题

* feat:  single person not done

* feat:  可添加单人 #96

* build(.gitignore): 添加 auto-imports.d.ts 到忽略文件

* fix: 🐛 上传、下载excel文件时修复路径错误

打包成应用和网页端的baseUrl不一样,使用环境变量来表示

* fix: 🐛 导入人员列表时处理有值为空的情况

* style: 💄 改变toaster的组件

* fix: 🐛 上传文件、解析数据与存储/读取数据的处理

、

* fix(Config): 更新备案信息链接样式

将备案信息的段落标签替换为可点击的链接标签,使用户能够直接跳转到工信部备案查询页面。同时添加了悬停效果样式,提升用户体验。

* 96 UI optimization (#136)

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96

卸载路由时清除requestAnimationFrame

* feat:  文件存储使用Blob格式

* style: 💄 修改部分类型any为具体类型

* feat:  界面设置中模块使用瀑布流布局 #96

* fix: 🐛 md文档更换文件夹解决控制台警告

* style: 💄 switch按钮改回使用daisyui组件

* refactor: ♻️ 所有人员列表提取tableColumn

* style: 💄 奖项列表中的图片类型修复

* fix(globalConfig): 修复当前音乐项类型缺失问题

* feat:  single person not done

* feat:  可添加单人 #96

* build(.gitignore): 添加 auto-imports.d.ts 到忽略文件

* fix: 🐛 上传、下载excel文件时修复路径错误

打包成应用和网页端的baseUrl不一样,使用环境变量来表示

* fix: 🐛 导入人员列表时处理有值为空的情况

* style: 💄 改变toaster的组件

* fix: 🐛 上传文件、解析数据与存储/读取数据的处理

、

* fix(Config): 更新备案信息链接样式

将备案信息的段落标签替换为可点击的链接标签,使用户能够直接跳转到工信部备案查询页面。同时添加了悬停效果样式,提升用户体验。

* feat:  首页奖项列表样式修改 not done #96

* chore(deps): ✏️ 更新依赖版本

* chore: ✏️ gsap list demo

* build: 🏗️ docker构建优化

* chore: ✏️ gsap scroll demo

* style: 💄 gsap demno

* feat:  demo smooth scroll gsap scrolltrigger

* feat(Demo): 添加更多颜色选项并注释GSAP动画

* refactor(PrizeList): 重构奖品列表组件结构

* feat(PrizeList): 重构奖品列表组件并添加滚动动画

* feat:  增加定时抽取功能 #96

* feat:  添加定时抽取功能的说明

* feat:  优化gsap #96

项数不多时不触发gsap

* style: 💄 文本修改

* feat:  优化

* feat:  优化奖项列表

* fix(Home): 修复奖品列表滚动检测逻辑

* fix(home): 修复抽奖停止逻辑避免重复执行;调整卡片垂直居中位置计算

* feat:  播放中奖音频 #96

* 96 UI optimization (#137)

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96

卸载路由时清除requestAnimationFrame

* feat:  文件存储使用Blob格式

* style: 💄 修改部分类型any为具体类型

* feat:  界面设置中模块使用瀑布流布局 #96

* fix: 🐛 md文档更换文件夹解决控制台警告

* style: 💄 switch按钮改回使用daisyui组件

* refactor: ♻️ 所有人员列表提取tableColumn

* style: 💄 奖项列表中的图片类型修复

* fix(globalConfig): 修复当前音乐项类型缺失问题

* feat:  single person not done

* feat:  可添加单人 #96

* build(.gitignore): 添加 auto-imports.d.ts 到忽略文件

* fix: 🐛 上传、下载excel文件时修复路径错误

打包成应用和网页端的baseUrl不一样,使用环境变量来表示

* fix: 🐛 导入人员列表时处理有值为空的情况

* style: 💄 改变toaster的组件

* fix: 🐛 上传文件、解析数据与存储/读取数据的处理

、

* fix(Config): 更新备案信息链接样式

将备案信息的段落标签替换为可点击的链接标签,使用户能够直接跳转到工信部备案查询页面。同时添加了悬停效果样式,提升用户体验。

* feat:  首页奖项列表样式修改 not done #96

* chore(deps): ✏️ 更新依赖版本

* chore: ✏️ gsap list demo

* build: 🏗️ docker构建优化

* chore: ✏️ gsap scroll demo

* style: 💄 gsap demno

* feat:  demo smooth scroll gsap scrolltrigger

* feat(Demo): 添加更多颜色选项并注释GSAP动画

* refactor(PrizeList): 重构奖品列表组件结构

* feat(PrizeList): 重构奖品列表组件并添加滚动动画

* feat:  增加定时抽取功能 #96

* feat:  添加定时抽取功能的说明

* feat:  优化gsap #96

项数不多时不触发gsap

* style: 💄 文本修改

* feat:  优化

* feat:  优化奖项列表

* fix(Home): 修复奖品列表滚动检测逻辑

* fix(home): 修复抽奖停止逻辑避免重复执行;调整卡片垂直居中位置计算

* feat:  播放中奖音频 #96

* style: 💄 下载模板成功后进行提示

* 96 UI optimization (#141)

* fix(home): 🐛 解决多次切换路由后页面卡顿的问题 #96

卸载路由时清除requestAnimationFrame

* feat:  文件存储使用Blob格式

* style: 💄 修改部分类型any为具体类型

* feat:  界面设置中模块使用瀑布流布局 #96

* fix: 🐛 md文档更换文件夹解决控制台警告

* style: 💄 switch按钮改回使用daisyui组件

* refactor: ♻️ 所有人员列表提取tableColumn

* style: 💄 奖项列表中的图片类型修复

* fix(globalConfig): 修复当前音乐项类型缺失问题

* feat:  single person not done

* feat:  可添加单人 #96

* build(.gitignore): 添加 auto-imports.d.ts 到忽略文件

* fix: 🐛 上传、下载excel文件时修复路径错误

打包成应用和网页端的baseUrl不一样,使用环境变量来表示

* fix: 🐛 导入人员列表时处理有值为空的情况

* style: 💄 改变toaster的组件

* fix: 🐛 上传文件、解析数据与存储/读取数据的处理

、

* fix(Config): 更新备案信息链接样式

将备案信息的段落标签替换为可点击的链接标签,使用户能够直接跳转到工信部备案查询页面。同时添加了悬停效果样式,提升用户体验。

* feat:  首页奖项列表样式修改 not done #96

* chore(deps): ✏️ 更新依赖版本

* chore: ✏️ gsap list demo

* build: 🏗️ docker构建优化

* chore: ✏️ gsap scroll demo

* style: 💄 gsap demno

* feat:  demo smooth scroll gsap scrolltrigger

* feat(Demo): 添加更多颜色选项并注释GSAP动画

* refactor(PrizeList): 重构奖品列表组件结构

* feat(PrizeList): 重构奖品列表组件并添加滚动动画

* feat:  增加定时抽取功能 #96

* feat:  添加定时抽取功能的说明

* feat:  优化gsap #96

项数不多时不触发gsap

* style: 💄 文本修改

* feat:  优化

* feat:  优化奖项列表

* fix(Home): 修复奖品列表滚动检测逻辑

* fix(home): 修复抽奖停止逻辑避免重复执行;调整卡片垂直居中位置计算

* feat:  播放中奖音频 #96

* style: 💄 下载模板成功后进行提示

* docs: 📝 readme更新

* ci: 👷 git action触发改为推送release版本时执行

* Feature action (#143)

* ci: 👷 整合github action配置文件

* docs: 📝 贡献文档修改

* Feature action (#144)

* ci: 👷 整合github action配置文件

* docs: 📝 贡献文档修改

* style: 💄 更新版本

* style: 💄 cargo.lock版本更新

* Feature action (#149)

* ci: 👷 整合github action配置文件

* docs: 📝 贡献文档修改

* style: 💄 更新版本

* style: 💄 cargo.lock版本更新

* feat(husky): 增强Git标签版本校验脚本

添加了对Git标签指向提交与release分支一致性的校验功能。
脚本现在会检查tag指向的提交是否与当前或任何release分支的最新提交一致,
确保发布流程的准确性。如果当前在release分支上,直接比较分支HEAD与tag指向的提交;
如果不在release分支上,则遍历所有release分支查找匹配的提交。

* feat:  国际化

* Feature action (#150)

* ci: 👷 整合github action配置文件

* docs: 📝 贡献文档修改

* style: 💄 更新版本

* style: 💄 cargo.lock版本更新

* feat(husky): 增强Git标签版本校验脚本

添加了对Git标签指向提交与release分支一致性的校验功能。
脚本现在会检查tag指向的提交是否与当前或任何release分支的最新提交一致,
确保发布流程的准确性。如果当前在release分支上,直接比较分支HEAD与tag指向的提交;
如果不在release分支上,则遍历所有release分支查找匹配的提交。

* feat:  国际化

* fix: 🐛 修复国际化问题;修复字体大小未生效问题

* fix: 🐛 修复部分问题

* docs: 📝 update readme
This commit is contained in:
LOG1997
2025-12-30 17:44:21 +08:00
committed by GitHub
parent f952607ca5
commit 46e3e5bc31
265 changed files with 19456 additions and 9475 deletions

View File

@@ -47,9 +47,9 @@ const actionsColumns = computed<any[]>(() => {
<tbody v-if="data.length > 0">
<!-- row -->
<tr v-for="item in data" :key="item.id" class="hover">
<th>{{ item.id }}</th>
<!-- <th>{{ item.id }}</th> -->
<td v-for="(column, index) in dataColumns" :key="index">
<span v-if="column.formatValue" v-html="column.formatValue(item)"></span>
<span v-if="column.formatValue" v-html="column.formatValue(item)" />
<span v-else>{{ item[column.props] }}</span>
</td>
<!-- action -->

View File

@@ -0,0 +1,73 @@
<script setup lang='ts'>
import { onMounted, ref, toRefs } from 'vue'
import i18n from '@/locales/i18n'
interface Props {
title: string
desc?: string
cancelText?: string
submitText?: string
submitFunc?: () => void
cancelFunc?: () => void
}
const props = withDefaults(defineProps<Props>(), {
cancelText: i18n.global.t('button.cancel'),
submitText: i18n.global.t('button.confirm'),
cancelFunc: () => {},
})
const visible = defineModel('visible', {
type: Boolean,
default: false,
})
const dialogRef = ref <HTMLDialogElement | null> (null)
function defaultCancelFunc() {
dialogRef.value?.close()
}
function showDialog() {
dialogRef.value?.showModal()
}
defineExpose({
showDialog,
closed,
})
onMounted(() => {
dialogRef.value?.addEventListener('close', () => {
visible.value = false
})
})
const { title, desc, cancelText, submitText, submitFunc, cancelFunc = defaultCancelFunc } = toRefs(props)
</script>
<template>
<dialog id="my_modal" ref="dialogRef" class="border-none modal">
<div class="modal-box">
<h3 v-if="title" class="text-lg font-bold">
{{ title }}
</h3>
<p v-if="desc" class="py-4">
{{ desc }}
</p>
<div>
<slot name="content" />
</div>
<div class="modal-action">
<form method="dialog" class="flex gap-3">
<!-- if there is a button in form, it will close the modal -->
<button class="btn" @click="cancelFunc">
{{ cancelText }}
</button>
<button class="btn" @click="submitFunc">
{{ submitText }}
</button>
</form>
</div>
</div>
</dialog>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,38 @@
<script setup lang='ts'>
import { ref } from 'vue'
const drawerTriggerRef = ref <HTMLDialogElement | null> (null)
const visible = ref(false)
function showDrawer() {
drawerTriggerRef.value?.click()
visible.value = true
}
function closeDrawer() {
drawerTriggerRef.value?.click()
visible.value = false
}
defineExpose({
showDrawer,
closeDrawer,
})
</script>
<template>
<div class="drawer drawer-end h-0 w-0">
<input id="my-drawer-1" type="checkbox" class="drawer-toggle">
<div class="drawer-content w-0 h-0 absolute">
<!-- Page content here -->
<label ref="drawerTriggerRef" for="my-drawer-1" />
</div>
<div class="drawer-side">
<label for="my-drawer-1" aria-label="close sidebar" class="drawer-overlay" />
<div v-if="visible" class="menu bg-base-200 min-h-full w-80 p-4">
<slot name="content" />
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,69 @@
<script setup lang='ts'>
import type { IFileData } from './type'
import { ListMusic, Upload, X } from 'lucide-vue-next'
import { ref } from 'vue'
import { getBlobObjectUrl, readFileAsJsonData, readFileData, readFileDataAsBlob } from '@/utils/file'
const props = defineProps<{
limitType?: string
mode?: 'file' | 'json'
}>()
const emits = defineEmits<{
uploadFile: [fileData: IFileData | null]
}>()
const originFileName = ref<string | null>(null)
const fileData = ref<IFileData | null>(null)
async function handleFileChange(e: Event) {
const file = ((e.target as HTMLInputElement).files as FileList)[0]
const type = file.type
if (props.mode === 'json') {
const fileRes = await readFileAsJsonData(file)
const jsonData = JSON.parse(fileRes)
fileData.value = { data: jsonData, fileName: file.name, type }
originFileName.value = file.name
emits('uploadFile', fileData.value)
return
}
const { data: blobData, fileName } = await readFileDataAsBlob(file)
fileData.value = { data: blobData, fileName, type }
originFileName.value = fileName
emits('uploadFile', fileData.value)
}
function removeFile() {
fileData.value = null
emits('uploadFile', null)
}
</script>
<template>
<div class="w-full h-full flex flex-col items-center mt-6">
<input
id="file-upload"
:disabled="fileData !== null"
type="file" class="w-full bg-red-400/50 max-h-52 cursor-pointer absolute" style="display: none;" :accept="limitType"
@change="handleFileChange"
>
<label for="file-upload" :class="fileData ? 'cursor-not-allowed' : null" class="w-full h-52 cursor-pointer border-2 border-dashed flex items-center justify-center overflow-hidden">
<img v-if="fileData && fileData.type.includes('image')" class="w-full object-cover stroke-0" :src="getBlobObjectUrl(fileData.data as Blob)" alt="">
<ListMusic v-else-if="fileData && fileData.type.includes('audio')" class="w-2/3 h-2/3 stroke-1 text-gray-500/50" />
<div v-else class="w-full h-full flex justify-center items-center flex-col gap-4">
<Upload class="w-2/3 h-2/3 stroke-1 text-gray-500/50" />
<span class="btn btn-neutral">点击上传</span>
</div>
</label>
<div v-if="fileData" class="w-full flex items-center justify-between mt-2">
<p class="max-w-[3/4] truncate text-sm">
{{ originFileName }}
</p>
<button class="btn btn-xs btn-square btn-ghost" @click="removeFile">
<X />
</button>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,5 @@
export interface IFileData {
data: string | Blob | ArrayBuffer
fileName: string
type: string
}

View File

@@ -1,35 +1,35 @@
<script setup lang='ts'>
import type { IFileData } from '../FileUpload/type'
import type { IImage } from '@/types/storeType'
import localforage from 'localforage'
import { onMounted, ref } from 'vue'
const props = defineProps({
imgItem: {
type: Object,
default: () => ({}),
},
})
interface IProps {
imgItem: IImage
}
const props = defineProps<IProps>()
const imageDbStore = localforage.createInstance({
name: 'imgStore',
})
const imgUrl = ref('')
async function getImageStoreItem(item: any): Promise<string> {
async function getImageStoreItem(item: IImage): Promise<string> {
let image = ''
if (item.url === 'Storage') {
const key = item.id
image = await imageDbStore.getItem(key) as string
const imageData = await imageDbStore.getItem<IFileData>(key)
image = URL.createObjectURL(imageData?.data as Blob)
}
else {
image = item.url
image = item.url as string
}
return image
}
onMounted(async () => {
const image = await getImageStoreItem(props.imgItem)
imgUrl.value = image
imgUrl.value = await getImageStoreItem(props.imgItem)
})
</script>

View File

@@ -0,0 +1,5 @@
import type { LoadingOptions } from './loading-context'
import Loading from './index.vue'
import { loadingKey, loadingState } from './loading-context'
export { Loading, loadingKey, LoadingOptions, loadingState }

View File

@@ -0,0 +1,24 @@
<script setup lang='ts'>
import type { LoadingOptions } from './loading-context'
import { inject } from 'vue'
import { useI18n } from 'vue-i18n'
import { loadingKey } from './loading-context'
// 注入全局状态
const loading = inject(loadingKey) as LoadingOptions
const { t } = useI18n()
// 解构状态(响应式)
const { visible, text } = loading
</script>
<template>
<div v-if="visible" class="fixed top-0 left-0 w-screen h-screen bg-[rgba(0,0,0,0.5)] flex flex-col gap-6 justify-center items-center z-50">
<span v-if="visible" class="loading loading-spinner loading-xl" />
<span>{{ text ? text : t('button.loading') }}</span>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,61 @@
// src/contexts/loading-context.ts
import type { InjectionKey, Ref } from 'vue'
import { ref } from 'vue'
// 定义 Loading 配置类型
export interface LoadingOptions {
visible: Ref<boolean>
text: Ref<string>
fullscreen: Ref<boolean>
zIndex: Ref<number>
count: Ref<number>
show: (options?: Partial<{ text: string, fullscreen: boolean, zIndex: number }>) => void
hide: () => void
}
// 注入密钥Symbol 确保唯一性)
export const loadingKey: InjectionKey<LoadingOptions> = Symbol('loading')
// 全局状态(单例)
const visible = ref(false)
const text = ref('')
const fullscreen = ref(true)
const zIndex = ref(9999)
const count = ref(0)
// 显示 Loading
function show(options?: Partial<{ text: string, fullscreen: boolean, zIndex: number }>) {
count.value++
if (count.value > 1)
return
visible.value = true
if (options) {
text.value = options.text || ''
fullscreen.value = options.fullscreen ?? true
zIndex.value = options.zIndex || 9999
}
}
// 隐藏 Loading
function hide() {
if (count.value <= 0)
return
count.value--
if (count.value === 0) {
visible.value = false
text.value = ''
fullscreen.value = true
zIndex.value = 9999
}
}
// 导出全局状态(供根组件提供)
export const loadingState: LoadingOptions = {
visible,
text,
fullscreen,
zIndex,
count,
show,
hide,
}

View File

@@ -0,0 +1,24 @@
<script setup lang='ts'>
const props = defineProps<{
title: string
}>()
</script>
<template>
<header>
<h1 class="text-lg leading-10">
{{ props.title }}
</h1>
<div class="button-group my-4">
<slot name="buttons" />
</div>
<div class="divider mt-0" />
<div>
<slot name="alerts" />
</div>
</header>
</template>
<style scoped>
</style>

View File

@@ -1,135 +0,0 @@
<script setup lang='ts'>
import useStore from '@/store'
import localforage from 'localforage'
import { storeToRefs } from 'pinia'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const audioDbStore = localforage.createInstance({
name: 'audioStore',
})
const audio = ref(new Audio())
const settingRef = ref()
// const audio = ref(new Audio())
const globalConfig = useStore().globalConfig
const { getMusicList: localMusicList, getCurrentMusic: currentMusic } = storeToRefs(globalConfig)
// const localMusicListValue = ref(localMusicList)
async function play(item: any) {
if (!item) {
return
}
// if (!audio.value.paused && !skip) {
// audio.value.pause()
// return
// }
let audioUrl = ''
if (!item.url) {
return
}
if (item.url === 'Storage') {
audioUrl = await audioDbStore.getItem(item.name) as string
}
else {
audioUrl = item.url
}
audio.value.pause()
audio.value.src = audioUrl
audio.value.play()
}
function playMusic(item: any, skip = false) {
if (!item) {
return
}
if (!currentMusic.value.paused && !skip) {
globalConfig.setCurrentMusic(item, true)
return
}
globalConfig.setCurrentMusic(item, false)
}
function nextPlay() {
// 播放下一首
if (localMusicList.value.length >= 1) {
let index = localMusicList.value.findIndex((item: any) => item.name === currentMusic.value.item.name)
index++
if (index >= localMusicList.value.length) {
index = 0
}
globalConfig.setCurrentMusic(localMusicList.value[index], false)
}
}
// 监听播放成后开始下一首
function onPlayEnd() {
audio.value.addEventListener('ended', nextPlay)
}
function enterConfig() {
router.push('/log-lottery/config')
}
function enterHome() {
router.push('/log-lottery')
}
onMounted(() => {
globalConfig.setCurrentMusic(localMusicList.value[0], true)
onPlayEnd()
// 不使用空格控制audio
})
onUnmounted(() => {
audio.value.removeEventListener('ended', nextPlay)
})
watch(currentMusic, (val: any) => {
if (!val.paused && audio.value) {
play(val.item)
}
else {
audio.value.pause()
}
}, { deep: true })
</script>
<template>
<div ref="settingRef" class="flex flex-col gap-3">
<div v-if="route.path.includes('/config')" class="tooltip tooltip-left" :data-tip="t('tooltip.toHome')">
<div
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
@click="enterHome"
>
<svg-icon name="home" />
</div>
</div>
<div v-else class="tooltip tooltip-left" :data-tip="t('tooltip.settingConfiguration')">
<div
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
@click="enterConfig"
>
<svg-icon name="setting" />
</div>
</div>
<div class="tooltip tooltip-left" :data-tip="currentMusic.item ? `${currentMusic.item.name}\n\r ${t('tooltip.nextSong')}` : t('tooltip.noSongPlay')">
<div
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
@click="playMusic(currentMusic.item)" @click.right.prevent="nextPlay"
>
<svg-icon :name="currentMusic.paused ? 'play' : 'pause'" />
</div>
</div>
</div>
</template>
<style lang='scss' scoped>
details {
// display: none;
summary {
display: none;
}
}
</style>

View File

@@ -1,71 +0,0 @@
<script setup lang='ts'>
import Sparticles from 'sparticles';
import { ref, onMounted, onUnmounted } from 'vue';
import { useElementSize } from '@vueuse/core';
import localforage from 'localforage'
const props = defineProps({
homeBackground: {
type: Object,
default: () => ({
id: '',
name: '',
url: ''
})
}
})
const imageDbStore = localforage.createInstance({
name: 'imgStore'
})
const imgUrl = ref('')
const starRef = ref();
const { width, height } = useElementSize(starRef);
let options = ref({ shape: 'star', parallax: 1.2, rotate: true, twinkle: true, speed: 10, count: 200 });
function addSparticles(node: any, width: number, height: number) {
new Sparticles(node, options.value, width, height);
}
// 页面大小改变时
const listenWindowSize = () => {
window.addEventListener('resize', () => {
if (width.value && height.value) {
addSparticles(starRef.value, width.value, height.value);
}
});
}
const getImageStoreItem = async (item: any): Promise<string> => {
let image = ''
if (item.url == 'Storage') {
const key = item.id;
image = await imageDbStore.getItem(key) as string
}
else {
image = item.url
}
return image
}
onMounted(() => {
getImageStoreItem(props.homeBackground).then((image) => {
imgUrl.value = image
})
addSparticles(starRef.value, width.value, height.value);
listenWindowSize()
})
onUnmounted(() => {
window.removeEventListener('resize', listenWindowSize)
})
</script>
<template>
<div class="home-background w-screen h-screen overflow-hidden" v-if="homeBackground.url">
<img :src="imgUrl" class="w-full h-full object-cover" alt="">
</div>
<div v-else class="w-screen h-screen overflow-hidden" ref="starRef">
</div>
</template>
<style lang='scss' scoped>
</style>

View File

@@ -36,8 +36,8 @@ const symbolId = computed(() => `#${props.prefix}-${props.name}`)
<style scoped>
.svg-icon {
width: 24px;
height: 24px;
/* width: 24px;
height: 24px; */
fill: currentColor;
}
</style>

View File

@@ -1,10 +1,10 @@
<script setup lang='ts'>
import { ChevronUp } from 'lucide-vue-next'
</script>
<template>
<div class="fixed z-50 flex items-center justify-center w-10 h-10 rounded-full shadow-lg cursor-pointer right-12 bottom-12 bg-slate-700 hover:bg-slate-600">
<svg-icon name="toTop" />
<ChevronUp />
</div>
</template>

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import { throttle } from 'lodash-es' // lodash-es 节流
import Masonry from 'masonry-layout'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
// 布局参数 Props
interface MasonryWaterfallProps {
columnWidth?: number | string // 列宽px/选择器)
gutter?: number // 列/行间距
fitWidth?: boolean // 容器宽度自适应居中
}
// 默认参数
const props = withDefaults(defineProps<MasonryWaterfallProps>(), {
columnWidth: 120,
gutter: 16,
fitWidth: true,
})
// Vue Ref 管理 DOM 容器和 masonry 实例
const masonryContainer: Ref<HTMLDivElement | null> = ref(null)
const masonryInstance: Ref<Masonry | null> = ref(null)
// 初始化 masonry仅执行一次因卡片固定
async function initMasonry() {
if (!masonryContainer.value)
return
// 等待插槽内容(固定卡片)完全渲染
await nextTick()
// 初始化 masonry 实例(固定卡片无需销毁旧实例)
masonryInstance.value = new Masonry(masonryContainer.value, {
itemSelector: '.masonry-container > *', // 匹配所有固定子项
columnWidth: props.columnWidth,
gutter: props.gutter,
fitWidth: props.fitWidth,
initLayout: true, // 固定卡片直接初始化布局
})
}
// 刷新布局(仅用于卡片内部内容高度变化)
async function refreshLayout() {
await nextTick()
if (masonryInstance.value) {
masonryInstance.value.layout?.()
}
}
// 窗口缩放节流重排(优化性能)
const handleResize = throttle(() => {
if (masonryInstance.value) {
masonryInstance.value.layout?.()
}
}, 300)
// 生命周期:挂载时初始化,卸载时清理
onMounted(async () => {
await initMasonry()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
// 销毁实例 + 释放内存
if (masonryInstance.value) {
masonryInstance.value.destroy?.()
masonryInstance.value = null
}
// 移除监听 + 取消节流任务
window.removeEventListener('resize', handleResize)
handleResize.cancel()
})
// 仅暴露刷新方法(适配卡片内部内容变化)
defineExpose({ refreshLayout })
</script>
<template>
<!-- masonry 容器ref 绑定接收固定插槽内容 -->
<div ref="masonryContainer" class="masonry-container">
<!-- 插槽直接传入固定的卡片/组件 -->
<slot />
</div>
</template>
<style scoped>
.masonry-container {
width: 100%;
/* max-width: 1400px; */
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
}
/* 固定卡片基础样式 */
.masonry-container > * {
margin-bottom: v-bind('`${gutter}px`');
box-sizing: border-box;
break-inside: avoid; /* 兼容 Safari */
min-height: 100px; /* 避免内容过矮导致布局异常 */
}
/* 响应式适配:小屏调整列宽 */
/* @media (max-width: 768px) {
.masonry-container {
padding: 10px;
}
.masonry-container > * {
width: calc(50% - v-bind('`${gutter}px`')) !important;
}
} */
</style>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import type { ButtonVariants } from "."
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from "."
interface Props extends PrimitiveProps {
variant?: ButtonVariants["variant"]
size?: ButtonVariants["size"]
class?: HTMLAttributes["class"]
}
const props = withDefaults(defineProps<Props>(), {
as: "button",
})
</script>
<template>
<Primitive
data-slot="button"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,38 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Button } from "./Button.vue"
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
"default": "h-9 px-4 py-2 has-[>svg]:px-3",
"sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
"icon": "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import type { ListboxRootEmits, ListboxRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ListboxRoot, useFilter, useForwardPropsEmits } from "reka-ui"
import { reactive, ref, watch } from "vue"
import { cn } from "@/lib/utils"
import { provideCommandContext } from "."
const props = withDefaults(defineProps<ListboxRootProps & { class?: HTMLAttributes["class"] }>(), {
modelValue: "",
})
const emits = defineEmits<ListboxRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const allItems = ref<Map<string, string>>(new Map())
const allGroups = ref<Map<string, Set<string>>>(new Map())
const { contains } = useFilter({ sensitivity: "base" })
const filterState = reactive({
search: "",
filtered: {
/** The count of all visible items. */
count: 0,
/** Map from visible item id to its search score. */
items: new Map() as Map<string, number>,
/** Set of groups with at least one visible item. */
groups: new Set() as Set<string>,
},
})
function filterItems() {
if (!filterState.search) {
filterState.filtered.count = allItems.value.size
// Do nothing, each item will know to show itself because search is empty
return
}
// Reset the groups
filterState.filtered.groups = new Set()
let itemCount = 0
// Check which items should be included
for (const [id, value] of allItems.value) {
const score = contains(value, filterState.search)
filterState.filtered.items.set(id, score ? 1 : 0)
if (score)
itemCount++
}
// Check which groups have at least 1 item shown
for (const [groupId, group] of allGroups.value) {
for (const itemId of group) {
if (filterState.filtered.items.get(itemId)! > 0) {
filterState.filtered.groups.add(groupId)
break
}
}
}
filterState.filtered.count = itemCount
}
watch(() => filterState.search, () => {
filterItems()
})
provideCommandContext({
allItems,
allGroups,
filterState,
})
</script>
<template>
<ListboxRoot
data-slot="command"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', props.class)"
>
<slot />
</ListboxRoot>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { useForwardPropsEmits } from "reka-ui"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import Command from "./Command.vue"
const props = withDefaults(defineProps<DialogRootProps & {
title?: string
description?: string
}>(), {
title: "Command Palette",
description: "Search for a command to run...",
})
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<Dialog v-slot="slotProps" v-bind="forwarded">
<DialogContent class="overflow-hidden p-0 ">
<DialogHeader class="sr-only">
<DialogTitle>{{ title }}</DialogTitle>
<DialogDescription>{{ description }}</DialogDescription>
</DialogHeader>
<Command>
<slot v-bind="slotProps" />
</Command>
</DialogContent>
</Dialog>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Primitive } from "reka-ui"
import { computed } from "vue"
import { cn } from "@/lib/utils"
import { useCommand } from "."
const props = defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const { filterState } = useCommand()
const isRender = computed(() => !!filterState.search && filterState.filtered.count === 0,
)
</script>
<template>
<Primitive
v-if="isRender"
data-slot="command-empty"
v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import type { ListboxGroupProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ListboxGroup, ListboxGroupLabel, useId } from "reka-ui"
import { computed, onMounted, onUnmounted } from "vue"
import { cn } from "@/lib/utils"
import { provideCommandGroupContext, useCommand } from "."
const props = defineProps<ListboxGroupProps & {
class?: HTMLAttributes["class"]
heading?: string
}>()
const delegatedProps = reactiveOmit(props, "class")
const { allGroups, filterState } = useCommand()
const id = useId()
const isRender = computed(() => !filterState.search ? true : filterState.filtered.groups.has(id))
provideCommandGroupContext({ id })
onMounted(() => {
if (!allGroups.value.has(id))
allGroups.value.set(id, new Set())
})
onUnmounted(() => {
allGroups.value.delete(id)
})
</script>
<template>
<ListboxGroup
v-bind="delegatedProps"
:id="id"
data-slot="command-group"
:class="cn('text-foreground overflow-hidden p-1', props.class)"
:hidden="isRender ? undefined : true"
>
<ListboxGroupLabel v-if="heading" data-slot="command-group-heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
{{ heading }}
</ListboxGroupLabel>
<slot />
</ListboxGroup>
</template>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import type { ListboxFilterProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Search } from "lucide-vue-next"
import { ListboxFilter, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
import { useCommand } from "."
defineOptions({
inheritAttrs: false,
})
const props = defineProps<ListboxFilterProps & {
class?: HTMLAttributes["class"]
}>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
const { filterState } = useCommand()
</script>
<template>
<div
data-slot="command-input-wrapper"
class="flex h-9 items-center gap-2 border-b px-3"
>
<Search class="size-4 shrink-0 opacity-50" />
<ListboxFilter
v-bind="{ ...forwardedProps, ...$attrs }"
v-model="filterState.search"
data-slot="command-input"
auto-focus
:class="cn('placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50', props.class)"
/>
</div>
</template>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import type { ListboxItemEmits, ListboxItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit, useCurrentElement } from "@vueuse/core"
import { ListboxItem, useForwardPropsEmits, useId } from "reka-ui"
import { computed, onMounted, onUnmounted, ref } from "vue"
import { cn } from "@/lib/utils"
import { useCommand, useCommandGroup } from "."
const props = defineProps<ListboxItemProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<ListboxItemEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const id = useId()
const { filterState, allItems, allGroups } = useCommand()
const groupContext = useCommandGroup()
const isRender = computed(() => {
if (!filterState.search) {
return true
}
else {
const filteredCurrentItem = filterState.filtered.items.get(id)
// If the filtered items is undefined means not in the all times map yet
// Do the first render to add into the map
if (filteredCurrentItem === undefined) {
return true
}
// Check with filter
return filteredCurrentItem > 0
}
})
const itemRef = ref()
const currentElement = useCurrentElement(itemRef)
onMounted(() => {
if (!(currentElement.value instanceof HTMLElement))
return
// textValue to perform filter
allItems.value.set(id, currentElement.value.textContent ?? (props.value?.toString() ?? ""))
const groupId = groupContext?.id
if (groupId) {
if (!allGroups.value.has(groupId)) {
allGroups.value.set(groupId, new Set([id]))
}
else {
allGroups.value.get(groupId)?.add(id)
}
}
})
onUnmounted(() => {
allItems.value.delete(id)
})
</script>
<template>
<ListboxItem
v-if="isRender"
v-bind="forwarded"
:id="id"
ref="itemRef"
data-slot="command-item"
:class="cn('data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
@select="() => {
filterState.search = ''
}"
>
<slot />
</ListboxItem>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import type { ListboxContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ListboxContent, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<ListboxContentProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardProps(delegatedProps)
</script>
<template>
<ListboxContent
data-slot="command-list"
v-bind="forwarded"
:class="cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', props.class)"
>
<div role="presentation">
<slot />
</div>
</ListboxContent>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { SeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Separator } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SeparatorProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<Separator
data-slot="command-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 h-px', props.class)"
>
<slot />
</Separator>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
data-slot="command-shortcut"
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
>
<slot />
</span>
</template>

View File

@@ -0,0 +1,25 @@
import type { Ref } from "vue"
import { createContext } from "reka-ui"
export { default as Command } from "./Command.vue"
export { default as CommandDialog } from "./CommandDialog.vue"
export { default as CommandEmpty } from "./CommandEmpty.vue"
export { default as CommandGroup } from "./CommandGroup.vue"
export { default as CommandInput } from "./CommandInput.vue"
export { default as CommandItem } from "./CommandItem.vue"
export { default as CommandList } from "./CommandList.vue"
export { default as CommandSeparator } from "./CommandSeparator.vue"
export { default as CommandShortcut } from "./CommandShortcut.vue"
export const [useCommand, provideCommandContext] = createContext<{
allItems: Ref<Map<string, string>>
allGroups: Ref<Map<string, Set<string>>>
filterState: {
search: string
filtered: { count: number, items: Map<string, number>, groups: Set<string> }
}
}>("Command")
export const [useCommandGroup, provideCommandGroupContext] = createContext<{
id?: string
}>("CommandGroup")

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot
v-slot="slotProps"
data-slot="dialog"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DialogRoot>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogCloseProps } from "reka-ui"
import { DialogClose } from "reka-ui"
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose
data-slot="dialog-close"
v-bind="props"
>
<slot />
</DialogClose>
</template>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import DialogOverlay from "./DialogOverlay.vue"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<DialogContentProps & { class?: HTMLAttributes["class"], showCloseButton?: boolean }>(), {
showCloseButton: true,
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay />
<DialogContent
data-slot="dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
props.class,
)"
>
<slot />
<DialogClose
v-if="showCloseButton"
data-slot="dialog-close"
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<X />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogDescription, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DialogDescription
data-slot="dialog-description"
v-bind="forwardedProps"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</DialogDescription>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="dialog-footer"
:class="cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="dialog-header"
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogOverlayProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogOverlay } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogOverlay
data-slot="dialog-overlay"
v-bind="delegatedProps"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
>
<slot />
</DialogOverlay>
</template>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
<DialogContent
:class="
cn(
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
props.class,
)
"
v-bind="{ ...$attrs, ...forwarded }"
@pointer-down-outside="(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
event.preventDefault();
}
}"
>
<slot />
<DialogClose
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
>
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { DialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogTitle, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DialogTitle
data-slot="dialog-title"
v-bind="forwardedProps"
:class="cn('text-lg leading-none font-semibold', props.class)"
>
<slot />
</DialogTitle>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogTriggerProps } from "reka-ui"
import { DialogTrigger } from "reka-ui"
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger
data-slot="dialog-trigger"
v-bind="props"
>
<slot />
</DialogTrigger>
</template>

View File

@@ -0,0 +1,10 @@
export { default as Dialog } from "./Dialog.vue"
export { default as DialogClose } from "./DialogClose.vue"
export { default as DialogContent } from "./DialogContent.vue"
export { default as DialogDescription } from "./DialogDescription.vue"
export { default as DialogFooter } from "./DialogFooter.vue"
export { default as DialogHeader } from "./DialogHeader.vue"
export { default as DialogOverlay } from "./DialogOverlay.vue"
export { default as DialogScrollContent } from "./DialogScrollContent.vue"
export { default as DialogTitle } from "./DialogTitle.vue"
export { default as DialogTrigger } from "./DialogTrigger.vue"

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui"
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DropdownMenuRootProps>()
const emits = defineEmits<DropdownMenuRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRoot
v-slot="slotProps"
data-slot="dropdown-menu"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DropdownMenuRoot>
</template>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Check } from "lucide-vue-next"
import {
DropdownMenuCheckboxItem,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuCheckboxItem
data-slot="dropdown-menu-checkbox-item"
v-bind="forwarded"
:class=" cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)"
>
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Check class="size-4" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuContent,
DropdownMenuPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
{
sideOffset: 4,
},
)
const emits = defineEmits<DropdownMenuContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DropdownMenuGroupProps } from "reka-ui"
import { DropdownMenuGroup } from "reka-ui"
const props = defineProps<DropdownMenuGroupProps>()
</script>
<template>
<DropdownMenuGroup
data-slot="dropdown-menu-group"
v-bind="props"
>
<slot />
</DropdownMenuGroup>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { DropdownMenuItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DropdownMenuItem, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<DropdownMenuItemProps & {
class?: HTMLAttributes["class"]
inset?: boolean
variant?: "default" | "destructive"
}>(), {
variant: "default",
})
const delegatedProps = reactiveOmit(props, "inset", "variant", "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuItem
data-slot="dropdown-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwardedProps"
:class="cn('focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
>
<slot />
</DropdownMenuItem>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { DropdownMenuLabelProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DropdownMenuLabel, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, "class", "inset")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuLabel
data-slot="dropdown-menu-label"
:data-inset="inset ? '' : undefined"
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui"
import {
DropdownMenuRadioGroup,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps<DropdownMenuRadioGroupProps>()
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRadioGroup
data-slot="dropdown-menu-radio-group"
v-bind="forwarded"
>
<slot />
</DropdownMenuRadioGroup>
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Circle } from "lucide-vue-next"
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuRadioItemEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuRadioItem
data-slot="dropdown-menu-radio-item"
v-bind="forwarded"
:class="cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)"
>
<span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<slot name="indicator-icon">
<Circle class="size-2 fill-current" />
</slot>
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import type { DropdownMenuSeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSeparator,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuSeparatorProps & {
class?: HTMLAttributes["class"]
}>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DropdownMenuSeparator
data-slot="dropdown-menu-separator"
v-bind="delegatedProps"
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
/>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
data-slot="dropdown-menu-shortcut"
:class="cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)"
>
<slot />
</span>
</template>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui"
import {
DropdownMenuSub,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps<DropdownMenuSubProps>()
const emits = defineEmits<DropdownMenuSubEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuSub v-slot="slotProps" data-slot="dropdown-menu-sub" v-bind="forwarded">
<slot v-bind="slotProps" />
</DropdownMenuSub>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DropdownMenuSubContent,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
>
<slot />
</DropdownMenuSubContent>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { DropdownMenuSubTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ChevronRight } from "lucide-vue-next"
import {
DropdownMenuSubTrigger,
useForwardProps,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, "class", "inset")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuSubTrigger
data-slot="dropdown-menu-sub-trigger"
v-bind="forwardedProps"
:class="cn(
'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
props.class,
)"
>
<slot />
<ChevronRight class="ml-auto size-4" />
</DropdownMenuSubTrigger>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { DropdownMenuTriggerProps } from "reka-ui"
import { DropdownMenuTrigger, useForwardProps } from "reka-ui"
const props = defineProps<DropdownMenuTriggerProps>()
const forwardedProps = useForwardProps(props)
</script>
<template>
<DropdownMenuTrigger
data-slot="dropdown-menu-trigger"
v-bind="forwardedProps"
>
<slot />
</DropdownMenuTrigger>
</template>

View File

@@ -0,0 +1,16 @@
export { default as DropdownMenu } from "./DropdownMenu.vue"
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue"
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue"
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue"
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue"
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue"
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue"
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue"
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue"
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue"
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
export { DropdownMenuPortal } from "reka-ui"

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { PopoverRootEmits, PopoverRootProps } from "reka-ui"
import { PopoverRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<PopoverRootProps>()
const emits = defineEmits<PopoverRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<PopoverRoot
v-slot="slotProps"
data-slot="popover"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</PopoverRoot>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { PopoverAnchorProps } from "reka-ui"
import { PopoverAnchor } from "reka-ui"
const props = defineProps<PopoverAnchorProps>()
</script>
<template>
<PopoverAnchor
data-slot="popover-anchor"
v-bind="props"
>
<slot />
</PopoverAnchor>
</template>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import type { PopoverContentEmits, PopoverContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
PopoverContent,
PopoverPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<PopoverContentProps & { class?: HTMLAttributes["class"] }>(),
{
align: "center",
sideOffset: 4,
},
)
const emits = defineEmits<PopoverContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<PopoverPortal>
<PopoverContent
data-slot="popover-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md origin-(--reka-popover-content-transform-origin) outline-hidden',
props.class,
)
"
>
<slot />
</PopoverContent>
</PopoverPortal>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { PopoverTriggerProps } from "reka-ui"
import { PopoverTrigger } from "reka-ui"
const props = defineProps<PopoverTriggerProps>()
</script>
<template>
<PopoverTrigger
data-slot="popover-trigger"
v-bind="props"
>
<slot />
</PopoverTrigger>
</template>

View File

@@ -0,0 +1,4 @@
export { default as Popover } from "./Popover.vue"
export { default as PopoverAnchor } from "./PopoverAnchor.vue"
export { default as PopoverContent } from "./PopoverContent.vue"
export { default as PopoverTrigger } from "./PopoverTrigger.vue"

View File

@@ -0,0 +1,42 @@
<script lang="ts" setup>
import type { ToasterProps } from "vue-sonner"
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, XIcon } from "lucide-vue-next"
import { Toaster as Sonner } from "vue-sonner"
import { cn } from "@/lib/utils"
const props = defineProps<ToasterProps>()
</script>
<template>
<Sonner
:class="cn('toaster group', props.class)"
:style="{
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
'--border-radius': 'var(--radius)',
}"
v-bind="props"
>
<template #success-icon>
<CircleCheckIcon class="size-4" />
</template>
<template #info-icon>
<InfoIcon class="size-4" />
</template>
<template #warning-icon>
<TriangleAlertIcon class="size-4" />
</template>
<template #error-icon>
<OctagonXIcon class="size-4" />
</template>
<template #loading-icon>
<div>
<Loader2Icon class="size-4 animate-spin" />
</div>
</template>
<template #close-icon>
<XIcon class="size-4" />
</template>
</Sonner>
</template>

View File

@@ -0,0 +1 @@
export { default as Toaster } from "./Sonner.vue"

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import type { SwitchRootEmits, SwitchRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
SwitchRoot,
SwitchThumb,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<SwitchRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SwitchRoot
v-slot="slotProps"
data-slot="switch"
v-bind="forwarded"
:class="cn(
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)"
>
<SwitchThumb
data-slot="switch-thumb"
:class="cn('bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0')"
>
<slot name="thumb" v-bind="slotProps" />
</SwitchThumb>
</SwitchRoot>
</template>

View File

@@ -0,0 +1 @@
export { default as Switch } from "./Switch.vue"