UPD: Migrate to electron-vite

Delete your node_modules. Use pnpm from now on.
This commit is contained in:
Robert Kossessa
2024-03-01 19:45:18 +01:00
parent 057b8a5fc7
commit 7b67516125
392 changed files with 4454 additions and 21172 deletions

22
src/renderer/index.html Normal file
View File

@@ -0,0 +1,22 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>ZERO/ONE Configuration Suite</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
<style>
html {
background: black;
}
</style>
</head>
<body class="dark bg-background">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

65
src/renderer/loading.html Normal file
View File

@@ -0,0 +1,65 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>ZERO/ONE Starting...</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
<style>
body {
background: black url(src/assets/images/splashBG.png) center;
background-size: cover;
color: white;
height: 100vh;
overflow: hidden;
}
.badge {
background: #f79c1e;
color: black;
display: inline-block;
align-self: center;
width: fit-content;
border-radius: 4px;
padding: 4px 8px;
}
.title {
margin-top: 10vh;
gap: 8px;
}
.status {
margin-top: 10vh;
}
.footer {
font-size: 0.5rem;
}
</style>
</head>
<body class="dark bg-background">
<div class="flex h-full flex-col text-center justify-between items-center p-2 px-8 select-none">
<div class="title flex flex-col gap-2">
<h1 class="text-5xl">ZERO<span class="opacity-70">/</span>ONE</h1>
<p class="text-muted-foreground">Configuration Suite v0.1</p>
<div class="badge text-xs">PUBLIC BETA</div>
<div class="status">
<p>Stand by...</p>
<p class="text-muted-foreground text-sm">The goblins are turning the cogs.</p>
</div>
</div>
<div class="footer flex flex-col gap-2 text-muted-foreground">
<img class="w-3 self-center" src="/src/assets/icons/binarisEmblem.svg" alt="binaris-logo">
<p>Copyright 2024 Binaris Circuitry Ltd.</p>
<p>This software is licensed under the Apache License, Version 2.0 (the "License"). Usage of this
software is
permitted only in compliance with the terms of the License.</p>
</div>
</div>
</body>
</html>

84
src/renderer/src/App.vue Normal file
View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import ProfileManager from '@renderer/components/profile/ProfileManager.vue'
import DevicePreview from '@renderer/components/device/DevicePreview.vue'
import ConfigPane from '@renderer/components/config/ConfigPane.vue'
import Navbar from '@renderer/components/navbar/Navbar.vue'
import { useStore } from '@renderer/store'
// import { useMessageHandlers } from '@renderer/device'
// const { electron } = window
const store = useStore()
// const menuActions = {
// connect: () => store.setConnected(!store.connected),
// orientation: () => store.cycleScreenOrientation(),
// skin: () => store.switchPreviewDeviceModel(),
// }
// electron?.onMenu((key) => {
// console.log('menu', key)
// if (menuActions[key]) {
// menuActions[key]()
// }
// })
// store.fetchProfiles() // TODO remove me!
// // handle device events
// const handlers = useMessageHandlers(store)
// window.nanodevices.on_event('device-attached', (evt, deviceid, data) => store.device_attached(deviceid))
// window.nanodevices.on_event('device-detached', (evt, deviceid, data) => store.device_detached(deviceid))
// window.nanodevices.on_event('device-error', (evt, deviceid, data) => { /* TODO handle connection errors */ })
// window.nanodevices.on_event('connected', (evt, deviceid, data) => store.device_connected(deviceid))
// window.nanodevices.on_event('disconnected', (evt, deviceid, data) => store.device_disconnected(deviceid))
// window.nanodevices.on_event('update', (evt, deviceid, data) => { handlers.handle_message(data) })
// // get list of the currently attached devices
// window.nanodevices.list_devices().then((devs)=>store.init_devices(devs))
</script>
<template>
<main class="select-none w-screen h-screen flex flex-col">
<Navbar class="flex-none" />
<div class="flex-1 min-h-0 flex flex-row justify-center">
<div class="basis-1/3 min-w-60 flex-1 flex overflow-hidden">
<Transition name="slide-left">
<ProfileManager
v-if="store.connected"
class="flex-1 max-w-full flex flex-col border-solid border-0 border-r bg-zinc-900 bg-opacity-50"
/>
</Transition>
</div>
<DevicePreview class="basis-1/3 flex-col flex" />
<div class="basis-2/5 flex-1 flex overflow-hidden">
<Transition name="slide-right">
<ConfigPane
v-if="store.connected"
class="flex-1 max-w-full flex flex-col border-solid border-0 border-l bg-zinc-900 bg-opacity-50"
/>
</Transition>
</div>
</div>
</main>
</template>
<style scoped>
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: transform 700ms ease;
}
.slide-left-enter-active,
.slide-right-enter-active {
transition-delay: 500ms;
}
.slide-left-enter-from,
.slide-left-leave-to {
transform: translateX(-100%);
}
.slide-right-enter-from,
.slide-right-leave-to {
transform: translateX(100%);
}
</style>

View File

