|
| 1 | +//! Preferences modal body: a sidebar of categories + the active pane. |
| 2 | +//! |
| 3 | +//! App-level only (theme, editor, keyboard, security, telemetry, about) — never |
| 4 | +//! per-connection. The Theme control is wired to the `Preferences` signal; the |
| 5 | +//! remaining controls are visual placeholders, as in the mockup. Note the CSS |
| 6 | +//! port only defines OS-driven theming, so changing Theme updates state without |
| 7 | +//! a manual override repaint (faithful to the mockup, which is also static). |
| 8 | +
|
| 9 | +use dioxus::prelude::*; |
| 10 | + |
| 11 | +use crate::state::preferences::{Preferences, Theme}; |
| 12 | +use crate::state::ui::ModalKind; |
| 13 | + |
| 14 | +const CATS: [(&str, &str); 6] = [ |
| 15 | + ("appearance", "Appearance"), |
| 16 | + ("editor", "Editor"), |
| 17 | + ("keyboard", "Keyboard"), |
| 18 | + ("security", "Security"), |
| 19 | + ("telemetry", "Telemetry"), |
| 20 | + ("about", "About"), |
| 21 | +]; |
| 22 | + |
| 23 | +#[component] |
| 24 | +pub fn PreferencesPanes() -> Element { |
| 25 | + let mut modal = use_context::<Signal<Option<ModalKind>>>(); |
| 26 | + let mut pane = use_signal(|| "appearance".to_string()); |
| 27 | + let current = pane.read().clone(); |
| 28 | + |
| 29 | + rsx! { |
| 30 | + div { class: "prefs-layout", |
| 31 | + div { class: "prefs-sidebar", |
| 32 | + for (key, label) in CATS { |
| 33 | + { |
| 34 | + let is_active = current == key; |
| 35 | + let k = key.to_string(); |
| 36 | + rsx! { |
| 37 | + div { |
| 38 | + class: if is_active { "prefs-cat active" } else { "prefs-cat" }, |
| 39 | + onclick: move |_| pane.set(k.clone()), |
| 40 | + "{label}" |
| 41 | + } |
| 42 | + } |
| 43 | + } |
| 44 | + } |
| 45 | + } |
| 46 | + div { class: "prefs-content", |
| 47 | + match current.as_str() { |
| 48 | + "editor" => rsx! { EditorPane {} }, |
| 49 | + "keyboard" => rsx! { KeyboardPane {} }, |
| 50 | + "security" => rsx! { SecurityPane {} }, |
| 51 | + "telemetry" => rsx! { TelemetryPane {} }, |
| 52 | + "about" => rsx! { AboutPane {} }, |
| 53 | + _ => rsx! { AppearancePane {} }, |
| 54 | + } |
| 55 | + } |
| 56 | + } |
| 57 | + div { class: "modal-footer", |
| 58 | + button { class: "btn primary", onclick: move |_| modal.set(None), "Done" } |
| 59 | + } |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +#[component] |
| 64 | +fn AppearancePane() -> Element { |
| 65 | + let mut prefs = use_context::<Signal<Preferences>>(); |
| 66 | + let theme = prefs.read().theme; |
| 67 | + let cls = |t: Theme| if theme == t { "active" } else { "" }; |
| 68 | + rsx! { |
| 69 | + div { class: "prefs-pane active", |
| 70 | + h2 { "Appearance" } |
| 71 | + p { "How the studio looks. Follows your OS theme by default." } |
| 72 | + div { class: "form-field", |
| 73 | + label { "Theme" } |
| 74 | + div { class: "segmented", style: "display:flex;", |
| 75 | + button { class: cls(Theme::Light), onclick: move |_| prefs.write().theme = Theme::Light, "Light" } |
| 76 | + button { class: cls(Theme::Dark), onclick: move |_| prefs.write().theme = Theme::Dark, "Dark" } |
| 77 | + button { class: cls(Theme::System), onclick: move |_| prefs.write().theme = Theme::System, "Follow OS" } |
| 78 | + } |
| 79 | + } |
| 80 | + div { class: "form-field", |
| 81 | + label { "Density" } |
| 82 | + div { class: "segmented", style: "display:flex;", |
| 83 | + button { "Compact" } button { class: "active", "Comfortable" } button { "Spacious" } |
| 84 | + } |
| 85 | + } |
| 86 | + div { class: "form-row", |
| 87 | + div { class: "form-field", label { "UI font size" } select { option { "12px" } option { "13px (default)" } option { "14px" } } } |
| 88 | + div { class: "form-field", label { "Editor font" } select { option { "JetBrains Mono" } option { "SF Mono" } option { "Menlo" } } } |
| 89 | + } |
| 90 | + div { class: "form-field", |
| 91 | + label { "Accent color" } |
| 92 | + div { style: "display:flex; gap:8px;", |
| 93 | + div { style: "width:24px; height:24px; background:#1a1a18; border-radius:4px; outline: 2px solid var(--text-primary); outline-offset: 2px;" } |
| 94 | + div { style: "width:24px; height:24px; background:#185fa5; border-radius:4px;" } |
| 95 | + div { style: "width:24px; height:24px; background:#3b6d11; border-radius:4px;" } |
| 96 | + div { style: "width:24px; height:24px; background:#854f0b; border-radius:4px;" } |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +#[component] |
| 104 | +fn EditorPane() -> Element { |
| 105 | + rsx! { |
| 106 | + div { class: "prefs-pane active", |
| 107 | + h2 { "Editor" } |
| 108 | + p { "Code editor behavior across SQL, Cypher, and the REPL." } |
| 109 | + div { class: "form-row", |
| 110 | + div { class: "form-field", label { "Tab size" } select { option { "2 spaces" } option { "4 spaces (default)" } option { "Tab" } } } |
| 111 | + div { class: "form-field", label { "Word wrap" } select { option { "Off" } option { "On" } } } |
| 112 | + } |
| 113 | + div { class: "form-field", label { "Auto-format on save" } div { class: "segmented", style: "display:flex;", button { "Off" } button { class: "active", "On" } } } |
| 114 | + div { class: "form-field", label { "Show whitespace" } div { class: "segmented", style: "display:flex;", button { class: "active", "Off" } button { "On" } } } |
| 115 | + div { class: "form-field", label { "Bracket pair colorization" } div { class: "segmented", style: "display:flex;", button { "Off" } button { class: "active", "On" } } } |
| 116 | + } |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +#[component] |
| 121 | +fn KeyboardPane() -> Element { |
| 122 | + let rows = [ |
| 123 | + ("Command palette", "⌘K"), |
| 124 | + ("Disconnect", "⌘D"), |
| 125 | + ("Run query", "⌘↵"), |
| 126 | + ("Open preferences", "⌘,"), |
| 127 | + ("New query tab", "⌘T"), |
| 128 | + ("Toggle theme", "⌘⇧L"), |
| 129 | + ("Focus Explorer sidebar", "⌘B"), |
| 130 | + ]; |
| 131 | + rsx! { |
| 132 | + div { class: "prefs-pane active", |
| 133 | + h2 { "Keyboard shortcuts" } |
| 134 | + p { "Customize bindings. Defaults shown." } |
| 135 | + div { style: "font-family: var(--font-mono); font-size: 12px;", |
| 136 | + for (cmd, key) in rows { |
| 137 | + div { style: "display: grid; grid-template-columns: 1fr auto; padding: 8px 0; border-bottom: 0.5px solid var(--border-soft);", |
| 138 | + span { "{cmd}" } |
| 139 | + span { "{key}" } |
| 140 | + } |
| 141 | + } |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | +} |
| 146 | + |
| 147 | +#[component] |
| 148 | +fn SecurityPane() -> Element { |
| 149 | + rsx! { |
| 150 | + div { class: "prefs-pane active", |
| 151 | + h2 { "Security" } |
| 152 | + p { "Credential storage and session protections. These are app-level — per-connection auth settings live with each connection." } |
| 153 | + div { class: "form-field", label { "Credential storage" } select { option { "OS keychain (default)" } option { "Encrypted file" } option { "In-memory only" } } } |
| 154 | + div { class: "form-field", label { "Lock app after idle" } select { option { "Never" } option { "5 minutes" } option { "15 minutes" } option { "1 hour" } } } |
| 155 | + div { class: "form-field", label { "Confirm destructive queries" } div { class: "segmented", style: "display:flex;", button { "Off" } button { class: "active", "On" } } } |
| 156 | + div { class: "form-field", label { "Mask sensitive query results" } div { class: "segmented", style: "display:flex;", button { class: "active", "Off" } button { "On" } } } |
| 157 | + } |
| 158 | + } |
| 159 | +} |
| 160 | + |
| 161 | +#[component] |
| 162 | +fn TelemetryPane() -> Element { |
| 163 | + rsx! { |
| 164 | + div { class: "prefs-pane active", |
| 165 | + h2 { "Telemetry" } |
| 166 | + p { "Anonymous usage data helps improve NodeDB-Studio. Off by default." } |
| 167 | + div { class: "form-field", label { "Send anonymous usage data" } div { class: "segmented", style: "display:flex;", button { class: "active", "Off" } button { "On" } } } |
| 168 | + div { class: "form-field", label { "Send crash reports" } div { class: "segmented", style: "display:flex;", button { "Off" } button { class: "active", "On" } } } |
| 169 | + div { style: "font-family: var(--font-mono); font-size: 11px; color: var(--text-tertiary); margin-top: 10px; line-height: 1.6;", "No connection data, credentials, or query content is ever transmitted. Only anonymized UI interactions and error stack traces." } |
| 170 | + } |
| 171 | + } |
| 172 | +} |
| 173 | + |
| 174 | +#[component] |
| 175 | +fn AboutPane() -> Element { |
| 176 | + rsx! { |
| 177 | + div { class: "prefs-pane active", |
| 178 | + h2 { "About NodeDB-Studio" } |
| 179 | + p { "Database studio built on Dioxus + Rust." } |
| 180 | + div { style: "font-family: var(--font-mono); font-size: 11px; line-height: 1.8; margin-top: 14px;", |
| 181 | + AboutRow { k: "version", v: "dev" } |
| 182 | + AboutRow { k: "build", v: "2026.06.13" } |
| 183 | + AboutRow { k: "runtime", v: "dioxus 0.6" } |
| 184 | + AboutRow { k: "platform", v: "darwin-arm64" } |
| 185 | + AboutRow { k: "license", v: "Apache 2.0" } |
| 186 | + } |
| 187 | + div { style: "margin-top: 18px; display: flex; gap: 8px;", |
| 188 | + button { class: "btn small", "Check for updates" } |
| 189 | + button { class: "btn small", "Release notes" } |
| 190 | + button { class: "btn small", "View license" } |
| 191 | + } |
| 192 | + } |
| 193 | + } |
| 194 | +} |
| 195 | + |
| 196 | +#[component] |
| 197 | +fn AboutRow(k: String, v: String) -> Element { |
| 198 | + rsx! { |
| 199 | + div { |
| 200 | + span { style: "color:var(--text-tertiary); display:inline-block; width:80px;", "{k}" } |
| 201 | + "{v}" |
| 202 | + } |
| 203 | + } |
| 204 | +} |
0 commit comments