diff --git a/internal/app/database/database.go b/internal/app/database/database.go
index 43bd256..feb7116 100644
--- a/internal/app/database/database.go
+++ b/internal/app/database/database.go
@@ -60,6 +60,11 @@ type Service interface {
SearchLists(searchQuery string) ([]*List, error)
ListTasksSearch(id int, searchQuery string) ([]*Task, error)
+
+ Groups() ([]*ListGroup, error)
+ CreateGroup(name string, position int) error
+ UpdateGroup(id int, name string) error
+ DeleteGroup(id int) error
}
type cacheEntry struct {
@@ -730,6 +735,7 @@ func (s *service) Lists(filter string) ([]*List, error) {
&data.Position,
&data.CreatedAt,
&data.UpdatedAt,
+ &data.GroupId,
)
if err != nil {
return nil, fmt.Errorf("DB.Lists - result scan failed: %v", err)
@@ -926,7 +932,8 @@ func (s *service) List(id string) (*List, error) {
&list.Base,
&list.Position,
&list.CreatedAt,
- &list.UpdatedAt)
+ &list.UpdatedAt,
+ &list.GroupId)
if err != nil {
return nil, fmt.Errorf("DB.List - query result failed: %v", err)
}
@@ -1273,3 +1280,114 @@ func (s *service) ListTasksSearch(id int, searchQuery string) ([]*Task, error) {
return tasks, nil
}
+
+func (s *service) Groups() ([]*ListGroup, error) {
+ const key = "groups"
+ if cached, ok := s.cache.get(key); ok {
+ return cached.([]*ListGroup), nil
+ }
+
+ query, err := s.db.Prepare("SELECT * FROM list_group ORDER BY position ASC, created_at ASC")
+ if err != nil {
+ return nil, fmt.Errorf("DB.Groups - prepare query failed: %v", err)
+ }
+ defer query.Close()
+
+ result, err := query.Query()
+ if err != nil {
+ return nil, fmt.Errorf("DB.Groups - query result failed: %v", err)
+ }
+
+ groups := make([]*ListGroup, 0)
+ for result.Next() {
+ data := new(ListGroup)
+ err := result.Scan(
+ &data.ID,
+ &data.Name,
+ &data.Position,
+ &data.CreatedAt,
+ &data.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("DB.Groups - result scan failed: %v", err)
+ }
+ groups = append(groups, data)
+ }
+
+ // Populate each group with its lists
+ lists, err := s.Lists("")
+ if err != nil {
+ return nil, fmt.Errorf("DB.Groups - get lists failed: %v", err)
+ }
+ for _, g := range groups {
+ g.Lists = make([]*List, 0)
+ for _, l := range lists {
+ if l.GroupId == g.ID {
+ g.Lists = append(g.Lists, l)
+ }
+ }
+ }
+
+ s.cache.set(key, groups)
+ return groups, nil
+}
+
+func (s *service) CreateGroup(name string, position int) error {
+ query, err := s.db.Prepare("INSERT INTO list_group (name, position) VALUES (?, ?)")
+ if err != nil {
+ return fmt.Errorf("DB.CreateGroup - prepare query failed: %v", err)
+ }
+ defer query.Close()
+
+ _, err = query.Exec(name, position)
+ if err != nil {
+ return fmt.Errorf("DB.CreateGroup - exec failed: %v", err)
+ }
+
+ s.cache.invalidate()
+ return nil
+}
+
+func (s *service) UpdateGroup(id int, name string) error {
+ query, err := s.db.Prepare("UPDATE list_group SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
+ if err != nil {
+ return fmt.Errorf("DB.UpdateGroup - prepare query failed: %v", err)
+ }
+ defer query.Close()
+
+ _, err = query.Exec(name, id)
+ if err != nil {
+ return fmt.Errorf("DB.UpdateGroup - exec failed: %v", err)
+ }
+
+ s.cache.invalidate()
+ return nil
+}
+
+func (s *service) DeleteGroup(id int) error {
+ // Unassign lists from this group first
+ unassign, err := s.db.Prepare("UPDATE list SET group_id = 0 WHERE group_id = ?")
+ if err != nil {
+ return fmt.Errorf("DB.DeleteGroup - prepare unassign query failed: %v", err)
+ }
+ defer unassign.Close()
+
+ _, err = unassign.Exec(id)
+ if err != nil {
+ return fmt.Errorf("DB.DeleteGroup - unassign exec failed: %v", err)
+ }
+
+ query, err := s.db.Prepare("DELETE FROM list_group WHERE id = ?")
+ if err != nil {
+ return fmt.Errorf("DB.DeleteGroup - prepare delete query failed: %v", err)
+ }
+ defer query.Close()
+
+ _, err = query.Exec(id)
+ if err != nil {
+ return fmt.Errorf("DB.DeleteGroup - delete exec failed: %v", err)
+ }
+
+ s.cache.invalidate()
+ return nil
+}
diff --git a/internal/app/database/migrations/004_add_list_groups.sql b/internal/app/database/migrations/004_add_list_groups.sql
new file mode 100644
index 0000000..2b11e10
--- /dev/null
+++ b/internal/app/database/migrations/004_add_list_groups.sql
@@ -0,0 +1,11 @@
+-- Add list_group table and group_id to list
+
+CREATE TABLE IF NOT EXISTS list_group (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ position INTEGER DEFAULT 0,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE list ADD COLUMN group_id INTEGER DEFAULT 0;
diff --git a/internal/app/database/schema.go b/internal/app/database/schema.go
index 3c3105d..16022f8 100644
--- a/internal/app/database/schema.go
+++ b/internal/app/database/schema.go
@@ -30,11 +30,21 @@ type List struct {
Pinned bool `json:"pinned"`
Base bool `json:"base"`
Position int `json:"position"`
+ GroupId int `json:"group_id"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Tasks []*Task
}
+type ListGroup struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Position int `json:"position"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+ Lists []*List
+}
+
type Total struct {
Tasks int `json:"total_tasks"`
Completed int `json:"total_completed"`
diff --git a/internal/handlers/tasks/groups.go b/internal/handlers/tasks/groups.go
new file mode 100644
index 0000000..310b037
--- /dev/null
+++ b/internal/handlers/tasks/groups.go
@@ -0,0 +1,130 @@
+package tasks
+
+import (
+ "rminder/internal/app"
+ "rminder/web"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+func CreateGroup(ctx *gin.Context) {
+ db := app.GetUserDatabase(ctx)
+ log := app.GetLogger(ctx)
+
+ err := ctx.Request.ParseForm()
+ if err != nil {
+ log.Error("error parsing form", "error", err)
+ app.ErrorBadRequestHTML(ctx, "Invalid form data.")
+ return
+ }
+
+ name := ctx.Request.FormValue("name")
+ if name == "" || len(name) < 3 || len(name) > 255 {
+ log.Error("error form field validation failed", "name", name)
+ app.ErrorBadRequestHTML(ctx, "Name must be between 3 and 255 characters.")
+ return
+ }
+
+ groups, err := db.Groups()
+ if err != nil {
+ log.Error("error loading groups for position", "error", err)
+ app.ErrorInternalHTML(ctx, "Failed to load groups.")
+ return
+ }
+
+ if err := db.CreateGroup(name, len(groups)+1); err != nil {
+ log.Error("error creating group", "error", err)
+ app.ErrorInternalHTML(ctx, "Failed to create group.")
+ return
+ }
+ log.Info("group created", "name", name)
+
+ renderSidebarLists(ctx)
+}
+
+func UpdateGroup(ctx *gin.Context) {
+ db := app.GetUserDatabase(ctx)
+ log := app.GetLogger(ctx)
+
+ groupID := ctx.Param("groupID")
+ id, _ := strconv.Atoi(groupID)
+
+ err := ctx.Request.ParseForm()
+ if err != nil {
+ log.Error("error parsing form", "error", err)
+ app.ErrorBadRequestHTML(ctx, "Invalid form data.")
+ return
+ }
+
+ name := ctx.Request.FormValue("name")
+ if name == "" || len(name) < 3 || len(name) > 255 {
+ log.Error("error form field validation failed", "name", name)
+ app.ErrorBadRequestHTML(ctx, "Name must be between 3 and 255 characters.")
+ return
+ }
+
+ if err := db.UpdateGroup(id, name); err != nil {
+ log.Error("error updating group", "groupID", groupID, "error", err)
+ app.ErrorInternalHTML(ctx, "Failed to update group.")
+ return
+ }
+ log.Info("group updated", "groupID", groupID, "name", name)
+
+ renderSidebarLists(ctx)
+}
+
+func DeleteGroup(ctx *gin.Context) {
+ db := app.GetUserDatabase(ctx)
+ log := app.GetLogger(ctx)
+
+ groupID := ctx.Param("groupID")
+ id, _ := strconv.Atoi(groupID)
+
+ if err := db.DeleteGroup(id); err != nil {
+ log.Error("error deleting group", "groupID", groupID, "error", err)
+ app.ErrorInternalHTML(ctx, "Failed to delete group.")
+ return
+ }
+ log.Info("group deleted", "groupID", groupID)
+
+ renderSidebarLists(ctx)
+}
+
+// renderSidebarLists is a shared helper to re-render the sidebar lists after group mutations.
+func renderSidebarLists(ctx *gin.Context) {
+ db := app.GetUserDatabase(ctx)
+ log := app.GetLogger(ctx)
+
+ lists, err := db.Lists("")
+ if err != nil {
+ log.Error("error loading lists", "error", err)
+ app.ErrorInternalHTML(ctx, "Failed to load lists.")
+ return
+ }
+
+ groups, err := db.Groups()
+ if err != nil {
+ log.Error("error loading groups", "error", err)
+ app.ErrorInternalHTML(ctx, "Failed to load groups.")
+ return
+ }
+
+ persistence, err := db.Persistence()
+ if err != nil {
+ log.Error("error loading persistence", "error", err)
+ app.ErrorInternalHTML(ctx, "Failed to load state.")
+ return
+ }
+
+ ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
+ err = web.Render(ctx.Writer, "sidebar-lists", map[string]any{
+ "Lists": lists,
+ "Groups": groups,
+ "Persistence": persistence,
+ })
+ if err != nil {
+ log.Error("error rendering sidebar-lists", "error", err)
+ app.ErrorInternalHTML(ctx, "Failed to render lists.")
+ }
+}
diff --git a/internal/handlers/tasks/lists.go b/internal/handlers/tasks/lists.go
index ca7b85c..9077e01 100644
--- a/internal/handlers/tasks/lists.go
+++ b/internal/handlers/tasks/lists.go
@@ -16,33 +16,7 @@ import (
)
func GetLists(ctx *gin.Context) {
- db := app.GetUserDatabase(ctx)
- log := app.GetLogger(ctx)
-
- lists, err := db.Lists("")
- if err != nil {
- log.Error("error handling GetLists", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to load lists.")
- return
- }
-
- persistence, err := db.Persistence()
- if err != nil {
- log.Error("error handling GetLists Persistence", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to load lists.")
- return
- }
-
- ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
-
- err = web.Render(ctx.Writer, "sidebar-lists", map[string]any{
- "Lists": lists,
- "Persistence": persistence,
- })
- if err != nil {
- log.Error("error rendering in GetLists", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to render lists.")
- }
+ renderSidebarLists(ctx)
}
func GetList(ctx *gin.Context) {
@@ -167,29 +141,7 @@ func CreateList(ctx *gin.Context) {
return
}
- lists, err := db.Lists("")
- if err != nil {
- log.Error("error handling lists", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to load lists.")
- return
- }
-
- persistence, err := db.Persistence()
- if err != nil {
- log.Error("error handling CreateList Persistence", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to load list state.")
- return
- }
-
- ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
- err = web.Render(ctx.Writer, "sidebar-lists", map[string]any{
- "Lists": lists,
- "Persistence": persistence,
- })
- if err != nil {
- log.Error("error rendering in Lists", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to render lists.")
- }
+ renderSidebarLists(ctx)
}
func DeleteList(ctx *gin.Context) {
@@ -216,13 +168,6 @@ func DeleteList(ctx *gin.Context) {
return
}
- lists, err := db.Lists("")
- if err != nil {
- log.Error("error handling DeleteList lists", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to load lists.")
- return
- }
-
persistence, err := db.Persistence()
if err != nil {
log.Error("error handling DeleteList Persistence", "error", err)
@@ -237,18 +182,9 @@ func DeleteList(ctx *gin.Context) {
app.ErrorInternalHTML(ctx, "Failed to update list state.")
return
}
- persistence.ListId = 0
}
- ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
- err = web.Render(ctx.Writer, "sidebar-lists", map[string]any{
- "Lists": lists,
- "Persistence": persistence,
- })
- if err != nil {
- log.Error("error rendering in Lists", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to render lists.")
- }
+ renderSidebarLists(ctx)
}
func UpdateList(ctx *gin.Context) {
@@ -301,29 +237,7 @@ func UpdateList(ctx *gin.Context) {
return
}
- lists, err := db.Lists("")
- if err != nil {
- log.Error("error handling lists", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to load lists.")
- return
- }
-
- persistence, err := db.Persistence()
- if err != nil {
- log.Error("error handling UpdateList Persistence", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to load list state.")
- return
- }
-
- ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
- err = web.Render(ctx.Writer, "sidebar-lists", map[string]any{
- "Lists": lists,
- "Persistence": persistence,
- })
- if err != nil {
- log.Error("error rendering in Lists", "error", err)
- app.ErrorInternalHTML(ctx, "Failed to render lists.")
- }
+ renderSidebarLists(ctx)
}
func SearchLists(ctx *gin.Context) {
diff --git a/internal/handlers/tasks/tasks.go b/internal/handlers/tasks/tasks.go
index e77e783..5be8b0d 100644
--- a/internal/handlers/tasks/tasks.go
+++ b/internal/handlers/tasks/tasks.go
@@ -23,6 +23,12 @@ func Load(ctx *gin.Context) {
return
}
+ groups, err := db.Groups()
+ if err != nil {
+ log.Error("error handling Tasks app Load Groups", "error", err)
+ return
+ }
+
persistence, err := db.Persistence()
if err != nil {
log.Error("error handling Tasks app Load Persistence", "error", err)
@@ -52,6 +58,7 @@ func Load(ctx *gin.Context) {
err = web.Render(ctx.Writer, "tasks-page", map[string]any{
"Lists": lists,
+ "Groups": groups,
"MultiList": multiList,
"Persistence": persistence,
"CSRFToken": app.GetCSRFToken(ctx),
diff --git a/internal/router/tasks.go b/internal/router/tasks.go
index c9e9503..80efdf1 100644
--- a/internal/router/tasks.go
+++ b/internal/router/tasks.go
@@ -34,6 +34,11 @@ func TasksRoutes(router *gin.Engine, application *app.App) {
lists.DELETE("/:listID", taskhandlers.DeleteList)
lists.PUT("/:listID", taskhandlers.UpdateList)
+ groups := partials.Group("/groups")
+ groups.POST("/create", taskhandlers.CreateGroup)
+ groups.PUT("/:groupID", taskhandlers.UpdateGroup)
+ groups.DELETE("/:groupID", taskhandlers.DeleteGroup)
+
// API: returns JSON
api := router.Group("/api", middleware.UserMiddleware(application), middleware.CSRFMiddleware())
diff --git a/web/assets/js/apps/tasks/tasks.entry.js b/web/assets/js/apps/tasks/tasks.entry.js
index c9f8be0..1ffce06 100644
--- a/web/assets/js/apps/tasks/tasks.entry.js
+++ b/web/assets/js/apps/tasks/tasks.entry.js
@@ -1,6 +1,7 @@
import "../../components/details.js";
import "../../components/sidebar.js";
import "../../components/listNavigation.js";
+import "../../components/groupNavigation.js";
import "../../components/appSwitcher.js";
import "../../components/settingsSwitcher.js";
import "./tasks.js";
diff --git a/web/assets/js/components/groupNavigation.js b/web/assets/js/components/groupNavigation.js
new file mode 100644
index 0000000..7b107b0
--- /dev/null
+++ b/web/assets/js/components/groupNavigation.js
@@ -0,0 +1,56 @@
+class GroupNavigationElement extends HTMLElement {
+ /** @type string */
+ groupId;
+ /** @type string */
+ groupName;
+ /** @type HTMLSpanElement */
+ settings;
+ /** @type HTMLDialogElement */
+ formContainer;
+
+ #editGroupHandler;
+
+ /**
+ * @param {MouseEvent} e
+ */
+ edit(e) {
+ e.preventDefault();
+
+ const form = this.formContainer.querySelector("form");
+ const nameInput = form.querySelector("input.new-group");
+ const updateBtn = form.querySelector(".btn.add-new-group");
+ const removeBtn = form.querySelector(".remove-group");
+
+ form.removeAttribute("hx-post");
+ form.setAttribute("hx-put", `/partials/groups/${this.groupId}`);
+ removeBtn.setAttribute("hx-delete", `/partials/groups/${this.groupId}`);
+
+ nameInput.value = this.groupName;
+ updateBtn.innerHTML = "Update";
+ removeBtn.classList.remove("hidden");
+
+ htmx.process(form);
+ htmx.process(removeBtn);
+
+ this.formContainer.showModal();
+ nameInput.focus();
+ }
+
+ connectedCallback() {
+ this.formContainer = document.querySelector(".group-form-container");
+ this.settings = this.querySelector(".group-settings");
+
+ this.groupId = this.dataset.groupId;
+ this.groupName = this.querySelector(".group-name")?.textContent?.trim() ?? "";
+
+ this.#editGroupHandler = (e) => this.edit(e);
+
+ this.settings?.addEventListener("click", this.#editGroupHandler);
+ }
+
+ disconnectedCallback() {
+ this.settings?.removeEventListener("click", this.#editGroupHandler);
+ }
+}
+
+customElements.define("rm-group-nav", GroupNavigationElement);
diff --git a/web/assets/js/components/sidebar.js b/web/assets/js/components/sidebar.js
index a073d79..8a15318 100644
--- a/web/assets/js/components/sidebar.js
+++ b/web/assets/js/components/sidebar.js
@@ -10,18 +10,31 @@ class SidebarElement extends HTMLElement {
/** @type HTMLInputElement */
searchInput;
+ /** @type HTMLButtonElement */
+ addGroup;
+ /** @type HTMLDivElement */
+ cancelGroupBtn;
+ /** @type HTMLDialogElement */
+ groupFormContainer;
+
#closePopoverHandler;
#formFocusHandler;
#searchClearHandler;
+ #closeGroupPopoverHandler;
+ #groupFormFocusHandler;
addEvents() {
this.#closePopoverHandler = () => this.closePopover();
this.#formFocusHandler = () => this.formFocus();
this.#searchClearHandler = () => this.clearSearch();
+ this.#closeGroupPopoverHandler = () => this.closeGroupPopover();
+ this.#groupFormFocusHandler = () => this.groupFormFocus();
this.addList.addEventListener("click", this.#formFocusHandler);
this.cancelBtn.addEventListener("click", this.#closePopoverHandler);
this.clearIcon.addEventListener("click", this.#searchClearHandler);
+ this.addGroup?.addEventListener("click", this.#groupFormFocusHandler);
+ this.cancelGroupBtn?.addEventListener("click", this.#closeGroupPopoverHandler);
}
closePopover() {
@@ -55,6 +68,27 @@ class SidebarElement extends HTMLElement {
this.formContainer.querySelector(".new-list").focus();
}
+ closeGroupPopover() {
+ this.groupFormContainer.close();
+ const form = this.groupFormContainer.querySelector("form");
+ const btn = form.querySelector(".btn.add-new-group");
+ const removeBtn = form.querySelector(".remove-group");
+ form.reset();
+
+ form.removeAttribute("hx-put");
+ form.setAttribute("hx-post", "/partials/groups/create");
+ btn.innerHTML = "Add";
+ removeBtn?.classList.add("hidden");
+ removeBtn?.removeAttribute("hx-delete");
+
+ htmx.process(form);
+ }
+
+ groupFormFocus() {
+ this.groupFormContainer.showModal();
+ this.groupFormContainer.querySelector(".new-group").focus();
+ }
+
clearSearch() {
this.searchInput.value = "";
}
@@ -66,6 +100,10 @@ class SidebarElement extends HTMLElement {
this.clearIcon = this.querySelector(".searchbox__container i.clear-icon");
this.searchInput = this.querySelector(".searchbox");
+ this.addGroup = this.querySelector(".add-group");
+ this.cancelGroupBtn = this.querySelector(".cancel-new-group");
+ this.groupFormContainer = this.querySelector(".group-form-container");
+
this.addEvents();
}
@@ -73,6 +111,8 @@ class SidebarElement extends HTMLElement {
this.addList.removeEventListener("click", this.#formFocusHandler);
this.cancelBtn.removeEventListener("click", this.#closePopoverHandler);
this.clearIcon.removeEventListener("click", this.#searchClearHandler);
+ this.addGroup?.removeEventListener("click", this.#groupFormFocusHandler);
+ this.cancelGroupBtn?.removeEventListener("click", this.#closeGroupPopoverHandler);
}
}
diff --git a/web/assets/js/dist/tasks.min.js b/web/assets/js/dist/tasks.min.js
index 597eaf2..da76145 100644
--- a/web/assets/js/dist/tasks.min.js
+++ b/web/assets/js/dist/tasks.min.js
@@ -1 +1 @@
-var Z=Object.defineProperty;var C=r=>{throw TypeError(r)};var ee=(r,n,e)=>n in r?Z(r,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[n]=e;var a=(r,n,e)=>ee(r,typeof n!="symbol"?n+"":n,e),x=(r,n,e)=>n.has(r)||C("Cannot "+e);var u=(r,n,e)=>(x(r,n,"read from private field"),e?e.call(r):n.get(r)),m=(r,n,e)=>n.has(r)?C("Cannot add the same private member more than once"):n instanceof WeakSet?n.add(r):n.set(r,e),E=(r,n,e,t)=>(x(r,n,"write to private field"),t?t.call(r,e):n.set(r,e),e),h=(r,n,e)=>(x(r,n,"access private method"),e);var q,P;class te extends HTMLElement{constructor(){super(...arguments);m(this,q);a(this,"quill")}get description(){return this.quill.getSemanticHTML()}async connectedCallback(){await h(this,q,P).call(this),this.getAttribute("task-id");const e=this.querySelector("button.close"),t=this.querySelector("button.remove"),s=this.querySelector("dialog.modal"),i=this.querySelector("dialog button.default"),o=this.querySelector("dialog button.warning");this.quill=new Quill("#note-editor",{modules:{toolbar:[[{header:[1,2,3,!1]}],["bold","italic","underline","strike"],["blockquote","code-block"],["link","image"],[{list:"ordered"},{list:"bullet"}]]},bounds:this,placeholder:"Add description...",theme:"snow"}),e.addEventListener("click",()=>{const l=document.querySelector("input[name='task-detail']:checked");l&&(l.checked=!1)}),t.addEventListener("click",()=>{s==null||s.showModal()}),i.addEventListener("click",()=>{s==null||s.close()}),o.addEventListener("click",()=>{s==null||s.close()})}}q=new WeakSet,P=async function(){if(!window.Quill){if(!document.querySelector('link[href="/assets/css/libs/quill.snow.css"]')){const e=document.createElement("link");e.rel="stylesheet",e.href="/assets/css/libs/quill.snow.css",document.head.appendChild(e)}return new Promise((e,t)=>{const s=document.createElement("script");s.src="/assets/js/libs/quill.js",s.onload=e,s.onerror=t,document.head.appendChild(s)})}};customElements.define("rm-task-details",te);var k,y,v;class se extends HTMLElement{constructor(){super(...arguments);a(this,"cancelBtn");a(this,"addList");a(this,"formContainer");a(this,"clearIcon");a(this,"searchInput");m(this,k);m(this,y);m(this,v)}addEvents(){E(this,k,()=>this.closePopover()),E(this,y,()=>this.formFocus()),E(this,v,()=>this.clearSearch()),this.addList.addEventListener("click",u(this,y)),this.cancelBtn.addEventListener("click",u(this,k)),this.clearIcon.addEventListener("click",u(this,v))}closePopover(){var i,o,l,p;this.formContainer.close();const e=this.formContainer.querySelector("form"),t=e.querySelector(".btn.add-new-list"),s=e.querySelector(".remove-list");e.reset(),e.removeAttribute("hx-put"),e.setAttribute("hx-post","/partials/lists/create"),(i=e.querySelectorAll("input:checked"))==null||i.forEach(f=>{f.removeAttribute("checked")}),(o=e.querySelectorAll("option"))==null||o.forEach(f=>{f.removeAttribute("selected")}),(l=e.querySelector('input[name="swatch"]'))==null||l.setAttribute("checked",""),(p=e.querySelector('input[name="icon"]'))==null||p.setAttribute("checked",""),t.innerHTML="Add",s==null||s.classList.add("hidden"),s==null||s.removeAttribute("hx-delete"),htmx.process(e)}formFocus(){this.formContainer.showModal(),this.formContainer.querySelector(".new-list").focus()}clearSearch(){this.searchInput.value=""}connectedCallback(){this.cancelBtn=this.querySelector(".cancel-new-list"),this.addList=this.querySelector(".add-list"),this.formContainer=this.querySelector(".list-form-container"),this.clearIcon=this.querySelector(".searchbox__container i.clear-icon"),this.searchInput=this.querySelector(".searchbox"),this.addEvents()}disconnectedCallback(){this.addList.removeEventListener("click",u(this,y)),this.cancelBtn.removeEventListener("click",u(this,k)),this.clearIcon.removeEventListener("click",u(this,v))}}k=new WeakMap,y=new WeakMap,v=new WeakMap;customElements.define("rm-sidebar",se);var b;class ie extends HTMLElement{constructor(){super(...arguments);a(this,"listId");a(this,"listName");a(this,"colour");a(this,"icon");a(this,"position");a(this,"pinned");a(this,"filter");a(this,"settings");a(this,"formContainer");m(this,b)}edit(e){var T;e.preventDefault();const t=this.formContainer.querySelector("form");(T=t.querySelectorAll("input:checked"))==null||T.forEach(w=>{w.removeAttribute("checked")});const s=t.querySelector("input.new-list"),i=t.querySelector('input[name="pin"]'),o=t.querySelector(`input[name="swatch"][value="${this.colour}"]`),l=t.querySelector(`input[name="icon"][value="${this.icon}"]`),p=t.querySelector(".btn.add-new-list"),f=t.querySelector(".remove-list"),K=t.querySelector('select[name="include"]'),M=t.querySelector('select[name="completed"]'),F=t.querySelector('select[name="important"]'),$=t.querySelector('select[name="priority"]'),j=t.querySelector('select[name="date"]'),J=t.querySelector('input[name="from"]'),z=t.querySelector('input[name="to"]'),[U,Q,X,Y,G,V,W]=this.filter.split(";").map(w=>w.split("=")[1]);t.removeAttribute("hx-post"),t.setAttribute("hx-put",`/partials/lists/${this.listId}`),f.setAttribute("hx-delete",`/partials/lists/${this.listId}`),s.value=this.listName,i.toggleAttribute("checked",this.pinned===""),o.setAttribute("checked",""),l.setAttribute("checked",""),p.innerHTML="Update",f.classList.remove("hidden"),K.value=U,M.value=Q,F.value=X,$.value=Y,j.value=G,J.value=V,z.value=W,htmx.process(t),this.formContainer.showModal()}connectedCallback(){var e;this.formContainer=document.querySelector(".list-form-container"),this.settings=this.querySelector(".settings"),this.listId=this.dataset.id,this.listName=this.dataset.name,this.colour=this.dataset.colour,this.icon=this.dataset.icon,this.pinned=this.dataset.pinned,this.position=this.dataset.position,this.filter=this.dataset.filter,E(this,b,t=>this.edit(t)),this.style.setProperty("--list-colour",`var(${this.colour})`),(e=this.settings)==null||e.addEventListener("click",u(this,b))}disconnectedCallback(){var e;(e=this.settings)==null||e.removeEventListener("click",u(this,b))}}b=new WeakMap;customElements.define("rm-list-nav",ie);class re extends HTMLElement{connectedCallback(){const n=this.getBoundingClientRect(),e=document.body.getBoundingClientRect();this.style.setProperty("--popover-top",`${n.bottom}px`),this.style.setProperty("--popover-right",`${e.right-n.right+10}px`)}}customElements.define("rm-app-switcher",re);async function L(r){try{return[null,await r]}catch(n){return[n]}}var S,N,_;class ne extends HTMLElement{constructor(){super(...arguments);m(this,S);a(this,"formContainer");a(this,"fileElem");a(this,"messages");a(this,"maxFileSize",10*1024*1024);a(this,"allowedTypes",["application/json"])}saveJSON(e,t){var s=JSON.stringify(e,null,2),i=new Blob([s],{type:"application/json"}),o=URL.createObjectURL(i),l=document.createElement("a");l.download=t+".json",l.href=o,l.id=t,document.body.appendChild(l),l.click(),document.querySelector("#"+l.id).remove()}validateFile(e){return this.messages.textContent="",this.allowedTypes.includes(e.type)?e.size>this.maxFileSize?(this.showError(`${e.name} exceeds the maximum file size of 10MB.`),!1):!0:(this.showError(`${e.name} is not an allowed file type.`),!1)}showError(e){this.messages.textContent=e}connectedCallback(){const e=this.getBoundingClientRect(),t=document.body.getBoundingClientRect();this.formContainer=this.querySelector(".file-form-container"),this.inputElem=this.querySelector("#file"),this.messages=this.querySelector("#messages"),this.style.setProperty("--popover-top",`${e.bottom}px`),this.style.setProperty("--popover-right",`${t.right-e.right+10}px`),this.handleClick=h(this,S,N).bind(this),this.handleFiles=h(this,S,_).bind(this),this.addEventListener("click",this.handleClick,!1),this.inputElem.addEventListener("change",this.handleFiles,!1)}disconnectedCallback(){this.removeEventListener("click",this.handleClick,!1),this.inputElem.removeEventListener("change",this.handleFiles,!1)}}S=new WeakSet,N=async function(e){if(e.target.dataset.action){if(e.target.dataset.action==="export"){const[t,s]=await L(fetch("/api/tasks/export"));t&&console.error("POST: Export lists: ",t);const[i,o]=await L(s.json());i&&console.error("JSON: Export lists response: ",i),this.saveJSON(o,"rminder_data")}e.target.dataset.action==="import"&&this.formContainer.showModal()}},_=function(){this.validateFile(this.inputElem.files[0])};customElements.define("rm-settings-switcher",ne);const c={Next:0,Previous:1},g={Task:0,List:1};function A(){var r;return((r=document.querySelector('meta[name="csrf-token"]'))==null?void 0:r.content)||""}var d,I,B,H,D,R,O;class oe extends HTMLElement{constructor(){super(...arguments);m(this,d);a(this,"observer");a(this,"isEditing",!1);a(this,"tasksList");a(this,"draggedTask",null)}setEdit(e,t){(e.tagName.toLowerCase()==="input"||e.tagName.toLowerCase()==="div"&&e.classList.contains("ql-editor"))&&(this.editing=t)}navigation(e,t){var l;const s=e===g.Task?this.querySelectorAll(".tasks__list > li input"):this.querySelectorAll("rm-list-nav input");if(s.length===0)return;const i=s.length-1,o=[...s].findIndex(p=>p.checked);t===c.Next&&o===i||t===c.Previous&&o===0||(o<0?s[t===c.Next?0:i].click():s[t===c.Next?o+1:o-1].click(),e===g.Task&&((l=this.querySelector(".tasks"))==null||l.scrollTo({top:o>=i/2?(o||1)*44:0,behavior:"smooth"})))}toggleTaskCompletion(){const e=[...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked);e>=0&&this.querySelectorAll(".tasks__list > li .completed-check")[e].click()}toggleTaskImportance(){const e=[...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked);e>=0&&this.querySelectorAll(".tasks__list > li .importance-check")[e].click()}switchPriority(){if([...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked)>=0){const t=this.querySelector(".details__priority select[name='priority']");t.value=t.value==="4"?0:Number(t.value)+1,t.dispatchEvent(new Event("change"))}}reorderTask(e){var i;window.getSelection().removeAllRanges();const t=this.querySelectorAll(".tasks__list > li input");if(t.length===0)return;const s=(i=[...t].find(o=>o.checked))==null?void 0:i.closest(".drag-item");s&&(e===c.Next&&s.nextSibling&&s.parentNode.insertBefore(s,s.nextSibling.nextSibling),e===c.Previous&&s.previousSibling&&s.parentNode.insertBefore(s,s.previousSibling),this.updateTaskPosition())}async updateTaskPosition(){var s;const e=[];if((s=this.querySelectorAll(".tasks__list > li"))==null||s.forEach((i,o)=>{+i.dataset.position!==o&&(e.push({id:+i.dataset.id,position:o}),i.dataset.position=o)}),e.length===0)return;const[t]=await L(fetch("/api/tasks/reorder",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":A()},body:JSON.stringify(e)}));t&&console.error("POST: Tasks reorder: ",t)}reorderList(e){var i;window.getSelection().removeAllRanges();const t=this.querySelectorAll(".lists-holder > rm-list-nav input");if(t.length===0)return;const s=(i=[...t].find(o=>o.checked))==null?void 0:i.closest("rm-list-nav");s&&(e===c.Next&&s.nextSibling&&s.parentNode.insertBefore(s,s.nextSibling.nextSibling),e===c.Previous&&s.previousSibling&&s.parentNode.insertBefore(s,s.previousSibling),this.updateListPosition())}async updateListPosition(){var s;const e=[];if((s=this.querySelectorAll(".lists-holder > rm-list-nav"))==null||s.forEach((i,o)=>{+i.dataset.position!==o&&(e.push({id:+i.dataset.id,position:o}),i.dataset.position=o)}),e.length===0)return;const[t]=await L(fetch("/api/lists/reorder",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":A()},body:JSON.stringify(e)}));t&&console.error("POST: Lists reorder: ",t)}connectedCallback(){var e,t,s;this.tasksList=this.querySelector(".tasks__list"),this.observer=new ResizeObserver(i=>{document.body.style.setProperty("--vh",`${document.body.clientHeight}px`)}),this.observer.observe(document.body),this.keybidings=h(this,d,I).bind(this),this.focusInHandler=h(this,d,B).bind(this),this.focusOutHandler=h(this,d,H).bind(this),this.handleDragStart=h(this,d,D).bind(this),this.handleDragOver=h(this,d,R).bind(this),this.handleDrop=h(this,d,O).bind(this),document.addEventListener("keydown",this.keybidings,!1),document.addEventListener("focusin",this.focusInHandler,!1),document.addEventListener("focusout",this.focusOutHandler,!1),(e=this.tasksList)==null||e.addEventListener("dragstart",this.handleDragStart,!1),(t=this.tasksList)==null||t.addEventListener("dragover",this.handleDragOver,!1),(s=this.tasksList)==null||s.addEventListener("drop",this.handleDrop,!1)}disconnectedCallback(){var e,t,s;this.observer.disconnect(),document.removeEventListener("keydown",this.keybidings,!1),document.removeEventListener("focusin",this.focusInHandler,!1),document.removeEventListener("focusout",this.focusOutHandler,!1),(e=this.tasksList)==null||e.removeEventListener("dragstart",this.handleDragStart,!1),(t=this.tasksList)==null||t.removeEventListener("dragover",this.handleDragOver,!1),(s=this.tasksList)==null||s.removeEventListener("drop",this.handleDrop,!1)}}d=new WeakSet,I=function(e){var t,s,i;if(!this.editing)switch(!0){case(e.shiftKey===!1&&(e.code==="ArrowDown"||e.code==="KeyJ")):this.navigation(g.Task,c.Next);break;case(e.shiftKey===!1&&(e.code==="ArrowUp"||e.code==="KeyK")):this.navigation(g.Task,c.Previous);break;case(e.shiftKey===!1&&e.code==="BracketLeft"):this.navigation(g.List,c.Next);break;case(e.shiftKey===!1&&e.code==="BracketRight"):this.navigation(g.List,c.Previous);break;case e.code==="KeyA":e.preventDefault(),(t=this.querySelector("#task"))==null||t.focus();break;case e.code==="KeyN":e.preventDefault(),(s=this.querySelector(".add-list"))==null||s.click();break;case e.code==="KeyF":e.preventDefault(),(i=this.querySelector(".searchbox"))==null||i.focus();break;case e.code==="KeyX":this.toggleTaskCompletion();break;case e.code==="KeyI":this.toggleTaskImportance();break;case e.code==="KeyP":this.switchPriority();break;case(e.shiftKey===!0&&(e.code==="ArrowDown"||e.code==="KeyJ")):this.reorderTask(c.Next);break;case(e.shiftKey===!0&&(e.code==="ArrowUp"||e.code==="KeyK")):this.reorderTask(c.Previous);break;case(e.shiftKey===!0&&e.code==="BracketLeft"):this.reorderList(c.Next);break;case(e.shiftKey===!0&&e.code==="BracketRight"):this.reorderList(c.Previous);break}},B=function(e){this.setEdit(e.target,!0)},H=function(e){this.setEdit(e.target,!1)},D=function(e){this.draggedTask=e.target,e.dataTransfer.effectAllowed="move",e.dataTransfer.setData("text/html",this.draggedTask.innerHTML),e.target.style.opacity="0.5"},R=function(e){e.preventDefault(),e.dataTransfer.dropEffect="move";const t=e.target.classList.contains("drag-item")?e.target:e.target.closest(".drag-item");if(t!==this.draggedTask&&t){const s=t.getBoundingClientRect(),i=s.top+s.height/2;e.clientY-i>0?(t.style.borderBottom="solid 2px transparent",t.style.borderTop=""):(t.style.borderTop="solid 2px transparent",t.style.borderBottom="")}},O=function(e){e.preventDefault();const t=e.target.classList.contains("drag-item")?e.target:e.target.closest(".drag-item");t!==this.draggedTask&&t&&(e.clientY>t.getBoundingClientRect().top+t.offsetHeight/2?t.parentNode.insertBefore(this.draggedTask,t.nextSibling):t.parentNode.insertBefore(this.draggedTask,t)),this.querySelectorAll(".drag-item").forEach(s=>{s.style.borderTop="",s.style.borderBottom=""}),this.draggedTask.style.opacity="",this.draggedTask=null,this.updateTaskPosition()};customElements.define("rm-tasks-app",oe);
+var se=Object.defineProperty;var P=r=>{throw TypeError(r)};var ie=(r,o,e)=>o in r?se(r,o,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[o]=e;var a=(r,o,e)=>ie(r,typeof o!="symbol"?o+"":o,e),A=(r,o,e)=>o.has(r)||P("Cannot "+e);var d=(r,o,e)=>(A(r,o,"read from private field"),e?e.call(r):o.get(r)),u=(r,o,e)=>o.has(r)?P("Cannot add the same private member more than once"):o instanceof WeakSet?o.add(r):o.set(r,e),m=(r,o,e,t)=>(A(r,o,"write to private field"),t?t.call(r,e):o.set(r,e),e),p=(r,o,e)=>(A(r,o,"access private method"),e);var C,H;class re extends HTMLElement{constructor(){super(...arguments);u(this,C);a(this,"quill")}get description(){return this.quill.getSemanticHTML()}async connectedCallback(){await p(this,C,H).call(this),this.getAttribute("task-id");const e=this.querySelector("button.close"),t=this.querySelector("button.remove"),s=this.querySelector("dialog.modal"),i=this.querySelector("dialog button.default"),n=this.querySelector("dialog button.warning");this.quill=new Quill("#note-editor",{modules:{toolbar:[[{header:[1,2,3,!1]}],["bold","italic","underline","strike"],["blockquote","code-block"],["link","image"],[{list:"ordered"},{list:"bullet"}]]},bounds:this,placeholder:"Add description...",theme:"snow"}),e.addEventListener("click",()=>{const l=document.querySelector("input[name='task-detail']:checked");l&&(l.checked=!1)}),t.addEventListener("click",()=>{s==null||s.showModal()}),i.addEventListener("click",()=>{s==null||s.close()}),n.addEventListener("click",()=>{s==null||s.close()})}}C=new WeakSet,H=async function(){if(!window.Quill){if(!document.querySelector('link[href="/assets/css/libs/quill.snow.css"]')){const e=document.createElement("link");e.rel="stylesheet",e.href="/assets/css/libs/quill.snow.css",document.head.appendChild(e)}return new Promise((e,t)=>{const s=document.createElement("script");s.src="/assets/js/libs/quill.js",s.onload=e,s.onerror=t,document.head.appendChild(s)})}};customElements.define("rm-task-details",re);var y,k,b,S,E;class oe extends HTMLElement{constructor(){super(...arguments);a(this,"cancelBtn");a(this,"addList");a(this,"formContainer");a(this,"clearIcon");a(this,"searchInput");a(this,"addGroup");a(this,"cancelGroupBtn");a(this,"groupFormContainer");u(this,y);u(this,k);u(this,b);u(this,S);u(this,E)}addEvents(){var e,t;m(this,y,()=>this.closePopover()),m(this,k,()=>this.formFocus()),m(this,b,()=>this.clearSearch()),m(this,S,()=>this.closeGroupPopover()),m(this,E,()=>this.groupFormFocus()),this.addList.addEventListener("click",d(this,k)),this.cancelBtn.addEventListener("click",d(this,y)),this.clearIcon.addEventListener("click",d(this,b)),(e=this.addGroup)==null||e.addEventListener("click",d(this,E)),(t=this.cancelGroupBtn)==null||t.addEventListener("click",d(this,S))}closePopover(){var i,n,l,f;this.formContainer.close();const e=this.formContainer.querySelector("form"),t=e.querySelector(".btn.add-new-list"),s=e.querySelector(".remove-list");e.reset(),e.removeAttribute("hx-put"),e.setAttribute("hx-post","/partials/lists/create"),(i=e.querySelectorAll("input:checked"))==null||i.forEach(g=>{g.removeAttribute("checked")}),(n=e.querySelectorAll("option"))==null||n.forEach(g=>{g.removeAttribute("selected")}),(l=e.querySelector('input[name="swatch"]'))==null||l.setAttribute("checked",""),(f=e.querySelector('input[name="icon"]'))==null||f.setAttribute("checked",""),t.innerHTML="Add",s==null||s.classList.add("hidden"),s==null||s.removeAttribute("hx-delete"),htmx.process(e)}formFocus(){this.formContainer.showModal(),this.formContainer.querySelector(".new-list").focus()}closeGroupPopover(){this.groupFormContainer.close();const e=this.groupFormContainer.querySelector("form"),t=e.querySelector(".btn.add-new-group"),s=e.querySelector(".remove-group");e.reset(),e.removeAttribute("hx-put"),e.setAttribute("hx-post","/partials/groups/create"),t.innerHTML="Add",s==null||s.classList.add("hidden"),s==null||s.removeAttribute("hx-delete"),htmx.process(e)}groupFormFocus(){this.groupFormContainer.showModal(),this.groupFormContainer.querySelector(".new-group").focus()}clearSearch(){this.searchInput.value=""}connectedCallback(){this.cancelBtn=this.querySelector(".cancel-new-list"),this.addList=this.querySelector(".add-list"),this.formContainer=this.querySelector(".list-form-container"),this.clearIcon=this.querySelector(".searchbox__container i.clear-icon"),this.searchInput=this.querySelector(".searchbox"),this.addGroup=this.querySelector(".add-group"),this.cancelGroupBtn=this.querySelector(".cancel-new-group"),this.groupFormContainer=this.querySelector(".group-form-container"),this.addEvents()}disconnectedCallback(){var e,t;this.addList.removeEventListener("click",d(this,k)),this.cancelBtn.removeEventListener("click",d(this,y)),this.clearIcon.removeEventListener("click",d(this,b)),(e=this.addGroup)==null||e.removeEventListener("click",d(this,E)),(t=this.cancelGroupBtn)==null||t.removeEventListener("click",d(this,S))}}y=new WeakMap,k=new WeakMap,b=new WeakMap,S=new WeakMap,E=new WeakMap;customElements.define("rm-sidebar",oe);var L;class ne extends HTMLElement{constructor(){super(...arguments);a(this,"listId");a(this,"listName");a(this,"colour");a(this,"icon");a(this,"position");a(this,"pinned");a(this,"filter");a(this,"settings");a(this,"formContainer");u(this,L)}edit(e){var N;e.preventDefault();const t=this.formContainer.querySelector("form");(N=t.querySelectorAll("input:checked"))==null||N.forEach(T=>{T.removeAttribute("checked")});const s=t.querySelector("input.new-list"),i=t.querySelector('input[name="pin"]'),n=t.querySelector(`input[name="swatch"][value="${this.colour}"]`),l=t.querySelector(`input[name="icon"][value="${this.icon}"]`),f=t.querySelector(".btn.add-new-list"),g=t.querySelector(".remove-list"),$=t.querySelector('select[name="include"]'),G=t.querySelector('select[name="completed"]'),j=t.querySelector('select[name="important"]'),J=t.querySelector('select[name="priority"]'),U=t.querySelector('select[name="date"]'),z=t.querySelector('input[name="from"]'),Q=t.querySelector('input[name="to"]'),[X,Y,V,W,Z,ee,te]=this.filter.split(";").map(T=>T.split("=")[1]);t.removeAttribute("hx-post"),t.setAttribute("hx-put",`/partials/lists/${this.listId}`),g.setAttribute("hx-delete",`/partials/lists/${this.listId}`),s.value=this.listName,i.toggleAttribute("checked",this.pinned===""),n.setAttribute("checked",""),l.setAttribute("checked",""),f.innerHTML="Update",g.classList.remove("hidden"),$.value=X,G.value=Y,j.value=V,J.value=W,U.value=Z,z.value=ee,Q.value=te,htmx.process(t),this.formContainer.showModal()}connectedCallback(){var e;this.formContainer=document.querySelector(".list-form-container"),this.settings=this.querySelector(".settings"),this.listId=this.dataset.id,this.listName=this.dataset.name,this.colour=this.dataset.colour,this.icon=this.dataset.icon,this.pinned=this.dataset.pinned,this.position=this.dataset.position,this.filter=this.dataset.filter,m(this,L,t=>this.edit(t)),this.style.setProperty("--list-colour",`var(${this.colour})`),(e=this.settings)==null||e.addEventListener("click",d(this,L))}disconnectedCallback(){var e;(e=this.settings)==null||e.removeEventListener("click",d(this,L))}}L=new WeakMap;customElements.define("rm-list-nav",ne);var q;class ae extends HTMLElement{constructor(){super(...arguments);a(this,"groupId");a(this,"groupName");a(this,"settings");a(this,"formContainer");u(this,q)}edit(e){e.preventDefault();const t=this.formContainer.querySelector("form"),s=t.querySelector("input.new-group"),i=t.querySelector(".btn.add-new-group"),n=t.querySelector(".remove-group");t.removeAttribute("hx-post"),t.setAttribute("hx-put",`/partials/groups/${this.groupId}`),n.setAttribute("hx-delete",`/partials/groups/${this.groupId}`),s.value=this.groupName,i.innerHTML="Update",n.classList.remove("hidden"),htmx.process(t),htmx.process(n),this.formContainer.showModal(),s.focus()}connectedCallback(){var e,t,s;this.formContainer=document.querySelector(".group-form-container"),this.settings=this.querySelector(".group-settings"),this.groupId=this.dataset.groupId,this.groupName=((t=(e=this.querySelector(".group-name"))==null?void 0:e.textContent)==null?void 0:t.trim())??"",m(this,q,i=>this.edit(i)),(s=this.settings)==null||s.addEventListener("click",d(this,q))}disconnectedCallback(){var e;(e=this.settings)==null||e.removeEventListener("click",d(this,q))}}q=new WeakMap;customElements.define("rm-group-nav",ae);class le extends HTMLElement{connectedCallback(){const o=this.getBoundingClientRect(),e=document.body.getBoundingClientRect();this.style.setProperty("--popover-top",`${o.bottom}px`),this.style.setProperty("--popover-right",`${e.right-o.right+10}px`)}}customElements.define("rm-app-switcher",le);async function w(r){try{return[null,await r]}catch(o){return[o]}}var x,B,_;class ce extends HTMLElement{constructor(){super(...arguments);u(this,x);a(this,"formContainer");a(this,"fileElem");a(this,"messages");a(this,"maxFileSize",10*1024*1024);a(this,"allowedTypes",["application/json"])}saveJSON(e,t){var s=JSON.stringify(e,null,2),i=new Blob([s],{type:"application/json"}),n=URL.createObjectURL(i),l=document.createElement("a");l.download=t+".json",l.href=n,l.id=t,document.body.appendChild(l),l.click(),document.querySelector("#"+l.id).remove()}validateFile(e){return this.messages.textContent="",this.allowedTypes.includes(e.type)?e.size>this.maxFileSize?(this.showError(`${e.name} exceeds the maximum file size of 10MB.`),!1):!0:(this.showError(`${e.name} is not an allowed file type.`),!1)}showError(e){this.messages.textContent=e}connectedCallback(){const e=this.getBoundingClientRect(),t=document.body.getBoundingClientRect();this.formContainer=this.querySelector(".file-form-container"),this.inputElem=this.querySelector("#file"),this.messages=this.querySelector("#messages"),this.style.setProperty("--popover-top",`${e.bottom}px`),this.style.setProperty("--popover-right",`${t.right-e.right+10}px`),this.handleClick=p(this,x,B).bind(this),this.handleFiles=p(this,x,_).bind(this),this.addEventListener("click",this.handleClick,!1),this.inputElem.addEventListener("change",this.handleFiles,!1)}disconnectedCallback(){this.removeEventListener("click",this.handleClick,!1),this.inputElem.removeEventListener("change",this.handleFiles,!1)}}x=new WeakSet,B=async function(e){if(e.target.dataset.action){if(e.target.dataset.action==="export"){const[t,s]=await w(fetch("/api/tasks/export"));t&&console.error("POST: Export lists: ",t);const[i,n]=await w(s.json());i&&console.error("JSON: Export lists response: ",i),this.saveJSON(n,"rminder_data")}e.target.dataset.action==="import"&&this.formContainer.showModal()}},_=function(){this.validateFile(this.inputElem.files[0])};customElements.define("rm-settings-switcher",ce);const c={Next:0,Previous:1},v={Task:0,List:1};function I(){var r;return((r=document.querySelector('meta[name="csrf-token"]'))==null?void 0:r.content)||""}var h,F,D,R,O,K,M;class de extends HTMLElement{constructor(){super(...arguments);u(this,h);a(this,"observer");a(this,"isEditing",!1);a(this,"tasksList");a(this,"draggedTask",null)}setEdit(e,t){(e.tagName.toLowerCase()==="input"||e.tagName.toLowerCase()==="div"&&e.classList.contains("ql-editor"))&&(this.editing=t)}navigation(e,t){var l;const s=e===v.Task?this.querySelectorAll(".tasks__list > li input"):this.querySelectorAll("rm-list-nav input");if(s.length===0)return;const i=s.length-1,n=[...s].findIndex(f=>f.checked);t===c.Next&&n===i||t===c.Previous&&n===0||(n<0?s[t===c.Next?0:i].click():s[t===c.Next?n+1:n-1].click(),e===v.Task&&((l=this.querySelector(".tasks"))==null||l.scrollTo({top:n>=i/2?(n||1)*44:0,behavior:"smooth"})))}toggleTaskCompletion(){const e=[...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked);e>=0&&this.querySelectorAll(".tasks__list > li .completed-check")[e].click()}toggleTaskImportance(){const e=[...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked);e>=0&&this.querySelectorAll(".tasks__list > li .importance-check")[e].click()}switchPriority(){if([...this.querySelectorAll(".tasks__list > li input")].findIndex(t=>t.checked)>=0){const t=this.querySelector(".details__priority select[name='priority']");t.value=t.value==="4"?0:Number(t.value)+1,t.dispatchEvent(new Event("change"))}}reorderTask(e){var i;window.getSelection().removeAllRanges();const t=this.querySelectorAll(".tasks__list > li input");if(t.length===0)return;const s=(i=[...t].find(n=>n.checked))==null?void 0:i.closest(".drag-item");s&&(e===c.Next&&s.nextSibling&&s.parentNode.insertBefore(s,s.nextSibling.nextSibling),e===c.Previous&&s.previousSibling&&s.parentNode.insertBefore(s,s.previousSibling),this.updateTaskPosition())}async updateTaskPosition(){var s;const e=[];if((s=this.querySelectorAll(".tasks__list > li"))==null||s.forEach((i,n)=>{+i.dataset.position!==n&&(e.push({id:+i.dataset.id,position:n}),i.dataset.position=n)}),e.length===0)return;const[t]=await w(fetch("/api/tasks/reorder",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":I()},body:JSON.stringify(e)}));t&&console.error("POST: Tasks reorder: ",t)}reorderList(e){var i;window.getSelection().removeAllRanges();const t=this.querySelectorAll(".lists-holder > rm-list-nav input");if(t.length===0)return;const s=(i=[...t].find(n=>n.checked))==null?void 0:i.closest("rm-list-nav");s&&(e===c.Next&&s.nextSibling&&s.parentNode.insertBefore(s,s.nextSibling.nextSibling),e===c.Previous&&s.previousSibling&&s.parentNode.insertBefore(s,s.previousSibling),this.updateListPosition())}async updateListPosition(){var s;const e=[];if((s=this.querySelectorAll(".lists-holder > rm-list-nav"))==null||s.forEach((i,n)=>{+i.dataset.position!==n&&(e.push({id:+i.dataset.id,position:n}),i.dataset.position=n)}),e.length===0)return;const[t]=await w(fetch("/api/lists/reorder",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":I()},body:JSON.stringify(e)}));t&&console.error("POST: Lists reorder: ",t)}connectedCallback(){var e,t,s;this.tasksList=this.querySelector(".tasks__list"),this.observer=new ResizeObserver(i=>{document.body.style.setProperty("--vh",`${document.body.clientHeight}px`)}),this.observer.observe(document.body),this.keybidings=p(this,h,F).bind(this),this.focusInHandler=p(this,h,D).bind(this),this.focusOutHandler=p(this,h,R).bind(this),this.handleDragStart=p(this,h,O).bind(this),this.handleDragOver=p(this,h,K).bind(this),this.handleDrop=p(this,h,M).bind(this),document.addEventListener("keydown",this.keybidings,!1),document.addEventListener("focusin",this.focusInHandler,!1),document.addEventListener("focusout",this.focusOutHandler,!1),(e=this.tasksList)==null||e.addEventListener("dragstart",this.handleDragStart,!1),(t=this.tasksList)==null||t.addEventListener("dragover",this.handleDragOver,!1),(s=this.tasksList)==null||s.addEventListener("drop",this.handleDrop,!1)}disconnectedCallback(){var e,t,s;this.observer.disconnect(),document.removeEventListener("keydown",this.keybidings,!1),document.removeEventListener("focusin",this.focusInHandler,!1),document.removeEventListener("focusout",this.focusOutHandler,!1),(e=this.tasksList)==null||e.removeEventListener("dragstart",this.handleDragStart,!1),(t=this.tasksList)==null||t.removeEventListener("dragover",this.handleDragOver,!1),(s=this.tasksList)==null||s.removeEventListener("drop",this.handleDrop,!1)}}h=new WeakSet,F=function(e){var t,s,i;if(!this.editing)switch(!0){case(e.shiftKey===!1&&(e.code==="ArrowDown"||e.code==="KeyJ")):this.navigation(v.Task,c.Next);break;case(e.shiftKey===!1&&(e.code==="ArrowUp"||e.code==="KeyK")):this.navigation(v.Task,c.Previous);break;case(e.shiftKey===!1&&e.code==="BracketLeft"):this.navigation(v.List,c.Next);break;case(e.shiftKey===!1&&e.code==="BracketRight"):this.navigation(v.List,c.Previous);break;case e.code==="KeyA":e.preventDefault(),(t=this.querySelector("#task"))==null||t.focus();break;case e.code==="KeyN":e.preventDefault(),(s=this.querySelector(".add-list"))==null||s.click();break;case e.code==="KeyF":e.preventDefault(),(i=this.querySelector(".searchbox"))==null||i.focus();break;case e.code==="KeyX":this.toggleTaskCompletion();break;case e.code==="KeyI":this.toggleTaskImportance();break;case e.code==="KeyP":this.switchPriority();break;case(e.shiftKey===!0&&(e.code==="ArrowDown"||e.code==="KeyJ")):this.reorderTask(c.Next);break;case(e.shiftKey===!0&&(e.code==="ArrowUp"||e.code==="KeyK")):this.reorderTask(c.Previous);break;case(e.shiftKey===!0&&e.code==="BracketLeft"):this.reorderList(c.Next);break;case(e.shiftKey===!0&&e.code==="BracketRight"):this.reorderList(c.Previous);break}},D=function(e){this.setEdit(e.target,!0)},R=function(e){this.setEdit(e.target,!1)},O=function(e){this.draggedTask=e.target,e.dataTransfer.effectAllowed="move",e.dataTransfer.setData("text/html",this.draggedTask.innerHTML),e.target.style.opacity="0.5"},K=function(e){e.preventDefault(),e.dataTransfer.dropEffect="move";const t=e.target.classList.contains("drag-item")?e.target:e.target.closest(".drag-item");if(t!==this.draggedTask&&t){const s=t.getBoundingClientRect(),i=s.top+s.height/2;e.clientY-i>0?(t.style.borderBottom="solid 2px transparent",t.style.borderTop=""):(t.style.borderTop="solid 2px transparent",t.style.borderBottom="")}},M=function(e){e.preventDefault();const t=e.target.classList.contains("drag-item")?e.target:e.target.closest(".drag-item");t!==this.draggedTask&&t&&(e.clientY>t.getBoundingClientRect().top+t.offsetHeight/2?t.parentNode.insertBefore(this.draggedTask,t.nextSibling):t.parentNode.insertBefore(this.draggedTask,t)),this.querySelectorAll(".drag-item").forEach(s=>{s.style.borderTop="",s.style.borderBottom=""}),this.draggedTask.style.opacity="",this.draggedTask=null,this.updateTaskPosition()};customElements.define("rm-tasks-app",de);
diff --git a/web/templates/apps/tasks/components/sidebar.html b/web/templates/apps/tasks/components/sidebar.html
index 9017986..2d4afe9 100644
--- a/web/templates/apps/tasks/components/sidebar.html
+++ b/web/templates/apps/tasks/components/sidebar.html
@@ -2,8 +2,8 @@
My lists