mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-27 07:52:03 +00:00
style(tui_v3): full 1:1 ui mockup parity pass with retro black and white theme
This commit is contained in:
1 parent
55384c1a2f
commit
1987d7707e
7 files changed
+554
-339
No files matched your search
+1
-1
@@ -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.
|
||||
|
||||
+31
-29
@@ -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;
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
"""
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
"""
|
||||
Reference in new issue
Block a user