Merge branch 'development'

This commit is contained in:
Jesse Malotaux 2025-05-03 21:31:46 +02:00
commit 358e4fea17
106 changed files with 10173 additions and 1027 deletions

17
.air.toml Normal file
View file

@ -0,0 +1,17 @@
# Working directory
root = "."
# The main Go file
main = "main.go"
bin = "tmp/main.exe"
# Watching all Go files, excluding certain directories
[build]
cmd = "go build -o ./tmp/main.exe main.go"
include_ext = ["go"]
exclude_dir = ["fe", "panels", "builds"]
# Restart on file changes
[log]
time = true

13
.gitignore vendored
View file

@ -1 +1,14 @@
.env
config.js
devices/*
tmp/
log.txt
Macrame.exe
public
macros/*
builds
node_modules
ToDo.md

47
add-gpl-header.sh Normal file
View file

@ -0,0 +1,47 @@
#!/bin/bash
# Directory to start searching from (default to current directory)
DIR="${1:-.}"
# Define the GPLv3 header content
GPL_HEADER="Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>."
# Loop through all files in the directory (and subdirectories), excluding node_modules
find "$DIR" \( -iname \*.go -o \( -path "$DIR/fe/src/*.js" -o -path "$DIR/fe/src/*.vue" -o -path "$DIR/fe/src/*.css" \) \) ! -path "*/node_modules/*" | while read file
do
# Check if the file already has a GPL header
if ! grep -q "Copyright (C) 2025 Jesse Malotaux" "$file"; then
# Check if it's a Vue file and handle it carefully
if [[ "$file" == *.vue ]]; then
echo "Adding GPL header to: $file"
# Prepend the GPL header to Vue files as raw text
# Make sure we don't add comment marks that break Vue syntax
echo -e "<!--\n$GPL_HEADER\n-->\n\n$(cat "$file")" > "$file"
else
echo "Adding GPL header to: $file"
# Prepend the GPL header to other files (go, js, ts, etc.) as comments
echo -e "/*\n$GPL_HEADER\n*/\n\n$(cat "$file")" > "$file"
fi
else
echo "GPL header already present in: $file"
fi
done
echo "GPL header addition complete!"

137
app/api.go Normal file
View file

@ -0,0 +1,137 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package app
import (
"macrame/app/helper"
"mime"
"net/http"
"path/filepath"
"strings"
)
func ApiCORS(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, *http.Request) {
origin := r.Header.Get("Origin")
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
if strings.HasPrefix(r.Host, "192.168.") {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Language, Accept-Encoding")
return w, r
}
func ApiGet(w http.ResponseWriter, r *http.Request) {
root, err := filepath.Abs("public")
if err != nil {
MCRMLog("ApiGet Abs Error: ", err)
return
}
var file string
if strings.Contains(r.URL.Path, "/config.js") {
file = filepath.Join(root, "config.js")
w.Header().Set("Content-Type", "text/javascript") // set content type header
} else if r.URL.Path != "/" {
file = filepath.Join(root, r.URL.Path)
}
contentType := mime.TypeByExtension(filepath.Ext(file)) // get content type
if contentType == "" {
file = filepath.Join(root, "index.html")
}
http.ServeFile(w, r, file)
}
func ApiPost(w http.ResponseWriter, r *http.Request) {
access, data, err := helper.EndpointAccess(w, r)
if !access || err != nil {
MCRMLog("ApiPost EndPointAccess Error: ", err)
return
}
if data != "" {
ApiAuth(data, w, r)
return
}
switch r.URL.Path {
case "/macro/check":
CheckMacro(w, r)
case "/macro/record":
SaveMacro(w, r)
case "/macro/list":
ListMacros(w, r)
case "/macro/open":
OpenMacro(w, r)
case "/macro/delete":
DeleteMacro(w, r)
case "/macro/play":
PlayMacro("", w, r)
case "/device/server/ip":
ListServerIP(w)
case "/device/list":
DeviceList(w, r)
case "/device/access/check":
DeviceAccessCheck(w, r)
case "/device/access/request":
DeviceAccessRequest(w, r)
case "/device/link/ping":
PingLink(w, r)
case "/device/link/start":
StartLink(w, r)
case "/device/link/poll":
PollLink(w, r)
case "/device/link/remove":
RemoveLink("", w, r)
case "/device/handshake":
Handshake(w, r)
case "/panel/list":
PanelList(w, r)
case "/panel/get":
GetPanel("", w, r)
case "/panel/save/json":
SavePanelJSON(w, r)
}
}
func ApiAuth(data string, w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/macro/play":
PlayMacro(data, w, r)
case "/device/link/remove":
RemoveLink(data, w, r)
case "/panel/list":
MCRMLog("Authenticated Panellist")
PanelList(w, r)
case "/panel/get":
GetPanel(data, w, r)
}
}

329
app/device.go Normal file
View file

@ -0,0 +1,329 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package app
import (
"encoding/json"
"fmt"
"macrame/app/helper"
"macrame/app/structs"
"math/rand"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
func ListServerIP(w http.ResponseWriter) {
ip, err := GetServerIp()
if err != nil {
MCRMLog("GetServerIP err: ", err)
return
}
json.NewEncoder(w).Encode(ip)
}
func GetServerIp() (string, error) {
ifs, err := net.Interfaces()
if err != nil {
MCRMLog(err)
return "", err
}
for _, ifi := range ifs {
// Skip interfaces that are down
if ifi.Flags&net.FlagUp == 0 {
continue
}
// Skip loopback interfaces
if ifi.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := ifi.Addrs()
if err != nil {
MCRMLog(err)
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.To4() == nil {
continue
}
// Skip APIPA (169.254.x.x) addresses
if ip.IsLinkLocalUnicast() {
continue
}
// Found a good IP, return it
return ip.String(), nil
}
}
return "", fmt.Errorf("No IP found")
}
func DeviceList(w http.ResponseWriter, r *http.Request) {
dir := "devices"
files, err := os.ReadDir(dir)
if err != nil {
os.MkdirAll(dir, 0600)
files = nil
MCRMLog("DeviceList Error: ", err)
}
devices := make(map[string]map[string]interface{})
for _, file := range files {
filePath := dir + "/" + file.Name()
ext := filepath.Ext(filePath)
device := strings.TrimSuffix(file.Name(), ext)
if _, ok := devices[device]; !ok {
devices[device] = make(map[string]interface{})
}
if ext == ".json" {
devices[device]["settings"] = readDeviceSettings(filePath)
}
if ext == ".key" {
devices[device]["key"] = true
}
}
result := map[string]interface{}{
"devices": devices,
}
json.NewEncoder(w).Encode(result)
}
func readDeviceSettings(filepath string) (settings structs.Settings) {
data, err := os.ReadFile(filepath)
if err != nil {
MCRMLog("readDeviceSettings Error: ", err)
}
err = json.Unmarshal(data, &settings)
if err != nil {
MCRMLog("readDeviceSettings JSON Error: ", err)
}
return settings
}
func DeviceAccessCheck(w http.ResponseWriter, r *http.Request) {
var req structs.Check
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
MCRMLog("DeviceAccessCheck Error: ", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_, errSett := os.Stat("devices/" + req.Uuid + ".json")
_, errKey := os.Stat("devices/" + req.Uuid + ".key")
if (errSett == nil) && (errKey == nil) {
json.NewEncoder(w).Encode("authorized")
} else if (errSett == nil) && (errKey != nil) {
MCRMLog("DeviceAccessCheck: UUID: ", req.Uuid, "; Access: Unauthorized")
json.NewEncoder(w).Encode("unauthorized")
} else {
MCRMLog("DeviceAccessCheck: UUID: ", req.Uuid, "; Access: Unlinked")
json.NewEncoder(w).Encode("unlinked")
}
}
func DeviceAccessRequest(w http.ResponseWriter, r *http.Request) {
var req structs.Request
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
deviceSettings := structs.Settings{Name: req.Name, Type: req.Type}
settingsJSON, err := json.Marshal(deviceSettings)
if err != nil {
MCRMLog("DeviceAccessRequest JSON Error: ", err)
return
}
err = os.WriteFile("devices/"+req.Uuid+".json", settingsJSON, 0644)
if err != nil {
MCRMLog("DeviceAccessRequest Error: ", err)
return
}
json.NewEncoder(w).Encode("unauthorized")
}
func PingLink(w http.ResponseWriter, r *http.Request) {
var req structs.Check
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
MCRMLog("PingLink Error: ", err)
json.NewEncoder(w).Encode(false)
return
}
key, keyErr := os.ReadFile("devices/" + req.Uuid + ".key")
pin, pinErr := os.ReadFile("devices/" + req.Uuid + ".tmp")
encryptedKey, encErr := helper.EncryptAES(string(pin), string(key))
if keyErr == nil && pinErr == nil && encErr == nil {
MCRMLog("PINGLINK FIXED")
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(encryptedKey))
return
} else {
MCRMLog("PingLink Error: keyErr:", keyErr, "; pinErr:", pinErr, "; encErr:", encErr)
}
json.NewEncoder(w).Encode(false)
}
func StartLink(w http.ResponseWriter, r *http.Request) {
var req structs.Check
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
MCRMLog("StartLink Error: ", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
pin := fmt.Sprintf("%04d", rand.Intn(10000))
deviceKey := helper.GenerateKey()
errKey := helper.SaveDeviceKey(req.Uuid, deviceKey)
savedPin, errPin := helper.TempPinFile(req.Uuid, pin)
if errKey == nil && errPin == nil && savedPin {
json.NewEncoder(w).Encode(pin)
} else {
MCRMLog("StartLink Error: errKey:", errKey, "; errPin:", errPin)
}
}
func PollLink(w http.ResponseWriter, r *http.Request) {
var req structs.Check
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
json.NewEncoder(w).Encode(false)
return
}
if helper.CheckPinFile(req.Uuid) {
json.NewEncoder(w).Encode(true)
return
}
json.NewEncoder(w).Encode(false)
}
func RemoveLink(data string, w http.ResponseWriter, r *http.Request) {
req := &structs.Check{}
_, err := helper.ParseRequest(req, data, r)
if err != nil {
MCRMLog("RemoveLink ParseRequest Error: ", err)
json.NewEncoder(w).Encode(false)
return
}
err = os.Remove("devices/" + req.Uuid + ".key")
if err != nil {
MCRMLog("RemoveLink Remove Error: ", err)
json.NewEncoder(w).Encode(false)
return
}
json.NewEncoder(w).Encode(true)
}
func Handshake(w http.ResponseWriter, r *http.Request) {
var req structs.Handshake
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return
}
deviceKey, err := helper.GetKeyByUuid(req.Uuid)
if err != nil {
MCRMLog("Handshake GetKeyByUuid Error: ", err)
json.NewEncoder(w).Encode(false)
return
}
decryptShake, _ := helper.DecryptAES(deviceKey, req.Shake)
helper.RemovePinFile(req.Uuid)
if decryptShake == getDateStr() {
json.NewEncoder(w).Encode(true)
return
} else {
os.Remove("devices/" + req.Uuid + ".key")
}
json.NewEncoder(w).Encode(false)
}
func getDateStr() string {
date := time.Now()
year, month, day := date.Date()
formattedDate := fmt.Sprintf("%04d%02d%02d", year, month, day)
return formattedDate
}

123
app/helper/api-helper.go Normal file
View file

@ -0,0 +1,123 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package helper
import (
"encoding/json"
"errors"
"net"
"net/http"
"strings"
"macrame/app/structs"
. "macrame/app/structs"
)
func EndpointAccess(w http.ResponseWriter, r *http.Request) (bool, string, error) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return false, "", errors.New(r.URL.Path + ": SplitHostPort error: " + err.Error())
}
if (isLocal(ip) && isEndpointAllowed("Local", r.URL.Path)) ||
(isLanRemote(ip) && isEndpointAllowed("Remote", r.URL.Path)) {
return true, "", nil
} else if isLanRemote(ip) && isEndpointAllowed("Auth", r.URL.Path) {
data, err := decryptAuth(r)
if err != nil {
return false, "", err
}
return data != "", data, nil
}
return false, "", errors.New(r.URL.Path + ": not authorized or accessible")
}
func isLocal(ip string) bool {
return ip == "127.0.0.1" || ip == "::1"
}
func isLanRemote(ip string) bool {
return strings.HasPrefix(ip, "192.168.")
}
func isEndpointAllowed(source string, endpoint string) bool {
var endpoints, err = getAllowedEndpoints(source)
if err != "" {
return false
}
if (endpoints != nil) && (len(endpoints) > 0) {
for _, e := range endpoints {
if e == endpoint {
return true
}
}
}
return false
}
func getAllowedEndpoints(source string) (endpoints []string, err string) {
if source == "Local" {
return Endpoints.Local, ""
}
if source == "Remote" {
return Endpoints.Remote, ""
}
if source == "Auth" {
return Endpoints.Auth, ""
}
return []string{}, "No allowed endpoints"
}
func decryptAuth(r *http.Request) (string, error) {
var req structs.Authcall
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil || req.Uuid == "" || req.Data == "" {
return "", errors.New("DecryptAuth Error: " + err.Error() + "; UUID: " + req.Uuid + "; Data: " + req.Data)
}
deviceKey, errKey := GetKeyByUuid(req.Uuid)
decryptData, errDec := DecryptAES(deviceKey, req.Data)
if errKey != nil && errDec != nil || decryptData == "" {
return "", errors.New("DecryptAuth Error: " + errKey.Error() + "; " + errDec.Error() + "; UUID: " + req.Uuid + "; Data: " + req.Data)
}
return decryptData, nil
}
func ParseRequest(req interface{}, data string, r *http.Request) (d interface{}, err error) {
if data != "" {
dataBytes := []byte(data)
return req, json.Unmarshal(dataBytes, &req)
} else {
return req, json.NewDecoder(r.Body).Decode(&req)
}
}

View file

@ -0,0 +1,44 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package helper
import (
"os/exec"
"runtime"
)
func OpenBrowser(url string) bool {
var args []string
switch runtime.GOOS {
case "darwin":
args = []string{"open"}
case "windows":
args = []string{"cmd", "/c", "start"}
default:
args = []string{"xdg-open"}
}
cmd := exec.Command(args[0], append(args[1:], url)...)
return cmd.Start() == nil
}

View file

@ -0,0 +1,66 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package helper
import (
"errors"
"os"
"time"
)
func TempPinFile(Uuid string, pin string) (bool, error) {
err := os.WriteFile("devices/"+Uuid+".tmp", []byte(pin), 0644)
if err != nil {
return false, errors.New("TempPinFile Error: " + err.Error())
}
time.AfterFunc(1*time.Minute, func() {
os.Remove("devices/" + Uuid + ".tmp")
})
return true, nil
}
func RemovePinFile(Uuid string) error { return os.Remove("devices/" + Uuid + ".tmp") }
func CheckPinFile(Uuid string) bool {
_, err := os.Stat("devices/" + Uuid + ".tmp")
return err == nil
}
func SaveDeviceKey(Uuid string, key string) error {
err := os.WriteFile("devices/"+Uuid+".key", []byte(key), 0644)
if err != nil {
return err
}
return nil
}
func GetKeyByUuid(Uuid string) (string, error) {
data, err := os.ReadFile("devices/" + Uuid + ".key")
if err != nil {
return "", err
}
return string(data), nil
}

View file

@ -0,0 +1,135 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package helper
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
mathRand "math/rand"
"strings"
)
func EncryptAES(key string, plaintext string) (string, error) {
origData := []byte(plaintext)
// Create AES cipher
block, err := aes.NewCipher(keyToBytes(key))
if err != nil {
return "", err
}
blockSize := block.BlockSize()
origData = PKCS5Padding(origData, blockSize)
iv := []byte(EnvGet("MCRM__IV"))
blockMode := cipher.NewCBCEncrypter(block, iv)
crypted := make([]byte, len(origData))
blockMode.CryptBlocks(crypted, origData)
cryptedString := base64.StdEncoding.EncodeToString(crypted)
return cryptedString, nil
}
func DecryptAES(key string, cryptedText string) (string, error) {
crypted, err := base64.StdEncoding.DecodeString(cryptedText)
if err != nil {
return "", err
}
block, err := aes.NewCipher(keyToBytes(key))
if err != nil {
return "", err
}
iv := []byte(EnvGet("MCRM__IV"))
blockMode := cipher.NewCBCDecrypter(block, iv)
origData := make([]byte, len(crypted))
blockMode.CryptBlocks(origData, crypted)
origData, err = PKCS5UnPadding(origData)
if err != nil || len(origData) <= 3 {
return "", errors.New("invalid key")
}
origDataString := string(origData)
return origDataString, nil
}
func GenerateRandomString(length int) string {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
return base64.StdEncoding.EncodeToString(b)
}
func GenerateRandomIntegerString(length int) string {
var sb strings.Builder
for i := 0; i < length; i++ {
sb.WriteByte('0' + byte(mathRand.Intn(10)))
}
return sb.String()
}
func GenerateKey() string {
return strings.Replace(GenerateRandomString(24), "=", "", -1)
}
func keyToBytes(key string) []byte {
// Convert key to bytes
keyBytes := []byte(key)
// If key is 4 characters, append salt
if len(key) == 4 {
keyBytes = []byte(key + EnvGet("MCRM__SALT"))
}
return keyBytes
}
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func PKCS5UnPadding(origData []byte) ([]byte, error) {
length := len(origData)
unpadding := int(origData[length-1])
if (unpadding >= length) || (unpadding == 0) {
return nil, errors.New("unpadding error")
}
return origData[:(length - unpadding)], nil
}

128
app/helper/env-helper.go Normal file
View file

@ -0,0 +1,128 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package helper
import (
"encoding/json"
"log"
"net"
"os"
"strconv"
"strings"
)
var configPath = "public/config.js"
func EnvGet(key string) string {
if !configFileExists() {
CreateConfigFile(configPath)
CheckFeDevDir()
}
data, err := os.ReadFile(configPath)
if err != nil {
log.Println("Error reading config.js:", err)
return ""
}
raw := strings.TrimSpace(string(data))
raw = strings.TrimPrefix(raw, "window.__CONFIG__ = ")
raw = strings.TrimSuffix(raw, ";")
var config map[string]string
if err := json.Unmarshal([]byte(raw), &config); err != nil {
log.Println("Error parsing config.js:", err)
return ""
}
return config[key]
}
func configFileExists() bool {
_, err := os.Stat(configPath)
return err == nil
}
func CheckFeDevDir() {
log.Println("Checking FE dev directory...")
_, err := os.Stat("fe")
if err != nil {
log.Println("Error checking FE dev directory:", err)
return
}
copyConfigToFe()
}
func copyConfigToFe() {
data, err := os.ReadFile(configPath)
if err != nil {
log.Println("Error reading config.js:", err)
return
}
if err := os.WriteFile("fe/config.js", data, 0644); err != nil {
log.Println("Error writing config.js:", err)
}
}
func CreateConfigFile(filename string) {
port, _ := findOpenPort()
saltKey := GenerateKey()
salt := saltKey[:28]
iv := GenerateRandomIntegerString(16)
config := map[string]string{
"MCRM__PORT": port,
"MCRM__SALT": salt,
"MCRM__IV": iv,
}
jsonData, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Println("Error creating config JSON:", err)
return
}
jsData := "window.__CONFIG__ = " + string(jsonData) + ";"
log.Println("Created JS config:", jsData)
if err := os.WriteFile(filename, []byte(jsData), 0644); err != nil {
log.Println("Error writing config.js:", err)
}
}
func findOpenPort() (string, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return "", err
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return "", err
}
defer l.Close()
return strconv.Itoa(l.Addr().(*net.TCPAddr).Port), nil
}

114
app/helper/macro-helper.go Normal file
View file

@ -0,0 +1,114 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package helper
import (
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/go-vgo/robotgo"
)
func FormatMacroFileName(s string) string {
// Remove invalid characters
re := regexp.MustCompile(`[\/\?\*\>\<\:\"\|
]`)
s = re.ReplaceAllString(s, "")
// Replace spaces with underscores
s = strings.ReplaceAll(s, " ", "_")
// Remove special characters
re = regexp.MustCompile(`[!@#$%^&\(\)\[\]\{\}\~]`)
s = re.ReplaceAllString(s, "")
// Truncate the string
if len(s) > 255 {
s = s[:255]
}
return s
}
func ReadMacroFile(filename string) (steps []map[string]interface{}, err error) {
content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
err = json.Unmarshal(content, &steps)
return steps, err
}
func RunMacroSteps(steps []map[string]interface{}) error {
for _, step := range steps {
switch step["type"] {
case "key":
keyCode := step["code"].(string)
if strings.Contains(keyCode, "|") {
keyCode = handleToggleCode(keyCode, step["direction"].(string))
}
robotgo.KeyToggle(keyCode, step["direction"].(string))
case "delay":
time.Sleep(time.Duration(step["value"].(float64)) * time.Millisecond)
default:
return errors.New("RunMacroSteps Unknown step type: %v" + fmt.Sprint(step["type"]))
}
}
return nil
}
var toggleCodes = map[string]bool{}
func handleToggleCode(keyCode string, direction string) string {
splitCodes := strings.Split(keyCode, "|")
if direction == "down" {
if _, ok := toggleCodes[splitCodes[0]]; !ok {
toggleCodes[splitCodes[0]] = true
return splitCodes[0]
}
return splitCodes[1]
}
if direction == "up" {
if toggleCodes[splitCodes[0]] {
toggleCodes[splitCodes[0]] = false
return splitCodes[0]
}
delete(toggleCodes, splitCodes[0])
return splitCodes[1]
}
return ""
}

View file