@@ -0,0 +1,10 @@
<svg viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="64" cy="64" r="64" fill="#2F3242"/>
<ellipse cx="63.9835" cy="23.2036" rx="4.48794" ry="4.495" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path d="M51.3954 39.5028C52.3733 39.6812 53.3108 39.033 53.4892 38.055C53.6676 37.0771 53.0194 36.1396 52.0414 35.9612L51.3954 39.5028ZM28.6153 43.5751L30.1748 44.4741L30.1748 44.4741L28.6153 43.5751ZM28.9393 60.9358C29.4332 61.7985 30.5329 62.0976 31.3957 61.6037C32.2585 61.1098 32.5575 60.0101 32.0636 59.1473L28.9393 60.9358ZM37.6935 66.7457C37.025 66.01 35.8866 65.9554 35.1508 66.6239C34.415 67.2924 34.3605 68.4308 35.029 69.1666L37.6935 66.7457ZM53.7489 81.7014L52.8478 83.2597L53.7489 81.7014ZM96.9206 89.515C97.7416 88.9544 97.9526 87.8344 97.3919 87.0135C96.8313 86.1925 95.7113 85.9815 94.8904 86.5422L96.9206 89.515ZM52.0414 35.9612C46.4712 34.9451 41.2848 34.8966 36.9738 35.9376C32.6548 36.9806 29.0841 39.1576 27.0559 42.6762L30.1748 44.4741C31.5693 42.0549 34.1448 40.3243 37.8188 39.4371C41.5009 38.5479 46.1547 38.5468 51.3954 39.5028L52.0414 35.9612ZM27.0559 42.6762C24.043 47.9029 25.2781 54.5399 28.9393 60.9358L32.0636 59.1473C28.6579 53.1977 28.1088 48.0581 30.1748 44.4741L27.0559 42.6762ZM35.029 69.1666C39.6385 74.24 45.7158 79.1355 52.8478 83.2597L54.6499 80.1432C47.8081 76.1868 42.0298 71.5185 37.6935 66.7457L35.029 69.1666ZM52.8478 83.2597C61.344 88.1726 70.0465 91.2445 77.7351 92.3608C85.359 93.4677 92.2744 92.6881 96.9206 89.515L94.8904 86.5422C91.3255 88.9767 85.4902 89.849 78.2524 88.7982C71.0793 87.7567 62.809 84.8612 54.6499 80.1432L52.8478 83.2597ZM105.359 84.9077C105.359 81.4337 102.546 78.6127 99.071 78.6127V82.2127C100.553 82.2127 101.759 83.4166 101.759 84.9077H105.359ZM99.071 78.6127C95.5956 78.6127 92.7831 81.4337 92.7831 84.9077H96.3831C96.3831 83.4166 97.5892 82.2127 99.071 82.2127V78.6127ZM92.7831 84.9077C92.7831 88.3817 95.5956 91.2027 99.071 91.2027V87.6027C97.5892 87.6027 96.3831 86.3988 96.3831 84.9077H92.7831ZM99.071 91.2027C102.546 91.2027 105.359 88.3817 105.359 84.9077H101.759C101.759 86.3988 100.553 87.6027 99.071 87.6027V91.2027Z" fill="#A2ECFB"/>
<path d="M91.4873 65.382C90.8456 66.1412 90.9409 67.2769 91.7002 67.9186C92.4594 68.5603 93.5951 68.465 94.2368 67.7058L91.4873 65.382ZM99.3169 43.6354L97.7574 44.5344L99.3169 43.6354ZM84.507 35.2412C83.513 35.2282 82.6967 36.0236 82.6838 37.0176C82.6708 38.0116 83.4661 38.8279 84.4602 38.8409L84.507 35.2412ZM74.9407 39.8801C75.9127 39.6716 76.5315 38.7145 76.323 37.7425C76.1144 36.7706 75.1573 36.1517 74.1854 36.3603L74.9407 39.8801ZM53.7836 46.3728L54.6847 47.931L53.7836 46.3728ZM25.5491 80.9047C25.6932 81.8883 26.6074 82.5688 27.5911 82.4247C28.5747 82.2806 29.2552 81.3664 29.1111 80.3828L25.5491 80.9047ZM94.2368 67.7058C97.8838 63.3907 100.505 58.927 101.752 54.678C103.001 50.4213 102.9 46.2472 100.876 42.7365L97.7574 44.5344C99.1494 46.9491 99.3603 50.0419 98.2974 53.6644C97.2323 57.2945 94.9184 61.3223 91.4873 65.382L94.2368 67.7058ZM100.876 42.7365C97.9119 37.5938 91.7082 35.335 84.507 35.2412L84.4602 38.8409C91.1328 38.9278 95.7262 41.0106 97.7574 44.5344L100.876 42.7365ZM74.1854 36.3603C67.4362 37.8086 60.0878 40.648 52.8826 44.8146L54.6847 47.931C61.5972 43.9338 68.5948 41.2419 74.9407 39.8801L74.1854 36.3603ZM52.8826 44.8146C44.1366 49.872 36.9669 56.0954 32.1491 62.3927C27.3774 68.63 24.7148 75.2115 25.5491 80.9047L29.1111 80.3828C28.4839 76.1026 30.4747 70.5062 35.0084 64.5802C39.496 58.7143 46.2839 52.7889 54.6847 47.931L52.8826 44.8146Z" fill="#A2ECFB"/>
<path d="M49.0825 87.2295C48.7478 86.2934 47.7176 85.8059 46.7816 86.1406C45.8455 86.4753 45.358 87.5055 45.6927 88.4416L49.0825 87.2295ZM78.5635 96.4256C79.075 95.5732 78.7988 94.4675 77.9464 93.9559C77.0941 93.4443 75.9884 93.7205 75.4768 94.5729L78.5635 96.4256ZM79.5703 85.1795C79.2738 86.1284 79.8027 87.1379 80.7516 87.4344C81.7004 87.7308 82.71 87.2019 83.0064 86.2531L79.5703 85.1795ZM84.3832 64.0673H82.5832H84.3832ZM69.156 22.5301C68.2477 22.1261 67.1838 22.535 66.7799 23.4433C66.3759 24.3517 66.7848 25.4155 67.6931 25.8194L69.156 22.5301ZM45.6927 88.4416C47.5994 93.7741 50.1496 98.2905 53.2032 101.505C56.2623 104.724 59.9279 106.731 63.9835 106.731V103.131C61.1984 103.131 58.4165 101.765 55.8131 99.0249C53.2042 96.279 50.8768 92.2477 49.0825 87.2295L45.6927 88.4416ZM63.9835 106.731C69.8694 106.731 74.8921 102.542 78.5635 96.4256L75.4768 94.5729C72.0781 100.235 68.0122 103.131 63.9835 103.131V106.731ZM83.0064 86.2531C85.0269 79.7864 86.1832 72.1831 86.1832 64.0673H82.5832C82.5832 71.8536 81.4723 79.0919 79.5703 85.1795L83.0064 86.2531ZM86.1832 64.0673C86.1832 54.1144 84.4439 44.922 81.4961 37.6502C78.5748 30.4436 74.3436 24.8371 69.156 22.5301L67.6931 25.8194C71.6364 27.5731 75.3846 32.1564 78.1598 39.0026C80.9086 45.7836 82.5832 54.507 82.5832 64.0673H86.1832Z" fill="#A2ECFB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M103.559 84.9077C103.559 82.4252 101.55 80.4127 99.071 80.4127C96.5924 80.4127 94.5831 82.4252 94.5831 84.9077C94.5831 87.3902 96.5924 89.4027 99.071 89.4027C101.55 89.4027 103.559 87.3902 103.559 84.9077V84.9077Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8143 89.4027C31.2929 89.4027 33.3023 87.3902 33.3023 84.9077C33.3023 82.4252 31.2929 80.4127 28.8143 80.4127C26.3357 80.4127 24.3264 82.4252 24.3264 84.9077C24.3264 87.3902 26.3357 89.4027 28.8143 89.4027V89.4027V89.4027Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.8501 68.0857C62.6341 68.5652 60.451 67.1547 59.9713 64.9353C59.4934 62.7159 60.9007 60.5293 63.1167 60.0489C65.3326 59.5693 67.5157 60.9798 67.9954 63.1992C68.4742 65.4186 67.066 67.6052 64.8501 68.0857Z" fill="#A2ECFB"/>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g><path d="M199.828,-457.918l0,510c0,11.038 -8.962,20 -20,20l-360,-0c-11.038,-0 -20,-8.962 -20,-20l0,-510c0,-11.038 8.962,-20 20,-20l360,-0c11.038,-0 20,8.962 20,20Z"/><clipPath id="_clip1"><path d="M199.828,-457.918l0,510c0,11.038 -8.962,20 -20,20l-360,-0c-11.038,-0 -20,-8.962 -20,-20l0,-510c0,-11.038 8.962,-20 20,-20l360,-0c11.038,-0 20,8.962 20,20Z"/></clipPath><g clip-path="url(#_clip1)"><clipPath id="_clip2"><rect x="-238.278" y="-191.846" width="488.555" height="299.15"/></clipPath><g clip-path="url(#_clip2)"><use xlink:href="#_Image3" x="0" y="0" width="12px" height="12px"/></g><rect x="-238.278" y="-144.214" width="442.547" height="254.163" style="fill:url(#_Linear4);"/></g><g><g><path d="M12,1.2l-0,9.6c-0,0.662 -0.538,1.2 -1.2,1.2l-9.6,-0c-0.662,-0 -1.2,-0.538 -1.2,-1.2l-0,-9.6c-0,-0.662 0.538,-1.2 1.2,-1.2l9.6,-0c0.662,-0 1.2,0.538 1.2,1.2Z" style="fill:#fff;"/><path d="M6.924,5.399c-0.076,0.077 -0.164,0.11 -0.273,0.11l-1.597,-0c-0.274,-0 -0.492,-0.23 -0.492,-0.504l-0,-1.345c-0,-0.274 0.218,-0.492 0.492,-0.492l1.597,-0c0.109,-0 0.197,0.032 0.273,0.109l0.405,0.405c0.077,0.076 0.109,0.164 0.109,0.273l0,0.755c0,0.109 -0.032,0.197 -0.109,0.274l-0.405,0.415Zm0,3.324c-0.076,0.077 -0.164,0.109 -0.273,0.109l-1.597,0c-0.274,0 -0.492,-0.218 -0.492,-0.492l-0,-1.335c-0,-0.273 0.218,-0.503 0.492,-0.503l1.597,0c0.109,0 0.197,0.033 0.273,0.11l0.405,0.415c0.077,0.077 0.109,0.164 0.109,0.274l0,0.744c0,0.109 -0.032,0.197 -0.109,0.273l-0.405,0.405Zm1.366,-3.291c0.098,-0.098 0.142,-0.208 0.142,-0.35l-0,-1.499c-0,-0.142 -0.044,-0.251 -0.142,-0.35l-0.917,-0.906c-0.088,-0.098 -0.208,-0.153 -0.361,-0.153l-2.953,0c-0.273,0 -0.491,0.218 -0.491,0.491l0,6.67c0,0.273 0.218,0.491 0.491,0.491l2.953,-0c0.153,-0 0.273,-0.055 0.361,-0.153l0.917,-0.906c0.098,-0.099 0.142,-0.219 0.142,-0.361l-0,-1.477c-0,-0.142 -0.044,-0.252 -0.142,-0.35l-0.218,-0.219c-0.099,-0.098 -0.142,-0.218 -0.142,-0.36c-0,-0.142 0.043,-0.251 0.142,-0.349l0.218,-0.219Z" style="fill-rule:nonzero;"/></g></g></g><defs><image id="_Image3" width="12px" height="12px" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAlklEQVQokYWRsQ7DIBBD7dNNFVLzd/n/OUOXqLQDuEtIyZE2t2GZdzZwnmdJQhuS6M/9kIRLAsld/GeWBIvmuC2CLFKvtlkUf3XYNzRiE/pOJAeARUIzNa3Weoi6RzKz09x9JwDwnPNQtI8Ue/iyLKdZzz6TJDyl9KVK0GYopRz6tEvu7sNzrs8V79sLNMAejuk+YePhAzm6bPMLck8IAAAAAElFTkSuQmCC"/><linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.17294e-14,-191.556,255.499,1.56448e-14,-17.004,47.3421)"><stop offset="0" style="stop-color:#000;stop-opacity:1"/><stop offset="1" style="stop-color:#000;stop-opacity:0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 68 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-miterlimit:1;"><rect id="ico-cd" x="0" y="0" width="68" height="68" style="fill:none;"/><path d="M48.541,13.761c11.171,8.025 13.724,23.61 5.698,34.78c-8.025,11.171 -23.61,13.724 -34.78,5.698c-11.171,-8.025 -13.724,-23.61 -5.698,-34.78c8.025,-11.171 23.61,-13.724 34.78,-5.698Zm-1.167,1.624c-10.274,-7.382 -24.608,-5.033 -31.989,5.241c-7.382,10.274 -5.033,24.608 5.241,31.989c10.274,7.382 24.608,5.033 31.989,-5.241c7.382,-10.273 5.033,-24.608 -5.241,-31.989Z" style="fill:#5c5c5c;"/><path d="M47.123,15.735l-2.838,3.949" style="fill:none;stroke:#5c5c5c;stroke-width:2px;"/><circle cx="34" cy="34" r="31" style="fill:none;stroke:#fff;stroke-width:2px;stroke-dasharray:0.8,18,0,0,0,0;"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 68 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-miterlimit:1;"><rect id="ico-fd" x="0" y="0" width="68" height="68" style="fill:none;"/><g><circle cx="34" cy="34" r="31" style="fill:none;stroke:#fff;stroke-width:2px;stroke-dasharray:0.8,8,0,0,0,0;"/><g><path d="M58.681,30.542c1.908,13.622 -7.602,26.23 -21.223,28.139c-13.622,1.908 -26.23,-7.602 -28.139,-21.223c-1.908,-13.622 7.602,-26.23 21.223,-28.139c13.622,-1.908 26.23,7.602 28.139,21.223Zm-1.981,0.278c-1.755,-12.529 -13.352,-21.275 -25.88,-19.52c-12.529,1.755 -21.275,13.352 -19.52,25.88c1.755,12.529 13.352,21.275 25.88,19.52c12.529,-1.755 21.275,-13.352 19.52,-25.88Z" style="fill:#5c5c5c;"/><path d="M56.273,30.88l-4.816,0.674" style="fill:none;stroke:#5c5c5c;stroke-width:2px;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 68 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-miterlimit:1;"><rect id="ico-rt" x="0" y="0" width="68" height="68" style="fill:none;"/><g><g><path d="M34,9.09c13.78,-0 24.968,11.188 24.968,24.968c-0,13.78 -11.188,24.968 -24.968,24.968c-13.78,-0 -24.968,-11.188 -24.968,-24.968c0,-13.78 11.188,-24.968 24.968,-24.968Zm-0,2c-12.676,-0 -22.968,10.291 -22.968,22.968c0,12.676 10.292,22.968 22.968,22.968c12.676,-0 22.968,-10.292 22.968,-22.968c-0,-12.677 -10.292,-22.968 -22.968,-22.968Z" style="fill:#5c5c5c;"/><path d="M34,11.526l-0,4.871" style="fill:none;stroke:#5c5c5c;stroke-width:2px;"/></g><path d="M34,3c17.141,-0 31.058,13.916 31.058,31.058" style="fill:none;stroke:#fff;stroke-width:2px;"/><path d="M2.942,34.058c0,-17.142 13.917,-31.058 31.058,-31.058" style="fill:none;stroke:#fff;stroke-width:2px;"/><circle cx="34" cy="3" r="3" style="fill:#fff;"/><g><path d="M34,65.724c-17.141,0 -31.058,-13.916 -31.058,-31.057" style="fill:none;stroke:#fff;stroke-width:2px;stroke-dasharray:0.8,4,0,0,0,0;"/><path d="M65.058,34.667c-0,17.141 -13.917,31.057 -31.058,31.057" style="fill:none;stroke:#fff;stroke-width:2px;stroke-dasharray:0.8,4,0,0,0,0;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 68 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-miterlimit:1;"><rect id="ico-vf" x="0" y="0" width="68" height="68" style="fill:none;"/><g><g><path d="M48.522,54.31c-11.209,8.015 -26.817,5.422 -34.832,-5.788c-8.015,-11.209 -5.422,-26.817 5.788,-34.832c11.209,-8.015 26.817,-5.422 34.832,5.788c8.015,11.209 5.422,26.817 -5.788,34.832Zm-1.163,-1.627c10.312,-7.373 12.697,-21.731 5.324,-32.042c-7.373,-10.312 -21.731,-12.697 -32.042,-5.324c-10.312,7.373 -12.697,21.731 -5.324,32.042c7.373,10.312 21.731,12.697 32.042,5.324Z" style="fill:#5c5c5c;"/><path d="M47.106,52.329l-2.834,-3.963" style="fill:none;stroke:#5c5c5c;stroke-width:2px;"/></g><path d="M37.221,0.181c-1.651,-0.127 -3.094,1.11 -3.221,2.761c-0.127,1.651 1.111,3.094 2.762,3.221c1.651,0.127 3.094,-1.111 3.22,-2.762c0.127,-1.651 -1.11,-3.094 -2.761,-3.22Z" style="fill:#fff;"/><path d="M27.788,0.41l6.212,2.532l-5.753,3.45l-0.459,-5.982Z" style="fill:#fff;"/><path d="M38.709,3.804c-0.272,-0.043 -0.459,-0.298 -0.417,-0.571c0.042,-0.273 0.298,-0.46 0.57,-0.418c7.605,1.179 14.315,5.075 19.106,10.665c4.731,5.519 7.59,12.688 7.59,20.52c-0,8.709 -3.535,16.598 -9.248,22.31c-5.712,5.713 -13.601,9.248 -22.31,9.248c-8.709,-0 -16.598,-3.535 -22.31,-9.248c-5.713,-5.712 -9.248,-13.601 -9.248,-22.31c0,-7.832 2.859,-15.001 7.59,-20.52c4.791,-5.59 11.501,-9.486 19.106,-10.665c0.272,-0.042 0.528,0.145 0.57,0.418c0.042,0.273 -0.145,0.528 -0.417,0.571c-7.363,1.14 -13.861,4.914 -18.5,10.327c-4.58,5.344 -7.349,12.286 -7.349,19.869c0,8.433 3.423,16.072 8.955,21.603c5.531,5.532 13.17,8.955 21.603,8.955c8.433,-0 16.072,-3.423 21.603,-8.955c5.532,-5.531 8.955,-13.17 8.955,-21.603c-0,-7.583 -2.769,-14.525 -7.349,-19.869c-4.639,-5.413 -11.137,-9.187 -18.5,-10.327Z" style="fill:#fff;"/></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 136 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-384.443,-1)">
<g id="ico-key" transform="matrix(2.10919,0,0,1.0625,384.443,1)">
<rect x="0" y="0" width="64.48" height="64" style="fill:none;"/>
<g transform="matrix(2.11336,0,0,4.19528,6.87951,-18.3454)">
<path d="M21.775,7.517L24,7.517L24,16.483L21.775,16.483L21.775,7.517ZM13.213,7.517L19.719,7.517C20.379,7.517 20.764,8.087 20.764,8.764L20.764,15.371C20.764,16.211 20.414,16.483 19.652,16.483L13.213,16.483L13.213,10.787L15.438,10.787L15.438,14.292L18.573,14.292L18.573,9.54L13.213,9.54L13.213,7.517ZM9.978,7.517L12.168,7.517L12.168,16.483L9.978,16.483L9.978,7.517ZM0,7.517L7.854,7.517C8.514,7.517 8.899,8.087 8.899,8.764L8.899,16.484L6.708,16.484L6.708,9.774L5.427,9.774L5.427,16.482L3.438,16.482L3.438,9.775L2.191,9.775L2.191,16.483L0,16.483L0,7.517Z" style="fill:rgb(235,235,235);fill-rule:nonzero;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 136 68" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-534.698,-1)">
<g id="ico-usb" transform="matrix(2.10919,0,0,1.0625,534.698,1)">
<rect x="0" y="0" width="64.48" height="64" style="fill:none;"/>
<g transform="matrix(0.0491017,0,0,0.0974729,14.909,6.75176)">
<path d="M641.5,256C641.5,259.1 639.8,262.1 637,263.5L547.9,317C546.5,317.8 545.1,318.4 543.4,318.4C542,318.4 540.3,318.1 538.9,317.3C536.1,315.6 534.4,312.8 534.4,309.5L534.4,273.9L295.7,273.9C321,313.5 336.2,380.8 365.3,380.8L392,380.8L392,354C392,349 395.9,345.1 400.9,345.1L490,345.1C495,345.1 498.9,349 498.9,354L498.9,443.1C498.9,448.1 495,452 490,452L400.9,452C395.9,452 392,448.1 392,443.1L392,416.4L365.3,416.4C289.9,416.4 284.2,273.9 240.6,273.9L140.3,273.9C132.2,304.5 104.4,327.4 71.3,327.4C32,327.3 0,295.3 0,256C0,216.7 32,184.7 71.3,184.7C104.4,184.7 132.3,207.5 140.3,238.2C179.4,238.2 184.2,247.7 214.9,177.8C255,88.7 273,95.7 323.8,95.7C331.3,74.8 350.8,60.1 374.2,60.1C403.7,60.1 427.7,84 427.7,113.6C427.7,143.2 403.8,167.1 374.2,167.1C350.8,167.1 331.3,152.3 323.8,131.5L294,131.5C264.9,131.5 249.7,198.9 224.4,238.4L534.5,238.4L534.5,202.8C534.5,199.5 536.2,196.7 539,195C541.8,193.3 545.4,193.6 547.9,195.3L637,248.8C639.8,249.9 641.5,252.9 641.5,256Z" style="fill:rgb(235,235,235);fill-rule:nonzero;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,151 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
@font-face {
font-family: 'ProtoMono';
src: url(fonts/ProtoMono-Regular.ttf), local('monospace');
}
@font-face {
font-family: 'RenoMono';
src: url(fonts/RenoMono.otf), local('monospace');
}
@font-face {
font-family: 'JetBrainsMono';
src: url(fonts/JetBrainsMono[wght].ttf), local('monospace');
}
@font-face {
font-family: 'PixelLg';
src: url(fonts/SG12.ttf), local('monospace');
}
@font-face {
font-family: 'PixelSm';
src: url(fonts/andina.ttf), local('monospace');
}
.font-default {
font-family: 'JetBrainsMono', monospace, sans-serif;
}
.font-pixellg {
font-family: 'PixelLg', monospace, sans-serif;
}
.font-pixelsm {
font-family: 'PixelSm', monospace, sans-serif;
}
.font-size-gui-extra {
font-size: 48px;
}
.font-heading {
font-family: 'ProtoMono', monospace, sans-serif;
}
h1, h2, h3, h4, h5, h6 {
@apply font-heading;
}
body {
@apply font-default bg-background text-foreground;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
::-webkit-scrollbar {
width: 0.5em;
height: 0.5em;
}
::-webkit-scrollbar-thumb { /* Foreground */
background: var(--scrollbar-foreground);
}
::-webkit-scrollbar-track { /* Background */
background: var(--scrollbar-background);
}
::selection {
background: #c66936;
color: black;
text-shadow: none;
}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 301 301" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;">
<g transform="matrix(1,0,0,1,-406.435,-188.7)">
<g transform="matrix(1,0,0,1,272.791,-68.96)">
<g transform="matrix(1.76453,0,0,1.76453,-791.267,-245.177)">
<g transform="matrix(0.12,0,0,0.12,0,-54.8614)">
<path d="M4811.28,4001.73C5065.82,4147.93 5390.68,4060.11 5536.88,3805.57C5621.18,3658.82 5630.54,3480.7 5562.1,3325.91" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:2.83px;"/>
</g>
<g transform="matrix(-0.0800815,-0.0893697,-0.0893697,0.0800815,1472.57,474.872)">
<path d="M5288.11,4270.32C5394.91,4235.59 5509.97,4235.59 5616.77,4270.32" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:2.83px;"/>
</g>
<g transform="matrix(0,-0.12,-0.12,0,1081.2,979.52)">
<path d="M5078,3228.5C4689.47,3228.5 4374.5,3543.47 4374.5,3932C4374.5,4320.53 4689.47,4635.5 5078,4635.5C5466.53,4635.5 5781.5,4320.53 5781.5,3932C5781.5,3543.47 5466.53,3228.5 5078,3228.5" style="fill:none;fill-rule:nonzero;stroke:rgb(92,92,92);stroke-width:2.83px;"/>
</g>
<g transform="matrix(0.12,0,0,0.12,0,-119.458)">
<path d="M5342.77,3619.45C5088.89,3472.12 4763.64,3558.49 4616.3,3812.38C4468.97,4066.26 4555.34,4391.52 4809.23,4538.85C4809.91,4539.25 4810.59,4539.64 4811.28,4540.03" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:2.83px;"/>
</g>
<g transform="matrix(0.12,0,0,0.12,0,-84.1415)">
<path d="M4811.23,4245.55C5065.12,4392.88 5390.37,4306.5 5537.7,4052.62C5685.03,3798.73 5598.66,3473.48 5344.77,3326.14C5344.09,3325.75 5343.41,3325.36 5342.73,3324.97" style="fill:none;fill-rule:nonzero;stroke:white;stroke-width:2.83px;"/>
</g>
<g transform="matrix(0,-0.12,-0.12,0,1081.08,979.16)">
<path d="M5076,3420.5C4792.95,3420.5 4563.5,3649.95 4563.5,3933C4563.5,4216.05 4792.95,4445.5 5076,4445.5C5359.05,4445.5 5588.5,4216.05 5588.5,3933C5588.5,3649.95 5359.05,3420.5 5076,3420.5" style="fill-rule:nonzero;stroke:black;stroke-width:2.83px;"/>
</g>
<g transform="matrix(0,-0.12,-0.12,0,1081.2,979.52)">
<path d="M5078,3332C4746.63,3332 4478,3600.63 4478,3932C4478,4263.37 4746.63,4532 5078,4532C5409.37,4532 5678,4263.37 5678,3932C5678,3600.63 5409.37,3332 5078,3332" style="fill:none;fill-rule:nonzero;stroke:rgb(92,92,92);stroke-width:2.83px;"/>
</g>
<g transform="matrix(0,-0.12,-0.12,0,1081.2,979.52)">
<path d="M5078,3374.5C4770.1,3374.5 4520.5,3624.1 4520.5,3932C4520.5,4239.9 4770.1,4489.5 5078,4489.5C5385.9,4489.5 5635.5,4239.9 5635.5,3932C5635.5,3624.1 5385.9,3374.5 5078,3374.5" style="fill:none;fill-rule:nonzero;stroke:rgb(92,92,92);stroke-width:2.83px;"/>
</g>
<g transform="matrix(0,-0.12,-0.12,0,1081.2,979.52)">
<path d="M5078,3318C4738.9,3318 4464,3592.9 4464,3932C4464,4271.1 4738.9,4546 5078,4546C5417.1,4546 5692,4271.1 5692,3932C5692,3592.9 5417.1,3318 5078,3318" style="fill:none;fill-rule:nonzero;stroke:rgb(92,92,92);stroke-width:2.83px;"/>
</g>
<g transform="matrix(0,-0.12,-0.12,0,1081.2,979.52)">
<path d="M5078,3223.5C4686.71,3223.5 4369.5,3540.71 4369.5,3932C4369.5,4323.29 4686.71,4640.5 5078,4640.5C5469.29,4640.5 5786.5,4323.29 5786.5,3932C5786.5,3540.71 5469.29,3223.5 5078,3223.5" style="fill:none;fill-rule:nonzero;stroke:rgb(92,92,92);stroke-width:2.83px;"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

View File

@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1422 800" opacity="0.3">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="oooscillate-grad">
<stop stop-color="hsl(206, 75%, 49%)" stop-opacity="1" offset="0%"></stop>
<stop stop-color="hsl(331, 90%, 56%)" stop-opacity="1" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke-width="1" stroke="url(#oooscillate-grad)" fill="none" stroke-linecap="round">
<path d="M 0 448 Q 355.5 -100 711 400 Q 1066.5 900 1422 448" opacity="0.05"></path>
<path d="M 0 420 Q 355.5 -100 711 400 Q 1066.5 900 1422 420" opacity="0.11"></path>
<path d="M 0 392 Q 355.5 -100 711 400 Q 1066.5 900 1422 392" opacity="0.18"></path>
<path d="M 0 364 Q 355.5 -100 711 400 Q 1066.5 900 1422 364" opacity="0.24"></path>
<path d="M 0 336 Q 355.5 -100 711 400 Q 1066.5 900 1422 336" opacity="0.30"></path>
<path d="M 0 308 Q 355.5 -100 711 400 Q 1066.5 900 1422 308" opacity="0.37"></path>
<path d="M 0 280 Q 355.5 -100 711 400 Q 1066.5 900 1422 280" opacity="0.43"></path>
<path d="M 0 252 Q 355.5 -100 711 400 Q 1066.5 900 1422 252" opacity="0.49"></path>
<path d="M 0 224 Q 355.5 -100 711 400 Q 1066.5 900 1422 224" opacity="0.56"></path>
<path d="M 0 196 Q 355.5 -100 711 400 Q 1066.5 900 1422 196" opacity="0.62"></path>
<path d="M 0 168 Q 355.5 -100 711 400 Q 1066.5 900 1422 168" opacity="0.68"></path>
<path d="M 0 140 Q 355.5 -100 711 400 Q 1066.5 900 1422 140" opacity="0.75"></path>
<path d="M 0 112 Q 355.5 -100 711 400 Q 1066.5 900 1422 112" opacity="0.81"></path>
<path d="M 0 84 Q 355.5 -100 711 400 Q 1066.5 900 1422 84" opacity="0.87"></path>
<path d="M 0 56 Q 355.5 -100 711 400 Q 1066.5 900 1422 56" opacity="0.94"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { reactive } from 'vue'
const versions = reactive({ ...window.electron.process.versions })
</script>
<template>
<ul class="versions">
<li class="electron-version">Electron v{{ versions.electron }}</li>
<li class="chrome-version">Chromium v{{ versions.chrome }}</li>
<li class="node-version">Node v{{ versions.node }}</li>
</ul>
</template>

View File

@@ -0,0 +1,20 @@
<template>
<div class="bg-wip w-full text-center p-8 text-zinc-600">
<span class="bg-black font-heading p-1">WORK IN PROGRESS</span>
</div>
</template>
<style scoped>
.bg-wip {
--stripe-color-a: rgb(82 82 91);
--stripe-color-b: black;
--stripe-width: 1em;
background: repeating-linear-gradient(
45deg,
var(--stripe-color-a),
var(--stripe-color-a) var(--stripe-width),
var(--stripe-color-b) var(--stripe-width),
var(--stripe-color-b) calc(var(--stripe-width) * 2)
);
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<Collapsible v-model:open="collapse" :default-open="true">
<div class="w-full bg-zinc-900 h-12 flex">
<div
class="flex-1 flex items-center px-4"
:class="{'cursor-pointer hover:bg-zinc-800': showToggle}"
@click="toggle = !toggle">
<component :is="iconComponent" v-if="iconComponent" class="h-4 w-4 mr-2" />
<h2 class="text-sm py-4">{{ title }}<slot name="title"/></h2>
<Switch
v-if="showToggle" :checked="toggle"
class="ml-auto" @click.stop="toggle=!toggle" />
</div>
<CollapsibleTrigger v-if="foldable" class="flex items-center justify-center h-12 aspect-square hover:bg-zinc-800">
<ChevronLeft class="chevrot h-4 w-4 mt-0.5 transition-transform text-muted-foreground" />
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<slot />
</CollapsibleContent>
</Collapsible>
</template>
<script setup>
import { ChevronLeft } from 'lucide-vue-next'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@renderer/components/ui/collapsible'
import { ref } from 'vue'
import { Switch } from '@renderer/components/ui/switch'
const collapse = ref(true)
const toggle = defineModel({
type: Boolean,
default: true,
})
defineProps({
title: {
type: String,
default: 'MISSING_TITLE',
},
iconComponent: {
type: [String, Object, Function],
default: undefined,
},
showToggle: {
type: Boolean,
default: false,
},
foldable: {
type: Boolean,
default: true,
},
})
</script>
<style scoped>
[data-state=open] > .chevrot {
transform: rotate(-90deg);
}
</style>

View File

@@ -0,0 +1,327 @@
<template>
<div
class="mx-2 flex p-4 font-heading rounded-b-lg border-x border-b border-zinc-800"
:class="{'rounded-t-lg': roundedTop}"
:style="{backgroundColor: color.hex()}">
<div
ref="colorFieldText" class="w-full flex opacity-70"
:class="!isDark(color) ? 'text-black selection:bg-black selection:text-white' : 'selection:bg-white selection:text-black'"
style="transition: color 0.2s ease-in-out">
<div>
<form @submit.prevent="onSubmitHueInput">
<label for="hueInput">H: </label><input
id="hueInput"
v-model="hueInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
</form>
<form @submit.prevent="onSubmitSaturationInput">
<label for="saturationInput">S: </label><input
id="saturationInput"
v-model="saturationInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
</form>
<form @submit.prevent="onSubmitValueInput">
<label for="valueInput">V: </label><input
id="valueInput"
v-model="valueInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
</form>
</div>
<div class="mx-auto">
<form @submit.prevent="onSubmitHexInput">
<label for="hexInput">#</label><input
id="hexInput"
v-model="hexInput" maxlength="6"
onfocus="this.select()"
class="w-16 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
</form>
</div>
<div>
<form @submit.prevent="onSubmitRGBInput">
<label for="rInput">R: </label><input
id="rInput"
v-model="rInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
</form>
<form @submit.prevent="onSubmitRGBInput">
<label for="gInput">G: </label><input
id="gInput"
v-model="gInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
</form>
<form @submit.prevent="onSubmitRGBInput">
<label for="bInput">B: </label><input
id="bInput"
v-model="bInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
</form>
</div>
</div>
</div>
<div class="flex py-4 px-8">
<SliderRoot
v-model="hueSliderModel" :max="359"
class="relative flex w-full touch-none select-none items-center h-10">
<SliderTrack
class="relative h-2.5 w-full grow overflow-hidden rounded-full border-2 border-zinc-900"
style="background: linear-gradient(90deg, rgba(255, 0, 0, 1) 0%, rgba(255, 154, 0, 1) 10%, rgba(208, 222, 33, 1) 20%, rgba(79, 220, 74, 1) 30%, rgba(63, 218, 216, 1) 40%, rgba(47, 201, 226, 1) 50%, rgba(28, 127, 238, 1) 60%, rgba(95, 21, 242, 1) 70%, rgba(186, 12, 248, 1) 80%, rgba(251, 7, 217, 1) 90%, rgba(255, 0, 0, 1) 100%)" />
<SliderThumb
class="flex h-6 w-8 rounded-[8px] hover:bg-zinc-200 border border-zinc-100 bg-zinc-300 focus-visible:outline-none focus-visible:ring-1 cursor-pointer focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 text-zinc-600 justify-center items-center"
style="box-shadow: -3px 0 15px 0 rgba(0,0,0,0.6)">
<MoreHorizontal class="h-full" />
</SliderThumb>
</SliderRoot>
</div>
<Separator />
<div class="flex py-4 px-8">
<SliderRoot
v-model="saturationSliderModel" :max="100"
class="relative flex w-full touch-none select-none items-center h-10">
<SliderTrack
class="relative h-2.5 w-full grow overflow-hidden rounded-full border-2 border-zinc-900"
:style="{background: `linear-gradient(90deg, hsl(0, 0%, ${saturationSliderColor.lightness()}%) 0%, hsl(${saturationSliderColor.hue()}, 100%, ${saturationSliderColor.lightness()}%) 100%)`}" />
<SliderThumb
class="flex h-6 w-8 rounded-[8px] hover:bg-zinc-200 border border-zinc-100 bg-zinc-300 focus-visible:outline-none focus-visible:ring-1 cursor-pointer focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 text-zinc-600 justify-center items-center"
style="box-shadow: -3px 0 15px 0 rgba(0,0,0,0.6)">
<MoreHorizontal class="h-full" />
</SliderThumb>
</SliderRoot>
</div>
<Separator />
<div class="flex py-4 px-8">
<SliderRoot
v-model="valueSliderModel" :max="100"
class="relative flex w-full touch-none select-none items-center h-10">
<SliderTrack
class="relative h-2.5 w-full grow overflow-hidden rounded-full border-2 border-zinc-900"
:style="{background: `linear-gradient(90deg, black, ${valueSliderColor.hex()} 100%`}" />
<SliderThumb
class="flex h-6 w-8 rounded-[8px] hover:bg-zinc-200 border border-zinc-100 bg-zinc-300 focus-visible:outline-none focus-visible:ring-1 cursor-pointer focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 text-zinc-600 justify-center items-center"
style="box-shadow: -3px 0 15px 0 rgba(0,0,0,0.6)">
<MoreHorizontal class="h-full" />
</SliderThumb>
</SliderRoot>
</div>
<Separator />
</template>
<script setup>
import { computed, onBeforeMount, ref, watch } from 'vue'
import Color from 'color'
import { SliderRoot, SliderThumb, SliderTrack } from 'radix-vue'
import { MoreHorizontal } from 'lucide-vue-next'
import { Separator } from '@renderer/components/ui/separator'
defineProps({
roundedTop: {
type: Boolean,
default: false,
},
})
const hueSliderValue = ref(0)
const saturationSliderValue = ref(100)
const valueSliderValue = ref(50)
const hueSliderModel = computed({
get() {
return [hueSliderValue.value]
},
set(hue) {
hueSliderValue.value = hue[0]
color.value = color.value.hue(hue[0])
},
})
const saturationSliderModel = computed({
get() {
return [saturationSliderValue.value]
},
set(saturation) {
saturationSliderValue.value = saturation[0]
color.value = color.value.saturationv(saturation[0])
},
})
const valueSliderModel = computed({
get() {
return [valueSliderValue.value]
},
set(value) {
valueSliderValue.value = value[0]
color.value = color.value.value(value[0])
},
})
const color = defineModel({
type: Color,
default: () => Color.rgb(255, 0, 0),
})
const saturationSliderColor = computed(() => {
return Color.hsv(hueSliderModel.value[0], 100, valueSliderModel.value[0])
})
const valueSliderColor = computed(() => {
return Color.hsv(hueSliderModel.value[0], saturationSliderModel.value[0], 100)
})
const hexInput = ref('FF0000')
const hueInput = ref('000')
const saturationInput = ref('100')
const valueInput = ref('050')
const rInput = ref('255')
const gInput = ref('000')
const bInput = ref('000')
function onSubmitHexInput() {
let input = hexInput.value
if (input[0] !== '#') {
input = '#' + input
}
if (input.match(/^#[0-9A-F]{6}$/i)) {
color.value = Color(input)
} else
shake()
}
function onSubmitHueInput() {
const input = parseInt(hueInput.value)
if (isNaN(input)) {
shake()
return
}
const newHue = Math.max(0, Math.min(input, 360))
if (newHue === color.value.hue()) {
updateInputs()
}
color.value = color.value.hue(newHue)
}
function onSubmitSaturationInput() {
const input = parseInt(saturationInput.value)
if (isNaN(input)) {
shake()
return
}
const newSaturation = Math.max(0, Math.min(input, 100))
if (newSaturation === color.value.saturationv()) {
updateInputs()
}
color.value = color.value.saturationv(newSaturation)
}
function onSubmitValueInput() {
const input = parseInt(valueInput.value)
if (isNaN(input)) {
shake()
return
}
const newValue = Math.max(0, Math.min(input, 100))
if (newValue === color.value.value()) {
updateInputs()
}
color.value = color.value.value(newValue)
}
function onSubmitRGBInput() {
const r = parseInt(rInput.value)
const g = parseInt(gInput.value)
const b = parseInt(bInput.value)
if (isNaN(r) || isNaN(g) || isNaN(b)) {
shake()
return
}
const newColor = Color.rgb(r, g, b)
if (newColor.hex() === color.value.hex()) {
updateInputs()
}
color.value = newColor
}
function updateInputs() {
hexInput.value = color.value.hex().substring(1, 7)
hueInput.value = String(parseInt(color.value.hue())).padStart(3, '0')
saturationInput.value = String(parseInt(color.value.saturationv())).padStart(3, '0')
valueInput.value = String(parseInt(color.value.value())).padStart(3, '0')
rInput.value = String(parseInt(color.value.red())).padStart(3, '0')
gInput.value = String(parseInt(color.value.green())).padStart(3, '0')
bInput.value = String(parseInt(color.value.blue())).padStart(3, '0')
hueSliderValue.value = color.value.hue()
saturationSliderValue.value = color.value.saturationv()
valueSliderValue.value = color.value.value()
}
watch(color, updateInputs)
onBeforeMount(updateInputs)
const colorFieldText = ref(null)
function shake() {
colorFieldText.value.classList.remove('shake')
setTimeout(() => {
colorFieldText.value.classList.add('shake')
}, 5)
}
function isDark(color) {
// YIQ equation from http://24ways.org/2010/calculating-color-contrast
const rgb = color.rgb().color
const yiq = (rgb[0] * 6126 + rgb[1] * 7152 + rgb[2] * 722) / 10000 // Changed r factor from 2126
return yiq < 128
}
</script>
<style scoped>
.shake {
animation-name: shake;
animation-fill-mode: forwards;
animation-duration: 250ms;
animation-timing-function: ease-in-out;
}
@keyframes shake {
0% {
transform: translateX(0);
}
15% {
transform: translateX(0.1rem);
}
30% {
transform: translateX(-0.1rem);
}
45% {
transform: translateX(0.1rem);
}
60% {
transform: translateX(-0.1rem);
}
75% {
transform: translateX(0.1rem);
}
90% {
transform: translateX(-0.1rem);
}
100% {
transform: translateX(0);
}
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<div
class="pt-2"
:style="{background: `linear-gradient(180deg, ${options[currentOption].color.hex()+'11'}, ${options[currentOption].color.hex()+'30'} 25%, ${options[currentOption].color.hex()+'30'} 40%, transparent 60%`}">
<div
class="mx-2 flex font-heading rounded-t-lg overflow-hidden border-t border-x border-zinc-800 bg-zinc-900">
<button
v-for="(option, key) in options" :key="key"
class="flex-1 py-2 items-center text-center rounded-t-lg min-w-0 transition-colors"
:class="currentOption!==key ? 'hover:bg-zinc-800 text-muted-foreground mx-[1px]' : 'text-black bg-zinc-300 hover:bg-zinc-200 border-x border-t border-zinc-100'"
@click="currentOption = key">
{{ $t(option.titleKey) }}
</button>
</div>
<div class="mx-2 flex border-x border-zinc-800 overflow-hidden">
<button
v-for="(option, key) in options" :key="key" class="flex-1 h-6"
:class="{ 'color-tab': currentOption === key}"
:style="{background: option.color.hex()}" @click="currentOption = key" />
</div>
<HSVInput v-model="options[currentOption].color" />
</div>
</template>
<script setup>
import HSVInput from '@renderer/components/common/HSVInput.vue'
import Color from 'color'
import { computed, onBeforeMount, reactive, ref } from 'vue'
const currentOption = ref(null)
const currentColorHex = computed(() => options[currentOption.value].color.hex())
const model = defineModel({
type: Object,
default: () => ({
one: {
titleKey: 'One',
color: Color('#ff0000'),
},
two: {
titleKey: 'Two',
color: Color('#00ff00'),
},
three: {
titleKey: 'Three',
color: Color('#0000ff'),
},
}),
})
const options = reactive(model.value)
onBeforeMount(() => {
if (currentOption.value === null)
currentOption.value = Object.keys(options)[0]
})
</script>
<style scoped>
.color-tab {
position: relative;
--rounded: var(--radius);
}
.color-tab:before,
.color-tab:after {
position: absolute;
bottom: -1px;
width: var(--rounded);
height: var(--rounded);
content: " ";
}
.color-tab:before {
left: calc(var(--rounded) * -1);
border-bottom-right-radius: var(--rounded);
border-width: 0 1px 1px 0;
box-shadow: calc(var(--rounded) / 4) calc(var(--rounded) / 4) 0 v-bind('currentColorHex');
z-index: 1;
}
.color-tab:after {
right: calc(var(--rounded) * -1);
border-bottom-left-radius: var(--rounded);
border-width: 0 0 1px 1px;
box-shadow: calc(var(--rounded) / 4 * -1) calc(var(--rounded) / 4) 0 v-bind('currentColorHex');
z-index: 1;
}
.color-tab:after, .color-tab:before {
border: none;
}
</style>

View File

@@ -0,0 +1,142 @@
<script setup>
import { onMounted, ref, watch } from 'vue'
import click from '@renderer/assets/sounds/click.mp3'
function playClick() {
const audio = new Audio(click)
audio.volume = 0.01 * (1 + Math.random() * 0.75 - 0.375)
audio.play()
}
const props = defineProps({
text: {
type: String,
default: '',
},
characterSet: {
type: String,
default: 'x01_-/',
},
scrambleOnHover: {
type: Boolean,
default: false,
},
fillInterval: {
type: Number,
default: 0,
},
scrambleAmount: {
type: Number,
default: 1,
},
replaceInterval: {
type: Number,
default: 15,
},
scrambleOnMount: {
type: Boolean,
default: false,
},
resize: {
type: Boolean,
default: true,
},
delay: {
type: Number,
default: 0,
},
})
const content = ref('')
function randomCharacter(characterSet = props.characterSet) {
return props.characterSet.charAt(Math.floor(Math.random() * characterSet.length))
}
function replaceContent(text = props.text, replaceInterval = props.replaceInterval, steps = 0) {
if (steps > text.length + 16) {
content.value = text
}
if (content.value !== text) {
// get all the indices of characters that don't match text
const indices = []
for (let i = 0; i < text.length; i++) {
if (content.value.charAt(i) !== text.charAt(i)) {
indices.push(i)
}
}
if (indices.length > 0) {
const index = indices[Math.floor(Math.random() * indices.length)]
content.value = content.value.substring(0, index) + text.charAt(index) + content.value.substring(index + 1)
} else if (content.value.length < text.length) {
content.value += text.charAt(content.value.length)
} else {
content.value = content.value.substring(0, content.value.length - 1)
}
//playClick()
setTimeout(() => {
replaceContent(text, replaceInterval, steps + 1)
}, replaceInterval * (1 + Math.random()))
} else {
emit('finish')
}
}
function scramble(scrambleAmount = props.scrambleAmount, replaceInterval = props.replaceInterval, fillInterval = props.fillInterval, characterSet = props.characterSet, text = props.text, fillText = props.text) {
content.value = ''
const spec = props.resize && (Math.random() > 0.99)
let i = 0
const specChars = atob('S09TUk8tRUFTVEVSRUdH')
const fillContent = function() {
if (content.value.length < text.length) {
const char = fillText.charAt(content.value.length) || ''
if (spec) {
content.value += specChars[i]
i = (i + 1) % specChars.length
} else {
if (char === ' ' || Math.random() > scrambleAmount) {
content.value += char
} else {
content.value += randomCharacter(characterSet)
}
}
if (fillInterval > 0) {
//playClick()
setTimeout(fillContent, fillInterval)
} else fillContent()
} else {
setTimeout(() => {
replaceContent(text, replaceInterval, 0)
}, spec * 500)
}
}
fillContent()
}
defineExpose({ scramble })
const emit = defineEmits(['finish'])
onMounted(() => {
if (props.scrambleOnMount) {
setTimeout(() => {
scramble()
}, props.delay)
} else {
content.value = props.text
}
})
watch(() => props.text, () => {
if (content.value === '') {
scramble()
} else {
replaceContent()
}
})
</script>
<template>
<span @mouseenter="scrambleOnHover && scramble">{{ content }}</span>
</template>

View File

@@ -0,0 +1,110 @@
<template>
<div class="flex flex-col px-8 my-4">
<span class="text-sm text-muted-foreground font-mono">{{ label }}</span>
<Slider
ref="steppedSlider" v-model="sliderModelValue" :max="max" :step="1"
class="pt-4" />
<div class="flex justify-between py-2">
<button
v-for="(position, index) in positions" :key="position"
class="min-w-0 text-nowrap group"
:class="{
'slider-start mr-4': index===0,
'slider-center': index > 0 && index < positions.length-1,
'slider-end ml-4': index === positions.length-1}"
@click="value = position.value">
<span
v-if="index===0" class="rounded-full w-2 h-1.5 inline-block mb-[1px] transition-colors"
:class="value===position.value ? 'bg-zinc-100' : 'bg-zinc-600 group-hover:bg-zinc-500'" />
<span
v-if="position.label" class="text-xs font-mono uppercase mx-1 transition-colors"
:class="value===position.value ? 'text-zinc-100' : 'text-zinc-600 group-hover:text-zinc-500'">{{ position.label }}</span>
<span
v-if="!position.label || index === positions.length-1"
class="rounded-full w-2 h-1.5 inline-block mb-[1px] transition-colors"
:class="value===position.value ? 'bg-zinc-100' : 'bg-zinc-600 group-hover:bg-zinc-500'" />
</button>
</div>
</div>
</template>
<script setup>
import { Slider } from '@renderer/components/ui/slider'
import { computed } from 'vue'
const value = defineModel({
type: Number,
default: 0,
})
const sliderModelValue = computed({
get: () => [value.value],
set: (val) => {
value.value = val[0]
},
})
const props = defineProps({
label: {
type: String,
default: null,
},
max: {
type: Number,
default: 4,
},
namedPositions: {
type: Array,
default: () => [
{
label: 'Min',
value: 0,
},
{
value: 2,
},
{
label: 'Max',
value: 4,
},
],
},
autoMarkers: {
type: Boolean,
default: true,
},
})
const positions = computed(() => {
if (props.autoMarkers) {
const filled = []
for (let i = 0; i <= props.max; i++) {
const position = props.namedPositions.find((p) => p.value === i) || { value: i }
filled.push(position)
}
return filled
}
return props.namedPositions
})
</script>
<style scoped>
.slider-start {
text-align: left;
}
.slider-center {
text-align: center;
}
.slider-end {
text-align: right;
}
.slider-start,
.slider-end {
flex: 0.5;
}
.slider-center {
flex: 1;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div class="p-2 border-solid border-0 border-b">
<div class="flex rounded-lg overflow-hidden border border-zinc-800">
<TransitionGroup name="flex">
<TabSelectButton
v-for="(option, key) in options" :key="key"
:ref="(el) => buttons[key] = el"
:title="$t(option.titleKey)"
:icon="option.icon" :selected="model===key"
class="min-w-0 overflow-hidden"
@select="model=key">
<template v-if="$slots[key]" #replace>
<slot :name="key" />
</template>
</TabSelectButton>
</TransitionGroup>
</div>
</div>
</template>
<script setup>
import { CircleDot } from 'lucide-vue-next'
import TabSelectButton from '@renderer/components/common/TabSelectButton.vue'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const model = defineModel({
type: String,
default: 'a',
})
const buttons = ref({})
const showBackground = ref(false)
const backgroundStyle = ref({
top: '0',
left: '0',
width: '0',
height: '0',
})
const updateBackgroundStyle = () => {
const selected = buttons.value[model.value]
if (selected) {
backgroundStyle.value = {
top: `${selected.$el.offsetTop}px`,
left: `${selected.$el.offsetLeft}px`,
width: `${selected.$el.offsetWidth}px`,
height: `${selected.$el.offsetHeight}px`,
}
}
}
watch([model, buttons], () => {
updateBackgroundStyle()
showBackground.value = true
})
let observer = null
onMounted(() => {
observer = new ResizeObserver(updateBackgroundStyle)
observer.observe(buttons.value[model.value].$el)
})
onBeforeUnmount(() => {
observer.disconnect()
})
defineProps({
options: {
type: Object,
default: () => ({
a: { titleKey: 'Option A', icon: CircleDot },
b: { titleKey: 'Option B', icon: CircleDot },
c: { titleKey: 'Option C', icon: CircleDot },
}),
},
})
</script>
<style scoped>
.flex-enter-active,
.flex-leave-active {
transition: flex-grow 75ms;
}
.flex-enter-from,
.flex-leave-to {
flex-grow: 0.000001;
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<button
class="flex-1 flex flex-col items-center rounded-lg p-2 gap-2 font-heading transition-all border"
:class="{'text-black bg-zinc-300 hover:bg-zinc-200 border-zinc-100': selected,
'hover:bg-zinc-800 text-muted-foreground border-transparent' : !selected}"
@click="$emit('select'); $refs.title?.scramble()">
<slot v-if="$slots['replace']" name="replace" />
<template v-else>
<img
v-if="icon"
draggable="false"
:src="icon" :alt="title"
class="h-16"
:class="{'invert': selected}">
<ScrambleText ref="title" :resize="false" class="text-xs text-wrap line-clamp-2 text-ellipsis overflow-hidden" :text="title" />
</template>
</button>
</template>
<script setup>
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
defineEmits(['select'])
defineProps({
title: {
type: String,
default: '',
},
icon: {
type: [String, Object, Function],
default: null,
},
selected: {
type: Boolean,
default: false,
},
})
</script>

View File

@@ -0,0 +1,50 @@
<template>
<div>
<template v-if="store.selectedProfile">
<TabSelect
v-if="showTabs"
v-model="configPage"
:options="configPages"
class="p-2 border solid border-b bg-zinc-900">
<template v-for="(page, key) in configPages" #[key] :key="key">
<ScrambleText ref="title" :text="$t(page.titleKey)" />
</template>
</TabSelect>
<div class="grow overflow-y-auto">
<component :is="store.currentConfigComponent" />
</div>
</template>
<template v-else>
<div class="flex grow justify-center items-center text-muted-foreground pb-16">
<ChevronLeft class="h-5 mb-0.5 inline-block" />
<ScrambleText
scramble-on-mount
:fill-interval="5"
:replace-interval="5"
text="Select a profile first" />
</div>
</template>
</div>
</template>
<script setup>
import { useStore } from '@renderer/store'
import TabSelect from '@renderer/components/common/TabSelect.vue'
import { computed } from 'vue'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { ChevronLeft } from 'lucide-vue-next'
const store = useStore()
const configPages = computed(() => store.currentConfigPages)
const configPage = computed({
get: () => store.currentConfigPage,
set: (value) => store.setCurrentConfigPage(value),
})
defineProps({
showTabs: {
type: Boolean,
default: true,
},
})
</script>

View File

@@ -0,0 +1,80 @@
<template>
<ConfigSection
:title="$t('config_options.feedback_designer.feedback_type.title')"
:icon-component="GaugeCircle">
<TabSelect v-model="feedbackType" :options="feedbackTypeOptions" />
</ConfigSection>
<ConfigSection
:title="$t('config_options.feedback_designer.haptic_response.title')"
:icon-component="AudioWaveform"
:show-toggle="true">
<SteppedSlider
v-model="feedbackStrength"
:label="$t('config_options.feedback_designer.haptic_response.feedback_strength')" />
<Separator />
<SteppedSlider
v-model="bounceBackStrength"
:label="$t('config_options.feedback_designer.haptic_response.bounce_back_strength')" />
<Separator />
<SteppedSlider
v-model="outputRampDampening"
:label="$t('config_options.feedback_designer.haptic_response.output_ramp_dampening')" />
</ConfigSection>
<ConfigSection
:title="$t('config_options.feedback_designer.auditory_response.title')"
:icon-component="AudioLines" :show-toggle="true">
<SteppedSlider
v-model="auditoryHapticLevel"
:label="$t('config_options.feedback_designer.auditory_response.haptic_level')" />
<Separator />
<SteppedSlider
v-model="auditoryMagnitude"
:label="$t('config_options.feedback_designer.auditory_response.magnitude')"
:max="3"
:named-positions="[
{value:0, label: 'Faint'},
{value:1, label: 'Soft'},
{value:2, label: 'Normal'},
{value:3, label: 'Loud'}]" />
</ConfigSection>
</template>
<script setup>
import { AudioLines, AudioWaveform, GaugeCircle } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import { Separator } from '@renderer/components/ui/separator'
import { ref } from 'vue'
import TabSelect from '@renderer/components/common/TabSelect.vue'
import FdIcon from '@renderer/assets/icons/iconFineDetents.svg'
import CdIcon from '@renderer/assets/icons/iconCoarseDetents.svg'
import VfIcon from '@renderer/assets/icons/iconViscousRotation.svg'
import RcIcon from '@renderer/assets/icons/iconReturnToCenter.svg'
import SteppedSlider from '@renderer/components/common/SteppedSlider.vue'
const feedbackType = ref('fineDetents') // TODO: replace with actual value
const feedbackTypeOptions = {
fineDetents: {
icon: FdIcon,
titleKey: 'config_options.feedback_designer.feedback_type.fine_detents',
},
coarseDetents: {
icon: CdIcon,
titleKey: 'config_options.feedback_designer.feedback_type.coarse_detents',
},
viscousRotation: {
icon: VfIcon,
titleKey: 'config_options.feedback_designer.feedback_type.viscous_rotation',
},
returnToCenter: {
icon: RcIcon,
titleKey: 'config_options.feedback_designer.feedback_type.return_to_center',
},
}
const feedbackStrength = ref(2)
const bounceBackStrength = ref(2)
const outputRampDampening = ref(2)
const auditoryHapticLevel = ref(2)
const auditoryMagnitude = ref(2)
</script>

View File

@@ -0,0 +1,23 @@
<template>
<ConfigSection title="Key Colors" :icon-component="Palette">
<PaletteInput v-model="keyColors" />
</ConfigSection>
</template>
<script setup>
import { Palette } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import PaletteInput from '@renderer/components/common/PaletteInput.vue'
import Color from 'color'
import { ref } from 'vue'
const keyColors = ref({
default: {
titleKey: 'default',
color: Color('#4f25ef'),
},
pressed: {
titleKey: 'pressed',
color: Color('#d0078f'),
},
})
</script>

View File

@@ -0,0 +1,81 @@
<template>
<ConfigSection title="Key Mapping" :icon-component="PlusSquare">
<template #title><span class="text-zinc-500"> ({{ store.selectedKey}})</span></template>
<div class="px-8 my-4">
<span class="text-sm text-muted-foreground font-mono">Action:</span>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
ref="comboboxButton"
variant="outline"
role="combobox"
:aria-expanded="open"
class="my-2 w-full justify-between">
<ScrambleText :text="value ? actionOptions[value] : 'Select an action...'" />
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="p-0" :style="{width: $refs.comboboxButton?.$el.offsetWidth}">
<Command>
<CommandInput class="h-9" placeholder="Search actions..." />
<CommandEmpty>
<ScrambleText
scramble-on-mount
text="No actions found." />
</CommandEmpty>
<CommandList>
<CommandGroup>
<CommandItem
v-for="(action, key) in actionOptions"
:key="key"
:value="action"
@select="() => {
value = key
open = false
}">
{{ action }}
<Check
:class="cn('ml-auto h-4 w-4',value === key ? 'opacity-100' : 'opacity-0')" />
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<WIP />
</ConfigSection>
</template>
<script setup>
import { PlusSquare, ChevronsUpDown, Check } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import WIP from '@renderer/components/WIP.vue'
import { Popover, PopoverTrigger, PopoverContent } from '@renderer/components/ui/popover'
import { Button } from '@renderer/components/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@renderer/components/ui/command'
import { ref } from 'vue'
import { cn } from '@renderer/lib/utils'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { useStore } from '@renderer/store'
const store = useStore()
const actionOptions = ref({
sendKey: 'Press Key or Combination',
sendString: 'Type a String',
sendMouse: 'Move, Scroll or Click',
sendGamepad: 'Send a Gamepad Input',
sendMidi: 'Send a MIDI Message',
sendOsc: 'Send an OSC Message',
sendSerial: 'Send a Serial Message',
controlMedia: 'Control Media Playback',
controlSystem: 'Control your OS',
runProgram: 'Start a Program',
})
const comboboxButton = ref(null)
const open = ref(false)
const value = ref('')
</script>

View File

@@ -0,0 +1,80 @@
<template>
<ConfigSection
:title="$t('config_options.feedback_designer.feedback_type.title')"
:icon-component="GaugeCircle">
<TabSelect v-model="feedbackType" :options="feedbackTypeOptions" />
</ConfigSection>
<ConfigSection
:title="$t('config_options.feedback_designer.haptic_response.title')"
:icon-component="AudioWaveform"
:show-toggle="true">
<SteppedSlider
v-model="feedbackStrength"
:label="$t('config_options.feedback_designer.haptic_response.feedback_strength')" />
<Separator />
<SteppedSlider
v-model="bounceBackStrength"
:label="$t('config_options.feedback_designer.haptic_response.bounce_back_strength')" />
<Separator />
<SteppedSlider
v-model="outputRampDampening"
:label="$t('config_options.feedback_designer.haptic_response.output_ramp_dampening')" />
</ConfigSection>
<ConfigSection
:title="$t('config_options.feedback_designer.auditory_response.title')"
:icon-component="AudioLines" :show-toggle="true">
<SteppedSlider
v-model="auditoryHapticLevel"
:label="$t('config_options.feedback_designer.auditory_response.haptic_level')" />
<Separator />
<SteppedSlider
v-model="auditoryMagnitude"
:label="$t('config_options.feedback_designer.auditory_response.magnitude')"
:max="3"
:named-positions="[
{value:0, label: 'Faint'},
{value:1, label: 'Soft'},
{value:2, label: 'Normal'},
{value:3, label: 'Loud'}]" />
</ConfigSection>
</template>
<script setup>
import { AudioLines, AudioWaveform, GaugeCircle } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import { ref } from 'vue'
import TabSelect from '@renderer/components/common/TabSelect.vue'
import FdIcon from '@renderer/assets/icons/iconFineDetents.svg'
import CdIcon from '@renderer/assets/icons/iconCoarseDetents.svg'
import VfIcon from '@renderer/assets/icons/iconViscousRotation.svg'
import RcIcon from '@renderer/assets/icons/iconReturnToCenter.svg'
import SteppedSlider from '@renderer/components/common/SteppedSlider.vue'
import { Separator } from '@renderer/components/ui/separator'
const feedbackType = ref('fineDetents') // TODO: replace with actual value
const feedbackTypeOptions = {
fineDetents: {
icon: FdIcon,
titleKey: 'config_options.feedback_designer.feedback_type.fine_detents',
},
coarseDetents: {
icon: CdIcon,
titleKey: 'config_options.feedback_designer.feedback_type.coarse_detents',
},
viscousRotation: {
icon: VfIcon,
titleKey: 'config_options.feedback_designer.feedback_type.viscous_rotation',
},
returnToCenter: {
icon: RcIcon,
titleKey: 'config_options.feedback_designer.feedback_type.return_to_center',
},
}
const feedbackStrength = ref(2)
const bounceBackStrength = ref(2)
const outputRampDampening = ref(2)
const auditoryHapticLevel = ref(2)
const auditoryMagnitude = ref(2)
</script>

View File

@@ -0,0 +1,27 @@
<template>
<ConfigSection title="Ring Colors" :icon-component="Palette">
<PaletteInput v-model="ringColors" />
</ConfigSection>
</template>
<script setup>
import { Palette } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import PaletteInput from '@renderer/components/common/PaletteInput.vue'
import Color from 'color'
import { ref } from 'vue'
const ringColors = ref({
primary: {
titleKey: 'config_options.light_designer.primary_color',
color: Color('#8f9af2'),
},
secondary: {
titleKey: 'config_options.light_designer.secondary_color',
color: Color('#c06300'),
},
pointer: {
titleKey: 'config_options.light_designer.pointer_color',
color: Color('#ffa346'),
},
})
</script>

View File

@@ -0,0 +1,72 @@
<template>
<ConfigSection title="Knob Mapping" :icon-component="PlusCircle">
<div class="px-8 my-4">
<span class="text-sm text-muted-foreground font-mono">Control:</span>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
ref="comboboxButton"
variant="outline"
role="combobox"
:aria-expanded="open"
class="my-2 w-full justify-between">
<ScrambleText :text="value ? knobMappingOptions[value] : 'Select an action...'" />
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="p-0" :style="{width: $refs.comboboxButton?.$el.offsetWidth}">
<Command>
<CommandInput class="h-9" placeholder="Search actions..." />
<CommandEmpty>
<ScrambleText
scramble-on-mount
text="No actions found." />
</CommandEmpty>
<CommandList>
<CommandGroup>
<CommandItem
v-for="(action, key) in knobMappingOptions"
:key="key"
:value="action"
@select="() => {
value = key
open = false
}">
{{ action }}
<Check
:class="cn('ml-auto h-4 w-4',value === key ? 'opacity-100' : 'opacity-0')" />
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<WIP />
</ConfigSection>
</template>
<script setup>
import { PlusCircle, ChevronsUpDown, Check } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import WIP from '@renderer/components/WIP.vue'
import { Popover, PopoverTrigger, PopoverContent } from '@renderer/components/ui/popover'
import { Button } from '@renderer/components/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@renderer/components/ui/command'
import { ref } from 'vue'
import { cn } from '@renderer/lib/utils'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
const knobMappingOptions = ref({
sendKey: 'Send a Key for each Step',
controlMidi: 'Control a MIDI Value',
controlOsc: 'Control an OSC Value',
controlVolume: 'Control your OS Volume',
moveMouse: 'Move the Mouse',
scrollMouse: 'Scroll the Mouse',
})
const comboboxButton = ref(null)
const open = ref(false)
const value = ref('')
</script>

