From 3595f89fec0fc0f0da682a2312753be1706db856 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 10 Feb 2026 12:32:15 +0300 Subject: [PATCH] pinakes-ui: add preview/source toggle to markdown viewer Signed-off-by: NotAShelf Change-Id: I91797e05e0747473ed8edb9878da73566a6a6964 --- .../src/components/markdown_viewer.rs | 46 +- crates/pinakes-ui/src/styles.rs | 556 +++++++++++++++++- 2 files changed, 586 insertions(+), 16 deletions(-) diff --git a/crates/pinakes-ui/src/components/markdown_viewer.rs b/crates/pinakes-ui/src/components/markdown_viewer.rs index bcd04d7..477d91e 100644 --- a/crates/pinakes-ui/src/components/markdown_viewer.rs +++ b/crates/pinakes-ui/src/components/markdown_viewer.rs @@ -12,8 +12,10 @@ pub fn MarkdownViewer( ) -> Element { let mut rendered_html = use_signal(String::new); let mut frontmatter_html = use_signal(|| Option::::None); + let mut raw_content = use_signal(String::new); let mut loading = use_signal(|| true); let mut error = use_signal(|| Option::::None); + let mut show_preview = use_signal(|| true); // Fetch content on mount let url = content_url.clone(); @@ -27,6 +29,7 @@ pub fn MarkdownViewer( match reqwest::get(&url).await { Ok(resp) => match resp.text().await { Ok(text) => { + raw_content.set(text.clone()); if mtype == "md" || mtype == "markdown" { let (fm_html, body_html) = render_markdown_with_frontmatter(&text); frontmatter_html.set(fm_html); @@ -88,9 +91,28 @@ pub fn MarkdownViewer( }); let is_loading = *loading.read(); + let is_preview = *show_preview.read(); rsx! { div { class: "markdown-viewer", + // View toggle toolbar + if !is_loading && error.read().is_none() { + div { class: "markdown-toolbar", + button { + class: if is_preview { "toolbar-btn active" } else { "toolbar-btn" }, + onclick: move |_| show_preview.set(true), + title: "Preview Mode", + "Preview" + } + button { + class: if !is_preview { "toolbar-btn active" } else { "toolbar-btn" }, + onclick: move |_| show_preview.set(false), + title: "Source Mode", + "Source" + } + } + } + if is_loading { div { class: "loading-overlay", div { class: "spinner" } @@ -106,15 +128,23 @@ pub fn MarkdownViewer( } if !is_loading && error.read().is_none() { - if let Some(ref fm) = *frontmatter_html.read() { - div { - class: "frontmatter-card", - dangerous_inner_html: "{fm}", + if is_preview { + // Preview mode - show rendered markdown + if let Some(ref fm) = *frontmatter_html.read() { + div { + class: "frontmatter-card", + dangerous_inner_html: "{fm}", + } + } + div { + class: "markdown-content", + dangerous_inner_html: "{rendered_html}", + } + } else { + // Source mode - show raw markdown + pre { class: "markdown-source", + code { "{raw_content}" } } - } - div { - class: "markdown-content", - dangerous_inner_html: "{rendered_html}", } } } diff --git a/crates/pinakes-ui/src/styles.rs b/crates/pinakes-ui/src/styles.rs index 4735bf3..1a8b240 100644 --- a/crates/pinakes-ui/src/styles.rs +++ b/crates/pinakes-ui/src/styles.rs @@ -199,7 +199,7 @@ body { .sidebar.collapsed .sidebar-footer .status-text { display: none; } .sidebar.collapsed .sidebar-footer .user-info { justify-content: center; } -/* ── Main ── */ +/* Main */ .main { flex: 1; display: flex; @@ -483,22 +483,45 @@ input[type="text"]:focus, textarea:focus, select:focus { } /* ── Stats ── */ -.stats-grid { +.statistics-page { + padding: 20px; +} + +.stats-overview { display: grid; - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: 12px; - margin-bottom: 20px; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; + margin-bottom: 32px; } .stat-card { background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius); - padding: 14px 16px; + padding: 20px; + display: flex; + align-items: center; + gap: 16px; +} + +.stat-primary { border-left: 3px solid var(--accent); } +.stat-success { border-left: 3px solid var(--success); } +.stat-info { border-left: 3px solid #6ca0d4; } +.stat-warning { border-left: 3px solid var(--warning); } +.stat-purple { border-left: 3px solid #9d8be0; } +.stat-danger { border-left: 3px solid var(--error); } + +.stat-icon { + flex-shrink: 0; + color: var(--text-2); +} + +.stat-content { + flex: 1; } .stat-value { - font-size: 22px; + font-size: 28px; font-weight: 700; color: var(--text-0); line-height: 1.2; @@ -506,10 +529,332 @@ input[type="text"]:focus, textarea:focus, select:focus { } .stat-label { + font-size: 12px; + color: var(--text-2); + margin-top: 4px; + font-weight: 500; +} + +.stats-section { + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 24px; + margin-bottom: 20px; +} + +.section-title { + font-size: 16px; + font-weight: 600; + color: var(--text-0); + margin-bottom: 20px; +} + +.chart-bars { + display: flex; + flex-direction: column; + gap: 16px; +} + +.bar-item { + display: grid; + grid-template-columns: 120px 1fr 80px; + align-items: center; + gap: 16px; +} + +.bar-label { + font-size: 13px; + font-weight: 500; + color: var(--text-1); + text-align: right; +} + +.bar-track { + height: 28px; + background: var(--bg-3); + border-radius: var(--radius-sm); + overflow: hidden; + position: relative; +} + +.bar-fill { + height: 100%; + transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: var(--radius-sm); +} + +.bar-primary { + background: linear-gradient(90deg, var(--accent) 0%, #7c7ef3 100%); +} + +.bar-success { + background: linear-gradient(90deg, var(--success) 0%, #66bb6a 100%); +} + +.bar-value { + font-size: 13px; + font-weight: 600; + color: var(--text-1); + text-align: right; + font-variant-numeric: tabular-nums; +} + +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.tag-item { + display: flex; + align-items: center; + gap: 8px; + background: var(--bg-3); + padding: 8px 12px; + border-radius: var(--radius-sm); +} + +.tag-badge { + font-size: 13px; + font-weight: 500; + color: var(--text-0); +} + +.tag-count { + font-size: 12px; + color: var(--text-2); + font-weight: 600; +} + +.collection-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.collection-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background: var(--bg-3); + border-radius: var(--radius-sm); +} + +.collection-icon { + color: var(--text-2); +} + +.collection-name { + flex: 1; + font-size: 13px; + font-weight: 500; + color: var(--text-0); +} + +.collection-count { + font-size: 12px; + color: var(--text-2); + font-weight: 600; +} + +.date-range { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; +} + +.date-item { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background: var(--bg-3); + border-radius: var(--radius-sm); +} + +.date-content { + flex: 1; +} + +.date-label { font-size: 11px; color: var(--text-2); - margin-top: 2px; + font-weight: 600; + text-transform: uppercase; + margin-bottom: 4px; +} + +.date-value { + font-size: 13px; + color: var(--text-0); + font-family: 'Menlo', 'Monaco', 'Courier New', monospace; +} + +/* ── Tasks ── */ +.tasks-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + gap: 16px; + padding: 16px; +} + +.task-card { + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + transition: all 0.2s; +} + +.task-card:hover { + border-color: var(--border-strong); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transform: translateY(-2px); +} + +.task-card-enabled { + border-left: 3px solid var(--success); +} + +.task-card-disabled { + border-left: 3px solid var(--text-3); + opacity: 0.7; +} + +.task-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 16px; + border-bottom: 1px solid var(--border-subtle); +} + +.task-header-left { + flex: 1; + min-width: 0; +} + +.task-name { + font-size: 16px; + font-weight: 600; + color: var(--text-0); + margin-bottom: 4px; +} + +.task-schedule { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-2); + font-family: 'Menlo', 'Monaco', 'Courier New', monospace; +} + +.schedule-icon { + font-size: 14px; +} + +.task-status-badge { + flex-shrink: 0; +} + +.status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: var(--radius-sm); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.status-enabled { + background: rgba(76, 175, 80, 0.12); + color: var(--success); +} + +.status-disabled { + background: var(--bg-3); + color: var(--text-2); +} + +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} + +.status-enabled .status-dot { + animation: pulse 1.5s infinite; +} + +.task-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; + padding: 16px; +} + +.task-info-item { + display: flex; + align-items: flex-start; + gap: 10px; +} + +.task-info-icon { + font-size: 18px; + color: var(--text-2); + flex-shrink: 0; +} + +.task-info-content { + flex: 1; + min-width: 0; +} + +.task-info-label { + font-size: 10px; + color: var(--text-2); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + margin-bottom: 4px; +} + +.task-info-value { + font-size: 12px; + color: var(--text-1); font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.task-card-actions { + display: flex; + gap: 8px; + padding: 12px 16px; + background: var(--bg-1); + border-top: 1px solid var(--border-subtle); +} + +.task-card-actions button { + flex: 1; +} + +.empty-icon { + font-size: 48px; + margin-bottom: 12px; +} + +.text-muted { + color: var(--text-2); + font-size: 13px; } /* ── Type badges ── */ @@ -875,6 +1220,7 @@ ul li { padding: 3px 0; font-size: 12px; color: var(--text-1); } .status-indicator { display: flex; align-items: center; + justify-content: center; gap: 6px; font-size: 11px; font-weight: 500; @@ -882,6 +1228,11 @@ ul li { padding: 3px 0; font-size: 12px; color: var(--text-1); } overflow: visible; } +/* In expanded sidebar, align left with gap */ +.sidebar:not(.collapsed) .status-indicator { + justify-content: flex-start; +} + .status-dot { width: 6px; height: 6px; @@ -2340,6 +2691,63 @@ ul li { padding: 3px 0; font-size: 12px; color: var(--text-1); } .markdown-viewer { padding: 16px; text-align: left; + display: flex; + flex-direction: column; + gap: 12px; +} + +.markdown-toolbar { + display: flex; + gap: 8px; + padding: 8px; + background: var(--bg-2); + border-radius: var(--radius); + border: 1px solid var(--border); +} + +.toolbar-btn { + padding: 6px 12px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-1); + color: var(--text-1); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; +} + +.toolbar-btn:hover { + background: var(--bg-3); + border-color: var(--border-strong); +} + +.toolbar-btn.active { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +.markdown-source { + max-width: 100%; + background: var(--bg-3); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; + overflow-x: auto; + font-family: 'Menlo', 'Monaco', 'Courier New', monospace; + font-size: 13px; + line-height: 1.6; + color: var(--text-0); + white-space: pre-wrap; + word-wrap: break-word; +} + +.markdown-source code { + font-family: inherit; + background: none; + padding: 0; + border: none; } .markdown-content { @@ -3482,6 +3890,50 @@ ul li { padding: 3px 0; font-size: 12px; color: var(--text-1); } .graph-svg { max-width: 100%; max-height: 100%; + cursor: grab; +} + +.graph-svg-container { + position: relative; + width: 100%; + height: 100%; +} + +.graph-zoom-controls { + position: absolute; + top: 16px; + left: 16px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 5; +} + +.zoom-btn { + width: 36px; + height: 36px; + border-radius: 6px; + background: var(--bg-2); + border: 1px solid var(--border); + color: var(--text-0); + font-size: 18px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.15s; + box-shadow: var(--shadow-sm); +} + +.zoom-btn:hover { + background: var(--bg-3); + border-color: var(--border-strong); + transform: scale(1.05); +} + +.zoom-btn:active { + transform: scale(0.95); } .graph-edges line { @@ -3595,6 +4047,94 @@ ul li { padding: 3px 0; font-size: 12px; color: var(--text-1); } color: var(--text-0); } +/* ── Physics Controls Panel ── */ +.physics-controls-panel { + position: absolute; + top: 16px; + right: 16px; + width: 300px; + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 16px; + z-index: 10; +} + +.physics-controls-panel h4 { + font-size: 13px; + font-weight: 600; + color: var(--text-0); + margin: 0 0 16px 0; + padding-bottom: 8px; + border-bottom: 1px solid var(--border-subtle); +} + +.control-group { + margin-bottom: 14px; +} + +.control-group label { + display: block; + font-size: 11px; + font-weight: 500; + color: var(--text-1); + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.control-group input[type="range"] { + width: 100%; + height: 4px; + border-radius: 2px; + background: var(--bg-3); + outline: none; + -webkit-appearance: none; +} + +.control-group input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--accent); + cursor: pointer; + transition: transform 0.1s; +} + +.control-group input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.15); +} + +.control-group input[type="range"]::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--accent); + cursor: pointer; + border: none; + transition: transform 0.1s; +} + +.control-group input[type="range"]::-moz-range-thumb:hover { + transform: scale(1.15); +} + +.control-value { + display: inline-block; + margin-top: 4px; + font-size: 11px; + color: var(--text-2); + font-family: 'JetBrains Mono', ui-monospace, monospace; +} + +.physics-controls-panel .btn { + width: 100%; + margin-top: 8px; +} + /* ── Wikilink Styles (in markdown) ── */ .wikilink { color: var(--accent-text);