- Add Tauri commands to inspect and adopt the gateway repo root - Retry locked vault commands by prompting for unlock - Improve mobile layout, editor mode toggles, and settings UI
925 lines
23 KiB
Svelte
925 lines
23 KiB
Svelte
<!-- @format -->
|
|
<script lang="ts">
|
|
import { goto } from "$app/navigation";
|
|
import AppModal from "$lib/components/AppModal.svelte";
|
|
import Navbar from "$lib/components/Navbar.svelte";
|
|
import {
|
|
addFragmentType,
|
|
addSettingsTag,
|
|
removeFragmentType,
|
|
removeSettingsTag,
|
|
setDefaultStartupView,
|
|
settingsDefaultStartupView,
|
|
settingsFragmentTypes,
|
|
settingsTags,
|
|
updateFragmentType,
|
|
updateSettingsTag,
|
|
} from "$lib/stores/settings";
|
|
import { invoke, isTauriRuntime } from "$lib/runtime/invoke";
|
|
import { onMount } from "svelte";
|
|
|
|
const activeSection = "settings";
|
|
|
|
let modalOpen = false;
|
|
let modalTitle = "";
|
|
let modalMessage = "";
|
|
let modalConfirmText = "OK";
|
|
let modalCancelText = "Cancel";
|
|
let modalShowCancel = false;
|
|
let modalTone: "default" | "danger" = "default";
|
|
let modalAction: "logout-confirm" | "logout-info" | null = null;
|
|
let newTag = "";
|
|
let newFragmentType = "";
|
|
let editingTagIndex: number | null = null;
|
|
let editingTagValue = "";
|
|
let editingFragmentTypeIndex: number | null = null;
|
|
let editingFragmentTypeValue = "";
|
|
let sidecarRoot = "";
|
|
let sidecarRootIsCustom = false;
|
|
let sidecarRootError = "";
|
|
let sidecarBrowseBusy = false;
|
|
let gatewayRootBusy = false;
|
|
let gatewayRootError = "";
|
|
let gatewayRootStatus: {
|
|
authoritativeRoot: string;
|
|
gatewayConfigPath: string;
|
|
configuredRoot?: string | null;
|
|
needsAdoption: boolean;
|
|
} | null = null;
|
|
let returnSection = "entries";
|
|
|
|
onMount(async () => {
|
|
const queryReturn =
|
|
new URLSearchParams(window.location.search)
|
|
.get("return")
|
|
?.trim()
|
|
.toLowerCase() ?? "";
|
|
if (
|
|
["entries", "calendar", "fragments", "todos", "lists"].includes(
|
|
queryReturn,
|
|
)
|
|
) {
|
|
returnSection = queryReturn;
|
|
}
|
|
|
|
try {
|
|
const result: any = await invoke("get_sidecar_root");
|
|
sidecarRoot = result.root;
|
|
sidecarRootIsCustom = result.isCustom;
|
|
} catch (e) {
|
|
sidecarRootError = String(e);
|
|
}
|
|
|
|
await refreshGatewayRootStatus();
|
|
});
|
|
|
|
async function saveSidecarRoot() {
|
|
sidecarRootError = "";
|
|
try {
|
|
const result: any = await invoke("set_sidecar_root", {
|
|
path: sidecarRoot,
|
|
});
|
|
sidecarRoot = result.root;
|
|
sidecarRootIsCustom = result.isCustom;
|
|
await refreshGatewayRootStatus();
|
|
} catch (e) {
|
|
sidecarRootError = String(e);
|
|
}
|
|
}
|
|
|
|
async function resetSidecarRoot() {
|
|
sidecarRootError = "";
|
|
try {
|
|
const result: any = await invoke("set_sidecar_root", { path: "" });
|
|
sidecarRoot = result.root;
|
|
sidecarRootIsCustom = result.isCustom;
|
|
await refreshGatewayRootStatus();
|
|
} catch (e) {
|
|
sidecarRootError = String(e);
|
|
}
|
|
}
|
|
|
|
async function browseSidecarRoot() {
|
|
sidecarRootError = "";
|
|
if (!isTauriRuntime()) {
|
|
sidecarRootError = "Folder picker is only available in desktop app.";
|
|
return;
|
|
}
|
|
sidecarBrowseBusy = true;
|
|
try {
|
|
const { open } = await import("@tauri-apps/plugin-dialog");
|
|
const picked = await open({
|
|
directory: true,
|
|
multiple: false,
|
|
title: "Select Sidecar Root Directory",
|
|
});
|
|
if (typeof picked === "string" && picked.trim()) {
|
|
sidecarRoot = picked;
|
|
await saveSidecarRoot();
|
|
}
|
|
} catch (e) {
|
|
sidecarRootError = String(e);
|
|
} finally {
|
|
sidecarBrowseBusy = false;
|
|
}
|
|
}
|
|
|
|
async function refreshGatewayRootStatus() {
|
|
gatewayRootError = "";
|
|
gatewayRootStatus = null;
|
|
if (!isTauriRuntime()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
gatewayRootStatus = await invoke<any>("get_gateway_root_status");
|
|
} catch (e) {
|
|
gatewayRootError = String(e);
|
|
}
|
|
}
|
|
|
|
async function adoptGatewayRoot() {
|
|
gatewayRootError = "";
|
|
if (!isTauriRuntime()) {
|
|
gatewayRootError =
|
|
"Gateway root adoption is only available in the desktop app.";
|
|
return;
|
|
}
|
|
|
|
gatewayRootBusy = true;
|
|
try {
|
|
gatewayRootStatus = await invoke<any>("adopt_sidecar_root_for_gateway");
|
|
} catch (e) {
|
|
gatewayRootError = String(e);
|
|
} finally {
|
|
gatewayRootBusy = false;
|
|
}
|
|
}
|
|
|
|
function showModal(options: {
|
|
action: "logout-confirm" | "logout-info";
|
|
title: string;
|
|
message: string;
|
|
confirmText?: string;
|
|
cancelText?: string;
|
|
showCancel?: boolean;
|
|
tone?: "default" | "danger";
|
|
}) {
|
|
modalAction = options.action;
|
|
modalTitle = options.title;
|
|
modalMessage = options.message;
|
|
modalConfirmText = options.confirmText ?? "OK";
|
|
modalCancelText = options.cancelText ?? "Cancel";
|
|
modalShowCancel = options.showCancel ?? false;
|
|
modalTone = options.tone ?? "default";
|
|
modalOpen = true;
|
|
}
|
|
|
|
function closeModal() {
|
|
modalOpen = false;
|
|
modalAction = null;
|
|
}
|
|
|
|
function handleModalConfirm() {
|
|
if (modalAction === "logout-confirm") {
|
|
showModal({
|
|
action: "logout-info",
|
|
title: "Logout Requested",
|
|
message: "You have been logged out.",
|
|
confirmText: "Close",
|
|
});
|
|
return;
|
|
}
|
|
|
|
closeModal();
|
|
}
|
|
|
|
function handleSelect(id: string) {
|
|
if (id === "logout") {
|
|
showModal({
|
|
action: "logout-confirm",
|
|
title: "Confirm Logout",
|
|
message: "Are you sure you want to log out?",
|
|
confirmText: "Log Out",
|
|
cancelText: "Cancel",
|
|
showCancel: true,
|
|
tone: "danger",
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (id === "settings") {
|
|
return;
|
|
}
|
|
|
|
if (id === "account") {
|
|
goto("/account");
|
|
return;
|
|
}
|
|
|
|
goto(`/?section=${encodeURIComponent(id)}`);
|
|
}
|
|
|
|
function closeSettings() {
|
|
goto(`/?section=${encodeURIComponent(returnSection)}`);
|
|
}
|
|
|
|
function addTag() {
|
|
if (addSettingsTag(newTag)) {
|
|
newTag = "";
|
|
}
|
|
}
|
|
|
|
function startEditTag(index: number, tag: string) {
|
|
editingTagIndex = index;
|
|
editingTagValue = tag;
|
|
}
|
|
|
|
function saveEditTag() {
|
|
if (editingTagIndex === null) return;
|
|
if (updateSettingsTag(editingTagIndex, editingTagValue)) {
|
|
editingTagIndex = null;
|
|
editingTagValue = "";
|
|
}
|
|
}
|
|
|
|
function cancelEditTag() {
|
|
editingTagIndex = null;
|
|
editingTagValue = "";
|
|
}
|
|
|
|
function removeTag(index: number) {
|
|
if (!removeSettingsTag(index)) return;
|
|
if (editingTagIndex === index) {
|
|
cancelEditTag();
|
|
}
|
|
}
|
|
|
|
function addFragmentTypeLocal() {
|
|
if (addFragmentType(newFragmentType)) {
|
|
newFragmentType = "";
|
|
}
|
|
}
|
|
|
|
function startEditFragmentType(index: number, fragmentType: string) {
|
|
editingFragmentTypeIndex = index;
|
|
editingFragmentTypeValue = fragmentType;
|
|
}
|
|
|
|
function saveEditFragmentType() {
|
|
if (editingFragmentTypeIndex === null) return;
|
|
if (
|
|
updateFragmentType(editingFragmentTypeIndex, editingFragmentTypeValue)
|
|
) {
|
|
editingFragmentTypeIndex = null;
|
|
editingFragmentTypeValue = "";
|
|
}
|
|
}
|
|
|
|
function cancelEditFragmentType() {
|
|
editingFragmentTypeIndex = null;
|
|
editingFragmentTypeValue = "";
|
|
}
|
|
|
|
function removeFragmentTypeLocal(index: number) {
|
|
if (!removeFragmentType(index)) return;
|
|
if (editingFragmentTypeIndex === index) {
|
|
cancelEditFragmentType();
|
|
}
|
|
}
|
|
|
|
function updateDefaultStartupView(value: string) {
|
|
setDefaultStartupView(value);
|
|
}
|
|
</script>
|
|
|
|
<div class="app-shell panel-closed">
|
|
<Navbar {activeSection} onSelect={handleSelect} />
|
|
|
|
<main class="route-view">
|
|
<header class="route-header">
|
|
<div class="route-header-main">
|
|
<h1>Settings</h1>
|
|
<p>Configure app behavior and interface options.</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="header-close-btn"
|
|
on:click={closeSettings}
|
|
aria-label="Close settings"
|
|
>
|
|
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
|
</button>
|
|
</header>
|
|
|
|
<div class="settings-grid">
|
|
<section class="route-card">
|
|
<div class="card-head">
|
|
<h2 class="card-title">
|
|
<span class="material-symbols-outlined" aria-hidden="true"
|
|
>rocket_launch</span
|
|
>
|
|
Startup
|
|
</h2>
|
|
</div>
|
|
<label>
|
|
Default startup view
|
|
<select
|
|
value={$settingsDefaultStartupView}
|
|
on:change={(event) =>
|
|
updateDefaultStartupView(
|
|
(event.currentTarget as HTMLSelectElement).value,
|
|
)}
|
|
>
|
|
<option value="entries">Entries</option>
|
|
<option value="calendar">Calendar</option>
|
|
<option value="fragments">Fragments</option>
|
|
<option value="todos">To-Do List</option>
|
|
<option value="lists">Lists</option>
|
|
</select>
|
|
</label>
|
|
</section>
|
|
|
|
<section class="route-card list-card">
|
|
<div class="card-head">
|
|
<h2 class="card-title">
|
|
<span class="material-symbols-outlined" aria-hidden="true"
|
|
>label</span
|
|
>
|
|
Tags
|
|
</h2>
|
|
<p class="section-copy">
|
|
Add and manage tags used for notes and entries.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="create-row">
|
|
<input
|
|
type="text"
|
|
placeholder="Add tag (example: Research)"
|
|
bind:value={newTag}
|
|
on:keydown={(event) => event.key === "Enter" && addTag()}
|
|
/>
|
|
<button type="button" class="secondary-btn" on:click={addTag}
|
|
>Add</button
|
|
>
|
|
</div>
|
|
|
|
<ul class="item-list">
|
|
{#each $settingsTags as tag, index}
|
|
<li class="item-row">
|
|
{#if editingTagIndex === index}
|
|
<input
|
|
type="text"
|
|
bind:value={editingTagValue}
|
|
on:keydown={(event) => {
|
|
if (event.key === "Enter") saveEditTag();
|
|
if (event.key === "Escape") cancelEditTag();
|
|
}}
|
|
/>
|
|
<div class="row-actions">
|
|
<button
|
|
type="button"
|
|
class="secondary-btn"
|
|
on:click={saveEditTag}>Save</button
|
|
>
|
|
<button
|
|
type="button"
|
|
class="ghost-btn"
|
|
on:click={cancelEditTag}>Cancel</button
|
|
>
|
|
</div>
|
|
{:else}
|
|
<span>{tag}</span>
|
|
<div class="row-actions">
|
|
<button
|
|
type="button"
|
|
class="ghost-btn"
|
|
on:click={() => startEditTag(index, tag)}>Edit</button
|
|
>
|
|
<button
|
|
type="button"
|
|
class="danger-btn"
|
|
on:click={() => removeTag(index)}>Remove</button
|
|
>
|
|
</div>
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</section>
|
|
|
|
<section class="route-card list-card">
|
|
<div class="card-head">
|
|
<h2 class="card-title">
|
|
<span class="material-symbols-outlined" aria-hidden="true"
|
|
>category</span
|
|
>
|
|
Fragment Types
|
|
</h2>
|
|
<p class="section-copy">
|
|
Configure custom fragment types for the Fragments section.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="create-row">
|
|
<input
|
|
type="text"
|
|
placeholder="Add fragment type (example: Observation)"
|
|
bind:value={newFragmentType}
|
|
on:keydown={(event) =>
|
|
event.key === "Enter" && addFragmentTypeLocal()}
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="secondary-btn"
|
|
on:click={addFragmentTypeLocal}>Add</button
|
|
>
|
|
</div>
|
|
|
|
<ul class="item-list">
|
|
{#each $settingsFragmentTypes as type, index}
|
|
<li class="item-row">
|
|
{#if editingFragmentTypeIndex === index}
|
|
<input
|
|
type="text"
|
|
bind:value={editingFragmentTypeValue}
|
|
on:keydown={(event) => {
|
|
if (event.key === "Enter") saveEditFragmentType();
|
|
if (event.key === "Escape") cancelEditFragmentType();
|
|
}}
|
|
/>
|
|
<div class="row-actions">
|
|
<button
|
|
type="button"
|
|
class="secondary-btn"
|
|
on:click={saveEditFragmentType}>Save</button
|
|
>
|
|
<button
|
|
type="button"
|
|
class="ghost-btn"
|
|
on:click={cancelEditFragmentType}>Cancel</button
|
|
>
|
|
</div>
|
|
{:else}
|
|
<span>{type}</span>
|
|
<div class="row-actions">
|
|
<button
|
|
type="button"
|
|
class="ghost-btn"
|
|
on:click={() => startEditFragmentType(index, type)}
|
|
>Edit</button
|
|
>
|
|
<button
|
|
type="button"
|
|
class="danger-btn"
|
|
on:click={() => removeFragmentTypeLocal(index)}
|
|
>Remove</button
|
|
>
|
|
</div>
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</section>
|
|
|
|
<section class="route-card">
|
|
<div class="card-head">
|
|
<h2 class="card-title">
|
|
<span class="material-symbols-outlined" aria-hidden="true">hub</span
|
|
>
|
|
Sidecar
|
|
</h2>
|
|
<p class="section-copy">
|
|
Root directory containing the Journal.Sidecar project.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="create-row">
|
|
<input
|
|
type="text"
|
|
placeholder="Auto-detected from working directory"
|
|
bind:value={sidecarRoot}
|
|
on:keydown={(event) => event.key === "Enter" && saveSidecarRoot()}
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="ghost-btn"
|
|
on:click={browseSidecarRoot}
|
|
disabled={sidecarBrowseBusy}
|
|
>
|
|
{sidecarBrowseBusy ? "Browsing..." : "Browse"}
|
|
</button>
|
|
{#if sidecarRootIsCustom}
|
|
<button type="button" class="ghost-btn" on:click={resetSidecarRoot}
|
|
>Reset</button
|
|
>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if sidecarRootError}
|
|
<p class="error-text">{sidecarRootError}</p>
|
|
{/if}
|
|
</section>
|
|
|
|
<section class="route-card">
|
|
<div class="card-head">
|
|
<h2 class="card-title">
|
|
<span class="material-symbols-outlined" aria-hidden="true"
|
|
>publish</span
|
|
>
|
|
Gateway Root
|
|
</h2>
|
|
<p class="section-copy">
|
|
Published WebGateway builds should point at one authoritative root.
|
|
Use this one-time adopt action to align the packaged gateway with
|
|
the desktop app.
|
|
</p>
|
|
</div>
|
|
|
|
{#if isTauriRuntime()}
|
|
<div class="row-actions">
|
|
<button
|
|
type="button"
|
|
class="ghost-btn"
|
|
on:click={refreshGatewayRootStatus}
|
|
disabled={gatewayRootBusy}
|
|
>
|
|
Refresh
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="secondary-btn"
|
|
on:click={adoptGatewayRoot}
|
|
disabled={gatewayRootBusy}
|
|
>
|
|
{gatewayRootBusy ? "Adopting..." : "Adopt Current Root"}
|
|
</button>
|
|
</div>
|
|
|
|
{#if gatewayRootStatus}
|
|
<label>
|
|
Desktop authoritative root
|
|
<input
|
|
type="text"
|
|
value={gatewayRootStatus.authoritativeRoot}
|
|
readonly
|
|
/>
|
|
</label>
|
|
<label>
|
|
Gateway config path
|
|
<input
|
|
type="text"
|
|
value={gatewayRootStatus.gatewayConfigPath}
|
|
readonly
|
|
/>
|
|
</label>
|
|
<label>
|
|
Gateway configured root
|
|
<input
|
|
type="text"
|
|
value={gatewayRootStatus.configuredRoot ?? "(not set)"}
|
|
readonly
|
|
/>
|
|
</label>
|
|
<label>
|
|
Status
|
|
<input
|
|
type="text"
|
|
value={gatewayRootStatus.needsAdoption ? "Needs adoption" : "Aligned"}
|
|
readonly
|
|
/>
|
|
</label>
|
|
{/if}
|
|
{:else}
|
|
<p class="section-copy">
|
|
Configure `GatewaySettings:RepoRoot` or `JOURNAL_PROJECT_ROOT`
|
|
before starting the published gateway.
|
|
</p>
|
|
{/if}
|
|
|
|
{#if gatewayRootError}
|
|
<p class="error-text">{gatewayRootError}</p>
|
|
{/if}
|
|
</section>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<AppModal
|
|
open={modalOpen}
|
|
title={modalTitle}
|
|
message={modalMessage}
|
|
confirmText={modalConfirmText}
|
|
cancelText={modalCancelText}
|
|
showCancel={modalShowCancel}
|
|
tone={modalTone}
|
|
onConfirm={handleModalConfirm}
|
|
onCancel={closeModal}
|
|
/>
|
|
|
|
<style>
|
|
.route-view {
|
|
height: 100vh;
|
|
height: 100dvh;
|
|
min-height: 0;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
overflow: hidden;
|
|
background: var(--bg-editor);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.route-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
padding: 2px 2px 0;
|
|
}
|
|
|
|
.route-header-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.route-header h1 {
|
|
font-size: 1.2rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.01em;
|
|
}
|
|
|
|
.route-header p {
|
|
color: var(--text-muted);
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.header-close-btn {
|
|
width: 32px;
|
|
height: 32px;
|
|
flex-shrink: 0;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border-soft);
|
|
background: color-mix(in srgb, var(--surface-1) 92%, var(--bg-editor) 8%);
|
|
color: var(--text-muted);
|
|
display: grid;
|
|
place-items: center;
|
|
cursor: pointer;
|
|
transition:
|
|
background-color 120ms ease,
|
|
color 120ms ease,
|
|
border-color 120ms ease,
|
|
transform 120ms ease;
|
|
}
|
|
|
|
.header-close-btn .material-symbols-outlined {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.header-close-btn:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
border-color: var(--border-strong);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.settings-grid {
|
|
flex: 1;
|
|
min-height: 0;
|
|
columns: 2;
|
|
column-gap: 14px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.route-card {
|
|
break-inside: avoid;
|
|
border: 1px solid var(--border-soft);
|
|
background: color-mix(in srgb, var(--surface-1) 92%, var(--bg-editor) 8%);
|
|
border-radius: 12px;
|
|
padding: 14px 14px 13px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
box-shadow:
|
|
inset 0 1px 0 color-mix(in srgb, var(--zinc-300) 8%, transparent 92%),
|
|
0 8px 24px color-mix(in srgb, var(--bg-app) 32%, transparent 68%);
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.list-card {
|
|
overflow: hidden;
|
|
max-height: clamp(280px, 46vh, 520px);
|
|
}
|
|
|
|
.card-head {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.card-title {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 0.96rem;
|
|
font-weight: 650;
|
|
color: var(--text-primary);
|
|
letter-spacing: 0.01em;
|
|
}
|
|
|
|
.card-title .material-symbols-outlined {
|
|
font-size: 1rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.route-card label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
color: var(--text-muted);
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.01em;
|
|
}
|
|
|
|
.route-card select {
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 8px;
|
|
background-color: color-mix(
|
|
in srgb,
|
|
var(--surface-2) 88%,
|
|
var(--bg-editor) 12%
|
|
);
|
|
color: var(--text-primary);
|
|
padding: 9px 34px 9px 10px;
|
|
font-size: 0.84rem;
|
|
}
|
|
|
|
.section-copy {
|
|
font-size: 0.8rem;
|
|
color: var(--text-muted);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.create-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.create-row input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.item-list {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
padding-right: 2px;
|
|
}
|
|
|
|
.item-row {
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 8px;
|
|
padding: 9px 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
font-size: 0.83rem;
|
|
color: var(--text-primary);
|
|
background: color-mix(in srgb, var(--surface-1) 90%, var(--bg-editor) 10%);
|
|
}
|
|
|
|
.item-row input,
|
|
.route-card input {
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 8px;
|
|
background: color-mix(in srgb, var(--surface-2) 88%, var(--bg-editor) 12%);
|
|
color: var(--text-primary);
|
|
padding: 8px 10px;
|
|
font-size: 0.84rem;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.row-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.secondary-btn,
|
|
.ghost-btn,
|
|
.danger-btn {
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 8px;
|
|
padding: 7px 11px;
|
|
font-size: 0.76rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.01em;
|
|
cursor: pointer;
|
|
color: var(--text-primary);
|
|
transition:
|
|
background-color 120ms ease,
|
|
color 120ms ease,
|
|
border-color 120ms ease,
|
|
transform 120ms ease;
|
|
}
|
|
|
|
.secondary-btn {
|
|
background: color-mix(in srgb, var(--surface-2) 84%, var(--bg-hover) 16%);
|
|
border-color: var(--border-strong);
|
|
}
|
|
|
|
.ghost-btn {
|
|
background: color-mix(in srgb, var(--surface-1) 92%, var(--bg-editor) 8%);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.danger-btn {
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.secondary-btn:hover,
|
|
.ghost-btn:hover,
|
|
.danger-btn:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
border-color: var(--border-strong);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.error-text {
|
|
color: #e74c3c;
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
@media (max-width: 1100px) {
|
|
.settings-grid {
|
|
columns: 1;
|
|
overflow: auto;
|
|
padding-right: 2px;
|
|
}
|
|
|
|
.list-card {
|
|
max-height: min(42dvh, 460px);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
.route-view {
|
|
padding: 14px 12px;
|
|
gap: 14px;
|
|
}
|
|
|
|
.route-card {
|
|
border-radius: 10px;
|
|
padding: 12px;
|
|
gap: 10px;
|
|
}
|
|
|
|
.route-header {
|
|
align-items: center;
|
|
}
|
|
|
|
.create-row {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.create-row > * {
|
|
flex: 1 1 auto;
|
|
}
|
|
|
|
.list-card {
|
|
max-height: min(48dvh, 360px);
|
|
}
|
|
|
|
.item-row {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.item-row input,
|
|
.route-card input {
|
|
min-width: 0;
|
|
}
|
|
|
|
.row-actions {
|
|
width: 100%;
|
|
justify-content: flex-end;
|
|
}
|
|
}
|
|
</style>
|