refactor: extract shared settings stylesheet (pg.css) and clean up inline styles

- Create cortex/static/pg.css with shared variables, nav, containers, form
  elements, button classes, text utilities, and feedback messages
- All four settings pages (settings, notifications, tools, help) now link to
  pg.css and have page-specific-only <style> blocks
- Style block line counts reduced: settings 244→70, notifications 189→42,
  tools 126→88, help 122→75
- Inline style= attributes reduced: settings 45→4, notifications 7→3,
  tools 12→4, help 0
- Introduced shared CSS classes: .btn-submit, .btn-save, .btn-cancel,
  .btn-secondary, .action-link, .hint, .section-note, .page-title,
  .page-subtitle, .page-wrap, .usage-table, .tool-cat-row
- Fix tools_settings.py: replace server-generated inline styles on category
  header rows with .tool-cat-row CSS class
- Fix settings.py: add btn-save/btn-cancel/persona-rename-cancel classes to
  server-rendered persona rename form buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-11 23:22:59 -04:00
parent 8ab1942514
commit 4c3d9a7a65
7 changed files with 415 additions and 718 deletions

View File

@@ -7,218 +7,65 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/pg.css">
<script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
<style>
:root {
--pg-bg: #0f1117; --pg-surface: #1a1d27;
--pg-border: #2d3148;
--pg-text: #e2e8f0; --pg-muted: #94a3b8;
--pg-dim: #64748b; --pg-dimmer: #475569;
--pg-bright: #cbd5e1; --pg-nav-hover: rgba(255,255,255,0.05);
/* ── Test action buttons ── */
.test-btn-row { display: flex; gap: 0.6rem; margin-top: 0.5rem; }
.test-btn {
flex: 1; padding: 0.6rem 0.75rem;
border: 1px solid var(--pg-border); border-radius: 6px;
background: var(--pg-bg); color: var(--pg-text);
font-size: 0.85rem; font-weight: 500; cursor: pointer;
transition: border-color 0.15s, color 0.15s; text-align: center;
}
[data-theme="light"] {
--pg-bg: #f4f2fa; --pg-surface: #ffffff;
--pg-border: #d0c8e8;
--pg-text: #1a1228; --pg-muted: #5a5478;
--pg-dim: #7a7290; --pg-dimmer: #9e98b0;
--pg-bright: #1a1228; --pg-nav-hover: rgba(0,0,0,0.05);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--pg-bg);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--pg-text);
padding: 1.5rem;
}
.card {
background: var(--pg-surface);
border: 1px solid var(--pg-border);
border-radius: 12px;
padding: 2.5rem 2rem;
width: 100%;
max-width: 520px;
}
.page-nav {
display: flex;
align-items: center;
gap: 0.25rem;
margin-bottom: 1.75rem;
flex-wrap: wrap;
}
.nav-link {
display: inline-flex;
align-items: center;
padding: 0.3rem 0.6rem;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
color: var(--pg-dim);
text-decoration: none;
transition: color 0.15s, background 0.15s;
white-space: nowrap;
}
.nav-link:hover { color: var(--pg-bright); background: var(--pg-nav-hover); }
.nav-link.active { color: #a78bfa; }
.nav-spacer { flex: 1; min-width: 0.5rem; }
.nav-link.nav-logout { color: var(--pg-dimmer); }
.nav-link.nav-logout:hover { color: var(--pg-muted); background: none; }
.logo { margin-bottom: 1.75rem; }
.logo h1 { font-size: 1.4rem; font-weight: 700; color: #a78bfa; }
.logo p { font-size: 0.8rem; color: var(--pg-muted); margin-top: 0.2rem; }
h2 {
font-size: 0.9rem;
font-weight: 600;
color: var(--pg-muted);
margin-bottom: 1rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--pg-border);
}
.section { margin-bottom: 2rem; }
label {
display: block;
font-size: 0.8rem;
font-weight: 500;
color: var(--pg-muted);
margin-bottom: 0.4rem;
}
input, select {
width: 100%;
padding: 0.65rem 0.85rem;
background: var(--pg-bg);
border: 1px solid var(--pg-border);
border-radius: 6px;
color: var(--pg-text);
font-size: 0.95rem;
outline: none;
transition: border-color 0.15s;
}
input:focus, select:focus { border-color: #7c3aed; }
input[type="password"] { font-family: monospace; letter-spacing: 0.05em; }
.field { margin-bottom: 1rem; }
button[type="submit"] {
width: 100%;
padding: 0.7rem;
margin-top: 0.25rem;
background: #7c3aed;
border: none;
border-radius: 6px;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
button[type="submit"]:hover { background: #6d28d9; }
.error { color: #f87171; font-size: 0.85rem; text-align: center; margin-bottom: 1rem; }
.success { color: #4ade80; font-size: 0.85rem; text-align: center; margin-bottom: 1rem; }
.btn-row { display: flex; gap: 0.6rem; margin-top: 0.5rem; }
.btn {
flex: 1;
padding: 0.6rem 0.75rem;
border: 1px solid var(--pg-border);
border-radius: 6px;
background: var(--pg-bg);
color: var(--pg-text);
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
text-align: center;
}
.btn:hover { border-color: #7c3aed; color: #a78bfa; }
.btn:disabled { opacity: 0.5; cursor: default; }
.test-btn:hover { border-color: var(--pg-action); color: var(--pg-accent); }
.test-btn:disabled { opacity: 0.5; cursor: default; }
.test-result {
margin-top: 0.75rem;
padding: 0.6rem 0.8rem;
border-radius: 6px;
font-size: 0.82rem;
line-height: 1.5;
display: none;
margin-top: 0.75rem; padding: 0.6rem 0.8rem; border-radius: 6px;
font-size: 0.82rem; line-height: 1.5; display: none;
}
.test-result.ok { background: rgba(74, 222, 128, 0.1); color: #4ade80; border: 1px solid rgba(74, 222, 128, 0.25); }
.test-result.err { background: rgba(248, 113, 113, 0.1); color: #f87171; border: 1px solid rgba(248, 113, 113, 0.25); }
.test-result.ok { background: rgba(74,222,128,0.1); color: #4ade80; border: 1px solid rgba(74,222,128,0.25); }
.test-result.err { background: rgba(248,113,113,0.1); color: #f87171; border: 1px solid rgba(248,113,113,0.25); }
.hint { font-size: 0.78rem; color: var(--pg-dim); margin-top: 0.35rem; line-height: 1.5; }
/* Channel config blocks */
/* ── Channel config collapsible blocks ── */
details.channel-block {
border: 1px solid var(--pg-border);
border-radius: 8px;
margin-bottom: 0.75rem;
overflow: hidden;
border: 1px solid var(--pg-border); border-radius: 8px;
margin-bottom: 0.75rem; overflow: hidden;
}
details.channel-block summary {
padding: 0.75rem 1rem;
font-size: 0.85rem;
font-weight: 600;
color: var(--pg-muted);
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 0.5rem;
user-select: none;
background: var(--pg-bg);
padding: 0.75rem 1rem; font-size: 0.85rem; font-weight: 600;
color: var(--pg-muted); cursor: pointer; list-style: none;
display: flex; align-items: center; gap: 0.5rem;
user-select: none; background: var(--pg-bg);
}
details.channel-block summary::-webkit-details-marker { display: none; }
details.channel-block summary::before {
content: '▶';
font-size: 0.65rem;
color: var(--pg-dimmer);
transition: transform 0.15s;
flex-shrink: 0;
content: '▶'; font-size: 0.65rem; color: var(--pg-dimmer);
transition: transform 0.15s; flex-shrink: 0;
}
details.channel-block[open] summary::before { transform: rotate(90deg); }
details.channel-block[open] summary { border-bottom: 1px solid var(--pg-border); }
.channel-block-body {
padding: 1rem 1rem 0.25rem;
}
.channel-block-body { padding: 1rem 1rem 0.25rem; }
.channel-hint {
font-size: 0.75rem;
color: var(--pg-dimmer);
margin-top: -0.6rem;
margin-bottom: 1rem;
line-height: 1.5;
font-size: 0.75rem; color: var(--pg-dimmer);
margin-top: -0.6rem; margin-bottom: 1rem; line-height: 1.5;
}
</style>
</head>
<body>
<div class="card">
<nav class="page-nav">
<a href="{{ back_href }}" class="nav-link">← Chat</a>
<a href="{{ help_href }}" class="nav-link">Help</a>
<a href="/settings" class="nav-link">Settings</a>
<a href="/settings/notifications" class="nav-link active">Notifications</a>
<a href="/settings/tools" class="nav-link">Tools</a>
<span class="nav-spacer"></span>
<a href="/logout" class="nav-link nav-logout">Sign out</a>
</nav>
<div class="logo">
<h1>Notifications</h1>
<p>How Inara reaches out proactively — reminders, cron jobs, and memory digests.</p>
</div>
<nav class="page-nav">
<a href="{{ back_href }}" class="nav-link">← Chat</a>
<a href="{{ help_href }}" class="nav-link">Help</a>
<a href="/settings" class="nav-link">Settings</a>
<a href="/settings/notifications" class="nav-link active">Notifications</a>
<a href="/settings/tools" class="nav-link">Tools</a>
<span class="nav-spacer"></span>
<a href="/logout" class="nav-link nav-logout">Sign out</a>
</nav>
<div class="page-wrap">
<h1 class="page-title">Notifications</h1>
<p class="page-subtitle">How Inara reaches out proactively — reminders, cron jobs, and memory digests.</p>
<!-- SUCCESS -->
<!-- ERROR -->
@@ -254,7 +101,7 @@
<!-- Nextcloud Talk -->
<div class="section">
<h2>Nextcloud Talk</h2>
<p class="hint" style="margin-bottom:1rem;">
<p class="section-note">
Configure to send and receive messages via your Nextcloud Talk bot.
<strong>Sending</strong> requires the bot URL, secret, and notification room.
<strong>Reading history</strong> (<code>nc_talk_history</code> tool) additionally
@@ -266,7 +113,7 @@
<div class="channel-block-body">
<p class="channel-hint">
Set these up in your Nextcloud Talk room → Bot settings.
See the <a href="/help" style="color:#a78bfa;">setup guide</a> for step-by-step instructions.
See the <a href="/help" style="color:var(--pg-accent);">setup guide</a> for step-by-step instructions.
</p>
<div class="field">
<label for="nc_url">Nextcloud URL</label>
@@ -322,7 +169,7 @@
<!-- Home Assistant -->
<div class="section">
<h2>Home Assistant</h2>
<p class="hint" style="margin-bottom:1rem;">
<p class="section-note">
Receive events from HA automations and let Inara call the HA REST API
(read states, control devices). Webhook ID is the shared secret used in your
HA <code>rest_command</code> URL.
@@ -374,7 +221,7 @@
<!-- Google Chat -->
<div class="section">
<h2>Google Chat</h2>
<p class="hint" style="margin-bottom:1rem;">
<p class="section-note">
Outbound webhook for proactive messages to a Google Chat space.
Incoming messages are handled separately via the Google Chat Add-on.
</p>
@@ -383,8 +230,7 @@
<summary>Outbound webhook</summary>
<div class="channel-block-body">
<p class="channel-hint">
Create a webhook in your Google Chat space → Manage webhooks.
Paste the full URL here.
Create a webhook in your Google Chat space → Manage webhooks. Paste the full URL here.
</p>
<div class="field">
<label for="gc_outbound_webhook">Webhook URL</label>
@@ -397,19 +243,19 @@
</details>
</div>
<button type="submit">Save notification settings</button>
<button type="submit" class="btn-submit">Save notification settings</button>
</form>
<!-- Test -->
<div class="section" style="margin-top:2rem;">
<h2>Test</h2>
<p class="hint" style="margin-bottom:0.85rem">
<p class="section-note">
Fire a notification via your configured channel or run the reminder check
immediately — no need to wait for the daily 09:00 scheduler job.
</p>
<div class="btn-row">
<button class="btn" id="btn-test-notify">Send Test Notification</button>
<button class="btn" id="btn-check-reminders">Check Reminders Now</button>
<div class="test-btn-row">
<button class="test-btn" id="btn-test-notify">Send Test Notification</button>
<button class="test-btn" id="btn-check-reminders">Check Reminders Now</button>
</div>
<div class="test-result" id="test-result"></div>
</div>