View File

@@ -0,0 +1,82 @@
<script setup>
import { ref, computed } from 'vue'
const model = defineModel({ type: Number, default: 60 })
const bar = ref(null)
const props = defineProps({
width: { type: Number, default: 160 },
count: { type: Number, default: 40 },
gapWidth: { type: Number, default: 2 },
})
const rectWidth = computed(() => {
return (props.width - ((props.count + 1) * props.gapWidth)) / props.count
})
const currentPosition = computed(() => {
return Math.round((model.value / 100) * (props.count - 1))
})
function onMouseDown() {
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
}
function onMouseMove(e) {
const rect = bar.value.getBoundingClientRect()
model.value = Math.max(0, Math.min(Math.round((e.clientX - rect.left - 9) / (props.width - 6) * 100), 100))
}
function onMouseUp() {
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
}
</script>
<template>
<span @mousedown="onMouseDown">
<svg ref="bar" :width="width+12" height="32">
<g>
<rect
v-for="(_, i) in count"
:key="`key${i}`"
:style="`fill:${i < currentPosition ? '#fff' : '#4a4a4a'}`"
:width="rectWidth"
:height="i===0 || i===count-1 ? 8 : 5"
:x="6+gapWidth+i*(rectWidth+gapWidth)"
y="10" />
<g :transform="`translate(${6+(rectWidth+gapWidth)*currentPosition},0)`">
<rect
style="fill:#000"
:width="6"
height="13"
x="0"
y="10"
/>
<rect
style="fill:#c66936"
:width="2"
height="11"
:x="2"
y="10"
/>
<rect
style="fill:#c66936"
:width="2"
:height="2"
x="0"
y="21"
/>
<rect
style="fill:#c66936"
:width="2"
:height="2"
:x="4"
y="21"
/>
</g>
</g>
</svg>
</span>
</template>

