From 1d05e18306b77265a83950dbb5d2d0b964161b03 Mon Sep 17 00:00:00 2001 From: 6d486f49 Date: Wed, 17 Jun 2026 22:04:42 -0400 Subject: [PATCH] ...vibing UI --- Fellowship/Build/Build.csproj | 4 + Fellowship/Build/Program.cs | 257 +- Fellowship/Model/BuffDoc.cs | 8 + Fellowship/Model/CharacterDoc.cs | 6 + Fellowship/Model/Class1.cs | 5 - Fellowship/Model/DebuffDoc.cs | 12 + Fellowship/Model/DocEntry.cs | 8 + Fellowship/Model/DocsData.cs | 1024 ++++ Fellowship/Model/KeyDoc.cs | 6 + Fellowship/Model/SkillDoc.cs | 42 + Fellowship/Web/Layout/NavMenu.razor | 5 + Fellowship/Web/Pages/Docs.razor | 331 ++ Fellowship/Web/Pages/Docs.razor.css | 433 ++ Fellowship/Web/Web.csproj | 3 + Fellowship/Web/_Imports.razor | 1 + fellowship.tasks/.obsidian/app.json | 1 + fellowship.tasks/.obsidian/appearance.json | 3 + .../.obsidian/community-plugins.json | 3 + fellowship.tasks/.obsidian/core-plugins.json | 33 + .../plugins/kanban-bases-view/main.js | 4140 +++++++++++++++++ .../plugins/kanban-bases-view/manifest.json | 10 + .../plugins/kanban-bases-view/styles.css | 617 +++ fellowship.tasks/.obsidian/workspace.json | 194 + fellowship.tasks/Generate a Website.md | 20 + fellowship.tasks/_AI Tasks.base | 25 + fellowship.tasks/_Tasks.base | 25 + 26 files changed, 7210 insertions(+), 6 deletions(-) create mode 100644 Fellowship/Model/BuffDoc.cs create mode 100644 Fellowship/Model/CharacterDoc.cs delete mode 100644 Fellowship/Model/Class1.cs create mode 100644 Fellowship/Model/DebuffDoc.cs create mode 100644 Fellowship/Model/DocEntry.cs create mode 100644 Fellowship/Model/DocsData.cs create mode 100644 Fellowship/Model/KeyDoc.cs create mode 100644 Fellowship/Model/SkillDoc.cs create mode 100644 Fellowship/Web/Pages/Docs.razor create mode 100644 Fellowship/Web/Pages/Docs.razor.css create mode 100644 fellowship.tasks/.obsidian/app.json create mode 100644 fellowship.tasks/.obsidian/appearance.json create mode 100644 fellowship.tasks/.obsidian/community-plugins.json create mode 100644 fellowship.tasks/.obsidian/core-plugins.json create mode 100644 fellowship.tasks/.obsidian/plugins/kanban-bases-view/main.js create mode 100644 fellowship.tasks/.obsidian/plugins/kanban-bases-view/manifest.json create mode 100644 fellowship.tasks/.obsidian/plugins/kanban-bases-view/styles.css create mode 100644 fellowship.tasks/.obsidian/workspace.json create mode 100644 fellowship.tasks/Generate a Website.md create mode 100644 fellowship.tasks/_AI Tasks.base create mode 100644 fellowship.tasks/_Tasks.base diff --git a/Fellowship/Build/Build.csproj b/Fellowship/Build/Build.csproj index efceba6..db9b3a3 100644 --- a/Fellowship/Build/Build.csproj +++ b/Fellowship/Build/Build.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/Fellowship/Build/Program.cs b/Fellowship/Build/Program.cs index 837131c..118184e 100644 --- a/Fellowship/Build/Program.cs +++ b/Fellowship/Build/Program.cs @@ -1 +1,256 @@ -Console.WriteLine("Hello, World!"); \ No newline at end of file +using System.Text; +using YamlDotNet.RepresentationModel; +using YamlDotNet.Core; + +// Resolve paths: when run as pre-build from Web project, working dir is Web/ +var solutionDir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..")); +var docsDir = Path.GetFullPath(Path.Combine(solutionDir, "..", "fellowship.docs")); +var outputFile = Path.Combine(solutionDir, "Model", "DocsData.cs"); + +if (!Directory.Exists(docsDir)) +{ + Console.Error.WriteLine($"Docs directory not found: {docsDir}"); + return 1; +} + +var mdFiles = Directory.EnumerateFiles(docsDir, "*.md", SearchOption.AllDirectories) + .OrderBy(f => f) + .ToArray(); + +Console.WriteLine($"Found {mdFiles.Length} markdown files in {docsDir}"); + +var sb = new StringBuilder(); +sb.AppendLine("// "); +sb.AppendLine("// Generated by the Build project. Do not edit manually."); +sb.AppendLine(); +sb.AppendLine("namespace Model;"); +sb.AppendLine(); +sb.AppendLine("public static class DocsData"); +sb.AppendLine("{"); +sb.AppendLine(" public static readonly List All = new()"); +sb.AppendLine(" {"); + +foreach (var file in mdFiles) +{ + var content = File.ReadAllText(file); + var (yamlBlock, body) = ExtractFrontmatter(content); + if (yamlBlock == null) continue; + + var fields = ParseYamlFields(yamlBlock); + var relativePath = Path.GetRelativePath(docsDir, file); + var fileName = Path.GetFileName(file); + var typeName = GetValue(fields, "type") ?? "Unknown"; + + var csharpType = typeName switch + { + "Skill" => "SkillDoc", + "Debuff" => "DebuffDoc", + "Buff" => "BuffDoc", + "Key" or "Mouse" => "KeyDoc", + "Character" => "CharacterDoc", + _ => "SkillDoc" + }; + + sb.AppendLine($" new {csharpType}"); + sb.AppendLine(" {"); + sb.AppendLine($" FileName = {EscapeString(fileName)},"); + sb.AppendLine($" FilePath = {EscapeString(relativePath)},"); + + if (body != null) + sb.AppendLine($" Body = {EscapeString(body.TrimEnd())},"); + + var knownProps = typeName switch + { + "Skill" => GetSkillProperties(fields), + "Debuff" => GetDebuffProperties(fields), + "Buff" => GetBuffProperties(fields), + "Key" or "Mouse" => GetKeyProperties(fields), + "Character" => GetCharacterProperties(fields), + _ => GetSkillProperties(fields) + }; + + foreach (var (propName, propValue) in knownProps) + sb.AppendLine($" {propName} = {propValue},"); + + sb.AppendLine(" },"); +} + +sb.AppendLine(" };"); +sb.AppendLine("}"); + +File.WriteAllText(outputFile, sb.ToString()); +Console.WriteLine($"Generated {outputFile}"); +return 0; + +static (string? yaml, string? body) ExtractFrontmatter(string content) +{ + if (!content.StartsWith("---")) return (null, null); + + var endIndex = content.IndexOf("---", 3, StringComparison.Ordinal); + if (endIndex == -1) return (null, null); + + var yaml = content[3..endIndex].Trim(); + var body = content[(endIndex + 3)..].Trim(); + return (yaml, string.IsNullOrEmpty(body) ? null : body); +} + +static Dictionary ParseYamlFields(string yamlBlock) +{ + var result = new Dictionary(); + try + { + var yamlStream = new YamlStream(); + yamlStream.Load(new StringReader(yamlBlock)); + + if (yamlStream.Documents.Count > 0 && yamlStream.Documents[0].RootNode is YamlMappingNode mapping) + foreach (var (keyNode, valueNode) in mapping) + { + var key = keyNode.ToString(); + result[key] = ConvertYamlNode(valueNode); + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"Warning: Failed to parse YAML: {ex.Message}"); + } + return result; +} + +static object? ConvertYamlNode(YamlNode node) +{ + return node switch + { + YamlScalarNode scalar => scalar.Value, + YamlSequenceNode seq => seq.Children.Select(ConvertYamlNode).ToList(), + YamlMappingNode map => map.ToDictionary(kvp => kvp.Key.ToString(), kvp => ConvertYamlNode(kvp.Value)), + _ => node.ToString() + }; +} + +static string? GetValue(Dictionary fields, string key) +{ + if (fields.TryGetValue(key, out var val) && val is string s && !string.IsNullOrEmpty(s)) + return s; + return null; +} + +static string EscapeString(string? s) +{ + if (s == null) return "null"; + if (s.Contains('\n') || s.Contains('"') || s.Contains('\\')) + { + var escaped = s.Replace("\"", "\"\""); + return $"@\"{escaped}\""; + } + return $"\"{s}\""; +} + +static string EscapeBool(object? val) +{ + if (val is string s) + { + if (bool.TryParse(s, out var b)) return b ? "true" : "false"; + return "null"; + } + return "null"; +} + +static string EscapeStringOrNull(object? val) +{ + if (val is string s) return EscapeString(s); + return "null"; +} + +static string EscapeList(object? val) +{ + if (val is List list && list.Count > 0) + { + var items = list.OfType().Select(EscapeString); + return $"new() {{ {string.Join(", ", items)} }}"; + } + return "null"; +} + +static string EscapeNumericString(object? val) +{ + if (val is string s) return EscapeString(s); + if (val is int i) return EscapeString(i.ToString()); + if (val is double d) return EscapeString(d.ToString("0.##")); + return "null"; +} + +static IEnumerable<(string prop, string value)> GetSkillProperties(Dictionary f) +{ + if (GetValue(f, "character") is { } c) yield return ("Character", EscapeString(c)); + if (f.TryGetValue("cast", out var cast)) yield return ("Cast", EscapeStringOrNull(cast)); + if (f.TryGetValue("description", out var desc)) yield return ("Description", EscapeStringOrNull(desc)); + if (f.TryGetValue("key", out var key)) yield return ("Key", EscapeStringOrNull(key)); + if (f.TryGetValue("range", out var range)) yield return ("Range", EscapeStringOrNull(range)); + if (f.TryGetValue("offGlobalCooldown", out var ogc)) + { + if (ogc is string s && bool.TryParse(s, out var b)) + yield return ("OffGlobalCooldown", b ? "true" : "false"); + else if (ogc is null) + yield return ("OffGlobalCooldown", "true"); + } + if (f.TryGetValue("damage", out var dmg)) yield return ("Damage", EscapeNumericString(dmg)); + if (f.TryGetValue("cooldown", out var cd)) yield return ("Cooldown", EscapeNumericString(cd)); + if (f.TryGetValue("mana", out var mana)) yield return ("Mana", EscapeNumericString(mana)); + if (f.TryGetValue("damageType", out var dt)) yield return ("DamageType", EscapeStringOrNull(dt)); + if (f.TryGetValue("heal", out var heal)) yield return ("Heal", EscapeNumericString(heal)); + if (f.TryGetValue("shield", out var shield)) yield return ("Shield", EscapeNumericString(shield)); + if (f.TryGetValue("order", out var order)) yield return ("Order", EscapeNumericString(order)); + if (f.TryGetValue("priority", out var priority)) yield return ("Priority", EscapeNumericString(priority)); + if (f.TryGetValue("completed", out var completed)) yield return ("Completed", EscapeBool(completed)); + if (f.TryGetValue("gdc", out var gdc)) yield return ("Gdc", EscapeNumericString(gdc)); + if (f.TryGetValue("tags", out var tags)) yield return ("Tags", EscapeList(tags)); + if (f.TryGetValue("related", out var related) || f.TryGetValue("releated", out related)) + yield return ("Related", EscapeList(related)); + if (f.TryGetValue("swiftReprievalChance", out var src)) yield return ("SwiftReprievalChance", EscapeNumericString(src)); + if (f.TryGetValue("areaDamagePercentage", out var adp)) yield return ("AreaDamagePercentage", EscapeNumericString(adp)); + if (f.TryGetValue("generatesSpirit", out var gs)) yield return ("GeneratesSpirit", EscapeNumericString(gs)); + if (f.TryGetValue("duration", out var dur)) yield return ("Duration", EscapeNumericString(dur)); + if (f.TryGetValue("damageReduction", out var dr)) yield return ("DamageReduction", EscapeNumericString(dr)); + if (f.TryGetValue("damageTickTime", out var dtt)) yield return ("DamageTickTime", EscapeNumericString(dtt)); + if (f.TryGetValue("isToggle", out var isTog)) yield return ("IsToggle", EscapeBool(isTog)); + if (f.TryGetValue("manaUpkeepTick", out var mut)) yield return ("ManaUpkeepTick", EscapeNumericString(mut)); + if (f.TryGetValue("parryChance", out var pc)) yield return ("ParryChance", EscapeNumericString(pc)); + if (f.TryGetValue("damageRedirection", out var dredirect)) yield return ("DamageRedirection", EscapeNumericString(dredirect)); + if (f.TryGetValue("spiritCost", out var sc)) yield return ("SpiritCost", EscapeNumericString(sc)); + if (f.TryGetValue("secondEffectDuration", out var sed)) yield return ("SecondEffectDuration", EscapeNumericString(sed)); + if (f.TryGetValue("secondEffectDamageReduction", out var sedr)) yield return ("SecondEffectDamageReduction", EscapeNumericString(sedr)); + if (f.TryGetValue("healingDuration", out var hd)) yield return ("HealingDuration", EscapeNumericString(hd)); + if (f.TryGetValue("healingTickTime", out var htt)) yield return ("HealingTickTime", EscapeNumericString(htt)); + if (f.TryGetValue("costSwiftReprieval", out var csr)) yield return ("CostSwiftReprieval", EscapeNumericString(csr)); + if (f.TryGetValue("gdcDuration", out var gd)) yield return ("GdcDuration", EscapeNumericString(gd)); + if (f.TryGetValue("effect", out var effect)) yield return ("Effect", EscapeStringOrNull(effect)); + if (f.TryGetValue("raw", out var raw)) yield return ("Raw", EscapeStringOrNull(raw)); +} + +static IEnumerable<(string prop, string value)> GetDebuffProperties(Dictionary f) +{ + if (GetValue(f, "character") is { } c) yield return ("Character", EscapeString(c)); + if (f.TryGetValue("maxStacks", out var ms)) yield return ("MaxStacks", EscapeNumericString(ms)); + if (f.TryGetValue("duration", out var dur)) yield return ("Duration", EscapeNumericString(dur)); + if (f.TryGetValue("description", out var desc)) yield return ("Description", EscapeStringOrNull(desc)); + if (f.TryGetValue("parryChanceBonus", out var pcb)) yield return ("ParryChanceBonus", EscapeNumericString(pcb)); + if (f.TryGetValue("manaRestoreBase", out var mrb)) yield return ("ManaRestoreBase", EscapeNumericString(mrb)); + if (f.TryGetValue("manaRestorePerStack", out var mrps)) yield return ("ManaRestorePerStack", EscapeNumericString(mrps)); +} + +static IEnumerable<(string prop, string value)> GetBuffProperties(Dictionary f) +{ + if (GetValue(f, "character") is { } c) yield return ("Character", EscapeString(c)); + if (f.TryGetValue("maxStacks", out var ms)) yield return ("MaxStacks", EscapeNumericString(ms)); + if (f.TryGetValue("description", out var desc)) yield return ("Description", EscapeStringOrNull(desc)); +} + +static IEnumerable<(string prop, string value)> GetKeyProperties(Dictionary f) +{ + if (f.TryGetValue("action", out var action)) yield return ("Action", EscapeStringOrNull(action)); +} + +static IEnumerable<(string prop, string value)> GetCharacterProperties(Dictionary f) +{ + if (GetValue(f, "character") is { } c) yield return ("Character", EscapeString(c)); +} diff --git a/Fellowship/Model/BuffDoc.cs b/Fellowship/Model/BuffDoc.cs new file mode 100644 index 0000000..7dfcf8d --- /dev/null +++ b/Fellowship/Model/BuffDoc.cs @@ -0,0 +1,8 @@ +namespace Model; + +public record BuffDoc : DocEntry +{ + public required string Character { get; init; } + public string? MaxStacks { get; init; } + public string? Description { get; init; } +} diff --git a/Fellowship/Model/CharacterDoc.cs b/Fellowship/Model/CharacterDoc.cs new file mode 100644 index 0000000..d6caa00 --- /dev/null +++ b/Fellowship/Model/CharacterDoc.cs @@ -0,0 +1,6 @@ +namespace Model; + +public record CharacterDoc : DocEntry +{ + public required string Character { get; init; } +} diff --git a/Fellowship/Model/Class1.cs b/Fellowship/Model/Class1.cs deleted file mode 100644 index 5177720..0000000 --- a/Fellowship/Model/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Model; - -public class Class1 -{ -} \ No newline at end of file diff --git a/Fellowship/Model/DebuffDoc.cs b/Fellowship/Model/DebuffDoc.cs new file mode 100644 index 0000000..b6ca5e8 --- /dev/null +++ b/Fellowship/Model/DebuffDoc.cs @@ -0,0 +1,12 @@ +namespace Model; + +public record DebuffDoc : DocEntry +{ + public required string Character { get; init; } + public string? MaxStacks { get; init; } + public string? Duration { get; init; } + public string? Description { get; init; } + public string? ParryChanceBonus { get; init; } + public string? ManaRestoreBase { get; init; } + public string? ManaRestorePerStack { get; init; } +} diff --git a/Fellowship/Model/DocEntry.cs b/Fellowship/Model/DocEntry.cs new file mode 100644 index 0000000..37e37c3 --- /dev/null +++ b/Fellowship/Model/DocEntry.cs @@ -0,0 +1,8 @@ +namespace Model; + +public abstract record DocEntry +{ + public required string FileName { get; init; } + public required string FilePath { get; init; } + public string? Body { get; init; } +} diff --git a/Fellowship/Model/DocsData.cs b/Fellowship/Model/DocsData.cs new file mode 100644 index 0000000..c879571 --- /dev/null +++ b/Fellowship/Model/DocsData.cs @@ -0,0 +1,1024 @@ +// +// Generated by the Build project. Do not edit manually. + +namespace Model; + +public static class DocsData +{ + public static readonly List All = new() + { + new KeyDoc + { + FileName = "G.md", + FilePath = "G.md", + }, + new KeyDoc + { + FileName = "1.md", + FilePath = @"Keys\1.md", + }, + new KeyDoc + { + FileName = "2.md", + FilePath = @"Keys\2.md", + }, + new KeyDoc + { + FileName = "3.md", + FilePath = @"Keys\3.md", + }, + new KeyDoc + { + FileName = "4.md", + FilePath = @"Keys\4.md", + }, + new KeyDoc + { + FileName = "5.md", + FilePath = @"Keys\5.md", + }, + new KeyDoc + { + FileName = "6.md", + FilePath = @"Keys\6.md", + }, + new KeyDoc + { + FileName = "A.md", + FilePath = @"Keys\A.md", + }, + new KeyDoc + { + FileName = "B.md", + FilePath = @"Keys\B.md", + }, + new KeyDoc + { + FileName = "C.md", + FilePath = @"Keys\C.md", + }, + new KeyDoc + { + FileName = "D.md", + FilePath = @"Keys\D.md", + Action = "Move Back", + }, + new KeyDoc + { + FileName = "E.md", + FilePath = @"Keys\E.md", + Body = "Move forward", + Action = "Move Forward", + }, + new KeyDoc + { + FileName = "F.md", + FilePath = @"Keys\F.md", + Action = "Move Right", + }, + new KeyDoc + { + FileName = "Mouse 1.md", + FilePath = @"Keys\Mouse 1.md", + }, + new KeyDoc + { + FileName = "Mouse 2.md", + FilePath = @"Keys\Mouse 2.md", + }, + new KeyDoc + { + FileName = "Mouse 3.md", + FilePath = @"Keys\Mouse 3.md", + }, + new KeyDoc + { + FileName = "Mouse 4.md", + FilePath = @"Keys\Mouse 4.md", + }, + new KeyDoc + { + FileName = "Mouse 5.md", + FilePath = @"Keys\Mouse 5.md", + }, + new KeyDoc + { + FileName = "Shift + 1.md", + FilePath = @"Keys\Shift + 1.md", + }, + new KeyDoc + { + FileName = "Shift + 2.md", + FilePath = @"Keys\Shift + 2.md", + }, + new KeyDoc + { + FileName = "Shift + 3.md", + FilePath = @"Keys\Shift + 3.md", + }, + new KeyDoc + { + FileName = "Shift + 4.md", + FilePath = @"Keys\Shift + 4.md", + }, + new KeyDoc + { + FileName = "Shift + 5.md", + FilePath = @"Keys\Shift + 5.md", + }, + new KeyDoc + { + FileName = "Shift + 6.md", + FilePath = @"Keys\Shift + 6.md", + }, + new KeyDoc + { + FileName = "Shift + A.md", + FilePath = @"Keys\Shift + A.md", + }, + new KeyDoc + { + FileName = "Shift + B.md", + FilePath = @"Keys\Shift + B.md", + }, + new KeyDoc + { + FileName = "Shift + C.md", + FilePath = @"Keys\Shift + C.md", + }, + new KeyDoc + { + FileName = "Shift + D.md", + FilePath = @"Keys\Shift + D.md", + Action = "Move Back", + }, + new KeyDoc + { + FileName = "Shift + E.md", + FilePath = @"Keys\Shift + E.md", + Body = "Move forward", + Action = "Move Forward", + }, + new KeyDoc + { + FileName = "Shift + F.md", + FilePath = @"Keys\Shift + F.md", + Action = "Move Right", + }, + new KeyDoc + { + FileName = "Shift + G.md", + FilePath = @"Keys\Shift + G.md", + }, + new KeyDoc + { + FileName = "Shift + Q.md", + FilePath = @"Keys\Shift + Q.md", + }, + new KeyDoc + { + FileName = "Shift + R.md", + FilePath = @"Keys\Shift + R.md", + }, + new KeyDoc + { + FileName = "Shift + S.md", + FilePath = @"Keys\Shift + S.md", + Action = "Move Left", + }, + new KeyDoc + { + FileName = "Shift + Space.md", + FilePath = @"Keys\Shift + Space.md", + Action = "Jump", + }, + new KeyDoc + { + FileName = "Shift + T.md", + FilePath = @"Keys\Shift + T.md", + }, + new KeyDoc + { + FileName = "Shift + V.md", + FilePath = @"Keys\Shift + V.md", + }, + new KeyDoc + { + FileName = "Shift + W.md", + FilePath = @"Keys\Shift + W.md", + }, + new KeyDoc + { + FileName = "Shift + X.md", + FilePath = @"Keys\Shift + X.md", + }, + new KeyDoc + { + FileName = "Shift + Z.md", + FilePath = @"Keys\Shift + Z.md", + }, + new KeyDoc + { + FileName = "Q.md", + FilePath = "Q.md", + }, + new KeyDoc + { + FileName = "R.md", + FilePath = "R.md", + }, + new SkillDoc + { + FileName = "Brain Freeze.md", + FilePath = @"Rime\Brain Freeze.md", + Body = @"![[Brain Freeze.png]] + +30yd range +20s Cooldown +Interrupt the target's spellcasting and prevent it from casting for 4 sec. +Must interrupt a spell, or it will have no effect. + +Can be used during Global Cooldown", + Character = "Rime", + }, + new SkillDoc + { + FileName = "Bursting Ice.md", + FilePath = @"Rime\Bursting Ice.md", + Body = @"![[Bursting Ice.png]] + +2.00s Cast +30yd range +10s Cooldown +Conjures an icy crystal inside a target that pulses frost damage, dealing 520 - 635 every 0.5 seconds for 3 seconds to the target and nearby enemies. + +Generates 1 Anima each time it pulses + +ANIMA +Your Anima is shown as a bar beneath your character. For every 9 Anima you generate, you gain 1 Winter Orb, used to cast your powerful spender abilities.", + Character = "Rime", + Cooldown = "10", + }, + new SkillDoc + { + FileName = "Cold Snap.md", + FilePath = @"Rime\Cold Snap.md", + Body = @"![[Cold Snap.png]] + +Instant +30yd range +12s Cooldown +Assault the target with extreme cold, dealing 3,283 - 4,012 damage. + +Cold Snap gains Cooldown Acceleration equal to your Haste. + +Generates 1 Winter Orb + +WINTER ORBS +Winter Orbs are required to cast your powerful spender abilities; Glacial Blast, Ice Comet, and Dance of Swallows.", + Character = "Rime", + Cast = "Instant", + Key = "R", + Damage = "3,283 - 4,012", + Cooldown = "12", + Effect = "Generate 1 Winter Orb", + }, + new SkillDoc + { + FileName = "Flight of the Navir.md", + FilePath = @"Rime\Flight of the Navir.md", + Body = @"![[Flight of the Navir.png]] + +Instant +Self +1m Cooldown +Summon 5 Frost Swallows to circle above Rime for 20 seconds. Your [[Cold Snap]] and [[Freezing Torrent]] command the frost swallows to swoop down on enemies, each dealing 612 - 748 damage to their target.", + Character = "Rime", + }, + new SkillDoc + { + FileName = "Freezing Torrent.md", + FilePath = @"Rime\Freezing Torrent.md", + Body = @"![[Freezing Torrent.png]] + +2.00s Channel +30yd range +15s Cooldown +Flay your target with a beam of frost energy, dealing 1,405 - 1,718 damage every 0.4 seconds for 2 seconds while channeling. + +Generates 1 Anima per tick + +ANIMA +Your Anima is shown as a bar beneath your character. For every 9 Anima you generate, you gain 1 Winter Orb, used to cast your powerful spender abilities.", + Character = "Rime", + Key = "G", + }, + new SkillDoc + { + FileName = "Frigid Winds.md", + FilePath = @"Rime\Frigid Winds.md", + Body = @"![[Frigid Winds.png]] + +Instant +Self +1m Cooldown +Blast enemies in front of you with arctic winds, Knocking Back enemies and Slowing them by 50% for 12 sec. + +Can be used during Global Cooldown", + Character = "Rime", + }, + new SkillDoc + { + FileName = "Frost Bolt.md", + FilePath = @"Rime\Frost Bolt.md", + Body = @"![[Frost Bolt.png]] +1.50s Cast +30yd range +Hurl a bolt of frost magic at target enemy, dealing 2,106 - 2,574 damage. + +Generates 1 Anima + +ANIMA +Your Anima is shown as a bar beneath your character. For every 9 Anima you generate, you gain 1 Winter Orb, used to cast your powerful spender abilities.", + Character = "Rime", + Cast = "1.5s", + Key = "T", + Range = "30", + Damage = "2,106 - 2,574", + Effect = "Generate 1 Anima", + }, + new SkillDoc + { + FileName = "Frost Ward.md", + FilePath = @"Rime\Frost Ward.md", + Body = @"![[Frost Ward.png]] + +Instant +Self +30s Cooldown +You take 40% reduced damage for 4 seconds. + +Can be used during Global Cooldown", + Character = "Rime", + }, + new SkillDoc + { + FileName = "Glacial Blast.md", + FilePath = @"Rime\Glacial Blast.md", + Body = @"![[Glacial Blast.png]] + +2.00s Cast +2 Winter Orbs +30yd range +Hurl a mass of ice at target enemy, dealing 10,693 - 13,069 damage.", + Character = "Rime", + }, + new SkillDoc + { + FileName = "Ice Blitz.md", + FilePath = @"Rime\Ice Blitz.md", + Body = @"![[Ice Blitz.png]] + +Instant +Self +2m Cooldown +You enter a state of focused casting for 20 seconds, causing you to deal 20% more damage for the duration. + +Can be used during Global Cooldown and while casting", + Character = "Rime", + }, + new SkillDoc + { + FileName = "Ice Comet.md", + FilePath = @"Rime\Ice Comet.md", + Body = @"![[Ice Comet.png]] + +Instant +2 Winter Orbs +30yd range +Unleash a large Ice Comet from above target enemy to crash down on them, dealing 4,261 - 5,208 damage to all enemies caught in the impact radius.", + Character = "Rime", + }, + new SkillDoc + { + FileName = "Ice Dash.md", + FilePath = @"Rime\Ice Dash.md", + Body = @"![[Ice Dash.png]] + +Instant +Self +25s Cooldown +Quickly travel forward a short distance. + +Can be used during Global Cooldown", + Character = "Rime", + }, + new SkillDoc + { + FileName = "Winter's Blessing.md", + FilePath = @"Rime\Winter's Blessing.md", + Body = @"![[Winter's Blessing.png]] + +Instant +Self +1m Cooldown +Your Spirit is increased by 20% for 20 seconds and 30% of all damage you deal is replicated as healing divided equally between all allies. + +Can be used during Global Cooldown", + Character = "Rime", + }, + new SkillDoc + { + FileName = "Wrath of Winter.md", + FilePath = @"Rime\Wrath of Winter.md", + Body = @"![[Wrath of Winter.png]] + +1.50s Cast +100 Spirit +Self +Invoke the spirits of the frozen tundra for 20 seconds, granting you 1 Winter Orb every 4 seconds. You deal +20% increased damage and your Glacial Blast ability is instant cast while Wrath of Winter is active. + +WINTER ORBS +Winter Orbs are required to cast your powerful spender abilities; Glacial Blast and Ice Comet. + +SPIRIT ABILITY +When you activate your Spirit ability you gain Spirit of Heroism, granting +30% Haste for 20 sec.", + Character = "Rime", + }, + new KeyDoc + { + FileName = "S.md", + FilePath = "S.md", + Action = "Move Left", + }, + new KeyDoc + { + FileName = "Space.md", + FilePath = "Space.md", + Action = "Jump", + }, + new KeyDoc + { + FileName = "T.md", + FilePath = "T.md", + }, + new KeyDoc + { + FileName = "V.md", + FilePath = "V.md", + Body = "Interrupts", + }, + new SkillDoc + { + FileName = "Avatar of Light.md", + FilePath = @"Vigour\Avatar of Light.md", + Body = @"![[Avatar of Light Icon.png]] +![[Avatar of Light.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Circle of Light.md", + FilePath = @"Vigour\Circle of Light.md", + Body = @"![[Circle of Light Icon.png]] + +![[Circle of Light.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Dawnbreaker Orb.md", + FilePath = @"Vigour\Dawnbreaker Orb.md", + Body = @"![[Dawnbreaker Orb Icon.png]] + +![[Dawnbreaker Orb.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Dawnflare.md", + FilePath = @"Vigour\Dawnflare.md", + Body = @"![[Dawnflare Icon.png]] +![[Dawnflare.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Greater Heal.md", + FilePath = @"Vigour\Greater Heal.md", + Body = @"![[Greater Heal Icon.png]] +![[Greater Heal.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Lightshaper's Ward.md", + FilePath = @"Vigour\Lightshaper's Ward.md", + Body = @"![[Lightshaper's Ward Icon.png]] +![[Lightshaper's Ward.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Luminous Barrier.md", + FilePath = @"Vigour\Luminous Barrier.md", + Body = @"![[Luminous Barrier Icon.png]] +![[Luminous Barrier.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Radiant Blast.md", + FilePath = @"Vigour\Radiant Blast.md", + Body = @"![[Radiant Blast Icon.png]] +![[Radiant Blast.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Remove Magic.md", + FilePath = @"Vigour\Remove Magic.md", + Body = @"![[Remove Magic Icon.png]] +![[Remove Magic.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Rune of Renewal.md", + FilePath = @"Vigour\Rune of Renewal.md", + Body = @"![[Rune of Renewal Icon.png]] +![[Rune of Renewal.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Runic Proliferation.md", + FilePath = @"Vigour\Runic Proliferation.md", + Body = @"![[Runic Proliferation Icon.png]] +![[Runic Proliferation.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Soulbrand.md", + FilePath = @"Vigour\Soulbrand.md", + Body = @"![[Soulbrand Icon.png]] +![[Soulbrand.png]]", + Character = "Vigour", + }, + new SkillDoc + { + FileName = "Throw Book.md", + FilePath = @"Vigour\Throw Book.md", + Body = @"![[Throw Book Icon.png]] +![[Throw Book.png]]", + Character = "Vigour", + }, + new KeyDoc + { + FileName = "W.md", + FilePath = "W.md", + }, + new KeyDoc + { + FileName = "X.md", + FilePath = "X.md", + }, + new SkillDoc + { + FileName = "Aura of Solace.md", + FilePath = @"Xavian\Aura of Solace.md", + Body = "![[Pasted image 20260605125121.png]]", + Character = "Xavian", + Cast = "Instant", + Description = @"Burn all enemies within a large radius for X magical damage every 1.5 seconds while active and increases your Parry chance by +8%. + +Aura of Solace causes 10% of all damage taken by nearby allies to be redirected to you.", + Key = "1", + Range = "0", + OffGlobalCooldown = true, + Damage = "342", + Cooldown = "3", + Mana = "10", + DamageType = "Magic", + Heal = "0", + Shield = "0", + Order = "500", + Priority = "500", + Completed = true, + Tags = new() { "Aura" }, + DamageTickTime = "1.5", + IsToggle = true, + ManaUpkeepTick = "1.5", + ParryChance = "0.8", + DamageRedirection = "0.1", + }, + new DebuffDoc + { + FileName = "Blind.md", + FilePath = @"Xavian\Blind.md", + Character = "Xavian", + MaxStacks = "1", + Duration = "8", + Description = "Increases chance for enemy auto attacks to be parried by 20%. Consumed on parry.", + ParryChanceBonus = "0.2", + }, + new SkillDoc + { + FileName = "Blinding Slash.md", + FilePath = @"Xavian\Blinding Slash.md", + Body = "![[Pasted image 20260605112156.png]]", + Character = "Xavian", + Cast = "Instant", + Description = @"Your sword is infused with the light of sun power as you slash at target enemy, dealing X physical damage to them and other enemies near it. + +Blind + +Swift Reprieval", + Key = "G", + Range = "5", + Damage = "1269", + Cooldown = "6", + Mana = "0", + DamageType = "Physical", + Heal = "0", + Shield = "0", + Order = "3", + Priority = "1", + Completed = true, + Gdc = "3", + Tags = new() { "Defensive" }, + Related = new() { "[[Blind]]" }, + SwiftReprievalChance = "0.15", + AreaDamagePercentage = "1.0", + Raw = "", + }, + new SkillDoc + { + FileName = "Brilliant Flare.md", + FilePath = @"Xavian\Brilliant Flare.md", + Body = "![[Pasted image 20260605114542.png]]", + Character = "Xavian", + Cast = "1.5s", + Description = "Heal target all for X health.", + Key = "W", + Range = "30", + Damage = "0", + Cooldown = "0", + Mana = "132", + DamageType = "", + Heal = "12510", + Shield = "0", + Order = "5", + Priority = "999", + Completed = true, + Gdc = "1", + Tags = new() { "Healing" }, + }, + new SkillDoc + { + FileName = "Brilliant Flash.md", + FilePath = @"Xavian\Brilliant Flash.md", + Body = "![[Pasted image 20260605113647.png]]", + Character = "Xavian", + Cast = "Instant", + Description = @"Instantly heal target ally for X health or deal X damage to target enemy. + +When you target an enemy with Brilliant Flash, you also heal yourself for X health. + +In all situations, Brilliant Flash heals you for 20% more than others.", + Key = "W", + Range = "30", + Damage = "2511", + Cooldown = "0", + Mana = "0", + DamageType = "Magic", + Heal = "12510", + Shield = "0", + Order = "5", + Priority = "0", + Completed = true, + Gdc = "1", + Tags = new() { "Healing" }, + CostSwiftReprieval = "1", + }, + new SkillDoc + { + FileName = "Decree of the Sun.md", + FilePath = @"Xavian\Decree of the Sun.md", + Body = "![[Pasted image 20260605122346.png]]", + Character = "Xavian", + Cast = "Instant", + Description = @"Envelope yourself in solar power, making you Immune to all damage for 3 seconds, ending with an explosion of light that deals X magical damage to nearby enemies. + +Once the immunity fades, you take 50% reduced damage for another 5 seconds. + +During Decree of the Sun you radiate solar magic in the area, healing nearby players for X every second. + +Spirit Ability", + Key = "Shift + Q", + Range = "", + OffGlobalCooldown = true, + Damage = "5787", + Cooldown = "0", + Mana = "0", + DamageType = "Magic", + Heal = "2556", + Shield = "0", + Order = "500", + Priority = "500", + Completed = true, + Tags = new() { "Ultimate" }, + Duration = "3", + DamageReduction = "1", + SpiritCost = "100", + SecondEffectDuration = "5", + SecondEffectDamageReduction = "0.5", + HealingDuration = "7", + HealingTickTime = "1", + }, + new SkillDoc + { + FileName = "Interupt.md", + FilePath = @"Xavian\Interupt.md", + Body = "![[Pasted image 20260605110650.png]]", + Character = "Xavian", + Cast = "Instant", + Description = @"Interrupt the target's spellcasting and prevent it from casting for 4 seconds. + +Must interrupt a spell, or it will have no effect.", + Key = "V", + Range = "5", + OffGlobalCooldown = true, + Damage = "0", + Cooldown = "12", + Mana = "0", + Heal = "0", + Shield = "0", + Order = "6", + Priority = "0", + Completed = true, + Gdc = "8", + Tags = new() { "Interrupt" }, + }, + new SkillDoc + { + FileName = "Omega Reprieval.md", + FilePath = @"Xavian\Omega Reprieval.md", + Body = "![[Pasted image 20260605124858.png]]", + Character = "Xavian", + Cast = "Instant", + Description = "Instantly gain 2 stacks of Omega Reprieveal, causing your Brilliant Flare to turn into Brilliant Flash with added enhancements when used to consume the 2 stacks.", + OffGlobalCooldown = true, + Cooldown = "60", + }, + new SkillDoc + { + FileName = "Omnistrike.md", + FilePath = @"Xavian\Omnistrike.md", + Body = "![[Pasted image 20260605112712.png]]", + Character = "Xavian", + Cast = "Instant", + Description = @"Swing your sword in a large arc around you, dealing X physical damage to all enemies around you. + +Generates 4 Spirit Points. + +Swift Reprieval.", + Key = "Shift + R", + Range = "0", + Damage = "2538", + Cooldown = "18", + Mana = "0", + DamageType = "Physical", + Heal = "0", + Shield = "0", + Order = "2", + Priority = "1", + Completed = true, + Gdc = "12", + SwiftReprievalChance = "0.3", + AreaDamagePercentage = "1.0", + GeneratesSpirit = "4", + }, + new SkillDoc + { + FileName = "Ruptured Dawn.md", + FilePath = @"Xavian\Ruptured Dawn.md", + Body = "![[Pasted image 20260605125057.png]]", + Character = "Xavian", + Cast = "Instant", + Description = "Rupture the ground in front of you, dealing X magic damage to all enemies in a case in front of you, Stunning them for 2 seconds and Pulling them to you.", + Key = "", + Range = "0", + OffGlobalCooldown = true, + Damage = "702", + Cooldown = "60", + Mana = "0", + DamageType = "Magic", + Heal = "0", + Shield = "0", + Order = "300", + Priority = "300", + Completed = true, + Gdc = "40", + Tags = new() { "Stun" }, + Related = new() { "Stunning", "Pulling" }, + AreaDamagePercentage = "1.0", + Duration = "2", + }, + new SkillDoc + { + FileName = "Shinning Halo.md", + FilePath = @"Xavian\Shinning Halo.md", + Body = "![[Pasted image 20260605120928.png]]", + Character = "Xavian", + Cast = "Instant", + Description = @"Sunlight envelopers the ground at your feet, burning enemies within its area fir 288 magic damage for every 1.5 seconds over 12 seconds. + +You take 20% reduced amage with standing in your Shining Halo. + +Brilliance", + Key = "Q", + Range = "0", + OffGlobalCooldown = true, + Damage = "288", + Cooldown = "30", + Mana = "218", + DamageType = "Magic", + Heal = "0", + Shield = "0", + Order = "7", + Priority = "1", + Completed = true, + Gdc = "20", + Tags = null, + AreaDamagePercentage = "1.0", + Duration = "12", + DamageReduction = "0.2", + DamageTickTime = "1.5", + GdcDuration = "8", + }, + new SkillDoc + { + FileName = "Sky Crash.md", + FilePath = @"Xavian\Sky Crash.md", + Body = "![[Pasted image 20260605114913.png]]", + Character = "Xavian", + Cast = "Instant", + Description = "Instantly ascend to the skies in a beam of light and crash down at the targeted location, dealing 558 magical damage to enemies in the area.", + Key = "T", + Range = "", + OffGlobalCooldown = true, + Damage = "558", + Cooldown = "60", + Mana = "0", + DamageType = "Magic", + Heal = "0", + Shield = "0", + Order = "999", + Priority = "999", + Completed = true, + Tags = new() { "Travel" }, + }, + new SkillDoc + { + FileName = "Solar Blades.md", + FilePath = @"Xavian\Solar Blades.md", + Body = "![[Pasted image 20260605113024.png]]", + Character = "Xavian", + Cast = "Instant", + Description = @"Conjure blades of pure sunlight to fall from above upon your target, dealing X magical damage to it and other enemies within a 700 radius. + +The main target takes 100% more damage from Solar Blades. + +Swift Reprieval", + Key = "Shift + G", + Range = "30", + Damage = "1674", + Cooldown = "9", + Mana = "55", + DamageType = "Magic", + Heal = "0", + Shield = "0", + Order = "4", + Priority = "1", + Completed = true, + Gdc = "6", + Related = new() { "[[Swift Reprival]]" }, + SwiftReprievalChance = "1", + AreaDamagePercentage = "1.0", + }, + new SkillDoc + { + FileName = "Solar Shield.md", + FilePath = @"Xavian\Solar Shield.md", + Body = "![[Pasted image 20260605123320.png]]", + Character = "Xavian", + Cast = "Instant", + Description = @"Instantly shield the target, absorbing up to X damage. This shield lasts for up to 8 seconds. + +While Solar Shield is active, the target takes 20% reduced damage.", + Key = "A", + Range = "", + OffGlobalCooldown = true, + Damage = "0", + Cooldown = "15", + Mana = "182", + DamageType = "", + Heal = "0", + Shield = "38330", + Order = "999", + Priority = "999", + Completed = true, + Gdc = "10", + Tags = new() { "Shield" }, + Duration = "8", + DamageReduction = "0.2", + GdcDuration = "5", + }, + new SkillDoc + { + FileName = "Sun Strike.md", + FilePath = @"Xavian\Sun Strike.md", + Body = "![[Pasted image 20260605132922.png]]", + Character = "Xavian", + Cast = "Instant", + Description = @"Strike your target, dealing X physical damage to it as well as strike enemies near your target with 30% of the power. + +Sun Strike applies [[Sunstruck]] to your main target for 20 seconds, stacking up to 10 times. + +Sunstruck + +Swift Reprieval", + Key = "R", + Range = "5", + Damage = "1440", + Cooldown = "0", + Mana = "0", + DamageType = "Physical", + Heal = "0", + Shield = "0", + Order = "1", + Priority = "999", + Completed = true, + Gdc = "1", + Tags = new() { "Filler" }, + Related = new() { "[[Sun Struck]]", "[[Swift Reprival]]" }, + SwiftReprievalChance = "0.15", + AreaDamagePercentage = "0.3", + }, + new DebuffDoc + { + FileName = "Sun Struck.md", + FilePath = @"Xavian\Sun Struck.md", + Character = "Xavian", + MaxStacks = "10", + Duration = "20", + Description = "Consumed by your next Omnistrike to replenish mana. 100 mana base + 50 per additional stack.", + ManaRestoreBase = "100", + ManaRestorePerStack = "50", + }, + new BuffDoc + { + FileName = "Swift Reprival.md", + FilePath = @"Xavian\Swift Reprival.md", + Character = "Xavian", + MaxStacks = "3", + Description = "Converts Brilliant Flash into Brilliant Flare and vice versa. Consumed by Brilliant Flare.", + }, + new SkillDoc + { + FileName = "Taunt.md", + FilePath = @"Xavian\Taunt.md", + Body = "![[Pasted image 20260605120732.png]]", + Character = "Xavian", + Cast = "Instant", + Description = "Taunt the target, forcing it to attack you for 6 sec.", + Key = "B", + Range = "40", + OffGlobalCooldown = true, + Damage = "0", + Cooldown = "8", + Mana = "0", + DamageType = "", + Heal = "0", + Shield = "0", + Order = "6", + Priority = "0", + Completed = true, + Tags = new() { "Taunt" }, + }, + new CharacterDoc + { + FileName = "Xavian.md", + FilePath = @"Xavian\Xavian.md", + Character = "Xavian", + }, + new KeyDoc + { + FileName = "Z.md", + FilePath = "Z.md", + Body = "Horse", + }, + }; +} diff --git a/Fellowship/Model/KeyDoc.cs b/Fellowship/Model/KeyDoc.cs new file mode 100644 index 0000000..782b6b3 --- /dev/null +++ b/Fellowship/Model/KeyDoc.cs @@ -0,0 +1,6 @@ +namespace Model; + +public record KeyDoc : DocEntry +{ + public string? Action { get; init; } +} diff --git a/Fellowship/Model/SkillDoc.cs b/Fellowship/Model/SkillDoc.cs new file mode 100644 index 0000000..4927a09 --- /dev/null +++ b/Fellowship/Model/SkillDoc.cs @@ -0,0 +1,42 @@ +namespace Model; + +public record SkillDoc : DocEntry +{ + public required string Character { get; init; } + public string? Cast { get; init; } + public string? Description { get; init; } + public string? Key { get; init; } + public string? Range { get; init; } + public bool? OffGlobalCooldown { get; init; } + public string? Damage { get; init; } + public string? Cooldown { get; init; } + public string? Mana { get; init; } + public string? DamageType { get; init; } + public string? Heal { get; init; } + public string? Shield { get; init; } + public string? Order { get; init; } + public string? Priority { get; init; } + public bool? Completed { get; init; } + public string? Gdc { get; init; } + public List? Tags { get; init; } + public List? Related { get; init; } + public string? SwiftReprievalChance { get; init; } + public string? AreaDamagePercentage { get; init; } + public string? GeneratesSpirit { get; init; } + public string? Duration { get; init; } + public string? DamageReduction { get; init; } + public string? DamageTickTime { get; init; } + public bool? IsToggle { get; init; } + public string? ManaUpkeepTick { get; init; } + public string? ParryChance { get; init; } + public string? DamageRedirection { get; init; } + public string? SpiritCost { get; init; } + public string? SecondEffectDuration { get; init; } + public string? SecondEffectDamageReduction { get; init; } + public string? HealingDuration { get; init; } + public string? HealingTickTime { get; init; } + public string? CostSwiftReprieval { get; init; } + public string? GdcDuration { get; init; } + public string? Effect { get; init; } + public string? Raw { get; init; } +} diff --git a/Fellowship/Web/Layout/NavMenu.razor b/Fellowship/Web/Layout/NavMenu.razor index c1c9a72..e7977c4 100644 --- a/Fellowship/Web/Layout/NavMenu.razor +++ b/Fellowship/Web/Layout/NavMenu.razor @@ -19,6 +19,11 @@ Keyboard + diff --git a/Fellowship/Web/Pages/Docs.razor b/Fellowship/Web/Pages/Docs.razor new file mode 100644 index 0000000..fc4ca5a --- /dev/null +++ b/Fellowship/Web/Pages/Docs.razor @@ -0,0 +1,331 @@ +@page "/docs" +@using System.Text.RegularExpressions + +Fellowship Docs + +
+
+

