From 1987d7707e070de34e98a513b221db54f21ba767 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:29:15 -0500 Subject: [PATCH] style(tui_v3): full 1:1 ui mockup parity pass with retro black and white theme --- docs/tui_v3_plan.md | 2 +- src/seedpass/tui_v3/app.py | 60 +- src/seedpass/tui_v3/widgets/action_bar.py | 31 +- src/seedpass/tui_v3/widgets/grid.py | 19 +- src/seedpass/tui_v3/widgets/header.py | 43 +- src/seedpass/tui_v3/widgets/inspector.py | 715 ++++++++++++++-------- src/seedpass/tui_v3/widgets/sidebar.py | 23 +- 7 files changed, 554 insertions(+), 339 deletions(-) diff --git a/docs/tui_v3_plan.md b/docs/tui_v3_plan.md index 89fa34ce..d9b72499 100644 --- a/docs/tui_v3_plan.md +++ b/docs/tui_v3_plan.md @@ -51,7 +51,7 @@ We have successfully scaffolded the core v3 shell: - [x] **Edit Workflows:** Unified `EditEntryScreen` implemented to support all types. ### Phase B: High-Fidelity Refinement -- [ ] **Mockup 1:1 Polish:** Final pass on spacing/density for all 9 PNG mockups. +- [x] **Mockup 1:1 Polish:** Final pass on spacing/density for all 9 PNG mockups. - [x] **Action Bar Hardening:** Bottom bar segments now reflect active entry kind and session state. - [x] **Notifications:** Unified snackbar/toast system for service feedback. - [x] **Search/Filter Parity:** Filter presets and archive scope controls wired. diff --git a/src/seedpass/tui_v3/app.py b/src/seedpass/tui_v3/app.py index 1474e62c..7d19c60c 100644 --- a/src/seedpass/tui_v3/app.py +++ b/src/seedpass/tui_v3/app.py @@ -135,50 +135,52 @@ class CommandProcessor: else: self.app.notify(f"Unknown v3 command: {cmd}", severity="warning") +class BrandFingerprint(Static): + """Placeholder for the top left fingerprint block matching mockups.""" + def render(self) -> str: + app = self.app + fp = app.active_fingerprint or "No Profile" + return f"{fp[:24]}" + + def on_mount(self) -> None: + self.watch(self.app, "active_fingerprint", self.refresh) + class MainScreen(Screen): def compose(self) -> ComposeResult: yield CommandPalette(id="palette") - yield Static("SeedPass ◈ UI v3", id="brand-strip") - yield RibbonHeader(id="top-ribbon") - with Vertical(id="body"): - with Horizontal(id="top-work"): - yield SidebarContainer(id="left") - with Vertical(id="center"): - yield GridContainer(id="grid-work") - with Vertical(id="right"): - yield Static("Inspector Board", id="inspector-heading") - yield BoardContainer(id="board-container") + with Horizontal(id="body"): + with Vertical(id="left-pane"): + yield BrandFingerprint(id="brand-fingerprint") + yield SidebarContainer(id="sidebar-container") + with Vertical(id="right-pane"): + yield RibbonHeader(id="ribbon-header") + yield GridContainer(id="grid-container") + with Vertical(id="inspector-pane"): + yield Static("Inspector Board", id="inspector-heading") + yield BoardContainer(id="board-container") yield ActionBar(id="action-bar") - yield Footer() class SeedPassTuiV3(App[None]): """ SeedPass TUI v3 - Rebuilt from scratch for modularity and mockup fidelity. """ CSS = """ - #brand-strip { - background: #0b0f13; - color: #58f29d; + #brand-fingerprint { + background: #000000; + color: #ffffff; text-style: bold; - border: solid #2abf75; - margin: 0 1; - padding: 0 1; height: 3; content-align: center middle; + border: solid #000000; + padding: 0; } #body { height: 1fr; margin: 0 1; } - #top-work { height: 6fr; } - #left { width: 31; border: solid #1a3024; background: #0d1114; } - #center { width: 1fr; border: solid #1a3024; background: #0d1114; margin-left: 1; } - #right { height: 5fr; border: heavy #3ce79c; background: #0d1114; margin-top: 1; } - #status { - height: 3; - padding: 0 1; - border: heavy #58f29d; - background: #11191f; - color: #e4fff2; - margin: 0 1; - } + #left-pane { width: 35; border: solid black; background: #999999; } + #right-pane { width: 1fr; border: solid black; background: #999999; margin-left: 0; } + #inspector-pane { height: 5fr; border-top: heavy black; background: #000000; margin-top: 0; } + #inspector-heading { background: #000000; color: #ffffff; padding: 0 1; } + + /* Global classes that might be used by children */ #sidebar-placeholder, #grid-placeholder, #inspector-placeholder { height: 1fr; content-align: center middle; diff --git a/src/seedpass/tui_v3/widgets/action_bar.py b/src/seedpass/tui_v3/widgets/action_bar.py index 0f4dabab..80f41ba7 100644 --- a/src/seedpass/tui_v3/widgets/action_bar.py +++ b/src/seedpass/tui_v3/widgets/action_bar.py @@ -12,12 +12,12 @@ class ActionBar(Static): DEFAULT_CSS = """ ActionBar { - height: 3; - background: #11191f; - color: #daf2e5; - border: heavy #2abf75; + height: 2; + background: #999999; + color: #000000; + border: solid black; padding: 0 1; - margin: 0 1; + margin: 0; } """ @@ -25,20 +25,21 @@ class ActionBar(Static): app = self.app selected_id = app.selected_entry_id - # Global Row + # Global Row - mockup format is_managed = False try: is_managed = len(getattr(app.services.get("vault")._manager, "profile_stack", [])) > 0 except: pass - exit_hint = " Exit (Shift+M)" if is_managed else "" + exit_hint = " [M]anaged Exit" if is_managed else "" global_row = ( - f"Settings (Shift+S) Add (Shift+A) Seed+ (Shift+C) " - f"Backup (B) Cmd (Ctrl+P){exit_hint}" + f"[b][S][/b]ettings [b][A][/b]dd New Entry " + f"[b][C][/b]reate New Seed [b][R][/b]emove Seed " + f"[b][E][/b]xport Data [b][I][/b]mport Data [b][B][/b]ackup Data{exit_hint}" ) - # Context Row + # Context Row (added from previous phase parity) if selected_id is None: context = "Select an entry to view actions." else: @@ -46,15 +47,15 @@ class ActionBar(Static): entry = app.services["entry"].retrieve_entry(selected_id) kind = str(entry.get("kind") or entry.get("type") or "").lower() - actions = ["Reveal (v)", "QR (g)", "Edit (e)", "Archive (a)", "Copy (c)", "Max (z)"] + actions = ["[b][v][/b] Reveal", "[b][g][/b] QR", "[b][e][/b] Edit", "[b][a][/b] Archive", "[b][c][/b] Copy", "[b][z][/b] Maximize"] if kind in {"managed_account", "seed"}: - actions.insert(4, "Load (m)") + actions.insert(4, "[b][m][/b] Load") if kind in {"document", "note"}: - actions.insert(5, "Export (x)") + actions.insert(5, "[b][x][/b] Export") - context = f"Entry #{selected_id} ({kind}) " + " ▣ ".join(actions) + context = f" Context ({kind}): " + " ▣ ".join(actions) except Exception: - context = f"Entry #{selected_id} [Error fetching details]" + context = f" Error fetching context details for Entry #{selected_id}" return f"{global_row}\n{context}" diff --git a/src/seedpass/tui_v3/widgets/grid.py b/src/seedpass/tui_v3/widgets/grid.py index 947bdd05..ce0fc0ea 100644 --- a/src/seedpass/tui_v3/widgets/grid.py +++ b/src/seedpass/tui_v3/widgets/grid.py @@ -172,26 +172,27 @@ class GridContainer(Static): DEFAULT_CSS = """ GridContainer { height: 1fr; - background: #0d1114; + background: #999999; } #grid-divider { - color: #1a3024; + color: #000000; height: 1; - padding: 0 1; + padding: 0; + background: black; } #entry-data-table { - background: transparent; - color: #97b8a6; + background: #999999; + color: #000000; border: none; height: 1fr; } #entry-data-table > .datatable--header { - background: #0b0f13; - color: #58f29d; + background: #999999; + color: #000000; text-style: bold; } #entry-data-table > .datatable--cursor { - background: #122019; - color: #58f29d; + background: #000000; + color: #ffffff; } """ diff --git a/src/seedpass/tui_v3/widgets/header.py b/src/seedpass/tui_v3/widgets/header.py index f5903913..2321ed4c 100644 --- a/src/seedpass/tui_v3/widgets/header.py +++ b/src/seedpass/tui_v3/widgets/header.py @@ -13,33 +13,41 @@ class RibbonHeader(Static): DEFAULT_CSS = """ RibbonHeader { - height: 3; - background: #10181f; - color: #daf2e5; - border: heavy #2abf75; + height: 1; + background: #999999; + color: #000000; padding: 0 1; - margin: 0 1; + margin: 0; text-style: bold; } """ def render(self) -> str: app = self.app - fp = app.active_fingerprint or "No Profile" - lock_icon = "🔒" if app.session_locked else "🔓" - status = "LOCKED" if app.session_locked else "UNLOCKED" - # In a real app, we'd fetch stats from the service. - # For the shell, we'll use placeholders that look like the mockup. - stats = "Entries: 0 | PWD: 0 | 2FA: 0 | Keys: 0" - sync = "Sync: Never" + # Gather metrics from actual services + managed = 0 + try: + managed = len(getattr(app.services.get("vault")._manager, "profile_stack", [])) + except: + pass + + entries = 0 + try: + table = app.screen.query_one("#entry-data-table") + if table: + entries = len(table.rows) + except: + pass + + sync = "YYY-MM-DDThh:mm:ssTZD" - # Layout: [Icon] Status | Fingerprint | Stats | Sync - left = f"{lock_icon} {status} | Seed: {fp}" - right = f"{stats} | {sync}" + # Build the exact string parts from the mockup + left = f"Managed Users: {managed} Entries: {entries} ◀ ▶ Kind = {app.filter_kind.capitalize()}" + right = f"Last Sync: {sync}" - # Calculate spacing to push sync to the right - available_width = self.size.width - len(left) - len(right) - 4 + # Calculate spacing to push sync to the right edge + available_width = self.size.width - len(left) - len(right) - 2 spacer = " " * max(1, available_width) return f"{left}{spacer}{right}" @@ -48,3 +56,4 @@ class RibbonHeader(Static): # Refresh the header whenever important app state changes self.watch(self.app, "active_fingerprint", self.refresh) self.watch(self.app, "session_locked", self.refresh) + self.watch(self.app, "filter_kind", self.refresh) diff --git a/src/seedpass/tui_v3/widgets/inspector.py b/src/seedpass/tui_v3/widgets/inspector.py index d9777434..4e246eff 100644 --- a/src/seedpass/tui_v3/widgets/inspector.py +++ b/src/seedpass/tui_v3/widgets/inspector.py @@ -10,274 +10,486 @@ class BaseBoard(Static): entry_data = reactive[dict[str, Any]]({}) reveal_data = reactive[dict[str, Any]]({}) - def watch_reveal_data(self, old, new): - self.refresh() + def watch_entry_data(self, old: dict, new: dict) -> None: + self.update_board() - def _render_card(self, title: str, rows: list[str]) -> str: - """Utility to render ASCII cards matching mockup style.""" - width = 50 - top = f"┌─ {title} " + "─" * (width - len(title) - 4) + "┐" - bottom = "└" + "─" * (width - 2) + "┘" - lines = [top] - for row in rows: - lines.append(f"│ {row:<{width-4}} │") - lines.append(bottom) - return "\n".join(lines) + def watch_reveal_data(self, old: dict, new: dict) -> None: + self.update_board() -class IdleBoard(Static): + def update_board(self) -> None: + pass + +class InspectorHeader(Static): + """Reusable high-fidelity header matching the UI Board mockups.""" + DEFAULT_CSS = """ + InspectorHeader { + height: 3; + layout: horizontal; + margin-bottom: 1; + background: #000000; + color: #ffffff; + border-bottom: solid #ffffff; + } + .title-block { width: 30%; height: 3; } + .title-text { text-style: bold; } + .type-text { color: #aaaaaa; } + + .meta-block { width: 1fr; layout: horizontal; height: 3; } + .meta-item { width: 1fr; text-align: left; } + + .edit-btn { background: #ffffff; color: #000000; padding: 0 2; height: 1; margin-top: 1; margin-right: 1; text-style: bold; } + """ + + def compose(self) -> ComposeResult: + with Vertical(classes="title-block"): + yield Label("Title", id="hdr-title", classes="title-text") + yield Label("Type", id="hdr-type", classes="type-text") + with Horizontal(classes="meta-block"): + yield Label("[b]Date Modified[/b]\nYYYY-MM-DD", id="hdr-date", classes="meta-item") + yield Label("[b]Black Listed? No[/b]", id="hdr-bl", classes="meta-item") + yield Label("[b]Index Num*:[/b]\n0", id="hdr-idx", classes="meta-item") + yield Label("[b]Entry Num:[/b]\n1", id="hdr-enum", classes="meta-item") + yield Label("Edit", classes="edit-btn") + + def update_data(self, entry: dict, kind_label: str): + if not self.is_mounted: return + try: + self.query_one("#hdr-title", Label).update(f"[b]{entry.get('label', 'Untitled')}[/b]") + self.query_one("#hdr-type", Label).update(kind_label) + idx = str(entry.get('id', 'N/A')) + self.query_one("#hdr-idx", Label).update(f"[b]Index Num*:[/b]\n{idx}") + self.query_one("#hdr-enum", Label).update(f"[b]Entry Num:[/b]\n{idx}") + except Exception: + pass + + +class IdleBoard(BaseBoard): """Shown when no entry is selected.""" - def render(self) -> str: - return """ + def compose(self) -> ComposeResult: + yield Label("\n\n[b]Inspector Idle[/b]\n\nSelect an entry from the grid\nto view details.", id="idle-text") -[b]Inspector Idle[/b] - -Select an entry from the grid -to view details.""" - class PasswordBoard(BaseBoard): """Matches 'Password Board.png' mockup.""" - def render(self) -> str: + DEFAULT_CSS = """ + PasswordBoard { + background: #000000; + color: #ffffff; + padding: 0 1; + height: 1fr; + } + .board-body { layout: horizontal; height: 1fr; } + .left-col { width: 1fr; margin-right: 2; height: 1fr; } + .right-col { width: 1fr; border: solid #ffffff; padding: 1; height: 1fr; } + + .field-box { border: solid #ffffff; padding: 0 1; margin-bottom: 1; height: auto; } + .field-box.highlight { background: #ffffff; color: #000000; } + .field-title { text-style: bold; } + .field-row { layout: horizontal; } + .field-val { width: 1fr; } + .action-btn { background: #000000; color: #ffffff; padding: 0 1; margin-left: 1; } + """ + + def compose(self) -> ComposeResult: + yield InspectorHeader(id="password-header") + with Horizontal(classes="board-body"): + with Vertical(classes="left-col"): + with Vertical(classes="field-box highlight"): + yield Label("Password*", classes="field-title") + with Horizontal(classes="field-row"): + yield Label("**********", id="fld-pass-val", classes="field-val") + yield Label("Copy", classes="action-btn") + yield Label("Create New", classes="action-btn") + with Vertical(classes="field-box"): + yield Label("Username*", classes="field-title") + yield Label("-", id="fld-user-val") + with Vertical(classes="field-box"): + yield Label("URL", classes="field-title") + yield Label("-", id="fld-url-val") + with Vertical(classes="field-box"): + yield Label("Tags", classes="field-title") + yield Label("-", id="fld-tags-val") + with Vertical(classes="right-col"): + yield Label("Notes", classes="field-title") + yield Label("", id="fld-notes-val") + + def update_board(self) -> None: + if not self.is_mounted: return d = self.entry_data r = self.reveal_data - # If we have a direct prompt (confirmation required), show ONLY that - if r.get("prompt"): - return self._render_card("SECURITY ALERT", r["prompt"].splitlines()) + try: + self.query_one("#password-header", InspectorHeader).update_data(d, "Password") + + pwd = "[b]**********[/b] (v to reveal)" + if r.get("content"): + pwd = r.get("content") + + self.query_one("#fld-pass-val", Label).update(pwd) + self.query_one("#fld-user-val", Label).update(d.get("username", "-")) + self.query_one("#fld-url-val", Label).update(d.get("url", "-")) + + tags = d.get('tags', []) + tag_str = ", ".join(tags) if isinstance(tags, list) else str(tags) + self.query_one("#fld-tags-val", Label).update(tag_str or "-") + + notes = d.get('notes', '') + self.query_one("#fld-notes-val", Label).update(notes) + except Exception: + pass - # If we have revealed content, show it - password_val = "[HIDDEN] (v to reveal)" - if r.get("content"): - password_val = f"[b]{r['content']}[/b]" - - cred_rows = [ - f"Label : {d.get('label', '')}", - f"Username : {d.get('username', '')}", - f"URL : {d.get('url', '')}", - f"Password : {password_val}", - ] - action_rows = [ - "▣ Reveal (v) ▣ QR (g)", - "▣ Edit (e) ▣ Archive (a) ▣ Max (z)", - ] - - return "\n".join([ - self._render_card("Credentials", cred_rows), - "", - self._render_card("Quick Actions", action_rows) - ]) class TotpBoard(BaseBoard): """Matches '2FA Board.png' mockup.""" - def render(self) -> str: + DEFAULT_CSS = """ + TotpBoard { background: #000000; color: #ffffff; padding: 0 1; height: 1fr; } + .board-body { layout: horizontal; height: 1fr; } + .left-col { width: 1fr; margin-right: 2; height: 1fr; } + .right-col { width: 1fr; border: solid #ffffff; padding: 1; height: 1fr; content-align: center middle; } + .qr-text { text-align: center; } + .field-box { border: solid #ffffff; padding: 0 1; margin-bottom: 1; height: auto; } + .field-box.highlight { background: #ffffff; color: #000000; } + .field-title { text-style: bold; } + .field-row { layout: horizontal; } + .field-val { width: 1fr; } + .action-btn { background: #000000; color: #ffffff; padding: 0 1; margin-left: 1; } + """ + + def compose(self) -> ComposeResult: + yield InspectorHeader(id="totp-header") + with Horizontal(classes="board-body"): + with Vertical(classes="left-col"): + with Vertical(classes="field-box highlight"): + yield Label("2FA Code*", classes="field-title") + with Horizontal(classes="field-row"): + yield Label("------ (30s left)", id="fld-code-val", classes="field-val") + yield Label("Copy", classes="action-btn") + with Vertical(classes="field-box"): + yield Label("Secret", classes="field-title") + yield Label("-", id="fld-secret-val") + with Vertical(classes="field-box"): + yield Label("Algorithm", classes="field-title") + yield Label("SHA1", id="fld-algo-val") + with Vertical(classes="right-col"): + yield Label("QR Code (Press g)", id="fld-qr-val", classes="qr-text") + + def update_board(self) -> None: + if not self.is_mounted: return d = self.entry_data r = self.reveal_data app = self.app - if r.get("prompt"): - return self._render_card("SECURITY ALERT", r["prompt"].splitlines()) + try: + self.query_one("#totp-header", InspectorHeader).update_data(d, "2FA") + + code = "------" + if app.services.get("entry") and not app.session_locked: + try: + code = app.services["entry"].get_totp_code(d.get("id")) + except: + pass + import time + remaining = 30 - (int(time.time()) % 30) + self.query_one("#fld-code-val", Label).update(f"[b]{code}[/b] ({remaining}s left)") - if r.get("content") and "##" in r["content"]: - return self._render_card("TOTP QR CODE", r["content"].splitlines()) + secret = "[b]**********[/b] (v to reveal)" + if r.get("content") and "##" not in r.get("content"): + secret = r.get("content") + + self.query_one("#fld-secret-val", Label).update(secret) + self.query_one("#fld-algo-val", Label).update(d.get("algorithm", "SHA1").upper()) - code = "------" - if app.services.get("entry") and not app.session_locked: - try: - code = app.services["entry"].get_totp_code(d.get("id")) - except: - pass + qr_content = "QR Code (Press g)" + if r.get("content") and "##" in r.get("content"): + qr_content = r.get("content") + self.query_one("#fld-qr-val", Label).update(qr_content) + + except Exception: + pass - import time - remaining = 30 - (int(time.time()) % 30) - bar_width = 20 - filled = int((remaining / 30) * bar_width) - bar = "█" * filled + "░" * (bar_width - filled) - - secret_val = r.get("content") if r.get("content") else "[HIDDEN] (v to reveal)" - - cred_rows = [ - f"Label : {d.get('label', '')}", - f"Code : [b][cyan]{code}[/b] ({remaining}s)", - f"Timer : {bar}", - f"Secret : {secret_val}", - f"Period : {d.get('period', 30)}s", - f"Digits : {d.get('digits', 6)}", - ] - action_rows = [ - "▣ Copy Code (c) ▣ Reveal Secret (v)", - "▣ Show QR (g) ▣ Archive (a) ▣ Max (z)", - ] - - return "\n".join([ - self._render_card("2FA Authenticator", cred_rows), - "", - self._render_card("Quick Actions", action_rows) - ]) - -class SeedBoard(BaseBoard): - """Matches 'BIP-39 Seed Board.png' mockup.""" - def render(self) -> str: - d = self.entry_data - r = self.reveal_data - - if r.get("prompt"): - return self._render_card("SECURITY ALERT", r["prompt"].splitlines()) - - if r.get("content") and "##" in r["content"]: - return self._render_card("SEED QR CODE", r["content"].splitlines()) - - phrase_val = "[HIDDEN] (vv to reveal)" - if r.get("content"): - phrase_val = f"[b]{r['content']}[/b]" - - cred_rows = [ - f"Label : {d.get('label', '')}", - f"Fingerprint: {d.get('fingerprint', 'Pending...')}", - f"Index : {d.get('index', 0)}", - f"Word Count : {d.get('word_count', 24)}", - f"Seed Phrase: {phrase_val}", - ] - action_rows = [ - "▣ Reveal Words (vv) ▣ Show SeedQR (gg)", - "▣ Load Session (ml) ▣ Archive (a) ▣ Max (z)", - ] - - return "\n".join([ - self._render_card("BIP-85 Derived Seed", cred_rows), - "", - self._render_card("Quick Actions", action_rows) - ]) - -class SshBoard(BaseBoard): - """Matches 'SSH Board.png' mockup.""" - def render(self) -> str: - d = self.entry_data - r = self.reveal_data - - if r.get("prompt"): - return self._render_card("SECURITY ALERT", r["prompt"].splitlines()) - - priv_val = "[HIDDEN] (vv to reveal)" - if r.get("content"): - priv_val = f"[b]{r['content']}[/b]" - - cred_rows = [ - f"Label : {d.get('label', '')}", - f"Key Type : {d.get('key_type', 'RSA/Ed25519')}", - f"Index : {d.get('index', 0)}", - "Pub Key : [PREVIEW AVAILABLE]", - f"Priv Key : {priv_val}", - ] - action_rows = [ - "▣ Copy Pub (c) ▣ Reveal Priv (vv)", - "▣ Export (x) ▣ Archive (a) ▣ Max (z)", - ] - - return "\n".join([ - self._render_card("SSH Key Pair", cred_rows), - "", - self._render_card("Quick Actions", action_rows) - ]) - -class PgpBoard(BaseBoard): - """Matches 'PGP Board.png' mockup.""" - def render(self) -> str: - d = self.entry_data - r = self.reveal_data - - if r.get("prompt"): - return self._render_card("SECURITY ALERT", r["prompt"].splitlines()) - - priv_val = "[HIDDEN] (vv to reveal)" - if r.get("content"): - priv_val = f"[b]{r['content']}[/b]" - - cred_rows = [ - f"Label : {d.get('label', '')}", - f"Identity : {d.get('user_id', '')}", - f"Fingerpr : {d.get('fingerprint', '')}", - f"Priv Key : {priv_val}", - ] - action_rows = [ - "▣ Copy Pub (c) ▣ Reveal Priv (vv)", - "▣ Export (x) ▣ Archive (a) ▣ Max (z)", - ] - - return "\n".join([ - self._render_card("PGP Key Pair", cred_rows), - "", - self._render_card("Quick Actions", action_rows) - ]) - -class NostrBoard(BaseBoard): - """Matches 'Nostr Board.png' mockup.""" - def render(self) -> str: - d = self.entry_data - r = self.reveal_data - - if r.get("prompt"): - return self._render_card("SECURITY ALERT", r["prompt"].splitlines()) - - nsec_val = "[HIDDEN] (v to reveal)" - if r.get("content") and "##" not in r["content"]: - nsec_val = f"[b]{r['content']}[/b]" - - if r.get("content") and "##" in r["content"]: - return self._render_card("NOSTR QR CODE", r["content"].splitlines()) - - cred_rows = [ - f"Label : {d.get('label', '')}", - f"npub : {d.get('npub', '')[:20]}...", - f"nsec : {nsec_val}", - ] - action_rows = [ - "▣ Reveal (v) ▣ QR (g)", - "▣ Sync (s) ▣ Archive (a) ▣ Max (z)", - ] - - return "\n".join([ - self._render_card("Nostr Agent Identity", cred_rows), - "", - self._render_card("Quick Actions", action_rows) - ]) - -class KeyValueBoard(BaseBoard): - """Generic meta-data board.""" - def render(self) -> str: - d = self.entry_data - rows = [ - f"Label : {d.get('label', '')}", - f"Key : {d.get('key', '')}", - f"Value : {d.get('value', '')}", - ] - action_rows = [ - "▣ Edit (e) ▣ Archive (a) ▣ Max (z)", - ] - - return "\n".join([ - self._render_card("Key-Value Meta", rows), - "", - self._render_card("Quick Actions", action_rows) - ]) class NoteBoard(BaseBoard): """Matches 'Note Board.png' mockup.""" - def render(self) -> str: + DEFAULT_CSS = """ + NoteBoard { background: #000000; color: #ffffff; padding: 0 1; height: 1fr; } + .board-body { layout: horizontal; height: 1fr; border: solid #ffffff; padding: 1; } + """ + + def compose(self) -> ComposeResult: + yield InspectorHeader(id="note-header") + with Vertical(classes="board-body"): + yield Label("Content", id="fld-content-val") + + def update_board(self) -> None: + if not self.is_mounted: return d = self.entry_data - content = str(d.get('content', '')) - preview = (content[:200] + '...') if len(content) > 200 else content + try: + kind_label = "Note" if d.get('kind', '') == 'note' else "Document" + self.query_one("#note-header", InspectorHeader).update_data(d, kind_label) + self.query_one("#fld-content-val", Label).update(d.get('content', '')) + except Exception: + pass + + +class SeedBoard(BaseBoard): + """Matches 'BIP-39 Seed Board.png' mockup.""" + DEFAULT_CSS = """ + SeedBoard { background: #000000; color: #ffffff; padding: 0 1; height: 1fr; } + .board-body { layout: horizontal; height: 1fr; } + .left-col { width: 1fr; margin-right: 2; height: 1fr; } + .right-col { width: 1fr; border: solid #ffffff; padding: 1; height: 1fr; content-align: center middle; } + .qr-text { text-align: center; } + .field-box { border: solid #ffffff; padding: 0 1; margin-bottom: 1; height: auto; } + .field-box.highlight { background: #ffffff; color: #000000; } + .field-title { text-style: bold; } + .field-row { layout: horizontal; } + .field-val { width: 1fr; } + .action-btn { background: #000000; color: #ffffff; padding: 0 1; margin-left: 1; } + """ + + def compose(self) -> ComposeResult: + yield InspectorHeader(id="seed-header") + with Horizontal(classes="board-body"): + with Vertical(classes="left-col"): + with Vertical(classes="field-box highlight"): + yield Label("Seed Phrase*", classes="field-title") + with Horizontal(classes="field-row"): + yield Label("[HIDDEN]", id="fld-phrase-val", classes="field-val") + yield Label("Copy", classes="action-btn") + with Vertical(classes="field-box"): + yield Label("Fingerprint", classes="field-title") + yield Label("-", id="fld-fp-val") + with Vertical(classes="field-box"): + yield Label("Word Count", classes="field-title") + yield Label("24", id="fld-wc-val") + with Vertical(classes="right-col"): + yield Label("QR Code (Press gg)", id="fld-qr-val", classes="qr-text") + + def update_board(self) -> None: + if not self.is_mounted: return + d = self.entry_data + r = self.reveal_data + try: + kind_label = "BIP-39" if d.get('kind', '') in {'seed', 'managed_account'} else d.get('kind', 'Seed') + self.query_one("#seed-header", InspectorHeader).update_data(d, kind_label.capitalize()) + + phrase = "[b]**********[/b] (vv to reveal)" + if r.get("content") and "##" not in r.get("content"): + phrase = r.get("content") + + self.query_one("#fld-phrase-val", Label).update(phrase) + self.query_one("#fld-fp-val", Label).update(d.get("fingerprint", "-")) + self.query_one("#fld-wc-val", Label).update(str(d.get("word_count", 24))) + + qr_content = "QR Code (Press gg)" + if r.get("content") and "##" in r.get("content"): + qr_content = r.get("content") + self.query_one("#fld-qr-val", Label).update(qr_content) + except Exception: + pass + + +class SshBoard(BaseBoard): + """Matches 'SSH Board.png' mockup.""" + DEFAULT_CSS = """ + SshBoard { background: #000000; color: #ffffff; padding: 0 1; height: 1fr; } + .board-body { layout: horizontal; height: 1fr; } + .left-col { width: 1fr; margin-right: 2; height: 1fr; } + .right-col { width: 1fr; border: solid #ffffff; padding: 1; height: 1fr; } + .field-box { border: solid #ffffff; padding: 0 1; margin-bottom: 1; height: auto; } + .field-box.highlight { background: #ffffff; color: #000000; } + .field-title { text-style: bold; } + .field-row { layout: horizontal; } + .field-val { width: 1fr; } + .action-btn { background: #000000; color: #ffffff; padding: 0 1; margin-left: 1; } + """ + + def compose(self) -> ComposeResult: + yield InspectorHeader(id="ssh-header") + with Horizontal(classes="board-body"): + with Vertical(classes="left-col"): + with Vertical(classes="field-box highlight"): + yield Label("Private Key*", classes="field-title") + with Horizontal(classes="field-row"): + yield Label("[HIDDEN]", id="fld-priv-val", classes="field-val") + yield Label("Copy", classes="action-btn") + with Vertical(classes="field-box"): + yield Label("Public Key", classes="field-title") + with Horizontal(classes="field-row"): + yield Label("[PREVIEW]", id="fld-pub-val", classes="field-val") + yield Label("Copy", classes="action-btn") + with Vertical(classes="field-box"): + yield Label("Key Type", classes="field-title") + yield Label("Ed25519", id="fld-type-val") + with Vertical(classes="right-col"): + yield Label("Notes", classes="field-title") + yield Label("", id="fld-notes-val") + + def update_board(self) -> None: + if not self.is_mounted: return + d = self.entry_data + r = self.reveal_data + try: + self.query_one("#ssh-header", InspectorHeader).update_data(d, "SSH") + + priv = "[b]**********[/b] (vv to reveal)" + if r.get("content"): + priv = r.get("content") + self.query_one("#fld-priv-val", Label).update(priv) + + pub = d.get('public_key', '[PREVIEW]') + if len(pub) > 30: pub = pub[:30] + "..." + self.query_one("#fld-pub-val", Label).update(pub) + + self.query_one("#fld-type-val", Label).update(d.get("key_type", "RSA/Ed25519")) + self.query_one("#fld-notes-val", Label).update(d.get("notes", "")) + except Exception: + pass + + +class PgpBoard(BaseBoard): + """Matches 'PGP Board.png' mockup.""" + DEFAULT_CSS = SshBoard.DEFAULT_CSS.replace("SshBoard", "PgpBoard") + + def compose(self) -> ComposeResult: + yield InspectorHeader(id="pgp-header") + with Horizontal(classes="board-body"): + with Vertical(classes="left-col"): + with Vertical(classes="field-box highlight"): + yield Label("Private Key*", classes="field-title") + with Horizontal(classes="field-row"): + yield Label("[HIDDEN]", id="fld-priv-val", classes="field-val") + yield Label("Copy", classes="action-btn") + with Vertical(classes="field-box"): + yield Label("Fingerprint / ID", classes="field-title") + yield Label("-", id="fld-fp-val") + with Vertical(classes="field-box"): + yield Label("Identity", classes="field-title") + yield Label("-", id="fld-ident-val") + with Vertical(classes="right-col"): + yield Label("Notes", classes="field-title") + yield Label("", id="fld-notes-val") + + def update_board(self) -> None: + if not self.is_mounted: return + d = self.entry_data + r = self.reveal_data + try: + self.query_one("#pgp-header", InspectorHeader).update_data(d, "PGP") + + priv = "[b]**********[/b] (vv to reveal)" + if r.get("content"): + priv = r.get("content") + self.query_one("#fld-priv-val", Label).update(priv) + + self.query_one("#fld-fp-val", Label).update(d.get("fingerprint", "-")) + self.query_one("#fld-ident-val", Label).update(d.get("user_id", "-")) + self.query_one("#fld-notes-val", Label).update(d.get("notes", "")) + except Exception: + pass + + +class NostrBoard(BaseBoard): + """Matches 'Nostr Board.png' mockup.""" + DEFAULT_CSS = TotpBoard.DEFAULT_CSS.replace("TotpBoard", "NostrBoard") + + def compose(self) -> ComposeResult: + yield InspectorHeader(id="nostr-header") + with Horizontal(classes="board-body"): + with Vertical(classes="left-col"): + with Vertical(classes="field-box highlight"): + yield Label("nsec (Secret)*", classes="field-title") + with Horizontal(classes="field-row"): + yield Label("[HIDDEN]", id="fld-nsec-val", classes="field-val") + yield Label("Copy", classes="action-btn") + with Vertical(classes="field-box"): + yield Label("npub (Public)", classes="field-title") + with Horizontal(classes="field-row"): + yield Label("-", id="fld-npub-val", classes="field-val") + yield Label("Copy", classes="action-btn") + with Vertical(classes="field-box"): + yield Label("Hex Key", classes="field-title") + yield Label("-", id="fld-hex-val") + with Vertical(classes="right-col"): + yield Label("QR Code (Press g)", id="fld-qr-val", classes="qr-text") + + def update_board(self) -> None: + if not self.is_mounted: return + d = self.entry_data + r = self.reveal_data + try: + self.query_one("#nostr-header", InspectorHeader).update_data(d, "Nostr") + + nsec = "[b]**********[/b] (v to reveal)" + if r.get("content") and "##" not in r.get("content"): + nsec = r.get("content") + self.query_one("#fld-nsec-val", Label).update(nsec) + + npub = d.get('npub', '-') + if len(npub) > 30: npub = npub[:30] + "..." + self.query_one("#fld-npub-val", Label).update(npub) + + self.query_one("#fld-hex-val", Label).update(d.get("hex", "-")) + + qr_content = "QR Code (Press g)" + if r.get("content") and "##" in r.get("content"): + qr_content = r.get("content") + self.query_one("#fld-qr-val", Label).update(qr_content) + except Exception: + pass + + +class GenericBoard(BaseBoard): + """Fallback high-fidelity board.""" + DEFAULT_CSS = """ + GenericBoard { background: #000000; color: #ffffff; padding: 0 1; height: 1fr; } + .board-body { layout: horizontal; height: 1fr; } + .left-col { width: 1fr; margin-right: 2; height: 1fr; } + .right-col { width: 1fr; border: solid #ffffff; padding: 1; height: 1fr; } + .field-box { border: solid #ffffff; padding: 0 1; margin-bottom: 1; height: auto; } + .field-box.highlight { background: #ffffff; color: #000000; } + .field-title { text-style: bold; } + """ + + def compose(self) -> ComposeResult: + yield InspectorHeader(id="generic-header") + with Horizontal(classes="board-body"): + with Vertical(classes="left-col"): + with Vertical(classes="field-box highlight"): + yield Label("Revealed Secret", classes="field-title") + yield Label("[HIDDEN]", id="fld-secret-val") + with Vertical(classes="field-box"): + yield Label("Details", classes="field-title") + yield Label("-", id="fld-details-val") + with Vertical(classes="right-col"): + yield Label("Notes", classes="field-title") + yield Label("", id="fld-notes-val") + + def update_board(self) -> None: + if not self.is_mounted: return + d = self.entry_data + r = self.reveal_data + kind = str(d.get('kind', d.get('type', 'Unknown'))).capitalize() - content_rows = preview.splitlines()[:10] - action_rows = [ - "▣ Edit Doc (e) ▣ Export", - "▣ Archive (a) ▣ Max (z)", - ] - - return "\n".join([ - self._render_card("Content Preview", content_rows), - "", - self._render_card("Quick Actions", action_rows) - ]) + try: + self.query_one("#generic-header", InspectorHeader).update_data(d, kind) + + secret = "[HIDDEN] (v to reveal)" + if r.get("content"): + secret = rf"{r.get('content')}" + self.query_one("#fld-secret-val", Label).update(secret) + + details = [] + for k, v in d.items(): + if k not in ('label', 'notes', 'tags', 'kind', 'id', 'type', 'content'): + details.append(f"[b]{k.capitalize()}:[/b] {v}") + self.query_one("#fld-details-val", Label).update("\n".join(details) or "-") + + self.query_one("#fld-notes-val", Label).update(d.get('notes', '')) + + except Exception: + pass + class BoardContainer(Vertical): """ @@ -286,8 +498,9 @@ class BoardContainer(Vertical): DEFAULT_CSS = """ BoardContainer { height: 1fr; - padding: 1; - content-align: center top; + padding: 0; + margin: 0; + background: #000000; } """ @@ -312,10 +525,10 @@ class BoardContainer(Vertical): if kind in {"password", "stored_password"}: board = self._show_board("password") - elif kind in {"document", "note"}: - board = self._show_board("note") elif kind == "totp": board = self._show_board("totp") + elif kind in {"document", "note"}: + board = self._show_board("note") elif kind in {"seed", "managed_account"}: board = self._show_board("seed") elif kind == "ssh": @@ -324,10 +537,8 @@ class BoardContainer(Vertical): board = self._show_board("pgp") elif kind == "nostr": board = self._show_board("nostr") - elif kind == "key_value": - board = self._show_board("key_value") else: - board = self._show_board("password") + board = self._show_board("generic") board.entry_data = entry @@ -336,15 +547,15 @@ class BoardContainer(Vertical): mapping = { "idle": IdleBoard, "password": PasswordBoard, - "note": NoteBoard, "totp": TotpBoard, + "note": NoteBoard, "seed": SeedBoard, "ssh": SshBoard, "pgp": PgpBoard, "nostr": NostrBoard, - "key_value": KeyValueBoard, + "generic": GenericBoard, } - board_cls = mapping.get(board_type, IdleBoard) + board_cls = mapping.get(board_type, GenericBoard) new_board = board_cls() self.mount(new_board) return new_board diff --git a/src/seedpass/tui_v3/widgets/sidebar.py b/src/seedpass/tui_v3/widgets/sidebar.py index 778c1b45..d7912f29 100644 --- a/src/seedpass/tui_v3/widgets/sidebar.py +++ b/src/seedpass/tui_v3/widgets/sidebar.py @@ -75,34 +75,25 @@ class ProfileTree(Tree): class SidebarContainer(Static): """Container for the sidebar tree and toggle.""" def compose(self) -> ComposeResult: - yield Static("PROFILES", id="sidebar-title") yield ProfileTree("Profiles", id="profile-tree") DEFAULT_CSS = """ SidebarContainer { - width: 31; - background: #0d1114; - border-right: solid #1a3024; - } - #sidebar-title { - background: #1a3024; - color: #58f29d; - text-style: bold; - padding: 0 1; - height: 1; + width: 100%; + background: #999999; } #profile-tree { - background: transparent; - color: #97b8a6; + background: #999999; + color: #000000; border: none; padding: 0; } #profile-tree > .tree--guides { - color: #1a3024; + color: #000000; } #profile-tree > .tree--cursor { - background: #122019; - color: #58f29d; + background: #000000; + color: #ffffff; text-style: bold; } """