View File

@@ -0,0 +1,38 @@
<template>
<div class="flex">
<button
v-for="(color, key) in keys"
:key="key" :class="{'outline outline-white ' : key === selected,
'hover:outline outline-zinc-400' : key !== selected}"
class="aspect-square flex-1 rounded-[2px] flex items-center justify-center transition-all outline-2"
:style="`box-shadow: 0 3px 20px -2px ${color.hex()}`"
@click="$emit('select', key)">
<span
class="font-heading text-2xl transition-colors"
:class="{'opacity-25 text-white': key!==selected}">{{ key }}
</span>
</button>
</div>
</template>
<script setup>
import Color from 'color'
defineProps({
keys: {
type: Object,
default: () => ({
a: Color.hsl(265, 100, 50),
b: Color.hsl(280, 100, 50),
c: Color.hsl(300, 100, 50),
d: Color.hsl(330, 100, 50),
}),
},
selected: {
type: String,
default: 'a',
},
})
defineEmits(['select'])
</script>

View File

@@ -0,0 +1,62 @@
<template>
<svg :viewBox="`0 0 ${size} ${size}`" filter="url(#blur)">
<filter id="blur" color-interpolation-filters="sRGB">
<feGaussianBlur
v-for="index in blurSteps" :key="index" in="SourceGraphic" :stdDeviation="blur*index"
:result="index" />
<feMerge result="blurMerge">
<feMergeNode v-for="index in blurSteps" :key="index" :in="index" />
</feMerge>
</filter>
<circle
v-for="index in ledCount" :key="index"
:transform="`rotate(${index/ledCount*360} ${size/2} ${size/2})`"
:r="ledRadius"
:cx="size/2"
:cy="padding + ledRadius"
:fill="leds[index-1]?.hex()" />
</svg>
</template>
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue'
import Color from 'color'
const props = defineProps({
value: {
type: Number,
default: 0,
},
})
const leds = ref(Array(60).fill(Color()))
const radius = ref(100)
const ledRadius = ref(3)
const ledCount = ref(60)
const blur = ref(2)
const blurSteps = ref(5)
const padding = ref(40)
const size = computed(() => (radius.value + ledRadius.value + padding.value) * 2)
let interval = null
onMounted(() => {
interval = setInterval(() => {
const valueIndex = Math.floor(props.value / 100 * ledCount.value)
leds.value.forEach((color, index) => {
const distance = Math.abs(index - valueIndex) % ledCount.value
if (distance < 1) {
leds.value[index] = Color.hsl(40, 100, 100)
} else if (distance < 2) {
leds.value[index] = Color.hsl(40, 100, 60)
} else {
leds.value[index] = color.mix(Color.hsl(310, 100, 20), 0.03)
}
})
})
onUnmounted(() => {
clearInterval(interval)
})
})
</script>

