更新设备管理平台的名字为地空业务支撑平台--生产管理子系统

This commit is contained in:
徐星 2026-04-01 17:32:13 +08:00
parent cbfd253720
commit 8d23513bc7
94 changed files with 270 additions and 5518 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,36 +0,0 @@
import{c as n,d as _,a as o,b as e,F as c,r as d,o as s,t as l,n as r,e as u,u as v,f as y,g as f,h as F}from"./index-C436_g8x.js";import{S as w}from"./server-Del2gqI5.js";import{C}from"./clock-gKlC1TlO.js";import{U as S}from"./upload-DdEqBuGd.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const D=n("circle-check-big",[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335",key:"yps3ct"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]]);/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const N=n("package-check",[["path",{d:"m16 16 2 2 4-4",key:"gfu2re"}],["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14",key:"e7tb2h"}],["path",{d:"m7.5 4.27 9 5.15",key:"1c824w"}],["polyline",{points:"3.29 7 12 12 20.71 7",key:"ousv84"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12",key:"a4e8g8"}]]);/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const z=n("target",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"6",key:"1vlfrh"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}]]);/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const A=n("trending-down",[["polyline",{points:"22 17 13.5 8.5 8.5 13.5 2 7",key:"1r2t7k"}],["polyline",{points:"16 17 22 17 22 11",key:"11uiuu"}]]);/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const V=n("trending-up",[["polyline",{points:"22 7 13.5 15.5 8.5 10.5 2 17",key:"126l90"}],["polyline",{points:"16 7 22 7 22 13",key:"kwv8wd"}]]);/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const M=n("wifi",[["path",{d:"M12 20h.01",key:"zekei9"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0",key:"dnpr2z"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0",key:"1x1e6c"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0",key:"1bycff"}]]);/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const j=n("wrench",[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z",key:"cbrjhi"}]]),B={class:"p-6"},L={class:"grid grid-cols-4 gap-6 mb-6"},T={class:"flex items-start justify-between"},U={class:"flex-1"},W={class:"text-sm mb-2",style:{color:"rgba(0, 0, 0, 0.65)"}},E={class:"text-3xl font-semibold mb-2"},G={class:"text-sm"},I={class:"bg-white p-6 rounded-lg mb-6",style:{"box-shadow":"0 1px 2px rgba(0, 0, 0, 0.05)"}},P={style:{display:"flex","flex-direction":"column",gap:"16px"}},R={style:{width:"60px","text-align":"right","font-size":"14px",color:"rgba(0,0,0,0.65)","flex-shrink":"0"}},q={style:{flex:"1","background-color":"#F5F5F5","border-radius":"4px",height:"24px",overflow:"hidden"}},H={style:{width:"40px","font-size":"14px",color:"rgba(0,0,0,0.85)"}},J={class:"bg-white p-6 rounded-lg",style:{"box-shadow":"0 1px 2px rgba(0, 0, 0, 0.05)"}},K={class:"grid grid-cols-2 gap-6"},O={class:"flex items-center justify-between mb-4"},Q={class:"text-base font-medium"},X={class:"px-2 py-1 rounded text-xs",style:{"background-color":"#F0F2F5",color:"rgba(0, 0, 0, 0.65)"}},Y={class:"flex-1"},Z={class:"text-sm mb-1"},$={class:"text-sm",style:{color:"rgba(0, 0, 0, 0.45)"}},ee={class:"flex items-center gap-3"},te={key:0,class:"text-xs",style:{color:"rgba(0, 0, 0, 0.45)"}},se={key:0,class:"w-full mt-3 text-center text-sm",style:{color:"#1890FF"}},ce=_({__name:"Dashboard",setup(le){const b=[{label:"",value:"5,234",trend:"up",trendValue:"+5.2%",color:"#1890FF",icon:w},{label:"",value:"4,856",trend:"up",trendValue:"+2.8%",color:"#52C41A",icon:M},{label:"",value:"4,912",trend:"up",trendValue:"+1.5%",color:"#1890FF",icon:D},{label:"",value:"156",color:"#722ED1",icon:N},{label:"",value:"23",trend:"down",trendValue:"-12.3%",color:"#FF4D4F",icon:j},{label:"",value:"56",color:"#FA8C16",icon:z},{label:"",value:"45",color:"#FAAD14",icon:C},{label:"",value:"8",color:"#13C2C2",icon:S}],x=[{name:"",value:45,color:"#52C41A"},{name:"",value:378,color:"#FF4D4F"},{name:"",value:286,color:"#FAAD14"},{name:"",value:7,color:"#8C8C8C"}],m=F(()=>Math.max(...x.map(h=>h.value))),g=[{title:"",count:8,tasks:[{deviceSN:"SN2024030710",description:"v2.3.5",time:"1"},{deviceSN:"SN2024030711",description:"v2.3.5",time:"1"}]},{title:"",count:5,tasks:[{deviceSN:"SN2024030530",description:"",time:"4"},{deviceSN:"SN2024030531",description:"",time:"6"}]}];return(h,a)=>(s(),o("div",B,[a[3]||(a[3]=e("div",{class:"mb-6"},[e("h2",{class:"text-2xl font-semibold mb-1"},""),e("p",{class:"text-sm",style:{color:"rgba(0, 0, 0, 0.45)"}},"")],-1)),e("div",L,[(s(),o(c,null,d(b,(t,p)=>e("div",{key:p,class:"bg-white p-6 rounded-lg",style:{"box-shadow":"0 1px 2px rgba(0, 0, 0, 0.05)"}},[e("div",T,[e("div",U,[e("div",W,l(t.label),1),e("div",E,l(t.value),1),t.trend&&t.trendValue?(s(),o("div",{key:0,class:"flex items-center gap-1",style:r({color:t.trend==="up"?"#52C41A":"#FF4D4F"})},[t.trend==="up"?(s(),u(v(V),{key:0,size:14})):(s(),u(v(A),{key:1,size:14})),e("span",G,l(t.trendValue),1)],4)):y("",!0)]),e("div",{class:"w-12 h-12 rounded-lg flex items-center justify-center",style:r({backgroundColor:t.color+"15"})},[(s(),u(f(t.icon),{size:24,style:r({color:t.color})},null,8,["style"]))],4)])])),64))]),e("div",I,[a[0]||(a[0]=e("h3",{class:"text-lg font-semibold mb-6"},"",-1)),e("div",P,[(s(),o(c,null,d(x,t=>e("div",{key:t.name,style:{display:"flex","align-items":"center",gap:"12px"}},[e("div",R,l(t.name),1),e("div",q,[e("div",{style:r({width:t.value/m.value*100+"%",height:"100%",backgroundColor:t.color,borderRadius:"0 4px 4px 0",transition:"width 0.3s ease",minWidth:t.value>0?"2px":"0"})},null,4)]),e("div",H,l(t.value),1)])),64))])]),e("div",J,[a[2]||(a[2]=e("h3",{class:"text-lg font-semibold mb-6"},"",-1)),e("div",K,[(s(),o(c,null,d(g,(t,p)=>e("div",{key:p},[e("div",O,[e("h4",Q,l(t.title),1),e("span",X,l(t.count),1)]),e("div",null,[(s(!0),o(c,null,d(t.tasks,(i,k)=>(s(),o("div",{key:k,class:"flex items-start justify-between py-3",style:{"border-bottom":"1px solid #F0F0F0"}},[e("div",Y,[e("div",Z,l(i.deviceSN),1),e("div",$,l(i.description),1)]),e("div",ee,[i.time?(s(),o("span",te,l(i.time),1)):y("",!0),a[1]||(a[1]=e("button",{class:"text-sm",style:{color:"#1890FF"}},"",-1))])]))),128))]),t.tasks.length<t.count?(s(),o("button",se," "+l(t.count)+" ",1)):y("",!0)])),64))])])]))}});export{ce as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{d as o,a,b as e,t as l,o as r}from"./index-C436_g8x.js";const n={class:"p-6"},d={class:"text-2xl font-semibold mb-4"},c=o({__name:"PlaceholderPage",props:{title:{}},setup(s){return(i,t)=>(r(),a("div",n,[e("h2",d,l(s.title),1),t[0]||(t[0]=e("div",{class:"bg-white p-6 rounded-lg",style:{"box-shadow":"0 1px 2px rgba(0,0,0,0.05)"}},[e("p",{style:{color:"rgba(0,0,0,0.45)"}},"此页面正在开发中...")],-1))]))}});export{c as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +0,0 @@
import{c as e}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const t=e("arrow-left",[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]]);export{t as A};

View File

@ -1,6 +0,0 @@
import{c}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const r=c("circle-check",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]);export{r as C};

View File

@ -1,6 +0,0 @@
import{c}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const r=c("circle-stop",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["rect",{x:"9",y:"9",width:"6",height:"6",rx:"1",key:"1ssd4o"}]]);export{r as C};

View File

@ -1,6 +0,0 @@
import{c}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const o=c("clock",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["polyline",{points:"12 6 12 12 16 14",key:"68esgv"}]]);export{o as C};

View File

