forked from miao-moe/QZMusic_PC
fork(fix): Clone AMLL 并修复 BUG
- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork) - 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
37
amll-local/packages/playground/core-legacy/index.html
Normal file
37
amll-local/packages/playground/core-legacy/index.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>AMLL Core Test</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="src/test.ts" type="module" defer></script>
|
||||
<style>
|
||||
:root {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 100vw;
|
||||
width: 100vw;
|
||||
max-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #222;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#player {
|
||||
max-height: 100vh;
|
||||
height: 100vh;
|
||||
max-width: 100vw;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="player"></div>
|
||||
</body>
|
||||
</html>
|
||||
91
amll-local/packages/playground/core-legacy/mg.html
Normal file
91
amll-local/packages/playground/core-legacy/mg.html
Normal file
@@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>AMLL Core Test</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="src/mg-test.ts" type="module" defer></script>
|
||||
<style>
|
||||
:root {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 100vw;
|
||||
width: 100vw;
|
||||
max-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #222;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#bg {
|
||||
max-height: 100vh;
|
||||
height: 100vh;
|
||||
max-width: 100vw;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#result {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
width: 50em;
|
||||
height: 20em;
|
||||
border: solid 1px #3337;
|
||||
background-color: #1117;
|
||||
resize: both;
|
||||
color: white;
|
||||
font-family:
|
||||
"Fira Code", "SF Pro Display", "PingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
#result::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.dragger {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: solid 3px #8888;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dragger-handle {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: #4af;
|
||||
border: solid 2px #fff;
|
||||
cursor: pointer;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.dragger-line {
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
background-color: #4af8;
|
||||
transform-origin: left center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.active {
|
||||
border-color: #f44;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<canvas id="bg"></canvas>
|
||||
<textarea id="result" placeholder="控制点代码将会在这里显示"></textarea>
|
||||
</body>
|
||||
</html>
|
||||
32
amll-local/packages/playground/core-legacy/package.json
Normal file
32
amll-local/packages/playground/core-legacy/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@applemusic-like-lyrics/playground-core-legacy",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"nx": {
|
||||
"tags": [
|
||||
"playground"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applemusic-like-lyrics/core": "workspace:^",
|
||||
"@applemusic-like-lyrics/lyric": "workspace:^",
|
||||
"@applemusic-like-lyrics/ttml": "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",
|
||||
"lil-gui": "^0.21.0",
|
||||
"stats.js": "^0.17.0",
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/stats.js": "^0.17.4"
|
||||
}
|
||||
}
|
||||
504
amll-local/packages/playground/core-legacy/src/mg-test.ts
Normal file
504
amll-local/packages/playground/core-legacy/src/mg-test.ts
Normal file
@@ -0,0 +1,504 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* 调试 MG 渲染器的测试脚本
|
||||
*
|
||||
* @author SteveXMH
|
||||
*/
|
||||
|
||||
import { MeshGradientRenderer } from "@applemusic-like-lyrics/core";
|
||||
import GUI from "lil-gui";
|
||||
import Stats from "stats.js";
|
||||
|
||||
const debugValues = {
|
||||
image: new URL(location.href).searchParams.get("image") || "",
|
||||
controlPointSize: 4,
|
||||
subdivideDepth: 15,
|
||||
wireFrame: false,
|
||||
};
|
||||
|
||||
const canvas = document.getElementById("bg") as HTMLCanvasElement;
|
||||
const mgRenderer = new MeshGradientRenderer(canvas);
|
||||
mgRenderer.setManualControl(true);
|
||||
mgRenderer.setRenderScale(1);
|
||||
mgRenderer.setFPS(Number.POSITIVE_INFINITY);
|
||||
|
||||
function updateControlPointDraggers() {
|
||||
for (const el of document.querySelectorAll(".dragger")) {
|
||||
const x = Number.parseInt(el.getAttribute("x") ?? "", 10);
|
||||
const y = Number.parseInt(el.getAttribute("y") ?? "", 10);
|
||||
const point = mgRenderer.getControlPoint(x, y);
|
||||
if (point === undefined) return;
|
||||
const px = (point.location.x + 1) * 50;
|
||||
const py = (1 - point.location.y) * 50;
|
||||
(el as HTMLElement).style.left = `${px}%`;
|
||||
(el as HTMLElement).style.top = `${py}%`;
|
||||
|
||||
// Update handles
|
||||
const uHandle = el.querySelector(".u-handle") as HTMLElement;
|
||||
const uLine = el.querySelector(".u-line") as HTMLElement;
|
||||
if (uHandle && uLine) {
|
||||
const uLen = point.uScale * 50;
|
||||
const uAngle = -point.uRot;
|
||||
uHandle.style.left = `${10 + Math.cos(uAngle) * uLen}px`;
|
||||
uHandle.style.top = `${10 + Math.sin(uAngle) * uLen}px`;
|
||||
uLine.style.width = `${uLen}px`;
|
||||
uLine.style.transform = `rotate(${uAngle}rad)`;
|
||||
uLine.style.left = "10px";
|
||||
uLine.style.top = "10px";
|
||||
}
|
||||
|
||||
const vHandle = el.querySelector(".v-handle") as HTMLElement;
|
||||
const vLine = el.querySelector(".v-line") as HTMLElement;
|
||||
if (vHandle && vLine) {
|
||||
const vLen = point.vScale * 50;
|
||||
// vRot is relative to vertical axis (PI/2) in WebGL
|
||||
const vAngle = -(point.vRot + Math.PI / 2);
|
||||
vHandle.style.left = `${10 + Math.cos(vAngle) * vLen}px`;
|
||||
vHandle.style.top = `${10 + Math.sin(vAngle) * vLen}px`;
|
||||
vLine.style.width = `${vLen}px`;
|
||||
vLine.style.transform = `rotate(${vAngle}rad)`;
|
||||
vLine.style.left = "10px";
|
||||
vLine.style.top = "10px";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let draggerGui: GUI | undefined;
|
||||
function setActiveDragger(x: number, y: number) {
|
||||
if (draggerGui) {
|
||||
draggerGui.destroy();
|
||||
draggerGui = undefined;
|
||||
}
|
||||
const point = mgRenderer.getControlPoint(x, y);
|
||||
if (point) {
|
||||
draggerGui = gui.addFolder(`控制点 (${x}, ${y})`);
|
||||
const obj = {
|
||||
uAngle: (point.uRot * 180) / Math.PI,
|
||||
vAngle: (point.vRot * 180) / Math.PI,
|
||||
uScale: point.uScale,
|
||||
vScale: point.vScale,
|
||||
};
|
||||
draggerGui
|
||||
.add(obj, "uAngle", -180, 180)
|
||||
.name("横向扭曲角度")
|
||||
.onChange((v: number) => {
|
||||
point.uRot = (v * Math.PI) / 180;
|
||||
updateControlPointDraggers();
|
||||
updateResult();
|
||||
});
|
||||
draggerGui
|
||||
.add(obj, "vAngle", -180, 180)
|
||||
.name("纵向扭曲角度")
|
||||
.onChange((v: number) => {
|
||||
point.vRot = (v * Math.PI) / 180;
|
||||
updateControlPointDraggers();
|
||||
updateResult();
|
||||
});
|
||||
draggerGui
|
||||
.add(obj, "uScale", 0.1, 10)
|
||||
.name("横向缩放")
|
||||
.onChange((v: number) => {
|
||||
point.uScale = v;
|
||||
updateControlPointDraggers();
|
||||
updateResult();
|
||||
});
|
||||
draggerGui
|
||||
.add(obj, "vScale", 0.1, 10)
|
||||
.name("纵向缩放")
|
||||
.onChange((v: number) => {
|
||||
point.vScale = v;
|
||||
updateControlPointDraggers();
|
||||
updateResult();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", updateControlPointDraggers);
|
||||
|
||||
const resultTextArea = document.getElementById("result") as HTMLTextAreaElement;
|
||||
resultTextArea.value = "// 控制点的设置代码将会在这里显示";
|
||||
function updateResult() {
|
||||
const result = [
|
||||
`preset(${debugValues.controlPointSize}, ${debugValues.controlPointSize}, [`,
|
||||
];
|
||||
for (let y = 0; y < debugValues.controlPointSize; y++) {
|
||||
for (let x = 0; x < debugValues.controlPointSize; x++) {
|
||||
const point = mgRenderer.getControlPoint(x, y);
|
||||
if (point === undefined) continue;
|
||||
|
||||
const px = Number(point.location.x.toFixed(4));
|
||||
const py = Number(point.location.y.toFixed(4));
|
||||
const ur = Number(point.uRot.toFixed(4));
|
||||
const vr = Number(point.vRot.toFixed(4));
|
||||
const up = Number(point.uScale.toFixed(4));
|
||||
const vp = Number(point.vScale.toFixed(4));
|
||||
|
||||
let pStr = ` p(${x}, ${y}, ${px}, ${py}`;
|
||||
if (ur !== 0 || vr !== 0 || up !== 1 || vp !== 1) {
|
||||
pStr += `, ${ur}, ${vr}`;
|
||||
if (up !== 1 || vp !== 1) {
|
||||
pStr += `, ${up}, ${vp}`;
|
||||
}
|
||||
}
|
||||
pStr += `),`;
|
||||
result.push(pStr);
|
||||
}
|
||||
}
|
||||
result.push("]),");
|
||||
resultTextArea.value = result.join("\n");
|
||||
}
|
||||
|
||||
function resizeControlPoint() {
|
||||
document.querySelectorAll(".dragger").forEach((el) => {
|
||||
el.parentElement?.removeChild(el);
|
||||
});
|
||||
mgRenderer.resizeControlPoints(
|
||||
debugValues.controlPointSize,
|
||||
debugValues.controlPointSize,
|
||||
);
|
||||
mgRenderer.resetSubdivition(debugValues.subdivideDepth);
|
||||
|
||||
for (let y = 0; y < debugValues.controlPointSize; y++) {
|
||||
for (let x = 0; x < debugValues.controlPointSize; x++) {
|
||||
const point = mgRenderer.getControlPoint(x, y);
|
||||
if (point === undefined) continue;
|
||||
const dragger = document.createElement("div");
|
||||
const draggerInput = document.createElement("input");
|
||||
draggerInput.type = "color";
|
||||
draggerInput.style.position = "absolute";
|
||||
draggerInput.style.visibility = "hidden";
|
||||
dragger.appendChild(draggerInput);
|
||||
dragger.setAttribute("x", `${x}`);
|
||||
dragger.setAttribute("y", `${y}`);
|
||||
dragger.className = "dragger";
|
||||
dragger.style.left = `${(x * 100) / (debugValues.controlPointSize - 1)}%`;
|
||||
dragger.style.top = `${
|
||||
((debugValues.controlPointSize - y - 1) * 100) /
|
||||
(debugValues.controlPointSize - 1)
|
||||
}%`;
|
||||
|
||||
// Add U/V handles
|
||||
const uLine = document.createElement("div");
|
||||
uLine.className = "dragger-line u-line";
|
||||
const uHandle = document.createElement("div");
|
||||
uHandle.className = "dragger-handle u-handle";
|
||||
uHandle.style.backgroundColor = "#f44";
|
||||
|
||||
const vLine = document.createElement("div");
|
||||
vLine.className = "dragger-line v-line";
|
||||
const vHandle = document.createElement("div");
|
||||
vHandle.className = "dragger-handle v-handle";
|
||||
vHandle.style.backgroundColor = "#4f4";
|
||||
|
||||
dragger.appendChild(uLine);
|
||||
dragger.appendChild(uHandle);
|
||||
dragger.appendChild(vLine);
|
||||
dragger.appendChild(vHandle);
|
||||
|
||||
// Handle U dragging
|
||||
uHandle.addEventListener("mousedown", (evt) => {
|
||||
if (point === undefined) return;
|
||||
evt.stopPropagation();
|
||||
const rect = dragger.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (point === undefined) return;
|
||||
const dx = e.clientX - centerX;
|
||||
const dy = e.clientY - centerY;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
const isCorner =
|
||||
(x === 0 || x === debugValues.controlPointSize - 1) &&
|
||||
(y === 0 || y === debugValues.controlPointSize - 1);
|
||||
const isEdge =
|
||||
x === 0 ||
|
||||
x === debugValues.controlPointSize - 1 ||
|
||||
y === 0 ||
|
||||
y === debugValues.controlPointSize - 1;
|
||||
|
||||
if (!isCorner) {
|
||||
if (isEdge) {
|
||||
// For edge points, only allow scaling, keep rotation at 0
|
||||
point.uRot = 0;
|
||||
} else {
|
||||
point.uRot = Math.atan2(-dy, dx);
|
||||
}
|
||||
}
|
||||
point.uScale = Math.max(0.1, dist / 50);
|
||||
|
||||
updateControlPointDraggers();
|
||||
updateResult();
|
||||
if (draggerGui) {
|
||||
draggerGui.controllersRecursive().forEach((c) => {
|
||||
c.updateDisplay();
|
||||
});
|
||||
}
|
||||
}
|
||||
function onMouseUp() {
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
}
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
});
|
||||
|
||||
// Handle V dragging
|
||||
vHandle.addEventListener("mousedown", (evt) => {
|
||||
if (point === undefined) return;
|
||||
evt.stopPropagation();
|
||||
const rect = dragger.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (point === undefined) return;
|
||||
const dx = e.clientX - centerX;
|
||||
const dy = e.clientY - centerY;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
const isCorner =
|
||||
(x === 0 || x === debugValues.controlPointSize - 1) &&
|
||||
(y === 0 || y === debugValues.controlPointSize - 1);
|
||||
const isEdge =
|
||||
x === 0 ||
|
||||
x === debugValues.controlPointSize - 1 ||
|
||||
y === 0 ||
|
||||
y === debugValues.controlPointSize - 1;
|
||||
|
||||
if (!isCorner) {
|
||||
if (isEdge) {
|
||||
// For edge points, only allow scaling, keep rotation at 0
|
||||
point.vRot = 0;
|
||||
} else {
|
||||
// vRot is relative to vertical axis
|
||||
point.vRot = Math.atan2(-dy, dx) - Math.PI / 2;
|
||||
}
|
||||
}
|
||||
point.vScale = Math.max(0.1, dist / 50);
|
||||
|
||||
updateControlPointDraggers();
|
||||
updateResult();
|
||||
if (draggerGui) {
|
||||
draggerGui.controllersRecursive().forEach((c) => {
|
||||
c.updateDisplay();
|
||||
});
|
||||
}
|
||||
}
|
||||
function onMouseUp() {
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
}
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
});
|
||||
|
||||
draggerInput.addEventListener("input", () => {
|
||||
// mgRenderer.getControlPoint(x, y).color = dragger.value;
|
||||
const c = draggerInput.value;
|
||||
console.log(c);
|
||||
dragger.style.backgroundColor = c;
|
||||
const color = c.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i);
|
||||
if (color) {
|
||||
point.color.r = Number.parseInt(color[1], 16) / 255;
|
||||
point.color.g = Number.parseInt(color[2], 16) / 255;
|
||||
point.color.b = Number.parseInt(color[3], 16) / 255;
|
||||
dragger.setAttribute("r", `${point.color.r}`);
|
||||
dragger.setAttribute("g", `${point.color.g}`);
|
||||
dragger.setAttribute("b", `${point.color.b}`);
|
||||
updateResult();
|
||||
}
|
||||
});
|
||||
let dragging = false;
|
||||
dragger.addEventListener("mousedown", (evt) => {
|
||||
evt.stopPropagation();
|
||||
const isCorner =
|
||||
(x === 0 || x === debugValues.controlPointSize - 1) &&
|
||||
(y === 0 || y === debugValues.controlPointSize - 1);
|
||||
|
||||
function onMouseMove(evt: MouseEvent) {
|
||||
if (!isCorner) {
|
||||
dragger.style.left = `${Math.min(
|
||||
window.innerWidth,
|
||||
Math.max(0, evt.clientX),
|
||||
)}px`;
|
||||
dragger.style.top = `${Math.min(
|
||||
window.innerHeight,
|
||||
Math.max(0, evt.clientY),
|
||||
)}px`;
|
||||
if (point) {
|
||||
point.location.x = Math.max(
|
||||
-1,
|
||||
Math.min(1, (evt.clientX / window.innerWidth) * 2 - 1),
|
||||
);
|
||||
point.location.y = Math.max(
|
||||
-1,
|
||||
Math.min(1, -((evt.clientY / window.innerHeight) * 2 - 1)),
|
||||
);
|
||||
}
|
||||
}
|
||||
dragging = true;
|
||||
updateControlPointDraggers();
|
||||
updateResult();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
function onMouseUp(evt: MouseEvent) {
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
} else if (dragger.classList.contains("active")) {
|
||||
draggerInput.click();
|
||||
} else {
|
||||
for (const el of document.querySelectorAll(".dragger.active")) {
|
||||
el.classList.remove("active");
|
||||
}
|
||||
dragger.classList.add("active");
|
||||
setActiveDragger(x, y);
|
||||
}
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
evt.stopPropagation();
|
||||
}
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
});
|
||||
document.body.appendChild(dragger);
|
||||
}
|
||||
}
|
||||
updateResult();
|
||||
}
|
||||
|
||||
function subdivide() {
|
||||
mgRenderer.resetSubdivition(debugValues.subdivideDepth);
|
||||
}
|
||||
|
||||
function reloadImage() {
|
||||
mgRenderer
|
||||
.setAlbum(debugValues.image)
|
||||
.catch(() =>
|
||||
mgRenderer.setAlbum(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAADUExURf///6fEG8gAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAKSURBVBjTY2AAAAACAAGYY2zXAAAAAElFTkSuQmCC",
|
||||
),
|
||||
)
|
||||
.finally(() => {
|
||||
resizeControlPoint();
|
||||
updateControlPointDraggers();
|
||||
subdivide();
|
||||
});
|
||||
}
|
||||
|
||||
reloadImage();
|
||||
|
||||
const gui = new GUI();
|
||||
gui.close();
|
||||
gui.title("MG Renderer 调试页面");
|
||||
gui.add(debugValues, "image").name("图片 URL").onFinishChange(reloadImage);
|
||||
gui
|
||||
.add(debugValues, "controlPointSize", 3, 10, 1)
|
||||
.name("控制点矩阵大小")
|
||||
.onFinishChange(resizeControlPoint);
|
||||
gui
|
||||
.add(debugValues, "subdivideDepth", 2, 50, 1)
|
||||
.name("细分深度")
|
||||
.onChange(subdivide);
|
||||
gui
|
||||
.add(debugValues, "wireFrame")
|
||||
.name("线框模式")
|
||||
.onChange((v: boolean) => mgRenderer.setWireFrame(v));
|
||||
|
||||
/** @internal */
|
||||
export interface ControlPointConf {
|
||||
cx: number;
|
||||
cy: number;
|
||||
x: number;
|
||||
y: number;
|
||||
ur: number;
|
||||
vr: number;
|
||||
up: number;
|
||||
vp: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface ControlPointPreset {
|
||||
width: number;
|
||||
height: number;
|
||||
conf: ControlPointConf[];
|
||||
}
|
||||
|
||||
const actions = {
|
||||
copyCode: () => {
|
||||
navigator.clipboard.writeText(resultTextArea.value).then(() => {
|
||||
alert("代码已复制到剪贴板");
|
||||
});
|
||||
},
|
||||
loadCode: () => {
|
||||
try {
|
||||
const code = resultTextArea.value;
|
||||
|
||||
const preset = (
|
||||
width: number,
|
||||
height: number,
|
||||
conf: ControlPointConf[],
|
||||
): ControlPointPreset => {
|
||||
return { width, height, conf };
|
||||
};
|
||||
|
||||
const p = (
|
||||
cx: number,
|
||||
cy: number,
|
||||
x: number,
|
||||
y: number,
|
||||
ur = 0,
|
||||
vr = 0,
|
||||
up = 1,
|
||||
vp = 1,
|
||||
): ControlPointConf => ({ cx, cy, x, y, ur, vr, up, vp });
|
||||
|
||||
// Remove trailing comma if exists
|
||||
const cleanCode = code.trim().replace(/,$/, "");
|
||||
|
||||
const fn = new Function("preset", "p", `return ${cleanCode}`);
|
||||
const loadedPreset = fn(preset, p) as ControlPointPreset | undefined;
|
||||
|
||||
if (loadedPreset) {
|
||||
debugValues.controlPointSize = loadedPreset.width;
|
||||
gui.controllersRecursive().forEach((c) => {
|
||||
c.updateDisplay();
|
||||
});
|
||||
|
||||
resizeControlPoint();
|
||||
|
||||
for (const conf of loadedPreset.conf) {
|
||||
const point = mgRenderer.getControlPoint(conf.cx, conf.cy);
|
||||
if (point) {
|
||||
point.location.x = conf.x;
|
||||
point.location.y = conf.y;
|
||||
point.uRot = conf.ur;
|
||||
point.vRot = conf.vr;
|
||||
point.uScale = conf.up;
|
||||
point.vScale = conf.vp;
|
||||
}
|
||||
}
|
||||
updateControlPointDraggers();
|
||||
updateResult();
|
||||
alert("预设加载成功");
|
||||
}
|
||||
} catch (e) {
|
||||
alert(`加载失败,请检查代码格式是否正确\n${e}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
gui.add(actions, "copyCode").name("复制预设代码");
|
||||
gui.add(actions, "loadCode").name("从文本框加载预设");
|
||||
|
||||
const stats = new Stats();
|
||||
stats.showPanel(0);
|
||||
stats.dom.style.left = "50px";
|
||||
document.body.appendChild(stats.dom);
|
||||
const frame = () => {
|
||||
stats.end();
|
||||
stats.begin();
|
||||
requestAnimationFrame(frame);
|
||||
};
|
||||
requestAnimationFrame(frame);
|
||||
449
amll-local/packages/playground/core-legacy/src/test.ts
Normal file
449
amll-local/packages/playground/core-legacy/src/test.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* 此处是一个简易的组件加载测试脚本,用来调试歌词
|
||||
*
|
||||
* @author SteveXMH
|
||||
*/
|
||||
|
||||
import type { LyricLine } from "@applemusic-like-lyrics/core";
|
||||
import {
|
||||
BackgroundRender,
|
||||
DomLyricPlayer,
|
||||
type LyricLineMouseEvent,
|
||||
MeshGradientRenderer,
|
||||
PixiRenderer,
|
||||
} from "@applemusic-like-lyrics/core";
|
||||
import * as lyrics from "@applemusic-like-lyrics/lyric";
|
||||
import {
|
||||
parseLrc,
|
||||
parseLys,
|
||||
parseQrc,
|
||||
parseYrc,
|
||||
type LyricLine as RawLyricLine,
|
||||
} from "@applemusic-like-lyrics/lyric";
|
||||
import { parseTTML } from "@applemusic-like-lyrics/ttml";
|
||||
import GUI from "lil-gui";
|
||||
import Stats from "stats.js";
|
||||
|
||||
export interface SpringParams {
|
||||
mass: number; // = 1.0
|
||||
damping: number; // = 10.0
|
||||
stiffness: number; // = 100.0
|
||||
soft: boolean; // = false
|
||||
}
|
||||
|
||||
window.lyrics = lyrics;
|
||||
|
||||
const audio = document.createElement("audio");
|
||||
audio.volume = 0.5;
|
||||
audio.preload = "auto";
|
||||
|
||||
audio.addEventListener("play", () => lyricPlayer.resume());
|
||||
audio.addEventListener("pause", () => lyricPlayer.pause());
|
||||
|
||||
const debugValues = {
|
||||
lyric: new URL(location.href).searchParams.get("lyric") || "",
|
||||
music: new URL(location.href).searchParams.get("music") || "",
|
||||
album: new URL(location.href).searchParams.get("album") || "",
|
||||
enableSpring: true,
|
||||
bgFPS: 60,
|
||||
bgMode: new URL(location.href).searchParams.get("bg") || "mg",
|
||||
bgScale: 1,
|
||||
bgFlowSpeed: 0.2,
|
||||
bgPlaying: true,
|
||||
bgStaticMode: false,
|
||||
currentTime: 0,
|
||||
enableBlur: true,
|
||||
playing: false,
|
||||
async mockPlay() {
|
||||
this.playing = true;
|
||||
const startTime = Date.now();
|
||||
const baseTime = this.currentTime * 1000;
|
||||
while (this.playing && this.currentTime < 300) {
|
||||
const time = Date.now() - startTime;
|
||||
this.currentTime = (baseTime + time) / 1000;
|
||||
progress.updateDisplay();
|
||||
lyricPlayer.setCurrentTime(baseTime + time);
|
||||
await waitFrame();
|
||||
}
|
||||
},
|
||||
forceUpdateAlbum() {
|
||||
window.globalBackground.setAlbum(debugValues.album);
|
||||
},
|
||||
forceUpdateLyric() {
|
||||
loadLyric();
|
||||
},
|
||||
play() {
|
||||
this.playing = true;
|
||||
audio.load();
|
||||
audio.play();
|
||||
},
|
||||
pause() {
|
||||
this.playing = false;
|
||||
if (audio.paused) {
|
||||
audio.play();
|
||||
} else {
|
||||
audio.pause();
|
||||
}
|
||||
},
|
||||
fadeWidth: 0.5,
|
||||
lineSprings: {
|
||||
posX: {
|
||||
mass: 1,
|
||||
damping: 10,
|
||||
stiffness: 100,
|
||||
soft: false,
|
||||
} as SpringParams,
|
||||
posY: {
|
||||
mass: 1,
|
||||
damping: 15,
|
||||
stiffness: 100,
|
||||
soft: false,
|
||||
} as SpringParams,
|
||||
scale: {
|
||||
mass: 1,
|
||||
damping: 20,
|
||||
stiffness: 100,
|
||||
soft: false,
|
||||
} as SpringParams,
|
||||
},
|
||||
};
|
||||
|
||||
function recreateBGRenderer(mode: string) {
|
||||
window.globalBackground?.dispose();
|
||||
if (mode === "pixi") {
|
||||
window.globalBackground = BackgroundRender.new(PixiRenderer);
|
||||
} else if (mode === "mg") {
|
||||
window.globalBackground = BackgroundRender.new(MeshGradientRenderer);
|
||||
} else {
|
||||
throw new Error("Unknown renderer mode");
|
||||
}
|
||||
const bg = window.globalBackground;
|
||||
bg.setFPS(debugValues.bgFPS);
|
||||
bg.setRenderScale(debugValues.bgScale);
|
||||
bg.setStaticMode(debugValues.bgStaticMode);
|
||||
bg.setFlowSpeed(debugValues.bgFlowSpeed);
|
||||
bg.getElement().style.position = "absolute";
|
||||
bg.getElement().style.top = "0";
|
||||
bg.getElement().style.left = "0";
|
||||
bg.getElement().style.width = "100%";
|
||||
bg.getElement().style.height = "100%";
|
||||
bg.setAlbum(debugValues.album);
|
||||
}
|
||||
|
||||
audio.src = debugValues.music;
|
||||
audio.load();
|
||||
|
||||
const gui = new GUI();
|
||||
gui.close();
|
||||
|
||||
gui.title("AMLL 歌词测试页面");
|
||||
const lyricController = gui
|
||||
.add(debugValues, "lyric")
|
||||
.name("歌词文件")
|
||||
.onFinishChange(async (url: string) => {
|
||||
lyricPlayer.setLyricLines(parseTTML(await (await fetch(url)).text()).lines);
|
||||
});
|
||||
const localFileApi = {
|
||||
openLocalLyricFile() {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".ttml,.lrc,.yrc,.lys,.qrc";
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
localLyricExt = file.name;
|
||||
if (localLyricUrl) {
|
||||
URL.revokeObjectURL(localLyricUrl);
|
||||
}
|
||||
localLyricUrl = URL.createObjectURL(file);
|
||||
debugValues.lyric = localLyricUrl;
|
||||
lyricController.updateDisplay();
|
||||
await loadLyric();
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
openLocalMusicFile() {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "audio/*";
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
if (localMusicUrl) {
|
||||
URL.revokeObjectURL(localMusicUrl);
|
||||
}
|
||||
localMusicUrl = URL.createObjectURL(file);
|
||||
debugValues.music = localMusicUrl;
|
||||
audio.src = localMusicUrl;
|
||||
audio.load();
|
||||
musicController.updateDisplay();
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
};
|
||||
gui.add(localFileApi, "openLocalLyricFile").name("打开本地歌词");
|
||||
gui.add(localFileApi, "openLocalMusicFile").name("打开本地歌曲");
|
||||
const musicController = gui
|
||||
.add(debugValues, "music")
|
||||
.name("歌曲")
|
||||
.onFinishChange((v: string) => {
|
||||
audio.src = v;
|
||||
});
|
||||
gui
|
||||
.add(debugValues, "album")
|
||||
.name("专辑图片")
|
||||
.onFinishChange((v: string) => {
|
||||
window.globalBackground.setAlbum(v);
|
||||
});
|
||||
gui.add(debugValues, "forceUpdateAlbum").name("强制更新专辑图片");
|
||||
gui.add(debugValues, "forceUpdateLyric").name("强制更新歌词");
|
||||
const bgGui = gui.addFolder("背景");
|
||||
bgGui
|
||||
.add(debugValues, "bgPlaying")
|
||||
.name("播放")
|
||||
.onFinishChange((v: boolean) => {
|
||||
if (v) {
|
||||
window.globalBackground.resume();
|
||||
} else {
|
||||
window.globalBackground.pause();
|
||||
}
|
||||
});
|
||||
bgGui
|
||||
.add(debugValues, "bgMode", ["pixi", "mg"])
|
||||
.name("背景渲染器")
|
||||
.onFinishChange((v: string) => {
|
||||
recreateBGRenderer(v);
|
||||
});
|
||||
bgGui
|
||||
.add(debugValues, "bgScale", 0.01, 1, 0.01)
|
||||
.name("分辨率比率")
|
||||
.onChange((v: number) => {
|
||||
window.globalBackground.setRenderScale(v);
|
||||
});
|
||||
bgGui
|
||||
.add(debugValues, "bgFPS", 1, 1000, 1)
|
||||
.name("帧率")
|
||||
.onFinishChange((v: number) => {
|
||||
window.globalBackground.setFPS(v);
|
||||
});
|
||||
bgGui
|
||||
.add(debugValues, "bgFlowSpeed", 0, 10, 0.1)
|
||||
.name("流动速度")
|
||||
.onFinishChange((v: number) => {
|
||||
window.globalBackground.setFlowSpeed(v);
|
||||
});
|
||||
bgGui
|
||||
.add(debugValues, "bgStaticMode")
|
||||
.name("静态模式")
|
||||
.onFinishChange((v: boolean) => {
|
||||
window.globalBackground.setStaticMode(v);
|
||||
});
|
||||
|
||||
{
|
||||
const animation = gui.addFolder("歌词行动画/效果");
|
||||
animation
|
||||
.add(debugValues, "fadeWidth", 0, 10, 0.01)
|
||||
.name("歌词渐变宽度")
|
||||
.onChange((v: number) => {
|
||||
lyricPlayer.setWordFadeWidth(v);
|
||||
});
|
||||
animation
|
||||
.add(debugValues, "enableBlur")
|
||||
.name("启用歌词模糊")
|
||||
.onChange((v: boolean) => {
|
||||
lyricPlayer.setEnableBlur(v);
|
||||
});
|
||||
animation
|
||||
.add(debugValues, "enableSpring")
|
||||
.name("使用弹簧动画")
|
||||
.onChange((v: boolean) => {
|
||||
lyricPlayer.setEnableSpring(v);
|
||||
});
|
||||
function addSpringDbg(name: string, obj: SpringParams, onChange: () => void) {
|
||||
const x = animation.addFolder(name);
|
||||
x.close();
|
||||
x.add(obj, "mass").name("质量").onFinishChange(onChange);
|
||||
x.add(obj, "damping").name("阻力").onFinishChange(onChange);
|
||||
x.add(obj, "stiffness").name("弹性").onFinishChange(onChange);
|
||||
x.add(obj, "soft")
|
||||
.name("强制软弹簧(当阻力小于 1 时有用)")
|
||||
.onFinishChange(onChange);
|
||||
}
|
||||
addSpringDbg("水平位移弹簧", debugValues.lineSprings.posX, () => {
|
||||
lyricPlayer.setLinePosXSpringParams(debugValues.lineSprings.posX);
|
||||
});
|
||||
addSpringDbg("垂直位移弹簧", debugValues.lineSprings.posY, () => {
|
||||
lyricPlayer.setLinePosYSpringParams(debugValues.lineSprings.posY);
|
||||
});
|
||||
addSpringDbg("缩放弹簧", debugValues.lineSprings.scale, () => {
|
||||
lyricPlayer.setLineScaleSpringParams(debugValues.lineSprings.scale);
|
||||
});
|
||||
}
|
||||
|
||||
const playerGui = gui.addFolder("音乐播放器");
|
||||
const progress = playerGui
|
||||
.add(debugValues, "currentTime")
|
||||
.min(0)
|
||||
.step(1)
|
||||
.name("当前进度")
|
||||
.onChange((v: number) => {
|
||||
audio.currentTime = v;
|
||||
lyricPlayer.setCurrentTime(v * 1000, true);
|
||||
});
|
||||
playerGui.add(debugValues, "play").name("加载/播放");
|
||||
playerGui.add(debugValues, "pause").name("暂停/继续");
|
||||
|
||||
const lyricPlayer = new DomLyricPlayer();
|
||||
|
||||
lyricPlayer.addEventListener("line-click", (evt) => {
|
||||
const e = evt as LyricLineMouseEvent;
|
||||
evt.preventDefault();
|
||||
evt.stopImmediatePropagation();
|
||||
evt.stopPropagation();
|
||||
console.log(e.line, e.lineIndex);
|
||||
const time = e.line.getLine().startTime;
|
||||
lyricPlayer.setCurrentTime(time, true);
|
||||
audio.currentTime = time / 1000;
|
||||
});
|
||||
|
||||
const stats = new Stats();
|
||||
stats.showPanel(0);
|
||||
document.body.appendChild(stats.dom);
|
||||
let lastTime = -1;
|
||||
const frame = (time: number) => {
|
||||
stats.end();
|
||||
if (lastTime === -1) {
|
||||
lastTime = time;
|
||||
}
|
||||
if (!audio.paused) {
|
||||
const time = (audio.currentTime * 1000) | 0;
|
||||
debugValues.currentTime = (time / 1000) | 0;
|
||||
progress.max(audio.duration | 0);
|
||||
progress.updateDisplay();
|
||||
lyricPlayer.setCurrentTime(time);
|
||||
}
|
||||
lyricPlayer.update(time - lastTime);
|
||||
lastTime = time;
|
||||
stats.begin();
|
||||
requestAnimationFrame(frame);
|
||||
};
|
||||
requestAnimationFrame(frame);
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
globalLyricPlayer: DomLyricPlayer;
|
||||
globalBackground:
|
||||
| BackgroundRender<PixiRenderer>
|
||||
| BackgroundRender<MeshGradientRenderer>;
|
||||
lyrics: typeof lyrics;
|
||||
}
|
||||
}
|
||||
|
||||
window.globalLyricPlayer = lyricPlayer;
|
||||
|
||||
const waitFrame = (): Promise<number> =>
|
||||
new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
let localLyricUrl: string | null = null;
|
||||
let localLyricExt: string | null = null;
|
||||
let localMusicUrl: string | null = null;
|
||||
const mapLyric = (
|
||||
line: RawLyricLine,
|
||||
_i: number,
|
||||
_lines: RawLyricLine[],
|
||||
): LyricLine => ({
|
||||
words: line.words.map((word) => ({
|
||||
...word,
|
||||
obscene: false,
|
||||
romanWord: word.romanWord ?? "",
|
||||
})),
|
||||
startTime: line.words[0]?.startTime ?? 0,
|
||||
endTime:
|
||||
line.words[line.words.length - 1]?.endTime ?? Number.POSITIVE_INFINITY,
|
||||
translatedLyric: "",
|
||||
romanLyric: "",
|
||||
isBG: false,
|
||||
isDuet: false,
|
||||
});
|
||||
|
||||
async function loadLyric() {
|
||||
const lyricFile = debugValues.lyric;
|
||||
const content = await (await fetch(lyricFile)).text();
|
||||
const lyricSource = (localLyricExt ?? lyricFile).toLowerCase();
|
||||
if (lyricSource.endsWith(".ttml")) {
|
||||
lyricPlayer.setLyricLines(parseTTML(content).lines);
|
||||
} else if (lyricSource.endsWith(".lrc")) {
|
||||
lyricPlayer.setLyricLines(parseLrc(content).map(mapLyric));
|
||||
} else if (lyricSource.endsWith(".yrc")) {
|
||||
lyricPlayer.setLyricLines(parseYrc(content).map(mapLyric));
|
||||
} else if (lyricSource.endsWith(".lys")) {
|
||||
lyricPlayer.setLyricLines(parseLys(content).map(mapLyric));
|
||||
} else if (lyricSource.endsWith(".qrc")) {
|
||||
lyricPlayer.setLyricLines(parseQrc(content).map(mapLyric));
|
||||
} else if (lyricFile === "bug") {
|
||||
const buildLyricLines = (
|
||||
lyric: string,
|
||||
startTime = 1000,
|
||||
otherParams: Partial<LyricLine> = {},
|
||||
): LyricLine => {
|
||||
let curTime = startTime;
|
||||
const words = [];
|
||||
for (const word of lyric.split("|")) {
|
||||
const [text, duration] = word.split(",");
|
||||
const endTime = curTime + Number.parseInt(duration, 10);
|
||||
words.push({
|
||||
word: text,
|
||||
romanWord: "",
|
||||
startTime: curTime,
|
||||
endTime,
|
||||
obscene: false,
|
||||
});
|
||||
curTime = endTime;
|
||||
}
|
||||
return {
|
||||
startTime,
|
||||
endTime: curTime + 3000,
|
||||
translatedLyric: "",
|
||||
romanLyric: "",
|
||||
isBG: false,
|
||||
isDuet: false,
|
||||
words,
|
||||
...otherParams,
|
||||
};
|
||||
};
|
||||
|
||||
const DEMO_LYRIC: LyricLine[] = [
|
||||
buildLyricLines(
|
||||
"Apple ,750|Music ,500|Like ,500|Ly,400|ri,500|cs ,250",
|
||||
1000,
|
||||
),
|
||||
buildLyricLines("BG ,750|Lyrics ,1000", 2000, {
|
||||
isBG: true,
|
||||
}),
|
||||
buildLyricLines("Next ,1000|Lyrics,1000", 2500, {
|
||||
// isDuet: true,
|
||||
}),
|
||||
];
|
||||
|
||||
lyricPlayer.setLyricLines(DEMO_LYRIC);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
recreateBGRenderer(debugValues.bgMode);
|
||||
audio.style.display = "none";
|
||||
// lyricPlayer.getBottomLineElement().innerHTML = "Test Bottom Line";
|
||||
const player = document.getElementById("player");
|
||||
if (player) {
|
||||
player.appendChild(audio);
|
||||
player.appendChild(window.globalBackground.getElement());
|
||||
player.appendChild(lyricPlayer.getElement());
|
||||
}
|
||||
if (!debugValues.enableSpring) {
|
||||
lyricPlayer.setEnableSpring(false);
|
||||
}
|
||||
await loadLyric();
|
||||
// debugValues.play();
|
||||
// debugValues.currentTime = 34;
|
||||
// debugValues.mockPlay();
|
||||
})();
|
||||
4
amll-local/packages/playground/core-legacy/tsconfig.json
Normal file
4
amll-local/packages/playground/core-legacy/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
20
amll-local/packages/playground/core-legacy/vite.config.ts
Normal file
20
amll-local/packages/playground/core-legacy/vite.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import path from "node:path";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@applemusic-like-lyrics/core": path.resolve(__dirname, "../../core/src"),
|
||||
"@applemusic-like-lyrics/core/style.css": path.resolve(
|
||||
__dirname,
|
||||
"../../core/src/styles/index.css",
|
||||
),
|
||||
"@applemusic-like-lyrics/lyric": path.resolve(
|
||||
__dirname,
|
||||
"../../lyric/src",
|
||||
),
|
||||
"@applemusic-like-lyrics/ttml": path.resolve(__dirname, "../../ttml/src"),
|
||||
"@amll-core-src": path.resolve(__dirname, "../../core/src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user