From 3e80ce78a00da99f56429c0deca21c19561af197 Mon Sep 17 00:00:00 2001 From: 6d486f49 Date: Wed, 10 Jun 2026 21:31:37 -0400 Subject: [PATCH] ... --- ET/Web/Layout/NavMenu.razor | 5 + ET/Web/Models/NoteInfo.cs | 20 + ET/Web/Pages/DocViewer.razor | 44 + ET/Web/Pages/Docs.razor | 32 + ET/Web/Program.cs | 2 + ET/Web/Services/DocsService.cs | 146 + ET/Web/SyncDocs.ps1 | 24 + ET/Web/Web.csproj | 5 + ET/Web/_Imports.razor | 4 +- ET/Web/wwwroot/css/app.css | 88 +- ET/Web/wwwroot/docs/notes-index.json | 380 ++ ET/Web/wwwroot/docs/notes/artificer.md | 6 + ET/Web/wwwroot/docs/notes/awareness.md | 1 + ET/Web/wwwroot/docs/notes/cache-map.md | 7 + ET/Web/wwwroot/docs/notes/card-library.md | 3 + ET/Web/wwwroot/docs/notes/concoliator.md | 6 + ET/Web/wwwroot/docs/notes/contents.md | 20 + ET/Web/wwwroot/docs/notes/crisis-markers.md | 0 .../wwwroot/docs/notes/crisis-resolution.md | 1 + ET/Web/wwwroot/docs/notes/crisis.md | 2 + ET/Web/wwwroot/docs/notes/dirt-roads.md | 1 + ET/Web/wwwroot/docs/notes/dolewood-canoe.md | 7 + ET/Web/wwwroot/docs/notes/e03.md | 10 + ET/Web/wwwroot/docs/notes/ecology.md | 0 ET/Web/wwwroot/docs/notes/endeavor-tokens.md | 0 ET/Web/wwwroot/docs/notes/energy-tokens.md | 0 ET/Web/wwwroot/docs/notes/event-cards.md | 7 + ET/Web/wwwroot/docs/notes/events.md | 3 + ET/Web/wwwroot/docs/notes/explore-phase.md | 3 + ET/Web/wwwroot/docs/notes/explorer.md | 10 + ET/Web/wwwroot/docs/notes/ferinodex.md | 7 + ET/Web/wwwroot/docs/notes/flora-meeples.md | 0 ET/Web/wwwroot/docs/notes/forest-1.md | 10 + ET/Web/wwwroot/docs/notes/forest-2.md | 9 + ET/Web/wwwroot/docs/notes/forest-3.md | 7 + ET/Web/wwwroot/docs/notes/forest-4.md | 9 + ET/Web/wwwroot/docs/notes/forest-5.md | 7 + ET/Web/wwwroot/docs/notes/forest.md | 0 ET/Web/wwwroot/docs/notes/gauzeblade.md | 7 + ET/Web/wwwroot/docs/notes/gear-cards.md | 0 ET/Web/wwwroot/docs/notes/grass-1.md | 11 + ET/Web/wwwroot/docs/notes/grass-2.md | 10 + ET/Web/wwwroot/docs/notes/grass-3.md | 9 + ET/Web/wwwroot/docs/notes/grass-4.md | 9 + ET/Web/wwwroot/docs/notes/grass-5.md | 10 + ET/Web/wwwroot/docs/notes/grasslands.md | 2 + ET/Web/wwwroot/docs/notes/guide.md | 10 + ET/Web/wwwroot/docs/notes/hidden-trail-map.md | 7 + ET/Web/wwwroot/docs/notes/injury-cards.md | 0 ET/Web/wwwroot/docs/notes/lake.md | 0 ET/Web/wwwroot/docs/notes/losing-the-game.md | 1 + ET/Web/wwwroot/docs/notes/market.md | 2 + ET/Web/wwwroot/docs/notes/mountain-1.md | 9 + ET/Web/wwwroot/docs/notes/mountain-2.md | 8 + ET/Web/wwwroot/docs/notes/mountain-3.md | 9 + ET/Web/wwwroot/docs/notes/mountain-4.md | 8 + ET/Web/wwwroot/docs/notes/mountain-5.md | 8 + ET/Web/wwwroot/docs/notes/mountain.md | 0 .../wwwroot/docs/notes/paratrepsis-whistle.md | 7 + ET/Web/wwwroot/docs/notes/paved-roads.md | 1 + ET/Web/wwwroot/docs/notes/perfect-day-1.md | 14 + .../wwwroot/docs/notes/phonoscopic-headset.md | 7 + ET/Web/wwwroot/docs/notes/player-boards.md | 1 + ET/Web/wwwroot/docs/notes/player-meeples.md | 0 ET/Web/wwwroot/docs/notes/predator-meeples.md | 0 ET/Web/wwwroot/docs/notes/prepare-phase.md | 9 + ET/Web/wwwroot/docs/notes/press-on.md | 1 + ET/Web/wwwroot/docs/notes/prey-meeples.md | 0 ET/Web/wwwroot/docs/notes/progress.md | 1 + ET/Web/wwwroot/docs/notes/ranger-badges.md | 0 ET/Web/wwwroot/docs/notes/ranger-meeples.md | 3 + ET/Web/wwwroot/docs/notes/research-station.md | 0 ET/Web/wwwroot/docs/notes/rest.md | 7 + ET/Web/wwwroot/docs/notes/role-cards.md | 15 + ET/Web/wwwroot/docs/notes/round.md | 0 ET/Web/wwwroot/docs/notes/ruins-map.md | 7 + ET/Web/wwwroot/docs/notes/scout.md | 8 + ET/Web/wwwroot/docs/notes/shaper.md | 4 + ET/Web/wwwroot/docs/notes/shepard.md | 10 + ET/Web/wwwroot/docs/notes/story.md | 0 ET/Web/wwwroot/docs/notes/supply.md | 1 + ET/Web/wwwroot/docs/notes/terrain-2.md | 9 + ET/Web/wwwroot/docs/notes/terrain-3.md | 11 + ET/Web/wwwroot/docs/notes/terrain-cards.md | 14 + ET/Web/wwwroot/docs/notes/terrain.md | 12 + .../wwwroot/docs/notes/totem-of-the-irix.md | 7 + ET/Web/wwwroot/docs/notes/trade.md | 7 + ET/Web/wwwroot/docs/notes/trader.md | 10 + ET/Web/wwwroot/docs/notes/trail-markers.md | 3 + ET/Web/wwwroot/docs/notes/travel-phase.md | 9 + ET/Web/wwwroot/docs/notes/traverse.md | 5 + .../wwwroot/docs/notes/unmaintained-roads.md | 1 + ET/Web/wwwroot/docs/notes/valley.md | 0 .../wwwroot/docs/notes/victory-condition.md | 3 + ET/Web/wwwroot/docs/notes/wasteland-1.md | 10 + ET/Web/wwwroot/docs/notes/wasteland.md | 0 ET/Web/wwwroot/docs/notes/water-1.md | 9 + ET/Web/wwwroot/docs/notes/water-2.md | 13 + ET/Web/wwwroot/docs/notes/water-3.md | 9 + ET/Web/wwwroot/docs/notes/water-4.md | 8 + ET/Web/wwwroot/docs/notes/water-5.md | 8 + ET/Web/wwwroot/docs/notes/weather.md | 0 ET/Web/wwwroot/docs/notes/white-sky.md | 0 ET/Web/wwwroot/docs/notes/xp-cubes.md | 0 ET/Web/wwwroot/docs/notes/xp.md | 3 + docs/.obsidian/community-plugins.json | 3 + .../plugins/kanban-bases-view/main.js | 4140 +++++++++++++++++ .../plugins/kanban-bases-view/manifest.json | 10 + .../plugins/kanban-bases-view/styles.css | 617 +++ docs/.obsidian/workspace.json | 13 +- docs/Notes/Artificer.md | 6 + docs/Notes/Concoliator.md | 6 + docs/Notes/Explorer.md | 10 + docs/Notes/Guide.md | 10 + docs/Notes/Shaper.md | 4 + docs/Notes/Shepard.md | 10 + docs/Notes/Trader.md | 10 + docs/Tasks/Generate a Markdown Website.md | 9 + docs/_Tasks.base | 141 + 119 files changed, 6234 insertions(+), 8 deletions(-) create mode 100644 ET/Web/Models/NoteInfo.cs create mode 100644 ET/Web/Pages/DocViewer.razor create mode 100644 ET/Web/Pages/Docs.razor create mode 100644 ET/Web/Services/DocsService.cs create mode 100644 ET/Web/SyncDocs.ps1 create mode 100644 ET/Web/wwwroot/docs/notes-index.json create mode 100644 ET/Web/wwwroot/docs/notes/artificer.md create mode 100644 ET/Web/wwwroot/docs/notes/awareness.md create mode 100644 ET/Web/wwwroot/docs/notes/cache-map.md create mode 100644 ET/Web/wwwroot/docs/notes/card-library.md create mode 100644 ET/Web/wwwroot/docs/notes/concoliator.md create mode 100644 ET/Web/wwwroot/docs/notes/contents.md create mode 100644 ET/Web/wwwroot/docs/notes/crisis-markers.md create mode 100644 ET/Web/wwwroot/docs/notes/crisis-resolution.md create mode 100644 ET/Web/wwwroot/docs/notes/crisis.md create mode 100644 ET/Web/wwwroot/docs/notes/dirt-roads.md create mode 100644 ET/Web/wwwroot/docs/notes/dolewood-canoe.md create mode 100644 ET/Web/wwwroot/docs/notes/e03.md create mode 100644 ET/Web/wwwroot/docs/notes/ecology.md create mode 100644 ET/Web/wwwroot/docs/notes/endeavor-tokens.md create mode 100644 ET/Web/wwwroot/docs/notes/energy-tokens.md create mode 100644 ET/Web/wwwroot/docs/notes/event-cards.md create mode 100644 ET/Web/wwwroot/docs/notes/events.md create mode 100644 ET/Web/wwwroot/docs/notes/explore-phase.md create mode 100644 ET/Web/wwwroot/docs/notes/explorer.md create mode 100644 ET/Web/wwwroot/docs/notes/ferinodex.md create mode 100644 ET/Web/wwwroot/docs/notes/flora-meeples.md create mode 100644 ET/Web/wwwroot/docs/notes/forest-1.md create mode 100644 ET/Web/wwwroot/docs/notes/forest-2.md create mode 100644 ET/Web/wwwroot/docs/notes/forest-3.md create mode 100644 ET/Web/wwwroot/docs/notes/forest-4.md create mode 100644 ET/Web/wwwroot/docs/notes/forest-5.md create mode 100644 ET/Web/wwwroot/docs/notes/forest.md create mode 100644 ET/Web/wwwroot/docs/notes/gauzeblade.md create mode 100644 ET/Web/wwwroot/docs/notes/gear-cards.md create mode 100644 ET/Web/wwwroot/docs/notes/grass-1.md create mode 100644 ET/Web/wwwroot/docs/notes/grass-2.md create mode 100644 ET/Web/wwwroot/docs/notes/grass-3.md create mode 100644 ET/Web/wwwroot/docs/notes/grass-4.md create mode 100644 ET/Web/wwwroot/docs/notes/grass-5.md create mode 100644 ET/Web/wwwroot/docs/notes/grasslands.md create mode 100644 ET/Web/wwwroot/docs/notes/guide.md create mode 100644 ET/Web/wwwroot/docs/notes/hidden-trail-map.md create mode 100644 ET/Web/wwwroot/docs/notes/injury-cards.md create mode 100644 ET/Web/wwwroot/docs/notes/lake.md create mode 100644 ET/Web/wwwroot/docs/notes/losing-the-game.md create mode 100644 ET/Web/wwwroot/docs/notes/market.md create mode 100644 ET/Web/wwwroot/docs/notes/mountain-1.md create mode 100644 ET/Web/wwwroot/docs/notes/mountain-2.md create mode 100644 ET/Web/wwwroot/docs/notes/mountain-3.md create mode 100644 ET/Web/wwwroot/docs/notes/mountain-4.md create mode 100644 ET/Web/wwwroot/docs/notes/mountain-5.md create mode 100644 ET/Web/wwwroot/docs/notes/mountain.md create mode 100644 ET/Web/wwwroot/docs/notes/paratrepsis-whistle.md create mode 100644 ET/Web/wwwroot/docs/notes/paved-roads.md create mode 100644 ET/Web/wwwroot/docs/notes/perfect-day-1.md create mode 100644 ET/Web/wwwroot/docs/notes/phonoscopic-headset.md create mode 100644 ET/Web/wwwroot/docs/notes/player-boards.md create mode 100644 ET/Web/wwwroot/docs/notes/player-meeples.md create mode 100644 ET/Web/wwwroot/docs/notes/predator-meeples.md create mode 100644 ET/Web/wwwroot/docs/notes/prepare-phase.md create mode 100644 ET/Web/wwwroot/docs/notes/press-on.md create mode 100644 ET/Web/wwwroot/docs/notes/prey-meeples.md create mode 100644 ET/Web/wwwroot/docs/notes/progress.md create mode 100644 ET/Web/wwwroot/docs/notes/ranger-badges.md create mode 100644 ET/Web/wwwroot/docs/notes/ranger-meeples.md create mode 100644 ET/Web/wwwroot/docs/notes/research-station.md create mode 100644 ET/Web/wwwroot/docs/notes/rest.md create mode 100644 ET/Web/wwwroot/docs/notes/role-cards.md create mode 100644 ET/Web/wwwroot/docs/notes/round.md create mode 100644 ET/Web/wwwroot/docs/notes/ruins-map.md create mode 100644 ET/Web/wwwroot/docs/notes/scout.md create mode 100644 ET/Web/wwwroot/docs/notes/shaper.md create mode 100644 ET/Web/wwwroot/docs/notes/shepard.md create mode 100644 ET/Web/wwwroot/docs/notes/story.md create mode 100644 ET/Web/wwwroot/docs/notes/supply.md create mode 100644 ET/Web/wwwroot/docs/notes/terrain-2.md create mode 100644 ET/Web/wwwroot/docs/notes/terrain-3.md create mode 100644 ET/Web/wwwroot/docs/notes/terrain-cards.md create mode 100644 ET/Web/wwwroot/docs/notes/terrain.md create mode 100644 ET/Web/wwwroot/docs/notes/totem-of-the-irix.md create mode 100644 ET/Web/wwwroot/docs/notes/trade.md create mode 100644 ET/Web/wwwroot/docs/notes/trader.md create mode 100644 ET/Web/wwwroot/docs/notes/trail-markers.md create mode 100644 ET/Web/wwwroot/docs/notes/travel-phase.md create mode 100644 ET/Web/wwwroot/docs/notes/traverse.md create mode 100644 ET/Web/wwwroot/docs/notes/unmaintained-roads.md create mode 100644 ET/Web/wwwroot/docs/notes/valley.md create mode 100644 ET/Web/wwwroot/docs/notes/victory-condition.md create mode 100644 ET/Web/wwwroot/docs/notes/wasteland-1.md create mode 100644 ET/Web/wwwroot/docs/notes/wasteland.md create mode 100644 ET/Web/wwwroot/docs/notes/water-1.md create mode 100644 ET/Web/wwwroot/docs/notes/water-2.md create mode 100644 ET/Web/wwwroot/docs/notes/water-3.md create mode 100644 ET/Web/wwwroot/docs/notes/water-4.md create mode 100644 ET/Web/wwwroot/docs/notes/water-5.md create mode 100644 ET/Web/wwwroot/docs/notes/weather.md create mode 100644 ET/Web/wwwroot/docs/notes/white-sky.md create mode 100644 ET/Web/wwwroot/docs/notes/xp-cubes.md create mode 100644 ET/Web/wwwroot/docs/notes/xp.md create mode 100644 docs/.obsidian/community-plugins.json create mode 100644 docs/.obsidian/plugins/kanban-bases-view/main.js create mode 100644 docs/.obsidian/plugins/kanban-bases-view/manifest.json create mode 100644 docs/.obsidian/plugins/kanban-bases-view/styles.css create mode 100644 docs/Notes/Artificer.md create mode 100644 docs/Notes/Concoliator.md create mode 100644 docs/Notes/Explorer.md create mode 100644 docs/Notes/Guide.md create mode 100644 docs/Notes/Shaper.md create mode 100644 docs/Notes/Shepard.md create mode 100644 docs/Notes/Trader.md create mode 100644 docs/Tasks/Generate a Markdown Website.md create mode 100644 docs/_Tasks.base diff --git a/ET/Web/Layout/NavMenu.razor b/ET/Web/Layout/NavMenu.razor index 49aabc0..290f263 100644 --- a/ET/Web/Layout/NavMenu.razor +++ b/ET/Web/Layout/NavMenu.razor @@ -24,6 +24,11 @@ Weather + diff --git a/ET/Web/Models/NoteInfo.cs b/ET/Web/Models/NoteInfo.cs new file mode 100644 index 0000000..5bddc7f --- /dev/null +++ b/ET/Web/Models/NoteInfo.cs @@ -0,0 +1,20 @@ +namespace Web.Models; + +public class NoteInfo +{ + public string Slug { get; set; } = ""; + public string Title { get; set; } = ""; +} + +public class NotesIndex +{ + public List Notes { get; set; } = new(); +} + +public class NoteDocument +{ + public string Slug { get; set; } = ""; + public string Title { get; set; } = ""; + public string FrontmatterHtml { get; set; } = ""; + public string HtmlContent { get; set; } = ""; +} diff --git a/ET/Web/Pages/DocViewer.razor b/ET/Web/Pages/DocViewer.razor new file mode 100644 index 0000000..1c2f364 --- /dev/null +++ b/ET/Web/Pages/DocViewer.razor @@ -0,0 +1,44 @@ +@page "/docs/{Slug}" +@inject Web.Services.DocsService DocsService + +@(doc?.Title ?? "Not Found") + +@if (loading) +{ +

Loading...

+} +else if (doc == null) +{ +

Not Found

+

The document "@Slug" could not be found.

+ ← Back to Docs +} +else +{ +

@doc.Title

+ + @if (!string.IsNullOrEmpty(doc.FrontmatterHtml)) + { +
+ Frontmatter + @((MarkupString)doc.FrontmatterHtml) +
+ } + +
+ @((MarkupString)doc.HtmlContent) +
+} + +@code { + [Parameter] public string Slug { get; set; } = ""; + + private Web.Models.NoteDocument? doc; + private bool loading = true; + + protected override async Task OnInitializedAsync() + { + doc = await DocsService.GetNoteAsync(Slug); + loading = false; + } +} diff --git a/ET/Web/Pages/Docs.razor b/ET/Web/Pages/Docs.razor new file mode 100644 index 0000000..59dda5c --- /dev/null +++ b/ET/Web/Pages/Docs.razor @@ -0,0 +1,32 @@ +@page "/docs" +@inject Web.Services.DocsService DocsService + +Docs + +

Documentation