View File

@@ -0,0 +1,171 @@
<template>
<div class="aspect-[800/1100]">
<div
class="bg-contain bg-top bg-no-repeat h-full w-full relative"
:style="{backgroundImage: `linear-gradient(to bottom, black, rgba(0,0,0,0.25) 12%, rgba(0,0,0,0.35) 95%, black), url(${previewDeviceImage})`,
backgroundBlendMode: 'multiply'}">
<Transition name="fade">
<div v-if="store.connected" class="px-10 h-12 flex justify-between items-center">
<h2>
<ScrambleText :delay="100" scramble-on-mount :fill-interval="50" :replace-interval="50" text="Nano_D++" />
</h2>
<div class="font-mono text-sm">
<span class="text-muted-foreground">Firmware: </span>
<ScrambleText :delay="100" scramble-on-mount :fill-interval="50" :replace-interval="50" text="v1.3.2a" />
</div>
</div>
</Transition>
<Transition name="fade-delayed">
<DeviceLEDRing
v-if="store.connected" :value="barValue"
class="absolute h-[66%] top-[12.5%] left-0 right-0 mx-auto" />
</Transition>
<div
class="rounded-full aspect-square absolute h-[30%] top-[30.5%] left-0 right-0 mx-auto flex flex-col justify-center items-center overflow-hidden"
style="background: linear-gradient(45deg, black 30%, #252525 50%, #232323 60%, black)">
<TransitionGroup name="fade-display">
<div
v-if="store.connected"
class="absolute flex flex-col items-center text-center pb-2 mix-blend-screen">
<img :src="LogoMidi" alt="midi-logo" class="opacity-50 h-4">
<h2 class="font-pixellg text-5xl">{{ parseInt(value) }}</h2>
<div class="font-pixelsm text-md">HIGH PASS</div>
<DeviceBar v-model="barValue" :count="30" :width="120" />
<span class="w-36 font-pixelsm text-[7pt] text-muted-foreground uppercase">
KORG MINILOGUE HIGH PASS FILTER 0-127
</span>
</div>
<div v-else class="flex flex-col items-center text-center mix-blend-screen">
<ScrambleText
:text="offlineText"
character-set="_()*=0011"
scramble-on-mount
:delay="1000"
:fill-interval="50"
:replace-interval="50"
class="uppercase font-pixelsm text-[7pt] text-muted-foreground"
@finish="nextOfflineText" />
</div>
</TransitionGroup>
</div>
<Transition name="fade-delayed">
<button
v-if="store.connected"
class="rounded-full outline-2 absolute h-[41.5%] top-[24.5%] aspect-square left-0 right-0 mx-auto transition-all"
:class="{'outline outline-white': store.selectedFeature==='knob',
'hover:outline outline-zinc-400': store.selectedFeature!=='knob'}"
@click="store.selectConfigFeature('knob')" />
</Transition>
<Transition name="fade-delayed">
<DeviceKeys
v-if="store.connected"
class="absolute w-[72.7%] top-[77.5%] gap-[2.2%] left-0 right-0 mx-auto"
:selected="store.selectedFeature === 'key' ? store.selectedKey : ''"
@select="store.selectKey" />
</Transition>
</div>
</div>
</template>
<script setup>
import RenderNanoOne from '@renderer/assets/images/renderNanoOneTransparent.png'
import RenderNanoZero from '@renderer/assets/images/renderNanoZeroTransparent.png'
import LogoMidi from '@renderer/assets/logos/logoMidi.svg'
import DeviceBar from '@renderer/components/device/DeviceBar.vue'
import { useStore } from '@renderer/store'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { computed, onMounted, ref } from 'vue'
import DeviceLEDRing from '@renderer/components/device/DeviceLEDRing.vue'
import gsap from 'gsap'
import DeviceKeys from '@renderer/components/device/DeviceKeys.vue'
const value = ref(69)
const barValue = computed(() => value.value / 127 * 100)
const store = useStore()
const previewDeviceImages = {
nanoOne: RenderNanoOne,
nanoZero: RenderNanoZero,
}
const previewDeviceImage = computed(() => previewDeviceImages[store.previewDeviceModel || 'nanoOne'])
const targetValue = ref(69)
const animateValue = () => {
targetValue.value = Math.floor(Math.random() * 127)
gsap.to(value, { duration: 1, value: targetValue.value, ease: 'power2.inOut' })
setTimeout(animateValue, 1500 + Math.random() * 2000)
}
const offlineText = ref('NO DEVICE CONNECTED')
const offlineTexts = [
'AWAITING CONNECTION',
'ARE YOU STILL THERE?',
'DEVICE OFFLINE',
'AWAITING CONNECTION',
'I MISS YOU',
'AWAITING CONNECTION',
'NO DEVICE CONNECTED',
'IS ANYONE THERE?',
'AWAITING CONNECTION',
'DEVICE OFFLINE',
'NAP TIME',
'NO DEVICE CONNECTED',
]
let offlineTextIndex = 0
const nextOfflineText = () => {
if (offlineText.value === '') {
offlineText.value = offlineTexts[offlineTextIndex]
offlineTextIndex = (offlineTextIndex + 1) % offlineTexts.length
} else {
setTimeout(() => {
offlineText.value = ''
}, 3500)
}
}
onMounted(() => {
animateValue()
})
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 500ms ease;
}
.fade-slow-enter-active,
.fade-slow-leave-active,
.fade-delayed-enter-active,
.fade-delayed-leave-active {
transition: opacity 1000ms ease;
}
.fade-delayed-enter-active,
.fade-slow-enter-active {
transition-delay: 150ms;
}
.fade-display-enter-active,
.fade-display-leave-active {
transition: opacity 500ms ease;
}
.fade-display-enter-active {
transition-delay: 1000ms;
}
.fade-enter-from,
.fade-leave-to,
.fade-slow-enter-from,
.fade-slow-leave-to,
.fade-delayed-enter-from,
.fade-delayed-leave-to,
.fade-display-enter-from,
.fade-display-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<button
class="app-titlebar-button text-muted-foreground flex items-center rounded-sm px-3 py-1 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
>
<slot />
</button>
</template>

