Files
onyx/desktop/src/index.html
T

564 lines
15 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Onyx</title>
<link
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<style>
:root {
--background-900: #f5f5f5;
--background-800: #ffffff;
--text-light-05: rgba(0, 0, 0, 0.95);
--text-light-03: rgba(0, 0, 0, 0.6);
--white-10: rgba(0, 0, 0, 0.1);
--white-15: rgba(0, 0, 0, 0.15);
--white-20: rgba(0, 0, 0, 0.2);
--white-30: rgba(0, 0, 0, 0.3);
--font-hanken-grotesk:
"Hanken Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, sans-serif;
}
.dark {
--background-900: #1a1a1a;
--background-800: #262626;
--text-light-05: rgba(255, 255, 255, 0.95);
--text-light-03: rgba(255, 255, 255, 0.6);
--white-10: rgba(255, 255, 255, 0.08);
--white-15: rgba(255, 255, 255, 0.12);
--white-20: rgba(255, 255, 255, 0.15);
--white-30: rgba(255, 255, 255, 0.25);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-hanken-grotesk);
background: linear-gradient(
135deg,
var(--background-900) 0%,
var(--background-800) 100%
);
min-height: 100vh;
color: var(--text-light-05);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
-webkit-user-select: none;
user-select: none;
transition:
background 0.3s ease,
color 0.3s ease;
}
.titlebar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 28px;
-webkit-app-region: drag;
z-index: 10000;
}
.settings-container {
max-width: 500px;
width: 100%;
opacity: 0;
transform: translateY(8px);
pointer-events: none;
transition:
opacity 0.18s ease,
transform 0.18s ease;
}
body.show-settings .settings-container {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.settings-panel {
background: var(--background-800);
backdrop-filter: blur(24px);
border-radius: 16px;
border: 1px solid var(--white-10);
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
transition:
background 0.3s ease,
border 0.3s ease;
}
.dark .settings-panel {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.settings-header {
padding: 24px;
border-bottom: 1px solid var(--white-10);
display: flex;
align-items: center;
gap: 12px;
}
.settings-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: var(--background-900);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: background 0.3s ease;
}
.settings-icon svg {
width: 24px;
height: 24px;
color: var(--text-light-05);
transition: color 0.3s ease;
}
.settings-title {
font-size: 20px;
font-weight: 600;
color: var(--text-light-05);
}
.settings-content {
padding: 24px;
}
.settings-section {
margin-bottom: 32px;
}
.settings-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-light-03);
margin-bottom: 12px;
}
.settings-group {
background: var(--background-900);
border-radius: 16px;
padding: 4px;
transition: background 0.3s ease;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
}
.setting-row-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.setting-label {
font-size: 14px;
font-weight: 400;
color: var(--text-light-05);
}
.setting-description {
font-size: 12px;
color: var(--text-light-03);
}
.setting-divider {
height: 1px;
background: var(--white-10);
margin: 0 4px;
}
.input-field {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--white-10);
border-radius: 8px;
font-size: 14px;
background: var(--background-800);
color: var(--text-light-05);
font-family: var(--font-hanken-grotesk);
transition: all 0.2s;
-webkit-app-region: no-drag;
}
.input-field:focus {
outline: none;
border-color: var(--white-30);
background: var(--background-900);
box-shadow: 0 0 0 2px var(--white-10);
}
.input-field::placeholder {
color: var(--text-light-03);
}
.input-field.error {
border-color: #ef4444;
}
.error-message {
color: #ef4444;
font-size: 12px;
margin-top: 4px;
padding-left: 12px;
display: none;
}
.error-message.visible {
display: block;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--white-15);
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: var(--background-800);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
transition: 0.3s;
border-radius: 50%;
}
.dark .toggle-slider:before {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
input:checked + .toggle-slider {
background-color: var(--white-30);
}
input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.button {
padding: 12px 24px;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.2s;
font-family: var(--font-hanken-grotesk);
width: 100%;
margin-top: 24px;
-webkit-app-region: no-drag;
}
.button.primary {
background: #286df8;
color: white;
}
.button.primary:hover {
background: #1e5cd6;
box-shadow: 0 4px 12px rgba(40, 109, 248, 0.3);
}
.button.primary:disabled {
opacity: 0.5;
cursor: not-allowed;
box-shadow: none;
}
kbd {
background: var(--white-10);
border: 1px solid var(--white-15);
border-radius: 4px;
padding: 2px 6px;
font-family: monospace;
font-weight: 500;
color: var(--text-light-05);
font-size: 11px;
transition: all 0.3s ease;
}
</style>
</head>
<body>
<div class="titlebar"></div>
<div class="settings-container">
<div class="settings-panel">
<div class="settings-header">
<div class="settings-icon">
<svg
viewBox="0 0 56 56"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M28 0 10.869 7.77 28 15.539l17.131-7.77L28 0Zm0 40.461-17.131 7.77L28 56l17.131-7.77L28 40.461Zm20.231-29.592L56 28.001l-7.769 17.131L40.462 28l7.769-17.131ZM15.538 28 7.77 10.869 0 28l7.769 17.131L15.538 28Z"
/>
</svg>
</div>
<h1 class="settings-title">Settings</h1>
</div>
<div class="settings-content">
<section class="settings-section">
<div class="section-title">GENERAL</div>
<div class="settings-group">
<div class="setting-row">
<div class="setting-row-content">
<label class="setting-label" for="onyxDomain"
>Server URL</label
>
<div class="setting-description">
The URL for your Onyx instance
</div>
</div>
</div>
<div class="setting-divider"></div>
<div class="setting-row" style="padding: 12px">
<input
type="text"
id="onyxDomain"
class="input-field"
placeholder="https://cloud.onyx.app"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
</div>
<div class="error-message" id="errorMessage">
Please enter a valid URL starting with http:// or https://
</div>
</div>
</section>
<button class="button primary" id="saveBtn">Save & Connect</button>
</div>
</div>
</div>
<script>
// Import Tauri API
const { invoke } = window.__TAURI__.core;
// Configuration
const DEFAULT_DOMAIN = "https://cloud.onyx.app";
let currentServerUrl = "";
// DOM elements
const domainInput = document.getElementById("onyxDomain");
const errorMessage = document.getElementById("errorMessage");
const saveBtn = document.getElementById("saveBtn");
// Theme detection based on system preferences
function applySystemTheme() {
const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
function updateTheme(e) {
if (e.matches) {
document.documentElement.classList.add("dark");
document.body.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
document.body.classList.remove("dark");
}
}
// Apply initial theme
updateTheme(darkModeQuery);
// Listen for changes
darkModeQuery.addEventListener("change", updateTheme);
}
function showSettings() {
document.body.classList.add("show-settings");
}
// Apply system theme immediately
applySystemTheme();
// Initialize the app
async function init() {
try {
const bootstrap = await invoke("get_bootstrap_state");
currentServerUrl = bootstrap.server_url;
// Set the input value
domainInput.value = currentServerUrl || DEFAULT_DOMAIN;
// Check if user came here explicitly (via Settings menu/shortcut)
const urlParams = new URLSearchParams(window.location.search);
const isExplicitSettings =
window.location.hash === "#settings" ||
urlParams.get("settings") === "true";
// If user explicitly opened settings, show modal
if (isExplicitSettings) {
// Modal is already visible, user can edit and save
showSettings();
return;
}
// Otherwise, check if this is first launch
// First launch = config doesn't exist
if (!bootstrap.config_exists || !currentServerUrl) {
// First launch - show modal, require user to configure
showSettings();
return;
}
// Not first launch and not explicit settings — confirm the server
// is reachable before handing the webview over. Otherwise the user
// lands on a native "connection refused" page with no way back.
try {
await invoke("check_server_reachable");
} catch (reachErr) {
showSettings();
showError(
`Could not connect to ${currentServerUrl}. Check the URL and your network connection.`
);
return;
}
window.location.href = currentServerUrl;
} catch (error) {
// On error, default to cloud
domainInput.value = DEFAULT_DOMAIN;
showSettings();
}
}
// Validate URL
function validateUrl(url) {
const trimmedUrl = url.trim();
if (!trimmedUrl) {
return { valid: false, error: "URL cannot be empty" };
}
if (
!trimmedUrl.startsWith("http://") &&
!trimmedUrl.startsWith("https://")
) {
return {
valid: false,
error: "URL must start with http:// or https://",
};
}
try {
new URL(trimmedUrl);
return { valid: true, url: trimmedUrl };
} catch {
return { valid: false, error: "Please enter a valid URL" };
}
}
// Show error
function showError(message) {
domainInput.classList.add("error");
errorMessage.textContent = message;
errorMessage.classList.add("visible");
}
// Clear error
function clearError() {
domainInput.classList.remove("error");
errorMessage.classList.remove("visible");
}
// Save configuration
async function saveConfiguration() {
clearError();
const validation = validateUrl(domainInput.value);
if (!validation.valid) {
showError(validation.error);
return;
}
try {
saveBtn.disabled = true;
saveBtn.textContent = "Saving...";
// Call Tauri command to save the URL
await invoke("set_server_url", { url: validation.url });
// Success - redirect to the new URL (login page)
window.location.href = validation.url;
} catch (error) {
showError(error || "Failed to save configuration");
saveBtn.disabled = false;
saveBtn.textContent = "Save & Connect";
}
}
// Event listeners
domainInput.addEventListener("input", clearError);
domainInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
saveConfiguration();
}
});
saveBtn.addEventListener("click", saveConfiguration);
// Initialize when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
</script>
</body>
</html>