@ -1,6 +0,0 @@
import{c as o}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const a=o("download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);export{a as D};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +0,0 @@
import{c}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const o=c("info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);export{o as I};

View File

@ -1,6 +0,0 @@
import{c as e}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const a=e("plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);export{a as P};

View File

@ -1,6 +0,0 @@
import{c}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const r=c("search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);export{r as S};

View File

@ -1,6 +0,0 @@
import{c as e}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const y=e("server",[["rect",{width:"20",height:"8",x:"2",y:"2",rx:"2",ry:"2",key:"ngkwjq"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2",ry:"2",key:"iecqi9"}],["line",{x1:"6",x2:"6.01",y1:"6",y2:"6",key:"16zg32"}],["line",{x1:"6",x2:"6.01",y1:"18",y2:"18",key:"nzw8ys"}]]);export{y as S};

View File

@ -1,6 +0,0 @@
import{c as e}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const a=e("trash-2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);export{a as T};

View File

@ -1,6 +0,0 @@
import{c as e}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const t=e("triangle-alert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);export{t as T};

View File

@ -1,6 +0,0 @@
import{c as o}from"./index-C436_g8x.js";/**
* @license lucide-vue-next v0.487.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const a=o("upload",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"17 8 12 3 7 8",key:"t8dd8p"}],["line",{x1:"12",x2:"12",y1:"3",y2:"15",key:"widbto"}]]);export{a as U};

6
dist/index.html vendored
View File

@ -3,9 +3,9 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>设备管理平台</title> <title>地空业务支撑平台——生产管理子系统</title>
<script type="module" crossorigin src="/assets/index-C436_g8x.js"></script> <script type="module" crossorigin src="/assets/index-C5t0nGiQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CHZHmOOh.css"> <link rel="stylesheet" crossorigin href="/assets/index-CRkCzBUD.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>设备管理平台</title> <title>地空业务支撑平台——生产管理子系统</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -1,25 +1,34 @@
<template> <template>
<header class="h-12 flex items-center justify-end px-6 flex-shrink-0 gap-4" <header class="h-14 flex items-center justify-between px-6 flex-shrink-0"
style="background-color: #fff; border-bottom: 1px solid #e8e8e8"> style="background-color: #fff; border-bottom: 1px solid #e8e8e8">
<router-link to="/reports" class="text-gray-500 hover:text-gray-900 transition-colors" title="数据统计"> <!-- Title -->
<BarChart3 :size="20" /> <div class="flex items-center gap-2">
</router-link> <h1 class="text-base font-semibold" style="color: #1a1a1a">地空业务支撑平台生产管理子系统</h1>
<router-link to="/cost-reports" class="text-gray-500 hover:text-gray-900 transition-colors" title="运营报告"> </div>
<FileText :size="20" />
</router-link> <!-- Right side -->
<div class="w-px h-5 bg-gray-200" /> <div class="flex items-center gap-4">
<div ref="menuRef" class="relative"> <router-link to="/reports" class="text-gray-500 hover:text-gray-900 transition-colors" title="数据统计">
<button @click="open = !open" class="flex items-center gap-2 cursor-pointer"> <BarChart3 :size="18" />
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm" </router-link>
style="background-color: #1890FF"></div> <router-link to="/cost-reports" class="text-gray-500 hover:text-gray-900 transition-colors" title="运营报告">
<span class="text-sm text-gray-700">管理员</span> <FileText :size="18" />
</button> </router-link>
<div v-if="open" class="absolute right-0 top-10 w-36 rounded shadow-lg py-1 z-50" <div class="w-px h-5 bg-gray-200" />
style="background-color: #fff; border: 1px solid #e8e8e8"> <div ref="menuRef" class="relative">
<router-link v-for="item in systemMenuItems" :key="item.path" :to="item.path" @click="open = false" <button @click="open = !open" class="flex items-center gap-2 cursor-pointer">
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"> <span class="text-sm" style="color: #1a1a1a">Super Admin</span>
{{ item.label }} <span class="px-2 py-0.5 rounded text-xs text-white" style="background-color: #4a7c59">管理员</span>
</router-link> </button>
<div v-if="open" class="absolute right-0 top-10 w-36 rounded shadow-lg py-1 z-50"
style="background-color: #fff; border: 1px solid #e8e8e8">
<router-link v-for="item in systemMenuItems" :key="item.path" :to="item.path" @click="open = false"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors">
{{ item.label }}
</router-link>
<div style="border-top: 1px solid #F0F0F0"></div>
<button class="block w-full text-left px-4 py-2 text-sm" style="color: #FF4D4F">退出</button>
</div>
</div> </div>
</div> </div>
</header> </header>
@ -28,17 +37,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { BarChart3, FileText } from 'lucide-vue-next' import { BarChart3, FileText } from 'lucide-vue-next'
const open = ref(false) const open = ref(false)
const menuRef = ref<HTMLDivElement>() const menuRef = ref<HTMLDivElement>()
const systemMenuItems = [ const systemMenuItems = [
{ path: '/users', label: '用户管理' }, { path: '/users', label: '用户管理' },
{ path: '/roles', label: '角色权限' }, { path: '/roles', label: '角色权限' },
{ path: '/logs', label: '操作日志' }, { path: '/logs', label: '操作日志' },
{ path: '/settings', label: '系统设置' }, { path: '/settings', label: '系统设置' },
] ]
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
if (menuRef.value && !menuRef.value.contains(e.target as Node)) open.value = false if (menuRef.value && !menuRef.value.contains(e.target as Node)) open.value = false
} }

View File

@ -1,47 +1,65 @@
<template> <template>
<aside class="w-[200px] h-screen flex-shrink-0 overflow-y-auto" style="background-color: #001529"> <aside class="w-[200px] h-screen flex-shrink-0 overflow-y-auto" style="background-color: #fff; border-right: 1px solid #e8e8e8">
<div class="h-1" style="background-color: #1890FF" /> <!-- Logo -->
<router-link to="/" class="block px-4 py-3 text-sm font-medium transition-colors" <div class="px-4 py-4" style="border-bottom: 1px solid #f0f0f0">
:style="{ color: route.path === '/' ? '#fff' : 'rgba(255,255,255,0.85)', backgroundColor: route.path === '/' ? '#1890FF' : 'transparent', borderBottom: '1px solid rgba(255,255,255,0.06)' }"> <div class="text-lg font-semibold" style="color: #4a7c59">Geomative</div>
</div>
<!-- Homepage -->
<router-link to="/" class="flex items-center gap-2 px-4 py-3 text-sm transition-colors"
:style="{ color: isActive('/') ? '#4a7c59' : 'rgba(0,0,0,0.65)', backgroundColor: isActive('/') ? '#eef5f0' : 'transparent', borderLeft: isActive('/') ? '3px solid #4a7c59' : '3px solid transparent', fontWeight: isActive('/') ? 600 : 400 }">
首页 首页
</router-link> </router-link>
<nav class="py-0">
<div v-for="group in menuGroups" :key="group.title"> <!-- Menu Groups -->
<button @click="toggleGroup(group.title)" <div v-for="group in menuGroups" :key="group.title">
class="w-full text-left px-4 py-2.5 text-sm font-medium cursor-pointer" <div class="px-4 py-2.5 text-xs font-semibold uppercase tracking-wider"
style="color: rgba(255,255,255,0.85); border-bottom: 1px solid rgba(255,255,255,0.06)"> style="color: rgba(0,0,0,0.35)">
{{ group.title }} {{ group.title }}
</button>
<template v-if="!collapsedGroups.has(group.title)">
<router-link v-for="item in group.items" :key="item.path" :to="item.path"
class="block px-8 py-2 text-sm transition-colors"
:style="{ color: route.path === item.path || (item.path !== '/' && route.path.startsWith(item.path)) ? '#fff' : 'rgba(255,255,255,0.65)', backgroundColor: route.path === item.path || (item.path !== '/' && route.path.startsWith(item.path)) ? '#1890FF' : 'transparent' }">
{{ item.label }}
</router-link>
</template>
</div> </div>
</nav> <router-link
v-for="item in group.items" :key="item.path" :to="item.path"
class="flex items-center gap-2 px-4 py-2 text-sm transition-colors"
:style="{
color: isActive(item.path) ? '#4a7c59' : 'rgba(0,0,0,0.65)',
backgroundColor: isActive(item.path) ? '#eef5f0' : 'transparent',
borderLeft: isActive(item.path) ? '3px solid #4a7c59' : '3px solid transparent',
fontWeight: isActive(item.path) ? 600 : 400,
}">
<component :is="item.icon" :size="16" :style="{ color: isActive(item.path) ? '#4a7c59' : 'rgba(0,0,0,0.35)' }" />
{{ item.label }}
</router-link>
</div>
</aside> </aside>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { Monitor, Settings2, Key, Cpu, FileCode, Gauge, Wrench } from 'lucide-vue-next'
const route = useRoute() const route = useRoute()
const collapsedGroups = ref(new Set<string>()) const isActive = (path: string) =>
path === '/' ? route.path === '/' : route.path === path || route.path.startsWith(path + '/')
const toggleGroup = (title: string) => {
if (collapsedGroups.value.has(title)) collapsedGroups.value.delete(title)
else collapsedGroups.value.add(title)
}
const menuGroups = [ const menuGroups = [
{ title: '设备', items: [{ path: '/devices', label: '设备列表' }, { path: '/models', label: '型号管理' }] }, { title: '设备', items: [
{ title: '授权', items: [{ path: '/licenses', label: '授权管理' }, { path: '/activation', label: '激活管理' }] }, { path: '/devices', label: '设备列表', icon: Monitor },
{ title: '固件', items: [{ path: '/firmware', label: '固件库' }] }, { path: '/models', label: '型号管理', icon: Settings2 },
{ title: '配置', items: [{ path: '/config-files', label: '配置管理' }] }, ]},
{ title: '校准', items: [{ path: '/calibration', label: '校准记录' }] }, { title: '授权', items: [
{ title: '维修', items: [{ path: '/repair', label: '维修工单' }, { path: '/repair/stats', label: '维修统计' }, { path: '/scrap', label: '报废回收' }] }, { path: '/licenses', label: '授权管理', icon: Key },
{ path: '/activation', label: '激活管理', icon: Key },
]},
{ title: '配置', items: [
{ path: '/config-files', label: '配置管理', icon: FileCode },
]},
{ title: '校准', items: [
{ path: '/calibration', label: '校准记录', icon: Gauge },
]},
{ title: '维修', items: [
{ path: '/repair', label: '维修工单', icon: Wrench },
{ path: '/repair/stats', label: '维修统计', icon: Wrench },
{ path: '/scrap', label: '报废回收', icon: Wrench },
]},
] ]
</script> </script>

View File

@ -1,66 +0,0 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "./utils";
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
);
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@ -1,157 +0,0 @@
"use client";
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "./utils";
import { buttonVariants } from "./button";
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
);
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={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/50",
className,
)}
{...props}
/>
);
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={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",
className,
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
);
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View File

@ -1,66 +0,0 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "./utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className,
)}
{...props}
/>
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className,
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };

View File

@ -1,11 +0,0 @@
"use client";
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
}
export { AspectRatio };

View File

@ -1,53 +0,0 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "./utils";
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className,
)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };

View File

@ -1,46 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "./utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

View File

@ -1,109 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "./utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className,
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
);
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
);
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

@ -1,58 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "./utils";
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 text-foreground 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 rounded-md",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@ -1,75 +0,0 @@
"use client";
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "./utils";
import { buttonVariants } from "./button";
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: React.ComponentProps<typeof DayPicker>) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-x-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md",
),
day: cn(
buttonVariants({ variant: "ghost" }),
"size-8 p-0 font-normal aria-selected:opacity-100",
),
day_range_start:
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
day_range_end:
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("size-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("size-4", className)} {...props} />
),
}}
{...props}
/>
);
}
export { Calendar };

View File

@ -1,92 +0,0 @@
import * as React from "react";
import { cn } from "./utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<h4
data-slot="card-title"
className={cn("leading-none", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<p
data-slot="card-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6 [&:last-child]:pb-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 pb-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@ -1,241 +0,0 @@
"use client";
import * as React from "react";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "./utils";
import { Button } from "./button";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return;
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) return;
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel();
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
);
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
);
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};

View File

@ -1,353 +0,0 @@
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "./utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
}) {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}) {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
},
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}) {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string,
) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};

View File

@ -1,32 +0,0 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "./utils";
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border bg-input-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View File

@ -1,33 +0,0 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
);
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@ -1,177 +0,0 @@
"use client";
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { SearchIcon } from "lucide-react";
import { cn } from "./utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "./dialog";
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className,
)}
{...props}
/>
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
description?: string;
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={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",
className,
)}
{...props}
/>
</div>
);
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className,
)}
{...props}
/>
);
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className,
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
);
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]: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=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@ -1,252 +0,0 @@
"use client";
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "./utils";
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
);
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
);
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
);
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
);
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
);
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={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-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={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-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
);
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={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",
className,
)}
{...props}
/>
);
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={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",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={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",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View File

@ -1,135 +0,0 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "./utils";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={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/50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={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",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="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">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@ -1,132 +0,0 @@
"use client";
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "./utils";
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={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/50",
className,
)}
{...props}
/>
);
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className,
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View File

@ -1,257 +0,0 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "./utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={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-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={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",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={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",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={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",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={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",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={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-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

View File

@ -1,168 +0,0 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { cn } from "./utils";
import { Label } from "./label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
);
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField();
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField();
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : props.children;
if (!body) {
return null;
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
);
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View File

@ -1,44 +0,0 @@
"use client";
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "./utils";
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
);
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={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-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@ -1,77 +0,0 @@
"use client";
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { MinusIcon } from "lucide-react";
import { cn } from "./utils";
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string;
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
);
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center gap-1", className)}
{...props}
/>
);
}
function InputOTPSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & {
index: number;
}) {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm bg-input-background transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
);
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
);
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@ -1,21 +0,0 @@
import * as React from "react";
import { cn } from "./utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"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",
className,
)}
{...props}
/>
);
}
export { Input };

View File

@ -1,24 +0,0 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "./utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@ -1,276 +0,0 @@
"use client";
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "./utils";
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className,
)}
{...props}
/>
);
}
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
);
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className,
)}
{...props}
/>
);
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in 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-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</MenubarPortal>
);
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={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",
className,
)}
{...props}
/>
);
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs 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",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
);
}
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs 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",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
);
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={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-none select-none data-[inset]:pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
);
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={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-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
};

View File

@ -1,168 +0,0 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "./utils";
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean;
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
);
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className,
)}
{...props}
/>
);
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
);
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
);
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
);
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className,
)}
{...props}
/>
);
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center",
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
{...props}
/>
</div>
);
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className,
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
);
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
};

View File

@ -1,127 +0,0 @@
import * as React from "react";
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react";
import { cn } from "./utils";
import { Button, buttonVariants } from "./button";
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
);
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />;
}
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">;
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className,
)}
{...props}
/>
);
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
);
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
);
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
);
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};

View File

@ -1,48 +0,0 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "./utils";
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={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 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@ -1,31 +0,0 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "./utils";
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}
export { Progress };

View File

@ -1,45 +0,0 @@
"use client";
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { CircleIcon } from "lucide-react";
import { cn } from "./utils";
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
);
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
export { RadioGroup, RadioGroupItem };

View File

@ -1,56 +0,0 @@
"use client";
import * as React from "react";
import { GripVerticalIcon } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "./utils";
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className,
)}
{...props}
/>
);
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@ -1,58 +0,0 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "./utils";
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };

View File

@ -1,189 +0,0 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "lucide-react";
import { cn } from "./utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 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 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View File

@ -1,28 +0,0 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "./utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator-root"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };

View File

@ -1,139 +0,0 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "./utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={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/50",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary 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">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@ -1,726 +0,0 @@
"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { VariantProps, cva } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";
import { useIsMobile } from "./use-mobile";
import { cn } from "./utils";
import { Button } from "./button";
import { Input } from "./input";
import { Separator } from "./separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "./sheet";
import { Skeleton } from "./skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className,
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className,
)}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
);
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar();
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar();
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className,
)}
{...props}
/>
);
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
);
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
);
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
);
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
);
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
);
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
);
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
);
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className,
)}
{...props}
/>
);
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
);
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};

View File

@ -1,13 +0,0 @@
import { cn } from "./utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
);
}
export { Skeleton };

View File

@ -1,63 +0,0 @@
"use client";
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "./utils";
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max],
);
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className,
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-4 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
);
}
export { Slider };

View File

@ -1,25 +0,0 @@
"use client";
import { useTheme } from "next-themes";
import { Toaster as Sonner, ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };

View File

@ -1,31 +0,0 @@
"use client";
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "./utils";
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-switch-background 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 transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-card dark:data-[state=unchecked]:bg-card-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",
)}
/>
</SwitchPrimitive.Root>
);
}
export { Switch };

View File

@ -1,116 +0,0 @@
"use client";
import * as React from "react";
import { cn } from "./utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
);
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
);
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className,
)}
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
);
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@ -1,66 +0,0 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "./utils";
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-xl p-[3px] flex",
className,
)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-card dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@ -1,18 +0,0 @@
import * as React from "react";
import { cn } from "./utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"resize-none border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-input-background px-3 py-2 text-base transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}
/>
);
}
export { Textarea };

View File

@ -1,73 +0,0 @@
"use client";
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "./utils";
import { toggleVariants } from "./toggle";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
});
function ToggleGroup({
className,
variant,
size,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
}
export { ToggleGroup, ToggleGroupItem };

View File

@ -1,47 +0,0 @@
"use client";
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "./utils";
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Toggle, toggleVariants };

View File

@ -1,61 +0,0 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "./utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@ -1,21 +0,0 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

View File

@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@ -68,10 +68,10 @@ const getStatusStyle = (status: ActivationRecord['status']) => {
<!-- Info Banner --> <!-- Info Banner -->
<div <div
class="mb-6 p-4 rounded-lg flex items-start gap-3" class="mb-6 p-4 rounded-lg flex items-start gap-3"
style="background-color: #E6F7FF; border: 1px solid #91D5FF" style="background-color: #eef5f0; border: 1px solid #a3c4ad"
> >
<Info :size="20" style="color: #1890FF; flex-shrink: 0; margin-top: 2px" /> <Info :size="20" style="color: #4a7c59; flex-shrink: 0; margin-top: 2px" />
<div style="color: #0050B3"> <div style="color: #2d5a3d">
<div class="font-medium mb-1">APP激活流程说明</div> <div class="font-medium mb-1">APP激活流程说明</div>
<div class="text-sm"> <div class="text-sm">
APP读取设备UID 与后台SN匹配 下载授权文件和配置 生成设备配置 APP读取设备UID 与后台SN匹配 下载授权文件和配置 生成设备配置
@ -113,7 +113,7 @@ const getStatusStyle = (status: ActivationRecord['status']) => {
/> />
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<button class="w-full px-4 py-2 rounded text-white" style="background-color: #1890FF"> <button class="w-full px-4 py-2 rounded text-white" style="background-color: #4a7c59">
查询 查询
</button> </button>
</div> </div>
@ -155,7 +155,7 @@ const getStatusStyle = (status: ActivationRecord['status']) => {
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" style="color: #1890FF">详情</button> <button class="text-sm" style="color: #4a7c59">详情</button>
</div> </div>
</td> </td>
</tr> </tr>
@ -170,14 +170,14 @@ const getStatusStyle = (status: ActivationRecord['status']) => {
<div class="flex items-center justify-between gap-6"> <div class="flex items-center justify-between gap-6">
<div <div
class="flex-1 p-6 rounded-lg text-center" class="flex-1 p-6 rounded-lg text-center"
style="background-color: #E6F7FF; border: 2px solid #1890FF" style="background-color: #eef5f0; border: 2px solid #4a7c59"
> >
<div <div
class="w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3" class="w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3"
style="background-color: #1890FF; color: #fff; font-size: 20px; font-weight: bold" style="background-color: #4a7c59; color: #fff; font-size: 20px; font-weight: bold"
></div> ></div>
<div class="font-medium mb-2" style="color: #1890FF">读取UID</div> <div class="font-medium mb-2" style="color: #4a7c59">读取UID</div>
<div class="text-sm" style="color: #0050B3">APP读取设备UID</div> <div class="text-sm" style="color: #2d5a3d">APP读取设备UID</div>
</div> </div>
<div style="color: #D9D9D9; font-size: 24px"></div> <div style="color: #D9D9D9; font-size: 24px"></div>
<div <div

View File

@ -87,7 +87,7 @@ const getStatusStyle = (status: CalibrationRecord['status']) => {
<div class="grid grid-cols-4 gap-6 mb-6"> <div class="grid grid-cols-4 gap-6 mb-6">
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">采集板校准总数</div> <div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">采集板校准总数</div>
<div class="text-3xl font-semibold" style="color: #1890FF">1,245</div> <div class="text-3xl font-semibold" style="color: #4a7c59">1,245</div>
</div> </div>
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">待校准采集板</div> <div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">待校准采集板</div>
@ -95,7 +95,7 @@ const getStatusStyle = (status: CalibrationRecord['status']) => {
</div> </div>
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">校准中</div> <div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">校准中</div>
<div class="text-3xl font-semibold" style="color: #1890FF">8</div> <div class="text-3xl font-semibold" style="color: #4a7c59">8</div>
</div> </div>
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">校准即将到期</div> <div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">校准即将到期</div>
@ -140,7 +140,7 @@ const getStatusStyle = (status: CalibrationRecord['status']) => {
</select> </select>
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<button class="w-full px-4 py-2 rounded text-white" style="background-color: #1890FF"> <button class="w-full px-4 py-2 rounded text-white" style="background-color: #4a7c59">
查询 查询
</button> </button>
</div> </div>
@ -184,8 +184,8 @@ const getStatusStyle = (status: CalibrationRecord['status']) => {
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" style="color: #1890FF">详情</button> <button class="text-sm" style="color: #4a7c59">详情</button>
<button class="text-sm" style="color: #1890FF">下载报告</button> <button class="text-sm" style="color: #4a7c59">下载报告</button>
</div> </div>
</td> </td>
</tr> </tr>
@ -208,7 +208,7 @@ const getStatusStyle = (status: CalibrationRecord['status']) => {
> >
上一页 上一页
</button> </button>
<button class="px-3 py-1 rounded" style="background-color: #1890FF; color: #fff">1</button> <button class="px-3 py-1 rounded" style="background-color: #4a7c59; color: #fff">1</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">2</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">2</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">3</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">3</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">下一页</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">下一页</button>

View File

@ -56,7 +56,7 @@ const getStatusStyle = (status: ConfigFile['status']) => {
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
class="px-4 py-2 rounded text-white flex items-center gap-2" class="px-4 py-2 rounded text-white flex items-center gap-2"
style="background-color: #1890FF" style="background-color: #4a7c59"
@click="router.push('/config-files/new')" @click="router.push('/config-files/new')"
> >
<Plus :size="16" /> <Plus :size="16" />
@ -70,10 +70,10 @@ const getStatusStyle = (status: ConfigFile['status']) => {
<!-- Info Banner --> <!-- Info Banner -->
<div <div
class="mb-6 p-4 rounded-lg flex items-start gap-3" class="mb-6 p-4 rounded-lg flex items-start gap-3"
style="background-color: #E6F7FF; border: 1px solid #91D5FF" style="background-color: #eef5f0; border: 1px solid #a3c4ad"
> >
<Info :size="20" style="color: #1890FF; flex-shrink: 0; margin-top: 2px" /> <Info :size="20" style="color: #4a7c59; flex-shrink: 0; margin-top: 2px" />
<div style="color: #0050B3"> <div style="color: #2d5a3d">
<div class="font-medium mb-1">配置文件说明</div> <div class="font-medium mb-1">配置文件说明</div>
<div class="text-sm"> <div class="text-sm">
配置文件按设备型号绑定适配包含发射参数采集参数网络参数等每个型号可以有1个配置版本 配置文件按设备型号绑定适配包含发射参数采集参数网络参数等每个型号可以有1个配置版本
@ -120,7 +120,7 @@ const getStatusStyle = (status: ConfigFile['status']) => {
<div class="flex items-end"> <div class="flex items-end">
<button <button
class="w-full px-4 py-2 rounded text-white flex items-center justify-center gap-2" class="w-full px-4 py-2 rounded text-white flex items-center justify-center gap-2"
style="background-color: #1890FF" style="background-color: #4a7c59"
> >
<Search :size="16" /> <Search :size="16" />
查询 查询
@ -153,7 +153,7 @@ const getStatusStyle = (status: ConfigFile['status']) => {
class="border-b" class="border-b"
style="border-color: #F0F0F0" style="border-color: #F0F0F0"
> >
<td class="px-6 py-4" style="color: #1890FF">{{ config.configId }}</td> <td class="px-6 py-4" style="color: #4a7c59">{{ config.configId }}</td>
<td class="px-6 py-4">{{ config.model }}</td> <td class="px-6 py-4">{{ config.model }}</td>
<td class="px-6 py-4" style="color: rgba(0, 0, 0, 0.65)">{{ config.version }}</td> <td class="px-6 py-4" style="color: rgba(0, 0, 0, 0.65)">{{ config.version }}</td>
<td class="px-6 py-4" style="color: rgba(0, 0, 0, 0.65)">{{ config.createdTime }}</td> <td class="px-6 py-4" style="color: rgba(0, 0, 0, 0.65)">{{ config.createdTime }}</td>
@ -164,10 +164,10 @@ const getStatusStyle = (status: ConfigFile['status']) => {
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" style="color: #1890FF">详情</button> <button class="text-sm" style="color: #4a7c59">详情</button>
<button class="text-sm" style="color: #1890FF">编辑</button> <button class="text-sm" style="color: #4a7c59">编辑</button>
<button class="text-sm" style="color: #1890FF">下发</button> <button class="text-sm" style="color: #4a7c59">下发</button>
<button class="text-sm" style="color: #1890FF">下载</button> <button class="text-sm" style="color: #4a7c59">下载</button>
<button class="text-sm" style="color: #FF4D4F">删除</button> <button class="text-sm" style="color: #FF4D4F">删除</button>
</div> </div>
</td> </td>
@ -191,7 +191,7 @@ const getStatusStyle = (status: ConfigFile['status']) => {
> >
上一页 上一页
</button> </button>
<button class="px-3 py-1 rounded" style="background-color: #1890FF; color: #fff">1</button> <button class="px-3 py-1 rounded" style="background-color: #4a7c59; color: #fff">1</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">2</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">2</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">3</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">3</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">下一页</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">下一页</button>

View File

@ -2,7 +2,7 @@
<div class="p-6"> <div class="p-6">
<div class="mb-6"> <div class="mb-6">
<h2 class="text-2xl font-semibold mb-1">首页</h2> <h2 class="text-2xl font-semibold mb-1">首页</h2>
<p class="text-sm" style="color: rgba(0, 0, 0, 0.45)">设备管理数据总览</p> <p class="text-sm" style="color: rgba(0, 0, 0, 0.45)">生产管理数据总览</p>
</div> </div>
<!-- Metric Cards (clickable) --> <!-- Metric Cards (clickable) -->
@ -62,11 +62,11 @@
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span v-if="task.time" class="text-xs" style="color: rgba(0, 0, 0, 0.45)">{{ task.time }}</span> <span v-if="task.time" class="text-xs" style="color: rgba(0, 0, 0, 0.45)">{{ task.time }}</span>
<router-link :to="task.link" class="text-sm" style="color: #1890FF">处理</router-link> <router-link :to="task.link" class="text-sm" style="color: #4a7c59">处理</router-link>
</div> </div>
</div> </div>
</div> </div>
<router-link v-if="group.tasks.length < group.count" :to="group.link" class="block w-full mt-3 text-center text-sm" style="color: #1890FF"> <router-link v-if="group.tasks.length < group.count" :to="group.link" class="block w-full mt-3 text-center text-sm" style="color: #4a7c59">
查看全部 {{ group.count }} 查看全部 {{ group.count }}
</router-link> </router-link>
</div> </div>
@ -80,9 +80,9 @@ import { computed } from 'vue'
import { TrendingUp, TrendingDown, Server, Wifi, CheckCircle, PackageCheck, Wrench, Target, Clock, Upload } from 'lucide-vue-next' import { TrendingUp, TrendingDown, Server, Wifi, CheckCircle, PackageCheck, Wrench, Target, Clock, Upload } from 'lucide-vue-next'
const metrics = [ const metrics = [
{ label: '设备总数', value: '5,234', trend: 'up' as const, trendValue: '+5.2%', color: '#1890FF', icon: Server, link: '/devices' }, { label: '设备总数', value: '5,234', trend: 'up' as const, trendValue: '+5.2%', color: '#4a7c59', icon: Server, link: '/devices' },
{ label: '装配中', value: '4,856', trend: 'up' as const, trendValue: '+2.8%', color: '#52C41A', icon: Wifi, link: '/devices' }, { label: '装配中', value: '4,856', trend: 'up' as const, trendValue: '+2.8%', color: '#52C41A', icon: Wifi, link: '/devices' },
{ label: '已激活', value: '4,912', trend: 'up' as const, trendValue: '+1.5%', color: '#1890FF', icon: CheckCircle, link: '/activation' }, { label: '已激活', value: '4,912', trend: 'up' as const, trendValue: '+1.5%', color: '#4a7c59', icon: CheckCircle, link: '/activation' },
{ label: '有新版本', value: '156', color: '#722ED1', icon: PackageCheck, link: '/firmware' }, { label: '有新版本', value: '156', color: '#722ED1', icon: PackageCheck, link: '/firmware' },
{ label: '维修中', value: '23', trend: 'down' as const, trendValue: '-12.3%', color: '#FF4D4F', icon: Wrench, link: '/repair' }, { label: '维修中', value: '23', trend: 'down' as const, trendValue: '-12.3%', color: '#FF4D4F', icon: Wrench, link: '/repair' },
{ label: '报废', value: '56', color: '#FA8C16', icon: Target, link: '/scrap' }, { label: '报废', value: '56', color: '#FA8C16', icon: Target, link: '/scrap' },

View File

@ -129,7 +129,7 @@
</div> </div>
<div> <div>
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">授权文件</div> <div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">授权文件</div>
<button class="text-sm" style="color: #1890FF">auth_gd30_v2.3.lic</button> <button class="text-sm" style="color: #4a7c59">auth_gd30_v2.3.lic</button>
</div> </div>
</div> </div>
</div> </div>
@ -165,7 +165,7 @@
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">维修历史</h3> <h3 class="text-lg font-semibold">维修历史</h3>
<router-link to="/repair" class="text-sm" style="color: #1890FF">查看全部</router-link> <router-link to="/repair" class="text-sm" style="color: #4a7c59">查看全部</router-link>
</div> </div>
<div class="relative"> <div class="relative">
<!-- Timeline line --> <!-- Timeline line -->
@ -179,9 +179,9 @@
<div class="flex flex-col items-center flex-shrink-0"> <div class="flex flex-col items-center flex-shrink-0">
<div <div
class="w-12 h-12 rounded-full flex items-center justify-center relative z-10" class="w-12 h-12 rounded-full flex items-center justify-center relative z-10"
style="background-color: #E6F7FF; border: 2px solid #1890FF" style="background-color: #eef5f0; border: 2px solid #4a7c59"
> >
<Clock :size="20" style="color: #1890FF" /> <Clock :size="20" style="color: #4a7c59" />
</div> </div>
</div> </div>
<div class="flex-1 pt-2"> <div class="flex-1 pt-2">
@ -208,7 +208,7 @@ import { ArrowLeft, StopCircle, Edit2, Download, Clock } from 'lucide-vue-next'
const router = useRouter() const router = useRouter()
const boards = [ const boards = [
{ name: '主板', version: 'MB-V2.3', sn: 'MB20240308001', bg: '#E6F7FF', color: '#1890FF' }, { name: '主板', version: 'MB-V2.3', sn: 'MB20240308001', bg: '#eef5f0', color: '#4a7c59' },
{ name: '采集板', version: 'RX-V1.8', sn: 'RX20240308002', bg: '#F0F5FF', color: '#597EF7' }, { name: '采集板', version: 'RX-V1.8', sn: 'RX20240308002', bg: '#F0F5FF', color: '#597EF7' },
{ name: '发射板', version: 'TX-V1.2', sn: 'TX20240308003', bg: '#FFF7E6', color: '#FA8C16' }, { name: '发射板', version: 'TX-V1.2', sn: 'TX20240308003', bg: '#FFF7E6', color: '#FA8C16' },
{ name: '测控板', version: 'MC-V1.5', sn: 'MC20240308004', bg: '#F6FFED', color: '#52C41A' }, { name: '测控板', version: 'MC-V1.5', sn: 'MC20240308004', bg: '#F6FFED', color: '#52C41A' },

View File

@ -14,7 +14,7 @@
</button> </button>
<button <button
class="px-4 py-2 rounded text-white flex items-center gap-2" class="px-4 py-2 rounded text-white flex items-center gap-2"
style="background-color: #1890FF" style="background-color: #4a7c59"
@click="router.push('/registration')" @click="router.push('/registration')"
> >
<Plus :size="16" /> <Plus :size="16" />
@ -75,7 +75,7 @@
<div class="flex items-end"> <div class="flex items-end">
<button <button
class="w-full px-4 py-2 rounded text-white flex items-center justify-center gap-2" class="w-full px-4 py-2 rounded text-white flex items-center justify-center gap-2"
style="background-color: #1890FF" style="background-color: #4a7c59"
> >
<Search :size="16" /> <Search :size="16" />
搜索 搜索
@ -122,8 +122,8 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-3 pt-4 border-t" style="border-color: #F0F0F0"> <div class="flex items-center gap-3 pt-4 border-t" style="border-color: #F0F0F0">
<button class="text-sm" style="color: #1890FF" @click="router.push('/devices/' + device.sn)">详情</button> <button class="text-sm" style="color: #4a7c59" @click="router.push('/devices/' + device.sn)">详情</button>
<button class="text-sm" style="color: #1890FF">BOM</button> <button class="text-sm" style="color: #4a7c59">BOM</button>
<template v-if="device.status === '已激活'"> <template v-if="device.status === '已激活'">
<span style="color: #D9D9D9">|</span> <span style="color: #D9D9D9">|</span>
<button class="text-sm flex items-center gap-1" style="color: #FF4D4F"> <button class="text-sm flex items-center gap-1" style="color: #FF4D4F">
@ -151,7 +151,7 @@
> >
上一页 上一页
</button> </button>
<button class="px-3 py-1 rounded" style="background-color: #1890FF; color: #fff">1</button> <button class="px-3 py-1 rounded" style="background-color: #4a7c59; color: #fff">1</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">2</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">2</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">3</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">3</button>
<span style="color: rgba(0, 0, 0, 0.45)">...</span> <span style="color: rgba(0, 0, 0, 0.45)">...</span>
@ -212,7 +212,7 @@ function getStatusStyle(status: Device['status']) {
case '已出厂': case '已出厂':
return { backgroundColor: '#FFF7E6', color: '#FA8C16', border: '1px solid #FFD591' } return { backgroundColor: '#FFF7E6', color: '#FA8C16', border: '1px solid #FFD591' }
case '装配中': case '装配中':
return { backgroundColor: '#E6F7FF', color: '#1890FF', border: '1px solid #91D5FF' } return { backgroundColor: '#eef5f0', color: '#4a7c59', border: '1px solid #a3c4ad' }
} }
} }

View File

@ -37,7 +37,7 @@ interface ChecklistData {
const activeTab = ref('GD30') const activeTab = ref('GD30')
const stats = [ const stats = [
{ label: '型号总数', value: '12', icon: Server, color: '#1890FF' }, { label: '型号总数', value: '12', icon: Server, color: '#4a7c59' },
{ label: '在产型号', value: '8', icon: CheckCircle2, color: '#52C41A' }, { label: '在产型号', value: '8', icon: CheckCircle2, color: '#52C41A' },
{ label: '停产型号', value: '3', icon: AlertTriangle, color: '#FAAD14' }, { label: '停产型号', value: '3', icon: AlertTriangle, color: '#FAAD14' },
{ label: '关联设备总数', value: '5,234', icon: Layers, color: '#722ED1' }, { label: '关联设备总数', value: '5,234', icon: Layers, color: '#722ED1' },
@ -183,10 +183,10 @@ const boardVersionData = [
<!-- Info Banner --> <!-- Info Banner -->
<div <div
class="mb-6 p-4 rounded-lg flex items-start gap-3" class="mb-6 p-4 rounded-lg flex items-start gap-3"
:style="{ backgroundColor: '#E6F7FF', border: '1px solid #91D5FF' }" :style="{ backgroundColor: '#eef5f0', border: '1px solid #a3c4ad' }"
> >
<Info :size="20" :style="{ color: '#1890FF', flexShrink: 0, marginTop: '2px' }" /> <Info :size="20" :style="{ color: '#4a7c59', flexShrink: 0, marginTop: '2px' }" />
<div :style="{ color: '#0050B3' }"> <div :style="{ color: '#2d5a3d' }">
型号管理是平台核心枢纽每个型号关联授权文件配置文件和固件版本 型号管理是平台核心枢纽每个型号关联授权文件配置文件和固件版本
</div> </div>
</div> </div>
@ -221,7 +221,7 @@ const boardVersionData = [
<h3 class="text-lg font-semibold">型号列表</h3> <h3 class="text-lg font-semibold">型号列表</h3>
<button <button
class="px-4 py-2 rounded text-white flex items-center gap-2" class="px-4 py-2 rounded text-white flex items-center gap-2"
:style="{ backgroundColor: '#1890FF' }" :style="{ backgroundColor: '#4a7c59' }"
@click="showDrawer = true" @click="showDrawer = true"
> >
<Plus :size="16" /> <Plus :size="16" />
@ -253,13 +253,13 @@ const boardVersionData = [
<td class="px-6 py-4">{{ model.name }}</td> <td class="px-6 py-4">{{ model.name }}</td>
<td class="px-6 py-4" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">{{ model.code }}</td> <td class="px-6 py-4" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">{{ model.code }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<router-link to="/licenses" class="text-sm" style="color: #1890FF">{{ model.authFile }}</router-link> <router-link to="/licenses" class="text-sm" style="color: #4a7c59">{{ model.authFile }}</router-link>
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<router-link to="/config-files" class="text-sm" style="color: #1890FF">{{ model.configFile }}</router-link> <router-link to="/config-files" class="text-sm" style="color: #4a7c59">{{ model.configFile }}</router-link>
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<router-link to="/firmware" class="text-sm" style="color: #1890FF">{{ model.firmwareVersion }}</router-link> <router-link to="/firmware" class="text-sm" style="color: #4a7c59">{{ model.firmwareVersion }}</router-link>
</td> </td>
<td class="px-6 py-4">{{ model.deviceCount.toLocaleString() }}</td> <td class="px-6 py-4">{{ model.deviceCount.toLocaleString() }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
@ -276,10 +276,10 @@ const boardVersionData = [
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" :style="{ color: '#1890FF' }">编辑</button> <button class="text-sm" :style="{ color: '#4a7c59' }">编辑</button>
<router-link to="/licenses" class="text-sm" style="color: #1890FF">授权</router-link> <router-link to="/licenses" class="text-sm" style="color: #4a7c59">授权</router-link>
<router-link to="/config-files" class="text-sm" style="color: #1890FF">配置</router-link> <router-link to="/config-files" class="text-sm" style="color: #4a7c59">配置</router-link>
<router-link to="/firmware" class="text-sm" style="color: #1890FF">固件</router-link> <router-link to="/firmware" class="text-sm" style="color: #4a7c59">固件</router-link>
</div> </div>
</td> </td>
</tr> </tr>
@ -302,8 +302,8 @@ const boardVersionData = [
@click="activeTab = model" @click="activeTab = model"
class="px-6 py-3 text-sm font-medium transition-colors" class="px-6 py-3 text-sm font-medium transition-colors"
:style="{ :style="{
color: activeTab === model ? '#1890FF' : 'rgba(0, 0, 0, 0.65)', color: activeTab === model ? '#4a7c59' : 'rgba(0, 0, 0, 0.65)',
borderBottom: activeTab === model ? '2px solid #1890FF' : 'none', borderBottom: activeTab === model ? '2px solid #4a7c59' : 'none',
marginBottom: activeTab === model ? '-1px' : '0', marginBottom: activeTab === model ? '-1px' : '0',
}" }"
> >
@ -339,7 +339,7 @@ const boardVersionData = [
v-if="editingCell?.model === activeTab && editingCell?.id === item.id && editingCell?.field === 'text'" v-if="editingCell?.model === activeTab && editingCell?.id === item.id && editingCell?.field === 'text'"
autofocus autofocus
class="w-full px-2 py-1 border rounded text-sm" class="w-full px-2 py-1 border rounded text-sm"
:style="{ borderColor: '#1890FF' }" :style="{ borderColor: '#4a7c59' }"
:value="item.text" :value="item.text"
@blur="handleEditBlur($event, activeTab, item.id)" @blur="handleEditBlur($event, activeTab, item.id)"
@keydown="handleEditKeydown" @keydown="handleEditKeydown"
@ -367,14 +367,14 @@ const boardVersionData = [
<button <button
@click="addChecklistItem(activeTab)" @click="addChecklistItem(activeTab)"
class="px-4 py-2 text-sm flex items-center gap-1 rounded transition-colors" class="px-4 py-2 text-sm flex items-center gap-1 rounded transition-colors"
:style="{ color: '#1890FF', border: '1px dashed #1890FF' }" :style="{ color: '#4a7c59', border: '1px dashed #4a7c59' }"
> >
<Plus :size="14" /> <Plus :size="14" />
添加检查项 添加检查项
</button> </button>
<button <button
class="px-6 py-2 text-sm rounded text-white" class="px-6 py-2 text-sm rounded text-white"
:style="{ backgroundColor: '#1890FF' }" :style="{ backgroundColor: '#4a7c59' }"
> >
确认保存 确认保存
</button> </button>
@ -496,11 +496,11 @@ const boardVersionData = [
<label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">状态</label> <label class="block text-sm mb-2" style="color: rgba(0,0,0,0.85)">状态</label>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer text-sm"> <label class="flex items-center gap-2 cursor-pointer text-sm">
<input type="radio" v-model="drawerForm.status" value="在产" style="accent-color: #1890FF" /> <input type="radio" v-model="drawerForm.status" value="在产" style="accent-color: #4a7c59" />
<span>在产</span> <span>在产</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer text-sm"> <label class="flex items-center gap-2 cursor-pointer text-sm">
<input type="radio" v-model="drawerForm.status" value="停产" style="accent-color: #1890FF" /> <input type="radio" v-model="drawerForm.status" value="停产" style="accent-color: #4a7c59" />
<span>停产</span> <span>停产</span>
</label> </label>
</div> </div>
@ -532,7 +532,7 @@ const boardVersionData = [
<button class="px-4 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showDrawer = false">取消</button> <button class="px-4 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showDrawer = false">取消</button>
<button <button
class="px-4 py-2 rounded text-white text-sm" class="px-4 py-2 rounded text-white text-sm"
:style="{ backgroundColor: drawerForm.name && drawerForm.code ? '#1890FF' : '#D9D9D9' }" :style="{ backgroundColor: drawerForm.name && drawerForm.code ? '#4a7c59' : '#D9D9D9' }"
:disabled="!drawerForm.name || !drawerForm.code" :disabled="!drawerForm.name || !drawerForm.code"
@click="showDrawer = false" @click="showDrawer = false"
>确认创建</button> >确认创建</button>

View File

@ -258,7 +258,7 @@ const toggleChecklistItem = (id: number) => {
<td class="px-6 py-4">{{ item.quantity }}</td> <td class="px-6 py-4">{{ item.quantity }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" :style="{ color: '#1890FF' }">编辑</button> <button class="text-sm" :style="{ color: '#4a7c59' }">编辑</button>
<button class="text-sm" :style="{ color: '#FF4D4F' }">删除</button> <button class="text-sm" :style="{ color: '#FF4D4F' }">删除</button>
</div> </div>
</td> </td>
@ -275,7 +275,7 @@ const toggleChecklistItem = (id: number) => {
<h3 class="text-lg font-semibold">装配Checklist</h3> <h3 class="text-lg font-semibold">装配Checklist</h3>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">完成进度</span> <span class="text-sm" :style="{ color: 'rgba(0, 0, 0, 0.65)' }">完成进度</span>
<span class="text-sm font-semibold" :style="{ color: '#1890FF' }"> <span class="text-sm font-semibold" :style="{ color: '#4a7c59' }">
{{ completedCount }}/{{ totalCount }} {{ completedCount }}/{{ totalCount }}
</span> </span>
</div> </div>
@ -327,7 +327,7 @@ const toggleChecklistItem = (id: number) => {
<button <button
v-if="item.needPhoto && item.completed && item.photos > 0" v-if="item.needPhoto && item.completed && item.photos > 0"
class="px-3 py-1 rounded text-xs flex items-center gap-1 cursor-pointer" class="px-3 py-1 rounded text-xs flex items-center gap-1 cursor-pointer"
:style="{ backgroundColor: '#E6F7FF', color: '#1890FF' }" :style="{ backgroundColor: '#eef5f0', color: '#4a7c59' }"
@click="openPhotoDialog(item.id)" @click="openPhotoDialog(item.id)"
> >
<Camera :size="12" /> <Camera :size="12" />
@ -336,7 +336,7 @@ const toggleChecklistItem = (id: number) => {
<button <button
v-if="item.needPhoto && !item.completed" v-if="item.needPhoto && !item.completed"
class="px-3 py-1 rounded text-xs flex items-center gap-1" class="px-3 py-1 rounded text-xs flex items-center gap-1"
:style="{ backgroundColor: '#1890FF', color: '#fff' }" :style="{ backgroundColor: '#4a7c59', color: '#fff' }"
@click="openPhotoDialog(item.id)" @click="openPhotoDialog(item.id)"
> >
<Camera :size="12" /> <Camera :size="12" />
@ -367,7 +367,7 @@ const toggleChecklistItem = (id: number) => {
</button> </button>
<button <button
class="px-6 py-2 rounded text-white" class="px-6 py-2 rounded text-white"
:style="{ backgroundColor: '#1890FF' }" :style="{ backgroundColor: '#4a7c59' }"
> >
提交 提交
</button> </button>
@ -428,7 +428,7 @@ const toggleChecklistItem = (id: number) => {
<button class="px-4 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showPhotoDialog = false">取消</button> <button class="px-4 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showPhotoDialog = false">取消</button>
<button <button
class="px-4 py-2 rounded text-white text-sm" class="px-4 py-2 rounded text-white text-sm"
:style="{ backgroundColor: photoFiles.length > 0 ? '#1890FF' : '#D9D9D9' }" :style="{ backgroundColor: photoFiles.length > 0 ? '#4a7c59' : '#D9D9D9' }"
:disabled="photoFiles.length === 0" :disabled="photoFiles.length === 0"
@click="confirmPhotos" @click="confirmPhotos"
>确认上传{{ photoFiles.length }}</button> >确认上传{{ photoFiles.length }}</button>
@ -460,7 +460,7 @@ const toggleChecklistItem = (id: number) => {
<div class="text-xs" style="color: rgba(0,0,0,0.45)">包含物料编码物料名称SN号规格型号等字段</div> <div class="text-xs" style="color: rgba(0,0,0,0.45)">包含物料编码物料名称SN号规格型号等字段</div>
</div> </div>
</div> </div>
<button class="px-3 py-1.5 rounded text-sm flex items-center gap-1" style="border: 1px solid #1890FF; color: #1890FF"> <button class="px-3 py-1.5 rounded text-sm flex items-center gap-1" style="border: 1px solid #4a7c59; color: #4a7c59">
<Download :size="14" /> <Download :size="14" />
下载模板 下载模板
</button> </button>
@ -503,7 +503,7 @@ const toggleChecklistItem = (id: number) => {
<button class="px-4 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showImportDialog = false; importFile = null">取消</button> <button class="px-4 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="showImportDialog = false; importFile = null">取消</button>
<button <button
class="px-4 py-2 rounded text-white text-sm" class="px-4 py-2 rounded text-white text-sm"
:style="{ backgroundColor: importFile ? '#1890FF' : '#D9D9D9' }" :style="{ backgroundColor: importFile ? '#4a7c59' : '#D9D9D9' }"
:disabled="!importFile" :disabled="!importFile"
@click="showImportDialog = false; importFile = null" @click="showImportDialog = false; importFile = null"
>确认导入</button> >确认导入</button>

View File

@ -65,7 +65,7 @@ const firmwares = ref<Firmware[]>([
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<h2 class="text-2xl font-semibold">固件库</h2> <h2 class="text-2xl font-semibold">固件库</h2>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="px-4 py-2 rounded text-white flex items-center gap-2" style="background-color: #1890FF"> <button class="px-4 py-2 rounded text-white flex items-center gap-2" style="background-color: #4a7c59">
<Upload :size="16" /> <Upload :size="16" />
上传固件 上传固件
</button> </button>
@ -120,18 +120,18 @@ const firmwares = ref<Firmware[]>([
<!-- Actions (expanded) --> <!-- Actions (expanded) -->
<div class="flex items-center gap-6 pt-2"> <div class="flex items-center gap-6 pt-2">
<button class="text-sm" style="color: #1890FF">下载</button> <button class="text-sm" style="color: #4a7c59">下载</button>
<button class="text-sm" style="color: #1890FF">编辑</button> <button class="text-sm" style="color: #4a7c59">编辑</button>
<button class="text-sm" style="color: #1890FF">撤回发布</button> <button class="text-sm" style="color: #4a7c59">撤回发布</button>
<router-link to="/firmware-upgrade" class="text-sm" style="color: #1890FF">查看升级任务</router-link> <router-link to="/firmware-upgrade" class="text-sm" style="color: #4a7c59">查看升级任务</router-link>
</div> </div>
</template> </template>
<!-- Collapsed actions --> <!-- Collapsed actions -->
<template v-else> <template v-else>
<div class="flex items-center gap-6 pt-3"> <div class="flex items-center gap-6 pt-3">
<button class="text-sm" style="color: #1890FF">下载</button> <button class="text-sm" style="color: #4a7c59">下载</button>
<button class="text-sm" style="color: #1890FF" @click="fw.expanded = true">查看详情</button> <button class="text-sm" style="color: #4a7c59" @click="fw.expanded = true">查看详情</button>
</div> </div>
</template> </template>
</div> </div>

View File

@ -73,9 +73,9 @@ const totalAvailable = computed(() => {
</div> </div>
<!-- Info Banner --> <!-- Info Banner -->
<div class="mb-6 p-4 rounded-lg flex items-start gap-3" style="background-color: #E6F7FF; border: 1px solid #91D5FF"> <div class="mb-6 p-4 rounded-lg flex items-start gap-3" style="background-color: #eef5f0; border: 1px solid #a3c4ad">
<Info :size="20" style="color: #1890FF; flex-shrink: 0; margin-top: 2px" /> <Info :size="20" style="color: #4a7c59; flex-shrink: 0; margin-top: 2px" />
<div style="color: #0050B3"> <div style="color: #2d5a3d">
<div class="text-sm"> <div class="text-sm">
不同设备型号支持不同的授权功能模块选择型号后下方表格会显示该型号可用的授权项生成的授权文件将在设备APP激活时自动下载到对应设备 不同设备型号支持不同的授权功能模块选择型号后下方表格会显示该型号可用的授权项生成的授权文件将在设备APP激活时自动下载到对应设备
</div> </div>
@ -118,7 +118,7 @@ const totalAvailable = computed(() => {
<span class="text-sm" style="color: rgba(0,0,0,0.45)">已选 {{ selectedCount }} / {{ totalAvailable }} </span> <span class="text-sm" style="color: rgba(0,0,0,0.45)">已选 {{ selectedCount }} / {{ totalAvailable }} </span>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" style="color: #1890FF" @click="selectAll">全选可用项</button> <button class="text-sm" style="color: #4a7c59" @click="selectAll">全选可用项</button>
<button class="text-sm" style="color: rgba(0,0,0,0.45)" @click="clearAll">清空</button> <button class="text-sm" style="color: rgba(0,0,0,0.45)" @click="clearAll">清空</button>
</div> </div>
</div> </div>
@ -136,7 +136,7 @@ const totalAvailable = computed(() => {
<input <input
type="checkbox" type="checkbox"
class="w-4 h-4" class="w-4 h-4"
style="accent-color: #1890FF" style="accent-color: #4a7c59"
:checked="checkedItems.has(item.name)" :checked="checkedItems.has(item.name)"
:disabled="!item.available" :disabled="!item.available"
@change="toggleItem(item.name)" @change="toggleItem(item.name)"

View File

@ -83,7 +83,7 @@ const getStatusStyle = (s: License['status']) => {
<button class="px-4 py-2 rounded flex items-center gap-2" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)"> <button class="px-4 py-2 rounded flex items-center gap-2" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)">
<Download :size="16" /> 导出 <Download :size="16" /> 导出
</button> </button>
<button class="px-4 py-2 rounded text-white flex items-center gap-2" style="background-color: #1890FF" @click="openDrawer"> <button class="px-4 py-2 rounded text-white flex items-center gap-2" style="background-color: #4a7c59" @click="openDrawer">
<Plus :size="16" /> 选择授权项生成 <Plus :size="16" /> 选择授权项生成
</button> </button>
</div> </div>
@ -116,7 +116,7 @@ const getStatusStyle = (s: License['status']) => {
</select> </select>
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<button class="w-full px-4 py-2 rounded text-white" style="background-color: #1890FF">查询</button> <button class="w-full px-4 py-2 rounded text-white" style="background-color: #4a7c59">查询</button>
</div> </div>
</div> </div>
</div> </div>
@ -138,7 +138,7 @@ const getStatusStyle = (s: License['status']) => {
</thead> </thead>
<tbody> <tbody>
<tr v-for="(lic, i) in licenses" :key="i" class="border-b" style="border-color: #F0F0F0"> <tr v-for="(lic, i) in licenses" :key="i" class="border-b" style="border-color: #F0F0F0">
<td class="px-6 py-4" style="color: #1890FF">{{ lic.licenseId }}</td> <td class="px-6 py-4" style="color: #4a7c59">{{ lic.licenseId }}</td>
<td class="px-6 py-4">{{ lic.model }}</td> <td class="px-6 py-4">{{ lic.model }}</td>
<td class="px-6 py-4 text-sm" style="color: rgba(0,0,0,0.65); max-width: 200px">{{ lic.modules }}</td> <td class="px-6 py-4 text-sm" style="color: rgba(0,0,0,0.65); max-width: 200px">{{ lic.modules }}</td>
<td class="px-6 py-4" style="color: rgba(0,0,0,0.65)">{{ lic.expiry }}</td> <td class="px-6 py-4" style="color: rgba(0,0,0,0.65)">{{ lic.expiry }}</td>
@ -146,8 +146,8 @@ const getStatusStyle = (s: License['status']) => {
<td class="px-6 py-4"><span class="px-2 py-1 rounded text-xs" :style="getStatusStyle(lic.status)">{{ lic.status }}</span></td> <td class="px-6 py-4"><span class="px-2 py-1 rounded text-xs" :style="getStatusStyle(lic.status)">{{ lic.status }}</span></td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" style="color: #1890FF">详情</button> <button class="text-sm" style="color: #4a7c59">详情</button>
<button class="text-sm" style="color: #1890FF">下载</button> <button class="text-sm" style="color: #4a7c59">下载</button>
<button v-if="lic.status === '草稿'" class="text-sm" style="color: #52C41A">发布</button> <button v-if="lic.status === '草稿'" class="text-sm" style="color: #52C41A">发布</button>
<button v-if="lic.status === '已发布'" class="text-sm" style="color: #FF4D4F">停用</button> <button v-if="lic.status === '已发布'" class="text-sm" style="color: #FF4D4F">停用</button>
</div> </div>
@ -163,7 +163,7 @@ const getStatusStyle = (s: License['status']) => {
<div class="text-sm" style="color: rgba(0,0,0,0.65)">显示 1-10 / 156 </div> <div class="text-sm" style="color: rgba(0,0,0,0.65)">显示 1-10 / 156 </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.45)" disabled>上一页</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.45)" disabled>上一页</button>
<button class="px-3 py-1 rounded" style="background-color: #1890FF; color: #fff">1</button> <button class="px-3 py-1 rounded" style="background-color: #4a7c59; color: #fff">1</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.85)">2</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.85)">2</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.85)">3</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.85)">3</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.85)">下一页</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0,0,0,0.85)">下一页</button>
@ -212,7 +212,7 @@ const getStatusStyle = (s: License['status']) => {
<span class="text-xs" style="color: rgba(0,0,0,0.45)">已选 {{ selectedCount }} / {{ totalAvailable }} </span> <span class="text-xs" style="color: rgba(0,0,0,0.45)">已选 {{ selectedCount }} / {{ totalAvailable }} </span>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-xs" style="color: #1890FF" @click="selectAll">全选</button> <button class="text-xs" style="color: #4a7c59" @click="selectAll">全选</button>
<button class="text-xs" style="color: rgba(0,0,0,0.45)" @click="clearAll">清空</button> <button class="text-xs" style="color: rgba(0,0,0,0.45)" @click="clearAll">清空</button>
</div> </div>
</div> </div>
@ -227,7 +227,7 @@ const getStatusStyle = (s: License['status']) => {
<tbody> <tbody>
<tr v-for="(item, i) in availableItems" :key="i" class="border-b" style="border-color: #F0F0F0"> <tr v-for="(item, i) in availableItems" :key="i" class="border-b" style="border-color: #F0F0F0">
<td class="px-3 py-2"> <td class="px-3 py-2">
<input type="checkbox" class="w-3.5 h-3.5" style="accent-color: #1890FF" :checked="checkedItems.has(item.name)" @change="toggleItem(item.name)" /> <input type="checkbox" class="w-3.5 h-3.5" style="accent-color: #4a7c59" :checked="checkedItems.has(item.name)" @change="toggleItem(item.name)" />
</td> </td>
<td class="px-3 py-2">{{ item.name }}</td> <td class="px-3 py-2">{{ item.name }}</td>
</tr> </tr>

View File

@ -82,7 +82,7 @@ const samplingRateOptions = ref([
<div> <div>
<label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">配置文件版本</label> <label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">配置文件版本</label>
<input v-if="isNew" type="text" v-model="configVersion" class="w-full px-3 py-2 border rounded" style="border-color: #D9D9D9" placeholder="如 v1.0.0" /> <input v-if="isNew" type="text" v-model="configVersion" class="w-full px-3 py-2 border rounded" style="border-color: #D9D9D9" placeholder="如 v1.0.0" />
<div v-else style="color: #1890FF">v1.2.0</div> <div v-else style="color: #4a7c59">v1.2.0</div>
</div> </div>
<div> <div>
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">{{ isNew ? '状态' : '最后更新时间' }}</div> <div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">{{ isNew ? '状态' : '最后更新时间' }}</div>
@ -138,9 +138,9 @@ const samplingRateOptions = ref([
:key="option.value" :key="option.value"
class="px-4 py-2 rounded border cursor-pointer transition-colors" class="px-4 py-2 rounded border cursor-pointer transition-colors"
:style="{ :style="{
borderColor: option.checked ? '#1890FF' : '#D9D9D9', borderColor: option.checked ? '#4a7c59' : '#D9D9D9',
backgroundColor: option.checked ? '#E6F7FF' : '#fff', backgroundColor: option.checked ? '#eef5f0' : '#fff',
color: option.checked ? '#1890FF' : 'rgba(0, 0, 0, 0.65)', color: option.checked ? '#4a7c59' : 'rgba(0, 0, 0, 0.65)',
}" }"
> >
<input type="checkbox" class="hidden" :checked="option.checked" readonly /> <input type="checkbox" class="hidden" :checked="option.checked" readonly />
@ -158,9 +158,9 @@ const samplingRateOptions = ref([
:key="option.value" :key="option.value"
class="px-4 py-2 rounded border cursor-pointer transition-colors" class="px-4 py-2 rounded border cursor-pointer transition-colors"
:style="{ :style="{
borderColor: option.checked ? '#1890FF' : '#D9D9D9', borderColor: option.checked ? '#4a7c59' : '#D9D9D9',
backgroundColor: option.checked ? '#E6F7FF' : '#fff', backgroundColor: option.checked ? '#eef5f0' : '#fff',
color: option.checked ? '#1890FF' : 'rgba(0, 0, 0, 0.65)', color: option.checked ? '#4a7c59' : 'rgba(0, 0, 0, 0.65)',
}" }"
> >
<input type="checkbox" class="hidden" :checked="option.checked" readonly /> <input type="checkbox" class="hidden" :checked="option.checked" readonly />
@ -171,7 +171,7 @@ const samplingRateOptions = ref([
<div> <div>
<label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">支持全波形测量</label> <label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">支持全波形测量</label>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="supportFullWaveform" class="w-4 h-4" style="accent-color: #1890FF" /> <input type="checkbox" v-model="supportFullWaveform" class="w-4 h-4" style="accent-color: #4a7c59" />
<span style="color: rgba(0, 0, 0, 0.65)">{{ supportFullWaveform ? '是' : '否' }}</span> <span style="color: rgba(0, 0, 0, 0.65)">{{ supportFullWaveform ? '是' : '否' }}</span>
</label> </label>
</div> </div>
@ -179,11 +179,11 @@ const samplingRateOptions = ref([
<label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">占空比支持</label> <label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">占空比支持</label>
<div class="flex items-center gap-6"> <div class="flex items-center gap-6">
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="supportDuty50" class="w-4 h-4" style="accent-color: #1890FF" /> <input type="checkbox" v-model="supportDuty50" class="w-4 h-4" style="accent-color: #4a7c59" />
<span style="color: rgba(0, 0, 0, 0.65)">50% 占空比</span> <span style="color: rgba(0, 0, 0, 0.65)">50% 占空比</span>
</label> </label>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="supportDuty100" class="w-4 h-4" style="accent-color: #1890FF" /> <input type="checkbox" v-model="supportDuty100" class="w-4 h-4" style="accent-color: #4a7c59" />
<span style="color: rgba(0, 0, 0, 0.65)">100% 占空比</span> <span style="color: rgba(0, 0, 0, 0.65)">100% 占空比</span>
</label> </label>
</div> </div>
@ -203,9 +203,9 @@ const samplingRateOptions = ref([
:key="option.value" :key="option.value"
class="px-4 py-2 rounded border cursor-pointer transition-colors" class="px-4 py-2 rounded border cursor-pointer transition-colors"
:style="{ :style="{
borderColor: option.checked ? '#1890FF' : '#D9D9D9', borderColor: option.checked ? '#4a7c59' : '#D9D9D9',
backgroundColor: option.checked ? '#E6F7FF' : '#fff', backgroundColor: option.checked ? '#eef5f0' : '#fff',
color: option.checked ? '#1890FF' : 'rgba(0, 0, 0, 0.65)', color: option.checked ? '#4a7c59' : 'rgba(0, 0, 0, 0.65)',
}" }"
@click="option.checked = !option.checked" @click="option.checked = !option.checked"
> >
@ -223,9 +223,9 @@ const samplingRateOptions = ref([
:key="option.value" :key="option.value"
class="px-4 py-2 rounded border cursor-pointer transition-colors" class="px-4 py-2 rounded border cursor-pointer transition-colors"
:style="{ :style="{
borderColor: option.checked ? '#1890FF' : '#D9D9D9', borderColor: option.checked ? '#4a7c59' : '#D9D9D9',
backgroundColor: option.checked ? '#E6F7FF' : '#fff', backgroundColor: option.checked ? '#eef5f0' : '#fff',
color: option.checked ? '#1890FF' : 'rgba(0, 0, 0, 0.65)', color: option.checked ? '#4a7c59' : 'rgba(0, 0, 0, 0.65)',
}" }"
> >
<input type="checkbox" class="hidden" :checked="option.checked" readonly /> <input type="checkbox" class="hidden" :checked="option.checked" readonly />
@ -275,7 +275,7 @@ const samplingRateOptions = ref([
<label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">过压保护</label> <label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">过压保护</label>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="overvoltageProtection" class="w-4 h-4" style="accent-color: #1890FF" /> <input type="checkbox" v-model="overvoltageProtection" class="w-4 h-4" style="accent-color: #4a7c59" />
<span style="color: rgba(0, 0, 0, 0.65)">启用</span> <span style="color: rgba(0, 0, 0, 0.65)">启用</span>
</label> </label>
<input v-if="overvoltageProtection" type="number" class="w-32 px-3 py-1 border rounded text-sm" style="border-color: #D9D9D9" v-model="overvoltageThreshold" /> <input v-if="overvoltageProtection" type="number" class="w-32 px-3 py-1 border rounded text-sm" style="border-color: #D9D9D9" v-model="overvoltageThreshold" />
@ -286,7 +286,7 @@ const samplingRateOptions = ref([
<label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">过流保护</label> <label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">过流保护</label>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="overcurrentProtection" class="w-4 h-4" style="accent-color: #1890FF" /> <input type="checkbox" v-model="overcurrentProtection" class="w-4 h-4" style="accent-color: #4a7c59" />
<span style="color: rgba(0, 0, 0, 0.65)">启用</span> <span style="color: rgba(0, 0, 0, 0.65)">启用</span>
</label> </label>
<input v-if="overcurrentProtection" type="number" class="w-32 px-3 py-1 border rounded text-sm" style="border-color: #D9D9D9" v-model="overcurrentThreshold" /> <input v-if="overcurrentProtection" type="number" class="w-32 px-3 py-1 border rounded text-sm" style="border-color: #D9D9D9" v-model="overcurrentThreshold" />
@ -296,7 +296,7 @@ const samplingRateOptions = ref([
<div> <div>
<label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">短路保护</label> <label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">短路保护</label>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="shortCircuitProtection" class="w-4 h-4" style="accent-color: #1890FF" /> <input type="checkbox" v-model="shortCircuitProtection" class="w-4 h-4" style="accent-color: #4a7c59" />
<span style="color: rgba(0, 0, 0, 0.65)">{{ shortCircuitProtection ? '已启用' : '未启用' }}</span> <span style="color: rgba(0, 0, 0, 0.65)">{{ shortCircuitProtection ? '已启用' : '未启用' }}</span>
</label> </label>
</div> </div>
@ -304,7 +304,7 @@ const samplingRateOptions = ref([
<label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">高温保护</label> <label class="block text-sm mb-2" style="color: rgba(0, 0, 0, 0.85)">高温保护</label>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="highTempProtection" class="w-4 h-4" style="accent-color: #1890FF" /> <input type="checkbox" v-model="highTempProtection" class="w-4 h-4" style="accent-color: #4a7c59" />
<span style="color: rgba(0, 0, 0, 0.65)">启用</span> <span style="color: rgba(0, 0, 0, 0.65)">启用</span>
</label> </label>
<input v-if="highTempProtection" type="number" class="w-32 px-3 py-1 border rounded text-sm" style="border-color: #D9D9D9" v-model="highTempThreshold" /> <input v-if="highTempProtection" type="number" class="w-32 px-3 py-1 border rounded text-sm" style="border-color: #D9D9D9" v-model="highTempThreshold" />
@ -419,7 +419,7 @@ const samplingRateOptions = ref([
</button> </button>
<button <button
class="px-6 py-2 rounded text-white" class="px-6 py-2 rounded text-white"
style="background-color: #1890FF" style="background-color: #4a7c59"
> >
下发配置 下发配置
</button> </button>

View File

@ -38,7 +38,7 @@ const processRecords = [
<div>工单号{{ orderId }}</div> <div>工单号{{ orderId }}</div>
<div> <div>
状态 状态
<span class="px-2 py-0.5 rounded text-xs" style="background-color: #E6F7FF; color: #1890FF; border: 1px solid #91D5FF">🔧 处理中</span> <span class="px-2 py-0.5 rounded text-xs" style="background-color: #eef5f0; color: #4a7c59; border: 1px solid #a3c4ad">🔧 处理中</span>
</div> </div>
<div>优先级<span style="color: #FF4D4F"></span></div> <div>优先级<span style="color: #FF4D4F"></span></div>
<div>创建时间2024-02-25 14:20:00</div> <div>创建时间2024-02-25 14:20:00</div>
@ -77,7 +77,7 @@ const processRecords = [
{{ record.time }} {{ record.person }} {{ record.action }} {{ record.time }} {{ record.person }} {{ record.action }}
</div> </div>
</div> </div>
<button class="text-sm mt-3" style="color: #1890FF">添加记录</button> <button class="text-sm mt-3" style="color: #4a7c59">添加记录</button>
</div> </div>
<!-- 板卡更换记录 --> <!-- 板卡更换记录 -->
@ -97,21 +97,21 @@ const processRecords = [
<p class="text-sm mb-4" style="color: rgba(0,0,0,0.65)">板卡更换后需要重新生成授权文件</p> <p class="text-sm mb-4" style="color: rgba(0,0,0,0.65)">板卡更换后需要重新生成授权文件</p>
<div class="space-y-3"> <div class="space-y-3">
<label class="flex items-center gap-2 cursor-pointer text-sm"> <label class="flex items-center gap-2 cursor-pointer text-sm">
<input type="checkbox" v-model="regenerateAuth" class="w-4 h-4" style="accent-color: #1890FF" /> <input type="checkbox" v-model="regenerateAuth" class="w-4 h-4" style="accent-color: #4a7c59" />
重新生成授权文件 重新生成授权文件
</label> </label>
<label class="flex items-center gap-2 cursor-pointer text-sm" style="color: rgba(0,0,0,0.65)"> <label class="flex items-center gap-2 cursor-pointer text-sm" style="color: rgba(0,0,0,0.65)">
<input type="checkbox" v-model="pushFirmware" class="w-4 h-4" style="accent-color: #1890FF" /> <input type="checkbox" v-model="pushFirmware" class="w-4 h-4" style="accent-color: #4a7c59" />
推送适配固件 推送适配固件
</label> </label>
</div> </div>
<button class="mt-4 px-4 py-2 rounded text-white text-sm" style="background-color: #1890FF">处理授权</button> <button class="mt-4 px-4 py-2 rounded text-white text-sm" style="background-color: #4a7c59">处理授权</button>
</div> </div>
<!-- Action Bar --> <!-- Action Bar -->
<div class="flex items-center justify-center gap-4 p-4 bg-white rounded-lg sticky bottom-0" style="box-shadow: 0 -2px 8px rgba(0,0,0,0.05)"> <div class="flex items-center justify-center gap-4 p-4 bg-white rounded-lg sticky bottom-0" style="box-shadow: 0 -2px 8px rgba(0,0,0,0.05)">
<button class="px-6 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="router.go(-1)">取消</button> <button class="px-6 py-2 rounded text-sm" style="border: 1px solid #D9D9D9; color: rgba(0,0,0,0.85)" @click="router.go(-1)">取消</button>
<button class="px-6 py-2 rounded text-white text-sm" style="background-color: #1890FF">关闭工单</button> <button class="px-6 py-2 rounded text-white text-sm" style="background-color: #4a7c59">关闭工单</button>
<button class="px-6 py-2 rounded text-sm" style="border: 1px solid #FF4D4F; color: #FF4D4F" @click="router.push('/scrap')">申请报废</button> <button class="px-6 py-2 rounded text-sm" style="border: 1px solid #FF4D4F; color: #FF4D4F" @click="router.push('/scrap')">申请报废</button>
</div> </div>
</div> </div>

View File

@ -50,9 +50,9 @@ const getStatusStyle = (status: ScrapDevice['status']) => {
} }
case '已审批': case '已审批':
return { return {
backgroundColor: '#E6F7FF', backgroundColor: '#eef5f0',
color: '#1890FF', color: '#4a7c59',
border: '1px solid #91D5FF', border: '1px solid #a3c4ad',
} }
case '待审批': case '待审批':
return { return {
@ -93,10 +93,10 @@ const getStatusStyle = (status: ScrapDevice['status']) => {
<!-- Info Banner --> <!-- Info Banner -->
<div <div
class="mb-6 p-4 rounded-lg flex items-start gap-3" class="mb-6 p-4 rounded-lg flex items-start gap-3"
style="background-color: #E6F7FF; border: 1px solid #91D5FF" style="background-color: #eef5f0; border: 1px solid #a3c4ad"
> >
<Info :size="20" style="color: #1890FF; flex-shrink: 0; margin-top: 2px" /> <Info :size="20" style="color: #4a7c59; flex-shrink: 0; margin-top: 2px" />
<div style="color: #0050B3"> <div style="color: #2d5a3d">
<div class="font-medium mb-1">报废流程说明</div> <div class="font-medium mb-1">报废流程说明</div>
<div class="text-sm"> <div class="text-sm">
报废单由维修工单中申请报废创建关联来源维修工单报废设备需经过审批流程审批通过后进行物料回收和入库 报废单由维修工单中申请报废创建关联来源维修工单报废设备需经过审批流程审批通过后进行物料回收和入库
@ -116,7 +116,7 @@ const getStatusStyle = (status: ScrapDevice['status']) => {
</div> </div>
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">已审批待回收</div> <div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">已审批待回收</div>
<div class="text-3xl font-semibold" style="color: #1890FF">8</div> <div class="text-3xl font-semibold" style="color: #4a7c59">8</div>
</div> </div>
<div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)"> <div class="bg-white p-6 rounded-lg" style="box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)">
<div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">已回收入库</div> <div class="text-sm mb-2" style="color: rgba(0, 0, 0, 0.45)">已回收入库</div>
@ -157,7 +157,7 @@ const getStatusStyle = (status: ScrapDevice['status']) => {
/> />
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<button class="w-full px-4 py-2 rounded text-white" style="background-color: #1890FF"> <button class="w-full px-4 py-2 rounded text-white" style="background-color: #4a7c59">
查询 查询
</button> </button>
</div> </div>
@ -199,15 +199,15 @@ const getStatusStyle = (status: ScrapDevice['status']) => {
</span> </span>
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<router-link :to="'/repair/' + device.sourceOrder" class="text-sm flex items-center gap-1" style="color: #1890FF"> <router-link :to="'/repair/' + device.sourceOrder" class="text-sm flex items-center gap-1" style="color: #4a7c59">
<LinkIcon :size="14" /> <LinkIcon :size="14" />
{{ device.sourceOrder }} {{ device.sourceOrder }}
</router-link> </router-link>
</td> </td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button class="text-sm" style="color: #1890FF">查看详情</button> <button class="text-sm" style="color: #4a7c59">查看详情</button>
<button class="text-sm" style="color: #1890FF">物料检测</button> <button class="text-sm" style="color: #4a7c59">物料检测</button>
<button v-if="device.status === '已审批'" class="text-sm" style="color: #52C41A" @click="router.push('/registration')">回收入库</button> <button v-if="device.status === '已审批'" class="text-sm" style="color: #52C41A" @click="router.push('/registration')">回收入库</button>
</div> </div>
</td> </td>
@ -231,7 +231,7 @@ const getStatusStyle = (status: ScrapDevice['status']) => {
> >
上一页 上一页
</button> </button>
<button class="px-3 py-1 rounded" style="background-color: #1890FF; color: #fff">1</button> <button class="px-3 py-1 rounded" style="background-color: #4a7c59; color: #fff">1</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">2</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">2</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">3</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">3</button>
<button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">下一页</button> <button class="px-3 py-1 rounded border" style="border-color: #D9D9D9; color: rgba(0, 0, 0, 0.85)">下一页</button>

View File

@ -8,13 +8,13 @@
--card-foreground: #000000; --card-foreground: #000000;
/* Enterprise Dashboard Colors */ /* Enterprise Dashboard Colors */
--primary: #1890FF; --primary: #4a7c59;
--success: #52C41A; --success: #52C41A;
--warning: #FAAD14; --warning: #FAAD14;
--error: #FF4D4F; --error: #FF4D4F;
--sidebar-bg: #001529; --sidebar-bg: #1a1a1a;
--sidebar-text: rgba(255, 255, 255, 0.85); --sidebar-text: rgba(255, 255, 255, 0.85);
--sidebar-active: #1890FF; --sidebar-active: #2d5a3d;
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
@ -188,3 +188,58 @@
line-height: 1.5; line-height: 1.5;
} }
} }
/* Global interactive hover effects */
button:not(:disabled),
a[href],
[role="button"],
router-link {
cursor: pointer;
transition: opacity 0.2s, filter 0.2s, transform 0.15s, box-shadow 0.2s;
}
/* Filled buttons (background-color set) */
button:not(:disabled):hover {
filter: brightness(1.08);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
/* Text/link style buttons */
button[style*="color: #4a7c59"]:not(:disabled):hover,
a[style*="color: #4a7c59"]:hover {
opacity: 0.75;
}
button[style*="color: #FF4D4F"]:not(:disabled):hover,
a[style*="color: #FF4D4F"]:hover {
opacity: 0.75;
}
button[style*="color: #52C41A"]:not(:disabled):hover,
a[style*="color: #52C41A"]:hover {
opacity: 0.75;
}
/* Cards and clickable containers */
a.block:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* Disabled state */
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
/* Table row hover */
tbody tr {
transition: background-color 0.15s;
}
tbody tr:hover {
background-color: #FAFAFA;
}
/* Sidebar menu item hover */
aside a:hover {
filter: brightness(1.15);
}