View File

@@ -0,0 +1,184 @@
<template>
<div class="flex app-titlebar">
<Menubar class="w-full h-14 rounded-none bg-zinc-900 justify-between text-muted-foreground font-mono px-3">
<div v-if="isMacOS" :style="{width: 80 / zoomFactor + 'px'}" />
<div class="flex items-center">
<h1
class="text-2xl min-w-32 app-titlebar-button text-zinc-100 text-nowrap"
@click="$refs.zerooneTitle.scramble(1,100,0); $refs.zerooneSubtitle.scramble(1,75,30)">
<ScrambleText
ref="zerooneTitle"
text=" ZERO/ONE" scramble-on-mount :scramble-amount="1" :fill-interval="100"
:replace-interval="100"
/>
</h1>
<h2 class="text-sm min-w-[188px] text-muted-foreground font-mono text-nowrap">
::
<ScrambleText
ref="zerooneSubtitle"
text="Configuration Suite" scramble-on-mount :scramble-amount="1" :fill-interval="35"
:replace-interval="40" />
</h2>
</div>
<div class="h-8 px-2">
<Separator orientation="vertical" />
</div>
<div class="flex gap-2">
<MenubarMenu>
<MenubarTrigger class="app-titlebar-button">
<template v-if="store.numAttachedDevices!==1">
Devices<span class="text-zinc-500">&nbsp;({{ ""+store.numAttachedDevices }})</span>
</template>
<template v-else>
Device
</template>
</MenubarTrigger>
<MenubarContent>
<!-- TODO: Switch keyboard shortcut icons based on platform -->
<MenubarItem @click="store.setConnected(!store.connected)">
{{ store.connected ? $t('navbar.device.disconnect') : $t('navbar.device.connect') }}
<MenubarShortcut>D</MenubarShortcut>
</MenubarItem>
<MenubarItem v-if="store.multipleDevicesConnected">Next Device
<MenubarShortcut>N</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem class="flex justify-between" @click="store.cycleScreenOrientation">
<p>Orientation:&nbsp;</p>
<p>{{ store.screenOrientation }}°</p>
<MenubarShortcut>R</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem class="flex justify-between" @click="store.switchPreviewDeviceModel">
<p>Skin:&nbsp;</p>
<p>{{ previewDeviceNames[store.previewDeviceModel || 'nanoOne'] }}</p>
<MenubarShortcut>S</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>{{ $t('navbar.device.export') }}
<MenubarShortcut>E</MenubarShortcut>
</MenubarItem>
<MenubarItem>{{ $t('navbar.device.import') }}
<MenubarShortcut>I</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>{{ $t('navbar.device.quit') }}
<MenubarShortcut>Q</MenubarShortcut>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarButton class="app-titlebar-button" @click="electron?.openExternal('https://discord.gg/jgRd77YN5T')">
Community
</MenubarButton>
<MenubarMenu>
<MenubarTrigger class="app-titlebar-button">Help</MenubarTrigger>
<MenubarContent>
<MenubarItem>Software Source</MenubarItem>
<MenubarItem>Hardware Source</MenubarItem>
<MenubarSeparator />
<MenubarItem>Report Software Issue</MenubarItem>
<MenubarItem>Report Hardware Issue</MenubarItem>
<MenubarSeparator />
<MenubarItem class="flex justify-between">
<p>Software Version:&nbsp;</p>
<p>v0.1</p>
</MenubarItem>
<MenubarItem>Contact Support</MenubarItem>
<template v-if="electron?.isDevelopment">
<MenubarSeparator />
<MenubarItem @click="electron?.openDevTools">Developer Tools</MenubarItem>
<MenubarItem @click="electron?.reload">Reload</MenubarItem>
</template>
</MenubarContent>
</MenubarMenu>
</div>
<div class="grow" />
<MenubarButton
v-if="showDisconnectButton"
class="app-titlebar-button border-2"
@click="store.setConnected(!store.connected)">
{{ store.connected ? 'Disconnect' : 'Connect' }}
</MenubarButton>
<div v-if="!isMacOS" class="flex h-full">
<button
v-if="minimizable"
class="grow flex justify-center items-center app-titlebar-button hover:text-white px-2"
@click="electron?.minimizeWindow">
<Minus class="h-5 w-5" />
</button>
<button
v-if="maximizable"
class="grow flex justify-center items-center app-titlebar-button hover:text-white px-2"
@click="electron?.toggleMaximizeWindow">
<Copy v-if="isMaximized" class="h-4 w-4" />
<Square v-else class="h-3.5 w-3.5 mr-0.5" />
</button>
<button
class="grow flex justify-center items-center app-titlebar-button hover:text-white px-2"
@click="electron?.closeWindow">
<X class="h-5 w-5 mr-0.5" />
</button>
</div>
</Menubar>
</div>
</template>
<script setup>
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarTrigger,
} from '@renderer/components/ui/menubar'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { Minus, Square, Copy, X } from 'lucide-vue-next'
import { onMounted, ref } from 'vue'
import { Separator } from '@renderer/components/ui/separator'
import { useStore } from '@renderer/store'
import MenubarButton from '@renderer/components/navbar/MenubarButton.vue'
const store = useStore()
const minimizable = ref(true)
const maximizable = ref(true)
const showDisconnectButton = ref(false)
const isMaximized = ref(false)
const { electron } = window
const isMacOS = electron?.platform === 'darwin'
const zoomFactor = ref(1)
const previewDeviceNames = ref({
nanoOne: 'One',
nanoZero: 'Zero',
})
onMounted(() => {
window.addEventListener('resize', () => {
zoomFactor.value = window.outerWidth / window.innerWidth
})
electron?.onMaximized((maximized) => {
console.log(maximized)
isMaximized.value = true
})
electron?.onUnmaximized(() => {
isMaximized.value = false
})
})
</script>
<style scoped>
.app-titlebar {
-webkit-user-select: none;
-webkit-app-region: drag;
}
.app-titlebar-button {
-webkit-app-region: no-drag;
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div>
<div
v-for="(config, index) in config_tabs"
:key="config"
:data-selected="current_tab===config.id"
class="px-4 h-20 flex items-center hover:bg-zinc-900 data-[selected=true]:bg-zinc-200 hover:data-[selected=true]:bg-zinc-100 data-[selected=true]:text-black border-solid border-0 border-b"
:class="config.disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="current_tab=config.disabled ? current_tab : config.id; $refs.configSelect[index].scramble()">
<div class="w-full">
<h1 class="text-lg text-nowrap" :class="{'text-muted-foreground': config.disabled}">
<ScrambleText ref="configSelect" class="align-middle" :text="$t(`config_options.${config.id}.title`)" />
<Badge
v-if="config.hasOwnProperty('badgeKey')"
v-t="config.badgeKey"
class="font-mono ml-2 rounded-full h-4 align-middle bg-zinc-900 text-muted-foreground" />
</h1>
</div>
<ChevronRight v-if="current_tab === config.id" class="float-right" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { Badge } from '@renderer/components/ui/badge'
import { ChevronRight } from 'lucide-vue-next'
const config_tabs = ref([
{ id: 'profile_settings' },
{ id: 'feedback_designer' },
{ id: 'mapping_configuration' },
{ id: 'light_designer' },
{
id: 'gui_designer',
disabled: true,
badgeKey: 'common.coming_soon',
},
{ id: 'dev_playground' },
])
const current_tab = defineModel({
type: String,
default: 'profile_settings',
})
</script>

View File

@@ -0,0 +1,8 @@
<template>
<WIP />
<DeviceLEDRing />
</template>
<script setup>
import WIP from '@renderer/components/WIP.vue'
import DeviceLEDRing from '@renderer/components/device/DeviceLEDRing.vue'
</script>

View File

@@ -0,0 +1,31 @@
<template>
<div>
<div class="flex px-4 pt-5 pb-3 items-baseline font-heading">
<span class="text-lg">{{ $t('preview.title') }}</span>
&nbsp;
<span class="text-zinc-600">(Nano_D++)</span>
</div>
<div class="flex justify-center">
<div
class="flex bg-cover mb-6 w-72 aspect-square"
style="background-image: url(../../assets/old/xl-bg-ico.svg)">
<div class="flex flex-col w-full justify-center m-9 rounded-full overflow-hidden">
<div class="self-center w-8 mb-1 opacity-50">
<img src="../../assets/logos/logoMidi.svg" alt="midi-logo">
</div>
<h2 class="self-center font-pixellg text-5xl ">1337</h2>
<div class="self-center font-pixelsm text-md pt-1 pb-2">Profile name</div>
<DeviceBar class="self-center" />
<span
class="self-center text-center w-48 font-pixelsm text-xs text-muted-foreground">
Profile description will go here! And hopefully it will be a long one! Much longer than this one! Actually, this is probably long enough. I don't think we need to make it any longer...
</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import DeviceBar from '@renderer/components/device/DeviceBar.vue'
import DeviceBackground from '@renderer/assets/old/xl-bg-ico.svg'
</script>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import draggable from 'vuedraggable'
import { ref } from 'vue'
const message = [
'vue.draggable',
'draggable',
'component',
'for',
'vue.js 2.0',
'based',
'on',
'Sortablejs',
]
const list = ref(
message.map((name, index) => {
return { name, order: index + 1 }
}),
)
const dragOptions = ref({
animation: 200,
group: 'description',
disabled: false,
ghostClass: 'ghost',
})
const drag = ref(false)
const sort = () => {
console.log('here')
list.value = list.value.sort((a, b) => a.order - b.order)
}
</script>
<template>
<div class="row">
<div class="col-2 flex">
<button class="btn btn-secondary button" @click="sort">
To original order
</button>
</div>
<div class="col-6 flex-col">
<h3>Transition</h3>
<transition-group>
<draggable
key="dragggable"
item-key="name"
:list="list"
v-bind="dragOptions"
@start="drag = true"
@end="drag = false"
>
<template #item="{ element }">
<li :key="element.name">
{{ element.name }}
</li>
</template>
</draggable>
</transition-group>
</div>
</div>
</template>
<style scoped>
.button {
margin-top: 35px;
}
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<ConfigSection
:title="$t('config_options.feedback_designer.feedback_type.title')"
:icon-component="GaugeCircle">
<TabSelect v-model="feedbackType" :options="feedbackTypeOptions" />
</ConfigSection>
<ConfigSection
:title="$t('config_options.feedback_designer.haptic_response.title')"
:icon-component="AudioWaveform"
:show-toggle="true">
<SteppedSlider
v-model="feedbackStrength"
:label="$t('config_options.feedback_designer.haptic_response.feedback_strength')" />
<Separator />
<SteppedSlider
v-model="bounceBackStrength"
:label="$t('config_options.feedback_designer.haptic_response.bounce_back_strength')" />
<Separator />
<SteppedSlider
v-model="outputRampDampening"
:label="$t('config_options.feedback_designer.haptic_response.output_ramp_dampening')" />
</ConfigSection>
<ConfigSection
:title="$t('config_options.feedback_designer.auditory_response.title')"
:icon-component="AudioLines" :show-toggle="true">
<SteppedSlider
v-model="auditoryHapticLevel"
:label="$t('config_options.feedback_designer.auditory_response.haptic_level')" />
<Separator />
<SteppedSlider
v-model="auditoryMagnitude"
:label="$t('config_options.feedback_designer.auditory_response.magnitude')"
:max="3"
:named-positions="[
{value:0, label: 'Faint'},
{value:1, label: 'Soft'},
{value:2, label: 'Normal'},
{value:3, label: 'Loud'}]" />
</ConfigSection>
</template>
<script setup>
import { AudioLines, AudioWaveform, GaugeCircle } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import SteppedSlider from '@renderer/components/common/SteppedSlider.vue'
import { ref } from 'vue'
import FdIcon from '@renderer/assets/icons/iconFineDetents.svg'
import CdIcon from '@renderer/assets/icons/iconCoarseDetents.svg'
import VfIcon from '@renderer/assets/icons/iconViscousRotation.svg'
import RcIcon from '@renderer/assets/icons/iconReturnToCenter.svg'
import TabSelect from '@renderer/components/common/TabSelect.vue'
const feedbackType = ref('fineDetents') // TODO: replace with actual value
const feedbackTypeOptions = {
fineDetents: {
icon: FdIcon,
titleKey: 'config_options.feedback_designer.feedback_type.fine_detents',
},
coarseDetents: {
icon: CdIcon,
titleKey: 'config_options.feedback_designer.feedback_type.coarse_detents',
},
viscousRotation: {
icon: VfIcon,
titleKey: 'config_options.feedback_designer.feedback_type.viscous_rotation',
},
returnToCenter: {
icon: RcIcon,
titleKey: 'config_options.feedback_designer.feedback_type.return_to_center',
},
}
const feedbackStrength = ref(2)
const bounceBackStrength = ref(2)
const outputRampDampening = ref(2)
const auditoryHapticLevel = ref(2)
const auditoryMagnitude = ref(2)
</script>

View File

@@ -0,0 +1,52 @@
<template>
<button
class="flex-1 flex flex-col items-center py-2"
:class="{'text-black bg-zinc-200 hover:bg-zinc-100': selected,
'hover:bg-zinc-800 text-muted-foreground' : !selected}"
@click="$emit('select'); $refs.title.scramble()">
<img
draggable="false"
:src="icon" alt="connection-type-icon"
class="w-16 py-2"
:class="{'invert': invert && selected}">
<ScrambleText ref="title" :resize="false" class="text-xs text-wrap p-1" :text="title" />
<span>
<ArrowBigUp class="inline-block" />
<Badge
class="shadow-none text-wrap inline-block p-0.5 mx-1"
:class="{'bg-orange-400': selected, 'bg-zinc-400': !selected}">Space</Badge>+<Badge
class="shadow-none text-wrap inline-block p-0.5 mx-1"
:class="{'bg-orange-400': selected, 'bg-zinc-400': !selected}">E</Badge>
</span>
</button>
</template>
<script setup>
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { Badge } from '@renderer/components/ui/badge'
import { ArrowBigUp } from 'lucide-vue-next'
defineEmits(['select'])
defineProps({
title: {
type: String,
default: '',
},
icon: {
type: [String, Object, Function],
default: '',
},
selected: {
type: Boolean,
default: false,
},
invert: {
type: Boolean,
default: true,
},
badge: {
type: [String, Object, Function],
default: '',
},
})
</script>

View File

