mirror of
https://github.com/lqtmcstudio/QZMusic_PC.git
synced 2026-06-22 08:24:25 +08:00
fork(fix): Clone AMLL 并修复 BUG
- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork) - 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
25
amll-local/packages/playground/core/components.json
Normal file
25
amll-local/packages/playground/core/components.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "reka-vega",
|
||||
"font": "inter",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/assets/index.tailwind.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"composables": "@/composables"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
18
amll-local/packages/playground/core/index.html
Normal file
18
amll-local/packages/playground/core/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AMLL Playground</title>
|
||||
<style>
|
||||
:root {
|
||||
background-color: light-dark(#fafafa, #18181b);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
48
amll-local/packages/playground/core/package.json
Normal file
48
amll-local/packages/playground/core/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@applemusic-like-lyrics/playground-core",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"build-only": "vite build",
|
||||
"build": "run-p typecheck \"build-only {@}\" --"
|
||||
},
|
||||
"dependencies": {
|
||||
"@applemusic-like-lyrics/core": "workspace:^",
|
||||
"@applemusic-like-lyrics/lyric": "workspace:^",
|
||||
"@pixi/app": "^7.4.3",
|
||||
"@pixi/core": "^7.4.3",
|
||||
"@pixi/display": "^7.4.3",
|
||||
"@pixi/filter-blur": "^7.4.3",
|
||||
"@pixi/filter-bulge-pinch": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.3",
|
||||
"@pixi/sprite": "^7.4.3",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"jss": "^10.10.0",
|
||||
"jss-preset-default": "^10.10.0",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"music-metadata": "^11.12.3",
|
||||
"pinia": "^3.0.4",
|
||||
"reka-ui": "^2.9.7",
|
||||
"shadcn-vue": "^2.7.3",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vue": "^3.5.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/radix-icons": "^1.2.6",
|
||||
"@iconify/vue": "^5.0.1",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@tsconfig/node22": "^22.0.5",
|
||||
"@types/node": "^25.8.0",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.13",
|
||||
"vue-tsc": "^3.2.9"
|
||||
}
|
||||
}
|
||||
27
amll-local/packages/playground/core/src/App.vue
Normal file
27
amll-local/packages/playground/core/src/App.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import SidebarShell from "@/components/SidebarShell.vue";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import LyricPlayer from "./components/MainPlayer.vue";
|
||||
</script>
|
||||
<template>
|
||||
<SidebarProvider>
|
||||
<SidebarShell variant="inset" />
|
||||
<SidebarInset class="relative overflow-hidden">
|
||||
<LyricPlayer />
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</template>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:root {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
</style>
|
||||
159
amll-local/packages/playground/core/src/assets/amll-logo.svg
Normal file
159
amll-local/packages/playground/core/src/assets/amll-logo.svg
Normal file
@@ -0,0 +1,159 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5_32)">
|
||||
<rect width="64" height="64" fill="url(#paint0_linear_5_32)"/>
|
||||
<g filter="url(#filter0_dii_5_32)">
|
||||
<path d="M29.8252 23.9748C29.8252 24.2495 29.7467 24.4845 29.5896 24.6799C29.4326 24.8754 29.2231 24.9995 28.9614 25.0523L22.1288 26.5576C21.8042 26.6316 21.5921 26.7266 21.4927 26.8428C21.3932 26.959 21.3435 27.2126 21.3435 27.6034V41.3098C21.3435 42.3873 21.1681 43.309 20.8173 44.0748C20.4665 44.8407 20.0084 45.4692 19.4429 45.9605C18.8774 46.4517 18.2727 46.8108 17.6287 47.038C16.9848 47.2651 16.3748 47.3786 15.7989 47.3786C14.5318 47.3786 13.4978 47.0063 12.6967 46.2615C11.8957 45.5168 11.4951 44.5581 11.4951 43.3856C11.4951 42.213 11.859 41.2438 12.5868 40.4779C13.3145 39.712 14.4324 39.1707 15.9402 38.8537L18.312 38.3467C18.9507 38.2199 19.2387 37.8027 19.1759 37.0949L19.1445 20.4413C19.1445 19.575 19.6576 19.0257 20.6838 18.7933L28.3331 17.1137C28.7415 17.0397 29.0923 17.1216 29.3854 17.3593C29.6786 17.597 29.8252 17.9271 29.8252 18.3496V23.9748Z" fill="url(#paint1_linear_5_32)"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_dii_5_32)">
|
||||
<path d="M36.5359 17.3981C35.4894 17.3981 34.9662 17.3981 34.5851 17.6399C34.4886 17.7012 34.3994 17.7727 34.3191 17.853C34.0379 18.1342 33.8641 18.5225 33.8641 18.9515C33.8641 19.3804 34.0379 19.7688 34.3191 20.0499C34.3994 20.1302 34.4886 20.2018 34.5851 20.263C34.9662 20.5049 35.4894 20.5049 36.5359 20.5049H48.5903C49.6368 20.5049 50.16 20.5049 50.5411 20.263C50.6376 20.2018 50.7268 20.1302 50.8072 20.0499C51.0883 19.7688 51.2621 19.3804 51.2621 18.9515C51.2621 18.5225 51.0883 18.1342 50.8072 17.853C50.7268 17.7727 50.6376 17.7012 50.5411 17.6399C50.16 17.3981 49.6368 17.3981 48.5903 17.3981H36.5359Z" fill="url(#paint2_linear_5_32)"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_dii_5_32)">
|
||||
<path d="M36.5359 26.0971C35.4894 26.0971 34.9662 26.0971 34.5851 26.3389C34.4886 26.4002 34.3994 26.4717 34.3191 26.5521C34.0379 26.8332 33.8641 27.2215 33.8641 27.6505C33.8641 28.0794 34.0379 28.4678 34.3191 28.7489C34.3994 28.8293 34.4886 28.9008 34.5851 28.9621C34.9662 29.2039 35.4894 29.2039 36.5359 29.2039H48.5903C49.6368 29.2039 50.16 29.2039 50.5411 28.9621C50.6376 28.9008 50.7268 28.8293 50.8072 28.7489C51.0883 28.4678 51.2621 28.0794 51.2621 27.6505C51.2621 27.2215 51.0883 26.8332 50.8072 26.5521C50.7268 26.4717 50.6376 26.4002 50.5411 26.3389C50.16 26.0971 49.6368 26.0971 48.5903 26.0971H36.5359Z" fill="url(#paint3_linear_5_32)"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_dii_5_32)">
|
||||
<path d="M36.5359 34.7961C35.4894 34.7961 34.9662 34.7961 34.5851 35.0379C34.4886 35.0992 34.3994 35.1707 34.3191 35.2511C34.0379 35.5322 33.8641 35.9206 33.8641 36.3495C33.8641 36.7785 34.0379 37.1668 34.3191 37.4479C34.3994 37.5283 34.4886 37.5998 34.5851 37.6611C34.9662 37.9029 35.4894 37.9029 36.5359 37.9029H48.5903C49.6368 37.9029 50.16 37.9029 50.5411 37.6611C50.6376 37.5998 50.7268 37.5283 50.8072 37.4479C51.0883 37.1668 51.2621 36.7785 51.2621 36.3495C51.2621 35.9206 51.0883 35.5322 50.8072 35.2511C50.7268 35.1707 50.6376 35.0992 50.5411 35.0379C50.16 34.7961 49.6368 34.7961 48.5903 34.7961H36.5359Z" fill="url(#paint4_linear_5_32)"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_dii_5_32)">
|
||||
<path d="M36.5359 43.4951C35.4894 43.4951 34.9662 43.4951 34.5851 43.737C34.4886 43.7982 34.3994 43.8698 34.3191 43.9501C34.0379 44.2312 33.8641 44.6196 33.8641 45.0485C33.8641 45.4775 34.0379 45.8659 34.3191 46.147C34.3994 46.2273 34.4886 46.2989 34.5851 46.3601C34.9662 46.6019 35.4894 46.6019 36.5359 46.6019H48.5903C49.6368 46.6019 50.16 46.6019 50.5411 46.3601C50.6376 46.2989 50.7268 46.2273 50.8072 46.147C51.0883 45.8659 51.2621 45.4775 51.2621 45.0485C51.2621 44.6196 51.0883 44.2312 50.8072 43.9501C50.7268 43.8698 50.6376 43.7982 50.5411 43.737C50.16 43.4951 49.6368 43.4951 48.5903 43.4951H36.5359Z" fill="url(#paint5_linear_5_32)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dii_5_32" x="9.01515" y="15.8474" width="23.2901" height="35.2513" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.24"/>
|
||||
<feGaussianBlur stdDeviation="1.24"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="plus-darker" in2="BackgroundImageFix" result="effect1_dropShadow_5_32"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5_32" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.155"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.531765 0 0 0 0 0.00148115 0 0 0 0 0.00148115 0 0 0 0.25 0"/>
|
||||
<feBlend mode="plus-darker" in2="shape" result="effect2_innerShadow_5_32"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.465"/>
|
||||
<feGaussianBlur stdDeviation="0.2325"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.111024 0 0 0 0 0.111024 0 0 0 0.18 0"/>
|
||||
<feBlend mode="plus-darker" in2="effect2_innerShadow_5_32" result="effect3_innerShadow_5_32"/>
|
||||
</filter>
|
||||
<filter id="filter1_dii_5_32" x="31.3841" y="16.1581" width="22.3581" height="8.06681" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.24"/>
|
||||
<feGaussianBlur stdDeviation="1.24"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="plus-darker" in2="BackgroundImageFix" result="effect1_dropShadow_5_32"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5_32" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.155"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.531765 0 0 0 0 0.00148115 0 0 0 0 0.00148115 0 0 0 0.25 0"/>
|
||||
<feBlend mode="plus-darker" in2="shape" result="effect2_innerShadow_5_32"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.465"/>
|
||||
<feGaussianBlur stdDeviation="0.2325"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.111024 0 0 0 0 0.111024 0 0 0 0.18 0"/>
|
||||
<feBlend mode="plus-darker" in2="effect2_innerShadow_5_32" result="effect3_innerShadow_5_32"/>
|
||||
</filter>
|
||||
<filter id="filter2_dii_5_32" x="31.3841" y="24.8571" width="22.3581" height="8.06678" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.24"/>
|
||||
<feGaussianBlur stdDeviation="1.24"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="plus-darker" in2="BackgroundImageFix" result="effect1_dropShadow_5_32"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5_32" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.155"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.531765 0 0 0 0 0.00148115 0 0 0 0 0.00148115 0 0 0 0.25 0"/>
|
||||
<feBlend mode="plus-darker" in2="shape" result="effect2_innerShadow_5_32"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.465"/>
|
||||
<feGaussianBlur stdDeviation="0.2325"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.111024 0 0 0 0 0.111024 0 0 0 0.18 0"/>
|
||||
<feBlend mode="plus-darker" in2="effect2_innerShadow_5_32" result="effect3_innerShadow_5_32"/>
|
||||
</filter>
|
||||
<filter id="filter3_dii_5_32" x="31.3841" y="33.5561" width="22.3581" height="8.06678" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.24"/>
|
||||
<feGaussianBlur stdDeviation="1.24"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="plus-darker" in2="BackgroundImageFix" result="effect1_dropShadow_5_32"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5_32" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.155"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.531765 0 0 0 0 0.00148115 0 0 0 0 0.00148115 0 0 0 0.25 0"/>
|
||||
<feBlend mode="plus-darker" in2="shape" result="effect2_innerShadow_5_32"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.465"/>
|
||||
<feGaussianBlur stdDeviation="0.2325"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.111024 0 0 0 0 0.111024 0 0 0 0.18 0"/>
|
||||
<feBlend mode="plus-darker" in2="effect2_innerShadow_5_32" result="effect3_innerShadow_5_32"/>
|
||||
</filter>
|
||||
<filter id="filter4_dii_5_32" x="31.3841" y="42.2551" width="22.3581" height="8.06678" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.24"/>
|
||||
<feGaussianBlur stdDeviation="1.24"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="plus-darker" in2="BackgroundImageFix" result="effect1_dropShadow_5_32"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5_32" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.155"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.531765 0 0 0 0 0.00148115 0 0 0 0 0.00148115 0 0 0 0.25 0"/>
|
||||
<feBlend mode="plus-darker" in2="shape" result="effect2_innerShadow_5_32"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.465"/>
|
||||
<feGaussianBlur stdDeviation="0.2325"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.111024 0 0 0 0 0.111024 0 0 0 0.18 0"/>
|
||||
<feBlend mode="plus-darker" in2="effect2_innerShadow_5_32" result="effect3_innerShadow_5_32"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_5_32" x1="34.375" y1="1.26961e-06" x2="34.375" y2="64" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F96868"/>
|
||||
<stop offset="1" stop-color="#FF4040"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_5_32" x1="20.6602" y1="17.0874" x2="20.6602" y2="47.3786" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEFEFE"/>
|
||||
<stop offset="1" stop-color="#ECECEC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_5_32" x1="42.5631" y1="17.3981" x2="42.5631" y2="20.5049" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEFEFE"/>
|
||||
<stop offset="1" stop-color="#ECECEC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_5_32" x1="42.5631" y1="26.0971" x2="42.5631" y2="29.2039" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEFEFE"/>
|
||||
<stop offset="1" stop-color="#ECECEC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_5_32" x1="42.5631" y1="34.7961" x2="42.5631" y2="37.9029" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEFEFE"/>
|
||||
<stop offset="1" stop-color="#ECECEC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_5_32" x1="42.5631" y1="43.4951" x2="42.5631" y2="46.6019" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEFEFE"/>
|
||||
<stop offset="1" stop-color="#ECECEC"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_5_32">
|
||||
<rect width="64" height="64" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,121 @@
|
||||
@import "tailwindcss";
|
||||
@import "shadcn-vue/tailwind.css";
|
||||
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-sans:
|
||||
-apple-system, BlinkMacSystemFont, "SF Pro Display", Inter, "PingFang SC",
|
||||
system-ui, sans-serif;
|
||||
--font-heading: var(--font-sans);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--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.629 0.215 25.963);
|
||||
--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.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);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: var(--primary);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.97 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.582 0.193 26.976);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.281 0 271.152);
|
||||
--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);
|
||||
--sidebar: oklch(0.218 0 271.152);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: var(--primary);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--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);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: color-mix(in oklab, var(--primary) 40%, transparent);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ImageIcon,
|
||||
MusicIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
TextAlignStartIcon,
|
||||
} from "lucide-vue-next";
|
||||
import { computed } from "vue";
|
||||
import { extractCoverBlob } from "@/lib/extract-cover";
|
||||
import { usePlayerStore } from "@/stores/player";
|
||||
import ModeToggle from "./ModeToggle.vue";
|
||||
import Button from "./ui/button/Button.vue";
|
||||
import ButtonGroup from "./ui/button-group/ButtonGroup.vue";
|
||||
import SidebarFooter from "./ui/sidebar/SidebarFooter.vue";
|
||||
import Slider from "./ui/slider/Slider.vue";
|
||||
|
||||
const player = usePlayerStore();
|
||||
|
||||
const currentTime = computed({
|
||||
get: () => [player.audio.currentTime],
|
||||
set: (value) => player.seek(value[0] ?? 0),
|
||||
});
|
||||
|
||||
const maxTime = computed(() =>
|
||||
Math.max(1, player.audio.duration, player.audio.currentTime),
|
||||
);
|
||||
|
||||
const currentTimeLabel = computed(() => formatTime(player.audio.currentTime));
|
||||
const durationLabel = computed(() => formatTime(player.audio.duration));
|
||||
|
||||
function formatTime(time: number): string {
|
||||
const normalizedTime = Math.max(0, Number.isFinite(time) ? time : 0);
|
||||
const minutes = Math.floor(normalizedTime / 60);
|
||||
const seconds = Math.floor(normalizedTime % 60);
|
||||
return `${minutes.toString().padStart(2, "0")}:${seconds
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function openFile(accept: string, onFile: (file: File) => void): void {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = accept;
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0];
|
||||
if (file) onFile(file);
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
async function openLocalMusicFile(file: File): Promise<void> {
|
||||
player.setLocalMusicFile(file);
|
||||
try {
|
||||
const cover = await extractCoverBlob(file);
|
||||
if (cover) {
|
||||
player.setExtractedAlbumBlob(cover, `${file.name} cover`);
|
||||
} else {
|
||||
player.clearExtractedAlbum();
|
||||
}
|
||||
} catch {
|
||||
player.clearExtractedAlbum();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarFooter class="border-t border-sidebar-border bg-sidebar p-3 gap-2">
|
||||
<div
|
||||
class="flex items-center justify-between px-1 text-xs text-muted-foreground"
|
||||
>
|
||||
<span>{{ currentTimeLabel }}</span>
|
||||
<span>{{ durationLabel }}</span>
|
||||
</div>
|
||||
<Slider
|
||||
v-model="currentTime"
|
||||
class="mt-1 mb-2"
|
||||
:max="maxTime"
|
||||
:step="1"
|
||||
:disabled="!player.source.musicUrl"
|
||||
/>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
:aria-label="player.audio.playing ? '暂停' : '播放'"
|
||||
@click="player.togglePlayback"
|
||||
:disabled="!player.source.musicUrl"
|
||||
>
|
||||
<PauseIcon v-if="player.audio.playing" />
|
||||
<PlayIcon v-else />
|
||||
</Button>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="打开歌词"
|
||||
@click="openFile('.ttml,.lrc,.alrc,.yrc,.lys,.lyl,.lqe,.qrc,.eslrc', player.setLocalLyricFile)"
|
||||
>
|
||||
<TextAlignStartIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="打开歌曲"
|
||||
@click="openFile('audio/*', openLocalMusicFile)"
|
||||
>
|
||||
<MusicIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="打开专辑图"
|
||||
@click="openFile('image/*,video/*', player.setLocalAlbumFile)"
|
||||
>
|
||||
<ImageIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
</template>
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { BrushIcon, MonitorPlayIcon } from "lucide-vue-next";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { usePlayerStore } from "@/stores/player";
|
||||
import ControllerSlider from "./ControllerSlider.vue";
|
||||
import ControllerSliderGroup from "./ControllerSliderGroup.vue";
|
||||
import ControllerSwitch from "./ControllerSwitch.vue";
|
||||
|
||||
const player = usePlayerStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4 py-1">
|
||||
<section class="space-y-2.5">
|
||||
<h3 class="text-sm font-bold flex items-center gap-1">
|
||||
<MonitorPlayIcon :size="16" />
|
||||
播放状态
|
||||
</h3>
|
||||
<ControllerSwitch
|
||||
v-model="player.background.playing"
|
||||
title="播放"
|
||||
description="暂停或恢复背景动画"
|
||||
/>
|
||||
<ControllerSwitch
|
||||
v-model="player.background.staticMode"
|
||||
title="静态模式"
|
||||
description="固定背景流动状态"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section class="space-y-2.5">
|
||||
<h3 class="text-sm font-bold flex items-center gap-1">
|
||||
<BrushIcon :size="16" />
|
||||
渲染选项
|
||||
</h3>
|
||||
<Select v-model="player.background.renderer">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue placeholder="选择渲染器" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="mg">Mesh Gradient 渲染器</SelectItem>
|
||||
<SelectItem value="pixi">Pixi 渲染器</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<ControllerSliderGroup>
|
||||
<ControllerSlider
|
||||
v-model="player.background.scale"
|
||||
title="分辨率比率"
|
||||
:min="0.01"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
/>
|
||||
<ControllerSlider
|
||||
v-model="player.background.fps"
|
||||
title="帧率"
|
||||
:min="1"
|
||||
:max="240"
|
||||
:step="1"
|
||||
suffix="FPS"
|
||||
/>
|
||||
<ControllerSlider
|
||||
v-model="player.background.flowSpeed"
|
||||
title="流动速度"
|
||||
:min="0"
|
||||
:max="5"
|
||||
:step="0.1"
|
||||
:precision="1"
|
||||
/>
|
||||
</ControllerSliderGroup>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: number;
|
||||
title: string;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
precision?: number;
|
||||
suffix?: string;
|
||||
}>(),
|
||||
{
|
||||
precision: undefined,
|
||||
suffix: "",
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: number];
|
||||
}>();
|
||||
|
||||
const value = computed({
|
||||
get: () => [props.modelValue],
|
||||
set: (nextValue) => emit("update:modelValue", nextValue[0] ?? props.min),
|
||||
});
|
||||
|
||||
const displayValue = computed(() => {
|
||||
const currentValue = props.modelValue;
|
||||
const formattedValue =
|
||||
props.precision === undefined
|
||||
? String(currentValue)
|
||||
: currentValue.toFixed(props.precision);
|
||||
|
||||
return props.suffix ? `${formattedValue} ${props.suffix}` : formattedValue;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="font-medium text-muted-foreground">{{ title }}</span>
|
||||
<span>{{ displayValue }}</span>
|
||||
</div>
|
||||
<Slider class="my-1" v-model="value" :min="min" :max="max" :step="step" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div class="rounded-md border p-2 flex flex-col gap-2.5"><slot></slot></div>
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: boolean];
|
||||
}>();
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (nextValue) => emit("update:modelValue", nextValue),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border p-2">
|
||||
<div>
|
||||
<div class="text-sm">{{ title }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ description }}</div>
|
||||
</div>
|
||||
<Switch v-model="value" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowDownUpIcon,
|
||||
ExpandIcon,
|
||||
TypeIcon,
|
||||
WandIcon,
|
||||
} from "lucide-vue-next";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { usePlayerStore } from "@/stores/player";
|
||||
import ControllerSlider from "./ControllerSlider.vue";
|
||||
import ControllerSliderGroup from "./ControllerSliderGroup.vue";
|
||||
import ControllerSwitch from "./ControllerSwitch.vue";
|
||||
import Input from "./ui/input/Input.vue";
|
||||
|
||||
const player = usePlayerStore();
|
||||
|
||||
const springFields = [
|
||||
{ key: "mass", label: "质量", min: 0.1, max: 5, step: 0.1 },
|
||||
{ key: "damping", label: "阻力", min: 0, max: 40, step: 0.5 },
|
||||
{ key: "stiffness", label: "弹性", min: 1, max: 300, step: 1 },
|
||||
] as const;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4 py-1">
|
||||
<section class="space-y-2.5">
|
||||
<h3 class="text-sm font-bold flex items-center gap-1">
|
||||
<TypeIcon :size="16" />
|
||||
歌词字体
|
||||
</h3>
|
||||
<Input
|
||||
id="font-family"
|
||||
v-model="player.lyric.fontFamily"
|
||||
placeholder="Inter, sans-serif"
|
||||
/>
|
||||
<ControllerSliderGroup>
|
||||
<ControllerSlider
|
||||
v-model="player.lyric.fontWeight"
|
||||
title="字重"
|
||||
:min="100"
|
||||
:max="900"
|
||||
:step="100"
|
||||
:precision="0"
|
||||
/>
|
||||
</ControllerSliderGroup>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section class="space-y-2.5">
|
||||
<h3 class="text-sm font-bold flex items-center gap-1">
|
||||
<WandIcon :size="16" />
|
||||
歌词行效果
|
||||
</h3>
|
||||
<ControllerSliderGroup>
|
||||
<ControllerSlider
|
||||
v-model="player.lyric.fadeWidth"
|
||||
title="歌词渐变宽度"
|
||||
:min="0"
|
||||
:max="3"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
/>
|
||||
</ControllerSliderGroup>
|
||||
<ControllerSwitch
|
||||
v-model="player.lyric.enableBlur"
|
||||
title="歌词模糊"
|
||||
description="为非焦点行启用模糊效果"
|
||||
/>
|
||||
<ControllerSwitch
|
||||
v-model="player.lyric.enableSpring"
|
||||
title="使用弹簧动画"
|
||||
description="使用物理弹簧替代 CSS transition"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section class="space-y-2.5">
|
||||
<h3 class="text-sm font-bold flex items-center gap-1">
|
||||
<ArrowDownUpIcon :size="16" />
|
||||
垂直位移弹簧
|
||||
</h3>
|
||||
<ControllerSliderGroup>
|
||||
<ControllerSlider
|
||||
v-for="field in springFields"
|
||||
:key="field.key"
|
||||
:title="field.label"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
:step="field.step"
|
||||
v-model="player.lyric.verticalSpring[field.key]"
|
||||
/>
|
||||
</ControllerSliderGroup>
|
||||
<ControllerSwitch
|
||||
v-model="player.lyric.verticalSpring.soft"
|
||||
title="强制软弹簧"
|
||||
description="阻力小于 1 时可用"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section class="space-y-2.5">
|
||||
<h3 class="text-sm font-bold flex items-center gap-1">
|
||||
<ExpandIcon :size="16" />
|
||||
缩放弹簧
|
||||
</h3>
|
||||
<ControllerSliderGroup>
|
||||
<ControllerSlider
|
||||
v-for="field in springFields"
|
||||
:key="field.key"
|
||||
:title="field.label"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
:step="field.step"
|
||||
v-model="player.lyric.scaleSpring[field.key]"
|
||||
/>
|
||||
</ControllerSliderGroup>
|
||||
<ControllerSwitch
|
||||
v-model="player.lyric.scaleSpring.soft"
|
||||
title="强制软弹簧"
|
||||
description="阻力小于 1 时可用"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,360 @@
|
||||
<script setup lang="ts">
|
||||
import type { LyricLine } from "@applemusic-like-lyrics/core";
|
||||
import {
|
||||
DomLyricPlayer,
|
||||
type LyricLineMouseEvent,
|
||||
} from "@applemusic-like-lyrics/core";
|
||||
import {
|
||||
parseEslrc,
|
||||
parseLqe,
|
||||
parseLrc,
|
||||
parseLrcA2,
|
||||
parseLyl,
|
||||
parseLys,
|
||||
parseQrc,
|
||||
parseTTML,
|
||||
parseYrc,
|
||||
} from "@applemusic-like-lyrics/lyric";
|
||||
import { onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
|
||||
import { audioRuntime } from "@/runtime/audio";
|
||||
import { backgroundRuntime } from "@/runtime/background";
|
||||
import { usePlayerStore } from "@/stores/player";
|
||||
import { SidebarTrigger } from "./ui/sidebar";
|
||||
|
||||
const player = usePlayerStore();
|
||||
const playerEl = ref<HTMLElement | null>(null);
|
||||
const lyricPlayerRef = shallowRef<DomLyricPlayer>();
|
||||
|
||||
let frameId = 0;
|
||||
let lastFrameTime = -1;
|
||||
let lyricLoadRevision = 0;
|
||||
|
||||
function applyLyricSettings(): void {
|
||||
const lyricPlayer = lyricPlayerRef.value;
|
||||
if (!lyricPlayer) return;
|
||||
lyricPlayer.setWordFadeWidth(player.lyric.fadeWidth);
|
||||
lyricPlayer.setEnableBlur(player.lyric.enableBlur);
|
||||
lyricPlayer.setEnableSpring(player.lyric.enableSpring);
|
||||
lyricPlayer.setLinePosYSpringParams({ ...player.lyric.verticalSpring });
|
||||
lyricPlayer.setLineScaleSpringParams({ ...player.lyric.scaleSpring });
|
||||
}
|
||||
|
||||
function mountBackground(): void {
|
||||
const host = playerEl.value;
|
||||
if (!host) return;
|
||||
|
||||
const lyricElement = lyricPlayerRef.value?.getElement() ?? null;
|
||||
backgroundRuntime.mount(host, player.background.renderer, lyricElement);
|
||||
player.setBackgroundError("");
|
||||
backgroundRuntime.applySettings(player);
|
||||
void backgroundRuntime.loadAlbum(player);
|
||||
}
|
||||
|
||||
function getSourceName(source: string, fallbackName: string): string {
|
||||
const rawName = fallbackName || source;
|
||||
const withoutHash = rawName.split("#", 1)[0] ?? rawName;
|
||||
return (withoutHash.split("?", 1)[0] ?? withoutHash).toLowerCase();
|
||||
}
|
||||
|
||||
function hasLrcA2Timestamps(content: string): boolean {
|
||||
return /<(?:(?:\d+:)*\d+(?:\.\d+)?)>/.test(content);
|
||||
}
|
||||
|
||||
function buildDemoLyricLine(
|
||||
lyric: string,
|
||||
startTime = 1000,
|
||||
otherParams: Partial<LyricLine> = {},
|
||||
): LyricLine {
|
||||
let currentTime = startTime;
|
||||
const words: LyricLine["words"] = [];
|
||||
for (const word of lyric.split("|")) {
|
||||
const [text = "", duration = "0"] = word.split(",");
|
||||
const endTime = currentTime + Number.parseInt(duration, 10);
|
||||
words.push({
|
||||
word: text,
|
||||
romanWord: "",
|
||||
startTime: currentTime,
|
||||
endTime,
|
||||
obscene: false,
|
||||
});
|
||||
currentTime = endTime;
|
||||
}
|
||||
|
||||
return {
|
||||
words,
|
||||
startTime,
|
||||
endTime: currentTime + 3000,
|
||||
translatedLyric: "",
|
||||
romanLyric: "",
|
||||
isBG: false,
|
||||
isDuet: false,
|
||||
...otherParams,
|
||||
};
|
||||
}
|
||||
|
||||
async function parseLyricSource(source: string): Promise<LyricLine[]> {
|
||||
const trimmedSource = source.trim();
|
||||
if (!trimmedSource) return [];
|
||||
if (trimmedSource === "bug") {
|
||||
return [
|
||||
buildDemoLyricLine(
|
||||
"Apple ,750|Music ,500|Like ,500|Ly,400|ri,500|cs ,250",
|
||||
1000,
|
||||
),
|
||||
buildDemoLyricLine("BG ,750|Lyrics ,1000", 2000, { isBG: true }),
|
||||
buildDemoLyricLine("Next ,1000|Lyrics,1000", 2500),
|
||||
];
|
||||
}
|
||||
|
||||
const response = await fetch(trimmedSource);
|
||||
if (!response.ok) {
|
||||
throw new Error(`歌词加载失败:${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const content = await response.text();
|
||||
const sourceName = getSourceName(trimmedSource, player.source.lyricName);
|
||||
|
||||
if (sourceName.endsWith(".ttml")) return parseTTML(content).lines;
|
||||
if (sourceName.endsWith(".alrc")) return parseLrcA2(content);
|
||||
if (sourceName.endsWith(".lrc")) {
|
||||
return hasLrcA2Timestamps(content)
|
||||
? parseLrcA2(content)
|
||||
: parseLrc(content);
|
||||
}
|
||||
if (sourceName.endsWith(".yrc")) return parseYrc(content);
|
||||
if (sourceName.endsWith(".lys")) return parseLys(content);
|
||||
if (sourceName.endsWith(".lyl")) return parseLyl(content);
|
||||
if (sourceName.endsWith(".lqe")) return parseLqe(content);
|
||||
if (sourceName.endsWith(".qrc")) return parseQrc(content);
|
||||
if (sourceName.endsWith(".eslrc")) return parseEslrc(content);
|
||||
|
||||
throw new Error("不支持的歌词格式");
|
||||
}
|
||||
|
||||
async function loadLyric(): Promise<void> {
|
||||
const lyricPlayer = lyricPlayerRef.value;
|
||||
if (!lyricPlayer) return;
|
||||
|
||||
const revision = ++lyricLoadRevision;
|
||||
player.setLyricLoading(true);
|
||||
player.setLyricError("");
|
||||
|
||||
try {
|
||||
const lines = await parseLyricSource(player.source.lyricUrl);
|
||||
if (revision !== lyricLoadRevision) return;
|
||||
|
||||
const currentTime = Math.round(player.audio.currentTime * 1000);
|
||||
lyricPlayer.setLyricLines(lines, currentTime);
|
||||
lyricPlayer.setCurrentTime(currentTime, true);
|
||||
backgroundRuntime.setHasLyric(lines.length > 0);
|
||||
applyLyricSettings();
|
||||
} catch (error) {
|
||||
if (revision !== lyricLoadRevision) return;
|
||||
lyricPlayer.setLyricLines([]);
|
||||
backgroundRuntime.setHasLyric(false);
|
||||
player.setLyricError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
} finally {
|
||||
if (revision === lyricLoadRevision) player.setLyricLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function applyMusicSource(): void {
|
||||
audioRuntime.setSource(player.source.musicUrl);
|
||||
}
|
||||
|
||||
function applyPlayback(playing: boolean): void {
|
||||
const lyricPlayer = lyricPlayerRef.value;
|
||||
if (!playing) {
|
||||
lyricPlayer?.pause();
|
||||
void audioRuntime.setPlaying(false);
|
||||
return;
|
||||
}
|
||||
|
||||
lyricPlayer?.resume();
|
||||
void audioRuntime.setPlaying(true);
|
||||
}
|
||||
|
||||
function seekCoreToStoreTime(): void {
|
||||
const currentTime = player.audio.currentTime;
|
||||
audioRuntime.seek(currentTime);
|
||||
lyricPlayerRef.value?.setCurrentTime(Math.round(currentTime * 1000), true);
|
||||
}
|
||||
|
||||
function startFrameLoop(): void {
|
||||
const onFrame = (time: number) => {
|
||||
if (lastFrameTime === -1) lastFrameTime = time;
|
||||
const delta = time - lastFrameTime;
|
||||
const lyricPlayer = lyricPlayerRef.value;
|
||||
|
||||
if (!audioRuntime.isPaused) {
|
||||
const currentTime = audioRuntime.currentTime;
|
||||
player.syncCurrentTime(currentTime);
|
||||
lyricPlayer?.setCurrentTime(Math.round(currentTime * 1000));
|
||||
}
|
||||
|
||||
lyricPlayer?.update(delta);
|
||||
lastFrameTime = time;
|
||||
frameId = requestAnimationFrame(onFrame);
|
||||
};
|
||||
|
||||
frameId = requestAnimationFrame(onFrame);
|
||||
}
|
||||
|
||||
function stopFrameLoop(): void {
|
||||
if (frameId) cancelAnimationFrame(frameId);
|
||||
frameId = 0;
|
||||
lastFrameTime = -1;
|
||||
}
|
||||
|
||||
function onLineClick(event: Event): void {
|
||||
const lineEvent = event as LyricLineMouseEvent;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
player.seek(lineEvent.line.getLine().startTime / 1000);
|
||||
}
|
||||
|
||||
function isEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
const tagName = target.tagName.toLowerCase();
|
||||
return (
|
||||
tagName === "input" ||
|
||||
tagName === "textarea" ||
|
||||
tagName === "select" ||
|
||||
target.isContentEditable
|
||||
);
|
||||
}
|
||||
|
||||
function onGlobalKeyDown(event: KeyboardEvent): void {
|
||||
if (event.defaultPrevented || isEditableTarget(event.target)) return;
|
||||
|
||||
if (event.code === "Space") {
|
||||
event.preventDefault();
|
||||
player.togglePlayback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
player.seek(player.audio.currentTime - 5);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
player.seek(player.audio.currentTime + 5);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const host = playerEl.value;
|
||||
if (!host) return;
|
||||
|
||||
audioRuntime.attachStore(player);
|
||||
audioRuntime.mount(host);
|
||||
|
||||
const lyricPlayer = new DomLyricPlayer();
|
||||
lyricPlayer.addEventListener("line-click", onLineClick);
|
||||
host.appendChild(lyricPlayer.getElement());
|
||||
lyricPlayerRef.value = lyricPlayer;
|
||||
|
||||
mountBackground();
|
||||
applyLyricSettings();
|
||||
applyMusicSource();
|
||||
applyPlayback(player.audio.playing);
|
||||
void loadLyric();
|
||||
startFrameLoop();
|
||||
window.addEventListener("keydown", onGlobalKeyDown);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopFrameLoop();
|
||||
window.removeEventListener("keydown", onGlobalKeyDown);
|
||||
|
||||
lyricPlayerRef.value?.removeEventListener("line-click", onLineClick);
|
||||
lyricPlayerRef.value?.dispose();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => player.source.musicUrl,
|
||||
() => applyMusicSource(),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [
|
||||
player.source.lyricUrl,
|
||||
player.source.lyricName,
|
||||
player.source.lyricRevision,
|
||||
],
|
||||
() => void loadLyric(),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [
|
||||
player.source.albumUrl,
|
||||
player.source.albumName,
|
||||
player.source.albumRevision,
|
||||
],
|
||||
() => void backgroundRuntime.loadAlbum(player),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => player.audio.playing,
|
||||
(playing) => applyPlayback(playing),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => player.audio.seekRevision,
|
||||
() => seekCoreToStoreTime(),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => player.background.renderer,
|
||||
() => mountBackground(),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [
|
||||
player.background.fps,
|
||||
player.background.scale,
|
||||
player.background.flowSpeed,
|
||||
player.background.staticMode,
|
||||
player.background.playing,
|
||||
],
|
||||
() => backgroundRuntime.applySettings(player),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [
|
||||
player.lyric.fadeWidth,
|
||||
player.lyric.enableBlur,
|
||||
player.lyric.enableSpring,
|
||||
player.lyric.verticalSpring.mass,
|
||||
player.lyric.verticalSpring.damping,
|
||||
player.lyric.verticalSpring.stiffness,
|
||||
player.lyric.verticalSpring.soft,
|
||||
player.lyric.scaleSpring.mass,
|
||||
player.lyric.scaleSpring.damping,
|
||||
player.lyric.scaleSpring.stiffness,
|
||||
player.lyric.scaleSpring.soft,
|
||||
],
|
||||
() => applyLyricSettings(),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarTrigger
|
||||
class="z-1 absolute m-3.5 text-white hover:bg-white/25! hover:text-white"
|
||||
/>
|
||||
<main
|
||||
ref="playerEl"
|
||||
id="player"
|
||||
class="absolute top-0 right-0 bottom-0 left-0 overflow-hidden bg-black text-white"
|
||||
:style="{
|
||||
fontFamily: player.lyric.fontFamily || undefined,
|
||||
fontWeight: player.lyric.fontWeight
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from "@vueuse/core";
|
||||
import { ContrastIcon, MoonIcon, SunIcon } from "lucide-vue-next";
|
||||
import { computed } from "vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
const { store } = useColorMode();
|
||||
const switchToLight = () => (store.value = "light");
|
||||
const switchToDark = () => (store.value = "dark");
|
||||
const switchToSystem = () => (store.value = "auto");
|
||||
|
||||
const isLight = computed(() => store.value === "light");
|
||||
const isDark = computed(() => store.value === "dark");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline">
|
||||
<SunIcon v-if="isLight" />
|
||||
<MoonIcon v-else-if="isDark" />
|
||||
<ContrastIcon v-else />
|
||||
<span class="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem @click="switchToLight"> 亮色 </DropdownMenuItem>
|
||||
<DropdownMenuItem @click="switchToDark"> 暗色 </DropdownMenuItem>
|
||||
<DropdownMenuItem @click="switchToSystem"> 跟随系统 </DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import amllLogoSvg from "@/assets/amll-logo.svg";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarHeader,
|
||||
type SidebarProps,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import AudioPlayer from "./AudioController.vue";
|
||||
import BackgroundController from "./BackgroundController.vue";
|
||||
import LyricController from "./LyricController.vue";
|
||||
import SourceController from "./SourceController.vue";
|
||||
|
||||
const props = withDefaults(defineProps<SidebarProps>(), {});
|
||||
const coreVersion = __AMLL_CORE_VERSION__;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Sidebar v-bind="props">
|
||||
<SidebarHeader class="border-b border-sidebar-border p-3">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-md bg-primary text-sidebar-primary-foreground overflow-hidden"
|
||||
>
|
||||
<img :src="amllLogoSvg">
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 leading-none">
|
||||
<span class="font-medium"> AMLL Playground </span>
|
||||
<span class="text-xs/3 opacity-75"> Core v{{ coreVersion }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
<SidebarContent class="p-3">
|
||||
<Tabs default-value="source">
|
||||
<TabsList class="flex w-[unset]">
|
||||
<TabsTrigger value="source"> 源 </TabsTrigger>
|
||||
<TabsTrigger value="lyric"> 歌词 </TabsTrigger>
|
||||
<TabsTrigger value="background"> 背景 </TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="source"> <SourceController /> </TabsContent>
|
||||
<TabsContent value="background"> <BackgroundController /> </TabsContent>
|
||||
<TabsContent value="lyric"> <LyricController /> </TabsContent>
|
||||
</Tabs>
|
||||
</SidebarContent>
|
||||
<AudioPlayer />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
[data-slot="sidebar-container"],
|
||||
[data-slot="sidebar-gap"] {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
[data-slot="sidebar"][data-collapsible="offcanvas"]
|
||||
+ [data-slot="sidebar-inset"] {
|
||||
margin: 0;
|
||||
padding: calc(var(--spacing) * 2);
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
FileAudioIcon,
|
||||
FileTextIcon,
|
||||
ImageIcon,
|
||||
MusicIcon,
|
||||
RefreshCwIcon,
|
||||
TextAlignStartIcon,
|
||||
} from "lucide-vue-next";
|
||||
import { computed } from "vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { extractCoverBlob } from "@/lib/extract-cover";
|
||||
import { usePlayerStore } from "@/stores/player";
|
||||
|
||||
const player = usePlayerStore();
|
||||
|
||||
const lyric = computed({
|
||||
get: () => player.source.lyricUrl,
|
||||
set: (value) => player.setLyricUrl(String(value)),
|
||||
});
|
||||
const music = computed({
|
||||
get: () => player.source.musicUrl,
|
||||
set: (value) => player.setMusicUrl(String(value)),
|
||||
});
|
||||
const album = computed({
|
||||
get: () => player.source.albumUrl,
|
||||
set: (value) => player.setAlbumUrl(String(value)),
|
||||
});
|
||||
|
||||
function openFile(accept: string, onFile: (file: File) => void): void {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = accept;
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0];
|
||||
if (file) onFile(file);
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
async function openLocalMusicFile(file: File): Promise<void> {
|
||||
player.setLocalMusicFile(file);
|
||||
try {
|
||||
const cover = await extractCoverBlob(file);
|
||||
if (cover) {
|
||||
player.setExtractedAlbumBlob(cover, `${file.name} cover`);
|
||||
} else {
|
||||
player.clearExtractedAlbum();
|
||||
}
|
||||
} catch {
|
||||
player.clearExtractedAlbum();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4 py-1">
|
||||
<section class="space-y-2.5">
|
||||
<h3 class="text-sm font-bold flex items-center gap-1">
|
||||
<TextAlignStartIcon :size="16" />
|
||||
歌词
|
||||
</h3>
|
||||
<Input id="lyric-url" v-model="lyric" placeholder="歌词文件 URI" />
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="openFile('.ttml,.lrc,.alrc,.yrc,.lys,.lyl,.lqe,.qrc,.eslrc', player.setLocalLyricFile)"
|
||||
>
|
||||
<FileTextIcon />
|
||||
本地歌词
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="player.reloadLyric">
|
||||
<RefreshCwIcon />
|
||||
刷新歌词
|
||||
</Button>
|
||||
</div>
|
||||
<p v-if="player.lyric.error" class="text-xs text-destructive">
|
||||
{{ player.lyric.error }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section class="space-y-2.5">
|
||||
<h3 class="text-sm font-bold flex items-center gap-1">
|
||||
<MusicIcon :size="16" />
|
||||
音频
|
||||
</h3>
|
||||
<Input id="music-url" v-model="music" placeholder="音频文件 URI" />
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="openFile('audio/*', openLocalMusicFile)"
|
||||
>
|
||||
<FileAudioIcon />
|
||||
打开本地歌曲
|
||||
</Button>
|
||||
<p v-if="player.audio.error" class="text-xs text-destructive">
|
||||
{{ player.audio.error }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section class="space-y-2.5">
|
||||
<h3 class="text-sm font-bold flex items-center gap-1">
|
||||
<ImageIcon :size="16" />
|
||||
专辑图
|
||||
</h3>
|
||||
<Input id="album-url" v-model="album" placeholder="专辑图片 URI" />
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="openFile('image/*,video/*', player.setLocalAlbumFile)"
|
||||
>
|
||||
<ImageIcon />
|
||||
本地图片
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="player.reloadAlbum">
|
||||
<RefreshCwIcon />
|
||||
刷新图片
|
||||
</Button>
|
||||
</div>
|
||||
<p v-if="player.background.error" class="text-xs text-destructive">
|
||||
{{ player.background.error }}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
22
amll-local/packages/playground/core/src/components/ui/button-group/ButtonGroup.vue
vendored
Normal file
22
amll-local/packages/playground/core/src/components/ui/button-group/ButtonGroup.vue
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonGroupVariants } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonGroupVariants } from '.'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
orientation?: ButtonGroupVariants['orientation']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
:data-orientation="props.orientation"
|
||||
:class="cn(buttonGroupVariants({ orientation: props.orientation }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
24
amll-local/packages/playground/core/src/components/ui/button-group/ButtonGroupSeparator.vue
vendored
Normal file
24
amll-local/packages/playground/core/src/components/ui/button-group/ButtonGroupSeparator.vue
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { SeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
orientation: 'vertical',
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
v-bind="delegatedProps"
|
||||
:orientation="props.orientation"
|
||||
:class="cn(
|
||||
'bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto',
|
||||
props.class,
|
||||
)"
|
||||
/>
|
||||
</template>
|
||||
29
amll-local/packages/playground/core/src/components/ui/button-group/ButtonGroupText.vue
vendored
Normal file
29
amll-local/packages/playground/core/src/components/ui/button-group/ButtonGroupText.vue
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonGroupVariants } from '.'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
class?: HTMLAttributes['class']
|
||||
orientation?: ButtonGroupVariants['orientation']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'div',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
:data-orientation="props.orientation"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn('bg-muted gap-2 rounded-md border px-2.5 text-sm font-medium shadow-xs [&_svg:not([class*=size-])]:size-4 flex items-center [&_svg]:pointer-events-none', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
25
amll-local/packages/playground/core/src/components/ui/button-group/index.ts
vendored
Normal file
25
amll-local/packages/playground/core/src/components/ui/button-group/index.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as ButtonGroup } from './ButtonGroup.vue'
|
||||
export { default as ButtonGroupSeparator } from './ButtonGroupSeparator.vue'
|
||||
export { default as ButtonGroupText } from './ButtonGroupText.vue'
|
||||
|
||||
export const buttonGroupVariants = cva(
|
||||
'has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 [&>[data-slot=select-trigger]:not([class*=\'w-\'])]:w-fit [&>input]:flex-1',
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
'[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
|
||||
vertical:
|
||||
'[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type ButtonGroupVariants = VariantProps<typeof buttonGroupVariants>
|
||||
31
amll-local/packages/playground/core/src/components/ui/button/Button.vue
vendored
Normal file
31
amll-local/packages/playground/core/src/components/ui/button/Button.vue
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonVariants } from '.'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '.'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants['variant']
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'button',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="button"
|
||||
:data-variant="variant"
|
||||
:data-size="size"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
35
amll-local/packages/playground/core/src/components/ui/button/index.ts
vendored
Normal file
35
amll-local/packages/playground/core/src/components/ui/button/index.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Button } from './Button.vue'
|
||||
|
||||
export const buttonVariants = cva(
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*=size-])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
outline: 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
||||
ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
|
||||
destructive: 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
'default': 'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
'xs': 'h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3',
|
||||
'sm': 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5',
|
||||
'lg': 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
'icon': 'size-9',
|
||||
'icon-xs': 'size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*=size-])]:size-3',
|
||||
'icon-sm': 'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||
19
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenu.vue
vendored
Normal file
19
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenu.vue
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from 'reka-ui'
|
||||
import { DropdownMenuRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>()
|
||||
const emits = defineEmits<DropdownMenuRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="dropdown-menu"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
43
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
vendored
Normal file
43
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from 'reka-ui'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CheckIcon } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuCheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*=size-])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span
|
||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
<DropdownMenuItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<CheckIcon />
|
||||
</slot>
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuCheckboxItem>
|
||||
</template>
|
||||
40
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuContent.vue
vendored
Normal file
40
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuContent.vue
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
align: 'start',
|
||||
sideOffset: 4,
|
||||
},
|
||||
)
|
||||
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
data-slot="dropdown-menu-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="cn('data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-md p-1 shadow-md ring-1 duration-100 cn-menu-translucent z-50 max-h-(--reka-dropdown-menu-content-available-height) w-(--reka-dropdown-menu-trigger-width) origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto data-[state=closed]:overflow-hidden', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</template>
|
||||
15
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuGroup.vue
vendored
Normal file
15
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuGroup.vue
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuGroupProps } from 'reka-ui'
|
||||
import { DropdownMenuGroup } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuGroupProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuGroup
|
||||
data-slot="dropdown-menu-group"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuGroup>
|
||||
</template>
|
||||
31
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuItem.vue
vendored
Normal file
31
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuItem.vue
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DropdownMenuItem, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<DropdownMenuItemProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
inset?: boolean
|
||||
variant?: 'default' | 'destructive'
|
||||
}>(), {
|
||||
variant: 'default',
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'inset', 'variant', 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuItem
|
||||
data-slot="dropdown-menu-item"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
:data-variant="variant"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*=size-])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
23
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuLabel.vue
vendored
Normal file
23
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuLabel.vue
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuLabelProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DropdownMenuLabel, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'inset')
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuLabel>
|
||||
</template>
|
||||
21
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
vendored
Normal file
21
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from 'reka-ui'
|
||||
import {
|
||||
DropdownMenuRadioGroup,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>()
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuRadioGroup>
|
||||
</template>
|
||||
44
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
vendored
Normal file
44
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from 'reka-ui'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CheckIcon } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioItem,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const emits = defineEmits<DropdownMenuRadioItemEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*=size-])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span
|
||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<DropdownMenuItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<CheckIcon />
|
||||
</slot>
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuRadioItem>
|
||||
</template>
|
||||
23
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
vendored
Normal file
23
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
DropdownMenuSeparator,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuSeparatorProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSeparator
|
||||
data-slot="dropdown-menu-separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border -mx-1 my-1 h-px', props.class)"
|
||||
/>
|
||||
</template>
|
||||
17
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
vendored
Normal file
17
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
:class="cn('text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
18
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuSub.vue
vendored
Normal file
18
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuSub.vue
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from 'reka-ui'
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuSubProps>()
|
||||
const emits = defineEmits<DropdownMenuSubEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSub v-slot="slotProps" data-slot="dropdown-menu-sub" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</DropdownMenuSub>
|
||||
</template>
|
||||
27
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
vendored
Normal file
27
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
v-bind="forwarded"
|
||||
:class="cn('data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100 cn-menu-translucent z-50 origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuSubContent>
|
||||
</template>
|
||||
32
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
vendored
Normal file
32
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuSubTriggerProps } from 'reka-ui'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronRightIcon } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuSubTrigger,
|
||||
useForwardProps,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'inset')
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
:data-inset="inset ? '' : undefined"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn(
|
||||
'focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm px-2 py-1.5 text-sm data-inset:pl-8 [&_svg:not([class*=size-])]:size-4 flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRightIcon class="cn-rtl-flip ml-auto" />
|
||||
</DropdownMenuSubTrigger>
|
||||
</template>
|
||||
17
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
vendored
Normal file
17
amll-local/packages/playground/core/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuTriggerProps } from 'reka-ui'
|
||||
import { DropdownMenuTrigger, useForwardProps } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>()
|
||||
|
||||
const forwardedProps = useForwardProps(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuTrigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
</template>
|
||||
16
amll-local/packages/playground/core/src/components/ui/dropdown-menu/index.ts
vendored
Normal file
16
amll-local/packages/playground/core/src/components/ui/dropdown-menu/index.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
export { default as DropdownMenu } from './DropdownMenu.vue'
|
||||
|
||||
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'
|
||||
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'
|
||||
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'
|
||||
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'
|
||||
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'
|
||||
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'
|
||||
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'
|
||||
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'
|
||||
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'
|
||||
export { default as DropdownMenuSub } from './DropdownMenuSub.vue'
|
||||
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'
|
||||
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'
|
||||
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
|
||||
export { DropdownMenuPortal } from 'reka-ui'
|
||||
31
amll-local/packages/playground/core/src/components/ui/input/Input.vue
vendored
Normal file
31
amll-local/packages/playground/core/src/components/ui/input/Input.vue
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
defaultValue?: string | number
|
||||
modelValue?: string | number
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:modelValue', payload: string | number): void
|
||||
}>()
|
||||
|
||||
const modelValue = useVModel(props, 'modelValue', emits, {
|
||||
passive: true,
|
||||
defaultValue: props.defaultValue,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
v-model="modelValue"
|
||||
data-slot="input"
|
||||
:class="cn(
|
||||
'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] file:h-7 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
</template>
|
||||
1
amll-local/packages/playground/core/src/components/ui/input/index.ts
vendored
Normal file
1
amll-local/packages/playground/core/src/components/ui/input/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Input } from './Input.vue'
|
||||
26
amll-local/packages/playground/core/src/components/ui/label/Label.vue
vendored
Normal file
26
amll-local/packages/playground/core/src/components/ui/label/Label.vue
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { LabelProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Label } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Label
|
||||
data-slot="label"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</Label>
|
||||
</template>
|
||||
1
amll-local/packages/playground/core/src/components/ui/label/index.ts
vendored
Normal file
1
amll-local/packages/playground/core/src/components/ui/label/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Label } from './Label.vue'
|
||||
20
amll-local/packages/playground/core/src/components/ui/number-field/NumberField.vue
vendored
Normal file
20
amll-local/packages/playground/core/src/components/ui/number-field/NumberField.vue
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { NumberFieldRootEmits, NumberFieldRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { NumberFieldRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<NumberFieldRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<NumberFieldRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NumberFieldRoot v-slot="slotProps" v-bind="forwarded" :class="cn('grid gap-1.5', props.class)">
|
||||
<slot v-bind="slotProps" />
|
||||
</NumberFieldRoot>
|
||||
</template>
|
||||
14
amll-local/packages/playground/core/src/components/ui/number-field/NumberFieldContent.vue
vendored
Normal file
14
amll-local/packages/playground/core/src/components/ui/number-field/NumberFieldContent.vue
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('relative [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5 [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
23
amll-local/packages/playground/core/src/components/ui/number-field/NumberFieldDecrement.vue
vendored
Normal file
23
amll-local/packages/playground/core/src/components/ui/number-field/NumberFieldDecrement.vue
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { NumberFieldDecrementProps } from 'reka-ui'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { MinusIcon } from 'lucide-vue-next'
|
||||
import { NumberFieldDecrement, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<NumberFieldDecrementProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NumberFieldDecrement data-slot="decrement" v-bind="forwarded" :class="cn('absolute top-1/2 -translate-y-1/2 left-0 p-3 disabled:cursor-not-allowed disabled:opacity-20', props.class)">
|
||||
<slot>
|
||||
<MinusIcon class="h-4 w-4" />
|
||||
</slot>
|
||||
</NumberFieldDecrement>
|
||||
</template>
|
||||
23
amll-local/packages/playground/core/src/components/ui/number-field/NumberFieldIncrement.vue
vendored
Normal file
23
amll-local/packages/playground/core/src/components/ui/number-field/NumberFieldIncrement.vue
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { NumberFieldIncrementProps } from 'reka-ui'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { PlusIcon } from 'lucide-vue-next'
|
||||
import { NumberFieldIncrement, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<NumberFieldIncrementProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NumberFieldIncrement data-slot="increment" v-bind="forwarded" :class="cn('absolute top-1/2 -translate-y-1/2 right-0 disabled:cursor-not-allowed disabled:opacity-20 p-3', props.class)">
|
||||
<slot>
|
||||
<PlusIcon class="h-4 w-4" />
|
||||
</slot>
|
||||
</NumberFieldIncrement>
|
||||
</template>
|
||||
16
amll-local/packages/playground/core/src/components/ui/number-field/NumberFieldInput.vue
vendored
Normal file
16
amll-local/packages/playground/core/src/components/ui/number-field/NumberFieldInput.vue
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { NumberFieldInput } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NumberFieldInput
|
||||
data-slot="input"
|
||||
:class="cn('flex h-9 w-full rounded-md border border-input bg-transparent py-1 text-sm text-center shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)"
|
||||
/>
|
||||
</template>
|
||||
5
amll-local/packages/playground/core/src/components/ui/number-field/index.ts
vendored
Normal file
5
amll-local/packages/playground/core/src/components/ui/number-field/index.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as NumberField } from './NumberField.vue'
|
||||
export { default as NumberFieldContent } from './NumberFieldContent.vue'
|
||||
export { default as NumberFieldDecrement } from './NumberFieldDecrement.vue'
|
||||
export { default as NumberFieldIncrement } from './NumberFieldIncrement.vue'
|
||||
export { default as NumberFieldInput } from './NumberFieldInput.vue'
|
||||
19
amll-local/packages/playground/core/src/components/ui/popover/Popover.vue
vendored
Normal file
19
amll-local/packages/playground/core/src/components/ui/popover/Popover.vue
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverRootEmits, PopoverRootProps } from 'reka-ui'
|
||||
import { PopoverRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<PopoverRootProps>()
|
||||
const emits = defineEmits<PopoverRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="popover"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</PopoverRoot>
|
||||
</template>
|
||||
15
amll-local/packages/playground/core/src/components/ui/popover/PopoverAnchor.vue
vendored
Normal file
15
amll-local/packages/playground/core/src/components/ui/popover/PopoverAnchor.vue
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverAnchorProps } from 'reka-ui'
|
||||
import { PopoverAnchor } from 'reka-ui'
|
||||
|
||||
const props = defineProps<PopoverAnchorProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverAnchor
|
||||
data-slot="popover-anchor"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</PopoverAnchor>
|
||||
</template>
|
||||
45
amll-local/packages/playground/core/src/components/ui/popover/PopoverContent.vue
vendored
Normal file
45
amll-local/packages/playground/core/src/components/ui/popover/PopoverContent.vue
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverContentEmits, PopoverContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
align: 'center',
|
||||
sideOffset: 4,
|
||||
},
|
||||
)
|
||||
const emits = defineEmits<PopoverContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverPortal>
|
||||
<PopoverContent
|
||||
data-slot="popover-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-4 rounded-md p-4 text-sm shadow-md ring-1 duration-100 z-50 w-72 origin-(--reka-popover-content-transform-origin) outline-hidden',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</PopoverContent>
|
||||
</PopoverPortal>
|
||||
</template>
|
||||
17
amll-local/packages/playground/core/src/components/ui/popover/PopoverDescription.vue
vendored
Normal file
17
amll-local/packages/playground/core/src/components/ui/popover/PopoverDescription.vue
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
data-slot="popover-description"
|
||||
:class="cn('text-muted-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
17
amll-local/packages/playground/core/src/components/ui/popover/PopoverHeader.vue
vendored
Normal file
17
amll-local/packages/playground/core/src/components/ui/popover/PopoverHeader.vue
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="popover-header"
|
||||
:class="cn('flex flex-col gap-1 text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
amll-local/packages/playground/core/src/components/ui/popover/PopoverTitle.vue
vendored
Normal file
17
amll-local/packages/playground/core/src/components/ui/popover/PopoverTitle.vue
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="popover-title"
|
||||
:class="cn('font-medium cn-font-heading', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
15
amll-local/packages/playground/core/src/components/ui/popover/PopoverTrigger.vue
vendored
Normal file
15
amll-local/packages/playground/core/src/components/ui/popover/PopoverTrigger.vue
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverTriggerProps } from 'reka-ui'
|
||||
import { PopoverTrigger } from 'reka-ui'
|
||||
|
||||
const props = defineProps<PopoverTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverTrigger
|
||||
data-slot="popover-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</PopoverTrigger>
|
||||
</template>
|
||||
7
amll-local/packages/playground/core/src/components/ui/popover/index.ts
vendored
Normal file
7
amll-local/packages/playground/core/src/components/ui/popover/index.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export { default as Popover } from './Popover.vue'
|
||||
export { default as PopoverAnchor } from './PopoverAnchor.vue'
|
||||
export { default as PopoverContent } from './PopoverContent.vue'
|
||||
export { default as PopoverDescription } from './PopoverDescription.vue'
|
||||
export { default as PopoverHeader } from './PopoverHeader.vue'
|
||||
export { default as PopoverTitle } from './PopoverTitle.vue'
|
||||
export { default as PopoverTrigger } from './PopoverTrigger.vue'
|
||||
19
amll-local/packages/playground/core/src/components/ui/select/Select.vue
vendored
Normal file
19
amll-local/packages/playground/core/src/components/ui/select/Select.vue
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectRootEmits, SelectRootProps } from 'reka-ui'
|
||||
import { SelectRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<SelectRootProps>()
|
||||
const emits = defineEmits<SelectRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="select"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</SelectRoot>
|
||||
</template>
|
||||
58
amll-local/packages/playground/core/src/components/ui/select/SelectContent.vue
vendored
Normal file
58
amll-local/packages/playground/core/src/components/ui/select/SelectContent.vue
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectContentEmits, SelectContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
SelectContent,
|
||||
SelectPortal,
|
||||
SelectViewport,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SelectScrollDownButton, SelectScrollUpButton } from '.'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<SelectContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
position: 'item-aligned',
|
||||
align: 'center',
|
||||
},
|
||||
)
|
||||
const emits = defineEmits<SelectContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectPortal>
|
||||
<SelectContent
|
||||
data-slot="select-content"
|
||||
:data-align-trigger="position === 'item-aligned'"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="cn(
|
||||
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-md shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 cn-menu-translucent relative z-50 max-h-(--reka-select-content-available-height) origin-(--reka-select-content-transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none',
|
||||
position === 'popper'
|
||||
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectViewport
|
||||
:data-position="position"
|
||||
:class="cn(
|
||||
'data-[position=popper]:h-[var(--reka-select-trigger-height)] data-[position=popper]:w-full data-[position=popper]:min-w-[var(--reka-select-trigger-width)]',
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</SelectViewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectContent>
|
||||
</SelectPortal>
|
||||
</template>
|
||||
21
amll-local/packages/playground/core/src/components/ui/select/SelectGroup.vue
vendored
Normal file
21
amll-local/packages/playground/core/src/components/ui/select/SelectGroup.vue
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectGroupProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { SelectGroup } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectGroup
|
||||
data-slot="select-group"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('scroll-my-1 p-1', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</SelectGroup>
|
||||
</template>
|
||||
45
amll-local/packages/playground/core/src/components/ui/select/SelectItem.vue
vendored
Normal file
45
amll-local/packages/playground/core/src/components/ui/select/SelectItem.vue
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectItemProps } from 'reka-ui'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CheckIcon } from 'lucide-vue-next'
|
||||
import {
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
useForwardProps,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItem
|
||||
data-slot="select-item"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm [&_svg:not([class*=size-])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
||||
<SelectItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<CheckIcon class="pointer-events-none" />
|
||||
</slot>
|
||||
</SelectItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectItemText>
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
</SelectItem>
|
||||
</template>
|
||||
15
amll-local/packages/playground/core/src/components/ui/select/SelectItemText.vue
vendored
Normal file
15
amll-local/packages/playground/core/src/components/ui/select/SelectItemText.vue
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectItemTextProps } from 'reka-ui'
|
||||
import { SelectItemText } from 'reka-ui'
|
||||
|
||||
const props = defineProps<SelectItemTextProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItemText
|
||||
data-slot="select-item-text"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
</template>
|
||||
17
amll-local/packages/playground/core/src/components/ui/select/SelectLabel.vue
vendored
Normal file
17
amll-local/packages/playground/core/src/components/ui/select/SelectLabel.vue
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectLabelProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { SelectLabel } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectLabelProps & { class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectLabel
|
||||
data-slot="select-label"
|
||||
:class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</SelectLabel>
|
||||
</template>
|
||||
27
amll-local/packages/playground/core/src/components/ui/select/SelectScrollDownButton.vue
vendored
Normal file
27
amll-local/packages/playground/core/src/components/ui/select/SelectScrollDownButton.vue
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectScrollDownButtonProps } from 'reka-ui'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronDownIcon } from 'lucide-vue-next'
|
||||
import { SelectScrollDownButton, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*=size-])]:size-4', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<ChevronDownIcon />
|
||||
</slot>
|
||||
</SelectScrollDownButton>
|
||||
</template>
|
||||
27
amll-local/packages/playground/core/src/components/ui/select/SelectScrollUpButton.vue
vendored
Normal file
27
amll-local/packages/playground/core/src/components/ui/select/SelectScrollUpButton.vue
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectScrollUpButtonProps } from 'reka-ui'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronUpIcon } from 'lucide-vue-next'
|
||||
import { SelectScrollUpButton, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*=size-])]:size-4', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<ChevronUpIcon />
|
||||
</slot>
|
||||
</SelectScrollUpButton>
|
||||
</template>
|
||||
19
amll-local/packages/playground/core/src/components/ui/select/SelectSeparator.vue
vendored
Normal file
19
amll-local/packages/playground/core/src/components/ui/select/SelectSeparator.vue
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectSeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { SelectSeparator } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectSeparator
|
||||
data-slot="select-separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border -mx-1 my-1 h-px pointer-events-none', props.class)"
|
||||
/>
|
||||
</template>
|
||||
34
amll-local/packages/playground/core/src/components/ui/select/SelectTrigger.vue
vendored
Normal file
34
amll-local/packages/playground/core/src/components/ui/select/SelectTrigger.vue
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectTriggerProps } from 'reka-ui'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronDownIcon } from 'lucide-vue-next'
|
||||
import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'], size?: 'sm' | 'default' }>(),
|
||||
{ size: 'default' },
|
||||
)
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'size')
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectTrigger
|
||||
data-slot="select-trigger"
|
||||
:data-size="size"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn(
|
||||
'border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-md border bg-transparent py-2 pr-2 pl-2.5 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*=size-])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
<SelectIcon as-child>
|
||||
<ChevronDownIcon class="text-muted-foreground size-4 pointer-events-none" />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
</template>
|
||||
15
amll-local/packages/playground/core/src/components/ui/select/SelectValue.vue
vendored
Normal file
15
amll-local/packages/playground/core/src/components/ui/select/SelectValue.vue
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectValueProps } from 'reka-ui'
|
||||
import { SelectValue } from 'reka-ui'
|
||||
|
||||
const props = defineProps<SelectValueProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectValue
|
||||
data-slot="select-value"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</SelectValue>
|
||||
</template>
|
||||
11
amll-local/packages/playground/core/src/components/ui/select/index.ts
vendored
Normal file
11
amll-local/packages/playground/core/src/components/ui/select/index.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export { default as Select } from './Select.vue'
|
||||
export { default as SelectContent } from './SelectContent.vue'
|
||||
export { default as SelectGroup } from './SelectGroup.vue'
|
||||
export { default as SelectItem } from './SelectItem.vue'
|
||||
export { default as SelectItemText } from './SelectItemText.vue'
|
||||
export { default as SelectLabel } from './SelectLabel.vue'
|
||||
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue'
|
||||
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue'
|
||||
export { default as SelectSeparator } from './SelectSeparator.vue'
|
||||
export { default as SelectTrigger } from './SelectTrigger.vue'
|
||||
export { default as SelectValue } from './SelectValue.vue'
|
||||
29
amll-local/packages/playground/core/src/components/ui/separator/Separator.vue
vendored
Normal file
29
amll-local/packages/playground/core/src/components/ui/separator/Separator.vue
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { SeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Separator } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<
|
||||
SeparatorProps & { class?: HTMLAttributes['class'] }
|
||||
>(), {
|
||||
orientation: 'horizontal',
|
||||
decorative: true,
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Separator
|
||||
data-slot="separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
1
amll-local/packages/playground/core/src/components/ui/separator/index.ts
vendored
Normal file
1
amll-local/packages/playground/core/src/components/ui/separator/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Separator } from './Separator.vue'
|
||||
19
amll-local/packages/playground/core/src/components/ui/sheet/Sheet.vue
vendored
Normal file
19
amll-local/packages/playground/core/src/components/ui/sheet/Sheet.vue
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
|
||||
import { DialogRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="sheet"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</DialogRoot>
|
||||
</template>
|
||||
15
amll-local/packages/playground/core/src/components/ui/sheet/SheetClose.vue
vendored
Normal file
15
amll-local/packages/playground/core/src/components/ui/sheet/SheetClose.vue
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogCloseProps } from 'reka-ui'
|
||||
import { DialogClose } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose
|
||||
data-slot="sheet-close"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
61
amll-local/packages/playground/core/src/components/ui/sheet/SheetContent.vue
vendored
Normal file
61
amll-local/packages/playground/core/src/components/ui/sheet/SheetContent.vue
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { XIcon } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import SheetOverlay from './SheetOverlay.vue'
|
||||
|
||||
interface SheetContentProps extends DialogContentProps {
|
||||
class?: HTMLAttributes['class']
|
||||
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||
showCloseButton?: boolean
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||
side: 'right',
|
||||
showCloseButton: true,
|
||||
})
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class', 'side', 'showCloseButton')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<SheetOverlay />
|
||||
<DialogContent
|
||||
data-slot="sheet-content"
|
||||
:data-side="side"
|
||||
:class="cn('bg-popover text-popover-foreground fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10', props.class)"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
v-if="showCloseButton"
|
||||
data-slot="sheet-close"
|
||||
as-child
|
||||
>
|
||||
<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm">
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
21
amll-local/packages/playground/core/src/components/ui/sheet/SheetDescription.vue
vendored
Normal file
21
amll-local/packages/playground/core/src/components/ui/sheet/SheetDescription.vue
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogDescriptionProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DialogDescription } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogDescription
|
||||
data-slot="sheet-description"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</DialogDescription>
|
||||
</template>
|
||||
15
amll-local/packages/playground/core/src/components/ui/sheet/SheetFooter.vue
vendored
Normal file
15
amll-local/packages/playground/core/src/components/ui/sheet/SheetFooter.vue
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
:class="cn('gap-2 p-4 mt-auto flex flex-col', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
15
amll-local/packages/playground/core/src/components/ui/sheet/SheetHeader.vue
vendored
Normal file
15
amll-local/packages/playground/core/src/components/ui/sheet/SheetHeader.vue
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
:class="cn('gap-1.5 p-4 flex flex-col', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
21
amll-local/packages/playground/core/src/components/ui/sheet/SheetOverlay.vue
vendored
Normal file
21
amll-local/packages/playground/core/src/components/ui/sheet/SheetOverlay.vue
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogOverlayProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DialogOverlay } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogOverlay
|
||||
data-slot="sheet-overlay"
|
||||
:class="cn('bg-black/10 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50 duration-100 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0', props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</DialogOverlay>
|
||||
</template>
|
||||
21
amll-local/packages/playground/core/src/components/ui/sheet/SheetTitle.vue
vendored
Normal file
21
amll-local/packages/playground/core/src/components/ui/sheet/SheetTitle.vue
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTitleProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { DialogTitle } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTitle
|
||||
data-slot="sheet-title"
|
||||
:class="cn('text-foreground font-medium cn-font-heading', props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</DialogTitle>
|
||||
</template>
|
||||
15
amll-local/packages/playground/core/src/components/ui/sheet/SheetTrigger.vue
vendored
Normal file
15
amll-local/packages/playground/core/src/components/ui/sheet/SheetTrigger.vue
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { DialogTriggerProps } from 'reka-ui'
|
||||
import { DialogTrigger } from 'reka-ui'
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger
|
||||
data-slot="sheet-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
||||
8
amll-local/packages/playground/core/src/components/ui/sheet/index.ts
vendored
Normal file
8
amll-local/packages/playground/core/src/components/ui/sheet/index.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
export { default as Sheet } from './Sheet.vue'
|
||||
export { default as SheetClose } from './SheetClose.vue'
|
||||
export { default as SheetContent } from './SheetContent.vue'
|
||||
export { default as SheetDescription } from './SheetDescription.vue'
|
||||
export { default as SheetFooter } from './SheetFooter.vue'
|
||||
export { default as SheetHeader } from './SheetHeader.vue'
|
||||
export { default as SheetTitle } from './SheetTitle.vue'
|
||||
export { default as SheetTrigger } from './SheetTrigger.vue'
|
||||
100
amll-local/packages/playground/core/src/components/ui/sidebar/Sidebar.vue
vendored
Normal file
100
amll-local/packages/playground/core/src/components/ui/sidebar/Sidebar.vue
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { SidebarProps } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Sheet, SheetContent } from '@/components/ui/sheet'
|
||||
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
|
||||
import SheetHeader from '@/components/ui/sheet/SheetHeader.vue'
|
||||
import SheetTitle from '@/components/ui/sheet/SheetTitle.vue'
|
||||
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<SidebarProps>(), {
|
||||
side: 'left',
|
||||
variant: 'sidebar',
|
||||
collapsible: 'offcanvas',
|
||||
})
|
||||
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="collapsible === 'none'"
|
||||
data-slot="sidebar"
|
||||
:class="cn('bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', props.class)"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
:side="side"
|
||||
class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
:style="{
|
||||
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||
}"
|
||||
>
|
||||
<SheetHeader class="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="group peer text-sidebar-foreground hidden md:block"
|
||||
data-slot="sidebar"
|
||||
:data-state="state"
|
||||
:data-collapsible="state === 'collapsed' ? collapsible : ''"
|
||||
:data-variant="variant"
|
||||
:data-side="side"
|
||||
>
|
||||
<!-- This is what handles the sidebar gap on desktop -->
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
:class="cn(
|
||||
'transition-[width] duration-200 ease-linear relative w-(--sidebar-width) bg-transparent',
|
||||
'group-data-[collapsible=offcanvas]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||
)"
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
:data-side="side"
|
||||
:class="cn(
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||
side === 'left'
|
||||
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||
props.class,
|
||||
)"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
class="bg-sidebar group-data-[variant=floating]:ring-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 flex size-full flex-col"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarContent.vue
vendored
Normal file
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarContent.vue
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<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="sidebar-content"
|
||||
data-sidebar="content"
|
||||
:class="cn('no-scrollbar gap-2 flex min-h-0 flex-1 flex-col overflow-auto group-data-[collapsible=icon]:overflow-hidden', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarFooter.vue
vendored
Normal file
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarFooter.vue
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<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="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
:class="cn('gap-2 p-2 flex flex-col', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarGroup.vue
vendored
Normal file
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarGroup.vue
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<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="sidebar-group"
|
||||
data-sidebar="group"
|
||||
:class="cn('p-2 relative flex w-full min-w-0 flex-col', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
25
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarGroupAction.vue
vendored
Normal file
25
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarGroupAction.vue
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<PrimitiveProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarGroupContent.vue
vendored
Normal file
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarGroupContent.vue
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<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="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
:class="cn('text-sm w-full', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
24
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarGroupLabel.vue
vendored
Normal file
24
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarGroupLabel.vue
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<PrimitiveProps & {
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(
|
||||
'text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs font-medium transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0',
|
||||
props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarHeader.vue
vendored
Normal file
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarHeader.vue
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<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="sidebar-header"
|
||||
data-sidebar="header"
|
||||
:class="cn('gap-2 p-2 flex flex-col', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
19
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarInput.vue
vendored
Normal file
19
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarInput.vue
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
:class="cn('bg-background h-8 w-full shadow-none', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Input>
|
||||
</template>
|
||||
20
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarInset.vue
vendored
Normal file
20
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarInset.vue
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
:class="cn(
|
||||
'bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 relative flex w-full flex-1 flex-col',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</main>
|
||||
</template>
|
||||
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenu.vue
vendored
Normal file
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenu.vue
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
:class="cn('gap-1 flex w-full min-w-0 flex-col', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ul>
|
||||
</template>
|
||||
30
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuAction.vue
vendored
Normal file
30
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuAction.vue
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<PrimitiveProps & {
|
||||
showOnHover?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>(), {
|
||||
as: 'button',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
:class="cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 aspect-square w-5 rounded-md p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0',
|
||||
showOnHover
|
||||
&& 'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0',
|
||||
props.class,
|
||||
)"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
21
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuBadge.vue
vendored
Normal file
21
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuBadge.vue
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<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="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
:class="cn(
|
||||
'text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 h-5 min-w-5 rounded-md px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 flex items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
48
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuButton.vue
vendored
Normal file
48
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuButton.vue
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
import type { SidebarMenuButtonProps } from './SidebarMenuButtonChild.vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import SidebarMenuButtonChild from './SidebarMenuButtonChild.vue'
|
||||
import { useSidebar } from './utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<SidebarMenuButtonProps & {
|
||||
tooltip?: string | Component
|
||||
}>(), {
|
||||
as: 'button',
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
})
|
||||
|
||||
const { isMobile, state } = useSidebar()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'tooltip')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...$attrs }">
|
||||
<slot />
|
||||
</SidebarMenuButtonChild>
|
||||
|
||||
<Tooltip v-else>
|
||||
<TooltipTrigger as-child>
|
||||
<SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...$attrs }">
|
||||
<slot />
|
||||
</SidebarMenuButtonChild>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
:hidden="state !== 'collapsed' || isMobile"
|
||||
>
|
||||
<template v-if="typeof tooltip === 'string'">
|
||||
{{ tooltip }}
|
||||
</template>
|
||||
<component :is="tooltip" v-else />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</template>
|
||||
36
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuButtonChild.vue
vendored
Normal file
36
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuButtonChild.vue
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { SidebarMenuButtonVariants } from '.'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { sidebarMenuButtonVariants } from '.'
|
||||
|
||||
export interface SidebarMenuButtonProps extends PrimitiveProps {
|
||||
variant?: SidebarMenuButtonVariants['variant']
|
||||
size?: SidebarMenuButtonVariants['size']
|
||||
isActive?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SidebarMenuButtonProps>(), {
|
||||
as: 'button',
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
:data-size="size"
|
||||
:data-active="isActive"
|
||||
:class="cn(sidebarMenuButtonVariants({ variant, size }), props.class)"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuItem.vue
vendored
Normal file
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuItem.vue
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
:class="cn('group/menu-item relative', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
35
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuSkeleton.vue
vendored
Normal file
35
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuSkeleton.vue
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
const props = defineProps<{
|
||||
showIcon?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const width = computed(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
:class="cn('h-8 gap-2 rounded-md px-2 flex items-center', props.class)"
|
||||
>
|
||||
<Skeleton
|
||||
v-if="showIcon"
|
||||
class="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
|
||||
<Skeleton
|
||||
class="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
:style="{ '--skeleton-width': width }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
21
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuSub.vue
vendored
Normal file
21
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuSub.vue
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
:class="cn(
|
||||
'border-sidebar-border mx-3.5 translate-x-px gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden flex min-w-0 flex-col',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</ul>
|
||||
</template>
|
||||
32
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuSubButton.vue
vendored
Normal file
32
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuSubButton.vue
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<PrimitiveProps & {
|
||||
size?: 'sm' | 'md'
|
||||
isActive?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>(), {
|
||||
as: 'a',
|
||||
size: 'md',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:data-size="size"
|
||||
:data-active="isActive ? '' : undefined"
|
||||
:class="cn(
|
||||
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-sm data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuSubItem.vue
vendored
Normal file
18
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarMenuSubItem.vue
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
:class="cn('group/menu-sub-item relative', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
82
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarProvider.vue
vendored
Normal file
82
amll-local/packages/playground/core/src/components/ui/sidebar/SidebarProvider.vue
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes, Ref } from 'vue'
|
||||
import { defaultDocument, useEventListener, useMediaQuery, useVModel } from '@vueuse/core'
|
||||
import { TooltipProvider } from 'reka-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { provideSidebarContext, SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './utils'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>(), {
|
||||
defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`),
|
||||
open: undefined,
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
'update:open': [open: boolean]
|
||||
}>()
|
||||
|
||||
const isMobile = useMediaQuery('(max-width: 768px)')
|
||||
const openMobile = ref(false)
|
||||
|
||||
const open = useVModel(props, 'open', emits, {
|
||||
defaultValue: props.defaultOpen ?? false,
|
||||
passive: (props.open === undefined) as false,
|
||||
}) as Ref<boolean>
|
||||
|
||||
function setOpen(value: boolean) {
|
||||
open.value = value // emits('update:open', value)
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
}
|
||||
|
||||
function setOpenMobile(value: boolean) {
|
||||
openMobile.value = value
|
||||
}
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
function toggleSidebar() {
|
||||
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
|
||||
}
|
||||
|
||||
useEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
})
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = computed(() => open.value ? 'expanded' : 'collapsed')
|
||||
|
||||
provideSidebarContext({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
:style="{
|
||||
'--sidebar-width': SIDEBAR_WIDTH,
|
||||
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||
}"
|
||||
:class="cn('group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', props.class)"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user