removed idea folder
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals", "eslint:recommended"],
|
||||||
|
"rules": {
|
||||||
|
"react/react-in-jsx-scope": "off", // Next.js doesn't require React in scope.
|
||||||
|
"no-console": "warn", // Allow console.logs but with a warning.
|
||||||
|
"semi": ["warn", "always"], // Enforce semicolons with a warning.
|
||||||
|
"quotes": ["warn", "single"], // Prefer single quotes with a warning.
|
||||||
|
"@next/next/no-html-link-for-pages": "off" // Disable Next.js-specific rule.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
.idea/
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
|
|||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
Generated
-12
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/jam-session-web.iml" filepath="$PROJECT_DIR$/.idea/jam-session-web.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# client/Dockerfile
|
||||||
|
# 1. Use an official Node.js image as the base
|
||||||
|
FROM node:19-alpine AS builder
|
||||||
|
|
||||||
|
# 2. Create and set the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 3. Copy package manifests and install dependencies
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# 4. Copy your Next.js source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 5. Build the Next.js app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 6. Use a lightweight webserver for the final image (or continue with Node SSR)
|
||||||
|
# Option A: If you use Next.js "standalone" + "output: 'standalone'" in next.config.js:
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
# Option B: Run Next.js in Node as usual.
|
||||||
|
# We'll do SSR with Node below.
|
||||||
|
|
||||||
|
FROM node:19-alpine AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the build output from the builder
|
||||||
|
COPY --from=builder /app/.next ./.next
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app/package*.json ./
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/config.json ./
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# For SSR, we run Next.js in Node:
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
FROM node:19-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["node", "./src/OSCWebServer/server.js"]
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"polling_interval": 500,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "mic",
|
||||||
|
"name": "Microphone",
|
||||||
|
"channels": [
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "piano",
|
||||||
|
"name": "Piano",
|
||||||
|
"channels": [
|
||||||
|
9
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "maschine",
|
||||||
|
"name": "Maschine",
|
||||||
|
"channels": [
|
||||||
|
11
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "synth",
|
||||||
|
"name": "Synthesizer",
|
||||||
|
"channels": [
|
||||||
|
13
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "loopstation",
|
||||||
|
"name": "Loopstation",
|
||||||
|
"channels": [
|
||||||
|
15
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pc",
|
||||||
|
"name": "PC",
|
||||||
|
"channels": [
|
||||||
|
25
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bus_controls": [
|
||||||
|
{
|
||||||
|
"id": "maschine_input_bus",
|
||||||
|
"name": "Maschine Input",
|
||||||
|
"bus": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "loopstation_input_1",
|
||||||
|
"name": "Loopstation Input 1",
|
||||||
|
"bus": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "monitor_mix_1",
|
||||||
|
"name": "Monitor Mix 1",
|
||||||
|
"bus": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "monitor_mix_2",
|
||||||
|
"name": "Monitor Mix 2",
|
||||||
|
"bus": 11
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+15
-5
@@ -1,16 +1,26 @@
|
|||||||
import { dirname } from "path";
|
import {dirname} from "path";
|
||||||
import { fileURLToPath } from "url";
|
import {fileURLToPath} from "url";
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
import {FlatCompat} from "@eslint/eslintrc";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
const compat = new FlatCompat({
|
||||||
baseDirectory: __dirname,
|
baseDirectory: __dirname,
|
||||||
});
|
});
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
...compat.config({
|
||||||
|
extends: [
|
||||||
|
"next/core-web-vitals",
|
||||||
|
"next/typescript"
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'react-hooks/exhaustive-deps': 'off'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig;
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ const nextConfig: NextConfig = {
|
|||||||
/* config options here */
|
/* config options here */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Generated
+1935
-118
File diff suppressed because it is too large
Load Diff
+20
-5
@@ -2,26 +2,41 @@
|
|||||||
"name": "jam-session-web",
|
"name": "jam-session-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
|
"start:websocket": "node ./src/OSCWebServer/server.js",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slider": "^1.2.2",
|
||||||
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
|
"@shadcn/ui": "^0.0.4",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lucide-react": "^0.471.0",
|
||||||
|
"next": "15.1.4",
|
||||||
|
"osc": "2.4.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"next": "15.1.4"
|
"react-icons": "^5.4.0",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@eslint/eslintrc": "^3",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"postcss": "^8",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.4",
|
"eslint-config-next": "15.1.4",
|
||||||
"@eslint/eslintrc": "^3"
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
import osc from "osc";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
|
const x32IP = process.env.X32_IP;
|
||||||
|
const x32Port = Number(process.env.X32_PORT);
|
||||||
|
|
||||||
|
const OSC_WEB_SOCKET_PORT = 8080;
|
||||||
|
|
||||||
|
// OSC Setup
|
||||||
|
const udpPort = new osc.UDPPort({
|
||||||
|
localAddress: "0.0.0.0",
|
||||||
|
localPort: 57122,
|
||||||
|
remoteAddress: x32IP,
|
||||||
|
remotePort: x32Port,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Connecting to X32 at ${x32IP}:${x32Port}`)
|
||||||
|
|
||||||
|
udpPort.open();
|
||||||
|
|
||||||
|
udpPort.on("ready", () => {
|
||||||
|
console.log("OSC connection is ready!");
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocket Server
|
||||||
|
console.log(`WebSocket server listening on ws://localhost:${OSC_WEB_SOCKET_PORT}`);
|
||||||
|
const wss = new WebSocket.Server({port: OSC_WEB_SOCKET_PORT});
|
||||||
|
|
||||||
|
wss.on("connection", (ws) => {
|
||||||
|
console.log("WebSocket client connected!");
|
||||||
|
|
||||||
|
// Handle messages from the frontend
|
||||||
|
ws.on("message", (message) => {
|
||||||
|
try {
|
||||||
|
const {action, path, value} = JSON.parse(message.toString());
|
||||||
|
|
||||||
|
if (action === "getValue" && path) {
|
||||||
|
// Query OSC for the requested value
|
||||||
|
const fetchValue = async () => {
|
||||||
|
const result = await new Promise((resolve) => {
|
||||||
|
const handleMessage = (oscMessage) => {
|
||||||
|
if (
|
||||||
|
oscMessage.address === path &&
|
||||||
|
oscMessage.args.length > 0
|
||||||
|
) {
|
||||||
|
udpPort.removeListener("message", handleMessage);
|
||||||
|
resolve(oscMessage.args[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
udpPort.on("message", handleMessage);
|
||||||
|
udpPort.send({address: path, args: []});
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
action: "response",
|
||||||
|
path,
|
||||||
|
value: result,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "sendValue" && path && value !== undefined) {
|
||||||
|
// Send OSC command
|
||||||
|
udpPort.send({address: path, args: [{type: "f", value}]});
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
action: "acknowledge",
|
||||||
|
path,
|
||||||
|
value,
|
||||||
|
status: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error processing WebSocket message:", err);
|
||||||
|
ws.send(JSON.stringify({error: "Invalid message format"}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
console.log("WebSocket client disconnected.");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const configPath = process.env.CONFIG_PATH || "./config.json";
|
||||||
|
const absolutePath = path.resolve(configPath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(absolutePath)) {
|
||||||
|
return NextResponse.json({error: "Config file not found"}, {status: 404});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(absolutePath, "utf-8");
|
||||||
|
const config = JSON.parse(fileContent);
|
||||||
|
|
||||||
|
return NextResponse.json(config);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {fetchConfig} from "@/app/utils/fetchConfig";
|
||||||
|
import InputControl from "@/app/components/InputControl";
|
||||||
|
|
||||||
|
const BusControl = ({busControlConfig, mainConfig}) => {
|
||||||
|
|
||||||
|
const [appConfig, setAppConfig] = useState(null);
|
||||||
|
const busId = busControlConfig.id;
|
||||||
|
const busChannel = busControlConfig.bus
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mainConfig) {
|
||||||
|
setAppConfig(mainConfig);
|
||||||
|
} else {
|
||||||
|
fetchConfig().then(config => {
|
||||||
|
setAppConfig(config);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [mainConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!appConfig ? <p>Loading...</p> : (
|
||||||
|
<div className={"p-2 rounded-md border border-gray-250"}>
|
||||||
|
<div className={"mb-2 pb-3 border-b border-b-gray-250"}>
|
||||||
|
<InputControl
|
||||||
|
id={`bus-${busId}-main`}
|
||||||
|
name={`Main Level`}
|
||||||
|
basePath={`/bus/${busChannel.toString().padStart(2, "0")}/mix`}
|
||||||
|
appConfig={appConfig} faderControlSuffix={undefined} muteControlSuffix={undefined} />
|
||||||
|
</div>
|
||||||
|
<div className={"flex flex-col gap-3"}>
|
||||||
|
<h3>Inputs</h3>
|
||||||
|
{appConfig.inputs.map((input) => (
|
||||||
|
<InputControl
|
||||||
|
key={input.id}
|
||||||
|
id={`bus-${busId}-input-${input.id}`}
|
||||||
|
name={input.name}
|
||||||
|
faderControlSuffix={"level"}
|
||||||
|
basePath={`/ch/${input.channels[0].toString().padStart(2, "0")}/mix/${busChannel.toString().padStart(2, "0")}`}
|
||||||
|
appConfig={appConfig} muteControlSuffix={undefined} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BusControl;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React, {useEffect, useMemo, useState} from "react";
|
||||||
|
import {fetchConfig} from "@/app/utils/fetchConfig";
|
||||||
|
import BusControl from "@/app/components/BusControl";
|
||||||
|
import {Config} from "@/app/types/config";
|
||||||
|
|
||||||
|
const BusControlTabs = () => {
|
||||||
|
const [busControlConfigs, setBusControlConfigs] = useState([]);
|
||||||
|
const [activeTab, setActiveTab] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfig().then((config: Config) => {
|
||||||
|
if (config) {
|
||||||
|
setBusControlConfigs(config.bus_controls);
|
||||||
|
// create tab components from bus control configs
|
||||||
|
const components = config.bus_controls.map((busControl) => {
|
||||||
|
return {
|
||||||
|
id: busControl.id,
|
||||||
|
label: busControl.name,
|
||||||
|
content: <BusControl busControlConfig={busControl} mainConfig={config}/>
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// setTabComponents(components);
|
||||||
|
|
||||||
|
// set the active tab to the first tab
|
||||||
|
if (components.length > 0) {
|
||||||
|
setActiveTab(components[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tabComponents = useMemo(() => {
|
||||||
|
return busControlConfigs.map((busControl) => ({
|
||||||
|
id: busControl.id,
|
||||||
|
label: busControl.name,
|
||||||
|
content: (
|
||||||
|
<BusControl
|
||||||
|
key={busControl.id}
|
||||||
|
busControlConfig={busControl} mainConfig={undefined}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}, [busControlConfigs]);
|
||||||
|
|
||||||
|
|
||||||
|
if (!tabComponents.length) {
|
||||||
|
return <p>Loading...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Tabs Header */}
|
||||||
|
<div className="flex border-b mb-4">
|
||||||
|
{tabComponents.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
className={`px-4 py-2 ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? "border-b-2 border-blue-500 text-blue-500"
|
||||||
|
: "text-gray-500"
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs Content */}
|
||||||
|
<div>
|
||||||
|
{tabComponents.map((tab) => (
|
||||||
|
<div
|
||||||
|
key={tab.id}
|
||||||
|
style={{display: activeTab === tab.id ? "block" : "none"}}
|
||||||
|
>
|
||||||
|
{tab.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BusControlTabs;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {useState, useEffect} from "react";
|
||||||
|
import {Slider} from "@/components/ui/slider"
|
||||||
|
import {fetchValue, sendValue} from "@/app/lib/x32CommunicationManager";
|
||||||
|
|
||||||
|
|
||||||
|
interface FaderProps {
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
|
pollingInterval: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Fader: React.FC<FaderProps> = ({id, path, pollingInterval}) => {
|
||||||
|
|
||||||
|
const [faderValue, setFaderValue] = useState(0); // Values for each path
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateCurrentFaderValue = async () => {
|
||||||
|
fetchValue(path).then((value) => {
|
||||||
|
if (value !== faderValue) {
|
||||||
|
setFaderValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Initial fetch and polling
|
||||||
|
updateCurrentFaderValue();
|
||||||
|
|
||||||
|
// add random interval to avoid all clients polling at the same time
|
||||||
|
const randomInterval = Math.random() * pollingInterval;
|
||||||
|
setTimeout(() => null, randomInterval);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
updateCurrentFaderValue();
|
||||||
|
}, pollingInterval);
|
||||||
|
|
||||||
|
return () => clearInterval(interval); // Cleanup interval
|
||||||
|
}, [path, pollingInterval]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleFaderChange = async (path: string, newValue: number) => {
|
||||||
|
setFaderValue(newValue)
|
||||||
|
sendValue(path, newValue); // Send new value
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slider
|
||||||
|
id={`${id}-${path}-fader`}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
value={faderValue ? [faderValue,] : [0,]}
|
||||||
|
onValueChange={(newValue) => handleFaderChange(path, newValue[0])}
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to right, #4ade80 75%, #ef4444 75%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Fader;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import Fader from "@/app/components/Fader";
|
||||||
|
import MuteButton from "@/app/components/MuteButton";
|
||||||
|
|
||||||
|
|
||||||
|
const InputControl = ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
basePath,
|
||||||
|
faderControlSuffix,
|
||||||
|
muteControlSuffix,
|
||||||
|
appConfig
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
if (faderControlSuffix === undefined) {
|
||||||
|
faderControlSuffix = "fader";
|
||||||
|
}
|
||||||
|
if (muteControlSuffix === undefined) {
|
||||||
|
muteControlSuffix = "on";
|
||||||
|
}
|
||||||
|
|
||||||
|
const faderControlPath = `${basePath}/${faderControlSuffix}`;
|
||||||
|
const muteControlPath = `${basePath}/${muteControlSuffix}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"flex gap-5 w-auto"}>
|
||||||
|
<div className={"min-w-24"}>
|
||||||
|
<MuteButton
|
||||||
|
key={`inputControl-${id}-mute`}
|
||||||
|
id={`inputControl-${id}-mute`}
|
||||||
|
label={name}
|
||||||
|
path={muteControlPath}
|
||||||
|
pollingInterval={appConfig.polling_interval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Fader
|
||||||
|
key={`inputControl-${id}`}
|
||||||
|
id={`inputControl-${id}`}
|
||||||
|
path={faderControlPath}
|
||||||
|
pollingInterval={appConfig.polling_interval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InputControl;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {fetchConfig} from "@/app/utils/fetchConfig";
|
||||||
|
import InputControl from "@/app/components/InputControl";
|
||||||
|
|
||||||
|
const MainMix = () => {
|
||||||
|
|
||||||
|
const [appConfig, setAppConfig] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfig().then(config => {
|
||||||
|
setAppConfig(config);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
if (!appConfig) {
|
||||||
|
return <p> Loading...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"p-2 rounded-md border border-gray-250"}>
|
||||||
|
<div className={"mb-3 pb-3 border-b border-b-gray-250"}>
|
||||||
|
<InputControl id={"mainMix"} name={"Main Level"} basePath={"/main/st/mix"} appConfig={appConfig}
|
||||||
|
faderControlSuffix={undefined} muteControlSuffix={undefined}/>
|
||||||
|
</div>
|
||||||
|
<div className={"flex flex-col gap-5"}>
|
||||||
|
{appConfig.inputs.map((input) => (
|
||||||
|
<InputControl
|
||||||
|
key={input.id}
|
||||||
|
id={input.id}
|
||||||
|
name={input.name}
|
||||||
|
basePath={`/ch/${input.channels[0].toString().padStart(2, "0")}/mix`}
|
||||||
|
appConfig={appConfig} faderControlSuffix={undefined} muteControlSuffix={undefined} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MainMix;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
|
||||||
|
import {fetchValue, sendValue} from "@/app/lib/x32CommunicationManager";
|
||||||
|
|
||||||
|
const MuteButton = ({id, label, path, pollingInterval}) => {
|
||||||
|
|
||||||
|
|
||||||
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
const updateCurrentMuteValue = async () => {
|
||||||
|
fetchValue(path).then((value) => {
|
||||||
|
if (value !== isMuted) {
|
||||||
|
setIsMuted(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Initial fetch and polling
|
||||||
|
updateCurrentMuteValue();
|
||||||
|
|
||||||
|
// add random interval to avoid all clients polling at the same time
|
||||||
|
const randomInterval = Math.random() * pollingInterval;
|
||||||
|
setTimeout(() => null, randomInterval);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
updateCurrentMuteValue();
|
||||||
|
}, pollingInterval);
|
||||||
|
|
||||||
|
return () => clearInterval(interval); // Cleanup interval
|
||||||
|
}, [path, pollingInterval]);
|
||||||
|
|
||||||
|
|
||||||
|
const toggleMute = () => {
|
||||||
|
const newIsMuted = !isMuted;
|
||||||
|
|
||||||
|
// disable or enable the input for each channel
|
||||||
|
sendValue(
|
||||||
|
path,
|
||||||
|
newIsMuted ? 1 : 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsMuted(newIsMuted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button key={`${id}-${path}-mute`}
|
||||||
|
className={`w-24 ${isMuted ? 'bg-green-500' : 'bg-red-500'}`}
|
||||||
|
onClick={toggleMute}>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MuteButton;
|
||||||
+61
-10
@@ -2,20 +2,71 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
body {
|
||||||
--background: #ffffff;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
--foreground: #171717;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: #0a0a0a;
|
--background: 0 0% 100%;
|
||||||
--foreground: #ededed;
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary: 240 5.9% 10%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 240 10% 3.9%;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--background: 240 10% 3.9%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 240 10% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 240 10% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 240 5.9% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 240 3.7% 15.9%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 240 3.7% 15.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 240 4.9% 83.9%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
@layer base {
|
||||||
color: var(--foreground);
|
* {
|
||||||
background: var(--background);
|
@apply border-border;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// A global manager (no React hooks)
|
||||||
|
let ws: WebSocket | null = null;
|
||||||
|
let isConnected = false;
|
||||||
|
|
||||||
|
// Your choice: store responses in memory, or just call callbacks
|
||||||
|
let messageCallbacks: Array<(msg: string) => void> = [];
|
||||||
|
|
||||||
|
|
||||||
|
export function initConnection(wsUrl: string) {
|
||||||
|
if (ws) return; // Already connected or connecting
|
||||||
|
|
||||||
|
console.log("Connecting to WebSocket:", wsUrl);
|
||||||
|
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log("Global WebSocket connected!");
|
||||||
|
isConnected = true;
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log("Global WebSocket disconnected.");
|
||||||
|
isConnected = false;
|
||||||
|
ws = null; // So we can reconnect if needed
|
||||||
|
};
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
console.error("Global WebSocket error:", err);
|
||||||
|
};
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
// Notify any registered callbacks
|
||||||
|
messageCallbacks.forEach((cb) => cb(message));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error parsing WebSocket message:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerMessageCallback(cb: (msg: any) => void) {
|
||||||
|
messageCallbacks.push(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeMessageCallback(cb: (response: any) => void) {
|
||||||
|
messageCallbacks = messageCallbacks.filter((f) => f !== cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a message to the X32
|
||||||
|
export function sendMessage(msg: any) {
|
||||||
|
if (ws && isConnected) {
|
||||||
|
ws.send(JSON.stringify(msg));
|
||||||
|
} else {
|
||||||
|
console.error("WebSocket not connected. Message not sent:", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchValue(path: string): Promise<any> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
sendMessage({action: "getValue", path});
|
||||||
|
|
||||||
|
// We'll listen for the matching response just once
|
||||||
|
const callback = (response: any) => {
|
||||||
|
if (response.action === "response" && response.path === path) {
|
||||||
|
removeMessageCallback(callback);
|
||||||
|
resolve(response.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerMessageCallback(callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendValue(path: string, value: any) {
|
||||||
|
sendMessage({action: "sendValue", path, value});
|
||||||
|
}
|
||||||
+79
-98
@@ -1,101 +1,82 @@
|
|||||||
import Image from "next/image";
|
"use client";
|
||||||
|
|
||||||
export default function Home() {
|
import {useEffect, useMemo, useState} from "react";
|
||||||
return (
|
import {fetchConfig} from "@/app/utils/fetchConfig";
|
||||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
import BusControlTabs from "@/app/components/BusControlTabs";
|
||||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
import MainMix from "@/app/components/MainMix";
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={180}
|
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
|
||||||
<li className="mb-2">
|
|
||||||
Get started by editing{" "}
|
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
|
||||||
src/app/page.tsx
|
|
||||||
</code>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li>Save and see your changes instantly.</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
// initialize global websocket connection
|
||||||
<a
|
import {initConnection} from "@/app/lib/x32CommunicationManager";
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
const Page = () => {
|
||||||
target="_blank"
|
const [appConfig, setAppConfig] = useState(null);
|
||||||
rel="noopener noreferrer"
|
const [activeTab, setActiveTab] = useState("mainMix");
|
||||||
>
|
|
||||||
<Image
|
useEffect(() => {
|
||||||
className="dark:invert"
|
initConnection(`ws://${window.location.hostname}:8080`);
|
||||||
src="/vercel.svg"
|
fetchConfig().then(config => {
|
||||||
alt="Vercel logomark"
|
setAppConfig(config);
|
||||||
width={20}
|
});
|
||||||
height={20}
|
}, []);
|
||||||
/>
|
|
||||||
Deploy now
|
const tabComponents = useMemo(() => {
|
||||||
</a>
|
return [
|
||||||
<a
|
{
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
id: "mainMix",
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
label: "Main Mix",
|
||||||
target="_blank"
|
content: (
|
||||||
rel="noopener noreferrer"
|
<MainMix/>
|
||||||
>
|
),
|
||||||
Read our docs
|
},
|
||||||
</a>
|
{
|
||||||
|
id: "busControl",
|
||||||
|
label: "Bus Control",
|
||||||
|
content: (
|
||||||
|
<BusControlTabs/>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
, []);
|
||||||
|
|
||||||
|
if (!appConfig) {
|
||||||
|
return <p>Error fetching config</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="p-4">
|
||||||
|
{/* Tabs Header */}
|
||||||
|
<div className="flex border-b mb-4">
|
||||||
|
{tabComponents.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
className={`px-4 py-2 ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? "border-b-2 border-blue-500 text-blue-500"
|
||||||
|
: "text-gray-500"
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs Content */}
|
||||||
|
<div>
|
||||||
|
{tabComponents.map((tab) => (
|
||||||
|
<div
|
||||||
|
key={tab.id}
|
||||||
|
style={{display: activeTab === tab.id ? "block" : "none"}}
|
||||||
|
>
|
||||||
|
{tab.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
);
|
||||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
};
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
export default Page;
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
export interface Config {
|
||||||
|
polling_interval: number
|
||||||
|
inputs: any
|
||||||
|
bus_controls: any
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export const fetchConfig = async <T>(): Promise<T | null> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/config");
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Failed to fetch config:`, response.statusText);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching config from:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
+52
-8
@@ -1,18 +1,62 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
background: "var(--background)",
|
background: 'hsl(var(--background))',
|
||||||
foreground: "var(--foreground)",
|
foreground: 'hsl(var(--foreground))',
|
||||||
},
|
card: {
|
||||||
},
|
DEFAULT: 'hsl(var(--card))',
|
||||||
|
foreground: 'hsl(var(--card-foreground))'
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
|
foreground: 'hsl(var(--popover-foreground))'
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))'
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))'
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))'
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))'
|
||||||
|
},
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
chart: {
|
||||||
|
'1': 'hsl(var(--chart-1))',
|
||||||
|
'2': 'hsl(var(--chart-2))',
|
||||||
|
'3': 'hsl(var(--chart-3))',
|
||||||
|
'4': 'hsl(var(--chart-4))',
|
||||||
|
'5': 'hsl(var(--chart-5))'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: 'var(--radius)',
|
||||||
|
md: 'calc(var(--radius) - 2px)',
|
||||||
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("tailwindcss-animate")],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,8 @@
|
|||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
|
|||||||
Reference in New Issue
Block a user