@@ -0,0 +1,88 @@
<template>
<ConfigSection
title="Ring Lights" :icon-component="Lightbulb"
:show-toggle="true">
<h2 class="p-6 inline-block">{{ $t('config_options.light_designer.led_mode') }}</h2> [TODO]
<div class="px-6 py-2">
<Slider v-model="brightnessSliderModel" max="100" class="h-10" />
</div>
</ConfigSection>
<ConfigSection
title="Key Lights" :icon-component="Lightbulb"
:show-toggle="true">
<h2 class="p-6 inline-block">{{ $t('config_options.light_designer.led_mode') }}</h2> [TODO]
<div class="px-6 py-2">
<Slider v-model="brightnessSliderModel" max="100" class="h-10" />
</div>
</ConfigSection>
<ConfigSection title="Ring Colors" :icon-component="Circle">
<PaletteInput v-model="ringColors" />
</ConfigSection>
<ConfigSection title="Key Colors" :icon-component="PanelBottom">
<PaletteInput v-model="keyColors" />
</ConfigSection>
<ConfigSection title="Key Colors (Pressed)" :icon-component="PanelBottomClose">
<PaletteInput v-model="keyColors" />
</ConfigSection>
</template>
<script setup>
import { Lightbulb, PanelBottomClose, Circle, PanelBottom } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import PaletteInput from '@renderer/components/common/PaletteInput.vue'
import Color from 'color'
import { computed, ref } from 'vue'
import { useStore } from '@renderer/store'
import { Slider } from '@renderer/components/ui/slider'
const store = useStore()
const ledConfig = computed(() => store.selectedProfile.ledConfig)
const brightnessSliderModel = computed({
get: () => [ledConfig.value.ledBrightness],
set: (val) => ledConfig.value.ledBrightness = val[0],
})
const ringColors = ref({
primary: {
key: 'config_options.light_designer.primary_color',
color: computed({
get: () => Color(ledConfig.value.primary),
set: (color) => [ledConfig.value.primary.h, ledConfig.value.primary.s, ledConfig.value.primary.v] = color.hsv().color,
}),
},
secondary: {
key: 'config_options.light_designer.secondary_color',
color: computed({
get: () => Color(ledConfig.value.secondary),
set: (color) => [ledConfig.value.secondary.h, ledConfig.value.secondary.s, ledConfig.value.secondary.v] = color.hsv().color,
}),
},
pointer: {
key: 'config_options.light_designer.pointer_color',
color: computed({
get: () => Color(ledConfig.value.pointer),
set: (color) => [ledConfig.value.pointer.h, ledConfig.value.pointer.s, ledConfig.value.pointer.v] = color.hsv().color,
}),
},
})
const keyColors = ref({
a: {
key: 'a',
color: Color('#ff2a7d'),
},
b: {
key: 'b',
color: Color('#f32a9c'),
},
c: {
key: 'c',
color: Color('#d12ab1'),
},
d: {
key: 'd',
color: Color('#a92ac3'),
},
})
</script>

View File

@@ -0,0 +1,130 @@
<template>
<ConfigSection title="Map some stuff idk" :icon-component="Keyboard">
<div v-if="false" class="flex font-heading">
<KeySelectButton
v-for="(option, key) in keySelectOptions" :key="key" :title="$t(option.titleKey)"
:invert="option.invert" :badge="option.badge"
:icon="option.icon" :selected="selectedKey===key" @select="selectedKey=key" />
</div>
<Separator />
<Command>
<CommandList>
<CommandEmpty>{{ $t('config_options.mapping_configuration.key_mapping.not_found') }}
</CommandEmpty>
<CommandInput
:placeholder="$t('config_options.mapping_configuration.key_mapping.search_placeholder')" />
<CommandGroup heading="Common">
<CommandItem value="backspace">
<Squircle color="grey" class="w-4 h-4 mr-2" />
Backspace
</CommandItem>
<CommandItem value="delete">
<Squircle color="grey" class="w-4 h-4 mr-2" />
Delete
</CommandItem>
<CommandItem value="enter">
<Squircle color="grey" class="w-4 h-4 mr-2" />
Enter
</CommandItem>
<CommandItem value="end">
<Squircle color="grey" class="w-4 h-4 mr-2" />
End
</CommandItem>
<CommandItem value="arrow up">
<Squircle color="grey" class="w-4 h-4 mr-2" />
Arrow Up
</CommandItem>
<CommandItem value="arrow down">
<Squircle color="grey" class="w-4 h-4 mr-2" />
Arrow Down
</CommandItem>
<CommandItem value="arrow left">
<Squircle color="grey" class="w-4 h-4 mr-2" />
Arrow Left
</CommandItem>
<CommandItem value="arrow right">
<Squircle color="grey" class="w-4 h-4 mr-2" />
Arrow Right
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="MIDI Control Changes">
<CommandItem value="cc0">
<KeyboardMusic color="grey" class="w-4 h-4 mr-2" />
Bank Select (CC0)
</CommandItem>
<CommandItem value="cc2">
<KeyboardMusic color="grey" class="w-4 h-4 mr-2" />
Modulation (CC1)
</CommandItem>
<CommandItem value="cc3">
<KeyboardMusic color="grey" class="w-4 h-4 mr-2" />
Foot Controller (CC4)
</CommandItem>
<CommandItem value="cc4">
<KeyboardMusic color="grey" class="w-4 h-4 mr-2" />
Portamento (CC5)
</CommandItem>
<CommandItem value="cc5">
<KeyboardMusic color="grey" class="w-4 h-4 mr-2" />
Volume (CC7)
</CommandItem>
</CommandGroup>
<CommandGroup heading="Macros">
<CommandItem value="Page Scroll">
<Squircle color="grey" class="w-4 h-4 mr-2" />
Page Scroller (M0)
</CommandItem>
</CommandGroup>
</CommandList>
<CommandSeparator />
</Command>
<Separator />
</ConfigSection>
</template>
<script setup>
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList, CommandSeparator,
} from '@renderer/components/ui/command'
import { KeyboardMusic, Squircle, Keyboard } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import KeyO from '@renderer/assets/icons/iconKeyOrange.svg'
import Key from '@renderer/assets/icons/iconKeyWhite.svg'
import KeyG from '@renderer/assets/icons/iconKeyGrey.svg'
import KeyD from '@renderer/assets/icons/iconKeyDark.svg'
import { ref } from 'vue'
import KeySelectButton from '@renderer/components/old/KeySelectButton.vue'
const selectedKey = ref('a') // TODO: replace with actual value
const keySelectOptions = {
a: {
icon: KeyO,
titleKey: 'config_options.mapping_configuration.key_mapping.switch.a',
invert: false,
},
b: {
icon: Key,
titleKey: 'config_options.mapping_configuration.key_mapping.switch.b',
invert: true,
},
c: {
icon: KeyG,
titleKey: 'config_options.mapping_configuration.key_mapping.switch.c',
invert: true,
},
d: {
icon: KeyD,
titleKey: 'config_options.mapping_configuration.key_mapping.switch.d',
invert: true,
},
}
</script>

View File

@@ -0,0 +1,34 @@
<script setup>
import schema from '@renderer/data/profileSchema.json'
import axios from 'axios'
import { inject, ref } from 'vue'
const ajv = inject('ajv')
const message = ref('Waiting...')
try {
const res = await axios.get('http://localhost:3001/profiles/5867')
const profiles = res.data
console.log(profiles)
const validate = ajv.compile(schema)
const valid = validate(profiles)
if (!valid) {
message.value = 'Invalid!'
console.log(validate.errors)
} else {
message.value = 'Valid!'
console.log('valid!!!!!!!!!!!!!!!1111elf')
}
} catch (e) {
console.error(e)
}
</script>
<template>
{{ message }}
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,49 @@
<template>
<div>
<div
v-for="(config, index) in config_tabs"
:key="config"
:data-selected="current_tab===config.id"
class="px-4 h-20 flex items-center hover:bg-zinc-900 data-[selected=true]:bg-zinc-200 hover:data-[selected=true]:bg-zinc-100 data-[selected=true]:text-black border-solid border-0 border-b"
:class="config.disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="current_tab=config.disabled ? current_tab : config.id; $refs.configSelect[index].scramble()">
<div class="w-full">
<h1 class="text-lg text-nowrap" :class="{'text-muted-foreground': config.disabled}">
<ScrambleText ref="configSelect" class="align-middle" :text="$t(`config_options.${config.id}.title`)" />
<Badge
v-if="config.hasOwnProperty('badgeKey')"
v-t="config.badgeKey"
class="font-mono ml-2 rounded-full h-4 align-middle bg-zinc-900 text-muted-foreground" />
</h1>
<p class="text-xs" :class="current_tab===config.id?'text-black' : 'text-muted-foreground'">
{{ $t(`config_options.${config.id}.subtitle`) }}
</p>
</div>
<ChevronRight v-if="current_tab === config.id" class="float-right" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { Badge } from '@renderer/components/ui/badge'
import { ChevronRight } from 'lucide-vue-next'
const config_tabs = ref([
{ id: 'profile_settings' },
{ id: 'feedback_designer' },
{ id: 'mapping_configuration' },
{ id: 'light_designer' },
{
id: 'gui_designer',
disabled: true,
badgeKey: 'common.coming_soon',
},
{ id: 'dev_playground' },
])
const current_tab = defineModel({
type: String,
default: 'profile_settings',
})
</script>

View File

@@ -0,0 +1,166 @@
<template>
<div
class="h-12 flex overflow-hidden rounded-lg m-2 transition-all"
:class="{'border border-zinc-100 bg-zinc-300': selected,
'border border-transparent hover:border-zinc-900': !selected,
'group': showHoverButtons}">
<form
v-if="nameEditable && editing"
class="flex-1 flex h-full text-left whitespace-nowrap overflow-hidden"
:class="{'bg-zinc-300' : selected}"
@submit.prevent="store.renameProfile(profile.id, nameInput); editing=false">
<input
ref="profileNameInput" v-model="nameInput"
onfocus="this.select()" :placeholder="$t('profiles.name_placeholder')"
class="flex-1 pl-8 h-full rounded-lg text-sm bg-transparent focus-visible:ring-0 focus-visible:outline-none min-w-0 transition-all"
:class="{'font-semibold bg-zinc-300 hover:bg-zinc-200 text-black' : selected,
'hover:bg-zinc-900 bg-opacity-50 text-muted-foreground': !selected}"
@blur="onNameInputBlur">
<button
ref="nameSubmitButton"
type="submit"
:class="{'bg-zinc-300 hover:bg-zinc-200 text-black' : selected,
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected}"
class="flex h-full rounded-lg aspect-square justify-center items-center flex-shrink-0 transition-all">
<Check class="h-4 w-4" />
</button>
</form>
<!-- TODO: Make hover buttons use Transition(Group) and v-if directive -->
<button
v-else
:class="{'font-semibold bg-zinc-300 hover:bg-zinc-200 text-black' : selected,
'hover:bg-zinc-900 bg-opacity-50 text-muted-foreground': !selected}"
class="flex-1 h-12 rounded-lg text-left text-sm whitespace-nowrap overflow-hidden text-ellipsis pr-4 transition-all"
@click="!editing && $emit('select') && $refs.profileTitle.scramble()">
<span class="ml-2 w-4 mr-2 cursor-grab" :class="{'ml-2': !draggable}">
<GripHorizontal
v-if="draggable"
:class="{'text-zinc-600': selected,
'text-muted-foreground': !selected}"
class="mb-0.5 h-4 w-4 opacity-0 group-hover:opacity-100 inline-block transition-all" />
</span>
<ScrambleText
ref="profileTitle"
class="transition-colors"
:class="{'text-black': selected, 'text-muted-foreground': !selected}"
:text="profile.name" />
<span
v-if="showId"
class="text-xs text-zinc-600 group-hover:hidden"> UID:{{ profile.id }}</span>
</button>
<template v-if="!confirmDelete">
<button
v-if="nameEditable"
:class="{'bg-zinc-300 hover:bg-zinc-200 text-black' : selected,
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected,
'group-hover:w-12' : !editing}"
class="flex w-0 h-12 rounded-lg justify-center items-center flex-shrink-0 transition-all"
@click="startEditing">
<PenLine class="h-4 w-4" />
</button>
<button
:class="{'bg-zinc-300 hover:bg-zinc-200 text-black' : selected,
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected,
'group-hover:w-12' : !editing,
'rounded-l-lg': !nameEditable}"
class="flex w-0 h-12 rounded-lg justify-center items-center flex-shrink-0 transition-all"
@click="$emit('duplicate')">
<Copy class="h-4 w-4" />
</button>
<button
:class="{'bg-orange-700 hover:bg-orange-600 text-black' : selected,
'hover:bg-opacity-100 bg-orange-900 text-zinc-100 bg-opacity-50': !selected,
'group-hover:w-12' : !editing}"
class="flex w-0 h-12 rounded-lg justify-center items-center flex-shrink-0 transition-all"
@click="confirmDelete=true">
<Trash2 class="h-4 w-4" />
</button>
</template>
<template v-else>
<button
:class="{'bg-orange-600 hover:bg-orange-500 text-black' : selected,
'hover:bg-opacity-100 bg-orange-900 text-zinc-100 bg-opacity-50': !selected,
'group-hover:w-12' : !editing}"
class="flex w-0 h-12 rounded-lg justify-center items-center flex-shrink-0 transition-all"
@click="$emit('delete', profile.id)">
<Check class="h-4 w-4" />
</button>
<button
:class="{'bg-zinc-300 hover:bg-zinc-200 text-black' : selected,
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected,
'group-hover:w-12' : !editing}"
class="flex w-0 h-12 rounded-lg justify-center items-center flex-shrink-0 transition-all"
@click="confirmDelete=false">
<X class="h-4 w-4" />
</button>
</template>
</div>
</template>
<script setup>
import { Check, Copy, PenLine, Trash2, X, GripHorizontal } from 'lucide-vue-next'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { nextTick, ref } from 'vue'
import { useStore } from '@renderer/store'
const store = useStore()
defineEmits(['select', 'duplicate', 'delete'])
const nameSubmitButton = ref(null)
const props = defineProps({
profile: {
type: Object,
default: () => ({
id: '1234',
name: 'Profile Name',
}),
required: true,
},
selected: {
type: Boolean,
default: false,
},
showId: {
type: Boolean,
default: false,
},
nameEditable: {
type: Boolean,
default: true,
},
initEditing: {
type: Boolean,
default: false,
},
draggable: {
// Not implemented yet
type: Boolean,
default: true,
},
showHoverButtons: {
type: Boolean,
default: true,
},
})
async function startEditing() {
editing.value = true
await nextTick()
profileNameInput.value.focus()
}
function onNameInputBlur(e) {
if (e.relatedTarget === nameSubmitButton.value) return
editing.value = false
}
const profileNameInput = ref(null)
const nameInput = ref(props.profile.name)
const editing = ref(props.initEditing)
const confirmDelete = ref(false)
</script>

View File

@@ -0,0 +1,60 @@
<template>
<ConfigSection :title="$t('config_options.profile_settings.profile_properties.title')" :icon-component="Type">
<div class="px-8 my-4">
<span class="text-sm text-muted-foreground font-mono">Title</span>
<Input class="font-pixelsm mt-2 uppercase" default-value="Title text" />
</div>
<div class="px-8 my-4">
<span class="text-sm text-muted-foreground font-mono">Description</span>
<Textarea class="font-pixelsm mt-2 uppercase" default-value="Descriptive description describing the profile" />
</div>
</ConfigSection>
<ConfigSection
v-if="false" :title="$t('config_options.profile_settings.connection_type.title')"
:icon-component="Cable">
<!-- TODO: Remove later if not needed -->
<TabSelect v-model="connectionType" :options="connectionTypeOptions" />
</ConfigSection>
<ConfigSection
:title="$t('config_options.profile_settings.internal_profile_toggle.title')"
:icon-component="Replace" :show-toggle="true">
<p class="flex flex-col p-8 py-4 text-muted-foreground text-xs">
{{ $t('config_options.profile_settings.internal_profile_toggle.subtitle') }}
<Separator class="mt-4" />
<span class="py-4 space-y-4">{{ $t('config_options.profile_settings.internal_profile_toggle.operation')
}}:<br>
<Badge class="bg-orange-500">SHIFT</Badge> + <Badge
class="bg-zinc-500">Fn3</Badge> + <Badge>Rotation</Badge></span>
<Separator />
<span class="pt-4">{{ $t('config_options.profile_settings.internal_profile_toggle.warning') }}</span>
</p>
</ConfigSection>
</template>
<script setup>
import { Cable, Replace, Type } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import { Separator } from '@renderer/components/ui/separator'
import { ref } from 'vue'
import UsbIcon from '@renderer/assets/logos/logoUsb.svg'
import MidiIcon from '@renderer/assets/logos/logoMidi.svg'
import { Badge } from '@renderer/components/ui/badge'
import WIP from '@renderer/components/WIP.vue'
import TabSelect from '@renderer/components/common/TabSelect.vue'
import { Input } from '@renderer/components/ui/input'
import { Textarea } from '@renderer/components/ui/textarea'
const connectionType = ref('usb') // TODO: replace with actual value
const connectionTypeOptions = {
usb: {
icon: UsbIcon,
titleKey: 'config_options.profile_settings.connection_type.usb',
},
midi: {
icon: MidiIcon,
titleKey: 'config_options.profile_settings.connection_type.midi',
},
}
</script>

