|
@@ -27,25 +27,29 @@ using Spry;
|
|
|
* Main CSS - Base styles for all pages
|
|
* Main CSS - Base styles for all pages
|
|
|
*/
|
|
*/
|
|
|
private const string MAIN_CSS = """
|
|
private const string MAIN_CSS = """
|
|
|
-/* Base Reset & Typography */
|
|
|
|
|
|
|
+/* Base Reset & Layout */
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
+html, body {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+}
|
|
|
body {
|
|
body {
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
line-height: 1.6;
|
|
line-height: 1.6;
|
|
|
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
min-height: 100vh;
|
|
min-height: 100vh;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Header */
|
|
/* Header */
|
|
|
header {
|
|
header {
|
|
|
- background: rgba(44, 62, 80, 0.95);
|
|
|
|
|
|
|
+ background: #2c3e50;
|
|
|
color: white;
|
|
color: white;
|
|
|
padding: 1rem 2rem;
|
|
padding: 1rem 2rem;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- backdrop-filter: blur(10px);
|
|
|
|
|
- box-shadow: 0 2px 20px rgba(0,0,0,0.2);
|
|
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
header nav a {
|
|
header nav a {
|
|
|
color: white;
|
|
color: white;
|
|
@@ -61,11 +65,13 @@ header .visit-info {
|
|
|
opacity: 0.8;
|
|
opacity: 0.8;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* Main Content */
|
|
|
|
|
|
|
+/* Main Content - grows to fill space */
|
|
|
main.container {
|
|
main.container {
|
|
|
|
|
+ flex: 1;
|
|
|
max-width: 1200px;
|
|
max-width: 1200px;
|
|
|
- margin: 2rem auto;
|
|
|
|
|
- padding: 0 1rem;
|
|
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ padding: 2rem 1rem;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Cards */
|
|
/* Cards */
|
|
@@ -74,7 +80,7 @@ main.container {
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
|
padding: 2rem;
|
|
padding: 2rem;
|
|
|
margin-bottom: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
|
- box-shadow: 0 10px 40px rgba(0,0,0,0.15);
|
|
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Typography */
|
|
/* Typography */
|
|
@@ -86,8 +92,8 @@ ul, ol { margin-left: 1.5rem; margin-bottom: 1rem; }
|
|
|
li { margin-bottom: 0.5rem; color: #555; }
|
|
li { margin-bottom: 0.5rem; color: #555; }
|
|
|
|
|
|
|
|
/* Links */
|
|
/* Links */
|
|
|
-a { color: #667eea; text-decoration: none; transition: color 0.2s; }
|
|
|
|
|
-a:hover { color: #764ba2; text-decoration: underline; }
|
|
|
|
|
|
|
+a { color: #3498db; text-decoration: none; transition: color 0.2s; }
|
|
|
|
|
+a:hover { color: #2980b9; text-decoration: underline; }
|
|
|
|
|
|
|
|
/* Code */
|
|
/* Code */
|
|
|
code {
|
|
code {
|
|
@@ -107,19 +113,18 @@ pre {
|
|
|
margin: 1rem 0;
|
|
margin: 1rem 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/* Footer */
|
|
|
|
|
|
|
+/* Footer - at bottom */
|
|
|
footer {
|
|
footer {
|
|
|
- background: rgba(44, 62, 80, 0.9);
|
|
|
|
|
|
|
+ background: #2c3e50;
|
|
|
color: rgba(255,255,255,0.7);
|
|
color: rgba(255,255,255,0.7);
|
|
|
padding: 1.5rem;
|
|
padding: 1.5rem;
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
- margin-top: 2rem;
|
|
|
|
|
- backdrop-filter: blur(10px);
|
|
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Buttons & Forms */
|
|
/* Buttons & Forms */
|
|
|
button {
|
|
button {
|
|
|
- background: #667eea;
|
|
|
|
|
|
|
+ background: #3498db;
|
|
|
color: white;
|
|
color: white;
|
|
|
border: none;
|
|
border: none;
|
|
|
padding: 0.75rem 1.5rem;
|
|
padding: 0.75rem 1.5rem;
|
|
@@ -130,9 +135,7 @@ button {
|
|
|
transition: all 0.2s;
|
|
transition: all 0.2s;
|
|
|
}
|
|
}
|
|
|
button:hover {
|
|
button:hover {
|
|
|
- background: #5a6fd6;
|
|
|
|
|
- transform: translateY(-1px);
|
|
|
|
|
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
|
|
|
|
|
+ background: #2980b9;
|
|
|
}
|
|
}
|
|
|
input[type="text"] {
|
|
input[type="text"] {
|
|
|
padding: 0.75rem;
|
|
padding: 0.75rem;
|
|
@@ -143,7 +146,7 @@ input[type="text"] {
|
|
|
}
|
|
}
|
|
|
input[type="text"]:focus {
|
|
input[type="text"]:focus {
|
|
|
outline: none;
|
|
outline: none;
|
|
|
- border-color: #667eea;
|
|
|
|
|
|
|
+ border-color: #3498db;
|
|
|
}
|
|
}
|
|
|
""";
|
|
""";
|
|
|
|
|
|
|
@@ -155,17 +158,15 @@ private const string ADMIN_CSS = """
|
|
|
.admin-section {
|
|
.admin-section {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
gap: 2rem;
|
|
gap: 2rem;
|
|
|
- margin-top: 1rem;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Admin Sidebar */
|
|
/* Admin Sidebar */
|
|
|
.admin-sidebar {
|
|
.admin-sidebar {
|
|
|
width: 220px;
|
|
width: 220px;
|
|
|
- background: rgba(52, 73, 94, 0.95);
|
|
|
|
|
|
|
+ background: #34495e;
|
|
|
padding: 1.5rem;
|
|
padding: 1.5rem;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
|
- box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
|
|
|
|
- backdrop-filter: blur(10px);
|
|
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
.admin-sidebar h3 {
|
|
.admin-sidebar h3 {
|
|
|
color: white;
|
|
color: white;
|
|
@@ -179,7 +180,7 @@ private const string ADMIN_CSS = """
|
|
|
margin: 0;
|
|
margin: 0;
|
|
|
padding: 0;
|
|
padding: 0;
|
|
|
}
|
|
}
|
|
|
-.admin-sidebar li { margin-bottom: 0.5rem; }
|
|
|
|
|
|
|
+.admin-sidebar li { margin-bottom: 0.5rem; margin-left: 0; }
|
|
|
.admin-sidebar a {
|
|
.admin-sidebar a {
|
|
|
color: rgba(255,255,255,0.7);
|
|
color: rgba(255,255,255,0.7);
|
|
|
display: block;
|
|
display: block;
|
|
@@ -206,12 +207,11 @@ private const string ADMIN_CSS = """
|
|
|
flex-wrap: wrap;
|
|
flex-wrap: wrap;
|
|
|
}
|
|
}
|
|
|
.stat-card {
|
|
.stat-card {
|
|
|
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
|
+ background: #3498db;
|
|
|
color: white;
|
|
color: white;
|
|
|
padding: 1.5rem;
|
|
padding: 1.5rem;
|
|
|
border-radius: 12px;
|
|
border-radius: 12px;
|
|
|
min-width: 180px;
|
|
min-width: 180px;
|
|
|
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
|
|
|
|
}
|
|
}
|
|
|
.stat-card h3 {
|
|
.stat-card h3 {
|
|
|
color: rgba(255,255,255,0.9);
|
|
color: rgba(255,255,255,0.9);
|
|
@@ -234,15 +234,13 @@ private const string ADMIN_CSS = """
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 1rem;
|
|
gap: 1rem;
|
|
|
padding: 1rem 1.25rem;
|
|
padding: 1rem 1.25rem;
|
|
|
- background: white;
|
|
|
|
|
|
|
+ background: #f8f9fa;
|
|
|
margin-bottom: 0.75rem;
|
|
margin-bottom: 0.75rem;
|
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
|
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
|
|
|
- transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
|
|
+ transition: background 0.2s;
|
|
|
}
|
|
}
|
|
|
.user-item:hover {
|
|
.user-item:hover {
|
|
|
- transform: translateX(4px);
|
|
|
|
|
- box-shadow: 0 4px 12px rgba(0,0,0,0.12);
|
|
|
|
|
|
|
+ background: #e9ecef;
|
|
|
}
|
|
}
|
|
|
.user-item .user-name {
|
|
.user-item .user-name {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
@@ -270,10 +268,10 @@ private const string ADMIN_CSS = """
|
|
|
.nav-link {
|
|
.nav-link {
|
|
|
display: inline-block;
|
|
display: inline-block;
|
|
|
margin-top: 1.5rem;
|
|
margin-top: 1.5rem;
|
|
|
- color: #667eea;
|
|
|
|
|
|
|
+ color: #3498db;
|
|
|
font-weight: 500;
|
|
font-weight: 500;
|
|
|
}
|
|
}
|
|
|
-.nav-link:hover { color: #764ba2; }
|
|
|
|
|
|
|
+.nav-link:hover { color: #2980b9; }
|
|
|
""";
|
|
""";
|
|
|
|
|
|
|
|
// =============================================================================
|
|
// =============================================================================
|
|
@@ -424,7 +422,7 @@ class HomePage : PageComponent {
|
|
|
<h2>Features</h2>
|
|
<h2>Features</h2>
|
|
|
<ul>
|
|
<ul>
|
|
|
<li><strong>MainLayoutTemplate</strong> - Provides base HTML, header, footer</li>
|
|
<li><strong>MainLayoutTemplate</strong> - Provides base HTML, header, footer</li>
|
|
|
- <li><strong>AdminSectionTemplate</strong> - Adds sidebar for /admin/* routes</li>
|
|
|
|
|
|
|
+ <li><modulestrong>AdminSectionTemplate</strong> - Adds sidebar for /admin/* routes</li>
|
|
|
<li><strong>PageComponent</strong> - Pages automatically inherit templates</li>
|
|
<li><strong>PageComponent</strong> - Pages automatically inherit templates</li>
|
|
|
</ul>
|
|
</ul>
|
|
|
|
|
|
|
@@ -517,7 +515,9 @@ class AdminDashboardPage : PageComponent {
|
|
|
/**
|
|
/**
|
|
|
* AdminUsersPage - Admin page with interactive user list
|
|
* AdminUsersPage - Admin page with interactive user list
|
|
|
*
|
|
*
|
|
|
- * Demonstrates HTMX interactions within a templated page
|
|
|
|
|
|
|
+ * Demonstrates HTMX interactions within a templated page.
|
|
|
|
|
+ * The form targets the card with outerHTML swap so the entire card
|
|
|
|
|
+ * is replaced with the updated content.
|
|
|
*/
|
|
*/
|
|
|
class AdminUsersPage : PageComponent {
|
|
class AdminUsersPage : PageComponent {
|
|
|
|
|
|
|
@@ -529,14 +529,14 @@ class AdminUsersPage : PageComponent {
|
|
|
|
|
|
|
|
public override string markup { get {
|
|
public override string markup { get {
|
|
|
return """
|
|
return """
|
|
|
- <div class="card">
|
|
|
|
|
|
|
+ <div class="card" sid="users-card">
|
|
|
<h1>User Management</h1>
|
|
<h1>User Management</h1>
|
|
|
|
|
|
|
|
- <div class="user-list" sid="user-list">
|
|
|
|
|
- <!-- Users rendered here -->
|
|
|
|
|
|
|
+ <div class="user-list">
|
|
|
|
|
+ <spry-outlet sid="user-list"/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <form class="add-form" sid="add-form" spry-action=":AddUser" spry-target="user-list" hx-swap="innerHTML">
|
|
|
|
|
|
|
+ <form class="add-form" sid="add-form" spry-action=":AddUser" spry-target="users-card" hx-swap="outerHTML">
|
|
|
<input type="text" name="username" placeholder="Enter username" required>
|
|
<input type="text" name="username" placeholder="Enter username" required>
|
|
|
<button type="submit">Add User</button>
|
|
<button type="submit">Add User</button>
|
|
|
</form>
|
|
</form>
|
|
@@ -632,14 +632,11 @@ void main(string[] args) {
|
|
|
|
|
|
|
|
// Register templates with route prefixes
|
|
// Register templates with route prefixes
|
|
|
// MainLayoutTemplate applies to ALL routes (empty prefix)
|
|
// MainLayoutTemplate applies to ALL routes (empty prefix)
|
|
|
- application.add_transient<MainLayoutTemplate>()
|
|
|
|
|
- .as<PageTemplate>()
|
|
|
|
|
- .with_metadata(new TemplateRoutePrefix(""));
|
|
|
|
|
-
|
|
|
|
|
|
|
+ var spry_cfg = application.configure_with<SpryConfigurator>();
|
|
|
|
|
+ spry_cfg.add_template<MainLayoutTemplate>("");
|
|
|
|
|
+
|
|
|
// AdminSectionTemplate applies to /admin/* routes
|
|
// AdminSectionTemplate applies to /admin/* routes
|
|
|
- application.add_transient<AdminSectionTemplate>()
|
|
|
|
|
- .as<PageTemplate>()
|
|
|
|
|
- .with_metadata(new TemplateRoutePrefix("/admin"));
|
|
|
|
|
|
|
+ spry_cfg.add_template<AdminSectionTemplate>("/admin");
|
|
|
|
|
|
|
|
// Register page components as endpoints
|
|
// Register page components as endpoints
|
|
|
application.add_transient<HomePage>();
|
|
application.add_transient<HomePage>();
|