Fellowship Reference

+

@DocsData.All.Count entries across @Groups.Count() categories

+ +
+ + + + + +
+
+ +
+ + +
+ @if (!FilteredGroups.Any()) + { +
+

No docs match "@searchTerm"

+ +
+ } + + @foreach (var group in FilteredGroups) + { + var groupId = GetGroupId(group.Key); +
+
+

@group.Key

+ @group.Count() +
+ + @foreach (var doc in group) + { + var docId = GetDocId(doc); + var typeName = doc.GetType().Name.Replace("Doc", ""); +
+
+
+

@GetDisplayName(doc)

+ @typeName +
+ @if (doc is SkillDoc { Key: { } key } && !string.IsNullOrEmpty(key)) + { +
+ + @key +
+ } +
+ +
+ @if (doc is SkillDoc { Description: { } desc } && !string.IsNullOrEmpty(desc)) + { +
+ @{ + var lines = desc.Split('\n'); + foreach (var line in lines) + { +

@RenderWikiLinks(line)

+ } + } +
+ } + +
+ @foreach (var field in GetOrderedFields(doc)) + { +
+ @field.Label + @field.Value +
+ } +
+ + @if (GetBody(doc) is { } body && !string.IsNullOrWhiteSpace(body) && (doc is not SkillDoc || string.IsNullOrWhiteSpace(((SkillDoc)doc).Description))) + { +
+
@body.Trim()
+
+ } +
+ + @if (doc is SkillDoc { Tags: { } tags } && tags.Count > 0) + { + + } +
+ } +
+ } +
+
+
+ +@code { + private string? searchTerm; + private string? activeFilter; + private List> Groups = []; + private IEnumerable> FilteredGroups => Groups + .Where(g => g.Any(d => MatchesFilter(d))) + .OrderBy(g => SortOrder(g.Key)); + + protected override void OnInitialized() + { + Groups = DocsData.All + .GroupBy(d => GetCharacterOrType(d)) + .ToList(); + } + + private bool MatchesFilter(DocEntry doc) + { + if (!string.IsNullOrEmpty(searchTerm)) + { + var term = searchTerm.Trim().ToLowerInvariant(); + var name = GetDisplayName(doc).ToLowerInvariant(); + if (!name.Contains(term)) return false; + } + if (activeFilter != null) + { + var type = doc.GetType().Name.Replace("Doc", ""); + if (type != activeFilter) return false; + } + return true; + } + + private static int SortOrder(string groupKey) => groupKey switch + { + "Xavian" => 0, + "Rime" => 1, + "Vigour" => 2, + "Keys" => 3, + _ => 99 + }; + + private void OnSearchChanged() + { + StateHasChanged(); + } + + private void ClearSearch() + { + searchTerm = null; + activeFilter = null; + } + + private void FilterAll() { activeFilter = null; } + private void FilterSkills() { activeFilter = activeFilter == "Skill" ? null : "Skill"; } + private void FilterDebuffs() { activeFilter = activeFilter == "Debuff" ? null : "Debuff"; } + private void FilterBuffs() { activeFilter = activeFilter == "Buff" ? null : "Buff"; } + private void FilterKeys() { activeFilter = activeFilter == "Key" ? null : "Key"; } + + private static string GetCharacterOrType(DocEntry doc) => doc switch + { + SkillDoc s => s.Character, + DebuffDoc d => d.Character, + BuffDoc b => b.Character, + CharacterDoc c => c.Character, + KeyDoc => "Keys", + _ => "Other" + }; + + private static string GetDisplayName(DocEntry doc) => + Path.GetFileNameWithoutExtension(doc.FileName); + + private static string GetGroupId(string group) => + $"group-{group.GetHashCode():x}"; + + private static string GetDocId(DocEntry doc) => + $"doc-{doc.FileName.GetHashCode():x}"; + + private static string GetTypeClass(DocEntry doc) => doc switch + { + SkillDoc => "type-skill", + DebuffDoc => "type-debuff", + BuffDoc => "type-buff", + KeyDoc => "type-key", + CharacterDoc => "type-character", + _ => "" + }; + + private static MarkupString RenderWikiLinks(string line) + { + var result = Regex.Replace(line, @"\[\[([^\]]+)\]\]", m => + { + var name = m.Groups[1].Value; + return $"{name}"; + }); + return new MarkupString(result); + } + + private static readonly Dictionary FieldLabels = new() + { + ["Character"] = "Character", + ["Cast"] = "Cast Time", + ["Key"] = "Key Bind", + ["Range"] = "Range", + ["Damage"] = "Damage", + ["DamageType"] = "Damage Type", + ["Heal"] = "Healing", + ["Shield"] = "Shield", + ["Cooldown"] = "Cooldown", + ["Mana"] = "Mana Cost", + ["OffGlobalCooldown"] = "Off GCD", + ["Gdc"] = "GDC", + ["Duration"] = "Duration", + ["DamageReduction"] = "Dmg Reduction", + ["DamageTickTime"] = "Tick Interval", + ["IsToggle"] = "Toggle", + ["ManaUpkeepTick"] = "Mana Upkeep", + ["ParryChance"] = "Parry Chance", + ["DamageRedirection"] = "Dmg Redirection", + ["SpiritCost"] = "Spirit Cost", + ["SecondEffectDuration"] = "Effect 2 Duration", + ["SecondEffectDamageReduction"] = "Effect 2 Dmg Reduction", + ["HealingDuration"] = "Healing Duration", + ["HealingTickTime"] = "Healing Tick", + ["CostSwiftReprieval"] = "Cost: Swift Reprieval", + ["GdcDuration"] = "GDC Duration", + ["Effect"] = "Effect", + ["GeneratesSpirit"] = "Generates Spirit", + ["AreaDamagePercentage"] = "Cleave %", + ["SwiftReprievalChance"] = "Swift Reprieval %", + ["MaxStacks"] = "Max Stacks", + ["Action"] = "Action", + ["ParryChanceBonus"] = "Parry Bonus", + ["ManaRestoreBase"] = "Mana Restore Base", + ["ManaRestorePerStack"] = "Mana Per Stack", + ["Order"] = "Order", + ["Priority"] = "Priority", + ["Completed"] = "Completed", + ["Raw"] = "Raw", + }; + + private static readonly Dictionary FieldOrder = new() + { + [typeof(SkillDoc)] = ["Character", "Cast", "Key", "Range", "Damage", "DamageType", "Heal", "Shield", "Cooldown", "Mana", "OffGlobalCooldown", "Gdc", "Duration", "DamageReduction", "DamageTickTime", "IsToggle", "ManaUpkeepTick", "ParryChance", "DamageRedirection", "GeneratesSpirit", "AreaDamagePercentage", "SwiftReprievalChance", "Effect", "SpiritCost", "SecondEffectDuration", "SecondEffectDamageReduction", "HealingDuration", "HealingTickTime", "CostSwiftReprieval", "GdcDuration"], + [typeof(DebuffDoc)] = ["Character", "MaxStacks", "Duration", "ParryChanceBonus", "ManaRestoreBase", "ManaRestorePerStack"], + [typeof(BuffDoc)] = ["Character", "MaxStacks"], + [typeof(KeyDoc)] = ["Action"], + [typeof(CharacterDoc)] = ["Character"], + }; + + private static List GetOrderedFields(DocEntry doc) + { + var result = new List(); + var type = doc.GetType(); + var order = FieldOrder.GetValueOrDefault(type, []); + var allProps = new Dictionary(); + + foreach (var prop in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)) + { + var name = prop.Name; + if (name is "FileName" or "FilePath" or "Body" or "Description") continue; + + var value = prop.GetValue(doc); + if (value == null) continue; + + if (value is bool bVal && !bVal) continue; + if (value is string s && string.IsNullOrEmpty(s)) continue; + + var display = value switch + { + List list => string.Join(", ", list), + _ => value.ToString() + }; + + if (string.IsNullOrEmpty(display)) continue; + allProps[name] = display; + } + + foreach (var key in order) + { + if (allProps.Remove(key, out var val)) + { + result.Add(new FieldEntry(FieldLabels.GetValueOrDefault(key, key), val)); + } + } + + foreach (var (key, val) in allProps) + { + result.Add(new FieldEntry(FieldLabels.GetValueOrDefault(key, key), val)); + } + + return result; + } + + private static string? GetBody(DocEntry doc) => doc.Body; + + private record FieldEntry(string Label, string Value); +} diff --git a/Fellowship/Web/Pages/Docs.razor.css b/Fellowship/Web/Pages/Docs.razor.css new file mode 100644 index 0000000..3b012ea --- /dev/null +++ b/Fellowship/Web/Pages/Docs.razor.css @@ -0,0 +1,433 @@ +.docs-page { + max-width: 1400px; + margin: 0 auto; + color: #e0e0e0; +} + +.docs-header { + padding: 24px 0 16px; + border-bottom: 1px solid #2a2a2a; + margin-bottom: 20px; +} + +.docs-title { + font-size: 28px; + font-weight: 700; + color: #fff; + margin: 0 0 4px; +} + +.docs-subtitle { + color: #888; + font-size: 13px; + margin: 0 0 16px; +} + +.docs-search { + position: relative; + margin-bottom: 12px; +} + +.search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + font-size: 14px; + opacity: 0.5; + pointer-events: none; +} + +.search-input { + width: 100%; + padding: 10px 36px 10px 36px; + border: 1px solid #333; + border-radius: 8px; + background: #1a1a1a; + color: #e0e0e0; + font-size: 14px; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; + box-sizing: border-box; +} + +.search-input:focus { + border-color: #5588ff; + box-shadow: 0 0 0 3px rgba(85, 136, 255, 0.15); +} + +.search-input::placeholder { + color: #555; +} + +.search-clear { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: #888; + font-size: 20px; + cursor: pointer; + padding: 4px 8px; + line-height: 1; +} + +.search-clear:hover { + color: #fff; +} + +.docs-filter-bar { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.filter-btn { + padding: 6px 14px; + border: 1px solid #333; + border-radius: 6px; + background: transparent; + color: #aaa; + font-size: 13px; + cursor: pointer; + transition: all 0.15s; +} + +.filter-btn:hover { + border-color: #5588ff; + color: #5588ff; + background: rgba(85, 136, 255, 0.08); +} + +.filter-btn.active { + border-color: #5588ff; + color: #fff; + background: #5588ff; +} + +.docs-body { + display: flex; + gap: 28px; +} + +.docs-sidebar { + width: 220px; + flex-shrink: 0; +} + +.sidebar-inner { + position: sticky; + top: 16px; + max-height: calc(100vh - 120px); + overflow-y: auto; + padding-right: 4px; +} + +.sidebar-inner::-webkit-scrollbar { + width: 4px; +} + +.sidebar-inner::-webkit-scrollbar-thumb { + background: #333; + border-radius: 2px; +} + +.sidebar-group { + margin-bottom: 16px; +} + +.sidebar-group-title { + display: block; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1.2px; + color: #666; + text-decoration: none; + padding: 4px 8px; + margin-bottom: 4px; + transition: color 0.15s; +} + +.sidebar-group-title:hover { + color: #aaa; +} + +.sidebar-items { + display: flex; + flex-direction: column; +} + +.sidebar-item { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 8px 5px 12px; + font-size: 13px; + color: #999; + text-decoration: none; + border-radius: 4px; + transition: all 0.15s; + line-height: 1.3; +} + +.sidebar-item:hover { + background: rgba(255, 255, 255, 0.04); + color: #ddd; +} + +.sidebar-item-badge { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.sidebar-item-badge.type-skill { background: #5588ff; } +.sidebar-item-badge.type-debuff { background: #ff5544; } +.sidebar-item-badge.type-buff { background: #44bb66; } +.sidebar-item-badge.type-key { background: #888; } +.sidebar-item-badge.type-character { background: #ffaa33; } + +.docs-content { + flex: 1; + min-width: 0; +} + +.no-results { + text-align: center; + padding: 60px 20px; + color: #888; +} + +.no-results p { + font-size: 16px; + margin-bottom: 16px; +} + +.group-section { + margin-bottom: 32px; +} + +.group-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 2px solid #2a2a2a; +} + +.group-title { + font-size: 22px; + font-weight: 700; + color: #fff; + margin: 0; +} + +.group-count { + font-size: 12px; + color: #666; + background: #1a1a1a; + padding: 2px 10px; + border-radius: 10px; + font-weight: 600; +} + +.doc-card { + background: #161616; + border: 1px solid #2a2a2a; + border-radius: 12px; + margin-bottom: 16px; + overflow: hidden; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.doc-card:hover { + border-color: #3a3a3a; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.doc-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 16px 20px 12px; + background: linear-gradient(135deg, #1e1e1e, #181818); + border-bottom: 1px solid #2a2a2a; + gap: 12px; +} + +.doc-card-title-row { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.doc-card-title { + font-size: 17px; + font-weight: 700; + color: #fff; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.doc-type-badge { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.8px; + padding: 3px 10px; + border-radius: 4px; + white-space: nowrap; +} + +.doc-type-badge.type-skill { background: rgba(85, 136, 255, 0.15); color: #7799ff; border: 1px solid rgba(85, 136, 255, 0.3); } +.doc-type-badge.type-debuff { background: rgba(255, 85, 68, 0.15); color: #ff7766; border: 1px solid rgba(255, 85, 68, 0.3); } +.doc-type-badge.type-buff { background: rgba(68, 187, 102, 0.15); color: #66dd88; border: 1px solid rgba(68, 187, 102, 0.3); } +.doc-type-badge.type-key { background: rgba(136, 136, 136, 0.15); color: #aaa; border: 1px solid rgba(136, 136, 136, 0.3); } +.doc-type-badge.type-character { background: rgba(255, 170, 51, 0.15); color: #ffbb55; border: 1px solid rgba(255, 170, 51, 0.3); } + +.doc-key-badge { + display: flex; + align-items: center; + gap: 6px; + background: #0f0f0f; + border: 1px solid #333; + border-radius: 6px; + padding: 5px 12px; + flex-shrink: 0; +} + +.key-icon { + font-size: 14px; + opacity: 0.6; +} + +.key-text { + font-size: 13px; + font-weight: 700; + color: #fff; + font-family: 'Courier New', monospace; +} + +.doc-card-body { + padding: 16px 20px; +} + +.doc-description { + margin-bottom: 14px; + padding: 12px 16px; + background: #121212; + border-radius: 8px; + border: 1px solid #222; +} + +.doc-description p { + margin: 0 0 4px; + font-size: 14px; + line-height: 1.6; + color: #ccc; +} + +.doc-description p:last-child { + margin-bottom: 0; +} + +.wiki-link { + color: #7799ff; + font-weight: 600; + border-bottom: 1px dashed rgba(85, 136, 255, 0.3); + cursor: default; +} + +.doc-fields { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 6px 16px; +} + +.doc-field { + display: flex; + flex-direction: column; + padding: 6px 10px; + background: #131313; + border-radius: 6px; + border: 1px solid #222; +} + +.doc-field-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.6px; + color: #666; + margin-bottom: 2px; +} + +.doc-field-value { + font-size: 14px; + color: #ddd; + font-weight: 500; + word-break: break-word; +} + +.doc-body { + margin-top: 14px; + padding: 12px 16px; + background: #0f0f0f; + border-radius: 8px; + border: 1px solid #1a1a1a; +} + +.doc-body-text { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: #888; + white-space: pre-wrap; + font-family: inherit; +} + +.doc-card-footer { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 10px 20px 14px; + border-top: 1px solid #222; +} + +.tag-badge { + font-size: 11px; + padding: 4px 12px; + border-radius: 4px; + background: rgba(85, 136, 255, 0.1); + color: #7799ff; + border: 1px solid rgba(85, 136, 255, 0.2); + font-weight: 500; +} + +@media (max-width: 900px) { + .docs-sidebar { + display: none; + } + + .docs-fields { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 600px) { + .doc-card-header { + flex-direction: column; + } + + .doc-fields { + grid-template-columns: 1fr; + } +} diff --git a/Fellowship/Web/Web.csproj b/Fellowship/Web/Web.csproj index e206369..4dcdc8b 100644 --- a/Fellowship/Web/Web.csproj +++ b/Fellowship/Web/Web.csproj @@ -16,4 +16,7 @@ + + + diff --git a/Fellowship/Web/_Imports.razor b/Fellowship/Web/_Imports.razor index eadfbc1..0b1deef 100644 --- a/Fellowship/Web/_Imports.razor +++ b/Fellowship/Web/_Imports.razor @@ -6,5 +6,6 @@ @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop +@using Model @using Web @using Web.Layout \ No newline at end of file diff --git a/fellowship.tasks/.obsidian/app.json b/fellowship.tasks/.obsidian/app.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/fellowship.tasks/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/fellowship.tasks/.obsidian/appearance.json b/fellowship.tasks/.obsidian/appearance.json new file mode 100644 index 0000000..4be7969 --- /dev/null +++ b/fellowship.tasks/.obsidian/appearance.json @@ -0,0 +1,3 @@ +{ + "theme": "obsidian" +} \ No newline at end of file diff --git a/fellowship.tasks/.obsidian/community-plugins.json b/fellowship.tasks/.obsidian/community-plugins.json new file mode 100644 index 0000000..2868535 --- /dev/null +++ b/fellowship.tasks/.obsidian/community-plugins.json @@ -0,0 +1,3 @@ +[ + "kanban-bases-view" +] \ No newline at end of file diff --git a/fellowship.tasks/.obsidian/core-plugins.json b/fellowship.tasks/.obsidian/core-plugins.json new file mode 100644 index 0000000..639b90d --- /dev/null +++ b/fellowship.tasks/.obsidian/core-plugins.json @@ -0,0 +1,33 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "footnotes": false, + "properties": true, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": true, + "bases": true, + "webviewer": false +} \ No newline at end of file diff --git a/fellowship.tasks/.obsidian/plugins/kanban-bases-view/main.js b/fellowship.tasks/.obsidian/plugins/kanban-bases-view/main.js new file mode 100644 index 0000000..25e1d84 --- /dev/null +++ b/fellowship.tasks/.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/fellowship.tasks/.obsidian/plugins/kanban-bases-view/manifest.json b/fellowship.tasks/.obsidian/plugins/kanban-bases-view/manifest.json new file mode 100644 index 0000000..79d42ff --- /dev/null +++ b/fellowship.tasks/.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/fellowship.tasks/.obsidian/plugins/kanban-bases-view/styles.css b/fellowship.tasks/.obsidian/plugins/kanban-bases-view/styles.css new file mode 100644 index 0000000..74c99e0 --- /dev/null +++ b/fellowship.tasks/.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/fellowship.tasks/.obsidian/workspace.json b/fellowship.tasks/.obsidian/workspace.json new file mode 100644 index 0000000..355990d --- /dev/null +++ b/fellowship.tasks/.obsidian/workspace.json @@ -0,0 +1,194 @@ +{ + "main": { + "id": "772d554b3c2dd3d9", + "type": "split", + "children": [ + { + "id": "c4142c578527be4f", + "type": "tabs", + "children": [ + { + "id": "53a2ae872ee75d50", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "Generate a Website.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "Generate a Website" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "b49e208e566c6705", + "type": "split", + "children": [ + { + "id": "7539461953238b30", + "type": "tabs", + "children": [ + { + "id": "ee7612952351843c", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "Files" + } + }, + { + "id": "de92584c0e0eb766", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "Search" + } + }, + { + "id": "1dd86bed19ceb91b", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "Bookmarks" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "90b13a36151bdebd", + "type": "split", + "children": [ + { + "id": "2bc8c5a8cffec15e", + "type": "tabs", + "children": [ + { + "id": "fa7bc0f928d0b131", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "file": "Generate a Website.md", + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "Backlinks for Generate a Website" + } + }, + { + "id": "771b1e8d32f6ef67", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "file": "Generate a Website.md", + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "Outgoing links from Generate a Website" + } + }, + { + "id": "34742e792f2e5de2", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "Tags" + } + }, + { + "id": "574dd5e3cde54473", + "type": "leaf", + "state": { + "type": "all-properties", + "state": { + "sortOrder": "frequency", + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-archive", + "title": "All properties" + } + }, + { + "id": "07f2e7a9777ee72d", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "file": "Generate a Website.md", + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "Outline of Generate a Website" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Open quick switcher": false, + "graph:Open graph view": false, + "canvas:Create new canvas": false, + "daily-notes:Open today's daily note": false, + "templates:Insert template": false, + "command-palette:Open command palette": false, + "bases:Create new base": false + } + }, + "active": "53a2ae872ee75d50", + "lastOpenFiles": [ + "Task.md", + "Generate a Website.md", + "Task Example.md", + "_AI Tasks.base", + "_Tasks.base" + ] +} \ No newline at end of file diff --git a/fellowship.tasks/Generate a Website.md b/fellowship.tasks/Generate a Website.md new file mode 100644 index 0000000..afed584 --- /dev/null +++ b/fellowship.tasks/Generate a Website.md @@ -0,0 +1,20 @@ +--- +category: AI Task +status: Done +--- +Check fellowship.docs for frontmatter and markdown docs. + +Create a document page with all the docs as markdown files. + +For the frontmatter of the docs, generate classes in the Model project. + +Also save all the data as C# data in the Model project, using those classes. + +These generated docs will be created via scripts in the build project. + +The build project will be ran as a pre-step to the Web project. + +The Web project will consume the the data in the Model project to display the docs and their frontmatter. + +See Omnistrike.md as one example of a markdown file with a lot of frontmatter. + diff --git a/fellowship.tasks/_AI Tasks.base b/fellowship.tasks/_AI Tasks.base new file mode 100644 index 0000000..5b5a59d --- /dev/null +++ b/fellowship.tasks/_AI Tasks.base @@ -0,0 +1,25 @@ +views: + - type: kanban-view + name: Table + filters: + and: + - category == "AI Task" + columnOrders: + file.file: + - _Tasks.base + - keep.txt + - Tasks.md + note.status: + - ToDo + - Working On + - Done + cardOrders: + file.file: {} + note.status: + Uncategorized: [] + Done: + - Untitled.md + columnColors: + file.file: {} + note.status: {} + groupByProperty: note.status diff --git a/fellowship.tasks/_Tasks.base b/fellowship.tasks/_Tasks.base new file mode 100644 index 0000000..e8f182a --- /dev/null +++ b/fellowship.tasks/_Tasks.base @@ -0,0 +1,25 @@ +views: + - type: kanban-view + name: Table + filters: + and: + - category == "Task" + columnOrders: + file.file: + - _Tasks.base + - keep.txt + - Tasks.md + note.status: + - ToDo + - Working On + - Done + cardOrders: + file.file: {} + note.status: + Uncategorized: [] + Done: + - Task.md + columnColors: + file.file: {} + note.status: {} + groupByProperty: note.status