+ +@if (notes == null) +{ +

Loading...

+} +else +{ +
+ @foreach (var note in notes.OrderBy(n => n.Title)) + { + +

@note.Title

+
+ } +
+} + +@code { + private List? notes; + + protected override async Task OnInitializedAsync() + { + var index = await DocsService.GetIndexAsync(); + notes = index.Notes; + } +} diff --git a/ET/Web/Program.cs b/ET/Web/Program.cs index 66a967b..364c166 100644 --- a/ET/Web/Program.cs +++ b/ET/Web/Program.cs @@ -1,11 +1,13 @@ using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Web; +using Web.Services; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +builder.Services.AddScoped(); await builder.Build().RunAsync(); \ No newline at end of file diff --git a/ET/Web/Services/DocsService.cs b/ET/Web/Services/DocsService.cs new file mode 100644 index 0000000..5bd1a4d --- /dev/null +++ b/ET/Web/Services/DocsService.cs @@ -0,0 +1,146 @@ +using System.Net.Http.Json; +using Markdig; +using Web.Models; + +namespace Web.Services; + +public class DocsService +{ + private readonly HttpClient _http; + private NotesIndex? _index; + private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder() + .UseYamlFrontMatter() + .Build(); + + public DocsService(HttpClient http) + { + _http = http; + } + + public async Task GetIndexAsync() + { + if (_index != null) + return _index; + + _index = await _http.GetFromJsonAsync("docs/notes-index.json") + ?? new NotesIndex(); + + return _index; + } + + public async Task GetNoteAsync(string slug) + { + var index = await GetIndexAsync(); + var noteInfo = index.Notes.FirstOrDefault(n => + string.Equals(n.Slug, slug, StringComparison.OrdinalIgnoreCase)); + if (noteInfo == null) return null; + + try + { + var markdown = await _http.GetStringAsync($"docs/notes/{slug}.md"); + return ParseDocument(slug, noteInfo.Title, markdown); + } + catch + { + return null; + } + } + + private static NoteDocument ParseDocument(string slug, string title, string markdown) + { + var doc = new NoteDocument + { + Slug = slug, + Title = title + }; + + var frontmatterLines = new List(); + var bodyLines = new List(); + var inFrontmatter = false; + var frontmatterDone = false; + + foreach (var line in markdown.Replace("\r\n", "\n").Split('\n')) + { + var trimmed = line.Trim(); + if (!frontmatterDone && trimmed == "---") + { + if (!inFrontmatter) + { + inFrontmatter = true; + continue; + } + inFrontmatter = false; + frontmatterDone = true; + continue; + } + + if (inFrontmatter) + frontmatterLines.Add(line); + else if (frontmatterDone || !string.IsNullOrWhiteSpace(line)) + bodyLines.Add(line); + } + + if (frontmatterLines.Count > 0) + { + var fmHtml = new System.Text.StringBuilder(); + fmHtml.Append(""); + foreach (var line in frontmatterLines) + { + var colonIdx = line.IndexOf(':'); + if (colonIdx > 0) + { + var key = line[..colonIdx].Trim(); + var value = line[(colonIdx + 1)..].Trim().Trim('"'); + fmHtml.Append(""); + } + else + { + fmHtml.Append(""); + } + } + fmHtml.Append("
"); + fmHtml.Append(System.Net.WebUtility.HtmlEncode(key)); + fmHtml.Append(""); + fmHtml.Append(System.Net.WebUtility.HtmlEncode(value)); + fmHtml.Append("
"); + fmHtml.Append(System.Net.WebUtility.HtmlEncode(line.Trim())); + fmHtml.Append("
"); + doc.FrontmatterHtml = fmHtml.ToString(); + } + + var body = string.Join("\n", bodyLines); + body = ConvertWikiLinks(body); + doc.HtmlContent = Markdown.ToHtml(body, Pipeline); + + return doc; + } + + private static string ConvertWikiLinks(string text) + { + return System.Text.RegularExpressions.Regex.Replace( + text, + @"\[\[([^\]]+)\]\]", + match => + { + var content = match.Groups[1].Value; + var parts = content.Split('|'); + var linkText = parts.Length > 1 ? parts[1].Trim() : parts[0].Trim(); + var target = parts[0].Trim(); + var slug = Slugify(target); + return $"{System.Net.WebUtility.HtmlEncode(linkText)}"; + }); + } + + public static string Slugify(string title) + { + var slug = title.ToLowerInvariant() + .Replace(' ', '-') + .Replace("'", "") + .Replace(".", "") + .Replace("(", "") + .Replace(")", ""); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[^a-z0-9\-]", ""); + slug = System.Text.RegularExpressions.Regex.Replace(slug, @"-+", "-"); + return slug.Trim('-'); + } +} diff --git a/ET/Web/SyncDocs.ps1 b/ET/Web/SyncDocs.ps1 new file mode 100644 index 0000000..89a7baf --- /dev/null +++ b/ET/Web/SyncDocs.ps1 @@ -0,0 +1,24 @@ +$src = Resolve-Path "$PSScriptRoot\..\..\docs\Notes" +$dst = "$PSScriptRoot\wwwroot\docs\notes" +if (Test-Path $dst) { Remove-Item "$dst\*" -Force -ErrorAction SilentlyContinue } +New-Item -ItemType Directory -Path $dst -Force | Out-Null + +$entries = @() +Get-ChildItem -Path $src -Filter "*.md" | ForEach-Object { + $base = $_.BaseName + $slug = $base.ToLowerInvariant() + $slug = $slug -replace "'", "" + $slug = $slug -replace "\.", "" + $slug = $slug -replace "[^a-z0-9 -]", "" + $slug = $slug -replace "\s+", "-" + $slug = $slug -replace "-+", "-" + $slug = $slug.Trim('-') + + Copy-Item $_.FullName (Join-Path $dst "$slug.md") + + $entries += @{ slug = $slug; title = $base } +} + +$indexPath = "$PSScriptRoot\wwwroot\docs\notes-index.json" +$index = @{ notes = $entries } | ConvertTo-Json +Set-Content -Path $indexPath -Value $index -Encoding UTF8 diff --git a/ET/Web/Web.csproj b/ET/Web/Web.csproj index 5d0fa8c..071f3f1 100644 --- a/ET/Web/Web.csproj +++ b/ET/Web/Web.csproj @@ -8,8 +8,13 @@ + + + + + diff --git a/ET/Web/_Imports.razor b/ET/Web/_Imports.razor index eadfbc1..41faa59 100644 --- a/ET/Web/_Imports.razor +++ b/ET/Web/_Imports.razor @@ -7,4 +7,6 @@ @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop @using Web -@using Web.Layout \ No newline at end of file +@using Web.Layout +@using Web.Models +@using Web.Services \ No newline at end of file diff --git a/ET/Web/wwwroot/css/app.css b/ET/Web/wwwroot/css/app.css index fe44b1d..1f93b02 100644 --- a/ET/Web/wwwroot/css/app.css +++ b/ET/Web/wwwroot/css/app.css @@ -112,4 +112,90 @@ code { .form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { text-align: start; -} \ No newline at end of file +} + +.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"); +} + +.docs-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.docs-card { + display: block; + padding: 0.75rem 1rem; + border: 1px solid #dee2e6; + border-radius: 8px; + text-decoration: none; + color: inherit; + transition: box-shadow 0.15s ease-in-out, border-color 0.15s ease-in-out; +} + +.docs-card:hover { + border-color: #1b6ec2; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + text-decoration: none; +} + +.docs-card h3 { + margin: 0; + font-size: 1rem; + font-weight: 500; +} + +.frontmatter-section { + margin: 1rem 0; + padding: 0.5rem; + border: 1px solid #e9ecef; + border-radius: 6px; + background: #f8f9fa; +} + +.frontmatter-section summary { + cursor: pointer; + font-weight: 600; + color: #495057; + padding: 0.25rem 0; +} + +table.frontmatter { + width: 100%; + border-collapse: collapse; + margin-top: 0.5rem; + font-size: 0.9rem; +} + +table.frontmatter td { + padding: 0.25rem 0.5rem; + border: 1px solid #dee2e6; + vertical-align: top; +} + +table.frontmatter td.fm-key { + font-weight: 600; + color: #495057; + white-space: nowrap; + width: 1%; + background: #e9ecef; +} + +.markdown-body { + line-height: 1.7; + margin-top: 1rem; +} + +.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; } \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes-index.json b/ET/Web/wwwroot/docs/notes-index.json new file mode 100644 index 0000000..58c9e87 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes-index.json @@ -0,0 +1,380 @@ +{ + "notes": [ + { + "slug": "artificer", + "title": "Artificer" + }, + { + "slug": "awareness", + "title": "Awareness" + }, + { + "slug": "cache-map", + "title": "Cache Map" + }, + { + "slug": "card-library", + "title": "Card Library" + }, + { + "slug": "concoliator", + "title": "Concoliator" + }, + { + "slug": "contents", + "title": "Contents" + }, + { + "slug": "crisis-markers", + "title": "Crisis Markers" + }, + { + "slug": "crisis-resolution", + "title": "Crisis Resolution" + }, + { + "slug": "crisis", + "title": "Crisis" + }, + { + "slug": "dirt-roads", + "title": "Dirt Roads" + }, + { + "slug": "dolewood-canoe", + "title": "Dolewood Canoe" + }, + { + "slug": "e03", + "title": "E.03" + }, + { + "slug": "ecology", + "title": "Ecology" + }, + { + "slug": "endeavor-tokens", + "title": "Endeavor Tokens" + }, + { + "slug": "energy-tokens", + "title": "Energy Tokens" + }, + { + "slug": "event-cards", + "title": "Event Cards" + }, + { + "slug": "events", + "title": "Events" + }, + { + "slug": "explore-phase", + "title": "Explore Phase" + }, + { + "slug": "explorer", + "title": "Explorer" + }, + { + "slug": "ferinodex", + "title": "Ferinodex" + }, + { + "slug": "flora-meeples", + "title": "Flora Meeples" + }, + { + "slug": "forest-1", + "title": "Forest 1" + }, + { + "slug": "forest-2", + "title": "Forest 2" + }, + { + "slug": "forest-3", + "title": "Forest 3" + }, + { + "slug": "forest-4", + "title": "Forest 4" + }, + { + "slug": "forest-5", + "title": "Forest 5" + }, + { + "slug": "forest", + "title": "Forest" + }, + { + "slug": "gauzeblade", + "title": "Gauzeblade" + }, + { + "slug": "gear-cards", + "title": "Gear Cards" + }, + { + "slug": "grass-1", + "title": "Grass 1" + }, + { + "slug": "grass-2", + "title": "Grass 2" + }, + { + "slug": "grass-3", + "title": "Grass 3" + }, + { + "slug": "grass-4", + "title": "Grass 4" + }, + { + "slug": "grass-5", + "title": "Grass 5" + }, + { + "slug": "grasslands", + "title": "Grasslands" + }, + { + "slug": "guide", + "title": "Guide" + }, + { + "slug": "hidden-trail-map", + "title": "Hidden Trail Map" + }, + { + "slug": "injury-cards", + "title": "Injury Cards" + }, + { + "slug": "lake", + "title": "Lake" + }, + { + "slug": "losing-the-game", + "title": "Losing the Game" + }, + { + "slug": "market", + "title": "Market" + }, + { + "slug": "mountain-1", + "title": "Mountain 1" + }, + { + "slug": "mountain-2", + "title": "Mountain 2" + }, + { + "slug": "mountain-3", + "title": "Mountain 3" + }, + { + "slug": "mountain-4", + "title": "Mountain 4" + }, + { + "slug": "mountain-5", + "title": "Mountain 5" + }, + { + "slug": "mountain", + "title": "Mountain" + }, + { + "slug": "paratrepsis-whistle", + "title": "Paratrepsis Whistle" + }, + { + "slug": "paved-roads", + "title": "Paved Roads" + }, + { + "slug": "perfect-day-1", + "title": "Perfect Day 1" + }, + { + "slug": "phonoscopic-headset", + "title": "Phonoscopic Headset" + }, + { + "slug": "player-boards", + "title": "Player Boards" + }, + { + "slug": "player-meeples", + "title": "Player Meeples" + }, + { + "slug": "predator-meeples", + "title": "Predator Meeples" + }, + { + "slug": "prepare-phase", + "title": "Prepare Phase" + }, + { + "slug": "press-on", + "title": "Press On" + }, + { + "slug": "prey-meeples", + "title": "Prey Meeples" + }, + { + "slug": "progress", + "title": "Progress" + }, + { + "slug": "ranger-badges", + "title": "Ranger Badges" + }, + { + "slug": "ranger-meeples", + "title": "Ranger Meeples" + }, + { + "slug": "research-station", + "title": "Research Station" + }, + { + "slug": "rest", + "title": "Rest" + }, + { + "slug": "role-cards", + "title": "Role Cards" + }, + { + "slug": "round", + "title": "Round" + }, + { + "slug": "ruins-map", + "title": "Ruins Map" + }, + { + "slug": "scout", + "title": "Scout" + }, + { + "slug": "shaper", + "title": "Shaper" + }, + { + "slug": "shepard", + "title": "Shepard" + }, + { + "slug": "story", + "title": "Story" + }, + { + "slug": "supply", + "title": "Supply" + }, + { + "slug": "terrain-2", + "title": "Terrain 2" + }, + { + "slug": "terrain-3", + "title": "Terrain 3" + }, + { + "slug": "terrain-cards", + "title": "Terrain Cards" + }, + { + "slug": "terrain", + "title": "Terrain" + }, + { + "slug": "totem-of-the-irix", + "title": "Totem of the Irix" + }, + { + "slug": "trade", + "title": "Trade" + }, + { + "slug": "trader", + "title": "Trader" + }, + { + "slug": "trail-markers", + "title": "Trail Markers" + }, + { + "slug": "travel-phase", + "title": "Travel Phase" + }, + { + "slug": "traverse", + "title": "Traverse" + }, + { + "slug": "unmaintained-roads", + "title": "Unmaintained Roads" + }, + { + "slug": "valley", + "title": "Valley" + }, + { + "slug": "victory-condition", + "title": "Victory Condition" + }, + { + "slug": "wasteland-1", + "title": "Wasteland 1" + }, + { + "slug": "wasteland", + "title": "Wasteland" + }, + { + "slug": "water-1", + "title": "Water 1" + }, + { + "slug": "water-2", + "title": "Water 2" + }, + { + "slug": "water-3", + "title": "Water 3" + }, + { + "slug": "water-4", + "title": "Water 4" + }, + { + "slug": "water-5", + "title": "Water 5" + }, + { + "slug": "weather", + "title": "Weather" + }, + { + "slug": "white-sky", + "title": "White Sky" + }, + { + "slug": "xp-cubes", + "title": "XP Cubes" + }, + { + "slug": "xp", + "title": "XP" + } + ] +} diff --git a/ET/Web/wwwroot/docs/notes/artificer.md b/ET/Web/wwwroot/docs/notes/artificer.md new file mode 100644 index 0000000..46f0cf8 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/artificer.md @@ -0,0 +1,6 @@ +--- +category: Role +Role Effect: Can have an additional gear and can spend focus to ready that gear or add a stored item from the supply. +Role Veteran Effect: Can have two additional gear and can spend focus to ready that gear or add a stored item from the supply +Unknown: "4" +--- diff --git a/ET/Web/wwwroot/docs/notes/awareness.md b/ET/Web/wwwroot/docs/notes/awareness.md new file mode 100644 index 0000000..4d85a7e --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/awareness.md @@ -0,0 +1 @@ +Green energy. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/cache-map.md b/ET/Web/wwwroot/docs/notes/cache-map.md new file mode 100644 index 0000000..5675443 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/cache-map.md @@ -0,0 +1,7 @@ +--- +category: Gear +Cost: 1 +Effect: "Travel: Discard this gear and spend 2 [[Progress]] while in this region to gain 1 [[Tool]]." +Location: Anywhere +Gear Category: Tool +--- diff --git a/ET/Web/wwwroot/docs/notes/card-library.md b/ET/Web/wwwroot/docs/notes/card-library.md new file mode 100644 index 0000000..cee6c0b --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/card-library.md @@ -0,0 +1,3 @@ +--- +count: 380 +--- diff --git a/ET/Web/wwwroot/docs/notes/concoliator.md b/ET/Web/wwwroot/docs/notes/concoliator.md new file mode 100644 index 0000000..1d25827 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/concoliator.md @@ -0,0 +1,6 @@ +--- +category: Role +Role Effect: Can have any number of companions with you. Can exchange companions with any Ranger nearby during the prepare phase. +Role Veteran Effect: Ignore the effects of collateral damage. Can exchange companions with any Ranger anywhere during the prepare phase. +Unknown: "4" +--- diff --git a/ET/Web/wwwroot/docs/notes/contents.md b/ET/Web/wwwroot/docs/notes/contents.md new file mode 100644 index 0000000..126e2b0 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/contents.md @@ -0,0 +1,20 @@ +| Item | Count | +| -------------------- | ----- | +| [[Terrain Cards]] | 198 | +| [[Card Library]] | 380 | +| [[Event Cards]] | 40 | +| [[Gear Cards]] | 50 | +| [[Injury Cards]] | 20 | +| [[Endeavor Tokens]] | 48 | +| [[Energy Tokens]] | 80 | +| [[Crisis Markers]] | 7 | +| [[Flora Meeples]] | 40 | +| [[Prey Meeples]] | 40 | +| [[Predator Meeples]] | 40 | +| [[XP Cubes]] | 5 | +| [[Ranger Meeples]] | 20 | +| [[Player Boards]] | 5 | +| [[Player Meeples]] | 5 | +| [[Role Cards]] | 10 | +| [[Trail Markers]] | 10 | + diff --git a/ET/Web/wwwroot/docs/notes/crisis-markers.md b/ET/Web/wwwroot/docs/notes/crisis-markers.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/crisis-resolution.md b/ET/Web/wwwroot/docs/notes/crisis-resolution.md new file mode 100644 index 0000000..6fae1b7 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/crisis-resolution.md @@ -0,0 +1 @@ +When the third Ranger is placed on a crisis. Resolve it, retire 1 of the Rangers, and roll the risk die to determine which of the others retire as well. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/crisis.md b/ET/Web/wwwroot/docs/notes/crisis.md new file mode 100644 index 0000000..7dcf972 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/crisis.md @@ -0,0 +1,2 @@ +Find and encounter a crisis card to attempt to solve the crisis yourself. + diff --git a/ET/Web/wwwroot/docs/notes/dirt-roads.md b/ET/Web/wwwroot/docs/notes/dirt-roads.md new file mode 100644 index 0000000..68f5305 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/dirt-roads.md @@ -0,0 +1 @@ +2 Progress + Ecology Meeples increases travel cost. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/dolewood-canoe.md b/ET/Web/wwwroot/docs/notes/dolewood-canoe.md new file mode 100644 index 0000000..15d2909 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/dolewood-canoe.md @@ -0,0 +1,7 @@ +--- +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 +--- diff --git a/ET/Web/wwwroot/docs/notes/e03.md b/ET/Web/wwwroot/docs/notes/e03.md new file mode 100644 index 0000000..71a1ed5 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/e03.md @@ -0,0 +1,10 @@ +--- +category: Event +isCrisis: true +Event Type: Mountain +Activates Ecology: +Lake Ecology: + - "[[Prey Meeples]]" +Description: Some of the best experience in life is hard-earned by taking foolish risks, and a few things are riskier than going into the mountains during a thunder-storm. +--- +Each Ranger can choose to suffer 2 fatigue to gain 2 XP. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/ecology.md b/ET/Web/wwwroot/docs/notes/ecology.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/endeavor-tokens.md b/ET/Web/wwwroot/docs/notes/endeavor-tokens.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/energy-tokens.md b/ET/Web/wwwroot/docs/notes/energy-tokens.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/event-cards.md b/ET/Web/wwwroot/docs/notes/event-cards.md new file mode 100644 index 0000000..130feb4 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/event-cards.md @@ -0,0 +1,7 @@ +--- +count: 40 +--- +Draw and resolve on event card at the start of each [[Round]]. + +Event cards spawn [[Crisis]], activate the [[Ecology]], and represent the [[Story]] and [[Weather]] affecting the [[Valley]]. + diff --git a/ET/Web/wwwroot/docs/notes/events.md b/ET/Web/wwwroot/docs/notes/events.md new file mode 100644 index 0000000..35d6517 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/events.md @@ -0,0 +1,3 @@ +[[Sun]] +[[Mountain]] +[[Seal]] diff --git a/ET/Web/wwwroot/docs/notes/explore-phase.md b/ET/Web/wwwroot/docs/notes/explore-phase.md new file mode 100644 index 0000000..4ba9615 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/explore-phase.md @@ -0,0 +1,3 @@ +Draw terrain cards as [[Progress]] until you encounter one. + +![[Press On]] \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/explorer.md b/ET/Web/wwwroot/docs/notes/explorer.md new file mode 100644 index 0000000..2db0f66 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/explorer.md @@ -0,0 +1,10 @@ +--- +category: Role +Awareness: 2 +Fitnesses: 2 +Knowledge: 1 +Spirit: 1 +Unknown: "4" +Role Effect: Ignores predators when suffering fatigue to continue exploring. +Role Veteran Effect: All nearby Rangers ignore predators when suffering fatigue to continue exploring. +--- diff --git a/ET/Web/wwwroot/docs/notes/ferinodex.md b/ET/Web/wwwroot/docs/notes/ferinodex.md new file mode 100644 index 0000000..9d333c6 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/ferinodex.md @@ -0,0 +1,7 @@ +--- +category: Gear +Cost: 2 +Gear Category: Tool +Effect: "Explore: Once per day, use in the place of 1[[Focus]]." +Location: Anywhere +--- diff --git a/ET/Web/wwwroot/docs/notes/flora-meeples.md b/ET/Web/wwwroot/docs/notes/flora-meeples.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/forest-1.md b/ET/Web/wwwroot/docs/notes/forest-1.md new file mode 100644 index 0000000..638cb60 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/forest-1.md @@ -0,0 +1,10 @@ +--- +category: Region +Connections: + - "[[Grass 1]]" + - "[[Grass 2]]" + - "[[Water 1]]" + - "[[Forest 2]]" +X: 60 +Y: 330 +--- diff --git a/ET/Web/wwwroot/docs/notes/forest-2.md b/ET/Web/wwwroot/docs/notes/forest-2.md new file mode 100644 index 0000000..42bf230 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/forest-2.md @@ -0,0 +1,9 @@ +--- +category: Region +Connections: + - "[[Grass 2]]" + - "[[Water 2]]" + - "[[Forest 1]]" +X: 250 +Y: 370 +--- diff --git a/ET/Web/wwwroot/docs/notes/forest-3.md b/ET/Web/wwwroot/docs/notes/forest-3.md new file mode 100644 index 0000000..db6e7b0 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/forest-3.md @@ -0,0 +1,7 @@ +--- +category: Region +Connections: + - "[[Water 2]]" +X: 150 +Y: 140 +--- diff --git a/ET/Web/wwwroot/docs/notes/forest-4.md b/ET/Web/wwwroot/docs/notes/forest-4.md new file mode 100644 index 0000000..6efb29b --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/forest-4.md @@ -0,0 +1,9 @@ +--- +category: Region +Connections: + - "[[Grass 4]]" + - "[[Wasteland 1]]" + - "[[Grass 5]]" +Y: 100 +X: 420 +--- diff --git a/ET/Web/wwwroot/docs/notes/forest-5.md b/ET/Web/wwwroot/docs/notes/forest-5.md new file mode 100644 index 0000000..22b164d --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/forest-5.md @@ -0,0 +1,7 @@ +--- +category: Region +Connections: + - "[[Water 5]]" +Y: 410 +X: 470 +--- diff --git a/ET/Web/wwwroot/docs/notes/forest.md b/ET/Web/wwwroot/docs/notes/forest.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/gauzeblade.md b/ET/Web/wwwroot/docs/notes/gauzeblade.md new file mode 100644 index 0000000..3f8fb48 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/gauzeblade.md @@ -0,0 +1,7 @@ +--- +category: Gear +Cost: 5 +Gear Category: 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]]" +--- diff --git a/ET/Web/wwwroot/docs/notes/gear-cards.md b/ET/Web/wwwroot/docs/notes/gear-cards.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/grass-1.md b/ET/Web/wwwroot/docs/notes/grass-1.md new file mode 100644 index 0000000..7ee589b --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/grass-1.md @@ -0,0 +1,11 @@ +--- +category: Region +Connections: + - "[[Mountain 1]]" + - "[[Water 2]]" + - "[[Forest 1]]" +X: 30 +Y: 250 +Landmarks: + - "[[Lone Tree Station]]" +--- diff --git a/ET/Web/wwwroot/docs/notes/grass-2.md b/ET/Web/wwwroot/docs/notes/grass-2.md new file mode 100644 index 0000000..155dd3e --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/grass-2.md @@ -0,0 +1,10 @@ +--- +category: Region +Connections: + - "[[Forest 1]]" + - "[[Forest 2]]" +X: 150 +Y: 400 +Landmarks: + - "[[Spire]]" +--- diff --git a/ET/Web/wwwroot/docs/notes/grass-3.md b/ET/Web/wwwroot/docs/notes/grass-3.md new file mode 100644 index 0000000..97e3916 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/grass-3.md @@ -0,0 +1,9 @@ +--- +category: Region +Connections: + - "[[Water 2]]" + - "[[Mountain 2]]" + - "[[Mountain 3]]" +X: 300 +Y: 230 +--- diff --git a/ET/Web/wwwroot/docs/notes/grass-4.md b/ET/Web/wwwroot/docs/notes/grass-4.md new file mode 100644 index 0000000..983345e --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/grass-4.md @@ -0,0 +1,9 @@ +--- +category: Region +Connections: + - "[[Mountain 4]]" + - "[[Wasteland 1]]" + - "[[Grass 4]]" +Y: 80 +X: 630 +--- diff --git a/ET/Web/wwwroot/docs/notes/grass-5.md b/ET/Web/wwwroot/docs/notes/grass-5.md new file mode 100644 index 0000000..3ed0792 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/grass-5.md @@ -0,0 +1,10 @@ +--- +category: Region +Connections: + - "[[Mountain 5]]" + - "[[Water 5]]" + - "[[Wasteland 1]]" + - "[[Forest 4]]" +Y: 290 +X: 550 +--- diff --git a/ET/Web/wwwroot/docs/notes/grasslands.md b/ET/Web/wwwroot/docs/notes/grasslands.md new file mode 100644 index 0000000..ec782bf --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/grasslands.md @@ -0,0 +1,2 @@ +Default region that has [[Lone Tree Station]]. + diff --git a/ET/Web/wwwroot/docs/notes/guide.md b/ET/Web/wwwroot/docs/notes/guide.md new file mode 100644 index 0000000..e7043fe --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/guide.md @@ -0,0 +1,10 @@ +--- +category: Role +Role Effect: Can help nearby Rangers. +Role Veteran Effect: Can help any Ranger anywhere. +Awareness: 2 +Fitnesses: 2 +Knowledge: 2 +Spirit: 2 +Unknown: "5" +--- diff --git a/ET/Web/wwwroot/docs/notes/hidden-trail-map.md b/ET/Web/wwwroot/docs/notes/hidden-trail-map.md new file mode 100644 index 0000000..c4ae602 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/hidden-trail-map.md @@ -0,0 +1,7 @@ +--- +category: Gear +Cost: 3 +Gear Category: Tool +Effect: "Prepare: Discard to travel to a nearby region." +Location: Anywhere +--- diff --git a/ET/Web/wwwroot/docs/notes/injury-cards.md b/ET/Web/wwwroot/docs/notes/injury-cards.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/lake.md b/ET/Web/wwwroot/docs/notes/lake.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/losing-the-game.md b/ET/Web/wwwroot/docs/notes/losing-the-game.md new file mode 100644 index 0000000..2b195bc --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/losing-the-game.md @@ -0,0 +1 @@ +If you ever need to deploy a [[Ranger Meeples]] from [[Lone Tree Station]] but can't you lose the game. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/market.md b/ET/Web/wwwroot/docs/notes/market.md new file mode 100644 index 0000000..8fc8d71 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/market.md @@ -0,0 +1,2 @@ +Cards you can buy. Some cards need you to be at a specific location to buy them. + diff --git a/ET/Web/wwwroot/docs/notes/mountain-1.md b/ET/Web/wwwroot/docs/notes/mountain-1.md new file mode 100644 index 0000000..01aacc9 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/mountain-1.md @@ -0,0 +1,9 @@ +--- +category: Region +Connections: + - "[[Grass 1]]" + - "[[Water 3]]" + - "[[Forest 3]]" +X: 20 +Y: 120 +--- diff --git a/ET/Web/wwwroot/docs/notes/mountain-2.md b/ET/Web/wwwroot/docs/notes/mountain-2.md new file mode 100644 index 0000000..72f1582 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/mountain-2.md @@ -0,0 +1,8 @@ +--- +category: Region +Connections: + - "[[Water 3]]" + - "[[Forest 3]]" +X: 260 +Y: 110 +--- diff --git a/ET/Web/wwwroot/docs/notes/mountain-3.md b/ET/Web/wwwroot/docs/notes/mountain-3.md new file mode 100644 index 0000000..e1cdeeb --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/mountain-3.md @@ -0,0 +1,9 @@ +--- +category: Region +Connections: + - "[[Mountain 5]]" + - "[[Forest 4]]" + - "[[Grass 3]]" +Y: 180 +X: 380 +--- diff --git a/ET/Web/wwwroot/docs/notes/mountain-4.md b/ET/Web/wwwroot/docs/notes/mountain-4.md new file mode 100644 index 0000000..ca8941f --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/mountain-4.md @@ -0,0 +1,8 @@ +--- +category: Region +Connections: + - "[[Forest 4]]" + - "[[Grass 4]]" +Y: 30 +X: 370 +--- diff --git a/ET/Web/wwwroot/docs/notes/mountain-5.md b/ET/Web/wwwroot/docs/notes/mountain-5.md new file mode 100644 index 0000000..d49e9df --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/mountain-5.md @@ -0,0 +1,8 @@ +--- +category: Region +Connections: + - "[[Grass 5]]" + - "[[Mountain 3]]" +Y: 330 +X: 430 +--- diff --git a/ET/Web/wwwroot/docs/notes/mountain.md b/ET/Web/wwwroot/docs/notes/mountain.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/paratrepsis-whistle.md b/ET/Web/wwwroot/docs/notes/paratrepsis-whistle.md new file mode 100644 index 0000000..cb93528 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/paratrepsis-whistle.md @@ -0,0 +1,7 @@ +--- +category: Gear +Cost: 6 +Gear Category: Tool +Effect: "Explore: You can spend 1 [[Focus]] to prevent suffering 1 injury." +Location: "[[Lone Tree Station]]" +--- diff --git a/ET/Web/wwwroot/docs/notes/paved-roads.md b/ET/Web/wwwroot/docs/notes/paved-roads.md new file mode 100644 index 0000000..b3616c8 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/paved-roads.md @@ -0,0 +1 @@ +1 Progress + Ecology Meeples increases travel cost. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/perfect-day-1.md b/ET/Web/wwwroot/docs/notes/perfect-day-1.md new file mode 100644 index 0000000..403b918 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/perfect-day-1.md @@ -0,0 +1,14 @@ +--- +category: Event +isCrisis: true +Event Type: Mountain +Forest Ecology: + - "[[Prey Meeples]]" +Lake Ecology: + - "[[Prey Meeples]]" +Grass Ecology: +Mountain Ecology: +Wasteland Ecology: +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. +Description: A massive crack of thunder heralds the approach of a storm rolling into the Valley. At the sound of the deafening boom, herbs of startled animals scatter and run for cover. +--- diff --git a/ET/Web/wwwroot/docs/notes/phonoscopic-headset.md b/ET/Web/wwwroot/docs/notes/phonoscopic-headset.md new file mode 100644 index 0000000..44b0189 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/phonoscopic-headset.md @@ -0,0 +1,7 @@ +--- +category: Gear +Cost: 2 +Gear Category: Garment +Effect: "Explore: Once per day, use in the place of 1 [[Awareness]]" +Location: +--- diff --git a/ET/Web/wwwroot/docs/notes/player-boards.md b/ET/Web/wwwroot/docs/notes/player-boards.md new file mode 100644 index 0000000..a1d7af2 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/player-boards.md @@ -0,0 +1 @@ +Containers helper notes around player reminder rules for a player. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/player-meeples.md b/ET/Web/wwwroot/docs/notes/player-meeples.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/predator-meeples.md b/ET/Web/wwwroot/docs/notes/predator-meeples.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/prepare-phase.md b/ET/Web/wwwroot/docs/notes/prepare-phase.md new file mode 100644 index 0000000..7ddca4a --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/prepare-phase.md @@ -0,0 +1,9 @@ +Spend energy to prepare for your day, or save it to explore. + +![[Scout]] + +![[Traverse]] + +![[Rest]] + +![[Trade]] \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/press-on.md b/ET/Web/wwwroot/docs/notes/press-on.md new file mode 100644 index 0000000..321ffef --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/press-on.md @@ -0,0 +1 @@ +When your explore step would end, you may suffer 1 fatigue (plus 1 for each predator) to continue exploring. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/prey-meeples.md b/ET/Web/wwwroot/docs/notes/prey-meeples.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/progress.md b/ET/Web/wwwroot/docs/notes/progress.md new file mode 100644 index 0000000..557a155 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/progress.md @@ -0,0 +1 @@ +Progress represents how far you come in order to travel on. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/ranger-badges.md b/ET/Web/wwwroot/docs/notes/ranger-badges.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/ranger-meeples.md b/ET/Web/wwwroot/docs/notes/ranger-meeples.md new file mode 100644 index 0000000..0f55bd9 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/ranger-meeples.md @@ -0,0 +1,3 @@ +When you run out of Ranger Meeples you lose the game. + +When three Ranger Meeples are on a crisis, they complete it. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/research-station.md b/ET/Web/wwwroot/docs/notes/research-station.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/rest.md b/ET/Web/wwwroot/docs/notes/rest.md new file mode 100644 index 0000000..37bbf52 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/rest.md @@ -0,0 +1,7 @@ +--- +actionType: Prepare +energyType: Focus +--- + + +Rest to recover fatigue or injuries. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/role-cards.md b/ET/Web/wwwroot/docs/notes/role-cards.md new file mode 100644 index 0000000..b8b4ca7 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/role-cards.md @@ -0,0 +1,15 @@ + +| ![[Role Example.png]] | ![[Role Example 2.png]]
| +| --------------------- | ---------------------------------------- | + +![[Explorer]] + +![[Shepard]] + +![[Artificer]] + +![[Concoliator]] + +![[Trader]] + +![[Guide]] \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/round.md b/ET/Web/wwwroot/docs/notes/round.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/ruins-map.md b/ET/Web/wwwroot/docs/notes/ruins-map.md new file mode 100644 index 0000000..b59f1aa --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/ruins-map.md @@ -0,0 +1,7 @@ +--- +category: Gear +Cost: 2 +Gear Category: Tool +Effect: "Travel: Retire this gear an spend 2[[Progress]] while in this region to gain 1 [[Artifact]]." +Location: Anywhere +--- diff --git a/ET/Web/wwwroot/docs/notes/scout.md b/ET/Web/wwwroot/docs/notes/scout.md new file mode 100644 index 0000000..cdd731a --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/scout.md @@ -0,0 +1,8 @@ +--- +actionType: + - "[[Prepare Phase]]" +energyType: + - "[[Awareness]]" +--- +Scout terrain cards, putting them on top or bottom. + diff --git a/ET/Web/wwwroot/docs/notes/shaper.md b/ET/Web/wwwroot/docs/notes/shaper.md new file mode 100644 index 0000000..97831d3 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/shaper.md @@ -0,0 +1,4 @@ +--- +category: Role +Unknown: "4" +--- diff --git a/ET/Web/wwwroot/docs/notes/shepard.md b/ET/Web/wwwroot/docs/notes/shepard.md new file mode 100644 index 0000000..1cfb70c --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/shepard.md @@ -0,0 +1,10 @@ +--- +category: Role +Awareness: 2 +Fitnesses: 1 +Knowledge: 1 +Spirit: 2 +Unknown: "4" +Role Effect: You can spend spirit to move an equal number of prey and predator from a nearby region to your region. +Role Veteran Effect: You can also move them out of your region to a nearby one. +--- diff --git a/ET/Web/wwwroot/docs/notes/story.md b/ET/Web/wwwroot/docs/notes/story.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/supply.md b/ET/Web/wwwroot/docs/notes/supply.md new file mode 100644 index 0000000..f84da93 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/supply.md @@ -0,0 +1 @@ +The deck of gear. Things that can get something, like a tool, and get the top most tool from the supply. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/terrain-2.md b/ET/Web/wwwroot/docs/notes/terrain-2.md new file mode 100644 index 0000000..f1df0d0 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/terrain-2.md @@ -0,0 +1,9 @@ +--- +category: Terrain +Description: You come across a squat building sitting in the shadow of [[Lone Tree Station]]. Other Rangers are coming and going, checking in with an older man who hunches over a map of the Valley, his impressively-wide hatbrim shading almost the entire table as he draws lines on the map, sending out Rangers to blaze new trails after a season of growth. +--- +1 [[Spirit]] Ask him to show you marked trails. Place one of your [[Trail Markers]] on any region. + +2 [[Awareness]] Offer to take over mapping. + +Walk by the outpost. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/terrain-3.md b/ET/Web/wwwroot/docs/notes/terrain-3.md new file mode 100644 index 0000000..07dccd9 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/terrain-3.md @@ -0,0 +1,11 @@ +--- +category: Terrain +Description: The shadow of the giga-redwood is a flurry of activity as Rangers go about their assigned duties and exchange stories with the people of the Valley. +--- +1 [[Spirit]] Listen to stories about goings-on throughout the Valley. + +1 [[Focus]] Recommend new duties to a Range out on the field. + +1 [[Awareness]] Request help with a crisis. + +X Leave everyone to their jobs. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/terrain-cards.md b/ET/Web/wwwroot/docs/notes/terrain-cards.md new file mode 100644 index 0000000..2e8f727 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/terrain-cards.md @@ -0,0 +1,14 @@ +--- +count: "198" +--- +Cards you draw when exploring the matching region. + +Each Terrain type appears to be spread out across 5 regions in the map, the exception being [[Wasteland]] which is just one location to the west. + +# Categories +![[Lake]] +![[Grasslands]] +![[Forest]] +![[Mountain]] +![[Wasteland]] + diff --git a/ET/Web/wwwroot/docs/notes/terrain.md b/ET/Web/wwwroot/docs/notes/terrain.md new file mode 100644 index 0000000..22c951c --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/terrain.md @@ -0,0 +1,12 @@ +--- +category: Terrain +Description: You climb to the top of Lone Tree Station to get the vantage from the circular platforms of Topside Mast. There, you see Ben Amon working on the Swift. It hasn't been functioning perfectly lately, but the artificer keeps it working enough to send Rangers out to deal with crises that arise. +--- +1 [[Spirit]] Send a Ranger on the Swift to a crisis. + +2 [[Focus]] Board the Swift yourself. Travel to any region. + + +X Climb down out of [[Lone Tree Station]]. + +If Mora Orlin is with you, offer her help repairing the Swift. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/totem-of-the-irix.md b/ET/Web/wwwroot/docs/notes/totem-of-the-irix.md new file mode 100644 index 0000000..f83f05d --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/totem-of-the-irix.md @@ -0,0 +1,7 @@ +--- +category: Gear +Cost: 2 +Gear Category: Consumable +Effect: "Explore: Discard to help a Ranger anywhere." +Location: Anywhere +--- diff --git a/ET/Web/wwwroot/docs/notes/trade.md b/ET/Web/wwwroot/docs/notes/trade.md new file mode 100644 index 0000000..b2a0dfc --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/trade.md @@ -0,0 +1,7 @@ +--- +actionType: Prepare +energyType: Spirit +--- + + +Trade to acquire gear from the market (or exchange). \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/trader.md b/ET/Web/wwwroot/docs/notes/trader.md new file mode 100644 index 0000000..51058c6 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/trader.md @@ -0,0 +1,10 @@ +--- +category: Role +Awareness: 1 +Fitnesses: 1 +Knowledge: 2 +Spirit: 2 +Unknown: "4" +Role Effect: All gear counts as one higher when trading. And can exchange gear with any nearby Ranger. +Role Veteran Effect: All of your gear counts as two higher when trading. And can exchange gear with any Ranger anywhere. +--- diff --git a/ET/Web/wwwroot/docs/notes/trail-markers.md b/ET/Web/wwwroot/docs/notes/trail-markers.md new file mode 100644 index 0000000..bd010d7 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/trail-markers.md @@ -0,0 +1,3 @@ +Spend a trailer marker from your region to search for any card from its terrain deck and put it on top. If it's another Ranger's marker, they get an [[XP]]! + +Spend 4 progress to place a Trail Marker as your location. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/travel-phase.md b/ET/Web/wwwroot/docs/notes/travel-phase.md new file mode 100644 index 0000000..ce54020 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/travel-phase.md @@ -0,0 +1,9 @@ +Spend progress to move along paths, place trail markers in your region, or complete other objectives. + +Keep in mind that when you leave a region, you discard all your remaining progress. + +![[Paved Roads]] + +![[Dirt Roads]] + +![[Unmaintained Roads]] \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/traverse.md b/ET/Web/wwwroot/docs/notes/traverse.md new file mode 100644 index 0000000..efe3d36 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/traverse.md @@ -0,0 +1,5 @@ +--- +actionType: Prepare +energyType: Fitness +--- +Traverse your region, adding [[Movement]] for this turn only. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/unmaintained-roads.md b/ET/Web/wwwroot/docs/notes/unmaintained-roads.md new file mode 100644 index 0000000..ea4511d --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/unmaintained-roads.md @@ -0,0 +1 @@ +3 Progress + Ecology Meeples increases travel cost. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/valley.md b/ET/Web/wwwroot/docs/notes/valley.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/victory-condition.md b/ET/Web/wwwroot/docs/notes/victory-condition.md new file mode 100644 index 0000000..da81063 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/victory-condition.md @@ -0,0 +1,3 @@ +You win the game by earing [[Ranger Badges]]. + +The exact number needed is determined by your game mode. \ No newline at end of file diff --git a/ET/Web/wwwroot/docs/notes/wasteland-1.md b/ET/Web/wwwroot/docs/notes/wasteland-1.md new file mode 100644 index 0000000..d3614a2 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/wasteland-1.md @@ -0,0 +1,10 @@ +--- +category: Region +Connections: + - "[[Forest 4]]" + - "[[Grass 4]]" + - "[[Grass 5]]" + - "[[Water 4]]" +Y: 120 +X: 560 +--- diff --git a/ET/Web/wwwroot/docs/notes/wasteland.md b/ET/Web/wwwroot/docs/notes/wasteland.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/water-1.md b/ET/Web/wwwroot/docs/notes/water-1.md new file mode 100644 index 0000000..a7668c0 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/water-1.md @@ -0,0 +1,9 @@ +--- +category: Region +Connections: + - "[[Forest 1]]" +X: 30 +Y: 410 +Landmarks: + - "[[Research Station]]" +--- diff --git a/ET/Web/wwwroot/docs/notes/water-2.md b/ET/Web/wwwroot/docs/notes/water-2.md new file mode 100644 index 0000000..c6c50df --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/water-2.md @@ -0,0 +1,13 @@ +--- +category: Region +Connections: + - "[[Forest 1]]" + - "[[Forest 2]]" + - "[[Grass 1]]" + - "[[Forest 3]]" + - "[[Grass 3]]" +X: 150 +Y: 160 +Landmarks: + - "[[White Sky]]" +--- diff --git a/ET/Web/wwwroot/docs/notes/water-3.md b/ET/Web/wwwroot/docs/notes/water-3.md new file mode 100644 index 0000000..4680c66 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/water-3.md @@ -0,0 +1,9 @@ +--- +category: Region +Connections: + - "[[Mountain 1]]" + - "[[Mountain 2]]" + - "[[Forest 3]]" +X: 50 +Y: 50 +--- diff --git a/ET/Web/wwwroot/docs/notes/water-4.md b/ET/Web/wwwroot/docs/notes/water-4.md new file mode 100644 index 0000000..e0d3e1d --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/water-4.md @@ -0,0 +1,8 @@ +--- +category: Region +Connections: + - "[[Grass 4]]" + - "[[Wasteland 1]]" +Y: 110 +X: 640 +--- diff --git a/ET/Web/wwwroot/docs/notes/water-5.md b/ET/Web/wwwroot/docs/notes/water-5.md new file mode 100644 index 0000000..6450ebb --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/water-5.md @@ -0,0 +1,8 @@ +--- +category: Region +Connections: + - "[[Grass 5]]" + - "[[Forest 5]]" +Y: 400 +X: 600 +--- diff --git a/ET/Web/wwwroot/docs/notes/weather.md b/ET/Web/wwwroot/docs/notes/weather.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/white-sky.md b/ET/Web/wwwroot/docs/notes/white-sky.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/xp-cubes.md b/ET/Web/wwwroot/docs/notes/xp-cubes.md new file mode 100644 index 0000000..e69de29 diff --git a/ET/Web/wwwroot/docs/notes/xp.md b/ET/Web/wwwroot/docs/notes/xp.md new file mode 100644 index 0000000..ea767c0 --- /dev/null +++ b/ET/Web/wwwroot/docs/notes/xp.md @@ -0,0 +1,3 @@ +Getting an XP moved the [[XP Cubes]] one space to the right. + +You can spend XP during the prepare step to permanently increase the amount of energy in one of your aspects or to upgrade your role card. \ No newline at end of file diff --git a/docs/.obsidian/community-plugins.json b/docs/.obsidian/community-plugins.json new file mode 100644 index 0000000..2868535 --- /dev/null +++ b/docs/.obsidian/community-plugins.json @@ -0,0 +1,3 @@ +[ + "kanban-bases-view" +] \ No newline at end of file diff --git a/docs/.obsidian/plugins/kanban-bases-view/main.js b/docs/.obsidian/plugins/kanban-bases-view/main.js new file mode 100644 index 0000000..25e1d84 --- /dev/null +++ b/docs/.obsidian/plugins/kanban-bases-view/main.js @@ -0,0 +1,4140 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ + +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/main.ts +var main_exports = {}; +__export(main_exports, { + KANBAN_VIEW_TYPE: () => KANBAN_VIEW_TYPE, + default: () => KanbanBasesViewPlugin +}); +module.exports = __toCommonJS(main_exports); +var import_obsidian6 = require("obsidian"); + +// src/constants.ts +var UNCATEGORIZED_LABEL = "Uncategorized"; +var HOVER_LINK_SOURCE_ID = "kanban-bases-view"; +var COLOR_PALETTE = [ + { name: "red", cssVar: "var(--color-red)" }, + { name: "orange", cssVar: "var(--color-orange)" }, + { name: "yellow", cssVar: "var(--color-yellow)" }, + { name: "green", cssVar: "var(--color-green)" }, + { name: "cyan", cssVar: "var(--color-cyan)" }, + { name: "blue", cssVar: "var(--color-blue)" }, + { name: "purple", cssVar: "var(--color-purple)" }, + { name: "pink", cssVar: "var(--color-pink)" } +]; +var SORTABLE_GROUP = "obk-columns"; +var SORTED_CARD_ORDER_NOTICE = "\u26A0\uFE0F Sort is active. Clear it to manually reorder cards within a column."; +var DATA_ATTRIBUTES = { + COLUMN_VALUE: "data-column-value", + ENTRY_PATH: "data-entry-path", + SORTABLE_CONTAINER: "data-sortable-container", + COLUMN_POSITION: "data-column-position", + COLUMN_COLOR: "data-column-color", + SWIMLANE_VALUE: "data-swimlane-value" +}; +var SWIMLANE_KEY_SEPARATOR = ""; +var CSS_CLASSES = { + // Container + VIEW_CONTAINER: "obk-view-container", + VIEW_CONTAINER_WITH_SWIMLANES: "obk-view-container--with-swimlanes", + BOARD: "obk-board", + BOARD_WITH_SWIMLANES: "obk-board--with-swimlanes", + // Swimlane (horizontal grouping band) + SWIMLANE: "obk-swimlane", + SWIMLANE_COLLAPSED: "obk-swimlane--collapsed", + SWIMLANE_HEADER: "obk-swimlane-header", + SWIMLANE_TITLE: "obk-swimlane-title", + SWIMLANE_COUNT: "obk-swimlane-count", + SWIMLANE_BODY: "obk-swimlane-body", + SWIMLANE_TOGGLE: "obk-swimlane-toggle", + SWIMLANE_DRAG_HANDLE: "obk-swimlane-drag-handle", + SWIMLANE_DRAGGING: "obk-swimlane-dragging", + SWIMLANE_GHOST: "obk-swimlane-ghost", + // Property selector (for future or framework-driven UI) + PROPERTY_SELECTOR: "obk-property-selector", + PROPERTY_LABEL: "obk-property-label", + PROPERTY_SELECT: "obk-property-select", + // Column + COLUMN: "obk-column", + COLUMN_HEADER: "obk-column-header", + COLUMN_TITLE: "obk-column-title", + COLUMN_COUNT: "obk-column-count", + COLUMN_BODY: "obk-column-body", + COLUMN_DRAG_HANDLE: "obk-column-drag-handle", + COLUMN_DRAGGING: "obk-column-dragging", + COLUMN_GHOST: "obk-column-ghost", + COLUMN_ADD_BTN: "obk-column-add-btn", + // Card + CARD: "obk-card", + CARD_TITLE: "obk-card-title", + CARD_PREVIEW: "obk-card-preview", + CARD_COVER: "obk-card-cover", + CARD_COVER_FIT_COVER: "obk-card-cover--fit-cover", + CARD_COVER_FIT_CONTAIN: "obk-card-cover--fit-contain", + CARD_ACTIVE: "obk-card--active", + CARD_HOVER: "obk-card--hover", + CARD_DRAGGING: "obk-card-dragging", + CARD_GHOST: "obk-card-ghost", + CARD_CHOSEN: "obk-card-chosen", + CARD_PROPERTY: "obk-card-property", + CARD_PROPERTY_WRAP: "obk-card-property-wrap", + CARD_PROPERTY_LABEL: "obk-card-property-label", + CARD_PROPERTY_VALUE: "obk-card-property-value", + // Empty state + EMPTY_STATE: "obk-empty-state", + // Sortable placeholder (fallback / shared ghost style) + SORTABLE_GHOST: "obk-sortable-ghost", + // Column remove button (shown only when column is empty) + COLUMN_REMOVE_BTN: "obk-column-remove-btn", + // Quick add modal + QUICK_ADD_FORM: "obk-quick-add-form", + QUICK_ADD_INPUT: "obk-quick-add-input", + QUICK_ADD_ACTIONS: "obk-quick-add-actions", + // Color picker + COLUMN_COLOR_BTN: "obk-column-color-btn", + COLUMN_COLOR_POPOVER: "obk-column-color-popover", + COLUMN_COLOR_SWATCH: "obk-column-color-swatch", + COLUMN_COLOR_SWATCH_ACTIVE: "obk-column-color-swatch--active", + COLUMN_COLOR_NONE: "obk-column-color-none" +}; +var SORTABLE_CONFIG = { + ANIMATION_DURATION: 150, + TOUCH_DELAY: 150, + TOUCH_START_THRESHOLD: 4 +}; +var DEBOUNCE_DELAY = 50; +var EMPTY_STATE_MESSAGES = { + NO_ENTRIES: "No entries found. Add some notes to your base.", + NO_PROPERTIES: "No properties found in entries." +}; + +// src/kanbanView.ts +var import_obsidian5 = require("obsidian"); + +// src/components/card.ts +var import_obsidian = require("obsidian"); +function computeCardFingerprint(entry, ctx) { + const parts = []; + for (const propId of ctx.order) { + if (propId === ctx.groupByPropertyId) continue; + const val = entry.getValue(propId); + parts.push(val === null ? "" : val.toString()); + } + if (ctx.cardTitlePropertyId) { + const val = entry.getValue(ctx.cardTitlePropertyId); + parts.push(val === null ? "" : val.toString()); + } + if (ctx.imagePropertyId) { + const val = entry.getValue(ctx.imagePropertyId); + parts.push(val === null ? "" : val.toString()); + } + return parts.join("\0"); +} +function renderCardTitle(titleEl, entry, ctx) { + if (!ctx.cardTitlePropertyId) { + titleEl.textContent = entry.file.basename; + return; + } + const titleValue = entry.getValue(ctx.cardTitlePropertyId); + if (!titleValue || titleValue instanceof import_obsidian.NullValue) { + titleEl.textContent = entry.file.basename; + return; + } + titleValue.renderTo(titleEl, ctx.app.renderContext); +} +function renderCardCover(coverEl, entry, filePath, ctx) { + if (!ctx.imagePropertyId) return false; + const value = entry.getValue(ctx.imagePropertyId); + if (!value || value instanceof import_obsidian.NullValue) return false; + const raw = value.toString().trim(); + if (!raw) return false; + if (/^https?:\/\//i.test(raw)) { + coverEl.createEl("img", { attr: { src: raw, alt: "" } }); + return true; + } + let linkText = raw.replace(/^!\s*/, ""); + const wikiMatch = linkText.match(/^\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]$/); + if (wikiMatch) linkText = wikiMatch[1]; + linkText = linkText.trim(); + if (!linkText) return false; + const app = ctx.app; + if (!app) return false; + const file = app.metadataCache.getFirstLinkpathDest(linkText, filePath); + if (!file) return false; + coverEl.createEl("img", { + attr: { src: app.vault.getResourcePath(file), alt: "" } + }); + return true; +} +function createCard(entry, ctx, cb) { + const cardEl = ctx.doc.createElement("div"); + cardEl.className = CSS_CLASSES.CARD; + const filePath = entry.file.path; + cardEl.setAttribute(DATA_ATTRIBUTES.ENTRY_PATH, filePath); + if (ctx.imagePropertyId) { + const coverEl = cardEl.createDiv({ cls: CSS_CLASSES.CARD_COVER }); + coverEl.classList.add( + ctx.imageFit === "contain" ? CSS_CLASSES.CARD_COVER_FIT_CONTAIN : CSS_CLASSES.CARD_COVER_FIT_COVER + ); + coverEl.style.aspectRatio = `1 / ${ctx.imageAspectRatio}`; + const rendered = renderCardCover(coverEl, entry, filePath, ctx); + if (!rendered) coverEl.remove(); + } + const titleEl = cardEl.createDiv({ cls: CSS_CLASSES.CARD_TITLE }); + renderCardTitle(titleEl, entry, ctx); + for (const propertyId of ctx.order) { + if (propertyId === ctx.groupByPropertyId) continue; + const value = entry.getValue(propertyId); + if (!value || value instanceof import_obsidian.NullValue) continue; + if (!value.toString().trim()) continue; + const label = ctx.getDisplayName(propertyId); + const propertyEl = cardEl.createDiv({ cls: CSS_CLASSES.CARD_PROPERTY }); + propertyEl.setAttribute("data-label", propertyId); + if (ctx.wrapValues) { + propertyEl.classList.add(CSS_CLASSES.CARD_PROPERTY_WRAP); + } + propertyEl.createSpan({ text: label, cls: CSS_CLASSES.CARD_PROPERTY_LABEL }); + const valueEl = propertyEl.createSpan({ cls: CSS_CLASSES.CARD_PROPERTY_VALUE }); + value.renderTo(valueEl, ctx.app.renderContext); + } + cardEl.addEventListener("mouseenter", () => cardEl.classList.add(CSS_CLASSES.CARD_HOVER)); + cardEl.addEventListener("mouseleave", () => cardEl.classList.remove(CSS_CLASSES.CARD_HOVER)); + cardEl.addEventListener("mouseover", (e) => { + if (e.target instanceof Element && e.target.closest("a")) return; + if (e.relatedTarget instanceof Element && cardEl.contains(e.relatedTarget)) return; + cb.onHoverPreview(filePath, "", e, cardEl); + }); + const clickHandler = (e) => { + if (e.target instanceof Element && e.target.closest("a")) return; + if (e.type === "auxclick" && e.button !== 1) return; + cb.onSetActiveCard(filePath); + if (!ctx.app?.workspace) return; + if (e.button === 1) { + cb.onOpenInBackgroundTab(entry.file); + return; + } + void ctx.app.workspace.openLinkText(filePath, "", import_obsidian.Keymap.isModEvent(e)); + }; + cardEl.addEventListener("click", clickHandler); + cardEl.addEventListener("auxclick", clickHandler); + cardEl.addEventListener("mousedown", (e) => { + if (e.button !== 1) return; + if (e.target instanceof Element && e.target.closest("a")) return; + e.preventDefault(); + }); + return cardEl; +} + +// src/components/quickAdd.ts +var import_obsidian3 = require("obsidian"); + +// src/quickAddModal.ts +var import_obsidian2 = require("obsidian"); +var QuickAddModal = class extends import_obsidian2.Modal { + constructor(app, options) { + super(app); + this.options = options; + this.input = null; + this.submitting = false; + } + onOpen() { + const { columnValue, swimlaneValue } = this.options; + this.setTitle(swimlaneValue ? `Add card to ${swimlaneValue} / ${columnValue}` : `Add card to ${columnValue}`); + const formEl = this.contentEl.createEl("form", { cls: CSS_CLASSES.QUICK_ADD_FORM }); + this.input = new import_obsidian2.TextComponent(formEl); + this.input.setPlaceholder("Card title"); + this.input.inputEl.classList.add(CSS_CLASSES.QUICK_ADD_INPUT); + const actionsEl = formEl.createDiv({ cls: CSS_CLASSES.QUICK_ADD_ACTIONS }); + const cancelBtn = actionsEl.createEl("button", { + text: "Cancel", + attr: { type: "button" } + }); + const submitBtn = actionsEl.createEl("button", { + text: "Add", + cls: "mod-cta", + attr: { type: "submit" } + }); + cancelBtn.addEventListener("click", () => this.close()); + formEl.addEventListener("submit", (evt) => { + evt.preventDefault(); + void this.submit(submitBtn); + }); + window.requestAnimationFrame(() => this.input?.inputEl.focus()); + } + onClose() { + this.contentEl.empty(); + this.input = null; + this.submitting = false; + } + async submit(submitBtn) { + if (this.submitting) return; + const title = this.input?.getValue().trim() ?? ""; + if (!title) { + this.input?.inputEl.focus(); + return; + } + this.submitting = true; + submitBtn.disabled = true; + try { + await this.options.onSubmit(title); + this.close(); + } catch (error) { + this.submitting = false; + submitBtn.disabled = false; + throw error; + } + } +}; + +// src/components/quickAdd.ts +var CREATED_CARD_TIMEOUT_MS = 2e3; +var CREATED_CARD_SETTLE_MS = 50; +function sanitizeBaseFileName(title) { + return title.trim().replace(/\.md$/i, "").replace(/[\\/:*?"<>|]/g, "-").replace(/\s+/g, " ").replace(/[.\s]+$/g, "").trim(); +} +function getWritableFrontmatterPropertyName(propertyId) { + if (!propertyId) return null; + const parsed = (0, import_obsidian3.parsePropertyId)(propertyId); + if (parsed.type !== "note") return null; + return parsed.name || null; +} +function getCreatedMarkdownFile(app, previousPaths, baseFileName) { + const createdFiles = app.vault.getMarkdownFiles().filter((file) => !previousPaths.has(file.path)); + if (createdFiles.length === 0) return null; + const preferredBasename = baseFileName.split("/").pop() ?? baseFileName; + return createdFiles.find((file) => file.basename === preferredBasename) ?? createdFiles[0] ?? null; +} +function getParentPath(path) { + const normalizedPath = (0, import_obsidian3.normalizePath)(path); + const separatorIndex = normalizedPath.lastIndexOf("/"); + return separatorIndex === -1 ? "" : normalizedPath.slice(0, separatorIndex); +} +function isFileInFolder(file, folder) { + return getParentPath(file.path) === (0, import_obsidian3.normalizePath)(folder); +} +function waitForCreatedMarkdownFile(app, previousPaths, baseFileName) { + if (typeof app.vault.on !== "function" || typeof app.vault.offref !== "function") { + return Promise.resolve(null); + } + return new Promise((resolve) => { + let eventRef = null; + let timeoutId = null; + let settled = false; + const cleanup = () => { + if (timeoutId !== null) window.clearTimeout(timeoutId); + if (eventRef) app.vault.offref(eventRef); + }; + const finish = (file) => { + if (settled) return; + settled = true; + cleanup(); + resolve(file); + }; + const finishIfCreated = () => { + const createdFile = getCreatedMarkdownFile(app, previousPaths, baseFileName); + if (createdFile) finish(createdFile); + }; + eventRef = app.vault.on("create", () => { + finishIfCreated(); + window.setTimeout(finishIfCreated, CREATED_CARD_SETTLE_MS); + }); + timeoutId = window.setTimeout(() => { + finish(getCreatedMarkdownFile(app, previousPaths, baseFileName)); + }, CREATED_CARD_TIMEOUT_MS); + }); +} +function getAvailablePath(app, folder, fileName) { + const extension = fileName.toLowerCase().endsWith(".md") ? ".md" : ""; + const basename = extension ? fileName.slice(0, -extension.length) : fileName; + let candidate = (0, import_obsidian3.normalizePath)(`${folder}/${extension ? fileName : `${fileName}.md`}`); + let counter = 1; + while (app.vault.getAbstractFileByPath(candidate)) { + candidate = (0, import_obsidian3.normalizePath)(`${folder}/${basename} ${counter}.md`); + counter++; + } + return candidate; +} +async function ensureCreatedCardInFolder(app, previousPaths, createdFilePromise, baseFileName, folder) { + const createdFile = getCreatedMarkdownFile(app, previousPaths, baseFileName) ?? await createdFilePromise; + if (!createdFile) { + new import_obsidian3.Notice(`Created card, but could not move it to ${folder}.`); + return; + } + if (isFileInFolder(createdFile, folder)) return; + const targetPath = getAvailablePath(app, folder, baseFileName); + if (targetPath === createdFile.path) return; + await app.fileManager.renameFile(createdFile, targetPath); +} +function closeNativeNewItemPopover(doc) { + const closePopovers = () => { + const popovers = Array.from(doc.querySelectorAll(".bases-new-item-popover")); + if (popovers.length === 0) return; + doc.body.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true })); + doc.body.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); + popovers.forEach((popover) => { + popover.remove(); + }); + }; + closePopovers(); + window.requestAnimationFrame(closePopovers); + for (const delay of [50, 250, 1e3]) { + window.setTimeout(closePopovers, delay); + } +} +async function createQuickAddCard(title, columnValue, swimlaneValue, ctx, cb) { + const baseFileName = sanitizeBaseFileName(title); + if (!baseFileName) { + new import_obsidian3.Notice("Enter a card title."); + return; + } + const columnPropertyName = getWritableFrontmatterPropertyName(ctx.prefsPropertyId); + if (!columnPropertyName) { + new import_obsidian3.Notice("Quick add needs a writable note property for columns."); + return; + } + const swimlanePropertyName = swimlaneValue ? getWritableFrontmatterPropertyName(ctx.prefsSwimlanePropertyId) : null; + if (swimlaneValue && !swimlanePropertyName) { + new import_obsidian3.Notice("Quick add needs a writable note property for swimlanes."); + return; + } + const targetFolder = ctx.quickAddFolder; + if (!targetFolder) { + new import_obsidian3.Notice("Quick add requires a folder to be configured."); + return; + } + if (!ctx.app?.vault.getFolderByPath(targetFolder)) { + new import_obsidian3.Notice(`Quick add folder not found: ${targetFolder}`); + return; + } + const fileNameToCreate = (0, import_obsidian3.normalizePath)(`${targetFolder}/${baseFileName}`); + const createdFilePaths = new Set(ctx.app.vault.getMarkdownFiles().map((file) => file.path)); + const createdFilePromise = waitForCreatedMarkdownFile(ctx.app, createdFilePaths, fileNameToCreate); + const setFrontmatter = (frontmatter) => { + if (columnValue === UNCATEGORIZED_LABEL) { + delete frontmatter[columnPropertyName]; + } else { + frontmatter[columnPropertyName] = columnValue; + } + if (!swimlaneValue || !swimlanePropertyName) return; + if (swimlaneValue === UNCATEGORIZED_LABEL) { + delete frontmatter[swimlanePropertyName]; + } else { + frontmatter[swimlanePropertyName] = swimlaneValue; + } + }; + try { + await cb.createFileForView(fileNameToCreate, setFrontmatter); + closeNativeNewItemPopover(ctx.doc); + await ensureCreatedCardInFolder(ctx.app, createdFilePaths, createdFilePromise, baseFileName, targetFolder); + } catch (error) { + console.error("Error creating kanban card:", error); + new import_obsidian3.Notice("Could not create card."); + } +} +function createAddButton(columnValue, swimlaneValue, ctx, cb) { + const btn = ctx.doc.createElement("div"); + btn.className = CSS_CLASSES.COLUMN_ADD_BTN; + btn.setAttribute( + "aria-label", + swimlaneValue ? `Add card to column: ${columnValue} in lane: ${swimlaneValue}` : `Add card to column: ${columnValue}` + ); + btn.setAttribute("role", "button"); + btn.setAttribute("tabindex", "0"); + (0, import_obsidian3.setIcon)(btn, "plus"); + const open = () => { + if (!ctx.app) return; + new QuickAddModal(ctx.app, { + columnValue, + swimlaneValue, + onSubmit: (title) => createQuickAddCard(title, columnValue, swimlaneValue, ctx, cb) + }).open(); + }; + btn.addEventListener("click", (e) => { + e.stopPropagation(); + open(); + }); + btn.addEventListener("keydown", (e) => { + if (e.key !== "Enter" && e.key !== " ") return; + e.preventDefault(); + e.stopPropagation(); + open(); + }); + return btn; +} + +// src/components/column.ts +function applyColumnColor(columnEl, colorName) { + if (!colorName) { + columnEl.style.removeProperty("--obk-column-accent-color"); + columnEl.removeAttribute(DATA_ATTRIBUTES.COLUMN_COLOR); + return; + } + const cssVar = COLOR_PALETTE.find((c) => c.name === colorName)?.cssVar ?? null; + if (!cssVar) { + columnEl.style.removeProperty("--obk-column-accent-color"); + columnEl.removeAttribute(DATA_ATTRIBUTES.COLUMN_COLOR); + return; + } + columnEl.style.setProperty("--obk-column-accent-color", cssVar); + columnEl.setAttribute(DATA_ATTRIBUTES.COLUMN_COLOR, colorName); +} +function createRemoveButton(doc, value, onRemove) { + const btn = doc.createElement("div"); + btn.className = CSS_CLASSES.COLUMN_REMOVE_BTN; + btn.setAttribute("aria-label", `Remove column: ${value}`); + btn.setAttribute("role", "button"); + btn.textContent = "\xD7"; + btn.addEventListener("click", (e) => { + e.stopPropagation(); + onRemove(); + }); + return btn; +} +function createColumn(value, entries, options, ctx, cb) { + const columnEl = ctx.doc.createElement("div"); + columnEl.className = CSS_CLASSES.COLUMN; + columnEl.setAttribute(DATA_ATTRIBUTES.COLUMN_VALUE, value); + const colorName = ctx.prefs.columnColors[value] ?? null; + cb.applyColumnColor(columnEl, colorName); + const headerEl = columnEl.createDiv({ cls: CSS_CLASSES.COLUMN_HEADER }); + const dragHandle = headerEl.createDiv({ cls: CSS_CLASSES.COLUMN_DRAG_HANDLE }); + dragHandle.textContent = "\u22EE\u22EE"; + const colorBtn = headerEl.createDiv({ cls: CSS_CLASSES.COLUMN_COLOR_BTN }); + colorBtn.setAttribute("aria-label", `Set color for column: ${value}`); + colorBtn.setAttribute("role", "button"); + colorBtn.addEventListener("click", (e) => { + e.stopPropagation(); + cb.onColorPickerClick(colorBtn, columnEl, value); + }); + headerEl.createSpan({ text: value, cls: CSS_CLASSES.COLUMN_TITLE }); + headerEl.createSpan({ text: `${entries.length}`, cls: CSS_CLASSES.COLUMN_COUNT }); + if (cb.getQuickAddFolder()) { + headerEl.appendChild(cb.createAddButton(value, options.swimlaneValue ?? null)); + } + if (entries.length === 0 && options.showRemoveButton !== false) { + headerEl.appendChild(createRemoveButton(ctx.doc, value, () => cb.onRemoveColumn(value, columnEl))); + } + const bodyEl = columnEl.createDiv({ cls: CSS_CLASSES.COLUMN_BODY }); + bodyEl.setAttribute(DATA_ATTRIBUTES.SORTABLE_CONTAINER, "true"); + entries.forEach((entry) => { + bodyEl.appendChild(createCard(entry, ctx.card, ctx.cardCb)); + }); + return columnEl; +} +function patchColumnCards(columnEl, newEntries, ctx, cb) { + const body = columnEl.querySelector(`.${CSS_CLASSES.COLUMN_BODY}`); + if (!body) return; + const countEl = columnEl.querySelector(`.${CSS_CLASSES.COLUMN_COUNT}`); + if (countEl) countEl.textContent = `${newEntries.length}`; + const headerEl = columnEl.querySelector(`.${CSS_CLASSES.COLUMN_HEADER}`); + const columnValue = columnEl.getAttribute(DATA_ATTRIBUTES.COLUMN_VALUE); + const existingRemoveBtn = headerEl?.querySelector(`.${CSS_CLASSES.COLUMN_REMOVE_BTN}`) ?? null; + const isInSwimlane = !!columnEl.closest(`.${CSS_CLASSES.SWIMLANE}`); + if (headerEl && newEntries.length === 0 && !existingRemoveBtn && columnValue && !isInSwimlane) { + headerEl.appendChild(createRemoveButton(ctx.doc, columnValue, () => cb.onRemoveColumn(columnValue, columnEl))); + } else if (newEntries.length > 0 && existingRemoveBtn) { + existingRemoveBtn.remove(); + } + const existingAddBtn = headerEl?.querySelector(`.${CSS_CLASSES.COLUMN_ADD_BTN}`) ?? null; + const hasFolder = !!cb.getQuickAddFolder(); + if (headerEl && columnValue && hasFolder && !existingAddBtn) { + const swimlaneEl = columnEl.closest(`[${DATA_ATTRIBUTES.SWIMLANE_VALUE}]`); + const swimlaneValue = swimlaneEl?.getAttribute(DATA_ATTRIBUTES.SWIMLANE_VALUE) ?? null; + headerEl.appendChild(cb.createAddButton(columnValue, swimlaneValue)); + } else if (!hasFolder && existingAddBtn) { + existingAddBtn.remove(); + } + const newPaths = new Set(newEntries.map((e) => e.file.path)); + body.querySelectorAll(`.${CSS_CLASSES.CARD}`).forEach((card) => { + const path = card.getAttribute(DATA_ATTRIBUTES.ENTRY_PATH); + if (path && !newPaths.has(path)) card.remove(); + }); + const existingCards = /* @__PURE__ */ new Map(); + body.querySelectorAll(`.${CSS_CLASSES.CARD}`).forEach((card) => { + const path = card.getAttribute(DATA_ATTRIBUTES.ENTRY_PATH); + if (path) existingCards.set(path, card); + }); + newEntries.forEach((entry) => { + const fp = computeCardFingerprint(entry, ctx.card); + const existing = existingCards.get(entry.file.path); + if (existing && ctx.cardFingerprints.get(entry.file.path) === fp) { + return; + } + const newCard = createCard(entry, ctx.card, ctx.cardCb); + ctx.cardFingerprints.set(entry.file.path, fp); + if (existing) { + body.replaceChild(newCard, existing); + } else { + body.appendChild(newCard); + } + }); + if (!ctx.dragging) { + const pathToCard = /* @__PURE__ */ new Map(); + body.querySelectorAll(`.${CSS_CLASSES.CARD}`).forEach((card) => { + const path = card.instanceOf(HTMLElement) ? card.getAttribute(DATA_ATTRIBUTES.ENTRY_PATH) : null; + if (path) pathToCard.set(path, card); + }); + newEntries.forEach((entry) => { + const card = pathToCard.get(entry.file.path); + if (card) body.appendChild(card); + }); + } +} + +// src/components/row.ts +var import_obsidian4 = require("obsidian"); +function updateSwimlaneToggle(toggleBtn, isCollapsed) { + const label = isCollapsed ? "Expand lane" : "Collapse lane"; + toggleBtn.empty(); + (0, import_obsidian4.setIcon)(toggleBtn, isCollapsed ? "chevron-right" : "chevron-down"); + toggleBtn.setAttribute("aria-label", label); + toggleBtn.setAttribute("title", label); + toggleBtn.setAttribute("aria-expanded", String(!isCollapsed)); +} +function sortSwimlaneValues(values) { + return [...values].sort((a, b) => { + if (a === UNCATEGORIZED_LABEL) return 1; + if (b === UNCATEGORIZED_LABEL) return -1; + return a.localeCompare(b); + }); +} +function getOrderedSwimlaneValues(liveValues, swimlaneOrder) { + if (!swimlaneOrder.length) { + return sortSwimlaneValues(liveValues); + } + const liveSet = new Set(liveValues); + const ordered = swimlaneOrder.filter((v) => liveSet.has(v)); + const orderedSet = new Set(ordered); + const newOnes = liveValues.filter((v) => !orderedSet.has(v)); + return [...ordered, ...newOnes]; +} +function buildSwimlaneElement(laneValue, laneEntries, orderedColumnValues, ctx, cb) { + const laneEl = ctx.doc.createElement("div"); + laneEl.className = CSS_CLASSES.SWIMLANE; + laneEl.setAttribute(DATA_ATTRIBUTES.SWIMLANE_VALUE, laneValue); + const isCollapsed = ctx.collapsedLanes.has(laneValue); + if (isCollapsed) laneEl.classList.add(CSS_CLASSES.SWIMLANE_COLLAPSED); + const headerEl = laneEl.createDiv({ cls: CSS_CLASSES.SWIMLANE_HEADER }); + const dragHandle = headerEl.createDiv({ cls: CSS_CLASSES.SWIMLANE_DRAG_HANDLE }); + dragHandle.textContent = "\u22EE\u22EE"; + dragHandle.setAttribute("aria-label", `Drag to reorder lane: ${laneValue}`); + headerEl.createSpan({ text: laneValue, cls: CSS_CLASSES.SWIMLANE_TITLE }); + const laneCount = orderedColumnValues.reduce((sum, col) => sum + (laneEntries.get(col)?.length ?? 0), 0); + headerEl.createSpan({ text: `${laneCount}`, cls: CSS_CLASSES.SWIMLANE_COUNT }); + const toggleBtn = headerEl.createEl("button", { + cls: CSS_CLASSES.SWIMLANE_TOGGLE, + attr: { type: "button" } + }); + updateSwimlaneToggle(toggleBtn, isCollapsed); + toggleBtn.addEventListener("click", (e) => { + e.stopPropagation(); + try { + cb.onToggleCollapsed(laneValue, laneEl, toggleBtn); + } catch (error) { + console.error("KanbanView: error toggling swimlane collapsed state", error); + } + }); + const bodyEl = laneEl.createDiv({ cls: CSS_CLASSES.SWIMLANE_BODY }); + orderedColumnValues.forEach((columnValue) => { + const columnEl = createColumn( + columnValue, + laneEntries.get(columnValue) ?? [], + { + showRemoveButton: false, + swimlaneValue: laneValue + }, + ctx, + cb + ); + bodyEl.appendChild(columnEl); + const cardBody = columnEl.querySelector( + `.${CSS_CLASSES.COLUMN_BODY}[${DATA_ATTRIBUTES.SORTABLE_CONTAINER}]` + ); + if (cardBody) cb.attachCardSortable(cardBody, cb.cardOrderKey(laneValue, columnValue)); + }); + return laneEl; +} + +// node_modules/sortablejs/modular/sortable.esm.js +function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object); + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object); + if (enumerableOnly) { + symbols = symbols.filter(function(sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable; + }); + } + keys.push.apply(keys, symbols); + } + return keys; +} +function _objectSpread2(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i] != null ? arguments[i] : {}; + if (i % 2) { + ownKeys(Object(source), true).forEach(function(key) { + _defineProperty(target, key, source[key]); + }); + } else if (Object.getOwnPropertyDescriptors) { + Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + } else { + ownKeys(Object(source)).forEach(function(key) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); + }); + } + } + return target; +} +function _typeof(obj) { + "@babel/helpers - typeof"; + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function(obj2) { + return typeof obj2; + }; + } else { + _typeof = function(obj2) { + return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2; + }; + } + return _typeof(obj); +} +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + return obj; +} +function _extends() { + _extends = Object.assign || function(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + return _extends.apply(this, arguments); +} +function _objectWithoutPropertiesLoose(source, excluded) { + if (source == null) return {}; + var target = {}; + var sourceKeys = Object.keys(source); + var key, i; + for (i = 0; i < sourceKeys.length; i++) { + key = sourceKeys[i]; + if (excluded.indexOf(key) >= 0) continue; + target[key] = source[key]; + } + return target; +} +function _objectWithoutProperties(source, excluded) { + if (source == null) return {}; + var target = _objectWithoutPropertiesLoose(source, excluded); + var key, i; + if (Object.getOwnPropertySymbols) { + var sourceSymbolKeys = Object.getOwnPropertySymbols(source); + for (i = 0; i < sourceSymbolKeys.length; i++) { + key = sourceSymbolKeys[i]; + if (excluded.indexOf(key) >= 0) continue; + if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; + target[key] = source[key]; + } + } + return target; +} +var version = "1.15.6"; +function userAgent(pattern) { + if (typeof window !== "undefined" && window.navigator) { + return !!/* @__PURE__ */ navigator.userAgent.match(pattern); + } +} +var IE11OrLess = userAgent(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i); +var Edge = userAgent(/Edge/i); +var FireFox = userAgent(/firefox/i); +var Safari = userAgent(/safari/i) && !userAgent(/chrome/i) && !userAgent(/android/i); +var IOS = userAgent(/iP(ad|od|hone)/i); +var ChromeForAndroid = userAgent(/chrome/i) && userAgent(/android/i); +var captureMode = { + capture: false, + passive: false +}; +function on(el, event, fn) { + el.addEventListener(event, fn, !IE11OrLess && captureMode); +} +function off(el, event, fn) { + el.removeEventListener(event, fn, !IE11OrLess && captureMode); +} +function matches(el, selector) { + if (!selector) return; + selector[0] === ">" && (selector = selector.substring(1)); + if (el) { + try { + if (el.matches) { + return el.matches(selector); + } else if (el.msMatchesSelector) { + return el.msMatchesSelector(selector); + } else if (el.webkitMatchesSelector) { + return el.webkitMatchesSelector(selector); + } + } catch (_) { + return false; + } + } + return false; +} +function getParentOrHost(el) { + return el.host && el !== document && el.host.nodeType ? el.host : el.parentNode; +} +function closest(el, selector, ctx, includeCTX) { + if (el) { + ctx = ctx || document; + do { + if (selector != null && (selector[0] === ">" ? el.parentNode === ctx && matches(el, selector) : matches(el, selector)) || includeCTX && el === ctx) { + return el; + } + if (el === ctx) break; + } while (el = getParentOrHost(el)); + } + return null; +} +var R_SPACE = /\s+/g; +function toggleClass(el, name, state) { + if (el && name) { + if (el.classList) { + el.classList[state ? "add" : "remove"](name); + } else { + var className = (" " + el.className + " ").replace(R_SPACE, " ").replace(" " + name + " ", " "); + el.className = (className + (state ? " " + name : "")).replace(R_SPACE, " "); + } + } +} +function css(el, prop, val) { + var style = el && el.style; + if (style) { + if (val === void 0) { + if (document.defaultView && document.defaultView.getComputedStyle) { + val = document.defaultView.getComputedStyle(el, ""); + } else if (el.currentStyle) { + val = el.currentStyle; + } + return prop === void 0 ? val : val[prop]; + } else { + if (!(prop in style) && prop.indexOf("webkit") === -1) { + prop = "-webkit-" + prop; + } + style[prop] = val + (typeof val === "string" ? "" : "px"); + } + } +} +function matrix(el, selfOnly) { + var appliedTransforms = ""; + if (typeof el === "string") { + appliedTransforms = el; + } else { + do { + var transform = css(el, "transform"); + if (transform && transform !== "none") { + appliedTransforms = transform + " " + appliedTransforms; + } + } while (!selfOnly && (el = el.parentNode)); + } + var matrixFn = window.DOMMatrix || window.WebKitCSSMatrix || window.CSSMatrix || window.MSCSSMatrix; + return matrixFn && new matrixFn(appliedTransforms); +} +function find(ctx, tagName, iterator) { + if (ctx) { + var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length; + if (iterator) { + for (; i < n; i++) { + iterator(list[i], i); + } + } + return list; + } + return []; +} +function getWindowScrollingElement() { + var scrollingElement = document.scrollingElement; + if (scrollingElement) { + return scrollingElement; + } else { + return document.documentElement; + } +} +function getRect(el, relativeToContainingBlock, relativeToNonStaticParent, undoScale, container) { + if (!el.getBoundingClientRect && el !== window) return; + var elRect, top, left, bottom, right, height, width; + if (el !== window && el.parentNode && el !== getWindowScrollingElement()) { + elRect = el.getBoundingClientRect(); + top = elRect.top; + left = elRect.left; + bottom = elRect.bottom; + right = elRect.right; + height = elRect.height; + width = elRect.width; + } else { + top = 0; + left = 0; + bottom = window.innerHeight; + right = window.innerWidth; + height = window.innerHeight; + width = window.innerWidth; + } + if ((relativeToContainingBlock || relativeToNonStaticParent) && el !== window) { + container = container || el.parentNode; + if (!IE11OrLess) { + do { + if (container && container.getBoundingClientRect && (css(container, "transform") !== "none" || relativeToNonStaticParent && css(container, "position") !== "static")) { + var containerRect = container.getBoundingClientRect(); + top -= containerRect.top + parseInt(css(container, "border-top-width")); + left -= containerRect.left + parseInt(css(container, "border-left-width")); + bottom = top + elRect.height; + right = left + elRect.width; + break; + } + } while (container = container.parentNode); + } + } + if (undoScale && el !== window) { + var elMatrix = matrix(container || el), scaleX = elMatrix && elMatrix.a, scaleY = elMatrix && elMatrix.d; + if (elMatrix) { + top /= scaleY; + left /= scaleX; + width /= scaleX; + height /= scaleY; + bottom = top + height; + right = left + width; + } + } + return { + top, + left, + bottom, + right, + width, + height + }; +} +function isScrolledPast(el, elSide, parentSide) { + var parent = getParentAutoScrollElement(el, true), elSideVal = getRect(el)[elSide]; + while (parent) { + var parentSideVal = getRect(parent)[parentSide], visible = void 0; + if (parentSide === "top" || parentSide === "left") { + visible = elSideVal >= parentSideVal; + } else { + visible = elSideVal <= parentSideVal; + } + if (!visible) return parent; + if (parent === getWindowScrollingElement()) break; + parent = getParentAutoScrollElement(parent, false); + } + return false; +} +function getChild(el, childNum, options, includeDragEl) { + var currentChild = 0, i = 0, children = el.children; + while (i < children.length) { + if (children[i].style.display !== "none" && children[i] !== Sortable.ghost && (includeDragEl || children[i] !== Sortable.dragged) && closest(children[i], options.draggable, el, false)) { + if (currentChild === childNum) { + return children[i]; + } + currentChild++; + } + i++; + } + return null; +} +function lastChild(el, selector) { + var last = el.lastElementChild; + while (last && (last === Sortable.ghost || css(last, "display") === "none" || selector && !matches(last, selector))) { + last = last.previousElementSibling; + } + return last || null; +} +function index(el, selector) { + var index2 = 0; + if (!el || !el.parentNode) { + return -1; + } + while (el = el.previousElementSibling) { + if (el.nodeName.toUpperCase() !== "TEMPLATE" && el !== Sortable.clone && (!selector || matches(el, selector))) { + index2++; + } + } + return index2; +} +function getRelativeScrollOffset(el) { + var offsetLeft = 0, offsetTop = 0, winScroller = getWindowScrollingElement(); + if (el) { + do { + var elMatrix = matrix(el), scaleX = elMatrix.a, scaleY = elMatrix.d; + offsetLeft += el.scrollLeft * scaleX; + offsetTop += el.scrollTop * scaleY; + } while (el !== winScroller && (el = el.parentNode)); + } + return [offsetLeft, offsetTop]; +} +function indexOfObject(arr, obj) { + for (var i in arr) { + if (!arr.hasOwnProperty(i)) continue; + for (var key in obj) { + if (obj.hasOwnProperty(key) && obj[key] === arr[i][key]) return Number(i); + } + } + return -1; +} +function getParentAutoScrollElement(el, includeSelf) { + if (!el || !el.getBoundingClientRect) return getWindowScrollingElement(); + var elem = el; + var gotSelf = false; + do { + if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) { + var elemCSS = css(elem); + if (elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == "auto" || elemCSS.overflowX == "scroll") || elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == "auto" || elemCSS.overflowY == "scroll")) { + if (!elem.getBoundingClientRect || elem === document.body) return getWindowScrollingElement(); + if (gotSelf || includeSelf) return elem; + gotSelf = true; + } + } + } while (elem = elem.parentNode); + return getWindowScrollingElement(); +} +function extend(dst, src) { + if (dst && src) { + for (var key in src) { + if (src.hasOwnProperty(key)) { + dst[key] = src[key]; + } + } + } + return dst; +} +function isRectEqual(rect1, rect2) { + return Math.round(rect1.top) === Math.round(rect2.top) && Math.round(rect1.left) === Math.round(rect2.left) && Math.round(rect1.height) === Math.round(rect2.height) && Math.round(rect1.width) === Math.round(rect2.width); +} +var _throttleTimeout; +function throttle(callback, ms) { + return function() { + if (!_throttleTimeout) { + var args = arguments, _this = this; + if (args.length === 1) { + callback.call(_this, args[0]); + } else { + callback.apply(_this, args); + } + _throttleTimeout = setTimeout(function() { + _throttleTimeout = void 0; + }, ms); + } + }; +} +function cancelThrottle() { + clearTimeout(_throttleTimeout); + _throttleTimeout = void 0; +} +function scrollBy(el, x, y) { + el.scrollLeft += x; + el.scrollTop += y; +} +function clone(el) { + var Polymer = window.Polymer; + var $ = window.jQuery || window.Zepto; + if (Polymer && Polymer.dom) { + return Polymer.dom(el).cloneNode(true); + } else if ($) { + return $(el).clone(true)[0]; + } else { + return el.cloneNode(true); + } +} +function getChildContainingRectFromElement(container, options, ghostEl2) { + var rect = {}; + Array.from(container.children).forEach(function(child) { + var _rect$left, _rect$top, _rect$right, _rect$bottom; + if (!closest(child, options.draggable, container, false) || child.animated || child === ghostEl2) return; + var childRect = getRect(child); + rect.left = Math.min((_rect$left = rect.left) !== null && _rect$left !== void 0 ? _rect$left : Infinity, childRect.left); + rect.top = Math.min((_rect$top = rect.top) !== null && _rect$top !== void 0 ? _rect$top : Infinity, childRect.top); + rect.right = Math.max((_rect$right = rect.right) !== null && _rect$right !== void 0 ? _rect$right : -Infinity, childRect.right); + rect.bottom = Math.max((_rect$bottom = rect.bottom) !== null && _rect$bottom !== void 0 ? _rect$bottom : -Infinity, childRect.bottom); + }); + rect.width = rect.right - rect.left; + rect.height = rect.bottom - rect.top; + rect.x = rect.left; + rect.y = rect.top; + return rect; +} +var expando = "Sortable" + (/* @__PURE__ */ new Date()).getTime(); +function AnimationStateManager() { + var animationStates = [], animationCallbackId; + return { + captureAnimationState: function captureAnimationState() { + animationStates = []; + if (!this.options.animation) return; + var children = [].slice.call(this.el.children); + children.forEach(function(child) { + if (css(child, "display") === "none" || child === Sortable.ghost) return; + animationStates.push({ + target: child, + rect: getRect(child) + }); + var fromRect = _objectSpread2({}, animationStates[animationStates.length - 1].rect); + if (child.thisAnimationDuration) { + var childMatrix = matrix(child, true); + if (childMatrix) { + fromRect.top -= childMatrix.f; + fromRect.left -= childMatrix.e; + } + } + child.fromRect = fromRect; + }); + }, + addAnimationState: function addAnimationState(state) { + animationStates.push(state); + }, + removeAnimationState: function removeAnimationState(target) { + animationStates.splice(indexOfObject(animationStates, { + target + }), 1); + }, + animateAll: function animateAll(callback) { + var _this = this; + if (!this.options.animation) { + clearTimeout(animationCallbackId); + if (typeof callback === "function") callback(); + return; + } + var animating = false, animationTime = 0; + animationStates.forEach(function(state) { + var time = 0, target = state.target, fromRect = target.fromRect, toRect = getRect(target), prevFromRect = target.prevFromRect, prevToRect = target.prevToRect, animatingRect = state.rect, targetMatrix = matrix(target, true); + if (targetMatrix) { + toRect.top -= targetMatrix.f; + toRect.left -= targetMatrix.e; + } + target.toRect = toRect; + if (target.thisAnimationDuration) { + if (isRectEqual(prevFromRect, toRect) && !isRectEqual(fromRect, toRect) && // Make sure animatingRect is on line between toRect & fromRect + (animatingRect.top - toRect.top) / (animatingRect.left - toRect.left) === (fromRect.top - toRect.top) / (fromRect.left - toRect.left)) { + time = calculateRealTime(animatingRect, prevFromRect, prevToRect, _this.options); + } + } + if (!isRectEqual(toRect, fromRect)) { + target.prevFromRect = fromRect; + target.prevToRect = toRect; + if (!time) { + time = _this.options.animation; + } + _this.animate(target, animatingRect, toRect, time); + } + if (time) { + animating = true; + animationTime = Math.max(animationTime, time); + clearTimeout(target.animationResetTimer); + target.animationResetTimer = setTimeout(function() { + target.animationTime = 0; + target.prevFromRect = null; + target.fromRect = null; + target.prevToRect = null; + target.thisAnimationDuration = null; + }, time); + target.thisAnimationDuration = time; + } + }); + clearTimeout(animationCallbackId); + if (!animating) { + if (typeof callback === "function") callback(); + } else { + animationCallbackId = setTimeout(function() { + if (typeof callback === "function") callback(); + }, animationTime); + } + animationStates = []; + }, + animate: function animate(target, currentRect, toRect, duration) { + if (duration) { + css(target, "transition", ""); + css(target, "transform", ""); + var elMatrix = matrix(this.el), scaleX = elMatrix && elMatrix.a, scaleY = elMatrix && elMatrix.d, translateX = (currentRect.left - toRect.left) / (scaleX || 1), translateY = (currentRect.top - toRect.top) / (scaleY || 1); + target.animatingX = !!translateX; + target.animatingY = !!translateY; + css(target, "transform", "translate3d(" + translateX + "px," + translateY + "px,0)"); + this.forRepaintDummy = repaint(target); + css(target, "transition", "transform " + duration + "ms" + (this.options.easing ? " " + this.options.easing : "")); + css(target, "transform", "translate3d(0,0,0)"); + typeof target.animated === "number" && clearTimeout(target.animated); + target.animated = setTimeout(function() { + css(target, "transition", ""); + css(target, "transform", ""); + target.animated = false; + target.animatingX = false; + target.animatingY = false; + }, duration); + } + } + }; +} +function repaint(target) { + return target.offsetWidth; +} +function calculateRealTime(animatingRect, fromRect, toRect, options) { + return Math.sqrt(Math.pow(fromRect.top - animatingRect.top, 2) + Math.pow(fromRect.left - animatingRect.left, 2)) / Math.sqrt(Math.pow(fromRect.top - toRect.top, 2) + Math.pow(fromRect.left - toRect.left, 2)) * options.animation; +} +var plugins = []; +var defaults = { + initializeByDefault: true +}; +var PluginManager = { + mount: function mount(plugin) { + for (var option2 in defaults) { + if (defaults.hasOwnProperty(option2) && !(option2 in plugin)) { + plugin[option2] = defaults[option2]; + } + } + plugins.forEach(function(p) { + if (p.pluginName === plugin.pluginName) { + throw "Sortable: Cannot mount plugin ".concat(plugin.pluginName, " more than once"); + } + }); + plugins.push(plugin); + }, + pluginEvent: function pluginEvent(eventName, sortable, evt) { + var _this = this; + this.eventCanceled = false; + evt.cancel = function() { + _this.eventCanceled = true; + }; + var eventNameGlobal = eventName + "Global"; + plugins.forEach(function(plugin) { + if (!sortable[plugin.pluginName]) return; + if (sortable[plugin.pluginName][eventNameGlobal]) { + sortable[plugin.pluginName][eventNameGlobal](_objectSpread2({ + sortable + }, evt)); + } + if (sortable.options[plugin.pluginName] && sortable[plugin.pluginName][eventName]) { + sortable[plugin.pluginName][eventName](_objectSpread2({ + sortable + }, evt)); + } + }); + }, + initializePlugins: function initializePlugins(sortable, el, defaults2, options) { + plugins.forEach(function(plugin) { + var pluginName = plugin.pluginName; + if (!sortable.options[pluginName] && !plugin.initializeByDefault) return; + var initialized = new plugin(sortable, el, sortable.options); + initialized.sortable = sortable; + initialized.options = sortable.options; + sortable[pluginName] = initialized; + _extends(defaults2, initialized.defaults); + }); + for (var option2 in sortable.options) { + if (!sortable.options.hasOwnProperty(option2)) continue; + var modified = this.modifyOption(sortable, option2, sortable.options[option2]); + if (typeof modified !== "undefined") { + sortable.options[option2] = modified; + } + } + }, + getEventProperties: function getEventProperties(name, sortable) { + var eventProperties = {}; + plugins.forEach(function(plugin) { + if (typeof plugin.eventProperties !== "function") return; + _extends(eventProperties, plugin.eventProperties.call(sortable[plugin.pluginName], name)); + }); + return eventProperties; + }, + modifyOption: function modifyOption(sortable, name, value) { + var modifiedValue; + plugins.forEach(function(plugin) { + if (!sortable[plugin.pluginName]) return; + if (plugin.optionListeners && typeof plugin.optionListeners[name] === "function") { + modifiedValue = plugin.optionListeners[name].call(sortable[plugin.pluginName], value); + } + }); + return modifiedValue; + } +}; +function dispatchEvent(_ref) { + var sortable = _ref.sortable, rootEl2 = _ref.rootEl, name = _ref.name, targetEl = _ref.targetEl, cloneEl2 = _ref.cloneEl, toEl = _ref.toEl, fromEl = _ref.fromEl, oldIndex2 = _ref.oldIndex, newIndex2 = _ref.newIndex, oldDraggableIndex2 = _ref.oldDraggableIndex, newDraggableIndex2 = _ref.newDraggableIndex, originalEvent = _ref.originalEvent, putSortable2 = _ref.putSortable, extraEventProperties = _ref.extraEventProperties; + sortable = sortable || rootEl2 && rootEl2[expando]; + if (!sortable) return; + var evt, options = sortable.options, onName = "on" + name.charAt(0).toUpperCase() + name.substr(1); + if (window.CustomEvent && !IE11OrLess && !Edge) { + evt = new CustomEvent(name, { + bubbles: true, + cancelable: true + }); + } else { + evt = document.createEvent("Event"); + evt.initEvent(name, true, true); + } + evt.to = toEl || rootEl2; + evt.from = fromEl || rootEl2; + evt.item = targetEl || rootEl2; + evt.clone = cloneEl2; + evt.oldIndex = oldIndex2; + evt.newIndex = newIndex2; + evt.oldDraggableIndex = oldDraggableIndex2; + evt.newDraggableIndex = newDraggableIndex2; + evt.originalEvent = originalEvent; + evt.pullMode = putSortable2 ? putSortable2.lastPutMode : void 0; + var allEventProperties = _objectSpread2(_objectSpread2({}, extraEventProperties), PluginManager.getEventProperties(name, sortable)); + for (var option2 in allEventProperties) { + evt[option2] = allEventProperties[option2]; + } + if (rootEl2) { + rootEl2.dispatchEvent(evt); + } + if (options[onName]) { + options[onName].call(sortable, evt); + } +} +var _excluded = ["evt"]; +var pluginEvent2 = function pluginEvent3(eventName, sortable) { + var _ref = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : {}, originalEvent = _ref.evt, data = _objectWithoutProperties(_ref, _excluded); + PluginManager.pluginEvent.bind(Sortable)(eventName, sortable, _objectSpread2({ + dragEl, + parentEl, + ghostEl, + rootEl, + nextEl, + lastDownEl, + cloneEl, + cloneHidden, + dragStarted: moved, + putSortable, + activeSortable: Sortable.active, + originalEvent, + oldIndex, + oldDraggableIndex, + newIndex, + newDraggableIndex, + hideGhostForTarget: _hideGhostForTarget, + unhideGhostForTarget: _unhideGhostForTarget, + cloneNowHidden: function cloneNowHidden() { + cloneHidden = true; + }, + cloneNowShown: function cloneNowShown() { + cloneHidden = false; + }, + dispatchSortableEvent: function dispatchSortableEvent(name) { + _dispatchEvent({ + sortable, + name, + originalEvent + }); + } + }, data)); +}; +function _dispatchEvent(info) { + dispatchEvent(_objectSpread2({ + putSortable, + cloneEl, + targetEl: dragEl, + rootEl, + oldIndex, + oldDraggableIndex, + newIndex, + newDraggableIndex + }, info)); +} +var dragEl; +var parentEl; +var ghostEl; +var rootEl; +var nextEl; +var lastDownEl; +var cloneEl; +var cloneHidden; +var oldIndex; +var newIndex; +var oldDraggableIndex; +var newDraggableIndex; +var activeGroup; +var putSortable; +var awaitingDragStarted = false; +var ignoreNextClick = false; +var sortables = []; +var tapEvt; +var touchEvt; +var lastDx; +var lastDy; +var tapDistanceLeft; +var tapDistanceTop; +var moved; +var lastTarget; +var lastDirection; +var pastFirstInvertThresh = false; +var isCircumstantialInvert = false; +var targetMoveDistance; +var ghostRelativeParent; +var ghostRelativeParentInitialScroll = []; +var _silent = false; +var savedInputChecked = []; +var documentExists = typeof document !== "undefined"; +var PositionGhostAbsolutely = IOS; +var CSSFloatProperty = Edge || IE11OrLess ? "cssFloat" : "float"; +var supportDraggable = documentExists && !ChromeForAndroid && !IOS && "draggable" in document.createElement("div"); +var supportCssPointerEvents = (function() { + if (!documentExists) return; + if (IE11OrLess) { + return false; + } + var el = document.createElement("x"); + el.style.cssText = "pointer-events:auto"; + return el.style.pointerEvents === "auto"; +})(); +var _detectDirection = function _detectDirection2(el, options) { + var elCSS = css(el), elWidth = parseInt(elCSS.width) - parseInt(elCSS.paddingLeft) - parseInt(elCSS.paddingRight) - parseInt(elCSS.borderLeftWidth) - parseInt(elCSS.borderRightWidth), child1 = getChild(el, 0, options), child2 = getChild(el, 1, options), firstChildCSS = child1 && css(child1), secondChildCSS = child2 && css(child2), firstChildWidth = firstChildCSS && parseInt(firstChildCSS.marginLeft) + parseInt(firstChildCSS.marginRight) + getRect(child1).width, secondChildWidth = secondChildCSS && parseInt(secondChildCSS.marginLeft) + parseInt(secondChildCSS.marginRight) + getRect(child2).width; + if (elCSS.display === "flex") { + return elCSS.flexDirection === "column" || elCSS.flexDirection === "column-reverse" ? "vertical" : "horizontal"; + } + if (elCSS.display === "grid") { + return elCSS.gridTemplateColumns.split(" ").length <= 1 ? "vertical" : "horizontal"; + } + if (child1 && firstChildCSS["float"] && firstChildCSS["float"] !== "none") { + var touchingSideChild2 = firstChildCSS["float"] === "left" ? "left" : "right"; + return child2 && (secondChildCSS.clear === "both" || secondChildCSS.clear === touchingSideChild2) ? "vertical" : "horizontal"; + } + return child1 && (firstChildCSS.display === "block" || firstChildCSS.display === "flex" || firstChildCSS.display === "table" || firstChildCSS.display === "grid" || firstChildWidth >= elWidth && elCSS[CSSFloatProperty] === "none" || child2 && elCSS[CSSFloatProperty] === "none" && firstChildWidth + secondChildWidth > elWidth) ? "vertical" : "horizontal"; +}; +var _dragElInRowColumn = function _dragElInRowColumn2(dragRect, targetRect, vertical) { + var dragElS1Opp = vertical ? dragRect.left : dragRect.top, dragElS2Opp = vertical ? dragRect.right : dragRect.bottom, dragElOppLength = vertical ? dragRect.width : dragRect.height, targetS1Opp = vertical ? targetRect.left : targetRect.top, targetS2Opp = vertical ? targetRect.right : targetRect.bottom, targetOppLength = vertical ? targetRect.width : targetRect.height; + return dragElS1Opp === targetS1Opp || dragElS2Opp === targetS2Opp || dragElS1Opp + dragElOppLength / 2 === targetS1Opp + targetOppLength / 2; +}; +var _detectNearestEmptySortable = function _detectNearestEmptySortable2(x, y) { + var ret; + sortables.some(function(sortable) { + var threshold = sortable[expando].options.emptyInsertThreshold; + if (!threshold || lastChild(sortable)) return; + var rect = getRect(sortable), insideHorizontally = x >= rect.left - threshold && x <= rect.right + threshold, insideVertically = y >= rect.top - threshold && y <= rect.bottom + threshold; + if (insideHorizontally && insideVertically) { + return ret = sortable; + } + }); + return ret; +}; +var _prepareGroup = function _prepareGroup2(options) { + function toFn(value, pull) { + return function(to, from, dragEl2, evt) { + var sameGroup = to.options.group.name && from.options.group.name && to.options.group.name === from.options.group.name; + if (value == null && (pull || sameGroup)) { + return true; + } else if (value == null || value === false) { + return false; + } else if (pull && value === "clone") { + return value; + } else if (typeof value === "function") { + return toFn(value(to, from, dragEl2, evt), pull)(to, from, dragEl2, evt); + } else { + var otherGroup = (pull ? to : from).options.group.name; + return value === true || typeof value === "string" && value === otherGroup || value.join && value.indexOf(otherGroup) > -1; + } + }; + } + var group = {}; + var originalGroup = options.group; + if (!originalGroup || _typeof(originalGroup) != "object") { + originalGroup = { + name: originalGroup + }; + } + group.name = originalGroup.name; + group.checkPull = toFn(originalGroup.pull, true); + group.checkPut = toFn(originalGroup.put); + group.revertClone = originalGroup.revertClone; + options.group = group; +}; +var _hideGhostForTarget = function _hideGhostForTarget2() { + if (!supportCssPointerEvents && ghostEl) { + css(ghostEl, "display", "none"); + } +}; +var _unhideGhostForTarget = function _unhideGhostForTarget2() { + if (!supportCssPointerEvents && ghostEl) { + css(ghostEl, "display", ""); + } +}; +if (documentExists && !ChromeForAndroid) { + document.addEventListener("click", function(evt) { + if (ignoreNextClick) { + evt.preventDefault(); + evt.stopPropagation && evt.stopPropagation(); + evt.stopImmediatePropagation && evt.stopImmediatePropagation(); + ignoreNextClick = false; + return false; + } + }, true); +} +var nearestEmptyInsertDetectEvent = function nearestEmptyInsertDetectEvent2(evt) { + if (dragEl) { + evt = evt.touches ? evt.touches[0] : evt; + var nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY); + if (nearest) { + var event = {}; + for (var i in evt) { + if (evt.hasOwnProperty(i)) { + event[i] = evt[i]; + } + } + event.target = event.rootEl = nearest; + event.preventDefault = void 0; + event.stopPropagation = void 0; + nearest[expando]._onDragOver(event); + } + } +}; +var _checkOutsideTargetEl = function _checkOutsideTargetEl2(evt) { + if (dragEl) { + dragEl.parentNode[expando]._isOutsideThisEl(evt.target); + } +}; +function Sortable(el, options) { + if (!(el && el.nodeType && el.nodeType === 1)) { + throw "Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(el)); + } + this.el = el; + this.options = options = _extends({}, options); + el[expando] = this; + var defaults2 = { + group: null, + sort: true, + disabled: false, + store: null, + handle: null, + draggable: /^[uo]l$/i.test(el.nodeName) ? ">li" : ">*", + swapThreshold: 1, + // percentage; 0 <= x <= 1 + invertSwap: false, + // invert always + invertedSwapThreshold: null, + // will be set to same as swapThreshold if default + removeCloneOnHide: true, + direction: function direction() { + return _detectDirection(el, this.options); + }, + ghostClass: "sortable-ghost", + chosenClass: "sortable-chosen", + dragClass: "sortable-drag", + ignore: "a, img", + filter: null, + preventOnFilter: true, + animation: 0, + easing: null, + setData: function setData(dataTransfer, dragEl2) { + dataTransfer.setData("Text", dragEl2.textContent); + }, + dropBubble: false, + dragoverBubble: false, + dataIdAttr: "data-id", + delay: 0, + delayOnTouchOnly: false, + touchStartThreshold: (Number.parseInt ? Number : window).parseInt(window.devicePixelRatio, 10) || 1, + forceFallback: false, + fallbackClass: "sortable-fallback", + fallbackOnBody: false, + fallbackTolerance: 0, + fallbackOffset: { + x: 0, + y: 0 + }, + // Disabled on Safari: #1571; Enabled on Safari IOS: #2244 + supportPointer: Sortable.supportPointer !== false && "PointerEvent" in window && (!Safari || IOS), + emptyInsertThreshold: 5 + }; + PluginManager.initializePlugins(this, el, defaults2); + for (var name in defaults2) { + !(name in options) && (options[name] = defaults2[name]); + } + _prepareGroup(options); + for (var fn in this) { + if (fn.charAt(0) === "_" && typeof this[fn] === "function") { + this[fn] = this[fn].bind(this); + } + } + this.nativeDraggable = options.forceFallback ? false : supportDraggable; + if (this.nativeDraggable) { + this.options.touchStartThreshold = 1; + } + if (options.supportPointer) { + on(el, "pointerdown", this._onTapStart); + } else { + on(el, "mousedown", this._onTapStart); + on(el, "touchstart", this._onTapStart); + } + if (this.nativeDraggable) { + on(el, "dragover", this); + on(el, "dragenter", this); + } + sortables.push(this.el); + options.store && options.store.get && this.sort(options.store.get(this) || []); + _extends(this, AnimationStateManager()); +} +Sortable.prototype = /** @lends Sortable.prototype */ +{ + constructor: Sortable, + _isOutsideThisEl: function _isOutsideThisEl(target) { + if (!this.el.contains(target) && target !== this.el) { + lastTarget = null; + } + }, + _getDirection: function _getDirection(evt, target) { + return typeof this.options.direction === "function" ? this.options.direction.call(this, evt, target, dragEl) : this.options.direction; + }, + _onTapStart: function _onTapStart(evt) { + if (!evt.cancelable) return; + var _this = this, el = this.el, options = this.options, preventOnFilter = options.preventOnFilter, type = evt.type, touch = evt.touches && evt.touches[0] || evt.pointerType && evt.pointerType === "touch" && evt, target = (touch || evt).target, originalTarget = evt.target.shadowRoot && (evt.path && evt.path[0] || evt.composedPath && evt.composedPath()[0]) || target, filter = options.filter; + _saveInputCheckedState(el); + if (dragEl) { + return; + } + if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) { + return; + } + if (originalTarget.isContentEditable) { + return; + } + if (!this.nativeDraggable && Safari && target && target.tagName.toUpperCase() === "SELECT") { + return; + } + target = closest(target, options.draggable, el, false); + if (target && target.animated) { + return; + } + if (lastDownEl === target) { + return; + } + oldIndex = index(target); + oldDraggableIndex = index(target, options.draggable); + if (typeof filter === "function") { + if (filter.call(this, evt, target, this)) { + _dispatchEvent({ + sortable: _this, + rootEl: originalTarget, + name: "filter", + targetEl: target, + toEl: el, + fromEl: el + }); + pluginEvent2("filter", _this, { + evt + }); + preventOnFilter && evt.preventDefault(); + return; + } + } else if (filter) { + filter = filter.split(",").some(function(criteria) { + criteria = closest(originalTarget, criteria.trim(), el, false); + if (criteria) { + _dispatchEvent({ + sortable: _this, + rootEl: criteria, + name: "filter", + targetEl: target, + fromEl: el, + toEl: el + }); + pluginEvent2("filter", _this, { + evt + }); + return true; + } + }); + if (filter) { + preventOnFilter && evt.preventDefault(); + return; + } + } + if (options.handle && !closest(originalTarget, options.handle, el, false)) { + return; + } + this._prepareDragStart(evt, touch, target); + }, + _prepareDragStart: function _prepareDragStart(evt, touch, target) { + var _this = this, el = _this.el, options = _this.options, ownerDocument = el.ownerDocument, dragStartFn; + if (target && !dragEl && target.parentNode === el) { + var dragRect = getRect(target); + rootEl = el; + dragEl = target; + parentEl = dragEl.parentNode; + nextEl = dragEl.nextSibling; + lastDownEl = target; + activeGroup = options.group; + Sortable.dragged = dragEl; + tapEvt = { + target: dragEl, + clientX: (touch || evt).clientX, + clientY: (touch || evt).clientY + }; + tapDistanceLeft = tapEvt.clientX - dragRect.left; + tapDistanceTop = tapEvt.clientY - dragRect.top; + this._lastX = (touch || evt).clientX; + this._lastY = (touch || evt).clientY; + dragEl.style["will-change"] = "all"; + dragStartFn = function dragStartFn2() { + pluginEvent2("delayEnded", _this, { + evt + }); + if (Sortable.eventCanceled) { + _this._onDrop(); + return; + } + _this._disableDelayedDragEvents(); + if (!FireFox && _this.nativeDraggable) { + dragEl.draggable = true; + } + _this._triggerDragStart(evt, touch); + _dispatchEvent({ + sortable: _this, + name: "choose", + originalEvent: evt + }); + toggleClass(dragEl, options.chosenClass, true); + }; + options.ignore.split(",").forEach(function(criteria) { + find(dragEl, criteria.trim(), _disableDraggable); + }); + on(ownerDocument, "dragover", nearestEmptyInsertDetectEvent); + on(ownerDocument, "mousemove", nearestEmptyInsertDetectEvent); + on(ownerDocument, "touchmove", nearestEmptyInsertDetectEvent); + if (options.supportPointer) { + on(ownerDocument, "pointerup", _this._onDrop); + !this.nativeDraggable && on(ownerDocument, "pointercancel", _this._onDrop); + } else { + on(ownerDocument, "mouseup", _this._onDrop); + on(ownerDocument, "touchend", _this._onDrop); + on(ownerDocument, "touchcancel", _this._onDrop); + } + if (FireFox && this.nativeDraggable) { + this.options.touchStartThreshold = 4; + dragEl.draggable = true; + } + pluginEvent2("delayStart", this, { + evt + }); + if (options.delay && (!options.delayOnTouchOnly || touch) && (!this.nativeDraggable || !(Edge || IE11OrLess))) { + if (Sortable.eventCanceled) { + this._onDrop(); + return; + } + if (options.supportPointer) { + on(ownerDocument, "pointerup", _this._disableDelayedDrag); + on(ownerDocument, "pointercancel", _this._disableDelayedDrag); + } else { + on(ownerDocument, "mouseup", _this._disableDelayedDrag); + on(ownerDocument, "touchend", _this._disableDelayedDrag); + on(ownerDocument, "touchcancel", _this._disableDelayedDrag); + } + on(ownerDocument, "mousemove", _this._delayedDragTouchMoveHandler); + on(ownerDocument, "touchmove", _this._delayedDragTouchMoveHandler); + options.supportPointer && on(ownerDocument, "pointermove", _this._delayedDragTouchMoveHandler); + _this._dragStartTimer = setTimeout(dragStartFn, options.delay); + } else { + dragStartFn(); + } + } + }, + _delayedDragTouchMoveHandler: function _delayedDragTouchMoveHandler(e) { + var touch = e.touches ? e.touches[0] : e; + if (Math.max(Math.abs(touch.clientX - this._lastX), Math.abs(touch.clientY - this._lastY)) >= Math.floor(this.options.touchStartThreshold / (this.nativeDraggable && window.devicePixelRatio || 1))) { + this._disableDelayedDrag(); + } + }, + _disableDelayedDrag: function _disableDelayedDrag() { + dragEl && _disableDraggable(dragEl); + clearTimeout(this._dragStartTimer); + this._disableDelayedDragEvents(); + }, + _disableDelayedDragEvents: function _disableDelayedDragEvents() { + var ownerDocument = this.el.ownerDocument; + off(ownerDocument, "mouseup", this._disableDelayedDrag); + off(ownerDocument, "touchend", this._disableDelayedDrag); + off(ownerDocument, "touchcancel", this._disableDelayedDrag); + off(ownerDocument, "pointerup", this._disableDelayedDrag); + off(ownerDocument, "pointercancel", this._disableDelayedDrag); + off(ownerDocument, "mousemove", this._delayedDragTouchMoveHandler); + off(ownerDocument, "touchmove", this._delayedDragTouchMoveHandler); + off(ownerDocument, "pointermove", this._delayedDragTouchMoveHandler); + }, + _triggerDragStart: function _triggerDragStart(evt, touch) { + touch = touch || evt.pointerType == "touch" && evt; + if (!this.nativeDraggable || touch) { + if (this.options.supportPointer) { + on(document, "pointermove", this._onTouchMove); + } else if (touch) { + on(document, "touchmove", this._onTouchMove); + } else { + on(document, "mousemove", this._onTouchMove); + } + } else { + on(dragEl, "dragend", this); + on(rootEl, "dragstart", this._onDragStart); + } + try { + if (document.selection) { + _nextTick(function() { + document.selection.empty(); + }); + } else { + window.getSelection().removeAllRanges(); + } + } catch (err) { + } + }, + _dragStarted: function _dragStarted(fallback, evt) { + awaitingDragStarted = false; + if (rootEl && dragEl) { + pluginEvent2("dragStarted", this, { + evt + }); + if (this.nativeDraggable) { + on(document, "dragover", _checkOutsideTargetEl); + } + var options = this.options; + !fallback && toggleClass(dragEl, options.dragClass, false); + toggleClass(dragEl, options.ghostClass, true); + Sortable.active = this; + fallback && this._appendGhost(); + _dispatchEvent({ + sortable: this, + name: "start", + originalEvent: evt + }); + } else { + this._nulling(); + } + }, + _emulateDragOver: function _emulateDragOver() { + if (touchEvt) { + this._lastX = touchEvt.clientX; + this._lastY = touchEvt.clientY; + _hideGhostForTarget(); + var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY); + var parent = target; + while (target && target.shadowRoot) { + target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY); + if (target === parent) break; + parent = target; + } + dragEl.parentNode[expando]._isOutsideThisEl(target); + if (parent) { + do { + if (parent[expando]) { + var inserted = void 0; + inserted = parent[expando]._onDragOver({ + clientX: touchEvt.clientX, + clientY: touchEvt.clientY, + target, + rootEl: parent + }); + if (inserted && !this.options.dragoverBubble) { + break; + } + } + target = parent; + } while (parent = getParentOrHost(parent)); + } + _unhideGhostForTarget(); + } + }, + _onTouchMove: function _onTouchMove(evt) { + if (tapEvt) { + var options = this.options, fallbackTolerance = options.fallbackTolerance, fallbackOffset = options.fallbackOffset, touch = evt.touches ? evt.touches[0] : evt, ghostMatrix = ghostEl && matrix(ghostEl, true), scaleX = ghostEl && ghostMatrix && ghostMatrix.a, scaleY = ghostEl && ghostMatrix && ghostMatrix.d, relativeScrollOffset = PositionGhostAbsolutely && ghostRelativeParent && getRelativeScrollOffset(ghostRelativeParent), dx = (touch.clientX - tapEvt.clientX + fallbackOffset.x) / (scaleX || 1) + (relativeScrollOffset ? relativeScrollOffset[0] - ghostRelativeParentInitialScroll[0] : 0) / (scaleX || 1), dy = (touch.clientY - tapEvt.clientY + fallbackOffset.y) / (scaleY || 1) + (relativeScrollOffset ? relativeScrollOffset[1] - ghostRelativeParentInitialScroll[1] : 0) / (scaleY || 1); + if (!Sortable.active && !awaitingDragStarted) { + if (fallbackTolerance && Math.max(Math.abs(touch.clientX - this._lastX), Math.abs(touch.clientY - this._lastY)) < fallbackTolerance) { + return; + } + this._onDragStart(evt, true); + } + if (ghostEl) { + if (ghostMatrix) { + ghostMatrix.e += dx - (lastDx || 0); + ghostMatrix.f += dy - (lastDy || 0); + } else { + ghostMatrix = { + a: 1, + b: 0, + c: 0, + d: 1, + e: dx, + f: dy + }; + } + var cssMatrix = "matrix(".concat(ghostMatrix.a, ",").concat(ghostMatrix.b, ",").concat(ghostMatrix.c, ",").concat(ghostMatrix.d, ",").concat(ghostMatrix.e, ",").concat(ghostMatrix.f, ")"); + css(ghostEl, "webkitTransform", cssMatrix); + css(ghostEl, "mozTransform", cssMatrix); + css(ghostEl, "msTransform", cssMatrix); + css(ghostEl, "transform", cssMatrix); + lastDx = dx; + lastDy = dy; + touchEvt = touch; + } + evt.cancelable && evt.preventDefault(); + } + }, + _appendGhost: function _appendGhost() { + if (!ghostEl) { + var container = this.options.fallbackOnBody ? document.body : rootEl, rect = getRect(dragEl, true, PositionGhostAbsolutely, true, container), options = this.options; + if (PositionGhostAbsolutely) { + ghostRelativeParent = container; + while (css(ghostRelativeParent, "position") === "static" && css(ghostRelativeParent, "transform") === "none" && ghostRelativeParent !== document) { + ghostRelativeParent = ghostRelativeParent.parentNode; + } + if (ghostRelativeParent !== document.body && ghostRelativeParent !== document.documentElement) { + if (ghostRelativeParent === document) ghostRelativeParent = getWindowScrollingElement(); + rect.top += ghostRelativeParent.scrollTop; + rect.left += ghostRelativeParent.scrollLeft; + } else { + ghostRelativeParent = getWindowScrollingElement(); + } + ghostRelativeParentInitialScroll = getRelativeScrollOffset(ghostRelativeParent); + } + ghostEl = dragEl.cloneNode(true); + toggleClass(ghostEl, options.ghostClass, false); + toggleClass(ghostEl, options.fallbackClass, true); + toggleClass(ghostEl, options.dragClass, true); + css(ghostEl, "transition", ""); + css(ghostEl, "transform", ""); + css(ghostEl, "box-sizing", "border-box"); + css(ghostEl, "margin", 0); + css(ghostEl, "top", rect.top); + css(ghostEl, "left", rect.left); + css(ghostEl, "width", rect.width); + css(ghostEl, "height", rect.height); + css(ghostEl, "opacity", "0.8"); + css(ghostEl, "position", PositionGhostAbsolutely ? "absolute" : "fixed"); + css(ghostEl, "zIndex", "100000"); + css(ghostEl, "pointerEvents", "none"); + Sortable.ghost = ghostEl; + container.appendChild(ghostEl); + css(ghostEl, "transform-origin", tapDistanceLeft / parseInt(ghostEl.style.width) * 100 + "% " + tapDistanceTop / parseInt(ghostEl.style.height) * 100 + "%"); + } + }, + _onDragStart: function _onDragStart(evt, fallback) { + var _this = this; + var dataTransfer = evt.dataTransfer; + var options = _this.options; + pluginEvent2("dragStart", this, { + evt + }); + if (Sortable.eventCanceled) { + this._onDrop(); + return; + } + pluginEvent2("setupClone", this); + if (!Sortable.eventCanceled) { + cloneEl = clone(dragEl); + cloneEl.removeAttribute("id"); + cloneEl.draggable = false; + cloneEl.style["will-change"] = ""; + this._hideClone(); + toggleClass(cloneEl, this.options.chosenClass, false); + Sortable.clone = cloneEl; + } + _this.cloneId = _nextTick(function() { + pluginEvent2("clone", _this); + if (Sortable.eventCanceled) return; + if (!_this.options.removeCloneOnHide) { + rootEl.insertBefore(cloneEl, dragEl); + } + _this._hideClone(); + _dispatchEvent({ + sortable: _this, + name: "clone" + }); + }); + !fallback && toggleClass(dragEl, options.dragClass, true); + if (fallback) { + ignoreNextClick = true; + _this._loopId = setInterval(_this._emulateDragOver, 50); + } else { + off(document, "mouseup", _this._onDrop); + off(document, "touchend", _this._onDrop); + off(document, "touchcancel", _this._onDrop); + if (dataTransfer) { + dataTransfer.effectAllowed = "move"; + options.setData && options.setData.call(_this, dataTransfer, dragEl); + } + on(document, "drop", _this); + css(dragEl, "transform", "translateZ(0)"); + } + awaitingDragStarted = true; + _this._dragStartId = _nextTick(_this._dragStarted.bind(_this, fallback, evt)); + on(document, "selectstart", _this); + moved = true; + window.getSelection().removeAllRanges(); + if (Safari) { + css(document.body, "user-select", "none"); + } + }, + // Returns true - if no further action is needed (either inserted or another condition) + _onDragOver: function _onDragOver(evt) { + var el = this.el, target = evt.target, dragRect, targetRect, revert, options = this.options, group = options.group, activeSortable = Sortable.active, isOwner = activeGroup === group, canSort = options.sort, fromSortable = putSortable || activeSortable, vertical, _this = this, completedFired = false; + if (_silent) return; + function dragOverEvent(name, extra) { + pluginEvent2(name, _this, _objectSpread2({ + evt, + isOwner, + axis: vertical ? "vertical" : "horizontal", + revert, + dragRect, + targetRect, + canSort, + fromSortable, + target, + completed, + onMove: function onMove(target2, after2) { + return _onMove(rootEl, el, dragEl, dragRect, target2, getRect(target2), evt, after2); + }, + changed + }, extra)); + } + function capture() { + dragOverEvent("dragOverAnimationCapture"); + _this.captureAnimationState(); + if (_this !== fromSortable) { + fromSortable.captureAnimationState(); + } + } + function completed(insertion) { + dragOverEvent("dragOverCompleted", { + insertion + }); + if (insertion) { + if (isOwner) { + activeSortable._hideClone(); + } else { + activeSortable._showClone(_this); + } + if (_this !== fromSortable) { + toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : activeSortable.options.ghostClass, false); + toggleClass(dragEl, options.ghostClass, true); + } + if (putSortable !== _this && _this !== Sortable.active) { + putSortable = _this; + } else if (_this === Sortable.active && putSortable) { + putSortable = null; + } + if (fromSortable === _this) { + _this._ignoreWhileAnimating = target; + } + _this.animateAll(function() { + dragOverEvent("dragOverAnimationComplete"); + _this._ignoreWhileAnimating = null; + }); + if (_this !== fromSortable) { + fromSortable.animateAll(); + fromSortable._ignoreWhileAnimating = null; + } + } + if (target === dragEl && !dragEl.animated || target === el && !target.animated) { + lastTarget = null; + } + if (!options.dragoverBubble && !evt.rootEl && target !== document) { + dragEl.parentNode[expando]._isOutsideThisEl(evt.target); + !insertion && nearestEmptyInsertDetectEvent(evt); + } + !options.dragoverBubble && evt.stopPropagation && evt.stopPropagation(); + return completedFired = true; + } + function changed() { + newIndex = index(dragEl); + newDraggableIndex = index(dragEl, options.draggable); + _dispatchEvent({ + sortable: _this, + name: "change", + toEl: el, + newIndex, + newDraggableIndex, + originalEvent: evt + }); + } + if (evt.preventDefault !== void 0) { + evt.cancelable && evt.preventDefault(); + } + target = closest(target, options.draggable, el, true); + dragOverEvent("dragOver"); + if (Sortable.eventCanceled) return completedFired; + if (dragEl.contains(evt.target) || target.animated && target.animatingX && target.animatingY || _this._ignoreWhileAnimating === target) { + return completed(false); + } + ignoreNextClick = false; + if (activeSortable && !options.disabled && (isOwner ? canSort || (revert = parentEl !== rootEl) : putSortable === this || (this.lastPutMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && group.checkPut(this, activeSortable, dragEl, evt))) { + vertical = this._getDirection(evt, target) === "vertical"; + dragRect = getRect(dragEl); + dragOverEvent("dragOverValid"); + if (Sortable.eventCanceled) return completedFired; + if (revert) { + parentEl = rootEl; + capture(); + this._hideClone(); + dragOverEvent("revert"); + if (!Sortable.eventCanceled) { + if (nextEl) { + rootEl.insertBefore(dragEl, nextEl); + } else { + rootEl.appendChild(dragEl); + } + } + return completed(true); + } + var elLastChild = lastChild(el, options.draggable); + if (!elLastChild || _ghostIsLast(evt, vertical, this) && !elLastChild.animated) { + if (elLastChild === dragEl) { + return completed(false); + } + if (elLastChild && el === evt.target) { + target = elLastChild; + } + if (target) { + targetRect = getRect(target); + } + if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) { + capture(); + if (elLastChild && elLastChild.nextSibling) { + el.insertBefore(dragEl, elLastChild.nextSibling); + } else { + el.appendChild(dragEl); + } + parentEl = el; + changed(); + return completed(true); + } + } else if (elLastChild && _ghostIsFirst(evt, vertical, this)) { + var firstChild = getChild(el, 0, options, true); + if (firstChild === dragEl) { + return completed(false); + } + target = firstChild; + targetRect = getRect(target); + if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) { + capture(); + el.insertBefore(dragEl, firstChild); + parentEl = el; + changed(); + return completed(true); + } + } else if (target.parentNode === el) { + targetRect = getRect(target); + var direction = 0, targetBeforeFirstSwap, differentLevel = dragEl.parentNode !== el, differentRowCol = !_dragElInRowColumn(dragEl.animated && dragEl.toRect || dragRect, target.animated && target.toRect || targetRect, vertical), side1 = vertical ? "top" : "left", scrolledPastTop = isScrolledPast(target, "top", "top") || isScrolledPast(dragEl, "top", "top"), scrollBefore = scrolledPastTop ? scrolledPastTop.scrollTop : void 0; + if (lastTarget !== target) { + targetBeforeFirstSwap = targetRect[side1]; + pastFirstInvertThresh = false; + isCircumstantialInvert = !differentRowCol && options.invertSwap || differentLevel; + } + direction = _getSwapDirection(evt, target, targetRect, vertical, differentRowCol ? 1 : options.swapThreshold, options.invertedSwapThreshold == null ? options.swapThreshold : options.invertedSwapThreshold, isCircumstantialInvert, lastTarget === target); + var sibling; + if (direction !== 0) { + var dragIndex = index(dragEl); + do { + dragIndex -= direction; + sibling = parentEl.children[dragIndex]; + } while (sibling && (css(sibling, "display") === "none" || sibling === ghostEl)); + } + if (direction === 0 || sibling === target) { + return completed(false); + } + lastTarget = target; + lastDirection = direction; + var nextSibling = target.nextElementSibling, after = false; + after = direction === 1; + var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after); + if (moveVector !== false) { + if (moveVector === 1 || moveVector === -1) { + after = moveVector === 1; + } + _silent = true; + setTimeout(_unsilent, 30); + capture(); + if (after && !nextSibling) { + el.appendChild(dragEl); + } else { + target.parentNode.insertBefore(dragEl, after ? nextSibling : target); + } + if (scrolledPastTop) { + scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop); + } + parentEl = dragEl.parentNode; + if (targetBeforeFirstSwap !== void 0 && !isCircumstantialInvert) { + targetMoveDistance = Math.abs(targetBeforeFirstSwap - getRect(target)[side1]); + } + changed(); + return completed(true); + } + } + if (el.contains(dragEl)) { + return completed(false); + } + } + return false; + }, + _ignoreWhileAnimating: null, + _offMoveEvents: function _offMoveEvents() { + off(document, "mousemove", this._onTouchMove); + off(document, "touchmove", this._onTouchMove); + off(document, "pointermove", this._onTouchMove); + off(document, "dragover", nearestEmptyInsertDetectEvent); + off(document, "mousemove", nearestEmptyInsertDetectEvent); + off(document, "touchmove", nearestEmptyInsertDetectEvent); + }, + _offUpEvents: function _offUpEvents() { + var ownerDocument = this.el.ownerDocument; + off(ownerDocument, "mouseup", this._onDrop); + off(ownerDocument, "touchend", this._onDrop); + off(ownerDocument, "pointerup", this._onDrop); + off(ownerDocument, "pointercancel", this._onDrop); + off(ownerDocument, "touchcancel", this._onDrop); + off(document, "selectstart", this); + }, + _onDrop: function _onDrop(evt) { + var el = this.el, options = this.options; + newIndex = index(dragEl); + newDraggableIndex = index(dragEl, options.draggable); + pluginEvent2("drop", this, { + evt + }); + parentEl = dragEl && dragEl.parentNode; + newIndex = index(dragEl); + newDraggableIndex = index(dragEl, options.draggable); + if (Sortable.eventCanceled) { + this._nulling(); + return; + } + awaitingDragStarted = false; + isCircumstantialInvert = false; + pastFirstInvertThresh = false; + clearInterval(this._loopId); + clearTimeout(this._dragStartTimer); + _cancelNextTick(this.cloneId); + _cancelNextTick(this._dragStartId); + if (this.nativeDraggable) { + off(document, "drop", this); + off(el, "dragstart", this._onDragStart); + } + this._offMoveEvents(); + this._offUpEvents(); + if (Safari) { + css(document.body, "user-select", ""); + } + css(dragEl, "transform", ""); + if (evt) { + if (moved) { + evt.cancelable && evt.preventDefault(); + !options.dropBubble && evt.stopPropagation(); + } + ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl); + if (rootEl === parentEl || putSortable && putSortable.lastPutMode !== "clone") { + cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl); + } + if (dragEl) { + if (this.nativeDraggable) { + off(dragEl, "dragend", this); + } + _disableDraggable(dragEl); + dragEl.style["will-change"] = ""; + if (moved && !awaitingDragStarted) { + toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : this.options.ghostClass, false); + } + toggleClass(dragEl, this.options.chosenClass, false); + _dispatchEvent({ + sortable: this, + name: "unchoose", + toEl: parentEl, + newIndex: null, + newDraggableIndex: null, + originalEvent: evt + }); + if (rootEl !== parentEl) { + if (newIndex >= 0) { + _dispatchEvent({ + rootEl: parentEl, + name: "add", + toEl: parentEl, + fromEl: rootEl, + originalEvent: evt + }); + _dispatchEvent({ + sortable: this, + name: "remove", + toEl: parentEl, + originalEvent: evt + }); + _dispatchEvent({ + rootEl: parentEl, + name: "sort", + toEl: parentEl, + fromEl: rootEl, + originalEvent: evt + }); + _dispatchEvent({ + sortable: this, + name: "sort", + toEl: parentEl, + originalEvent: evt + }); + } + putSortable && putSortable.save(); + } else { + if (newIndex !== oldIndex) { + if (newIndex >= 0) { + _dispatchEvent({ + sortable: this, + name: "update", + toEl: parentEl, + originalEvent: evt + }); + _dispatchEvent({ + sortable: this, + name: "sort", + toEl: parentEl, + originalEvent: evt + }); + } + } + } + if (Sortable.active) { + if (newIndex == null || newIndex === -1) { + newIndex = oldIndex; + newDraggableIndex = oldDraggableIndex; + } + _dispatchEvent({ + sortable: this, + name: "end", + toEl: parentEl, + originalEvent: evt + }); + this.save(); + } + } + } + this._nulling(); + }, + _nulling: function _nulling() { + pluginEvent2("nulling", this); + rootEl = dragEl = parentEl = ghostEl = nextEl = cloneEl = lastDownEl = cloneHidden = tapEvt = touchEvt = moved = newIndex = newDraggableIndex = oldIndex = oldDraggableIndex = lastTarget = lastDirection = putSortable = activeGroup = Sortable.dragged = Sortable.ghost = Sortable.clone = Sortable.active = null; + savedInputChecked.forEach(function(el) { + el.checked = true; + }); + savedInputChecked.length = lastDx = lastDy = 0; + }, + handleEvent: function handleEvent(evt) { + switch (evt.type) { + case "drop": + case "dragend": + this._onDrop(evt); + break; + case "dragenter": + case "dragover": + if (dragEl) { + this._onDragOver(evt); + _globalDragOver(evt); + } + break; + case "selectstart": + evt.preventDefault(); + break; + } + }, + /** + * Serializes the item into an array of string. + * @returns {String[]} + */ + toArray: function toArray() { + var order = [], el, children = this.el.children, i = 0, n = children.length, options = this.options; + for (; i < n; i++) { + el = children[i]; + if (closest(el, options.draggable, this.el, false)) { + order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); + } + } + return order; + }, + /** + * Sorts the elements according to the array. + * @param {String[]} order order of the items + */ + sort: function sort(order, useAnimation) { + var items = {}, rootEl2 = this.el; + this.toArray().forEach(function(id, i) { + var el = rootEl2.children[i]; + if (closest(el, this.options.draggable, rootEl2, false)) { + items[id] = el; + } + }, this); + useAnimation && this.captureAnimationState(); + order.forEach(function(id) { + if (items[id]) { + rootEl2.removeChild(items[id]); + rootEl2.appendChild(items[id]); + } + }); + useAnimation && this.animateAll(); + }, + /** + * Save the current sorting + */ + save: function save() { + var store = this.options.store; + store && store.set && store.set(this); + }, + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * @param {HTMLElement} el + * @param {String} [selector] default: `options.draggable` + * @returns {HTMLElement|null} + */ + closest: function closest$1(el, selector) { + return closest(el, selector || this.options.draggable, this.el, false); + }, + /** + * Set/get option + * @param {string} name + * @param {*} [value] + * @returns {*} + */ + option: function option(name, value) { + var options = this.options; + if (value === void 0) { + return options[name]; + } else { + var modifiedValue = PluginManager.modifyOption(this, name, value); + if (typeof modifiedValue !== "undefined") { + options[name] = modifiedValue; + } else { + options[name] = value; + } + if (name === "group") { + _prepareGroup(options); + } + } + }, + /** + * Destroy + */ + destroy: function destroy() { + pluginEvent2("destroy", this); + var el = this.el; + el[expando] = null; + off(el, "mousedown", this._onTapStart); + off(el, "touchstart", this._onTapStart); + off(el, "pointerdown", this._onTapStart); + if (this.nativeDraggable) { + off(el, "dragover", this); + off(el, "dragenter", this); + } + Array.prototype.forEach.call(el.querySelectorAll("[draggable]"), function(el2) { + el2.removeAttribute("draggable"); + }); + this._onDrop(); + this._disableDelayedDragEvents(); + sortables.splice(sortables.indexOf(this.el), 1); + this.el = el = null; + }, + _hideClone: function _hideClone() { + if (!cloneHidden) { + pluginEvent2("hideClone", this); + if (Sortable.eventCanceled) return; + css(cloneEl, "display", "none"); + if (this.options.removeCloneOnHide && cloneEl.parentNode) { + cloneEl.parentNode.removeChild(cloneEl); + } + cloneHidden = true; + } + }, + _showClone: function _showClone(putSortable2) { + if (putSortable2.lastPutMode !== "clone") { + this._hideClone(); + return; + } + if (cloneHidden) { + pluginEvent2("showClone", this); + if (Sortable.eventCanceled) return; + if (dragEl.parentNode == rootEl && !this.options.group.revertClone) { + rootEl.insertBefore(cloneEl, dragEl); + } else if (nextEl) { + rootEl.insertBefore(cloneEl, nextEl); + } else { + rootEl.appendChild(cloneEl); + } + if (this.options.group.revertClone) { + this.animate(dragEl, cloneEl); + } + css(cloneEl, "display", ""); + cloneHidden = false; + } + } +}; +function _globalDragOver(evt) { + if (evt.dataTransfer) { + evt.dataTransfer.dropEffect = "move"; + } + evt.cancelable && evt.preventDefault(); +} +function _onMove(fromEl, toEl, dragEl2, dragRect, targetEl, targetRect, originalEvent, willInsertAfter) { + var evt, sortable = fromEl[expando], onMoveFn = sortable.options.onMove, retVal; + if (window.CustomEvent && !IE11OrLess && !Edge) { + evt = new CustomEvent("move", { + bubbles: true, + cancelable: true + }); + } else { + evt = document.createEvent("Event"); + evt.initEvent("move", true, true); + } + evt.to = toEl; + evt.from = fromEl; + evt.dragged = dragEl2; + evt.draggedRect = dragRect; + evt.related = targetEl || toEl; + evt.relatedRect = targetRect || getRect(toEl); + evt.willInsertAfter = willInsertAfter; + evt.originalEvent = originalEvent; + fromEl.dispatchEvent(evt); + if (onMoveFn) { + retVal = onMoveFn.call(sortable, evt, originalEvent); + } + return retVal; +} +function _disableDraggable(el) { + el.draggable = false; +} +function _unsilent() { + _silent = false; +} +function _ghostIsFirst(evt, vertical, sortable) { + var firstElRect = getRect(getChild(sortable.el, 0, sortable.options, true)); + var childContainingRect = getChildContainingRectFromElement(sortable.el, sortable.options, ghostEl); + var spacer = 10; + return vertical ? evt.clientX < childContainingRect.left - spacer || evt.clientY < firstElRect.top && evt.clientX < firstElRect.right : evt.clientY < childContainingRect.top - spacer || evt.clientY < firstElRect.bottom && evt.clientX < firstElRect.left; +} +function _ghostIsLast(evt, vertical, sortable) { + var lastElRect = getRect(lastChild(sortable.el, sortable.options.draggable)); + var childContainingRect = getChildContainingRectFromElement(sortable.el, sortable.options, ghostEl); + var spacer = 10; + return vertical ? evt.clientX > childContainingRect.right + spacer || evt.clientY > lastElRect.bottom && evt.clientX > lastElRect.left : evt.clientY > childContainingRect.bottom + spacer || evt.clientX > lastElRect.right && evt.clientY > lastElRect.top; +} +function _getSwapDirection(evt, target, targetRect, vertical, swapThreshold, invertedSwapThreshold, invertSwap, isLastTarget) { + var mouseOnAxis = vertical ? evt.clientY : evt.clientX, targetLength = vertical ? targetRect.height : targetRect.width, targetS1 = vertical ? targetRect.top : targetRect.left, targetS2 = vertical ? targetRect.bottom : targetRect.right, invert = false; + if (!invertSwap) { + if (isLastTarget && targetMoveDistance < targetLength * swapThreshold) { + if (!pastFirstInvertThresh && (lastDirection === 1 ? mouseOnAxis > targetS1 + targetLength * invertedSwapThreshold / 2 : mouseOnAxis < targetS2 - targetLength * invertedSwapThreshold / 2)) { + pastFirstInvertThresh = true; + } + if (!pastFirstInvertThresh) { + if (lastDirection === 1 ? mouseOnAxis < targetS1 + targetMoveDistance : mouseOnAxis > targetS2 - targetMoveDistance) { + return -lastDirection; + } + } else { + invert = true; + } + } else { + if (mouseOnAxis > targetS1 + targetLength * (1 - swapThreshold) / 2 && mouseOnAxis < targetS2 - targetLength * (1 - swapThreshold) / 2) { + return _getInsertDirection(target); + } + } + } + invert = invert || invertSwap; + if (invert) { + if (mouseOnAxis < targetS1 + targetLength * invertedSwapThreshold / 2 || mouseOnAxis > targetS2 - targetLength * invertedSwapThreshold / 2) { + return mouseOnAxis > targetS1 + targetLength / 2 ? 1 : -1; + } + } + return 0; +} +function _getInsertDirection(target) { + if (index(dragEl) < index(target)) { + return 1; + } else { + return -1; + } +} +function _generateId(el) { + var str = el.tagName + el.className + el.src + el.href + el.textContent, i = str.length, sum = 0; + while (i--) { + sum += str.charCodeAt(i); + } + return sum.toString(36); +} +function _saveInputCheckedState(root) { + savedInputChecked.length = 0; + var inputs = root.getElementsByTagName("input"); + var idx = inputs.length; + while (idx--) { + var el = inputs[idx]; + el.checked && savedInputChecked.push(el); + } +} +function _nextTick(fn) { + return setTimeout(fn, 0); +} +function _cancelNextTick(id) { + return clearTimeout(id); +} +if (documentExists) { + on(document, "touchmove", function(evt) { + if ((Sortable.active || awaitingDragStarted) && evt.cancelable) { + evt.preventDefault(); + } + }); +} +Sortable.utils = { + on, + off, + css, + find, + is: function is(el, selector) { + return !!closest(el, selector, el, false); + }, + extend, + throttle, + closest, + toggleClass, + clone, + index, + nextTick: _nextTick, + cancelNextTick: _cancelNextTick, + detectDirection: _detectDirection, + getChild, + expando +}; +Sortable.get = function(element) { + return element[expando]; +}; +Sortable.mount = function() { + for (var _len = arguments.length, plugins2 = new Array(_len), _key = 0; _key < _len; _key++) { + plugins2[_key] = arguments[_key]; + } + if (plugins2[0].constructor === Array) plugins2 = plugins2[0]; + plugins2.forEach(function(plugin) { + if (!plugin.prototype || !plugin.prototype.constructor) { + throw "Sortable: Mounted plugin must be a constructor function, not ".concat({}.toString.call(plugin)); + } + if (plugin.utils) Sortable.utils = _objectSpread2(_objectSpread2({}, Sortable.utils), plugin.utils); + PluginManager.mount(plugin); + }); +}; +Sortable.create = function(el, options) { + return new Sortable(el, options); +}; +Sortable.version = version; +var autoScrolls = []; +var scrollEl; +var scrollRootEl; +var scrolling = false; +var lastAutoScrollX; +var lastAutoScrollY; +var touchEvt$1; +var pointerElemChangedInterval; +function AutoScrollPlugin() { + function AutoScroll() { + this.defaults = { + scroll: true, + forceAutoScrollFallback: false, + scrollSensitivity: 30, + scrollSpeed: 10, + bubbleScroll: true + }; + for (var fn in this) { + if (fn.charAt(0) === "_" && typeof this[fn] === "function") { + this[fn] = this[fn].bind(this); + } + } + } + AutoScroll.prototype = { + dragStarted: function dragStarted(_ref) { + var originalEvent = _ref.originalEvent; + if (this.sortable.nativeDraggable) { + on(document, "dragover", this._handleAutoScroll); + } else { + if (this.options.supportPointer) { + on(document, "pointermove", this._handleFallbackAutoScroll); + } else if (originalEvent.touches) { + on(document, "touchmove", this._handleFallbackAutoScroll); + } else { + on(document, "mousemove", this._handleFallbackAutoScroll); + } + } + }, + dragOverCompleted: function dragOverCompleted(_ref2) { + var originalEvent = _ref2.originalEvent; + if (!this.options.dragOverBubble && !originalEvent.rootEl) { + this._handleAutoScroll(originalEvent); + } + }, + drop: function drop3() { + if (this.sortable.nativeDraggable) { + off(document, "dragover", this._handleAutoScroll); + } else { + off(document, "pointermove", this._handleFallbackAutoScroll); + off(document, "touchmove", this._handleFallbackAutoScroll); + off(document, "mousemove", this._handleFallbackAutoScroll); + } + clearPointerElemChangedInterval(); + clearAutoScrolls(); + cancelThrottle(); + }, + nulling: function nulling() { + touchEvt$1 = scrollRootEl = scrollEl = scrolling = pointerElemChangedInterval = lastAutoScrollX = lastAutoScrollY = null; + autoScrolls.length = 0; + }, + _handleFallbackAutoScroll: function _handleFallbackAutoScroll(evt) { + this._handleAutoScroll(evt, true); + }, + _handleAutoScroll: function _handleAutoScroll(evt, fallback) { + var _this = this; + var x = (evt.touches ? evt.touches[0] : evt).clientX, y = (evt.touches ? evt.touches[0] : evt).clientY, elem = document.elementFromPoint(x, y); + touchEvt$1 = evt; + if (fallback || this.options.forceAutoScrollFallback || Edge || IE11OrLess || Safari) { + autoScroll(evt, this.options, elem, fallback); + var ogElemScroller = getParentAutoScrollElement(elem, true); + if (scrolling && (!pointerElemChangedInterval || x !== lastAutoScrollX || y !== lastAutoScrollY)) { + pointerElemChangedInterval && clearPointerElemChangedInterval(); + pointerElemChangedInterval = setInterval(function() { + var newElem = getParentAutoScrollElement(document.elementFromPoint(x, y), true); + if (newElem !== ogElemScroller) { + ogElemScroller = newElem; + clearAutoScrolls(); + } + autoScroll(evt, _this.options, newElem, fallback); + }, 10); + lastAutoScrollX = x; + lastAutoScrollY = y; + } + } else { + if (!this.options.bubbleScroll || getParentAutoScrollElement(elem, true) === getWindowScrollingElement()) { + clearAutoScrolls(); + return; + } + autoScroll(evt, this.options, getParentAutoScrollElement(elem, false), false); + } + } + }; + return _extends(AutoScroll, { + pluginName: "scroll", + initializeByDefault: true + }); +} +function clearAutoScrolls() { + autoScrolls.forEach(function(autoScroll2) { + clearInterval(autoScroll2.pid); + }); + autoScrolls = []; +} +function clearPointerElemChangedInterval() { + clearInterval(pointerElemChangedInterval); +} +var autoScroll = throttle(function(evt, options, rootEl2, isFallback) { + if (!options.scroll) return; + var x = (evt.touches ? evt.touches[0] : evt).clientX, y = (evt.touches ? evt.touches[0] : evt).clientY, sens = options.scrollSensitivity, speed = options.scrollSpeed, winScroller = getWindowScrollingElement(); + var scrollThisInstance = false, scrollCustomFn; + if (scrollRootEl !== rootEl2) { + scrollRootEl = rootEl2; + clearAutoScrolls(); + scrollEl = options.scroll; + scrollCustomFn = options.scrollFn; + if (scrollEl === true) { + scrollEl = getParentAutoScrollElement(rootEl2, true); + } + } + var layersOut = 0; + var currentParent = scrollEl; + do { + var el = currentParent, rect = getRect(el), top = rect.top, bottom = rect.bottom, left = rect.left, right = rect.right, width = rect.width, height = rect.height, canScrollX = void 0, canScrollY = void 0, scrollWidth = el.scrollWidth, scrollHeight = el.scrollHeight, elCSS = css(el), scrollPosX = el.scrollLeft, scrollPosY = el.scrollTop; + if (el === winScroller) { + canScrollX = width < scrollWidth && (elCSS.overflowX === "auto" || elCSS.overflowX === "scroll" || elCSS.overflowX === "visible"); + canScrollY = height < scrollHeight && (elCSS.overflowY === "auto" || elCSS.overflowY === "scroll" || elCSS.overflowY === "visible"); + } else { + canScrollX = width < scrollWidth && (elCSS.overflowX === "auto" || elCSS.overflowX === "scroll"); + canScrollY = height < scrollHeight && (elCSS.overflowY === "auto" || elCSS.overflowY === "scroll"); + } + var vx = canScrollX && (Math.abs(right - x) <= sens && scrollPosX + width < scrollWidth) - (Math.abs(left - x) <= sens && !!scrollPosX); + var vy = canScrollY && (Math.abs(bottom - y) <= sens && scrollPosY + height < scrollHeight) - (Math.abs(top - y) <= sens && !!scrollPosY); + if (!autoScrolls[layersOut]) { + for (var i = 0; i <= layersOut; i++) { + if (!autoScrolls[i]) { + autoScrolls[i] = {}; + } + } + } + if (autoScrolls[layersOut].vx != vx || autoScrolls[layersOut].vy != vy || autoScrolls[layersOut].el !== el) { + autoScrolls[layersOut].el = el; + autoScrolls[layersOut].vx = vx; + autoScrolls[layersOut].vy = vy; + clearInterval(autoScrolls[layersOut].pid); + if (vx != 0 || vy != 0) { + scrollThisInstance = true; + autoScrolls[layersOut].pid = setInterval(function() { + if (isFallback && this.layer === 0) { + Sortable.active._onTouchMove(touchEvt$1); + } + var scrollOffsetY = autoScrolls[this.layer].vy ? autoScrolls[this.layer].vy * speed : 0; + var scrollOffsetX = autoScrolls[this.layer].vx ? autoScrolls[this.layer].vx * speed : 0; + if (typeof scrollCustomFn === "function") { + if (scrollCustomFn.call(Sortable.dragged.parentNode[expando], scrollOffsetX, scrollOffsetY, evt, touchEvt$1, autoScrolls[this.layer].el) !== "continue") { + return; + } + } + scrollBy(autoScrolls[this.layer].el, scrollOffsetX, scrollOffsetY); + }.bind({ + layer: layersOut + }), 24); + } + } + layersOut++; + } while (options.bubbleScroll && currentParent !== winScroller && (currentParent = getParentAutoScrollElement(currentParent, false))); + scrolling = scrollThisInstance; +}, 30); +var drop = function drop2(_ref) { + var originalEvent = _ref.originalEvent, putSortable2 = _ref.putSortable, dragEl2 = _ref.dragEl, activeSortable = _ref.activeSortable, dispatchSortableEvent = _ref.dispatchSortableEvent, hideGhostForTarget = _ref.hideGhostForTarget, unhideGhostForTarget = _ref.unhideGhostForTarget; + if (!originalEvent) return; + var toSortable = putSortable2 || activeSortable; + hideGhostForTarget(); + var touch = originalEvent.changedTouches && originalEvent.changedTouches.length ? originalEvent.changedTouches[0] : originalEvent; + var target = document.elementFromPoint(touch.clientX, touch.clientY); + unhideGhostForTarget(); + if (toSortable && !toSortable.el.contains(target)) { + dispatchSortableEvent("spill"); + this.onSpill({ + dragEl: dragEl2, + putSortable: putSortable2 + }); + } +}; +function Revert() { +} +Revert.prototype = { + startIndex: null, + dragStart: function dragStart(_ref2) { + var oldDraggableIndex2 = _ref2.oldDraggableIndex; + this.startIndex = oldDraggableIndex2; + }, + onSpill: function onSpill(_ref3) { + var dragEl2 = _ref3.dragEl, putSortable2 = _ref3.putSortable; + this.sortable.captureAnimationState(); + if (putSortable2) { + putSortable2.captureAnimationState(); + } + var nextSibling = getChild(this.sortable.el, this.startIndex, this.options); + if (nextSibling) { + this.sortable.el.insertBefore(dragEl2, nextSibling); + } else { + this.sortable.el.appendChild(dragEl2); + } + this.sortable.animateAll(); + if (putSortable2) { + putSortable2.animateAll(); + } + }, + drop +}; +_extends(Revert, { + pluginName: "revertOnSpill" +}); +function Remove() { +} +Remove.prototype = { + onSpill: function onSpill2(_ref4) { + var dragEl2 = _ref4.dragEl, putSortable2 = _ref4.putSortable; + var parentSortable = putSortable2 || this.sortable; + parentSortable.captureAnimationState(); + dragEl2.parentNode && dragEl2.parentNode.removeChild(dragEl2); + parentSortable.animateAll(); + }, + drop +}; +_extends(Remove, { + pluginName: "removeOnSpill" +}); +Sortable.mount(new AutoScrollPlugin()); +Sortable.mount(Remove, Revert); +var sortable_esm_default = Sortable; + +// src/utils/debounce.ts +function debounce(fn, delay) { + let timer = null; + const debounced = Object.assign( + function(...args) { + if (timer !== null) { + window.clearTimeout(timer); + } + timer = window.setTimeout(() => { + timer = null; + fn(...args); + }, delay); + }, + { + cancel() { + if (timer !== null) { + window.clearTimeout(timer); + timer = null; + } + } + } + ); + return debounced; +} + +// src/utils/grouping.ts +function ensureGroupExists(grouped, key) { + if (!grouped.has(key)) { + grouped.set(key, []); + } + const group = grouped.get(key); + if (!group) { + const newGroup = []; + grouped.set(key, newGroup); + return newGroup; + } + return group; +} +function normalizePropertyValue(value) { + if (value === null || value === void 0) { + return UNCATEGORIZED_LABEL; + } + if (typeof value === "object") { + const stringValue2 = value.toString().trim(); + return stringValue2 === "" || stringValue2 === "null" ? UNCATEGORIZED_LABEL : stringValue2; + } + const stringValue = typeof value === "string" ? value : String(value); + const trimmed = stringValue.trim(); + return trimmed === "" || trimmed === "null" ? UNCATEGORIZED_LABEL : trimmed; +} + +// src/kanbanView.ts +function isRecord(value) { + return typeof value === "object" && value !== null; +} +function isStringArray(value) { + return Array.isArray(value) && value.every((item) => typeof item === "string"); +} +function isStringArrayRecord(value) { + return isRecord(value) && !Array.isArray(value) && Object.values(value).every(isStringArray); +} +function isColumnOrders(value) { + return isStringArrayRecord(value); +} +function isColumnColors(value) { + return isRecord(value) && Object.values(value).every((v) => isRecord(v) && Object.values(v).every((c) => typeof c === "string")); +} +function isCardOrders(value) { + return isRecord(value) && !Array.isArray(value) && Object.values(value).every((v) => isRecord(v) && !Array.isArray(v) && Object.values(v).every(isStringArray)); +} +function isCollapsedLanes(value) { + return isStringArrayRecord(value); +} +var KanbanView = class extends import_obsidian5.BasesView { + constructor(controller, scrollEl2, legacyData = null) { + super(controller); + this.type = "kanban-view"; + this.hoverPopover = null; + this.groupByPropertyId = null; + this.swimlaneByPropertyId = null; + this.cardTitlePropertyId = null; + this.imagePropertyId = null; + this._columnSortables = /* @__PURE__ */ new Map(); + this._entryMap = /* @__PURE__ */ new Map(); + this.swimlaneSortable = null; + this.swimlaneColumnSortables = /* @__PURE__ */ new Map(); + this.activeColorPicker = null; + /** + * In-memory display preferences — the single source of truth during a session. + * + * Loaded from config once when groupByPropertyId changes. Renders read from + * here exclusively and never call config.set(). Only explicit user actions + * (drag-drop, column remove, color change) update _prefs and then call + * _persistPrefs() to write back to config. + * + * This breaks the config.set() → onDataUpdated() feedback loop that caused + * state thrashing on every render cycle. + */ + this._lastOrderKey = ""; + this._lastWrapValue = null; + this._lastCardTitlePropertyId = void 0; + this._lastImagePropertyId = void 0; + this._lastImageFit = void 0; + this._lastImageAspectRatio = void 0; + this._lastSwimlanePropertyId = void 0; + this._lastQuickAddFolder = void 0; + this._cardFingerprints = /* @__PURE__ */ new Map(); + this._deferredSortableListeners = /* @__PURE__ */ new Map(); + this._prefs = { + columnOrder: [], + swimlaneOrder: [], + cardOrders: {}, + columnColors: {}, + // columnValue → colorName + collapsedLanes: /* @__PURE__ */ new Set() + }; + this._prefsPropertyId = null; + this._prefsSwimlanePropertyId = null; + /** + * True while a card or column drag is in flight. When set, patchColumnCards + * skips DOM reordering so Sortable's live drag preview is not disturbed by + * re-renders triggered during the drag. + */ + this._dragging = false; + this._activeCardPath = null; + this.scrollEl = scrollEl2; + this.containerEl = scrollEl2.createDiv({ cls: CSS_CLASSES.VIEW_CONTAINER }); + this.legacyData = legacyData; + this.containerEl.on("click", "a.internal-link", (evt, linkEl) => { + evt.preventDefault(); + const href = linkEl.getAttribute("data-href") || linkEl.getAttribute("href"); + if (href && this.app) { + const cardEl = linkEl.closest(`[${DATA_ATTRIBUTES.ENTRY_PATH}]`); + const sourcePath = cardEl.instanceOf(HTMLElement) ? cardEl.getAttribute(DATA_ATTRIBUTES.ENTRY_PATH) ?? "" : ""; + void this.app.workspace.openLinkText(href, sourcePath, import_obsidian5.Keymap.isModEvent(evt)); + } + }); + this.containerEl.on("auxclick", "a.internal-link", (evt, linkEl) => { + if (!evt.instanceOf(MouseEvent) || evt.button !== 1) return; + evt.preventDefault(); + const href = linkEl.getAttribute("data-href") || linkEl.getAttribute("href"); + if (!href || !this.app) return; + const cardEl = linkEl.closest(`[${DATA_ATTRIBUTES.ENTRY_PATH}]`); + const sourcePath = cardEl.instanceOf(HTMLElement) ? cardEl.getAttribute(DATA_ATTRIBUTES.ENTRY_PATH) ?? "" : ""; + const file = this.app.metadataCache.getFirstLinkpathDest(href, sourcePath); + if (file) this.openInBackgroundTab(file); + }); + this.containerEl.on("mouseover", "a.internal-link", (evt, linkEl) => { + if (!evt.instanceOf(MouseEvent)) return; + const href = linkEl.getAttribute("data-href") || linkEl.getAttribute("href"); + if (!href) return; + const cardEl = linkEl.closest(`[${DATA_ATTRIBUTES.ENTRY_PATH}]`); + const sourcePath = cardEl.instanceOf(HTMLElement) ? cardEl.getAttribute(DATA_ATTRIBUTES.ENTRY_PATH) ?? "" : ""; + this.triggerHoverPreview(href, sourcePath, evt, linkEl); + }); + this._debouncedRender = debounce(() => { + try { + this.loadConfig(); + this.render(); + } catch (error) { + console.error("KanbanView error:", error); + } + }, DEBOUNCE_DELAY); + } + onDataUpdated() { + this._debouncedRender(); + } + loadConfig() { + this.groupByPropertyId = this.config.getAsPropertyId("groupByProperty"); + this.swimlaneByPropertyId = this.config.getAsPropertyId("swimlaneByProperty"); + this.cardTitlePropertyId = this.config.getAsPropertyId("cardTitleProperty"); + this.imagePropertyId = this.config.getAsPropertyId("imageProperty"); + } + triggerHoverPreview(linktext, sourcePath, event, targetEl) { + this.app?.workspace.trigger("hover-link", { + event, + source: HOVER_LINK_SOURCE_ID, + hoverParent: this, + targetEl, + linktext, + sourcePath + }); + } + /** + * Composite key used by `_prefs.cardOrders` to disambiguate card order across + * swimlanes. When swimlanes are inactive, returns the bare column value so + * existing flat-mode persistence continues to round-trip unchanged. + */ + cardOrderKey(swimlaneValue, columnValue) { + return swimlaneValue === null ? columnValue : `${swimlaneValue}${SWIMLANE_KEY_SEPARATOR}${columnValue}`; + } + swimlanePrefsKey(groupPropertyId, swimlanePropertyId) { + return `${groupPropertyId}${SWIMLANE_KEY_SEPARATOR}${swimlanePropertyId}`; + } + /** + * Load display preferences from config for the given propertyId. + * Called once when groupByPropertyId changes; subsequent renders reuse _prefs. + */ + _loadPrefs(propertyId, swimlanePropertyId) { + this._prefsPropertyId = propertyId; + this._prefsSwimlanePropertyId = swimlanePropertyId; + const swimlaneScopedKey = swimlanePropertyId ? this.swimlanePrefsKey(propertyId, swimlanePropertyId) : null; + const rawOrders = this.config?.get("columnOrders"); + const allOrders = isColumnOrders(rawOrders) ? rawOrders : {}; + let columnOrder = allOrders[propertyId] ?? null; + const legacyOrder = this.legacyData?.columnOrders[propertyId] ?? null; + if (!columnOrder && legacyOrder) { + columnOrder = legacyOrder; + this.config?.set("columnOrders", { + ...allOrders, + [propertyId]: legacyOrder + }); + } + this._prefs.columnOrder = columnOrder ? [...columnOrder] : []; + const rawCardOrders = this.config?.get("cardOrders"); + const allCardOrders = isCardOrders(rawCardOrders) ? rawCardOrders : {}; + const savedCardOrders = allCardOrders[swimlaneScopedKey ?? propertyId] ?? {}; + this._prefs.cardOrders = Object.fromEntries(Object.entries(savedCardOrders).map(([k, v]) => [k, [...v]])); + const rawColors = this.config?.get("columnColors"); + const allColors = isColumnColors(rawColors) ? rawColors : {}; + let columnColors = allColors[propertyId] ?? null; + const legacyColors = this.legacyData?.columnColors[propertyId]; + if (!columnColors && legacyColors && Object.keys(legacyColors).length > 0) { + columnColors = legacyColors; + this.config?.set("columnColors", { + ...allColors, + [propertyId]: legacyColors + }); + } + this._prefs.columnColors = columnColors ? { ...columnColors } : {}; + const rawCollapsed = this.config?.get("collapsedLanes"); + const allCollapsed = isCollapsedLanes(rawCollapsed) ? rawCollapsed : {}; + this._prefs.collapsedLanes = new Set(swimlaneScopedKey ? allCollapsed[swimlaneScopedKey] ?? [] : []); + const rawSwimlaneOrders = this.config?.get("swimlaneOrders"); + const allSwimlaneOrders = isColumnOrders(rawSwimlaneOrders) ? rawSwimlaneOrders : {}; + this._prefs.swimlaneOrder = swimlaneScopedKey && allSwimlaneOrders[swimlaneScopedKey] ? [...allSwimlaneOrders[swimlaneScopedKey]] : []; + } + /** + * Write _prefs back to config. Called only on user actions (drag-drop, + * column remove, color change) — never during renders. + * + * Change guards skip config.set() when the value hasn't changed, preventing + * spurious onDataUpdated() triggers. + */ + _persistConfigKey(key, guard, newValue, storageKey = this._prefsPropertyId) { + if (!storageKey) return; + const raw = this.config?.get(key); + const all = guard(raw) ? raw : {}; + if (JSON.stringify(all[storageKey]) !== JSON.stringify(newValue)) { + this.config?.set(key, { ...all, [storageKey]: newValue }); + } + } + _persistPrefs() { + if (!this._prefsPropertyId) return; + const swimlaneScopedKey = this._prefsSwimlanePropertyId ? this.swimlanePrefsKey(this._prefsPropertyId, this._prefsSwimlanePropertyId) : null; + this._persistConfigKey("columnOrders", isColumnOrders, this._prefs.columnOrder, this._prefsPropertyId); + this._persistConfigKey( + "cardOrders", + isCardOrders, + this._prefs.cardOrders, + swimlaneScopedKey ?? this._prefsPropertyId + ); + this._persistConfigKey("columnColors", isColumnColors, this._prefs.columnColors, this._prefsPropertyId); + if (swimlaneScopedKey) { + this._persistConfigKey("swimlaneOrders", isColumnOrders, this._prefs.swimlaneOrder, swimlaneScopedKey); + this._persistConfigKey( + "collapsedLanes", + isCollapsedLanes, + Array.from(this._prefs.collapsedLanes), + swimlaneScopedKey + ); + } + } + render() { + try { + const entries = this.data?.data || []; + const availablePropertyIds = this.allProperties || []; + if (!this.groupByPropertyId && availablePropertyIds.length === 0) { + this.fullReset(); + this.containerEl.createDiv({ + text: EMPTY_STATE_MESSAGES.NO_PROPERTIES, + cls: CSS_CLASSES.EMPTY_STATE + }); + return; + } + if (!this.groupByPropertyId) { + this.groupByPropertyId = availablePropertyIds[0]; + } + const swimlanePropertyId = this.swimlaneByPropertyId && this.swimlaneByPropertyId !== this.groupByPropertyId ? this.swimlaneByPropertyId : null; + const groupChanged = this.groupByPropertyId !== this._prefsPropertyId; + if (groupChanged || swimlanePropertyId !== this._prefsSwimlanePropertyId) { + this._loadPrefs(this.groupByPropertyId, swimlanePropertyId); + } + const hasNoEntries = entries.length === 0; + const hasNoSavedColumns = this._prefs.columnOrder.length === 0; + if (hasNoEntries && hasNoSavedColumns) { + this.fullReset(); + this.containerEl.createDiv({ + text: EMPTY_STATE_MESSAGES.NO_ENTRIES, + cls: CSS_CLASSES.EMPTY_STATE + }); + return; + } + this._entryMap = new Map(entries.map((e) => [e.file.path, e])); + const groupedByLane = swimlanePropertyId ? this.groupEntriesBySwimlaneAndColumn(entries, swimlanePropertyId, this.groupByPropertyId) : null; + const groupedEntries = groupedByLane ? this.flattenLanes(groupedByLane) : this.groupEntriesByProperty(entries, this.groupByPropertyId); + const sortActive = this.hasActiveSort(); + if (!sortActive && groupedByLane) { + groupedByLane.forEach((columns, laneValue) => { + columns.forEach((cellEntries, columnValue) => { + const savedOrder = this._prefs.cardOrders[this.cardOrderKey(laneValue, columnValue)]; + if (savedOrder) { + columns.set(columnValue, this.applyCardOrder(cellEntries, savedOrder)); + } + }); + }); + } else if (!sortActive) { + groupedEntries.forEach((columnEntries, value) => { + const savedOrder = this._prefs.cardOrders[this.cardOrderKey(null, value)]; + if (savedOrder) { + groupedEntries.set(value, this.applyCardOrder(columnEntries, savedOrder)); + } + }); + } + const liveValues = Array.from(groupedEntries.keys()); + const liveValueSet = new Set(liveValues); + let shouldPersistColumnOrder = false; + if (this._prefs.columnOrder.includes(UNCATEGORIZED_LABEL) && !liveValueSet.has(UNCATEGORIZED_LABEL)) { + this._prefs.columnOrder = this._prefs.columnOrder.filter((value) => value !== UNCATEGORIZED_LABEL); + shouldPersistColumnOrder = true; + } + const newValues = liveValues.filter((v) => !this._prefs.columnOrder.includes(v)); + if (newValues.length > 0) { + const isInitialOrder = this._prefs.columnOrder.length === 0; + this._prefs.columnOrder = isInitialOrder ? [...newValues].sort() : [...this._prefs.columnOrder, ...newValues]; + shouldPersistColumnOrder = true; + } + if (shouldPersistColumnOrder) { + this._persistPrefs(); + } + const orderedValues = this.getOrderedColumnValues(liveValues); + const currentOrderKey = JSON.stringify(this.config?.getOrder() ?? []); + const orderChanged = currentOrderKey !== this._lastOrderKey; + this._lastOrderKey = currentOrderKey; + const currentWrapValue = this.config?.get("wrapPropertyValues") === true; + const wrapChanged = currentWrapValue !== this._lastWrapValue; + this._lastWrapValue = currentWrapValue; + const currentCardTitlePropertyId = this.cardTitlePropertyId; + const cardTitleChanged = currentCardTitlePropertyId !== this._lastCardTitlePropertyId; + this._lastCardTitlePropertyId = currentCardTitlePropertyId; + const currentImagePropertyId = this.imagePropertyId; + const imagePropertyChanged = currentImagePropertyId !== this._lastImagePropertyId; + this._lastImagePropertyId = currentImagePropertyId; + const currentImageFit = this.config?.get("imageFit") === "contain" ? "contain" : "cover"; + const imageFitChanged = currentImageFit !== this._lastImageFit; + this._lastImageFit = currentImageFit; + const rawRatio = Number(this.config?.get("imageAspectRatio")); + const currentImageAspectRatio = Number.isFinite(rawRatio) && rawRatio > 0 ? rawRatio : 0.5; + const imageAspectRatioChanged = currentImageAspectRatio !== this._lastImageAspectRatio; + this._lastImageAspectRatio = currentImageAspectRatio; + const currentSwimlanePropertyId = swimlanePropertyId; + const swimlanePropertyChanged = currentSwimlanePropertyId !== this._lastSwimlanePropertyId; + this._lastSwimlanePropertyId = currentSwimlanePropertyId; + const currentQuickAddFolder = this.getQuickAddFolder(); + const quickAddFolderChanged = currentQuickAddFolder !== this._lastQuickAddFolder; + this._lastQuickAddFolder = currentQuickAddFolder; + const existingBoard = this.containerEl.querySelector(`.${CSS_CLASSES.BOARD}`); + const optionsChanged = orderChanged || wrapChanged || cardTitleChanged || imagePropertyChanged || imageFitChanged || imageAspectRatioChanged || swimlanePropertyChanged || quickAddFolderChanged; + const lanes = /* @__PURE__ */ new Map(); + if (groupedByLane) { + groupedByLane.forEach((v, k) => lanes.set(k, v)); + } else { + lanes.set(null, groupedEntries); + } + const hasSwimlanes = groupedByLane !== null; + const existingIsSwimlane = existingBoard?.classList.contains(CSS_CLASSES.BOARD_WITH_SWIMLANES) ?? false; + const modeChanged = hasSwimlanes !== existingIsSwimlane; + if (!existingBoard || modeChanged || groupChanged || optionsChanged) { + this.fullRebuild(orderedValues, lanes, hasSwimlanes); + } else { + this.patchBoard(orderedValues, lanes, hasSwimlanes); + } + this.reapplyActiveCard(); + } catch (error) { + console.error("KanbanView error:", error); + } + } + destroySortables() { + this._columnSortables.forEach((s) => s.destroy()); + this._columnSortables.clear(); + if (this.swimlaneSortable) { + this.swimlaneSortable.destroy(); + this.swimlaneSortable = null; + } + this.swimlaneColumnSortables.forEach((s) => s.destroy()); + this.swimlaneColumnSortables.clear(); + this._deferredSortableListeners.forEach(({ el, handler }) => { + el.removeEventListener("pointerdown", handler); + }); + this._deferredSortableListeners.clear(); + } + fullReset() { + this.containerEl.empty(); + this.destroySortables(); + this._entryMap.clear(); + this._cardFingerprints.clear(); + } + fullRebuild(orderedColumnValues, lanes, hasSwimlanes) { + this.containerEl.empty(); + this.containerEl.classList.toggle(CSS_CLASSES.VIEW_CONTAINER_WITH_SWIMLANES, hasSwimlanes); + this.destroySortables(); + const boardEl = this.containerEl.createDiv({ + cls: hasSwimlanes ? `${CSS_CLASSES.BOARD} ${CSS_CLASSES.BOARD_WITH_SWIMLANES}` : CSS_CLASSES.BOARD + }); + if (hasSwimlanes) { + const liveLaneValues = [...lanes.keys()].filter((k) => k !== null); + const newLaneValues = liveLaneValues.filter((v) => !this._prefs.swimlaneOrder.includes(v)); + if (newLaneValues.length > 0) { + const isInitialOrder = this._prefs.swimlaneOrder.length === 0; + if (isInitialOrder) { + this._prefs.swimlaneOrder = this._sortSwimlaneValues(newLaneValues); + } else { + this._prefs.swimlaneOrder = [...this._prefs.swimlaneOrder, ...newLaneValues]; + } + this._persistPrefs(); + } + const orderedLanes = this.getOrderedSwimlaneValues(liveLaneValues); + orderedLanes.forEach((laneValue) => { + const laneEntries = lanes.get(laneValue) ?? /* @__PURE__ */ new Map(); + const laneEl = this._buildSwimlaneElement(laneValue, laneEntries, orderedColumnValues); + boardEl.appendChild(laneEl); + const bodyEl = laneEl.querySelector(`.${CSS_CLASSES.SWIMLANE_BODY}`); + if (bodyEl) this.swimlaneColumnSortables.set(laneValue, this._createColumnSortable(bodyEl)); + }); + this.initializeSwimlaneSortable(boardEl); + } else { + const colEntries = lanes.get(null) ?? /* @__PURE__ */ new Map(); + orderedColumnValues.forEach((colValue) => { + const colEl = this.createColumn(colValue, colEntries.get(colValue) ?? []); + boardEl.appendChild(colEl); + const cardBody = colEl.querySelector( + `.${CSS_CLASSES.COLUMN_BODY}[${DATA_ATTRIBUTES.SORTABLE_CONTAINER}]` + ); + if (cardBody) this.attachCardSortable(cardBody, this.cardOrderKey(null, colValue)); + }); + this.swimlaneColumnSortables.set(null, this._createColumnSortable(boardEl)); + } + } + _buildRowCtx() { + return { + ...this._buildColumnCtx(), + collapsedLanes: this._prefs.collapsedLanes + }; + } + _buildRowCallbacks() { + return { + ...this._buildColumnCallbacks(), + onToggleCollapsed: (laneVal, laneEl, toggleBtn) => this.toggleSwimlaneCollapsed(laneVal, laneEl, toggleBtn), + attachCardSortable: (body, key) => this.attachCardSortable(body, key), + cardOrderKey: (laneVal, colVal) => this.cardOrderKey(laneVal, colVal) + }; + } + _buildSwimlaneElement(laneValue, laneEntries, orderedColumnValues) { + return buildSwimlaneElement( + laneValue, + laneEntries, + orderedColumnValues, + this._buildRowCtx(), + this._buildRowCallbacks() + ); + } + _createColumnSortable(containerEl) { + return new sortable_esm_default(containerEl, { + animation: SORTABLE_CONFIG.ANIMATION_DURATION, + handle: `.${CSS_CLASSES.COLUMN_DRAG_HANDLE}`, + draggable: `.${CSS_CLASSES.COLUMN}`, + ghostClass: CSS_CLASSES.COLUMN_GHOST, + dragClass: CSS_CLASSES.COLUMN_DRAGGING, + onStart: () => { + this._dragging = true; + }, + onEnd: (evt) => { + this._dragging = false; + try { + this.handleSwimlaneColumnDrop(evt); + } catch (error) { + console.error("KanbanView: error handling column drop", error); + } + } + }); + } + initializeSwimlaneSortable(boardEl) { + if (this.swimlaneSortable) { + this.swimlaneSortable.destroy(); + this.swimlaneSortable = null; + } + this.swimlaneSortable = new sortable_esm_default(boardEl, { + animation: SORTABLE_CONFIG.ANIMATION_DURATION, + handle: `.${CSS_CLASSES.SWIMLANE_DRAG_HANDLE}`, + draggable: `.${CSS_CLASSES.SWIMLANE}`, + ghostClass: CSS_CLASSES.SWIMLANE_GHOST, + dragClass: CSS_CLASSES.SWIMLANE_DRAGGING, + onStart: () => { + this._dragging = true; + }, + onEnd: () => { + this._dragging = false; + this.handleSwimlaneDrop(boardEl); + } + }); + } + handleSwimlaneDrop(boardEl) { + const lanes = boardEl.querySelectorAll(`.${CSS_CLASSES.SWIMLANE}`); + const order = Array.from(lanes).map((lane) => lane.getAttribute(DATA_ATTRIBUTES.SWIMLANE_VALUE)).filter((v) => v !== null); + this._prefs.swimlaneOrder = order; + this._persistPrefs(); + } + handleSwimlaneColumnDrop(evt) { + if (!this._prefsPropertyId || !evt.to.instanceOf(HTMLElement)) return; + const order = Array.from(evt.to.children).filter( + (child) => child.instanceOf(HTMLElement) && child.classList.contains(CSS_CLASSES.COLUMN) + ).map((col) => col.getAttribute(DATA_ATTRIBUTES.COLUMN_VALUE)).filter((v) => v !== null); + if (order.length === 0) return; + this._prefs.columnOrder = order; + this._persistPrefs(); + this.render(); + } + patchBoard(orderedColumnValues, lanes, hasSwimlanes) { + const boardEl = this.containerEl.querySelector(`.${CSS_CLASSES.BOARD}`); + if (!boardEl) { + console.error("KanbanView: patchBoard called but board element not found; skipping patch"); + return; + } + const scrollPositions = /* @__PURE__ */ new Map(); + boardEl.querySelectorAll(`.${CSS_CLASSES.COLUMN_BODY}`).forEach((body) => { + const colEl = body.closest(`.${CSS_CLASSES.COLUMN}`); + const colVal = colEl?.getAttribute(DATA_ATTRIBUTES.COLUMN_VALUE); + const laneEl = body.closest(`.${CSS_CLASSES.SWIMLANE}`); + const laneVal = laneEl?.getAttribute(DATA_ATTRIBUTES.SWIMLANE_VALUE) ?? null; + if (colVal) scrollPositions.set(this.cardOrderKey(laneVal, colVal), body.scrollTop); + }); + if (hasSwimlanes) { + const liveLaneValues = [...lanes.keys()].filter((k) => k !== null); + const newLaneValues = liveLaneValues.filter((v) => !this._prefs.swimlaneOrder.includes(v)); + if (newLaneValues.length > 0) { + const isInitialOrder = this._prefs.swimlaneOrder.length === 0; + if (isInitialOrder) { + this._prefs.swimlaneOrder = this._sortSwimlaneValues(newLaneValues); + } else { + this._prefs.swimlaneOrder = [...this._prefs.swimlaneOrder, ...newLaneValues]; + } + this._persistPrefs(); + } + const orderedLanes = this.getOrderedSwimlaneValues(liveLaneValues); + const newLaneSet = new Set(orderedLanes); + const existingLanes = /* @__PURE__ */ new Map(); + boardEl.querySelectorAll(`.${CSS_CLASSES.SWIMLANE}`).forEach((laneEl) => { + const val = laneEl.getAttribute(DATA_ATTRIBUTES.SWIMLANE_VALUE); + if (val !== null) existingLanes.set(val, laneEl); + }); + existingLanes.forEach((laneEl, laneValue) => { + if (!newLaneSet.has(laneValue)) { + const colSortable = this.swimlaneColumnSortables.get(laneValue); + if (colSortable) { + colSortable.destroy(); + this.swimlaneColumnSortables.delete(laneValue); + } + orderedColumnValues.forEach((colVal) => { + const key = this.cardOrderKey(laneValue, colVal); + const s = this._columnSortables.get(key); + if (s) { + s.destroy(); + this._columnSortables.delete(key); + } + }); + laneEl.remove(); + existingLanes.delete(laneValue); + } + }); + orderedLanes.forEach((laneValue) => { + const laneEntries = lanes.get(laneValue) ?? /* @__PURE__ */ new Map(); + if (!existingLanes.has(laneValue)) { + const laneEl = this._buildSwimlaneElement(laneValue, laneEntries, orderedColumnValues); + boardEl.appendChild(laneEl); + existingLanes.set(laneValue, laneEl); + const bodyEl = laneEl.querySelector(`.${CSS_CLASSES.SWIMLANE_BODY}`); + if (bodyEl) { + this.swimlaneColumnSortables.set(laneValue, this._createColumnSortable(bodyEl)); + } else { + console.error("KanbanView: swimlane body element not found; column sorting will be broken", laneValue); + } + } else { + const laneEl = existingLanes.get(laneValue); + if (laneEl) { + const countEl = laneEl.querySelector(`.${CSS_CLASSES.SWIMLANE_COUNT}`); + if (countEl) { + const count = orderedColumnValues.reduce((sum, col) => sum + (laneEntries.get(col)?.length ?? 0), 0); + countEl.textContent = `${count}`; + } + const bodyEl = laneEl.querySelector(`.${CSS_CLASSES.SWIMLANE_BODY}`); + if (bodyEl) this._patchColumns(bodyEl, orderedColumnValues, laneEntries, laneValue); + } + } + }); + orderedLanes.forEach((laneValue) => { + const laneEl = existingLanes.get(laneValue); + if (laneEl) boardEl.appendChild(laneEl); + }); + if (!this.swimlaneSortable) this.initializeSwimlaneSortable(boardEl); + } else { + const colEntries = lanes.get(null) ?? /* @__PURE__ */ new Map(); + this._patchColumns(boardEl, orderedColumnValues, colEntries, null); + } + window.requestAnimationFrame(() => { + try { + boardEl.querySelectorAll(`.${CSS_CLASSES.COLUMN_BODY}`).forEach((body) => { + const colEl = body.closest(`.${CSS_CLASSES.COLUMN}`); + const colVal = colEl?.getAttribute(DATA_ATTRIBUTES.COLUMN_VALUE); + const laneEl = body.closest(`.${CSS_CLASSES.SWIMLANE}`); + const laneVal = laneEl?.getAttribute(DATA_ATTRIBUTES.SWIMLANE_VALUE) ?? null; + if (colVal) { + const top = scrollPositions.get(this.cardOrderKey(laneVal, colVal)); + if (top !== void 0) body.scrollTop = top; + } + }); + } catch (error) { + console.error("KanbanView: error restoring scroll positions", error); + } + }); + } + _patchColumns(containerEl, orderedColumnValues, groupedEntries, laneValue) { + const existingColumns = /* @__PURE__ */ new Map(); + containerEl.querySelectorAll(`.${CSS_CLASSES.COLUMN}`).forEach((col) => { + const val = col.getAttribute(DATA_ATTRIBUTES.COLUMN_VALUE); + if (val !== null) existingColumns.set(val, col); + }); + const newColSet = new Set(orderedColumnValues); + existingColumns.forEach((colEl, colValue) => { + if (!newColSet.has(colValue)) { + const key = this.cardOrderKey(laneValue, colValue); + const s = this._columnSortables.get(key); + if (s) { + s.destroy(); + this._columnSortables.delete(key); + } + colEl.remove(); + existingColumns.delete(colValue); + } + }); + orderedColumnValues.forEach((colValue) => { + const entries = groupedEntries.get(colValue) ?? []; + if (!existingColumns.has(colValue)) { + const options = laneValue !== null ? { showRemoveButton: false, swimlaneValue: laneValue } : {}; + const colEl = this.createColumn(colValue, entries, options); + containerEl.appendChild(colEl); + existingColumns.set(colValue, colEl); + const cardBody = colEl.querySelector( + `.${CSS_CLASSES.COLUMN_BODY}[${DATA_ATTRIBUTES.SORTABLE_CONTAINER}]` + ); + if (cardBody) { + const key = this.cardOrderKey(laneValue, colValue); + const attachOnce = () => { + this.attachCardSortable(cardBody, key); + this._deferredSortableListeners.delete(key); + cardBody.removeEventListener("pointerdown", attachOnce); + }; + cardBody.addEventListener("pointerdown", attachOnce); + this._deferredSortableListeners.set(key, { + el: cardBody, + handler: attachOnce + }); + } else { + console.warn("KanbanView: column body not found for new column; card drag will not work", colValue); + } + } else { + const colEl = existingColumns.get(colValue); + if (colEl) this.patchColumnCards(colEl, entries); + } + }); + orderedColumnValues.forEach((colValue) => { + const colEl = existingColumns.get(colValue); + if (colEl) containerEl.appendChild(colEl); + }); + } + _computeCardFingerprint(entry) { + return computeCardFingerprint(entry, this._buildCardCtx()); + } + patchColumnCards(columnEl, newEntries) { + patchColumnCards(columnEl, newEntries, this._buildColumnCtx(), this._buildColumnCallbacks()); + } + groupEntriesByProperty(entries, propertyId) { + const grouped = /* @__PURE__ */ new Map(); + entries.forEach((entry) => { + try { + const propValue = entry.getValue(propertyId); + const value = normalizePropertyValue(propValue); + const group = ensureGroupExists(grouped, value); + group.push(entry); + } catch (error) { + console.warn("Error processing entry:", entry.file.path, error); + const uncategorizedGroup = ensureGroupExists(grouped, UNCATEGORIZED_LABEL); + uncategorizedGroup.push(entry); + } + }); + return grouped; + } + /** + * Two-axis bucketing: swimlane → column → entries. Entries that fail to read + * either property fall through to UNCATEGORIZED_LABEL on the offending axis. + */ + groupEntriesBySwimlaneAndColumn(entries, swimlanePropertyId, columnPropertyId) { + const grouped = /* @__PURE__ */ new Map(); + const ensureLane = (laneKey) => { + const existing = grouped.get(laneKey); + if (existing) return existing; + const lane = /* @__PURE__ */ new Map(); + grouped.set(laneKey, lane); + return lane; + }; + entries.forEach((entry) => { + let laneKey = UNCATEGORIZED_LABEL; + let columnKey = UNCATEGORIZED_LABEL; + try { + laneKey = normalizePropertyValue(entry.getValue(swimlanePropertyId)); + } catch (error) { + console.warn("Error reading swimlane property for entry:", entry.file.path, error); + } + try { + columnKey = normalizePropertyValue(entry.getValue(columnPropertyId)); + } catch (error) { + console.warn("Error reading column property for entry:", entry.file.path, error); + } + const lane = ensureLane(laneKey); + ensureGroupExists(lane, columnKey).push(entry); + }); + return grouped; + } + toggleSwimlaneCollapsed(laneValue, laneEl, toggleBtn) { + const willCollapse = !this._prefs.collapsedLanes.has(laneValue); + if (willCollapse) this._prefs.collapsedLanes.add(laneValue); + else this._prefs.collapsedLanes.delete(laneValue); + laneEl.classList.toggle(CSS_CLASSES.SWIMLANE_COLLAPSED, willCollapse); + updateSwimlaneToggle(toggleBtn, willCollapse); + this._persistPrefs(); + } + _sortSwimlaneValues(values) { + return sortSwimlaneValues(values); + } + getOrderedSwimlaneValues(liveValues) { + return getOrderedSwimlaneValues(liveValues, this._prefs.swimlaneOrder); + } + /** + * Flatten a lane→column→entries map into the column→entries shape the + * single-axis render path expects, preserving union of column values across + * all lanes so empty cells still render as empty bodies. + */ + flattenLanes(byLane) { + const flat = /* @__PURE__ */ new Map(); + byLane.forEach((columns) => { + columns.forEach((entries, columnValue) => { + const existing = flat.get(columnValue); + if (existing) existing.push(...entries); + else flat.set(columnValue, [...entries]); + }); + }); + return flat; + } + _buildColumnCtx() { + return { + doc: this.containerEl.doc, + card: this._buildCardCtx(), + cardCb: this._buildCardCallbacks(), + prefs: { columnColors: this._prefs.columnColors }, + dragging: this._dragging, + cardFingerprints: this._cardFingerprints + }; + } + _buildColumnCallbacks() { + return { + applyColumnColor: (el, name) => this.applyColumnColor(el, name), + onColorPickerClick: (anchor, col, val) => this.openColorPicker(anchor, col, val), + onRemoveColumn: (val, el) => this.removeColumn(val, el), + createAddButton: (colVal, laneVal) => this.createAddButton(colVal, laneVal), + getQuickAddFolder: () => this.getQuickAddFolder() + }; + } + createColumn(value, entries, options = {}) { + return createColumn(value, entries, options, this._buildColumnCtx(), this._buildColumnCallbacks()); + } + _buildCardCtx() { + return { + app: this.app, + doc: this.containerEl.doc, + groupByPropertyId: this.groupByPropertyId, + cardTitlePropertyId: this.cardTitlePropertyId, + imagePropertyId: this.imagePropertyId, + imageFit: this._lastImageFit ?? "cover", + imageAspectRatio: this._lastImageAspectRatio ?? 0.5, + wrapValues: this._lastWrapValue ?? false, + order: this.config?.getOrder() ?? [], + getDisplayName: (id) => this.config?.getDisplayName(id) ?? id + }; + } + _buildCardCallbacks() { + return { + onHoverPreview: (lt, sp, e, el) => this.triggerHoverPreview(lt, sp, e, el), + onSetActiveCard: (path) => this.setActiveCard(path), + onOpenInBackgroundTab: (file) => this.openInBackgroundTab(file) + }; + } + createCard(entry) { + return createCard(entry, this._buildCardCtx(), this._buildCardCallbacks()); + } + applyColumnColor(columnEl, colorName) { + applyColumnColor(columnEl, colorName); + } + openColorPicker(anchorEl, columnEl, columnValue) { + this.activeColorPicker?.remove(); + this.activeColorPicker = null; + const popover = anchorEl.doc.createElement("div"); + popover.className = CSS_CLASSES.COLUMN_COLOR_POPOVER; + const currentColor = columnEl.getAttribute(DATA_ATTRIBUTES.COLUMN_COLOR); + const noneSwatch = anchorEl.doc.createElement("div"); + noneSwatch.className = `${CSS_CLASSES.COLUMN_COLOR_SWATCH} ${CSS_CLASSES.COLUMN_COLOR_NONE}`; + if (!currentColor) noneSwatch.classList.add(CSS_CLASSES.COLUMN_COLOR_SWATCH_ACTIVE); + noneSwatch.title = "No color"; + noneSwatch.addEventListener("click", () => { + this.applyColumnColor(columnEl, null); + delete this._prefs.columnColors[columnValue]; + this._persistPrefs(); + popover.remove(); + this.activeColorPicker = null; + }); + popover.appendChild(noneSwatch); + for (const color of COLOR_PALETTE) { + const swatch = anchorEl.doc.createElement("div"); + swatch.className = CSS_CLASSES.COLUMN_COLOR_SWATCH; + swatch.style.background = color.cssVar; + swatch.title = color.name; + if (currentColor === color.name) swatch.classList.add(CSS_CLASSES.COLUMN_COLOR_SWATCH_ACTIVE); + swatch.addEventListener("click", () => { + this.applyColumnColor(columnEl, color.name); + this._prefs.columnColors[columnValue] = color.name; + this._persistPrefs(); + popover.remove(); + this.activeColorPicker = null; + }); + popover.appendChild(swatch); + } + const rect = anchorEl.getBoundingClientRect(); + popover.style.top = `${rect.bottom + 4}px`; + popover.style.left = `${rect.left}px`; + anchorEl.doc.body.appendChild(popover); + this.activeColorPicker = popover; + const dismiss = (e) => { + if (e.target instanceof Node && !popover.contains(e.target) && e.target !== anchorEl) { + popover.remove(); + this.activeColorPicker = null; + anchorEl.doc.removeEventListener("click", dismiss); + } + }; + anchorEl.doc.addEventListener("click", dismiss); + } + getQuickAddFolder() { + const raw = this.config?.get("quickAddFolder"); + if (typeof raw !== "string") return null; + const trimmed = raw.trim(); + if (!trimmed) return null; + return (0, import_obsidian5.normalizePath)(trimmed); + } + _buildQuickAddCtx() { + return { + app: this.app, + doc: this.containerEl.doc, + prefsPropertyId: this._prefsPropertyId, + prefsSwimlanePropertyId: this._prefsSwimlanePropertyId, + quickAddFolder: this.getQuickAddFolder() + }; + } + _buildQuickAddCallbacks() { + return { + createFileForView: (path, setFm) => this.createFileForView(path, setFm) + }; + } + createAddButton(columnValue, swimlaneValue) { + return createAddButton(columnValue, swimlaneValue, this._buildQuickAddCtx(), this._buildQuickAddCallbacks()); + } + async createQuickAddCard(title, columnValue, swimlaneValue) { + return createQuickAddCard( + title, + columnValue, + swimlaneValue, + this._buildQuickAddCtx(), + this._buildQuickAddCallbacks() + ); + } + closeNativeNewItemPopover() { + closeNativeNewItemPopover(this.containerEl.doc); + } + detachColumn(value, colEl) { + const sortable = this._columnSortables.get(value); + if (sortable) { + sortable.destroy(); + this._columnSortables.delete(value); + } + colEl.remove(); + } + removeColumn(value, columnEl) { + if (!this._prefsPropertyId) return; + this._prefs.columnOrder = this._prefs.columnOrder.filter((v) => v !== value); + this._persistPrefs(); + this.detachColumn(value, columnEl); + } + attachCardSortable(body, value) { + const sortable = new sortable_esm_default(body, { + group: SORTABLE_GROUP, + animation: SORTABLE_CONFIG.ANIMATION_DURATION, + // require a press-and-hold before drag begins on touch so that + // swiping to scroll a column isn't mistaken for a card drag + delay: SORTABLE_CONFIG.TOUCH_DELAY, + delayOnTouchOnly: true, + touchStartThreshold: SORTABLE_CONFIG.TOUCH_START_THRESHOLD, + // Keep same-column sorting enabled so Sortable can report whether the + // user actually tried to move a card. Sorted boards snap back in + // handleCardDrop after optionally showing an action-specific notice. + sort: true, + dragClass: CSS_CLASSES.CARD_DRAGGING, + ghostClass: CSS_CLASSES.CARD_GHOST, + chosenClass: CSS_CLASSES.CARD_CHOSEN, + onStart: (evt) => { + this._dragging = true; + if (evt.item.instanceOf(HTMLElement)) evt.item.classList.remove(CSS_CLASSES.CARD_HOVER); + }, + onEnd: (evt) => { + this._dragging = false; + this.setActiveCard(null); + void this.handleCardDrop(evt); + } + }); + this._columnSortables.set(value, sortable); + } + async handleCardDrop(evt) { + if (!evt.item.instanceOf(HTMLElement)) { + console.warn("Card element is not an HTMLElement:", evt.item); + return; + } + const cardEl = evt.item; + const entryPath = cardEl.getAttribute(DATA_ATTRIBUTES.ENTRY_PATH); + if (!entryPath) { + console.warn("No entry path found on card"); + return; + } + const columnSelector = `.${CSS_CLASSES.COLUMN}`; + const oldColumnEl = evt.from.closest(columnSelector); + const newColumnEl = evt.to.closest(columnSelector); + if (!newColumnEl || !newColumnEl.instanceOf(HTMLElement)) { + console.warn("Could not find new column element"); + return; + } + const oldColumnValue = oldColumnEl?.instanceOf(HTMLElement) ? oldColumnEl.getAttribute(DATA_ATTRIBUTES.COLUMN_VALUE) : null; + const newColumnValue = newColumnEl.getAttribute(DATA_ATTRIBUTES.COLUMN_VALUE); + if (!newColumnValue) { + console.warn("No column value found"); + return; + } + if (!this._prefsPropertyId) { + console.warn("No group by property ID set"); + return; + } + const swimlaneSelector = `.${CSS_CLASSES.SWIMLANE}`; + const oldLaneEl = evt.from.closest(swimlaneSelector); + const newLaneEl = evt.to.closest(swimlaneSelector); + const swimlaneActive = newLaneEl?.instanceOf(HTMLElement) ?? false; + const oldLaneValue = oldLaneEl?.instanceOf(HTMLElement) ? oldLaneEl.getAttribute(DATA_ATTRIBUTES.SWIMLANE_VALUE) : null; + const newLaneValue = swimlaneActive ? newLaneEl.getAttribute(DATA_ATTRIBUTES.SWIMLANE_VALUE) : null; + const getColumnPaths = (bodyEl) => Array.from(bodyEl.querySelectorAll(`.${CSS_CLASSES.CARD}`)).map((c) => c.instanceOf(HTMLElement) ? c.getAttribute(DATA_ATTRIBUTES.ENTRY_PATH) : null).filter((p) => p !== null); + const oldKey = this.cardOrderKey(oldLaneValue, oldColumnValue ?? ""); + const newKey = this.cardOrderKey(newLaneValue, newColumnValue); + const sortActive = this.hasActiveSort(); + if (oldLaneValue === newLaneValue && oldColumnValue === newColumnValue) { + if (sortActive) { + if (this.didSortableIndexChange(evt)) { + new import_obsidian5.Notice(SORTED_CARD_ORDER_NOTICE, 4e3); + } + this.render(); + return; + } + this._prefs.cardOrders[newKey] = getColumnPaths(evt.to); + this._persistPrefs(); + return; + } + if (!sortActive) { + if (oldColumnEl?.instanceOf(HTMLElement) && oldColumnValue) { + const oldBody = oldColumnEl.querySelector(`.${CSS_CLASSES.COLUMN_BODY}`); + if (oldBody) this._prefs.cardOrders[oldKey] = getColumnPaths(oldBody); + } + this._prefs.cardOrders[newKey] = getColumnPaths(evt.to); + this._persistPrefs(); + } + const entry = this._entryMap.get(entryPath); + if (!entry) { + console.warn("Entry not found for path:", entryPath); + return; + } + if (!this.app?.fileManager) { + console.warn("File manager not available"); + return; + } + try { + const columnValueToSet = newColumnValue === UNCATEGORIZED_LABEL ? "" : newColumnValue; + const columnPropertyName = (0, import_obsidian5.parsePropertyId)(this._prefsPropertyId).name; + const swimlanePropertyId = swimlaneActive ? this._prefsSwimlanePropertyId : null; + const swimlaneCrossed = swimlaneActive && swimlanePropertyId !== null && newLaneValue !== null && oldLaneValue !== newLaneValue; + const swimlanePropertyName = swimlaneCrossed ? (0, import_obsidian5.parsePropertyId)(swimlanePropertyId).name : null; + const swimlaneValueToSet = swimlaneCrossed && newLaneValue !== UNCATEGORIZED_LABEL ? newLaneValue : ""; + await this.app.fileManager.processFrontMatter(entry.file, (frontmatter) => { + if (columnValueToSet === "") { + delete frontmatter[columnPropertyName]; + } else { + frontmatter[columnPropertyName] = columnValueToSet; + } + if (swimlanePropertyName) { + if (swimlaneValueToSet === "") { + delete frontmatter[swimlanePropertyName]; + } else { + frontmatter[swimlanePropertyName] = swimlaneValueToSet; + } + } + }); + } catch (error) { + console.error("Error updating entry property:", error); + this.render(); + } + } + findCardEl(path) { + return Array.from(this.containerEl.querySelectorAll(`.${CSS_CLASSES.CARD}`)).find( + (el) => el.getAttribute(DATA_ATTRIBUTES.ENTRY_PATH) === path + ) ?? null; + } + /** + * Open a file in a new background tab, keeping the kanban as the active leaf. + * + * Obsidian's getLeaf('tab') makes the new tab the visible one in its group, + * so we capture the kanban's leaf, kick off openFile (fire-and-forget), and + * switch the active leaf back synchronously — before the browser repaints — + * so the new tab is never visible to the user. { focus: false } avoids an + * extra focus-driven scroll-into-view; the kanban still becomes the active + * (visible) leaf. + * + * During the leaf swap a transient layout pass clamps column scrollTop on + * image-backed cards (their hasn't decoded, so scrollHeight briefly + * shrinks). We capture column scroll positions and restore them aggressively — + * synchronously plus over several animation frames — so no paint shows the + * clamped state. + */ + openInBackgroundTab(file) { + if (!this.app?.workspace) return; + const scrollPositions = []; + this.containerEl.querySelectorAll(`.${CSS_CLASSES.COLUMN_BODY}`).forEach((body) => { + if (body.scrollTop > 0) scrollPositions.push([body, body.scrollTop]); + }); + const previousLeaf = this.app.workspace.getMostRecentLeaf(); + const newLeaf = this.app.workspace.getLeaf("tab"); + void newLeaf.openFile(file, { active: false }); + if (previousLeaf && previousLeaf !== newLeaf) { + this.app.workspace.setActiveLeaf(previousLeaf, { focus: false }); + } + if (scrollPositions.length === 0) return; + const restore = () => { + scrollPositions.forEach(([body, top]) => { + if (body.scrollTop !== top) body.scrollTop = top; + }); + }; + restore(); + let frames = 4; + const tick = () => { + restore(); + if (--frames > 0) window.requestAnimationFrame(tick); + }; + window.requestAnimationFrame(tick); + } + setActiveCard(path) { + if (this._activeCardPath) { + this.findCardEl(this._activeCardPath)?.classList.remove(CSS_CLASSES.CARD_ACTIVE); + } + this._activeCardPath = path; + if (path) { + this.findCardEl(path)?.classList.add(CSS_CLASSES.CARD_ACTIVE); + } + } + reapplyActiveCard() { + if (!this._activeCardPath) return; + this.findCardEl(this._activeCardPath)?.classList.add(CSS_CLASSES.CARD_ACTIVE); + } + didSortableIndexChange(evt) { + if (evt.oldDraggableIndex !== void 0 || evt.newDraggableIndex !== void 0) { + return evt.oldDraggableIndex !== evt.newDraggableIndex; + } + if (evt.oldIndex !== void 0 || evt.newIndex !== void 0) { + return evt.oldIndex !== evt.newIndex; + } + return false; + } + hasActiveSort() { + const sortConfig = this.config?.getSort(); + if (Array.isArray(sortConfig)) return sortConfig.length > 0; + if (!sortConfig || typeof sortConfig !== "object") return Boolean(sortConfig); + return Object.keys(sortConfig).length > 0; + } + getOrderedColumnValues(liveValues) { + if (!this._prefs.columnOrder.length) return liveValues.sort(); + const newValues = liveValues.filter((v) => !this._prefs.columnOrder.includes(v)); + return [...this._prefs.columnOrder, ...newValues]; + } + applyCardOrder(entries, savedOrder) { + const entryMap = new Map(entries.map((e) => [e.file.path, e])); + const ordered = savedOrder.map((p) => entryMap.get(p)).filter((e) => e !== void 0); + const unsaved = entries.filter((e) => !savedOrder.includes(e.file.path)); + return [...ordered, ...unsaved]; + } + onClose() { + this._debouncedRender.cancel(); + this.destroySortables(); + this.activeColorPicker?.remove(); + this.activeColorPicker = null; + } + /** + * Column state (order and colors) is persisted using BasesViewConfig.set/get + * (https://docs.obsidian.md/Reference/TypeScript+API/BasesViewConfig#Methods) + * rather than Plugin.saveData/loadData + * (https://docs.obsidian.md/Plugins/User+interface/Settings). + * + * Why: Plugin.saveData writes a single plugin-wide plugin.data.json, so all + * bases shared the same column state keyed only by property ID. Using the + * BasesViewConfig API instead means each .base file carries its own state — + * deleting and re-adding the plugin no longer wipes configuration, and two bases + * that group by the same property can have independent column orders and colors. + * + * Migration: versions prior to 0.3.0 wrote to plugin.data.json. The + * legacyData parameter passed from main.ts holds that data. On the first + * render after upgrade, the legacy value is written into the base config via + * set() and subsequent renders use _prefs which is already populated — so + * this migration path is exercised at most once per base. + * + * plugin.data.json is intentionally left in place after migration rather than + * deleted: removing it would be destructive if something went wrong mid-upgrade, + * and the file simply becomes stale once each base has migrated its own state. + */ + static getViewOptions() { + return [ + { + displayName: "Group by", + type: "property", + key: "groupByProperty", + filter: (prop) => !prop.startsWith("file."), + placeholder: "Select property" + }, + { + displayName: "Swimlane by", + type: "property", + key: "swimlaneByProperty", + filter: (prop) => !prop.startsWith("file."), + placeholder: "Optional: horizontal grouping" + }, + { + displayName: "Add card to column folder", + type: "folder", + key: "quickAddFolder", + placeholder: "Required for + button" + }, + { + displayName: "Card title property", + type: "property", + key: "cardTitleProperty", + placeholder: "Default: file name" + }, + { + displayName: "Image property", + type: "property", + key: "imageProperty", + placeholder: "Optional: image link property" + }, + { + displayName: "Image fit", + type: "dropdown", + key: "imageFit", + default: "cover", + options: { cover: "Cover", contain: "Contain" } + }, + { + displayName: "Image aspect ratio", + type: "slider", + key: "imageAspectRatio", + default: 0.5, + min: 0.25, + max: 2.5, + step: 0.05 + }, + { + displayName: "Wrap property values", + type: "toggle", + key: "wrapPropertyValues" + } + ]; + } +}; + +// src/main.ts +var KANBAN_VIEW_TYPE = "kanban-view"; +function parseLegacyData(data) { + if (!isRecord(data)) return null; + if ("columnOrders" in data && isColumnOrders(data.columnOrders)) { + return { + columnOrders: data.columnOrders, + columnColors: isColumnColors(data.columnColors) ? data.columnColors : {} + }; + } + if (isColumnOrders(data)) { + return { + columnOrders: data, + columnColors: {} + }; + } + return null; +} +var KanbanBasesViewPlugin = class extends import_obsidian6.Plugin { + async onload() { + const raw = await this.loadData(); + const legacyData = parseLegacyData(raw); + this.registerHoverLinkSource(HOVER_LINK_SOURCE_ID, { + display: "Kanban", + defaultMod: true + }); + this.registerBasesView(KANBAN_VIEW_TYPE, { + name: "Kanban", + icon: "columns", + factory: (controller, scrollEl2) => { + return new KanbanView(controller, scrollEl2, legacyData); + }, + options: KanbanView.getViewOptions + }); + } + onunload() { + } +}; +/*! Bundled license information: + +sortablejs/modular/sortable.esm.js: + (**! + * Sortable 1.15.6 + * @author RubaXa + * @author owenm + * @license MIT + *) +*/ + +/* nosourcemap */ \ No newline at end of file diff --git a/docs/.obsidian/plugins/kanban-bases-view/manifest.json b/docs/.obsidian/plugins/kanban-bases-view/manifest.json new file mode 100644 index 0000000..79d42ff --- /dev/null +++ b/docs/.obsidian/plugins/kanban-bases-view/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "kanban-bases-view", + "name": "Kanban Bases View", + "version": "0.10.1", + "minAppVersion": "1.10.2", + "description": "A kanban-style drag-and-drop custom view for Bases.", + "author": "I. Welch Canavan", + "authorUrl": "https://welchcanavan.com", + "isDesktopOnly": false +} diff --git a/docs/.obsidian/plugins/kanban-bases-view/styles.css b/docs/.obsidian/plugins/kanban-bases-view/styles.css new file mode 100644 index 0000000..74c99e0 --- /dev/null +++ b/docs/.obsidian/plugins/kanban-bases-view/styles.css @@ -0,0 +1,617 @@ +/* Kanban View Container */ +.obk-view-container { + container-type: inline-size; + height: 100%; + display: flex; + flex-direction: column; + padding: 10px; + overflow: hidden; +} + +/* Property Selector */ +.obk-property-selector { + margin-bottom: 15px; + padding: 10px; + background: var(--background-secondary); + border-radius: 6px; + display: flex; + align-items: center; + gap: 10px; +} + +.obk-property-label { + font-weight: 500; + color: var(--text-normal); +} + +.obk-property-select { + padding: 6px 12px; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + background: var(--background-primary); + color: var(--text-normal); + font-size: 14px; + cursor: pointer; +} + +.obk-property-select:hover { + border-color: var(--interactive-hover); +} + +.obk-property-select:focus { + outline: 2px solid var(--interactive-accent); + outline-offset: 2px; +} + +/* Empty State */ +.obk-empty-state { + padding: 40px 20px; + text-align: center; + color: var(--text-muted); + font-style: italic; +} + +/* Kanban Board */ +.obk-board { + display: flex; + gap: 15px; + overflow-x: auto; + overflow-y: hidden; + flex: 1; + padding-bottom: 10px; +} + +.obk-board::-webkit-scrollbar { + height: 8px; +} + +.obk-board::-webkit-scrollbar-track { + background: var(--background-secondary); + border-radius: 4px; +} + +.obk-board::-webkit-scrollbar-thumb { + background: var(--background-modifier-border); + border-radius: 4px; +} + +.obk-board::-webkit-scrollbar-thumb:hover { + background: var(--background-modifier-border-hover); +} + +/* Swimlane mode: stack lanes vertically. The lane body becomes the + horizontal column flex (replacing what .obk-board does in flat mode). */ +.obk-board--with-swimlanes { + flex-direction: column; + overflow-x: hidden; + overflow-y: auto; + gap: 12px; +} + +.obk-swimlane { + display: flex; + flex-direction: column; + background: var(--background-secondary-alt); + border-radius: 8px; + border: 1px solid var(--background-modifier-border); + overflow: hidden; +} + +.obk-swimlane-header { + padding: 8px 14px; + background: var(--background-primary-alt); + border-bottom: 1px solid var(--background-modifier-border); + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; + font-size: 13px; + color: var(--text-normal); + text-transform: capitalize; + flex-shrink: 0; +} + +.obk-swimlane-count { + font-size: 12px; + color: var(--text-muted); + background: var(--background-modifier-border); + padding: 2px 8px; + border-radius: 12px; + font-weight: 500; +} + +.obk-swimlane-body { + display: flex; + align-items: stretch; + gap: 12px; + overflow-x: auto; + overflow-y: visible; + padding: 12px; + min-height: 140px; +} + +/* In swimlane mode, each lane grows tall enough to fit the fullest column, + and shorter column bodies stretch to that height so their Sortable drop + target spans the whole lane row. */ +.obk-board--with-swimlanes .obk-column { + min-height: 0; + max-height: none; + height: auto; + overflow: visible; + align-self: stretch; +} + +.obk-board--with-swimlanes .obk-column-body { + flex: 1 1 auto; + max-height: none; + overflow-y: visible; + min-height: 0; +} + +/* The outer container caps height in flat mode; release it in swimlane mode + so the board grows to fit all lanes and the parent scroll-area scrolls. */ +.obk-view-container--with-swimlanes { + overflow: visible; + height: auto; +} + +/* Collapsed lane: cap the column body at about 30% less than the original + 420px height and scroll within the + column. The lane and column themselves stay flexible — only the card + container is capped. */ +.obk-swimlane--collapsed .obk-column-body { + max-height: 294px; + overflow-y: auto; +} + +.obk-swimlane-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + color: var(--text-muted); + background: var(--background-modifier-border); + border: 0; + border-radius: 4px; + cursor: pointer; + user-select: none; + --icon-size: var(--icon-xs); + transition: + background 0.1s ease, + color 0.1s ease; +} + +.obk-swimlane-toggle:hover { + color: var(--text-normal); + background: var(--background-modifier-border-hover); +} + +.obk-swimlane-toggle:focus-visible { + outline: 2px solid var(--background-modifier-border-focus); + outline-offset: 2px; +} + +.obk-swimlane-drag-handle { + cursor: grab; + padding: 2px 4px; + opacity: 0.5; + user-select: none; + font-size: 14px; + line-height: 1; + color: var(--text-muted); + display: flex; + align-items: center; + flex-shrink: 0; +} + +.obk-swimlane-drag-handle:hover { + opacity: 1; + color: var(--text-normal); +} + +.obk-swimlane-drag-handle:active { + cursor: grabbing; +} + +.obk-swimlane-dragging { + opacity: 0.5; +} + +.obk-swimlane-ghost { + opacity: 0.3; + background: var(--background-modifier-border); +} + +.obk-swimlane-body::-webkit-scrollbar { + height: 8px; +} +.obk-swimlane-body::-webkit-scrollbar-track { + background: transparent; +} +.obk-swimlane-body::-webkit-scrollbar-thumb { + background: var(--background-modifier-border); + border-radius: 4px; +} +.obk-swimlane-body::-webkit-scrollbar-thumb:hover { + background: var(--background-modifier-border-hover); +} + +/* Kanban Column */ +.obk-column { + --obk-column-accent-color: transparent; + flex: 0 0 clamp(200px, 60cqw, 280px); + display: flex; + flex-direction: column; + background: var(--background-secondary); + border-radius: 8px; + border: 1px solid var(--background-modifier-border); + min-height: 200px; + max-height: 100%; + overflow: hidden; +} + +.obk-column-header { + padding: 12px 16px; + background: color-mix(in srgb, var(--obk-column-accent-color, transparent) 15%, var(--background-primary-alt)); + border-bottom: 1px solid var(--background-modifier-border); + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; + gap: 8px; +} + +/* Column color picker button */ +.obk-column-color-btn { + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid var(--background-modifier-border); + background: var(--obk-column-accent-color, transparent); + cursor: pointer; + flex-shrink: 0; + transition: + transform 0.1s ease, + border-color 0.1s ease; +} + +.obk-column-color-btn:hover { + border-color: var(--text-muted); + transform: scale(1.15); +} + +/* Color picker popover */ +.obk-column-color-popover { + position: fixed; + background: var(--background-primary); + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + padding: 8px; + display: flex; + gap: 6px; + flex-wrap: wrap; + width: 164px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 100; +} + +.obk-column-color-swatch { + width: 20px; + height: 20px; + border-radius: 50%; + cursor: pointer; + border: 2px solid transparent; + transition: + transform 0.1s ease, + border-color 0.1s ease; +} + +.obk-column-color-swatch:hover { + transform: scale(1.2); +} + +.obk-column-color-swatch--active { + border-color: var(--text-normal); +} + +.obk-column-color-none { + background: var(--background-modifier-border); + position: relative; +} + +.obk-column-color-none::before, +.obk-column-color-none::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 10px; + height: 1.5px; + background: var(--text-muted); + border-radius: 1px; + transform-origin: center; +} + +.obk-column-color-none::before { + transform: translate(-50%, -50%) rotate(45deg); +} + +.obk-column-color-none::after { + transform: translate(-50%, -50%) rotate(-45deg); +} + +.obk-column-drag-handle { + cursor: grab; + padding: 4px; + opacity: 0.5; + user-select: none; + font-size: 16px; + line-height: 1; + color: var(--text-muted); + display: flex; + align-items: center; +} + +.obk-column-drag-handle:hover { + opacity: 1; + color: var(--text-normal); +} + +.obk-column-drag-handle:active { + cursor: grabbing; +} + +.obk-column-title { + flex: 1; + font-weight: 600; + font-size: 14px; + color: var(--text-normal); + text-transform: capitalize; +} + +.obk-column-count { + font-size: 12px; + color: var(--text-muted); + background: color-mix(in srgb, var(--obk-column-accent-color, transparent) 15%, var(--background-modifier-border)); + padding: 2px 8px; + border-radius: 12px; +} + +.obk-column-add-btn, +.obk-column-remove-btn { + width: 24px; + height: 24px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + cursor: pointer; + flex-shrink: 0; + transition: + background 0.1s ease, + color 0.1s ease, + opacity 0.1s ease; +} + +.obk-column-add-btn { + opacity: 0.55; +} + +.obk-column:hover .obk-column-add-btn, +.obk-column-add-btn:focus-visible { + opacity: 1; +} + +.obk-column-add-btn:hover, +.obk-column-remove-btn:hover { + color: var(--text-normal); + background: var(--background-modifier-hover); +} + +.obk-column-add-btn .svg-icon { + width: 16px; + height: 16px; +} + +.obk-column-remove-btn { + font-size: 18px; + line-height: 1; +} + +.obk-column-body { + flex: 1; + overflow-y: auto; + padding: 10px; + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; +} + +.obk-column-body::-webkit-scrollbar { + width: 6px; +} + +.obk-column-body::-webkit-scrollbar-track { + background: transparent; +} + +.obk-column-body::-webkit-scrollbar-thumb { + background: var(--background-modifier-border); + border-radius: 3px; +} + +.obk-column-body::-webkit-scrollbar-thumb:hover { + background: var(--background-modifier-border-hover); +} + +/* Kanban Card */ +.obk-card { + background: var(--background-primary); + border: 1px solid + color-mix(in srgb, var(--obk-column-accent-color, transparent) 15%, var(--background-modifier-border)); + border-radius: 6px; + padding: 12px; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +/* targets touch-first devices (tablets, phones) to make the kanban genuinely usable on + touch screens — any-pointer: coarse alone would also match hybrid devices (e.g. + touchscreen laptops) where the primary pointer is still a mouse */ +@media (any-pointer: coarse) and (hover: none) { + .obk-card { + user-select: none; + -webkit-user-select: none; + } +} + +.obk-card--hover { + border-color: var(--interactive-accent); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); +} + +.obk-card--active { + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--interactive-accent) 25%, transparent); +} + +.obk-card-cover { + display: block; + /* Bleed the cover to the card's inner border edge. Card has padding: 12px, + so we expand the width by 24px and pull the box out with negative margins. + width: 100% alone only fills the content box and leaves a 12px gap on each side. */ + width: calc(100% + 24px); + margin: -12px -12px 8px -12px; + /* aspect-ratio is set inline from the imageAspectRatio config */ + overflow: hidden; + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} + +.obk-card-cover img { + width: 100%; + height: 100%; + display: block; +} + +.obk-card-cover--fit-cover img { + object-fit: cover; +} + +.obk-card-cover--fit-contain img { + object-fit: contain; + background: var(--background-secondary); +} + +.obk-card-title { + font-weight: 500; + font-size: 14px; + color: var(--text-normal); + line-height: 1.4; + word-wrap: break-word; +} + +.obk-card-preview { + font-size: 12px; + color: var(--text-muted); + line-height: 1.4; + margin-top: 6px; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.obk-card-property { + font-size: var(--font-ui-smaller); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: flex; + gap: 6px; + margin-top: 4px; +} + +.obk-card-property-wrap { + white-space: normal; + text-overflow: initial; +} + +.obk-card-property-wrap .obk-card-property-value { + white-space: normal; + text-overflow: initial; +} + +.obk-card-property-label { + color: var(--text-muted); + flex-shrink: 0; +} + +.obk-card-property-value { + overflow: hidden; + text-overflow: ellipsis; +} + +.obk-card-property-value p { + display: inline; + margin: 0; +} + +.obk-quick-add-form { + display: flex; + flex-direction: column; + gap: 14px; +} + +.obk-quick-add-input { + width: 100%; +} + +.obk-quick-add-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +/* Drag and Drop States */ +.obk-card-dragging { + opacity: 0.5; + transform: rotate(2deg); +} + +.obk-card-ghost { + opacity: 0.3; + background: var(--interactive-accent); + border-color: var(--interactive-accent); +} + +.obk-card-chosen { + cursor: grabbing; + transform: rotate(2deg); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +/* Column Drag and Drop States */ +.obk-column-dragging { + opacity: 0.5; +} + +.obk-column-ghost { + opacity: 0.3; + background: var(--background-modifier-border); +} + +/* Sortable placeholder */ +.obk-sortable-ghost { + opacity: 0.4; + background: var(--interactive-accent); + border: 2px dashed var(--interactive-accent); + border-radius: 6px; +} diff --git a/docs/.obsidian/workspace.json b/docs/.obsidian/workspace.json index a261d38..aed4061 100644 --- a/docs/.obsidian/workspace.json +++ b/docs/.obsidian/workspace.json @@ -102,13 +102,14 @@ "id": "5747e94d63da0f33", "type": "leaf", "state": { - "type": "bases", + "type": "markdown", "state": { - "file": "_Tasks.base", - "viewName": "Table" + "file": "Tasks/Generate a Markdown Website.md", + "mode": "source", + "source": false }, - "icon": "columns", - "title": "_Tasks" + "icon": "lucide-file", + "title": "Generate a Markdown Website" } } ], @@ -276,9 +277,9 @@ }, "active": "5747e94d63da0f33", "lastOpenFiles": [ + "_Tasks.base", "Untitled.md", "Untitled 2.md", - "_Tasks.base", "Tasks/Generate a Markdown Website.md", "Tasks", "Notes/Artificer.md", diff --git a/docs/Notes/Artificer.md b/docs/Notes/Artificer.md new file mode 100644 index 0000000..46f0cf8 --- /dev/null +++ b/docs/Notes/Artificer.md @@ -0,0 +1,6 @@ +--- +category: Role +Role Effect: Can have an additional gear and can spend focus to ready that gear or add a stored item from the supply. +Role Veteran Effect: Can have two additional gear and can spend focus to ready that gear or add a stored item from the supply +Unknown: "4" +--- diff --git a/docs/Notes/Concoliator.md b/docs/Notes/Concoliator.md new file mode 100644 index 0000000..1d25827 --- /dev/null +++ b/docs/Notes/Concoliator.md @@ -0,0 +1,6 @@ +--- +category: Role +Role Effect: Can have any number of companions with you. Can exchange companions with any Ranger nearby during the prepare phase. +Role Veteran Effect: Ignore the effects of collateral damage. Can exchange companions with any Ranger anywhere during the prepare phase. +Unknown: "4" +--- diff --git a/docs/Notes/Explorer.md b/docs/Notes/Explorer.md new file mode 100644 index 0000000..2db0f66 --- /dev/null +++ b/docs/Notes/Explorer.md @@ -0,0 +1,10 @@ +--- +category: Role +Awareness: 2 +Fitnesses: 2 +Knowledge: 1 +Spirit: 1 +Unknown: "4" +Role Effect: Ignores predators when suffering fatigue to continue exploring. +Role Veteran Effect: All nearby Rangers ignore predators when suffering fatigue to continue exploring. +--- diff --git a/docs/Notes/Guide.md b/docs/Notes/Guide.md new file mode 100644 index 0000000..e7043fe --- /dev/null +++ b/docs/Notes/Guide.md @@ -0,0 +1,10 @@ +--- +category: Role +Role Effect: Can help nearby Rangers. +Role Veteran Effect: Can help any Ranger anywhere. +Awareness: 2 +Fitnesses: 2 +Knowledge: 2 +Spirit: 2 +Unknown: "5" +--- diff --git a/docs/Notes/Shaper.md b/docs/Notes/Shaper.md new file mode 100644 index 0000000..97831d3 --- /dev/null +++ b/docs/Notes/Shaper.md @@ -0,0 +1,4 @@ +--- +category: Role +Unknown: "4" +--- diff --git a/docs/Notes/Shepard.md b/docs/Notes/Shepard.md new file mode 100644 index 0000000..1cfb70c --- /dev/null +++ b/docs/Notes/Shepard.md @@ -0,0 +1,10 @@ +--- +category: Role +Awareness: 2 +Fitnesses: 1 +Knowledge: 1 +Spirit: 2 +Unknown: "4" +Role Effect: You can spend spirit to move an equal number of prey and predator from a nearby region to your region. +Role Veteran Effect: You can also move them out of your region to a nearby one. +--- diff --git a/docs/Notes/Trader.md b/docs/Notes/Trader.md new file mode 100644 index 0000000..51058c6 --- /dev/null +++ b/docs/Notes/Trader.md @@ -0,0 +1,10 @@ +--- +category: Role +Awareness: 1 +Fitnesses: 1 +Knowledge: 2 +Spirit: 2 +Unknown: "4" +Role Effect: All gear counts as one higher when trading. And can exchange gear with any nearby Ranger. +Role Veteran Effect: All of your gear counts as two higher when trading. And can exchange gear with any Ranger anywhere. +--- diff --git a/docs/Tasks/Generate a Markdown Website.md b/docs/Tasks/Generate a Markdown Website.md new file mode 100644 index 0000000..a14d4cd --- /dev/null +++ b/docs/Tasks/Generate a Markdown Website.md @@ -0,0 +1,9 @@ +--- +category: Task +status: Working On +--- +Based on the files in docs/Notes, create a document website in the ET/Web Blazor website. + +I should see all the notes listed in pages. This will be in a /docs href. Each Note will be a sub page. Such as /docs/crisis + +Ensure the markdown frontmatter is visible. \ No newline at end of file diff --git a/docs/_Tasks.base b/docs/_Tasks.base new file mode 100644 index 0000000..0379fe9 --- /dev/null +++ b/docs/_Tasks.base @@ -0,0 +1,141 @@ +views: + - type: kanban-view + name: Table + filters: + and: + - category == "Task" + order: + - file.name + - status + columnOrders: + file.file: + - Bases/Event.base + - Bases/Regions.base + - Bases/Terrain.base + - Bases/_Gear.base + - Bases/_Roles.base + - Images/Contents.png + - Images/Event Card Example 2.png + - Images/Event Card Example.png + - Images/Map 1.png + - Images/Map.png + - Images/Market Deck.png + - Images/Market Example.png + - Images/Pasted image 20260609163414.png + - Images/Pasted image 20260609163625.png + - Images/Pasted image 20260609163839.png + - Images/Pasted image 20260609170252.png + - Images/Pasted image 20260609170321.png + - Images/Pasted image 20260609170335.png + - Images/Pasted image 20260609211711.png + - Images/Role Example 2.png + - Images/Role Example.png + - Images/Table MVP Example.png + - Notes/Awareness.md + - Notes/Cache Map.md + - Notes/Card Library.md + - Notes/Contents.md + - Notes/Crisis Markers.md + - Notes/Crisis Resolution.md + - Notes/Crisis.md + - Notes/Dirt Roads.md + - Notes/Dolewood Canoe.md + - Notes/E.03.md + - Notes/Ecology.md + - Notes/Endeavor Tokens.md + - Notes/Energy Tokens.md + - Notes/Event Cards.md + - Notes/Events.md + - Notes/Explore Phase.md + - Notes/Ferinodex.md + - Notes/Flora Meeples.md + - Notes/Forest 1.md + - Notes/Forest 2.md + - Notes/Forest 3.md + - Notes/Forest 4.md + - Notes/Forest 5.md + - Notes/Forest.md + - Notes/Gauzeblade.md + - Notes/Gear Cards.md + - Notes/Grass 1.md + - Notes/Grass 2.md + - Notes/Grass 3.md + - Notes/Grass 4.md + - Notes/Grass 5.md + - Notes/Grasslands.md + - Notes/Hidden Trail Map.md + - Notes/Injury Cards.md + - Notes/Lake.md + - Notes/Losing the Game.md + - Notes/Market.md + - Notes/Mountain 1.md + - Notes/Mountain 2.md + - Notes/Mountain 3.md + - Notes/Mountain 4.md + - Notes/Mountain 5.md + - Notes/Mountain.md + - Notes/Paratrepsis Whistle.md + - Notes/Paved Roads.md + - Notes/Perfect Day 1.md + - Notes/Phonoscopic Headset.md + - Notes/Player Boards.md + - Notes/Player Meeples.md + - Notes/Predator Meeples.md + - Notes/Prepare Phase.md + - Notes/Press On.md + - Notes/Prey Meeples.md + - Notes/Progress.md + - Notes/Ranger Badges.md + - Notes/Ranger Meeples.md + - Notes/Research Station.md + - Notes/Rest.md + - Notes/Role Cards.md + - Notes/Round.md + - Notes/Ruins Map.md + - Notes/Scout.md + - Notes/Story.md + - Notes/Supply.md + - Notes/Terrain 2.md + - Notes/Terrain 3.md + - Notes/Terrain Cards.md + - Notes/Terrain.md + - Notes/Totem of the Irix.md + - Notes/Trade.md + - Notes/Trail Markers.md + - Notes/Travel Phase.md + - Notes/Traverse.md + - Notes/Unmaintained Roads.md + - Notes/Valley.md + - Notes/Victory Condition.md + - Notes/Wasteland 1.md + - Notes/Wasteland.md + - Notes/Water 1.md + - Notes/Water 2.md + - Notes/Water 3.md + - Notes/Water 4.md + - Notes/Water 5.md + - Notes/Weather.md + - Notes/White Sky.md + - Notes/XP Cubes.md + - Notes/XP.md + - Roles/Artificer.md + - Roles/Concoliator.md + - Roles/Explorer.md + - Roles/Guide.md + - Roles/Shaper.md + - Roles/Shepard.md + - Roles/Trader.md + - Untitled.base + - _Overview.md + - Untitled.md + note.status: + - TODO + - Working On + - Done + cardOrders: + file.file: {} + note.status: {} + columnColors: + file.file: {} + note.status: {} + groupByProperty: note.status