@ -0,0 +1,101 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package helper
import (
"strings"
)
var translations = map[string]string{
"ArrowUp": "up",
"ArrowDown": "down",
"ArrowRight": "right",
"ArrowLeft": "left",
"Meta": "cmd",
"MetaLeft": "lcmd",
"MetaRight": "rcmd",
"Alt": "alt",
"AltLeft": "lalt",
"AltRight": "ralt",
"Control": "ctrl",
"ControlLeft": "lctrl",
"ControlRight": "rctrl",
"Shift": "shift",
"ShiftLeft": "lshift",
"ShiftRight": "rshift",
"AudioVolumeMute": "audio_mute",
"AudioVolumeDown": "audio_vol_down",
"AudioVolumeUp": "audio_vol_up",
"MediaTrackPrevious": "audio_prev",
"MediaTrackNext": "audio_next",
"MediaPlayPause": "audio_play|audio_pause",
"Numpad0": "num0",
"Numpad1": "num1",
"Numpad2": "num2",
"Numpad3": "num3",
"Numpad4": "num4",
"Numpad5": "num5",
"Numpad6": "num6",
"Numpad7": "num7",
"Numpad8": "num8",
"Numpad9": "num9",
"NumLock": "num_lock",
"NumpadDecimal": "num.",
"NumpadAdd": "num+",
"NumpadSubtract": "num-",
"NumpadMultiply": "num*",
"NumpadDivide": "num/",
"NumpadEnter": "num_enter",
"Clear": "num_clear",
"BracketLeft": "[",
"BracketRight": "]",
"Quote": "'",
"Semicolon": ";",
"Backquote": "`",
"Backslash": "\\",
"IntlBackslash": "\\",
"Slash": "/",
"Comma": ",",
"Period": ".",
"Equal": "=",
"Minus": "-",
}
func Translate(code string) string {
if val, ok := translations[code]; ok {
return val
}
return strings.ToLower(code)
}
func ReverseTranslate(name string) string {
if name == "\\" {
return "Backslash"
}
for key, value := range translations {
if value == name {
return key
}
}
return name
}

45
app/log.go Normal file
View file

@ -0,0 +1,45 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package app
import (
"log"
"os"
)
var logFile *os.File
func MCRMLogInit() {
var err error
// Open or create the log file with append permissions
logFile, err = os.OpenFile("log.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err) // Exit if we can't open the log file
}
// Optionally set log to write to file in addition to standard log output
log.SetOutput(logFile)
}
func MCRMLog(v ...interface{}) {
log.Println(v...) // Logs to terminal as well
}

229
app/macro.go Normal file
View file

