diff --git a/Cargo.lock b/Cargo.lock index 23936c2..f8ce4ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2330,7 +2330,6 @@ dependencies = [ "dioxus-code", "dioxus-code-editor", "strum", - "strum_macros", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 54dc2b6..28a1d47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,7 @@ edition = "2021" dioxus = { version = "0.7.1", features = ["router"] } dioxus-code = "0.1.2" dioxus-code-editor = { version = "0.1.2", features = ["all-languages"] } -strum = { version = "0.28.0", features = ["strum_macros", "derive"] } -strum_macros = "0.28.0" +strum = { version = "0.28.0", features = ["derive"] } [features] default = ["desktop"] diff --git a/assets/css/components.css b/assets/css/components.css index 924dadc..e935786 100644 --- a/assets/css/components.css +++ b/assets/css/components.css @@ -49,30 +49,6 @@ transform: scaleY(1); } -/* --- Workspace selector tab ------------------------------------------------- */ - -.workspace-selector { - padding: var(--dn-space-2) var(--dn-space-4); - border-radius: var(--dn-radius); - color: var(--dn-fg-muted); - font-size: var(--dn-text-base); - font-weight: 500; - text-decoration: none; - white-space: nowrap; - transition: color var(--dn-dur-fast) var(--dn-ease), - background-color var(--dn-dur-fast) var(--dn-ease); -} - -.workspace-selector:hover { - color: var(--dn-fg); - background: var(--dn-bg-raised); -} - -.workspace-selector.active { - color: var(--dn-fg-on-accent); - background: var(--dn-accent); -} - /* --- Buttons ---------------------------------------------------------------- */ .btn { diff --git a/assets/css/layout.css b/assets/css/layout.css index 046e469..a45b77f 100644 --- a/assets/css/layout.css +++ b/assets/css/layout.css @@ -25,15 +25,6 @@ padding: var(--dn-space-6); } -.workspace-selector-links { - display: flex; - gap: var(--dn-space-1); - padding: var(--dn-space-1); - background: var(--dn-bg-sunken); - border: 1px solid var(--dn-border); - border-radius: var(--dn-radius-lg); -} - .workspace-selector-view { width: 100%; max-width: 34rem; diff --git a/assets/tailwind.css b/assets/tailwind.css index 7360ecc..5651d50 100644 --- a/assets/tailwind.css +++ b/assets/tailwind.css @@ -198,9 +198,18 @@ .flex { display: flex; } + .table { + display: table; + } + .border-collapse { + border-collapse: collapse; + } .transform { transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); } + .resize { + resize: both; + } .border { border-style: var(--tw-border-style); border-width: 1px; @@ -208,6 +217,9 @@ .text-wrap { text-wrap: wrap; } + .underline { + text-decoration-line: underline; + } .outline { outline-style: var(--tw-outline-style); outline-width: 1px; diff --git a/src/main.rs b/src/main.rs index 03fd349..7e18e53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,12 @@ -mod views; mod components; +mod route; +mod views; +mod workspace; use dioxus::prelude::*; -use crate::views::editor::dionotes_editor::EditorRoute; -use crate::views::workspace_selector::selector::WorkspaceRoute; + +use crate::route::Route; +use crate::workspace::Workspace; const FAVICON: Asset = asset!("/assets/favicon.ico"); @@ -22,7 +25,11 @@ fn main() { #[component] fn App() -> Element { - let workspace_selected: Signal = use_signal(|| false); + // Der offene Workspace als Context, nicht als Prop: Er wird im Selector + // gesetzt und in der Datei-Leiste gelesen — beide hängen unter einem + // Outlet, durch das sich keine Props durchreichen lassen. + use_context_provider(|| Signal::new(None::)); + rsx! { document::Link { rel: "icon", href: FAVICON } // Tailwind first: its preflight lives in @layer base, our unlayered @@ -32,12 +39,6 @@ fn App() -> Element { document::Link { rel: "stylesheet", href: BASE_CSS } document::Link { rel: "stylesheet", href: LAYOUT_CSS } document::Link { rel: "stylesheet", href: COMPONENTS_CSS } - if workspace_selected() { - Router:: {} - } else { - Router:: {} - } + Router:: {} } } - - diff --git a/src/route.rs b/src/route.rs new file mode 100644 index 0000000..df9b9d4 --- /dev/null +++ b/src/route.rs @@ -0,0 +1,36 @@ +use dioxus::prelude::*; +use strum::EnumMessage; + +use crate::views::editor::dionotes_editor::{Editor, EditorShell, Home}; +use crate::views::workspace_selector::selector::Selector; + +/// Alle Routen der App in einem Enum. +/// +/// Bewusst *ein* Router statt zweier: Zwei Routable-Enums teilen sich dieselbe +/// History, und beide hatten `/` belegt — beim Umschalten hätte der Editor eine +/// URL zu sehen bekommen, die er nicht kennt. +#[derive(Debug, Clone, Routable, PartialEq, EnumMessage)] +#[rustfmt::skip] +pub enum Route { + #[nest("/workspace")] + #[layout(EditorShell)] + #[route("/")] + #[strum(message = "Home")] + Home {}, + #[route("/editor")] + #[strum(message = "Editor")] + Editor {}, + #[end_layout] + #[end_nest] + #[route("/")] + Selector {}, +} + +impl Route { + /// Die Einträge der Datei-Leiste im Editor. + /// + /// Explizit statt über `EnumIter`, weil das Enum jetzt auch den Selector + /// enthält und der in dieser Leiste nichts zu suchen hat. Wermutstropfen: + /// eine neue Editor-Route hier einzutragen erzwingt der Compiler nicht. + pub const EDITOR_TABS: [Self; 2] = [Self::Home {}, Self::Editor {}]; +} diff --git a/src/views/editor/dionotes_editor.rs b/src/views/editor/dionotes_editor.rs index 5895874..7fdc841 100644 --- a/src/views/editor/dionotes_editor.rs +++ b/src/views/editor/dionotes_editor.rs @@ -1,32 +1,24 @@ use dioxus::prelude::*; use dioxus_code::Theme; use dioxus_code_editor::{CodeEditor, Language}; +use strum::EnumMessage; + use crate::components::custom_link::custom_link_component::CustomLink; -use strum::{EnumMessage, IntoEnumIterator}; -use strum_macros::EnumIter; +use crate::route::Route; +use crate::workspace::Workspace; const HEADER_SVG: Asset = asset!("/assets/header.svg"); -#[derive(Debug, Clone, Routable, PartialEq, EnumIter, EnumMessage)] -#[rustfmt::skip] -pub enum EditorRoute { - #[layout(Navbar)] - #[route("/")] - #[strum(message = "Home")] - Home {}, - #[route("/editor")] - #[strum(message = "Editor")] - Editor {}, -} - /// Home page #[component] -fn Home() -> Element { +pub fn Home() -> Element { rsx! { Hero {} } } +// TODO: Unveränderter Dioxus-Vorlagentext. Ersetzen, sobald klar ist, was +// beim Öffnen eines Workspace angezeigt wird, bevor eine Notiz gewählt wurde. #[component] pub fn Hero() -> Element { rsx! { @@ -46,7 +38,7 @@ pub fn Hero() -> Element { } #[component] -fn Editor() -> Element { +pub fn Editor() -> Element { let mut source = use_signal(String::new); rsx! { @@ -60,15 +52,34 @@ fn Editor() -> Element { } } -/// Shared navbar component. +/// Die Editor-Hülle: Datei-Leiste links, Editorfläche rechts. +/// +/// Trägt zugleich den Zustands-Guard — ohne offenen Workspace hat der Editor +/// nichts anzuzeigen. Der Fall ist real: Hot-Reload setzt den Context zurück, +/// während die Route bestehen bleibt. #[component] -fn Navbar() -> Element { +pub fn EditorShell() -> Element { + let workspace = use_context::>>(); + let navigator = use_navigator(); + + use_effect(move || { + if workspace.read().is_none() { + navigator.replace(Route::Selector {}); + } + }); + + if workspace.read().is_none() { + return rsx! {}; + } + rsx! { div { class: "layout-container", div { class: "files-bar", - for entry in EditorRoute::iter() { + // TODO: Durch den Notiz-Baum des offenen Workspace ersetzen + // (rekursiv eingelesen, aufklappbar). Bis dahin die Routen. + for entry in Route::EDITOR_TABS { CustomLink { key: "{entry}", label: entry.get_message().unwrap_or_default(), @@ -79,8 +90,8 @@ fn Navbar() -> Element { } div { class: "editor-container", - Outlet:: { } + Outlet:: { } } } } -} \ No newline at end of file +} diff --git a/src/views/workspace_selector/create_new_workspace.rs b/src/views/workspace_selector/create_new_workspace.rs deleted file mode 100644 index 76a4e86..0000000 --- a/src/views/workspace_selector/create_new_workspace.rs +++ /dev/null @@ -1,11 +0,0 @@ -use dioxus::prelude::*; - -/// Create new Workspace Layout -// TODO: Platzhalter — Formular zum Anlegen eines Workspace (Name, Pfad) und -// Verdrahtung als Ziel von WorkspaceRoute::NewWorkspace fehlen noch. -#[component] -fn NewWorkspaceLayout() -> Element { - rsx! { - div {} - } -} \ No newline at end of file diff --git a/src/views/workspace_selector/mod.rs b/src/views/workspace_selector/mod.rs index 94569ef..199a414 100644 --- a/src/views/workspace_selector/mod.rs +++ b/src/views/workspace_selector/mod.rs @@ -1,2 +1 @@ pub mod selector; -pub mod create_new_workspace; \ No newline at end of file diff --git a/src/views/workspace_selector/selector.rs b/src/views/workspace_selector/selector.rs index dbddde8..e7b48ad 100644 --- a/src/views/workspace_selector/selector.rs +++ b/src/views/workspace_selector/selector.rs @@ -1,61 +1,26 @@ use dioxus::prelude::*; -use strum::{EnumMessage, IntoEnumIterator}; -use strum_macros::EnumIter; -use crate::components::custom_link::custom_link_component::CustomLink; -#[derive(Debug, Clone, Routable, PartialEq, EnumIter, EnumMessage)] -#[rustfmt::skip] -pub enum WorkspaceRoute { - #[layout(Workspace)] - #[route("/")] - #[strum(message = "Create new Workspace")] - NewWorkspace {}, - #[route("/ws_open")] - #[strum(message = "Open Workspace")] - OpenWorkspace {}, -} - -/// Page to create new Workspace +/// Der Bildschirm vor dem Editor: einen bekannten Workspace öffnen oder einen +/// hinzufügen. Eine einzige Route, keine Reiter — die Workspace-Liste führt, +/// das Hinzufügen ist Nebenaktion. #[component] -fn NewWorkspace() -> Element { +pub fn Selector() -> Element { rsx! { - div { - "New Workspace", - } - } -} - -/// Page to open available Workspaces -#[component] -fn OpenWorkspace() -> Element { - rsx! { - div { - "Open Workspace", - } - } -} - -/// Workspace Layout -#[component] -fn Workspace() -> Element { - rsx! { - div { - class: "workspace-selector-layout", - div { - class: "workspace-selector-links", - for route in WorkspaceRoute::iter() { - CustomLink { - key: "{route}", - label: route.get_message().unwrap_or_default(), - href: route.clone(), - css: "workspace-selector", + div { class: "workspace-selector-layout", + div { class: "workspace-selector-view", + // TODO: Workspace-Liste aus workspaces.ron laden und darstellen + // (Ordnername, Pfad darunter, Statusabzeichen). Bis dahin nur + // der leere Zustand. + div { class: "empty-state", + span { class: "empty-state-title", "Noch kein Workspace" } + span { "Der erste Workspace ist ein Ordner deiner Wahl." } + // TODO: Nativen Ordner-Dialog anbinden, Marker anlegen und + // den Workspace in den Context schreiben. + button { class: "btn btn-primary", disabled: true, + "Workspace hinzufügen…" } } } - div { - class: "workspace-selector-view", - Outlet:: { } - } } } } diff --git a/src/workspace.rs b/src/workspace.rs new file mode 100644 index 0000000..15e8217 --- /dev/null +++ b/src/workspace.rs @@ -0,0 +1,34 @@ +use std::path::PathBuf; + +/// Ein geöffneter Workspace — der Ordner auf der Platte, in dem die Notizen liegen. +/// +/// Heute genau ein Feld. Der Typ existiert trotzdem als eigener Name, weil +/// `CONTEXT.md` „Workspace" als Domänenbegriff führt und die Einstellungen aus +/// `.dionotes/workspace.ron` hier andocken werden. +#[derive(Debug, Clone, PartialEq)] +pub struct Workspace { + pub root: PathBuf, +} + +impl Workspace { + /// Anzeigename: der Ordnername. Bewusst abgeleitet statt gespeichert — + /// ein eigenes Namensfeld wäre nach dem ersten Umbenennen des Ordners falsch. + pub fn name(&self) -> String { + self.root + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| self.root.to_string_lossy().into_owned()) + } +} + +#[cfg(test)] +mod tests { + use super::Workspace; + use std::path::PathBuf; + + #[test] + fn name_is_the_folder_name() { + let ws = Workspace { root: PathBuf::from("C:/Users/marvin/Notizen") }; + assert_eq!(ws.name(), "Notizen"); + } +}