Day 2 vibes

This commit is contained in:
2026-06-11 09:04:54 -04:00
parent adeb4ae7cb
commit 1388182ebe
53 changed files with 54413 additions and 45907 deletions
+97 -27
View File
@@ -1,5 +1,7 @@
using System.Text.RegularExpressions;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.RegularExpressions;
var exeDir = AppContext.BaseDirectory;
@@ -18,13 +20,17 @@ if (!Directory.Exists(srcDir))
return 1;
}
SyncNotes(srcDir, dstDir);
var entries = SyncNotes(srcDir, dstDir);
CopyOverview(solutionDir, dstDir, entries);
CopyImages(solutionDir, Path.GetFullPath(Path.Combine(solutionDir, "Web", "wwwroot", "docs")));
var indexJson = BuildIndex(entries);
File.WriteAllText(Path.Combine(Path.GetDirectoryName(dstDir)!, "notes-index.json"), indexJson);
GenerateMap(srcDir, Path.GetFullPath(Path.Combine(solutionDir, "Web", "wwwroot", "docs")));
Console.WriteLine("Done.");
return 0;
static void SyncNotes(string srcDir, string dstDir)
static List<object> SyncNotes(string srcDir, string dstDir)
{
Directory.CreateDirectory(dstDir);
@@ -38,16 +44,33 @@ static void SyncNotes(string srcDir, string dstDir)
var name = Path.GetFileNameWithoutExtension(file);
var slug = Slugify(name);
File.Copy(file, Path.Combine(dstDir, $"{slug}.md"), overwrite: true);
File.Copy(file, Path.Combine(dstDir, $"{slug}.md"), true);
string? category = null;
string? cost = null;
string? gearCategory = null;
string? effect = null;
string? location = null;
var content = File.ReadAllText(file);
var fmMatch = Regex.Match(content, @"^---\s*\n(.*?)\n---", RegexOptions.Singleline);
if (fmMatch.Success)
{
var catMatch = Regex.Match(fmMatch.Groups[1].Value, @"(?m)^category:\s*(.+)$");
var fm = fmMatch.Groups[1].Value;
var catMatch = Regex.Match(fm, @"(?m)^category:\s*(.+)$", RegexOptions.IgnoreCase);
if (catMatch.Success)
category = catMatch.Groups[1].Value.Trim().Trim('"');
var costMatch = Regex.Match(fm, @"(?m)^cost:\s*(.+)$", RegexOptions.IgnoreCase);
if (costMatch.Success)
cost = costMatch.Groups[1].Value.Trim().Trim('"');
var gcMatch = Regex.Match(fm, @"(?m)^gear category:\s*(.+)$", RegexOptions.IgnoreCase);
if (gcMatch.Success)
gearCategory = gcMatch.Groups[1].Value.Trim().Trim('"');
var effMatch = Regex.Match(fm, @"(?m)^effect:\s*(.+)$", RegexOptions.IgnoreCase);
if (effMatch.Success)
effect = effMatch.Groups[1].Value.Trim().Trim('"');
var locMatch = Regex.Match(fm, @"(?m)^location:\s*(.+)$", RegexOptions.IgnoreCase);
if (locMatch.Success)
location = locMatch.Groups[1].Value.Trim().Trim('"');
}
var entry = new Dictionary<string, object?>
@@ -57,18 +80,61 @@ static void SyncNotes(string srcDir, string dstDir)
};
if (category != null)
entry["category"] = category;
if (cost != null)
entry["cost"] = cost;
if (gearCategory != null)
entry["gearCategory"] = gearCategory;
if (effect != null)
entry["effect"] = effect;
if (location != null)
entry["location"] = location;
entries.Add(entry);
}
var index = new Dictionary<string, object> { ["notes"] = entries };
var json = JsonSerializer.Serialize(index, new JsonSerializerOptions { WriteIndented = true });
var indexPath = Path.Combine(Path.GetDirectoryName(dstDir)!, "notes-index.json");
File.WriteAllText(indexPath, json);
Console.WriteLine($"Copied {entries.Count} notes.");
Console.WriteLine($"Index written to: {indexPath}");
return entries;
}
static void CopyOverview(string solutionDir, string dstDir, List<object> entries)
{
var srcOverview = Path.GetFullPath(Path.Combine(solutionDir, "..", "docs", "Overview.md"));
if (!File.Exists(srcOverview)) return;
var slug = "overview";
File.Copy(srcOverview, Path.Combine(dstDir, $"{slug}.md"), true);
entries.Add(new Dictionary<string, object?>
{
["slug"] = slug,
["title"] = "Overview",
["category"] = "Overview"
});
Console.WriteLine("Copied Overview.md.");
}
static void CopyImages(string solutionDir, string dstDir)
{
var srcImgs = Path.GetFullPath(Path.Combine(solutionDir, "..", "docs", "Images"));
if (!Directory.Exists(srcImgs)) return;
var dstImgs = Path.Combine(dstDir, "images");
Directory.CreateDirectory(dstImgs);
foreach (var file in Directory.EnumerateFiles(srcImgs))
{
var name = Path.GetFileName(file);
File.Copy(file, Path.Combine(dstImgs, name), true);
}
Console.WriteLine($"Copied images from {srcImgs}.");
}
static string BuildIndex(List<object> entries)
{
var index = new Dictionary<string, object> { ["notes"] = entries };
return JsonSerializer.Serialize(index, new JsonSerializerOptions { WriteIndented = true });
}
static void GenerateMap(string srcDir, string dstDir)
@@ -79,7 +145,7 @@ static void GenerateMap(string srcDir, string dstDir)
["Forest"] = "#2e7d32",
["Mountain"] = "#78909c",
["Water"] = "#42a5f5",
["Wasteland"] = "#8d6e63",
["Wasteland"] = "#8d6e63"
};
var regionFiles = Directory.EnumerateFiles(srcDir, "*.md")
@@ -130,19 +196,19 @@ static void GenerateMap(string srcDir, string dstDir)
var srcMapImage = Path.GetFullPath(Path.Combine(srcDir, "..", "Images", "Map.png"));
var dstMapImage = Path.Combine(dstDir, "Map.png");
if (File.Exists(srcMapImage))
File.Copy(srcMapImage, dstMapImage, overwrite: true);
File.Copy(srcMapImage, dstMapImage, true);
var nameLookup = regions.ToDictionary(r => r.Name);
var pad = 60;
var maxX = (regions.Count > 0 ? regions.Max(r => r.X) : 0) + pad * 2;
var maxY = (regions.Count > 0 ? regions.Max(r => r.Y) : 0) + pad * 2;
var svg = new System.Text.StringBuilder();
var svg = new StringBuilder();
svg.AppendLine("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 700 461\">");
svg.AppendLine("""<image href="Map.png" width="700" height="461" preserveAspectRatio="xMidYMid meet"/>""");
svg.AppendLine($"""<defs>""");
svg.AppendLine("""<defs>""");
foreach (var (terrain, color) in terrainColors)
{
svg.AppendLine($"""<radialGradient id="glow-{terrain}" cx="50%" cy="50%" r="50%">""");
@@ -150,17 +216,18 @@ static void GenerateMap(string srcDir, string dstDir)
svg.AppendLine($"""<stop offset="100%" stop-color="{color}" stop-opacity="0"/>""");
svg.AppendLine("</radialGradient>");
}
svg.AppendLine("</defs>");
foreach (var region in regions)
{
foreach (var conn in region.Connections)
{
if (!nameLookup.TryGetValue(conn, out var target) || string.Compare(region.Name, conn, StringComparison.OrdinalIgnoreCase) >= 0)
if (!nameLookup.TryGetValue(conn, out var target) ||
string.Compare(region.Name, conn, StringComparison.OrdinalIgnoreCase) >= 0)
continue;
svg.AppendLine($"""<line x1="{region.X}" y1="{region.Y}" x2="{target.X}" y2="{target.Y}" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>""");
}
svg.AppendLine(
$"""<line x1="{region.X}" y1="{region.Y}" x2="{target.X}" y2="{target.Y}" stroke="rgba(255,255,255,0.12)" stroke-width="2" stroke-linecap="round"/>""");
}
foreach (var region in regions)
@@ -171,17 +238,20 @@ static void GenerateMap(string srcDir, string dstDir)
svg.AppendLine($"""<a href="/docs/{region.Slug}" target="_top">""");
svg.AppendLine($"""<circle cx="{cx}" cy="{cy}" r="32" fill="url(#glow-{region.Terrain})"/>""");
svg.AppendLine($"""<circle cx="{cx}" cy="{cy}" r="18" fill="{color}cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>""");
svg.AppendLine(
$"""<circle cx="{cx}" cy="{cy}" r="18" fill="{color}cc" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>""");
var labelX = cx + 24;
svg.AppendLine($"""<text x="{labelX}" y="{cy + 4}" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">""");
svg.Append(System.Text.Encodings.Web.HtmlEncoder.Default.Encode(region.Name));
svg.AppendLine(
$"""<text x="{labelX}" y="{cy + 4}" fill="rgba(255,255,255,0.9)" font-family="system-ui,sans-serif" font-size="11" font-weight="600">""");
svg.Append(HtmlEncoder.Default.Encode(region.Name));
svg.AppendLine("</text>");
foreach (var lm in region.Landmarks)
{
svg.AppendLine($"""<text x="{labelX}" y="{cy + 18}" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic">""");
svg.Append(System.Text.Encodings.Web.HtmlEncoder.Default.Encode($"\u2605 {lm}"));
svg.AppendLine(
$"""<text x="{labelX}" y="{cy + 18}" fill="rgba(255,255,255,0.45)" font-family="system-ui,sans-serif" font-size="9" font-style="italic">""");
svg.Append(HtmlEncoder.Default.Encode($"\u2605 {lm}"));
svg.AppendLine("</text>");
}
@@ -231,10 +301,11 @@ static string? FindContainingDir(string startDir, string markerFile)
return dir.FullName;
dir = dir.Parent;
}
return null;
}
class RegionData
internal class RegionData
{
public string Name { get; set; } = "";
public string Slug { get; set; } = "";
@@ -244,4 +315,3 @@ class RegionData
public List<string> Connections { get; set; } = new();
public List<string> Landmarks { get; set; } = new();
}
+2 -1
View File
@@ -1,4 +1,5 @@
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
@using Web.Pages
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(NotFound)">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
+2
View File
@@ -5,7 +5,9 @@
</div>
<main>
<article class="content px-4">
<TelerikRootComponent>
@Body
</TelerikRootComponent>
</article>
</main>
</div>
+2 -1
View File
@@ -9,7 +9,8 @@ main {
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
background-color: var(--bg-sidebar);
border-right: 1px solid var(--border-color);
}
.top-row {
+17 -4
View File
@@ -1,8 +1,10 @@
@inject Web.Services.DocsService DocsService
@inject DocsService DocsService
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">Web</a>
<a class="navbar-brand" href="">
<span class="brand-text">Trailblazer</span>
</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
@@ -16,6 +18,16 @@
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="docs/overview">
<span class="bi bi-book-nav-menu" aria-hidden="true"></span> Overview
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="gear">
<span class="bi bi-tools-nav-menu" aria-hidden="true"></span> Gear
</NavLink>
</div>
@if (groupedNotes == null)
{
@@ -53,7 +65,7 @@
protected override async Task OnInitializedAsync()
{
var index = await DocsService.GetIndexAsync();
groupedNotes = (index.Notes ?? new())
groupedNotes = (index.Notes ?? new List<NoteInfo>())
.GroupBy(n => string.IsNullOrEmpty(n.Category) ? "Uncategorized" : n.Category)
.OrderBy(g => g.Key)
.Select(g => new NoteGroup { Category = g.Key, Notes = g.OrderBy(n => n.Title).ToList() })
@@ -63,6 +75,7 @@
private class NoteGroup
{
public string Category { get; set; } = "";
public List<Web.Models.NoteInfo> Notes { get; set; } = new();
public List<NoteInfo> Notes { get; set; } = new();
}
}
+40 -22
View File
@@ -9,9 +9,17 @@
}
.navbar-brand {
font-size: 1.1rem;
font-weight: 600;
letter-spacing: 0.02em;
font-family: 'Outfit', sans-serif;
font-size: 1.25rem;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--accent) !important;
}
.brand-text {
background: linear-gradient(135deg, var(--accent) 0%, var(--primary-light) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.bi {
@@ -37,6 +45,14 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.bi-book-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-book' viewBox='0 0 16 16'%3E%3Cpath d='M1 2.828c.885-.37 2.154-.769 4-.388A5 5 0 0 1 8 3.03V13.5a.5.5 0 0 1-.5.5c-1.777 0-3.158-.551-4.21-1.11-.534-.284-.822-.702-1.02-1.106C1.879 11.08 1.5 10.183 1.5 9V3c0-.138.172-.255.5-.172zM2 10.304c.367.214.9.482 1.5.662.6.18 1.174.243 1.5.256V3.881c-.563-.064-1.242-.08-2-.354C2.462 3.267 2.184 3.04 2 2.816v7.488zM7.5 13.5V3.03a5 5 0 0 1 2.5-.59c1.846-.38 3.115-.018 4 .388V2.67c0-.138.328-.255.5-.172.328.083.5.255.5.172v7.5c0 .828-.5 1.5-1.5 1.5s-1.5-.672-1.5-1.5v-5.5a.5.5 0 0 0-1 0v5.5c0 .828-.5 1.5-1.5 1.5s-1.5-.672-1.5-1.5v-.5z'/%3E%3C/svg%3E");
}
.bi-tools-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-gear' viewBox='0 0 16 16'%3E%3Cpath d='M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z'/%3E%3Cpath d='M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z'/%3E%3C/svg%3E");
}
.bi-file-text-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-file-text' viewBox='0 0 16 16'%3E%3Cpath d='M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zM5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1H5z'/%3E%3Cpath d='M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z'/%3E%3C/svg%3E");
}
@@ -47,40 +63,42 @@
}
.nav-item ::deep a {
color: rgba(255, 255, 255, 0.75);
border-radius: 0;
height: 2.6rem;
color: var(--text-main);
border-radius: 8px;
height: 2.8rem;
display: flex;
align-items: center;
line-height: 2.6rem;
line-height: 2.8rem;
padding: 0 1rem;
border-left: 3px solid transparent;
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
margin: 0.2rem 0.5rem;
transition: all 0.2s ease;
}
.nav-item ::deep a.active {
background-color: rgba(255, 255, 255, 0.12);
color: white;
border-left-color: rgba(255, 255, 255, 0.7);
background-color: var(--primary);
color: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.nav-item ::deep a:hover {
background-color: rgba(255, 255, 255, 0.07);
color: white;
background-color: rgba(255, 255, 255, 0.05);
color: var(--accent);
}
.nav-item-doc ::deep a {
padding-left: 1.75rem !important;
font-size: 0.8rem;
height: 1.85rem !important;
line-height: 1.85rem !important;
border-left-color: transparent;
padding-left: 2.5rem !important;
font-size: 0.85rem;
height: 2.2rem !important;
line-height: 2.2rem !important;
margin: 0.1rem 0.5rem;
}
.nav-item-doc ::deep a.active {
background-color: rgba(255, 255, 255, 0.1);
color: white;
border-left-color: #6ea8fe;
background-color: rgba(88, 129, 87, 0.2);
color: var(--accent);
border-left: 2px solid var(--accent);
border-radius: 0 8px 8px 0;
margin-left: 0;
}
.nav-item-doc ::deep a:hover {
+4
View File
@@ -5,6 +5,10 @@ public class NoteInfo
public string Slug { get; set; } = "";
public string Title { get; set; } = "";
public string? Category { get; set; }
public string? Cost { get; set; }
public string? GearCategory { get; set; }
public string? Effect { get; set; }
public string? Location { get; set; }
}
public class NotesIndex
+3 -2
View File
@@ -1,5 +1,5 @@
@page "/docs/{Slug}"
@inject Web.Services.DocsService DocsService
@inject DocsService DocsService
<PageTitle>@(doc?.Title ?? "Not Found")</PageTitle>
@@ -39,7 +39,7 @@ else
@code {
[Parameter] public string Slug { get; set; } = "";
private Web.Models.NoteDocument? doc;
private NoteDocument? doc;
private bool loading = true;
protected override async Task OnParametersSetAsync()
@@ -49,4 +49,5 @@ else
doc = await DocsService.GetNoteAsync(Slug);
loading = false;
}
}
+38 -3
View File
@@ -1,7 +1,42 @@
@page "/docs"
@inject DocsService DocsService
<PageTitle>Docs</PageTitle>
<PageTitle>Documentation</PageTitle>
<h1>Documentation</h1>
<div class="section-header d-flex align-items-center mb-4">
<h1 class="mb-0">Documentation</h1>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div>
<p>Select a note from the sidebar to view its contents.</p>
@if (index == null)
{
<div class="d-flex justify-content-center py-5">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
else
{
<div class="docs-grid">
@foreach (var note in index.Notes.OrderBy(n => n.Title))
{
<NavLink href="@($"docs/{note.Slug}")" class="docs-card">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge bg-dark text-success border border-success border-opacity-25">@note.Category</span>
</div>
<h3>@note.Title</h3>
<p class="text-muted small mb-0">Explore details about @note.Title</p>
</NavLink>
}
</div>
}
@code {
private NotesIndex? index;
protected override async Task OnInitializedAsync()
{
index = await DocsService.GetIndexAsync();
}
}
+51
View File
@@ -0,0 +1,51 @@
@page "/gear"
@inject DocsService DocsService
<PageTitle>Gear & Equipment</PageTitle>
<div class="section-header d-flex align-items-center mb-4">
<h1 class="mb-0">Gear & Equipment</h1>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div>
@if (gearNotes == null)
{
<div class="d-flex justify-content-center py-5">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
else
{
<div class="grid-container">
<TelerikGrid Data="@gearNotes" Pageable="true" PageSize="50" Sortable="true" FilterMode="@GridFilterMode.FilterRow"
Height="calc(100vh - 250px)">
<GridColumns>
<GridColumn Field="@(nameof(NoteInfo.Title))" Title="Item Name" Width="200px">
<Template>
<NavLink href="@($"docs/{(context as NoteInfo)!.Slug}")" class="fw-bold">@((context as NoteInfo)!.Title)</NavLink>
</Template>
</GridColumn>
<GridColumn Field="@(nameof(NoteInfo.Cost))" Title="Cost" Width="90px" />
<GridColumn Field="@(nameof(NoteInfo.GearCategory))" Title="Category" Width="140px"/>
<GridColumn Field="@(nameof(NoteInfo.Effect))" Title="Effect"/>
<GridColumn Field="@(nameof(NoteInfo.Location))" Title="Acquisition" Width="150px"/>
</GridColumns>
</TelerikGrid>
</div>
}
@code {
private List<NoteInfo>? gearNotes;
protected override async Task OnInitializedAsync()
{
var index = await DocsService.GetIndexAsync();
gearNotes = index.Notes
.Where(n => string.Equals(n.Category, "Gear", StringComparison.OrdinalIgnoreCase))
.OrderBy(n => n.Title)
.ToList();
}
}
+16 -2
View File
@@ -2,8 +2,22 @@
<PageTitle>Earthborne Trailblazer</PageTitle>
<h1>Earthborne Trailblazer</h1>
<div class="hero-section text-center">
<div class="hero-content py-5">
<h1 class="display-4 mb-3">Earthborne Trailblazer</h1>
<p class="lead mb-4">Your essential companion guide for navigating the Valley and mastering your craft.</p>
<div class="hero-actions">
<NavLink href="docs/overview" class="btn btn-primary btn-lg px-4">Begin Journey</NavLink>
</div>
</div>
</div>
<div class="map-container">
<div class="container-fluid px-0 mt-4">
<div class="section-header d-flex align-items-center mb-3">
<h2 class="h4 mb-0">Valley Map</h2>
<div class="ms-3 flex-grow-1 border-bottom opacity-25"></div>
</div>
<div class="map-container shadow-lg">
<object data="docs/map.svg" type="image/svg+xml" class="map-svg"></object>
</div>
</div>
+15 -4
View File
@@ -1,5 +1,16 @@
@page "/not-found"
@layout MainLayout
@page "/404"
@page "/not-found"
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
<PageTitle>404 - Not Found</PageTitle>
<div class="d-flex flex-column align-items-center justify-content-center py-5 text-center">
<div class="mb-4">
<i class="bi bi-exclamation-triangle text-warning" style="font-size: 4rem;"></i>
</div>
<h1 class="display-4 mb-3">404</h1>
<h2 class="mb-4">Path Not Found</h2>
<p class="lead mb-5 text-muted">The trail you are following seems to have vanished into the wilderness.</p>
<NavLink href="" class="btn btn-primary px-4">
Return to Safety
</NavLink>
</div>
+2
View File
@@ -10,4 +10,6 @@ builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<DocsService>();
builder.Services.AddTelerikBlazor();
await builder.Build().RunAsync();
+82 -15
View File
@@ -1,4 +1,7 @@
using System.Net;
using System.Net.Http.Json;
using System.Text;
using System.Text.RegularExpressions;
using Markdig;
using Web.Models;
@@ -6,12 +9,19 @@ namespace Web.Services;
public class DocsService
{
private readonly HttpClient _http;
private NotesIndex? _index;
private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder()
.UseYamlFrontMatter()
.Build();
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".bmp"
};
private readonly HttpClient _http;
private NotesIndex? _index;
private readonly Dictionary<string, string> _markdownCache = new();
public DocsService(HttpClient http)
{
_http = http;
@@ -38,7 +48,7 @@ public class DocsService
try
{
var markdown = await _http.GetStringAsync($"docs/notes/{slug}.md");
return ParseDocument(slug, noteInfo.Title, markdown);
return await ParseDocument(slug, noteInfo.Title, markdown);
}
catch
{
@@ -46,7 +56,7 @@ public class DocsService
}
}
private static NoteDocument ParseDocument(string slug, string title, string markdown)
private async Task<NoteDocument> ParseDocument(string slug, string title, string markdown, int depth = 0)
{
var doc = new NoteDocument
{
@@ -69,6 +79,7 @@ public class DocsService
inFrontmatter = true;
continue;
}
inFrontmatter = false;
frontmatterDone = true;
continue;
@@ -89,13 +100,11 @@ public class DocsService
{
var key = line[..colonIdx].Trim();
if (string.Equals(key, "category", StringComparison.OrdinalIgnoreCase))
{
doc.Category = line[(colonIdx + 1)..].Trim().Trim('"');
}
}
}
var fmHtml = new System.Text.StringBuilder();
var fmHtml = new StringBuilder();
fmHtml.Append("<table class=\"frontmatter\">");
foreach (var line in frontmatterLines)
{
@@ -105,33 +114,91 @@ public class DocsService
var key = line[..colonIdx].Trim();
var value = line[(colonIdx + 1)..].Trim().Trim('"');
fmHtml.Append("<tr><td class=\"fm-key\">");
fmHtml.Append(System.Net.WebUtility.HtmlEncode(key));
fmHtml.Append(WebUtility.HtmlEncode(key));
fmHtml.Append("</td><td class=\"fm-value\">");
var encoded = System.Net.WebUtility.HtmlEncode(value);
var encoded = WebUtility.HtmlEncode(value);
fmHtml.Append(ConvertWikiLinks(encoded));
fmHtml.Append("</td></tr>");
}
else
{
fmHtml.Append("<tr><td colspan=\"2\">");
fmHtml.Append(ConvertWikiLinks(System.Net.WebUtility.HtmlEncode(line.Trim())));
fmHtml.Append(ConvertWikiLinks(WebUtility.HtmlEncode(line.Trim())));
fmHtml.Append("</td></tr>");
}
}
fmHtml.Append("</table>");
doc.FrontmatterHtml = fmHtml.ToString();
}
var body = string.Join("\n", bodyLines);
body = ConvertWikiLinks(body);
doc.HtmlContent = Markdown.ToHtml(body, Pipeline);
var html = Markdown.ToHtml(body, Pipeline);
html = await ResolveEmbedsInHtml(html, depth);
doc.HtmlContent = html;
return doc;
}
private async Task<string> ResolveEmbedsInHtml(string html, int depth)
{
if (depth > 10) return html;
var regex = new Regex(@"(?:<p>)?!\[\[([^\]]+)\]\](?:</p>)?");
var sb = new StringBuilder();
var lastIndex = 0;
foreach (Match match in regex.Matches(html))
{
sb.Append(html, lastIndex, match.Index - lastIndex);
var filename = match.Groups[1].Value.Trim();
var replacement = await ResolveEmbed(filename, depth);
sb.Append(replacement);
lastIndex = match.Index + match.Length;
}
sb.Append(html, lastIndex, html.Length - lastIndex);
return sb.ToString();
}
private async Task<string> ResolveEmbed(string filename, int depth)
{
var ext = Path.GetExtension(filename);
if (ImageExtensions.Contains(ext))
{
return $"<img src=\"/docs/images/{filename}\" alt=\"{WebUtility.HtmlEncode(filename)}\" />";
}
var slug = Slugify(filename);
try
{
var markdown = await GetMarkdownAsync(slug);
var embedded = await ParseDocument(slug, filename, markdown, depth + 1);
return $"<div class=\"embed\">{embedded.HtmlContent}</div>";
}
catch
{
return $"<div class=\"embed-error\">[Embed not found: {WebUtility.HtmlEncode(filename)}]</div>";
}
}
private async Task<string> GetMarkdownAsync(string slug)
{
if (_markdownCache.TryGetValue(slug, out var cached))
return cached;
var markdown = await _http.GetStringAsync($"docs/notes/{slug}.md");
_markdownCache[slug] = markdown;
return markdown;
}
private static string ConvertWikiLinks(string text)
{
return System.Text.RegularExpressions.Regex.Replace(
return Regex.Replace(
text,
@"\[\[([^\]]+)\]\]",
match =>
@@ -141,7 +208,7 @@ public class DocsService
var linkText = parts.Length > 1 ? parts[1].Trim() : parts[0].Trim();
var target = parts[0].Trim();
var slug = Slugify(target);
return $"<a href=\"/docs/{slug}\">{System.Net.WebUtility.HtmlEncode(linkText)}</a>";
return $"<a href=\"/docs/{slug}\">{WebUtility.HtmlEncode(linkText)}</a>";
});
}
@@ -153,8 +220,8 @@ public class DocsService
.Replace(".", "")
.Replace("(", "")
.Replace(")", "");
slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[^a-z0-9\-]", "");
slug = System.Text.RegularExpressions.Regex.Replace(slug, @"-+", "-");
slug = Regex.Replace(slug, @"[^a-z0-9\-]", "");
slug = Regex.Replace(slug, @"-+", "-");
return slug.Trim('-');
}
}
+4
View File
@@ -11,6 +11,10 @@
<PackageReference Include="Markdig" Version="0.40.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all"/>
<PackageReference Include="Telerik.ReportViewer.Blazor" Version="20.1.26.520"/>
<PackageReference Include="Telerik.ReportViewer.BlazorNative" Version="20.1.26.520"/>
<PackageReference Include="Telerik.UI.for.Blazor" Version="14.0.0"/>
<PackageReference Include="Telerik.WebReportDesigner.Blazor" Version="20.1.26.520"/>
</ItemGroup>
<Target Name="SyncDocsNotes" BeforeTargets="BeforeBuild">
+2
View File
@@ -9,4 +9,6 @@
@using Web
@using Web.Layout
@using Web.Models
@using Telerik.Blazor
@using Telerik.Blazor.Components
@using Web.Services
+493 -49
View File
@@ -1,5 +1,66 @@
:root {
--primary: #3a5a40;
--primary-light: #588157;
--primary-dark: #344e41;
--accent: #a3b18a;
--bg-dark: #1b1c17;
--bg-sidebar: #24251f;
--text-main: #dad7cd;
--text-muted: #a3b18a;
--border-color: rgba(218, 215, 205, 0.1);
/* Telerik UI Overrides - Earthborne Ranger Theme */
--kendo-color-app-surface: var(--bg-dark);
--kendo-color-on-app-surface: var(--text-main);
--kendo-color-subtle: var(--text-muted);
--kendo-color-surface: var(--bg-sidebar);
--kendo-color-surface-alt: rgba(255, 255, 255, 0.02);
--kendo-color-border: var(--border-color);
--kendo-color-border-alt: rgba(255, 255, 255, 0.15);
--kendo-color-base: var(--bg-sidebar);
--kendo-color-base-hover: rgba(255, 255, 255, 0.05);
--kendo-color-base-active: rgba(255, 255, 255, 0.1);
--kendo-color-on-base: var(--text-main);
--kendo-color-primary: var(--primary);
--kendo-color-primary-hover: var(--primary-light);
--kendo-color-primary-active: var(--primary-dark);
--kendo-color-on-primary: #ffffff;
--kendo-color-primary-subtle: rgba(58, 90, 64, 0.15);
--kendo-color-primary-on-subtle: var(--primary-light);
--kendo-color-secondary: var(--accent);
--kendo-color-on-secondary: var(--bg-dark);
--kendo-color-success: var(--primary-light);
--kendo-color-warning: #e9c46a;
--kendo-color-error: #e76f51;
--kendo-color-info: #5fa8d3;
--kendo-color-tertiary: #d4a373;
--kendo-color-inverse: #ffffff;
--kendo-color-on-inverse: #000000;
/* Series Colors */
--kendo-color-series: var(--primary);
--kendo-color-series-a: var(--kendo-color-series);
--kendo-color-series-b: var(--accent);
--kendo-color-series-c: var(--primary-light);
--kendo-color-series-d: #d4a373;
--kendo-color-series-e: #ccd5ae;
--kendo-color-series-f: #e9edc9;
}
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--bg-dark);
color: var(--text-main);
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Outfit', sans-serif;
font-weight: 600;
color: #fff;
}
h1:focus {
@@ -7,13 +68,30 @@ h1:focus {
}
a, .btn-link {
color: #0071c1;
color: var(--primary-light);
text-decoration: none;
transition: color 0.2s ease;
}
a:hover, .btn-link:hover {
color: var(--accent);
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
background-color: var(--primary);
border-color: var(--primary-dark);
font-weight: 500;
padding: 0.5rem 1.25rem;
border-radius: 6px;
transition: all 0.2s ease;
}
.btn-primary:hover {
background-color: var(--primary-light);
border-color: var(--primary);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(58, 90, 64, 0.3);
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
@@ -118,95 +196,201 @@ code {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-file-text' viewBox='0 0 16 16'%3E%3Cpath d='M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zM5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1H5z'/%3E%3Cpath d='M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z'/%3E%3C/svg%3E");
}
.hero-section {
background: linear-gradient(135deg, var(--bg-sidebar) 0%, var(--bg-dark) 100%);
border: 1px solid var(--border-color);
border-radius: 16px;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.hero-section::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(58, 90, 64, 0.1) 0%, transparent 70%);
z-index: 0;
}
.hero-content {
position: relative;
z-index: 1;
}
.lead {
color: var(--text-muted);
font-weight: 400;
}
.docs-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.docs-card {
display: block;
padding: 0.75rem 1rem;
border: 1px solid #dee2e6;
border-radius: 8px;
display: flex;
flex-direction: column;
padding: 1.5rem;
background: var(--bg-sidebar);
border: 1px solid var(--border-color);
border-radius: 12px;
text-decoration: none;
color: inherit;
transition: box-shadow 0.15s ease-in-out, border-color 0.15s ease-in-out;
color: var(--text-main);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
height: 100%;
}
.docs-card:hover {
border-color: #1b6ec2;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-color: var(--primary-light);
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
text-decoration: none;
color: #fff;
}
.docs-card h3 {
margin: 0;
font-size: 1rem;
font-weight: 500;
margin: 0.5rem 0 0.75rem 0;
font-size: 1.25rem;
color: #fff;
font-family: 'Outfit', sans-serif;
}
.frontmatter-section {
margin: 1rem 0;
padding: 0.5rem;
border: 1px solid #e9ecef;
border-radius: 6px;
background: #f8f9fa;
margin: 2rem 0;
padding: 1.25rem;
border: 1px solid var(--border-color);
border-radius: 12px;
background: rgba(255, 255, 255, 0.02);
}
.frontmatter-section summary {
cursor: pointer;
font-weight: 600;
color: #495057;
padding: 0.25rem 0;
color: var(--accent);
padding: 0;
font-family: 'Outfit', sans-serif;
}
table.frontmatter {
width: 100%;
border-collapse: collapse;
margin-top: 0.5rem;
border-collapse: separate;
border-spacing: 0;
margin-top: 1rem;
font-size: 0.9rem;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border-color);
}
table.frontmatter td {
padding: 0.25rem 0.5rem;
border: 1px solid #dee2e6;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color);
vertical-align: top;
background: transparent;
color: var(--text-main);
}
table.frontmatter tr:last-child td {
border-bottom: none;
}
table.frontmatter td.fm-key {
font-weight: 600;
color: #495057;
color: var(--accent);
white-space: nowrap;
width: 1%;
background: #e9ecef;
width: 30%;
background: rgba(255, 255, 255, 0.03);
}
.markdown-body {
line-height: 1.7;
margin-top: 1rem;
line-height: 1.8;
margin-top: 2rem;
color: #d1d1d1;
}
.markdown-body h1 { font-size: 1.75rem; margin: 1.5rem 0 0.75rem; }
.markdown-body h2 { font-size: 1.4rem; margin: 1.25rem 0 0.5rem; }
.markdown-body h3 { font-size: 1.15rem; margin: 1rem 0 0.5rem; }
.markdown-body p { margin: 0.5rem 0; }
.markdown-body ul, .markdown-body ol { margin: 0.5rem 0; padding-left: 1.5rem; }
.markdown-body table { border-collapse: collapse; margin: 0.75rem 0; }
.markdown-body th, .markdown-body td { border: 1px solid #dee2e6; padding: 0.4rem 0.6rem; }
.markdown-body th { background: #f8f9fa; }
.markdown-body code { background: #f0f0f0; padding: 0.15rem 0.3rem; border-radius: 3px; font-size: 0.9em; }
.markdown-body pre code { background: none; padding: 0; }
.markdown-body blockquote { border-left: 3px solid #dee2e6; padding-left: 1rem; color: #6c757d; margin: 0.75rem 0; }
.markdown-body h1 {
font-size: 2.25rem;
margin: 2rem 0 1rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.5rem;
}
.markdown-body h2 {
font-size: 1.75rem;
margin: 2rem 0 1rem;
color: var(--accent);
}
.markdown-body h3 {
font-size: 1.4rem;
margin: 1.5rem 0 0.75rem;
color: var(--primary-light);
}
.markdown-body p {
margin: 1rem 0;
}
.markdown-body table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 1.5rem 0;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border-color);
}
.markdown-body th, .markdown-body td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color);
}
.markdown-body th {
background: rgba(255, 255, 255, 0.05);
color: var(--accent);
font-weight: 600;
text-align: left;
}
.markdown-body tr:last-child td {
border-bottom: none;
}
.markdown-body code {
background: rgba(255, 255, 255, 0.1);
color: var(--accent);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-size: 0.9em;
}
.markdown-body pre code {
background: none;
padding: 0;
}
.markdown-body blockquote {
border-left: 3px solid #dee2e6;
padding-left: 1rem;
color: #6c757d;
margin: 0.75rem 0;
}
.map-container {
max-width: 100%;
margin: 1.5rem 0;
border: 1px solid #dee2e6;
border-radius: 8px;
margin: 1rem 0;
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
background: #1a1a2e;
background: #0f100d;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.map-svg {
@@ -229,3 +413,263 @@ table.frontmatter td.fm-key {
color: #1b6ec2;
border: 1px solid #b8daff;
}
.embed {
margin: 1rem 0;
padding: 0.75rem 1rem;
border-left: 3px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.03);
border-radius: 0 6px 6px 0;
}
.embed-error {
color: #e06c75;
font-style: italic;
padding: 0.5rem 0;
}
/* Document Content */
.doc-content {
max-width: 900px;
margin: 0 auto;
background: var(--bg-sidebar);
padding: 2.5rem;
border-radius: 16px;
border: 1px solid var(--border-color);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
/* Gear Styles */
.gear-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.gear-card {
background: var(--bg-sidebar);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
transition: all 0.2s ease;
}
.gear-card:hover {
border-color: var(--primary-light);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
.gear-header {
background: rgba(255, 255, 255, 0.03);
padding: 1.25rem;
border-bottom: 1px solid var(--border-color);
}
.gear-body {
padding: 1.25rem;
}
.gear-stat {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.gear-stat-label {
color: var(--text-muted);
}
.gear-stat-value {
color: var(--text-main);
font-weight: 500;
}
/* Telerik Grid Overrides */
.k-grid {
background-color: var(--kendo-color-surface) !important;
border: 1px solid var(--kendo-color-border) !important;
color: var(--kendo-color-on-surface) !important;
border-radius: 12px !important;
overflow: hidden !important;
}
.k-grid-header,
.k-grid-header-wrap,
.k-header,
.k-table-header,
.k-table-thead,
.k-grid-header-table,
.k-table-header-wrap,
.k-table-thead > tr,
.k-column-title,
.k-filter-row {
background-color: var(--kendo-color-app-surface) !important;
background-image: none !important;
border-color: var(--kendo-color-border) !important;
}
.k-grid-header .k-header,
.k-table-th {
background: transparent !important;
color: var(--accent) !important;
border-color: var(--kendo-color-border) !important;
font-family: 'Outfit', sans-serif !important;
font-weight: 600 !important;
text-transform: uppercase;
font-size: 0.85rem;
padding: 1rem !important;
letter-spacing: 0.025em;
}
.k-grid td,
.k-table-td {
padding: 1rem !important;
border-color: var(--kendo-color-border) !important;
background: transparent !important;
vertical-align: middle !important;
}
.k-grid tr,
.k-table-row {
background-color: transparent !important;
}
.k-grid tr.k-alt,
.k-table-row.k-alt,
.k-table-row.k-table-alt-row {
background-color: rgba(255, 255, 255, 0.01) !important;
}
.k-grid tr:hover,
.k-table-row:hover {
background-color: rgba(255, 255, 255, 0.03) !important;
}
.k-grid-content,
.k-grid-table,
.k-table {
background-color: transparent !important;
}
.k-grid-header-wrap {
border-color: var(--border-color) !important;
}
/* Pager Styles */
.k-pager-wrap,
.k-pager,
.k-grid-pager,
.k-pager-td,
.k-pager-numbers-wrap {
background-color: var(--kendo-color-app-surface) !important;
background-image: none !important;
border-top: 1px solid var(--kendo-color-border) !important;
color: var(--text-muted) !important;
padding: 0.75rem !important;
}
.k-pager-numbers .k-link {
background-color: transparent !important;
color: var(--text-muted) !important;
border-radius: 6px !important;
transition: all 0.2s !important;
margin: 0 2px !important;
}
.k-pager-numbers .k-link.k-selected,
.k-pager-numbers .k-link.k-state-selected {
background-color: var(--kendo-color-primary) !important;
color: var(--kendo-color-on-primary) !important;
border-color: var(--primary-light) !important;
}
.k-pager-numbers .k-link:hover {
background-color: var(--kendo-color-primary-hover) !important;
color: var(--kendo-color-on-primary) !important;
}
.k-pager-nav.k-link {
background-color: transparent !important;
color: var(--kendo-color-on-app-surface) !important;
}
/* Filter Row Styles */
.k-filter-row td,
.k-filter-row th {
background-color: rgba(0, 0, 0, 0.2) !important;
padding: 0.5rem 1rem !important;
border-color: var(--kendo-color-border) !important;
}
.k-filtercell .k-textbox,
.k-filtercell .k-input,
.k-filtercell .k-input-inner {
background-color: var(--kendo-color-app-surface) !important;
color: var(--kendo-color-on-app-surface) !important;
border: 1px solid var(--kendo-color-border) !important;
border-radius: 4px !important;
}
.k-filtercell .k-input-inner {
padding: 4px 8px !important;
}
.k-filtercell .k-button {
background-color: var(--kendo-color-primary-active) !important;
border-color: var(--kendo-color-border) !important;
color: var(--kendo-color-on-primary) !important;
}
.k-filtercell .k-button:hover {
background-color: var(--kendo-color-primary) !important;
}
/* Custom scrollbar for grid content */
.k-grid-content::-webkit-scrollbar {
width: 10px;
}
.k-grid-content::-webkit-scrollbar-track {
background: var(--kendo-color-app-surface);
}
.k-grid-content::-webkit-scrollbar-thumb {
background: var(--kendo-color-primary-active);
border-radius: 5px;
border: 2px solid var(--kendo-color-app-surface);
}
.k-grid-content::-webkit-scrollbar-thumb:hover {
background: var(--kendo-color-primary-hover);
}
/* Typography in Grid */
.k-grid td .fw-bold {
color: var(--primary-light);
}
.k-grid td .fw-bold:hover {
color: var(--accent);
text-decoration: underline;
}
/* Loading and Empty State */
.k-loading-mask {
background-color: rgba(0, 0, 0, 0.5) !important;
}
.k-loading-image {
background-image: none !important;
}
.k-loading-color {
background-color: var(--primary) !important;
}
.k-grid-norecords {
color: var(--text-muted) !important;
padding: 3rem !important;
text-align: center !important;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

+52 -10
View File
@@ -12,7 +12,11 @@
{
"slug": "cache-map",
"title": "Cache Map",
"category": "Gear"
"category": "Gear",
"cost": "1",
"gearCategory": "Tool",
"effect": "Travel: Discard this gear and spend 2 [[Progress]] while in this region to gain 1 [[Tool]].",
"location": "Anywhere"
},
{
"slug": "card-library",
@@ -46,7 +50,11 @@
{
"slug": "dolewood-canoe",
"title": "Dolewood Canoe",
"category": "Gear"
"category": "Gear",
"cost": "4",
"gearCategory": "Tool",
"effect": "Boat. Travel: You need 1 less Progress to place trail markers in water regions.",
"location": "[[White Sky]]"
},
{
"slug": "e03",
@@ -85,7 +93,11 @@
{
"slug": "ferinodex",
"title": "Ferinodex",
"category": "Gear"
"category": "Gear",
"cost": "2",
"gearCategory": "Tool",
"effect": "Explore: Once per day, use in the place of 1[[Focus]].",
"location": "Anywhere"
},
{
"slug": "flora-meeples",
@@ -123,7 +135,11 @@
{
"slug": "gauzeblade",
"title": "Gauzeblade",
"category": "Gear"
"category": "Gear",
"cost": "5",
"gearCategory": "Weapon",
"effect": "Explore: Spend 2 [[Fitness]] before encountering a predator or prey to store it. The stored being can be traded as a gear of value 2.",
"location": "[[Lone Tree Station]]"
},
{
"slug": "gear-cards",
@@ -166,7 +182,11 @@
{
"slug": "hidden-trail-map",
"title": "Hidden Trail Map",
"category": "Gear"
"category": "Gear",
"cost": "3",
"gearCategory": "Tool",
"effect": "Prepare: Discard to travel to a nearby region.",
"location": "Anywhere"
},
{
"slug": "injury-cards",
@@ -216,7 +236,11 @@
{
"slug": "paratrepsis-whistle",
"title": "Paratrepsis Whistle",
"category": "Gear"
"category": "Gear",
"cost": "6",
"gearCategory": "Tool",
"effect": "Explore: You can spend 1 [[Focus]] to prevent suffering 1 injury.",
"location": "[[Lone Tree Station]]"
},
{
"slug": "paved-roads",
@@ -225,12 +249,17 @@
{
"slug": "perfect-day-1",
"title": "Perfect Day 1",
"category": "Event"
"category": "Event",
"effect": "In each region with 3 or more prey, they stampede! Each Ranger in that region suffers 1 injury, then distribute the prey nearby. Shuffle and add 5 random cards from E to the top of the event deck."
},
{
"slug": "phonoscopic-headset",
"title": "Phonoscopic Headset",
"category": "Gear"
"category": "Gear",
"cost": "2",
"gearCategory": "Garment",
"effect": "Explore: Once per day, use in the place of 1 [[Awareness]]",
"location": ""
},
{
"slug": "player-boards",
@@ -287,7 +316,11 @@
{
"slug": "ruins-map",
"title": "Ruins Map",
"category": "Gear"
"category": "Gear",
"cost": "2",
"gearCategory": "Tool",
"effect": "Travel: Retire this gear an spend 2[[Progress]] while in this region to gain 1 [[Artifact]].",
"location": "Anywhere"
},
{
"slug": "scout",
@@ -333,7 +366,11 @@
{
"slug": "totem-of-the-irix",
"title": "Totem of the Irix",
"category": "Gear"
"category": "Gear",
"cost": "2",
"gearCategory": "Consumable",
"effect": "Explore: Discard to help a Ranger anywhere.",
"location": "Anywhere"
},
{
"slug": "trade",
@@ -417,6 +454,11 @@
{
"slug": "xp",
"title": "XP"
},
{
"slug": "overview",
"title": "Overview",
"category": "Overview"
}
]
}
+1 -1
View File
@@ -3,5 +3,5 @@ category: Gear
Cost: 4
Gear Category: Tool
Effect: "Boat. Travel: You need 1 less Progress to place trail markers in water regions."
Location: White Sky
Location: "[[White Sky]]"
---
+13 -8
View File
@@ -3,13 +3,17 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Web</title>
<base href="/"/>
<link rel="preload" id="webassembly" />
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link id="webassembly" rel="preload"/>
<link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"/>
<link href="favicon.png" rel="icon" type="image/png"/>
<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@400;500;600&family=Outfit:wght@500;700&display=swap" rel="stylesheet">
<link href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" rel="stylesheet"/>
<link href="css/app.css" rel="stylesheet"/>
<link href="Web.styles.css" rel="stylesheet"/>
<script type="importmap"></script>
</head>
@@ -17,17 +21,18 @@
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
<circle cx="50%" cy="50%" r="40%"/>
<circle cx="50%" cy="50%" r="40%"/>
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<a class="reload" href=".">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script defer src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js"></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -224,6 +224,7 @@ h6, h5, h4, h3, h2, h1 {
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
@@ -233,6 +234,7 @@ h1 {
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
@@ -242,6 +244,7 @@ h2 {
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
@@ -251,6 +254,7 @@ h3 {
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
@@ -351,6 +355,7 @@ a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
@@ -375,6 +380,7 @@ pre {
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
@@ -386,6 +392,7 @@ code {
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
@@ -397,6 +404,7 @@ kbd {
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
@@ -474,6 +482,7 @@ select {
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
@@ -488,6 +497,7 @@ button,
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
@@ -519,11 +529,13 @@ legend {
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
@@ -224,6 +224,7 @@ h6, h5, h4, h3, h2, h1 {
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
@@ -233,6 +234,7 @@ h1 {
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
@@ -242,6 +244,7 @@ h2 {
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
@@ -251,6 +254,7 @@ h3 {
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
@@ -351,6 +355,7 @@ a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
@@ -375,6 +380,7 @@ pre {
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
@@ -386,6 +392,7 @@ code {
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
@@ -397,6 +404,7 @@ kbd {
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
@@ -474,6 +482,7 @@ select {
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
@@ -488,6 +497,7 @@ button,
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
@@ -519,11 +529,13 @@ legend {
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
@@ -553,6 +565,7 @@ legend + * {
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
@@ -591,4 +604,5 @@ progress {
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12 -10
View File
@@ -99,15 +99,17 @@
}
},
{
"id": "eb09ddd2e6309f90",
"id": "69eee9d5e6de1fd6",
"type": "leaf",
"state": {
"type": "image",
"type": "markdown",
"state": {
"file": "Images/Map.png"
"file": "Overview.md",
"mode": "source",
"source": false
},
"icon": "lucide-image",
"title": "Map"
"icon": "lucide-file",
"title": "Overview"
}
}
],
@@ -275,20 +277,22 @@
},
"active": "e8ba8e9287dfab25",
"lastOpenFiles": [
"Tasks.base",
"Overview.md",
"Bases/_Gear.base",
"Bases/Regions.base",
"Images/Map.png",
"Notes/White Sky.md",
"Tasks/Generate a csharp script to do what you did.md",
"Tasks/Generate a Markdown Website.md",
"_Tasks.base",
"Tasks/Generate a map of regions and how they connect.md",
"Bases/_Roles.base",
"_Overview.md",
"Untitled 2.md",
"Tasks",
"Notes/Artificer.md",
"Bases/Terrain.base",
"Notes/Contents.md",
"Notes/Dolewood Canoe.md",
"Bases/_Gear.base",
"Notes/Concoliator.md",
"Notes/Explorer.md",
"Notes/Guide.md",
@@ -307,13 +311,11 @@
"Notes/Scout.md",
"Images/Pasted image 20260609211711.png",
"Images/Market Deck.png",
"Images/Map.png",
"Images/Event Card Example.png",
"Images/Event Card Example 2.png",
"Images/Pasted image 20260609163414.png",
"Images",
"Notes/Research Station.md",
"Notes/White Sky.md",
"Notes/Wasteland.md",
"Notes/Wasteland 1.md",
"Notes/Mountain 5.md",
+1 -1
View File
@@ -3,5 +3,5 @@ category: Gear
Cost: 4
Gear Category: Tool
Effect: "Boat. Travel: You need 1 less Progress to place trail markers in water regions."
Location: White Sky
Location: "[[White Sky]]"
---
+26
View File
@@ -0,0 +1,26 @@
![[Contents]]
![[Map.png]]
![[Terrain Cards]]
![[Victory Condition]]
![[Event Cards]]
![[Market]]
![[Prepare Phase]]
![[Explore Phase]]
![[Travel Phase]]
![[Role Cards]]