优化
This commit is contained in:
21
monisuo-admin/src/App.vue
Normal file
21
monisuo-admin/src/App.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import Loading from '@/components/loading.vue'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { useSystemTheme } from '@/composables/use-system-theme'
|
||||
|
||||
useSystemTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toaster />
|
||||
|
||||
<Suspense>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<component :is="Component" :key="route" />
|
||||
</router-view>
|
||||
|
||||
<template #fallback>
|
||||
<Loading />
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
118
monisuo-admin/src/assets/chart-theme.css
Normal file
118
monisuo-admin/src/assets/chart-theme.css
Normal file
@@ -0,0 +1,118 @@
|
||||
/* theme red */
|
||||
.theme-red {
|
||||
--chart-1: oklch(0.808 0.114 19.571);
|
||||
--chart-2: oklch(0.637 0.237 25.331);
|
||||
--chart-3: oklch(0.577 0.245 27.325);
|
||||
--chart-4: oklch(0.505 0.213 27.518);
|
||||
--chart-5: oklch(0.444 0.177 26.899);
|
||||
}
|
||||
|
||||
.theme-red.dark {
|
||||
--chart-1: oklch(0.808 0.114 19.571);
|
||||
--chart-2: oklch(0.637 0.237 25.331);
|
||||
--chart-3: oklch(0.577 0.245 27.325);
|
||||
--chart-4: oklch(0.505 0.213 27.518);
|
||||
--chart-5: oklch(0.444 0.177 26.899);
|
||||
}
|
||||
|
||||
/* theme rose */
|
||||
.theme-rose {
|
||||
--chart-1: oklch(0.81 0.117 11.638);
|
||||
--chart-2: oklch(0.645 0.246 16.439);
|
||||
--chart-3: oklch(0.586 0.253 17.585);
|
||||
--chart-4: oklch(0.514 0.222 16.935);
|
||||
--chart-5: oklch(0.455 0.188 13.697);
|
||||
}
|
||||
|
||||
.theme-rose.dark {
|
||||
--chart-1: oklch(0.81 0.117 11.638);
|
||||
--chart-2: oklch(0.645 0.246 16.439);
|
||||
--chart-3: oklch(0.586 0.253 17.585);
|
||||
--chart-4: oklch(0.514 0.222 16.935);
|
||||
--chart-5: oklch(0.455 0.188 13.697);
|
||||
}
|
||||
|
||||
/* theme orange */
|
||||
.theme-orange {
|
||||
--chart-1: oklch(0.837 0.128 66.29);
|
||||
--chart-2: oklch(0.705 0.213 47.604);
|
||||
--chart-3: oklch(0.646 0.222 41.116);
|
||||
--chart-4: oklch(0.553 0.195 38.402);
|
||||
--chart-5: oklch(0.47 0.157 37.304);
|
||||
}
|
||||
|
||||
.theme-orange.dark {
|
||||
--chart-1: oklch(0.837 0.128 66.29);
|
||||
--chart-2: oklch(0.705 0.213 47.604);
|
||||
--chart-3: oklch(0.646 0.222 41.116);
|
||||
--chart-4: oklch(0.553 0.195 38.402);
|
||||
--chart-5: oklch(0.47 0.157 37.304);
|
||||
}
|
||||
|
||||
/* theme green */
|
||||
.theme-green {
|
||||
--chart-1: oklch(0.871 0.15 154.449);
|
||||
--chart-2: oklch(0.723 0.219 149.579);
|
||||
--chart-3: oklch(0.627 0.194 149.214);
|
||||
--chart-4: oklch(0.527 0.154 150.069);
|
||||
--chart-5: oklch(0.448 0.119 151.328);
|
||||
}
|
||||
|
||||
.theme-green.dark {
|
||||
--chart-1: oklch(0.871 0.15 154.449);
|
||||
--chart-2: oklch(0.723 0.219 149.579);
|
||||
--chart-3: oklch(0.627 0.194 149.214);
|
||||
--chart-4: oklch(0.527 0.154 150.069);
|
||||
--chart-5: oklch(0.448 0.119 151.328);
|
||||
}
|
||||
|
||||
/* theme blue */
|
||||
.theme-blue {
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
}
|
||||
|
||||
.theme-blue.dark {
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
}
|
||||
|
||||
/* theme yellow */
|
||||
.theme-yellow {
|
||||
--chart-1: oklch(0.905 0.182 98.111);
|
||||
--chart-2: oklch(0.795 0.184 86.047);
|
||||
--chart-3: oklch(0.681 0.162 75.834);
|
||||
--chart-4: oklch(0.554 0.135 66.442);
|
||||
--chart-5: oklch(0.476 0.114 61.907);
|
||||
}
|
||||
|
||||
.theme-yellow.dark {
|
||||
--chart-1: oklch(0.905 0.182 98.111);
|
||||
--chart-2: oklch(0.795 0.184 86.047);
|
||||
--chart-3: oklch(0.681 0.162 75.834);
|
||||
--chart-4: oklch(0.554 0.135 66.442);
|
||||
--chart-5: oklch(0.476 0.114 61.907);
|
||||
}
|
||||
|
||||
/* theme violet */
|
||||
.theme-violet {
|
||||
--chart-1: oklch(0.811 0.111 293.571);
|
||||
--chart-2: oklch(0.606 0.25 292.717);
|
||||
--chart-3: oklch(0.541 0.281 293.009);
|
||||
--chart-4: oklch(0.491 0.27 292.581);
|
||||
--chart-5: oklch(0.432 0.232 292.759);
|
||||
}
|
||||
|
||||
.theme-violet.dark {
|
||||
--chart-1: oklch(0.811 0.111 293.571);
|
||||
--chart-2: oklch(0.606 0.25 292.717);
|
||||
--chart-3: oklch(0.541 0.281 293.009);
|
||||
--chart-4: oklch(0.491 0.27 292.581);
|
||||
--chart-5: oklch(0.432 0.232 292.759);
|
||||
}
|
||||
4
monisuo-admin/src/assets/icons/arrow-dark.svg
Normal file
4
monisuo-admin/src/assets/icons/arrow-dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="144" height="141" viewBox="0 0 144 141" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M129.189 0.0490494C128.744 0.119441 126.422 0.377545 124.03 0.635648C114.719 1.6446 109.23 2.4893 108.058 3.09936C107.119 3.56864 106.674 4.34295 106.674 5.44576C106.674 6.71281 107.424 7.51058 109.043 7.97986C110.403 8.37875 110.825 8.42567 118.87 9.52847C121.778 9.92736 124.288 10.3028 124.475 10.3732C124.663 10.4436 122.951 11.1006 120.676 11.8749C110.028 15.4414 100.412 20.7677 91.7339 27.9242C88.38 30.7164 81.6957 37.4271 79.2096 40.5009C73.8387 47.2116 69.6874 54.8139 66.5681 63.7302C65.9348 65.4665 65.3484 66.8978 65.2546 66.8978C65.1374 66.8978 63.7771 66.7336 62.2291 66.5693C52.9649 65.5134 43.1847 68.1649 34.1316 74.2186C24.7735 80.46 18.5349 87.7338 10.5371 101.742C2.53943 115.726 -1.0959 127.482 0.287874 135.014C0.89767 138.463 2.0469 140.035 3.97011 140.082C5.28352 140.105 5.37733 139.659 4.20465 139.049C3.05541 138.463 2.6567 137.9 2.32835 136.281C0.616228 128.021 6.24512 113.028 17.4325 96.1104C23.2725 87.241 28.362 81.9147 35.5622 77.1046C43.8649 71.5437 52.7069 69.033 61.1737 69.8308C64.9967 70.1828 64.6917 69.9247 64.1992 72.4822C62.2525 82.5013 63.8005 92.6378 67.9753 97.354C73.1116 103.079 81.9771 102 85.0027 95.2657C86.3395 92.2858 86.3864 87.7103 85.1434 83.9796C83.1498 78.0901 80.007 73.8197 75.4335 70.8163C73.8152 69.7604 70.4848 68.1883 69.875 68.1883C69.359 68.1883 69.4294 67.6487 70.2268 65.3257C72.3377 59.2486 75.457 52.7021 78.4122 48.244C83.2436 40.9232 91.4524 32.5701 99.1687 27.103C105.806 22.4102 113.241 18.5386 120.512 16.0045C123.772 14.8548 129.87 13.1889 130.081 13.3766C130.128 13.447 129.541 14.362 128.791 15.4414C124.78 21.0258 122.716 26.0706 122.388 30.998C122.224 33.7198 122.341 34.588 122.88 34.2595C122.998 34.1891 123.678 32.969 124.405 31.5611C126.281 27.8069 131.722 20.6738 139.579 11.6402C141.127 9.85697 142.652 7.86254 143.027 7.08823C144.552 4.03792 143.52 1.48035 140.377 0.471397C139.439 0.166366 138.102 0.0490408 134.584 0.0255769C132.074 -0.021351 129.635 0.00212153 129.189 0.0490494ZM137.117 4.92955C137.187 5.0234 136.718 5.63346 136.061 6.29045L134.865 7.48712L131.042 6.73627C128.931 6.33739 126.727 5.9385 126.14 5.8681C124.827 5.68039 124.123 5.32843 124.968 5.28151C125.296 5.28151 126.868 5.11725 128.486 4.953C131.3 4.64797 136.812 4.62451 137.117 4.92955ZM71.5168 72.5292C76.2075 74.899 79.4441 78.8175 81.3204 84.355C83.6189 91.1361 81.2266 96.8378 76.0433 96.8847C73.3227 96.9082 70.9773 95.2188 69.5936 92.2389C68.2802 89.4232 67.6938 86.5606 67.5765 82.1259C67.4593 78.3248 67.6 76.4242 68.2333 72.7403L68.4912 71.2856L69.359 71.5906C69.8515 71.7548 70.8132 72.1772 71.5168 72.5292Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
5
monisuo-admin/src/assets/icons/arrow-light.svg
Normal file
5
monisuo-admin/src/assets/icons/arrow-light.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="144" height="141" viewBox="0 0 144 141" fill="white" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M129.189 0.0490494C128.744 0.119441 126.422 0.377545 124.03 0.635648C114.719 1.6446 109.23 2.4893 108.058 3.09936C107.119 3.56864 106.674 4.34295 106.674 5.44576C106.674 6.71281 107.424 7.51058 109.043 7.97986C110.403 8.37875 110.825 8.42567 118.87 9.52847C121.778 9.92736 124.288 10.3028 124.475 10.3732C124.663 10.4436 122.951 11.1006 120.676 11.8749C110.028 15.4414 100.412 20.7677 91.7339 27.9242C88.38 30.7164 81.6957 37.4271 79.2096 40.5009C73.8387 47.2116 69.6874 54.8139 66.5681 63.7302C65.9348 65.4665 65.3484 66.8978 65.2546 66.8978C65.1374 66.8978 63.7771 66.7336 62.2291 66.5693C52.9649 65.5134 43.1847 68.1649 34.1316 74.2186C24.7735 80.46 18.5349 87.7338 10.5371 101.742C2.53943 115.726 -1.0959 127.482 0.287874 135.014C0.89767 138.463 2.0469 140.035 3.97011 140.082C5.28352 140.105 5.37733 139.659 4.20465 139.049C3.05541 138.463 2.6567 137.9 2.32835 136.281C0.616228 128.021 6.24512 113.028 17.4325 96.1104C23.2725 87.241 28.362 81.9147 35.5622 77.1046C43.8649 71.5437 52.7069 69.033 61.1737 69.8308C64.9967 70.1828 64.6917 69.9247 64.1992 72.4822C62.2525 82.5013 63.8005 92.6378 67.9753 97.354C73.1116 103.079 81.9771 102 85.0027 95.2657C86.3395 92.2858 86.3864 87.7103 85.1434 83.9796C83.1498 78.0901 80.007 73.8197 75.4335 70.8163C73.8152 69.7604 70.4848 68.1883 69.875 68.1883C69.359 68.1883 69.4294 67.6487 70.2268 65.3257C72.3377 59.2486 75.457 52.7021 78.4122 48.244C83.2436 40.9232 91.4524 32.5701 99.1687 27.103C105.806 22.4102 113.241 18.5386 120.512 16.0045C123.772 14.8548 129.87 13.1889 130.081 13.3766C130.128 13.447 129.541 14.362 128.791 15.4414C124.78 21.0258 122.716 26.0706 122.388 30.998C122.224 33.7198 122.341 34.588 122.88 34.2595C122.998 34.1891 123.678 32.969 124.405 31.5611C126.281 27.8069 131.722 20.6738 139.579 11.6402C141.127 9.85697 142.652 7.86254 143.027 7.08823C144.552 4.03792 143.52 1.48035 140.377 0.471397C139.439 0.166366 138.102 0.0490408 134.584 0.0255769C132.074 -0.021351 129.635 0.00212153 129.189 0.0490494ZM137.117 4.92955C137.187 5.0234 136.718 5.63346 136.061 6.29045L134.865 7.48712L131.042 6.73627C128.931 6.33739 126.727 5.9385 126.14 5.8681C124.827 5.68039 124.123 5.32843 124.968 5.28151C125.296 5.28151 126.868 5.11725 128.486 4.953C131.3 4.64797 136.812 4.62451 137.117 4.92955ZM71.5168 72.5292C76.2075 74.899 79.4441 78.8175 81.3204 84.355C83.6189 91.1361 81.2266 96.8378 76.0433 96.8847C73.3227 96.9082 70.9773 95.2188 69.5936 92.2389C68.2802 89.4232 67.6938 86.5606 67.5765 82.1259C67.4593 78.3248 67.6 76.4242 68.2333 72.7403L68.4912 71.2856L69.359 71.5906C69.8515 71.7548 70.8132 72.1772 71.5168 72.5292Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
143
monisuo-admin/src/assets/index.css
Normal file
143
monisuo-admin/src/assets/index.css
Normal file
@@ -0,0 +1,143 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(1 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--reka-accordion-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--reka-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
61
monisuo-admin/src/assets/nprogress.css
Normal file
61
monisuo-admin/src/assets/nprogress.css
Normal file
@@ -0,0 +1,61 @@
|
||||
@reference './index.css';
|
||||
|
||||
/* Make clicks pass-through */
|
||||
#nprogress {
|
||||
@apply pointer-events-none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
@apply bg-primary fixed left-0 top-0 z-[1031] h-[2px] w-full;
|
||||
}
|
||||
|
||||
/* Fancy blur effect */
|
||||
#nprogress .peg {
|
||||
@apply absolute right-0 block h-full w-[100px];
|
||||
|
||||
box-shadow:
|
||||
0 0 10px hsl(var(--primary)),
|
||||
0 0 5px hsl(var(--primary));
|
||||
opacity: 1;
|
||||
transform: rotate(3deg) translate(0, -4px);
|
||||
}
|
||||
|
||||
/* Remove these to get rid of the spinner */
|
||||
#nprogress .spinner {
|
||||
@apply fixed right-4 top-4 z-[1031] block;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
@apply border-t-primary border-l-primary size-4 rounded-full border-[2px] border-solid border-transparent;
|
||||
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent {
|
||||
@apply relative overflow-hidden;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent #nprogress .spinner,
|
||||
.nprogress-custom-parent #nprogress .bar {
|
||||
@apply absolute;
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
25
monisuo-admin/src/assets/scrollbar.css
Normal file
25
monisuo-admin/src/assets/scrollbar.css
Normal file
@@ -0,0 +1,25 @@
|
||||
* {
|
||||
scrollbar-color: #8885 var(--c-border);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:horizontal {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--c-border);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #8885;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #8886;
|
||||
}
|
||||
559
monisuo-admin/src/assets/themes.css
Normal file
559
monisuo-admin/src/assets/themes.css
Normal file
@@ -0,0 +1,559 @@
|
||||
/* theme yellow */
|
||||
.theme-yellow {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.795 0.184 86.047);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.795 0.184 86.047);
|
||||
}
|
||||
|
||||
.theme-yellow.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.554 0.135 66.442);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.554 0.135 66.442);
|
||||
}
|
||||
|
||||
/* theme red */
|
||||
.theme-red {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.637 0.237 25.331);
|
||||
--primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.637 0.237 25.331);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.637 0.237 25.331);
|
||||
--sidebar-primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.637 0.237 25.331);
|
||||
}
|
||||
|
||||
.theme-red.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.637 0.237 25.331);
|
||||
--primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.637 0.237 25.331);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.637 0.237 25.331);
|
||||
--sidebar-primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.637 0.237 25.331);
|
||||
}
|
||||
|
||||
/* theme rose */
|
||||
.theme-rose {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.645 0.246 16.439);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.645 0.246 16.439);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.645 0.246 16.439);
|
||||
}
|
||||
|
||||
.theme-rose.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.645 0.246 16.439);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.645 0.246 16.439);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.645 0.246 16.439);
|
||||
}
|
||||
|
||||
/* theme orange */
|
||||
.theme-orange {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.705 0.213 47.604);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.213 47.604);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.705 0.213 47.604);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.213 47.604);
|
||||
}
|
||||
|
||||
.theme-orange.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.646 0.222 41.116);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.646 0.222 41.116);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.646 0.222 41.116);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.646 0.222 41.116);
|
||||
}
|
||||
|
||||
/* theme green */
|
||||
.theme-green {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.723 0.219 149.579);
|
||||
--primary-foreground: oklch(0.982 0.018 155.826);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.723 0.219 149.579);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.723 0.219 149.579);
|
||||
--sidebar-primary-foreground: oklch(0.982 0.018 155.826);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.723 0.219 149.579);
|
||||
}
|
||||
|
||||
.theme-green.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.696 0.17 162.48);
|
||||
--primary-foreground: oklch(0.393 0.095 152.535);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.527 0.154 150.069);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.696 0.17 162.48);
|
||||
--sidebar-primary-foreground: oklch(0.393 0.095 152.535);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.527 0.154 150.069);
|
||||
}
|
||||
|
||||
/* theme blue */
|
||||
.theme-blue {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.623 0.214 259.815);
|
||||
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.623 0.214 259.815);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.623 0.214 259.815);
|
||||
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.623 0.214 259.815);
|
||||
}
|
||||
|
||||
.theme-blue.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.546 0.245 262.881);
|
||||
--primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.488 0.243 264.376);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.546 0.245 262.881);
|
||||
--sidebar-primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.488 0.243 264.376);
|
||||
}
|
||||
|
||||
/* theme yellow */
|
||||
.theme-yellow {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.795 0.184 86.047);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.795 0.184 86.047);
|
||||
}
|
||||
|
||||
.theme-yellow.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.554 0.135 66.442);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.554 0.135 66.442);
|
||||
}
|
||||
|
||||
/* theme violet */
|
||||
.theme-violet {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.606 0.25 292.717);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.606 0.25 292.717);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.606 0.25 292.717);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.606 0.25 292.717);
|
||||
}
|
||||
|
||||
.theme-violet.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.541 0.281 293.009);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.541 0.281 293.009);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.541 0.281 293.009);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.541 0.281 293.009);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
AudioWaveform,
|
||||
Command,
|
||||
GalleryVerticalEnd,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/composables/use-sidebar'
|
||||
|
||||
import type { SidebarData, Team, User } from '../types'
|
||||
|
||||
const user: User = {
|
||||
name: 'shadcn',
|
||||
email: 'm@example.com',
|
||||
avatar: '/avatars/shadcn.jpg',
|
||||
}
|
||||
|
||||
const teams: Team[] = [
|
||||
{
|
||||
name: 'Acme Inc',
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: 'Enterprise',
|
||||
},
|
||||
{
|
||||
name: 'Acme Corp.',
|
||||
logo: AudioWaveform,
|
||||
plan: 'Startup',
|
||||
},
|
||||
{
|
||||
name: 'Evil Corp.',
|
||||
logo: Command,
|
||||
plan: 'Free',
|
||||
},
|
||||
]
|
||||
|
||||
const { navData } = useSidebar()
|
||||
|
||||
export const sidebarData: SidebarData = {
|
||||
user,
|
||||
teams,
|
||||
navMain: navData.value!,
|
||||
}
|
||||
24
monisuo-admin/src/components/app-sidebar/index.vue
Normal file
24
monisuo-admin/src/components/app-sidebar/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { sidebarData } from './data/sidebar-data'
|
||||
import NavFooter from './nav-footer.vue'
|
||||
import NavTeam from './nav-team.vue'
|
||||
import TeamSwitcher from './team-switcher.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebar collapsible="icon" class="z-50">
|
||||
<UiSidebarHeader>
|
||||
<TeamSwitcher :teams="sidebarData.teams" />
|
||||
</UiSidebarHeader>
|
||||
|
||||
<UiSidebarContent>
|
||||
<NavTeam :nav-main="sidebarData.navMain" />
|
||||
</UiSidebarContent>
|
||||
|
||||
<UiSidebarFooter>
|
||||
<NavFooter :user="sidebarData.user" />
|
||||
</UiSidebarFooter>
|
||||
|
||||
<UiSidebarRail />
|
||||
</UiSidebar>
|
||||
</template>
|
||||
108
monisuo-admin/src/components/app-sidebar/nav-footer.vue
Normal file
108
monisuo-admin/src/components/app-sidebar/nav-footer.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
UserRoundCog,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import type { User } from './types'
|
||||
|
||||
const { user } = defineProps<
|
||||
{ user: User }
|
||||
>()
|
||||
|
||||
const { logout } = useAuth()
|
||||
const { isMobile, open } = useSidebar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebarMenu>
|
||||
<UiSidebarMenuItem>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiSidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<UiAvatar class="size-8 rounded-lg">
|
||||
<UiAvatarImage :src="user.avatar" :alt="user.name" />
|
||||
<UiAvatarFallback class="rounded-lg">
|
||||
CN
|
||||
</UiAvatarFallback>
|
||||
</UiAvatar>
|
||||
<div class="grid flex-1 text-sm leading-tight text-left">
|
||||
<span class="font-semibold truncate">{{ user.name }}</span>
|
||||
<span class="text-xs truncate">{{ user.email }}</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto size-4" />
|
||||
</UiSidebarMenuButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent
|
||||
class="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
:side="(isMobile || open) ? 'bottom' : 'right'"
|
||||
align="start"
|
||||
:side-offset="4"
|
||||
>
|
||||
<UiDropdownMenuLabel class="p-0 font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<UiAvatar class="size-8 rounded-lg">
|
||||
<UiAvatarImage :src="user.avatar" :alt="user.name" />
|
||||
<UiAvatarFallback class="rounded-lg">
|
||||
CN
|
||||
</UiAvatarFallback>
|
||||
</UiAvatar>
|
||||
<div class="grid flex-1 text-sm leading-tight text-left">
|
||||
<span class="font-semibold truncate">{{ user.name }}</span>
|
||||
<span class="text-xs truncate">{{ user.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiDropdownMenuLabel>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuGroup>
|
||||
<UiDropdownMenuItem @click="$router.push('/billing/')">
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuGroup>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuGroup>
|
||||
<UiDropdownMenuItem @click="$router.push('/billing?type=billing')">
|
||||
<CreditCard />
|
||||
Billing
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuGroup>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuGroup>
|
||||
<UiDropdownMenuItem @click="$router.push('/settings/')">
|
||||
<UserRoundCog />
|
||||
Profile
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="$router.push('/settings/account')">
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="$router.push('/settings/notifications')">
|
||||
<Bell />
|
||||
Notifications
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuGroup>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem @click="logout">
|
||||
<LogOut />
|
||||
{{ $t('logout') }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</UiSidebarMenuItem>
|
||||
</UiSidebarMenu>
|
||||
</template>
|
||||
91
monisuo-admin/src/components/app-sidebar/nav-team-add.vue
Normal file
91
monisuo-admin/src/components/app-sidebar/nav-team-add.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
|
||||
import { teamAddValidator } from './validators/team.validator'
|
||||
|
||||
const emits = defineEmits(['close'])
|
||||
|
||||
const teamAddFormSchema = toTypedSchema(teamAddValidator)
|
||||
|
||||
const { handleSubmit } = useForm({
|
||||
validationSchema: teamAddFormSchema,
|
||||
initialValues: {},
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit((values) => {
|
||||
toast('You submitted the following values:', {
|
||||
position: 'top-center',
|
||||
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
|
||||
})
|
||||
|
||||
emits('close')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UiDialogHeader>
|
||||
<UiDialogTitle>
|
||||
Add New Team
|
||||
</UiDialogTitle>
|
||||
<UiDialogDescription>
|
||||
Add a new team by your self.
|
||||
</UiDialogDescription>
|
||||
</UiDialogHeader>
|
||||
|
||||
<form class="space-y-4" @submit="onSubmit">
|
||||
<FormField v-slot="{ componentField }" name="name">
|
||||
<FormItem>
|
||||
<FormLabel class="text-base">
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<UiInput v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set the name for the team.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField }" name="slug">
|
||||
<FormItem>
|
||||
<FormLabel class="text-base">
|
||||
Slug
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<UiInput v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set the slug for the team.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField }" name="logo">
|
||||
<FormItem>
|
||||
<FormLabel class="text-base">
|
||||
Logo
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<UiInput v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set the logo of the team.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<div class="flex justify-start mt-4">
|
||||
<UiButton type="submit">
|
||||
Add team
|
||||
</UiButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
108
monisuo-admin/src/components/app-sidebar/nav-team.vue
Normal file
108
monisuo-admin/src/components/app-sidebar/nav-team.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ChevronRight,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import type { NavGroup, NavItem } from './types'
|
||||
|
||||
const { navMain } = defineProps<{
|
||||
navMain: NavGroup[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { state, isMobile } = useSidebar()
|
||||
|
||||
function isCollapsed(menu: NavItem): boolean {
|
||||
const pathname = route.path
|
||||
navMain.forEach((group) => {
|
||||
group.items.forEach((item) => {
|
||||
if (item.url === pathname) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
})
|
||||
return !!menu.items?.some(item => item.url === pathname)
|
||||
}
|
||||
|
||||
function isActive(menu: NavItem): boolean {
|
||||
const pathname = route.path
|
||||
if (menu.url) {
|
||||
return pathname === menu.url
|
||||
}
|
||||
return !!menu.items?.some(item => item.url === pathname)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebarGroup v-for="group in navMain" :key="group.title">
|
||||
<UiSidebarGroupLabel>{{ group.title }}</UiSidebarGroupLabel>
|
||||
<UiSidebarMenu>
|
||||
<template v-for="menu in group.items" :key="menu.title">
|
||||
<UiSidebarMenuItem v-if="!menu.items">
|
||||
<UiSidebarMenuButton as-child :is-active="isActive(menu)" :tooltip="menu.title">
|
||||
<router-link :to="menu.url">
|
||||
<component :is="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
</router-link>
|
||||
</UiSidebarMenuButton>
|
||||
</UiSidebarMenuItem>
|
||||
|
||||
<UiSidebarMenuItem v-else>
|
||||
<!-- sidebar expanded -->
|
||||
<UiCollapsible
|
||||
v-if="state !== 'collapsed' || isMobile"
|
||||
as-child :default-open="isCollapsed(menu)"
|
||||
class="group/collapsible"
|
||||
>
|
||||
<UiSidebarMenuItem>
|
||||
<UiCollapsibleTrigger as-child>
|
||||
<UiSidebarMenuButton :tooltip="menu.title">
|
||||
<component :is="menu.icon" v-if="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</UiSidebarMenuButton>
|
||||
</UiCollapsibleTrigger>
|
||||
</UiSidebarMenuItem>
|
||||
<UiCollapsibleContent>
|
||||
<UiSidebarMenuSub>
|
||||
<UiSidebarMenuSubItem v-for="subItem in menu.items" :key="subItem.title">
|
||||
<UiSidebarMenuSubButton as-child :is-active="isActive(subItem as NavItem)">
|
||||
<router-link :to="subItem?.url || '/'">
|
||||
<component :is="subItem.icon" v-if="subItem.icon" />
|
||||
<span>{{ subItem.title }}</span>
|
||||
</router-link>
|
||||
</UiSidebarMenuSubButton>
|
||||
</UiSidebarMenuSubItem>
|
||||
</UiSidebarMenuSub>
|
||||
</UiCollapsibleContent>
|
||||
</UiCollapsible>
|
||||
|
||||
<!-- sidebar collapsed -->
|
||||
<UiDropdownMenu v-else>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiSidebarMenuButton :tooltip="menu.title">
|
||||
<component :is="menu.icon" v-if="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
</UiSidebarMenuButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="start" side="right">
|
||||
<UiDropdownMenuLabel>{{ menu.title }}</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem v-for="subItem in menu.items" :key="subItem.title" as-child>
|
||||
<router-link :to="subItem?.url || '/'">
|
||||
<component :is="subItem.icon" v-if="subItem.icon" />
|
||||
<span>{{ subItem.title }}</span>
|
||||
</router-link>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</UiSidebarMenuItem>
|
||||
</template>
|
||||
</UiSidebarMenu>
|
||||
</UiSidebarGroup>
|
||||
</template>
|
||||
100
monisuo-admin/src/components/app-sidebar/team-switcher.vue
Normal file
100
monisuo-admin/src/components/app-sidebar/team-switcher.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ChevronsUpDown,
|
||||
Plus,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import type { Team } from './types'
|
||||
|
||||
const { teams } = defineProps<{
|
||||
teams: Team[]
|
||||
}>()
|
||||
|
||||
const { isMobile, open } = useSidebar()
|
||||
|
||||
const activeTeam = ref<Team>(teams[0])
|
||||
function setActiveTeam(team: Team) {
|
||||
activeTeam.value = team
|
||||
}
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const showComponent = shallowRef<Component | null>(null)
|
||||
type TComponent = 'team-add'
|
||||
|
||||
function handleSelect(command: TComponent) {
|
||||
switch (command) {
|
||||
case 'team-add':
|
||||
showComponent.value = defineAsyncComponent(() => import('./nav-team-add.vue'))
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebarMenu>
|
||||
<UiSidebarMenuItem>
|
||||
<UiDialog v-model:open="isOpen">
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiSidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center rounded-lg aspect-square size-8 bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
>
|
||||
<component :is="activeTeam.logo" class="size-4" />
|
||||
</div>
|
||||
<div class="grid flex-1 text-sm leading-tight text-left">
|
||||
<span class="font-semibold truncate">{{ activeTeam.name }}</span>
|
||||
<span class="text-xs truncate">{{ activeTeam.plan }}</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto" />
|
||||
</UiSidebarMenuButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent
|
||||
class="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
align="start"
|
||||
:side="(isMobile || open) ? 'bottom' : 'right'"
|
||||
:side-offset="4"
|
||||
>
|
||||
<UiDropdownMenuLabel class="text-xs text-muted-foreground">
|
||||
Teams
|
||||
</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuItem
|
||||
v-for="(team, index) in teams"
|
||||
:key="team.name"
|
||||
class="gap-2 p-2"
|
||||
@click="setActiveTeam(team)"
|
||||
>
|
||||
<div class="flex items-center justify-center border rounded-sm size-6">
|
||||
<component :is="team.logo" class="size-4 shrink-0" />
|
||||
</div>
|
||||
{{ team.name }}
|
||||
<UiDropdownMenuShortcut>⌘{{ index + 1 }}</UiDropdownMenuShortcut>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuSeparator />
|
||||
|
||||
<UiDialogTrigger as-child>
|
||||
<UiDropdownMenuItem class="gap-2 p-2" @click.stop="handleSelect('team-add')">
|
||||
<div class="flex items-center justify-center border rounded-md size-6 bg-background">
|
||||
<Plus class="size-4" />
|
||||
</div>
|
||||
<div class="font-medium text-muted-foreground">
|
||||
Add team
|
||||
</div>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDialogTrigger>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
|
||||
<UiDialogContent>
|
||||
<component :is="showComponent" @close="isOpen = false" />
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</UiSidebarMenuItem>
|
||||
</UiSidebarMenu>
|
||||
</template>
|
||||
42
monisuo-admin/src/components/app-sidebar/types.ts
Normal file
42
monisuo-admin/src/components/app-sidebar/types.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { LucideProps } from 'lucide-vue-next'
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
|
||||
type NavIcon = FunctionalComponent<LucideProps, Record<any, any>, any, Record<any, any>>
|
||||
|
||||
interface BaseNavItem {
|
||||
title: string
|
||||
icon?: NavIcon
|
||||
}
|
||||
|
||||
export type NavItem
|
||||
= | BaseNavItem & {
|
||||
items: (BaseNavItem & { url?: string })[]
|
||||
url?: never
|
||||
isActive?: boolean
|
||||
} | BaseNavItem & {
|
||||
url: string
|
||||
items?: never
|
||||
}
|
||||
|
||||
export interface NavGroup {
|
||||
title: string
|
||||
items: NavItem[]
|
||||
}
|
||||
|
||||
export interface User {
|
||||
name: string
|
||||
avatar: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
name: string
|
||||
logo: NavIcon
|
||||
plan: string
|
||||
}
|
||||
|
||||
export interface SidebarData {
|
||||
user: User
|
||||
teams: Team[]
|
||||
navMain: NavGroup[]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const teamAddValidator = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, { error: 'Group name is required' })
|
||||
.max(50, { error: 'Group name must be less than 50 characters' }),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1, { error: 'Group name is required' })
|
||||
.max(50, { error: 'Group name must be less than 50 characters' }),
|
||||
logo: z
|
||||
.string()
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type TeamAddValidator = z.infer<typeof teamAddValidator>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { Moon, Sun, SunMoon } from 'lucide-vue-next'
|
||||
|
||||
import CommandItemHasIcon from './command-item-has-icon.vue'
|
||||
|
||||
defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
|
||||
const mode = useColorMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiCommandGroup heading="Theme">
|
||||
<UiCommandItem value="light" @click="mode = 'light', $emit('click')">
|
||||
<CommandItemHasIcon name="Light" :icon="Sun" />
|
||||
</UiCommandItem>
|
||||
<UiCommandItem value="dark" @click="mode = 'dark', $emit('click')">
|
||||
<CommandItemHasIcon name="Dark" :icon="Moon" />
|
||||
</UiCommandItem>
|
||||
<UiCommandItem value="system" @click="mode = 'auto', $emit('click')">
|
||||
<CommandItemHasIcon name="System" :icon="SunMoon" />
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import { Milestone } from 'lucide-vue-next'
|
||||
|
||||
const { icon } = defineProps<{
|
||||
name: string
|
||||
icon?: Component
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<component :is="icon" v-if="icon" class="size-4" />
|
||||
<Milestone v-else class="size-4" />
|
||||
{{ name }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { useSidebar } from '@/composables/use-sidebar'
|
||||
|
||||
import type { NavGroup, NavItem } from '../app-sidebar/types'
|
||||
|
||||
import CommandItemHasIcon from './command-item-has-icon.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
|
||||
const { navData, otherPages } = useSidebar()
|
||||
|
||||
function getFlatNavItems(navData: NavGroup[]): NavItem[] {
|
||||
const flatItems: NavItem[] = []
|
||||
navData.forEach((group) => {
|
||||
group.items.forEach((item) => {
|
||||
if (item.items) {
|
||||
flatItems.push(...getFlatNavItems([item as unknown as NavGroup]))
|
||||
}
|
||||
else {
|
||||
flatItems.push(item)
|
||||
}
|
||||
})
|
||||
})
|
||||
return flatItems
|
||||
}
|
||||
|
||||
const commands = getFlatNavItems([...navData.value!, ...otherPages.value!])
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
function commandItemClick(url: string) {
|
||||
emit('click')
|
||||
if (route.fullPath !== url) {
|
||||
router.push(url)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiCommandGroup heading="Pages">
|
||||
<UiCommandItem
|
||||
v-for="command in commands"
|
||||
:key="command.title"
|
||||
:value="command.title"
|
||||
@click="commandItemClick(command.url!)"
|
||||
>
|
||||
<CommandItemHasIcon :name="command.title" :icon="command.icon" />
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</template>
|
||||
66
monisuo-admin/src/components/command-menu-panel/index.vue
Normal file
66
monisuo-admin/src/components/command-menu-panel/index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { MenuIcon, SearchIcon } from 'lucide-vue-next'
|
||||
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
|
||||
|
||||
import CommandChangeTheme from './command-change-theme.vue'
|
||||
import CommandToPage from './command-to-page.vue'
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
useEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault()
|
||||
handleOpenChange()
|
||||
}
|
||||
})
|
||||
|
||||
function handleOpenChange() {
|
||||
open.value = !open.value
|
||||
}
|
||||
|
||||
const firstKey = computed(() => navigator?.userAgent.includes('Mac OS') ? '⌘' : 'Ctrl')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="text-sm items-center justify-between text-muted-foreground border border-border bg-muted/5 px-4 py-2 rounded-md md:min-w-[220px] cursor-pointer hidden md:flex"
|
||||
@click="handleOpenChange"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<SearchIcon class="size-4" />
|
||||
<span class="text-xs font-semibold text-muted-foreground">{{ $t('homePage.searchKeyWords') }}</span>
|
||||
</div>
|
||||
<UiKbd>{{ firstKey }} + k</UiKbd>
|
||||
</div>
|
||||
|
||||
<UiButton variant="outline" size="icon" class="md:hidden" @click="handleOpenChange">
|
||||
<SearchIcon />
|
||||
</UiButton>
|
||||
|
||||
<UiCommandDialog v-model:open="open">
|
||||
<UiCommandInput placeholder="Type a command or search..." />
|
||||
<UiCommandList>
|
||||
<UiCommandEmpty>
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<MenuIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No menu found.</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Try searching for a command or check the spelling.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
</UiCommandEmpty>
|
||||
|
||||
<CommandToPage @click="handleOpenChange" />
|
||||
<UiCommandSeparator />
|
||||
<CommandChangeTheme @click="handleOpenChange" />
|
||||
</UiCommandList>
|
||||
</UiCommandDialog>
|
||||
</div>
|
||||
</template>
|
||||
71
monisuo-admin/src/components/confirm-dialog.vue
Normal file
71
monisuo-admin/src/components/confirm-dialog.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang='ts' setup>
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isLoading?: boolean
|
||||
disabled?: boolean
|
||||
cancelButtonText?: string
|
||||
confirmButtonText?: string
|
||||
destructive?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
isLoading = false,
|
||||
disabled = false,
|
||||
destructive = false,
|
||||
cancelButtonText = 'Cancel',
|
||||
confirmButtonText = 'Continue',
|
||||
} = defineProps<ConfirmDialogProps>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'confirm'): void
|
||||
}>()
|
||||
|
||||
const openModel = defineModel<boolean>('open', {
|
||||
default: false,
|
||||
})
|
||||
|
||||
function handleConfirm() {
|
||||
emits('confirm')
|
||||
openModel.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialog :open="openModel">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader class="text-start">
|
||||
<AlertDialogTitle>
|
||||
<slot name="title" />
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription as-child>
|
||||
<slot name="description" />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<slot />
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel :disabled="isLoading" @click="openModel = false">
|
||||
{{ cancelButtonText }}
|
||||
</AlertDialogCancel>
|
||||
|
||||
<UiButton
|
||||
:variant="destructive ? 'destructive' : 'default'"
|
||||
:disabled="disabled || isLoading"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ confirmButtonText }}
|
||||
</UiButton>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</template>
|
||||
34
monisuo-admin/src/components/custom-error.vue
Normal file
34
monisuo-admin/src/components/custom-error.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
code: number
|
||||
subtitle: string
|
||||
error: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h1 class="font-bold text-8xl">
|
||||
{{ code }}
|
||||
</h1>
|
||||
<h2 class="mt-4 text-2xl font-bold">
|
||||
{{ subtitle }}
|
||||
</h2>
|
||||
<p class="text-stone-400">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<footer class="mt-8">
|
||||
<slot>
|
||||
<div class="flex justify-center gap-2">
|
||||
<UiButton variant="outline" @click="$router.back()">
|
||||
Go Back
|
||||
</UiButton>
|
||||
<UiButton @click="$router.push('/')">
|
||||
Back to Home
|
||||
</UiButton>
|
||||
</div>
|
||||
</slot>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
30
monisuo-admin/src/components/custom-theme/content-layout.vue
Normal file
30
monisuo-admin/src/components/custom-theme/content-layout.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { CONTENT_LAYOUTS } from '@/constants/themes'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const { setContentLayout } = themeStore
|
||||
const { contentLayout } = storeToRefs(themeStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-1.5 pt-6">
|
||||
<UiLabel for="radius" class="text-xs">
|
||||
Content Layout
|
||||
</UiLabel>
|
||||
<div class="grid grid-cols-2 gap-2 py-1.5">
|
||||
<UiButton
|
||||
v-for="layout in CONTENT_LAYOUTS" :key="layout.label"
|
||||
variant="outline"
|
||||
class="justify-center h-8 px-3"
|
||||
:class="contentLayout === layout.value ? 'border-foreground border-2' : ''"
|
||||
@click="setContentLayout(layout.value)"
|
||||
>
|
||||
<component :is="layout.icon" />
|
||||
{{ layout.label }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
40
monisuo-admin/src/components/custom-theme/custom-color.vue
Normal file
40
monisuo-admin/src/components/custom-theme/custom-color.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { THEME_PRIMARY_COLORS, THEMES } from '@/constants/themes'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const { setTheme } = themeStore
|
||||
const { theme: t } = storeToRefs(themeStore)
|
||||
|
||||
watchEffect(() => {
|
||||
document.documentElement.classList.remove(...THEMES.map(theme => `theme-${theme}`))
|
||||
document.documentElement.classList.add(`theme-${t.value}`)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-1.5 pt-6">
|
||||
<UiLabel for="radius" class="text-xs">
|
||||
Color
|
||||
</UiLabel>
|
||||
<div class="grid grid-cols-2 gap-2 py-1.5">
|
||||
<UiButton
|
||||
v-for="theme in THEME_PRIMARY_COLORS" :key="theme.theme"
|
||||
variant="outline"
|
||||
class="justify-center h-8 px-3"
|
||||
:class="t === theme.theme ? 'border-foreground border-2' : ''"
|
||||
@click="setTheme(theme.theme)"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
'--theme-primary': theme.primaryColor,
|
||||
}"
|
||||
class="size-2 rounded-full bg-(--theme-primary)"
|
||||
/>
|
||||
<span class="text-xs">{{ theme.theme[0].toUpperCase() }}{{ theme.theme.slice(1) }}</span>
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
33
monisuo-admin/src/components/custom-theme/custom-radius.vue
Normal file
33
monisuo-admin/src/components/custom-theme/custom-radius.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { RADIUS } from '@/constants/themes'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const { setRadius } = themeStore
|
||||
const { radius } = storeToRefs(themeStore)
|
||||
|
||||
watchEffect(() => {
|
||||
document.documentElement.style.setProperty('--radius', `${radius.value}rem`)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-1.5 pt-6">
|
||||
<UiLabel for="radius" class="text-xs">
|
||||
Radius
|
||||
</UiLabel>
|
||||
<div class="grid grid-cols-5 gap-2 py-1.5">
|
||||
<UiButton
|
||||
v-for="rayon in RADIUS" :key="rayon"
|
||||
variant="outline"
|
||||
class="justify-center h-8 px-3"
|
||||
:class="rayon === radius ? 'border-foreground border-2' : ''"
|
||||
@click="setRadius(rayon)"
|
||||
>
|
||||
<span class="text-xs">{{ rayon }}</span>
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid space-y-1">
|
||||
<h1 class="font-semibold text-md text-foreground">
|
||||
Customize
|
||||
</h1>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Pick a style and color for your components.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
33
monisuo-admin/src/components/custom-theme/theme-popover.vue
Normal file
33
monisuo-admin/src/components/custom-theme/theme-popover.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { Paintbrush } from 'lucide-vue-next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
|
||||
import ContentLayout from './content-layout.vue'
|
||||
import CustomColor from './custom-color.vue'
|
||||
import CustomRadius from './custom-radius.vue'
|
||||
import CustomThemeTitle from './custom-theme-title.vue'
|
||||
import ToggleColorMode from './toggle-color-mode.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<Paintbrush />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end">
|
||||
<CustomThemeTitle />
|
||||
<CustomColor />
|
||||
<CustomRadius />
|
||||
<ToggleColorMode />
|
||||
<ContentLayout />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BasicColorSchema } from '@vueuse/core'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { Moon, Sun, SunMoon } from 'lucide-vue-next'
|
||||
|
||||
const mode = useColorMode()
|
||||
|
||||
const colorModes: {
|
||||
colorMode: BasicColorSchema
|
||||
icon: Component
|
||||
}[] = [
|
||||
{ colorMode: 'light', icon: Sun },
|
||||
{ colorMode: 'dark', icon: Moon },
|
||||
{ colorMode: 'auto', icon: SunMoon },
|
||||
]
|
||||
|
||||
function setColorMode(colorMode: BasicColorSchema) {
|
||||
mode.value = colorMode
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-1.5 pt-6">
|
||||
<UiLabel for="radius" class="text-xs">
|
||||
Color Mode
|
||||
</UiLabel>
|
||||
<div class="grid grid-cols-3 gap-2 py-1.5">
|
||||
<UiButton
|
||||
v-for="item in colorModes" :key="item.colorMode"
|
||||
variant="outline"
|
||||
class="justify-center items-center h-8 px-3"
|
||||
:class="item.colorMode === mode ? 'border-foreground border-2' : ''"
|
||||
@click="setColorMode(item.colorMode)"
|
||||
>
|
||||
<component :is="item.icon" />
|
||||
<span class="text-xs">{{ item.colorMode }}</span>
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
86
monisuo-admin/src/components/data-table/bulk-actions.vue
Normal file
86
monisuo-admin/src/components/data-table/bulk-actions.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script lang='ts' setup generic="T">
|
||||
import type { Table as VueTable } from '@tanstack/vue-table'
|
||||
|
||||
import { XIcon } from 'lucide-vue-next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface BulkActionsProps<T> {
|
||||
table: VueTable<T>
|
||||
entityName: string
|
||||
}
|
||||
|
||||
const { table, entityName } = defineProps<BulkActionsProps<T>>()
|
||||
|
||||
const selectedRows = computed(() => table.getSelectedRowModel().rows)
|
||||
const selectedCount = computed(() => selectedRows.value.length || 0)
|
||||
|
||||
function handleClearSelection() {
|
||||
table.resetRowSelection()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn(
|
||||
'fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-xl',
|
||||
'transition-all delay-100 duration-300 ease-out hover:scale-105',
|
||||
'focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none',
|
||||
)"
|
||||
>
|
||||
<section
|
||||
v-if="selectedCount" :class="cn(
|
||||
'p-2 shadow-xl',
|
||||
'rounded-xl border',
|
||||
'bg-background/95 supports-backdrop-filter:bg-background/60 backdrop-blur-lg',
|
||||
'flex items-center gap-x-2',
|
||||
)"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="size-6 rounded-full"
|
||||
aria-label="Clear selection"
|
||||
title="Clear selection (Escape)"
|
||||
@click="handleClearSelection"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Clear selection</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Clear selection (Escape)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator
|
||||
class="h-5"
|
||||
orientation="vertical"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<section id="bulk-actions-description" class="flex items-center gap-x-1 text-sm">
|
||||
<UiBadge
|
||||
class="min-w-8 rounded-lg"
|
||||
:aria-label="`${selectedCount} selected`"
|
||||
>
|
||||
{{ selectedCount }}
|
||||
</UiBadge>
|
||||
{{ entityName }} selected
|
||||
</section>
|
||||
|
||||
<Separator
|
||||
class="h-5"
|
||||
orientation="vertical"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
93
monisuo-admin/src/components/data-table/column-header.vue
Normal file
93
monisuo-admin/src/components/data-table/column-header.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { Column } from '@tanstack/vue-table'
|
||||
|
||||
import { ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, ChevronsUpDownIcon, EyeOffIcon, PinIcon, PinOffIcon } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface DataTableColumnHeaderProps {
|
||||
column: Column<T, any>
|
||||
title: string
|
||||
}
|
||||
|
||||
const props = defineProps<DataTableColumnHeaderProps>()
|
||||
|
||||
const canPinned = computed(() => props.column.getCanPin())
|
||||
const canSorted = computed(() => props.column.getCanSort())
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="canSorted || canPinned" :class="cn('flex items-center space-x-2', $attrs.class ?? '')">
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="-ml-3 h-8 data-[state=open]:bg-accent"
|
||||
>
|
||||
<template v-if="canPinned">
|
||||
<PinIcon v-if="props.column.getIsPinned()" class="ml-2 size-4 text-primary" />
|
||||
</template>
|
||||
|
||||
<span>{{ title }}</span>
|
||||
|
||||
<template v-if="canSorted">
|
||||
<ArrowDownIcon v-if="props.column.getIsSorted() === 'desc'" class="ml-2 size-4" />
|
||||
<ArrowUpIcon v-else-if="props.column.getIsSorted() === 'asc'" class="ml-2 size-4" />
|
||||
<ChevronsUpDownIcon v-else class="ml-2 size-4" />
|
||||
</template>
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
|
||||
<UiDropdownMenuContent align="start">
|
||||
<template v-if="canSorted">
|
||||
<UiDropdownMenuItem @click="props.column.toggleSorting(false)">
|
||||
<ArrowUpIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Asc
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="props.column.toggleSorting(true)">
|
||||
<ArrowDownIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Desc
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="props.column.clearSorting()">
|
||||
<ChevronsUpDownIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Clear Sorting
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuSeparator />
|
||||
</template>
|
||||
|
||||
<UiDropdownMenuItem @click="props.column.toggleVisibility(false)">
|
||||
<EyeOffIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Hide
|
||||
</UiDropdownMenuItem>
|
||||
|
||||
<template v-if="canPinned">
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem @click="props.column.pin('left')">
|
||||
<ArrowLeftIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Pin Left
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="props.column.pin('right')">
|
||||
<ArrowRightIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Pin Right
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="props.column.pin(false)">
|
||||
<PinOffIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Unpin
|
||||
</UiDropdownMenuItem>
|
||||
</template>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</div>
|
||||
|
||||
<div v-else :class="$attrs?.class ?? ''">
|
||||
{{ title }}
|
||||
</div>
|
||||
</template>
|
||||
82
monisuo-admin/src/components/data-table/data-table.vue
Normal file
82
monisuo-admin/src/components/data-table/data-table.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { Column, Table as VueTable } from '@tanstack/vue-table'
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
import { FlexRender } from '@tanstack/vue-table'
|
||||
|
||||
import NoResultFound from '@/components/no-result-found.vue'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
|
||||
import type { DataTableProps } from './types'
|
||||
|
||||
import DataTableLoading from './table-loading.vue'
|
||||
import DataTablePagination from './table-pagination.vue'
|
||||
|
||||
defineProps<DataTableProps<T> & {
|
||||
table: VueTable<T>
|
||||
}>()
|
||||
|
||||
function getCommonPinningStyles(column: Column<T>): CSSProperties {
|
||||
const isPinned = column.getIsPinned()
|
||||
return {
|
||||
left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
|
||||
right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
|
||||
position: isPinned ? 'sticky' : 'relative',
|
||||
width: `${column.getSize()}px`,
|
||||
zIndex: isPinned ? 1 : 0,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<slot name="toolbar" />
|
||||
|
||||
<div class="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
||||
<TableHead
|
||||
v-for="header in headerGroup.headers"
|
||||
:key="header.id"
|
||||
:style="getCommonPinningStyles(header.column)"
|
||||
:class="{ 'bg-background': header.column.getIsPinned() }"
|
||||
>
|
||||
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody v-if="!loading">
|
||||
<template v-if="table.getRowModel().rows?.length">
|
||||
<TableRow
|
||||
v-for="row in table.getRowModel().rows"
|
||||
:key="row.id"
|
||||
:data-state="row.getIsSelected() && 'selected'"
|
||||
>
|
||||
<TableCell
|
||||
v-for="cell in row.getVisibleCells()"
|
||||
:key="cell.id"
|
||||
:style="getCommonPinningStyles(cell.column)"
|
||||
:class="{ 'bg-background': cell.column.getIsPinned() }"
|
||||
>
|
||||
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
|
||||
<TableRow v-else>
|
||||
<TableCell
|
||||
:colspan="columns.length"
|
||||
class="h-24 text-center"
|
||||
>
|
||||
<NoResultFound />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DataTableLoading v-if="loading" />
|
||||
</div>
|
||||
|
||||
<DataTablePagination v-if="!loading" :table="table" :server-pagination="serverPagination" />
|
||||
</div>
|
||||
</template>
|
||||
121
monisuo-admin/src/components/data-table/faceted-filter.vue
Normal file
121
monisuo-admin/src/components/data-table/faceted-filter.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { Column } from '@tanstack/vue-table'
|
||||
|
||||
import { Check, CirclePlus } from 'lucide-vue-next'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { FacetedFilterOption } from './types'
|
||||
|
||||
interface DataTableFacetedFilter {
|
||||
column?: Column<T, any>
|
||||
title?: string
|
||||
options: FacetedFilterOption[]
|
||||
}
|
||||
|
||||
const props = defineProps<DataTableFacetedFilter>()
|
||||
|
||||
const facets = computed(() => props.column?.getFacetedUniqueValues())
|
||||
const selectedValues = computed(() => new Set(props.column?.getFilterValue() as string[]))
|
||||
const filterFunction = (list: DataTableFacetedFilter['options'], term: string) => list.filter(i => i.label.toLowerCase()?.includes(term))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover>
|
||||
<UiPopoverTrigger as-child>
|
||||
<UiButton variant="outline" size="sm" class="h-8 border-dashed">
|
||||
<CirclePlus class="size-4 mr-2" />
|
||||
{{ title }}
|
||||
<template v-if="selectedValues.size > 0">
|
||||
<UiSeparator orientation="vertical" class="h-4 mx-2" />
|
||||
<UiBadge
|
||||
variant="secondary"
|
||||
class="px-1 font-normal rounded-sm lg:hidden"
|
||||
>
|
||||
{{ selectedValues.size }}
|
||||
</UiBadge>
|
||||
<div class="hidden space-x-1 lg:flex">
|
||||
<UiBadge
|
||||
v-if="selectedValues.size > 2"
|
||||
variant="secondary"
|
||||
class="px-1 font-normal rounded-sm"
|
||||
>
|
||||
{{ selectedValues.size }} selected
|
||||
</UiBadge>
|
||||
|
||||
<template v-else>
|
||||
<UiBadge
|
||||
v-for="option in options
|
||||
.filter((option) => selectedValues.has(option.value))"
|
||||
:key="option.value"
|
||||
variant="secondary"
|
||||
class="px-1 font-normal rounded-sm"
|
||||
>
|
||||
{{ option.label }}
|
||||
</UiBadge>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</UiButton>
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent class="w-[200px] p-0" align="start">
|
||||
<UiCommand
|
||||
:filter-function="filterFunction as unknown as any"
|
||||
>
|
||||
<UiCommandInput :placeholder="title" />
|
||||
<UiCommandList>
|
||||
<UiCommandEmpty>No results found.</UiCommandEmpty>
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
@select="(_e) => {
|
||||
const isSelected = selectedValues.has(option.value)
|
||||
if (isSelected) {
|
||||
selectedValues.delete(option.value)
|
||||
}
|
||||
else {
|
||||
selectedValues.add(option.value)
|
||||
}
|
||||
const filterValues = Array.from(selectedValues)
|
||||
column?.setFilterValue(
|
||||
filterValues.length ? filterValues : undefined,
|
||||
)
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:class="cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
selectedValues.has(option.value)
|
||||
? 'bg-primary'
|
||||
: 'opacity-50 [&_svg]:invisible',
|
||||
)"
|
||||
>
|
||||
<Check :class="cn('h-4 w-4', selectedValues.has(option.value) ? 'text-primary-foreground' : '')" />
|
||||
</div>
|
||||
<component :is="option.icon" v-if="option.icon" class="size-4 mr-2 text-muted-foreground" />
|
||||
<span>{{ option.label }}</span>
|
||||
<span v-if="facets?.get(option.value)" class="flex items-center justify-center size-4 ml-auto font-mono text-xs">
|
||||
{{ facets.get(option.value) }}
|
||||
</span>
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
|
||||
<template v-if="selectedValues.size > 0">
|
||||
<UiCommandSeparator />
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem
|
||||
:value="{ label: 'Clear filters' }"
|
||||
class="justify-center text-center"
|
||||
@select="column?.setFilterValue(undefined)"
|
||||
>
|
||||
Clear filters
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</template>
|
||||
</UiCommandList>
|
||||
</UiCommand>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</template>
|
||||
10
monisuo-admin/src/components/data-table/index.ts
Normal file
10
monisuo-admin/src/components/data-table/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { default as DataTableBulkActions } from './bulk-actions.vue'
|
||||
export { default as DataTableColumnHeader } from './column-header.vue'
|
||||
export { default as DataTable } from './data-table.vue'
|
||||
export { default as DataTableFacetedFilter } from './faceted-filter.vue'
|
||||
export { RadioSelectColumn, SelectColumn } from './table-columns'
|
||||
export { default as DataTableLoading } from './table-loading.vue'
|
||||
export { default as DataTablePagination } from './table-pagination.vue'
|
||||
export type * from './types'
|
||||
export { useGenerateVueTable } from './use-generate-vue-table'
|
||||
export { default as DataTableViewOptions } from './view-options.vue'
|
||||
35
monisuo-admin/src/components/data-table/radio-cell.vue
Normal file
35
monisuo-admin/src/components/data-table/radio-cell.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { CircleIcon } from 'lucide-vue-next'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineProps<{
|
||||
checked: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [event: MouseEvent]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
:aria-checked="checked"
|
||||
:class="
|
||||
cn(
|
||||
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 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',
|
||||
'hover:border-ring cursor-pointer',
|
||||
)
|
||||
"
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
<span
|
||||
v-if="checked"
|
||||
class="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon class="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
49
monisuo-admin/src/components/data-table/table-columns.ts
Normal file
49
monisuo-admin/src/components/data-table/table-columns.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
import { h } from 'vue'
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
|
||||
import RadioCell from './radio-cell.vue'
|
||||
|
||||
const FIXED_WIDTH_COLUMN = {
|
||||
size: 32,
|
||||
minSize: 32,
|
||||
maxSize: 32,
|
||||
enableResizing: false,
|
||||
} as const
|
||||
|
||||
export const SelectColumn: ColumnDef<any> = {
|
||||
id: 'select',
|
||||
...FIXED_WIDTH_COLUMN,
|
||||
header: ({ table }) => h(Checkbox, {
|
||||
'modelValue': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
|
||||
'onUpdate:modelValue': value => table.toggleAllPageRowsSelected(!!value),
|
||||
'ariaLabel': 'Select all',
|
||||
}),
|
||||
cell: ({ row }) => h(Checkbox, {
|
||||
'modelValue': row.getIsSelected(),
|
||||
'onUpdate:modelValue': value => row.toggleSelected(!!value),
|
||||
'ariaLabel': 'Select row',
|
||||
}),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}
|
||||
|
||||
export const RadioSelectColumn: ColumnDef<any> = {
|
||||
id: 'radio-select',
|
||||
...FIXED_WIDTH_COLUMN,
|
||||
header: () => null,
|
||||
cell: ({ row, table }) => h(RadioCell, {
|
||||
checked: row.getIsSelected(),
|
||||
onClick: (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
// cancel selection of all rows
|
||||
table.toggleAllRowsSelected(false)
|
||||
// select the current row
|
||||
row.toggleSelected(true)
|
||||
},
|
||||
}),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="h-120 w-full flex items-center justify-center">
|
||||
<UiSpinner class="size-10" />
|
||||
</div>
|
||||
</template>
|
||||
167
monisuo-admin/src/components/data-table/table-pagination.vue
Normal file
167
monisuo-admin/src/components/data-table/table-pagination.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
|
||||
import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeft, ChevronsRight } from 'lucide-vue-next'
|
||||
|
||||
import { PAGE_SIZES } from '@/constants/pagination'
|
||||
|
||||
import type { ServerPagination } from './types'
|
||||
|
||||
interface DataTablePaginationProps {
|
||||
table: Table<T>
|
||||
serverPagination?: ServerPagination
|
||||
}
|
||||
const props = defineProps<DataTablePaginationProps>()
|
||||
|
||||
const isServerPagination = computed(() => !!props.serverPagination)
|
||||
|
||||
const currentPage = computed(() => {
|
||||
if (isServerPagination.value && props.serverPagination) {
|
||||
return props.serverPagination.page
|
||||
}
|
||||
return props.table.getState().pagination.pageIndex + 1
|
||||
})
|
||||
|
||||
const currentPageSize = computed(() => {
|
||||
if (isServerPagination.value && props.serverPagination) {
|
||||
return props.serverPagination.pageSize
|
||||
}
|
||||
return props.table.getState().pagination.pageSize
|
||||
})
|
||||
|
||||
const totalPages = computed(() => {
|
||||
if (isServerPagination.value && props.serverPagination) {
|
||||
return Math.ceil(props.serverPagination.total / props.serverPagination.pageSize)
|
||||
}
|
||||
return props.table.getPageCount()
|
||||
})
|
||||
|
||||
const canPreviousPage = computed(() => {
|
||||
if (isServerPagination.value) {
|
||||
return currentPage.value > 1
|
||||
}
|
||||
return props.table.getCanPreviousPage()
|
||||
})
|
||||
|
||||
const canNextPage = computed(() => {
|
||||
if (isServerPagination.value) {
|
||||
return currentPage.value < totalPages.value
|
||||
}
|
||||
return props.table.getCanNextPage()
|
||||
})
|
||||
|
||||
function handlePageSizeChange(value: any) {
|
||||
if (!value)
|
||||
return
|
||||
const newPageSize = Number(value)
|
||||
if (isServerPagination.value && props.serverPagination?.onPageSizeChange) {
|
||||
props.serverPagination.onPageSizeChange(newPageSize)
|
||||
}
|
||||
else {
|
||||
props.table.setPageSize(newPageSize)
|
||||
}
|
||||
}
|
||||
|
||||
function goToFirstPage() {
|
||||
if (isServerPagination.value && props.serverPagination?.onPageChange) {
|
||||
props.serverPagination.onPageChange(1)
|
||||
}
|
||||
else {
|
||||
props.table.setPageIndex(0)
|
||||
}
|
||||
}
|
||||
|
||||
function goToPreviousPage() {
|
||||
if (isServerPagination.value && props.serverPagination?.onPageChange) {
|
||||
props.serverPagination.onPageChange(currentPage.value - 1)
|
||||
}
|
||||
else {
|
||||
props.table.previousPage()
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextPage() {
|
||||
if (isServerPagination.value && props.serverPagination?.onPageChange) {
|
||||
props.serverPagination.onPageChange(currentPage.value + 1)
|
||||
}
|
||||
else {
|
||||
props.table.nextPage()
|
||||
}
|
||||
}
|
||||
|
||||
function goToLastPage() {
|
||||
if (isServerPagination.value && props.serverPagination?.onPageChange) {
|
||||
props.serverPagination.onPageChange(totalPages.value)
|
||||
}
|
||||
else {
|
||||
props.table.setPageIndex(props.table.getPageCount() - 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between px-2 py-2 bg-background">
|
||||
<div class="flex-1" />
|
||||
<div class="flex items-center space-x-6 lg:space-x-8">
|
||||
<div class="flex items-center space-x-2">
|
||||
<p class="hidden text-sm font-medium line-clamp-1 md:block">
|
||||
Rows per page
|
||||
</p>
|
||||
<UiSelect
|
||||
:model-value="`${currentPageSize}`"
|
||||
@update:model-value="handlePageSizeChange"
|
||||
>
|
||||
<UiSelectTrigger class="h-8 w-[70px]">
|
||||
<UiSelectValue :placeholder="`${currentPageSize}`" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent side="top">
|
||||
<UiSelectItem v-for="pageSize in PAGE_SIZES" :key="pageSize" :value="`${pageSize}`">
|
||||
{{ pageSize }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||
Page {{ currentPage }} of {{ totalPages }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="hidden size-8 p-0 lg:flex"
|
||||
:disabled="!canPreviousPage"
|
||||
@click="goToFirstPage"
|
||||
>
|
||||
<span class="sr-only">Go to first page</span>
|
||||
<ChevronsLeft class="size-4" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="size-8 p-0"
|
||||
:disabled="!canPreviousPage"
|
||||
@click="goToPreviousPage"
|
||||
>
|
||||
<span class="sr-only">Go to previous page</span>
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="size-8 p-0"
|
||||
:disabled="!canNextPage"
|
||||
@click="goToNextPage"
|
||||
>
|
||||
<span class="sr-only">Go to next page</span>
|
||||
<ChevronRightIcon class="size-4" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="hidden size-8 p-0 lg:flex"
|
||||
:disabled="!canNextPage"
|
||||
@click="goToLastPage"
|
||||
>
|
||||
<span class="sr-only">Go to last page</span>
|
||||
<ChevronsRight class="size-4" />
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
22
monisuo-admin/src/components/data-table/types.ts
Normal file
22
monisuo-admin/src/components/data-table/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
export interface FacetedFilterOption {
|
||||
label: string
|
||||
value: string
|
||||
icon?: Component
|
||||
}
|
||||
|
||||
export interface ServerPagination {
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
onPageChange: (page: number) => void
|
||||
onPageSizeChange: (pageSize: number) => void
|
||||
}
|
||||
|
||||
export interface DataTableProps<T> {
|
||||
loading?: boolean
|
||||
columns: ColumnDef<T, any>[]
|
||||
data: T[]
|
||||
serverPagination?: ServerPagination
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { ColumnFiltersState, ColumnPinningState, PaginationState, SortingState, TableOptionsWithReactiveData, VisibilityState } from '@tanstack/vue-table'
|
||||
|
||||
import { getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useVueTable } from '@tanstack/vue-table'
|
||||
|
||||
import { DEFAULT_PAGE_SIZE } from '@/constants/pagination'
|
||||
import { valueUpdater } from '@/lib/utils'
|
||||
|
||||
import type { DataTableProps } from './types'
|
||||
|
||||
export function useGenerateVueTable<T>(props: DataTableProps<T>) {
|
||||
const sorting = ref<SortingState>([])
|
||||
const columnFilters = ref<ColumnFiltersState>([])
|
||||
const columnVisibility = ref<VisibilityState>({})
|
||||
const columnPinning = ref<ColumnPinningState>({ left: [], right: [] })
|
||||
const rowSelection = ref({})
|
||||
const pagination = ref<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
})
|
||||
|
||||
const useServerPagination = !!props.serverPagination
|
||||
|
||||
const pageIndex = computed(() => {
|
||||
if (useServerPagination && props.serverPagination) {
|
||||
return props.serverPagination.page - 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const pageSize = computed(() => {
|
||||
if (useServerPagination && props.serverPagination) {
|
||||
return props.serverPagination.pageSize
|
||||
}
|
||||
return DEFAULT_PAGE_SIZE
|
||||
})
|
||||
|
||||
const pageCount = computed(() => {
|
||||
if (useServerPagination && props.serverPagination) {
|
||||
return Math.ceil(props.serverPagination.total / props.serverPagination.pageSize)
|
||||
}
|
||||
return -1
|
||||
})
|
||||
|
||||
const tableConfig: TableOptionsWithReactiveData<T> = {
|
||||
get data() { return props.data },
|
||||
get columns() { return props.columns },
|
||||
state: {
|
||||
get sorting() { return sorting.value },
|
||||
get columnFilters() { return columnFilters.value },
|
||||
get columnVisibility() { return columnVisibility.value },
|
||||
get columnPinning() { return columnPinning.value },
|
||||
get rowSelection() { return rowSelection.value },
|
||||
get pagination() {
|
||||
if (useServerPagination) {
|
||||
return {
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize.value,
|
||||
}
|
||||
}
|
||||
return pagination.value
|
||||
},
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
|
||||
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
|
||||
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
|
||||
onColumnPinningChange: updaterOrValue => valueUpdater(updaterOrValue, columnPinning),
|
||||
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
|
||||
onPaginationChange: updaterOrValue => valueUpdater(updaterOrValue, pagination),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
}
|
||||
|
||||
if (useServerPagination) {
|
||||
tableConfig.pageCount = pageCount.value
|
||||
tableConfig.manualPagination = true
|
||||
}
|
||||
else {
|
||||
tableConfig.getPaginationRowModel = getPaginationRowModel()
|
||||
}
|
||||
|
||||
const table = useVueTable<T>(tableConfig)
|
||||
|
||||
return table
|
||||
}
|
||||
59
monisuo-admin/src/components/data-table/view-options.vue
Normal file
59
monisuo-admin/src/components/data-table/view-options.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
|
||||
import { RefreshCcw, Settings2 } from 'lucide-vue-next'
|
||||
|
||||
interface DataTableViewOptionsProps {
|
||||
table: Table<T>
|
||||
}
|
||||
|
||||
const props = defineProps<DataTableViewOptionsProps>()
|
||||
|
||||
const columns = computed(() => props.table.getAllColumns()
|
||||
.filter(
|
||||
column =>
|
||||
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
|
||||
))
|
||||
|
||||
function resetColumnVisible() {
|
||||
columns.value.forEach(column => column.toggleVisibility(true))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="hidden h-8 ml-auto lg:flex"
|
||||
>
|
||||
<Settings2 class="size-4 mr-2" />
|
||||
Columns View
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="end" class="w-[150px]">
|
||||
<UiDropdownMenuLabel>Toggle columns</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
|
||||
<UiDropdownMenuCheckboxItem
|
||||
v-for="column in columns"
|
||||
:key="column.id"
|
||||
class="capitalize"
|
||||
:model-value="column.getIsVisible()"
|
||||
@update:model-value="(value:boolean) => column.toggleVisibility(!!value)"
|
||||
>
|
||||
{{ column.id }}
|
||||
</UiDropdownMenuCheckboxItem>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem
|
||||
class="capitalize"
|
||||
@click="resetColumnVisible"
|
||||
>
|
||||
<RefreshCcw />
|
||||
Reset
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
29
monisuo-admin/src/components/global-layout/basic-header.vue
Normal file
29
monisuo-admin/src/components/global-layout/basic-header.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { LayoutHeaderProps } from './types'
|
||||
|
||||
defineProps<LayoutHeaderProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
:class="cn(
|
||||
'flex flex-col md:flex-row gap-2 justify-between py-2',
|
||||
sticky ? 'sticky top-0 z-40 bg-background' : '',
|
||||
)"
|
||||
>
|
||||
<main>
|
||||
<h1 class="text-2xl font-bold">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="description" class="text-muted-foreground">
|
||||
{{ description }}
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<aside class="flex items-center gap-2 flex-wrap">
|
||||
<slot name="actions" />
|
||||
</aside>
|
||||
</header>
|
||||
</template>
|
||||
25
monisuo-admin/src/components/global-layout/basic-page.vue
Normal file
25
monisuo-admin/src/components/global-layout/basic-page.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LayoutHeaderProps } from './types'
|
||||
|
||||
import BasicHeader from './basic-header.vue'
|
||||
|
||||
defineProps<LayoutHeaderProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<BasicHeader
|
||||
:title="title"
|
||||
:description="description"
|
||||
:sticky="sticky"
|
||||
>
|
||||
<template #actions>
|
||||
<slot name="actions" />
|
||||
</template>
|
||||
</BasicHeader>
|
||||
|
||||
<main class="py-4">
|
||||
<slot />
|
||||
</main>
|
||||
</main>
|
||||
</template>
|
||||
6
monisuo-admin/src/components/global-layout/index.ts
Normal file
6
monisuo-admin/src/components/global-layout/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as BasicHeader } from './basic-header.vue'
|
||||
export { default as BasicPage } from './basic-page.vue'
|
||||
export { default as TwoColAside } from './two-col-aside.vue'
|
||||
export { default as TwoColLayout } from './two-col.vue'
|
||||
|
||||
export type * from './types'
|
||||
48
monisuo-admin/src/components/global-layout/two-col-aside.vue
Normal file
48
monisuo-admin/src/components/global-layout/two-col-aside.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronsUpDownIcon } from 'lucide-vue-next'
|
||||
|
||||
import type { TwoColAsideNavItem } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
nav: TwoColAsideNavItem[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const currentPath = computed(() => route.path)
|
||||
const activeClass = 'text-primary font-semibold bg-primary/5'
|
||||
|
||||
const currentLink = computed(() => props.nav.find(link => link.url === currentPath.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="flex flex-col gap-2">
|
||||
<router-link
|
||||
v-for="link in props.nav" :key="link.url"
|
||||
:to="link.url"
|
||||
class="items-center hidden px-2 py-1 rounded-md lg:flex hover:bg-primary/5"
|
||||
:class="link.url === currentPath ? activeClass : ''"
|
||||
>
|
||||
<component :is="link.icon" class="size-4 mr-1" />
|
||||
<span>{{ link.title }}</span>
|
||||
</router-link>
|
||||
|
||||
<UiDropdownMenu class="lg:hidden">
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton variant="outline" class="w-48 lg:hidden">
|
||||
<component :is="currentLink?.icon" class="size-4 mr-1" />
|
||||
<span>{{ currentLink?.title }}</span>
|
||||
<ChevronsUpDownIcon class="size-4 ml-auto" />
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent class="w-48" align="start">
|
||||
<UiDropdownMenuItem
|
||||
v-for="link in props.nav" :key="link.url"
|
||||
@click="$router.push(link.url)"
|
||||
>
|
||||
<component :is="link.icon" class="size-4 mr-1" />
|
||||
{{ link.title }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</nav>
|
||||
</template>
|
||||
19
monisuo-admin/src/components/global-layout/two-col.vue
Normal file
19
monisuo-admin/src/components/global-layout/two-col.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang='ts' setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn(
|
||||
`grid grid-cols-1 lg:grid-cols-[200px_1fr] gap-4 w-full`,
|
||||
)"
|
||||
>
|
||||
<aside>
|
||||
<slot name="aside" />
|
||||
</aside>
|
||||
|
||||
<section>
|
||||
<slot name="default" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
13
monisuo-admin/src/components/global-layout/types.ts
Normal file
13
monisuo-admin/src/components/global-layout/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Component } from 'vue'
|
||||
|
||||
export interface LayoutHeaderProps {
|
||||
title: string
|
||||
description: string
|
||||
sticky?: boolean
|
||||
}
|
||||
|
||||
export interface TwoColAsideNavItem {
|
||||
title: string
|
||||
url: string
|
||||
icon?: Component
|
||||
}
|
||||
185
monisuo-admin/src/components/inspira-ui/flickering-grid.vue
Normal file
185
monisuo-admin/src/components/inspira-ui/flickering-grid.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface FlickeringGridProps {
|
||||
squareSize?: number
|
||||
gridGap?: number
|
||||
flickerChance?: number
|
||||
color?: string
|
||||
width?: number
|
||||
height?: number
|
||||
class?: string
|
||||
maxOpacity?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FlickeringGridProps>(), {
|
||||
squareSize: 4,
|
||||
gridGap: 6,
|
||||
flickerChance: 0.3,
|
||||
color: 'rgb(0, 0, 0)',
|
||||
maxOpacity: 0.3,
|
||||
})
|
||||
|
||||
const { squareSize, gridGap, flickerChance, color, maxOpacity, width, height } = toRefs(props)
|
||||
|
||||
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
|
||||
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
|
||||
const context = ref<CanvasRenderingContext2D>()
|
||||
|
||||
const isInView = ref(false)
|
||||
const canvasSize = ref({ width: 0, height: 0 })
|
||||
|
||||
const hexColorRegex = /^#/
|
||||
const computedColor = computed(() => {
|
||||
if (!context.value)
|
||||
return 'rgba(255, 0, 0,'
|
||||
|
||||
const hex = color.value.replace(hexColorRegex, '')
|
||||
const bigint = Number.parseInt(hex, 16)
|
||||
const r = (bigint >> 16) & 255
|
||||
const g = (bigint >> 8) & 255
|
||||
const b = bigint & 255
|
||||
return `rgba(${r}, ${g}, ${b},`
|
||||
})
|
||||
|
||||
function setupCanvas(
|
||||
canvas: HTMLCanvasElement,
|
||||
width: number,
|
||||
height: number,
|
||||
): {
|
||||
cols: number
|
||||
rows: number
|
||||
squares: Float32Array
|
||||
dpr: number
|
||||
} {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
|
||||
const cols = Math.floor(width / (squareSize.value + gridGap.value))
|
||||
const rows = Math.floor(height / (squareSize.value + gridGap.value))
|
||||
|
||||
const squares = new Float32Array(cols * rows)
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
squares[i] = Math.random() * maxOpacity.value
|
||||
}
|
||||
return { cols, rows, squares, dpr }
|
||||
}
|
||||
|
||||
function updateSquares(squares: Float32Array, deltaTime: number) {
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
if (Math.random() < flickerChance.value * deltaTime) {
|
||||
squares[i] = Math.random() * maxOpacity.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawGrid(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cols: number,
|
||||
rows: number,
|
||||
squares: Float32Array,
|
||||
dpr: number,
|
||||
) {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.fillStyle = 'transparent'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const opacity = squares[i * rows + j]
|
||||
ctx.fillStyle = `${computedColor.value}${opacity})`
|
||||
ctx.fillRect(
|
||||
i * (squareSize.value + gridGap.value) * dpr,
|
||||
j * (squareSize.value + gridGap.value) * dpr,
|
||||
squareSize.value * dpr,
|
||||
squareSize.value * dpr,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const gridParams = ref<ReturnType<typeof setupCanvas>>()
|
||||
|
||||
function updateCanvasSize() {
|
||||
const newWidth = width.value || containerRef.value!.clientWidth
|
||||
const newHeight = height.value || containerRef.value!.clientHeight
|
||||
|
||||
canvasSize.value = { width: newWidth, height: newHeight }
|
||||
gridParams.value = setupCanvas(canvasRef.value!, newWidth, newHeight)
|
||||
}
|
||||
|
||||
let animationFrameId: number | undefined
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let intersectionObserver: IntersectionObserver | undefined
|
||||
let lastTime = 0
|
||||
|
||||
function animate(time: number) {
|
||||
if (!isInView.value)
|
||||
return
|
||||
|
||||
const deltaTime = (time - lastTime) / 1000
|
||||
lastTime = time
|
||||
|
||||
updateSquares(gridParams.value!.squares, deltaTime)
|
||||
drawGrid(
|
||||
context.value!,
|
||||
canvasRef.value!.width,
|
||||
canvasRef.value!.height,
|
||||
gridParams.value!.cols,
|
||||
gridParams.value!.rows,
|
||||
gridParams.value!.squares,
|
||||
gridParams.value!.dpr,
|
||||
)
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!canvasRef.value || !containerRef.value)
|
||||
return
|
||||
context.value = canvasRef.value.getContext('2d')!
|
||||
if (!context.value)
|
||||
return
|
||||
|
||||
updateCanvasSize()
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
updateCanvasSize()
|
||||
})
|
||||
intersectionObserver = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
isInView.value = entry.isIntersecting
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
},
|
||||
{ threshold: 0 },
|
||||
)
|
||||
|
||||
resizeObserver.observe(containerRef.value)
|
||||
intersectionObserver.observe(canvasRef.value)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
}
|
||||
resizeObserver?.disconnect()
|
||||
intersectionObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:class="cn('w-full h-full', props.class)"
|
||||
>
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="pointer-events-none"
|
||||
:width="canvasSize.width"
|
||||
:height="canvasSize.height"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
197
monisuo-admin/src/components/inspira-ui/glowing-effect.vue
Normal file
197
monisuo-admin/src/components/inspira-ui/glowing-effect.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { animate } from 'motion-v'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props {
|
||||
blur?: number
|
||||
inactiveZone?: number
|
||||
proximity?: number
|
||||
spread?: number
|
||||
variant?: 'default' | 'white'
|
||||
glow?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
disabled?: boolean
|
||||
movementDuration?: number
|
||||
borderWidth?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
blur: 0,
|
||||
inactiveZone: 0.7,
|
||||
proximity: 0,
|
||||
spread: 20,
|
||||
variant: 'default',
|
||||
glow: false,
|
||||
movementDuration: 2,
|
||||
borderWidth: 1,
|
||||
disabled: true,
|
||||
})
|
||||
|
||||
const containerRef = useTemplateRef('containerRef')
|
||||
const lastPosition = ref({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const animationFrame = ref(0)
|
||||
|
||||
const containerStyles = computed(() => {
|
||||
return {
|
||||
'--blur': `${props.blur}px`,
|
||||
'--spread': props.spread,
|
||||
'--start': '0',
|
||||
'--active': '0',
|
||||
'--glowingeffect-border-width': `${props.borderWidth}px`,
|
||||
'--repeating-conic-gradient-times': '5',
|
||||
'--gradient':
|
||||
props.variant === 'white'
|
||||
? `repeating-conic-gradient(
|
||||
from 236.84deg at 50% 50%,
|
||||
var(--black),
|
||||
var(--black) calc(25% / var(--repeating-conic-gradient-times))
|
||||
)`
|
||||
: `radial-gradient(circle, #dd7bbb 10%, #dd7bbb00 20%),
|
||||
radial-gradient(circle at 40% 40%, #d79f1e 5%, #d79f1e00 15%),
|
||||
radial-gradient(circle at 60% 60%, #5a922c 10%, #5a922c00 20%),
|
||||
radial-gradient(circle at 40% 60%, #4c7894 10%, #4c789400 20%),
|
||||
repeating-conic-gradient(
|
||||
from 236.84deg at 50% 50%,
|
||||
#dd7bbb 0%,
|
||||
#d79f1e calc(25% / var(--repeating-conic-gradient-times)),
|
||||
#5a922c calc(50% / var(--repeating-conic-gradient-times)),
|
||||
#4c7894 calc(75% / var(--repeating-conic-gradient-times)),
|
||||
#dd7bbb calc(100% / var(--repeating-conic-gradient-times))
|
||||
)`,
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.disabled)
|
||||
return
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
document.body.addEventListener('pointermove', handlePointerMove, {
|
||||
passive: true,
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationFrame.value) {
|
||||
cancelAnimationFrame(animationFrame.value)
|
||||
}
|
||||
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
document.body.removeEventListener('pointermove', handlePointerMove)
|
||||
})
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
handleMove(e)
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
handleMove()
|
||||
}
|
||||
|
||||
function handleMove(e?: MouseEvent | PointerEvent | { x: number, y: number }) {
|
||||
if (!containerRef.value)
|
||||
return
|
||||
|
||||
if (animationFrame.value) {
|
||||
cancelAnimationFrame(animationFrame.value)
|
||||
}
|
||||
|
||||
animationFrame.value = requestAnimationFrame(() => {
|
||||
const element = containerRef.value
|
||||
|
||||
if (!element)
|
||||
return
|
||||
|
||||
const { left, top, width, height } = element.getBoundingClientRect()
|
||||
|
||||
const mouseX = e?.x ?? lastPosition.value.x
|
||||
const mouseY = e?.y ?? lastPosition.value.y
|
||||
|
||||
if (e) {
|
||||
lastPosition.value = { x: mouseX, y: mouseY }
|
||||
}
|
||||
|
||||
const center = [left + width * 0.5, top + height * 0.5]
|
||||
const distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1])
|
||||
const inactiveRadius = 0.5 * Math.min(width, height) * props.inactiveZone
|
||||
|
||||
if (distanceFromCenter < inactiveRadius) {
|
||||
element.style.setProperty('--active', '0')
|
||||
return
|
||||
}
|
||||
|
||||
const isActive
|
||||
= mouseX > left - props.proximity
|
||||
&& mouseX < left + width + props.proximity
|
||||
&& mouseY > top - props.proximity
|
||||
&& mouseY < top + height + props.proximity
|
||||
|
||||
element.style.setProperty('--active', isActive ? '1' : '0')
|
||||
|
||||
if (!isActive)
|
||||
return
|
||||
|
||||
const currentAngle = Number.parseFloat(element.style.getPropertyValue('--start')) || 0
|
||||
const targetAngle = (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90
|
||||
|
||||
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180
|
||||
const newAngle = currentAngle + angleDiff
|
||||
|
||||
animate(currentAngle, newAngle, {
|
||||
duration: props.movementDuration,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
onUpdate: (value) => {
|
||||
element.style.setProperty('--start', String(value))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity',
|
||||
glow && 'opacity-100',
|
||||
variant === 'white' && 'border-white',
|
||||
disabled && 'block!',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:style="containerStyles"
|
||||
:class="
|
||||
cn(
|
||||
'pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity',
|
||||
glow && 'opacity-100',
|
||||
blur > 0 && 'blur-(--blur)',
|
||||
props.class,
|
||||
disabled && 'hidden!',
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'glow',
|
||||
'rounded-[inherit]',
|
||||
`after:content-[''] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]`,
|
||||
'after:[border:var(--glowingeffect-border-width)_solid_transparent]',
|
||||
'after:[background:var(--gradient)] after:bg-fixed',
|
||||
'after:opacity-(--active) after:transition-opacity after:duration-300',
|
||||
'after:[mask-clip:padding-box,border-box]',
|
||||
'after:mask-intersect',
|
||||
'after:mask-[linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]',
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
76
monisuo-admin/src/components/inspira-ui/marquee/index.vue
Normal file
76
monisuo-admin/src/components/inspira-ui/marquee/index.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
class?: string
|
||||
reverse?: boolean
|
||||
pauseOnHover?: boolean
|
||||
vertical?: boolean
|
||||
repeat?: number
|
||||
}>(),
|
||||
{
|
||||
pauseOnHover: false,
|
||||
vertical: false,
|
||||
repeat: 4,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] gap-(--gap)',
|
||||
vertical ? 'flex-col' : 'flex-row',
|
||||
$props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="index in repeat"
|
||||
:key="index"
|
||||
:class="
|
||||
cn(
|
||||
'flex shrink-0 justify-around gap-(--gap)',
|
||||
vertical ? 'animate-marquee-vertical flex-col' : 'animate-marquee flex-row',
|
||||
pauseOnHover ? 'group-hover:paused' : '',
|
||||
)
|
||||
"
|
||||
:style="{
|
||||
animationDirection: reverse ? 'reverse' : 'normal',
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-marquee {
|
||||
animation: marquee var(--duration) linear infinite;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
.animate-marquee-vertical {
|
||||
animation: marquee-vertical var(--duration) linear infinite;
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-vertical {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
img: string
|
||||
name: string
|
||||
username: string
|
||||
body: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<figure
|
||||
class="relative w-64 cursor-pointer overflow-hidden rounded-xl border border-gray-950/10 bg-gray-950/1 p-4 hover:bg-gray-950/5 dark:border-gray-50/10 dark:bg-gray-50/10 dark:hover:bg-gray-50/15"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<img :src="img" class="rounded-full" width="32" height="32" alt="">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-medium dark:text-white">
|
||||
{{ name }}
|
||||
</span>
|
||||
<p class="text-xs font-medium dark:text-white/40">
|
||||
{{ username }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<blockquote class="mt-2 text-sm">
|
||||
{{ body }}
|
||||
</blockquote>
|
||||
</figure>
|
||||
</template>
|
||||
46
monisuo-admin/src/components/inspira-ui/ripple/circle.vue
Normal file
46
monisuo-admin/src/components/inspira-ui/ripple/circle.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props {
|
||||
size?: number
|
||||
class?: string
|
||||
opacity?: number
|
||||
animationDelay?: number
|
||||
borderStyle?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 210,
|
||||
opacity: 0.24,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('absolute shadow-xl', 'animate-ripple-circle', props.class)" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-ripple-circle {
|
||||
animation: ripple-effect var(--duration, 2s) ease-in-out calc(var(--i, 0) * 0.2s) infinite;
|
||||
border-width: 1px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: v-bind('`${props.size}px`');
|
||||
height: v-bind('`${props.size}px`');
|
||||
animation-delay: v-bind('`${props.animationDelay}ms`');
|
||||
opacity: v-bind('props.opacity');
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
border-style: v-bind('props.borderStyle');
|
||||
}
|
||||
|
||||
@keyframes ripple-effect {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
monisuo-admin/src/components/inspira-ui/ripple/container.vue
Normal file
36
monisuo-admin/src/components/inspira-ui/ripple/container.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import RippleCircle from './circle.vue'
|
||||
|
||||
interface Props {
|
||||
baseCircleSize?: number
|
||||
baseCircleOpacity?: number
|
||||
spaceBetweenCircle?: number
|
||||
circleOpacityDowngradeRatio?: number
|
||||
circleClass?: string
|
||||
waveSpeed?: number
|
||||
numberOfCircles?: number
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
baseCircleSize: 210,
|
||||
baseCircleOpacity: 0.24,
|
||||
circleOpacityDowngradeRatio: 0.03,
|
||||
waveSpeed: 80,
|
||||
spaceBetweenCircle: 70,
|
||||
numberOfCircles: 7,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0">
|
||||
<RippleCircle
|
||||
v-for="index in numberOfCircles"
|
||||
:key="index"
|
||||
:opacity="baseCircleOpacity - index * circleOpacityDowngradeRatio"
|
||||
:size="baseCircleSize + index * spaceBetweenCircle"
|
||||
:animation-delay="index * waveSpeed"
|
||||
:border-style="index === numberOfCircles - 1 ? 'dashed' : 'solid'"
|
||||
:class="circleClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
36
monisuo-admin/src/components/inspira-ui/ripple/index.vue
Normal file
36
monisuo-admin/src/components/inspira-ui/ripple/index.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import RippleCircle from './circle.vue'
|
||||
|
||||
interface Props {
|
||||
baseCircleSize?: number
|
||||
baseCircleOpacity?: number
|
||||
spaceBetweenCircle?: number
|
||||
circleOpacityDowngradeRatio?: number
|
||||
circleClass?: string
|
||||
waveSpeed?: number
|
||||
numberOfCircles?: number
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
baseCircleSize: 210,
|
||||
baseCircleOpacity: 0.24,
|
||||
circleOpacityDowngradeRatio: 0.03,
|
||||
waveSpeed: 80,
|
||||
spaceBetweenCircle: 70,
|
||||
numberOfCircles: 7,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0">
|
||||
<RippleCircle
|
||||
v-for="index in numberOfCircles"
|
||||
:key="index"
|
||||
:opacity="baseCircleOpacity - index * circleOpacityDowngradeRatio"
|
||||
:size="baseCircleSize + index * spaceBetweenCircle"
|
||||
:animation-delay="index * waveSpeed"
|
||||
:border-style="index === numberOfCircles - 1 ? 'dashed' : 'solid'"
|
||||
:class="circleClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
55
monisuo-admin/src/components/language-change.vue
Normal file
55
monisuo-admin/src/components/language-change.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import type { AcceptableValue } from 'reka-ui'
|
||||
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { Language } from '@/plugins/i18n'
|
||||
|
||||
import { appLocale, DEFAULT_LOCALE, SUPPORTED_LOCALES } from '@/plugins/i18n'
|
||||
|
||||
const { locale } = useI18n()
|
||||
|
||||
function setDefaultLanguage() {
|
||||
locale.value = DEFAULT_LOCALE
|
||||
appLocale.value = DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
function handleLocaleChange(val: AcceptableValue) {
|
||||
if (typeof val !== 'string' || !SUPPORTED_LOCALES.has(val as Language)) {
|
||||
setDefaultLanguage()
|
||||
return
|
||||
}
|
||||
|
||||
locale.value = val as Language
|
||||
appLocale.value = val as Language
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton variant="outline">
|
||||
<Icon icon="mdi:translate" class="mr-2" />
|
||||
{{ $t('language') }}
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent>
|
||||
<UiDropdownMenuLabel>{{ $t('changeLanguage') }}</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuRadioGroup
|
||||
v-model="locale"
|
||||
@update:model-value="handleLocaleChange"
|
||||
>
|
||||
<UiDropdownMenuRadioItem value="en">
|
||||
<Icon icon="flag:us-4x3" />
|
||||
<span class="ml-2">English</span>
|
||||
</UiDropdownMenuRadioItem>
|
||||
<UiDropdownMenuRadioItem value="zh">
|
||||
<Icon icon="flag:cn-4x3" />
|
||||
<span class="ml-2">中文</span>
|
||||
</UiDropdownMenuRadioItem>
|
||||
</UiDropdownMenuRadioGroup>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
3
monisuo-admin/src/components/loading.vue
Normal file
3
monisuo-admin/src/components/loading.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<UiSpinner class="w-24 h-24 animate-spin" />
|
||||
</template>
|
||||
48
monisuo-admin/src/components/marketing-layout/the-footer.vue
Normal file
48
monisuo-admin/src/components/marketing-layout/the-footer.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
|
||||
const mode = useColorMode()
|
||||
|
||||
const links = [
|
||||
{
|
||||
name: 'bluesky',
|
||||
icon: 'simple-icons:bluesky',
|
||||
url: 'https://bsky.app/profile/bitmc.bsky.social',
|
||||
},
|
||||
{
|
||||
name: 'github',
|
||||
icon: 'simple-icons:github',
|
||||
url: 'https://www.github.com/whbbit1999/shadcn-vue-admin',
|
||||
},
|
||||
{
|
||||
name: 'bilibili',
|
||||
icon: 'simple-icons:bilibili',
|
||||
url: 'https://space.bilibili.com/104376935',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="min-h-18 flex items-center justify-between">
|
||||
<UiAvatar>
|
||||
<UiAvatarImage :src="`${mode === 'dark' ? '/logo.svg' : '/logo-black.svg'}`" alt="Logo" />
|
||||
</UiAvatar>
|
||||
|
||||
<div>© 2025 Whbbit1999</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<UiButton
|
||||
v-for="link in links"
|
||||
:key="link.name"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
as="a"
|
||||
:href="link.url"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon :icon="link.icon" />
|
||||
</UiButton>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
40
monisuo-admin/src/components/marketing-layout/the-header.vue
Normal file
40
monisuo-admin/src/components/marketing-layout/the-header.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
|
||||
import LanguageChange from '@/components/language-change.vue'
|
||||
import SignInButton from '@/components/sign-in-button.vue'
|
||||
import SignUpButton from '@/components/sign-up-button.vue'
|
||||
import ToggleTheme from '@/components/toggle-theme.vue'
|
||||
|
||||
const mode = useColorMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="h-14 flex items-center marketing-header sticky top-0 z-99">
|
||||
<router-link to="/" class="flex items-center gap-2">
|
||||
<UiAvatar>
|
||||
<UiAvatarImage :src="`${mode === 'dark' ? '/logo.svg' : '/logo-black.svg'}`" alt="Logo" />
|
||||
</UiAvatar>
|
||||
<span class="text-base font-bold">Shadcn Vue Admin</span>
|
||||
</router-link>
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<div class="mr-2 hidden lg:flex lg:gap-2">
|
||||
<SignInButton />
|
||||
<SignUpButton />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<LanguageChange />
|
||||
<ToggleTheme />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.marketing-header {
|
||||
backdrop-filter: saturate(50%) blur(4px);
|
||||
background-size: 4px 4px;
|
||||
}
|
||||
</style>
|
||||
91
monisuo-admin/src/components/marketing/evaluation.vue
Normal file
91
monisuo-admin/src/components/marketing/evaluation.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import Marquee from '@/components/inspira-ui/marquee/index.vue'
|
||||
import MarqueeReviewCard from '@/components/inspira-ui/marquee/review-card.vue'
|
||||
|
||||
const reviews = [
|
||||
{
|
||||
name: 'Jack',
|
||||
username: '@jack',
|
||||
body: 'I\'ve never seen anything like this before. It\'s amazing. I love it.',
|
||||
img: 'https://avatar.vercel.sh/jack',
|
||||
},
|
||||
{
|
||||
name: 'Jill',
|
||||
username: '@jill',
|
||||
body: 'I don\'t know what to say. I\'m speechless. This is amazing.',
|
||||
img: 'https://avatar.vercel.sh/jill',
|
||||
},
|
||||
{
|
||||
name: 'John',
|
||||
username: '@john',
|
||||
body: 'I\'m at a loss for words. This is amazing. I love it.',
|
||||
img: 'https://avatar.vercel.sh/john',
|
||||
},
|
||||
{
|
||||
name: 'Jane',
|
||||
username: '@jane',
|
||||
body: 'I\'m at a loss for words. This is amazing. I love it.',
|
||||
img: 'https://avatar.vercel.sh/jane',
|
||||
},
|
||||
{
|
||||
name: 'Jenny',
|
||||
username: '@jenny',
|
||||
body: 'I\'m at a loss for words. This is amazing. I love it.',
|
||||
img: 'https://avatar.vercel.sh/jenny',
|
||||
},
|
||||
{
|
||||
name: 'James',
|
||||
username: '@james',
|
||||
body: 'I\'m at a loss for words. This is amazing. I love it.',
|
||||
img: 'https://avatar.vercel.sh/james',
|
||||
},
|
||||
]
|
||||
|
||||
// Split reviews into two rows
|
||||
const firstRow = ref(reviews.slice(0, reviews.length / 2))
|
||||
const secondRow = ref(reviews.slice(reviews.length / 2))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-4xl font-black my-4 text-center">
|
||||
{{ $t('marketing.evaluation.title') }}
|
||||
</h2>
|
||||
<h4 class="text-center mb-4">
|
||||
{{ $t('marketing.evaluation.subtitle') }}
|
||||
</h4>
|
||||
<div
|
||||
class="relative flex w-full flex-col items-center justify-center overflow-hidden"
|
||||
>
|
||||
<Marquee pause-on-hover class="[--duration:50s]">
|
||||
<MarqueeReviewCard
|
||||
v-for="review in firstRow"
|
||||
:key="review.username"
|
||||
:img="review.img"
|
||||
:name="review.name"
|
||||
:username="review.username"
|
||||
:body="review.body"
|
||||
/>
|
||||
</Marquee>
|
||||
|
||||
<Marquee reverse pause-on-hover class="[--duration:50s]">
|
||||
<MarqueeReviewCard
|
||||
v-for="review in secondRow"
|
||||
:key="review.username"
|
||||
:img="review.img"
|
||||
:name="review.name"
|
||||
:username="review.username"
|
||||
:body="review.body"
|
||||
/>
|
||||
</Marquee>
|
||||
|
||||
<!-- Left Gradient -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 left-0 w-1/3 bg-linear-to-r from-(--ui-bg) dark:from-(--ui-bg)"
|
||||
/>
|
||||
|
||||
<!-- Right Gradient -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 right-0 w-1/3 bg-linear-to-l from-(--ui-bg) dark:from-(--ui-bg)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
84
monisuo-admin/src/components/marketing/features.vue
Normal file
84
monisuo-admin/src/components/marketing/features.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import GlowingEffect from '@/components/inspira-ui/glowing-effect.vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const gridItems = computed(() => [
|
||||
{
|
||||
icon: 'lucide:box',
|
||||
title: t('marketing.features.feature1.title'),
|
||||
description: t('marketing.features.feature1.description'),
|
||||
},
|
||||
{
|
||||
icon: 'lucide:settings',
|
||||
title: t('marketing.features.feature2.title'),
|
||||
description: t('marketing.features.feature2.description'),
|
||||
},
|
||||
{
|
||||
icon: 'lucide:sparkles',
|
||||
title: t('marketing.features.feature3.title'),
|
||||
description: t('marketing.features.feature3.description'),
|
||||
},
|
||||
{
|
||||
icon: 'lucide:search',
|
||||
title: t('marketing.features.feature4.title'),
|
||||
description: t('marketing.features.feature4.description'),
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-center mb-8">
|
||||
{{ $t('marketing.features.title') }}
|
||||
</h2>
|
||||
|
||||
<ul
|
||||
class="grid grid-cols-1 grid-rows-none gap-4 overflow-auto xl:max-h-[56rem] xl:grid-rows-2 lg:gap-4 md:grid-cols-2 md:grid-rows-3"
|
||||
>
|
||||
<li
|
||||
v-for="item in gridItems"
|
||||
:key="item.title"
|
||||
:class="cn('min-h-[14rem] list-none')"
|
||||
>
|
||||
<div class="rounded-2.5xl relative h-full border p-2 md:rounded-3xl md:p-3">
|
||||
<GlowingEffect
|
||||
:spread="40"
|
||||
:glow="true"
|
||||
:disabled="false"
|
||||
:proximity="64"
|
||||
:inactive-zone="0.01"
|
||||
/>
|
||||
<div
|
||||
class="border-0.75 relative flex h-full flex-col justify-between gap-6 overflow-hidden rounded-xl p-6 md:p-6 dark:shadow-[0px_0px_27px_0px_#2D2D2D]"
|
||||
>
|
||||
<div class="relative flex flex-1 flex-col justify-between gap-3">
|
||||
<div class="w-fit rounded-lg border border-gray-600 p-2">
|
||||
<Icon
|
||||
class="size-4 text-black dark:text-neutral-500"
|
||||
:icon="item.icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<h3
|
||||
class="-tracking-4 text-balance pt-0.5 font-sans text-xl/[1.375rem] font-semibold text-black md:text-2xl/[1.875rem] dark:text-white"
|
||||
>
|
||||
{{ item.title }}
|
||||
</h3>
|
||||
<h2
|
||||
class="font-sans text-sm/[1.125rem] text-black md:text-base/[1.375rem] dark:text-neutral-400 [&_b]:md:font-semibold [&_strong]:md:font-semibold"
|
||||
>
|
||||
{{ item.description }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
81
monisuo-admin/src/components/marketing/hero.vue
Normal file
81
monisuo-admin/src/components/marketing/hero.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts" setup>
|
||||
import Autoplay from 'embla-carousel-autoplay'
|
||||
|
||||
const images = [
|
||||
'https://picsum.photos/640/640?random=1',
|
||||
'https://picsum.photos/640/640?random=2',
|
||||
'https://picsum.photos/640/640?random=3',
|
||||
'https://picsum.photos/640/640?random=4',
|
||||
'https://picsum.photos/640/640?random=5',
|
||||
'https://picsum.photos/640/640?random=6',
|
||||
]
|
||||
|
||||
const users: { avatar: string, name: string, id: number }[] = [
|
||||
{ avatar: 'https://github.com/benjamincanac.png', name: 'Benjamin Canac', id: 1 },
|
||||
{ avatar: 'https://github.com/romhml.png', name: 'Benjamin Canac', id: 2 },
|
||||
{ avatar: 'https://github.com/noook.png', name: 'Benjamin Canac', id: 3 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="flex gap-8 justify-between flex-col lg:flex-row">
|
||||
<aside class="w-full lg:w-1/3">
|
||||
<p class="text-4xl font-black relative">
|
||||
{{ $t('marketing.hero.title') }}
|
||||
</p>
|
||||
|
||||
<div class="font-bold mt-2 relative">
|
||||
{{ $t('marketing.hero.subtitle') }}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 my-12 relative">
|
||||
<UiButton>
|
||||
{{ $t('marketing.hero.getMore') }}
|
||||
</UiButton>
|
||||
|
||||
<img
|
||||
src="@/assets/icons/arrow-dark.svg"
|
||||
alt=""
|
||||
class="dark:hidden block w-12 h-12 absolute top-[110%] left-8 -rotate-90"
|
||||
>
|
||||
<img
|
||||
src="@/assets/icons/arrow-light.svg"
|
||||
alt=""
|
||||
class="dark:block hidden w-12 h-12 absolute top-[110%] left-8 -rotate-90"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex gap-2">
|
||||
<UiAvatar v-for="user in users" :key="user.id">
|
||||
<UiAvatarImage :src="user.avatar" />
|
||||
</UiAvatar>
|
||||
</div>
|
||||
|
||||
<span class="font-black">
|
||||
{{ $t('marketing.hero.learnPeople') }}
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<aside class="w-full lg:w-2/3 lg:px-2">
|
||||
<UiCarousel
|
||||
:opts="{
|
||||
align: 'start',
|
||||
loop: true,
|
||||
}"
|
||||
:plugins="[Autoplay({
|
||||
delay: 2000,
|
||||
})]"
|
||||
>
|
||||
<UiCarouselContent>
|
||||
<UiCarouselItem v-for="image in images" :key="image" class="basis-1/3">
|
||||
<img :src="image" width="320" height="320" class="rounded-lg">
|
||||
</UiCarouselItem>
|
||||
</UiCarouselContent>
|
||||
<UiCarouselPrevious class="hidden lg:flex" />
|
||||
<UiCarouselNext class="hidden lg:flex" />
|
||||
</UiCarousel>
|
||||
</aside>
|
||||
</main>
|
||||
</template>
|
||||
42
monisuo-admin/src/components/marketing/logos.vue
Normal file
42
monisuo-admin/src/components/marketing/logos.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
import Marquee from '@/components/inspira-ui/marquee/index.vue'
|
||||
|
||||
const types = [
|
||||
{ name: 'Nuxt', icon: 'simple-icons:nuxt' },
|
||||
{ name: 'Vue', icon: 'simple-icons:vitess' },
|
||||
{ name: 'Vite', icon: 'simple-icons:vite' },
|
||||
{ name: 'vitest', icon: 'simple-icons:vitest' },
|
||||
{ name: 'vscode', icon: 'simple-icons:visualstudiocode' },
|
||||
{ name: 'mysql', icon: 'simple-icons:mysql' },
|
||||
{ name: 'prisma', icon: 'simple-icons:prisma' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex w-full flex-col items-center justify-center overflow-hidden -rotate-3"
|
||||
>
|
||||
<Marquee pause-on-hover reverse class="[--duration:50s]">
|
||||
<div
|
||||
v-for="type in types"
|
||||
:key="type.name"
|
||||
class="flex items-center gap-2 mx-4"
|
||||
>
|
||||
<Icon :icon="type.icon" class="w-12 h-12" />
|
||||
<span class="font-black text-4xl">{{ type.name }}</span>
|
||||
</div>
|
||||
</Marquee>
|
||||
|
||||
<!-- Left Gradient -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 left-0 w-1/3 bg-linear-to-r from-(--ui-bg) dark:from-(--ui-bg)"
|
||||
/>
|
||||
|
||||
<!-- Right Gradient -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 right-0 w-1/3 bg-linear-to-l from-(--ui-bg) dark:from-(--ui-bg)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
141
monisuo-admin/src/components/marketing/pricing-plans/index.vue
Normal file
141
monisuo-admin/src/components/marketing/pricing-plans/index.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Plan {
|
||||
id: string | number
|
||||
title: string
|
||||
description: string
|
||||
badge?: string
|
||||
price: string
|
||||
unit: string
|
||||
discount: string
|
||||
recommendation?: boolean
|
||||
billing?: {
|
||||
cycle: string
|
||||
period: string
|
||||
}
|
||||
features: string[]
|
||||
}
|
||||
|
||||
const plans = computed<Plan[]>(() => [
|
||||
{
|
||||
id: 1,
|
||||
title: t('marketing.pricingPlans.hobby.title'),
|
||||
description: t('marketing.pricingPlans.hobby.description'),
|
||||
price: t('marketing.pricingPlans.hobby.price'),
|
||||
discount: t('marketing.pricingPlans.hobby.discount'),
|
||||
unit: t('marketing.pricingPlans.hobby.unit'),
|
||||
billing: {
|
||||
cycle: t('marketing.pricingPlans.hobby.billing.cycle'),
|
||||
period: t('marketing.pricingPlans.hobby.billing.period'),
|
||||
},
|
||||
features: [
|
||||
t('marketing.pricingPlans.hobby.features.feature1'),
|
||||
t('marketing.pricingPlans.hobby.features.feature2'),
|
||||
t('marketing.pricingPlans.hobby.features.feature3'),
|
||||
t('marketing.pricingPlans.hobby.features.feature4'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
recommendation: true,
|
||||
title: t('marketing.pricingPlans.starter.title'),
|
||||
description: t('marketing.pricingPlans.starter.description'),
|
||||
price: t('marketing.pricingPlans.starter.price'),
|
||||
discount: t('marketing.pricingPlans.starter.discount'),
|
||||
unit: t('marketing.pricingPlans.starter.unit'),
|
||||
billing: {
|
||||
cycle: t('marketing.pricingPlans.starter.billing.cycle'),
|
||||
period: t('marketing.pricingPlans.starter.billing.period'),
|
||||
},
|
||||
features: [
|
||||
t('marketing.pricingPlans.starter.features.feature1'),
|
||||
t('marketing.pricingPlans.starter.features.feature2'),
|
||||
t('marketing.pricingPlans.starter.features.feature3'),
|
||||
t('marketing.pricingPlans.starter.features.feature4'),
|
||||
t('marketing.pricingPlans.starter.features.feature5'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: t('marketing.pricingPlans.business.title'),
|
||||
description: t('marketing.pricingPlans.business.description'),
|
||||
price: t('marketing.pricingPlans.business.price'),
|
||||
discount: t('marketing.pricingPlans.business.discount'),
|
||||
unit: t('marketing.pricingPlans.business.unit'),
|
||||
billing: {
|
||||
cycle: t('marketing.pricingPlans.business.billing.cycle'),
|
||||
period: t('marketing.pricingPlans.business.billing.period'),
|
||||
},
|
||||
features: [
|
||||
t('marketing.pricingPlans.business.features.feature1'),
|
||||
t('marketing.pricingPlans.business.features.feature2'),
|
||||
t('marketing.pricingPlans.business.features.feature3'),
|
||||
t('marketing.pricingPlans.business.features.feature4'),
|
||||
t('marketing.pricingPlans.business.features.feature5'),
|
||||
t('marketing.pricingPlans.business.features.feature6'),
|
||||
],
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="pricing-plans">
|
||||
<h2 class="text-center font-black my-4 text-4xl">
|
||||
{{ $t('marketing.pricingPlans.title') }}
|
||||
</h2>
|
||||
<h4 class="text-center text-xl">
|
||||
{{ $t('marketing.pricingPlans.subtitle') }}
|
||||
</h4>
|
||||
<div
|
||||
class="flex flex-col lg:flex-row lg:items-start items-center justify-center gap-4 mt-8"
|
||||
>
|
||||
<UiCard
|
||||
v-for="plan in plans"
|
||||
:key="plan.id"
|
||||
class="w-full lg:w-1/5"
|
||||
:class="{
|
||||
'border-2 border-primary bg-primary/10':
|
||||
plan.recommendation,
|
||||
}"
|
||||
>
|
||||
<h3 class="text-xl font-black text-center">
|
||||
{{ plan.title }}
|
||||
</h3>
|
||||
<div class="text-sm text-center text-neutral-400">
|
||||
{{ plan.description }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-top my-2 justify-center">
|
||||
<div class="text-2xl font-black">
|
||||
{{ plan.unit }}
|
||||
<span class="text-4xl">{{ plan.price }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="plan.discount"
|
||||
class="text-sm font-bold line-through text-neutral-400"
|
||||
>
|
||||
{{ plan.unit }}{{ plan.discount }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sm mb-4 text-center">
|
||||
<ul>
|
||||
<li v-for="feature in plan.features" :key="feature" class="mb-1">
|
||||
<Icon icon="carbon:checkmark" class="inline-block" />
|
||||
{{ feature }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex justify-center mx-8">
|
||||
<UiButton block>
|
||||
{{ $t('marketing.pricingPlans.buy') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
34
monisuo-admin/src/components/marketing/setup.vue
Normal file
34
monisuo-admin/src/components/marketing/setup.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import Ripple from '@/components/inspira-ui/ripple/index.vue'
|
||||
import SignInButton from '@/components/sign-in-button.vue'
|
||||
import SignUpButton from '@/components/sign-up-button.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex h-[450px] w-full flex-col items-center justify-center overflow-hidden rounded-lg lg:w-full md:w-full"
|
||||
>
|
||||
<p class="z-10 whitespace-pre-wrap text-center text-5xl font-medium tracking-tighter text-black dark:text-white">
|
||||
{{ $t('marketing.setup.title') }}
|
||||
</p>
|
||||
<small class="mt-2">
|
||||
{{ $t('marketing.setup.subtitle') }}
|
||||
</small>
|
||||
|
||||
<div class="flex items-center gap-3 my-2 z-100">
|
||||
<SignInButton />
|
||||
<SignUpButton />
|
||||
</div>
|
||||
|
||||
<Ripple
|
||||
class="bg-white/5 mask-[linear-gradient(to_bottom,white,transparent)]"
|
||||
circle-class="border-[hsl(var(--primary))] bg-primary/25 blobed"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.blobed) {
|
||||
border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
|
||||
}
|
||||
</style>
|
||||
19
monisuo-admin/src/components/no-result-found.vue
Normal file
19
monisuo-admin/src/components/no-result-found.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang='ts' setup>
|
||||
import { FolderOpenIcon } from 'lucide-vue-next'
|
||||
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<FolderOpenIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No result found.</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Please try a different search term or check the spelling.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
</template>
|
||||
72
monisuo-admin/src/components/prop-ui/copy/Copy.vue
Normal file
72
monisuo-admin/src/components/prop-ui/copy/Copy.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { Copy, CopyCheck } from 'lucide-vue-next'
|
||||
|
||||
import type { ButtonVariants } from '@/components/ui/button'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { copyVariants } from '.'
|
||||
|
||||
interface Props {
|
||||
content: string
|
||||
size?: 'sm' | 'default'
|
||||
variant?: ButtonVariants['variant']
|
||||
class?: HTMLAttributes['class']
|
||||
copyTooltipText?: string
|
||||
copiedTooltipText?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'default',
|
||||
variant: 'outline',
|
||||
copyTooltipText: 'Copy',
|
||||
copiedTooltipText: 'Copied',
|
||||
})
|
||||
|
||||
const iconSize = computed(() => {
|
||||
return props.size === 'sm' ? 'sm' : 'default'
|
||||
})
|
||||
|
||||
const size = computed(() => {
|
||||
return props.size === 'sm' ? 'sm' : 'icon'
|
||||
})
|
||||
|
||||
const source = computed(() => props.content)
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard({ source })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="isSupported">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
:variant="props.variant"
|
||||
:size="size"
|
||||
:class="cn(props.class)"
|
||||
@click="copy(source)"
|
||||
>
|
||||
<Copy v-if="!copied" :class="cn(copyVariants({ iconSize }))" />
|
||||
<CopyCheck v-else :class="cn(copyVariants({ iconSize }))" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p v-if="!copied">{{ props.copyTooltipText }}: {{ props.content }}</p>
|
||||
<p v-else>{{ props.copiedTooltipText }}: {{ props.content }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
<span v-else>Your browser does not support Clipboard API</span>
|
||||
</template>
|
||||
22
monisuo-admin/src/components/prop-ui/copy/index.ts
Normal file
22
monisuo-admin/src/components/prop-ui/copy/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Copy } from './Copy.vue'
|
||||
|
||||
export const copyVariants = cva(
|
||||
'',
|
||||
{
|
||||
variants: {
|
||||
iconSize: {
|
||||
default: 'size-4',
|
||||
sm: 'size-3',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
iconSize: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type CopyVariants = VariantProps<typeof copyVariants>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { InlineTipVariants } from '.'
|
||||
|
||||
import { inlineTipVariants } from '.'
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
variant?: InlineTipVariants['variant']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: 'info',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn(
|
||||
'bg-secondary text-secondary-foreground text-sm inline-grid grid-cols-[4px_1fr] items-start gap-3 rounded-md border p-3',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<div
|
||||
:class="cn(
|
||||
'h-full w-1 rounded-full',
|
||||
inlineTipVariants({ variant: props.variant }))"
|
||||
/>
|
||||
|
||||
<div class="text-muted-foreground">
|
||||
<strong class="text-sm font-semibold text-foreground mr-2">{{ props.label }}:</strong>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
24
monisuo-admin/src/components/prop-ui/inline-tip/index.ts
Normal file
24
monisuo-admin/src/components/prop-ui/inline-tip/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as InlineTip } from './InlineTip.vue'
|
||||
|
||||
export const inlineTipVariants = cva(
|
||||
'',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
info: 'bg-stone-400 dark:bg-stone-600',
|
||||
warning: 'bg-yellow-400 dark:bg-yellow-600',
|
||||
success: 'bg-green-400 dark:bg-green-600',
|
||||
error: 'bg-rose-400 dark:bg-rose-600',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'info',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type InlineTipVariants = VariantProps<typeof inlineTipVariants>
|
||||
28
monisuo-admin/src/components/prop-ui/modal/Modal.vue
Normal file
28
monisuo-admin/src/components/prop-ui/modal/Modal.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DialogRootProps } from 'reka-ui'
|
||||
import type { DrawerRootProps } from 'vaul-vue'
|
||||
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
type Props = DrawerRootProps | DialogRootProps
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits<{
|
||||
'update:open': [value: boolean]
|
||||
}>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Root"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
21
monisuo-admin/src/components/prop-ui/modal/ModalClose.vue
Normal file
21
monisuo-admin/src/components/prop-ui/modal/ModalClose.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DialogCloseProps } from 'reka-ui'
|
||||
import type { DrawerCloseProps } from 'vaul-vue'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
type Props = DrawerCloseProps | DialogCloseProps
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Close"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
29
monisuo-admin/src/components/prop-ui/modal/ModalContent.vue
Normal file
29
monisuo-admin/src/components/prop-ui/modal/ModalContent.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const { Modal, contentClass } = useModal()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
|
||||
const mergedClass = computed(() => cn(contentClass.value, props.class))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Content"
|
||||
v-bind="forwarded"
|
||||
:class="mergedClass"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DialogDescriptionProps } from 'reka-ui'
|
||||
import type { DrawerDescriptionProps } from 'vaul-vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { useForwardProps } from 'reka-ui'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
type Props = (DrawerDescriptionProps | DialogDescriptionProps) & { class?: HTMLAttributes['class'] }
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Description"
|
||||
v-bind="forwardedProps"
|
||||
:class="props.class"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
22
monisuo-admin/src/components/prop-ui/modal/ModalFooter.vue
Normal file
22
monisuo-admin/src/components/prop-ui/modal/ModalFooter.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
interface Props {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Footer"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
22
monisuo-admin/src/components/prop-ui/modal/ModalHeader.vue
Normal file
22
monisuo-admin/src/components/prop-ui/modal/ModalHeader.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
interface Props {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Header"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
30
monisuo-admin/src/components/prop-ui/modal/ModalTitle.vue
Normal file
30
monisuo-admin/src/components/prop-ui/modal/ModalTitle.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DialogTitleProps } from 'reka-ui'
|
||||
import type { DrawerTitleProps } from 'vaul-vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { useForwardProps } from 'reka-ui'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
type Props = (DialogTitleProps | DrawerTitleProps) & { class?: HTMLAttributes['class'] }
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Title"
|
||||
v-bind="forwardedProps"
|
||||
:class="props.class"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
21
monisuo-admin/src/components/prop-ui/modal/ModalTrigger.vue
Normal file
21
monisuo-admin/src/components/prop-ui/modal/ModalTrigger.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DialogTriggerProps } from 'reka-ui'
|
||||
import type { DrawerTriggerProps } from 'vaul-vue'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
type Props = DialogTriggerProps | DrawerTriggerProps
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
10
monisuo-admin/src/components/prop-ui/modal/index.ts
Normal file
10
monisuo-admin/src/components/prop-ui/modal/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { default as Modal } from './Modal.vue'
|
||||
export { default as ModalClose } from './ModalClose.vue'
|
||||
export { default as ModalContent } from './ModalContent.vue'
|
||||
export { default as ModalDescription } from './ModalDescription.vue'
|
||||
export { default as ModalFooter } from './ModalFooter.vue'
|
||||
export { default as ModalHeader } from './ModalHeader.vue'
|
||||
export { default as ModalTitle } from './ModalTitle.vue'
|
||||
export { default as ModalTrigger } from './ModalTrigger.vue'
|
||||
|
||||
export * from './use-modal'
|
||||
31
monisuo-admin/src/components/prop-ui/modal/use-modal.ts
Normal file
31
monisuo-admin/src/components/prop-ui/modal/use-modal.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createSharedComposable, useMediaQuery } from '@vueuse/core'
|
||||
|
||||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer'
|
||||
|
||||
const useSharedModal = createSharedComposable(() => {
|
||||
const isDesktop = useMediaQuery('(min-width: 768px)')
|
||||
|
||||
const Modal = computed(() => ({
|
||||
Root: isDesktop.value ? Dialog : Drawer,
|
||||
Trigger: isDesktop.value ? DialogTrigger : DrawerTrigger,
|
||||
Content: isDesktop.value ? DialogContent : DrawerContent,
|
||||
Header: isDesktop.value ? DialogHeader : DrawerHeader,
|
||||
Title: isDesktop.value ? DialogTitle : DrawerTitle,
|
||||
Description: isDesktop.value ? DialogDescription : DrawerDescription,
|
||||
Footer: isDesktop.value ? DialogFooter : DrawerFooter,
|
||||
Close: isDesktop.value ? DialogClose : DrawerClose,
|
||||
}))
|
||||
|
||||
const contentClass = computed(() => (isDesktop.value ? '' : 'px-2 pb-8 *:px-4'))
|
||||
|
||||
return {
|
||||
isDesktop,
|
||||
Modal,
|
||||
contentClass,
|
||||
}
|
||||
})
|
||||
|
||||
export function useModal() {
|
||||
return useSharedModal()
|
||||
}
|
||||
33
monisuo-admin/src/components/prop-ui/status-badge/Status.vue
Normal file
33
monisuo-admin/src/components/prop-ui/status-badge/Status.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { StatusVariants } from '.'
|
||||
|
||||
import { statusVariants } from '.'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
color?: StatusVariants['color']
|
||||
rounded?: StatusVariants['rounded']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="cn(
|
||||
'mr-1',
|
||||
statusVariants({ color, rounded }),
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span
|
||||
:class="cn(statusVariants({ color, rounded }), 'absolute inline-flex h-full w-full animate-ping opacity-75')"
|
||||
/>
|
||||
<span :class="cn(statusVariants({ color, rounded }), props.class, 'relative inline-flex')" />
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import type { BadgeVariants } from '@/components/ui/badge'
|
||||
|
||||
import { badgeVariants } from '@/components/ui/badge'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { StatusVariants } from '.'
|
||||
|
||||
import Status from './Status.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
variant?: BadgeVariants['variant']
|
||||
rounded?: StatusVariants['rounded']
|
||||
class?: HTMLAttributes['class']
|
||||
color?: StatusVariants['color']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn(badgeVariants({ variant }), props.class)">
|
||||
<Status :color="props.color" :rounded="props.rounded" />
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
35
monisuo-admin/src/components/prop-ui/status-badge/index.ts
Normal file
35
monisuo-admin/src/components/prop-ui/status-badge/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Status } from './Status.vue'
|
||||
export { default as StatusBadge } from './StatusBadge.vue'
|
||||
|
||||
export const statusVariants = cva(
|
||||
'relative flex size-2',
|
||||
{
|
||||
variants: {
|
||||
rounded: {
|
||||
default: 'rounded-full',
|
||||
xs: 'rounded-xs',
|
||||
},
|
||||
color: {
|
||||
green: 'bg-green-500',
|
||||
red: 'bg-rose-500',
|
||||
blue: 'bg-blue-500',
|
||||
orange: 'bg-orange-500',
|
||||
purple: 'bg-purple-500',
|
||||
gray: 'bg-gray-300',
|
||||
},
|
||||
size: {
|
||||
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
color: 'green',
|
||||
rounded: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type StatusVariants = VariantProps<typeof statusVariants>
|
||||
9
monisuo-admin/src/components/sign-in-button.vue
Normal file
9
monisuo-admin/src/components/sign-in-button.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiButton as="a" href="/auth/sign-in">
|
||||
{{ $t('login') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
9
monisuo-admin/src/components/sign-up-button.vue
Normal file
9
monisuo-admin/src/components/sign-up-button.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiButton as="a" href="/auth/sign-up" variant="outline">
|
||||
{{ $t('register') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
2
monisuo-admin/src/components/sort-select/index.ts
Normal file
2
monisuo-admin/src/components/sort-select/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as SortSelect } from './sort-select.vue'
|
||||
export type * from './types'
|
||||
38
monisuo-admin/src/components/sort-select/sort-select.vue
Normal file
38
monisuo-admin/src/components/sort-select/sort-select.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowDownAZ, ArrowDownZA, SlidersHorizontal } from 'lucide-vue-next'
|
||||
|
||||
import type { TSort } from './types'
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:sort', payload: TSort): void
|
||||
}>()
|
||||
|
||||
const sort = defineModel<TSort>({ default: 'asc' })
|
||||
watch(sort, (newValue) => {
|
||||
emits('update:sort', newValue!)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSelect v-model:model-value="sort">
|
||||
<UiSelectTrigger class="w-16">
|
||||
<UiSelectValue>
|
||||
<SlidersHorizontal :size="16" />
|
||||
</UiSelectValue>
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent align="end">
|
||||
<UiSelectItem value="asc">
|
||||
<div class="flex items-center gap-4">
|
||||
<ArrowDownAZ :size="16" />
|
||||
<span>Ascending</span>
|
||||
</div>
|
||||
</UiSelectItem>
|
||||
<UiSelectItem value="desc">
|
||||
<div class="flex items-center gap-4">
|
||||
<ArrowDownZA :size="16" />
|
||||
<span>Descending</span>
|
||||
</div>
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</template>
|
||||
1
monisuo-admin/src/components/sort-select/types.ts
Normal file
1
monisuo-admin/src/components/sort-select/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type TSort = 'asc' | 'desc'
|
||||
32
monisuo-admin/src/components/toggle-theme.vue
Normal file
32
monisuo-admin/src/components/toggle-theme.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { Moon, Sun, SunMoon } from 'lucide-vue-next'
|
||||
|
||||
const mode = useColorMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton variant="outline" size="icon">
|
||||
<Moon class=" rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Sun class="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="end">
|
||||
<UiDropdownMenuItem @click="mode = 'light'">
|
||||
<Sun />
|
||||
Light
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="mode = 'dark'">
|
||||
<Moon />
|
||||
Dark
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="mode = 'auto'">
|
||||
<SunMoon />
|
||||
System
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
18
monisuo-admin/src/components/ui/accordion/Accordion.vue
Normal file
18
monisuo-admin/src/components/ui/accordion/Accordion.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionRootEmits, AccordionRootProps } from "reka-ui"
|
||||
import {
|
||||
AccordionRoot,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
|
||||
const props = defineProps<AccordionRootProps>()
|
||||
const emits = defineEmits<AccordionRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionRoot v-slot="slotProps" data-slot="accordion" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</AccordionRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { AccordionContent } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionContent
|
||||
data-slot="accordion-content"
|
||||
v-bind="delegatedProps"
|
||||
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
>
|
||||
<div :class="cn('pt-0 pb-4', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</template>
|
||||
24
monisuo-admin/src/components/ui/accordion/AccordionItem.vue
Normal file
24
monisuo-admin/src/components/ui/accordion/AccordionItem.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { AccordionItem, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionItem
|
||||
v-slot="slotProps"
|
||||
data-slot="accordion-item"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('border-b last:border-b-0', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</AccordionItem>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionTriggerProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ChevronDown } from "lucide-vue-next"
|
||||
import {
|
||||
AccordionHeader,
|
||||
AccordionTrigger,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionHeader class="flex">
|
||||
<AccordionTrigger
|
||||
data-slot="accordion-trigger"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
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',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<slot name="icon">
|
||||
<ChevronDown
|
||||
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
|
||||
/>
|
||||
</slot>
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
</template>
|
||||
4
monisuo-admin/src/components/ui/accordion/index.ts
Normal file
4
monisuo-admin/src/components/ui/accordion/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Accordion } from "./Accordion.vue"
|
||||
export { default as AccordionContent } from "./AccordionContent.vue"
|
||||
export { default as AccordionItem } from "./AccordionItem.vue"
|
||||
export { default as AccordionTrigger } from "./AccordionTrigger.vue"
|
||||
15
monisuo-admin/src/components/ui/alert-dialog/AlertDialog.vue
Normal file
15
monisuo-admin/src/components/ui/alert-dialog/AlertDialog.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogEmits, AlertDialogProps } from "reka-ui"
|
||||
import { AlertDialogRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<AlertDialogProps>()
|
||||
const emits = defineEmits<AlertDialogEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogRoot v-slot="slotProps" data-slot="alert-dialog" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</AlertDialogRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogActionProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { AlertDialogAction } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
|
||||
<slot />
|
||||
</AlertDialogAction>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogCancelProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { AlertDialogCancel } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogCancel
|
||||
v-bind="delegatedProps"
|
||||
:class="cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'mt-2 sm:mt-0',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogCancel>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogContentEmits, AlertDialogContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<AlertDialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80"
|
||||
/>
|
||||
<AlertDialogContent
|
||||
data-slot="alert-dialog-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogDescriptionProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
AlertDialogDescription,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogDescription
|
||||
data-slot="alert-dialog-description"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogDescription>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
:class="cn('flex flex-col gap-2 text-center sm:text-left', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogTitleProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { AlertDialogTitle } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogTitle
|
||||
data-slot="alert-dialog-title"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('text-lg font-semibold', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogTitle>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user