Vibe documents

This commit is contained in:
6d486f49
2026-06-23 13:30:43 -04:00
parent 6eaf7b0980
commit 89bd1ca1f9
92 changed files with 69503 additions and 67 deletions
+237
View File
@@ -0,0 +1,237 @@
@namespace Shared.Pages
@page "/docs"
<PageTitle>Docs</PageTitle>
<div class="docs-page">
<div class="docs-header">
<h1>Documentation</h1>
<p class="text-muted">Rules, keywords, factions, and more.</p>
</div>
<div class="docs-container">
<aside class="docs-sidebar">
<div class="search-box">
<i class="bi bi-search"></i>
<input type="text" placeholder="Search docs..." @bind="searchQuery" @bind:event="oninput" />
</div>
<div class="category-list">
<div class="category-item @(selectedCategory == null ? "active" : "")" @onclick="() => SelectCategory(null)">
All Pages
</div>
@foreach (var category in categories)
{
var cat = category;
<div class="category-item @(selectedCategory == cat ? "active" : "")" @onclick="() => SelectCategory(cat)">
@cat
</div>
}
</div>
</aside>
<main class="docs-content">
@if (selectedDoc == null)
{
<div class="doc-list">
@foreach (var doc in filteredDocs)
{
<div class="doc-card" @onclick="() => SelectDoc(doc)">
<div class="doc-card-category">@doc.Category</div>
<h3 class="doc-card-title">@doc.Title</h3>
@{
var desc = GetDescription(doc);
}
@if (desc != null)
{
<p class="doc-card-desc">@desc</p>
}
</div>
}
@if (!filteredDocs.Any())
{
<div class="empty-state">
<i class="bi bi-search"></i>
<p>No documentation found matching your search.</p>
</div>
}
</div>
}
else
{
<article class="doc-article">
<button class="btn-back" @onclick="() => SelectDoc(null)">
<i class="bi bi-arrow-left"></i> Back to list
</button>
<header class="doc-header">
<div class="doc-meta">@selectedDoc.Category</div>
<h1>@selectedDoc.Title</h1>
@{
var desc = GetDescription(selectedDoc);
}
@if (desc != null)
{
<p class="doc-description">@desc</p>
}
</header>
@if (!string.IsNullOrWhiteSpace(selectedDoc.Content))
{
<div class="doc-body">
@((MarkupString)FormatContent(selectedDoc.Content))
</div>
}
@if (HasExtraFrontmatter(selectedDoc))
{
<div class="doc-extra">
<h3>Details</h3>
<div class="doc-frontmatter">
@foreach (var kvp in selectedDoc.Frontmatter.Where(kvp => kvp.Key != "category" && kvp.Key != "description"))
{
<div class="fm-item">
<span class="fm-key">@kvp.Key</span>
<span class="fm-value">@kvp.Value</span>
</div>
}
</div>
</div>
}
</article>
}
</main>
</div>
</div>
@code {
private string searchQuery = "";
private string? selectedCategory;
private DocData? selectedDoc;
private List<string> categories = [];
private List<DocData> allDocs = [];
protected override void OnInitialized()
{
allDocs = DocDatabase.Docs.OrderBy(d => d.Category).ThenBy(d => d.Title).ToList();
categories = allDocs.Select(d => d.Category).Distinct().OrderBy(c => c).ToList();
}
private IEnumerable<DocData> filteredDocs => allDocs
.Where(d => (selectedCategory == null || d.Category == selectedCategory) &&
(string.IsNullOrWhiteSpace(searchQuery) ||
d.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ||
(GetDescription(d)?.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ?? false) ||
d.Content.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)));
private void SelectCategory(string? category)
{
selectedCategory = category;
selectedDoc = null;
}
private void SelectDoc(DocData? doc)
{
selectedDoc = doc;
}
private static string? GetDescription(DocData doc)
{
return doc.Frontmatter.TryGetValue("description", out var desc) && !string.IsNullOrWhiteSpace(desc) ? desc : null;
}
private static bool HasExtraFrontmatter(DocData doc)
{
var count = doc.Frontmatter.Count;
if (count == 0) return false;
if (count == 1 && doc.Frontmatter.ContainsKey("category")) return false;
if (count == 2 && doc.Frontmatter.ContainsKey("category") && doc.Frontmatter.ContainsKey("description")) return false;
return doc.Frontmatter.Any(kvp => kvp.Key != "category" && kvp.Key != "description");
}
private string FormatContent(string content)
{
if (string.IsNullOrWhiteSpace(content)) return "";
var lines = content.Replace("\r\n", "\n").Split('\n');
var html = new System.Text.StringBuilder();
var inList = false;
foreach (var rawLine in lines)
{
var line = rawLine.TrimEnd();
if (string.IsNullOrWhiteSpace(line))
{
if (inList) { html.AppendLine("</ul>"); inList = false; }
continue;
}
if (line.StartsWith("- "))
{
var item = line[2..].Trim();
if (item.Length > 0)
{
if (!inList) { html.AppendLine("<ul class=\"doc-list-items\">"); inList = true; }
html.Append("<li>");
html.Append(EncodeInline(item));
html.AppendLine("</li>");
}
continue;
}
if (inList) { html.AppendLine("</ul>"); inList = false; }
if (line.EndsWith(":") || line.Contains(":\n"))
{
var trimmed = line.TrimEnd(':');
if (trimmed.Length > 0)
{
html.Append("<p class=\"doc-label\">");
html.Append(System.Web.HttpUtility.HtmlEncode(trimmed));
html.AppendLine("</p>");
}
}
else
{
html.Append("<p>");
html.Append(EncodeInline(line));
html.AppendLine("</p>");
}
}
if (inList) html.AppendLine("</ul>");
return html.ToString();
}
private string EncodeInline(string text)
{
var result = new System.Text.StringBuilder();
var remaining = text;
while (remaining.Length > 0)
{
var boldIdx = remaining.IndexOf("**", StringComparison.Ordinal);
if (boldIdx >= 0)
{
var endBold = remaining.IndexOf("**", boldIdx + 2, StringComparison.Ordinal);
if (endBold >= 0)
{
result.Append(System.Web.HttpUtility.HtmlEncode(remaining[..boldIdx]));
result.Append("<strong>");
result.Append(System.Web.HttpUtility.HtmlEncode(remaining[(boldIdx + 2)..endBold]));
result.Append("</strong>");
remaining = remaining[(endBold + 2)..];
continue;
}
}
result.Append(System.Web.HttpUtility.HtmlEncode(remaining));
break;
}
return result.ToString();
}
}
+325
View File
@@ -0,0 +1,325 @@
.docs-page {
padding: 1rem 2rem 3rem;
max-width: 1200px;
margin: 0 auto;
}
.docs-header {
margin-bottom: 2rem;
}
.docs-header h1 {
margin-bottom: 0.25rem;
}
.docs-header p {
font-size: 0.9rem;
margin: 0;
}
.docs-container {
display: grid;
grid-template-columns: 220px 1fr;
gap: 2.5rem;
}
.docs-sidebar {
position: sticky;
top: 2rem;
align-self: start;
}
.search-box {
position: relative;
margin-bottom: 1.5rem;
}
.search-box i {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
font-size: 0.85rem;
}
.search-box input {
width: 100%;
padding: 0.55rem 0.75rem 0.55rem 2.25rem;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-primary);
font-size: 0.88rem;
font-family: inherit;
}
.search-box input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-glow);
}
.category-list {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.category-item {
padding: 0.45rem 0.75rem;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.88rem;
color: var(--text-secondary);
transition: all var(--transition);
}
.category-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.category-item.active {
background: rgba(108, 99, 255, 0.12);
color: var(--accent);
font-weight: 600;
}
.doc-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.doc-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.1rem 1.4rem;
cursor: pointer;
transition: all var(--transition);
}
.doc-card:hover {
border-color: var(--accent);
box-shadow: 0 0 15px var(--accent-glow);
transform: translateY(-1px);
}
.doc-card-category {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--accent);
font-weight: 600;
margin-bottom: 0.3rem;
}
.doc-card-title {
margin: 0 0 0.35rem;
font-size: 1.15rem;
font-weight: 600;
}
.doc-card-desc {
font-size: 0.88rem;
color: var(--text-secondary);
line-height: 1.5;
margin: 0;
}
.doc-article {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem 2.5rem;
}
.btn-back {
background: none;
border: none;
color: var(--text-muted);
font-size: 0.88rem;
padding: 0;
margin-bottom: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.4rem;
font-family: inherit;
transition: color var(--transition);
}
.btn-back:hover {
color: var(--text-primary);
}
.doc-header {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border);
}
.doc-meta {
font-size: 0.8rem;
color: var(--accent);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.4rem;
}
.doc-header h1 {
margin: 0 0 0.75rem;
font-size: 1.8rem;
font-weight: 700;
}
.doc-description {
font-size: 1.05rem;
line-height: 1.6;
color: var(--text-secondary);
margin: 0;
}
.doc-body {
line-height: 1.7;
font-size: 1rem;
color: var(--text-primary);
margin-bottom: 1.5rem;
}
.doc-body p {
margin: 0 0 0.6rem;
}
.doc-body p:last-child {
margin-bottom: 0;
}
.doc-label {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.4rem;
}
.doc-list-items {
margin: 0 0 0.8rem;
padding-left: 1.5rem;
list-style: none;
}
.doc-list-items li {
position: relative;
padding: 0.2rem 0;
color: var(--text-secondary);
font-size: 0.95rem;
}
.doc-list-items li::before {
content: "▸";
position: absolute;
left: -1.2rem;
color: var(--accent);
font-size: 0.75rem;
}
.doc-extra {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.doc-extra h3 {
font-size: 0.9rem;
font-weight: 600;
margin: 0 0 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.doc-frontmatter {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.4rem 1rem;
padding: 0.75rem 1rem;
background: var(--bg-elevated);
border-radius: var(--radius);
border: 1px solid var(--border);
}
.fm-item {
display: contents;
}
.fm-key {
color: var(--text-muted);
font-weight: 500;
font-size: 0.85rem;
white-space: nowrap;
}
.fm-value {
color: var(--text-secondary);
font-size: 0.85rem;
}
.doc-link-btn {
background: none;
border: none;
padding: 0;
font-family: inherit;
font-size: inherit;
font-weight: 600;
color: var(--accent);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-color: rgba(108, 99, 255, 0.3);
transition: color var(--transition), text-decoration-color var(--transition);
}
.doc-link-btn:hover {
color: #7b73ff;
text-decoration-color: #7b73ff;
}
.empty-state {
text-align: center;
padding: 4rem 1rem;
color: var(--text-muted);
}
.empty-state i {
font-size: 3rem;
display: block;
margin-bottom: 1rem;
}
@media (max-width: 900px) {
.docs-container {
grid-template-columns: 1fr;
}
.docs-sidebar {
position: static;
}
.category-list {
flex-direction: row;
overflow-x: auto;
gap: 0.4rem;
padding-bottom: 0.5rem;
}
.category-item {
white-space: nowrap;
flex-shrink: 0;
}
.doc-article {
padding: 1.25rem;
}
}