View File

@@ -0,0 +1,258 @@
<template>
<div>
<div>
<div
class="w-full h-12 px-4 flex items-center justify-between flex-nowrap text-nowrap bg-zinc-900">
<button
class="flex flex-1 items-center h-full min-w-0 font-heading"
@click="showProfileConfig=store.selectedProfile && !showProfileConfig">
<component :is="showProfileConfig ? ArrowLeft : List" class="w-5 h-full mr-1 shrink-0" />
<ScrambleText
:text="showProfileConfig ? store.selectedProfile?.name : $t('profiles.title')"
class="text-ellipsis overflow-hidden min-w-0" />
<ScrambleText
v-if="!showProfileConfig" class="text-sm text-zinc-600 text-ellipsis overflow-hidden min-w-0"
scramble-on-mount
:fill-interval="20"
:delay="500"
:text="`(${store.profiles.length}/${ maxProfiles})`" />
</button>
<DropdownMenu>
<DropdownMenuTrigger>
<Transition name="fade">
<button
v-if="!showProfileConfig"
class="bg-zinc-300 text-black hover:bg-zinc-200 border border-zinc-100 rounded-lg h-8 aspect-square flex justify-center items-center"
@click="store.addProfile">
<Plus class="h-4" />
</button>
</Transition>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
Profile
</DropdownMenuItem>
<DropdownMenuItem>
Category
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Separator />
</div>
<div class="grow overflow-y-auto relative">
<div
v-if="renderProfileList"
class="absolute w-full">
<div v-if="store.profiles.length === 0">
<div class="flex flex-col items-center justify-center h-32">
<ScrambleText
scramble-on-mount :fill-interval="5" class="text-sm text-muted-foreground"
:text="$t('profiles.not_found')" />
</div>
</div>
<div v-else>
<draggable
key="categoriesDraggable"
group="profileCategories"
item-key="name"
:list="store.profileCategories"
v-bind="dragOptions"
@start="drag = true"
@end="drag = false"
@change="onCategoryDrop">
<template #item="dragCategory">
<Collapsible
v-model:open="collapse[dragCategory.element.name]"
:default-open="true">
<!-- TODO: Make profile groups computed instead defining them of using v-for -->
<CollapsibleTrigger
class="w-full h-12 py-2 text-left text-muted-foreground text-sm bg-zinc-900 border-0 border-b">
<ChevronRight class="chevrot h-4 w-4 mb-0.5 ml-4 inline-block transition-transform" />
{{ dragCategory.element.name }}<span
class="font-heading text-sm text-zinc-600"> ({{ dragCategory.element.profiles?.length || 0
}})</span>
</CollapsibleTrigger>
<CollapsibleContent>
<TransitionGroup>
<draggable
key="profilesDraggable"
group="profiles"
item-key="id"
:list="dragCategory.element.profiles"
v-bind="dragOptions"
@start="drag = true"
@end="drag = false"
@change="(event)=>onProfileDrop(event, dragCategory.index)">
<template v-if="dragCategory.element.profiles.length === 0" #header>
<div class="flex h-12 justify-center items-center hideable-header">
<MoreHorizontal class="w-4 text-zinc-600" />
</div>
</template>
<template #item="dragProfile">
<div :key="dragProfile.element.name">
<ProfileButton
:profile="dragProfile.element"
:show-hover-buttons="!drag"
:selected="store.selectedProfile?.id === dragProfile.element.id"
@select="store.selectProfile(dragProfile.element.id); showProfileConfig=true"
@duplicate="store.duplicateProfile(dragProfile.element.id)"
@delete="store.removeProfile(dragProfile.element.id)" />
</div>
</template>
</draggable>
</TransitionGroup>
</CollapsibleContent>
</Collapsible>
</template>
</draggable>
</div>
</div>
<Transition name="slide">
<div v-if="showProfileConfig" class="absolute bg-[#101013] h-full">
<ProfileConfig />
</div>
</Transition>
</div>
</div>
</template>
<script setup>
import { Separator } from '@renderer/components/ui/separator'
import { ChevronRight, Plus, ArrowLeft, List, MoreHorizontal } from 'lucide-vue-next'
import { ref, watch } from 'vue'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@renderer/components/ui/collapsible'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { useStore } from '@renderer/store'
import ProfileButton from '@renderer/components/profile/ProfileButton.vue'
import ProfileConfig from '@renderer/components/profile/ProfileConfig.vue'
import draggable from 'vuedraggable'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@renderer/components/ui/dropdown-menu'
defineProps({
showFilter: {
type: Boolean,
default: false,
},
})
const dragOptions = ref({
ghostClass: 'ghost',
animation: 150,
direction: 'vertical',
})
const maxProfiles = 32
const store = useStore()
const collapse = ref({})
const showProfileConfig = ref(false)
const renderProfileConfig = ref(showProfileConfig.value)
const renderProfileList = ref(!showProfileConfig.value)
const drag = ref(false)
watch(showProfileConfig, value => {
if (value) {
renderProfileConfig.value = true
setTimeout(() => {
renderProfileList.value = false
}, 300)
} else {
renderProfileList.value = true
setTimeout(() => {
renderProfileConfig.value = false
}, 300)
}
})
// const filteredProfiles = computed(() => {
// if (!filter.value) {
// return store.profiles
// }
// const filterLower = filter.value.toLowerCase()
// return store.profiles.filter(profile => {
// const nameLower = profile.name.toLowerCase()
// const idLower = profile.id.toLowerCase()
// const tagLower = profile.profileTag.toLowerCase()
// return nameLower.includes(filterLower) || idLower.includes(filterLower) || tagLower.includes(filterLower)
// })
// })
//
// const filteredProfilesByTag = computed(() => {
// const map = new Map()
// filteredProfiles.value.forEach(profile => {
// const tag = profile.profileTag || 'Uncategorized'
// if (!map.has(tag)) {
// map.set(tag, [])
// }
// map.get(tag).push(profile)
// })
// return map
// })
const onCategoryDrop = (event) => {
if (event.moved) {
const category = event.moved.element
const oldIndex = event.moved.oldIndex
const newIndex = event.moved.newIndex
store.moveProfileCategory(category.name, oldIndex, newIndex)
}
}
const onProfileDrop = (event, categoryIndex) => {
if (event.moved) {
const profile = event.moved.element
const oldIndex = event.moved.oldIndex
const newIndex = event.moved.newIndex
store.moveProfile(profile.id, oldIndex, newIndex)
}
if (event.added) {
const profile = event.added.element
const newIndex = event.added.newIndex
store.changeProfileCategory(profile.id, categoryIndex, newIndex)
}
}
</script>
<style scoped>
[data-state=open] > .chevrot {
transform: rotate(90deg);
}
.slide-enter-active,
.slide-leave-active {
transition: transform 300ms ease;
}
.slide-enter-active {
transition-delay: 150ms;
}
.slide-enter-from,
.slide-leave-to {
transform: translateX(-100%);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 150ms ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.sortable-drag {
opacity: 0;
}
.hideable-header:not(:only-child) {
display: none;
}
.hideable-header:only-child {
display: flex;
}
</style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { type BadgeVariants, badgeVariants } from '.'
import { cn } from '@renderer/lib/utils'
const props = defineProps<{
variant?: BadgeVariants['variant']
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,25 @@
import { type VariantProps, cva } from 'class-variance-authority'
export { default as Badge } from './Badge.vue'
export const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Primitive, type PrimitiveProps } from 'radix-vue'
import { type ButtonVariants, buttonVariants } from '.'
import { cn } from '@renderer/lib/utils'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
as?: string
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
})
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -0,0 +1,34 @@
import { type VariantProps, cva } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import { CollapsibleRoot, useForwardPropsEmits } from 'radix-vue'
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'radix-vue'
const props = defineProps<CollapsibleRootProps>()
const emits = defineEmits<CollapsibleRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<CollapsibleRoot v-slot="{ open }" v-bind="forwarded">
<slot :open="open" />
</CollapsibleRoot>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { CollapsibleContent, type CollapsibleContentProps } from 'radix-vue'
const props = defineProps<CollapsibleContentProps>()
</script>
<template>
<CollapsibleContent v-bind="props" class="overflow-hidden transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
<slot />
</CollapsibleContent>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { CollapsibleTrigger, type CollapsibleTriggerProps } from 'radix-vue'
const props = defineProps<CollapsibleTriggerProps>()
</script>
<template>
<CollapsibleTrigger v-bind="props">
<slot />
</CollapsibleTrigger>
</template>

View File

@@ -0,0 +1,3 @@
export { default as Collapsible } from './Collapsible.vue'
export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue'
export { default as CollapsibleContent } from './CollapsibleContent.vue'

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { ComboboxRootEmits, ComboboxRootProps } from 'radix-vue'
import { ComboboxRoot, useForwardPropsEmits } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
open: true,
modelValue: '',
})
const emits = defineEmits<ComboboxRootEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxRoot
v-bind="forwarded"
:class="cn('flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground', props.class)"
>
<slot />
</ComboboxRoot>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { useForwardPropsEmits } from 'radix-vue'
import type { DialogRootEmits, DialogRootProps } from 'radix-vue'
import Command from './Command.vue'
import { Dialog, DialogContent } from '@renderer/components/ui/dialog'
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<Dialog v-bind="forwarded">
<DialogContent class="overflow-hidden p-0 shadow-lg">
<Command class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<slot />
</Command>
</DialogContent>
</Dialog>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { ComboboxEmptyProps } from 'radix-vue'
import { ComboboxEmpty } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ComboboxEmpty v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
<slot />
</ComboboxEmpty>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { ComboboxGroupProps } from 'radix-vue'
import { ComboboxGroup, ComboboxLabel } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<ComboboxGroupProps & {
class?: HTMLAttributes['class']
heading?: string
}>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ComboboxGroup
v-bind="delegatedProps"
:class="cn('overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', props.class)"
>
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
{{ heading }}
</ComboboxLabel>
<slot />
</ComboboxGroup>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { MagnifyingGlassIcon } from '@radix-icons/vue'
import { ComboboxInput, type ComboboxInputProps, useForwardProps } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
defineOptions({
inheritAttrs: false,
})
const props = defineProps<ComboboxInputProps & {
class?: HTMLAttributes['class']
}>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
<MagnifyingGlassIcon class="mr-2 h-4 w-4 shrink-0 opacity-50" />
<ComboboxInput
v-bind="{ ...forwardedProps, ...$attrs }"
auto-focus
:class="cn('flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)"
/>
</div>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { ComboboxItemEmits, ComboboxItemProps } from 'radix-vue'
import { ComboboxItem, useForwardPropsEmits } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ComboboxItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxItem
v-bind="forwarded"
:class="cn('relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50', props.class)"
>
<slot />
</ComboboxItem>
</template>

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { ComboboxContentEmits, ComboboxContentProps } from 'radix-vue'
import { ComboboxContent, useForwardPropsEmits } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ComboboxContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ComboboxContent v-bind="forwarded" :class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)">
<div role="presentation">
<slot />
</div>
</ComboboxContent>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import type { ComboboxSeparatorProps } from 'radix-vue'
import { ComboboxSeparator } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<ComboboxSeparator
v-bind="delegatedProps"
:class="cn('-mx-1 h-px bg-border', props.class)"
>
<slot />
</ComboboxSeparator>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)">
<slot />
</span>
</template>

View File

@@ -0,0 +1,9 @@
export { default as Command } from './Command.vue'
export { default as CommandDialog } from './CommandDialog.vue'
export { default as CommandEmpty } from './CommandEmpty.vue'
export { default as CommandGroup } from './CommandGroup.vue'
export { default as CommandInput } from './CommandInput.vue'
export { default as CommandItem } from './CommandItem.vue'
export { default as CommandList } from './CommandList.vue'
export { default as CommandSeparator } from './CommandSeparator.vue'
export { default as CommandShortcut } from './CommandShortcut.vue'

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'radix-vue'
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot v-bind="forwarded">
<slot />
</DialogRoot>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogClose, type DialogCloseProps } from 'radix-vue'
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose v-bind="props">
<slot />
</DialogClose>
</template>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from 'radix-vue'
import { Cross2Icon } from '@radix-icons/vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<DialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class,
)"
>
<slot />
<DialogClose
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
>
<Cross2Icon class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DialogDescription
v-bind="forwardedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot />
</DialogDescription>
</template>

View File

@@ -0,0 +1,12 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
</script>
<template>
<div :class="cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from 'radix-vue'
import { X } from 'lucide-vue-next'
import { cn } from '@renderer/lib/utils'
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
<DialogContent
:class="
cn(
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
props.class,
)
"
v-bind="forwarded"
@pointer-down-outside="(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
event.preventDefault();
}
}"
>
<slot />
<DialogClose
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
>
<Cross2Icon class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>

View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { DialogTitle, type DialogTitleProps, useForwardProps } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DialogTitle
v-bind="forwardedProps"
:class="
cn(
'text-lg font-semibold leading-none tracking-tight',
props.class,
)
"
>
<slot />
</DialogTitle>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogTrigger, type DialogTriggerProps } from 'radix-vue'
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger v-bind="props">
<slot />
</DialogTrigger>
</template>

View File

@@ -0,0 +1,9 @@
export { default as Dialog } from './Dialog.vue'
export { default as DialogClose } from './DialogClose.vue'
export { default as DialogTrigger } from './DialogTrigger.vue'
export { default as DialogHeader } from './DialogHeader.vue'
export { default as DialogTitle } from './DialogTitle.vue'
export { default as DialogDescription } from './DialogDescription.vue'
export { default as DialogContent } from './DialogContent.vue'
export { default as DialogScrollContent } from './DialogScrollContent.vue'
export { default as DialogFooter } from './DialogFooter.vue'

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { DropdownMenuRoot, type DropdownMenuRootEmits, type DropdownMenuRootProps, useForwardPropsEmits } from 'radix-vue'
const props = defineProps<DropdownMenuRootProps>()
const emits = defineEmits<DropdownMenuRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRoot v-bind="forwarded">
<slot />
</DropdownMenuRoot>
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
DropdownMenuCheckboxItem,
type DropdownMenuCheckboxItemEmits,
type DropdownMenuCheckboxItemProps,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from 'radix-vue'
import { CheckIcon } from '@radix-icons/vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuCheckboxItem
v-bind="forwarded"
:class=" cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<CheckIcon class="w-4 h-4" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
DropdownMenuContent,
type DropdownMenuContentEmits,
type DropdownMenuContentProps,
DropdownMenuPortal,
useForwardPropsEmits,
} from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
{
sideOffset: 4,
},
)
const emits = defineEmits<DropdownMenuContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
v-bind="forwarded"
:class="cn('z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'radix-vue'
const props = defineProps<DropdownMenuGroupProps>()
</script>
<template>
<DropdownMenuGroup v-bind="props">
<slot />
</DropdownMenuGroup>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes['class']; inset?: boolean }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuItem
v-bind="forwardedProps"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
props.class,
)"
>
<slot />
</DropdownMenuItem>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from 'radix-vue'
import { cn } from '@renderer/lib/utils'
const props = defineProps<
DropdownMenuLabelProps & { class?: HTMLAttributes['class']; inset?: boolean }
>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuLabel
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
DropdownMenuRadioGroup,
type DropdownMenuRadioGroupEmits,
type DropdownMenuRadioGroupProps,
useForwardPropsEmits,
} from 'radix-vue'
const props = defineProps<DropdownMenuRadioGroupProps>()
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRadioGroup v-bind="forwarded">
<slot />
</DropdownMenuRadioGroup>
</template>

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