@ -0,0 +1,229 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package app
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"macrame/app/helper"
"macrame/app/structs"
)
func CheckMacro(w http.ResponseWriter, r *http.Request) {
var req structs.MacroRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
MCRMLog("OpenMacro Decode Error: ", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var filename = helper.FormatMacroFileName(req.Macro)
macroFile, err := helper.ReadMacroFile(fmt.Sprintf("macros/%s.json", filename))
if macroFile != nil && err == nil {
json.NewEncoder(w).Encode(true)
return
} else {
MCRMLog("OpenMacro ReadMacroFile Error: ", err)
json.NewEncoder(w).Encode(false)
return
}
}
func SaveMacro(w http.ResponseWriter, r *http.Request) {
var newMacro structs.NewMacro
body, err := io.ReadAll(r.Body)
if err != nil {
MCRMLog("SaveMacro ReadAll Error: ", err)
return
}
err = json.Unmarshal(body, &newMacro)
if err != nil {
MCRMLog("SaveMacro Unmarshal Error: ", err)
return
}
simplifiedSteps := make([]map[string]interface{}, 0)
for _, step := range newMacro.Steps {
simplifiedSteps = append(simplifiedSteps, simplifyMacro(step))
}
stepsJSON, err := json.Marshal(simplifiedSteps)
if err != nil {
MCRMLog("SaveMacro Marshal Error: ", err)
return
}
err = os.WriteFile("macros/"+helper.FormatMacroFileName(newMacro.Name)+".json", stepsJSON, 0644)
if err != nil {
MCRMLog("SaveMacro WriteFile Error: ", err)
return
}
}
func simplifyMacro(step structs.Step) map[string]interface{} {
simplified := make(map[string]interface{})
simplified["type"] = step.Type
switch step.Type {
case "delay":
simplified["value"] = step.Value
case "key":
keyCode := step.Code
if keyCode == "" || (strings.Contains(keyCode, "Digit")) {
keyCode = step.Key
} else if strings.Contains(keyCode, "Key") {
keyCode = strings.Replace(keyCode, "Key", "", 1)
}
simplified["code"] = helper.Translate(keyCode)
simplified["direction"] = step.Direction
}
return simplified
}
func ListMacros(w http.ResponseWriter, r *http.Request) {
dir := "macros"
files, err := os.ReadDir(dir)
if err != nil {
MCRMLog("ListMacros ReadDir Error: ", err)
json.NewEncoder(w).Encode(false)
return
}
var macroList []structs.MacroInfo
for _, file := range files {
filename := filepath.Base(file.Name())
macroname := strings.TrimSuffix(filename, filepath.Ext(filename))
nicename := strings.Replace(macroname, "_", " ", -1)
macroList = append(macroList, structs.MacroInfo{
Name: nicename,
Macroname: macroname,
})
}
json.NewEncoder(w).Encode(macroList)
}
func DeleteMacro(w http.ResponseWriter, r *http.Request) {
var req structs.MacroRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
MCRMLog("DeleteMacro Decode Error: ", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var filename = helper.FormatMacroFileName(req.Macro)
err = os.Remove("macros/" + filename + ".json")
if err != nil {
MCRMLog("DeleteMacro Remove Error: ", err)
json.NewEncoder(w).Encode(false)
return
}
log.Println("Deleted Macro:", req.Macro)
json.NewEncoder(w).Encode(true)
}
func PlayMacro(data string, w http.ResponseWriter, r *http.Request) {
req := &structs.MacroRequest{}
_, err := helper.ParseRequest(req, data, r)
if err != nil {
MCRMLog("PlayMacro ParseRequest Error: ", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
macro := req.Macro
MCRMLog("Playing Macro: ", macro)
var filename = helper.FormatMacroFileName(macro)
var filepath = fmt.Sprintf("macros/%s.json", filename)
macroFile, err := helper.ReadMacroFile(filepath)
if err != nil {
MCRMLog("PlayMacro ReadMacroFile Error: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = helper.RunMacroSteps(macroFile)
if err != nil {
MCRMLog("PlayMacro RunMacroSteps Error: ", err)
return
}
}
func OpenMacro(w http.ResponseWriter, r *http.Request) {
var req structs.MacroRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
MCRMLog("OpenMacro Decode Error: ", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
macroFile, err := helper.ReadMacroFile(fmt.Sprintf("macros/%s.json", req.Macro))
if err != nil {
MCRMLog("OpenMacro ReadMacroFile Error: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Walk through the macro file and reverse translate codes
for i, action := range macroFile {
if actionType, ok := action["type"].(string); ok && actionType == "key" {
if code, ok := action["code"].(string); ok {
macroFile[i]["code"] = helper.ReverseTranslate(code)
}
}
}
json.NewEncoder(w).Encode(macroFile)
}

176
app/panel.go Normal file
View file

@ -0,0 +1,176 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package app
import (
"encoding/base64"
"encoding/json"
"macrame/app/helper"
"macrame/app/structs"
"net/http"
"os"
"strings"
)
func PanelList(w http.ResponseWriter, r *http.Request) {
panelDirs, err := os.ReadDir("panels")
if err != nil {
MCRMLog("PanelList ReadDir Error: ", err)
json.NewEncoder(w).Encode(false)
return
}
var panelList []interface{}
for _, panelDir := range panelDirs {
if panelDir.IsDir() {
panelList = append(panelList, getPanelInfo(panelDir.Name()))
}
}
if len(panelList) == 0 {
MCRMLog("PanelList: No panels found")
json.NewEncoder(w).Encode(false)
return
}
json.NewEncoder(w).Encode(panelList)
}
func getPanelInfo(dirname string) structs.PanelInfo {
var panelInfo structs.PanelInfo
jsonFile, err := os.ReadFile("panels/" + dirname + "/panel.json")
if err != nil {
panelInfo.Name = strings.Replace(dirname, "_", " ", -1)
panelInfo.Description = "null"
panelInfo.AspectRatio = "null"
panelInfo.Macros = make(map[string]string)
} else {
err = json.Unmarshal(jsonFile, &panelInfo)
if err != nil {
MCRMLog("getPanelInfo Unmarshal Error: ", err)
}
}
panelInfo.Dir = dirname
thumb := getPanelThumb(dirname)
panelInfo.Thumb = thumb
return panelInfo
}
func getPanelThumb(dirname string) string {
extensions := []string{".jpg", ".jpeg", ".png", ".webp"}
for _, ext := range extensions {
filename := "thumbnail" + ext
file, err := os.Open("panels/" + dirname + "/" + filename)
if err != nil {
MCRMLog("getPanelThumb Open Error: ", err)
continue
}
defer file.Close()
return encodeImg(file)
}
return ""
}
func getPanelCode(dirname string) (html string, css string) {
htmlBytes, _ := os.ReadFile("panels/" + dirname + "/index.html")
cssBytes, _ := os.ReadFile("panels/" + dirname + "/output.css")
return string(htmlBytes), string(cssBytes)
}
func encodeImg(file *os.File) string {
contents, err := os.ReadFile(file.Name())
if err != nil {
MCRMLog("encodeImg ReadFile Error: ", err)
return ""
}
encoded := base64.StdEncoding.EncodeToString(contents)
return encoded
}
func GetPanel(data string, w http.ResponseWriter, r *http.Request) {
req := &structs.PanelRequest{}
_, err := helper.ParseRequest(req, data, r)
if err != nil {
MCRMLog("GetPanel ParseRequest Error: ", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var response = structs.PanelResponse{}
panelInfo := getPanelInfo(req.Dir)
panelHtml, panelCss := getPanelCode(req.Dir)
response.Dir = panelInfo.Dir
response.Name = panelInfo.Name
response.Description = panelInfo.Description
response.AspectRatio = panelInfo.AspectRatio
response.Macros = panelInfo.Macros
response.Thumb = panelInfo.Thumb
response.HTML = panelHtml
response.Style = panelCss
json.NewEncoder(w).Encode(response)
}
func SavePanelJSON(w http.ResponseWriter, r *http.Request) {
var req structs.PanelSaveJSON
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
MCRMLog("SavePanelJSON Decode Error: ", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
filePath := "panels/" + req.Dir + "/panel.json"
req.Dir = ""
// Marshal the data to JSON without the dir field
jsonData, err := json.Marshal(req)
if err != nil {
MCRMLog("SavePanelJSON Marshal Error: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = os.WriteFile(filePath, jsonData, 0644)
if err != nil {
MCRMLog("SavePanelJSON WriteFile Error: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

66
app/structs/api-struct.go Normal file
View file

@ -0,0 +1,66 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package structs
type Allowed struct {
Local []string
Remote []string
Auth []string
}
var Endpoints = Allowed{
Local: []string{
"/macro/check",
"/macro/record",
"/macro/list",
"/macro/open",
"/macro/delete",
"/macro/play",
"/device/server/ip",
"/device/list",
"/device/access/check",
"/device/access/request",
"/device/link/ping",
"/device/link/start",
"/device/link/poll",
"/device/link/remove",
"/device/handshake",
"/panel/get",
"/panel/list",
"/panel/save/json",
},
Remote: []string{
"/device/access/check",
"/device/access/request",
"/device/server/ip",
"/device/link/ping",
"/device/link/end",
"/device/handshake",
"/device/auth",
},
Auth: []string{
"/macro/play",
"/device/link/remove",
"/panel/get",
"/panel/list",
},
}

View file

@ -0,0 +1,52 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package structs
type Settings struct {
Name string `json:"name"`
Type string `json:"type"`
}
type RemoteWebhook struct {
Event string `json:"event"`
Data string `json:"data"`
}
type Check struct {
Uuid string `json:"uuid"`
}
type Request struct {
Uuid string `json:"uuid"`
Name string `json:"name"`
Type string `json:"type"`
}
type Handshake struct {
Uuid string `json:"uuid"`
Shake string `json:"shake"`
}
type Authcall struct {
Uuid string `json:"uuid"`
Data string `json:"d"`
}

View file

@ -0,0 +1,58 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package structs
type MacroRequest struct {
Macro string `json:"macro"`
}
type Step struct {
Type string `json:"type"`
Key string `json:"key"`
Code string `json:"code"`
Location int `json:"location"`
Direction string `json:"direction"`
Value int `json:"value"`
}
type NewMacro struct {
Name string `json:"name"`
Steps []Step `json:"steps"`
}
type MacroInfo struct {
Name string `json:"name"`
Macroname string `json:"macroname"`
}
type MacroKey struct {
Type string `json:"type"`
Key string `json:"key"`
Code string `json:"code"`
Location int `json:"location"`
Direction string `json:"direction"`
}
type MacroDelay struct {
Type string `json:"type"`
Value int `json:"value"`
}

View file

@ -0,0 +1,65 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package structs
type PanelJSON struct {
Name string `json:"name"`
Description string `json:"description"`
AspectRatio string `json:"aspectRatio"`
Macros map[string]string `json:"macros"`
}
type PanelSaveJSON struct {
// Name string `json:"name"`
// Description string `json:"description"`
// AspectRatio string `json:"aspectRatio"`
// Macros map[string]string `json:"macros"`
Dir string `json:"dir"`
Name string `json:"name"`
Description string `json:"description"`
AspectRatio string `json:"aspectRatio"`
Macros interface{} `json:"macros"`
}
type PanelInfo struct {
Dir string `json:"dir"`
Name string `json:"name"`
Description string `json:"description"`
AspectRatio string `json:"aspectRatio"`
Macros map[string]string `json:"macros"`
Thumb string `json:"thumb"`
}
type PanelRequest struct {
Dir string `json:"dir"`
}
type PanelResponse struct {
Dir string `json:"dir"`
Name string `json:"name"`
Description string `json:"description"`
AspectRatio string `json:"aspectRatio"`
Macros map[string]string `json:"macros"`
Thumb string `json:"thumb"`
HTML string `json:"html"`
Style string `json:"style"`
}

64
app/systray.go Normal file
View file

@ -0,0 +1,64 @@
package app
import (
"macrame/app/helper"
"os"
"github.com/getlantern/systray"
)
func InitSystray() {
go func() {
systray.Run(OnReady, OnExit)
}()
}
func OnReady() {
systray.SetIcon(getIcon("favicon.ico"))
systray.SetTitle("Macrame")
systray.SetTooltip("Macrame - Server")
ip, err := GetServerIp()
if err == nil {
systray.AddMenuItem("IP: "+ip+":"+helper.EnvGet("MCRM__PORT"), "Server IP")
}
systray.AddSeparator()
addMCRMItem("Dashboard", "/")
addMCRMItem("Panels", "/panels")
addMCRMItem("Macros", "/macros")
addMCRMItem("Devices", "/devices")
systray.AddSeparator()
mQuit := systray.AddMenuItem("Quit Macrame", "Quit Macrame")
go func() {
<-mQuit.ClickedCh
os.Exit(0)
}()
}
func addMCRMItem(name, urlPath string) {
m := systray.AddMenuItem(name, name)
go func() {
<-m.ClickedCh
helper.OpenBrowser("http://localhost:" + helper.EnvGet("MCRM__PORT") + urlPath)
}()
}
func OnExit() {
systray.Quit()
}
func getIcon(path string) []byte {
icon, err := os.ReadFile(path)
if err != nil {
MCRMLog("getIcon Error: ", err)
}
return icon
}

35
build.sh Normal file
View file

@ -0,0 +1,35 @@
#!/bin/bash
# Set the name of the build directory
BUILD_DIR="Macrame_$(date +'%m%d%H%M%S')"
# Build the Go application
go build -ldflags "-H=windowsgui" -o Macrame.exe main.go
# Build the frontend
cd fe
npm run build
cd ../builds
# Create the new build directory
mkdir $BUILD_DIR
mkdir $BUILD_DIR/macros
mkdir $BUILD_DIR/panels
mkdir $BUILD_DIR/panels/test_panel
mkdir $BUILD_DIR/public
mkdir $BUILD_DIR/public/assets
# Move the generated files to the new build directory
cp ../Macrame.exe $BUILD_DIR/Macrame.exe
cp ../favicon.ico $BUILD_DIR/favicon.ico
find ../macros -type f ! -name 'ED-*' -exec cp --parents {} "$BUILD_DIR/macros/" \;
cp -r ../panels/test_panel/* $BUILD_DIR/panels/test_panel/
mv ../public/* $BUILD_DIR/public/
# cp ../install.bat $BUILD_DIR/install.bat
powershell -Command "Compress-Archive -Path $BUILD_DIR/* -DestinationPath $BUILD_DIR.zip -Force"
# Print the path to the new build directory
echo "Build directory: ../$BUILD_DIR"

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

9
fe/.editorconfig Normal file
View file

@ -0,0 +1,9 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

1
fe/.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto eol=lf

30
fe/.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

7
fe/.prettierrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

8
fe/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode"
]
}

View file

@ -2,14 +2,16 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="src/assets/Macrame-Logo-gradient.svg" />
<link rel="icon" type="image/svg+xml" href="mcrm-icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<link rel="preconnect" href="https://fonts.bunny.net" />
<link
href="https://fonts.bunny.net/css?family=fira-code:300,500,700|roboto:100,300,700"
rel="stylesheet"
/>
<title>Vite + Vue</title>
<title>Macrame</title>
<script src="config.js"></script>
</head>
<body>
<div id="app"></div>

60
fe/mcrm-icon.svg Normal file
View file

@ -0,0 +1,60 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px"
viewBox="0 0 140 80"
style="enable-background:new 0 0 140 80;"
xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);}
.st2{fill:url(#SVGID_3_);}
.st3{fill:url(#SVGID_4_);}
.st4{fill:url(#SVGID_5_);}
.st5{fill:url(#SVGID_6_);}
.st6{fill:url(#SVGID_7_);}
</style>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="28.05" x2="140" y2="28.05">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st0" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12L95.5,18.3z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="28" x2="140" y2="28">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st1" d="M57.3,29.5L46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3s-0.4,0-0.5,0.1l-0.2,0.1L36.8,26l12,12L57.3,29.5z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="65.25" x2="140" y2="65.25">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st2" d="M94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.7,8.1-2.2L94.7,67
L94.7,67z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="32.9162" x2="140" y2="32.9162">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st3" d="M114,15.5l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19
l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8
s-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3c3.1-3.1,4.7-7.2,4.7-11.3c0-4.1-1.5-8.2-4.6-11.3L114,15.5z"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="38.2163" x2="140" y2="38.2163">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st4" d="M105.4,56.5l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7c-1-1-2.1-1.8-3.2-2.5
C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9C9.7,32,8.1,36.1,8.1,40.2c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3
l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8s0.4-2.1,1.1-2.8L34.7,24l8.5-8.5l0.1,0.1c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l2.1,2.1
l8.5,8.5l2.1,2.1l8.5,8.5l16.2,16.2L97,65l8.4,8.4c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8l-3.5-3.4L105.4,56.5z"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="59.85" x2="140" y2="59.85">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st5" d="M70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.8,8.1-2.2
c1.1-0.7,2.2-1.5,3.2-2.5L70,59.3l8.5-8.5L70.1,42.3z"/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="52.6" x2="140" y2="52.6">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st6" d="M43.1,65.1l0.1-0.1l16.2-16.2l8.5-8.5l-8.4-8.6L51,40.2L38.2,53l-3.5,3.5l-5,5L26.2,65l7.8,7.8
c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5L43.1,65.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

1238
fe/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite --profile",
"dev": "vite --host",
"build": "vite build --emptyOutDir",
"preview": "vite preview",
"lint": "eslint . --fix",
@ -27,6 +27,7 @@
"eslint-plugin-vue": "^9.32.0",
"pinia": "^3.0.1",
"prettier": "^3.5.1",
"qrcode": "^1.5.4",
"sass-embedded": "^1.85.1",
"tailwindcss": "^4.0.9",
"uuid": "^11.1.0",

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="app-background">
<img src="./assets/img/bg-gradient.svg" aria-hidden="true" />
@ -5,21 +26,46 @@
</div>
<MainMenu />
<RouterView />
<AlertComp
v-if="!isLocal && !handshake && route.fullPath !== '/devices'"
variant="warning"
:page-wide="true"
href="/devices"
>
<h4>Not authorized!</h4>
<p>Click here to start authorization and open the "Devices" page on your PC.</p>
</AlertComp>
</template>
<script setup>
import MainMenu from '@/components/base/MainMenu.vue'
import { onMounted } from 'vue'
import { RouterView } from 'vue-router'
import { onMounted, ref } from 'vue'
import { RouterView, useRoute } from 'vue-router'
import { useDeviceStore } from './stores/device'
import { isLocal } from './services/ApiService'
import AlertComp from './components/base/AlertComp.vue'
const device = useDeviceStore()
const route = useRoute()
const handshake = ref(false)
onMounted(() => {
// Setting device uuid from localstorage
// If not present in LocalStorage a new uuidV4 will be generated
device.uuid()
if (!isLocal) appHandshake()
device.$subscribe(() => {
if (device.key()) handshake.value = true
})
})
async function appHandshake() {
const hsReq = await device.remoteHandshake()
handshake.value = hsReq
}
</script>
<style scoped>
@ -45,7 +91,6 @@ onMounted(() => {
top-[10%]
left-[10%]
scale-[1.8]
p-28
opacity-35
mix-blend-overlay;
}

View file

@ -1,89 +1,38 @@
@import "./style/_macro.css";
@import "./style/_mcrm-block.css";
@import "./style/_panel.css";
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
@import "tailwindcss";
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@import './style/_content.css';
@import './style/_form.css';
@import './style/_scrollbar.css';
@import './style/_macro.css';
@import './style/_mcrm-block.css';
@import './style/_panel.css';
@import 'tailwindcss';
@variant dark (&:where(.dark, .dark *));
@theme {
--font-sans: "Roboto", sans-serif;
--font-mono: "Fira Code", monospace;
}
body {
@apply font-sans
font-light
tracking-wide
bg-slate-900
text-slate-50;
}
h1,
h2 {
@apply font-mono
font-bold;
}
h3,
h4,
h5,
h6 {
@apply font-semibold;
}
h1 {
@apply text-4xl;
}
h2 {
@apply text-3xl;
}
h3 {
@apply text-2xl;
}
h4 {
@apply text-xl;
}
h5 {
@apply text-lg;
}
input {
@apply w-full
px-2 py-1
border
border-slate-400
text-white
rounded-md
bg-black/20;
}
:has(> input + span) {
@apply flex;
input {
@apply rounded-r-none;
}
span {
@apply flex
items-center
px-2
rounded-r-md
text-white
bg-slate-400;
}
}
ul {
@apply list-disc
list-inside;
}
strong {
@apply font-bold;
html,
body,
:not(#panel-html__body) {
--font-sans: 'Roboto', sans-serif;
--font-mono: 'Fira Code', monospace;
}

View file

@ -0,0 +1,68 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
body {
@apply font-sans font-light tracking-wide bg-slate-900 text-slate-50;
}
h1,
h2 {
@apply font-mono font-bold;
}
h3,
h4,
h5,
h6 {
@apply font-semibold;
}
h1 {
@apply text-4xl;
}
h2 {
@apply text-3xl;
}
h3 {
@apply text-2xl;
}
h4 {
@apply text-xl;
}
h5 {
@apply text-lg;
}
ul {
@apply list-disc list-inside;
}
strong {
@apply font-bold;
}
a {
@apply underline text-amber-400 hover:text-amber-300;
}

View file

@ -0,0 +1,49 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.input-group {
@apply grid gap-2;
}
input,
select {
@apply w-full px-2 py-1 text-white border rounded-md border-slate-400 bg-black/20;
}
:has(> input + span) {
@apply flex;
input {
@apply rounded-r-none;
}
span {
@apply flex items-center px-2 text-white rounded-r-md bg-slate-400;
}
}
select option {
@apply bg-slate-700;
&:not([disabled]) {
@apply cursor-pointer;
}
}

View file

@ -1,3 +1,24 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* @reference "main"; */
hr.spacer {
@apply relative

View file

@ -1,11 +1,26 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.mcrm-block {
@apply relative
p-6
gap-x-6
gap-y-2
backdrop-blur-lg
rounded-2xl
overflow-hidden;
@apply relative p-6 overflow-hidden gap-x-6 gap-y-2 backdrop-blur-lg rounded-2xl;
&::before {
@apply content-['']
@ -16,7 +31,8 @@
size-full
bg-gradient-to-br
to-transparent
z-[-1];
z-[10]
pointer-events-none;
mask:
linear-gradient(#000 0 0) exclude,
@ -40,18 +56,18 @@
}
&.block__primary {
@apply bg-sky-300/40;
@apply bg-sky-300/20;
&::before {
@apply from-sky-100/40;
@apply from-sky-100/20;
}
}
&.block__secondary {
@apply bg-amber-300/40;
@apply bg-amber-300/20;
&::before {
@apply from-amber-100/40;
@apply from-amber-100/20;
}
}

View file

@ -1,3 +1,24 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.panel {
@apply grid
grid-rows-[auto_1fr]
@ -22,19 +43,12 @@
}
.panel__title {
@apply bg-gradient-to-r
w-fit
from-amber-300
to-white/50
pt-3
pl-16 sm:pl-4
bg-clip-text
text-transparent;
@apply pt-3 pl-16 text-transparent bg-gradient-to-r w-fit from-amber-300 to-white/50 sm:pl-4 bg-clip-text;
}
.panel__content {
@apply grid
h-full
h-[calc(100%-1rem)]
pt-4 sm:pt-0
pl-0 sm:pl-4
overflow-auto;

View file

@ -0,0 +1,44 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
::-webkit-scrollbar {
@apply w-2;
}
::-moz-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-thumb {
@apply rounded bg-slate-400/80;
}
::-moz-scrollbar-thumb {
@apply rounded bg-slate-400/80;
}
::-webkit-scrollbar-track {
@apply mr-1 rounded bg-slate-100/10;
}
::-moz-scrollbar-track {
@apply mr-1 rounded bg-slate-100/10;
}

View file

@ -1,20 +1,76 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="accordion">
<header>
<slot name="title" />
<header @click="toggleAccordion(!accordionOpen)">
<h4>{{ title }}</h4>
<ButtonComp variant="ghost" size="sm" class="!px-1">
<IconChevronDown v-if="!accordionOpen" />
<IconChevronUp v-else />
</ButtonComp>
</header>
<section :class="`accordion__content ${open ? 'open' : ''}`">
<div>
<slot name="content" />
<section :class="`accordion__wrapper ${accordionOpen ? 'open' : ''}`">
<div class="accordion__content">
<slot />
</div>
</section>
</div>
</template>
<script setup>
defineProps({
import { onMounted, onUpdated, ref } from 'vue'
import ButtonComp from './ButtonComp.vue'
import { IconChevronDown, IconChevronUp } from '@tabler/icons-vue'
const emit = defineEmits(['onOpen', 'onClose', 'onToggle'])
defineExpose({ toggleAccordion })
const props = defineProps({
title: String,
open: Boolean,
})
const accordionOpen = ref(false)
onMounted(() => {
if (props.open) toggleAccordion(props.open)
})
onUpdated(() => {
if (props.open) toggleAccordion(props.open)
})
function toggleAccordion(open = false) {
if (open) {
accordionOpen.value = true
emit('onOpen')
} else {
accordionOpen.value = false
emit('onClose')
}
emit('onToggle')
}
</script>
<style scoped>
@ -23,16 +79,40 @@ defineProps({
.accordion {
@apply grid;
.accordion__content {
header {
@apply grid
grid-cols-[1fr_auto]
px-4 py-2
cursor-pointer;
}
.accordion__wrapper {
@apply grid
grid-rows-[0fr]
overflow-hidden
border-y
border-b-white/60
border-t-transparent
duration-300
ease-in-out;
div {
.accordion__content {
@apply grid
grid-rows-[0fr];
grid-rows-[0fr]
overflow-hidden
opacity-0
transition-opacity
delay-0;
}
&.open {
@apply grid-rows-[1fr]
border-t-white/20;
.accordion__content {
@apply grid-rows-[1fr]
opacity-100
delay-200;
}
}
}
}

View file

@ -1,10 +1,36 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div :class="`alert alert__${type}`">
<IconInfoCircle v-if="type === 'info'" />
<IconCheck v-if="type === 'success'" />
<IconExclamationCircle v-if="type === 'warning'" />
<IconAlertTriangle v-if="type === 'error'" />
<slot />
<div
:class="`alert alert__${variant} ${pageWide ? 'page-wide' : ''}`"
@click="href ? router.push(href) : null"
>
<IconInfoCircle v-if="variant === 'info'" />
<IconCheck v-if="variant === 'success'" />
<IconExclamationCircle v-if="variant === 'warning'" />
<IconAlertTriangle v-if="variant === 'error'" />
<div class="alert__content">
<slot />
</div>
</div>
</template>
@ -15,10 +41,15 @@ import {
IconExclamationCircle,
IconInfoCircle,
} from '@tabler/icons-vue'
import { useRouter } from 'vue-router'
defineProps({
type: String, // info, success, warning, error
variant: String, // info, success, warning, error
pageWide: Boolean,
href: String,
})
const router = useRouter()
</script>
<style scoped>
@ -51,5 +82,19 @@ defineProps({
&.alert__error {
@apply text-rose-400 bg-rose-400/10;
}
&.page-wide {
@apply fixed
bottom-0 left-0
w-full;
}
&[href] {
@apply cursor-pointer;
}
.alert__content {
@apply grid gap-2;
}
}
</style>

View file

@ -1,8 +1,29 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<template v-if="href">
<a :href="href" :class="classString">
<RouterLink :to="href" :class="classString">
<slot />
</a>
</RouterLink>
</template>
<template v-else>
<button :class="classString">
@ -12,7 +33,7 @@
</template>
<script setup>
import { computed, onMounted } from 'vue'
import { computed } from 'vue'
const props = defineProps({
href: String,
@ -45,7 +66,8 @@ button,
tracking-wide
font-normal
transition-all
cursor-pointer;
cursor-pointer
no-underline;
transition:
border-color 0.1s ease-in-out,
@ -64,19 +86,29 @@ button,
@apply size-5 transition-[stroke] duration-400 ease-in-out;
}
&.btn__sm svg {
@apply size-4;
&.btn__sm {
@apply px-3 py-1
text-sm;
svg {
@apply size-4;
}
}
&.btn__lg svg {
@apply size-6;
&.btn__lg {
@apply px-6 py-3
text-lg;
svg {
@apply size-6;
}
}
&:hover {
@apply !text-white;
@apply text-white;
svg {
@apply !stroke-white;
@apply stroke-current;
}
}

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="button-group">
<slot />

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="context-menu">
<div class="context-menu__trigger" @click="toggle">
@ -25,8 +46,6 @@ onMounted(() => {
})
function toggle() {
console.log('toggle')
menuOpen.value = !menuOpen.value
}
</script>

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="dialog-container">
<div class="trigger" @click="toggleDialog(true)">

View file

@ -0,0 +1,59 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div v-if="loading" class="loading-component">
<span v-if="text">
{{ text }}
</span>
<IconLoader3 class="duration-1000 animate-spin" />
</div>
</template>
<script setup>
import { IconLoader3 } from '@tabler/icons-vue'
defineProps({
loading: Boolean,
text: String,
})
</script>
<style>
@reference "@/assets/main.css";
&:has(.loading-component) {
@apply relative;
}
.loading-component {
@apply absolute
inset-0
size-full
flex gap-2
flex-col
justify-center
items-center
text-sm
bg-black/50
backdrop-blur-md;
}
</style>

View file

@ -1,12 +1,29 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<nav id="main-menu">
<button
id="menu-toggle"
:class="menuOpen ? 'open' : ''"
@click="menuOpen = !menuOpen"
>
<button id="menu-toggle" :class="menuOpen ? 'open' : ''" @click="menuOpen = !menuOpen">
<img
class="logo p-1"
class="p-1 logo"
:class="{ 'opacity-0': menuOpen }"
src="@/assets/img/Macrame-Logo-gradient.svg"
aria-hidden="true"
@ -15,36 +32,30 @@
</button>
<ul :class="menuOpen ? 'open' : ''">
<li>
<RouterLink @click="menuOpen = false" to="/">
<IconHome />Dashboard
</RouterLink>
<RouterLink @click="menuOpen = false" to="/"> <IconHome />Dashboard </RouterLink>
</li>
<li>
<RouterLink @click="menuOpen = false" to="/panels">
<IconLayoutGrid />Panels
</RouterLink>
<RouterLink @click="menuOpen = false" to="/panels"> <IconLayoutGrid />Panels </RouterLink>
</li>
<li>
<RouterLink @click="menuOpen = false" to="/macros">
<IconKeyboard />Macros
</RouterLink>
<li v-if="isLocal()">
<RouterLink @click="menuOpen = false" to="/macros"> <IconKeyboard />Macros </RouterLink>
</li>
<li>
<RouterLink @click="menuOpen = false" to="/devices">
<IconDevices />Device
<IconDevices />{{ isLocal() ? 'Devices' : 'Server' }}
</RouterLink>
</li>
<li>
<!-- <li>
<RouterLink @click="menuOpen = false" to="/settings">
<IconSettings />Settings
</RouterLink>
</li>
</li> -->
</ul>
</nav>
</template>
<script setup>
import { RouterLink } from "vue-router";
import { RouterLink } from 'vue-router'
import {
IconDevices,
IconHome,
@ -52,10 +63,11 @@ import {
IconLayoutGrid,
IconSettings,
IconX,
} from "@tabler/icons-vue";
import { ref } from "vue";
} from '@tabler/icons-vue'
import { ref } from 'vue'
import { isLocal } from '@/services/ApiService'
const menuOpen = ref(false);
const menuOpen = ref(false)
</script>
<style>
@ -116,6 +128,8 @@ nav {
items-center
gap-2
px-4 py-2
text-white
no-underline
border-transparent
transition-colors;

View file

@ -0,0 +1,107 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="remote-dashboard">
<div id="panels" class="dashboard-block mcrm-block block__light" v-if="server.handshake">
<div class="icon__container">
<IconLayoutGrid />
</div>
<h4>{{ server.panelCount }} {{ server.panelCount != 1 ? 'Panels' : 'Panel' }}</h4>
<template v-if="server.panelCount == 0">
<p><em>No panels found. </em></p>
<p>Learn how to create a panel <a href="#" target="_blank">here</a>.</p>
</template>
<template v-else>
<p>Start using a panel!</p>
<ButtonComp variant="danger" href="/panels"> <IconLayoutGrid /> View panels </ButtonComp>
</template>
</div>
<div id="server" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconServer />
</div>
<h4>Server</h4>
<template v-if="server.handshake">
<p>
Linked with: <strong class="text-center">{{ server.ip }}</strong>
</p>
<ButtonComp variant="primary" href="/devices"> <IconServer /> View server</ButtonComp>
</template>
<template v-else>
<p>
<em>Not linked</em>
</p>
<ButtonComp variant="primary" href="/devices"> <IconLink /> Link with server</ButtonComp>
</template>
</div>
</div>
</template>
<script setup>
import { IconLayoutGrid, IconLink, IconServer } from '@tabler/icons-vue'
import { onMounted, reactive } from 'vue'
import ButtonComp from '../base/ButtonComp.vue'
import { useDeviceStore } from '@/stores/device'
import { usePanelStore } from '@/stores/panel'
const device = useDeviceStore()
const panel = usePanelStore()
const server = reactive({
ip: '',
handshake: '',
panelCount: 0,
})
onMounted(async () => {
const serverIp = await device.serverGetIP()
server.ip = serverIp
if (device.key()) server.handshake = true
device.$subscribe(() => {
if (device.key()) server.handshake = true
})
const panelCount = await panel.getList(true)
server.panelCount = panelCount
})
</script>
<style scoped>
@reference "@/assets/main.css";
#remote-dashboard {
@apply grid
pt-8
gap-4
md:w-fit
h-fit
content-start;
&.not__linked #server {
@apply row-start-1 md:col-start-1;
}
}
</style>

View file

@ -0,0 +1,154 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div
id="server-dashboard"
:class="`${server.remoteCount == 0 ? 'no__devices' : 'devices__found'} ${server.macroCount == 0 ? 'no__macros' : 'macros__found'}`"
>
<div id="devices" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconDevices />
</div>
<h4>{{ server.remoteCount }} {{ server.remoteCount != 1 ? 'Devices' : 'Device' }}</h4>
<template v-if="server.remoteCount == 0">
<p><em>No devices found.</em></p>
<ButtonComp variant="primary" href="/devices"> <IconLink /> Link a device</ButtonComp>
</template>
<template v-else>
<p>Unlink a device or add new devices.</p>
<ButtonComp variant="primary" href="/devices"><IconDevices /> View devices</ButtonComp>
</template>
</div>
<div id="macros" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconKeyboard />
</div>
<h4>{{ server.macroCount }} {{ server.macroCount != 1 ? 'Macros' : 'Macro' }}</h4>
<template v-if="server.macroCount == 0">
<p><em>No macros found.</em></p>
<ButtonComp variant="secondary" href="/macros"> <IconLayoutGrid /> Create macro</ButtonComp>
</template>
<template v-else>
<p>Edit and view your macros.</p>
<ButtonComp variant="secondary" href="/macros"><IconKeyboard /> View macros</ButtonComp>
</template>
</div>
<div id="panels" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconLayoutGrid />
</div>
<h4>{{ server.panelCount }} {{ server.panelCount != 1 ? 'Panels' : 'Panel' }}</h4>
<template v-if="server.panelCount == 0">
<p><em>No panels found. </em></p>
<p>Learn how to create a panel <a href="#" target="_blank">here</a>.</p>
</template>
<template v-else>
<p>Link macros to panels or view a panel.</p>
<ButtonComp variant="danger" href="/panels"> <IconLayoutGrid /> View panels </ButtonComp>
</template>
</div>
</div>
</template>
<script setup>
import { useDeviceStore } from '@/stores/device'
import { usePanelStore } from '@/stores/panel'
import { IconDevices, IconKeyboard, IconLayoutGrid, IconLink } from '@tabler/icons-vue'
import { onMounted, reactive } from 'vue'
import ButtonComp from '../base/ButtonComp.vue'
import { GetMacroList } from '@/services/MacroService'
const device = useDeviceStore()
const panel = usePanelStore()
const server = reactive({
ip: '',
port: '',
fullPath: '',
remoteCount: 0,
macroCount: 0,
panelCount: 0,
})
onMounted(async () => {
const serverIP = await device.serverGetIP()
server.ip = serverIP
// server.port = window.__CONFIG__.MCRM__PORT
// server.fullPath = `http://${server.ip}:${server.port}`
const remoteCount = await device.serverGetRemotes(true)
server.remoteCount = remoteCount
const macroCount = await GetMacroList(true)
server.macroCount = macroCount
const panelCount = await panel.getList(true)
server.panelCount = panelCount
console.log(server)
})
</script>
<style scoped>
@reference "@/assets/main.css";
#server-dashboard {
@apply grid
grid-cols-1
grid-rows-3
md:grid-cols-3
md:grid-rows-1
gap-4
w-fit
h-fit
pt-8;
&.no__devices #devices {
@apply row-start-1 md:col-start-1;
}
&.no__macros.devices__found #devices {
@apply row-start-3 md:col-start-3;
}
&.devices__found #devices {
@apply row-start-3 md:col-start-3;
}
&.no__devices.no__macros #macros {
@apply row-start-2 md:col-start-2;
}
&.no__macros #macros {
@apply row-start-1 md:col-start-1;
}
&.macros__found #macros {
@apply row-start-2 md:col-start-2;
}
&.no__devices.macros__found #macros {
@apply row-start-3 md:col-start-3;
}
}
</style>

View file

@ -1,14 +1,33 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="server-overview">
<AlertComp type="info">
<div class="grid">
<strong>This is a remote device.</strong>
<em>UUID: {{ device.uuid() }} </em>
</div>
<AlertComp variant="info">
<strong>This is a remote device.</strong>
<em>UUID: {{ device.uuid() }} </em>
</AlertComp>
<div class="mcrm-block block__light grid gap-4">
<h4 class="text-lg flex gap-4 items-center justify-between">
<div class="grid gap-4 mcrm-block block__light">
<h4 class="flex items-center justify-between gap-4 text-lg">
<span class="flex gap-4"><IconServer />Server</span>
<ButtonComp variant="primary" @click="checkServerStatus()"><IconReload /></ButtonComp>
</h4>
@ -18,15 +37,25 @@
</p>
<!-- Alerts -->
<AlertComp v-if="server.status === 'authorized'" type="success">Authorized</AlertComp>
<AlertComp v-if="server.status === 'unlinked'" type="warning">Not linked</AlertComp>
<AlertComp v-if="server.status === 'unauthorized'" type="info">
<AlertComp v-if="server.status === 'authorized'" variant="success">Authorized</AlertComp>
<AlertComp v-if="server.status === 'unlinked'" variant="warning">Not linked</AlertComp>
<AlertComp v-if="server.status === 'unauthorized'" variant="info">
<div class="grid gap-2">
<strong>Access requested</strong>
<p>
Navigate to <em class="font-semibold">http://localhost:6970/devices</em> on your pc to
authorize.
</p>
<ul class="mb-4">
<li>
Navigate to <em class="font-semibold">http://localhost:{{ server.port }}/devices</em>.
</li>
<li>
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
Click on
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
<IconLink class="size-4" /> Link device
</span>
</div>
</li>
<li>Enter the the pin shown on the desktop in the dialog that will appear.</li>
</ul>
<template v-if="server.link == 'checking'">
<div class="grid grid-cols-[2rem_1fr] gap-2">
<IconReload class="animate-spin" />
@ -40,14 +69,6 @@
</template>
</div>
</AlertComp>
<ButtonComp
v-if="server.status === 'unauthorized'"
variant="primary"
@click="requestAccess()"
>
<IconKey />
Request access
</ButtonComp>
<ButtonComp
variant="danger"
v-if="server.status === 'authorized'"
@ -59,15 +80,17 @@
</div>
<DialogComp ref="linkPinDialog">
<template #content>
<div class="grid gap-4 w-64">
<div class="grid w-64 gap-4">
<h3>Server link pin:</h3>
<form class="grid gap-4" @submit.prevent="decryptKey()">
<input
ref="linkPinInput"
class="input"
id="input-pin"
type="text"
pattern="[0-9]{4}"
v-model="server.inputPin"
autocomplete="off"
/>
<ButtonComp variant="primary">Enter</ButtonComp>
</form>
@ -85,7 +108,7 @@
// - - if checkAccess -> pingLink -> check for device.tmp (go)
// - - if [devicePin] -> handshake -> save key local, close dialog, update server status
import { IconKey, IconPlugConnectedX, IconReload, IconServer } from '@tabler/icons-vue'
import { IconKey, IconLink, IconPlugConnectedX, IconReload, IconServer } from '@tabler/icons-vue'
import AlertComp from '../base/AlertComp.vue'
import ButtonComp from '../base/ButtonComp.vue'
import { onMounted, onUpdated, reactive, ref } from 'vue'
@ -99,9 +122,11 @@ import { appUrl } from '@/services/ApiService'
const device = useDeviceStore()
const linkPinDialog = ref()
const linkPinInput = ref()
const server = reactive({
host: '',
port: window.__CONFIG__.MCRM__PORT,
status: false,
link: false,
inputPin: '',
@ -115,6 +140,8 @@ onMounted(async () => {
onUpdated(() => {
if (!server.status) checkServerStatus()
if (server.status === 'authorized' && server.inputPin) server.inputPin = ''
})
async function checkServerStatus(request = true) {
@ -160,11 +187,13 @@ function pingLink() {
server.encryptedKey = encryptedKey
linkPinDialog.value.toggleDialog(true)
linkPinInput.value.focus()
})
}
async function decryptKey() {
const decryptedKey = decryptAES(server.inputPin, server.encryptedKey)
const handshake = await device.remoteHandshake(decryptedKey)
if (handshake) {

View file

@ -1,63 +1,136 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="device-overview">
<AlertComp type="info">
<div class="grid">
<strong>This is a server!</strong>
<em>UUID: {{ device.uuid() }} </em>
</div>
<AlertComp variant="info">
<strong>This is a server!</strong>
<em>UUID: {{ device.uuid() }} </em>
</AlertComp>
<div class="mcrm-block block__light flex flex-wrap items-start gap-4">
<h4 class="w-full flex gap-4 items-center justify-between mb-4">
<span class="flex gap-4"> <IconDevices />Remote devices </span>
<ButtonComp variant="primary" @click="device.serverGetRemotes()"><IconReload /></ButtonComp>
<div class="flex flex-wrap items-start gap-4 mcrm-block block__light">
<h4 class="flex items-center justify-between w-full gap-4 mb-4">
<span class="flex gap-4">
<IconDevices />{{ Object.keys(remote.devices).length }}
{{ Object.keys(remote.devices).length == 1 ? 'Device' : 'Devices' }}
</span>
<ButtonComp v-if="!remote.poll" variant="primary" @click="device.serverGetRemotes()"
><IconReload
/></ButtonComp>
</h4>
<!-- {{ Object.keys(remote.devices).length }} -->
<template v-if="Object.keys(remote.devices).length > 0">
<div
class="mcrm-block block__dark block-size__sm w-64 grid !gap-4 content-start"
v-for="(remoteDevice, id) in remote.devices"
:key="id"
>
<div class="grid gap-2">
<h5 class="grid grid-cols-[auto_1fr] gap-2">
<IconDeviceUnknown v-if="remoteDevice.settings.type == 'unknown'" />
<IconDeviceMobile v-if="remoteDevice.settings.type == 'mobile'" />
<IconDeviceTablet v-if="remoteDevice.settings.type == 'tablet'" />
<IconDeviceDesktop v-if="remoteDevice.settings.type == 'desktop'" />
<span class="w-full truncate">
{{ remoteDevice.settings.name }}
</span>
</h5>
<em>{{ id }}</em>
<template v-for="(remoteDevice, id) in remote.devices" :key="id">
<div class="mcrm-block block__dark block-size__sm w-64 grid !gap-4 content-start">
<div class="grid gap-2">
<h5 class="grid grid-cols-[auto_1fr] gap-2">
<IconDeviceUnknown v-if="remoteDevice.settings.type == 'unknown'" />
<IconDeviceMobile v-if="remoteDevice.settings.type == 'mobile'" />
<IconDeviceTablet v-if="remoteDevice.settings.type == 'tablet'" />
<IconDeviceDesktop v-if="remoteDevice.settings.type == 'desktop'" />
<span class="w-full truncate">
{{ remoteDevice.settings.name }}
</span>
</h5>
<em>{{ id }}</em>
</div>
<template v-if="remoteDevice.key">
<AlertComp variant="success">Authorized</AlertComp>
<ButtonComp variant="danger" @click="unlinkDevice(id)">
<IconLinkOff />Unlink device
</ButtonComp>
</template>
<template v-else>
<AlertComp variant="warning">Unauthorized</AlertComp>
<ButtonComp variant="primary" @click="startLink(id)">
<IconLink />Link device
</ButtonComp>
</template>
<template v-if="remote.pinlink.uuid == id">
<AlertComp variant="info">One time pin: {{ remote.pinlink.pin }}</AlertComp>
</template>
</div>
<template v-if="remoteDevice.key">
<AlertComp type="success">Authorized</AlertComp>
<ButtonComp variant="danger" @click="unlinkDevice(id)">
<IconLinkOff />Unlink device
</ButtonComp>
</template>
<template v-else>
<AlertComp type="warning">Unauthorized</AlertComp>
<ButtonComp variant="primary" @click="startLink(id)">
<IconLink />Link device
</ButtonComp>
</template>
<template v-if="remote.pinlink.uuid == id">
<AlertComp type="info">One time pin: {{ remote.pinlink.pin }}</AlertComp>
</template>
</div>
</template>
</template>
<template v-else>
<!-- <template v-else>
<div class="grid w-full gap-4">
<em class="text-slate-300">No remote devices</em>
<em class="text-slate-300">No remote devices</em>
</div>
</template>
</template> -->
<AccordionComp
class="w-full mt-8 border-t border-t-white/50"
title="How to connect a device?"
:open="Object.keys(remote.devices).length == 0"
>
<div class="grid py-4">
<ul class="space-y-2">
<li>
Scan the QR code with the remote device.
<div class="grid gap-4 py-4 pl-6">
<canvas ref="serverQr"></canvas>
<p>
Or manually type the IP address: <br />
<strong>{{ server.ip }}/devices</strong>
</p>
</div>
</li>
<li>
The device will automatically request access, if you see "Access requested" on the
device.
</li>
<li v-if="!remote.poll">
<div class="inline-flex items-center gap-2">
Click the
<span class="p-1 border rounded-sm"><IconReload class="size-4" /></span> to reload
the devices.
</div>
</li>
<li>
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
Click on
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
<IconLink class="size-4" /> Link device
</span>
A one-time-pin will be shown in a dialog.
</div>
</li>
<li>Enter the pin on the remote device.</li>
<li>
Congratulations! You have linked a device! You can now start using panels on that
device.
</li>
</ul>
</div>
</AccordionComp>
<DialogComp ref="pinDialog">
<template #content>
<div class="grid gap-4">
<h3>Pin code</h3>
<span class="text-4xl font-mono tracking-wide">{{ remote.pinlink.pin }}</span>
<span class="font-mono text-4xl tracking-wide">{{ remote.pinlink.pin }}</span>
</div>
</template>
</DialogComp>
@ -66,12 +139,7 @@
</template>
<script setup>
// TODO
// - startLink -> responsePin also in device block
// - startLink -> poll removal of pin file, if removed close dialog, update device list
// - Make unlink work
import { onMounted, reactive, ref } from 'vue'
import { onMounted, onUpdated, reactive, ref } from 'vue'
import AlertComp from '../base/AlertComp.vue'
import { useDeviceStore } from '@/stores/device'
import {
@ -88,21 +156,54 @@ import ButtonComp from '../base/ButtonComp.vue'
import DialogComp from '../base/DialogComp.vue'
import axios from 'axios'
import { appUrl } from '@/services/ApiService'
import AccordionComp from '../base/AccordionComp.vue'
import QRCode from 'qrcode'
const device = useDeviceStore()
const pinDialog = ref()
const serverQr = ref()
const remote = reactive({ devices: [], pinlink: false })
const server = reactive({
ip: '',
})
onMounted(() => {
const remote = reactive({ devices: [], pinlink: false, poll: false })
onMounted(async () => {
device.serverGetRemotes()
device.$subscribe((mutation, state) => {
if (Object.keys(state.remote).length) remote.devices = device.remote
if (state.remote !== remote.devices) remote.devices = device.remote
})
getIp()
})
onUpdated(() => {
getIp()
if (Object.keys(remote.devices).length == 0 && !remote.poll) {
remote.poll = setInterval(() => {
device.serverGetRemotes()
}, 1000)
}
if (Object.keys(remote.devices).length > 0 && remote.poll) {
clearInterval(remote.poll)
remote.poll = false
}
})
async function getIp() {
const serverIP = await device.serverGetIP()
server.ip = serverIP
QRCode.toCanvas(serverQr.value, `${server.ip}/devices`, (error) => {
if (error) console.log('QRCode error: ', error)
})
}
async function startLink(deviceUuid) {
const pin = await device.serverStartLink(deviceUuid)
@ -135,7 +236,9 @@ function resetPinLink() {
function unlinkDevice(id) {
axios.post(appUrl() + '/device/link/remove', { uuid: id }).then((data) => {
if (data.data) device.serverGetRemotes()
if (data.data) {
device.serverGetRemotes()
}
})
}
</script>

View file

@ -0,0 +1,176 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="input-group form-select">
<label v-if="label">
{{ label }}
</label>
<div class="select__container">
<template v-if="search">
<div class="select__search-bar">
<input
type="search"
ref="selectSearch"
:list="`${name}-search__options`"
v-model="select.search"
@change="selectSearchValue($event)"
:disabled="!select.searchActive"
autocomplete="on"
/>
<datalist :id="`${name}-search__options`">
<option v-for="option in select.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</datalist>
<ButtonComp v-if="!select.searchActive" variant="ghost" size="sm" @click="initSearch">
<IconSearch />
</ButtonComp>
<ButtonComp v-else variant="ghost" size="sm" @click="resetSearch">
<IconSearchOff />
</ButtonComp>
</div>
</template>
<select :name="name" ref="selectEl" v-model="select.value" @change="changeSelect($event)">
<option value="" disabled>- Select {{ label.toLocaleLowerCase() }} -</option>
<option v-for="option in select.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
</template>
<script setup>
import { IconSearch, IconSearchOff } from '@tabler/icons-vue'
import { onMounted, onUpdated, reactive, ref } from 'vue'
import ButtonComp from '../base/ButtonComp.vue'
const emit = defineEmits(['change'])
const props = defineProps({
label: String,
name: String,
options: [Array, Object],
search: Boolean,
value: String,
})
const select = reactive({
options: [],
search: '',
searchActive: false,
changed: false,
value: '',
})
const selectEl = ref(null)
const selectSearch = ref(null)
onMounted(() => {
setValue()
if (typeof props.options == 'object') select.options = Object.values(props.options)
})
onUpdated(() => {
setValue()
})
const setValue = () => {
if ((select.value == '' && props.value) || (!select.changed && props.value != select.value)) {
select.value = props.value
}
select.changed = false
}
const initSearch = () => {
select.searchActive = true
select.search = ''
selectEl.value.classList = 'search__is-active'
setTimeout(() => {
selectSearch.value.focus()
}, 50)
}
const resetSearch = () => {
select.search = ''
select.searchActive = false
selectEl.value.classList = ''
}
const selectSearchValue = (event) => {
changeSelect(event)
resetSearch()
}
const changeSelect = (event) => {
select.changed = true
select.value = event.target.value
emit('change', select.value)
}
</script>
<style scoped>
@reference "@/assets/main.css";
.select__container {
@apply relative
h-8;
select,
.select__search-bar {
@apply absolute top-0 h-8;
}
}
.select__search-bar {
@apply right-0
grid
grid-cols-[1fr_auto]
items-center
w-full
pr-4
z-10
pointer-events-none;
button {
@apply pointer-events-auto;
}
input {
@apply border-0 bg-transparent pointer-events-auto px-2 py-0 focus:outline-0;
&[disabled] {
@apply pointer-events-none;
}
}
datalist {
@apply absolute
top-full left-0;
}
}
select.search__is-active {
@apply text-transparent;
}
</style>

View file

@ -1,40 +1,115 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="macro-overview mcrm-block block__dark">
<h4 class="border-b-2 border-transparent">Saved Macros</h4>
<div class="macro-overview__list">
<LoadComp :loading="macros.loading" text="Loading macros..." />
<div class="macro-item" v-for="(macro, i) in macros.list" :key="i">
<ButtonComp variant="dark" class="w-full" size="sm" @click.prevent="runMacro(macro)">
<IconKeyboard /> {{ macro }}
<ButtonComp
:variant="macroRecorder.macroName === macro.name ? 'secondary' : 'dark'"
class="overview__macro-open"
size="sm"
@click="macroRecorder.openMacro(macro.macroname, macro.name)"
>
<IconKeyboard /> <span>{{ macro.name }}</span>
</ButtonComp>
<div class="overview__macro-delete">
<ButtonComp
class="!text-red-500 hover:!text-red-300"
variant="ghost"
size="sm"
@click="startDelete(macro.name)"
>
<IconTrash />
</ButtonComp>
</div>
</div>
</div>
<DialogComp ref="deleteDialog">
<template #content>
<div class="grid gap-2">
<h4 class="pr-4">Are you sure you want to delete:</h4>
<h3 class="mb-2 text-center text-sky-500">{{ macroToBeDeleted }}</h3>
<div class="flex justify-between">
<ButtonComp size="sm" variant="subtle" @click="deleteDialog.toggleDialog(false)">
No
</ButtonComp>
<ButtonComp size="sm" variant="danger" @click="deleteMacro()">Yes</ButtonComp>
</div>
</div>
</template>
</DialogComp>
</div>
</template>
<script setup>
import { IconKeyboard } from '@tabler/icons-vue'
// TODO
// - delete macro
import { IconKeyboard, IconTrash } from '@tabler/icons-vue'
import ButtonComp from '../base/ButtonComp.vue'
import { onMounted, reactive } from 'vue'
import axios from 'axios'
import { appUrl, isLocal } from '@/services/ApiService'
import { AuthCall } from '@/services/EncryptService'
import { onMounted, reactive, ref } from 'vue'
import { GetMacroList } from '@/services/MacroService'
import LoadComp from '../base/LoadComp.vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import DialogComp from '../base/DialogComp.vue'
const macros = reactive({
loading: true,
list: [],
})
const macroRecorder = useMacroRecorderStore()
const macroToBeDeleted = ref('')
const deleteDialog = ref()
onMounted(() => {
axios.post(appUrl() + '/macro/list').then((data) => {
if (data.data.length > 0) macros.list = data.data
})
loadMacroList()
})
function runMacro(macro) {
const data = isLocal() ? { macro: macro } : AuthCall({ macro: macro })
const loadMacroList = async () => {
const list = await GetMacroList()
macros.list = list
macros.loading = false
}
axios.post(appUrl() + '/macro/play', data).then((data) => {
console.log(data)
})
const startDelete = (macroFilename) => {
macroToBeDeleted.value = macroFilename
deleteDialog.value.toggleDialog(true)
}
const deleteMacro = async () => {
const resp = await macroRecorder.deleteMacro(macroToBeDeleted.value)
if (resp) {
deleteDialog.value.toggleDialog(false)
if (macroToBeDeleted.value === macroRecorder.macroName) macroRecorder.resetMacro()
macroToBeDeleted.value = ''
loadMacroList()
}
}
</script>
@ -57,16 +132,32 @@ function runMacro(macro) {
}
.macro-overview__list {
@apply grid
@apply flex
flex-col
pr-1
-mr-1
gap-1
content-start;
h-[calc(100vh-11.7rem)]
overflow-auto;
}
.macro-item {
@apply flex items-center;
@apply grid items-center grid-cols-[1fr_0fr] transition-[grid-template-columns] delay-0 duration-300;
button {
@apply w-full;
&:hover {
@apply grid-cols-[1fr_auto] delay-500;
}
button.overview__macro-open {
@apply w-full grid grid-cols-[1rem_1fr] justify-items-start;
span {
@apply truncate w-full text-left;
}
}
div.overview__macro-delete {
@apply grid overflow-hidden transition;
}
}
}

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="macro-recorder mcrm-block block__light">
<div class="recorder-interface">

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<span :class="`delay ${active ? 'active' : ''} ${preset ? 'preset' : ''}`">
<template v-if="value < 10000"> {{ value }} <i>ms</i> </template>

View file

@ -1,6 +1,27 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="delete-key-dialog" class="dialog__content">
<h4 class="text-slate-50 mb-4">Delete key</h4>
<h4 class="mb-4 text-slate-50">Delete key</h4>
<div class="flex justify-center w-full mb-4">
<MacroKey v-if="keyObj" :key-obj="keyObj" />
</div>
@ -26,9 +47,6 @@ const keyObj = ref(null)
onMounted(() => {
keyObj.value = filterKey(macroRecorder.getEditKey())
// console.log(macroRecorder.getEditKey());
// console.log(keyObj.value);
// console.log('---------');
})
</script>

View file

@ -1,6 +1,27 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="edit-delay-dialog" class="dialog__content">
<h4 class="text-slate-50 mb-4">Edit delay</h4>
<h4 class="mb-4 text-slate-50">Edit delay</h4>
<div v-if="editable.delay.value" class="flex justify-center">
<DelaySpan class="!text-lg" :value="editable.delay.value" />
</div>
@ -41,7 +62,6 @@ const editable = reactive({
onMounted(() => {
editable.delay = macroRecorder.getEditDelay()
editable.newDelay.value = editable.delay.value
console.log(editable)
})
const changeDelay = () => {

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="edit-key-dialog" class="dialog__content">
<h4 class="text-slate-50 mb-4">Press a key</h4>

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<ContextMenu ref="ctxtMenu">
<template #trigger>

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="insert-key-dialog" class="dialog__content w-96">
<h4 class="text-slate-50 mb-4">Insert key {{ position }}</h4>

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<kbd :class="`${active ? 'active' : ''} ${empty ? 'empty' : ''}`">
<template v-if="keyObj">

View file

@ -1,6 +1,27 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="validation-error__dialog" class="dialog__content">
<h4 class="text-slate-50 mb-4">There's an error in your macro</h4>
<h4 class="mb-4 text-slate-50">There's an error in your macro</h4>
<div class="grid gap-4" v-if="(errors && errors.up.length > 0) || errors.down.length > 0">
<div v-if="errors.down.length > 0">
@ -50,7 +71,6 @@ onMounted(() => {
errors.down =
mutation.events.newValue !== false ? macroRecorder.state.validationErrors.down : []
}
console.log(mutation)
})
})
</script>

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="macro-edit__dialogs" v-if="macroRecorder.state.edit !== false">
<div

View file

@ -1,9 +1,29 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="macro-recorder__footer">
<ButtonComp
v-if="macroRecorder.steps.length > 0"
variant="danger"
size="sm"
@click="macroRecorder.reset()"
>
<IconRestore /> Reset
@ -14,13 +34,26 @@
<ValidationErrorDialog />
</template>
</DialogComp>
<DialogComp ref="overwriteDialog">
<template #content>
<div class="grid gap-2">
<h4 class="pr-4">Are you sure you want to overwrite:</h4>
<h3 class="mb-2 text-center text-sky-500">{{ macroRecorder.macroName }}</h3>
<div class="flex justify-between">
<ButtonComp size="sm" variant="subtle" @click="overwriteDialog.toggleDialog(false)"
>No</ButtonComp
>
<ButtonComp size="sm" variant="primary" @click="saveMacro()">Yes</ButtonComp>
</div>
</div>
</template>
</DialogComp>
<ButtonComp
v-if="macroRecorder.steps.length > 0"
:disabled="macroRecorder.state.record || macroRecorder.state.edit"
variant="success"
size="sm"
@click="toggleSave()"
@click="startCheck()"
>
<IconDeviceFloppy />
Save
@ -40,6 +73,7 @@ import { onMounted, ref } from 'vue'
const macroRecorder = useMacroRecorderStore()
const errorDialog = ref()
const overwriteDialog = ref()
onMounted(() => {
macroRecorder.$subscribe((mutation) => {
@ -49,8 +83,20 @@ onMounted(() => {
})
})
const toggleSave = () => {
if (!macroRecorder.save()) errorDialog.value.toggleDialog(true)
const startCheck = async () => {
const checkResp = await macroRecorder.checkMacro()
if (checkResp) overwriteDialog.value.toggleDialog(true)
else saveMacro()
}
const saveMacro = async () => {
overwriteDialog.value.toggleDialog(false)
const saveResp = await macroRecorder.saveMacro()
if (!saveResp) errorDialog.value.toggleDialog(true)
else window.location.reload()
}
</script>

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="macro-recorder__header">
<div class="w-full grid grid-cols-[auto_1fr_auto] gap-2">
@ -7,6 +28,7 @@
id="macro-name"
type="text"
@input.prevent="changeName($event.target.value)"
:value="macroName"
placeholder="New macro"
/>
<div :class="`recording__buttons ${!nameSet || macroRecorder.state.edit ? 'disabled' : ''}`">
@ -14,7 +36,6 @@
<ButtonComp
v-if="!macroRecorder.state.record"
variant="primary"
size="sm"
@click="macroRecorder.state.record = true"
>
<IconPlayerRecordFilled class="text-red-500" />Record
@ -22,7 +43,6 @@
<ButtonComp
v-if="macroRecorder.state.record"
variant="danger"
size="sm"
@click="macroRecorder.state.record = false"
>
<IconPlayerStopFilled class="text-white" />Stop
@ -37,7 +57,6 @@
<ButtonComp
v-if="!macroRecorder.state.edit"
variant="secondary"
size="sm"
@click="macroRecorder.state.edit = true"
>
<IconPencil />Edit
@ -45,7 +64,6 @@
<ButtonComp
v-if="macroRecorder.state.edit"
variant="danger"
size="sm"
@click="macroRecorder.resetEdit()"
>
<IconPlayerStopFilled />Stop
@ -66,12 +84,18 @@ import FixedDelayMenu from '../components/FixedDelayMenu.vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import EditDialogs from './EditDialogs.vue'
import { computed, onMounted, onUpdated, ref } from 'vue'
import { computed, onUpdated, ref } from 'vue'
const macroRecorder = useMacroRecorderStore()
const macroName = computed(() => macroRecorder.macroName)
const nameSet = ref(false)
onUpdated(() => {
nameSet.value = macroName.value && macroName.value.length > 0
})
function changeName(name) {
macroRecorder.changeName(name)
nameSet.value = name.length > 0

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div :class="`recorder-input__container ${macroRecorder.state.record && 'record'}`">
<input
@ -5,7 +26,6 @@
:class="`macro-recorder__input ${macroRecorder.state.record && 'record'}`"
type="text"
ref="macroInput"
@focus="console.log('focus')"
@keydown.prevent="macroRecorder.recordStep($event, 'down')"
@keyup.prevent="macroRecorder.recordStep($event, 'up')"
/>

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div
:class="`macro-recorder__output ${macroRecorder.state.record && 'record'} ${macroRecorder.state.edit && 'edit'}`"

View file

@ -0,0 +1,253 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="panel-edit" class="mcrm-block block__dark !p-0 !gap-0" v-if="editPanel">
<div class="panel-preview">
<div class="panel-preview__content" ref="panelPreview" v-html="editPanel.html"></div>
</div>
<div class="panel-settings">
<AccordionComp title="Panel info" ref="infoAccordion">
<div class="grid grid-cols-[auto_1fr] gap-2 p-4">
<span>Name:</span><strong class="text-right">{{ editPanel.name }}</strong>
<span>Aspect ratio:</span><strong class="text-right">{{ editPanel.aspectRatio }}</strong>
<template v-if="editPanel.macros">
<span>Linked Macros:</span>
<strong class="text-right">{{ Object.keys(editPanel.macros).length }}</strong>
</template>
</div>
</AccordionComp>
<div>
<AccordionComp
v-if="editButton.id"
title="Button"
ref="buttonAccordion"
:open="editButton.id != ''"
>
<div class="grid gap-4 p-4">
<div class="grid grid-cols-[auto_1fr] gap-2">
<span>Button ID:</span>
<strong class="text-right">{{ editButton.id }}</strong>
</div>
<div class="grid">
<FormSelect
name="button_macro"
label="Button macro"
:search="true"
:options="macroList"
:value="editButton.macro"
@change="checkNewMacro(editButton.id, $event)"
/>
<div class="grid grid-cols-2 mt-4">
<ButtonComp
v-if="editButton.macro != ''"
class="col-start-1 w-fit"
size="sm"
variant="danger"
@click="unlinkMacro(editButton.id)"
ref="unlinkButton"
>
<IconTrash /> Unlink
</ButtonComp>
<ButtonComp
v-if="editButton.changed"
class="col-start-2 w-fit justify-self-end"
size="sm"
variant="primary"
@click="linkMacro(editButton.id)"
ref="linkButton"
>
<IconLink /> Link
</ButtonComp>
</div>
</div>
</div>
</AccordionComp>
</div>
<footer class="flex items-end justify-end h-full p-4">
<ButtonComp v-if="panelMacros.changed" variant="success" @click="savePanelChanges()">
<IconDeviceFloppy /> Save changes
</ButtonComp>
</footer>
</div>
</div>
</template>
<script setup>
import { CheckMacroListChange, GetMacroList } from '@/services/MacroService'
import {
PanelButtonListeners,
PanelDialogListeners,
RemovePanelStyle,
SetPanelStyle,
StripPanelHTML,
} from '@/services/PanelService'
import { usePanelStore } from '@/stores/panel'
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import AccordionComp from '../base/AccordionComp.vue'
import FormSelect from '../form/FormSelect.vue'
import ButtonComp from '../base/ButtonComp.vue'
import { IconDeviceFloppy, IconLink, IconTrash } from '@tabler/icons-vue'
import axios from 'axios'
import { appUrl } from '@/services/ApiService'
const props = defineProps({
dirname: String,
})
const panel = usePanelStore()
const panelPreview = ref(false)
const editPanel = ref({})
const panelMacros = reactive({
old: {},
changed: false,
})
const macroList = ref({})
const infoAccordion = ref(false)
const buttonAccordion = ref(false)
const unlinkButton = ref(null)
const linkButton = ref(null)
const editButton = reactive({
id: '',
macro: '',
newMacro: '',
changed: false,
})
onMounted(async () => {
const currentPanel = await panel.get(props.dirname)
editPanel.value = currentPanel
editPanel.value.dir = props.dirname
editPanel.value.html = StripPanelHTML(editPanel.value.html, editPanel.value.aspectRatio)
panelMacros.old = JSON.stringify(currentPanel.macros)
infoAccordion.value.toggleAccordion(true)
const macros = await GetMacroList()
macroList.value = Object.assign(
{},
...Object.keys(macros).map((key) => ({
[key]: { value: macros[key].macroname, label: macros[key].name },
})),
)
SetPanelStyle(editPanel.value.style)
EditButtonListeners()
})
onUnmounted(() => {
RemovePanelStyle()
})
function EditButtonListeners() {
const callback = (button) => {
infoAccordion.value.toggleAccordion(false)
setEditButton(button.id)
}
PanelButtonListeners(panelPreview.value, callback)
PanelDialogListeners(panelPreview.value)
}
function setEditButton(id) {
editButton.id = id
editButton.macro = editPanel.value.macros[id] ? editPanel.value.macros[id] : ''
}
function checkNewMacro(id, macro) {
editButton.changed = editPanel.value.macros[id] != macro
editButton.newMacro = macro
}
function linkMacro(id) {
editPanel.value.macros[id] = editButton.newMacro
editButton.macro = editButton.newMacro
editButton.newMacro = ''
panelMacros.changed = CheckMacroListChange(panelMacros.old, editPanel.value.macros)
}
function unlinkMacro(id) {
delete editPanel.value.macros[id]
buttonAccordion.value.toggleAccordion(false)
panelMacros.changed = CheckMacroListChange(panelMacros.old, editPanel.value.macros)
}
function savePanelChanges() {
const panelData = {
dir: editPanel.value.dir,
name: editPanel.value.name,
description: editPanel.value.description,
aspectRatio: editPanel.value.aspectRatio,
macros: editPanel.value.macros,
}
axios.post(appUrl() + '/panel/save/json', panelData)
}
</script>
<style>
@reference "@/assets/main.css";
[mcrm__button] {
@apply cursor-pointer;
}
#panel-edit {
@apply grid
grid-cols-[1fr_30ch]
size-full
overflow-hidden;
.panel-preview {
@apply border-r
border-slate-700;
.panel-preview__content {
@apply relative
grid
justify-center
size-full
p-8;
#panel-html__body {
@apply size-full
max-w-full max-h-full;
}
}
}
.panel-settings {
@apply grid
grid-rows-[auto_auto_1fr]
bg-black/30;
}
}
</style>

View file

@ -0,0 +1,103 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="panel-view">
<div class="panel-preview__content" ref="panelView" v-html="viewPanel.html"></div>
</div>
</template>
<script setup>
import { RunMacro } from '@/services/MacroService'
import {
PanelButtonListeners,
PanelDialogListeners,
RemovePanelScripts,
RemovePanelStyle,
SetPanelStyle,
StripPanelHTML,
} from '@/services/PanelService'
import { usePanelStore } from '@/stores/panel'
import { onMounted, onUnmounted, ref } from 'vue'
const panel = usePanelStore()
const props = defineProps({
dirname: String,
})
const panelView = ref(null)
const viewPanel = ref({})
onMounted(async () => {
const currentPanel = await panel.get(props.dirname)
viewPanel.value = currentPanel
viewPanel.value.html = StripPanelHTML(viewPanel.value.html, viewPanel.value.aspectRatio)
SetPanelStyle(viewPanel.value.style)
setTimeout(() => {
viewPanelListeners()
if (typeof window.onPanelLoaded === 'function') {
window.onPanelLoaded()
}
}, 50)
})
onUnmounted(() => {
RemovePanelStyle()
RemovePanelScripts()
})
const viewPanelListeners = () => {
const callback = (button) => {
RunMacro(viewPanel.value.macros[button.id])
}
PanelButtonListeners(panelView.value, callback)
PanelDialogListeners(panelView.value)
}
</script>
<style scoped>
@reference "@/assets/main.css";
#panel-view {
@apply fixed
inset-0
size-full
bg-black;
.panel-preview__content {
@apply relative
grid
justify-center
size-full;
#panel-html__body {
@apply size-full
max-w-full max-h-full;
}
}
}
</style>

View file

@ -0,0 +1,183 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="panels-overview">
<AlertComp v-if="Object.keys(panels.list).length == 0" variant="info">
No panels found
</AlertComp>
<div class="panel-list">
<div class="panel-item mcrm-block block__dark" v-for="(panel, i) in panels.list" :key="i">
<div class="panel-item__content" @click="panelItemClick(panel.dir)">
<div class="thumb">
<img v-if="panel.thumb" :src="`data:image/jpeg;base64,${panel.thumb}`" alt="" />
<IconLayoutGrid v-else />
</div>
<h4>{{ panel.name }}</h4>
<div class="description" v-if="isLocal()">
<div class="content">
<strong class="block mb-1 text-slate-400">{{ panel.name }}</strong>
<hr class="mb-2 border-slate-600" />
<p v-if="panel.description != 'null'" class="text-slate-200">
{{ panel.description }}
</p>
</div>
<footer>
<ButtonComp variant="subtle" size="sm" :href="`/panel/view/${panel.dir}`">
<IconEye /> Preview
</ButtonComp>
<ButtonComp variant="primary" size="sm" :href="`/panel/edit/${panel.dir}`">
<IconPencil /> Edit
</ButtonComp>
</footer>
</div>
</div>
<template v-if="!isLocal()"> </template>
</div>
</div>
</div>
</template>
<script setup>
import { usePanelStore } from '@/stores/panel'
import { onMounted, reactive } from 'vue'
import AlertComp from '../base/AlertComp.vue'
import { IconEye, IconLayoutGrid, IconPencil } from '@tabler/icons-vue'
import ButtonComp from '../base/ButtonComp.vue'
import { isLocal } from '@/services/ApiService'
import { useRouter } from 'vue-router'
const panel = usePanelStore()
const panels = reactive({
list: {},
})
const router = useRouter()
onMounted(async () => {
const panelList = await panel.getList()
panels.list = panelList
})
function panelItemClick(dir) {
if (isLocal()) return
router.push(`/panel/view/${dir}`)
}
</script>
<style scoped>
@reference "@/assets/main.css";
.panel-list {
@apply grid
grid-cols-2
md:grid-cols-4
lg:grid-cols-6
gap-4
w-full h-fit;
}
.panel-item {
@apply p-px
overflow-hidden;
.thumb {
@apply flex
justify-center
items-center
w-full
aspect-[4/3];
img {
@apply size-full
object-cover;
}
&:not(:has(img)) {
@apply bg-sky-950;
}
svg {
@apply size-12;
}
}
h4 {
@apply px-4 py-2
h-12
truncate;
}
&:hover .description {
@apply opacity-100;
}
.description {
@apply absolute
inset-0
size-full
pt-2
pr-1
pb-13
bg-slate-900/60
backdrop-blur-md
text-slate-100
opacity-0
transition-opacity
cursor-default
z-10;
.content {
@apply h-full
p-4
pt-2
overflow-y-auto;
}
footer {
@apply absolute
bottom-0 left-0
w-full
h-12
grid
grid-cols-2
bg-slate-900
border-t
border-slate-600;
.btn {
@apply size-full
rounded-none
justify-center
border-0;
&:last-child {
@apply border-l
border-slate-600;
}
}
}
}
}
</style>

View file

@ -1,15 +1,37 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// import './assets/jemx.scss'
import "@/assets/main.css";
import '@/assets/main.css'
import '@/assets/img/Macrame-Logo-gradient.svg'
import { createApp } from "vue";
import { createPinia } from "pinia";
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from "@/App.vue";
import router from "@/router";
import App from '@/App.vue'
import router from '@/router'
const app = createApp(App);
const app = createApp(App)
app.use(createPinia());
app.use(router);
app.use(createPinia())
app.use(router)
app.mount("#app");
app.mount('#app')

View file

@ -1,34 +1,70 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import DashboardView from '../views/DashboardView.vue'
import { checkAuth, isLocal } from '@/services/ApiService'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
name: 'dashboard',
component: DashboardView,
},
{
path: '/panels',
name: 'panels',
component: () => import('../views/PanelsView.vue'),
meta: { requiresAuth: true },
},
{
path: '/panel/edit/:dirname',
name: 'panel-edit',
component: () => import('../views/PanelsView.vue'),
meta: { requiresAuth: true },
},
{
path: '/panel/view/:dirname',
name: 'panel-view',
component: () => import('../views/PanelsView.vue'),
meta: { requiresAuth: true },
},
{
path: '/macros',
name: 'macros',
component: () => import('../views/MacrosView.vue'),
meta: { localOnly: true },
},
{
path: '/devices',
name: 'devices',
component: () => import('../views/DevicesView.vue'),
},
{
path: '/settings',
name: 'settings',
component: () => import('../views/SettingsView.vue'),
},
// {
// path: '/settings',
// name: 'settings',
// component: () => import('../views/SettingsView.vue'),
// },
// {
// path: '/about',
// name: 'about',
@ -40,4 +76,12 @@ const router = createRouter({
],
})
router.beforeEach(async (to, from, next) => {
const auth = await checkAuth()
if (to.meta.requiresAuth && !auth && !isLocal()) next('/devices')
else if (to.meta.localOnly && !isLocal()) next('/')
else next()
})
export default router

View file

@ -1,10 +1,34 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useDeviceStore } from '@/stores/device'
import CryptoJS from 'crypto-js'
export const appUrl = () => {
return window.location.port !== 6970 ? `http://${window.location.hostname}:6970` : ''
const port = window.location.port == 5173 ? window.__CONFIG__.MCRM__PORT : window.location.port
return `http://${window.location.hostname}:${port}`
}
export const isLocal = () => {
export const isLocal = () => {
return window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost'
}
@ -17,3 +41,15 @@ export const encrypt = (data, key = false) => {
return false
}
}
export const checkAuth = async () => {
const device = useDeviceStore()
const handshake = await device.remoteHandshake()
if (handshake === true) return true
if (device.key()) return true
return false
}

View file

@ -1,10 +1,32 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useDeviceStore } from '@/stores/device'
import { AES, enc, pad } from 'crypto-js'
import { isLocal } from './ApiService'
export const encryptAES = (key, str) => {
key = keyPad(key)
let iv = enc.Utf8.parse(import.meta.env.VITE_MCRM__IV)
let iv = enc.Utf8.parse(window.__CONFIG__.MCRM__IV)
let encrypted = AES.encrypt(str, key, {
iv: iv,
padding: pad.Pkcs7,
@ -15,7 +37,7 @@ export const encryptAES = (key, str) => {
export const decryptAES = (key, str) => {
key = keyPad(key)
let iv = enc.Utf8.parse(import.meta.env.VITE_MCRM__IV)
let iv = enc.Utf8.parse(window.__CONFIG__.MCRM__IV)
let encrypted = AES.decrypt(str.toString(), key, {
iv: iv,
padding: pad.Pkcs7,
@ -23,7 +45,11 @@ export const decryptAES = (key, str) => {
return encrypted.toString(enc.Utf8)
}
export const AuthCall = (data) => {
export const AuthCall = (data = false) => {
if (isLocal()) return data
if (!data) data = {empty: true}
const device = useDeviceStore()
return {
@ -36,7 +62,7 @@ function keyPad(key) {
let returnKey = key
if (key.length == 4) {
returnKey = key + import.meta.env.VITE_MCRM__SALT
returnKey = key + window.__CONFIG__.MCRM__SALT
}
return enc.Utf8.parse(returnKey)

View file

@ -1,3 +1,24 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const keyMap = {
// Modifier keys
Control: 'Ctrl',
@ -125,3 +146,47 @@ export const invalidMacro = (steps) => {
return { down: downKeys, up: upKeys }
}
export const translateJSON = (json) => {
const steps = []
json.forEach((step) => {
if (step.type === 'delay') steps.push(step)
if (step.type === 'key') steps.push(codeToStep(step.code, step.direction))
})
return steps
}
export const codeToStep = (code, direction) => {
let key = ''
let location = 0
let codeStr = code
if (code.includes('Left')) {
key = code.replace('Left', '')
location = 1
}
if (code.includes('Right')) {
key = code.replace('Right', '')
location = 2
}
if (code.includes('Numpad')) {
key = code.replace('Numpad', '')
location = 3
}
if (code.includes('Media')) codeStr = ''
if (key === '') key = code
const stepObj = {
type: 'key',
code: codeStr,
key: key,
location: location,
direction: direction,
}
return { ...stepObj, keyObj: filterKey(stepObj) }
}

View file

@ -0,0 +1,47 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import axios from 'axios'
import { appUrl, isLocal } from './ApiService'
import { AuthCall } from './EncryptService'
export const GetMacroList = async (count = false) => {
const request = await axios.post(appUrl() + '/macro/list')
if (!request.data) return 0
if (!count) return sortMacroList(request.data)
else return request.data.length
}
const sortMacroList = (list) => {
return [...list].sort((a, b) => a.name.localeCompare(b.name))
}
export const RunMacro = async (macro) => {
const data = isLocal() ? { macro: macro } : AuthCall({ macro: macro })
const request = await axios.post(appUrl() + '/macro/play', data)
return request.data
}
export const CheckMacroListChange = (oldList, newList) => {
return oldList !== JSON.stringify(newList)
}

View file

@ -0,0 +1,120 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const SetPanelStyle = (styleStr) => {
const styleEl = document.createElement('style')
styleEl.setAttribute('custom_panel_style', true)
styleEl.innerHTML = styleStr
document.head.appendChild(styleEl)
}
export const RemovePanelStyle = () => {
const styleEl = document.querySelector('style[custom_panel_style]')
if (styleEl) {
styleEl.remove()
}
}
export const StripPanelHTML = (html, aspectRatio) => {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
let scripts = []
if (doc.querySelectorAll('script').length > 0) {
const stripped = StripPanelScripts(doc)
doc.body = stripped.body
scripts = stripped.scripts
}
const body = doc.body
const bodyContents = body.innerHTML
const panelBody = document.createElement('div')
panelBody.id = 'panel-html__body'
panelBody.style = `aspect-ratio: ${aspectRatio}`
panelBody.innerHTML = bodyContents
if (scripts.length > 0) {
SetPanelScripts(scripts)
}
return panelBody.outerHTML
}
export const StripPanelScripts = (doc) => {
const scriptEls = doc.querySelectorAll('script')
const scripts = []
scriptEls.forEach((script) => {
if (script.getAttribute('no-compile') != '') scripts.push(script.innerHTML)
script.remove()
})
return { body: doc.body, scripts }
}
export const SetPanelScripts = (scripts) => {
scripts.forEach((script) => {
const scriptEl = document.createElement('script')
scriptEl.setAttribute('custom_panel_script', true)
scriptEl.innerHTML = script
document.body.appendChild(scriptEl)
})
}
export const RemovePanelScripts = () => {
const scripts = document.querySelectorAll('script[custom_panel_script]')
scripts.forEach((script) => {
script.remove()
})
}
export const PanelButtonListeners = (panelEl, callback) => {
panelEl.querySelectorAll('[mcrm__button]').forEach((button) => {
button.addEventListener('click', () => {
callback(button)
})
})
}
export const PanelDialogListeners = (panelEl) => {
panelEl.querySelectorAll('[mcrm__dialog-trigger]').forEach((dialogTrigger) => {
const dialogEl = document.querySelector(dialogTrigger.getAttribute('dialog-trigger'))
if (dialogEl) {
dialogTrigger.addEventListener('click', () => {
dialogEl.show()
})
}
})
document.querySelectorAll('dialog, dialog .dialog__close').forEach((dialogClose) => {
dialogClose.addEventListener('click', (e) => {
if (
e.target.classList.contains('dialog__close') ||
e.target.closest('.dialog__close') ||
e.target.tagName == 'DIALOG'
) {
dialogClose.closest('dialog').close()
}
})
})
}

View file

@ -1,3 +1,24 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

View file

@ -1,3 +1,24 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ref } from 'vue'
import { defineStore } from 'pinia'
import axios from 'axios'
@ -50,11 +71,21 @@ export const useDeviceStore = defineStore('device', () => {
localStorage.removeItem('deviceKey')
}
const serverGetIP = async () => {
const request = await axios.post(appUrl() + '/device/server/ip')
return `http://${request.data}:${window.__CONFIG__.MCRM__PORT}`
}
// Server application
const serverGetRemotes = async (remoteUuid) => {
axios.post(appUrl() + '/device/list', { uuid: remoteUuid }).then((data) => {
if (data.data.devices) remote.value = data.data.devices
})
const serverGetRemotes = async (count = false) => {
const request = await axios.post(appUrl() + '/device/list')
if (!request.data.devices) return false
remote.value = request.data.devices
if (!count) return remote.value
else return Object.keys(remote.value).length
}
const serverStartLink = async (deviceUuid) => {
@ -78,8 +109,6 @@ export const useDeviceStore = defineStore('device', () => {
return request
}
const remotePingLink = async (cb) => {
// const linkRequest = await axios.post(appUrl() + '/device/link/ping', { uuid: deviceUuid })
// if (linkRequest.data)
const pingInterval = setInterval(() => {
axios.post(appUrl() + '/device/link/ping', { uuid: uuid() }).then((data) => {
if (data.data) {
@ -90,12 +119,17 @@ export const useDeviceStore = defineStore('device', () => {
}, 1000)
}
const remoteHandshake = async (key) => {
const remoteHandshake = async (keyStr = false) => {
if (!keyStr) keyStr = key()
if (!keyStr) return false
const handshake = await axios.post(appUrl() + '/device/handshake', {
uuid: uuid(),
shake: encryptAES(key, getDateStr()),
shake: encryptAES(keyStr, getDateStr()),
})
console.log(handshake)
if (!handshake.data) removeDeviceKey()
return handshake.data
}
@ -108,6 +142,7 @@ export const useDeviceStore = defineStore('device', () => {
key,
setDeviceKey,
removeDeviceKey,
serverGetIP,
serverGetRemotes,
serverStartLink,
remoteCheckServerAccess,

View file

@ -1,7 +1,28 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { filterKey, isRepeat, invalidMacro } from '../services/MacroRecordService'
import { filterKey, isRepeat, invalidMacro, translateJSON } from '../services/MacroRecordService'
import axios from 'axios'
import { appUrl } from '@/services/ApiService'
@ -48,6 +69,8 @@ export const useMacroRecorderStore = defineStore('macrorecorder', () => {
// Setters - Actions
const recordStep = (e, direction = false, key = false) => {
if ((e.ctrlKey, e.shiftKey, e.altKey, e.metaKey)) e.preventDefault()
const lastStep = steps.value[steps.value.length - 1]
let stepVal = {}
@ -130,7 +153,6 @@ export const useMacroRecorderStore = defineStore('macrorecorder', () => {
const changeName = (name) => {
macroName.value = name
console.log(macroName.value)
}
const changeDelay = (fixed) => {
@ -164,29 +186,58 @@ export const useMacroRecorderStore = defineStore('macrorecorder', () => {
state.value.editDelay = false
}
const reset = () => {
const resetMacro = () => {
state.value.record = false
delay.value.start = 0
macroName.value = ''
steps.value = []
if (state.value.edit) resetEdit()
}
const save = () => {
const checkMacro = async () => {
const resp = await axios.post(appUrl() + '/macro/check', {
macro: macroName.value,
})
return resp.data
}
const saveMacro = async () => {
state.value.validationErrors = invalidMacro(steps.value)
if (state.value.validationErrors) return false
axios
.post(appUrl() + '/macro/record', { name: macroName.value, steps: steps.value })
.then((data) => {
console.log(data)
})
return true
const resp = await axios.post(appUrl() + '/macro/record', {
name: macroName.value,
steps: steps.value,
})
return resp.status == 200
}
const deleteMacro = async (macroFilename) => {
const resp = await axios.post(appUrl() + '/macro/delete', {
macro: macroFilename,
})
if (resp.status == 200) return resp.data
else return false
}
const openMacro = async (macroFileName, name) => {
const openResp = await axios.post(appUrl() + '/macro/open', {
macro: macroFileName,
})
if (openResp.data) steps.value = translateJSON(openResp.data)
macroName.value = name
}
return {
state,
macroName,
steps,
delay,
getEditKey,
@ -200,7 +251,10 @@ export const useMacroRecorderStore = defineStore('macrorecorder', () => {
changeDelay,
toggleEdit,
resetEdit,
reset,
save,
resetMacro,
checkMacro,
saveMacro,
deleteMacro,
openMacro,
}
})

82
fe/src/stores/panel.js Normal file
View file

@ -0,0 +1,82 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { appUrl } from '@/services/ApiService'
import { AuthCall } from '@/services/EncryptService'
import axios from 'axios'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const usePanelStore = defineStore('panel', () => {
const current = ref({
dir: false,
name: false,
description: false,
aspectRatio: false,
macros: false,
thumb: false,
html: false,
style: false,
})
const list = ref([])
const get = async (dir) => {
const data = AuthCall({ dir: dir })
const resp = await axios.post(appUrl() + '/panel/get', data)
if (!resp.data && !current.value.html) return false
current.value.name = resp.data.name
current.value.description = resp.data.description
current.value.aspectRatio = resp.data.aspectRatio
current.value.macros = resp.data.macros
current.value.thumb = resp.data.thumb
current.value.html = resp.data.html
current.value.style = resp.data.style
return current.value
}
const getList = async (count = false) => {
if (list.value.length > 0 && !count) return list.value
else if (list.value.length > 0 && count) return list.value.length
const data = AuthCall()
const resp = await axios.post(appUrl() + '/panel/list', data)
list.value = resp.data
if (!resp.data && !count) return false
else if (!resp.data && count) return 0
if (!count) return list.value
else return list.value.length
}
return {
current,
list,
get,
getList,
}
})

View file

@ -0,0 +1,105 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="dashboard" class="panel">
<div class="panel__title">
<h1>Dashboard</h1>
<div>
<em v-if="isLocal()">This is the server dashboard.</em>
<em v-else>This is the remote dashboard.</em>
</div>
</div>
<div class="panel__content !h-fit !gap-y-16">
<ServerView v-if="isLocal()" />
<RemoteView v-else />
<div class="grid gap-2 text-slate-300">
<h3>About Macrame</h3>
<p>
Macrame is an open-source application designed to turn any device into a customizable
button panel. Whether you're optimizing your workflow or enhancing your gaming experience,
Macrame makes it simple to create and link macros to your button panels.
</p>
<p>
For more information, including details on licensing, visit
<a href="https://macrame.github.io" target="_blank">https://macrame.github.io</a>
</p>
</div>
</div>
</div>
</template>
<script setup>
import { isLocal } from '@/services/ApiService'
import ServerView from '@/components/dashboard/ServerView.vue'
import RemoteView from '@/components/dashboard/RemoteView.vue'
</script>
<style>
@reference "@/assets/main.css";
.dashboard-block {
@apply md:!row-start-1
grid
justify-items-center
gap-4;
&#devices .icon__container,
&#server .icon__container {
@apply bg-sky-300/30
text-sky-400
border-sky-300/60;
}
&#macros .icon__container {
@apply bg-amber-300/30
text-amber-400
border-amber-300/60;
}
&#panels .icon__container {
@apply bg-rose-300/30
text-rose-400
border-rose-300/60;
}
.icon__container {
@apply flex
justify-center
items-center
size-16
aspect-square
rounded-full
border;
svg {
@apply size-8;
}
}
p {
@apply opacity-50
w-42
text-center;
}
}
</style>

View file

@ -1,9 +1,30 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="devices-view" class="panel">
<h1 class="panel__title">
Devices <span class="text-sm">{{ isLocal() ? 'remote' : 'servers' }}</span>
{{ isLocal() ? 'Remote devices' : 'Server' }}
</h1>
<div class="panel__content grid gap-8">
<div class="grid gap-8 pr-2 panel__content">
<ServerView v-if="isLocal()" />
<RemoteView v-else />
</div>

View file

@ -1,7 +1,28 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="macros" class="panel">
<h1 class="panel__title">Macros</h1>
<div class="panel__content !p-0">
<div class="panel__content !p-0 !overflow-hidden">
<div class="macro-panel__content">
<MacroOverview />
<MacroRecorder />
@ -13,19 +34,6 @@
<script setup>
import MacroOverview from '@/components/macros/MacroOverview.vue'
import MacroRecorder from '../components/macros/MacroRecorder.vue'
import { onMounted, ref } from 'vue'
const recordMacro = ref(false)
const macroInput = ref(null)
onMounted(() => {
// macroInput.value.focus()
})
const keyDown = (e) => {
console.log(e)
}
</script>
<style scoped>

View file

@ -1,7 +1,82 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div></div>
<div id="panels" class="panel">
<h1 class="flex items-end justify-between !w-full panel__title">
<div>Panels</div>
<ButtonComp
v-if="panel.function != 'overview'"
variant="subtle"
size="sm"
@click="router.push('/panels')"
>
<IconArrowLeft /> Overview
</ButtonComp>
</h1>
<div :class="`panel__content !p-0 !pt-4 ${panel.function == 'overview' ?? '!pr-4'}`">
<PanelsOverview v-if="panel.function == 'overview'" />
<PanelEdit v-if="panel.function == 'edit'" :dirname="panel.dirname" />
<PanelView v-if="panel.function == 'preview'" :dirname="panel.dirname" />
</div>
</div>
</template>
<script setup></script>
<script setup>
import ButtonComp from '@/components/base/ButtonComp.vue'
import PanelEdit from '@/components/panels/PanelEdit.vue'
import PanelView from '@/components/panels/PanelView.vue'
import PanelsOverview from '@/components/panels/PanelsOverview.vue'
import { isLocal } from '@/services/ApiService'
import { IconArrowLeft } from '@tabler/icons-vue'
import { onMounted, onUpdated, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
<style lang="scss" scoped></style>
const route = useRoute()
const router = useRouter()
const panel = reactive({
function: '',
dirname: '',
})
onMounted(() => {
setVarsByRoute()
})
onUpdated(() => {
setVarsByRoute()
})
const setVarsByRoute = () => {
if (route.name.includes('panel-')) {
panel.function = route.name == 'panel-edit' ? 'edit' : 'preview'
} else {
panel.function = 'overview'
}
panel.dirname = route.params.dirname
}
</script>
<style scoped>
@reference "@/assets/main.css";
</style>

View file

@ -1,3 +1,24 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div></div>
</template>

View file

@ -16,16 +16,16 @@ export default defineConfig({
},
plugins: [vue(), vueDevTools(), tailwindcss()],
envDir: '../',
assets: ['assets'],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
base: '/',
// publicDir: "../public",
build: {
outDir: '../public',
sourcemap: false,
minify: false,
sourcemap: true,
minify: true,
},
})

46
go.mod Normal file
View file

@ -0,0 +1,46 @@
module macrame
go 1.24.1
require (
github.com/getlantern/systray v1.2.2
github.com/go-vgo/robotgo v0.110.7
)
require (
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/gen2brain/shm v0.1.1 // indirect
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/jezek/xgb v1.1.1 // indirect
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/otiai10/gosseract v2.2.1+incompatible // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/robotn/xgb v0.10.0 // indirect
github.com/robotn/xgbutil v0.10.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/vcaesar/gops v0.40.0 // indirect
github.com/vcaesar/imgo v0.40.2 // indirect
github.com/vcaesar/keycode v0.10.1 // indirect
github.com/vcaesar/tt v0.20.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
)

102
go.sum Normal file
View file

@ -0,0 +1,102 @@
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e h1:L+XrFvD0vBIBm+Wf9sFN6aU395t7JROoai0qXZraA4U=
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gen2brain/shm v0.1.1 h1:1cTVA5qcsUFixnDHl14TmRoxgfWEEZlTezpUj1vm5uQ=
github.com/gen2brain/shm v0.1.1/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-vgo/robotgo v0.110.7 h1:4scqQrJOBHoFCfcMROYEVFBxHvB3nF/UN6DWoRIFzBE=
github.com/go-vgo/robotgo v0.110.7/go.mod h1:eBUjTHY1HYjzdi1+UWJUbxB+b9gE+l4Ei7vQU/9SnLw=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c h1:1IlzDla/ZATV/FsRn1ETf7ir91PHS2mrd4VMunEtd9k=
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I=
github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
github.com/robotn/xgb v0.10.0 h1:O3kFbIwtwZ3pgLbp1h5slCQ4OpY8BdwugJLrUe6GPIM=
github.com/robotn/xgb v0.10.0/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
github.com/robotn/xgbutil v0.10.0 h1:gvf7mGQqCWQ68aHRtCxgdewRk+/KAJui6l3MJQQRCKw=
github.com/robotn/xgbutil v0.10.0/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35 h1:wAZbkTZkqDzWsqxPh2qkBd3KvFU7tcxV0BP0Rnhkxog=
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35/go.mod h1:aMd4yDHLjbOuYP6fMxj1d9ACDQlSWwYztcpybGHCQc8=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/vcaesar/gops v0.40.0 h1:I+1RCGiV+LkZJUYNzAd373xs0uM2UyeFdZBmow8HfCM=
github.com/vcaesar/gops v0.40.0/go.mod h1:3u/USW7JovqUK6i13VOD3qWfvXXd2TIIKE4PYIv4TOM=
github.com/vcaesar/imgo v0.40.2 h1:5GWScRLdBCMtO1v2I1bs+ZmDLZFINxYSMZ+mtUw5qPM=
github.com/vcaesar/imgo v0.40.2/go.mod h1:MVCl+FxHI2gTgmiHoi0n5xNCbYcfv9SVtdEOUC92+eo=
github.com/vcaesar/keycode v0.10.1 h1:0DesGmMAPWpYTCYddOFiCMKCDKgNnwiQa2QXindVUHw=
github.com/vcaesar/keycode v0.10.1/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ=
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

73
main.go Normal file
View file

@ -0,0 +1,73 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"log"
"net/http"
"os"
"macrame/app"
"macrame/app/helper"
)
func main() {
app.MCRMLogInit()
switchDir()
if helper.EnvGet("MCRM__PORT") == "" {
app.MCRMLog("Error: MCRM__PORT is not set")
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
apiInit(w, r)
})
helper.OpenBrowser("http://localhost:" + helper.EnvGet("MCRM__PORT"))
app.MCRMLog("Listening on http://localhost:" + helper.EnvGet("MCRM__PORT"))
app.InitSystray()
app.MCRMLog(http.ListenAndServe(":"+helper.EnvGet("MCRM__PORT"), nil))
}
func switchDir() {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
log.Println(cwd)
}
func apiInit(w http.ResponseWriter, r *http.Request) {
app.ApiCORS(w, r)
if r.Method == "GET" {
app.ApiGet(w, r)
} else if r.Method == "POST" {
app.ApiPost(w, r)
}
}

View file

@ -0,0 +1,913 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0" /> -->
<title>Document</title>
<link rel="stylesheet" href="./output.css" />
</head>
<body id="panel-html__body" class="relative m-0">
<div
class="absolute inset-0 bg-slate-950 size-full grid grid-cols-[1fr_2fr_1fr] gap-2 p-2"
>
<div class="group-left">
<div id="menu-spacer" class="size-16"></div>
<div id="maps" class="ed-panel pnl__blue">
<h3>Maps</h3>
<div class="grid-cols-2 ed-button-group__horizontal">
<div
class="ed-button btn__yellow btn__vertical"
id="System__map"
mcrm__button
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g style="fill: currentColor">
<circle cx="21.6" cy="17.1" r="0.9" />
<circle cx="21.6" cy="14.6" r="0.9" />
<circle cx="21.6" cy="12" r="0.9" />
<circle cx="15.4" cy="12" r="0.9" />
<circle cx="15.4" cy="7.9" r="1.9" />
<circle cx="21.6" cy="7.9" r="1.9" />
<circle cx="5.9" cy="7.6" r="5.4" />
<circle cx="5.9" cy="19.2" r="2.6" />
</g>
</svg>
System
</div>
<div
class="ed-button btn__orange btn__vertical"
id="Galaxy__map"
mcrm__button
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
style="fill: currentColor"
d="M22.1,8.9c-0.3,0-0.6,0.1-0.9,0.3L17,4.9c0.2-0.4,0.3-0.8,0.3-1.3c0-1.6-1.3-2.8-2.8-2.8S11.7,2,11.7,3.6
c0,0.8,0.4,1.6,0.9,2.1l-5.5,7.9c-0.5-0.2-1.1-0.3-1.7-0.3c-2.7,0-5,2.2-5,5s2.2,5,5,5s5-2.2,5-5c0-1.6-0.7-3-1.9-3.9l4.8-8.2
c0.3,0.2,0.7,0.2,1.1,0.2c0.6,0,1.1-0.2,1.5-0.5l4.7,3.8c-0.1,0.2-0.1,0.4-0.1,0.6c0,0.8,0.6,1.4,1.4,1.4s1.4-0.6,1.4-1.4
C23.5,9.6,22.9,8.9,22.1,8.9z"
/>
</svg>
Galaxy
</div>
</div>
</div>
<div id="fsd" class="ed-panel pnl__yellow">
<h3>Frame Shift Drive</h3>
<div
id="FSD__toggle"
class="ed-button btn__blue !rounded-b-none"
mcrm__button
>
Toggle FSD
</div>
<div class="grid-cols-2 ed-button-group__horizontal">
<div
id="Super__Cruise"
class="!rounded-tl-none ed-button btn__yellow"
mcrm__button
>
Super Cruise
</div>
<div
id="Hyper__Space"
class="!rounded-tr-none ed-button btn__orange"
mcrm__button
>
Hyper Space
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div
id="mode-switch"
class="grid gap-2 justify-items-center ed-panel pnl__white"
>
<h4>Mode</h4>
<span>Analysis</span>
<div
id="Mode__toggle"
class="ed-toggle toggle__vertical"
inactive-wrapper="border-sky-600 bg-sky-400/30"
inactive-indicator="bg-sky-400"
active-wrapper="border-red-600 bg-red-400/30"
active-indicator="bg-red-400"
mcrm__button
toggle__button
>
<div class="toggle__wrapper">
<div class="toggle__indicator"></div>
</div>
</div>
<span>Combat</span>
</div>
<div
id="mode-switch"
class="grid gap-2 justify-items-center ed-panel pnl__red"
>
<h4>Hardpoints</h4>
<span>Retract</span>
<div
id="Hardpoints__toggle"
class="ed-toggle toggle__vertical"
inactive-wrapper="border-sky-600 bg-sky-400/30"
inactive-indicator="bg-sky-400"
active-wrapper="border-red-600 bg-red-400/30"
active-indicator="bg-red-400"
mcrm__button
toggle__button
>
<div class="toggle__wrapper">
<div class="toggle__indicator"></div>
</div>
</div>
<span>Deploy</span>
</div>
</div>
</div>
<div class="group-middle">
<div id="ship-panels" class="ed-panel pnl__blue">
<h3>Panels</h3>
<div class="grid-cols-4 ed-button-group__horizontal">
<div
class="ed-button btn__orange"
id="External__panel"
mcrm__button
>
External
</div>
<div class="ed-button btn__yellow" id="Comms__panel" mcrm__button>
Comms
</div>
<div class="ed-button btn__yellow" id="Roles__panel" mcrm__button>
Roles
</div>
<div
class="ed-button btn__orange"
id="Internal__panel"
mcrm__button
>
Internal
</div>
</div>
</div>
<div class="grid grid-cols-[3fr_1fr_1fr] gap-2">
<div class="ed-panel pnl__blue">
<h3>Scanner</h3>
<div class="grid-cols-2 ed-button-group__horizontal">
<div
id="Scanner__FSS"
class="ed-button btn__blue btn__filled"
mcrm__button
>
FSS
</div>
<div
id="Scanner__DiscScan"
class="ed-button btn__blue"
mcrm__button
>
Discovery
</div>
</div>
</div>
<div
id="Route__NextSystem"
class="ed-button btn__yellow !px-2"
mcrm__button
>
<div>
<span class="text-base opacity-80">Route:</span>
<strong>Next System</strong>
</div>
</div>
<div
id="Speed_0percent"
class="ed-button btn__white !px-2"
mcrm__button
>
<div>
<span class="text-base opacity-80">Speed:</span>
<strong>0%</strong>
</div>
</div>
</div>
<div class="ed-panel pnl__red">
<h3>Fighters</h3>
<div class="grid grid-cols-[2fr_1fr] gap-2">
<div class="grid gap-2">
<div class="grid-cols-2 ed-button-group__horizontal">
<div
class="!justify-start ed-button btn__red btn__filled btn__vertical"
id="Fighter_attack"
mcrm__button
>
<svg
xmlns="http://www.w3.org/2000/svg"
style="fill: currentColor"
viewBox="0 0 24 24"
>
<path
d="M12,1.5C6.5,1.5,2,8,2,11.5s1.5,4.1,1.6,5.6c0.1,1,0.9,1.7,1.8,1.6c0.1,0-0.1-0.1,0.1-0.1v-2h1.2v1.9v2.1
c0,1,0.8,1.8,1.8,1.8H12h3.5c1,0,1.8-0.8,1.8-1.8v-2.1v-1.9h1.2v2c0.2,0-0.1,0.1,0.1,0.1c0.9,0.1,1.7-0.6,1.8-1.6
c0.2-1.5,1.6-2.1,1.6-5.6S17.5,1.5,12,1.5z M10.4,11.5c-0.6,1.4-2.3,2-3.7,1.4s-2-2.3-1.4-3.7C6,7.9,7.7,9.3,9.1,10
S11,10.2,10.4,11.5z M13,15.6c-0.3,0-0.7-0.3-0.8-0.6c-0.1-0.3-0.3-0.3-0.4,0c-0.1,0.3-0.5,0.6-0.8,0.6s-0.3-0.9,0.1-1.9l0.3-0.8
c0.4-1,0.9-1,1.3,0l0.3,0.8C13.3,14.7,13.3,15.6,13,15.6z M17.3,12.9c-1.4,0.6-3,0-3.7-1.4c-0.6-1.4-0.1-1,1.3-1.6s3.1-2.1,3.7-0.7
C19.3,10.7,18.7,12.3,17.3,12.9z"
/>
</svg>
Attack
</div>
<div
class="!justify-start text-red-400 ed-button btn__red btn__vertical"
id="Fighter__engage"
mcrm__button
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
style="fill: currentColor"
>
<path
d="M14,4.3c2.8,0.7,5,2.9,5.7,5.7h2.1C21,6.1,17.9,3,14,2.2V4.3z"
/>
<path
d="M4.3,10C5,7.2,7.2,5,10,4.3V2.2C6.1,3,3,6.1,2.2,10H4.3z"
/>
<path
d="M10,19.7C7.2,19,5,16.8,4.3,14H2.2c0.8,3.9,3.9,7,7.8,7.8V19.7z"
/>
<path
d="M19.7,14c-0.7,2.8-2.9,5-5.7,5.7v2.1c3.9-0.8,7-3.9,7.8-7.8H19.7z"
/>
<polygon points="13,1 11,1 11,6.5 13,6.5 13,1 " />
<polygon points="6.5,11 1,11 1,13 6.5,13 6.5,11 " />
<polygon points="23,11 17.5,11 17.5,13 23,13 23,11 " />
<polygon points="13,17.5 11,17.5 11,23 13,23 13,17.5 " />
</svg>
Engage
</div>
</div>
<div class="grid-cols-2 ed-button-group__horizontal">
<div
class="!justify-start ed-button btn__orange btn__filled btn__vertical"
id="Fighter_defend"
mcrm__button
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
style="fill: currentColor"
>
<path
style="opacity: 0.8"
d="M12,2.4l0.3,0.1l7.2,2.6l0.6,0.2l0,0.7c0,0.5,0.6,12.6-7.9,15.4L12,21.6l-0.3-0.1C3.2,18.6,3.8,6.6,3.8,6.1l0-0.7l0.6-0.2
l7.2-2.6L12,2.4 M12,0.3l-1,0.4L3.8,3.3L1.9,4l-0.1,2c0,0.6-0.7,14.1,9.2,17.4l0.9,0.3l0.9-0.3c9.9-3.3,9.3-16.8,9.2-17.4l-0.1-2
l-1.9-0.7L13,0.7L12,0.3L12,0.3z"
/>
<path
d="M19.2,6.1L12,3.5L4.8,6.1c0,0-0.7,11.8,7.2,14.4C19.8,17.9,19.2,6.1,19.2,6.1z"
/>
</svg>
Defend
</div>
<div
class="!justify-start text-red-400 ed-button btn__orange btn__vertical"
id="Fighter__hold"
mcrm__button
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
style="fill: currentColor"
>
<polygon
style="opacity: 0.8"
points="13,1 11,1 11,6.5 13,6.5 13,1 "
/>
<polygon
style="opacity: 0.8"
points="6.5,11 1,11 1,13 6.5,13 6.5,11 "
/>
<polygon
style="opacity: 0.8"
points="23,11 17.5,11 17.5,13 23,13 23,11 "
/>
<polygon
style="opacity: 0.8"
points="13,17.5 11,17.5 11,23 13,23 13,17.5 "
/>
<path
style="opacity: 0.8"
d="M6.1,20c1.1,0.8,2.5,1.5,3.9,1.8v-2.1c-0.9-0.2-1.7-0.6-2.5-1.1L6.1,20z"
/>
<path
style="opacity: 0.8"
d="M4.3,14H2.2c0.3,1.4,0.9,2.8,1.8,3.9l1.5-1.5C4.9,15.7,4.5,14.9,4.3,14z"
/>
<path
style="opacity: 0.8"
d="M7.5,5.4C8.3,4.9,9.1,4.5,10,4.3V2.2C8.6,2.5,7.2,3.1,6.1,4L7.5,5.4z"
/>
<path
style="opacity: 0.8"
d="M5.4,7.5L4,6.1C3.1,7.2,2.5,8.6,2.2,10h2.1C4.5,9.1,4.9,8.3,5.4,7.5z"
/>
<path
style="opacity: 0.8"
d="M18.6,7.5c0.5,0.7,0.9,1.6,1.1,2.5h2.1c-0.3-1.4-0.9-2.8-1.8-3.9L18.6,7.5z"
/>
<path
style="opacity: 0.8"
d="M17.9,4c-1.1-0.8-2.5-1.5-3.9-1.8v2.1c0.9,0.2,1.7,0.6,2.5,1.1L17.9,4z"
/>
<path
style="opacity: 0.8"
d="M18.6,16.5l1.5,1.5c0.8-1.1,1.5-2.5,1.8-3.9h-2.1C19.5,14.9,19.1,15.7,18.6,16.5z"
/>
<path
style="opacity: 0.8"
d="M16.5,18.6c-0.7,0.5-1.6,0.9-2.5,1.1v2.1c1.4-0.3,2.8-0.9,3.9-1.8L16.5,18.6z"
/>
<polygon
points="21.2,19.8 13.4,12 21.2,4.2 19.8,2.8 12,10.6 4.2,2.8 2.8,4.2 10.6,12 2.8,19.8 4.2,21.2 12,13.4 19.8,21.2 "
/>
</svg>
Hold
</div>
</div>
<div class="grid-cols-2 ed-button-group__horizontal">
<div
class="!justify-start ed-button btn__blue btn__filled btn__vertical"
id="Fighter_follow"
mcrm__button
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
style="fill: currentColor"
>
<polygon
points="11,13 2.5,13 4.5,15 0.3,19.2 4.8,23.7 9,19.5 11,21.5 "
/>
<polygon
points="11,11 11,2.5 9,4.5 4.8,0.3 0.3,4.8 4.5,9 2.5,11 "
/>
<polygon
points="13,11 21.5,11 19.5,9 23.7,4.8 19.2,0.3 15,4.5 13,2.5 "
/>
<polygon
points="13,13 13,21.5 15,19.5 19.2,23.7 23.7,19.2 19.5,15 21.5,13 "
/>
</svg>
Follow
</div>
<div
class="!justify-start text-red-400 ed-button btn__blue btn__vertical"
id="Fighter__recall"
mcrm__button
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
style="fill: currentColor"
>
<path
d="M12,1C5.9,1,1,5.9,1,12s4.9,11,11,11s11-4.9,11-11S18.1,1,12,1z M12,20c-4.4,0-8-3.6-8-8c0-3,1.6-5.5,4-6.9V10H5l7,7l7-7
h-3V5.1c2.4,1.4,4,4,4,6.9C20,16.4,16.4,20,12,20z"
/>
</svg>
Recall
</div>
</div>
</div>
<div>
<div class="h-full grid-rows-3 ed-button-group__vertical">
<div
id="Fighter__wingman1"
class="ed-button btn__yellow btn__vertical"
mcrm__button
>
<span class="text-base opacity-80">Wingman</span>
<strong>#1</strong>
</div>
<div
id="Fighter__wingman2"
class="ed-button btn__yellow btn__vertical"
mcrm__button
>
<span class="text-base opacity-80">Wingman</span>
<strong>#2</strong>
</div>
<div
id="Fighter__wingman3"
class="ed-button btn__yellow btn__vertical"
mcrm__button
>
<span class="text-base opacity-80">Wingman</span>
<strong>#3</strong>
</div>
</div>
</div>
</div>
</div>
<div class="ed-panel pnl__red">
<h3>Counter Measures</h3>
<div class="grid-cols-4 ed-button-group__horizontal">
<div id="CM__Heatsink" class="ed-button btn__red" mcrm__button>
Heatsink
</div>
<div
id="CM__Chaff"
class="ed-button btn__red btn__filled"
mcrm__button
>
Chaff
</div>
<div
id="CM__ECM"
class="ed-button btn__red btn__filled"
mcrm__button
>
ECM
</div>
<div id="CM__Shieldcell" class="ed-button btn__red" mcrm__button>
Shieldcell
</div>
</div>
</div>
</div>
<div class="group-right">
<div class="ed-panel pnl__white">
<h3>Flight Assist</h3>
<div class="grid grid-cols-2 gap-2">
<div class="grid gap-2 justify-items-center">
<div
id="FlightAssist__toggle"
class="ed-toggle toggle__horizontal"
inactive-wrapper="border-red-600 bg-red-400/30"
inactive-indicator="bg-red-400"
active-wrapper="border-lime-600 bg-lime-400/30"
active-indicator="bg-lime-400"
mcrm__button
toggle__button
active="true"
>
<div class="toggle__wrapper">
<div class="toggle__indicator"></div>
</div>
</div>
<div class="flex justify-between w-full">
<span>Off</span>
<span>On</span>
</div>
</div>
<div
id="Rotational__Correction"
class="!px-2 ed-button btn__blue w-fit"
mcrm__button
>
<span class="text-xs">Rotational Correction</span>
</div>
</div>
</div>
<div
class="grid grid-cols-2 divide-x ed-panel pnl__white divider-white !p-0"
>
<div id="light-switch" class="grid gap-2 p-2 justify-items-center">
<h4>Lights</h4>
<div
id="Lights__toggle"
class="ed-toggle toggle__horizontal"
inactive-wrapper="border-white bg-white/30"
inactive-indicator="bg-white"
active-wrapper="border-sky-600 bg-sky-400/30"
active-indicator="bg-sky-400"
mcrm__button
toggle__button
>
<div class="toggle__wrapper">
<div class="toggle__indicator"></div>
</div>
</div>
<div class="flex justify-between w-full">
<span>Off</span>
<span>On</span>
</div>
</div>
<div id="light-switch" class="grid gap-2 p-2 justify-items-center">
<h4>Night Vis.</h4>
<div
id="NightVis__toggle"
class="ed-toggle toggle__horizontal"
inactive-wrapper="border-white bg-white/30"
inactive-indicator="bg-white"
active-wrapper="border-lime-600 bg-lime-400/30"
active-indicator="bg-lime-400"
mcrm__button
toggle__button
>
<div class="toggle__wrapper">
<div class="toggle__indicator"></div>
</div>
</div>
<div class="flex justify-between w-full">
<span>Off</span>
<span>On</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="grid gap-2 text-center ed-panel pnl__orange">
<h4>Silent Running</h4>
<div
id="SilentRunning__toggle"
class="ed-toggle toggle__horizontal"
inactive-wrapper="border-white bg-white/30"
inactive-indicator="bg-white"
active-wrapper="border-red-600 bg-red-400/30"
active-indicator="bg-red-400"
mcrm__button
toggle__button
>
<div class="toggle__wrapper">
<div class="toggle__indicator"></div>
</div>
</div>
<div class="flex justify-between w-full">
<span>Off</span>
<span>On</span>
</div>
</div>
<div class="flex items-center justify-between">
<div
id="Jettison__Cargo"
class="flex flex-col items-center justify-center gap-2 text-center text-white border-red-500 rounded-full border-3 bg-red-500/80 aspect-square"
mcrm__button
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 1.67c.955 0 1.845 .467 2.39 1.247l.105 .16l8.114 13.548a2.914 2.914 0 0 1 -2.307 4.363l-.195 .008h-16.225a2.914 2.914 0 0 1 -2.582 -4.2l.099 -.185l8.11 -13.538a2.914 2.914 0 0 1 2.491 -1.403zm.01 13.33l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -7a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z"
/>
</svg>
<span> JETTISON CARGO </span>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div
class="ed-button btn__blue btn__vertical"
dialog-trigger="#camera-dialog"
mcrm__dialog-trigger
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
style="fill: currentColor"
>
<path
d="M22.6,9.1c-0.1-0.5-0.3-1-0.5-1.6c-1.5,0-5.3,0-6.8,0c2.3,2.3,5.1,5.1,7.3,7.3C23.1,13.1,23.1,10.9,22.6,9.1z"
/>
<path
d="M6.6,12.8c0-3.2,0-7.2,0-10.4C4.4,3.7,2.6,5.7,1.7,8C2.3,8.6,6.6,12.8,6.6,12.8z"
/>
<path
d="M12.8,17.4c-3.2,0-7.2,0-10.4,0c1.2,2.2,3.2,3.9,5.6,4.8C8.6,21.7,12.8,17.4,12.8,17.4z"
/>
<path
d="M17.4,11.2c0,3.2,0,7.2,0,10.4c2.2-1.2,3.9-3.2,4.8-5.6C21.7,15.4,17.4,11.2,17.4,11.2z"
/>
<path
d="M11.2,6.6c3.2,0,7.2,0,10.4,0c-1.2-2.2-3.2-3.9-5.6-4.8C15.4,2.3,11.2,6.6,11.2,6.6z"
/>
<path
d="M7.6,16.4h1.2c-2.3-2.3-5.1-5.1-7.3-7.3c-0.7,2.4-0.5,5.1,0.5,7.3C2.7,16.4,6.9,16.4,7.6,16.4z"
/>
<path
d="M14.3,17.4c-1.3,1.3-3.8,3.8-5.2,5.2c2.4,0.7,5.1,0.5,7.3-0.5c0-1.5,0-5.3,0-6.8C15.9,15.8,14.8,16.9,14.3,17.4z"
/>
<path
d="M14.9,1.4c-2.4-0.7-5.1-0.5-7.3,0.5c0,1.5,0,5.3,0,6.8C8.1,8.2,13.5,2.8,14.9,1.4z"
/>
</svg>
<span> CAMERA </span>
</div>
<dialog id="camera-dialog" mcrm__dialog>
<div
class="dialog__content ed-panel pnl__blue !w-fit !bg-sky-900 relative !p-4"
>
<div class="dialog__close">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-8"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M18 6l-12 12" />
<path d="M6 6l12 12" />
</svg>
</div>
<h4>Camera</h4>
<div class="grid grid-cols-[1fr_2fr] gap-2 mt-4">
<div
id="Camera__Suite"
class="ed-button btn__orange btn__filled btn__vertical !text-gray-800"
mcrm__button
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-gradienter"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M3.227 14c.917 4 4.497 7 8.773 7c4.277 0 7.858 -3 8.773 -7"
/>
<path
d="M20.78 10a9 9 0 0 0 -8.78 -7a8.985 8.985 0 0 0 -8.782 7"
/>
<path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
</svg>
<span>Camera Suite</span>
</div>
<div class="grid-cols-2 ed-button-group__horizontal">
<div
id="Previous__Camera"
class="ed-button btn__yellow !text-gray-800 btn__filled !pt-8"
mcrm__button
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M20.341 4.247l-8 7a1 1 0 0 0 0 1.506l8 7c.647 .565 1.659 .106 1.659 -.753v-14c0 -.86 -1.012 -1.318 -1.659 -.753z"
/>
<path
d="M9.341 4.247l-8 7a1 1 0 0 0 0 1.506l8 7c.647 .565 1.659 .106 1.659 -.753v-14c0 -.86 -1.012 -1.318 -1.659 -.753z"
/>
</svg>
<div class="relative text-right w-28">
<span
class="absolute right-0 text-base opacity-90 bottom-full"
>
Previous
</span>
<strong>Camera</strong>
</div>
</div>
<div
id="Next__Camera"
class="!pt-8 ed-button btn__yellow"
mcrm__button
>
<div class="relative text-left w-28">
<span class="absolute text-base opacity-80 bottom-full">
Next
</span>
<strong>Camera</strong>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M2 5v14c0 .86 1.012 1.318 1.659 .753l8 -7a1 1 0 0 0 0 -1.506l-8 -7c-.647 -.565 -1.659 -.106 -1.659 .753z"
/>
<path
d="M13 5v14c0 .86 1.012 1.318 1.659 .753l8 -7a1 1 0 0 0 0 -1.506l-8 -7c-.647 -.565 -1.659 -.106 -1.659 .753z"
/>
</svg>
</div>
</div>
<div
id="Free__Camera"
class="ed-button btn__orange btn__vertical"
mcrm__button
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M18 9l3 3l-3 3" />
<path d="M15 12h6" />
<path d="M6 9l-3 3l3 3" />
<path d="M3 12h6" />
<path d="M9 18l3 3l3 -3" />
<path d="M12 15v6" />
<path d="M15 6l-3 -3l-3 3" />
<path d="M12 3v6" />
</svg>
<span>Free Camera</span>
</div>
<div class="grid-cols-2 ed-button-group__horizontal">
<div
id="Camera__Pos1"
class="ed-button btn__yellow !text-gray-800 btn__filled btn__vertical"
mcrm__button
>
<span class="text-base opacity-80">Position</span>
<strong>#1</strong>
</div>
<div
id="Camera__Pos2"
class="ed-button btn__yellow btn__vertical"
mcrm__button
>
<span class="text-base opacity-80">Position</span>
<strong>#2</strong>
</div>
</div>
</div>
</div>
</dialog>
</div>
<div class="flex items-end justify-end h-28">
<div id="clock">
<div class="hours-minutes"></div>
<sup class="seconds"></sup>
</div>
</div>
</div>
</div>
<script>
function onPanelLoaded() {
document
.querySelectorAll("#panel-html__body [toggle__button]")
.forEach((toggleButton) => {
if (toggleButton.getAttribute("active")) {
toggleClasses(toggleButton, true);
} else {
toggleClasses(toggleButton, false);
}
toggleButton.addEventListener("click", (e) => {
if (!toggleButton.getAttribute("active")) {
toggleButton.setAttribute("active", true);
toggleClasses(toggleButton, true);
} else {
toggleButton.removeAttribute("active");
toggleClasses(toggleButton, false);
}
});
});
function toggleClasses(toggleButton, active) {
const wrapper = toggleButton.querySelector(".toggle__wrapper");
const indicator = toggleButton.querySelector(".toggle__indicator");
const stateClasses = getStateClasses(toggleButton);
if (active) {
wrapper.classList.remove(...stateClasses.wrapper.inactive);
wrapper.classList.add(...stateClasses.wrapper.active);
indicator.classList.remove(...stateClasses.indicator.inactive);
indicator.classList.add(...stateClasses.indicator.active);
} else {
wrapper.classList.remove(...stateClasses.wrapper.active);
wrapper.classList.add(...stateClasses.wrapper.inactive);
indicator.classList.remove(...stateClasses.indicator.active);
indicator.classList.add(...stateClasses.indicator.inactive);
}
}
function getStateClasses(toggleButton) {
return {
wrapper: {
active: mapClasses(toggleButton.getAttribute("active-wrapper")),
inactive: mapClasses(
toggleButton.getAttribute("inactive-wrapper")
),
},
indicator: {
active: mapClasses(toggleButton.getAttribute("active-indicator")),
inactive: mapClasses(
toggleButton.getAttribute("inactive-indicator")
),
},
};
}
function mapClasses(classStr) {
return classStr.split(" ").map((c) => c.trim());
}
// document
// .querySelectorAll("[dialog-trigger]")
// .forEach((dialogTrigger) => {
// dialogTrigger.addEventListener("click", (e) => {
// document
// .querySelector(dialogTrigger.getAttribute("dialog-trigger"))
// .showModal();
// });
// });
// document
// .querySelectorAll("dialog, dialog .dialog__close")
// .forEach((dialogClose) => {
// const dialog = dialogClose.closest("dialog");
// dialogClose.addEventListener("click", (e) => {
// if (
// e.target.classList.contains("dialog__close") ||
// e.target.closest(".dialog__close") ||
// e.target.tagName == "DIALOG"
// ) {
// dialog.close();
// }
// });
// });
setInterval(() => {
const clockEl = document.querySelector("#clock");
const hoursMins = clockEl.querySelector(".hours-minutes");
const seconds = clockEl.querySelector(".seconds");
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
const secondsStr = now.getSeconds();
hoursMins.innerHTML = formatTimeToHTML(
`${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}`
);
seconds.innerHTML = formatTimeToHTML(
secondsStr.toString().padStart(2, "0")
);
}, 1000);
function formatTimeToHTML(timeStr) {
let htmlStr = "";
timeStr.split("").forEach((char) => {
if (char === ":") htmlStr += "<i>:</i>";
else htmlStr += "<span>" + char + "</span>";
});
return htmlStr;
}
}
</script>
<script no-compile>
onPanelLoaded();
</script>
</body>
</html>

View file

@ -0,0 +1,253 @@
@import url(https://fonts.bunny.net/css?family=orbitron:400,600,800);
@layer theme, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
@layer theme {
/* :root {
} */
}
html,
body,
#panel-html__body {
@apply relative;
}
#panel-html__body {
--font-sans: "Orbitron", sans-serif;
@apply font-sans text-sm font-light tracking-wide bg-gray-900 text-slate-50 size-full;
* {
box-sizing: border-box;
}
}
.group-left,
.group-middle,
.group-right {
@apply grid gap-2 h-fit;
}
.ed-panel {
@apply w-full p-2 border h-fit rounded-b-xl;
h3,
h4 {
@apply m-0 mb-1 font-extralight;
}
h3 {
@apply text-base;
}
h4 {
@apply text-sm;
}
&.pnl__blue {
@apply border-sky-300 bg-sky-500/30;
h3,
h4 {
@apply text-sky-100;
text-shadow: 0 0 0.2em var(--color-sky-300);
}
}
&.pnl__yellow {
@apply border-amber-300 bg-amber-500/30;
h3,
h4 {
@apply text-amber-100;
text-shadow: 0 0 0.2em var(--color-amber-300);
}
}
&.pnl__orange {
@apply border-orange-300 bg-orange-500/30;
h3,
h4 {
@apply text-orange-100;
text-shadow: 0 0 0.2em var(--color-orange-300);
}
}
&.pnl__red {
@apply border-rose-300 bg-rose-500/30;
h3,
h4 {
@apply text-rose-100;
text-shadow: 0 0 0.2em var(--color-rose-400);
}
}
&.pnl__white {
@apply border-white bg-white/20;
h3,
h4 {
@apply text-white;
text-shadow: 0 0 0.2em var(--color-white);
}
}
}
.ed-button {
@apply flex items-center justify-center px-4 py-2 text-base text-center rounded-lg border-3;
svg {
@apply block !size-5;
}
&.btn__vertical {
@apply flex-col;
}
&.btn__filled {
@apply text-gray-900;
}
&.btn__orange {
@apply text-orange-100 border-orange-400 bg-orange-500/50;
&.btn__filled {
@apply bg-orange-400;
}
}
&.btn__yellow {
@apply text-orange-100 border-amber-400 bg-amber-500/50;
&.btn__filled {
@apply bg-amber-400;
}
}
&.btn__blue {
@apply border-sky-400 bg-sky-500/50 text-sky-100;
&.btn__filled {
@apply bg-sky-500;
}
}
&.btn__red {
@apply border-rose-500 bg-rose-600/50 text-rose-100;
&.btn__filled {
@apply bg-rose-600;
}
}
&.btn__white {
@apply border-white bg-white/30;
&.btn__filled {
@apply bg-white;
}
}
}
.ed-button-group__horizontal {
@apply grid divide-x;
.ed-button {
@apply rounded-none;
&:first-child {
@apply rounded-l-lg;
}
&:last-child {
@apply rounded-r-lg;
}
}
}
.ed-button-group__vertical {
@apply grid divide-y;
.ed-button {
@apply rounded-none;
&:first-child {
@apply rounded-t-lg;
}
&:last-child {
@apply rounded-b-lg;
}
}
}
.ed-toggle {
.toggle__wrapper {
@apply relative p-1.5 border-2 rounded-full size-full;
}
.toggle__indicator {
@apply absolute transition rounded-full aspect-square;
}
&.toggle__horizontal {
@apply w-20 h-12;
.toggle__indicator {
@apply left-1.5 translate-x-0 h-[calc(100%-.75rem)];
}
&[active] .toggle__indicator {
@apply translate-x-full;
}
}
&.toggle__vertical {
@apply w-12 h-20;
.toggle__indicator {
@apply top-1.5 translate-y-0 w-[calc(100%-.75rem)];
}
&[active] .toggle__indicator {
@apply translate-y-full;
}
}
}
dialog[open] {
@apply absolute -translate-x-1/2 -translate-y-1/2 bg-transparent border-0 outline-0 top-1/2 left-1/2;
@apply backdrop:absolute backdrop:bg-black/50;
.dialog__close {
@apply absolute text-white top-3 right-3;
}
}
#clock {
@apply relative flex pr-16 text-3xl w-fit;
i {
@apply pl-1 not-italic;
}
.hours-minutes,
.seconds {
@apply flex gap-1;
}
span {
@apply inline-block w-[.75em] text-center;
}
sup {
@apply absolute right-0 w-16 pl-2 text-lg text-left opacity-80;
}
}

View file

@ -0,0 +1,688 @@
/*! tailwindcss v4.1.4 | MIT License | https://tailwindcss.com */
@import url(https://fonts.bunny.net/css?family=orbitron:400,600,800);
@layer properties;
@layer theme, utilities;
@layer theme {
:root, :host {
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--color-red-400: oklch(70.4% 0.191 22.216);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
--color-orange-100: oklch(95.4% 0.038 75.164);
--color-orange-300: oklch(83.7% 0.128 66.29);
--color-orange-400: oklch(75% 0.183 55.934);
--color-orange-500: oklch(70.5% 0.213 47.604);
--color-amber-100: oklch(96.2% 0.059 95.617);
--color-amber-300: oklch(87.9% 0.169 91.605);
--color-amber-400: oklch(82.8% 0.189 84.429);
--color-amber-500: oklch(76.9% 0.188 70.08);
--color-lime-400: oklch(84.1% 0.238 128.85);
--color-lime-600: oklch(64.8% 0.2 131.684);
--color-sky-100: oklch(95.1% 0.026 236.824);
--color-sky-300: oklch(82.8% 0.111 230.318);
--color-sky-400: oklch(74.6% 0.16 232.661);
--color-sky-500: oklch(68.5% 0.169 237.323);
--color-sky-600: oklch(58.8% 0.158 241.966);
--color-sky-900: oklch(39.1% 0.09 240.876);
--color-rose-100: oklch(94.1% 0.03 12.58);
--color-rose-300: oklch(81% 0.117 11.638);
--color-rose-400: oklch(71.2% 0.194 13.428);
--color-rose-500: oklch(64.5% 0.246 16.439);
--color-rose-600: oklch(58.6% 0.253 17.585);
--color-slate-50: oklch(98.4% 0.003 247.858);
--color-slate-950: oklch(12.9% 0.042 264.695);
--color-gray-800: oklch(27.8% 0.033 256.848);
--color-gray-900: oklch(21% 0.034 264.665);
--color-black: #000;
--color-white: #fff;
--spacing: 0.25rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-base: 1rem;
--text-base--line-height: calc(1.5 / 1);
--text-lg: 1.125rem;
--text-lg--line-height: calc(1.75 / 1.125);
--text-3xl: 1.875rem;
--text-3xl--line-height: calc(2.25 / 1.875);
--font-weight-extralight: 200;
--font-weight-light: 300;
--tracking-wide: 0.025em;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
}
@layer utilities {
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.inset-0 {
inset: calc(var(--spacing) * 0);
}
.right-0 {
right: calc(var(--spacing) * 0);
}
.bottom-full {
bottom: 100%;
}
.m-0 {
margin: calc(var(--spacing) * 0);
}
.mt-4 {
margin-top: calc(var(--spacing) * 4);
}
.flex {
display: flex;
}
.grid {
display: grid;
}
.aspect-square {
aspect-ratio: 1 / 1;
}
.size-8 {
width: calc(var(--spacing) * 8);
height: calc(var(--spacing) * 8);
}
.size-16 {
width: calc(var(--spacing) * 16);
height: calc(var(--spacing) * 16);
}
.size-full {
width: 100%;
height: 100%;
}
.h-28 {
height: calc(var(--spacing) * 28);
}
.h-full {
height: 100%;
}
.\!w-fit {
width: fit-content !important;
}
.w-28 {
width: calc(var(--spacing) * 28);
}
.w-fit {
width: fit-content;
}
.w-full {
width: 100%;
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid-cols-\[1fr_2fr\] {
grid-template-columns: 1fr 2fr;
}
.grid-cols-\[1fr_2fr_1fr\] {
grid-template-columns: 1fr 2fr 1fr;
}
.grid-cols-\[2fr_1fr\] {
grid-template-columns: 2fr 1fr;
}
.grid-cols-\[3fr_1fr_1fr\] {
grid-template-columns: 3fr 1fr 1fr;
}
.grid-rows-3 {
grid-template-rows: repeat(3, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.items-end {
align-items: flex-end;
}
.\!justify-start {
justify-content: flex-start !important;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.justify-end {
justify-content: flex-end;
}
.justify-items-center {
justify-items: center;
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.divide-x {
:where(& > :not(:last-child)) {
--tw-divide-x-reverse: 0;
border-inline-style: var(--tw-border-style);
border-inline-start-width: calc(1px * var(--tw-divide-x-reverse));
border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse)));
}
}
.rounded-full {
border-radius: calc(infinity * 1px);
}
.\!rounded-tl-none {
border-top-left-radius: 0 !important;
}
.\!rounded-tr-none {
border-top-right-radius: 0 !important;
}
.\!rounded-b-none {
border-bottom-right-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
.border-3 {
border-style: var(--tw-border-style);
border-width: 3px;
}
.border-lime-600 {
border-color: var(--color-lime-600);
}
.border-red-500 {
border-color: var(--color-red-500);
}
.border-red-600 {
border-color: var(--color-red-600);
}
.border-sky-600 {
border-color: var(--color-sky-600);
}
.border-white {
border-color: var(--color-white);
}
.\!bg-sky-900 {
background-color: var(--color-sky-900) !important;
}
.bg-lime-400 {
background-color: var(--color-lime-400);
}
.bg-lime-400\/30 {
background-color: color-mix(in srgb, oklch(84.1% 0.238 128.85) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-lime-400) 30%, transparent);
}
}
.bg-red-400 {
background-color: var(--color-red-400);
}
.bg-red-400\/30 {
background-color: color-mix(in srgb, oklch(70.4% 0.191 22.216) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-red-400) 30%, transparent);
}
}
.bg-red-500\/80 {
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 80%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-red-500) 80%, transparent);
}
}
.bg-sky-400 {
background-color: var(--color-sky-400);
}
.bg-sky-400\/30 {
background-color: color-mix(in srgb, oklch(74.6% 0.16 232.661) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-sky-400) 30%, transparent);
}
}
.bg-slate-950 {
background-color: var(--color-slate-950);
}
.bg-white {
background-color: var(--color-white);
}
.bg-white\/30 {
background-color: color-mix(in srgb, #fff 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 30%, transparent);
}
}
.\!p-0 {
padding: calc(var(--spacing) * 0) !important;
}
.\!p-4 {
padding: calc(var(--spacing) * 4) !important;
}
.p-2 {
padding: calc(var(--spacing) * 2);
}
.\!px-2 {
padding-inline: calc(var(--spacing) * 2) !important;
}
.\!pt-8 {
padding-top: calc(var(--spacing) * 8) !important;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-base {
font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height));
}
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.\!text-gray-800 {
color: var(--color-gray-800) !important;
}
.text-red-400 {
color: var(--color-red-400);
}
.text-white {
color: var(--color-white);
}
.opacity-80 {
opacity: 80%;
}
.opacity-90 {
opacity: 90%;
}
}
@layer theme;
html, body, #panel-html__body {
position: relative;
}
#panel-html__body {
--font-sans: "Orbitron", sans-serif;
width: 100%;
height: 100%;
background-color: var(--color-gray-900);
font-family: var(--font-sans);
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
--tw-font-weight: var(--font-weight-light);
font-weight: var(--font-weight-light);
--tw-tracking: var(--tracking-wide);
letter-spacing: var(--tracking-wide);
color: var(--color-slate-50);
* {
box-sizing: border-box;
}
}
.group-left, .group-middle, .group-right {
display: grid;
height: fit-content;
gap: calc(var(--spacing) * 2);
}
.ed-panel {
height: fit-content;
width: 100%;
border-bottom-right-radius: var(--radius-xl);
border-bottom-left-radius: var(--radius-xl);
border-style: var(--tw-border-style);
border-width: 1px;
padding: calc(var(--spacing) * 2);
h3, h4 {
margin: calc(var(--spacing) * 0);
margin-bottom: calc(var(--spacing) * 1);
--tw-font-weight: var(--font-weight-extralight);
font-weight: var(--font-weight-extralight);
}
h3 {
font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height));
}
h4 {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
&.pnl__blue {
border-color: var(--color-sky-300);
background-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-sky-500) 30%, transparent);
}
h3, h4 {
color: var(--color-sky-100);
text-shadow: 0 0 0.2em var(--color-sky-300);
}
}
&.pnl__yellow {
border-color: var(--color-amber-300);
background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-amber-500) 30%, transparent);
}
h3, h4 {
color: var(--color-amber-100);
text-shadow: 0 0 0.2em var(--color-amber-300);
}
}
&.pnl__orange {
border-color: var(--color-orange-300);
background-color: color-mix(in srgb, oklch(70.5% 0.213 47.604) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-orange-500) 30%, transparent);
}
h3, h4 {
color: var(--color-orange-100);
text-shadow: 0 0 0.2em var(--color-orange-300);
}
}
&.pnl__red {
border-color: var(--color-rose-300);
background-color: color-mix(in srgb, oklch(64.5% 0.246 16.439) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-rose-500) 30%, transparent);
}
h3, h4 {
color: var(--color-rose-100);
text-shadow: 0 0 0.2em var(--color-rose-400);
}
}
&.pnl__white {
border-color: var(--color-white);
background-color: color-mix(in srgb, #fff 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 20%, transparent);
}
h3, h4 {
color: var(--color-white);
text-shadow: 0 0 0.2em var(--color-white);
}
}
}
.ed-button {
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
border-style: var(--tw-border-style);
border-width: 3px;
padding-inline: calc(var(--spacing) * 4);
padding-block: calc(var(--spacing) * 2);
text-align: center;
font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height));
svg {
display: block;
width: calc(var(--spacing) * 5) !important;
height: calc(var(--spacing) * 5) !important;
}
&.btn__vertical {
flex-direction: column;
}
&.btn__filled {
color: var(--color-gray-900);
}
&.btn__orange {
border-color: var(--color-orange-400);
background-color: color-mix(in srgb, oklch(70.5% 0.213 47.604) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-orange-500) 50%, transparent);
}
color: var(--color-orange-100);
&.btn__filled {
background-color: var(--color-orange-400);
}
}
&.btn__yellow {
border-color: var(--color-amber-400);
background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-amber-500) 50%, transparent);
}
color: var(--color-orange-100);
&.btn__filled {
background-color: var(--color-amber-400);
}
}
&.btn__blue {
border-color: var(--color-sky-400);
background-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-sky-500) 50%, transparent);
}
color: var(--color-sky-100);
&.btn__filled {
background-color: var(--color-sky-500);
}
}
&.btn__red {
border-color: var(--color-rose-500);
background-color: color-mix(in srgb, oklch(58.6% 0.253 17.585) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-rose-600) 50%, transparent);
}
color: var(--color-rose-100);
&.btn__filled {
background-color: var(--color-rose-600);
}
}
&.btn__white {
border-color: var(--color-white);
background-color: color-mix(in srgb, #fff 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 30%, transparent);
}
&.btn__filled {
background-color: var(--color-white);
}
}
}
.ed-button-group__horizontal {
display: grid;
:where(& > :not(:last-child)) {
--tw-divide-x-reverse: 0;
border-inline-style: var(--tw-border-style);
border-inline-start-width: calc(1px * var(--tw-divide-x-reverse));
border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse)));
}
.ed-button {
border-radius: 0;
&:first-child {
border-top-left-radius: var(--radius-lg);
border-bottom-left-radius: var(--radius-lg);
}
&:last-child {
border-top-right-radius: var(--radius-lg);
border-bottom-right-radius: var(--radius-lg);
}
}
}
.ed-button-group__vertical {
display: grid;
:where(& > :not(:last-child)) {
--tw-divide-y-reverse: 0;
border-bottom-style: var(--tw-border-style);
border-top-style: var(--tw-border-style);
border-top-width: calc(1px * var(--tw-divide-y-reverse));
border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
}
.ed-button {
border-radius: 0;
&:first-child {
border-top-left-radius: var(--radius-lg);
border-top-right-radius: var(--radius-lg);
}
&:last-child {
border-bottom-right-radius: var(--radius-lg);
border-bottom-left-radius: var(--radius-lg);
}
}
}
.ed-toggle {
.toggle__wrapper {
position: relative;
width: 100%;
height: 100%;
border-radius: calc(infinity * 1px);
border-style: var(--tw-border-style);
border-width: 2px;
padding: calc(var(--spacing) * 1.5);
}
.toggle__indicator {
position: absolute;
aspect-ratio: 1 / 1;
border-radius: calc(infinity * 1px);
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
&.toggle__horizontal {
height: calc(var(--spacing) * 12);
width: calc(var(--spacing) * 20);
.toggle__indicator {
left: calc(var(--spacing) * 1.5);
height: calc(100% - .75rem);
--tw-translate-x: calc(var(--spacing) * 0);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
&[active] .toggle__indicator {
--tw-translate-x: 100%;
translate: var(--tw-translate-x) var(--tw-translate-y);
}
}
&.toggle__vertical {
height: calc(var(--spacing) * 20);
width: calc(var(--spacing) * 12);
.toggle__indicator {
top: calc(var(--spacing) * 1.5);
width: calc(100% - .75rem);
--tw-translate-y: calc(var(--spacing) * 0);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
&[active] .toggle__indicator {
--tw-translate-y: 100%;
translate: var(--tw-translate-x) var(--tw-translate-y);
}
}
}
dialog[open] {
position: absolute;
top: calc(1/2 * 100%);
left: calc(1/2 * 100%);
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
border-style: var(--tw-border-style);
border-width: 0px;
background-color: transparent;
outline-style: var(--tw-outline-style);
outline-width: 0px;
&::backdrop {
background-color: color-mix(in srgb, #000 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-black) 50%, transparent);
}
}
.dialog__close {
position: absolute;
top: calc(var(--spacing) * 3);
right: calc(var(--spacing) * 3);
color: var(--color-white);
}
}
#clock {
position: relative;
display: flex;
width: fit-content;
padding-right: calc(var(--spacing) * 16);
font-size: var(--text-3xl);
line-height: var(--tw-leading, var(--text-3xl--line-height));
i {
padding-left: calc(var(--spacing) * 1);
font-style: normal;
}
.hours-minutes, .seconds {
display: flex;
gap: calc(var(--spacing) * 1);
}
span {
display: inline-block;
width: .75em;
text-align: center;
}
sup {
position: absolute;
right: calc(var(--spacing) * 0);
width: calc(var(--spacing) * 16);
padding-left: calc(var(--spacing) * 2);
text-align: left;
font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height));
opacity: 80%;
}
}
@property --tw-divide-x-reverse {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-font-weight {
syntax: "*";
inherits: false;
}
@property --tw-tracking {
syntax: "*";
inherits: false;
}
@property --tw-divide-y-reverse {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-x {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-y {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-z {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-divide-x-reverse: 0;
--tw-border-style: solid;
--tw-font-weight: initial;
--tw-tracking: initial;
--tw-divide-y-reverse: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
--tw-outline-style: solid;
}
}
}

974
panels/Elite_Dangerous/package-lock.json generated Normal file
View file

@ -0,0 +1,974 @@
{
"name": "test_panel",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "test_panel",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@tailwindcss/cli": "^4.1.2",
"tailwindcss": "^4.1.2"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.4.tgz",
"integrity": "sha512-gP05Qihh+cZ2FqD5fa0WJXx3KEk2YWUYv/RBKAyiOg0V4vYVDr/xlLc0sacpnVEXM45BVUR9U2hsESufYs6YTA==",
"dev": true,
"dependencies": {
"@parcel/watcher": "^2.5.1",
"@tailwindcss/node": "4.1.4",
"@tailwindcss/oxide": "4.1.4",
"enhanced-resolve": "^5.18.1",
"mri": "^1.2.0",
"picocolors": "^1.1.1",
"tailwindcss": "4.1.4"
},
"bin": {
"tailwindcss": "dist/index.mjs"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz",
"integrity": "sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==",
"dev": true,
"dependencies": {
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "1.29.2",
"tailwindcss": "4.1.4"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.4.tgz",
"integrity": "sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==",
"dev": true,
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.4",
"@tailwindcss/oxide-darwin-arm64": "4.1.4",
"@tailwindcss/oxide-darwin-x64": "4.1.4",
"@tailwindcss/oxide-freebsd-x64": "4.1.4",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.4",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.4",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.4",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.4",
"@tailwindcss/oxide-linux-x64-musl": "4.1.4",
"@tailwindcss/oxide-wasm32-wasi": "4.1.4",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.4",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.4"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.4.tgz",
"integrity": "sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.4.tgz",
"integrity": "sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.4.tgz",
"integrity": "sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.4.tgz",
"integrity": "sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.4.tgz",
"integrity": "sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.4.tgz",
"integrity": "sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.4.tgz",
"integrity": "sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.4.tgz",
"integrity": "sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.4.tgz",
"integrity": "sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.4.tgz",
"integrity": "sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"dev": true,
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.0",
"@emnapi/runtime": "^1.4.0",
"@emnapi/wasi-threads": "^1.0.1",
"@napi-rs/wasm-runtime": "^0.2.8",
"@tybys/wasm-util": "^0.9.0",
"tslib": "^2.8.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.4.tgz",
"integrity": "sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.4.tgz",
"integrity": "sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/jiti": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"dev": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/lightningcss": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
"dev": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-darwin-arm64": "1.29.2",
"lightningcss-darwin-x64": "1.29.2",
"lightningcss-freebsd-x64": "1.29.2",
"lightningcss-linux-arm-gnueabihf": "1.29.2",
"lightningcss-linux-arm64-gnu": "1.29.2",
"lightningcss-linux-arm64-musl": "1.29.2",
"lightningcss-linux-x64-gnu": "1.29.2",
"lightningcss-linux-x64-musl": "1.29.2",
"lightningcss-win32-arm64-msvc": "1.29.2",
"lightningcss-win32-x64-msvc": "1.29.2"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss/node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tailwindcss": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
"integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==",
"dev": true
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
}
}
}

View file

@ -0,0 +1,16 @@
{
"name": "test_panel",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "npx tailwindcss -i ./input.css -o ./output.css --watch"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@tailwindcss/cli": "^4.1.2",
"tailwindcss": "^4.1.2"
}
}

View file

@ -0,0 +1,48 @@
{
"dir": "",
"name": "Elite Dangerous",
"description": "A Semi Realistic button panel for Elite Dangerous, made by JaxxMoss.",
"aspectRatio": "16/10",
"macros": {
"CM__Chaff": "ED-CM-Chaff",
"CM__ECM": "ED-CM-ECM",
"CM__Heatsink": "ED-CM-Heat_Sink",
"CM__Shieldcell": "ED-CM-Schield_Cell",
"Camera__Pos1": "ED-Camera-Position1",
"Camera__Pos2": "ED-Camera-Position2",
"Camera__Suite": "ED-Camera-Suite",
"Comms__panel": "ED-Comms_Panel",
"External__panel": "ED-External_Panel",
"FSD__toggle": "ED-Toggle_FSD",
"Fighter__engage": "ED-Fighter-Engage",
"Fighter__hold": "ED-Fighter-Hold",
"Fighter__recall": "ED-Fighter-Recall",
"Fighter__wingman1": "ED-Fighter-Wingman1",
"Fighter__wingman2": "ED-Fighter-Wingman2",
"Fighter__wingman3": "ED-Fighter-Wingman3",
"Fighter_attack": "ED-Fighter-Attack",
"Fighter_defend": "ED-Fighter-Defend",
"Fighter_follow": "ED-Fighter-Follow",
"FlightAssist__toggle": "ED-Flight_Assist",
"Free__Camera": "ED-Camera-Free",
"Galaxy__map": "ED-Galaxy_Map",
"Hardpoints__toggle": "ED-Hardpoints_Switch",
"Hyper__Space": "ED-Hyperspace",
"Internal__panel": "ED-Internal_Panel",
"Jettison__Cargo": "ED-Jettison_Cargo",
"Lights__toggle": "ED-Flight_Assist",
"Mode__toggle": "ED-Mode_Switch",
"Next__Camera": "ED-Camera-Next",
"NightVis__toggle": "ED-Night_Vision",
"Previous__Camera": "ED-Camera-Previous",
"Roles__panel": "ED-Roles_Panel",
"Rotational__Correction": "ED-Rotational_Correction",
"Route__NextSystem": "ED-Route_Next_System",
"Scanner__DiscScan": "ED-Scanner_Discovery",
"Scanner__FSS": "ED-Scanner-FSS",
"SilentRunning__toggle": "ED-Silent_Running",
"Speed_0percent": "ED-0percent_Speed",
"Super__Cruise": "ED-Super_Cruise",
"System__map": "ED-System_Map"
}
}

View file

@ -0,0 +1,17 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html", // Ensure this path is correct
],
theme: {
extend: {},
},
safelist: [
{
pattern: /^(border|bg)-(blue|red|sky|orange|lime)-(200|400|300\/40)$/,
},
],
plugins: [],
preflight: false,
mode: "jit",
};

View file

@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./output.css" />
</head>
<body class="bg-slate-400 w-screen h-screen m-0 aspect-[9/20]">
<div class="h-full bg-slate-500">
<div class="grid grid-cols-2 gap-2 size-full grid-rows-8">
<div
class="flex items-center justify-center bg-sky-400"
id="button_1"
mcrm__button
>
Task manager
</div>
<div
class="flex items-center justify-center bg-sky-400"
id="button_2"
mcrm__button
>
Close application
</div>
<div
class="flex items-center justify-center bg-sky-400"
id="button_3"
mcrm__button
>
Run
</div>
<div
class="flex items-center justify-center bg-sky-400"
id="button_4"
mcrm__button
>
Files
</div>
<div
class="flex items-center justify-center bg-sky-400"
id="button_5"
mcrm__button
>
Settings
</div>
<div
class="flex items-center justify-center bg-sky-400"
id="button_6"
mcrm__button
>
New Desktop
</div>
<div
class="flex items-center justify-center bg-sky-400"
id="button_7"
mcrm__button
>
Displays
</div>
<div
class="flex items-center justify-center bg-sky-400"
id="button_8"
mcrm__button
>
Task View
</div>
<div
class="flex items-center justify-center bg-rose-400"
id="button_9"
mcrm__button
>
New Window
</div>
<div
class="flex items-center justify-center bg-rose-400"
id="button_10"
mcrm__button
>
Close Window
</div>
<div
class="flex items-center justify-center bg-rose-400"
id="button_11"
mcrm__button
>
Previous Tab
</div>
<div
class="flex items-center justify-center bg-rose-400"
id="button_12"
mcrm__button
>
Next Tab
</div>
<div
class="flex items-center justify-center bg-rose-400"
id="button_13"
mcrm__button
>
Close Tab
</div>
<div
class="flex items-center justify-center bg-rose-400"
id="button_14"
mcrm__button
>
New Tab
</div>
<div
class="flex items-center justify-center bg-rose-400"
id="button_15"
mcrm__button
>
Fullscreen
</div>
<div
class="flex items-center justify-center bg-rose-400"
id="button_16"
mcrm__button
>
Home
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,6 @@
@layer theme, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
body * {
box-sizing: border-box;
}

View file

@ -0,0 +1,69 @@
/*! tailwindcss v4.1.2 | MIT License | https://tailwindcss.com */
@layer theme, utilities;
@layer theme {
:root, :host {
--color-red-500: oklch(63.7% 0.237 25.331);
--color-sky-400: oklch(74.6% 0.16 232.661);
--color-rose-400: oklch(71.2% 0.194 13.428);
--color-slate-400: oklch(70.4% 0.04 256.788);
--color-slate-500: oklch(55.4% 0.046 257.417);
--spacing: 0.25rem;
}
}
@layer utilities {
.m-0 {
margin: calc(var(--spacing) * 0);
}
.flex {
display: flex;
}
.grid {
display: grid;
}
.aspect-\[9\/20\] {
aspect-ratio: 9/20;
}
.size-full {
width: 100%;
height: 100%;
}
.h-full {
height: 100%;
}
.h-screen {
height: 100vh;
}
.w-screen {
width: 100vw;
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-rows-8 {
grid-template-rows: repeat(8, minmax(0, 1fr));
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.bg-rose-400 {
background-color: var(--color-rose-400);
}
.bg-sky-400 {
background-color: var(--color-sky-400);
}
.bg-slate-400 {
background-color: var(--color-slate-400);
}
.bg-slate-500 {
background-color: var(--color-slate-500);
}
}
body * {
box-sizing: border-box;
}

Some files were not shown because too many files have changed in this diff Show more