From f51c41ec1b643aa7674cc232e1a48c5c58ca6eb9 Mon Sep 17 00:00:00 2001 From: Moira Daniella A Sebastian Date: Mon, 16 Mar 2026 18:30:15 +0100 Subject: [PATCH 01/73] Change priority for WEB --- pkg/web/handler/dashboard.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/web/handler/dashboard.go b/pkg/web/handler/dashboard.go index 4c32a37..6184afd 100644 --- a/pkg/web/handler/dashboard.go +++ b/pkg/web/handler/dashboard.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "sort" "strings" "github.com/LazyBachelor/LazyPM/internal/models" @@ -20,6 +21,11 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { return } + // Sort issues by highest priority first (4 -> 0) + sort.Slice(issues, func(i, j int) bool { + return issues[i].Priority > issues[j].Priority + }) + // Check if board view is requested isBoardView := r.URL.Query().Get("board") == "true" From ada6c9c6c39f85306b6e908e88113e88aab7d423 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 17 Mar 2026 14:48:27 +0100 Subject: [PATCH 02/73] make sure the description is visible in terminals --- pkg/repl/options.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/repl/options.go b/pkg/repl/options.go index 0d0918c..73e1eb1 100644 --- a/pkg/repl/options.go +++ b/pkg/repl/options.go @@ -10,10 +10,14 @@ const OptionMaxSuggestions = 5 func promptOptions(history []string) []prompt.Option { return []prompt.Option{ prompt.OptionPrefixTextColor(prompt.Cyan), + prompt.OptionSuggestionTextColor(prompt.White), prompt.OptionMaxSuggestion(OptionMaxSuggestions), + prompt.OptionSelectedSuggestionTextColor(prompt.Cyan), prompt.OptionSuggestionBGColor(prompt.DefaultColor), prompt.OptionSelectedSuggestionBGColor(prompt.DefaultColor), + prompt.OptionDescriptionTextColor(prompt.White), prompt.OptionDescriptionBGColor(prompt.DefaultColor), + prompt.OptionSelectedDescriptionTextColor(prompt.White), prompt.OptionSelectedDescriptionBGColor(prompt.DefaultColor), prompt.OptionPreviewSuggestionBGColor(prompt.DefaultColor), prompt.OptionScrollbarBGColor(prompt.DefaultColor), From 6e591fa81d789c68cac2e6cdcdca4fea3a3c3501 Mon Sep 17 00:00:00 2001 From: Ine Maria Nilssen Aanonsen Date: Tue, 17 Mar 2026 15:26:25 +0100 Subject: [PATCH 03/73] SPRINT PLANNING TASK, remove duplicate task requirement --- cmd/pm/tasks/sprintPlanning.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cmd/pm/tasks/sprintPlanning.go b/cmd/pm/tasks/sprintPlanning.go index a21629b..86b26fb 100644 --- a/cmd/pm/tasks/sprintPlanning.go +++ b/cmd/pm/tasks/sprintPlanning.go @@ -152,22 +152,16 @@ func (t *SprintPlanningTask) Validate(ctx context.Context) ValidationFeedback { top := sorted[:topN] var plannedCount int - var readyToSprintCount int for _, issue := range top { if issue.Status == models.StatusReadyToSprint || issue.Status == models.StatusInProgress || issue.Status == models.StatusClosed { plannedCount++ } - if issue.Status == models.StatusReadyToSprint { - readyToSprintCount++ - } } expect.Assert(plannedCount >= 3, "Expected at least 3 of the 5 highest-priority issues to be moved into 'ready_to_sprint', 'in_progress', or 'closed' for the sprint.") - expect.Assert(readyToSprintCount >= 1, - "Expected at least one of the highest-priority issues to be marked as 'ready_to_sprint' to indicate it is planned for the sprint.") return expect.Complete() } From 6f452a5f9ee8b7b129f6b118315dbcd39ba47991 Mon Sep 17 00:00:00 2001 From: Ine Maria Nilssen Aanonsen Date: Tue, 17 Mar 2026 15:47:40 +0100 Subject: [PATCH 04/73] LPM-81: Assign to me --- pkg/web/components/assignee_form.templ | 29 +++++++++++++++++++------- pkg/web/handler/issues.go | 7 ++++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/pkg/web/components/assignee_form.templ b/pkg/web/components/assignee_form.templ index e67032c..89f9cc2 100644 --- a/pkg/web/components/assignee_form.templ +++ b/pkg/web/components/assignee_form.templ @@ -12,12 +12,27 @@ templ AssigneeForm(props AssigneeFormProps) { hx-patch={ props.PatchAction } hx-swap="innerHTML" > - @base.Input(base.InputProps{ - Name: "assignee", - Type: "text", - Placeholder: "Enter assingee", - Value: props.Assignee, - }) - +
+ + if props.Assignee != "" { +

+ Current assignee: { props.Assignee } +

+ } +
+ } diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index 3f48bc1..dbcaa94 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -196,7 +196,12 @@ func UpdateIssue(w http.ResponseWriter, r *http.Request) { func UpdateAssignee(w http.ResponseWriter, r *http.Request) { issue := r.Context().Value(issueKey).(*models.Issue) - assignee := r.FormValue("assignee") + assignMe := r.FormValue("assign_me") + + assignee := "" + if assignMe != "" { + assignee = "Me" + } if err := App(r).Issues.UpdateIssue(r.Context(), issue.ID, map[string]any{"assignee": assignee}, ""); err != nil { http.Error(w, "Failed to update assignee", http.StatusInternalServerError) From 6a91abc13f1fc169e04bc228ac080939b914de44 Mon Sep 17 00:00:00 2001 From: Moira Daniella A Sebastian Date: Tue, 17 Mar 2026 15:53:25 +0100 Subject: [PATCH 05/73] Fixed --- cmd/pm/tasks/issueReviewCleanup.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/pm/tasks/issueReviewCleanup.go b/cmd/pm/tasks/issueReviewCleanup.go index 9d7834a..69241e4 100644 --- a/cmd/pm/tasks/issueReviewCleanup.go +++ b/cmd/pm/tasks/issueReviewCleanup.go @@ -11,10 +11,8 @@ const issueReviewCleanupDescription = `You are responsible for reviewing and mai Using the system, complete the following steps: -1. Open three different issues and read their titles and descriptions -2. Add a comment to two issues -3. Delete this cleanup task issue ("Issue Review and Cleanup Task") from the issue list — do not delete the other project issues -4. Confirm that the cleanup task issue no longer appears in the list` +1. Add a comment to two issues +2. Delete this cleanup task issue ("Issue Review and Cleanup Task") from the issue list — do not delete the other project issues` type IssueReviewCleanupTask struct { done bool From 9ac216cbb419c71eb22e13fb70c5e14fefe7287c Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 17 Mar 2026 16:45:21 +0100 Subject: [PATCH 06/73] update these for better ux and ui inside os --- build/Dockerfile | 3 +- build/setup-desktop.sh | 103 +++++++++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/build/Dockerfile b/build/Dockerfile index 8cfc7fa..00a3e9a 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -6,8 +6,7 @@ RUN apk add --no-cache \ gcompat \ libc6-compat \ bash \ - bash-completion \ - alacritty && \ + bash-completion && \ update-ca-certificates COPY pm /usr/local/bin/pm diff --git a/build/setup-desktop.sh b/build/setup-desktop.sh index e076b15..af1e18f 100644 --- a/build/setup-desktop.sh +++ b/build/setup-desktop.sh @@ -2,14 +2,14 @@ # This script runs as root when the container starts echo "**** Setting up custom desktop shortcuts ****" -# Ensure the Desktop folder exists for the default 'abc' user mkdir -p /config/Desktop +mkdir -p /config/.config/xfce4/xfconf/xfce-perchannel-xml cat < /config/Desktop/LazyPM.desktop [Desktop Entry] Version=1.0 Type=Application -Name=Project Management Survey +Name=PM Survey Comment=Launch the Project Management Survey Exec=/usr/local/bin/pm start Icon=utilities-terminal @@ -17,30 +17,85 @@ Terminal=true Categories=Utility; EOF -chmod +x /config/Desktop/LazyPM.desktop -chown abc:abc /config/Desktop/LazyPM.desktop +cat < /config/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-terminal.xml + -pm # initialize the service to ensure the database is set up - -echo "**** Setting default browser to Chromium... ****" - -xdg-settings set default-web-browser chromium.desktop - -echo "**** Setting default handlers for HTTP and HTTPS... ****" -xdg-mime default chromium.desktop x-scheme-handler/http -xdg-mime default chromium.desktop x-scheme-handler/https - -echo "**** Setting up bash completion... ****" -source /etc/bash/bash_completion.sh - -echo "Setting Alacritty as default terminal..." -mkdir -p /config/.config/xfce4 - -cat > /config/.config/xfce4/helpers.rc < + + + + + + + + + + + + + + + + + + + + + + + + EOF -chown -R abc:abc /config/.config +cat < /config/.config/xfce4/xfconf/xfce-perchannel-xml/xsettings.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +EOF + +chmod +x /config/Desktop/LazyPM.desktop +chown -R abc:abc /config/Desktop /config/.config echo "**** Desktop setup complete ****" \ No newline at end of file From 40d25cc716b5ded4b38bd17f77cf5391f74752c6 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 18 Mar 2026 11:23:10 +0100 Subject: [PATCH 07/73] start with upgdrade (broken right now) --- cmd/pm/intro.go | 6 +- cmd/pm/intro_questionnare.go | 4 +- cmd/pm/main.go | 4 +- cmd/pm/runner.go | 4 +- cmd/pm/tasks/backlogRefinement.go | 2 +- cmd/pm/tasks/base.go | 2 +- cmd/pm/tasks/codingTask.go | 2 +- cmd/pm/tasks/createIssue.go | 2 +- cmd/pm/tasks/dependencyManagement.go | 2 +- cmd/pm/tasks/gitTask.go | 2 +- cmd/pm/tasks/priorityManagement.go | 2 +- cmd/pm/tasks/sprintPlanning.go | 2 +- go.mod | 64 ++++++------ go.sum | 136 ++++++++++++-------------- internal/app/initializer.go | 2 +- internal/commands/issues/close.go | 2 +- internal/commands/issues/delete.go | 2 +- internal/commands/issues/list.go | 6 +- internal/commands/issues/root.go | 2 +- internal/models/task.go | 2 +- internal/storage/mongo.go | 8 +- internal/style/styles.go | 2 +- internal/style/themes.go | 8 +- pkg/task/questionnaire.go | 4 +- pkg/task/runner.go | 2 +- pkg/task/taskui.go | 4 +- pkg/tui/components/header.go | 2 +- pkg/tui/components/helpbar.go | 2 +- pkg/tui/components/issue_detail.go | 6 +- pkg/tui/components/issue_list.go | 13 +-- pkg/tui/components/keymap.go | 3 +- pkg/tui/components/modals.go | 2 +- pkg/tui/issues/operations.go | 45 +++++++-- pkg/tui/styles/styles.go | 2 +- pkg/tui/tui.go | 2 +- pkg/tui/views/dashboard/keys.go | 4 +- pkg/tui/views/dashboard/model.go | 20 ++-- pkg/tui/views/dashboard/operations.go | 4 +- pkg/tui/views/dashboard/view.go | 2 +- pkg/tui/views/kanban/keys.go | 4 +- pkg/tui/views/kanban/model.go | 37 +++---- pkg/tui/views/kanban/operations.go | 4 +- pkg/tui/views/kanban/view.go | 2 +- pkg/tui/views/root.go | 2 +- pkg/web/web.go | 2 +- 45 files changed, 218 insertions(+), 218 deletions(-) diff --git a/cmd/pm/intro.go b/cmd/pm/intro.go index b686afe..9d23844 100644 --- a/cmd/pm/intro.go +++ b/cmd/pm/intro.go @@ -3,11 +3,11 @@ package main import ( "strings" + "charm.land/bubbles/v2/key" + "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/style" - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) const stages = 2 diff --git a/cmd/pm/intro_questionnare.go b/cmd/pm/intro_questionnare.go index 2bdf809..d9486a9 100644 --- a/cmd/pm/intro_questionnare.go +++ b/cmd/pm/intro_questionnare.go @@ -1,9 +1,9 @@ package main import ( + "charm.land/bubbletea/v2" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/pkg/task" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/huh" ) type IntroQuestionnaire struct{} diff --git a/cmd/pm/main.go b/cmd/pm/main.go index d6bd53c..49c5dbf 100644 --- a/cmd/pm/main.go +++ b/cmd/pm/main.go @@ -3,9 +3,9 @@ package main import ( "context" + "charm.land/fang/v2" "github.com/LazyBachelor/LazyPM/internal/app" - issues "github.com/LazyBachelor/LazyPM/internal/commands/issues" - "github.com/charmbracelet/fang" + "github.com/LazyBachelor/LazyPM/internal/commands/issues" ) var App *app.App diff --git a/cmd/pm/runner.go b/cmd/pm/runner.go index 98c69d3..d524ed8 100644 --- a/cmd/pm/runner.go +++ b/cmd/pm/runner.go @@ -7,11 +7,11 @@ import ( "math/rand" "time" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/cmd/pm/tasks" "github.com/LazyBachelor/LazyPM/internal/commands/survey" "github.com/LazyBachelor/LazyPM/internal/storage" "github.com/LazyBachelor/LazyPM/pkg/task" - "github.com/charmbracelet/huh" "github.com/spf13/cobra" ) @@ -54,7 +54,7 @@ func runStartCmd(cmd *cobra.Command, args []string) error { Title("Do you want to continue without submitting your responses?"). Description("You can fix your database connection and submit your responses later with the submit command."). Value(&continueWithoutSubmitting). - WithTheme(huh.ThemeBase16()). + WithTheme(huh.ThemeBase16(true)). RunAccessible(cmd.OutOrStdout(), cmd.InOrStdin()); err != nil { return fmt.Errorf("failed to read user input: %w", err) } diff --git a/cmd/pm/tasks/backlogRefinement.go b/cmd/pm/tasks/backlogRefinement.go index c39c1a0..f04201f 100644 --- a/cmd/pm/tasks/backlogRefinement.go +++ b/cmd/pm/tasks/backlogRefinement.go @@ -4,9 +4,9 @@ import ( "context" "strings" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" - "github.com/charmbracelet/huh" ) const backlogRefinementDescription = `You are tasked with backlog refinement. diff --git a/cmd/pm/tasks/base.go b/cmd/pm/tasks/base.go index f0be177..057ea88 100644 --- a/cmd/pm/tasks/base.go +++ b/cmd/pm/tasks/base.go @@ -3,13 +3,13 @@ package tasks import ( "context" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/repl" "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/LazyBachelor/LazyPM/pkg/tui" "github.com/LazyBachelor/LazyPM/pkg/web" - "github.com/charmbracelet/huh" ) type App = app.App diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 0573ced..18d313a 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -5,9 +5,9 @@ import ( "os" "strings" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" - "github.com/charmbracelet/huh" ) const codingDescription = `You are tasked with doing a chore in the codebase. diff --git a/cmd/pm/tasks/createIssue.go b/cmd/pm/tasks/createIssue.go index b78f194..c34dc8e 100644 --- a/cmd/pm/tasks/createIssue.go +++ b/cmd/pm/tasks/createIssue.go @@ -4,9 +4,9 @@ import ( "context" "fmt" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" - "github.com/charmbracelet/huh" ) const description = `You are tasked with creating a new issue in the project management system. diff --git a/cmd/pm/tasks/dependencyManagement.go b/cmd/pm/tasks/dependencyManagement.go index ac620f7..d1c6a4a 100644 --- a/cmd/pm/tasks/dependencyManagement.go +++ b/cmd/pm/tasks/dependencyManagement.go @@ -4,9 +4,9 @@ import ( "context" "fmt" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" - "github.com/charmbracelet/huh" ) const dependencyManagementDescription = `You are tasked with managing issue dependencies. diff --git a/cmd/pm/tasks/gitTask.go b/cmd/pm/tasks/gitTask.go index 5ed1515..40be4f6 100644 --- a/cmd/pm/tasks/gitTask.go +++ b/cmd/pm/tasks/gitTask.go @@ -6,9 +6,9 @@ import ( "os" "strings" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" - "github.com/charmbracelet/huh" "github.com/go-git/go-git/v6" ) diff --git a/cmd/pm/tasks/priorityManagement.go b/cmd/pm/tasks/priorityManagement.go index 2351e19..2bb729b 100644 --- a/cmd/pm/tasks/priorityManagement.go +++ b/cmd/pm/tasks/priorityManagement.go @@ -4,9 +4,9 @@ import ( "context" "fmt" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" - "github.com/charmbracelet/huh" ) const priorityManagementDescription = `You are tasked with managing issue priorities. diff --git a/cmd/pm/tasks/sprintPlanning.go b/cmd/pm/tasks/sprintPlanning.go index 86b26fb..68330ca 100644 --- a/cmd/pm/tasks/sprintPlanning.go +++ b/cmd/pm/tasks/sprintPlanning.go @@ -3,9 +3,9 @@ package tasks import ( "context" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" - "github.com/charmbracelet/huh" ) const sprintPlanningDescription = `You are tasked with sprint planning. diff --git a/go.mod b/go.mod index c41c87b..ee97108 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,26 @@ module github.com/LazyBachelor/LazyPM -go 1.25.6 +go 1.26.1 require ( - charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 - github.com/go-git/go-git/v6 v6.0.0-20260222090600-424e9964d3a3 + github.com/go-git/go-git/v6 v6.0.0-20260317113930-fb0d09929504 github.com/joho/godotenv v1.5.1 github.com/muesli/reflow v0.3.0 github.com/steveyegge/beads v0.49.6 go.mongodb.org/mongo-driver v1.17.9 + go.mongodb.org/mongo-driver/v2 v2.5.0 ) // Terminal dependencies require ( + charm.land/bubbles/v2 v2.0.0 + charm.land/bubbletea/v2 v2.0.2 + charm.land/fang/v2 v2.0.1 + charm.land/huh/v2 v2.0.3 + charm.land/lipgloss/v2 v2.0.2 github.com/c-bata/go-prompt v0.2.6 - github.com/charmbracelet/bubbles v0.21.1 - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/fang v0.4.4 - github.com/charmbracelet/huh v0.8.0 - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/spf13/cobra v1.10.2 - golang.org/x/term v0.40.0 + golang.org/x/term v0.41.0 ) // Web dependencies @@ -36,61 +36,55 @@ require ( require ( github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20260209111912-3cca7cf7b09b // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260316091819-b93f6a3b8502 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/exp/charmtone v0.0.0-20260209194814-eeb2896ac759 // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20260316093931-f2fb44ab3145 // indirect + github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect github.com/charmbracelet/x/exp/strings v0.1.0 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/cli/browser v1.3.0 // indirect - github.com/clipperhouse/displaywidth v0.10.0 // indirect - github.com/clipperhouse/uax29/v2 v2.6.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect - github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // indirect + github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/haatos/goshipit v0.0.0-20260305043009-36e5c9a2e5c6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/kevinburke/ssh_config v1.5.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/kevinburke/ssh_config v1.6.0 // indirect + github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/mattn/go-tty v0.0.7 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/montanaflynn/stats v0.7.1 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/mango v0.2.0 // indirect github.com/muesli/mango-cobra v1.3.0 // indirect github.com/muesli/mango-pflag v0.2.0 // indirect github.com/muesli/roff v0.1.0 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/natefinch/atomic v1.0.1 // indirect github.com/ncruces/go-sqlite3 v0.30.5 // indirect github.com/ncruces/julianday v1.0.0 // indirect @@ -98,6 +92,7 @@ require ( github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/sergi/go-diff v1.4.0 // indirect @@ -113,14 +108,15 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.48.0 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/exp v0.0.0-20260209203927-2842357ff358 // indirect golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.50.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/tools v0.42.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3ce74b3..18ba4da 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,21 @@ -charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k= -charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU= +charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= +charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= +charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= +charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= +charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY= +charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII= +charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= +charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= +charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= +charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= +github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= github.com/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY= @@ -20,42 +28,30 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= -github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/charmbracelet/bubbles v0.21.1 h1:nj0decPiixaZeL9diI4uzzQTkkz1kYY8+jgzCZXSmW0= -github.com/charmbracelet/bubbles v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= -github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= -github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY= -github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo= -github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= -github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/ultraviolet v0.0.0-20260209111912-3cca7cf7b09b h1:jyHmbVXscPtC1S4Cg2OW1Zq3bwTJoY6/q40Ahi8CqaA= -github.com/charmbracelet/ultraviolet v0.0.0-20260209111912-3cca7cf7b09b/go.mod h1:42rCfhmE+4ZM7twEctghIzlIWyPj6FCDTBiMepHE2Ss= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/ultraviolet v0.0.0-20260316091819-b93f6a3b8502 h1:hzWNs3UQRSUTS6YCbLaQnwqKBFXT5Yh1OOw6+26apqg= +github.com/charmbracelet/ultraviolet v0.0.0-20260316091819-b93f6a3b8502/go.mod h1:mkUCcxn9w9j89JJp3pOza5tmDQZPgIB75UfmQlFYvas= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= -github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= -github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= -github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= -github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= +github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20260209194814-eeb2896ac759 h1:U0li+kYsNefPhJPP9SfwQJNbQ58iXWRtidnj1Rqf//g= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20260209194814-eeb2896ac759/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260316093931-f2fb44ab3145 h1:btzh82+J19XPOKCSIBuEGmfFVnSb6UNIg1xAkrUeeAU= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260316093931-f2fb44ab3145/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= +github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8= github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA= github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= @@ -64,16 +60,16 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= -github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= -github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw= +github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= -github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= -github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= -github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -88,28 +84,26 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= -github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= -github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY= -github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc= +github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f h1:Uvbx7nITO3Sd1GdXarX0TbyYmOaSNIJP0mm4LocEyyA= +github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc= github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII= github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw= -github.com/go-git/go-git/v6 v6.0.0-20260222090600-424e9964d3a3 h1:lgm4zCVktmdFyACShxJvn/WJZgtUA7ysOKFeVD4UZpY= -github.com/go-git/go-git/v6 v6.0.0-20260222090600-424e9964d3a3/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU= +github.com/go-git/go-git/v6 v6.0.0-20260317113930-fb0d09929504 h1:ANi8zxy/EJBPilQZPMEG2bBzSNf9RkT0qrd5sB+ked0= +github.com/go-git/go-git/v6 v6.0.0-20260317113930-fb0d09929504/go.mod h1:DI8P0o+7Go2ainlUPsVcn9PMtEkg3umUXnxw6Kxppag= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= @@ -124,23 +118,22 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/haatos/goshipit v0.0.0-20260305043009-36e5c9a2e5c6 h1:Yakj3J1ZxYCpFtBHUVkSBNA80LK/8bNrl/LA8fSvvFw= github.com/haatos/goshipit v0.0.0-20260305043009-36e5c9a2e5c6/go.mod h1:2A31H3xgQHTgNp8OlX7aliXaY3QFwtI7XHuaP8G1ZSw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kevinburke/ssh_config v1.5.0 h1:3cPZmE54xb5j3G5xQCjSvokqNwU2uW+3ry1+PRLSPpA= -github.com/kevinburke/ssh_config v1.5.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= +github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -162,22 +155,16 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ= @@ -190,8 +177,6 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/ncruces/go-sqlite3 v0.30.5 h1:6usmTQ6khriL8oWilkAZSJM/AIpAlVL2zFrlcpDldCE= @@ -258,12 +243,14 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c= golang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -272,13 +259,13 @@ golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -289,22 +276,21 @@ golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/app/initializer.go b/internal/app/initializer.go index c523275..79eb8fb 100644 --- a/internal/app/initializer.go +++ b/internal/app/initializer.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/charmbracelet/huh" + "charm.land/huh/v2" ) type Initializer interface { diff --git a/internal/commands/issues/close.go b/internal/commands/issues/close.go index ce4bfcb..3dcd1f2 100644 --- a/internal/commands/issues/close.go +++ b/internal/commands/issues/close.go @@ -3,7 +3,7 @@ package issues import ( "fmt" - "github.com/charmbracelet/huh" + "charm.land/huh/v2" "github.com/spf13/cobra" ) diff --git a/internal/commands/issues/delete.go b/internal/commands/issues/delete.go index 019b3f9..c8ca2b2 100644 --- a/internal/commands/issues/delete.go +++ b/internal/commands/issues/delete.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/charmbracelet/huh" "github.com/spf13/cobra" ) diff --git a/internal/commands/issues/list.go b/internal/commands/issues/list.go index e9eae24..46c0167 100644 --- a/internal/commands/issues/list.go +++ b/internal/commands/issues/list.go @@ -44,12 +44,10 @@ func runGetIssuesCmd(cmd *cobra.Command, args []string) error { // Only set filter fields if the corresponding flags // were explicitly provided by the user. if cmd.Flags().Changed("status") { - s := models.Status(listFlags.status) - filter.Status = &s + filter.Status = new(models.Status(listFlags.status)) } if cmd.Flags().Changed("type") { - t := models.IssueType(listFlags.issueType) - filter.IssueType = &t + filter.IssueType = new(models.IssueType(listFlags.issueType)) } if cmd.Flags().Changed("priority") { filter.Priority = &listFlags.priority diff --git a/internal/commands/issues/root.go b/internal/commands/issues/root.go index bfae1d4..3c1e5d3 100644 --- a/internal/commands/issues/root.go +++ b/internal/commands/issues/root.go @@ -4,8 +4,8 @@ import ( "bytes" "context" + "charm.land/fang/v2" "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/charmbracelet/fang" "github.com/spf13/cobra" ) diff --git a/internal/models/task.go b/internal/models/task.go index efbb145..8eaa938 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -3,7 +3,7 @@ package models import ( "context" - "github.com/charmbracelet/huh" + "charm.land/huh/v2" ) type Tasker interface { diff --git a/internal/storage/mongo.go b/internal/storage/mongo.go index 474b819..b00e06a 100644 --- a/internal/storage/mongo.go +++ b/internal/storage/mongo.go @@ -7,11 +7,11 @@ import ( "os" "strings" + "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/charmbracelet/huh" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" ) type MongoStorage struct { diff --git a/internal/style/styles.go b/internal/style/styles.go index 85c8b50..5e56d2f 100644 --- a/internal/style/styles.go +++ b/internal/style/styles.go @@ -1,7 +1,7 @@ package style import ( - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" ) // Color palette diff --git a/internal/style/themes.go b/internal/style/themes.go index c6861bd..d677340 100644 --- a/internal/style/themes.go +++ b/internal/style/themes.go @@ -1,12 +1,12 @@ package style import ( - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" + "charm.land/huh/v2" + "charm.land/lipgloss/v2" ) -func HuhCenterTheme() *huh.Theme { - theme := huh.ThemeBase16() +func HuhCenterTheme() *huh.Styles { + theme := huh.ThemeBase16(true) theme.Focused.Base = lipgloss.NewStyle().Align(lipgloss.Center) diff --git a/pkg/task/questionnaire.go b/pkg/task/questionnaire.go index 369ef08..b94e318 100644 --- a/pkg/task/questionnaire.go +++ b/pkg/task/questionnaire.go @@ -1,11 +1,11 @@ package task import ( + "charm.land/bubbletea/v2" + "charm.land/huh/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/style" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/huh" ) type Questions = models.Questions diff --git a/pkg/task/runner.go b/pkg/task/runner.go index e65f59e..a87ca7b 100644 --- a/pkg/task/runner.go +++ b/pkg/task/runner.go @@ -5,8 +5,8 @@ import ( "fmt" "log/slog" + "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/models" - tea "github.com/charmbracelet/bubbletea" ) type App = models.App diff --git a/pkg/task/taskui.go b/pkg/task/taskui.go index 0d50afd..0a5fbff 100644 --- a/pkg/task/taskui.go +++ b/pkg/task/taskui.go @@ -4,11 +4,11 @@ import ( "fmt" "strings" + "charm.land/bubbles/v2/key" + "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/style" - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" ) type TaskDetails = models.TaskDetails diff --git a/pkg/tui/components/header.go b/pkg/tui/components/header.go index 87d434d..62bde83 100644 --- a/pkg/tui/components/header.go +++ b/pkg/tui/components/header.go @@ -1,8 +1,8 @@ package components import ( + "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" - "github.com/charmbracelet/lipgloss" ) type Header struct { diff --git a/pkg/tui/components/helpbar.go b/pkg/tui/components/helpbar.go index c413784..1facdc9 100644 --- a/pkg/tui/components/helpbar.go +++ b/pkg/tui/components/helpbar.go @@ -1,8 +1,8 @@ package components import ( + "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" - "github.com/charmbracelet/lipgloss" ) type ViewKind int diff --git a/pkg/tui/components/issue_detail.go b/pkg/tui/components/issue_detail.go index 9d15b65..5a3430d 100644 --- a/pkg/tui/components/issue_detail.go +++ b/pkg/tui/components/issue_detail.go @@ -3,10 +3,10 @@ package components import ( "time" + "charm.land/bubbles/v2/viewport" + "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" - "github.com/charmbracelet/bubbles/viewport" - "github.com/charmbracelet/lipgloss" ) type IssueDetail struct { @@ -23,7 +23,6 @@ func NewIssueDetail() IssueDetail { } } - func (i *IssueDetail) SetIssue(issue models.Issue) { i.issue = issue i.refreshContent() @@ -35,7 +34,6 @@ func (i *IssueDetail) SetComments(comments []*models.Comment) { i.refreshContent() } - func (i *IssueDetail) SetSize(width, height int) { i.viewport.Height = height i.viewport.Width = width diff --git a/pkg/tui/components/issue_list.go b/pkg/tui/components/issue_list.go index da81d08..47d63b7 100644 --- a/pkg/tui/components/issue_list.go +++ b/pkg/tui/components/issue_list.go @@ -6,18 +6,17 @@ import ( "io" "sort" + "charm.land/bubbles/v2/list" + "charm.land/bubbles/v2/textarea" + "charm.land/bubbles/v2/textinput" + "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/textarea" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/muesli/reflow/truncate" ) - type IssueList struct { list list.Model app *app.App @@ -26,7 +25,6 @@ type IssueList struct { highlightSelected bool } - type ListIssue struct { models.Issue } @@ -157,7 +155,6 @@ func ListenForValidation(ch chan models.ValidationFeedback) tea.Cmd { } } - func NewIssueList(app *app.App, width, height int) IssueList { // NewIssueList creates an IssueList populated from the app. issues, err := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) diff --git a/pkg/tui/components/keymap.go b/pkg/tui/components/keymap.go index 89ce532..e2c4092 100644 --- a/pkg/tui/components/keymap.go +++ b/pkg/tui/components/keymap.go @@ -1,6 +1,6 @@ package components -import "github.com/charmbracelet/bubbles/key" +import "charm.land/bubbles/v2/key" // CommonKeyMap holds the key bindings shared between the issues dashboard // and the kanban board. @@ -82,4 +82,3 @@ func DefaultCommonKeyMap() CommonKeyMap { ), } } - diff --git a/pkg/tui/components/modals.go b/pkg/tui/components/modals.go index 5eee8d7..156d561 100644 --- a/pkg/tui/components/modals.go +++ b/pkg/tui/components/modals.go @@ -1,9 +1,9 @@ package components import ( + "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" - "github.com/charmbracelet/lipgloss" ) // modalBoxWidth returns a clamped width for modal content. Never returns a value < 1. diff --git a/pkg/tui/issues/operations.go b/pkg/tui/issues/operations.go index 6798692..823ee5f 100644 --- a/pkg/tui/issues/operations.go +++ b/pkg/tui/issues/operations.go @@ -3,22 +3,47 @@ package issues import ( "context" + "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" - tea "github.com/charmbracelet/bubbletea" ) // Msg types used by both dashboard and kanban TUI views. type ( - TitleUpdatedMsg struct{ IssueID string; Err error } - DescriptionUpdatedMsg struct{ IssueID string; Err error } - StatusUpdatedMsg struct{ IssueID string; Err error } - PriorityUpdatedMsg struct{ IssueID string; Err error } - TypeUpdatedMsg struct{ IssueID string; Err error } - AssigneeUpdatedMsg struct{ IssueID string; Err error } - SelectIssueMsg struct{ IssueID string } - CreatedMsg struct{ Issue *models.Issue; Err error } - DeletedMsg struct{ IssueID string; Err error; PreviousIndex int } + TitleUpdatedMsg struct { + IssueID string + Err error + } + DescriptionUpdatedMsg struct { + IssueID string + Err error + } + StatusUpdatedMsg struct { + IssueID string + Err error + } + PriorityUpdatedMsg struct { + IssueID string + Err error + } + TypeUpdatedMsg struct { + IssueID string + Err error + } + AssigneeUpdatedMsg struct { + IssueID string + Err error + } + SelectIssueMsg struct{ IssueID string } + CreatedMsg struct { + Issue *models.Issue + Err error + } + DeletedMsg struct { + IssueID string + Err error + PreviousIndex int + } ) // UpdateIssueTitleCmd returns a command that updates an issue's title. diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go index 73909f3..5e6e70f 100644 --- a/pkg/tui/styles/styles.go +++ b/pkg/tui/styles/styles.go @@ -1,6 +1,6 @@ package styles -import "github.com/charmbracelet/lipgloss" +import "charm.land/lipgloss/v2" var ( Primary = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index f7df2e8..34d4377 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -3,10 +3,10 @@ package tui import ( "context" + "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/views" - tea "github.com/charmbracelet/bubbletea" ) type Config = models.Config diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 0793cb8..9c7c1b3 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -1,10 +1,10 @@ package dashboard import ( + "charm.land/bubbles/v2/key" + "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" ) type DashboardKeyMap struct { diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index bed24a1..bc75e80 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -3,12 +3,12 @@ package dashboard import ( "context" + "charm.land/bubbles/v2/textarea" + "charm.land/bubbles/v2/textinput" + "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" - "github.com/charmbracelet/bubbles/textarea" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" ) // Use shared types from components for consistency. @@ -46,21 +46,21 @@ type Model struct { deleteConfirmID string deleteConfirmIndex int - choosingStatus bool + choosingStatus bool statusIssueID string - choosingPriority bool + choosingPriority bool priorityIssueID string - choosingType bool + choosingType bool typeIssueID string - editingAssignee bool + editingAssignee bool assigneeInput textinput.Model assigneeIssueID string - addingComment bool + addingComment bool commentInput textarea.Model commentIssueID string - choosingCloseReason bool + choosingCloseReason bool closeReasonIssueID string - closingOtherReason bool + closingOtherReason bool closeReasonInput textarea.Model feedbackChan chan models.ValidationFeedback quitChan chan bool diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 306d009..64fc0e6 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -5,12 +5,12 @@ import ( "os" "os/user" + "charm.land/bubbles/v2/list" + "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/issues" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" ) func defaultCommentAuthor() string { diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index 7824256..b26b33a 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -1,9 +1,9 @@ package dashboard import ( + "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" - "github.com/charmbracelet/lipgloss" ) func (m *Model) View() string { diff --git a/pkg/tui/views/kanban/keys.go b/pkg/tui/views/kanban/keys.go index df8345b..03ca4e1 100644 --- a/pkg/tui/views/kanban/keys.go +++ b/pkg/tui/views/kanban/keys.go @@ -1,10 +1,10 @@ package kanban import ( + "charm.land/bubbles/v2/key" + "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" ) type KanbanKeyMap struct { diff --git a/pkg/tui/views/kanban/model.go b/pkg/tui/views/kanban/model.go index 5a38984..57e371e 100644 --- a/pkg/tui/views/kanban/model.go +++ b/pkg/tui/views/kanban/model.go @@ -1,14 +1,15 @@ package kanban + import ( "context" + "charm.land/bubbles/v2/textarea" + "charm.land/bubbles/v2/textinput" + "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/issues" - "github.com/charmbracelet/bubbles/textarea" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" ) type ( @@ -48,15 +49,15 @@ type Model struct { deleteConfirmID string deleteConfirmIndex int - choosingStatus bool // true while choosing a status - statusIssueID string - choosingPriority bool // true while choosing a priority - priorityIssueID string - choosingType bool // true while choosing a type - typeIssueID string - editingAssignee bool // true while editing assignee - assigneeInput textinput.Model - assigneeIssueID string + choosingStatus bool // true while choosing a status + statusIssueID string + choosingPriority bool // true while choosing a priority + priorityIssueID string + choosingType bool // true while choosing a type + typeIssueID string + editingAssignee bool // true while editing assignee + assigneeInput textinput.Model + assigneeIssueID string choosingCloseReason bool // true while choosing a close reason closeReasonIssueID string closingOtherReason bool // true while entering a custom close reason @@ -70,11 +71,11 @@ type Model struct { func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *Model { m := &Model{ - header: components.NewHeader("Kanban Board"), - keyMap: defaultKanbanKeyMap, - app: app, - width: 80, - height: 24, + header: components.NewHeader("Kanban Board"), + keyMap: defaultKanbanKeyMap, + app: app, + width: 80, + height: 24, focusedColumn: 0, focusOnDetail: false, feedbackChan: feedbackChan, @@ -261,5 +262,5 @@ func (m *Model) moveIssue(delta int) tea.Cmd { } newStatus := statusForColumn(newCol) - return issues.UpdateIssueStatusCmd(m.app, selected.ID, string(newStatus)) + return issues.UpdateIssueStatusCmd(m.app, selected.ID, string(newStatus)) } diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index db09d9f..30971f3 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -3,11 +3,11 @@ package kanban import ( "context" + "charm.land/bubbles/v2/list" + "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/issues" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" ) func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { diff --git a/pkg/tui/views/kanban/view.go b/pkg/tui/views/kanban/view.go index 8e6a279..3f4ec43 100644 --- a/pkg/tui/views/kanban/view.go +++ b/pkg/tui/views/kanban/view.go @@ -1,9 +1,9 @@ package kanban import ( + "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" - "github.com/charmbracelet/lipgloss" ) func (m *Model) View() string { diff --git a/pkg/tui/views/root.go b/pkg/tui/views/root.go index 4920610..75b91d6 100644 --- a/pkg/tui/views/root.go +++ b/pkg/tui/views/root.go @@ -1,12 +1,12 @@ package views import ( + "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" "github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard" "github.com/LazyBachelor/LazyPM/pkg/tui/views/kanban" - tea "github.com/charmbracelet/bubbletea" ) type RootModel struct { diff --git a/pkg/web/web.go b/pkg/web/web.go index c1be8d4..f49e819 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -12,13 +12,13 @@ import ( "syscall" "time" + "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/browser" "github.com/LazyBachelor/LazyPM/pkg/web/handler" "github.com/LazyBachelor/LazyPM/pkg/web/server" - tea "github.com/charmbracelet/bubbletea" ) type Config = models.Config From 3e9a903506756063e38f5307df47cb3f28bda7b0 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 18 Mar 2026 11:58:31 +0100 Subject: [PATCH 08/73] works now --- cmd/pm/intro.go | 21 ++++++++------ cmd/pm/intro_questionnare.go | 2 +- cmd/pm/runner.go | 3 +- internal/app/initializer.go | 3 +- internal/commands/issues/close.go | 5 ++-- internal/commands/issues/delete.go | 5 ++-- internal/storage/mongo.go | 11 ++++---- internal/style/themes.go | 40 +++++++++++++++++++++++++-- pkg/task/questionnaire.go | 16 ++++++++--- pkg/task/runner.go | 4 +-- pkg/task/taskui.go | 13 ++++++--- pkg/tui/components/header.go | 1 - pkg/tui/components/issue_detail.go | 19 +++++++------ pkg/tui/components/issue_list.go | 8 ------ pkg/tui/styles/styles.go | 27 ++++++++++-------- pkg/tui/tui.go | 3 +- pkg/tui/views/dashboard/keys.go | 2 +- pkg/tui/views/dashboard/operations.go | 4 +-- pkg/tui/views/dashboard/view.go | 35 +++++++++++------------ pkg/tui/views/kanban/keys.go | 2 +- pkg/tui/views/kanban/operations.go | 4 +-- pkg/tui/views/kanban/view.go | 13 +++++---- pkg/tui/views/root.go | 2 +- pkg/web/web.go | 10 ++++--- 24 files changed, 154 insertions(+), 99 deletions(-) diff --git a/cmd/pm/intro.go b/cmd/pm/intro.go index 9d23844..9353ebb 100644 --- a/cmd/pm/intro.go +++ b/cmd/pm/intro.go @@ -50,7 +50,7 @@ func newIntroModel() introModel { key.WithHelp("enter", "start survey"), ), Continue: key.NewBinding( - key.WithKeys(" ", "j", "l", "down", "right"), + key.WithKeys("space", "j", "l", "down", "right"), key.WithHelp("space", "continue"), ), Back: key.NewBinding( @@ -69,11 +69,11 @@ func newIntroModel() introModel { } func (m introModel) Run() error { - model, err := tea.NewProgram(m, tea.WithAltScreen()).Run() + model, err := tea.NewProgram(m).Run() if err != nil { return err } - if m, ok := model.(introModel); ok && m.userQuit { + if im, ok := model.(introModel); ok && im.userQuit { return models.ErrUserQuit } return nil @@ -87,7 +87,7 @@ func (m introModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.SetSize(msg.Width, msg.Height) - case tea.KeyMsg: + case tea.KeyPressMsg: switch { case key.Matches(msg, m.keys.Start) && m.stage == stages: return m, tea.Quit @@ -109,10 +109,13 @@ func (m introModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m introModel) View() string { +func (m introModel) View() tea.View { if m.width < 55 || m.height < 16 { - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, + content := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, style.TextStyle.Render("Terminal too small.")) + v := tea.NewView(content) + v.AltScreen = true + return v } var content string @@ -122,7 +125,7 @@ func (m introModel) View() string { case 2: content = Disclaimer default: - return "" + return tea.NewView("") } boxWidth := min(m.width-10, 120) @@ -149,7 +152,9 @@ func (m introModel) View() string { final := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, b.String()) - return final + v := tea.NewView(final) + v.AltScreen = true + return v } func (m *introModel) SetSize(width, height int) { diff --git a/cmd/pm/intro_questionnare.go b/cmd/pm/intro_questionnare.go index d9486a9..c5bbdd2 100644 --- a/cmd/pm/intro_questionnare.go +++ b/cmd/pm/intro_questionnare.go @@ -14,7 +14,7 @@ func newIntroQuestionnaire() *IntroQuestionnaire { func (iq *IntroQuestionnaire) Run() (map[string]any, error) { model := task.NewQuestionnaireModel(iq.Questions(), iq.Keys()) - app := tea.NewProgram(model, tea.WithAltScreen()) + app := tea.NewProgram(model) m, err := app.Run() if err != nil { diff --git a/cmd/pm/runner.go b/cmd/pm/runner.go index d524ed8..0872e30 100644 --- a/cmd/pm/runner.go +++ b/cmd/pm/runner.go @@ -11,6 +11,7 @@ import ( "github.com/LazyBachelor/LazyPM/cmd/pm/tasks" "github.com/LazyBachelor/LazyPM/internal/commands/survey" "github.com/LazyBachelor/LazyPM/internal/storage" + "github.com/LazyBachelor/LazyPM/internal/style" "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/spf13/cobra" ) @@ -54,7 +55,7 @@ func runStartCmd(cmd *cobra.Command, args []string) error { Title("Do you want to continue without submitting your responses?"). Description("You can fix your database connection and submit your responses later with the submit command."). Value(&continueWithoutSubmitting). - WithTheme(huh.ThemeBase16(true)). + WithTheme(style.Base16Theme{}). RunAccessible(cmd.OutOrStdout(), cmd.InOrStdin()); err != nil { return fmt.Errorf("failed to read user input: %w", err) } diff --git a/internal/app/initializer.go b/internal/app/initializer.go index 79eb8fb..a6966e2 100644 --- a/internal/app/initializer.go +++ b/internal/app/initializer.go @@ -5,6 +5,7 @@ import ( "os" "charm.land/huh/v2" + "github.com/LazyBachelor/LazyPM/internal/style" ) type Initializer interface { @@ -30,7 +31,7 @@ func (i InteractiveInitializer) Init(path string) error { Title("PM is not initialized in this directory!"). Description("Do you want to initialize it here?"). Value(&initialize), - )).WithTheme(huh.ThemeBase16()).WithAccessible(true).Run() + )).WithTheme(style.Base16Theme{}).WithAccessible(true).Run() if err != nil { return err diff --git a/internal/commands/issues/close.go b/internal/commands/issues/close.go index 3dcd1f2..59d3b95 100644 --- a/internal/commands/issues/close.go +++ b/internal/commands/issues/close.go @@ -4,6 +4,7 @@ import ( "fmt" "charm.land/huh/v2" + "github.com/LazyBachelor/LazyPM/internal/style" "github.com/spf13/cobra" ) @@ -47,13 +48,13 @@ func runCloseCmd(cmd *cobra.Command, args []string) error { huh.NewOption("Won't fix", "Won't fix"), huh.NewOption("Obsolete", "Obsolete"), huh.NewOption("Other", "Other"), - ).WithTheme(huh.ThemeBase()).Run(); err != nil { + ).WithTheme(style.BaseTheme{}).Run(); err != nil { return fmt.Errorf("error getting close reason: %w", err) } if closeReason == "Other" { if err = huh.NewInput().Value(&closeReason). - Title("Enter closing reason:").WithTheme(huh.ThemeBase()).Run(); err != nil { + Title("Enter closing reason:").WithTheme(style.BaseTheme{}).Run(); err != nil { return fmt.Errorf("error getting close reason: %w", err) } if closeReason == "" { diff --git a/internal/commands/issues/delete.go b/internal/commands/issues/delete.go index c8ca2b2..e3e9aa7 100644 --- a/internal/commands/issues/delete.go +++ b/internal/commands/issues/delete.go @@ -7,6 +7,7 @@ import ( "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/style" "github.com/spf13/cobra" ) @@ -62,7 +63,7 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { if !cmd.Flags().Changed("yes") { huh.NewConfirm().Value(&confirmDelete). Title("You want to delete this issue?"). - Inline(true).WithTheme(huh.ThemeBase()).Run() + Inline(true).WithTheme(style.BaseTheme{}).Run() } // If user did not confirm, cancel deletion. @@ -102,7 +103,7 @@ func runDeleteInteractive(ctx context.Context) error { huh.NewGroup( huh.NewMultiSelect[string](). Options(options...).Value(&deleteIDs). - Title("Select issues to delete"))).WithTheme(huh.ThemeBase()) + Title("Select issues to delete"))).WithTheme(style.BaseTheme{}) if err := form.Run(); err != nil { return fmt.Errorf("error running interactive form: %w", err) diff --git a/internal/storage/mongo.go b/internal/storage/mongo.go index b00e06a..e408386 100644 --- a/internal/storage/mongo.go +++ b/internal/storage/mongo.go @@ -9,6 +9,7 @@ import ( "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/style" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" @@ -24,7 +25,7 @@ func NewMongoStorage(ctx context.Context, uri, username, password string) (*Mong Password: password, } - client, err := mongo.Connect(ctx, + client, err := mongo.Connect( options.Client().ApplyURI(uri).SetAuth(credentials)) if err != nil { @@ -44,7 +45,7 @@ func NewMongoStorageInteractive(ctx context.Context, uri string) (*MongoStorage, if err := huh.NewInput(). Title("Enter the Database Username"). Value(&username). - WithTheme(huh.ThemeBase16()).Run(); err != nil { + WithTheme(style.Base16Theme{}).Run(); err != nil { return nil, fmt.Errorf("failed to read username: %w", err) } } else { @@ -60,7 +61,7 @@ func NewMongoStorageInteractive(ctx context.Context, uri string) (*MongoStorage, Title("Enter the Survey Password"). EchoMode(huh.EchoModePassword). Value(&password). - WithTheme(huh.ThemeBase16()).Run(); err != nil { + WithTheme(style.Base16Theme{}).Run(); err != nil { return nil, fmt.Errorf("failed to read password: %w", err) } } else { @@ -110,7 +111,7 @@ func (s *MongoStorage) SubmitSurveyResponsesCmd(ctx context.Context, dir string) _, err = userStatscollection.UpdateOne(ctx, bson.M{"_id": stats.ID}, bson.M{"$set": stats}, - options.Update().SetUpsert(true)) + options.UpdateOne().SetUpsert(true)) if err != nil { return fmt.Errorf("Failed to insert stats into database: %v", err) @@ -140,7 +141,7 @@ func (s *MongoStorage) SubmitSurveyResponsesCmd(ctx context.Context, dir string) } _, err = taskMetricsCollection.UpdateOne(ctx, bson.M{"_id": metrics.ID}, bson.M{"$set": metrics}, - options.Update().SetUpsert(true)) + options.UpdateOne().SetUpsert(true)) if err != nil { fmt.Printf("failed to insert metrics from %s: %v", file, err) diff --git a/internal/style/themes.go b/internal/style/themes.go index d677340..c0e4d5f 100644 --- a/internal/style/themes.go +++ b/internal/style/themes.go @@ -5,10 +5,44 @@ import ( "charm.land/lipgloss/v2" ) -func HuhCenterTheme() *huh.Styles { - theme := huh.ThemeBase16(true) +// BaseTheme wraps huh.ThemeBase for use as a huh.Theme +type BaseTheme struct{} - theme.Focused.Base = lipgloss.NewStyle().Align(lipgloss.Center) +func (t BaseTheme) Theme(isDark bool) *huh.Styles { + return huh.ThemeBase(isDark) +} + +// Base16Theme wraps huh.ThemeBase16 for use as a huh.Theme +type Base16Theme struct{} + +func (t Base16Theme) Theme(isDark bool) *huh.Styles { + return huh.ThemeBase16(isDark) +} + +// CenterTheme is a theme that centers form content while preserving ThemeBase16 styling +type CenterTheme struct{} + +func (t CenterTheme) Theme(isDark bool) *huh.Styles { + theme := huh.ThemeBase16(isDark) + // Center the field content + theme.Focused.Base = theme.Focused.Base.Align(lipgloss.Center) + theme.Focused.Card = lipgloss.NewStyle() // Remove card border + theme.Group.Base = lipgloss.NewStyle() // Remove group padding return theme } + +// HuhCenterTheme returns a center-aligned theme for questionnaires +func HuhCenterTheme() huh.Theme { + return CenterTheme{} +} + +// HuhBaseTheme returns a base theme +func HuhBaseTheme() huh.Theme { + return BaseTheme{} +} + +// HuhBase16Theme returns a base16 theme +func HuhBase16Theme() huh.Theme { + return Base16Theme{} +} diff --git a/pkg/task/questionnaire.go b/pkg/task/questionnaire.go index b94e318..ec0e21b 100644 --- a/pkg/task/questionnaire.go +++ b/pkg/task/questionnaire.go @@ -20,7 +20,9 @@ type QuestionnaireModel struct { func NewQuestionnaireModel(questions Questions, keys []string) *QuestionnaireModel { form := huh.NewForm(questions...). - WithTheme(style.HuhCenterTheme()).WithLayout(huh.LayoutGrid(1, 1)) + WithTheme(style.HuhCenterTheme()). + WithLayout(huh.LayoutGrid(1, 1)). + WithWidth(80) return &QuestionnaireModel{ Questions: questions, @@ -37,7 +39,9 @@ func (q *QuestionnaireModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: q.SetSize(msg.Width, msg.Height) - case tea.KeyMsg: + // Update form width to match window + q.form.WithWidth(msg.Width) + case tea.KeyPressMsg: switch msg.String() { case "q", "ctrl+c": q.userQuit = true @@ -57,14 +61,18 @@ func (q *QuestionnaireModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return q, cmd } -func (q *QuestionnaireModel) View() string { +func (q *QuestionnaireModel) View() tea.View { form := lipgloss.NewStyle(). Width(q.width).Align(lipgloss.Center). Render(q.form.View()) - return lipgloss.Place( + content := lipgloss.Place( q.width, q.height, lipgloss.Center, lipgloss.Center, form, ) + + v := tea.NewView(content) + v.AltScreen = true + return v } func (q *QuestionnaireModel) SetSize(width, height int) { diff --git a/pkg/task/runner.go b/pkg/task/runner.go index a87ca7b..5aeae15 100644 --- a/pkg/task/runner.go +++ b/pkg/task/runner.go @@ -157,7 +157,7 @@ func (r *RunLifecycle) Finish(ctx context.Context, runErr error) error { } func runIntro(details models.TaskDetails) error { - model, err := tea.NewProgram(NewTaskModel(details), tea.WithAltScreen()).Run() + model, err := tea.NewProgram(NewTaskModel(details)).Run() if err != nil { return err } @@ -179,7 +179,7 @@ func runQuestionnaire(t Tasker, iType InterfaceType, collector *taskRunCollector keys = provider.QuestionnaireKeys(iType) } - model, err := tea.NewProgram(NewQuestionnaireModel(questions, keys), tea.WithAltScreen()).Run() + model, err := tea.NewProgram(NewQuestionnaireModel(questions, keys)).Run() if err != nil { return err } diff --git a/pkg/task/taskui.go b/pkg/task/taskui.go index 0a5fbff..d5fb921 100644 --- a/pkg/task/taskui.go +++ b/pkg/task/taskui.go @@ -57,7 +57,7 @@ func (m TaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.SetSize(msg.Width, msg.Height) - case tea.KeyMsg: + case tea.KeyPressMsg: switch { case key.Matches(msg, m.keys.Quit): m.userQuit = true @@ -73,10 +73,13 @@ func (m TaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m TaskModel) View() string { +func (m TaskModel) View() tea.View { if m.width < 55 || m.height < 16 { - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, + content := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, style.TextStyle.Render("Terminal too small.")) + v := tea.NewView(content) + v.AltScreen = true + return v } boxWidth := min(m.width-10, 120) @@ -111,7 +114,9 @@ func (m TaskModel) View() string { final := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, b.String()) - return final + v := tea.NewView(final) + v.AltScreen = true + return v } func (m *TaskModel) SetSize(width, height int) { diff --git a/pkg/tui/components/header.go b/pkg/tui/components/header.go index 62bde83..c5d625b 100644 --- a/pkg/tui/components/header.go +++ b/pkg/tui/components/header.go @@ -21,7 +21,6 @@ func (h Header) View(width int) string { lipgloss.Left, title, lipgloss.WithWhitespaceChars("─"), - lipgloss.WithWhitespaceForeground(styles.Primary), ) } diff --git a/pkg/tui/components/issue_detail.go b/pkg/tui/components/issue_detail.go index 5a3430d..11def7a 100644 --- a/pkg/tui/components/issue_detail.go +++ b/pkg/tui/components/issue_detail.go @@ -17,7 +17,7 @@ type IssueDetail struct { } func NewIssueDetail() IssueDetail { - vp := viewport.New(0, 0) + vp := viewport.New(viewport.WithWidth(0), viewport.WithHeight(0)) return IssueDetail{ viewport: vp, } @@ -35,8 +35,7 @@ func (i *IssueDetail) SetComments(comments []*models.Comment) { } func (i *IssueDetail) SetSize(width, height int) { - i.viewport.Height = height - i.viewport.Width = width + i.viewport = viewport.New(viewport.WithWidth(width), viewport.WithHeight(height)) i.refreshContent() } @@ -102,19 +101,21 @@ func formatCommentTime(t time.Time) string { func (i IssueDetail) View() string { content := i.viewport.View() + vpWidth := i.viewport.Width() + vpHeight := i.viewport.Height() if i.focused { return styles.DetailsContainerStyle. BorderForeground(styles.PrimaryBorder). - Width(i.viewport.Width). - Height(i.viewport.Height). - MaxHeight(i.viewport.Height). + Width(vpWidth). + Height(vpHeight). + MaxHeight(vpHeight). Render(content) } return styles.DetailsContainerStyle. - Width(i.viewport.Width). - Height(i.viewport.Height). - MaxHeight(i.viewport.Height). + Width(vpWidth). + Height(vpHeight). + MaxHeight(vpHeight). Render(content) } diff --git a/pkg/tui/components/issue_list.go b/pkg/tui/components/issue_list.go index 47d63b7..087a5c3 100644 --- a/pkg/tui/components/issue_list.go +++ b/pkg/tui/components/issue_list.go @@ -177,10 +177,6 @@ func NewIssueList(app *app.App, width, height int) IssueList { l.SetShowHelp(false) l.SetShowStatusBar(false) l.SetFilteringEnabled(true) - l.FilterInput.PromptStyle = styles.FilterPromptStyle - l.FilterInput.Cursor.Style = styles.FilterStyle - l.FilterInput.TextStyle = styles.FilterInputStyle - l.FilterInput.Prompt = "🔍 " return IssueList{ list: l, @@ -208,10 +204,6 @@ func NewIssueListFromIssues(app *app.App, issues []*models.Issue, width, height l.SetShowHelp(false) l.SetShowStatusBar(false) l.SetFilteringEnabled(true) - l.FilterInput.PromptStyle = styles.FilterPromptStyle - l.FilterInput.Cursor.Style = styles.FilterStyle - l.FilterInput.TextStyle = styles.FilterInputStyle - l.FilterInput.Prompt = "🔍 " return IssueList{ list: l, app: app, diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go index 5e6e70f..4f2af0f 100644 --- a/pkg/tui/styles/styles.go +++ b/pkg/tui/styles/styles.go @@ -1,23 +1,26 @@ package styles -import "charm.land/lipgloss/v2" +import ( + "charm.land/lipgloss/v2" + "charm.land/lipgloss/v2/compat" +) var ( - Primary = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} - Secondary = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} + Primary = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")} + Secondary = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")} - Success = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} - Warning = lipgloss.AdaptiveColor{Light: "#F59E0B", Dark: "#F59E0B"} - Error = lipgloss.AdaptiveColor{Light: "#FE5F86", Dark: "#FE5F86"} + Success = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")} + Warning = compat.AdaptiveColor{Light: lipgloss.Color("#F59E0B"), Dark: lipgloss.Color("#F59E0B")} + Error = compat.AdaptiveColor{Light: lipgloss.Color("#FE5F86"), Dark: lipgloss.Color("#FE5F86")} - PrimaryText = lipgloss.AdaptiveColor{Light: "#1A1A1A", Dark: "#E0E0E0"} - SecondaryText = lipgloss.AdaptiveColor{Light: "#666666", Dark: "#999999"} - FaintText = lipgloss.AdaptiveColor{Light: "#999999", Dark: "#666666"} + PrimaryText = compat.AdaptiveColor{Light: lipgloss.Color("#1A1A1A"), Dark: lipgloss.Color("#E0E0E0")} + SecondaryText = compat.AdaptiveColor{Light: lipgloss.Color("#666666"), Dark: lipgloss.Color("#999999")} + FaintText = compat.AdaptiveColor{Light: lipgloss.Color("#999999"), Dark: lipgloss.Color("#666666")} - PrimaryBorder = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} - SecondaryBorder = lipgloss.AdaptiveColor{Light: "#CCCCCC", Dark: "#444444"} + PrimaryBorder = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")} + SecondaryBorder = compat.AdaptiveColor{Light: lipgloss.Color("#CCCCCC"), Dark: lipgloss.Color("#444444")} - SelectedBackground = lipgloss.AdaptiveColor{Light: "#E8E8E8", Dark: "#333333"} + SelectedBackground = compat.AdaptiveColor{Light: lipgloss.Color("#E8E8E8"), Dark: lipgloss.Color("#333333")} ) const ( diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index 34d4377..faafc0a 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -30,8 +30,7 @@ func (t *Tui) Run(ctx context.Context, config Config) error { defer cleanup() - p := tea.NewProgram(views.NewRootView(app, t.feedbackChan, t.quitChan, t.submitChan), - tea.WithAltScreen(), tea.WithMouseAllMotion()) + p := tea.NewProgram(views.NewRootView(app, t.feedbackChan, t.quitChan, t.submitChan)) if t.quitChan != nil { go func() { diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 9c7c1b3..0d159e3 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -79,7 +79,7 @@ var defaultDashboardKeyMap = DashboardKeyMap{ ), } -func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { +func (d *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { var cmd tea.Cmd switch { diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 64fc0e6..2db5467 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -322,7 +322,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return issues.SelectIssueMsg{IssueID: selectedIssue.ID} }) - case tea.KeyMsg: + case tea.KeyPressMsg: if m.confirmingDelete { switch msg.String() { case "y", "Y": @@ -619,7 +619,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - cmd := m.handleKeyMsg(msg) + cmd := m.handleKeyPressMsg(msg) if cmd != nil { return m, cmd } diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index b26b33a..569cff7 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -1,15 +1,16 @@ package dashboard import ( + "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" ) -func (m *Model) View() string { +func (m *Model) View() tea.View { if m.width == 0 || m.height == 0 { // if there is no space just print a loading message - return "Loading..." + return tea.NewView("Loading...") } m.helpBar.SetWidth(m.width) @@ -63,7 +64,7 @@ func (m *Model) View() string { if m.editingAssignee { editBoxWidth := min(60, m.width-4) - m.assigneeInput.Width = editBoxWidth - 2 + m.assigneeInput.SetWidth(editBoxWidth - 2) editContent := lipgloss.JoinVertical(lipgloss.Left, styles.LabelStyle.Render("Edit assignee (Enter to save, Esc to cancel):"), m.assigneeInput.View(), @@ -72,12 +73,12 @@ func (m *Model) View() string { Width(editBoxWidth). BorderForeground(styles.PrimaryBorder). Render(editContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) } if m.editingTitle { editBoxWidth := min(60, m.width-4) - m.titleInput.Width = editBoxWidth - 2 + m.titleInput.SetWidth(editBoxWidth - 2) editContent := lipgloss.JoinVertical(lipgloss.Left, styles.LabelStyle.Render("Edit title (Enter to save, Esc to cancel):"), m.titleInput.View(), @@ -86,7 +87,7 @@ func (m *Model) View() string { Width(editBoxWidth). BorderForeground(styles.PrimaryBorder). Render(editContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) } if m.addingComment { @@ -101,7 +102,7 @@ func (m *Model) View() string { Width(editBoxWidth). BorderForeground(styles.PrimaryBorder). Render(editContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) } if m.editingDescription { @@ -116,12 +117,12 @@ func (m *Model) View() string { Width(editBoxWidth). BorderForeground(styles.PrimaryBorder). Render(editContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) } if m.creatingIssue { createBoxWidth := min(60, m.width-4) - m.createTitleInput.Width = createBoxWidth - 2 + m.createTitleInput.SetWidth(createBoxWidth - 2) createContent := lipgloss.JoinVertical(lipgloss.Left, styles.LabelStyle.Render("New issue (Enter to create, Esc to cancel):"), m.createTitleInput.View(), @@ -130,7 +131,7 @@ func (m *Model) View() string { Width(createBoxWidth). BorderForeground(styles.PrimaryBorder). Render(createContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, createBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, createBox)) } if m.confirmingDelete { @@ -143,7 +144,7 @@ func (m *Model) View() string { Width(confirmBoxWidth). BorderForeground(styles.PrimaryBorder). Render(confirmContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, confirmBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, confirmBox)) } if m.choosingStatus { @@ -157,7 +158,7 @@ func (m *Model) View() string { Width(statusBoxWidth). BorderForeground(styles.PrimaryBorder). Render(statusContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, statusBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, statusBox)) } if m.choosingPriority { @@ -171,7 +172,7 @@ func (m *Model) View() string { Width(priorityBoxWidth). BorderForeground(styles.PrimaryBorder). Render(priorityContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, priorityBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, priorityBox)) } if m.choosingCloseReason { @@ -185,7 +186,7 @@ func (m *Model) View() string { Width(reasonBoxWidth). BorderForeground(styles.PrimaryBorder). Render(reasonContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, reasonBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, reasonBox)) } if m.closingOtherReason { @@ -200,7 +201,7 @@ func (m *Model) View() string { Width(editBoxWidth). BorderForeground(styles.PrimaryBorder). Render(editContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) } if m.choosingType { @@ -214,9 +215,9 @@ func (m *Model) View() string { Width(typeBoxWidth). BorderForeground(styles.PrimaryBorder). Render(typeContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, typeBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, typeBox)) } - return mainView + return tea.NewView(mainView) } diff --git a/pkg/tui/views/kanban/keys.go b/pkg/tui/views/kanban/keys.go index 03ca4e1..bd78dbe 100644 --- a/pkg/tui/views/kanban/keys.go +++ b/pkg/tui/views/kanban/keys.go @@ -45,7 +45,7 @@ var defaultKanbanKeyMap = KanbanKeyMap{ ), } -func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { +func (d *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { var cmd tea.Cmd switch { diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index 30971f3..144e144 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -258,7 +258,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return issues.SelectIssueMsg{IssueID: selectedIssue.ID} }) - case tea.KeyMsg: + case tea.KeyPressMsg: if m.confirmingDelete { switch msg.String() { case "y", "Y": @@ -499,7 +499,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } - cmd := m.handleKeyMsg(msg) + cmd := m.handleKeyPressMsg(msg) if cmd != nil { return m, cmd } diff --git a/pkg/tui/views/kanban/view.go b/pkg/tui/views/kanban/view.go index 3f4ec43..4b0d38e 100644 --- a/pkg/tui/views/kanban/view.go +++ b/pkg/tui/views/kanban/view.go @@ -1,15 +1,16 @@ package kanban import ( + "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" ) -func (m *Model) View() string { +func (m *Model) View() tea.View { if m.width == 0 || m.height == 0 { // if there is no space just print a loading message - return "Loading..." + return tea.NewView("Loading...") } m.helpBar.SetWidth(m.width) @@ -93,7 +94,7 @@ func (m *Model) View() string { Width(reasonBoxWidth). BorderForeground(styles.PrimaryBorder). Render(reasonContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, reasonBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, reasonBox)) } if m.closingOtherReason { @@ -108,10 +109,10 @@ func (m *Model) View() string { Width(editBoxWidth). BorderForeground(styles.PrimaryBorder). Render(editContent) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) } - return components.RenderModals( + return tea.NewView(components.RenderModals( m.width, m.height, m.editingTitle, @@ -131,7 +132,7 @@ func (m *Model) View() string { m.editingAssignee, m.assigneeInput.View(), mainView, - ) + )) } diff --git a/pkg/tui/views/root.go b/pkg/tui/views/root.go index 75b91d6..ea51efa 100644 --- a/pkg/tui/views/root.go +++ b/pkg/tui/views/root.go @@ -81,6 +81,6 @@ func (r *RootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return r, cmd } -func (r *RootModel) View() string { +func (r *RootModel) View() tea.View { return r.currentView.View() } diff --git a/pkg/web/web.go b/pkg/web/web.go index f49e819..42fcd3a 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -54,7 +54,7 @@ func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.width = msg.Width m.height = msg.Height return m, nil - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "q", "esc", "ctrl+c": select { @@ -69,11 +69,14 @@ func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m tuiModel) View() string { - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, fmt.Sprintf( +func (m tuiModel) View() tea.View { + content := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, fmt.Sprintf( "Web server running at %s\n\nPress q, esc, or Ctrl+C to stop the task and server.\n", m.address, )) + v := tea.NewView(content) + v.AltScreen = true + return v } func (w *Web) Run(ctx context.Context, config Config) error { @@ -106,7 +109,6 @@ func (w *Web) Run(ctx context.Context, config Config) error { address: address, quitChan: uiQuitChan, }, - tea.WithAltScreen(), ) screenDone = make(chan struct{}) From 2ebb8be83bef87923bfa128996e805815c791ddb Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 18 Mar 2026 14:31:36 +0100 Subject: [PATCH 09/73] make sure to use alt screen --- pkg/tui/views/root.go | 4 +++- pkg/tui/views/views.go | 12 ------------ 2 files changed, 3 insertions(+), 13 deletions(-) delete mode 100644 pkg/tui/views/views.go diff --git a/pkg/tui/views/root.go b/pkg/tui/views/root.go index ea51efa..fc8118d 100644 --- a/pkg/tui/views/root.go +++ b/pkg/tui/views/root.go @@ -82,5 +82,7 @@ func (r *RootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (r *RootModel) View() tea.View { - return r.currentView.View() + v := r.currentView.View() + v.AltScreen = true + return v } diff --git a/pkg/tui/views/views.go b/pkg/tui/views/views.go deleted file mode 100644 index e26dbd7..0000000 --- a/pkg/tui/views/views.go +++ /dev/null @@ -1,12 +0,0 @@ -package views - -import ( - "github.com/LazyBachelor/LazyPM/internal/app" - "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard" -) - -func NewDashboardView(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *dashboard.Model { - return dashboard.NewDashboard(app, feedbackChan, quitChan, submitChan) -} - From 49608e5f7886bf8c9884119d828ad8ca587ecc89 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 18 Mar 2026 14:38:09 +0100 Subject: [PATCH 10/73] fix so it runs --- pkg/web/components/assignee_form.templ | 2 -- pkg/web/components/assignee_form_templ.go | 40 ++++++++++++++++------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/pkg/web/components/assignee_form.templ b/pkg/web/components/assignee_form.templ index 89f9cc2..7a7cd36 100644 --- a/pkg/web/components/assignee_form.templ +++ b/pkg/web/components/assignee_form.templ @@ -1,7 +1,5 @@ package components -import "github.com/LazyBachelor/LazyPM/pkg/web/components/base" - type AssigneeFormProps struct { PatchAction string Assignee string diff --git a/pkg/web/components/assignee_form_templ.go b/pkg/web/components/assignee_form_templ.go index ae68a4f..8110518 100644 --- a/pkg/web/components/assignee_form_templ.go +++ b/pkg/web/components/assignee_form_templ.go @@ -8,8 +8,6 @@ package components import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -import "github.com/LazyBachelor/LazyPM/pkg/web/components/base" - type AssigneeFormProps struct { PatchAction string Assignee string @@ -43,26 +41,46 @@ func AssigneeForm(props AssigneeFormProps) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(props.PatchAction) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/assignee_form.templ`, Line: 12, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/assignee_form.templ`, Line: 10, Col: 30} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" hx-swap=\"innerHTML\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" hx-swap=\"innerHTML\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if props.Assignee != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

Current assignee: ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(props.Assignee) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/assignee_form.templ`, Line: 28, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } From 08d9f741ea5dfc76f4a2f2b03841c92f82d6f414 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 18 Mar 2026 14:51:34 +0100 Subject: [PATCH 11/73] update workflows to go 1.26.1 --- .github/workflows/go.yml | 2 +- .github/workflows/goreleaser.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a5f81ca..09a859d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.25.6' + go-version: '1.26.1' - name: Build run: go build -v ./... diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index b05894e..3357045 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -21,7 +21,7 @@ jobs: name: Set up Go uses: actions/setup-go@v6 with: - go-version: '1.25.6' + go-version: '1.26.1' - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7 From ed018b53965292a97cd7a833f372e8bd1636de96 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 18 Mar 2026 14:55:42 +0100 Subject: [PATCH 12/73] update dockerfile to latest go version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7a732b4..8fb89a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:1.25.6-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.26.1-alpine AS builder WORKDIR /app RUN apk add --no-cache git ca-certificates tzdata From 720be210c35252b4478305b0bc1c88d17d8ca25a Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 18 Mar 2026 15:00:15 +0100 Subject: [PATCH 13/73] add commands to start tui and web --- cmd/pm/init.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cmd/pm/init.go b/cmd/pm/init.go index fe69b17..5a6fcfb 100644 --- a/cmd/pm/init.go +++ b/cmd/pm/init.go @@ -104,6 +104,8 @@ func init() { RootCmd.AddGroup(&cobra.Group{ID: "other", Title: "Additional Commands"}) RootCmd.SetHelpCommandGroupID("other") RootCmd.AddCommand(replCmd) + RootCmd.AddCommand(tuiCmd) + RootCmd.AddCommand(webCmd) } @@ -117,6 +119,26 @@ var replCmd = &cobra.Command{ }, } +var tuiCmd = &cobra.Command{ + Use: "tui", + GroupID: "other", + Short: "Start the interactive TUI interface", + Long: `Start the interactive Terminal User Interface (TUI) for managing your projects and issues in an interactive terminal environment.`, + RunE: func(cmd *cobra.Command, args []string) error { + return tui.New().Run(cmd.Context(), App.Config) + }, +} + +var webCmd = &cobra.Command{ + Use: "web", + GroupID: "other", + Short: "Start the interactive web interface", + Long: `Start the interactive web interface for managing your projects and issues in a web browser.`, + RunE: func(cmd *cobra.Command, args []string) error { + return web.New().Run(cmd.Context(), App.Config) + }, +} + func initializeApp(ctx context.Context) (*app.App, func(), error) { return app.New(ctx, tasks.BaseConfig().WithAutoInit(true)) } From 2510429a7f150d73ae8d5024a8a31ee637f7c8ef Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 18 Mar 2026 15:20:16 +0100 Subject: [PATCH 14/73] upgrade mongodb driver dep --- go.mod | 1 - go.sum | 2 -- internal/app/app.go | 4 ++-- internal/app/statistics.go | 6 +++--- internal/models/app.go | 4 ++-- internal/models/statistics.go | 22 +++++++++++----------- pkg/task/lifecycle.go | 4 ++-- pkg/task/metrics_store.go | 8 ++++---- pkg/web/handler/issues.go | 8 ++++---- 9 files changed, 28 insertions(+), 31 deletions(-) diff --git a/go.mod b/go.mod index ee97108..823223e 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/joho/godotenv v1.5.1 github.com/muesli/reflow v0.3.0 github.com/steveyegge/beads v0.49.6 - go.mongodb.org/mongo-driver v1.17.9 go.mongodb.org/mongo-driver/v2 v2.5.0 ) diff --git a/go.sum b/go.sum index 18ba4da..1dc87ce 100644 --- a/go.sum +++ b/go.sum @@ -241,8 +241,6 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= -go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/internal/app/app.go b/internal/app/app.go index 1ee6aa2..ace5983 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,7 +7,7 @@ import ( "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/storage" "github.com/steveyegge/beads" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) type App = models.App @@ -43,7 +43,7 @@ func New(ctx context.Context, config Config, opts ...Option) (*App, func(), erro if b.statsService == nil { statStore := storage.NewJsonStorage(config.StatisticsStoragePath, &models.Statistics{ - ID: primitive.NewObjectID(), + ID: bson.NewObjectID(), StartTime: time.Now(), }) diff --git a/internal/app/statistics.go b/internal/app/statistics.go index abd12a6..bd594c5 100644 --- a/internal/app/statistics.go +++ b/internal/app/statistics.go @@ -9,7 +9,7 @@ import ( "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/storage" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) type StatisticsService struct { @@ -43,12 +43,12 @@ func (s *StatisticsService) GetStatistics() (models.Statistics, error) { return *s.storage.Data, nil } -func (s *StatisticsService) GetParticipantID() primitive.ObjectID { +func (s *StatisticsService) GetParticipantID() bson.ObjectID { s.mu.Lock() defer s.mu.Unlock() if s.storage.Data == nil { - return primitive.NilObjectID + return bson.NilObjectID } return s.storage.Data.ID } diff --git a/internal/models/app.go b/internal/models/app.go index e1e5603..3736d60 100644 --- a/internal/models/app.go +++ b/internal/models/app.go @@ -4,7 +4,7 @@ import ( "context" "log/slog" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) type App struct { @@ -59,7 +59,7 @@ type StatsService interface { Load(ctx context.Context) error Save(ctx context.Context) error GetStatistics() (Statistics, error) - GetParticipantID() primitive.ObjectID + GetParticipantID() bson.ObjectID RecordTaskRun(ctx context.Context, run TaskRunMetrics) error RecordIntroQuestionnaireAnswers(answers map[string]any) error } diff --git a/internal/models/statistics.go b/internal/models/statistics.go index fe4c8df..ed81b9d 100644 --- a/internal/models/statistics.go +++ b/internal/models/statistics.go @@ -3,14 +3,14 @@ package models import ( "time" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) type Statistics struct { - ID primitive.ObjectID `bson:"_id" json:"id"` - StartTime time.Time `bson:"start_time" json:"start_time"` - EndTime time.Time `bson:"end_time" json:"end_time"` - DurationMs int64 `bson:"duration_ms" json:"duration_ms"` + ID bson.ObjectID `bson:"_id" json:"id"` + StartTime time.Time `bson:"start_time" json:"start_time"` + EndTime time.Time `bson:"end_time" json:"end_time"` + DurationMs int64 `bson:"duration_ms" json:"duration_ms"` LastInterfaceType InterfaceType `bson:"last_interface_type" json:"last_interface_type"` TaskRuns int `bson:"task_runs" json:"task_runs"` @@ -38,12 +38,12 @@ type Statistics struct { } type TaskMetricsFile struct { - ID primitive.ObjectID `bson:"_id" json:"id"` - ParticipantID primitive.ObjectID `bson:"participant_id" json:"participant_id"` - TaskName string `bson:"task_name" json:"task_name"` - UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` - Summary TaskStatsSummary `bson:"summary" json:"summary"` - Runs []TaskRunMetrics `bson:"runs" json:"runs"` + ID bson.ObjectID `bson:"_id" json:"id"` + ParticipantID bson.ObjectID `bson:"participant_id" json:"participant_id"` + TaskName string `bson:"task_name" json:"task_name"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` + Summary TaskStatsSummary `bson:"summary" json:"summary"` + Runs []TaskRunMetrics `bson:"runs" json:"runs"` } type TaskStatsSummary struct { diff --git a/pkg/task/lifecycle.go b/pkg/task/lifecycle.go index 75b18d9..56063e2 100644 --- a/pkg/task/lifecycle.go +++ b/pkg/task/lifecycle.go @@ -4,7 +4,7 @@ import ( "log/slog" "github.com/LazyBachelor/LazyPM/internal/models" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) type RunLifecycle struct { @@ -24,7 +24,7 @@ func NewRunLifecycle(app *App, config Config, details models.TaskDetails, iType collector.recordUserAction(action) }) - var participantID primitive.ObjectID + var participantID bson.ObjectID var store MetricsStore if config.StatisticsStoragePath != "" { participantID = app.Stats.GetParticipantID() diff --git a/pkg/task/metrics_store.go b/pkg/task/metrics_store.go index a499de8..11ca88d 100644 --- a/pkg/task/metrics_store.go +++ b/pkg/task/metrics_store.go @@ -10,7 +10,7 @@ import ( "time" "github.com/LazyBachelor/LazyPM/internal/models" - "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/v2/bson" ) type MetricsStore interface { @@ -19,11 +19,11 @@ type MetricsStore interface { type FileMetricsStore struct { path string - participantID primitive.ObjectID + participantID bson.ObjectID logger *slog.Logger } -func NewFileMetricsStore(path string, participantID primitive.ObjectID, logger *slog.Logger) *FileMetricsStore { +func NewFileMetricsStore(path string, participantID bson.ObjectID, logger *slog.Logger) *FileMetricsStore { return &FileMetricsStore{ path: path, participantID: participantID, @@ -44,7 +44,7 @@ func (s *FileMetricsStore) Append(ctx context.Context, taskName string, run mode } metrics := models.TaskMetricsFile{ - ID: primitive.NewObjectID(), + ID: bson.NewObjectID(), ParticipantID: s.participantID, TaskName: taskName, Runs: []models.TaskRunMetrics{}, diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index 3f48bc1..5870316 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -62,7 +62,7 @@ func CreateIssue(w http.ResponseWriter, r *http.Request) { from := r.URL.Query().Get("from") referer := r.Header.Get("Referer") boardView := from == "board" || strings.Contains(referer, "board=true") - + if boardView { w.Header().Set("HX-Redirect", "/?board=true") } else { @@ -181,7 +181,7 @@ func UpdateIssue(w http.ResponseWriter, r *http.Request) { from := r.URL.Query().Get("from") referer := r.Header.Get("Referer") boardView := from == "board" || strings.Contains(referer, "board=true") - + if boardView { w.Header().Set("HX-Redirect", "/?board=true") } else { @@ -213,7 +213,7 @@ func UpdateAssignee(w http.ResponseWriter, r *http.Request) { // Check if we're in board view referer := r.Header.Get("Referer") boardView := strings.Contains(referer, "board=true") - + if boardView { w.Header().Set("HX-Redirect", "/?board=true&selected-issue="+issue.ID) } else { @@ -239,7 +239,7 @@ func DeleteIssue(w http.ResponseWriter, r *http.Request) { from := r.URL.Query().Get("from") referer := r.Header.Get("Referer") boardView := from == "board" || strings.Contains(referer, "board=true") - + if boardView { w.Header().Set("HX-Redirect", "/?board=true") } else { From 45e95a287fe56f001bb45391e58281ddbcf00925 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 18 Mar 2026 15:56:10 +0100 Subject: [PATCH 15/73] (hotfix) set git config at init --- build/setup-desktop.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/setup-desktop.sh b/build/setup-desktop.sh index af1e18f..072d241 100644 --- a/build/setup-desktop.sh +++ b/build/setup-desktop.sh @@ -98,4 +98,7 @@ EOF chmod +x /config/Desktop/LazyPM.desktop chown -R abc:abc /config/Desktop /config/.config +git config --global user.name "LazyPM" +git config --global user.email "lazy@pm.com" + echo "**** Desktop setup complete ****" \ No newline at end of file From 925cf089be7eb1609d68c91c6b04f194d7f4d0fe Mon Sep 17 00:00:00 2001 From: Ine Maria Nilssen Aanonsen Date: Thu, 19 Mar 2026 06:56:29 +0100 Subject: [PATCH 16/73] BACKLOG REFINEMENT removed duplicate close issue button --- pkg/web/routes/dashboard.templ | 11 -- pkg/web/routes/dashboard_templ.go | 125 +++++++--------- pkg/web/routes/issue_detail.templ | 11 -- pkg/web/routes/issue_detail_templ.go | 207 ++++++++++++--------------- 4 files changed, 143 insertions(+), 211 deletions(-) diff --git a/pkg/web/routes/dashboard.templ b/pkg/web/routes/dashboard.templ index 4ad5c02..db55c44 100644 --- a/pkg/web/routes/dashboard.templ +++ b/pkg/web/routes/dashboard.templ @@ -63,17 +63,6 @@ templ DashboardContent(props DashboardProps) { > Edit - if props.SelectedIssue.Status != "closed" { - - } ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">Edit ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + props.SelectedIssue.ID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 68, Col: 51} } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" hx-target=\"main\" hx-swap=\"innerHTML\" hx-push-url=\"true\">Details") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -169,12 +146,12 @@ func DashboardContent(props DashboardProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if props.SelectedIssue != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -184,12 +161,12 @@ func DashboardContent(props DashboardProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -213,12 +190,12 @@ func DashboardIssueList(issues []*models.Issue, selectedID string) templ.Compone }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var7 := templ.GetChildren(ctx) - if templ_7745c5c3_Var7 == nil { - templ_7745c5c3_Var7 = templ.NopComponent + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -238,7 +215,7 @@ func DashboardIssueList(issues []*models.Issue, selectedID string) templ.Compone if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -262,13 +239,13 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var8 := templ.GetChildren(ctx) - if templ_7745c5c3_Var8 == nil { - templ_7745c5c3_Var8 = templ.NopComponent + templ_7745c5c3_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if len(issues) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "No issues") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "No issues") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -278,77 +255,77 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone if selectedID == issue.ID { active = "bg-primary/10" } - var templ_7745c5c3_Var9 = []any{"hover cursor-pointer min-h-full w-full select-none", active} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) + var templ_7745c5c3_Var8 = []any{"hover cursor-pointer min-h-full w-full select-none", active} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 143, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 134, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 145, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 135, Col: 46} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 146, Col: 46} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -356,7 +333,7 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -364,7 +341,7 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -372,7 +349,7 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/routes/issue_detail.templ b/pkg/web/routes/issue_detail.templ index d483a83..5706792 100644 --- a/pkg/web/routes/issue_detail.templ +++ b/pkg/web/routes/issue_detail.templ @@ -47,17 +47,6 @@ templ IssueDetailContent(props IssueDetailProps) { > Edit - if props.Issue.Status != "closed" { - - }
@components.IssueDetail(components.IssueDetailProps{Issue: props.Issue}) diff --git a/pkg/web/routes/issue_detail_templ.go b/pkg/web/routes/issue_detail_templ.go index 6139e82..9328d69 100644 --- a/pkg/web/routes/issue_detail_templ.go +++ b/pkg/web/routes/issue_detail_templ.go @@ -86,30 +86,102 @@ func IssueDetailContent(props IssueDetailProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">Edit ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">Edit
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if props.Issue.Status != "closed" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + templ_7745c5c3_Err = components.IssueDetail(components.IssueDetailProps{Issue: props.Issue}).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.CommentSection(components.CommentSectionProps{ + IssueID: props.Issue.ID, + Comments: props.Comments, + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func IssueDetail(props IssueDetailProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var4 := templ.GetChildren(ctx) + if templ_7745c5c3_Var4 == nil { + templ_7745c5c3_Var4 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Err = IssueDetailContent(props).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func IssueDetailModalContent(props IssueDetailModalProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -136,99 +208,4 @@ func IssueDetailContent(props IssueDetailProps) templ.Component { }) } -func IssueDetail(props IssueDetailProps) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var5 := templ.GetChildren(ctx) - if templ_7745c5c3_Var5 == nil { - templ_7745c5c3_Var5 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var6 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = IssueDetailContent(props).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) - templ_7745c5c3_Err = BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func IssueDetailModalContent(props IssueDetailModalProps) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var7 := templ.GetChildren(ctx) - if templ_7745c5c3_Var7 == nil { - templ_7745c5c3_Var7 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = components.IssueDetail(components.IssueDetailProps{Issue: props.Issue}).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = components.CommentSection(components.CommentSectionProps{ - IssueID: props.Issue.ID, - Comments: props.Comments, - }).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - var _ = templruntime.GeneratedTemplate From 04061792e82f3b54c30c4123309da2913c3f8fac Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 19 Mar 2026 09:37:54 +0100 Subject: [PATCH 17/73] make sure modal handles overflow --- pkg/web/assets/css/styles.css | 4 +- pkg/web/components/modal.templ | 12 ++- pkg/web/components/modal_templ.go | 158 +++++++++++++----------------- 3 files changed, 79 insertions(+), 95 deletions(-) diff --git a/pkg/web/assets/css/styles.css b/pkg/web/assets/css/styles.css index e4072d2..2b9eab5 100644 --- a/pkg/web/assets/css/styles.css +++ b/pkg/web/assets/css/styles.css @@ -1,2 +1,2 @@ -/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-gray-500:oklch(55.1% .027 264.364);--color-black:#000;--spacing:.25rem;--container-xs:20rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}@media (prefers-color-scheme:dark){:root:not([data-theme]){color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}}:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dark]:checked),[data-theme=dark]{color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=cupcake]:checked),[data-theme=cupcake]{color-scheme:light;--color-base-100:oklch(97.788% .004 56.375);--color-base-200:oklch(93.982% .007 61.449);--color-base-300:oklch(91.586% .006 53.44);--color-base-content:oklch(23.574% .066 313.189);--color-primary:oklch(85% .138 181.071);--color-primary-content:oklch(43% .078 188.216);--color-secondary:oklch(89% .061 343.231);--color-secondary-content:oklch(45% .187 3.815);--color-accent:oklch(90% .076 70.697);--color-accent-content:oklch(47% .157 37.304);--color-neutral:oklch(27% .006 286.033);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(68% .169 237.323);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(69% .17 162.48);--color-success-content:oklch(26% .051 172.552);--color-warning:oklch(79% .184 86.047);--color-warning-content:oklch(28% .066 53.813);--color-error:oklch(64% .246 16.439);--color-error-content:oklch(27% .105 12.094);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:0}:root:has(input.theme-controller[value=bumblebee]:checked),[data-theme=bumblebee]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(92% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(85% .199 91.936);--color-primary-content:oklch(42% .095 57.708);--color-secondary:oklch(75% .183 55.934);--color-secondary-content:oklch(40% .123 38.172);--color-accent:oklch(0% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(37% .01 67.558);--color-neutral-content:oklch(92% .003 48.717);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(39% .09 240.876);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(39% .141 25.723);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=emerald]:checked),[data-theme=emerald]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(35.519% .032 262.988);--color-primary:oklch(76.662% .135 153.45);--color-primary-content:oklch(33.387% .04 162.24);--color-secondary:oklch(61.302% .202 261.294);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(72.772% .149 33.2);--color-accent-content:oklch(0% 0 0);--color-neutral:oklch(35.519% .032 262.988);--color-neutral-content:oklch(98.462% .001 247.838);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=corporate]:checked),[data-theme=corporate]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(22.389% .031 278.072);--color-primary:oklch(58% .158 241.966);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(55% .046 257.417);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(60% .118 184.704);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(0% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(60% .126 221.723);--color-info-content:oklch(100% 0 0);--color-success:oklch(62% .194 149.214);--color-success-content:oklch(100% 0 0);--color-warning:oklch(85% .199 91.936);--color-warning-content:oklch(0% 0 0);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(0% 0 0);--radius-selector:.25rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=synthwave]:checked),[data-theme=synthwave]{color-scheme:dark;--color-base-100:oklch(15% .09 281.288);--color-base-200:oklch(20% .09 281.288);--color-base-300:oklch(25% .09 281.288);--color-base-content:oklch(78% .115 274.713);--color-primary:oklch(71% .202 349.761);--color-primary-content:oklch(28% .109 3.907);--color-secondary:oklch(82% .111 230.318);--color-secondary-content:oklch(29% .066 243.157);--color-accent:oklch(75% .183 55.934);--color-accent-content:oklch(26% .079 36.259);--color-neutral:oklch(45% .24 277.023);--color-neutral-content:oklch(87% .065 274.039);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(77% .152 181.912);--color-success-content:oklch(27% .046 192.524);--color-warning:oklch(90% .182 98.111);--color-warning-content:oklch(42% .095 57.708);--color-error:oklch(73.7% .121 32.639);--color-error-content:oklch(23.501% .096 290.329);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=retro]:checked),[data-theme=retro]{color-scheme:light;--color-base-100:oklch(91.637% .034 90.515);--color-base-200:oklch(88.272% .049 91.774);--color-base-300:oklch(84.133% .065 90.856);--color-base-content:oklch(41% .112 45.904);--color-primary:oklch(80% .114 19.571);--color-primary-content:oklch(39% .141 25.723);--color-secondary:oklch(92% .084 155.995);--color-secondary-content:oklch(44% .119 151.328);--color-accent:oklch(68% .162 75.834);--color-accent-content:oklch(41% .112 45.904);--color-neutral:oklch(44% .011 73.639);--color-neutral-content:oklch(86% .005 56.366);--color-info:oklch(58% .158 241.966);--color-info-content:oklch(96% .059 95.617);--color-success:oklch(51% .096 186.391);--color-success-content:oklch(96% .059 95.617);--color-warning:oklch(64% .222 41.116);--color-warning-content:oklch(96% .059 95.617);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(40% .123 38.172);--radius-selector:.25rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=cyberpunk]:checked),[data-theme=cyberpunk]{color-scheme:light;--color-base-100:oklch(94.51% .179 104.32);--color-base-200:oklch(91.51% .179 104.32);--color-base-300:oklch(85.51% .179 104.32);--color-base-content:oklch(0% 0 0);--color-primary:oklch(74.22% .209 6.35);--color-primary-content:oklch(14.844% .041 6.35);--color-secondary:oklch(83.33% .184 204.72);--color-secondary-content:oklch(16.666% .036 204.72);--color-accent:oklch(71.86% .217 310.43);--color-accent-content:oklch(14.372% .043 310.43);--color-neutral:oklch(23.04% .065 269.31);--color-neutral-content:oklch(94.51% .179 104.32);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:0rem;--radius-field:0rem;--radius-box:0rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=valentine]:checked),[data-theme=valentine]{color-scheme:light;--color-base-100:oklch(97% .014 343.198);--color-base-200:oklch(94% .028 342.258);--color-base-300:oklch(89% .061 343.231);--color-base-content:oklch(52% .223 3.958);--color-primary:oklch(65% .241 354.308);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(62% .265 303.9);--color-secondary-content:oklch(97% .014 308.299);--color-accent:oklch(82% .111 230.318);--color-accent-content:oklch(39% .09 240.876);--color-neutral:oklch(40% .153 2.432);--color-neutral-content:oklch(89% .061 343.231);--color-info:oklch(86% .127 207.078);--color-info-content:oklch(44% .11 240.79);--color-success:oklch(84% .143 164.978);--color-success-content:oklch(43% .095 166.913);--color-warning:oklch(75% .183 55.934);--color-warning-content:oklch(26% .079 36.259);--color-error:oklch(63% .237 25.331);--color-error-content:oklch(97% .013 17.38);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=halloween]:checked),[data-theme=halloween]{color-scheme:dark;--color-base-100:oklch(21% .006 56.043);--color-base-200:oklch(14% .004 49.25);--color-base-300:oklch(0% 0 0);--color-base-content:oklch(84.955% 0 0);--color-primary:oklch(77.48% .204 60.62);--color-primary-content:oklch(19.693% .004 196.779);--color-secondary:oklch(45.98% .248 305.03);--color-secondary-content:oklch(89.196% .049 305.03);--color-accent:oklch(64.8% .223 136.073);--color-accent-content:oklch(0% 0 0);--color-neutral:oklch(24.371% .046 65.681);--color-neutral-content:oklch(84.874% .009 65.681);--color-info:oklch(54.615% .215 262.88);--color-info-content:oklch(90.923% .043 262.88);--color-success:oklch(62.705% .169 149.213);--color-success-content:oklch(12.541% .033 149.213);--color-warning:oklch(66.584% .157 58.318);--color-warning-content:oklch(13.316% .031 58.318);--color-error:oklch(65.72% .199 27.33);--color-error-content:oklch(13.144% .039 27.33);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=garden]:checked),[data-theme=garden]{color-scheme:light;--color-base-100:oklch(92.951% .002 17.197);--color-base-200:oklch(86.445% .002 17.197);--color-base-300:oklch(79.938% .001 17.197);--color-base-content:oklch(16.961% .001 17.32);--color-primary:oklch(62.45% .278 3.836);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(48.495% .11 355.095);--color-secondary-content:oklch(89.699% .022 355.095);--color-accent:oklch(56.273% .054 154.39);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(24.155% .049 89.07);--color-neutral-content:oklch(92.951% .002 17.197);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=forest]:checked),[data-theme=forest]{color-scheme:dark;--color-base-100:oklch(20.84% .008 17.911);--color-base-200:oklch(18.522% .007 17.911);--color-base-300:oklch(16.203% .007 17.911);--color-base-content:oklch(83.768% .001 17.911);--color-primary:oklch(68.628% .185 148.958);--color-primary-content:oklch(0% 0 0);--color-secondary:oklch(69.776% .135 168.327);--color-secondary-content:oklch(13.955% .027 168.327);--color-accent:oklch(70.628% .119 185.713);--color-accent-content:oklch(14.125% .023 185.713);--color-neutral:oklch(30.698% .039 171.364);--color-neutral-content:oklch(86.139% .007 171.364);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=aqua]:checked),[data-theme=aqua]{color-scheme:dark;--color-base-100:oklch(37% .146 265.522);--color-base-200:oklch(28% .091 267.935);--color-base-300:oklch(22% .091 267.935);--color-base-content:oklch(90% .058 230.902);--color-primary:oklch(85.661% .144 198.645);--color-primary-content:oklch(40.124% .068 197.603);--color-secondary:oklch(60.682% .108 309.782);--color-secondary-content:oklch(96% .016 293.756);--color-accent:oklch(93.426% .102 94.555);--color-accent-content:oklch(18.685% .02 94.555);--color-neutral:oklch(27% .146 265.522);--color-neutral-content:oklch(80% .146 265.522);--color-info:oklch(54.615% .215 262.88);--color-info-content:oklch(90.923% .043 262.88);--color-success:oklch(62.705% .169 149.213);--color-success-content:oklch(12.541% .033 149.213);--color-warning:oklch(66.584% .157 58.318);--color-warning-content:oklch(27% .077 45.635);--color-error:oklch(73.95% .19 27.33);--color-error-content:oklch(14.79% .038 27.33);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=lofi]:checked),[data-theme=lofi]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(94% 0 0);--color-base-content:oklch(0% 0 0);--color-primary:oklch(15.906% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(21.455% .001 17.278);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(26.861% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(0% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(79.54% .103 205.9);--color-info-content:oklch(15.908% .02 205.9);--color-success:oklch(90.13% .153 164.14);--color-success-content:oklch(18.026% .03 164.14);--color-warning:oklch(88.37% .135 79.94);--color-warning-content:oklch(17.674% .027 79.94);--color-error:oklch(78.66% .15 28.47);--color-error-content:oklch(15.732% .03 28.47);--radius-selector:2rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=pastel]:checked),[data-theme=pastel]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98.462% .001 247.838);--color-base-300:oklch(92.462% .001 247.838);--color-base-content:oklch(20% 0 0);--color-primary:oklch(90% .063 306.703);--color-primary-content:oklch(49% .265 301.924);--color-secondary:oklch(89% .058 10.001);--color-secondary-content:oklch(51% .222 16.935);--color-accent:oklch(90% .093 164.15);--color-accent-content:oklch(50% .118 165.612);--color-neutral:oklch(55% .046 257.417);--color-neutral-content:oklch(92% .013 255.508);--color-info:oklch(86% .127 207.078);--color-info-content:oklch(52% .105 223.128);--color-success:oklch(87% .15 154.449);--color-success-content:oklch(52% .154 150.069);--color-warning:oklch(83% .128 66.29);--color-warning-content:oklch(55% .195 38.402);--color-error:oklch(80% .114 19.571);--color-error-content:oklch(50% .213 27.518);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:0;--noise:0}:root:has(input.theme-controller[value=fantasy]:checked),[data-theme=fantasy]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(27.807% .029 256.847);--color-primary:oklch(37.45% .189 325.02);--color-primary-content:oklch(87.49% .037 325.02);--color-secondary:oklch(53.92% .162 241.36);--color-secondary-content:oklch(90.784% .032 241.36);--color-accent:oklch(75.98% .204 56.72);--color-accent-content:oklch(15.196% .04 56.72);--color-neutral:oklch(27.807% .029 256.847);--color-neutral-content:oklch(85.561% .005 256.847);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=wireframe]:checked),[data-theme=wireframe]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(94% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(87% 0 0);--color-primary-content:oklch(26% 0 0);--color-secondary:oklch(87% 0 0);--color-secondary-content:oklch(26% 0 0);--color-accent:oklch(87% 0 0);--color-accent-content:oklch(26% 0 0);--color-neutral:oklch(87% 0 0);--color-neutral-content:oklch(26% 0 0);--color-info:oklch(44% .11 240.79);--color-info-content:oklch(90% .058 230.902);--color-success:oklch(43% .095 166.913);--color-success-content:oklch(90% .093 164.15);--color-warning:oklch(47% .137 46.201);--color-warning-content:oklch(92% .12 95.746);--color-error:oklch(44% .177 26.899);--color-error-content:oklch(88% .062 18.334);--radius-selector:0rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=black]:checked),[data-theme=black]{color-scheme:dark;--color-base-100:oklch(0% 0 0);--color-base-200:oklch(19% 0 0);--color-base-300:oklch(22% 0 0);--color-base-content:oklch(87.609% 0 0);--color-primary:oklch(35% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(35% 0 0);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(35% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(35% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(45.201% .313 264.052);--color-info-content:oklch(89.04% .062 264.052);--color-success:oklch(51.975% .176 142.495);--color-success-content:oklch(90.395% .035 142.495);--color-warning:oklch(96.798% .211 109.769);--color-warning-content:oklch(19.359% .042 109.769);--color-error:oklch(62.795% .257 29.233);--color-error-content:oklch(12.559% .051 29.233);--radius-selector:0rem;--radius-field:0rem;--radius-box:0rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=luxury]:checked),[data-theme=luxury]{color-scheme:dark;--color-base-100:oklch(14.076% .004 285.822);--color-base-200:oklch(20.219% .004 308.229);--color-base-300:oklch(23.219% .004 308.229);--color-base-content:oklch(75.687% .123 76.89);--color-primary:oklch(100% 0 0);--color-primary-content:oklch(20% 0 0);--color-secondary:oklch(27.581% .064 261.069);--color-secondary-content:oklch(85.516% .012 261.069);--color-accent:oklch(36.674% .051 338.825);--color-accent-content:oklch(87.334% .01 338.825);--color-neutral:oklch(24.27% .057 59.825);--color-neutral-content:oklch(93.203% .089 90.861);--color-info:oklch(79.061% .121 237.133);--color-info-content:oklch(15.812% .024 237.133);--color-success:oklch(78.119% .192 132.154);--color-success-content:oklch(15.623% .038 132.154);--color-warning:oklch(86.127% .136 102.891);--color-warning-content:oklch(17.225% .027 102.891);--color-error:oklch(71.753% .176 22.568);--color-error-content:oklch(14.35% .035 22.568);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dracula]:checked),[data-theme=dracula]{color-scheme:dark;--color-base-100:oklch(28.822% .022 277.508);--color-base-200:oklch(26.805% .02 277.508);--color-base-300:oklch(24.787% .019 277.508);--color-base-content:oklch(97.747% .007 106.545);--color-primary:oklch(75.461% .183 346.812);--color-primary-content:oklch(15.092% .036 346.812);--color-secondary:oklch(74.202% .148 301.883);--color-secondary-content:oklch(14.84% .029 301.883);--color-accent:oklch(83.392% .124 66.558);--color-accent-content:oklch(16.678% .024 66.558);--color-neutral:oklch(39.445% .032 275.524);--color-neutral-content:oklch(87.889% .006 275.524);--color-info:oklch(88.263% .093 212.846);--color-info-content:oklch(17.652% .018 212.846);--color-success:oklch(87.099% .219 148.024);--color-success-content:oklch(17.419% .043 148.024);--color-warning:oklch(95.533% .134 112.757);--color-warning-content:oklch(19.106% .026 112.757);--color-error:oklch(68.22% .206 24.43);--color-error-content:oklch(13.644% .041 24.43);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=cmyk]:checked),[data-theme=cmyk]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(95% 0 0);--color-base-300:oklch(90% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(71.772% .133 239.443);--color-primary-content:oklch(14.354% .026 239.443);--color-secondary:oklch(64.476% .202 359.339);--color-secondary-content:oklch(12.895% .04 359.339);--color-accent:oklch(94.228% .189 105.306);--color-accent-content:oklch(18.845% .037 105.306);--color-neutral:oklch(21.778% 0 0);--color-neutral-content:oklch(84.355% 0 0);--color-info:oklch(68.475% .094 217.284);--color-info-content:oklch(13.695% .018 217.284);--color-success:oklch(46.949% .162 321.406);--color-success-content:oklch(89.389% .032 321.406);--color-warning:oklch(71.236% .159 52.023);--color-warning-content:oklch(14.247% .031 52.023);--color-error:oklch(62.013% .208 28.717);--color-error-content:oklch(12.402% .041 28.717);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=autumn]:checked),[data-theme=autumn]{color-scheme:light;--color-base-100:oklch(95.814% 0 0);--color-base-200:oklch(89.107% 0 0);--color-base-300:oklch(82.4% 0 0);--color-base-content:oklch(19.162% 0 0);--color-primary:oklch(40.723% .161 17.53);--color-primary-content:oklch(88.144% .032 17.53);--color-secondary:oklch(61.676% .169 23.865);--color-secondary-content:oklch(12.335% .033 23.865);--color-accent:oklch(73.425% .094 60.729);--color-accent-content:oklch(14.685% .018 60.729);--color-neutral:oklch(54.367% .037 51.902);--color-neutral-content:oklch(90.873% .007 51.902);--color-info:oklch(69.224% .097 207.284);--color-info-content:oklch(13.844% .019 207.284);--color-success:oklch(60.995% .08 174.616);--color-success-content:oklch(12.199% .016 174.616);--color-warning:oklch(70.081% .164 56.844);--color-warning-content:oklch(14.016% .032 56.844);--color-error:oklch(53.07% .241 24.16);--color-error-content:oklch(90.614% .048 24.16);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=business]:checked),[data-theme=business]{color-scheme:dark;--color-base-100:oklch(24.353% 0 0);--color-base-200:oklch(22.648% 0 0);--color-base-300:oklch(20.944% 0 0);--color-base-content:oklch(84.87% 0 0);--color-primary:oklch(41.703% .099 251.473);--color-primary-content:oklch(88.34% .019 251.473);--color-secondary:oklch(64.092% .027 229.389);--color-secondary-content:oklch(12.818% .005 229.389);--color-accent:oklch(67.271% .167 35.791);--color-accent-content:oklch(13.454% .033 35.791);--color-neutral:oklch(27.441% .013 253.041);--color-neutral-content:oklch(85.488% .002 253.041);--color-info:oklch(62.616% .143 240.033);--color-info-content:oklch(12.523% .028 240.033);--color-success:oklch(70.226% .094 156.596);--color-success-content:oklch(14.045% .018 156.596);--color-warning:oklch(77.482% .115 81.519);--color-warning-content:oklch(15.496% .023 81.519);--color-error:oklch(51.61% .146 29.674);--color-error-content:oklch(90.322% .029 29.674);--radius-selector:0rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=acid]:checked),[data-theme=acid]{color-scheme:light;--color-base-100:oklch(98% 0 0);--color-base-200:oklch(95% 0 0);--color-base-300:oklch(91% 0 0);--color-base-content:oklch(0% 0 0);--color-primary:oklch(71.9% .357 330.759);--color-primary-content:oklch(14.38% .071 330.759);--color-secondary:oklch(73.37% .224 48.25);--color-secondary-content:oklch(14.674% .044 48.25);--color-accent:oklch(92.78% .264 122.962);--color-accent-content:oklch(18.556% .052 122.962);--color-neutral:oklch(21.31% .128 278.68);--color-neutral-content:oklch(84.262% .025 278.68);--color-info:oklch(60.72% .227 252.05);--color-info-content:oklch(12.144% .045 252.05);--color-success:oklch(85.72% .266 158.53);--color-success-content:oklch(17.144% .053 158.53);--color-warning:oklch(91.01% .212 100.5);--color-warning-content:oklch(18.202% .042 100.5);--color-error:oklch(64.84% .293 29.349);--color-error-content:oklch(12.968% .058 29.349);--radius-selector:1rem;--radius-field:1rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=lemonade]:checked),[data-theme=lemonade]{color-scheme:light;--color-base-100:oklch(98.71% .02 123.72);--color-base-200:oklch(91.8% .018 123.72);--color-base-300:oklch(84.89% .017 123.72);--color-base-content:oklch(19.742% .004 123.72);--color-primary:oklch(58.92% .199 134.6);--color-primary-content:oklch(11.784% .039 134.6);--color-secondary:oklch(77.75% .196 111.09);--color-secondary-content:oklch(15.55% .039 111.09);--color-accent:oklch(85.39% .201 100.73);--color-accent-content:oklch(17.078% .04 100.73);--color-neutral:oklch(30.98% .075 108.6);--color-neutral-content:oklch(86.196% .015 108.6);--color-info:oklch(86.19% .047 224.14);--color-info-content:oklch(17.238% .009 224.14);--color-success:oklch(86.19% .047 157.85);--color-success-content:oklch(17.238% .009 157.85);--color-warning:oklch(86.19% .047 102.15);--color-warning-content:oklch(17.238% .009 102.15);--color-error:oklch(86.19% .047 25.85);--color-error-content:oklch(17.238% .009 25.85);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=night]:checked),[data-theme=night]{color-scheme:dark;--color-base-100:oklch(20.768% .039 265.754);--color-base-200:oklch(19.314% .037 265.754);--color-base-300:oklch(17.86% .034 265.754);--color-base-content:oklch(84.153% .007 265.754);--color-primary:oklch(75.351% .138 232.661);--color-primary-content:oklch(15.07% .027 232.661);--color-secondary:oklch(68.011% .158 276.934);--color-secondary-content:oklch(13.602% .031 276.934);--color-accent:oklch(72.36% .176 350.048);--color-accent-content:oklch(14.472% .035 350.048);--color-neutral:oklch(27.949% .036 260.03);--color-neutral-content:oklch(85.589% .007 260.03);--color-info:oklch(68.455% .148 237.251);--color-info-content:oklch(0% 0 0);--color-success:oklch(78.452% .132 181.911);--color-success-content:oklch(15.69% .026 181.911);--color-warning:oklch(83.242% .139 82.95);--color-warning-content:oklch(16.648% .027 82.95);--color-error:oklch(71.785% .17 13.118);--color-error-content:oklch(14.357% .034 13.118);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=coffee]:checked),[data-theme=coffee]{color-scheme:dark;--color-base-100:oklch(24% .023 329.708);--color-base-200:oklch(21% .021 329.708);--color-base-300:oklch(16% .019 329.708);--color-base-content:oklch(72.354% .092 79.129);--color-primary:oklch(71.996% .123 62.756);--color-primary-content:oklch(14.399% .024 62.756);--color-secondary:oklch(34.465% .029 199.194);--color-secondary-content:oklch(86.893% .005 199.194);--color-accent:oklch(42.621% .074 224.389);--color-accent-content:oklch(88.524% .014 224.389);--color-neutral:oklch(16.51% .015 326.261);--color-neutral-content:oklch(83.302% .003 326.261);--color-info:oklch(79.49% .063 184.558);--color-info-content:oklch(15.898% .012 184.558);--color-success:oklch(74.722% .072 131.116);--color-success-content:oklch(14.944% .014 131.116);--color-warning:oklch(88.15% .14 87.722);--color-warning-content:oklch(17.63% .028 87.722);--color-error:oklch(77.318% .128 31.871);--color-error-content:oklch(15.463% .025 31.871);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=winter]:checked),[data-theme=winter]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97.466% .011 259.822);--color-base-300:oklch(93.268% .016 262.751);--color-base-content:oklch(41.886% .053 255.824);--color-primary:oklch(56.86% .255 257.57);--color-primary-content:oklch(91.372% .051 257.57);--color-secondary:oklch(42.551% .161 282.339);--color-secondary-content:oklch(88.51% .032 282.339);--color-accent:oklch(59.939% .191 335.171);--color-accent-content:oklch(11.988% .038 335.171);--color-neutral:oklch(19.616% .063 257.651);--color-neutral-content:oklch(83.923% .012 257.651);--color-info:oklch(88.127% .085 214.515);--color-info-content:oklch(17.625% .017 214.515);--color-success:oklch(80.494% .077 197.823);--color-success-content:oklch(16.098% .015 197.823);--color-warning:oklch(89.172% .045 71.47);--color-warning-content:oklch(17.834% .009 71.47);--color-error:oklch(73.092% .11 20.076);--color-error-content:oklch(14.618% .022 20.076);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=dim]:checked),[data-theme=dim]{color-scheme:dark;--color-base-100:oklch(30.857% .023 264.149);--color-base-200:oklch(28.036% .019 264.182);--color-base-300:oklch(26.346% .018 262.177);--color-base-content:oklch(82.901% .031 222.959);--color-primary:oklch(86.133% .141 139.549);--color-primary-content:oklch(17.226% .028 139.549);--color-secondary:oklch(73.375% .165 35.353);--color-secondary-content:oklch(14.675% .033 35.353);--color-accent:oklch(74.229% .133 311.379);--color-accent-content:oklch(14.845% .026 311.379);--color-neutral:oklch(24.731% .02 264.094);--color-neutral-content:oklch(82.901% .031 222.959);--color-info:oklch(86.078% .142 206.182);--color-info-content:oklch(17.215% .028 206.182);--color-success:oklch(86.171% .142 166.534);--color-success-content:oklch(17.234% .028 166.534);--color-warning:oklch(86.163% .142 94.818);--color-warning-content:oklch(17.232% .028 94.818);--color-error:oklch(82.418% .099 33.756);--color-error-content:oklch(16.483% .019 33.756);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=nord]:checked),[data-theme=nord]{color-scheme:light;--color-base-100:oklch(95.127% .007 260.731);--color-base-200:oklch(93.299% .01 261.788);--color-base-300:oklch(89.925% .016 262.749);--color-base-content:oklch(32.437% .022 264.182);--color-primary:oklch(59.435% .077 254.027);--color-primary-content:oklch(11.887% .015 254.027);--color-secondary:oklch(69.651% .059 248.687);--color-secondary-content:oklch(13.93% .011 248.687);--color-accent:oklch(77.464% .062 217.469);--color-accent-content:oklch(15.492% .012 217.469);--color-neutral:oklch(45.229% .035 264.131);--color-neutral-content:oklch(89.925% .016 262.749);--color-info:oklch(69.207% .062 332.664);--color-info-content:oklch(13.841% .012 332.664);--color-success:oklch(76.827% .074 131.063);--color-success-content:oklch(15.365% .014 131.063);--color-warning:oklch(85.486% .089 84.093);--color-warning-content:oklch(17.097% .017 84.093);--color-error:oklch(60.61% .12 15.341);--color-error-content:oklch(12.122% .024 15.341);--radius-selector:1rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=sunset]:checked),[data-theme=sunset]{color-scheme:dark;--color-base-100:oklch(22% .019 237.69);--color-base-200:oklch(20% .019 237.69);--color-base-300:oklch(18% .019 237.69);--color-base-content:oklch(77.383% .043 245.096);--color-primary:oklch(74.703% .158 39.947);--color-primary-content:oklch(14.94% .031 39.947);--color-secondary:oklch(72.537% .177 2.72);--color-secondary-content:oklch(14.507% .035 2.72);--color-accent:oklch(71.294% .166 299.844);--color-accent-content:oklch(14.258% .033 299.844);--color-neutral:oklch(26% .019 237.69);--color-neutral-content:oklch(70% .019 237.69);--color-info:oklch(85.559% .085 206.015);--color-info-content:oklch(17.111% .017 206.015);--color-success:oklch(85.56% .085 144.778);--color-success-content:oklch(17.112% .017 144.778);--color-warning:oklch(85.569% .084 74.427);--color-warning-content:oklch(17.113% .016 74.427);--color-error:oklch(85.511% .078 16.886);--color-error-content:oklch(17.102% .015 16.886);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=caramellatte]:checked),[data-theme=caramellatte]{color-scheme:light;--color-base-100:oklch(98% .016 73.684);--color-base-200:oklch(95% .038 75.164);--color-base-300:oklch(90% .076 70.697);--color-base-content:oklch(40% .123 38.172);--color-primary:oklch(0% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(22.45% .075 37.85);--color-secondary-content:oklch(90% .076 70.697);--color-accent:oklch(46.44% .111 37.85);--color-accent-content:oklch(90% .076 70.697);--color-neutral:oklch(55% .195 38.402);--color-neutral-content:oklch(98% .016 73.684);--color-info:oklch(42% .199 265.638);--color-info-content:oklch(90% .076 70.697);--color-success:oklch(43% .095 166.913);--color-success-content:oklch(90% .076 70.697);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(39% .141 25.723);--radius-selector:2rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:1}:root:has(input.theme-controller[value=abyss]:checked),[data-theme=abyss]{color-scheme:dark;--color-base-100:oklch(20% .08 209);--color-base-200:oklch(15% .08 209);--color-base-300:oklch(10% .08 209);--color-base-content:oklch(90% .076 70.697);--color-primary:oklch(92% .2653 125);--color-primary-content:oklch(50% .2653 125);--color-secondary:oklch(83.27% .0764 298.3);--color-secondary-content:oklch(43.27% .0764 298.3);--color-accent:oklch(43% 0 0);--color-accent-content:oklch(98% 0 0);--color-neutral:oklch(30% .08 209);--color-neutral-content:oklch(90% .076 70.697);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(79% .209 151.711);--color-success-content:oklch(26% .065 152.934);--color-warning:oklch(84.8% .1962 84.62);--color-warning-content:oklch(44.8% .1962 84.62);--color-error:oklch(65% .1985 24.22);--color-error-content:oklch(27% .1985 24.22);--radius-selector:2rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=silk]:checked),[data-theme=silk]{color-scheme:light;--color-base-100:oklch(97% .0035 67.78);--color-base-200:oklch(95% .0081 61.42);--color-base-300:oklch(90% .0081 61.42);--color-base-content:oklch(40% .0081 61.42);--color-primary:oklch(23.27% .0249 284.3);--color-primary-content:oklch(94.22% .2505 117.44);--color-secondary:oklch(23.27% .0249 284.3);--color-secondary-content:oklch(73.92% .2135 50.94);--color-accent:oklch(23.27% .0249 284.3);--color-accent-content:oklch(88.92% .2061 189.9);--color-neutral:oklch(20% 0 0);--color-neutral-content:oklch(80% .0081 61.42);--color-info:oklch(80.39% .1148 241.68);--color-info-content:oklch(30.39% .1148 241.68);--color-success:oklch(83.92% .0901 136.87);--color-success-content:oklch(23.92% .0901 136.87);--color-warning:oklch(83.92% .1085 80);--color-warning-content:oklch(43.92% .1085 80);--color-error:oklch(75.1% .1814 22.37);--color-error-content:oklch(35.1% .1814 22.37);--radius-selector:2rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:0}:root{--fx-noise:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E");scrollbar-color:currentColor #0000}@supports (color:color-mix(in lab, red, red)){:root{scrollbar-color:color-mix(in oklch, currentColor 35%, #0000) #0000}}@property --radialprogress{syntax:"";inherits:true;initial-value:0%}:root:not(span){overflow:var(--page-overflow)}:root{background:var(--page-scroll-bg,var(--root-bg));--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000), var(--root-bg,#0000)) var(--root-bg,#0000)}@supports (color:color-mix(in lab, red, red)){:root{--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000), var(--root-bg,#0000)) color-mix(in srgb, var(--root-bg,#0000), oklch(0% 0 0) calc(var(--page-has-backdrop,0) * 40%))}}:root{--page-scroll-transition-on:background-color .3s ease-out;transition:var(--page-scroll-transition);scrollbar-gutter:var(--page-scroll-gutter,unset);scrollbar-gutter:if(style(--page-has-scroll: 1): var(--page-scroll-gutter,unset) ; else: unset)}@keyframes set-page-has-scroll{0%,to{--page-has-scroll:1}}:root,[data-theme]{background:var(--page-scroll-bg,var(--root-bg));color:var(--color-base-content)}:where(:root,[data-theme]){--root-bg:var(--color-base-100)}}@layer components;@layer utilities{@layer daisyui.l1.l2.l3{.modal{pointer-events:none;visibility:hidden;width:100%;max-width:none;height:100%;max-height:none;color:inherit;transition:visibility .3s allow-discrete, background-color .3s ease-out, opacity .1s ease-out;overscroll-behavior:contain;z-index:999;scrollbar-gutter:auto;background-color:#0000;place-items:center;margin:0;padding:0;display:grid;position:fixed;inset:0;overflow:clip}.modal::backdrop{display:none}.tooltip{--tt-bg:var(--color-neutral);--tt-off:calc(100% + .5rem);--tt-tail:calc(100% + 1px + .25rem);display:inline-block;position:relative}.tooltip>.tooltip-content,.tooltip[data-tip]:before{border-radius:var(--radius-field);text-align:center;white-space:normal;max-width:20rem;color:var(--color-neutral-content);opacity:0;background-color:var(--tt-bg);pointer-events:none;z-index:2;--tw-content:attr(data-tip);content:var(--tw-content);width:max-content;padding-block:.25rem;padding-inline:.5rem;font-size:.875rem;line-height:1.25;position:absolute}.tooltip:after{opacity:0;background-color:var(--tt-bg);content:"";pointer-events:none;--mask-tooltip:url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A");width:.625rem;height:.25rem;-webkit-mask-position:-1px 0;mask-position:-1px 0;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-image:var(--mask-tooltip);-webkit-mask-image:var(--mask-tooltip);mask-image:var(--mask-tooltip);display:block;position:absolute}@media (prefers-reduced-motion:no-preference){.tooltip>.tooltip-content,.tooltip[data-tip]:before,.tooltip:after{transition:opacity .2s cubic-bezier(.4,0,.2,1) 75ms,transform .2s cubic-bezier(.4,0,.2,1) 75ms}}:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))>.tooltip-content,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))[data-tip]:before,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible)):after{opacity:1;--tt-pos:0rem}@media (prefers-reduced-motion:no-preference){:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))>.tooltip-content,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))[data-tip]:before,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible)):after{transition:opacity .2s cubic-bezier(.4,0,.2,1),transform .2s cubic-bezier(.4,0,.2,1)}}.tab{cursor:pointer;appearance:none;text-align:center;webkit-user-select:none;-webkit-user-select:none;user-select:none;flex-wrap:wrap;justify-content:center;align-items:center;display:inline-flex;position:relative}@media (hover:hover){.tab:hover{color:var(--color-base-content)}}.tab{--tab-p:.75rem;--tab-bg:var(--color-base-100);--tab-border-color:var(--color-base-300);--tab-radius-ss:0;--tab-radius-se:0;--tab-radius-es:0;--tab-radius-ee:0;--tab-order:0;--tab-radius-min:calc(.75rem - var(--border));--tab-radius-limit:min(var(--radius-field), var(--tab-radius-min));--tab-radius-grad:#0000 calc(69% - var(--border)), var(--tab-border-color) calc(69% - var(--border) + .25px), var(--tab-border-color) 69%, var(--tab-bg) calc(69% + .25px);order:var(--tab-order);height:var(--tab-height);padding-inline:var(--tab-p);border-color:#0000;font-size:.875rem}.tab:is(input[type=radio]){min-width:fit-content}.tab:is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.tab:is(label){position:relative}.tab:is(label) input{cursor:pointer;appearance:none;opacity:0;position:absolute;inset:0}:is(.tab:checked,.tab:is(label:has(:checked)),.tab:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]))+.tab-content{display:block}.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:color-mix(in oklab, var(--color-base-content) 50%, transparent)}}.tab:not(input):empty{cursor:default;flex-grow:1}.tab:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.tab:focus{outline-offset:2px;outline:2px solid #0000}}.tab:focus-visible,.tab:is(label:has(:checked:focus-visible)){outline-offset:-5px;outline:2px solid}.tab[disabled]{pointer-events:none;opacity:.4}.menu{--menu-active-fg:var(--color-neutral-content);--menu-active-bg:var(--color-neutral);flex-flow:column wrap;width:fit-content;padding:.5rem;font-size:.875rem;display:flex}.menu :where(li ul){white-space:nowrap;margin-inline-start:1rem;padding-inline-start:.5rem;position:relative}.menu :where(li ul):before{background-color:var(--color-base-content);opacity:.1;width:var(--border);content:"";inset-inline-start:0;position:absolute;top:.75rem;bottom:.75rem}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--radius-field);text-align:start;text-wrap:balance;-webkit-user-select:none;user-select:none;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;align-content:flex-start;align-items:center;gap:.5rem;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:grid}.menu :where(li>details>summary){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li>details>summary){outline-offset:2px;outline:2px solid #0000}}.menu :where(li>details>summary)::-webkit-details-marker{display:none}:is(.menu :where(li>details>summary),.menu :where(li>.menu-dropdown-toggle)):after{content:"";transform-origin:50%;pointer-events:none;justify-self:flex-end;width:.375rem;height:.375rem;transition-property:rotate,translate;transition-duration:.2s;display:block;translate:0 -1px;rotate:-135deg;box-shadow:inset 2px 2px}.menu details{interpolate-size:allow-keywords;overflow:hidden}.menu details::details-content{block-size:0}@media (prefers-reduced-motion:no-preference){.menu details::details-content{transition-behavior:allow-discrete;transition-property:block-size,content-visibility;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.menu details[open]::details-content{block-size:auto}.menu :where(li>details[open]>summary):after,.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after{translate:0 1px;rotate:45deg}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{color:var(--color-base-content);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){box-shadow:inset 0 1px oklch(0% 0 0/.01),inset 0 -1px oklch(100% 0 0/.01)}.menu :where(li:empty){background-color:var(--color-base-content);opacity:.1;height:1px;margin:.5rem 1rem}.menu :where(li){flex-flow:column wrap;flex-shrink:0;align-items:stretch;display:flex;position:relative}.menu :where(li) .badge{justify-self:flex-end}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{outline-offset:2px;outline:2px solid #0000}}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{color:var(--menu-active-fg);background-color:var(--menu-active-bg);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise)}:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):not(:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):active){box-shadow:0 2px calc(var(--depth) * 3px) -2px var(--menu-active-bg)}.menu :where(li).menu-disabled{pointer-events:none;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li).menu-disabled{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.menu .dropdown:focus-within .menu-dropdown-toggle:after{translate:0 1px;rotate:45deg}.menu .dropdown-content{margin-top:.5rem;padding:.5rem}.menu .dropdown-content:before{display:none}:where(.btn){width:unset}.btn{cursor:pointer;text-align:center;vertical-align:middle;outline-offset:2px;webkit-user-select:none;-webkit-user-select:none;user-select:none;padding-inline:var(--btn-p);color:var(--btn-fg);--tw-prose-links:var(--btn-fg);height:var(--size);font-size:var(--fontsize,.875rem);outline-color:var(--btn-color,var(--color-base-content));background-color:var(--btn-bg);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--btn-noise);border-width:var(--border);border-style:solid;border-color:var(--btn-border);text-shadow:0 .5px oklch(100% 0 0 / calc(var(--depth) * .15));touch-action:manipulation;box-shadow:0 .5px 0 .5px oklch(100% 0 0 / calc(var(--depth) * 6%)) inset, var(--btn-shadow);--size:calc(var(--size-field,.25rem) * 10);--btn-bg:var(--btn-color,var(--color-base-200));--btn-fg:var(--color-base-content);--btn-p:1rem;--btn-border:var(--btn-bg);border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-wrap:nowrap;flex-shrink:0;justify-content:center;align-items:center;gap:.375rem;font-weight:600;transition-property:color,background-color,border-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:inline-flex}@supports (color:color-mix(in lab, red, red)){.btn{--btn-border:color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%))}}.btn{--btn-shadow:0 3px 2px -2px var(--btn-bg), 0 4px 3px -2px var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn{--btn-shadow:0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000), 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000)}}.btn{--btn-noise:var(--fx-noise)}@media (hover:hover){.btn:hover{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:hover{--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}}.btn:focus-visible,.btn:has(:focus-visible){isolation:isolate;outline-width:2px;outline-style:solid}.btn:active:not(.btn-active){--btn-bg:var(--btn-color,var(--color-base-200));translate:0 .5px}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 5%)}}.btn:active:not(.btn-active){--btn-border:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-border:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}.btn:active:not(.btn-active){--btn-shadow:0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0)}.btn:is(input[type=checkbox],input[type=radio]){appearance:none}.btn:is(input[type=checkbox],input[type=radio])[aria-label]:after{--tw-content:attr(aria-label);content:var(--tw-content)}.btn:where(input:checked:not(.filter .btn)){--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content);isolation:isolate}.loading{pointer-events:none;aspect-ratio:1;vertical-align:middle;width:calc(var(--size-selector,.25rem) * 6);background-color:currentColor;display:inline-block;-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:100%;mask-size:100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.collapse{border-radius:var(--radius-box,1rem);isolation:isolate;grid-template-rows:max-content 0fr;grid-template-columns:minmax(0,1fr);width:100%;display:grid;position:relative;overflow:hidden}@media (prefers-reduced-motion:no-preference){.collapse{transition:grid-template-rows .2s}}.collapse>input:is([type=checkbox],[type=radio]){appearance:none;opacity:0;z-index:1;grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close)),.collapse:not(.collapse-close):has(>input:is([type=checkbox],[type=radio]):checked){grid-template-rows:max-content 1fr}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){content-visibility:visible;min-height:fit-content}@supports not (content-visibility:visible){.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){visibility:visible}}.collapse:focus-visible,.collapse:has(>input:is([type=checkbox],[type=radio]):focus-visible),.collapse:has(summary:focus-visible){outline-color:var(--color-base-content);outline-offset:2px;outline-width:2px;outline-style:solid}.collapse:not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-close)>input[type=radio]:not(:checked),.collapse:not(.collapse-close)>.collapse-title{cursor:pointer}:is(.collapse[tabindex]:focus:not(.collapse-close,.collapse[open]),.collapse[tabindex]:focus-within:not(.collapse-close,.collapse[open]))>.collapse-title{cursor:unset}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){padding-bottom:1rem}.collapse:is(details){width:100%}@media (prefers-reduced-motion:no-preference){.collapse:is(details)::details-content{transition:content-visibility .2s allow-discrete, visibility .2s allow-discrete, min-height .2s ease-out allow-discrete, padding .1s ease-out 20ms, background-color .2s ease-out, height .2s;interpolate-size:allow-keywords;height:0}.collapse:is(details):where([open])::details-content{height:auto}}.collapse:is(details) summary{display:block;position:relative}.collapse:is(details) summary::-webkit-details-marker{display:none}.collapse:is(details)>.collapse-content{content-visibility:visible}.collapse:is(details) summary{outline:none}.validator-hint{visibility:hidden;margin-top:.5rem;font-size:.75rem}.validator:user-valid{--input-color:var(--color-success)}.validator:user-valid:focus{--input-color:var(--color-success)}.validator:user-valid:checked{--input-color:var(--color-success)}.validator:user-valid[aria-checked=true]{--input-color:var(--color-success)}.validator:user-valid:focus-within{--input-color:var(--color-success)}.validator:has(:user-valid){--input-color:var(--color-success)}.validator:has(:user-valid):focus{--input-color:var(--color-success)}.validator:has(:user-valid):checked{--input-color:var(--color-success)}.validator:has(:user-valid)[aria-checked=true]{--input-color:var(--color-success)}.validator:has(:user-valid):focus-within{--input-color:var(--color-success)}.validator:user-invalid{--input-color:var(--color-error)}.validator:user-invalid:focus{--input-color:var(--color-error)}.validator:user-invalid:checked{--input-color:var(--color-error)}.validator:user-invalid[aria-checked=true]{--input-color:var(--color-error)}.validator:user-invalid:focus-within{--input-color:var(--color-error)}.validator:user-invalid~.validator-hint{visibility:visible;color:var(--color-error)}.validator:has(:user-invalid){--input-color:var(--color-error)}.validator:has(:user-invalid):focus{--input-color:var(--color-error)}.validator:has(:user-invalid):checked{--input-color:var(--color-error)}.validator:has(:user-invalid)[aria-checked=true]{--input-color:var(--color-error)}.validator:has(:user-invalid):focus-within{--input-color:var(--color-error)}.validator:has(:user-invalid)~.validator-hint{visibility:visible;color:var(--color-error)}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))),:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):focus,:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):checked,:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))[aria-checked=true],:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):focus-within{--input-color:var(--color-error)}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))~.validator-hint{visibility:visible;color:var(--color-error)}.list{flex-direction:column;font-size:.875rem;display:flex}.list .list-row{--list-grid-cols:minmax(0, auto) 1fr;border-radius:var(--radius-box);word-break:break-word;grid-auto-flow:column;grid-template-columns:var(--list-grid-cols);gap:1rem;padding:1rem;display:grid;position:relative}:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{content:"";border-bottom:var(--border) solid;inset-inline:var(--radius-box);border-color:var(--color-base-content);position:absolute;bottom:0}@supports (color:color-mix(in lab, red, red)){:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{border-color:color-mix(in oklab, var(--color-base-content) 5%, transparent)}}.toggle{border:var(--border) solid currentColor;color:var(--input-color);cursor:pointer;appearance:none;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)));padding:var(--toggle-p);flex-shrink:0;grid-template-columns:0fr 1fr 1fr;place-content:center;display:inline-grid;position:relative;box-shadow:inset 0 1px}@supports (color:color-mix(in lab, red, red)){.toggle{box-shadow:0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset}}.toggle{--input-color:var(--color-base-content);transition:color .3s,grid-template-columns .2s}@supports (color:color-mix(in lab, red, red)){.toggle{--input-color:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}.toggle{--toggle-p:calc(var(--size) * .125);--size:calc(var(--size-selector,.25rem) * 6);width:calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2);height:var(--size)}.toggle>*{z-index:1;cursor:pointer;appearance:none;background-color:#0000;border:none;grid-column:2/span 1;grid-row-start:1;height:100%;padding:.125rem;transition:opacity .2s,rotate .4s}.toggle>:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.toggle>:focus{outline-offset:2px;outline:2px solid #0000}}.toggle>:nth-child(2){color:var(--color-base-100);rotate:none}.toggle>:nth-child(3){color:var(--color-base-100);opacity:0;rotate:-15deg}.toggle:has(:checked)>:nth-child(2){opacity:0;rotate:15deg}.toggle:has(:checked)>:nth-child(3){opacity:1;rotate:none}.toggle:before{aspect-ratio:1;border-radius:var(--radius-selector);--tw-content:"";content:var(--tw-content);width:100%;height:100%;box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor;background-color:currentColor;grid-row-start:1;grid-column-start:2;transition:background-color .1s,translate .2s,inset-inline-start .2s;position:relative;inset-inline-start:0;translate:0}@supports (color:color-mix(in lab, red, red)){.toggle:before{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000)}}.toggle:before{background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise)}@media (forced-colors:active){.toggle:before{outline-style:var(--tw-outline-style);outline-offset:calc(1px * -1);outline-width:1px}}@media print{.toggle:before{outline-offset:-1rem;outline:.25rem solid}}.toggle:focus-visible,.toggle:has(:focus-visible){outline-offset:2px;outline:2px solid}.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked){background-color:var(--color-base-100);--input-color:var(--color-base-content);grid-template-columns:1fr 1fr 0fr}:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{background-color:currentColor}@starting-style{:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{opacity:0}}.toggle:indeterminate{grid-template-columns:.5fr 1fr .5fr}.toggle:disabled{cursor:not-allowed;opacity:.3}.toggle:disabled:before{border:var(--border) solid currentColor;background-color:#0000}.input{cursor:text;border:var(--border) solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;white-space:nowrap;width:clamp(3rem,20rem,100%);height:var(--size);font-size:max(var(--font-size,.875rem), .875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.5rem;padding-inline:.75rem;display:inline-flex;position:relative}@supports (color:color-mix(in lab, red, red)){.input{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.input{--size:calc(var(--size-field,.25rem) * 10);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.input:where(input){display:inline-flex}.input :where(input){appearance:none;background-color:#0000;border:none;width:100%;height:100%;display:inline-flex}.input :where(input):focus,.input :where(input):focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.input :where(input):focus,.input :where(input):focus-within{outline-offset:2px;outline:2px solid #0000}}.input :where(input[type=url]),.input :where(input[type=email]){direction:ltr}.input :where(input[type=date]){display:inline-flex}.input:focus,.input:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.input:focus,.input:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.input:focus,.input:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.input:focus,.input:focus-within{--font-size:1rem}}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{box-shadow:none}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.input[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input::-webkit-calendar-picker-indicator{position:absolute;inset-inline-end:.75em}.input:has(>input[type=date]) :where(input[type=date]){webkit-appearance:none;appearance:none;display:inline-flex}.input:has(>input[type=date]) input[type=date]::-webkit-calendar-picker-indicator{cursor:pointer;width:1em;height:1em;position:absolute;inset-inline-end:.75em}.table{border-collapse:separate;--tw-border-spacing-x:calc(.25rem * 0);--tw-border-spacing-y:calc(.25rem * 0);width:100%;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y);border-radius:var(--radius-box);text-align:left;font-size:.875rem;position:relative}.table:where(:dir(rtl),[dir=rtl],[dir=rtl] *){text-align:right}@media (hover:hover){:is(.table tr.row-hover,.table tr.row-hover:nth-child(2n)):hover{background-color:var(--color-base-200)}}.table :where(th,td){vertical-align:middle;padding-block:.75rem;padding-inline:1rem}.table :where(thead,tfoot){white-space:nowrap;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead,tfoot){color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.table :where(thead,tfoot){font-size:.875rem;font-weight:600}.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)}}.table :where(.table-pin-rows thead tr){z-index:1;background-color:var(--color-base-100);position:sticky;top:0}.table :where(.table-pin-rows tfoot tr){z-index:1;background-color:var(--color-base-100);position:sticky;bottom:0}.table :where(.table-pin-cols tr th){background-color:var(--color-base-100);position:sticky;left:0;right:0}.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)}}.steps{counter-reset:step;grid-auto-columns:1fr;grid-auto-flow:column;display:inline-grid;overflow:auto hidden}.steps .step{text-align:center;--step-bg:var(--color-base-300);--step-fg:var(--color-base-content);grid-template-rows:40px 1fr;grid-template-columns:auto;place-items:center;min-width:4rem;display:grid}.steps .step:before{width:100%;height:.5rem;color:var(--step-bg);background-color:var(--step-bg);content:"";border:1px solid;grid-row-start:1;grid-column-start:1;margin-inline-start:-100%;top:0}.steps .step>.step-icon,.steps .step:not(:has(.step-icon)):after{--tw-content:counter(step);content:var(--tw-content);counter-increment:step;z-index:1;color:var(--step-fg);background-color:var(--step-bg);border:1px solid var(--step-bg);border-radius:3.40282e38px;grid-row-start:1;grid-column-start:1;place-self:center;place-items:center;width:2rem;height:2rem;display:grid;position:relative}.steps .step:first-child:before{--tw-content:none;content:var(--tw-content)}.steps .step[data-content]:after{--tw-content:attr(data-content);content:var(--tw-content)}.range{appearance:none;webkit-appearance:none;--range-thumb:var(--color-base-100);--range-thumb-size:calc(var(--size-selector,.25rem) * 6);--range-progress:currentColor;--range-fill:1;--range-p:.25rem;--range-bg:currentColor}@supports (color:color-mix(in lab, red, red)){.range{--range-bg:color-mix(in oklab, currentColor 10%, #0000)}}.range{cursor:pointer;vertical-align:middle;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));width:clamp(3rem,20rem,100%);height:var(--range-thumb-size);background-color:#0000;border:none;overflow:hidden}[dir=rtl] .range{--range-dir:-1}.range:focus{outline:none}.range:focus-visible{outline-offset:2px;outline:2px solid}.range::-webkit-slider-runnable-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size) * .5)}@media (forced-colors:active){.range::-webkit-slider-runnable-track{border:1px solid}.range::-moz-range-track{border:1px solid}}.range::-webkit-slider-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));background-color:var(--range-thumb);height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p) solid;appearance:none;webkit-appearance:none;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill));position:relative;top:50%;transform:translateY(-50%)}@supports (color:color-mix(in lab, red, red)){.range::-webkit-slider-thumb{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill))}}.range::-moz-range-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size) * .5)}.range::-moz-range-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p) solid;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill));background-color:currentColor;position:relative;top:50%}@supports (color:color-mix(in lab, red, red)){.range::-moz-range-thumb{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill))}}.range:disabled{cursor:not-allowed;opacity:.3}.select{border:var(--border) solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);height:var(--size);touch-action:manipulation;white-space:nowrap;text-overflow:ellipsis;box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;background-image:linear-gradient(45deg,#0000 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,#0000 50%);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-repeat:no-repeat;background-size:4px 4px,4px 4px;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.375rem;padding-inline:.75rem 1.75rem;font-size:.875rem;display:inline-flex;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.select{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.select{border-color:var(--input-color);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.select{--size:calc(var(--size-field,.25rem) * 10)}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}[dir=rtl] .select::picker(select){translate:.5rem}[dir=rtl] .select select::picker(select){translate:.5rem}.select[multiple]{background-image:none;height:auto;padding-block:.75rem;padding-inline-end:.75rem;overflow:auto}.select select{appearance:none;width:calc(100% + 2.75rem);height:calc(100% - calc(var(--border) * 2));background:inherit;border-radius:inherit;border-style:none;align-items:center;margin-inline:-.75rem -1.75rem;padding-inline:.75rem 1.75rem}.select select:focus,.select select:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.select select:focus,.select select:focus-within{outline-offset:2px;outline:2px solid #0000}}.select select:not(:last-child){background-image:none;margin-inline-end:-1.375rem}.select:focus,.select:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.select:focus,.select:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.select:focus,.select:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.select:has(>select[disabled])>select[disabled]{cursor:not-allowed}@supports (appearance:base-select){.select,.select select{appearance:base-select}:is(.select,.select select)::picker(select){appearance:base-select}}:is(.select,.select select)::picker(select){color:inherit;border:var(--border) solid var(--color-base-200);border-radius:var(--radius-box);background-color:inherit;max-height:min(24rem,70dvh);box-shadow:0 2px calc(var(--depth) * 3px) -2px oklch(0% 0 0/.2);box-shadow:0 20px 25px -5px rgb(0 0 0/calc(var(--depth) * .1)), 0 8px 10px -6px rgb(0 0 0/calc(var(--depth) * .1));margin-block:.5rem;margin-inline:.5rem;padding:.5rem;translate:-.5rem}:is(.select,.select select)::picker-icon{display:none}:is(.select,.select select) optgroup{padding-top:.5em}:is(.select,.select select) optgroup option:first-child{margin-top:.5em}:is(.select,.select select) option{border-radius:var(--radius-field);white-space:normal;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{outline-offset:2px;outline:2px solid #0000}}:is(.select,.select select) option:not(:disabled):active{background-color:var(--color-neutral);color:var(--color-neutral-content);box-shadow:0 2px calc(var(--depth) * 3px) -2px var(--color-neutral)}.swap{cursor:pointer;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;place-content:center;display:inline-grid;position:relative}.swap input{appearance:none;border:none}.swap>*{grid-row-start:1;grid-column-start:1}@media (prefers-reduced-motion:no-preference){.swap>*{transition-property:transform,rotate,opacity;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.swap .swap-on,.swap .swap-indeterminate,.swap input:indeterminate~.swap-on,.swap input:is(:checked,:indeterminate)~.swap-off{opacity:0}.swap input:checked~.swap-on,.swap input:indeterminate~.swap-indeterminate{opacity:1;backface-visibility:visible}.checkbox{border:var(--border) solid var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox{border:var(--border) solid var(--input-color,color-mix(in oklab, var(--color-base-content) 20%, #0000))}}.checkbox{cursor:pointer;appearance:none;border-radius:var(--radius-selector);vertical-align:middle;color:var(--color-base-content);box-shadow:0 1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 0 #0000 inset, 0 0 #0000;--size:calc(var(--size-selector,.25rem) * 6);width:var(--size);height:var(--size);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);flex-shrink:0;padding:.25rem;transition:background-color .2s,box-shadow .2s;display:inline-block;position:relative}.checkbox:before{--tw-content:"";content:var(--tw-content);opacity:0;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,70% 80%,70% 100%);width:100%;height:100%;box-shadow:0px 3px 0 0px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;background-color:currentColor;font-size:1rem;line-height:.75;transition:clip-path .3s .1s,opacity .1s .1s,rotate .3s .1s,translate .3s .1s;display:block;rotate:45deg}.checkbox:focus-visible{outline:2px solid var(--input-color,currentColor);outline-offset:2px}.checkbox:checked,.checkbox[aria-checked=true]{background-color:var(--input-color,#0000);box-shadow:0 0 #0000 inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * .1))}:is(.checkbox:checked,.checkbox[aria-checked=true]):before{clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 0%,70% 0%,70% 100%);opacity:1}@media (forced-colors:active){:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:none}}@media print{:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:none}}.checkbox:indeterminate{background-color:var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox:indeterminate{background-color:var(--input-color,color-mix(in oklab, var(--color-base-content) 20%, #0000))}}.checkbox:indeterminate:before{opacity:1;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,80% 80%,80% 100%);translate:0 -35%;rotate:none}.radio{cursor:pointer;appearance:none;vertical-align:middle;border:var(--border) solid var(--input-color,currentColor);border-radius:3.40282e38px;flex-shrink:0;padding:.25rem;display:inline-block;position:relative}@supports (color:color-mix(in lab, red, red)){.radio{border:var(--border) solid var(--input-color,color-mix(in srgb, currentColor 20%, #0000))}}.radio{box-shadow:0 1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset;--size:calc(var(--size-selector,.25rem) * 6);width:var(--size);height:var(--size);color:var(--input-color,currentColor)}.radio:before{--tw-content:"";content:var(--tw-content);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);border-radius:3.40282e38px;width:100%;height:100%;display:block}.radio:focus-visible{outline:2px solid}.radio:checked,.radio[aria-checked=true]{background-color:var(--color-base-100);border-color:currentColor}@media (prefers-reduced-motion:no-preference){.radio:checked,.radio[aria-checked=true]{animation:.2s ease-out radio}}:is(.radio:checked,.radio[aria-checked=true]):before{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * .1));background-color:currentColor}@media (forced-colors:active){:is(.radio:checked,.radio[aria-checked=true]):before{outline-style:var(--tw-outline-style);outline-offset:calc(1px * -1);outline-width:1px}}@media print{:is(.radio:checked,.radio[aria-checked=true]):before{outline-offset:-1rem;outline:.25rem solid}}.navbar{align-items:center;width:100%;min-height:4rem;padding:.5rem;display:flex}.card{border-radius:var(--radius-box);outline-offset:2px;outline:0 solid #0000;flex-direction:column;transition:outline .2s ease-in-out;display:flex;position:relative}.card:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.card:focus{outline-offset:2px;outline:2px solid #0000}}.card:focus-visible{outline-color:currentColor}.card :where(figure:first-child){border-start-start-radius:inherit;border-start-end-radius:inherit;border-end-end-radius:unset;border-end-start-radius:unset;overflow:hidden}.card :where(figure:last-child){border-start-start-radius:unset;border-start-end-radius:unset;border-end-end-radius:inherit;border-end-start-radius:inherit;overflow:hidden}.card figure{justify-content:center;align-items:center;display:flex}.card:has(>input:is(input[type=checkbox],input[type=radio])){cursor:pointer;-webkit-user-select:none;user-select:none}.card:has(>:checked){outline:2px solid}.stats{border-radius:var(--radius-box);grid-auto-flow:column;display:inline-grid;position:relative;overflow-x:auto}.progress{appearance:none;border-radius:var(--radius-box);background-color:currentColor;width:100%;height:.5rem;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.progress{background-color:color-mix(in oklab, currentcolor 20%, transparent)}}.progress{color:var(--color-base-content)}.progress:indeterminate{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%}@media (prefers-reduced-motion:no-preference){.progress:indeterminate{animation:5s ease-in-out infinite progress}}@supports ((-moz-appearance:none)){.progress:indeterminate::-moz-progress-bar{background-color:#0000}@media (prefers-reduced-motion:no-preference){.progress:indeterminate::-moz-progress-bar{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%;animation:5s ease-in-out infinite progress}}.progress::-moz-progress-bar{border-radius:var(--radius-box);background-color:currentColor}}@supports ((-webkit-appearance:none)){.progress::-webkit-progress-bar{border-radius:var(--radius-box);background-color:#0000}.progress::-webkit-progress-value{border-radius:var(--radius-box);background-color:currentColor}}.textarea{border:var(--border) solid #0000;appearance:none;border-radius:var(--radius-field);background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);min-height:5rem;font-size:max(var(--font-size,.875rem), .875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;flex-shrink:1;padding-block:.5rem;padding-inline:.75rem}@supports (color:color-mix(in lab, red, red)){.textarea{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.textarea{--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.textarea textarea{appearance:none;background-color:#0000;border:none}.textarea textarea:focus,.textarea textarea:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.textarea textarea:focus,.textarea textarea:focus-within{outline-offset:2px;outline:2px solid #0000}}.textarea:focus,.textarea:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.textarea:focus,.textarea:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.textarea:focus,.textarea:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.textarea:focus,.textarea:focus-within{--font-size:1rem}}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){box-shadow:none}.textarea:has(>textarea[disabled])>textarea[disabled]{cursor:not-allowed}.divider{white-space:nowrap;height:1rem;margin:var(--divider-m,1rem 0);--divider-color:var(--color-base-content);flex-direction:row;align-self:stretch;align-items:center;display:flex}@supports (color:color-mix(in lab, red, red)){.divider{--divider-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.divider:before,.divider:after{content:"";background-color:var(--divider-color);flex-grow:1;width:100%;height:.125rem}@media print{.divider:before,.divider:after{border:.5px solid}}.divider:not(:empty){gap:1rem}.filter{flex-wrap:wrap;display:flex}.filter input[type=radio]{width:auto}.filter input{opacity:1;transition:margin .1s,opacity .3s,padding .3s,border-width .1s;overflow:hidden;scale:1}.filter input:not(:last-child){margin-inline-end:.25rem}.filter input.filter-reset{aspect-ratio:1}.filter input.filter-reset:after{--tw-content:"×";content:var(--tw-content)}.filter:not(:has(input:checked:not(.filter-reset))) .filter-reset,.filter:not(:has(input:checked:not(.filter-reset))) input[type=reset],.filter:has(input:checked:not(.filter-reset)) input:not(:checked,.filter-reset,input[type=reset]){opacity:0;border-width:0;width:0;margin-inline:0;padding-inline:0;scale:0}.label{white-space:nowrap;color:currentColor;align-items:center;gap:.375rem;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.label{color:color-mix(in oklab, currentcolor 60%, transparent)}}.label:has(input){cursor:pointer}.label:is(.input>*,.select>*){white-space:nowrap;height:calc(100% - .5rem);font-size:inherit;align-items:center;padding-inline:.75rem;display:flex}.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border) solid currentColor;margin-inline:-.75rem .75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border) solid color-mix(in oklab, currentColor 10%, #0000)}}.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border) solid currentColor;margin-inline:.75rem -.75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border) solid color-mix(in oklab, currentColor 10%, #0000)}}.fieldset-legend{color:var(--color-base-content);justify-content:space-between;align-items:center;gap:.5rem;margin-bottom:-.25rem;padding-block:.5rem;font-weight:600;display:flex}.status{aspect-ratio:1;border-radius:var(--radius-selector);background-color:var(--color-base-content);width:.5rem;height:.5rem;display:inline-block}@supports (color:color-mix(in lab, red, red)){.status{background-color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.status{vertical-align:middle;color:#0000004d;background-position:50%;background-repeat:no-repeat}@supports (color:color-mix(in lab, red, red)){.status{color:color-mix(in oklab, var(--color-black) 30%, transparent)}}.status{background-image:radial-gradient(circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * .5)), #0000);box-shadow:0 2px 3px -1px}@supports (color:color-mix(in lab, red, red)){.status{box-shadow:0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), #0000)}}.badge{border-radius:var(--radius-selector);vertical-align:middle;color:var(--badge-fg);border:var(--border) solid var(--badge-color,var(--color-base-200));background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);background-color:var(--badge-bg);--badge-bg:var(--badge-color,var(--color-base-100));--badge-fg:var(--color-base-content);--size:calc(var(--size-selector,.25rem) * 6);width:fit-content;height:var(--size);padding-inline:calc(var(--size) / 2 - var(--border));justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;display:inline-flex}.footer{grid-auto-flow:row;place-items:start;gap:2.5rem 1rem;width:100%;font-size:.875rem;line-height:1.25rem;display:grid}.footer>*{place-items:start;gap:.5rem;display:grid}.footer.footer-center{text-align:center;grid-auto-flow:column dense;place-items:center}.footer.footer-center>*{place-items:center}.navbar-end{justify-content:flex-end;align-items:center;width:50%;display:inline-flex}.navbar-start{justify-content:flex-start;align-items:center;width:50%;display:inline-flex}.card-body{padding:var(--card-p,1.5rem);font-size:var(--card-fs,.875rem);flex-direction:column;flex:auto;gap:.5rem;display:flex}.card-body :where(p){flex-grow:1}.navbar-center{flex-shrink:0;align-items:center;display:inline-flex}.fieldset-label{color:var(--color-base-content);align-items:center;gap:.375rem;display:flex}@supports (color:color-mix(in lab, red, red)){.fieldset-label{color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.fieldset-label:has(input){cursor:pointer}.alert{--alert-border-color:var(--color-base-200);border-radius:var(--radius-box);color:var(--color-base-content);background-color:var(--alert-color,var(--color-base-200));text-align:start;background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);box-shadow:0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * .08)) inset, 0 1px #000, 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * .08));border-style:solid;grid-template-columns:auto;grid-auto-flow:column;justify-content:start;place-items:center start;gap:1rem;padding-block:.75rem;padding-inline:1rem;font-size:.875rem;line-height:1.25rem;display:grid}@supports (color:color-mix(in lab, red, red)){.alert{box-shadow:0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * .08)) inset, 0 1px color-mix(in oklab, color-mix(in oklab, #000 20%, var(--alert-color,var(--color-base-200))) calc(var(--depth) * 20%), #0000), 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * .08))}}.alert:has(:nth-child(2)){grid-template-columns:auto minmax(auto,1fr)}.fieldset{grid-template-columns:1fr;grid-auto-rows:max-content;gap:.375rem;padding-block:.25rem;font-size:.75rem;display:grid}.mask{vertical-align:middle;display:inline-block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.link{cursor:pointer;text-decoration-line:underline}.link:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.link:focus{outline-offset:2px;outline:2px solid #0000}}.link:focus-visible{outline-offset:2px;outline:2px solid}.btn-error{--btn-color:var(--color-error);--btn-fg:var(--color-error-content)}.btn-primary{--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content)}}@layer daisyui.l1.l2{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{pointer-events:auto;visibility:visible;opacity:1;transition:visibility 0s allow-discrete, background-color .3s ease-out, opacity .1s ease-out;background-color:oklch(0% 0 0/.4)}:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal) .modal-box{opacity:1;translate:0;scale:1}:root:has(:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal)){--page-has-backdrop:1;--page-overflow:hidden;--page-scroll-bg:var(--page-scroll-bg-on);--page-scroll-gutter:stable;--page-scroll-transition:var(--page-scroll-transition-on);animation:forwards set-page-has-scroll;animation-timeline:scroll()}@starting-style{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{opacity:0}}.tooltip>.tooltip-content,.tooltip[data-tip]:before{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-off) 50%}.tooltip:after{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-tail) 50%}.btn:disabled:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.btn:disabled:not(.btn-link,.btn-ghost){box-shadow:none}.btn:disabled{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled{--btn-fg:color-mix(in oklch, var(--color-base-content) 20%, #0000)}}.btn[disabled]:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.btn[disabled]:not(.btn-link,.btn-ghost){box-shadow:none}.btn[disabled]{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]{--btn-fg:color-mix(in oklch, var(--color-base-content) 20%, #0000)}}@media (prefers-reduced-motion:no-preference){.collapse[open].collapse-arrow>.collapse-title:after,.collapse.collapse-open.collapse-arrow>.collapse-title:after{transform:translateY(-50%)rotate(225deg)}}.collapse.collapse-open.collapse-plus>.collapse-title:after{--tw-content:"−";content:var(--tw-content)}:is(.collapse[tabindex].collapse-arrow:focus:not(.collapse-close),.collapse.collapse-arrow[tabindex]:focus-within:not(.collapse-close))>.collapse-title:after,.collapse.collapse-arrow:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{transform:translateY(-50%)rotate(225deg)}.collapse[open].collapse-plus>.collapse-title:after,.collapse[tabindex].collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse.collapse-plus:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{--tw-content:"−";content:var(--tw-content)}.list .list-row:has(.list-col-grow:first-child){--list-grid-cols:1fr}.list .list-row:has(.list-col-grow:nth-child(2)){--list-grid-cols:minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(3)){--list-grid-cols:minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(4)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(5)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(6)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row>*{grid-row-start:1}.steps .step-neutral+.step-neutral:before,.steps .step-neutral:after,.steps .step-neutral>.step-icon{--step-bg:var(--color-neutral);--step-fg:var(--color-neutral-content)}.steps .step-primary+.step-primary:before,.steps .step-primary:after,.steps .step-primary>.step-icon{--step-bg:var(--color-primary);--step-fg:var(--color-primary-content)}.steps .step-secondary+.step-secondary:before,.steps .step-secondary:after,.steps .step-secondary>.step-icon{--step-bg:var(--color-secondary);--step-fg:var(--color-secondary-content)}.steps .step-accent+.step-accent:before,.steps .step-accent:after,.steps .step-accent>.step-icon{--step-bg:var(--color-accent);--step-fg:var(--color-accent-content)}.steps .step-info+.step-info:before,.steps .step-info:after,.steps .step-info>.step-icon{--step-bg:var(--color-info);--step-fg:var(--color-info-content)}.steps .step-success+.step-success:before,.steps .step-success:after,.steps .step-success>.step-icon{--step-bg:var(--color-success);--step-fg:var(--color-success-content)}.steps .step-warning+.step-warning:before,.steps .step-warning:after,.steps .step-warning>.step-icon{--step-bg:var(--color-warning);--step-fg:var(--color-warning-content)}.steps .step-error+.step-error:before,.steps .step-error:after,.steps .step-error>.step-icon{--step-bg:var(--color-error);--step-fg:var(--color-error-content)}.menu-vertical{flex-direction:column;display:inline-flex}.menu-vertical>li:not(.menu-title)>details>ul{background-color:revert-layer;border-radius:revert-layer;animation:revert-layer;box-shadow:revert-layer;margin-inline-start:1rem;margin-top:0;padding-block:0;padding-inline-end:0;transition:revert-layer;position:relative}.checkbox:disabled,.radio:disabled{cursor:not-allowed;opacity:.2}:where(.navbar){position:relative}.tooltip-top>.tooltip-content,.tooltip-top[data-tip]:before{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-off) 50%}.tooltip-top:after{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-tail) 50%}.input-lg{--size:calc(var(--size-field,.25rem) * 12);font-size:max(var(--font-size,1.125rem), 1.125rem)}.input-lg[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input-md{--size:calc(var(--size-field,.25rem) * 10);font-size:max(var(--font-size,.875rem), .875rem)}.input-md[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input-sm{--size:calc(var(--size-field,.25rem) * 8);font-size:max(var(--font-size,.75rem), .75rem)}.input-sm[type=number]::-webkit-inner-spin-button{margin-block:-.5rem;margin-inline-end:-.75rem}.input-xl{--size:calc(var(--size-field,.25rem) * 14);font-size:max(var(--font-size,1.375rem), 1.375rem)}.input-xl[type=number]::-webkit-inner-spin-button{margin-block:-1rem;margin-inline-end:-.75rem}.input-xs{--size:calc(var(--size-field,.25rem) * 6);font-size:max(var(--font-size,.6875rem), .6875rem)}.input-xs[type=number]::-webkit-inner-spin-button{margin-block:-.25rem;margin-inline-end:-.75rem}.badge-ghost{border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content);background-image:none}.badge-outline{color:var(--badge-color);--badge-bg:#0000;background-image:none;border-color:currentColor}.select-lg{--size:calc(var(--size-field,.25rem) * 12);font-size:1.125rem}.select-lg option{padding-block:.375rem;padding-inline:1rem}.select-sm{--size:calc(var(--size-field,.25rem) * 8);font-size:.75rem}.select-sm option{padding-block:.25rem;padding-inline:.625rem}.select-xl{--size:calc(var(--size-field,.25rem) * 14);font-size:1.375rem}.select-xl option{padding-block:.375rem;padding-inline:1.25rem}.select-xs{--size:calc(var(--size-field,.25rem) * 6);font-size:.6875rem}.select-xs option{padding-block:.25rem;padding-inline:.5rem}.table-sm :not(thead,tfoot) tr{font-size:.75rem}.table-sm :where(th,td){padding-block:.5rem;padding-inline:.75rem}.badge-sm{--size:calc(var(--size-selector,.25rem) * 5);font-size:.75rem}.textarea-lg{font-size:max(var(--font-size,1.125rem), 1.125rem)}.textarea-sm{font-size:max(var(--font-size,.75rem), .75rem)}.textarea-xl{font-size:max(var(--font-size,1.375rem), 1.375rem)}.textarea-xs{font-size:max(var(--font-size,.6875rem), .6875rem)}.alert-error{color:var(--color-error-content);--alert-border-color:var(--color-error);--alert-color:var(--color-error)}.link-primary{color:var(--color-primary)}@media (hover:hover){.link-primary:hover{color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.link-primary:hover{color:color-mix(in oklab, var(--color-primary) 80%, #000)}}}.btn-sm{--fontsize:.75rem;--btn-p:.75rem;--size:calc(var(--size-field,.25rem) * 8)}.btn-xs{--fontsize:.6875rem;--btn-p:.5rem;--size:calc(var(--size-field,.25rem) * 6)}.badge-error{--badge-color:var(--color-error);--badge-fg:var(--color-error-content)}.badge-info{--badge-color:var(--color-info);--badge-fg:var(--color-info-content)}.badge-secondary{--badge-color:var(--color-secondary);--badge-fg:var(--color-secondary-content)}.badge-success{--badge-color:var(--color-success);--badge-fg:var(--color-success-content)}.badge-warning{--badge-color:var(--color-warning);--badge-fg:var(--color-warning-content)}.range-lg{--range-thumb-size:calc(var(--size-selector,.25rem) * 7)}.range-sm{--range-thumb-size:calc(var(--size-selector,.25rem) * 5)}.range-xl{--range-thumb-size:calc(var(--size-selector,.25rem) * 8)}.range-xs{--range-thumb-size:calc(var(--size-selector,.25rem) * 4)}.textarea-error,.textarea-error:focus,.textarea-error:focus-within{--input-color:var(--color-error)}}.prose :where(a.btn:not(.btn-link)):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse:not(td,tr,colgroup){visibility:revert-layer}.validator:user-invalid~.validator-hint{display:revert-layer}.validator:has(:user-invalid)~.validator-hint{display:revert-layer}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))~.validator-hint{display:revert-layer}.collapse{visibility:collapse}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.right-3{right:calc(var(--spacing) * 3)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-2{margin:calc(var(--spacing) * 2)}.mx-auto{margin-inline:auto}.my-2{margin-block:calc(var(--spacing) * 2)}.mt-0\!{margin-top:calc(var(--spacing) * 0)!important}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-5{margin-right:calc(var(--spacing) * 5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.alert{border-width:var(--border);border-color:var(--alert-border-color,var(--color-base-200))}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-screen{height:100vh}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-full{min-height:100%}.w-4{width:calc(var(--spacing) * 4)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-7{max-width:calc(var(--spacing) * 7)}.max-w-60{max-width:calc(var(--spacing) * 60)}.max-w-lg{max-width:var(--container-lg)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-20{min-width:calc(var(--spacing) * 20)}.flex-1{flex:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-base-200{border-color:var(--color-base-200)}.border-base-300{border-color:var(--color-base-300)}.bg-base-100{background-color:var(--color-base-100)}.bg-base-200{background-color:var(--color-base-200)}.bg-gray-500\/50{background-color:#6a728280}@supports (color:color-mix(in lab, red, red)){.bg-gray-500\/50{background-color:color-mix(in oklab, var(--color-gray-500) 50%, transparent)}}.bg-primary,.bg-primary\/10{background-color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.p-0{padding:calc(var(--spacing) * 0)}.p-0\!{padding:calc(var(--spacing) * 0)!important}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-4{padding-block:calc(var(--spacing) * 4)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-pre-wrap{white-space:pre-wrap}.text-base-content,.text-base-content\/60{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/60{color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.text-error{color:var(--color-error)}.text-primary{color:var(--color-primary)}.text-primary-content{color:var(--color-primary-content)}.italic{font-style:italic}.opacity-70{opacity:.7}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@layer daisyui.l1{.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)){--btn-shadow:"";--btn-bg:#0000;--btn-border:#0000;--btn-noise:none}.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)):not(:disabled,[disabled],.btn-disabled){--btn-fg:var(--btn-color,currentColor);outline-color:currentColor}@media (hover:none){.btn-ghost:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color,currentColor);--btn-border:#0000;--btn-noise:none;outline-color:currentColor}}.btn-outline:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn),:disabled,[disabled],.btn-disabled){--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}@media (hover:none){.btn-outline:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:text-primary:hover{color:var(--color-primary)}}@media not all and (min-width:770px){.max-\[770px\]\:hidden{display:none}}}@keyframes rating{0%,40%{filter:brightness(1.05)contrast(1.05);scale:1.1}}@keyframes dropdown{0%{opacity:0}}@keyframes radio{0%{padding:5px}50%{padding:3px}}@keyframes toast{0%{opacity:0;scale:.9}to{opacity:1;scale:1}}@keyframes rotator{89.9999%,to{--first-item-position:0 0%}90%,99.9999%{--first-item-position:0 calc(var(--items) * 100%)}to{translate:0 -100%}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}@keyframes menu{0%{opacity:0}}@keyframes progress{50%{background-position-x:-115%}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} \ No newline at end of file +/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-gray-500:oklch(55.1% .027 264.364);--color-black:#000;--spacing:.25rem;--container-xs:20rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-lg:.5rem;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}@media (prefers-color-scheme:dark){:root:not([data-theme]){color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}}:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dark]:checked),[data-theme=dark]{color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=cupcake]:checked),[data-theme=cupcake]{color-scheme:light;--color-base-100:oklch(97.788% .004 56.375);--color-base-200:oklch(93.982% .007 61.449);--color-base-300:oklch(91.586% .006 53.44);--color-base-content:oklch(23.574% .066 313.189);--color-primary:oklch(85% .138 181.071);--color-primary-content:oklch(43% .078 188.216);--color-secondary:oklch(89% .061 343.231);--color-secondary-content:oklch(45% .187 3.815);--color-accent:oklch(90% .076 70.697);--color-accent-content:oklch(47% .157 37.304);--color-neutral:oklch(27% .006 286.033);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(68% .169 237.323);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(69% .17 162.48);--color-success-content:oklch(26% .051 172.552);--color-warning:oklch(79% .184 86.047);--color-warning-content:oklch(28% .066 53.813);--color-error:oklch(64% .246 16.439);--color-error-content:oklch(27% .105 12.094);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:0}:root:has(input.theme-controller[value=bumblebee]:checked),[data-theme=bumblebee]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(92% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(85% .199 91.936);--color-primary-content:oklch(42% .095 57.708);--color-secondary:oklch(75% .183 55.934);--color-secondary-content:oklch(40% .123 38.172);--color-accent:oklch(0% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(37% .01 67.558);--color-neutral-content:oklch(92% .003 48.717);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(39% .09 240.876);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(39% .141 25.723);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=emerald]:checked),[data-theme=emerald]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(35.519% .032 262.988);--color-primary:oklch(76.662% .135 153.45);--color-primary-content:oklch(33.387% .04 162.24);--color-secondary:oklch(61.302% .202 261.294);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(72.772% .149 33.2);--color-accent-content:oklch(0% 0 0);--color-neutral:oklch(35.519% .032 262.988);--color-neutral-content:oklch(98.462% .001 247.838);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=corporate]:checked),[data-theme=corporate]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(22.389% .031 278.072);--color-primary:oklch(58% .158 241.966);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(55% .046 257.417);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(60% .118 184.704);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(0% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(60% .126 221.723);--color-info-content:oklch(100% 0 0);--color-success:oklch(62% .194 149.214);--color-success-content:oklch(100% 0 0);--color-warning:oklch(85% .199 91.936);--color-warning-content:oklch(0% 0 0);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(0% 0 0);--radius-selector:.25rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=synthwave]:checked),[data-theme=synthwave]{color-scheme:dark;--color-base-100:oklch(15% .09 281.288);--color-base-200:oklch(20% .09 281.288);--color-base-300:oklch(25% .09 281.288);--color-base-content:oklch(78% .115 274.713);--color-primary:oklch(71% .202 349.761);--color-primary-content:oklch(28% .109 3.907);--color-secondary:oklch(82% .111 230.318);--color-secondary-content:oklch(29% .066 243.157);--color-accent:oklch(75% .183 55.934);--color-accent-content:oklch(26% .079 36.259);--color-neutral:oklch(45% .24 277.023);--color-neutral-content:oklch(87% .065 274.039);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(77% .152 181.912);--color-success-content:oklch(27% .046 192.524);--color-warning:oklch(90% .182 98.111);--color-warning-content:oklch(42% .095 57.708);--color-error:oklch(73.7% .121 32.639);--color-error-content:oklch(23.501% .096 290.329);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=retro]:checked),[data-theme=retro]{color-scheme:light;--color-base-100:oklch(91.637% .034 90.515);--color-base-200:oklch(88.272% .049 91.774);--color-base-300:oklch(84.133% .065 90.856);--color-base-content:oklch(41% .112 45.904);--color-primary:oklch(80% .114 19.571);--color-primary-content:oklch(39% .141 25.723);--color-secondary:oklch(92% .084 155.995);--color-secondary-content:oklch(44% .119 151.328);--color-accent:oklch(68% .162 75.834);--color-accent-content:oklch(41% .112 45.904);--color-neutral:oklch(44% .011 73.639);--color-neutral-content:oklch(86% .005 56.366);--color-info:oklch(58% .158 241.966);--color-info-content:oklch(96% .059 95.617);--color-success:oklch(51% .096 186.391);--color-success-content:oklch(96% .059 95.617);--color-warning:oklch(64% .222 41.116);--color-warning-content:oklch(96% .059 95.617);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(40% .123 38.172);--radius-selector:.25rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=cyberpunk]:checked),[data-theme=cyberpunk]{color-scheme:light;--color-base-100:oklch(94.51% .179 104.32);--color-base-200:oklch(91.51% .179 104.32);--color-base-300:oklch(85.51% .179 104.32);--color-base-content:oklch(0% 0 0);--color-primary:oklch(74.22% .209 6.35);--color-primary-content:oklch(14.844% .041 6.35);--color-secondary:oklch(83.33% .184 204.72);--color-secondary-content:oklch(16.666% .036 204.72);--color-accent:oklch(71.86% .217 310.43);--color-accent-content:oklch(14.372% .043 310.43);--color-neutral:oklch(23.04% .065 269.31);--color-neutral-content:oklch(94.51% .179 104.32);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:0rem;--radius-field:0rem;--radius-box:0rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=valentine]:checked),[data-theme=valentine]{color-scheme:light;--color-base-100:oklch(97% .014 343.198);--color-base-200:oklch(94% .028 342.258);--color-base-300:oklch(89% .061 343.231);--color-base-content:oklch(52% .223 3.958);--color-primary:oklch(65% .241 354.308);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(62% .265 303.9);--color-secondary-content:oklch(97% .014 308.299);--color-accent:oklch(82% .111 230.318);--color-accent-content:oklch(39% .09 240.876);--color-neutral:oklch(40% .153 2.432);--color-neutral-content:oklch(89% .061 343.231);--color-info:oklch(86% .127 207.078);--color-info-content:oklch(44% .11 240.79);--color-success:oklch(84% .143 164.978);--color-success-content:oklch(43% .095 166.913);--color-warning:oklch(75% .183 55.934);--color-warning-content:oklch(26% .079 36.259);--color-error:oklch(63% .237 25.331);--color-error-content:oklch(97% .013 17.38);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=halloween]:checked),[data-theme=halloween]{color-scheme:dark;--color-base-100:oklch(21% .006 56.043);--color-base-200:oklch(14% .004 49.25);--color-base-300:oklch(0% 0 0);--color-base-content:oklch(84.955% 0 0);--color-primary:oklch(77.48% .204 60.62);--color-primary-content:oklch(19.693% .004 196.779);--color-secondary:oklch(45.98% .248 305.03);--color-secondary-content:oklch(89.196% .049 305.03);--color-accent:oklch(64.8% .223 136.073);--color-accent-content:oklch(0% 0 0);--color-neutral:oklch(24.371% .046 65.681);--color-neutral-content:oklch(84.874% .009 65.681);--color-info:oklch(54.615% .215 262.88);--color-info-content:oklch(90.923% .043 262.88);--color-success:oklch(62.705% .169 149.213);--color-success-content:oklch(12.541% .033 149.213);--color-warning:oklch(66.584% .157 58.318);--color-warning-content:oklch(13.316% .031 58.318);--color-error:oklch(65.72% .199 27.33);--color-error-content:oklch(13.144% .039 27.33);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=garden]:checked),[data-theme=garden]{color-scheme:light;--color-base-100:oklch(92.951% .002 17.197);--color-base-200:oklch(86.445% .002 17.197);--color-base-300:oklch(79.938% .001 17.197);--color-base-content:oklch(16.961% .001 17.32);--color-primary:oklch(62.45% .278 3.836);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(48.495% .11 355.095);--color-secondary-content:oklch(89.699% .022 355.095);--color-accent:oklch(56.273% .054 154.39);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(24.155% .049 89.07);--color-neutral-content:oklch(92.951% .002 17.197);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=forest]:checked),[data-theme=forest]{color-scheme:dark;--color-base-100:oklch(20.84% .008 17.911);--color-base-200:oklch(18.522% .007 17.911);--color-base-300:oklch(16.203% .007 17.911);--color-base-content:oklch(83.768% .001 17.911);--color-primary:oklch(68.628% .185 148.958);--color-primary-content:oklch(0% 0 0);--color-secondary:oklch(69.776% .135 168.327);--color-secondary-content:oklch(13.955% .027 168.327);--color-accent:oklch(70.628% .119 185.713);--color-accent-content:oklch(14.125% .023 185.713);--color-neutral:oklch(30.698% .039 171.364);--color-neutral-content:oklch(86.139% .007 171.364);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=aqua]:checked),[data-theme=aqua]{color-scheme:dark;--color-base-100:oklch(37% .146 265.522);--color-base-200:oklch(28% .091 267.935);--color-base-300:oklch(22% .091 267.935);--color-base-content:oklch(90% .058 230.902);--color-primary:oklch(85.661% .144 198.645);--color-primary-content:oklch(40.124% .068 197.603);--color-secondary:oklch(60.682% .108 309.782);--color-secondary-content:oklch(96% .016 293.756);--color-accent:oklch(93.426% .102 94.555);--color-accent-content:oklch(18.685% .02 94.555);--color-neutral:oklch(27% .146 265.522);--color-neutral-content:oklch(80% .146 265.522);--color-info:oklch(54.615% .215 262.88);--color-info-content:oklch(90.923% .043 262.88);--color-success:oklch(62.705% .169 149.213);--color-success-content:oklch(12.541% .033 149.213);--color-warning:oklch(66.584% .157 58.318);--color-warning-content:oklch(27% .077 45.635);--color-error:oklch(73.95% .19 27.33);--color-error-content:oklch(14.79% .038 27.33);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=lofi]:checked),[data-theme=lofi]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(94% 0 0);--color-base-content:oklch(0% 0 0);--color-primary:oklch(15.906% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(21.455% .001 17.278);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(26.861% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(0% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(79.54% .103 205.9);--color-info-content:oklch(15.908% .02 205.9);--color-success:oklch(90.13% .153 164.14);--color-success-content:oklch(18.026% .03 164.14);--color-warning:oklch(88.37% .135 79.94);--color-warning-content:oklch(17.674% .027 79.94);--color-error:oklch(78.66% .15 28.47);--color-error-content:oklch(15.732% .03 28.47);--radius-selector:2rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=pastel]:checked),[data-theme=pastel]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98.462% .001 247.838);--color-base-300:oklch(92.462% .001 247.838);--color-base-content:oklch(20% 0 0);--color-primary:oklch(90% .063 306.703);--color-primary-content:oklch(49% .265 301.924);--color-secondary:oklch(89% .058 10.001);--color-secondary-content:oklch(51% .222 16.935);--color-accent:oklch(90% .093 164.15);--color-accent-content:oklch(50% .118 165.612);--color-neutral:oklch(55% .046 257.417);--color-neutral-content:oklch(92% .013 255.508);--color-info:oklch(86% .127 207.078);--color-info-content:oklch(52% .105 223.128);--color-success:oklch(87% .15 154.449);--color-success-content:oklch(52% .154 150.069);--color-warning:oklch(83% .128 66.29);--color-warning-content:oklch(55% .195 38.402);--color-error:oklch(80% .114 19.571);--color-error-content:oklch(50% .213 27.518);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:0;--noise:0}:root:has(input.theme-controller[value=fantasy]:checked),[data-theme=fantasy]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(27.807% .029 256.847);--color-primary:oklch(37.45% .189 325.02);--color-primary-content:oklch(87.49% .037 325.02);--color-secondary:oklch(53.92% .162 241.36);--color-secondary-content:oklch(90.784% .032 241.36);--color-accent:oklch(75.98% .204 56.72);--color-accent-content:oklch(15.196% .04 56.72);--color-neutral:oklch(27.807% .029 256.847);--color-neutral-content:oklch(85.561% .005 256.847);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=wireframe]:checked),[data-theme=wireframe]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(94% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(87% 0 0);--color-primary-content:oklch(26% 0 0);--color-secondary:oklch(87% 0 0);--color-secondary-content:oklch(26% 0 0);--color-accent:oklch(87% 0 0);--color-accent-content:oklch(26% 0 0);--color-neutral:oklch(87% 0 0);--color-neutral-content:oklch(26% 0 0);--color-info:oklch(44% .11 240.79);--color-info-content:oklch(90% .058 230.902);--color-success:oklch(43% .095 166.913);--color-success-content:oklch(90% .093 164.15);--color-warning:oklch(47% .137 46.201);--color-warning-content:oklch(92% .12 95.746);--color-error:oklch(44% .177 26.899);--color-error-content:oklch(88% .062 18.334);--radius-selector:0rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=black]:checked),[data-theme=black]{color-scheme:dark;--color-base-100:oklch(0% 0 0);--color-base-200:oklch(19% 0 0);--color-base-300:oklch(22% 0 0);--color-base-content:oklch(87.609% 0 0);--color-primary:oklch(35% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(35% 0 0);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(35% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(35% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(45.201% .313 264.052);--color-info-content:oklch(89.04% .062 264.052);--color-success:oklch(51.975% .176 142.495);--color-success-content:oklch(90.395% .035 142.495);--color-warning:oklch(96.798% .211 109.769);--color-warning-content:oklch(19.359% .042 109.769);--color-error:oklch(62.795% .257 29.233);--color-error-content:oklch(12.559% .051 29.233);--radius-selector:0rem;--radius-field:0rem;--radius-box:0rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=luxury]:checked),[data-theme=luxury]{color-scheme:dark;--color-base-100:oklch(14.076% .004 285.822);--color-base-200:oklch(20.219% .004 308.229);--color-base-300:oklch(23.219% .004 308.229);--color-base-content:oklch(75.687% .123 76.89);--color-primary:oklch(100% 0 0);--color-primary-content:oklch(20% 0 0);--color-secondary:oklch(27.581% .064 261.069);--color-secondary-content:oklch(85.516% .012 261.069);--color-accent:oklch(36.674% .051 338.825);--color-accent-content:oklch(87.334% .01 338.825);--color-neutral:oklch(24.27% .057 59.825);--color-neutral-content:oklch(93.203% .089 90.861);--color-info:oklch(79.061% .121 237.133);--color-info-content:oklch(15.812% .024 237.133);--color-success:oklch(78.119% .192 132.154);--color-success-content:oklch(15.623% .038 132.154);--color-warning:oklch(86.127% .136 102.891);--color-warning-content:oklch(17.225% .027 102.891);--color-error:oklch(71.753% .176 22.568);--color-error-content:oklch(14.35% .035 22.568);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dracula]:checked),[data-theme=dracula]{color-scheme:dark;--color-base-100:oklch(28.822% .022 277.508);--color-base-200:oklch(26.805% .02 277.508);--color-base-300:oklch(24.787% .019 277.508);--color-base-content:oklch(97.747% .007 106.545);--color-primary:oklch(75.461% .183 346.812);--color-primary-content:oklch(15.092% .036 346.812);--color-secondary:oklch(74.202% .148 301.883);--color-secondary-content:oklch(14.84% .029 301.883);--color-accent:oklch(83.392% .124 66.558);--color-accent-content:oklch(16.678% .024 66.558);--color-neutral:oklch(39.445% .032 275.524);--color-neutral-content:oklch(87.889% .006 275.524);--color-info:oklch(88.263% .093 212.846);--color-info-content:oklch(17.652% .018 212.846);--color-success:oklch(87.099% .219 148.024);--color-success-content:oklch(17.419% .043 148.024);--color-warning:oklch(95.533% .134 112.757);--color-warning-content:oklch(19.106% .026 112.757);--color-error:oklch(68.22% .206 24.43);--color-error-content:oklch(13.644% .041 24.43);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=cmyk]:checked),[data-theme=cmyk]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(95% 0 0);--color-base-300:oklch(90% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(71.772% .133 239.443);--color-primary-content:oklch(14.354% .026 239.443);--color-secondary:oklch(64.476% .202 359.339);--color-secondary-content:oklch(12.895% .04 359.339);--color-accent:oklch(94.228% .189 105.306);--color-accent-content:oklch(18.845% .037 105.306);--color-neutral:oklch(21.778% 0 0);--color-neutral-content:oklch(84.355% 0 0);--color-info:oklch(68.475% .094 217.284);--color-info-content:oklch(13.695% .018 217.284);--color-success:oklch(46.949% .162 321.406);--color-success-content:oklch(89.389% .032 321.406);--color-warning:oklch(71.236% .159 52.023);--color-warning-content:oklch(14.247% .031 52.023);--color-error:oklch(62.013% .208 28.717);--color-error-content:oklch(12.402% .041 28.717);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=autumn]:checked),[data-theme=autumn]{color-scheme:light;--color-base-100:oklch(95.814% 0 0);--color-base-200:oklch(89.107% 0 0);--color-base-300:oklch(82.4% 0 0);--color-base-content:oklch(19.162% 0 0);--color-primary:oklch(40.723% .161 17.53);--color-primary-content:oklch(88.144% .032 17.53);--color-secondary:oklch(61.676% .169 23.865);--color-secondary-content:oklch(12.335% .033 23.865);--color-accent:oklch(73.425% .094 60.729);--color-accent-content:oklch(14.685% .018 60.729);--color-neutral:oklch(54.367% .037 51.902);--color-neutral-content:oklch(90.873% .007 51.902);--color-info:oklch(69.224% .097 207.284);--color-info-content:oklch(13.844% .019 207.284);--color-success:oklch(60.995% .08 174.616);--color-success-content:oklch(12.199% .016 174.616);--color-warning:oklch(70.081% .164 56.844);--color-warning-content:oklch(14.016% .032 56.844);--color-error:oklch(53.07% .241 24.16);--color-error-content:oklch(90.614% .048 24.16);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=business]:checked),[data-theme=business]{color-scheme:dark;--color-base-100:oklch(24.353% 0 0);--color-base-200:oklch(22.648% 0 0);--color-base-300:oklch(20.944% 0 0);--color-base-content:oklch(84.87% 0 0);--color-primary:oklch(41.703% .099 251.473);--color-primary-content:oklch(88.34% .019 251.473);--color-secondary:oklch(64.092% .027 229.389);--color-secondary-content:oklch(12.818% .005 229.389);--color-accent:oklch(67.271% .167 35.791);--color-accent-content:oklch(13.454% .033 35.791);--color-neutral:oklch(27.441% .013 253.041);--color-neutral-content:oklch(85.488% .002 253.041);--color-info:oklch(62.616% .143 240.033);--color-info-content:oklch(12.523% .028 240.033);--color-success:oklch(70.226% .094 156.596);--color-success-content:oklch(14.045% .018 156.596);--color-warning:oklch(77.482% .115 81.519);--color-warning-content:oklch(15.496% .023 81.519);--color-error:oklch(51.61% .146 29.674);--color-error-content:oklch(90.322% .029 29.674);--radius-selector:0rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=acid]:checked),[data-theme=acid]{color-scheme:light;--color-base-100:oklch(98% 0 0);--color-base-200:oklch(95% 0 0);--color-base-300:oklch(91% 0 0);--color-base-content:oklch(0% 0 0);--color-primary:oklch(71.9% .357 330.759);--color-primary-content:oklch(14.38% .071 330.759);--color-secondary:oklch(73.37% .224 48.25);--color-secondary-content:oklch(14.674% .044 48.25);--color-accent:oklch(92.78% .264 122.962);--color-accent-content:oklch(18.556% .052 122.962);--color-neutral:oklch(21.31% .128 278.68);--color-neutral-content:oklch(84.262% .025 278.68);--color-info:oklch(60.72% .227 252.05);--color-info-content:oklch(12.144% .045 252.05);--color-success:oklch(85.72% .266 158.53);--color-success-content:oklch(17.144% .053 158.53);--color-warning:oklch(91.01% .212 100.5);--color-warning-content:oklch(18.202% .042 100.5);--color-error:oklch(64.84% .293 29.349);--color-error-content:oklch(12.968% .058 29.349);--radius-selector:1rem;--radius-field:1rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=lemonade]:checked),[data-theme=lemonade]{color-scheme:light;--color-base-100:oklch(98.71% .02 123.72);--color-base-200:oklch(91.8% .018 123.72);--color-base-300:oklch(84.89% .017 123.72);--color-base-content:oklch(19.742% .004 123.72);--color-primary:oklch(58.92% .199 134.6);--color-primary-content:oklch(11.784% .039 134.6);--color-secondary:oklch(77.75% .196 111.09);--color-secondary-content:oklch(15.55% .039 111.09);--color-accent:oklch(85.39% .201 100.73);--color-accent-content:oklch(17.078% .04 100.73);--color-neutral:oklch(30.98% .075 108.6);--color-neutral-content:oklch(86.196% .015 108.6);--color-info:oklch(86.19% .047 224.14);--color-info-content:oklch(17.238% .009 224.14);--color-success:oklch(86.19% .047 157.85);--color-success-content:oklch(17.238% .009 157.85);--color-warning:oklch(86.19% .047 102.15);--color-warning-content:oklch(17.238% .009 102.15);--color-error:oklch(86.19% .047 25.85);--color-error-content:oklch(17.238% .009 25.85);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=night]:checked),[data-theme=night]{color-scheme:dark;--color-base-100:oklch(20.768% .039 265.754);--color-base-200:oklch(19.314% .037 265.754);--color-base-300:oklch(17.86% .034 265.754);--color-base-content:oklch(84.153% .007 265.754);--color-primary:oklch(75.351% .138 232.661);--color-primary-content:oklch(15.07% .027 232.661);--color-secondary:oklch(68.011% .158 276.934);--color-secondary-content:oklch(13.602% .031 276.934);--color-accent:oklch(72.36% .176 350.048);--color-accent-content:oklch(14.472% .035 350.048);--color-neutral:oklch(27.949% .036 260.03);--color-neutral-content:oklch(85.589% .007 260.03);--color-info:oklch(68.455% .148 237.251);--color-info-content:oklch(0% 0 0);--color-success:oklch(78.452% .132 181.911);--color-success-content:oklch(15.69% .026 181.911);--color-warning:oklch(83.242% .139 82.95);--color-warning-content:oklch(16.648% .027 82.95);--color-error:oklch(71.785% .17 13.118);--color-error-content:oklch(14.357% .034 13.118);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=coffee]:checked),[data-theme=coffee]{color-scheme:dark;--color-base-100:oklch(24% .023 329.708);--color-base-200:oklch(21% .021 329.708);--color-base-300:oklch(16% .019 329.708);--color-base-content:oklch(72.354% .092 79.129);--color-primary:oklch(71.996% .123 62.756);--color-primary-content:oklch(14.399% .024 62.756);--color-secondary:oklch(34.465% .029 199.194);--color-secondary-content:oklch(86.893% .005 199.194);--color-accent:oklch(42.621% .074 224.389);--color-accent-content:oklch(88.524% .014 224.389);--color-neutral:oklch(16.51% .015 326.261);--color-neutral-content:oklch(83.302% .003 326.261);--color-info:oklch(79.49% .063 184.558);--color-info-content:oklch(15.898% .012 184.558);--color-success:oklch(74.722% .072 131.116);--color-success-content:oklch(14.944% .014 131.116);--color-warning:oklch(88.15% .14 87.722);--color-warning-content:oklch(17.63% .028 87.722);--color-error:oklch(77.318% .128 31.871);--color-error-content:oklch(15.463% .025 31.871);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=winter]:checked),[data-theme=winter]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97.466% .011 259.822);--color-base-300:oklch(93.268% .016 262.751);--color-base-content:oklch(41.886% .053 255.824);--color-primary:oklch(56.86% .255 257.57);--color-primary-content:oklch(91.372% .051 257.57);--color-secondary:oklch(42.551% .161 282.339);--color-secondary-content:oklch(88.51% .032 282.339);--color-accent:oklch(59.939% .191 335.171);--color-accent-content:oklch(11.988% .038 335.171);--color-neutral:oklch(19.616% .063 257.651);--color-neutral-content:oklch(83.923% .012 257.651);--color-info:oklch(88.127% .085 214.515);--color-info-content:oklch(17.625% .017 214.515);--color-success:oklch(80.494% .077 197.823);--color-success-content:oklch(16.098% .015 197.823);--color-warning:oklch(89.172% .045 71.47);--color-warning-content:oklch(17.834% .009 71.47);--color-error:oklch(73.092% .11 20.076);--color-error-content:oklch(14.618% .022 20.076);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=dim]:checked),[data-theme=dim]{color-scheme:dark;--color-base-100:oklch(30.857% .023 264.149);--color-base-200:oklch(28.036% .019 264.182);--color-base-300:oklch(26.346% .018 262.177);--color-base-content:oklch(82.901% .031 222.959);--color-primary:oklch(86.133% .141 139.549);--color-primary-content:oklch(17.226% .028 139.549);--color-secondary:oklch(73.375% .165 35.353);--color-secondary-content:oklch(14.675% .033 35.353);--color-accent:oklch(74.229% .133 311.379);--color-accent-content:oklch(14.845% .026 311.379);--color-neutral:oklch(24.731% .02 264.094);--color-neutral-content:oklch(82.901% .031 222.959);--color-info:oklch(86.078% .142 206.182);--color-info-content:oklch(17.215% .028 206.182);--color-success:oklch(86.171% .142 166.534);--color-success-content:oklch(17.234% .028 166.534);--color-warning:oklch(86.163% .142 94.818);--color-warning-content:oklch(17.232% .028 94.818);--color-error:oklch(82.418% .099 33.756);--color-error-content:oklch(16.483% .019 33.756);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=nord]:checked),[data-theme=nord]{color-scheme:light;--color-base-100:oklch(95.127% .007 260.731);--color-base-200:oklch(93.299% .01 261.788);--color-base-300:oklch(89.925% .016 262.749);--color-base-content:oklch(32.437% .022 264.182);--color-primary:oklch(59.435% .077 254.027);--color-primary-content:oklch(11.887% .015 254.027);--color-secondary:oklch(69.651% .059 248.687);--color-secondary-content:oklch(13.93% .011 248.687);--color-accent:oklch(77.464% .062 217.469);--color-accent-content:oklch(15.492% .012 217.469);--color-neutral:oklch(45.229% .035 264.131);--color-neutral-content:oklch(89.925% .016 262.749);--color-info:oklch(69.207% .062 332.664);--color-info-content:oklch(13.841% .012 332.664);--color-success:oklch(76.827% .074 131.063);--color-success-content:oklch(15.365% .014 131.063);--color-warning:oklch(85.486% .089 84.093);--color-warning-content:oklch(17.097% .017 84.093);--color-error:oklch(60.61% .12 15.341);--color-error-content:oklch(12.122% .024 15.341);--radius-selector:1rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=sunset]:checked),[data-theme=sunset]{color-scheme:dark;--color-base-100:oklch(22% .019 237.69);--color-base-200:oklch(20% .019 237.69);--color-base-300:oklch(18% .019 237.69);--color-base-content:oklch(77.383% .043 245.096);--color-primary:oklch(74.703% .158 39.947);--color-primary-content:oklch(14.94% .031 39.947);--color-secondary:oklch(72.537% .177 2.72);--color-secondary-content:oklch(14.507% .035 2.72);--color-accent:oklch(71.294% .166 299.844);--color-accent-content:oklch(14.258% .033 299.844);--color-neutral:oklch(26% .019 237.69);--color-neutral-content:oklch(70% .019 237.69);--color-info:oklch(85.559% .085 206.015);--color-info-content:oklch(17.111% .017 206.015);--color-success:oklch(85.56% .085 144.778);--color-success-content:oklch(17.112% .017 144.778);--color-warning:oklch(85.569% .084 74.427);--color-warning-content:oklch(17.113% .016 74.427);--color-error:oklch(85.511% .078 16.886);--color-error-content:oklch(17.102% .015 16.886);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=caramellatte]:checked),[data-theme=caramellatte]{color-scheme:light;--color-base-100:oklch(98% .016 73.684);--color-base-200:oklch(95% .038 75.164);--color-base-300:oklch(90% .076 70.697);--color-base-content:oklch(40% .123 38.172);--color-primary:oklch(0% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(22.45% .075 37.85);--color-secondary-content:oklch(90% .076 70.697);--color-accent:oklch(46.44% .111 37.85);--color-accent-content:oklch(90% .076 70.697);--color-neutral:oklch(55% .195 38.402);--color-neutral-content:oklch(98% .016 73.684);--color-info:oklch(42% .199 265.638);--color-info-content:oklch(90% .076 70.697);--color-success:oklch(43% .095 166.913);--color-success-content:oklch(90% .076 70.697);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(39% .141 25.723);--radius-selector:2rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:1}:root:has(input.theme-controller[value=abyss]:checked),[data-theme=abyss]{color-scheme:dark;--color-base-100:oklch(20% .08 209);--color-base-200:oklch(15% .08 209);--color-base-300:oklch(10% .08 209);--color-base-content:oklch(90% .076 70.697);--color-primary:oklch(92% .2653 125);--color-primary-content:oklch(50% .2653 125);--color-secondary:oklch(83.27% .0764 298.3);--color-secondary-content:oklch(43.27% .0764 298.3);--color-accent:oklch(43% 0 0);--color-accent-content:oklch(98% 0 0);--color-neutral:oklch(30% .08 209);--color-neutral-content:oklch(90% .076 70.697);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(79% .209 151.711);--color-success-content:oklch(26% .065 152.934);--color-warning:oklch(84.8% .1962 84.62);--color-warning-content:oklch(44.8% .1962 84.62);--color-error:oklch(65% .1985 24.22);--color-error-content:oklch(27% .1985 24.22);--radius-selector:2rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=silk]:checked),[data-theme=silk]{color-scheme:light;--color-base-100:oklch(97% .0035 67.78);--color-base-200:oklch(95% .0081 61.42);--color-base-300:oklch(90% .0081 61.42);--color-base-content:oklch(40% .0081 61.42);--color-primary:oklch(23.27% .0249 284.3);--color-primary-content:oklch(94.22% .2505 117.44);--color-secondary:oklch(23.27% .0249 284.3);--color-secondary-content:oklch(73.92% .2135 50.94);--color-accent:oklch(23.27% .0249 284.3);--color-accent-content:oklch(88.92% .2061 189.9);--color-neutral:oklch(20% 0 0);--color-neutral-content:oklch(80% .0081 61.42);--color-info:oklch(80.39% .1148 241.68);--color-info-content:oklch(30.39% .1148 241.68);--color-success:oklch(83.92% .0901 136.87);--color-success-content:oklch(23.92% .0901 136.87);--color-warning:oklch(83.92% .1085 80);--color-warning-content:oklch(43.92% .1085 80);--color-error:oklch(75.1% .1814 22.37);--color-error-content:oklch(35.1% .1814 22.37);--radius-selector:2rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:0}:root{--fx-noise:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E");scrollbar-color:currentColor #0000}@supports (color:color-mix(in lab, red, red)){:root{scrollbar-color:color-mix(in oklch, currentColor 35%, #0000) #0000}}@property --radialprogress{syntax:"";inherits:true;initial-value:0%}:root:not(span){overflow:var(--page-overflow)}:root{background:var(--page-scroll-bg,var(--root-bg));--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000), var(--root-bg,#0000)) var(--root-bg,#0000)}@supports (color:color-mix(in lab, red, red)){:root{--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000), var(--root-bg,#0000)) color-mix(in srgb, var(--root-bg,#0000), oklch(0% 0 0) calc(var(--page-has-backdrop,0) * 40%))}}:root{--page-scroll-transition-on:background-color .3s ease-out;transition:var(--page-scroll-transition);scrollbar-gutter:var(--page-scroll-gutter,unset);scrollbar-gutter:if(style(--page-has-scroll: 1): var(--page-scroll-gutter,unset) ; else: unset)}@keyframes set-page-has-scroll{0%,to{--page-has-scroll:1}}:root,[data-theme]{background:var(--page-scroll-bg,var(--root-bg));color:var(--color-base-content)}:where(:root,[data-theme]){--root-bg:var(--color-base-100)}}@layer components;@layer utilities{@layer daisyui.l1.l2.l3{.modal{pointer-events:none;visibility:hidden;width:100%;max-width:none;height:100%;max-height:none;color:inherit;transition:visibility .3s allow-discrete, background-color .3s ease-out, opacity .1s ease-out;overscroll-behavior:contain;z-index:999;scrollbar-gutter:auto;background-color:#0000;place-items:center;margin:0;padding:0;display:grid;position:fixed;inset:0;overflow:clip}.modal::backdrop{display:none}.tooltip{--tt-bg:var(--color-neutral);--tt-off:calc(100% + .5rem);--tt-tail:calc(100% + 1px + .25rem);display:inline-block;position:relative}.tooltip>.tooltip-content,.tooltip[data-tip]:before{border-radius:var(--radius-field);text-align:center;white-space:normal;max-width:20rem;color:var(--color-neutral-content);opacity:0;background-color:var(--tt-bg);pointer-events:none;z-index:2;--tw-content:attr(data-tip);content:var(--tw-content);width:max-content;padding-block:.25rem;padding-inline:.5rem;font-size:.875rem;line-height:1.25;position:absolute}.tooltip:after{opacity:0;background-color:var(--tt-bg);content:"";pointer-events:none;--mask-tooltip:url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A");width:.625rem;height:.25rem;-webkit-mask-position:-1px 0;mask-position:-1px 0;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-image:var(--mask-tooltip);-webkit-mask-image:var(--mask-tooltip);mask-image:var(--mask-tooltip);display:block;position:absolute}@media (prefers-reduced-motion:no-preference){.tooltip>.tooltip-content,.tooltip[data-tip]:before,.tooltip:after{transition:opacity .2s cubic-bezier(.4,0,.2,1) 75ms,transform .2s cubic-bezier(.4,0,.2,1) 75ms}}:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))>.tooltip-content,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))[data-tip]:before,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible)):after{opacity:1;--tt-pos:0rem}@media (prefers-reduced-motion:no-preference){:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))>.tooltip-content,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))[data-tip]:before,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible)):after{transition:opacity .2s cubic-bezier(.4,0,.2,1),transform .2s cubic-bezier(.4,0,.2,1)}}.tab{cursor:pointer;appearance:none;text-align:center;webkit-user-select:none;-webkit-user-select:none;user-select:none;flex-wrap:wrap;justify-content:center;align-items:center;display:inline-flex;position:relative}@media (hover:hover){.tab:hover{color:var(--color-base-content)}}.tab{--tab-p:.75rem;--tab-bg:var(--color-base-100);--tab-border-color:var(--color-base-300);--tab-radius-ss:0;--tab-radius-se:0;--tab-radius-es:0;--tab-radius-ee:0;--tab-order:0;--tab-radius-min:calc(.75rem - var(--border));--tab-radius-limit:min(var(--radius-field), var(--tab-radius-min));--tab-radius-grad:#0000 calc(69% - var(--border)), var(--tab-border-color) calc(69% - var(--border) + .25px), var(--tab-border-color) 69%, var(--tab-bg) calc(69% + .25px);order:var(--tab-order);height:var(--tab-height);padding-inline:var(--tab-p);border-color:#0000;font-size:.875rem}.tab:is(input[type=radio]){min-width:fit-content}.tab:is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.tab:is(label){position:relative}.tab:is(label) input{cursor:pointer;appearance:none;opacity:0;position:absolute;inset:0}:is(.tab:checked,.tab:is(label:has(:checked)),.tab:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]))+.tab-content{display:block}.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:color-mix(in oklab, var(--color-base-content) 50%, transparent)}}.tab:not(input):empty{cursor:default;flex-grow:1}.tab:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.tab:focus{outline-offset:2px;outline:2px solid #0000}}.tab:focus-visible,.tab:is(label:has(:checked:focus-visible)){outline-offset:-5px;outline:2px solid}.tab[disabled]{pointer-events:none;opacity:.4}.menu{--menu-active-fg:var(--color-neutral-content);--menu-active-bg:var(--color-neutral);flex-flow:column wrap;width:fit-content;padding:.5rem;font-size:.875rem;display:flex}.menu :where(li ul){white-space:nowrap;margin-inline-start:1rem;padding-inline-start:.5rem;position:relative}.menu :where(li ul):before{background-color:var(--color-base-content);opacity:.1;width:var(--border);content:"";inset-inline-start:0;position:absolute;top:.75rem;bottom:.75rem}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--radius-field);text-align:start;text-wrap:balance;-webkit-user-select:none;user-select:none;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;align-content:flex-start;align-items:center;gap:.5rem;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:grid}.menu :where(li>details>summary){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li>details>summary){outline-offset:2px;outline:2px solid #0000}}.menu :where(li>details>summary)::-webkit-details-marker{display:none}:is(.menu :where(li>details>summary),.menu :where(li>.menu-dropdown-toggle)):after{content:"";transform-origin:50%;pointer-events:none;justify-self:flex-end;width:.375rem;height:.375rem;transition-property:rotate,translate;transition-duration:.2s;display:block;translate:0 -1px;rotate:-135deg;box-shadow:inset 2px 2px}.menu details{interpolate-size:allow-keywords;overflow:hidden}.menu details::details-content{block-size:0}@media (prefers-reduced-motion:no-preference){.menu details::details-content{transition-behavior:allow-discrete;transition-property:block-size,content-visibility;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.menu details[open]::details-content{block-size:auto}.menu :where(li>details[open]>summary):after,.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after{translate:0 1px;rotate:45deg}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{color:var(--color-base-content);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){box-shadow:inset 0 1px oklch(0% 0 0/.01),inset 0 -1px oklch(100% 0 0/.01)}.menu :where(li:empty){background-color:var(--color-base-content);opacity:.1;height:1px;margin:.5rem 1rem}.menu :where(li){flex-flow:column wrap;flex-shrink:0;align-items:stretch;display:flex;position:relative}.menu :where(li) .badge{justify-self:flex-end}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{outline-offset:2px;outline:2px solid #0000}}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{color:var(--menu-active-fg);background-color:var(--menu-active-bg);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise)}:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):not(:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):active){box-shadow:0 2px calc(var(--depth) * 3px) -2px var(--menu-active-bg)}.menu :where(li).menu-disabled{pointer-events:none;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li).menu-disabled{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.menu .dropdown:focus-within .menu-dropdown-toggle:after{translate:0 1px;rotate:45deg}.menu .dropdown-content{margin-top:.5rem;padding:.5rem}.menu .dropdown-content:before{display:none}.dropdown{position-area:var(--anchor-v,bottom) var(--anchor-h,span-right);display:inline-block;position:relative}.dropdown>:not(:has(~[class*=dropdown-content])):focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.dropdown>:not(:has(~[class*=dropdown-content])):focus{outline-offset:2px;outline:2px solid #0000}}.dropdown .dropdown-content{position:absolute}.dropdown.dropdown-close .dropdown-content,.dropdown:not(details,.dropdown-open,.dropdown-hover:hover,:focus-within) .dropdown-content,.dropdown.dropdown-hover:not(:hover) [tabindex]:first-child:focus:not(:focus-visible)~.dropdown-content{transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover],.dropdown .dropdown-content{z-index:999}@media (prefers-reduced-motion:no-preference){.dropdown[popover],.dropdown .dropdown-content{transition-behavior:allow-discrete;transition-property:opacity,scale,display;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);animation:.2s dropdown}}@starting-style{.dropdown[popover],.dropdown .dropdown-content{opacity:0;scale:.95}}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within)>[tabindex]:first-child{pointer-events:none}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within) .dropdown-content,.dropdown:not(.dropdown-close).dropdown-hover:hover .dropdown-content{opacity:1;scale:1}.dropdown:is(details) summary::-webkit-details-marker{display:none}.dropdown:where([popover]){background:0 0}.dropdown[popover]{color:inherit;position:fixed}@supports not (position-area:bottom){.dropdown[popover]{margin:auto}.dropdown[popover].dropdown-close{transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover].dropdown-open:not(:popover-open){transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover]::backdrop{background-color:oklab(0% none none/.3)}}:is(.dropdown[popover].dropdown-close,.dropdown[popover]:not(.dropdown-open,:popover-open)){transform-origin:top;opacity:0;display:none;scale:.95}:where(.btn){width:unset}.btn{cursor:pointer;text-align:center;vertical-align:middle;outline-offset:2px;webkit-user-select:none;-webkit-user-select:none;user-select:none;padding-inline:var(--btn-p);color:var(--btn-fg);--tw-prose-links:var(--btn-fg);height:var(--size);font-size:var(--fontsize,.875rem);outline-color:var(--btn-color,var(--color-base-content));background-color:var(--btn-bg);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--btn-noise);border-width:var(--border);border-style:solid;border-color:var(--btn-border);text-shadow:0 .5px oklch(100% 0 0 / calc(var(--depth) * .15));touch-action:manipulation;box-shadow:0 .5px 0 .5px oklch(100% 0 0 / calc(var(--depth) * 6%)) inset, var(--btn-shadow);--size:calc(var(--size-field,.25rem) * 10);--btn-bg:var(--btn-color,var(--color-base-200));--btn-fg:var(--color-base-content);--btn-p:1rem;--btn-border:var(--btn-bg);border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-wrap:nowrap;flex-shrink:0;justify-content:center;align-items:center;gap:.375rem;font-weight:600;transition-property:color,background-color,border-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:inline-flex}@supports (color:color-mix(in lab, red, red)){.btn{--btn-border:color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%))}}.btn{--btn-shadow:0 3px 2px -2px var(--btn-bg), 0 4px 3px -2px var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn{--btn-shadow:0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000), 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000)}}.btn{--btn-noise:var(--fx-noise)}@media (hover:hover){.btn:hover{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:hover{--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}}.btn:focus-visible,.btn:has(:focus-visible){isolation:isolate;outline-width:2px;outline-style:solid}.btn:active:not(.btn-active){--btn-bg:var(--btn-color,var(--color-base-200));translate:0 .5px}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 5%)}}.btn:active:not(.btn-active){--btn-border:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-border:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}.btn:active:not(.btn-active){--btn-shadow:0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0)}.btn:is(input[type=checkbox],input[type=radio]){appearance:none}.btn:is(input[type=checkbox],input[type=radio])[aria-label]:after{--tw-content:attr(aria-label);content:var(--tw-content)}.btn:where(input:checked:not(.filter .btn)){--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content);isolation:isolate}.loading{pointer-events:none;aspect-ratio:1;vertical-align:middle;width:calc(var(--size-selector,.25rem) * 6);background-color:currentColor;display:inline-block;-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:100%;mask-size:100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.collapse{border-radius:var(--radius-box,1rem);isolation:isolate;grid-template-rows:max-content 0fr;grid-template-columns:minmax(0,1fr);width:100%;display:grid;position:relative;overflow:hidden}@media (prefers-reduced-motion:no-preference){.collapse{transition:grid-template-rows .2s}}.collapse>input:is([type=checkbox],[type=radio]){appearance:none;opacity:0;z-index:1;grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close)),.collapse:not(.collapse-close):has(>input:is([type=checkbox],[type=radio]):checked){grid-template-rows:max-content 1fr}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){content-visibility:visible;min-height:fit-content}@supports not (content-visibility:visible){.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){visibility:visible}}.collapse:focus-visible,.collapse:has(>input:is([type=checkbox],[type=radio]):focus-visible),.collapse:has(summary:focus-visible){outline-color:var(--color-base-content);outline-offset:2px;outline-width:2px;outline-style:solid}.collapse:not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-close)>input[type=radio]:not(:checked),.collapse:not(.collapse-close)>.collapse-title{cursor:pointer}:is(.collapse[tabindex]:focus:not(.collapse-close,.collapse[open]),.collapse[tabindex]:focus-within:not(.collapse-close,.collapse[open]))>.collapse-title{cursor:unset}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){padding-bottom:1rem}.collapse:is(details){width:100%}@media (prefers-reduced-motion:no-preference){.collapse:is(details)::details-content{transition:content-visibility .2s allow-discrete, visibility .2s allow-discrete, min-height .2s ease-out allow-discrete, padding .1s ease-out 20ms, background-color .2s ease-out, height .2s;interpolate-size:allow-keywords;height:0}.collapse:is(details):where([open])::details-content{height:auto}}.collapse:is(details) summary{display:block;position:relative}.collapse:is(details) summary::-webkit-details-marker{display:none}.collapse:is(details)>.collapse-content{content-visibility:visible}.collapse:is(details) summary{outline:none}.collapse-content{content-visibility:hidden;min-height:0;cursor:unset;grid-row-start:2;grid-column-start:1;padding-left:1rem;padding-right:1rem}@supports not (content-visibility:hidden){.collapse-content{visibility:hidden}}@media (prefers-reduced-motion:no-preference){.collapse-content{transition:content-visibility .2s allow-discrete, visibility .2s allow-discrete, min-height .2s ease-out allow-discrete, padding .1s ease-out 20ms, background-color .2s ease-out}}.validator-hint{visibility:hidden;margin-top:.5rem;font-size:.75rem}.validator:user-valid{--input-color:var(--color-success)}.validator:user-valid:focus{--input-color:var(--color-success)}.validator:user-valid:checked{--input-color:var(--color-success)}.validator:user-valid[aria-checked=true]{--input-color:var(--color-success)}.validator:user-valid:focus-within{--input-color:var(--color-success)}.validator:has(:user-valid){--input-color:var(--color-success)}.validator:has(:user-valid):focus{--input-color:var(--color-success)}.validator:has(:user-valid):checked{--input-color:var(--color-success)}.validator:has(:user-valid)[aria-checked=true]{--input-color:var(--color-success)}.validator:has(:user-valid):focus-within{--input-color:var(--color-success)}.validator:user-invalid{--input-color:var(--color-error)}.validator:user-invalid:focus{--input-color:var(--color-error)}.validator:user-invalid:checked{--input-color:var(--color-error)}.validator:user-invalid[aria-checked=true]{--input-color:var(--color-error)}.validator:user-invalid:focus-within{--input-color:var(--color-error)}.validator:user-invalid~.validator-hint{visibility:visible;color:var(--color-error)}.validator:has(:user-invalid){--input-color:var(--color-error)}.validator:has(:user-invalid):focus{--input-color:var(--color-error)}.validator:has(:user-invalid):checked{--input-color:var(--color-error)}.validator:has(:user-invalid)[aria-checked=true]{--input-color:var(--color-error)}.validator:has(:user-invalid):focus-within{--input-color:var(--color-error)}.validator:has(:user-invalid)~.validator-hint{visibility:visible;color:var(--color-error)}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))),:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):focus,:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):checked,:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))[aria-checked=true],:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):focus-within{--input-color:var(--color-error)}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))~.validator-hint{visibility:visible;color:var(--color-error)}.list{flex-direction:column;font-size:.875rem;display:flex}.list .list-row{--list-grid-cols:minmax(0, auto) 1fr;border-radius:var(--radius-box);word-break:break-word;grid-auto-flow:column;grid-template-columns:var(--list-grid-cols);gap:1rem;padding:1rem;display:grid;position:relative}:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{content:"";border-bottom:var(--border) solid;inset-inline:var(--radius-box);border-color:var(--color-base-content);position:absolute;bottom:0}@supports (color:color-mix(in lab, red, red)){:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{border-color:color-mix(in oklab, var(--color-base-content) 5%, transparent)}}.toast{translate:var(--toast-x,0) var(--toast-y,0);inset-inline:auto 1rem;background-color:#0000;flex-direction:column;gap:.5rem;width:max-content;max-width:calc(100vw - 2rem);display:flex;position:fixed;top:auto;bottom:1rem}@media (prefers-reduced-motion:no-preference){.toast>*{animation:.25s ease-out toast}}.toggle{border:var(--border) solid currentColor;color:var(--input-color);cursor:pointer;appearance:none;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)));padding:var(--toggle-p);flex-shrink:0;grid-template-columns:0fr 1fr 1fr;place-content:center;display:inline-grid;position:relative;box-shadow:inset 0 1px}@supports (color:color-mix(in lab, red, red)){.toggle{box-shadow:0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset}}.toggle{--input-color:var(--color-base-content);transition:color .3s,grid-template-columns .2s}@supports (color:color-mix(in lab, red, red)){.toggle{--input-color:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}.toggle{--toggle-p:calc(var(--size) * .125);--size:calc(var(--size-selector,.25rem) * 6);width:calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2);height:var(--size)}.toggle>*{z-index:1;cursor:pointer;appearance:none;background-color:#0000;border:none;grid-column:2/span 1;grid-row-start:1;height:100%;padding:.125rem;transition:opacity .2s,rotate .4s}.toggle>:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.toggle>:focus{outline-offset:2px;outline:2px solid #0000}}.toggle>:nth-child(2){color:var(--color-base-100);rotate:0deg}.toggle>:nth-child(3){color:var(--color-base-100);opacity:0;rotate:-15deg}.toggle:has(:checked)>:nth-child(2){opacity:0;rotate:15deg}.toggle:has(:checked)>:nth-child(3){opacity:1;rotate:0deg}.toggle:before{aspect-ratio:1;border-radius:var(--radius-selector);--tw-content:"";content:var(--tw-content);width:100%;height:100%;box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor;background-color:currentColor;grid-row-start:1;grid-column-start:2;transition:background-color .1s,translate .2s,inset-inline-start .2s;position:relative;inset-inline-start:0;translate:0}@supports (color:color-mix(in lab, red, red)){.toggle:before{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000)}}.toggle:before{background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise)}@media (forced-colors:active){.toggle:before{outline-style:var(--tw-outline-style);outline-offset:calc(1px * -1);outline-width:1px}}@media print{.toggle:before{outline-offset:-1rem;outline:.25rem solid}}.toggle:focus-visible,.toggle:has(:focus-visible){outline-offset:2px;outline:2px solid}.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked){background-color:var(--color-base-100);--input-color:var(--color-base-content);grid-template-columns:1fr 1fr 0fr}:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{background-color:currentColor}@starting-style{:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{opacity:0}}.toggle:indeterminate{grid-template-columns:.5fr 1fr .5fr}.toggle:disabled{cursor:not-allowed;opacity:.3}.toggle:disabled:before{border:var(--border) solid currentColor;background-color:#0000}.input{cursor:text;border:var(--border) solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;white-space:nowrap;width:clamp(3rem,20rem,100%);height:var(--size);font-size:max(var(--font-size,.875rem), .875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.5rem;padding-inline:.75rem;display:inline-flex;position:relative}@supports (color:color-mix(in lab, red, red)){.input{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.input{--size:calc(var(--size-field,.25rem) * 10);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.input:where(input){display:inline-flex}.input :where(input){appearance:none;background-color:#0000;border:none;width:100%;height:100%;display:inline-flex}.input :where(input):focus,.input :where(input):focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.input :where(input):focus,.input :where(input):focus-within{outline-offset:2px;outline:2px solid #0000}}.input :where(input[type=url]),.input :where(input[type=email]){direction:ltr}.input :where(input[type=date]){display:inline-flex}.input:focus,.input:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.input:focus,.input:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.input:focus,.input:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.input:focus,.input:focus-within{--font-size:1rem}}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{box-shadow:none}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.input[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input::-webkit-calendar-picker-indicator{position:absolute;inset-inline-end:.75em}.input:has(>input[type=date]) :where(input[type=date]){webkit-appearance:none;appearance:none;display:inline-flex}.input:has(>input[type=date]) input[type=date]::-webkit-calendar-picker-indicator{cursor:pointer;width:1em;height:1em;position:absolute;inset-inline-end:.75em}.table{border-collapse:separate;--tw-border-spacing-x:calc(.25rem * 0);--tw-border-spacing-y:calc(.25rem * 0);width:100%;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y);border-radius:var(--radius-box);text-align:left;font-size:.875rem;position:relative}.table:where(:dir(rtl),[dir=rtl],[dir=rtl] *){text-align:right}@media (hover:hover){:is(.table tr.row-hover,.table tr.row-hover:nth-child(2n)):hover{background-color:var(--color-base-200)}}.table :where(th,td){vertical-align:middle;padding-block:.75rem;padding-inline:1rem}.table :where(thead,tfoot){white-space:nowrap;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead,tfoot){color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.table :where(thead,tfoot){font-size:.875rem;font-weight:600}.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)}}.table :where(.table-pin-rows thead tr){z-index:1;background-color:var(--color-base-100);position:sticky;top:0}.table :where(.table-pin-rows tfoot tr){z-index:1;background-color:var(--color-base-100);position:sticky;bottom:0}.table :where(.table-pin-cols tr th){background-color:var(--color-base-100);position:sticky;left:0;right:0}.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)}}.steps{counter-reset:step;grid-auto-columns:1fr;grid-auto-flow:column;display:inline-grid;overflow:auto hidden}.steps .step{text-align:center;--step-bg:var(--color-base-300);--step-fg:var(--color-base-content);grid-template-rows:40px 1fr;grid-template-columns:auto;place-items:center;min-width:4rem;display:grid}.steps .step:before{width:100%;height:.5rem;color:var(--step-bg);background-color:var(--step-bg);content:"";border:1px solid;grid-row-start:1;grid-column-start:1;margin-inline-start:-100%;top:0}.steps .step>.step-icon,.steps .step:not(:has(.step-icon)):after{--tw-content:counter(step);content:var(--tw-content);counter-increment:step;z-index:1;color:var(--step-fg);background-color:var(--step-bg);border:1px solid var(--step-bg);border-radius:3.40282e38px;grid-row-start:1;grid-column-start:1;place-self:center;place-items:center;width:2rem;height:2rem;display:grid;position:relative}.steps .step:first-child:before{--tw-content:none;content:var(--tw-content)}.steps .step[data-content]:after{--tw-content:attr(data-content);content:var(--tw-content)}.range{appearance:none;webkit-appearance:none;--range-thumb:var(--color-base-100);--range-thumb-size:calc(var(--size-selector,.25rem) * 6);--range-progress:currentColor;--range-fill:1;--range-p:.25rem;--range-bg:currentColor}@supports (color:color-mix(in lab, red, red)){.range{--range-bg:color-mix(in oklab, currentColor 10%, #0000)}}.range{cursor:pointer;vertical-align:middle;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));width:clamp(3rem,20rem,100%);height:var(--range-thumb-size);background-color:#0000;border:none;overflow:hidden}[dir=rtl] .range{--range-dir:-1}.range:focus{outline:none}.range:focus-visible{outline-offset:2px;outline:2px solid}.range::-webkit-slider-runnable-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size) * .5)}@media (forced-colors:active){.range::-webkit-slider-runnable-track{border:1px solid}.range::-moz-range-track{border:1px solid}}.range::-webkit-slider-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));background-color:var(--range-thumb);height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p) solid;appearance:none;webkit-appearance:none;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill));position:relative;top:50%;transform:translateY(-50%)}@supports (color:color-mix(in lab, red, red)){.range::-webkit-slider-thumb{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill))}}.range::-moz-range-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size) * .5)}.range::-moz-range-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p) solid;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill));background-color:currentColor;position:relative;top:50%}@supports (color:color-mix(in lab, red, red)){.range::-moz-range-thumb{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill))}}.range:disabled{cursor:not-allowed;opacity:.3}.select{border:var(--border) solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);height:var(--size);touch-action:manipulation;white-space:nowrap;text-overflow:ellipsis;box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;background-image:linear-gradient(45deg,#0000 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,#0000 50%);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-repeat:no-repeat;background-size:4px 4px,4px 4px;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.375rem;padding-inline:.75rem 1.75rem;font-size:.875rem;display:inline-flex;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.select{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.select{border-color:var(--input-color);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.select{--size:calc(var(--size-field,.25rem) * 10)}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}[dir=rtl] .select::picker(select){translate:.5rem}[dir=rtl] .select select::picker(select){translate:.5rem}.select[multiple]{background-image:none;height:auto;padding-block:.75rem;padding-inline-end:.75rem;overflow:auto}.select select{appearance:none;width:calc(100% + 2.75rem);height:calc(100% - calc(var(--border) * 2));background:inherit;border-radius:inherit;border-style:none;align-items:center;margin-inline:-.75rem -1.75rem;padding-inline:.75rem 1.75rem}.select select:focus,.select select:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.select select:focus,.select select:focus-within{outline-offset:2px;outline:2px solid #0000}}.select select:not(:last-child){background-image:none;margin-inline-end:-1.375rem}.select:focus,.select:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.select:focus,.select:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.select:focus,.select:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.select:has(>select[disabled])>select[disabled]{cursor:not-allowed}@supports (appearance:base-select){.select,.select select{appearance:base-select}:is(.select,.select select)::picker(select){appearance:base-select}}:is(.select,.select select)::picker(select){color:inherit;border:var(--border) solid var(--color-base-200);border-radius:var(--radius-box);background-color:inherit;max-height:min(24rem,70dvh);box-shadow:0 2px calc(var(--depth) * 3px) -2px oklch(0% 0 0/.2);box-shadow:0 20px 25px -5px rgb(0 0 0/calc(var(--depth) * .1)), 0 8px 10px -6px rgb(0 0 0/calc(var(--depth) * .1));margin-block:.5rem;margin-inline:.5rem;padding:.5rem;translate:-.5rem}:is(.select,.select select)::picker-icon{display:none}:is(.select,.select select) optgroup{padding-top:.5em}:is(.select,.select select) optgroup option:first-child{margin-top:.5em}:is(.select,.select select) option{border-radius:var(--radius-field);white-space:normal;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{outline-offset:2px;outline:2px solid #0000}}:is(.select,.select select) option:not(:disabled):active{background-color:var(--color-neutral);color:var(--color-neutral-content);box-shadow:0 2px calc(var(--depth) * 3px) -2px var(--color-neutral)}.swap{cursor:pointer;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;place-content:center;display:inline-grid;position:relative}.swap input{appearance:none;border:none}.swap>*{grid-row-start:1;grid-column-start:1}@media (prefers-reduced-motion:no-preference){.swap>*{transition-property:transform,rotate,opacity;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.swap .swap-on,.swap .swap-indeterminate,.swap input:indeterminate~.swap-on,.swap input:is(:checked,:indeterminate)~.swap-off{opacity:0}.swap input:checked~.swap-on,.swap input:indeterminate~.swap-indeterminate{opacity:1;backface-visibility:visible}.collapse-title{grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out;position:relative}.checkbox{border:var(--border) solid var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox{border:var(--border) solid var(--input-color,color-mix(in oklab, var(--color-base-content) 20%, #0000))}}.checkbox{cursor:pointer;appearance:none;border-radius:var(--radius-selector);vertical-align:middle;color:var(--color-base-content);box-shadow:0 1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 0 #0000 inset, 0 0 #0000;--size:calc(var(--size-selector,.25rem) * 6);width:var(--size);height:var(--size);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);flex-shrink:0;padding:.25rem;transition:background-color .2s,box-shadow .2s;display:inline-block;position:relative}.checkbox:before{--tw-content:"";content:var(--tw-content);opacity:0;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,70% 80%,70% 100%);width:100%;height:100%;box-shadow:0px 3px 0 0px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;background-color:currentColor;font-size:1rem;line-height:.75;transition:clip-path .3s .1s,opacity .1s .1s,rotate .3s .1s,translate .3s .1s;display:block;rotate:45deg}.checkbox:focus-visible{outline:2px solid var(--input-color,currentColor);outline-offset:2px}.checkbox:checked,.checkbox[aria-checked=true]{background-color:var(--input-color,#0000);box-shadow:0 0 #0000 inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * .1))}:is(.checkbox:checked,.checkbox[aria-checked=true]):before{clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 0%,70% 0%,70% 100%);opacity:1}@media (forced-colors:active){:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:0deg}}@media print{:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:0deg}}.checkbox:indeterminate{background-color:var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox:indeterminate{background-color:var(--input-color,color-mix(in oklab, var(--color-base-content) 20%, #0000))}}.checkbox:indeterminate:before{opacity:1;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,80% 80%,80% 100%);translate:0 -35%;rotate:0deg}.radio{cursor:pointer;appearance:none;vertical-align:middle;border:var(--border) solid var(--input-color,currentColor);border-radius:3.40282e38px;flex-shrink:0;padding:.25rem;display:inline-block;position:relative}@supports (color:color-mix(in lab, red, red)){.radio{border:var(--border) solid var(--input-color,color-mix(in srgb, currentColor 20%, #0000))}}.radio{box-shadow:0 1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset;--size:calc(var(--size-selector,.25rem) * 6);width:var(--size);height:var(--size);color:var(--input-color,currentColor)}.radio:before{--tw-content:"";content:var(--tw-content);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);border-radius:3.40282e38px;width:100%;height:100%;display:block}.radio:focus-visible{outline:2px solid}.radio:checked,.radio[aria-checked=true]{background-color:var(--color-base-100);border-color:currentColor}@media (prefers-reduced-motion:no-preference){.radio:checked,.radio[aria-checked=true]{animation:.2s ease-out radio}}:is(.radio:checked,.radio[aria-checked=true]):before{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * .1));background-color:currentColor}@media (forced-colors:active){:is(.radio:checked,.radio[aria-checked=true]):before{outline-style:var(--tw-outline-style);outline-offset:calc(1px * -1);outline-width:1px}}@media print{:is(.radio:checked,.radio[aria-checked=true]):before{outline-offset:-1rem;outline:.25rem solid}}.rating{vertical-align:middle;display:inline-flex;position:relative}.rating input{appearance:none;border:none}.rating :where(*){background-color:var(--color-base-content);opacity:.2;border-radius:0;width:1.5rem;height:1.5rem}@media (prefers-reduced-motion:no-preference){.rating :where(*){animation:.25s ease-out rating}}.rating :where(*):is(input){cursor:pointer}.rating .rating-hidden{background-color:#0000;width:.5rem}.rating input[type=radio]:checked{background-image:none}.rating :checked,.rating [aria-checked=true],.rating [aria-current=true],.rating :has(~:checked,~[aria-checked=true],~[aria-current=true]){opacity:1}.rating :focus-visible{scale:1.1}@media (prefers-reduced-motion:no-preference){.rating :focus-visible{transition:scale .2s ease-out}}.rating :active:focus{animation:none;scale:1.1}.navbar{align-items:center;width:100%;min-height:4rem;padding:.5rem;display:flex}.card{border-radius:var(--radius-box);outline-offset:2px;outline:0 solid #0000;flex-direction:column;transition:outline .2s ease-in-out;display:flex;position:relative}.card:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.card:focus{outline-offset:2px;outline:2px solid #0000}}.card:focus-visible{outline-color:currentColor}.card :where(figure:first-child){border-start-start-radius:inherit;border-start-end-radius:inherit;border-end-end-radius:unset;border-end-start-radius:unset;overflow:hidden}.card :where(figure:last-child){border-start-start-radius:unset;border-start-end-radius:unset;border-end-end-radius:inherit;border-end-start-radius:inherit;overflow:hidden}.card figure{justify-content:center;align-items:center;display:flex}.card:has(>input:is(input[type=checkbox],input[type=radio])){cursor:pointer;-webkit-user-select:none;user-select:none}.card:has(>:checked){outline:2px solid}.stats{border-radius:var(--radius-box);grid-auto-flow:column;display:inline-grid;position:relative;overflow-x:auto}.progress{appearance:none;border-radius:var(--radius-box);background-color:currentColor;width:100%;height:.5rem;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.progress{background-color:color-mix(in oklab, currentcolor 20%, transparent)}}.progress{color:var(--color-base-content)}.progress:indeterminate{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%}@media (prefers-reduced-motion:no-preference){.progress:indeterminate{animation:5s ease-in-out infinite progress}}@supports ((-moz-appearance:none)){.progress:indeterminate::-moz-progress-bar{background-color:#0000}@media (prefers-reduced-motion:no-preference){.progress:indeterminate::-moz-progress-bar{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%;animation:5s ease-in-out infinite progress}}.progress::-moz-progress-bar{border-radius:var(--radius-box);background-color:currentColor}}@supports ((-webkit-appearance:none)){.progress::-webkit-progress-bar{border-radius:var(--radius-box);background-color:#0000}.progress::-webkit-progress-value{border-radius:var(--radius-box);background-color:currentColor}}.textarea{border:var(--border) solid #0000;appearance:none;border-radius:var(--radius-field);background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);min-height:5rem;font-size:max(var(--font-size,.875rem), .875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;flex-shrink:1;padding-block:.5rem;padding-inline:.75rem}@supports (color:color-mix(in lab, red, red)){.textarea{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.textarea{--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.textarea textarea{appearance:none;background-color:#0000;border:none}.textarea textarea:focus,.textarea textarea:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.textarea textarea:focus,.textarea textarea:focus-within{outline-offset:2px;outline:2px solid #0000}}.textarea:focus,.textarea:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.textarea:focus,.textarea:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.textarea:focus,.textarea:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.textarea:focus,.textarea:focus-within{--font-size:1rem}}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){box-shadow:none}.textarea:has(>textarea[disabled])>textarea[disabled]{cursor:not-allowed}.tab-content{order:var(--tabcontent-order);--tabcontent-radius-ss:var(--radius-box);--tabcontent-radius-se:var(--radius-box);--tabcontent-radius-es:var(--radius-box);--tabcontent-radius-ee:var(--radius-box);--tabcontent-order:1;width:100%;height:calc(100% - var(--tab-height) + var(--border));margin:var(--tabcontent-margin);border-color:#0000;border-width:var(--border);border-start-start-radius:var(--tabcontent-radius-ss);border-start-end-radius:var(--tabcontent-radius-se);border-end-end-radius:var(--tabcontent-radius-ee);border-end-start-radius:var(--tabcontent-radius-es);display:none}.modal-box{background-color:var(--color-base-100);border-top-left-radius:var(--modal-tl,var(--radius-box));border-top-right-radius:var(--modal-tr,var(--radius-box));border-bottom-left-radius:var(--modal-bl,var(--radius-box));border-bottom-right-radius:var(--modal-br,var(--radius-box));opacity:0;overscroll-behavior:contain;grid-row-start:1;grid-column-start:1;width:91.6667%;max-width:32rem;max-height:100vh;padding:1.5rem;transition:translate .3s ease-out,scale .3s ease-out,opacity .2s ease-out 50ms,box-shadow .3s ease-out;overflow-y:auto;scale:.95;box-shadow:0 25px 50px -12px oklch(0% 0 0/.25)}.divider{white-space:nowrap;height:1rem;margin:var(--divider-m,1rem 0);--divider-color:var(--color-base-content);flex-direction:row;align-self:stretch;align-items:center;display:flex}@supports (color:color-mix(in lab, red, red)){.divider{--divider-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.divider:before,.divider:after{content:"";background-color:var(--divider-color);flex-grow:1;width:100%;height:.125rem}@media print{.divider:before,.divider:after{border:.5px solid}}.divider:not(:empty){gap:1rem}.filter{flex-wrap:wrap;display:flex}.filter input[type=radio]{width:auto}.filter input{opacity:1;transition:margin .1s,opacity .3s,padding .3s,border-width .1s;overflow:hidden;scale:1}.filter input:not(:last-child){margin-inline-end:.25rem}.filter input.filter-reset{aspect-ratio:1}.filter input.filter-reset:after{--tw-content:"×";content:var(--tw-content)}.filter:not(:has(input:checked:not(.filter-reset))) .filter-reset,.filter:not(:has(input:checked:not(.filter-reset))) input[type=reset],.filter:has(input:checked:not(.filter-reset)) input:not(:checked,.filter-reset,input[type=reset]){opacity:0;border-width:0;width:0;margin-inline:0;padding-inline:0;scale:0}.label{white-space:nowrap;color:currentColor;align-items:center;gap:.375rem;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.label{color:color-mix(in oklab, currentcolor 60%, transparent)}}.label:has(input){cursor:pointer}.label:is(.input>*,.select>*){white-space:nowrap;height:calc(100% - .5rem);font-size:inherit;align-items:center;padding-inline:.75rem;display:flex}.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border) solid currentColor;margin-inline:-.75rem .75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border) solid color-mix(in oklab, currentColor 10%, #0000)}}.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border) solid currentColor;margin-inline:.75rem -.75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border) solid color-mix(in oklab, currentColor 10%, #0000)}}.fieldset-legend{color:var(--color-base-content);justify-content:space-between;align-items:center;gap:.5rem;margin-bottom:-.25rem;padding-block:.5rem;font-weight:600;display:flex}.status{aspect-ratio:1;border-radius:var(--radius-selector);background-color:var(--color-base-content);width:.5rem;height:.5rem;display:inline-block}@supports (color:color-mix(in lab, red, red)){.status{background-color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.status{vertical-align:middle;color:#0000004d;background-position:50%;background-repeat:no-repeat}@supports (color:color-mix(in lab, red, red)){.status{color:color-mix(in oklab, var(--color-black) 30%, transparent)}}.status{background-image:radial-gradient(circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * .5)), #0000);box-shadow:0 2px 3px -1px}@supports (color:color-mix(in lab, red, red)){.status{box-shadow:0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), #0000)}}.badge{border-radius:var(--radius-selector);vertical-align:middle;color:var(--badge-fg);border:var(--border) solid var(--badge-color,var(--color-base-200));background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);background-color:var(--badge-bg);--badge-bg:var(--badge-color,var(--color-base-100));--badge-fg:var(--color-base-content);--size:calc(var(--size-selector,.25rem) * 6);width:fit-content;height:var(--size);padding-inline:calc(var(--size) / 2 - var(--border));justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;display:inline-flex}.footer{grid-auto-flow:row;place-items:start;gap:2.5rem 1rem;width:100%;font-size:.875rem;line-height:1.25rem;display:grid}.footer>*{place-items:start;gap:.5rem;display:grid}.footer.footer-center{text-align:center;grid-auto-flow:column dense;place-items:center}.footer.footer-center>*{place-items:center}.navbar-end{justify-content:flex-end;align-items:center;width:50%;display:inline-flex}.navbar-start{justify-content:flex-start;align-items:center;width:50%;display:inline-flex}.card-body{padding:var(--card-p,1.5rem);font-size:var(--card-fs,.875rem);flex-direction:column;flex:auto;gap:.5rem;display:flex}.card-body :where(p){flex-grow:1}.navbar-center{flex-shrink:0;align-items:center;display:inline-flex}.fieldset-label{color:var(--color-base-content);align-items:center;gap:.375rem;display:flex}@supports (color:color-mix(in lab, red, red)){.fieldset-label{color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.fieldset-label:has(input){cursor:pointer}.alert{--alert-border-color:var(--color-base-200);border-radius:var(--radius-box);color:var(--color-base-content);background-color:var(--alert-color,var(--color-base-200));text-align:start;background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);box-shadow:0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * .08)) inset, 0 1px #000, 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * .08));border-style:solid;grid-template-columns:auto;grid-auto-flow:column;justify-content:start;place-items:center start;gap:1rem;padding-block:.75rem;padding-inline:1rem;font-size:.875rem;line-height:1.25rem;display:grid}@supports (color:color-mix(in lab, red, red)){.alert{box-shadow:0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * .08)) inset, 0 1px color-mix(in oklab, color-mix(in oklab, #000 20%, var(--alert-color,var(--color-base-200))) calc(var(--depth) * 20%), #0000), 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * .08))}}.alert:has(:nth-child(2)){grid-template-columns:auto minmax(auto,1fr)}.fieldset{grid-template-columns:1fr;grid-auto-rows:max-content;gap:.375rem;padding-block:.25rem;font-size:.75rem;display:grid}.card-title{font-size:var(--cardtitle-fs,1.125rem);align-items:center;gap:.5rem;font-weight:600;display:flex}.mask{vertical-align:middle;display:inline-block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.skeleton{border-radius:var(--radius-box);background-color:var(--color-base-300)}@media (prefers-reduced-motion:reduce){.skeleton{transition-duration:15s}}.skeleton{will-change:background-position;background-image:linear-gradient(105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100%);background-position-x:-50%;background-size:200%}@media (prefers-reduced-motion:no-preference){.skeleton{animation:1.8s ease-in-out infinite skeleton}}.link{cursor:pointer;text-decoration-line:underline}.link:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.link:focus{outline-offset:2px;outline:2px solid #0000}}.link:focus-visible{outline-offset:2px;outline:2px solid}.btn-error{--btn-color:var(--color-error);--btn-fg:var(--color-error-content)}.btn-primary{--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content)}}@layer daisyui.l1.l2{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{pointer-events:auto;visibility:visible;opacity:1;transition:visibility 0s allow-discrete, background-color .3s ease-out, opacity .1s ease-out;background-color:oklch(0% 0 0/.4)}:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal) .modal-box{opacity:1;translate:0;scale:1}:root:has(:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal)){--page-has-backdrop:1;--page-overflow:hidden;--page-scroll-bg:var(--page-scroll-bg-on);--page-scroll-gutter:stable;--page-scroll-transition:var(--page-scroll-transition-on);animation:forwards set-page-has-scroll;animation-timeline:scroll()}@starting-style{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{opacity:0}}.tooltip>.tooltip-content,.tooltip[data-tip]:before{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-off) 50%}.tooltip:after{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-tail) 50%}.btn:disabled:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.btn:disabled:not(.btn-link,.btn-ghost){box-shadow:none}.btn:disabled{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled{--btn-fg:color-mix(in oklch, var(--color-base-content) 20%, #0000)}}.btn[disabled]:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.btn[disabled]:not(.btn-link,.btn-ghost){box-shadow:none}.btn[disabled]{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]{--btn-fg:color-mix(in oklch, var(--color-base-content) 20%, #0000)}}@media (prefers-reduced-motion:no-preference){.collapse[open].collapse-arrow>.collapse-title:after,.collapse.collapse-open.collapse-arrow>.collapse-title:after{transform:translateY(-50%)rotate(225deg)}}.collapse.collapse-open.collapse-plus>.collapse-title:after{--tw-content:"−";content:var(--tw-content)}:is(.collapse[tabindex].collapse-arrow:focus:not(.collapse-close),.collapse.collapse-arrow[tabindex]:focus-within:not(.collapse-close))>.collapse-title:after,.collapse.collapse-arrow:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{transform:translateY(-50%)rotate(225deg)}.collapse[open].collapse-plus>.collapse-title:after,.collapse[tabindex].collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse.collapse-plus:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{--tw-content:"−";content:var(--tw-content)}.list .list-row:has(.list-col-grow:first-child){--list-grid-cols:1fr}.list .list-row:has(.list-col-grow:nth-child(2)){--list-grid-cols:minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(3)){--list-grid-cols:minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(4)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(5)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(6)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row>*{grid-row-start:1}.steps .step-neutral+.step-neutral:before,.steps .step-neutral:after,.steps .step-neutral>.step-icon{--step-bg:var(--color-neutral);--step-fg:var(--color-neutral-content)}.steps .step-primary+.step-primary:before,.steps .step-primary:after,.steps .step-primary>.step-icon{--step-bg:var(--color-primary);--step-fg:var(--color-primary-content)}.steps .step-secondary+.step-secondary:before,.steps .step-secondary:after,.steps .step-secondary>.step-icon{--step-bg:var(--color-secondary);--step-fg:var(--color-secondary-content)}.steps .step-accent+.step-accent:before,.steps .step-accent:after,.steps .step-accent>.step-icon{--step-bg:var(--color-accent);--step-fg:var(--color-accent-content)}.steps .step-info+.step-info:before,.steps .step-info:after,.steps .step-info>.step-icon{--step-bg:var(--color-info);--step-fg:var(--color-info-content)}.steps .step-success+.step-success:before,.steps .step-success:after,.steps .step-success>.step-icon{--step-bg:var(--color-success);--step-fg:var(--color-success-content)}.steps .step-warning+.step-warning:before,.steps .step-warning:after,.steps .step-warning>.step-icon{--step-bg:var(--color-warning);--step-fg:var(--color-warning-content)}.steps .step-error+.step-error:before,.steps .step-error:after,.steps .step-error>.step-icon{--step-bg:var(--color-error);--step-fg:var(--color-error-content)}.menu-vertical{flex-direction:column;display:inline-flex}.menu-vertical>li:not(.menu-title)>details>ul{background-color:revert-layer;border-radius:revert-layer;animation:revert-layer;box-shadow:revert-layer;margin-inline-start:1rem;margin-top:0;padding-block:0;padding-inline-end:0;transition:revert-layer;position:relative}.checkbox:disabled,.radio:disabled{cursor:not-allowed;opacity:.2}.rating.rating-xs :where(:not(.rating-hidden)){width:1rem;height:1rem}.rating.rating-sm :where(:not(.rating-hidden)){width:1.25rem;height:1.25rem}.rating.rating-md :where(:not(.rating-hidden)){width:1.5rem;height:1.5rem}.rating.rating-lg :where(:not(.rating-hidden)){width:1.75rem;height:1.75rem}.rating.rating-xl :where(:not(.rating-hidden)){width:2rem;height:2rem}:where(.navbar){position:relative}.tooltip-top>.tooltip-content,.tooltip-top[data-tip]:before{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-off) 50%}.tooltip-top:after{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-tail) 50%}.btn-active{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn-active{--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}.btn-active{--btn-shadow:0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0);isolation:isolate}.input-lg{--size:calc(var(--size-field,.25rem) * 12);font-size:max(var(--font-size,1.125rem), 1.125rem)}.input-lg[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input-md{--size:calc(var(--size-field,.25rem) * 10);font-size:max(var(--font-size,.875rem), .875rem)}.input-md[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input-sm{--size:calc(var(--size-field,.25rem) * 8);font-size:max(var(--font-size,.75rem), .75rem)}.input-sm[type=number]::-webkit-inner-spin-button{margin-block:-.5rem;margin-inline-end:-.75rem}.input-xl{--size:calc(var(--size-field,.25rem) * 14);font-size:max(var(--font-size,1.375rem), 1.375rem)}.input-xl[type=number]::-webkit-inner-spin-button{margin-block:-1rem;margin-inline-end:-.75rem}.input-xs{--size:calc(var(--size-field,.25rem) * 6);font-size:max(var(--font-size,.6875rem), .6875rem)}.input-xs[type=number]::-webkit-inner-spin-button{margin-block:-.25rem;margin-inline-end:-.75rem}.badge-ghost{border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content);background-image:none}.badge-outline{color:var(--badge-color);--badge-bg:#0000;background-image:none;border-color:currentColor}.select-lg{--size:calc(var(--size-field,.25rem) * 12);font-size:1.125rem}.select-lg option{padding-block:.375rem;padding-inline:1rem}.select-sm{--size:calc(var(--size-field,.25rem) * 8);font-size:.75rem}.select-sm option{padding-block:.25rem;padding-inline:.625rem}.select-xl{--size:calc(var(--size-field,.25rem) * 14);font-size:1.375rem}.select-xl option{padding-block:.375rem;padding-inline:1.25rem}.select-xs{--size:calc(var(--size-field,.25rem) * 6);font-size:.6875rem}.select-xs option{padding-block:.25rem;padding-inline:.5rem}.table-sm :not(thead,tfoot) tr{font-size:.75rem}.table-sm :where(th,td){padding-block:.5rem;padding-inline:.75rem}.badge-sm{--size:calc(var(--size-selector,.25rem) * 5);font-size:.75rem}.textarea-lg{font-size:max(var(--font-size,1.125rem), 1.125rem)}.textarea-sm{font-size:max(var(--font-size,.75rem), .75rem)}.textarea-xl{font-size:max(var(--font-size,1.375rem), 1.375rem)}.textarea-xs{font-size:max(var(--font-size,.6875rem), .6875rem)}.alert-error{color:var(--color-error-content);--alert-border-color:var(--color-error);--alert-color:var(--color-error)}.checkbox-primary{color:var(--color-primary-content);--input-color:var(--color-primary)}.link-primary{color:var(--color-primary)}@media (hover:hover){.link-primary:hover{color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.link-primary:hover{color:color-mix(in oklab, var(--color-primary) 80%, #000)}}}.btn-sm{--fontsize:.75rem;--btn-p:.75rem;--size:calc(var(--size-field,.25rem) * 8)}.btn-xs{--fontsize:.6875rem;--btn-p:.5rem;--size:calc(var(--size-field,.25rem) * 6)}.badge-error{--badge-color:var(--color-error);--badge-fg:var(--color-error-content)}.badge-info{--badge-color:var(--color-info);--badge-fg:var(--color-info-content)}.badge-primary{--badge-color:var(--color-primary);--badge-fg:var(--color-primary-content)}.badge-secondary{--badge-color:var(--color-secondary);--badge-fg:var(--color-secondary-content)}.badge-success{--badge-color:var(--color-success);--badge-fg:var(--color-success-content)}.badge-warning{--badge-color:var(--color-warning);--badge-fg:var(--color-warning-content)}.range-lg{--range-thumb-size:calc(var(--size-selector,.25rem) * 7)}.range-sm{--range-thumb-size:calc(var(--size-selector,.25rem) * 5)}.range-xl{--range-thumb-size:calc(var(--size-selector,.25rem) * 8)}.range-xs{--range-thumb-size:calc(var(--size-selector,.25rem) * 4)}.textarea-error,.textarea-error:focus,.textarea-error:focus-within{--input-color:var(--color-error)}}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse:not(td,tr,colgroup){visibility:revert-layer}.validator:user-invalid~.validator-hint{display:revert-layer}.validator:has(:user-invalid)~.validator-hint{display:revert-layer}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))~.validator-hint{display:revert-layer}.collapse{visibility:collapse}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.join{--join-ss:0;--join-se:0;--join-es:0;--join-ee:0;align-items:stretch;display:inline-flex}.join :where(.join-item){border-start-start-radius:var(--join-ss,0);border-start-end-radius:var(--join-se,0);border-end-end-radius:var(--join-ee,0);border-end-start-radius:var(--join-es,0)}.join :where(.join-item) *{--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.join>.join-item:where(:first-child),.join :first-child:not(:last-child) :where(.join-item){--join-ss:var(--radius-field);--join-se:0;--join-es:var(--radius-field);--join-ee:0}.join>.join-item:where(:last-child),.join :last-child:not(:first-child) :where(.join-item){--join-ss:0;--join-se:var(--radius-field);--join-es:0;--join-ee:var(--radius-field)}.join>.join-item:where(:only-child),.join :only-child :where(.join-item){--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.join>:where(:focus,:has(:focus)){z-index:1}@media (hover:hover){.join>:where(.btn:hover,:has(.btn:hover)){isolation:isolate}}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-2{margin:calc(var(--spacing) * 2)}.mx-auto{margin-inline:auto}.my-2{margin-block:calc(var(--spacing) * 2)}.join-item:where(:not(:first-child,:disabled,[disabled],.btn-disabled)){margin-block-start:0;margin-inline-start:calc(var(--border,1px) * -1)}.join-item:where(:is(:disabled,[disabled],.btn-disabled)){border-width:var(--border,1px) 0 var(--border,1px) var(--border,1px)}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows), 0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:oklab(21% -.00316127 -.0338527/.1);--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.mt-0{margin-top:calc(var(--spacing) * 0)}.mt-0\!{margin-top:calc(var(--spacing) * 0)!important}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-5{margin-right:calc(var(--spacing) * 5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.alert{border-width:var(--border);border-color:var(--alert-border-color,var(--color-base-200))}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:root .prose{--tw-prose-body:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-body:color-mix(in oklab, var(--color-base-content) 80%, #0000)}}:root .prose{--tw-prose-headings:var(--color-base-content);--tw-prose-lead:var(--color-base-content);--tw-prose-links:var(--color-base-content);--tw-prose-bold:var(--color-base-content);--tw-prose-counters:var(--color-base-content);--tw-prose-bullets:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-bullets:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-hr:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-hr:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-quotes:var(--color-base-content);--tw-prose-quote-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-quote-borders:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-captions:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-captions:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-code:var(--color-base-content);--tw-prose-pre-code:var(--color-neutral-content);--tw-prose-pre-bg:var(--color-neutral);--tw-prose-th-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-th-borders:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-td-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-td-borders:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-kbd:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-kbd:color-mix(in oklab, var(--color-base-content) 80%, #0000)}}:root .prose :where(code):not(pre>code){background-color:var(--color-base-200);border-radius:var(--radius-selector);border:var(--border) solid var(--color-base-300);font-weight:inherit;padding-block:.2em;padding-inline:.5em}:root .prose :where(code):not(pre>code):before,:root .prose :where(code):not(pre>code):after{display:none}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[calc\(100vh-2rem\)\]{max-height:calc(100vh - 2rem)}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-\[120px\]{min-height:120px}.min-h-dvh{min-height:100dvh}.min-h-full{min-height:100%}.w-4{width:calc(var(--spacing) * 4)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-7{max-width:calc(var(--spacing) * 7)}.max-w-60{max-width:calc(var(--spacing) * 60)}.max-w-lg{max-width:var(--container-lg)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-20{min-width:calc(var(--spacing) * 20)}.min-w-80{min-width:calc(var(--spacing) * 80)}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-base-200{border-color:var(--color-base-200)}.border-base-300{border-color:var(--color-base-300)}.bg-base-100{background-color:var(--color-base-100)}.bg-base-200{background-color:var(--color-base-200)}.bg-gray-500{background-color:var(--color-gray-500)}.bg-gray-500\/50{background-color:#6a728280}@supports (color:color-mix(in lab, red, red)){.bg-gray-500\/50{background-color:color-mix(in oklab, var(--color-gray-500) 50%, transparent)}}.bg-primary,.bg-primary\/10{background-color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.p-0{padding:calc(var(--spacing) * 0)}.p-0\!{padding:calc(var(--spacing) * 0)!important}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-4{padding-block:calc(var(--spacing) * 4)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-pre-wrap{white-space:pre-wrap}.text-base-content,.text-base-content\/50{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/50{color:color-mix(in oklab, var(--color-base-content) 50%, transparent)}}.text-base-content\/60{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/60{color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.text-base-content\/70{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/70{color:color-mix(in oklab, var(--color-base-content) 70%, transparent)}}.text-base-content\/80{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/80{color:color-mix(in oklab, var(--color-base-content) 80%, transparent)}}.text-error{color:var(--color-error)}.text-primary{color:var(--color-primary)}.text-primary-content{color:var(--color-primary-content)}.italic{font-style:italic}.prose :where(a.btn:not(.btn-link)):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-primary{--tw-ring-color:var(--color-primary)}@layer daisyui.l1{.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)){--btn-shadow:"";--btn-bg:#0000;--btn-border:#0000;--btn-noise:none}.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)):not(:disabled,[disabled],.btn-disabled){--btn-fg:var(--btn-color,currentColor);outline-color:currentColor}@media (hover:none){.btn-ghost:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color,currentColor);--btn-border:#0000;--btn-noise:none;outline-color:currentColor}}.btn-outline:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn),:disabled,[disabled],.btn-disabled){--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}@media (hover:none){.btn-outline:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;user-select:none}.ring-inset{--tw-ring-inset:inset}@media (hover:hover){.hover\:text-primary:hover{color:var(--color-primary)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.active\:cursor-grabbing:active{cursor:grabbing}@media not all and (min-width:770px){.max-\[770px\]\:hidden{display:none}}}[x-cloak]{display:none!important}@keyframes rating{0%,40%{filter:brightness(1.05)contrast(1.05);scale:1.1}}@keyframes dropdown{0%{opacity:0}}@keyframes radio{0%{padding:5px}50%{padding:3px}}@keyframes toast{0%{opacity:0;scale:.9}to{opacity:1;scale:1}}@keyframes rotator{89.9999%,to{--first-item-position:0 0%}90%,99.9999%{--first-item-position:0 calc(var(--items) * 100%)}to{translate:0 -100%}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}@keyframes menu{0%{opacity:0}}@keyframes progress{50%{background-position-x:-115%}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false} \ No newline at end of file diff --git a/pkg/web/components/modal.templ b/pkg/web/components/modal.templ index 1a0f6ec..454cbab 100644 --- a/pkg/web/components/modal.templ +++ b/pkg/web/components/modal.templ @@ -18,7 +18,7 @@ templ Modal(props ModalProps) { } }}
-
-
-
+
+
+
-
+
if props.Content != nil { @props.Content } diff --git a/pkg/web/components/modal_templ.go b/pkg/web/components/modal_templ.go index a49250e..70939c0 100644 --- a/pkg/web/components/modal_templ.go +++ b/pkg/web/components/modal_templ.go @@ -43,86 +43,68 @@ func Modal(props ModalProps) templ.Component { if maxWidth == "" { maxWidth = "max-w-2xl" } - var templ_7745c5c3_Var2 = []any{"fixed inset-0 z-50 items-center justify-center mx-auto", maxWidth} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var4 string - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.ID) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 22, Col: 15} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + var templ_7745c5c3_Var4 = []any{"relative w-full rounded-lg bg-base-100 pointer-events-auto flex flex-col max-h-[calc(100vh-2rem)] overflow-hidden", maxWidth} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" x-data=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var6 = []any{"relative bg-base-100 rounded-lg w-full pointer-events-auto"} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...) + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 35, Col: 65} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 33, Col: 65} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -132,7 +114,7 @@ func Modal(props ModalProps) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -156,12 +138,12 @@ func ModalContainer() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var9 := templ.GetChildren(ctx) - if templ_7745c5c3_Var9 == nil { - templ_7745c5c3_Var9 = templ.NopComponent + templ_7745c5c3_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -195,113 +177,113 @@ func ConfirmModal(props ConfirmModalProps) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var10 := templ.GetChildren(ctx) - if templ_7745c5c3_Var10 == nil { - templ_7745c5c3_Var10 = templ.NopComponent + templ_7745c5c3_Var8 := templ.GetChildren(ctx) + if templ_7745c5c3_Var8 == nil { + templ_7745c5c3_Var8 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title) + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 74, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 76, Col: 73} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(props.Message) + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(props.Message) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 78, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 80, Col: 52} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 = []any{"btn", props.ConfirmClass} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...) + var templ_7745c5c3_Var12 = []any{"btn", props.ConfirmClass} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } From a7fd27192801d3a48e8d1ea4af48c1e537e2e61b Mon Sep 17 00:00:00 2001 From: Ine Maria Nilssen Aanonsen Date: Thu, 19 Mar 2026 11:49:08 +0100 Subject: [PATCH 18/73] INTERFACES SKIP TASK implemented more intuitive 'skip task' in each interface --- cmd/pm/tasks/base.go | 14 +++++++++++--- internal/commands/issues/root.go | 4 +++- pkg/task/taskui.go | 15 ++++++++++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cmd/pm/tasks/base.go b/cmd/pm/tasks/base.go index 057ea88..d634629 100644 --- a/cmd/pm/tasks/base.go +++ b/cmd/pm/tasks/base.go @@ -61,18 +61,26 @@ func BaseDetails(interfaceType InterfaceType) TaskDetails { - REPL stands for Read-Eval-Print Loop. It is an interactive programming environment that takes single user inputs (reads), executes them (eval), and returns the result to the user (print), then waits for the next input (loop). - In this task, you will interact with the task through a REPL interface, which allows you to execute commands and receive immediate feedback in a command-line environment. - You can type commands to perform actions related to the task, and the REPL will process those commands and provide responses based on your input. -- The REPL interface is designed to facilitate a more dynamic and interactive way of completing the task, allowing you to experiment and receive real-time feedback as you work through the task requirements.` +- The REPL interface is designed to facilitate a more dynamic and interactive way of completing the task, allowing you to experiment and receive real-time feedback as you work through the task requirements. +- Write "exit" to skip task during the survey.` case InterfaceTypeTUI: interfaceDesc = `What is a TUI Interface? - TUI stands for Text User Interface. It is a user interface that uses text-based elements to allow users to interact with the application. - The main way to interact with a TUI is through keyboard inputs, where you can navigate through menus, select options, and input data using the keyboard. - In this task, you will interact with the task through a TUI interface, which provides a more structured and visually organized way to complete the task using text-based menus, forms, and other interactive elements. -- The TUI interface is designed to enhance usability and provide a more engaging experience while working through the task requirements, allowing you to navigate through options and input information in a more intuitive way.` +- The TUI interface is designed to enhance usability and provide a more engaging experience while working through the task requirements, allowing you to navigate through options and input information in a more intuitive way. +- Press "q" to quit task during the survey.` case InterfaceTypeWeb: interfaceDesc = `What is a Web Interface? - A Web Interface is a user interface that is accessed through a web browser. It allows users to interact with the application using graphical elements such as buttons, forms, and menus. - In this task, you will interact with the task through a Web interface, which provides a more visually rich and user-friendly way to complete the task using a web-based platform. -- The Web interface is designed to enhance usability and provide a more engaging experience while working through the task requirements, allowing you to navigate through options and input information in a more intuitive way using a graphical interface.` +- The Web interface is designed to enhance usability and provide a more engaging experience while working through the task requirements, allowing you to navigate through options and input information in a more intuitive way using a graphical interface. +- Press esc/q in the terminal to skip the task.` + case InterfaceTypeCLI: + interfaceDesc = `What is a CLI Interface? +- CLI stands for Command-Line Interface. It is a text-based interface where you interact with the application by typing commands. +- In this task, you will interact with the task through a CLI interface and execute commands directly in the terminal. +- Write "exit" to skip task during the survey.` default: interfaceDesc = "Unknown Interface" } diff --git a/internal/commands/issues/root.go b/internal/commands/issues/root.go index 3c1e5d3..d86646e 100644 --- a/internal/commands/issues/root.go +++ b/internal/commands/issues/root.go @@ -35,7 +35,9 @@ type Flags struct { var RootCmd = &cobra.Command{ Use: "pm", Short: "Project Management User Interface Comparison CLI", - Long: `Project Management User Interface Comparison CLI is a tool designed to evaluate and compare different project management interfaces through a series of tasks and surveys.`, + Long: `Project Management User Interface Comparison CLI is a tool designed to evaluate and compare different project management interfaces through a series of tasks and surveys. + +Write 'exit' to skip this task.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { // Inject app into context for all commands if app != nil { diff --git a/pkg/task/taskui.go b/pkg/task/taskui.go index d5fb921..5f1426d 100644 --- a/pkg/task/taskui.go +++ b/pkg/task/taskui.go @@ -109,7 +109,7 @@ func (m TaskModel) View() tea.View { b.WriteString("\n") - helpText := "Press " + m.keys.Start.Help().Key + " to start • " + m.keys.Quit.Help().Key + " to quit • " + m.keys.About.Help().Key + " " + m.keys.About.Help().Desc + helpText := "Press " + m.keys.Start.Help().Key + " to start • " + m.getQuitHelpText() + " • " + m.keys.About.Help().Key + " " + m.keys.About.Help().Desc b.WriteString(style.HelpStyle.Render(helpText)) final := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, b.String()) @@ -119,6 +119,19 @@ func (m TaskModel) View() tea.View { return v } +func (m TaskModel) getQuitHelpText() string { + switch m.InterfaceType { + case models.InterfaceTypeWeb: + return "Press esc/q in the terminal to skip the task" + case models.InterfaceTypeTUI: + return "Press 'q' to skip task during the survey" + case models.InterfaceTypeCLI, models.InterfaceTypeREPL: + return "Write 'exit' to skip task during the survey" + default: + return m.keys.Quit.Help().Key + " to quit" + } +} + func (m *TaskModel) SetSize(width, height int) { m.width, m.height = width, height } From 5519b9d2eb388e2263a6128032b6ea339402bf62 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 19 Mar 2026 12:41:48 +0100 Subject: [PATCH 19/73] make it even more clear and add a line describing how to submit --- internal/commands/issues/root.go | 4 +--- pkg/task/taskui.go | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/internal/commands/issues/root.go b/internal/commands/issues/root.go index d86646e..3c1e5d3 100644 --- a/internal/commands/issues/root.go +++ b/internal/commands/issues/root.go @@ -35,9 +35,7 @@ type Flags struct { var RootCmd = &cobra.Command{ Use: "pm", Short: "Project Management User Interface Comparison CLI", - Long: `Project Management User Interface Comparison CLI is a tool designed to evaluate and compare different project management interfaces through a series of tasks and surveys. - -Write 'exit' to skip this task.`, + Long: `Project Management User Interface Comparison CLI is a tool designed to evaluate and compare different project management interfaces through a series of tasks and surveys.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { // Inject app into context for all commands if app != nil { diff --git a/pkg/task/taskui.go b/pkg/task/taskui.go index 5f1426d..b3ef3e2 100644 --- a/pkg/task/taskui.go +++ b/pkg/task/taskui.go @@ -98,6 +98,8 @@ func (m TaskModel) View() tea.View { style.TextStyle.Render(m.Description), "\n", style.TextStyle.Foreground(style.SecondaryColor).Render(detailsText), + style.ErrorStyle.Render(m.interfaceHelpText()), + style.ErrorStyle.Render(m.getQuitHelpText()), ) if m.aboutVisible { @@ -109,7 +111,7 @@ func (m TaskModel) View() tea.View { b.WriteString("\n") - helpText := "Press " + m.keys.Start.Help().Key + " to start • " + m.getQuitHelpText() + " • " + m.keys.About.Help().Key + " " + m.keys.About.Help().Desc + helpText := "Press " + m.keys.Start.Help().Key + " to start • " + m.keys.Quit.Help().Key + " to quit • " + m.keys.About.Help().Key + " " + m.keys.About.Help().Desc b.WriteString(style.HelpStyle.Render(helpText)) final := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, b.String()) @@ -132,6 +134,19 @@ func (m TaskModel) getQuitHelpText() string { } } +func (m TaskModel) interfaceHelpText() string { + switch m.InterfaceType { + case models.InterfaceTypeWeb: + return "Press the button in the upper right corner to view task progress and completion criteria" + case models.InterfaceTypeTUI: + return "Press Shift+S to view task progress and completion criteria" + case models.InterfaceTypeCLI, models.InterfaceTypeREPL: + return "Write 'status' to view task progress and completion criteria" + default: + return m.keys.Quit.Help().Key + " to quit" + } +} + func (m *TaskModel) SetSize(width, height int) { m.width, m.height = width, height } From 151d7572091de2968c0e59f98eb995774d6bbed3 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 19 Mar 2026 13:51:46 +0100 Subject: [PATCH 20/73] write task description to file --- cmd/pm/runner.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/pm/runner.go b/cmd/pm/runner.go index 0872e30..a4de2c6 100644 --- a/cmd/pm/runner.go +++ b/cmd/pm/runner.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "math/rand" + "os" "time" "charm.land/huh/v2" @@ -183,6 +184,10 @@ func taskLoop(ctx context.Context, application *task.App, surveyTasks map[string runner := task.NewTaskRunner(application) + if err := os.WriteFile("Task Details.txt", []byte(t.Details(tasks.InterfaceToType(selected)).Description), 0644); err != nil { + return fmt.Errorf("failed to write task details: %w", err) + } + if err := runner.Run(ctx, t, selected, tasks.InterfaceToType(selected)); err != nil { return err } From 64a02a6ed566045a225b0bbc6541abc8087596e2 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 19 Mar 2026 15:20:50 +0100 Subject: [PATCH 21/73] make Contains expector more durable --- internal/utils/check/expect.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/utils/check/expect.go b/internal/utils/check/expect.go index 256c6e6..eba9385 100644 --- a/internal/utils/check/expect.go +++ b/internal/utils/check/expect.go @@ -109,9 +109,11 @@ func (e *Expector) NotNil(value any, message string) *Expector { } func (e *Expector) Contains(s, substr, message string) *Expector { - check := NewCheck(message, strings.Contains(s, substr)) - e.Checks = append(e.Checks, check) - return e + if !strings.Contains(s, substr) { + return e.Fail(fmt.Sprintf(`%s expected to contain "%v", but it does not.`, message, substr)) + } else { + return e.Pass(fmt.Sprintf(`%s contains "%v"`, message, substr)) + } } func (e *Expector) NotContains(s, substr, message string) *Expector { From b0c76b056574351c39c03a5ae655fdbe236e2389 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 19 Mar 2026 15:21:30 +0100 Subject: [PATCH 22/73] remove setup issues and simplify validaiton --- cmd/pm/tasks/backlogRefinement.go | 20 ++------ cmd/pm/tasks/base.go | 11 ++-- cmd/pm/tasks/codingTask.go | 75 ++++++---------------------- cmd/pm/tasks/createIssue.go | 20 ++------ cmd/pm/tasks/dependencyManagement.go | 31 +++--------- cmd/pm/tasks/gitTask.go | 36 ++++++------- cmd/pm/tasks/issueReviewCleanup.go | 43 +++++++--------- cmd/pm/tasks/priorityManagement.go | 36 ++----------- cmd/pm/tasks/sprintPlanning.go | 30 +++-------- internal/commands/issues/update.go | 8 +++ 10 files changed, 91 insertions(+), 219 deletions(-) diff --git a/cmd/pm/tasks/backlogRefinement.go b/cmd/pm/tasks/backlogRefinement.go index f04201f..30e66ae 100644 --- a/cmd/pm/tasks/backlogRefinement.go +++ b/cmd/pm/tasks/backlogRefinement.go @@ -23,9 +23,8 @@ The product backlog has become cluttered with old and unclear issues. You need t Focus on making the backlog a reliable source of upcoming work.` type BacklogRefinementTask struct { - done bool - app *App - setupIssue *Issue + done bool + app *App } func NewBacklogRefinementTask(app *App) *BacklogRefinementTask { @@ -115,24 +114,15 @@ func (t *BacklogRefinementTask) Setup(ctx context.Context) error { Build(), } - if err := t.app.Issues.CreateIssues(ctx, refinementIssues, ""); err != nil { - return err - } - - t.setupIssue = NewIssueBuilder(). - WithTitle("Backlog Refinement Session"). - WithDescription(backlogRefinementDescription). - Build() - - return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") + return t.app.Issues.CreateIssues(ctx, refinementIssues, "") } func (t *BacklogRefinementTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - issues, err := FetchIssues(ctx, t.app, t.setupIssue) + issues, err := FetchIssues(ctx, t.app) if err != nil { - return expect.ValidationFeedback + return expect.Fatal("Could not fetch issues") } var closedDuplicate *models.Issue diff --git a/cmd/pm/tasks/base.go b/cmd/pm/tasks/base.go index d634629..93f359f 100644 --- a/cmd/pm/tasks/base.go +++ b/cmd/pm/tasks/base.go @@ -168,9 +168,8 @@ func TUIQuestion(interfaceType InterfaceType, fields ...huh.Field) *huh.Group { return huh.NewGroup(fields...) } -// FetchIssues retrieves all issues from the app and returns those that are relevant for validation, -// excluding the setup issue. It also updates the setup issue with the latest data from the app. -func FetchIssues(ctx context.Context, app *App, setupIssue *Issue) ([]*Issue, error) { +// FetchIssues retrieves all issues from the app and returns those that are relevant for validation +func FetchIssues(ctx context.Context, app *App) ([]*Issue, error) { issues, err := app.Issues.SearchIssues(ctx, "", models.IssueFilter{}) if err != nil { return nil, err @@ -178,11 +177,7 @@ func FetchIssues(ctx context.Context, app *App, setupIssue *Issue) ([]*Issue, er var relevantIssues []*Issue for _, issue := range issues { - if issue.ID != setupIssue.ID { - relevantIssues = append(relevantIssues, issue) - } else { - *setupIssue = *issue - } + relevantIssues = append(relevantIssues, issue) } return relevantIssues, nil diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 18d313a..12d5e34 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -6,7 +6,6 @@ import ( "strings" "charm.land/huh/v2" - "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" ) @@ -18,15 +17,10 @@ The MongoDB Driver dependency in the file is outdated and needs to be updated to This is a common task for developers, and it requires attention to detail and the ability to follow instructions carefully. Your task: -1. Create a New Issue and give it these details: - - Title: "Upgrade MongoDB Driver Dependency" - - Description: "We need to upgrade the MongoDB Driver dependency to the latest version." - - Status: "In Progress" - - Issue Type: "Chore" -2. Assign the issue to yourself as "Me". -3. A file will appear in the current directory named "code.txt". +1. Assingn the given issue to yourself as 'Me'. +2. A file will appear in the current directory named "code.txt". Open it and follow the instructions inside. And save the file after you are done. -4. When you are done, mark this and the issue you made as "Closed".` +3. When you are done, mark this and the issue you made as "Closed".` var textFileDescription = ` @@ -76,9 +70,9 @@ tool ( var textFileContent = codingDescription + textFileDescription + "\n" + code type CodingTask struct { - done bool - setupIssue *Issue - app *App + done bool + app *App + issue *Issue } func NewCodingTask(app *App) *CodingTask { @@ -124,60 +118,27 @@ func (t *CodingTask) Setup(ctx context.Context) error { return err } - t.setupIssue = NewIssueBuilder(). - WithTitle("Coding Task - Upgrade MongoDB Driver"). - WithDescription(codingDescription). - WithStatus(models.StatusOpen). - WithIssueType(models.TypeTask). - Build() - - if err := t.app.Issues.CreateIssue(ctx, t.setupIssue, "LazyPM"); err != nil { - return err - } - if err := os.WriteFile("./code.txt", []byte(textFileContent), 0644); err != nil { return err } - return nil -} + t.issue = NewIssueBuilder(). + WithTitle("Upgrade MongoDB Driver"). + WithDescription(codingDescription). + Build() -var codingTaskInProgress = false + return t.app.Issues.CreateIssue(ctx, t.issue, "") +} func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - issues, err := FetchIssues(ctx, t.app, t.setupIssue) + issue, err := t.app.Issues.GetIssue(ctx, t.issue.ID) if err != nil { - return expect.ValidationFeedback + return expect.Fatal("Could not fetch issues") } - if len(issues) == 0 { - expect.Fail("No new issues created") - return expect.ValidationFeedback - } else { - expect.Pass("An issue was created") - } - - issue := issues[0] - - expect.Assert(len(issues) < 2, "Multiple issues were created instead of one") - - expect.NotEmptyAndEqual(issue.Title, "Upgrade MongoDB Driver Dependency", "Issue title") - - expect.NotEmptyAndEqual(issue.Description, - "We need to upgrade the MongoDB Driver dependency to the latest version.", "Issue description") - - expect.NotEmptyAndEqual(issue.Assignee, "Me", "Issue Assignee") - - expect.Equal(issue.IssueType, models.TypeChore, "Issue type") - - if issue.Status == models.StatusInProgress || codingTaskInProgress { - codingTaskInProgress = true - } else { - expect.Fail("The issue should be marked as In Progress while working on the task.") - return expect.ValidationFeedback - } + expect.Equal(issue.Assignee, "Me", "Issue Assignee") if _, err := os.Stat("./code.txt"); os.IsNotExist(err) { expect.Fail("The code.txt file should exist on the desktop.") @@ -196,11 +157,7 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { return expect.ValidationFeedback } - expect.Assert(strings.Contains(code, "go.mongodb.org/mongo-driver v1.17.9"), - "The MongoDB Driver dependency should be updated to version v1.17.9 in the file.") - - expect.Assert(codingTaskInProgress && issue.Status == models.StatusClosed, - "The issue should be marked as Closed after completing the task.") + expect.Contains(code, "go.mongodb.org/mongo-driver v1.17.9", "MongoDB Driver version") return expect.Complete() } diff --git a/cmd/pm/tasks/createIssue.go b/cmd/pm/tasks/createIssue.go index c34dc8e..4893e8d 100644 --- a/cmd/pm/tasks/createIssue.go +++ b/cmd/pm/tasks/createIssue.go @@ -2,7 +2,6 @@ package tasks import ( "context" - "fmt" "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" @@ -69,37 +68,28 @@ func (t *CreateIssueTask) Setup(ctx context.Context) error { return err } - t.setupIssue = NewIssueBuilder(). - WithTitle("Create a New Issue"). - WithDescription(description). - Build() - - return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") + return nil } func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - issues, err := FetchIssues(ctx, t.app, t.setupIssue) + issues, err := FetchIssues(ctx, t.app) if err != nil { - return expect.ValidationFeedback + return expect.Fatal("Could not fetch issues") } if len(issues) == 0 { - expect.Fail("No new issues created") + expect.Fail("No issues were created") return expect.ValidationFeedback } issue := issues[0] - expect.Assert(len(issues) < 2, "Multiple issues were created instead of one") - expect.NotEmptyAndEqual(issue.Title, "My first Issue", "Issue title") expect.NotEmptyAndEqual(issue.Description, "I need to do some coding", "Issue description") expect.NotEmptyAndEqual(issue.Assignee, "Me", "Issue assignee") - - expect.Assert(issue.Status == models.StatusInProgress, - fmt.Sprintf("Issue status should be 'In Progress', but was '%s'", issue.Status)) + expect.Equal(issue.Status, models.StatusInProgress, "Issue status") return expect.Complete() } diff --git a/cmd/pm/tasks/dependencyManagement.go b/cmd/pm/tasks/dependencyManagement.go index d1c6a4a..798d2e9 100644 --- a/cmd/pm/tasks/dependencyManagement.go +++ b/cmd/pm/tasks/dependencyManagement.go @@ -11,8 +11,9 @@ import ( const dependencyManagementDescription = `You are tasked with managing issue dependencies. -Several issues in your project have dependencies on other issues. You need to: +Several issues in your project have dependencies on other issues. +You need to: 1. Find the 4 issues that mention dependencies in their detail description. For example: "Depends on Issue '123'". Set their status to "blocked". 2. Find the 2 foundational issues that are mentioned by the other issues. 3. Set priority of the 2 foundational issues to 3 (high). @@ -22,10 +23,9 @@ Several issues in your project have dependencies on other issues. You need to: Resolving dependencies in the right order is critical for efficient team workflow.` type DependencyManagementTask struct { - done bool - app *App - setupIssue *Issue - depIssues []*Issue + done bool + app *App + depIssues []*Issue } func NewDependencyManagementTask(app *App) *DependencyManagementTask { @@ -115,23 +115,13 @@ func (t *DependencyManagementTask) Setup(ctx context.Context) error { Build(), } - if err := t.app.Issues.CreateIssues(ctx, t.depIssues, ""); err != nil { - return err - } - - t.setupIssue = NewIssueBuilder(). - WithTitle("Dependency Management"). - WithDescription(dependencyManagementDescription). - Build() - - return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") + return t.app.Issues.CreateIssues(ctx, t.depIssues, "") } func (t *DependencyManagementTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - taskIssue := t.setupIssue - issues, err := FetchIssues(ctx, t.app, t.setupIssue) + issues, err := FetchIssues(ctx, t.app) if err != nil { return expect.Fatal("Could not fetch issues") } @@ -157,12 +147,5 @@ func (t *DependencyManagementTask) Validate(ctx context.Context) ValidationFeedb } - if !expect.Valid() { - return expect.ValidationFeedback - } - - expect.Equal(taskIssue.Status, models.StatusClosed, - fmt.Sprintf("%s", taskIssue.Title)) - return expect.Complete() } diff --git a/cmd/pm/tasks/gitTask.go b/cmd/pm/tasks/gitTask.go index 40be4f6..3ce4f54 100644 --- a/cmd/pm/tasks/gitTask.go +++ b/cmd/pm/tasks/gitTask.go @@ -14,12 +14,14 @@ import ( const gitTaskDescription = `You are tasked with performing a Git operation. -This task will test your ability to use Git effectively within a project management workflow. Your goal is to modify a file in a Git repository and commit the change. +This task will test your ability to use Git effectively within a project management workflow. +Your goal is to modify a file in a Git repository and commit the change. Your task: -1. Set the Issue status to "In Progress" when you are ready to start. -2. A folder called "task" is created in the project directory when you start this task. Open it. -3. Inside the folder you will find README.md. Edit this file and add something to it (e.g. your name, a short note, or a new line). The file must be different from its original content. +1. Assign the given issue to yourself as 'Me'. +2. A folder called "task" is created in the project directory when you start this task. +3. Inside the folder you will find README.md. Edit this file and add something to it. + The file must be different from its original content. 4. Commit your change: - Open a terminal and change into the task folder. - Run "git add ." to stage the changes. @@ -102,7 +104,7 @@ func (t *GitTask) Setup(ctx context.Context) error { _ = os.WriteFile("./task/.gitattributes", []byte("* text=auto\n"), 0o644) t.setupIssue = NewIssueBuilder(). - WithTitle("Git Task Setup Issue"). + WithTitle("Upgrade the codebase"). WithDescription(gitTaskDescription). WithIssueType(models.TypeTask). Build() @@ -117,19 +119,14 @@ func (t *GitTask) Setup(ctx context.Context) error { func (t *GitTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - issues, err := FetchIssues(ctx, t.app, t.setupIssue) + issue, err := t.app.Issues.GetIssue(ctx, t.setupIssue.ID) if err != nil { - return expect.ValidationFeedback + return expect.Fatal("Could not fetch issue") } - _ = issues + expect.Equal(issue.Assignee, "Me", "Issue Assignee") - issue := t.setupIssue - - if issue.Status == models.StatusInProgress || gitTaskInProgress { - gitTaskInProgress = true - } else { - expect.Fail("The issue should be marked as In Progress while working on the Git task.") + if !expect.Valid() { return expect.ValidationFeedback } @@ -175,16 +172,19 @@ func (t *GitTask) Validate(ctx context.Context) ValidationFeedback { } expect.Assert(readmeContent != gitTaskReadmeContent, - "You should modify README.md content before committing (make appropriate changes to complete the task).") + "You should modify README.md content before committing") if wt, err := t.repo.Worktree(); err == nil { if status, err := wt.Status(); err == nil { - expect.Assert(status.IsClean(), "The working tree should be clean after committing (no unstaged changes).") + expect.Assert(status.IsClean(), "The working tree should be clean after committing") } } - expect.Assert(gitTaskInProgress && issue.Status == models.StatusClosed, - "The issue should be marked as Closed after completing the Git task.") + if !expect.Valid() { + return expect.ValidationFeedback + } + + expect.Equal(issue.Status, models.StatusClosed, "Issue Status") return expect.Complete() } diff --git a/cmd/pm/tasks/issueReviewCleanup.go b/cmd/pm/tasks/issueReviewCleanup.go index 69241e4..194d760 100644 --- a/cmd/pm/tasks/issueReviewCleanup.go +++ b/cmd/pm/tasks/issueReviewCleanup.go @@ -12,12 +12,12 @@ const issueReviewCleanupDescription = `You are responsible for reviewing and mai Using the system, complete the following steps: 1. Add a comment to two issues -2. Delete this cleanup task issue ("Issue Review and Cleanup Task") from the issue list — do not delete the other project issues` +2. Delete the issue titled "Delete this issue"` type IssueReviewCleanupTask struct { - done bool - app *App - setupIssue *models.Issue + done bool + app *App + reviewIssues []*models.Issue } func NewIssueReviewCleanupTask(app *App) *IssueReviewCleanupTask { @@ -49,10 +49,10 @@ func (t *IssueReviewCleanupTask) Setup(ctx context.Context) error { return err } - reviewIssues := []*models.Issue{ + t.reviewIssues = []*models.Issue{ models.NewIssueBuilder(). - WithTitle("Fix login page layout"). - WithDescription("The login form is misaligned on smaller screens. Needs responsive CSS adjustments."). + WithTitle("Delete this issue"). + WithDescription(""). WithPriority(2). WithStatus(models.StatusOpen). WithIssueType(models.TypeTask). @@ -87,22 +87,13 @@ func (t *IssueReviewCleanupTask) Setup(ctx context.Context) error { Build(), } - if err := t.app.Issues.CreateIssues(ctx, reviewIssues, ""); err != nil { - return err - } - - t.setupIssue = models.NewBaseIssue(). - WithTitle("Issue Review and Cleanup Task"). - WithDescription(issueReviewCleanupDescription). - Build() - - return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") + return t.app.Issues.CreateIssues(ctx, t.reviewIssues, "") } func (t *IssueReviewCleanupTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - issues, err := FetchIssues(ctx, t.app, t.setupIssue) + issues, err := FetchIssues(ctx, t.app) if err != nil { return expect.Fatal("Could not fetch issues") } @@ -123,16 +114,18 @@ func (t *IssueReviewCleanupTask) Validate(ctx context.Context) ValidationFeedbac } expect.Equal(commentsInIssues, 2, "Comments on issues") + expect.Equal(len(issues), len(t.reviewIssues)-1, "Remaining issues after cleanup") - // Fetching the setup issue as as FetchIssues only updates the setup issue if it exists, - // we can check if it was deleted by seeing if it can be fetched again - if t.setupIssue, err = t.app.Issues.GetIssue(ctx, t.setupIssue.ID); t.setupIssue != nil { - expect.Fail("Setup issue still exists") - } else { - expect.Pass("Setup issue deleted") + issue, err := t.app.Issues.GetIssue(ctx, t.reviewIssues[0].ID) + if err != nil { + return expect.Fatal("Deleted issue should not be found") } - expect.Equal(len(issues), 5, "Number of remaining issues") + if issue != nil { + expect.Fail("The issue titled 'Delete this issue' should have been deleted.") + } else { + expect.Pass("Issue deleted successfully") + } return expect.Complete() } diff --git a/cmd/pm/tasks/priorityManagement.go b/cmd/pm/tasks/priorityManagement.go index 2bb729b..338bd6e 100644 --- a/cmd/pm/tasks/priorityManagement.go +++ b/cmd/pm/tasks/priorityManagement.go @@ -17,16 +17,14 @@ The database is not working properly and users are not able to connect and acces You need to rebalance the current sprint priorities: -1. Assign the task Issue you are currently reading to yourself as "Me" and set status to "In Progress". -2. A new issue has appeared in the list that needs urgent attention. Change the database related issue's priority to 4 (critical). -3. Set the priority of the feature and chore issues in the list to 1 (low). -4. Change this issue status to "Closed". +1. A new issue has appeared in the list that needs urgent attention. + Change the database related issue's priority to 4 (critical). +2. Set the priority of the feature and chore issues in the list to 1 (low). ` type PriorityManagementTask struct { done bool app *App - setupIssue *Issue priorityIssues []*models.Issue isInProgress bool } @@ -111,38 +109,17 @@ func (t *PriorityManagementTask) Setup(ctx context.Context) error { Build(), } - if err := t.app.Issues.CreateIssues(ctx, t.priorityIssues, ""); err != nil { - return err - } - - t.setupIssue = NewIssueBuilder(). - WithTitle("Priority Rebalancing"). - WithDescription(priorityManagementDescription). - Build() - - return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") + return t.app.Issues.CreateIssues(ctx, t.priorityIssues, "") } func (t *PriorityManagementTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - issues, err := FetchIssues(ctx, t.app, t.setupIssue) + issues, err := FetchIssues(ctx, t.app) if err != nil { return expect.Fatal("Failed to fetch issues for validation") } - expect.NotEmptyAndEqual(t.setupIssue.Assignee, "Me", - fmt.Sprintf("%s assignee", t.setupIssue.Title)) - - if t.setupIssue.Status != models.StatusClosed { - expect.Equal(t.setupIssue.Status, models.StatusInProgress, - fmt.Sprintf("%s status", t.setupIssue.Title)) - } - - if !expect.Valid() { - return expect.ValidationFeedback - } - for _, issue := range issues { if issue.Title == t.priorityIssues[0].Title { expect.Equal(issue.Priority, 4, @@ -153,8 +130,5 @@ func (t *PriorityManagementTask) Validate(ctx context.Context) ValidationFeedbac } } - expect.Equal(t.setupIssue.Status, models.StatusClosed, - fmt.Sprintf("%s status", t.setupIssue.Title)) - return expect.Complete() } diff --git a/cmd/pm/tasks/sprintPlanning.go b/cmd/pm/tasks/sprintPlanning.go index 68330ca..7d4c87b 100644 --- a/cmd/pm/tasks/sprintPlanning.go +++ b/cmd/pm/tasks/sprintPlanning.go @@ -23,9 +23,8 @@ Your task: The goal is to create a realistic sprint plan that delivers value while respecting team capacity.` type SprintPlanningTask struct { - done bool - app *App - setupIssue *Issue + done bool + app *App } func NewSprintPlanningTask(app *App) *SprintPlanningTask { @@ -109,35 +108,21 @@ func (t *SprintPlanningTask) Setup(ctx context.Context) error { Build(), } - if err := t.app.Issues.CreateIssues(ctx, backlogIssues, ""); err != nil { - return err - } - - t.setupIssue = NewIssueBuilder(). - WithTitle("Sprint Planning - Week 1"). - WithDescription(sprintPlanningDescription). - Build() - - return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") + return t.app.Issues.CreateIssues(ctx, backlogIssues, "") } func (t *SprintPlanningTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - issues, err := FetchIssues(ctx, t.app, t.setupIssue) + issues, err := FetchIssues(ctx, t.app) if err != nil { return expect.ValidationFeedback } - if len(issues) == 0 { - expect.Fail("No backlog issues found to plan a sprint with.") - return expect.ValidationFeedback - } - // Sort by priority ascending (0 is highest priority). sorted := make([]*models.Issue, len(issues)) copy(sorted, issues) - for i := 0; i < len(sorted); i++ { + for i := range sorted { for j := i + 1; j < len(sorted); j++ { if sorted[j].Priority < sorted[i].Priority { sorted[i], sorted[j] = sorted[j], sorted[i] @@ -145,10 +130,7 @@ func (t *SprintPlanningTask) Validate(ctx context.Context) ValidationFeedback { } } - topN := 5 - if len(sorted) < topN { - topN = len(sorted) - } + topN := min(len(sorted), 5) top := sorted[:topN] var plannedCount int diff --git a/internal/commands/issues/update.go b/internal/commands/issues/update.go index 29a6b29..3c2bb7a 100644 --- a/internal/commands/issues/update.go +++ b/internal/commands/issues/update.go @@ -6,6 +6,7 @@ import ( "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/shellcomp" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var updateFlags Flags @@ -52,6 +53,13 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { cmd.Printf("Updated issue to:\n%s", models.IssueString(*updatedIssue)) + updateFlags = Flags{} + + cmd.Flags().VisitAll(func(f *pflag.Flag) { + f.Changed = false + _ = f.Value.Set(f.DefValue) + }) + return nil } From fbbb3dc5c618c6e0813eca460bb81f2849897556 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 19 Mar 2026 15:22:09 +0100 Subject: [PATCH 23/73] update .dockerignore to dont have .env files --- .dockerignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.dockerignore b/.dockerignore index 97fdde2..5547959 100644 --- a/.dockerignore +++ b/.dockerignore @@ -18,6 +18,9 @@ package.json *.db *.ext +.env +.env.example + Makefile # Added by goreleaser init: From 5bd728fe4e6e0a63a927db3a683e6e726f7bf41d Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:49:17 +0100 Subject: [PATCH 24/73] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/gitTask.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/pm/tasks/gitTask.go b/cmd/pm/tasks/gitTask.go index 3ce4f54..c19a329 100644 --- a/cmd/pm/tasks/gitTask.go +++ b/cmd/pm/tasks/gitTask.go @@ -123,6 +123,10 @@ func (t *GitTask) Validate(ctx context.Context) ValidationFeedback { if err != nil { return expect.Fatal("Could not fetch issue") } + if issue == nil { + expect.Fail("Issue could not be found. It may have been deleted; please recreate it and try again.") + return expect.ValidationFeedback + } expect.Equal(issue.Assignee, "Me", "Issue Assignee") From 256b3ed2dbb45e1a37b5525641730bba16b3b28a Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:49:34 +0100 Subject: [PATCH 25/73] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- internal/utils/check/expect.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/utils/check/expect.go b/internal/utils/check/expect.go index eba9385..fdb410f 100644 --- a/internal/utils/check/expect.go +++ b/internal/utils/check/expect.go @@ -111,11 +111,11 @@ func (e *Expector) NotNil(value any, message string) *Expector { func (e *Expector) Contains(s, substr, message string) *Expector { if !strings.Contains(s, substr) { return e.Fail(fmt.Sprintf(`%s expected to contain "%v", but it does not.`, message, substr)) - } else { - return e.Pass(fmt.Sprintf(`%s contains "%v"`, message, substr)) } + return e.Pass(fmt.Sprintf(`%s contains "%v"`, message, substr)) } + func (e *Expector) NotContains(s, substr, message string) *Expector { check := NewCheck(message, !strings.Contains(s, substr)) e.Checks = append(e.Checks, check) From d07d1ecfc1014df9b21ca35da7f09e13254e0bed Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:50:30 +0100 Subject: [PATCH 26/73] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/codingTask.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 12d5e34..592cfed 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -17,10 +17,10 @@ The MongoDB Driver dependency in the file is outdated and needs to be updated to This is a common task for developers, and it requires attention to detail and the ability to follow instructions carefully. Your task: -1. Assingn the given issue to yourself as 'Me'. +1. Assign the given issue to yourself as 'Me'. 2. A file will appear in the current directory named "code.txt". Open it and follow the instructions inside. And save the file after you are done. -3. When you are done, mark this and the issue you made as "Closed".` +3. When you are done, mark this task as "Closed".` var textFileDescription = ` From 8bcbb37decbf4895e174a3ada9dd93a183cadd4e Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:50:43 +0100 Subject: [PATCH 27/73] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/codingTask.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 592cfed..f01577f 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -137,6 +137,9 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { if err != nil { return expect.Fatal("Could not fetch issues") } + if issue == nil { + return expect.Fatal("Issue was deleted or could not be found") + } expect.Equal(issue.Assignee, "Me", "Issue Assignee") From 35114a3e0f4b69ed25ba761486ed535a0eccc1e7 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 19 Mar 2026 21:12:28 +0100 Subject: [PATCH 28/73] make web submit when action done by user --- Task Details.txt | 12 ++++++++++++ go.mod | 2 +- pkg/web/components/modal.templ | 8 +++++++- pkg/web/components/modal_templ.go | 12 ++++++------ pkg/web/components/status.templ | 6 +++--- pkg/web/components/status_templ.go | 17 +++++++++++++++-- pkg/web/handler/task.go | 8 ++++++-- pkg/web/routes/layout.templ | 2 +- pkg/web/routes/layout_templ.go | 2 +- 9 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 Task Details.txt diff --git a/Task Details.txt b/Task Details.txt new file mode 100644 index 0000000..c3e26ee --- /dev/null +++ b/Task Details.txt @@ -0,0 +1,12 @@ +You are tasked with backlog refinement. + +The product backlog has become cluttered with old and unclear issues. You need to groom the backlog: + +1. Go to the backlog. +2. Find two issues that got the same name or describe the same problem. +3. Open one of these issues. +4. Select "Close issue" +5. Choose "Duplicate issue" as closing reason. +6. Save/close issue. + +Focus on making the backlog a reliable source of upcoming work. \ No newline at end of file diff --git a/go.mod b/go.mod index 823223e..e5a75b7 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-git/go-git/v6 v6.0.0-20260317113930-fb0d09929504 github.com/joho/godotenv v1.5.1 github.com/muesli/reflow v0.3.0 + github.com/spf13/pflag v1.0.10 github.com/steveyegge/beads v0.49.6 go.mongodb.org/mongo-driver/v2 v2.5.0 ) @@ -97,7 +98,6 @@ require ( github.com/sergi/go-diff v1.4.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tetratelabs/wazero v1.11.0 // indirect diff --git a/pkg/web/components/modal.templ b/pkg/web/components/modal.templ index 454cbab..a7e862b 100644 --- a/pkg/web/components/modal.templ +++ b/pkg/web/components/modal.templ @@ -46,7 +46,13 @@ templ Modal(props ModalProps) { } templ ModalContainer() { - + } type ConfirmModalProps struct { diff --git a/pkg/web/components/modal_templ.go b/pkg/web/components/modal_templ.go index 70939c0..4dc498e 100644 --- a/pkg/web/components/modal_templ.go +++ b/pkg/web/components/modal_templ.go @@ -143,7 +143,7 @@ func ModalContainer() templ.Component { templ_7745c5c3_Var7 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -189,7 +189,7 @@ func ConfirmModal(props ConfirmModalProps) templ.Component { var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 76, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 82, Col: 73} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -202,7 +202,7 @@ func ConfirmModal(props ConfirmModalProps) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(props.Message) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 80, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 86, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -216,7 +216,7 @@ func ConfirmModal(props ConfirmModalProps) templ.Component { var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(props.CancelText) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 89, Col: 25} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 95, Col: 25} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -257,7 +257,7 @@ func ConfirmModal(props ConfirmModalProps) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(props.ConfirmAction) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 97, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 103, Col: 37} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -271,7 +271,7 @@ func ConfirmModal(props ConfirmModalProps) templ.Component { var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(props.ConfirmText) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 102, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 108, Col: 26} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { diff --git a/pkg/web/components/status.templ b/pkg/web/components/status.templ index ad255c0..0555f98 100644 --- a/pkg/web/components/status.templ +++ b/pkg/web/components/status.templ @@ -1,7 +1,7 @@ package components -templ Status() { -
- +templ Status(msg string) { +
+
} diff --git a/pkg/web/components/status_templ.go b/pkg/web/components/status_templ.go index 41694c7..f46932f 100644 --- a/pkg/web/components/status_templ.go +++ b/pkg/web/components/status_templ.go @@ -8,7 +8,7 @@ package components import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -func Status() templ.Component { +func Status(msg string) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -29,7 +29,20 @@ func Status() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/handler/task.go b/pkg/web/handler/task.go index e5e79da..36c3c1a 100644 --- a/pkg/web/handler/task.go +++ b/pkg/web/handler/task.go @@ -30,11 +30,15 @@ func HandleTaskStatus(w http.ResponseWriter, r *http.Request) { hx := HTMX(r) if hx.IsHxRequest() { - hx.WriteString(` + if taskFeedback.Success { + w.Header().Set("HX-Trigger", "task-status-success") + w.WriteHeader(http.StatusOK) + } else { + hx.WriteString(`
`) - + } return } diff --git a/pkg/web/routes/layout.templ b/pkg/web/routes/layout.templ index caa7a8d..7b464a2 100644 --- a/pkg/web/routes/layout.templ +++ b/pkg/web/routes/layout.templ @@ -30,7 +30,7 @@ var baseLayout = components.LayoutProps{ }, Header: components.HeaderProps{ Title: "LazyPM", - NavbarEnd: components.Status(), + NavbarEnd: components.Status(""), }, Modal: components.ModalContainer(), } diff --git a/pkg/web/routes/layout_templ.go b/pkg/web/routes/layout_templ.go index a47058e..e3d833a 100644 --- a/pkg/web/routes/layout_templ.go +++ b/pkg/web/routes/layout_templ.go @@ -38,7 +38,7 @@ var baseLayout = components.LayoutProps{ }, Header: components.HeaderProps{ Title: "LazyPM", - NavbarEnd: components.Status(), + NavbarEnd: components.Status(""), }, Modal: components.ModalContainer(), } From 3bc97024c3a5dd4ba049bb47b1fa6ece1042b1bc Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 19 Mar 2026 22:08:13 +0100 Subject: [PATCH 29/73] make repl submit each command --- pkg/repl/executor.go | 2 +- pkg/repl/repl.go | 71 ++++++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/pkg/repl/executor.go b/pkg/repl/executor.go index eac6855..d12a89c 100644 --- a/pkg/repl/executor.go +++ b/pkg/repl/executor.go @@ -8,7 +8,7 @@ import ( ) // execute processes the input command and returns the output -func execute(input string) (string, error) { +func (r *REPL) execute(input string) (string, error) { if input == "" { return "", nil } diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index bfc825b..abe2cb8 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -29,10 +29,11 @@ Type 'status' to check task progress.` ) type REPL struct { - feedbackChan chan ValidationFeedback - quitChan chan bool - submitChan chan<- struct{} - app *App + feedbackChan chan ValidationFeedback + quitChan chan bool + submitChan chan<- struct{} + completionChan chan struct{} + app *App currentFeedback ValidationFeedback exitRequested bool @@ -49,6 +50,7 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { r.taskCompleted = false r.exitRequested = false r.currentFeedback = ValidationFeedback{} + r.completionChan = make(chan struct{}, 1) // Set terminal to raw mode to capture input properly in the REPL. // This allows us to handle input character by character and provide a better user experience. @@ -89,28 +91,33 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { // Start the REPL loop, which continues until the user types "exit" or "quit" or task completes. reader := bufio.NewReader(os.Stdin) +replLoop: for !r.exitRequested { - // Check if task completed - wait for Enter before exiting - if r.taskCompleted { - fmt.Println(style.TitleStyle.Render("Task completed successfully!")) - fmt.Print("Press Enter to exit...") - // Restore terminal to normal mode for input - term.Restore(int(os.Stdin.Fd()), oldState) - reader.ReadString('\n') - break - } - // Check if we should exit before prompting (non-blocking check) if r.exitRequested { break } // Prompt the user for input, and provide suggestions. - input := prompt.Input( - PromptPrefix, - completer, - promptOptions(history)..., - ) + inputChan := make(chan string, 1) + go func(hist []string) { + inputChan <- prompt.Input( + PromptPrefix, + completer, + promptOptions(hist)..., + ) + }(history) + + var input string + select { + case input = <-inputChan: + case <-r.completionChan: + fmt.Println(style.TitleStyle.Render("Task completed successfully!")) + fmt.Print("Press Enter to exit...") + term.Restore(int(os.Stdin.Fd()), oldState) + reader.ReadString('\n') + break replLoop + } // Check again after prompt returns (in case validation completed while waiting) if r.exitRequested { @@ -118,15 +125,6 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { } // Check if task completed while waiting at prompt - if r.taskCompleted { - fmt.Println(style.TitleStyle.Render("Task completed successfully!")) - fmt.Print("Press Enter to exit...") - // Restore terminal to normal mode for input - term.Restore(int(os.Stdin.Fd()), oldState) - reader.ReadString('\n') - break - } - // Trim whitespace from the input to ensure consistent command processing. input = strings.TrimSpace(input) @@ -144,7 +142,16 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { // Add the input to the history for future navigation. history = append(history, input) - output, err := execute(input) + output, err := r.execute(input) + + // Send submit signal to trigger validation after any command + if r.submitChan != nil { + select { + case r.submitChan <- struct{}{}: + default: + } + } + if err != nil { // Show command output (even on error) in normal text style if output != "" { @@ -177,6 +184,12 @@ func (r *REPL) watchValidation() { } if feedback.Success { r.taskCompleted = true + if r.completionChan != nil { + select { + case r.completionChan <- struct{}{}: + default: + } + } return } case <-r.quitChan: From d78e9d4ded095d4c2456c982835aea4703d4fe06 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 10:46:24 +0100 Subject: [PATCH 30/73] clean up help bar --- pkg/tui/components/helpbar.go | 205 +++++++++++++++++++++++++--------- 1 file changed, 151 insertions(+), 54 deletions(-) diff --git a/pkg/tui/components/helpbar.go b/pkg/tui/components/helpbar.go index 1facdc9..873a88c 100644 --- a/pkg/tui/components/helpbar.go +++ b/pkg/tui/components/helpbar.go @@ -1,6 +1,8 @@ package components import ( + "strings" + "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" ) @@ -12,21 +14,21 @@ const ( ViewKanban ) -type ShortItem struct { +const ( + fullHelpKeyWidth = 10 + fullHelpDescWidth = 18 + fullHelpColGap = 4 + maxFullHelpCols = 4 +) + +type HelpItem struct { Key string Desc string } -type FullRow struct { - LeftKey string - LeftDesc string - RightKey string - RightDesc string -} - type HelpBarConfig struct { - ShortItems []ShortItem - FullRows []FullRow + ShortItems []HelpItem + FullItems []HelpItem } type HelpBar struct { @@ -37,7 +39,10 @@ type HelpBar struct { } func NewHelpBar(view ViewKind) HelpBar { - return HelpBar{view: view, config: helpBarConfig(view)} + return HelpBar{ + view: view, + config: helpBarConfig(view), + } } func (h *HelpBar) SetWidth(width int) { @@ -48,18 +53,25 @@ func (h HelpBar) View() string { if h.width == 0 { return "" } + if h.showAll { return h.fullHelp() } + return h.shortHelp() } func (h HelpBar) shortHelp() string { - keys := make([]string, 0, len(h.config.ShortItems)) + items := make([]string, 0, len(h.config.ShortItems)) for _, item := range h.config.ShortItems { - keys = append(keys, styles.HighlightKey(item.Key)+item.Desc+" ") + items = append( + items, + styles.HighlightKey(item.Key)+item.Desc+" ", + ) } - content := lipgloss.JoinHorizontal(lipgloss.Left, keys...) + + content := lipgloss.JoinHorizontal(lipgloss.Left, items...) + return lipgloss.NewStyle(). Border(lipgloss.Border{Top: "─"}, true, false, false, false). BorderForeground(styles.SecondaryBorder). @@ -69,29 +81,60 @@ func (h HelpBar) shortHelp() string { } func (h HelpBar) fullHelp() string { - keyStyle := lipgloss.NewStyle().Width(8).Align(lipgloss.Right) - descStyle := lipgloss.NewStyle().Width(12) + if len(h.config.FullItems) == 0 { + return "" + } - renderHelpItem := func(key, desc string) string { - return lipgloss.JoinHorizontal( - lipgloss.Left, - keyStyle.Render(styles.HighlightKey(key)), - " ", - descStyle.Render(desc), + keyStyle := lipgloss.NewStyle(). + Width(fullHelpKeyWidth). + Align(lipgloss.Right) + + descStyle := lipgloss.NewStyle(). + Width(fullHelpDescWidth) + + cellStyle := lipgloss.NewStyle(). + Width(fullHelpKeyWidth + 1 + fullHelpDescWidth) + + renderHelpItem := func(item HelpItem) string { + return cellStyle.Render( + lipgloss.JoinHorizontal( + lipgloss.Left, + keyStyle.Render(styles.HighlightKey(item.Key)), + " ", + descStyle.Render(item.Desc), + ), ) } - renderRow := func(leftKey, leftDesc, rightKey, rightDesc string) string { - leftItem := renderHelpItem(leftKey, leftDesc) - rightItem := renderHelpItem(rightKey, rightDesc) - return lipgloss.JoinHorizontal(lipgloss.Left, leftItem, " ", rightItem) + innerWidth := h.width - 2 + if innerWidth < 1 { + innerWidth = 1 } - rows := make([]string, 0, len(h.config.FullRows)) - for _, r := range h.config.FullRows { - rows = append(rows, renderRow(r.LeftKey, r.LeftDesc, r.RightKey, r.RightDesc)) + cellWidth := fullHelpKeyWidth + 1 + fullHelpDescWidth + cols := fitHelpColumns( + innerWidth, + cellWidth, + fullHelpColGap, + maxFullHelpCols, + ) + + gap := strings.Repeat(" ", fullHelpColGap) + rows := make([]string, 0, (len(h.config.FullItems)+cols-1)/cols) + + for i := 0; i < len(h.config.FullItems); i += cols { + end := min(i + cols, len(h.config.FullItems)) + + cells := make([]string, 0, cols) + for _, item := range h.config.FullItems[i:end] { + cells = append(cells, renderHelpItem(item)) + } + + rows = append(rows, joinHorizontalWithGap(cells, gap)) } + content := lipgloss.JoinVertical(lipgloss.Left, rows...) + return lipgloss.NewStyle(). Border(lipgloss.Border{Top: "─"}, true, false, false, false). BorderForeground(styles.SecondaryBorder). @@ -104,6 +147,7 @@ func (h HelpBar) Height() int { if h.width == 0 { return 0 } + return lipgloss.Height(h.View()) } @@ -115,11 +159,43 @@ func (h *HelpBar) ToggleHelp() { h.showAll = !h.showAll } +func fitHelpColumns( + availableWidth int, + cellWidth int, + gapWidth int, + maxCols int, +) int { + for cols := maxCols; cols >= 1; cols-- { + neededWidth := cols*cellWidth + (cols-1)*gapWidth + if neededWidth <= availableWidth { + return cols + } + } + + return 1 +} + +func joinHorizontalWithGap(cells []string, gap string) string { + if len(cells) == 0 { + return "" + } + + parts := make([]string, 0, len(cells)*2-1) + for i, cell := range cells { + if i > 0 { + parts = append(parts, gap) + } + parts = append(parts, cell) + } + + return lipgloss.JoinHorizontal(lipgloss.Left, parts...) +} + func helpBarConfig(view ViewKind) HelpBarConfig { switch view { case ViewIssues: return HelpBarConfig{ - ShortItems: []ShortItem{ + ShortItems: []HelpItem{ {Key: "tab", Desc: "switch"}, {Key: "v", Desc: "kanban"}, {Key: "↑/k", Desc: "up"}, @@ -133,23 +209,33 @@ func helpBarConfig(view ViewKind) HelpBarConfig { {Key: "q", Desc: "quit"}, {Key: "?", Desc: "help"}, }, - FullRows: []FullRow{ - {LeftKey: "tab", LeftDesc: "switch window", RightKey: "↑/k", RightDesc: "up"}, - {LeftKey: "enter", LeftDesc: "view issue", RightKey: "↓/j", RightDesc: "down"}, - {LeftKey: "pgup", LeftDesc: "page up", RightKey: "pgdn", RightDesc: "page down"}, - {LeftKey: "b", LeftDesc: "back to list", RightKey: "a", RightDesc: "add issue"}, - {LeftKey: "c", LeftDesc: "add comment", RightKey: "", RightDesc: ""}, - {LeftKey: "e", LeftDesc: "edit title", RightKey: "d", RightDesc: "edit description"}, - {LeftKey: "s", LeftDesc: "change status", RightKey: "p", RightDesc: "change priority"}, - {LeftKey: "t", LeftDesc: "change type", RightKey: "A", RightDesc: "change assignee"}, - {LeftKey: "x", LeftDesc: "delete issue", RightKey: "v", RightDesc: "kanban"}, - {LeftKey: "q", LeftDesc: "quit", RightKey: "S", RightDesc: "submit"}, - {LeftKey: "?", LeftDesc: "help", RightKey: "", RightDesc: ""}, + FullItems: []HelpItem{ + {Key: "tab", Desc: "switch window"}, + {Key: "enter", Desc: "view issue"}, + {Key: "b", Desc: "back to list"}, + {Key: "v", Desc: "kanban"}, + {Key: "↑/k", Desc: "up"}, + {Key: "↓/j", Desc: "down"}, + {Key: "pgup", Desc: "page up"}, + {Key: "pgdn", Desc: "page down"}, + {Key: "a", Desc: "add issue"}, + {Key: "c", Desc: "add comment"}, + {Key: "x", Desc: "delete issue"}, + {Key: "S", Desc: "submit"}, + {Key: "e", Desc: "edit title"}, + {Key: "d", Desc: "edit description"}, + {Key: "s", Desc: "change status"}, + {Key: "p", Desc: "change priority"}, + {Key: "t", Desc: "change type"}, + {Key: "A", Desc: "change assignee"}, + {Key: "q", Desc: "quit"}, + {Key: "?", Desc: "help"}, }, } + case ViewKanban: return HelpBarConfig{ - ShortItems: []ShortItem{ + ShortItems: []HelpItem{ {Key: "v", Desc: "list view"}, {Key: "↑/k", Desc: "up"}, {Key: "↓/j", Desc: "down"}, @@ -163,19 +249,30 @@ func helpBarConfig(view ViewKind) HelpBarConfig { {Key: "S", Desc: "submit"}, {Key: "?", Desc: "help"}, }, - FullRows: []FullRow{ - {LeftKey: "v", LeftDesc: "list view", RightKey: "↑/k", RightDesc: "up"}, - {LeftKey: "enter", LeftDesc: "view issue", RightKey: "↓/j", RightDesc: "down"}, - {LeftKey: "pgup", LeftDesc: "page up", RightKey: "pgdn", RightDesc: "page down"}, - {LeftKey: "h/l", LeftDesc: "switch column", RightKey: "←/→", RightDesc: "move issue"}, - {LeftKey: "b", LeftDesc: "back to list", RightKey: "a", RightDesc: "add issue"}, - {LeftKey: "e", LeftDesc: "edit title", RightKey: "d", RightDesc: "edit description"}, - {LeftKey: "s", LeftDesc: "change status", RightKey: "p", RightDesc: "change priority"}, - {LeftKey: "t", LeftDesc: "change type", RightKey: "A", RightDesc: "change assignee"}, - {LeftKey: "x", LeftDesc: "delete issue", RightKey: "q", RightDesc: "quit"}, - {LeftKey: "S", LeftDesc: "submit", RightKey: "?", RightDesc: "help"}, + FullItems: []HelpItem{ + {Key: "v", Desc: "list view"}, + {Key: "enter", Desc: "view issue"}, + {Key: "b", Desc: "back to list"}, + {Key: "h/l", Desc: "switch column"}, + {Key: "←/→", Desc: "move issue"}, + {Key: "↑/k", Desc: "up"}, + {Key: "↓/j", Desc: "down"}, + {Key: "pgup", Desc: "page up"}, + {Key: "pgdn", Desc: "page down"}, + {Key: "a", Desc: "add issue"}, + {Key: "x", Desc: "delete issue"}, + {Key: "S", Desc: "submit"}, + {Key: "e", Desc: "edit title"}, + {Key: "d", Desc: "edit description"}, + {Key: "s", Desc: "change status"}, + {Key: "p", Desc: "change priority"}, + {Key: "t", Desc: "change type"}, + {Key: "A", Desc: "change assignee"}, + {Key: "q", Desc: "quit"}, + {Key: "?", Desc: "help"}, }, } + default: return HelpBarConfig{} } From 977d0e2df0a0fbc65c621558cc6fb8b95ca503d2 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 10:47:27 +0100 Subject: [PATCH 31/73] users should understand how to view details --- pkg/tui/components/modals.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pkg/tui/components/modals.go b/pkg/tui/components/modals.go index 156d561..b204001 100644 --- a/pkg/tui/components/modals.go +++ b/pkg/tui/components/modals.go @@ -3,6 +3,7 @@ package components import ( "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/style" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" ) @@ -202,9 +203,6 @@ func RenderModals( return mainView } -// RenderFooter renders the shared footer with the help bar and optional -// validation feedback message. - // truncateToWidth trims the given text so that its rendered width does not // exceed maxWidth. If truncation occurs and there is room, an ellipsis is // appended to indicate that the text was shortened. @@ -237,12 +235,14 @@ func truncateToWidth(text string, maxWidth int) string { return current + ellipsis } +// RenderFooter renders the shared footer with the help bar and optional +// validation feedback message. func RenderFooter(width int, helpBar *HelpBar, feedback models.ValidationFeedback) string { feedbackStatus := feedback.Message // Ensure the feedback message does not exceed the total available width. if feedback.Message != "" { - feedbackStatus = truncateToWidth(feedbackStatus+" [Press Shift+S to re-submit]", width) + feedbackStatus = truncateToWidth(style.ErrorStyle.Render(feedbackStatus+" [Press '?' for details]"), width) if helpBar.IsExpanded() && feedbackStatus != "" { for _, check := range feedback.Checks { @@ -254,10 +254,7 @@ func RenderFooter(width int, helpBar *HelpBar, feedback models.ValidationFeedbac } // Ensure each check line does not exceed the available width. - remainingWidth := width - lipgloss.Width(prefix) - if remainingWidth < 0 { - remainingWidth = 0 - } + remainingWidth := max(width-lipgloss.Width(prefix), 0) truncatedMsg := truncateToWidth(check.Message, remainingWidth) feedbackStatus += "\n" + prefix + truncatedMsg @@ -269,10 +266,7 @@ func RenderFooter(width int, helpBar *HelpBar, feedback models.ValidationFeedbac return helpBar.View() } - helpWidth := width - lipgloss.Width(feedbackStatus) - if helpWidth < 0 { - helpWidth = 0 - } + helpWidth := max(width-lipgloss.Width(feedbackStatus), 0) helpBar.SetWidth(helpWidth) return lipgloss.JoinHorizontal(lipgloss.Left, helpBar.View(), feedbackStatus) From 3e7ab044a6f25d399575aae196ff99ae7e4468d9 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 10:47:55 +0100 Subject: [PATCH 32/73] sent submission at init to make submission lablel appear --- pkg/tui/views/dashboard/model.go | 6 +++++- pkg/tui/views/kanban/model.go | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index bc75e80..a21f5e0 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -5,7 +5,7 @@ import ( "charm.land/bubbles/v2/textarea" "charm.land/bubbles/v2/textinput" - "charm.land/bubbletea/v2" + tea "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" @@ -193,6 +193,10 @@ func (m *Model) startEditAssignee(selected ListIssue) { } func (m *Model) Init() tea.Cmd { + if m.submitChan != nil { + m.submitChan <- struct{}{} + m.logAction("tui submitted validation") + } return components.ListenForValidation(m.feedbackChan) } diff --git a/pkg/tui/views/kanban/model.go b/pkg/tui/views/kanban/model.go index 57e371e..8c2568e 100644 --- a/pkg/tui/views/kanban/model.go +++ b/pkg/tui/views/kanban/model.go @@ -5,7 +5,7 @@ import ( "charm.land/bubbles/v2/textarea" "charm.land/bubbles/v2/textinput" - "charm.land/bubbletea/v2" + tea "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" @@ -169,7 +169,20 @@ func (m *Model) startEditAssignee(selected ListIssue) { m.assigneeInput.CursorEnd() } +func (m *Model) logAction(action string) { + if m.app != nil { + m.app.LogAction(models.EncodeActionEvent(models.ActionEvent{ + Source: "tui", + Action: action, + })) + } +} + func (m *Model) Init() tea.Cmd { + if m.submitChan != nil { + m.submitChan <- struct{}{} + m.logAction("tui submitted validation") + } return components.ListenForValidation(m.feedbackChan) } From 41766284acce563c730390b61325d4c235e2cc13 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 10:48:25 +0100 Subject: [PATCH 33/73] remove unused funcs and submit on issue refreshes --- pkg/tui/views/dashboard/operations.go | 71 ++++----------------------- 1 file changed, 10 insertions(+), 61 deletions(-) diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 2db5467..5f0c9be 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -6,7 +6,7 @@ import ( "os/user" "charm.land/bubbles/v2/list" - "charm.land/bubbletea/v2" + tea "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" @@ -78,66 +78,6 @@ func addIssueCommentCmd(app *app.App, issueID, author, text string) tea.Cmd { } } -func updateIssueTitleCmd(app *app.App, issueID, newTitle string) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"title": newTitle} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return issueTitleUpdatedMsg{IssueID: issueID, Err: err} - } -} - -func updateIssueDescriptionCmd(app *app.App, issueID, newDescription string) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"description": newDescription} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return issueDescriptionUpdatedMsg{IssueID: issueID, Err: err} - } -} - -func updateIssueStatusCmd(app *app.App, issueID, status string) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"status": status} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return issueStatusUpdatedMsg{IssueID: issueID, Err: err} - } -} - -func updateIssuePriorityCmd(app *app.App, issueID string, priority int) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"priority": priority} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return issuePriorityUpdatedMsg{IssueID: issueID, Err: err} - } -} - -func updateIssueTypeCmd(app *app.App, issueID string, issueType models.IssueType) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"issue_type": string(issueType)} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return issueTypeUpdatedMsg{IssueID: issueID, Err: err} - } -} - -func createIssueCmd(app *app.App, title string) tea.Cmd { - return func() tea.Msg { - issue := &models.Issue{ - Title: title, - Status: models.StatusOpen, - IssueType: models.TypeTask, - Priority: 2, - } - err := app.Issues.CreateIssue(context.Background(), issue, "tui") - return issueCreatedMsg{Issue: issue, Err: err} - } -} - -func deleteIssueCmd(app *app.App, issueID string, currentIndex int) tea.Cmd { - return func() tea.Msg { - err := app.Issues.DeleteIssue(context.Background(), issueID) - return issueDeletedMsg{IssueID: issueID, Err: err, PreviousIndex: currentIndex} - } -} - func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { /* update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. @@ -154,6 +94,15 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { break } } + + if m.submitChan != nil { + select { + case m.submitChan <- struct{}{}: + m.logAction("tui submitted validation") + default: + } + } + return tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return issues.SelectIssueMsg{IssueID: issueID} }) } From c5bcc670bd7f65d3fa0d685e40a84c58f679756d Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 10:49:16 +0100 Subject: [PATCH 34/73] submit no list refreshes kanban --- pkg/tui/views/kanban/operations.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index 144e144..1ae5883 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -55,6 +55,14 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { m.blockedList.SelectIssueID(issueID) m.doneList.SelectIssueID(issueID) + if m.submitChan != nil { + select { + case m.submitChan <- struct{}{}: + m.logAction("tui submitted validation") + default: + } + } + return tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd) } From aa9de18c9977111ea9b2b5749f7c82e6a7232f3c Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 10:49:44 +0100 Subject: [PATCH 35/73] comment out closed issues list, will find a better way to show closed issues --- pkg/tui/views/dashboard/view.go | 35 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index 569cff7..539b91e 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -1,7 +1,7 @@ package dashboard import ( - "charm.land/bubbletea/v2" + tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" @@ -24,21 +24,18 @@ func (m *Model) View() tea.View { // To avoid layer overflow or clipping, the label heights are calculated and subtracted from the available height before calculating the list heights to avoid layout overflow or clipping. contentHeight := m.height - headerHeight - footerHeight - mainLabel := styles.LabelStyle.Render("Display issues") - closedLabel := styles.LabelStyle.Render("Closed issues") - labelHeight := lipgloss.Height(mainLabel) + lipgloss.Height(closedLabel) - availableForLists := contentHeight - labelHeight - halfHeight := availableForLists / 2 - if halfHeight < 1 { - halfHeight = 1 - } + //mainLabel := styles.LabelStyle.Render("Display issues") + //closedLabel := styles.LabelStyle.Render("Closed issues") + //labelHeight := lipgloss.Height(mainLabel) + lipgloss.Height(closedLabel) + availableForLists := contentHeight + halfHeight := max(availableForLists / 2, 1) totalContentWidth := m.width - 1 listWidth := totalContentWidth * styles.ListViewRatio / 100 detailWidth := totalContentWidth - listWidth m.issueList.SetSize(listWidth, halfHeight) - m.closedIssueList.SetSize(listWidth, halfHeight) + //m.closedIssueList.SetSize(listWidth, halfHeight) m.issueDetail.SetSize(detailWidth, contentHeight) // Only highlight the focused list; unfocused list should not show selection highlight. @@ -46,17 +43,19 @@ func (m *Model) View() tea.View { m.closedIssueList.SetHighlightSelected(m.focusedWindow == 1 && m.focusedPaneClosed == 0) listView := m.issueList.View() - closedListView := m.closedIssueList.View() + //closedListView := m.closedIssueList.View() detailView := m.issueDetail.View() - if m.focusedWindow == 0 { - mainLabel = lipgloss.NewStyle().Foreground(styles.Primary).Bold(true).Render("Display issues ▶") - } else { - closedLabel = lipgloss.NewStyle().Foreground(styles.Primary).Bold(true).Render("Closed issues ▶") - } + //if m.focusedWindow == 0 { + // mainLabel = lipgloss.NewStyle().Foreground(styles.Primary).Bold(true).Render("Display issues ▶") + //} else { + // closedLabel = lipgloss.NewStyle().Foreground(styles.Primary).Bold(true).Render("Closed issues ▶") + //} + leftColumn := lipgloss.JoinVertical(lipgloss.Left, - mainLabel, listView, - closedLabel, closedListView, + //mainLabel, + listView, + //closedLabel, closedListView, ) content := lipgloss.JoinHorizontal(lipgloss.Left, leftColumn, detailView) From d4bd9877d0bfc7515445466235864cb93d2065e5 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:21:53 +0100 Subject: [PATCH 36/73] move operations to msgs --- pkg/tui/issues/operations.go | 130 ------------------------- pkg/tui/msgs/msgs.go | 149 ++++++++++++++++++++++++++++- pkg/tui/views/kanban/operations.go | 65 +++++++------ 3 files changed, 177 insertions(+), 167 deletions(-) delete mode 100644 pkg/tui/issues/operations.go diff --git a/pkg/tui/issues/operations.go b/pkg/tui/issues/operations.go deleted file mode 100644 index 823ee5f..0000000 --- a/pkg/tui/issues/operations.go +++ /dev/null @@ -1,130 +0,0 @@ -package issues - -import ( - "context" - - "charm.land/bubbletea/v2" - "github.com/LazyBachelor/LazyPM/internal/app" - "github.com/LazyBachelor/LazyPM/internal/models" -) - -// Msg types used by both dashboard and kanban TUI views. -type ( - TitleUpdatedMsg struct { - IssueID string - Err error - } - DescriptionUpdatedMsg struct { - IssueID string - Err error - } - StatusUpdatedMsg struct { - IssueID string - Err error - } - PriorityUpdatedMsg struct { - IssueID string - Err error - } - TypeUpdatedMsg struct { - IssueID string - Err error - } - AssigneeUpdatedMsg struct { - IssueID string - Err error - } - SelectIssueMsg struct{ IssueID string } - CreatedMsg struct { - Issue *models.Issue - Err error - } - DeletedMsg struct { - IssueID string - Err error - PreviousIndex int - } -) - -// UpdateIssueTitleCmd returns a command that updates an issue's title. -func UpdateIssueTitleCmd(app *app.App, issueID, newTitle string) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"title": newTitle} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return TitleUpdatedMsg{IssueID: issueID, Err: err} - } -} - -// UpdateIssueDescriptionCmd returns a command that updates an issue's description. -func UpdateIssueDescriptionCmd(app *app.App, issueID, newDescription string) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"description": newDescription} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return DescriptionUpdatedMsg{IssueID: issueID, Err: err} - } -} - -// UpdateIssueStatusCmd returns a command that updates an issue's status. -func UpdateIssueStatusCmd(app *app.App, issueID, status string) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"status": status} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return StatusUpdatedMsg{IssueID: issueID, Err: err} - } -} - -// UpdateIssuePriorityCmd returns a command that updates an issue's priority. -func UpdateIssuePriorityCmd(app *app.App, issueID string, priority int) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"priority": priority} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return PriorityUpdatedMsg{IssueID: issueID, Err: err} - } -} - -// UpdateIssueTypeCmd returns a command that updates an issue's type. -func UpdateIssueTypeCmd(app *app.App, issueID string, issueType models.IssueType) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"issue_type": string(issueType)} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return TypeUpdatedMsg{IssueID: issueID, Err: err} - } -} - -// UpdateIssueAssigneeCmd returns a command that updates an issue's assignee. -func UpdateIssueAssigneeCmd(app *app.App, issueID, assignee string) tea.Cmd { - return func() tea.Msg { - updates := map[string]interface{}{"assignee": assignee} - err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") - return AssigneeUpdatedMsg{IssueID: issueID, Err: err} - } -} - -// CreateIssueCmd returns a command that creates a new issue. -func CreateIssueCmd(app *app.App, title string) tea.Cmd { - return func() tea.Msg { - issue := &models.Issue{ - Title: title, - Status: models.StatusOpen, - IssueType: models.TypeTask, - Priority: 2, - } - err := app.Issues.CreateIssue(context.Background(), issue, "tui") - return CreatedMsg{Issue: issue, Err: err} - } -} - -// DeleteIssueCmd returns a command that deletes an issue. -func DeleteIssueCmd(app *app.App, issueID string, currentIndex int) tea.Cmd { - return func() tea.Msg { - err := app.Issues.DeleteIssue(context.Background(), issueID) - return DeletedMsg{IssueID: issueID, Err: err, PreviousIndex: currentIndex} - } -} - -func CloseIssueCmd(app *app.App, issueID, reason string) tea.Cmd { - return func() tea.Msg { - err := app.Issues.CloseIssue(context.Background(), issueID, reason, "tui", "") - return StatusUpdatedMsg{IssueID: issueID, Err: err} - } -} diff --git a/pkg/tui/msgs/msgs.go b/pkg/tui/msgs/msgs.go index 047787f..40e1169 100644 --- a/pkg/tui/msgs/msgs.go +++ b/pkg/tui/msgs/msgs.go @@ -1,7 +1,148 @@ package msgs -// SwitchToDashboardMsg signals to switch to the main dashboard view. -type SwitchToDashboardMsg struct{} +import ( + "context" -// SwitchToKanbanBoardMsg signals to switch to the kanban board view. -type SwitchToKanbanBoardMsg struct{} + tea "charm.land/bubbletea/v2" + "github.com/LazyBachelor/LazyPM/internal/app" + "github.com/LazyBachelor/LazyPM/internal/models" +) + +// Msg types used by both dashboard and kanban TUI views. +type ( + TitleUpdatedMsg struct { + IssueID string + Err error + } + DescriptionUpdatedMsg struct { + IssueID string + Err error + } + StatusUpdatedMsg struct { + IssueID string + Err error + } + PriorityUpdatedMsg struct { + IssueID string + Err error + } + TypeUpdatedMsg struct { + IssueID string + Err error + } + AssigneeUpdatedMsg struct { + IssueID string + Err error + } + SelectIssueMsg struct{ IssueID string } + CreatedMsg struct { + Issue *models.Issue + Err error + } + DeletedMsg struct { + IssueID string + Err error + PreviousIndex int + } + + IssueCommentAddedMsg struct { + IssueID string + Err error + } + + // SwitchToDashboardMsg signals to switch to the main dashboard view. + SwitchToDashboardMsg struct{} + + // SwitchToKanbanBoardMsg signals to switch to the kanban board view. + SwitchToKanbanBoardMsg struct{} +) + +// UpdateIssueTitleCmd returns a command that updates an issue's title. +func UpdateIssueTitleCmd(app *app.App, issueID, newTitle string) tea.Cmd { + return func() tea.Msg { + updates := map[string]interface{}{"title": newTitle} + err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") + return TitleUpdatedMsg{IssueID: issueID, Err: err} + } +} + +// UpdateIssueDescriptionCmd returns a command that updates an issue's description. +func UpdateIssueDescriptionCmd(app *app.App, issueID, newDescription string) tea.Cmd { + return func() tea.Msg { + updates := map[string]interface{}{"description": newDescription} + err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") + return DescriptionUpdatedMsg{IssueID: issueID, Err: err} + } +} + +// UpdateIssueStatusCmd returns a command that updates an issue's status. +func UpdateIssueStatusCmd(app *app.App, issueID, status string) tea.Cmd { + return func() tea.Msg { + updates := map[string]interface{}{"status": status} + err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") + return StatusUpdatedMsg{IssueID: issueID, Err: err} + } +} + +// UpdateIssuePriorityCmd returns a command that updates an issue's priority. +func UpdateIssuePriorityCmd(app *app.App, issueID string, priority int) tea.Cmd { + return func() tea.Msg { + updates := map[string]interface{}{"priority": priority} + err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") + return PriorityUpdatedMsg{IssueID: issueID, Err: err} + } +} + +// UpdateIssueTypeCmd returns a command that updates an issue's type. +func UpdateIssueTypeCmd(app *app.App, issueID string, issueType models.IssueType) tea.Cmd { + return func() tea.Msg { + updates := map[string]interface{}{"issue_type": string(issueType)} + err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") + return TypeUpdatedMsg{IssueID: issueID, Err: err} + } +} + +// UpdateIssueAssigneeCmd returns a command that updates an issue's assignee. +func UpdateIssueAssigneeCmd(app *app.App, issueID, assignee string) tea.Cmd { + return func() tea.Msg { + updates := map[string]interface{}{"assignee": assignee} + err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") + return AssigneeUpdatedMsg{IssueID: issueID, Err: err} + } +} + +// CreateIssueCmd returns a command that creates a new issue. +func CreateIssueCmd(app *app.App, title string) tea.Cmd { + return func() tea.Msg { + issue := &models.Issue{ + Title: title, + Status: models.StatusOpen, + IssueType: models.TypeTask, + Priority: 2, + } + err := app.Issues.CreateIssue(context.Background(), issue, "tui") + return CreatedMsg{Issue: issue, Err: err} + } +} + +// DeleteIssueCmd returns a command that deletes an issue. +func DeleteIssueCmd(app *app.App, issueID string, currentIndex int) tea.Cmd { + return func() tea.Msg { + err := app.Issues.DeleteIssue(context.Background(), issueID) + return DeletedMsg{IssueID: issueID, Err: err, PreviousIndex: currentIndex} + } +} + +func CloseIssueCmd(app *app.App, issueID, reason string) tea.Cmd { + return func() tea.Msg { + err := app.Issues.CloseIssue(context.Background(), issueID, reason, "tui", "") + return StatusUpdatedMsg{IssueID: issueID, Err: err} + } +} + +func AddIssueCommentCmd(app *app.App, issueID, author, text string) tea.Cmd { + return func() tea.Msg { + _, err := app.Issues.AddIssueComment(context.Background(), issueID, author, text) + return IssueCommentAddedMsg{IssueID: issueID, Err: err} + } +} diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index 1ae5883..b19ac84 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -7,13 +7,12 @@ import ( "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" - "github.com/LazyBachelor/LazyPM/pkg/tui/issues" + "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" ) +// update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view +// Fetch all msgs, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { - /* update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view - Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. - */ allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { return nil @@ -68,7 +67,7 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case issues.TitleUpdatedMsg: + case msgs.TitleUpdatedMsg: m.editingTitle = false m.editingIssueID = "" m.titleInput.Blur() @@ -77,7 +76,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.DescriptionUpdatedMsg: + case msgs.DescriptionUpdatedMsg: m.editingDescription = false m.editingDescIssueID = "" m.descriptionInput.Blur() @@ -86,7 +85,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.StatusUpdatedMsg: + case msgs.StatusUpdatedMsg: m.choosingStatus = false m.statusIssueID = "" if msg.Err != nil { @@ -94,7 +93,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.PriorityUpdatedMsg: + case msgs.PriorityUpdatedMsg: m.choosingPriority = false m.priorityIssueID = "" if msg.Err != nil { @@ -102,7 +101,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.TypeUpdatedMsg: + case msgs.TypeUpdatedMsg: m.choosingType = false m.typeIssueID = "" if msg.Err != nil { @@ -110,7 +109,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.AssigneeUpdatedMsg: + case msgs.AssigneeUpdatedMsg: m.editingAssignee = false m.assigneeIssueID = "" m.assigneeInput.Blur() @@ -119,14 +118,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.SelectIssueMsg: + case msgs.SelectIssueMsg: m.todoList.SelectIssueID(msg.IssueID) m.inProgList.SelectIssueID(msg.IssueID) m.blockedList.SelectIssueID(msg.IssueID) m.doneList.SelectIssueID(msg.IssueID) return m, nil - case issues.CreatedMsg: + case msgs.CreatedMsg: m.creatingIssue = false m.createTitleInput.Blur() m.createTitleInput.Reset() @@ -161,8 +160,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.issueDetail.SetIssue(*selectedIssue) - return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return issues.SelectIssueMsg{IssueID: selectedIssue.ID} }) - case issues.DeletedMsg: + return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} }) + case msgs.DeletedMsg: m.confirmingDelete = false m.deleteConfirmID = "" if msg.Err != nil { @@ -183,7 +182,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { blockedCmd := m.blockedList.SetIssues(blockedIssues) doneCmd := m.doneList.SetIssues(doneIssues) - // If there are no issues at all, clear the detail view and return. + // If there are no msgs at all, clear the detail view and return. if len(todoIssues) == 0 && len(inProgIssues) == 0 && len(blockedIssues) == 0 && len(doneIssues) == 0 { m.issueDetail.SetIssue(models.Issue{}) return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd) @@ -263,7 +262,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { selectedIssue := targetIssues[newIndex] m.issueDetail.SetIssue(*selectedIssue) return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { - return issues.SelectIssueMsg{IssueID: selectedIssue.ID} + return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} }) case tea.KeyPressMsg: @@ -274,7 +273,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { idx := m.deleteConfirmIndex m.confirmingDelete = false m.deleteConfirmID = "" - return m, issues.DeleteIssueCmd(m.app, issueID, idx) + return m, msgs.DeleteIssueCmd(m.app, issueID, idx) case "n", "N", "esc": m.confirmingDelete = false m.deleteConfirmID = "" @@ -288,22 +287,22 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusOpen)) + return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusOpen)) case "i": issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) + return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) case "b": issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusBlocked)) + return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusBlocked)) case "r": issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) + return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) case "c": issueID := m.statusIssueID m.choosingStatus = false @@ -345,7 +344,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { issueID := m.closeReasonIssueID m.choosingCloseReason = false m.closeReasonIssueID = "" - return m, issues.CloseIssueCmd(m.app, issueID, reason) + return m, msgs.CloseIssueCmd(m.app, issueID, reason) } } @@ -358,7 +357,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.closingOtherReason = false m.closeReasonIssueID = "" m.closeReasonInput.Blur() - return m, issues.CloseIssueCmd(m.app, issueID, reason) + return m, msgs.CloseIssueCmd(m.app, issueID, reason) } case "esc": m.closingOtherReason = false @@ -378,7 +377,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { priority := int(msg.String()[0] - '0') m.choosingPriority = false m.priorityIssueID = "" - return m, issues.UpdateIssuePriorityCmd(m.app, issueID, priority) + return m, msgs.UpdateIssuePriorityCmd(m.app, issueID, priority) case "esc": m.choosingPriority = false m.priorityIssueID = "" @@ -394,27 +393,27 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" - return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeBug) + return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeBug) case "f": issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" - return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeFeature) + return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeFeature) case "t": issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" - return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeTask) + return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeTask) case "e": issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" - return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeEpic) + return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeEpic) case "c": issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" - return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeChore) + return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeChore) case "esc": m.choosingType = false m.typeIssueID = "" @@ -428,7 +427,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "enter" { title := m.createTitleInput.Value() if title != "" { - return m, issues.CreateIssueCmd(m.app, title) + return m, msgs.CreateIssueCmd(m.app, title) } } if msg.String() == "esc" { @@ -445,7 +444,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.editingAssignee { if msg.String() == "enter" { assignee := m.assigneeInput.Value() - return m, issues.UpdateIssueAssigneeCmd(m.app, m.assigneeIssueID, assignee) + return m, msgs.UpdateIssueAssigneeCmd(m.app, m.assigneeIssueID, assignee) } if msg.String() == "esc" { m.editingAssignee = false @@ -462,7 +461,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "enter" { newTitle := m.titleInput.Value() if newTitle != "" { - return m, issues.UpdateIssueTitleCmd(m.app, m.editingIssueID, newTitle) + return m, msgs.UpdateIssueTitleCmd(m.app, m.editingIssueID, newTitle) } } if msg.String() == "esc" { @@ -483,7 +482,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.editingDescription = false m.editingDescIssueID = "" m.descriptionInput.Blur() - return m, issues.UpdateIssueDescriptionCmd(m.app, issueID, newDesc) + return m, msgs.UpdateIssueDescriptionCmd(m.app, issueID, newDesc) } if msg.String() == "esc" { m.editingDescription = false From 0b1675d21aa1f957967ffaba540e7dd508ad3f05 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:22:09 +0100 Subject: [PATCH 37/73] remove unused --- pkg/tui/views/dashboard/operations.go | 176 ++++++-------------------- 1 file changed, 37 insertions(+), 139 deletions(-) diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 5f0c9be..b2d6e81 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -7,10 +7,9 @@ import ( "charm.land/bubbles/v2/list" tea "charm.land/bubbletea/v2" - "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" - "github.com/LazyBachelor/LazyPM/pkg/tui/issues" + "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" ) func defaultCommentAuthor() string { @@ -26,68 +25,15 @@ func defaultCommentAuthor() string { return "user" } -type issueTitleUpdatedMsg struct { - IssueID string - Err error -} - -type issueDescriptionUpdatedMsg struct { - IssueID string - Err error -} - -type issueStatusUpdatedMsg struct { - IssueID string - Err error -} - -type issuePriorityUpdatedMsg struct { - IssueID string - Err error -} - -type issueTypeUpdatedMsg struct { - IssueID string - Err error -} - -type selectIssueMsg struct { - IssueID string -} - -type issueCreatedMsg struct { - Issue *models.Issue - Err error -} - -type issueDeletedMsg struct { - IssueID string - Err error - PreviousIndex int -} - -type issueCommentAddedMsg struct { - IssueID string - Err error -} - -func addIssueCommentCmd(app *app.App, issueID, author, text string) tea.Cmd { - return func() tea.Msg { - _, err := app.Issues.AddIssueComment(context.Background(), issueID, author, text) - return issueCommentAddedMsg{IssueID: issueID, Err: err} - } -} - func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { /* update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view - Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. + Fetch all msgs, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. */ allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { return nil } - setItemsCmd := m.issueList.SetIssues(components.OpenAndInProgressOnly(allIssues)) - closedSetCmd := m.closedIssueList.SetIssues(components.ClosedOnly(allIssues)) + setItemsCmd := m.issueList.SetIssues(components.SortedIssues(allIssues)) for _, issue := range allIssues { if issue.ID == issueID { m.setDetailIssueWithComments(*issue) @@ -103,12 +49,12 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { } } - return tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return issues.SelectIssueMsg{IssueID: issueID} }) + return tea.Sequence(setItemsCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: issueID} }) } func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case issues.TitleUpdatedMsg: + case msgs.TitleUpdatedMsg: m.editingTitle = false m.editingIssueID = "" m.titleInput.Blur() @@ -119,7 +65,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.logAction("tui updated issue title") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.DescriptionUpdatedMsg: + case msgs.DescriptionUpdatedMsg: m.editingDescription = false m.editingDescIssueID = "" m.descriptionInput.Blur() @@ -130,7 +76,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.logAction("tui updated issue description") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.StatusUpdatedMsg: + case msgs.StatusUpdatedMsg: m.choosingStatus = false m.statusIssueID = "" if msg.Err != nil { @@ -140,7 +86,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.logAction("tui updated issue status") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.PriorityUpdatedMsg: + case msgs.PriorityUpdatedMsg: m.choosingPriority = false m.priorityIssueID = "" if msg.Err != nil { @@ -150,7 +96,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.logAction("tui updated issue priority") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.TypeUpdatedMsg: + case msgs.TypeUpdatedMsg: m.choosingType = false m.typeIssueID = "" if msg.Err != nil { @@ -160,7 +106,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.logAction("tui updated issue type") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.AssigneeUpdatedMsg: + case msgs.AssigneeUpdatedMsg: m.editingAssignee = false m.assigneeIssueID = "" m.assigneeInput.Blur() @@ -171,12 +117,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.logAction("tui updated issue assignee") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.SelectIssueMsg: + case msgs.SelectIssueMsg: m.issueList.SelectIssueID(msg.IssueID) m.closedIssueList.SelectIssueID(msg.IssueID) return m, nil - case issues.CreatedMsg: + case msgs.CreatedMsg: m.creatingIssue = false m.createTitleInput.Blur() m.createTitleInput.Reset() @@ -188,8 +134,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if err != nil { return m, nil } - setItemsCmd := m.issueList.SetIssues(components.OpenAndInProgressOnly(allIssues)) - closedSetCmd := m.closedIssueList.SetIssues(components.ClosedOnly(allIssues)) + setItemsCmd := m.issueList.SetIssues(components.SortedIssues(allIssues)) // Determine the created issue from the refreshed list to ensure all fields (like ID) are populated. selectedIssue := msg.Issue @@ -205,8 +150,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.setDetailIssueWithComments(*selectedIssue) m.logAction("tui created issue") - return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return issues.SelectIssueMsg{IssueID: selectedIssue.ID} }) - case issueCommentAddedMsg: + return m, tea.Sequence(setItemsCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} }) + case msgs.IssueCommentAddedMsg: m.addingComment = false m.commentIssueID = "" m.commentInput.Blur() @@ -215,7 +160,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) - case issues.DeletedMsg: + case msgs.DeletedMsg: m.confirmingDelete = false m.deleteConfirmID = "" if msg.Err != nil { @@ -223,53 +168,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } m.logAction("tui deleted issue") - allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) - if err != nil { - return m, nil - } - openIssues := components.OpenAndInProgressOnly(allIssues) - closedIssues := components.ClosedOnly(allIssues) - setItemsCmd := m.issueList.SetIssues(openIssues) - closedSetCmd := m.closedIssueList.SetIssues(closedIssues) - // If there are no issues at all, clear the detail view and return. - if len(openIssues) == 0 && len(closedIssues) == 0 { - m.setDetailIssueWithComments(models.Issue{}) - return m, tea.Sequence(setItemsCmd, closedSetCmd) - } - - // Determine which list to use for the next selection. - var targetIssues []*models.Issue - if m.focusedWindow == 0 { - targetIssues = openIssues - if len(targetIssues) == 0 && len(closedIssues) > 0 { - // The open list became empty; fall back to closed issues. - targetIssues = closedIssues - m.focusedWindow = 1 - } - } else { - targetIssues = closedIssues - if len(targetIssues) == 0 && len(openIssues) > 0 { - // The closed list became empty; fall back to open/in-progress issues. - targetIssues = openIssues - m.focusedWindow = 0 - } - } - - // Safety: if targetIssues is still empty here, just clear detail and return. - if len(targetIssues) == 0 { - m.setDetailIssueWithComments(models.Issue{}) - return m, tea.Sequence(setItemsCmd, closedSetCmd) - } - - newIndex := msg.PreviousIndex - if newIndex >= len(targetIssues) { - newIndex = len(targetIssues) - 1 - } - selectedIssue := targetIssues[newIndex] - m.setDetailIssueWithComments(*selectedIssue) - return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { - return issues.SelectIssueMsg{IssueID: selectedIssue.ID} - }) + return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case tea.KeyPressMsg: if m.confirmingDelete { @@ -280,7 +179,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { idx := m.deleteConfirmIndex m.confirmingDelete = false m.deleteConfirmID = "" - return m, issues.DeleteIssueCmd(m.app, issueID, idx) + return m, msgs.DeleteIssueCmd(m.app, issueID, idx) case "n", "N", "esc": m.logAction("tui canceled issue deletion") m.confirmingDelete = false @@ -296,25 +195,25 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusOpen)) + return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusOpen)) case "i": m.logAction("tui selected issue status in_progress") issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) + return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) case "b": m.logAction("tui selected issue status blocked") issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusBlocked)) + return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusBlocked)) case "r": m.logAction("tui selected issue status ready_to_sprint") issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) + return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) case "c": m.logAction("tui selected issue status closing") issueID := m.statusIssueID @@ -364,7 +263,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { issueID := m.closeReasonIssueID m.choosingCloseReason = false m.closeReasonIssueID = "" - return m, issues.CloseIssueCmd(m.app, issueID, reason) + return m, msgs.CloseIssueCmd(m.app, issueID, reason) } } @@ -378,7 +277,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.closingOtherReason = false m.closeReasonIssueID = "" m.closeReasonInput.Blur() - return m, issues.CloseIssueCmd(m.app, issueID, reason) + return m, msgs.CloseIssueCmd(m.app, issueID, reason) } case "esc": m.logAction("tui canceled custom close reason") @@ -400,7 +299,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { priority := int(msg.String()[0] - '0') m.choosingPriority = false m.priorityIssueID = "" - return m, issues.UpdateIssuePriorityCmd(m.app, issueID, priority) + return m, msgs.UpdateIssuePriorityCmd(m.app, issueID, priority) case "esc": m.logAction("tui canceled priority picker") m.choosingPriority = false @@ -418,31 +317,31 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" - return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeBug) + return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeBug) case "f": m.logAction("tui selected issue type feature") issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" - return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeFeature) + return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeFeature) case "t": m.logAction("tui selected issue type task") issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" - return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeTask) + return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeTask) case "e": m.logAction("tui selected issue type epic") issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" - return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeEpic) + return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeEpic) case "c": m.logAction("tui selected issue type chore") issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" - return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeChore) + return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeChore) case "esc": m.logAction("tui canceled type picker") m.choosingType = false @@ -458,7 +357,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { title := m.createTitleInput.Value() if title != "" { m.logAction("tui submitted new issue") - return m, issues.CreateIssueCmd(m.app, title) + return m, msgs.CreateIssueCmd(m.app, title) } } if msg.String() == "esc" { @@ -477,7 +376,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "enter" { assignee := m.assigneeInput.Value() m.logAction("tui submitted assignee edit") - return m, issues.UpdateIssueAssigneeCmd(m.app, m.assigneeIssueID, assignee) + return m, msgs.UpdateIssueAssigneeCmd(m.app, m.assigneeIssueID, assignee) } if msg.String() == "esc" { m.logAction("tui canceled assignee edit") @@ -496,7 +395,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { newTitle := m.titleInput.Value() if newTitle != "" { m.logAction("tui submitted issue title edit") - return m, issues.UpdateIssueTitleCmd(m.app, m.editingIssueID, newTitle) + return m, msgs.UpdateIssueTitleCmd(m.app, m.editingIssueID, newTitle) } } if msg.String() == "esc" { @@ -520,7 +419,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.commentIssueID = "" m.commentInput.Blur() m.commentInput.Reset() - return m, addIssueCommentCmd(m.app, issueID, defaultCommentAuthor(), text) + return m, msgs.AddIssueCommentCmd(m.app, issueID, defaultCommentAuthor(), text) } } if msg.String() == "esc" { @@ -543,7 +442,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.editingDescription = false m.editingDescIssueID = "" m.descriptionInput.Blur() - return m, issues.UpdateIssueDescriptionCmd(m.app, issueID, newDesc) + return m, msgs.UpdateIssueDescriptionCmd(m.app, issueID, newDesc) } if msg.String() == "esc" { m.logAction("tui canceled issue description edit") @@ -557,9 +456,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } - focusedList := m.FocusedIssueList() - if focusedList.FilterState() == list.Filtering { - cmd, _ := focusedList.Update(msg) + if m.issueList.FilterState() == list.Filtering { + cmd, _ := m.issueList.Update(msg) return m, cmd } From 47f670da286aa9fcdfe1413406de04d1f11d7e50 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:22:21 +0100 Subject: [PATCH 38/73] refine helpbar --- pkg/tui/components/helpbar.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/pkg/tui/components/helpbar.go b/pkg/tui/components/helpbar.go index 873a88c..54fe6f6 100644 --- a/pkg/tui/components/helpbar.go +++ b/pkg/tui/components/helpbar.go @@ -106,10 +106,7 @@ func (h HelpBar) fullHelp() string { ) } - innerWidth := h.width - 2 - if innerWidth < 1 { - innerWidth = 1 - } + innerWidth := max(h.width - 2, 1) cellWidth := fullHelpKeyWidth + 1 + fullHelpDescWidth cols := fitHelpColumns( @@ -196,32 +193,23 @@ func helpBarConfig(view ViewKind) HelpBarConfig { case ViewIssues: return HelpBarConfig{ ShortItems: []HelpItem{ - {Key: "tab", Desc: "switch"}, {Key: "v", Desc: "kanban"}, {Key: "↑/k", Desc: "up"}, {Key: "↓/j", Desc: "down"}, - {Key: "pgup/pgdn", Desc: "page"}, {Key: "a", Desc: "add"}, {Key: "c", Desc: "comment"}, {Key: "e/d/s/p/t/A", Desc: "edit"}, {Key: "x", Desc: "delete"}, - {Key: "S", Desc: "submit"}, {Key: "q", Desc: "quit"}, {Key: "?", Desc: "help"}, }, FullItems: []HelpItem{ - {Key: "tab", Desc: "switch window"}, - {Key: "enter", Desc: "view issue"}, - {Key: "b", Desc: "back to list"}, {Key: "v", Desc: "kanban"}, {Key: "↑/k", Desc: "up"}, {Key: "↓/j", Desc: "down"}, - {Key: "pgup", Desc: "page up"}, - {Key: "pgdn", Desc: "page down"}, {Key: "a", Desc: "add issue"}, {Key: "c", Desc: "add comment"}, {Key: "x", Desc: "delete issue"}, - {Key: "S", Desc: "submit"}, {Key: "e", Desc: "edit title"}, {Key: "d", Desc: "edit description"}, {Key: "s", Desc: "change status"}, @@ -246,13 +234,10 @@ func helpBarConfig(view ViewKind) HelpBarConfig { {Key: "e/d/s/p/t/A", Desc: "edit"}, {Key: "x", Desc: "delete"}, {Key: "q", Desc: "quit"}, - {Key: "S", Desc: "submit"}, {Key: "?", Desc: "help"}, }, FullItems: []HelpItem{ {Key: "v", Desc: "list view"}, - {Key: "enter", Desc: "view issue"}, - {Key: "b", Desc: "back to list"}, {Key: "h/l", Desc: "switch column"}, {Key: "←/→", Desc: "move issue"}, {Key: "↑/k", Desc: "up"}, @@ -261,7 +246,6 @@ func helpBarConfig(view ViewKind) HelpBarConfig { {Key: "pgdn", Desc: "page down"}, {Key: "a", Desc: "add issue"}, {Key: "x", Desc: "delete issue"}, - {Key: "S", Desc: "submit"}, {Key: "e", Desc: "edit title"}, {Key: "d", Desc: "edit description"}, {Key: "s", Desc: "change status"}, From ad961747fc781893f99bed3e485d195f0917a2c2 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:22:45 +0100 Subject: [PATCH 39/73] show closing reason and refine comment rendering --- pkg/tui/components/issue_detail.go | 52 +++++++++++++++++++----------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/pkg/tui/components/issue_detail.go b/pkg/tui/components/issue_detail.go index 11def7a..83f8948 100644 --- a/pkg/tui/components/issue_detail.go +++ b/pkg/tui/components/issue_detail.go @@ -60,6 +60,18 @@ func (i *IssueDetail) refreshContent() { styles.LabelStyle.Render("Status:") + styles.StatusStyle(string(i.issue.Status)).Render(string(i.issue.Status)), ) + var closingReasonRow string + if i.issue.Status == models.StatusClosed { + var closingReason string + if i.issue.CloseReason == "" { + closingReason = "N/A" + } else { + closingReason = string(i.issue.CloseReason) + } + closingReasonRow = styles.RowStyle.Render( + styles.LabelStyle.Render("Close reason: ") + styles.ValueStyle.Render(closingReason)) + } + priorityRow := styles.RowStyle.Render( styles.LabelStyle.Render("Priority:") + styles.ValueStyle.Render(PriorityCodeName(i.issue.Priority)), ) @@ -71,25 +83,11 @@ func (i *IssueDetail) refreshContent() { descLabel := styles.LabelStyle.Render("Description:") descContent := styles.ValueStyle.Render(i.issue.Description) - var parts []string - parts = append(parts, titleRow, idRow, typeRow, statusRow, priorityRow, assigneeRow, descLabel, descContent) + commentsLabel := styles.LabelStyle.MarginTop(1).Render("Comments:") - // Comments section - commentsLabel := styles.LabelStyle.Render("Comments:") - parts = append(parts, commentsLabel) - if len(i.comments) == 0 { - parts = append(parts, lipgloss.NewStyle().Foreground(styles.FaintText).Render(" No comments yet.")) - } else { - for _, c := range i.comments { - authorDate := lipgloss.NewStyle().Foreground(styles.Primary).Render(c.Author) + " " + - lipgloss.NewStyle().Foreground(styles.FaintText).Render(formatCommentTime(c.CreatedAt)) - commentRow := lipgloss.JoinVertical(lipgloss.Left, - authorDate, - styles.ValueStyle.Render(c.Text), - ) - parts = append(parts, commentRow) - } - } + var parts []string + parts = append(parts, titleRow, idRow, typeRow, statusRow, closingReasonRow, priorityRow, assigneeRow, descLabel, descContent, commentsLabel) + parts = append(parts, i.renderComments()...) content := lipgloss.JoinVertical(lipgloss.Left, parts...) i.viewport.SetContent(content) @@ -126,3 +124,21 @@ func (i *IssueDetail) ScrollUp(lines int) { func (i *IssueDetail) ScrollDown(lines int) { i.viewport.ScrollDown(lines) } + +func (i *IssueDetail) renderComments() []string { + var parts []string + if len(i.comments) == 0 { + parts = append(parts, styles.ValueStyle.Render("No comments yet.")) + } else { + for _, c := range i.comments { + authorDate := lipgloss.NewStyle().Foreground(styles.Primary).Render(c.Author) + " " + + lipgloss.NewStyle().Foreground(styles.FaintText).Render(formatCommentTime(c.CreatedAt)) + commentRow := lipgloss.JoinVertical(lipgloss.Left, + authorDate, + styles.ValueStyle.MarginLeft(1).Render(c.Text), + ) + parts = append(parts, commentRow) + } + } + return parts +} From 097cb3af56679c7fa2b0b6ca5e1ffc22ce468754 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:23:14 +0100 Subject: [PATCH 40/73] remove closed issues view and just show closed issues with strikethroug --- pkg/tui/components/issue_list.go | 87 +++++++++++++++++++------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/pkg/tui/components/issue_list.go b/pkg/tui/components/issue_list.go index 087a5c3..d82a16a 100644 --- a/pkg/tui/components/issue_list.go +++ b/pkg/tui/components/issue_list.go @@ -9,7 +9,7 @@ import ( "charm.land/bubbles/v2/list" "charm.land/bubbles/v2/textarea" "charm.land/bubbles/v2/textinput" - "charm.land/bubbletea/v2" + tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" @@ -41,29 +41,54 @@ type tableColumn struct { func getTableColumns(width int) []tableColumn { switch { + case width < 20: + return []tableColumn{ + {width: 10, label: "ID", key: "id"}, + } + case width < 30: + return []tableColumn{ + {width: 10, label: "ID", key: "id"}, + {width: 10, label: "TITLE", key: "title"}, + } case width < 45: return []tableColumn{ {width: 10, label: "ID", key: "id"}, - {width: uint(width - 10), label: "TITLE", key: "title"}, + {width: 20, label: "TITLE", key: "title"}, } case width < 60: return []tableColumn{ {width: 10, label: "ID", key: "id"}, - {width: 20, label: "TITLE", key: "title"}, + {width: 22, label: "TITLE", key: "title"}, {width: 15, label: "STATUS", key: "status"}, } case width < 75: + return []tableColumn{ + {width: 10, label: "ID", key: "id"}, + {width: 25, label: "TITLE", key: "title"}, + {width: 15, label: "STATUS", key: "status"}, + {width: 10, label: "TYPE", key: "type"}, + } + case width < 95: return []tableColumn{ {width: 12, label: "ID", key: "id"}, - {width: 20, label: "TITLE", key: "title"}, + {width: 25, label: "TITLE", key: "title"}, {width: 15, label: "STATUS", key: "status"}, {width: 10, label: "TYPE", key: "type"}, {width: 15, label: "PRIORITY", key: "priority"}, } + case width < 120: + return []tableColumn{ + {width: 12, label: "ID", key: "id"}, + {width: 30, label: "TITLE", key: "title"}, + {width: 15, label: "STATUS", key: "status"}, + {width: 11, label: "TYPE", key: "type"}, + {width: 14, label: "PRIORITY", key: "priority"}, + {width: 12, label: "ASSIGNEE", key: "assignee"}, + } default: return []tableColumn{ {width: 12, label: "ID", key: "id"}, - {width: 20, label: "TITLE", key: "title"}, + {width: 40, label: "TITLE", key: "title"}, {width: 15, label: "STATUS", key: "status"}, {width: 11, label: "TYPE", key: "type"}, {width: 14, label: "PRIORITY", key: "priority"}, @@ -213,31 +238,9 @@ func NewIssueListFromIssues(app *app.App, issues []*models.Issue, width, height } } -func OpenAndInProgressOnly(issues []*models.Issue) []*models.Issue { - // used to display open & in-progress issues in the first window in the dashboard - out := make([]*models.Issue, 0, len(issues)) - for _, issue := range issues { - if issue.Status == models.StatusOpen || - issue.Status == models.StatusInProgress || - issue.Status == models.StatusBlocked || - issue.Status == models.StatusReadyToSprint { - out = append(out, issue) - } - } - sortByPriorityDesc(out) - return out -} - -func ClosedOnly(issues []*models.Issue) []*models.Issue { - // used to display issues in the second window in the dashboard - out := make([]*models.Issue, 0, len(issues)) - for _, issue := range issues { - if issue.Status == models.StatusClosed { - out = append(out, issue) - } - } - sortByPriorityDesc(out) - return out +func SortedIssues(issues []*models.Issue) []*models.Issue { + sortByClosedThenPriorityDesc(issues) + return issues } // StatusOnly returns issues that exactly match the given status, sorted by priority. @@ -249,13 +252,24 @@ func StatusOnly(issues []*models.Issue, status models.Status) []*models.Issue { out = append(out, issue) } } - sortByPriorityDesc(out) + sortByClosedThenPriorityDesc(out) return out } -func sortByPriorityDesc(issues []*models.Issue) { - sort.Slice(issues, func(i, j int) bool { - return issues[i].Priority > issues[j].Priority +func sortByClosedThenPriorityDesc(issues []*models.Issue) { + sort.SliceStable(issues, func(i, j int) bool { + iClosed := issues[i].Status == models.StatusClosed + jClosed := issues[j].Status == models.StatusClosed + + if iClosed != jClosed { + return !iClosed + } + + if issues[i].Priority != issues[j].Priority { + return issues[i].Priority > issues[j].Priority + } + + return issues[i].ID < issues[j].ID }) } @@ -353,7 +367,7 @@ func (l *IssueList) SetIssues(issues []*models.Issue) tea.Cmd { func (l *IssueList) SelectIssueID(issueID string) { items := l.list.Items() - for i := 0; i < len(items); i++ { + for i := range items { if item, ok := items[i].(ListIssue); ok && item.ID == issueID { l.list.Select(i) return @@ -399,6 +413,9 @@ func renderRow(issue ListIssue, isSelected bool, cols []tableColumn) string { if isSelected { style = style.Background(styles.SelectedBackground).Bold(true) } + if issue.Status == models.StatusClosed { + style = style.Strikethrough(true).Foreground(styles.FaintText) + } truncated := truncate.StringWithTail(value, colWidth, "...") parts = append(parts, style.Render(truncated)) From ecafe0c17e655929112a31560e37d60a2160f748 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:23:26 +0100 Subject: [PATCH 41/73] we dont use this --- pkg/tui/components/keymap.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pkg/tui/components/keymap.go b/pkg/tui/components/keymap.go index e2c4092..c5bc96e 100644 --- a/pkg/tui/components/keymap.go +++ b/pkg/tui/components/keymap.go @@ -7,8 +7,6 @@ import "charm.land/bubbles/v2/key" type CommonKeyMap struct { Help key.Binding Quit key.Binding - SelectIssue key.Binding - BackToList key.Binding ScrollUp key.Binding ScrollDown key.Binding EditTitle key.Binding @@ -32,14 +30,6 @@ func DefaultCommonKeyMap() CommonKeyMap { key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit"), ), - SelectIssue: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "view issue"), - ), - BackToList: key.NewBinding( - key.WithKeys("b"), - key.WithHelp("b", "back to list"), - ), ScrollUp: key.NewBinding( key.WithKeys("up", "k"), key.WithHelp("↑/k", "up"), From 78696e7a4bcc3c094562fc009bdf038f0dd4b337 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:23:49 +0100 Subject: [PATCH 42/73] remove submission keybind as its now automatic --- pkg/tui/views/dashboard/keys.go | 44 +++++++-------------------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 0d159e3..367a3ea 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -2,14 +2,13 @@ package dashboard import ( "charm.land/bubbles/v2/key" - "charm.land/bubbletea/v2" + tea "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" ) type DashboardKeyMap struct { components.CommonKeyMap - SwitchWindow key.Binding SwitchToKanbanBoard key.Binding Quit key.Binding SelectIssue key.Binding @@ -25,15 +24,10 @@ type DashboardKeyMap struct { AddComment key.Binding AddIssue key.Binding DeleteIssue key.Binding - SubmitValidation key.Binding } var defaultDashboardKeyMap = DashboardKeyMap{ CommonKeyMap: components.DefaultCommonKeyMap(), - SwitchWindow: key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", "switch window"), - ), SwitchToKanbanBoard: key.NewBinding( key.WithKeys("v"), key.WithHelp("v", "switch to kanban")), @@ -73,10 +67,6 @@ var defaultDashboardKeyMap = DashboardKeyMap{ key.WithKeys("x"), key.WithHelp("x", "delete issue"), ), - SubmitValidation: key.NewBinding( - key.WithKeys("S"), - key.WithHelp("S", "submit validation"), - ), } func (d *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { @@ -89,16 +79,8 @@ func (d *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { case key.Matches(msg, d.keyMap.Quit): d.logAction("tui quit requested") return tea.Quit - case key.Matches(msg, d.keyMap.SwitchWindow): - d.ToggleFocusedWindow() case !d.IsInModal() && key.Matches(msg, d.keyMap.SwitchToKanbanBoard): return func() tea.Msg { return msgs.SwitchToKanbanBoardMsg{} } - case d.IsFocusedOnList() && key.Matches(msg, d.keyMap.SelectIssue): - d.FocusDetail() - d.logAction("tui opened issue detail") - case d.IsFocusedOnDetail() && (key.Matches(msg, d.keyMap.BackToList) || key.Matches(msg, d.keyMap.SelectIssue)): - d.FocusList() - d.logAction("tui returned to issue list") case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollUp): d.issueDetail.ScrollUp(1) d.logAction("tui scrolled issue detail up") @@ -106,40 +88,40 @@ func (d *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { d.issueDetail.ScrollDown(1) d.logAction("tui scrolled issue detail down") case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.EditTitle): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { + if selected := d.issueList.SelectedItem(); selected.ID != "" { d.startEditTitle(selected) cmd = d.titleInput.Focus() d.logAction("tui started editing issue title") } case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.EditDescription): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { + if selected := d.issueList.SelectedItem(); selected.ID != "" { d.startEditDescription(selected) cmd = d.descriptionInput.Focus() d.logAction("tui started editing issue description") } case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangeStatus): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { + if selected := d.issueList.SelectedItem(); selected.ID != "" { d.startChooseStatus(selected) d.logAction("tui opened status picker") } case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangePriority): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { + if selected := d.issueList.SelectedItem(); selected.ID != "" { d.startChoosePriority(selected) d.logAction("tui opened priority picker") } case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangeType): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { + if selected := d.issueList.SelectedItem(); selected.ID != "" { d.startChooseType(selected) d.logAction("tui opened type picker") } case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangeAssignee): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { + if selected := d.issueList.SelectedItem(); selected.ID != "" { d.startEditAssignee(selected) cmd = d.assigneeInput.Focus() d.logAction("tui started editing assignee") } case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.AddComment): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { + if selected := d.issueList.SelectedItem(); selected.ID != "" { d.startAddComment(selected) cmd = d.commentInput.Focus() } @@ -148,19 +130,11 @@ func (d *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { cmd = d.createTitleInput.Focus() d.logAction("tui started creating issue") case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.DeleteIssue): - fl := d.FocusedIssueList() + fl := d.issueList if selected := fl.SelectedItem(); selected.ID != "" { d.startConfirmDelete(selected.ID, fl.Index()) d.logAction("tui opened delete confirmation") } - case key.Matches(msg, d.keyMap.SubmitValidation): - if d.submitChan != nil { - select { - case d.submitChan <- struct{}{}: - d.logAction("tui submitted validation") - default: - } - } } return cmd From 5d53ba2c11d25268c6d9fa55eff7621567d73061 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:24:05 +0100 Subject: [PATCH 43/73] we no longer have to change focus --- pkg/tui/views/dashboard/model.go | 52 +------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index a21f5e0..9ca4fce 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -85,9 +85,8 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui } allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) - m.issueList = components.NewIssueListFromIssues(app, components.OpenAndInProgressOnly(allIssues), 0, 0) + m.issueList = components.NewIssueListFromIssues(app, components.SortedIssues(allIssues), 0, 0) m.issueDetail = components.NewIssueDetail() - m.closedIssueList = components.NewIssueListFromIssues(app, components.ClosedOnly(allIssues), 0, 0) m.helpBar = components.NewHelpBar(components.ViewIssues) inputs := components.NewIssueInputs() @@ -221,52 +220,3 @@ func (m *Model) IsFocusedOnDetail() bool { } return m.focusedPaneClosed == 1 } - -func (m *Model) FocusList() { - if m.focusedWindow == 0 { - m.focusedPaneMain = 0 - } else { - m.focusedPaneClosed = 0 - } - m.issueDetail.SetFocused(false) -} - -func (m *Model) FocusDetail() { - if m.focusedWindow == 0 { - m.focusedPaneMain = 1 - } else { - m.focusedPaneClosed = 1 - } - m.issueDetail.SetFocused(true) -} - -func (m *Model) ToggleFocus() { - if m.IsFocusedOnList() { - m.FocusDetail() - } else { - m.FocusList() - } -} - -func (m *Model) FocusedIssueList() *IssueList { - // return the issue list of the currently focused window so we can use two tui windows for issues - if m.focusedWindow == 0 { - return &m.issueList - } - return &m.closedIssueList -} - -func (m *Model) ToggleFocusedWindow() { - // switch focus between open/in-progress and closed issues window - m.focusedWindow = 1 - m.focusedWindow - if m.focusedWindow == 0 { - if selected := m.issueList.SelectedItem(); selected.ID != "" { - m.setDetailIssueWithComments(selected.Issue) - } - } else { - if selected := m.closedIssueList.SelectedItem(); selected.ID != "" { - m.setDetailIssueWithComments(selected.Issue) - } - } - m.issueDetail.SetFocused(m.IsFocusedOnDetail()) -} From 341a00d42c392f17219ecaac98f4704627710302 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:24:16 +0100 Subject: [PATCH 44/73] we no longer have to change focus --- pkg/tui/views/kanban/keys.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/tui/views/kanban/keys.go b/pkg/tui/views/kanban/keys.go index bd78dbe..f146489 100644 --- a/pkg/tui/views/kanban/keys.go +++ b/pkg/tui/views/kanban/keys.go @@ -2,7 +2,7 @@ package kanban import ( "charm.land/bubbles/v2/key" - "charm.land/bubbletea/v2" + tea "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" ) @@ -78,10 +78,6 @@ func (d *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { cmd = d.moveIssue(+1) case !d.IsInModal() && key.Matches(msg, d.keyMap.MoveIssueLeft): cmd = d.moveIssue(-1) - case d.IsFocusedOnList() && key.Matches(msg, d.keyMap.SelectIssue): - d.FocusDetail() - case d.IsFocusedOnDetail() && (key.Matches(msg, d.keyMap.BackToList) || key.Matches(msg, d.keyMap.SelectIssue)): - d.FocusList() case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollUp): d.issueDetail.ScrollUp(1) case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown): From 0774f20b7c111f5b202ff8c8cf1bbbdde0004fba Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:25:12 +0100 Subject: [PATCH 45/73] clean this --- pkg/tui/views/kanban/model.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/tui/views/kanban/model.go b/pkg/tui/views/kanban/model.go index 8c2568e..ad9d68c 100644 --- a/pkg/tui/views/kanban/model.go +++ b/pkg/tui/views/kanban/model.go @@ -9,7 +9,7 @@ import ( "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" - "github.com/LazyBachelor/LazyPM/pkg/tui/issues" + "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" ) type ( @@ -82,6 +82,8 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui quitChan: quitChan, submitChan: submitChan, } + m.issueDetail = components.NewIssueDetail() + m.helpBar = components.NewHelpBar(components.ViewKanban) allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) todoIssues := components.StatusOnly(allIssues, models.StatusOpen) @@ -89,12 +91,10 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked) doneIssues := components.StatusOnly(allIssues, models.StatusClosed) - m.todoList = components.NewIssueListFromIssues(app, todoIssues, 0, 0) - m.inProgList = components.NewIssueListFromIssues(app, inProgIssues, 0, 0) - m.blockedList = components.NewIssueListFromIssues(app, blockedIssues, 0, 0) - m.doneList = components.NewIssueListFromIssues(app, doneIssues, 0, 0) - m.issueDetail = components.NewIssueDetail() - m.helpBar = components.NewHelpBar(components.ViewKanban) + m.todoList = components.NewIssueListFromIssues(app, todoIssues, 20, 10) + m.inProgList = components.NewIssueListFromIssues(app, inProgIssues, 20, 10) + m.blockedList = components.NewIssueListFromIssues(app, blockedIssues, 20, 10) + m.doneList = components.NewIssueListFromIssues(app, doneIssues, 20, 10) inputs := components.NewIssueInputs() m.titleInput = inputs.Title @@ -275,5 +275,5 @@ func (m *Model) moveIssue(delta int) tea.Cmd { } newStatus := statusForColumn(newCol) - return issues.UpdateIssueStatusCmd(m.app, selected.ID, string(newStatus)) + return msgs.UpdateIssueStatusCmd(m.app, selected.ID, string(newStatus)) } From bee0becde4d58b12b55b7711aab50f0da4559474 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:41:50 +0100 Subject: [PATCH 46/73] make full help show in items in vertical order --- pkg/tui/components/helpbar.go | 53 +++++++++++------------------------ 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/pkg/tui/components/helpbar.go b/pkg/tui/components/helpbar.go index 54fe6f6..85d004a 100644 --- a/pkg/tui/components/helpbar.go +++ b/pkg/tui/components/helpbar.go @@ -85,53 +85,34 @@ func (h HelpBar) fullHelp() string { return "" } - keyStyle := lipgloss.NewStyle(). - Width(fullHelpKeyWidth). - Align(lipgloss.Right) + keyStyle := lipgloss.NewStyle().Width(fullHelpKeyWidth).Align(lipgloss.Right) + descStyle := lipgloss.NewStyle().Width(fullHelpDescWidth) + cellStyle := lipgloss.NewStyle().Width(fullHelpKeyWidth + 1 + fullHelpDescWidth) - descStyle := lipgloss.NewStyle(). - Width(fullHelpDescWidth) - - cellStyle := lipgloss.NewStyle(). - Width(fullHelpKeyWidth + 1 + fullHelpDescWidth) - - renderHelpItem := func(item HelpItem) string { + renderItem := func(item HelpItem) string { return cellStyle.Render( - lipgloss.JoinHorizontal( - lipgloss.Left, - keyStyle.Render(styles.HighlightKey(item.Key)), - " ", - descStyle.Render(item.Desc), - ), + keyStyle.Render(styles.HighlightKey(item.Key)) + " " + descStyle.Render(item.Desc), ) } - innerWidth := max(h.width - 2, 1) - + innerWidth := max(h.width-2, 1) cellWidth := fullHelpKeyWidth + 1 + fullHelpDescWidth - cols := fitHelpColumns( - innerWidth, - cellWidth, - fullHelpColGap, - maxFullHelpCols, - ) - + cols := fitHelpColumns(innerWidth, cellWidth, fullHelpColGap, maxFullHelpCols) + rows := (len(h.config.FullItems) + cols - 1) / cols gap := strings.Repeat(" ", fullHelpColGap) - rows := make([]string, 0, (len(h.config.FullItems)+cols-1)/cols) - for i := 0; i < len(h.config.FullItems); i += cols { - end := min(i + cols, len(h.config.FullItems)) - - cells := make([]string, 0, cols) - for _, item := range h.config.FullItems[i:end] { - cells = append(cells, renderHelpItem(item)) + var result []string + for r := range rows { + var cells []string + for c := range cols { + if idx := r + c*rows; idx < len(h.config.FullItems) { + cells = append(cells, renderItem(h.config.FullItems[idx])) + } } - - rows = append(rows, joinHorizontalWithGap(cells, gap)) + result = append(result, joinHorizontalWithGap(cells, gap)) } - content := lipgloss.JoinVertical(lipgloss.Left, rows...) - + content := lipgloss.JoinVertical(lipgloss.Left, result...) return lipgloss.NewStyle(). Border(lipgloss.Border{Top: "─"}, true, false, false, false). BorderForeground(styles.SecondaryBorder). From 7d623439b9ff3507bacd68fc2c8032b2cfb497a0 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:42:06 +0100 Subject: [PATCH 47/73] make issue critical --- cmd/pm/tasks/codingTask.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index f01577f..8d5f2e7 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -124,7 +124,7 @@ func (t *CodingTask) Setup(ctx context.Context) error { t.issue = NewIssueBuilder(). WithTitle("Upgrade MongoDB Driver"). - WithDescription(codingDescription). + WithPriority(4).WithDescription(codingDescription). Build() return t.app.Issues.CreateIssue(ctx, t.issue, "") From 5253745445951adc5eba66e6bd90bdeb0bd98daa Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:42:30 +0100 Subject: [PATCH 48/73] fix description suddenly cutting off --- pkg/tui/components/issue_detail.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/tui/components/issue_detail.go b/pkg/tui/components/issue_detail.go index 83f8948..abc7022 100644 --- a/pkg/tui/components/issue_detail.go +++ b/pkg/tui/components/issue_detail.go @@ -44,6 +44,8 @@ func (i *IssueDetail) SetFocused(focused bool) { } func (i *IssueDetail) refreshContent() { + contentWidth := max(i.viewport.Width()-2, 1) + titleRow := styles.RowStyle.Render( styles.TitleStyle.Render(i.issue.Title), ) @@ -81,7 +83,8 @@ func (i *IssueDetail) refreshContent() { ) descLabel := styles.LabelStyle.Render("Description:") - descContent := styles.ValueStyle.Render(i.issue.Description) + descStyle := styles.ValueStyle.Width(contentWidth) + descContent := descStyle.Render(i.issue.Description) commentsLabel := styles.LabelStyle.MarginTop(1).Render("Comments:") @@ -126,6 +129,8 @@ func (i *IssueDetail) ScrollDown(lines int) { } func (i *IssueDetail) renderComments() []string { + contentWidth := max(i.viewport.Width()-4, 1) + var parts []string if len(i.comments) == 0 { parts = append(parts, styles.ValueStyle.Render("No comments yet.")) @@ -133,9 +138,10 @@ func (i *IssueDetail) renderComments() []string { for _, c := range i.comments { authorDate := lipgloss.NewStyle().Foreground(styles.Primary).Render(c.Author) + " " + lipgloss.NewStyle().Foreground(styles.FaintText).Render(formatCommentTime(c.CreatedAt)) + commentTextStyle := styles.ValueStyle.Width(contentWidth) commentRow := lipgloss.JoinVertical(lipgloss.Left, authorDate, - styles.ValueStyle.MarginLeft(1).Render(c.Text), + commentTextStyle.MarginLeft(1).Render(c.Text), ) parts = append(parts, commentRow) } From 26210c4afb856f194fb040fb3eeec76b04a2ea47 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 12:42:48 +0100 Subject: [PATCH 49/73] centralize submission handling --- pkg/tui/views/dashboard/model.go | 12 ++++++++++ pkg/tui/views/dashboard/operations.go | 31 ++++++++++++++------------ pkg/tui/views/kanban/model.go | 11 +++++++++ pkg/tui/views/kanban/operations.go | 32 +++++++++++++++------------ 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index 9ca4fce..a1bce80 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -143,6 +143,18 @@ func (m *Model) logAction(action string) { } } +// submitValidation sends a validation request to the submit channel. +// Call this after every successful user action that modifies issues. +func (m *Model) submitValidation() { + if m.submitChan != nil { + select { + case m.submitChan <- struct{}{}: + m.logAction("tui submitted validation") + default: + } + } +} + func (m *Model) startEditTitle(selected ListIssue) { m.editingTitle = true m.editingIssueID = selected.ID diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index b2d6e81..a5c4da4 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -41,17 +41,17 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { } } - if m.submitChan != nil { - select { - case m.submitChan <- struct{}{}: - m.logAction("tui submitted validation") - default: - } - } - return tea.Sequence(setItemsCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: issueID} }) } +// refreshAndSubmit refreshes the issue lists and submits validation. +// This is a wrapper that should be used after any successful user action. +func (m *Model) refreshAndSubmit(issueID string) tea.Cmd { + refreshCmd := m.refreshIssueListsAndSelectIssue(issueID) + m.submitValidation() + return refreshCmd +} + func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case msgs.TitleUpdatedMsg: @@ -63,7 +63,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } m.logAction("tui updated issue title") - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.DescriptionUpdatedMsg: m.editingDescription = false @@ -74,7 +74,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } m.logAction("tui updated issue description") - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.StatusUpdatedMsg: m.choosingStatus = false @@ -84,7 +84,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } m.logAction("tui updated issue status") - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.PriorityUpdatedMsg: m.choosingPriority = false @@ -94,7 +94,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } m.logAction("tui updated issue priority") - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.TypeUpdatedMsg: m.choosingType = false @@ -104,7 +104,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } m.logAction("tui updated issue type") - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.AssigneeUpdatedMsg: m.editingAssignee = false @@ -115,7 +115,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } m.logAction("tui updated issue assignee") - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.SelectIssueMsg: m.issueList.SelectIssueID(msg.IssueID) @@ -150,6 +150,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.setDetailIssueWithComments(*selectedIssue) m.logAction("tui created issue") + m.submitValidation() return m, tea.Sequence(setItemsCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} }) case msgs.IssueCommentAddedMsg: m.addingComment = false @@ -159,6 +160,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } + m.submitValidation() return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case msgs.DeletedMsg: m.confirmingDelete = false @@ -168,6 +170,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } m.logAction("tui deleted issue") + m.submitValidation() return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case tea.KeyPressMsg: diff --git a/pkg/tui/views/kanban/model.go b/pkg/tui/views/kanban/model.go index ad9d68c..d1afc1d 100644 --- a/pkg/tui/views/kanban/model.go +++ b/pkg/tui/views/kanban/model.go @@ -178,6 +178,17 @@ func (m *Model) logAction(action string) { } } +// submitValidation sends a validation request to the submit channel. +func (m *Model) submitValidation() { + if m.submitChan != nil { + select { + case m.submitChan <- struct{}{}: + m.logAction("tui submitted validation") + default: + } + } +} + func (m *Model) Init() tea.Cmd { if m.submitChan != nil { m.submitChan <- struct{}{} diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index b19ac84..dd4d73c 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -54,17 +54,17 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { m.blockedList.SelectIssueID(issueID) m.doneList.SelectIssueID(issueID) - if m.submitChan != nil { - select { - case m.submitChan <- struct{}{}: - m.logAction("tui submitted validation") - default: - } - } - return tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd) } +// refreshAndSubmit refreshes the issue lists and submits validation. +// This is a wrapper that should be used after any successful user action. +func (m *Model) refreshAndSubmit(issueID string) tea.Cmd { + refreshCmd := m.refreshIssueListsAndSelectIssue(issueID) + m.submitValidation() + return refreshCmd +} + func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case msgs.TitleUpdatedMsg: @@ -74,7 +74,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.DescriptionUpdatedMsg: m.editingDescription = false @@ -83,7 +83,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.StatusUpdatedMsg: m.choosingStatus = false @@ -91,7 +91,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.PriorityUpdatedMsg: m.choosingPriority = false @@ -99,7 +99,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.TypeUpdatedMsg: m.choosingType = false @@ -107,7 +107,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.AssigneeUpdatedMsg: m.editingAssignee = false @@ -116,7 +116,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + return m, m.refreshAndSubmit(msg.IssueID) case msgs.SelectIssueMsg: m.todoList.SelectIssueID(msg.IssueID) @@ -160,6 +160,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.issueDetail.SetIssue(*selectedIssue) + m.submitValidation() return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} }) case msgs.DeletedMsg: m.confirmingDelete = false @@ -185,6 +186,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // If there are no msgs at all, clear the detail view and return. if len(todoIssues) == 0 && len(inProgIssues) == 0 && len(blockedIssues) == 0 && len(doneIssues) == 0 { m.issueDetail.SetIssue(models.Issue{}) + m.submitValidation() return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd) } @@ -252,6 +254,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Safety: if targetIssues is still empty here, just clear detail and return. if len(targetIssues) == 0 { m.issueDetail.SetIssue(models.Issue{}) + m.submitValidation() return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd) } @@ -261,6 +264,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } selectedIssue := targetIssues[newIndex] m.issueDetail.SetIssue(*selectedIssue) + m.submitValidation() return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} }) From 7c3eab74612ade5f8fb2ffbdf44f40c2f04d2402 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:44:09 +0100 Subject: [PATCH 50/73] Delete Task Details.txt --- Task Details.txt | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 Task Details.txt diff --git a/Task Details.txt b/Task Details.txt deleted file mode 100644 index c3e26ee..0000000 --- a/Task Details.txt +++ /dev/null @@ -1,12 +0,0 @@ -You are tasked with backlog refinement. - -The product backlog has become cluttered with old and unclear issues. You need to groom the backlog: - -1. Go to the backlog. -2. Find two issues that got the same name or describe the same problem. -3. Open one of these issues. -4. Select "Close issue" -5. Choose "Duplicate issue" as closing reason. -6. Save/close issue. - -Focus on making the backlog a reliable source of upcoming work. \ No newline at end of file From 969d7d07d621e89bfb0d358eb40406578f256744 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:57:34 +0100 Subject: [PATCH 51/73] Update pkg/tui/views/kanban/operations.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/tui/views/kanban/operations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index dd4d73c..6ac8f16 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -183,7 +183,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { blockedCmd := m.blockedList.SetIssues(blockedIssues) doneCmd := m.doneList.SetIssues(doneIssues) - // If there are no msgs at all, clear the detail view and return. + // If there are no issues at all, clear the detail view and return. if len(todoIssues) == 0 && len(inProgIssues) == 0 && len(blockedIssues) == 0 && len(doneIssues) == 0 { m.issueDetail.SetIssue(models.Issue{}) m.submitValidation() From dcd094b6308632a066f5ad832d4971d4407c4486 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:57:47 +0100 Subject: [PATCH 52/73] Update pkg/web/handler/task.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/web/handler/task.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/web/handler/task.go b/pkg/web/handler/task.go index 36c3c1a..87c02be 100644 --- a/pkg/web/handler/task.go +++ b/pkg/web/handler/task.go @@ -32,7 +32,7 @@ func HandleTaskStatus(w http.ResponseWriter, r *http.Request) { if hx.IsHxRequest() { if taskFeedback.Success { w.Header().Set("HX-Trigger", "task-status-success") - w.WriteHeader(http.StatusOK) + w.WriteHeader(http.StatusNoContent) } else { hx.WriteString(`
From c1309e6f31044a224c3912e8c280447b5436b763 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:57:58 +0100 Subject: [PATCH 53/73] Update pkg/tui/views/kanban/operations.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/tui/views/kanban/operations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index 6ac8f16..2a9f790 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -11,7 +11,7 @@ import ( ) // update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view -// Fetch all msgs, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. +// Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { From 89167a6e5aacefb7a695c23044d84793be0a926d Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:58:21 +0100 Subject: [PATCH 54/73] Update pkg/tui/views/dashboard/operations.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/tui/views/dashboard/operations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index a5c4da4..22eaa31 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -27,7 +27,7 @@ func defaultCommentAuthor() string { func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { /* update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view - Fetch all msgs, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. + Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. */ allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { From 6d72c9195bfc8eb00e203431f8dae714efc02370 Mon Sep 17 00:00:00 2001 From: Moira Daniella A Sebastian Date: Fri, 20 Mar 2026 13:55:00 +0100 Subject: [PATCH 55/73] TUI: add quit confirmation --- pkg/tui/components/issue_list.go | 1 + pkg/tui/components/modals.go | 20 ++++++++++++++++ pkg/tui/views/dashboard/keys.go | 3 ++- pkg/tui/views/dashboard/model.go | 34 ++++++++++++++++++++++++++- pkg/tui/views/dashboard/operations.go | 24 +++++++++++++++++++ pkg/tui/views/dashboard/view.go | 13 ++++++++++ pkg/tui/views/kanban/keys.go | 3 ++- pkg/tui/views/kanban/model.go | 31 +++++++++++++++++++++++- pkg/tui/views/kanban/operations.go | 21 +++++++++++++++++ pkg/tui/views/kanban/view.go | 5 ++++ 10 files changed, 151 insertions(+), 4 deletions(-) diff --git a/pkg/tui/components/issue_list.go b/pkg/tui/components/issue_list.go index da81d08..ceb0783 100644 --- a/pkg/tui/components/issue_list.go +++ b/pkg/tui/components/issue_list.go @@ -211,6 +211,7 @@ func NewIssueListFromIssues(app *app.App, issues []*models.Issue, width, height l.SetShowHelp(false) l.SetShowStatusBar(false) l.SetFilteringEnabled(true) + l.DisableQuitKeybindings() l.FilterInput.PromptStyle = styles.FilterPromptStyle l.FilterInput.Cursor.Style = styles.FilterStyle l.FilterInput.TextStyle = styles.FilterInputStyle diff --git a/pkg/tui/components/modals.go b/pkg/tui/components/modals.go index 5eee8d7..fbbf583 100644 --- a/pkg/tui/components/modals.go +++ b/pkg/tui/components/modals.go @@ -68,6 +68,22 @@ func RenderCreateIssue(width, height int, inputView string) string { return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, createBox) } +func RenderConfirmQuit(width, height int) string { + if width < 5 || height < 5 { + return "" + } + confirmContent := lipgloss.JoinVertical(lipgloss.Left, + styles.LabelStyle.Render("Sure you want to quit?"), + lipgloss.NewStyle().Foreground(styles.FaintText).Render("y = quit n/Esc = cancel"), + ) + confirmBoxWidth := modalBoxWidth(40, width) + confirmBox := styles.ContainerStyle. + Width(confirmBoxWidth). + BorderForeground(styles.PrimaryBorder). + Render(confirmContent) + return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, confirmBox) +} + func RenderConfirmDelete(width, height int, issueID string) string { if width < 5 || height < 5 { return "" @@ -157,6 +173,7 @@ func RenderChooseType(width, height int, issueID string) string { // no modal is active. func RenderModals( width, height int, + confirmingQuit bool, editingTitle bool, titleInputView string, editingDescription bool, descriptionInputView string, creatingIssue bool, createTitleInputView string, @@ -167,6 +184,9 @@ func RenderModals( editingAssignee bool, assigneeInputView string, mainView string, ) string { + if confirmingQuit { + return RenderConfirmQuit(width, height) + } if editingTitle { return RenderEditTitle(width, height, titleInputView) } diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 0793cb8..0d9ca0d 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -87,8 +87,9 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { d.helpBar.ToggleHelp() d.logAction("tui toggled help") case key.Matches(msg, d.keyMap.Quit): + d.startConfirmQuit() d.logAction("tui quit requested") - return tea.Quit + return nil case key.Matches(msg, d.keyMap.SwitchWindow): d.ToggleFocusedWindow() case !d.IsInModal() && key.Matches(msg, d.keyMap.SwitchToKanbanBoard): diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index bed24a1..0c8432b 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -45,6 +45,7 @@ type Model struct { confirmingDelete bool // true while confirming a delete deleteConfirmID string deleteConfirmIndex int + confirmingQuit bool // true while confirming quit choosingStatus bool statusIssueID string @@ -170,6 +171,37 @@ func (m *Model) startConfirmDelete(issueID string, index int) { m.deleteConfirmIndex = index } +func (m *Model) startConfirmQuit() { + m.confirmingQuit = true +} + +// cancelAllModals closes any open modal/input so quit can be triggered from anywhere. +func (m *Model) cancelAllModals() { + m.creatingIssue = false + m.createTitleInput.Blur() + m.createTitleInput.Reset() + m.editingTitle = false + m.titleInput.Blur() + m.editingDescription = false + m.descriptionInput.Blur() + m.addingComment = false + m.commentInput.Blur() + m.commentInput.Reset() + m.editingAssignee = false + m.assigneeInput.Blur() + m.choosingStatus = false + m.statusIssueID = "" + m.choosingPriority = false + m.priorityIssueID = "" + m.choosingType = false + m.typeIssueID = "" + m.confirmingDelete = false + m.deleteConfirmID = "" + m.choosingCloseReason = false + m.closingOtherReason = false + m.closeReasonInput.Blur() +} + func (m *Model) startChooseStatus(selected ListIssue) { m.choosingStatus = true m.statusIssueID = selected.ID @@ -200,7 +232,7 @@ func (m *Model) Init() tea.Cmd { func (m *Model) IsInModal() bool { return m.editingTitle || m.creatingIssue || m.editingDescription || m.choosingStatus || m.choosingPriority || m.confirmingDelete || - m.choosingType || m.editingAssignee || + m.confirmingQuit || m.choosingType || m.editingAssignee || m.choosingCloseReason || m.closingOtherReason } diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 306d009..8a067d1 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -10,6 +10,7 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/issues" "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" ) @@ -323,6 +324,29 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) case tea.KeyMsg: + // Always allow quit with q/ctrl+c, even when in inputs or other modals + if !m.confirmingQuit && (key.Matches(msg, m.keyMap.Quit) || msg.String() == "q" || msg.String() == "ctrl+c") { + m.cancelAllModals() + m.startConfirmQuit() + m.logAction("tui quit requested") + return m, nil + } + + if m.confirmingQuit { + switch msg.String() { + case "y", "Y", "ctrl+c": + m.logAction("tui confirmed quit") + m.confirmingQuit = false + return m, tea.Quit + case "n", "N", "esc": + m.logAction("tui canceled quit") + m.confirmingQuit = false + return m, nil + default: + return m, nil + } + } + if m.confirmingDelete { switch msg.String() { case "y", "Y": diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index 7824256..f582749 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -61,6 +61,19 @@ func (m *Model) View() string { mainView := lipgloss.JoinVertical(lipgloss.Left, header, content, footer) + if m.confirmingQuit { + confirmContent := lipgloss.JoinVertical(lipgloss.Left, + styles.LabelStyle.Render("Sure you want to quit?"), + lipgloss.NewStyle().Foreground(styles.FaintText).Render("y = quit n/Esc = cancel"), + ) + confirmBoxWidth := min(40, m.width-4) + confirmBox := styles.ContainerStyle. + Width(confirmBoxWidth). + BorderForeground(styles.PrimaryBorder). + Render(confirmContent) + return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, confirmBox) + } + if m.editingAssignee { editBoxWidth := min(60, m.width-4) m.assigneeInput.Width = editBoxWidth - 2 diff --git a/pkg/tui/views/kanban/keys.go b/pkg/tui/views/kanban/keys.go index df8345b..21e8642 100644 --- a/pkg/tui/views/kanban/keys.go +++ b/pkg/tui/views/kanban/keys.go @@ -59,7 +59,8 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { case key.Matches(msg, d.keyMap.Help): d.helpBar.ToggleHelp() case key.Matches(msg, d.keyMap.Quit): - return tea.Quit + d.startConfirmQuit() + return nil case !d.IsInModal() && msg.String() == " ": return func() tea.Msg { return nil } // consume space case !d.IsInModal() && key.Matches(msg, d.keyMap.SwitchToDashboard): diff --git a/pkg/tui/views/kanban/model.go b/pkg/tui/views/kanban/model.go index 5a38984..715ff2d 100644 --- a/pkg/tui/views/kanban/model.go +++ b/pkg/tui/views/kanban/model.go @@ -47,6 +47,7 @@ type Model struct { confirmingDelete bool // true while confirming a delete deleteConfirmID string deleteConfirmIndex int + confirmingQuit bool // true while confirming quit choosingStatus bool // true while choosing a status statusIssueID string @@ -146,6 +147,34 @@ func (m *Model) startConfirmDelete(issueID string, index int) { m.deleteConfirmIndex = index } +func (m *Model) startConfirmQuit() { + m.confirmingQuit = true +} + +// cancelAllModals closes any open modal/input so quit can be triggered from anywhere. +func (m *Model) cancelAllModals() { + m.creatingIssue = false + m.createTitleInput.Blur() + m.createTitleInput.Reset() + m.editingTitle = false + m.titleInput.Blur() + m.editingDescription = false + m.descriptionInput.Blur() + m.editingAssignee = false + m.assigneeInput.Blur() + m.choosingStatus = false + m.statusIssueID = "" + m.choosingPriority = false + m.priorityIssueID = "" + m.choosingType = false + m.typeIssueID = "" + m.confirmingDelete = false + m.deleteConfirmID = "" + m.choosingCloseReason = false + m.closingOtherReason = false + m.closeReasonInput.Blur() +} + func (m *Model) startChooseStatus(selected ListIssue) { m.choosingStatus = true m.statusIssueID = selected.ID @@ -176,7 +205,7 @@ func (m *Model) Init() tea.Cmd { func (m *Model) IsInModal() bool { return m.editingTitle || m.creatingIssue || m.editingDescription || m.choosingStatus || m.choosingPriority || m.confirmingDelete || - m.choosingType || m.editingAssignee || + m.confirmingQuit || m.choosingType || m.editingAssignee || m.choosingCloseReason || m.closingOtherReason } diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index db09d9f..4e1af9b 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -6,6 +6,7 @@ import ( "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/issues" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" ) @@ -259,6 +260,26 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) case tea.KeyMsg: + // Always allow quit with q/ctrl+c, even when in inputs or other modals + if !m.confirmingQuit && (key.Matches(msg, m.keyMap.Quit) || msg.String() == "q" || msg.String() == "ctrl+c") { + m.cancelAllModals() + m.startConfirmQuit() + return m, nil + } + + if m.confirmingQuit { + switch msg.String() { + case "y", "Y", "ctrl+c": + m.confirmingQuit = false + return m, tea.Quit + case "n", "N", "esc": + m.confirmingQuit = false + return m, nil + default: + return m, nil + } + } + if m.confirmingDelete { switch msg.String() { case "y", "Y": diff --git a/pkg/tui/views/kanban/view.go b/pkg/tui/views/kanban/view.go index 8e6a279..371df5a 100644 --- a/pkg/tui/views/kanban/view.go +++ b/pkg/tui/views/kanban/view.go @@ -82,6 +82,10 @@ func (m *Model) View() string { mainView = lipgloss.JoinVertical(lipgloss.Left, header, content, spacer, footer) } + if m.confirmingQuit { + return components.RenderConfirmQuit(m.width, m.height) + } + if m.choosingCloseReason { reasonContent := lipgloss.JoinVertical(lipgloss.Left, styles.LabelStyle.Render("Choose closing reason for "+m.closeReasonIssueID+":"), @@ -114,6 +118,7 @@ func (m *Model) View() string { return components.RenderModals( m.width, m.height, + m.confirmingQuit, m.editingTitle, m.titleInput.View(), m.editingDescription, From 7facfb6afe95da290a6020b77c5abf615e7f8d28 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:28:10 -0700 Subject: [PATCH 56/73] Merge pull request #73 from LazyBachelor/LPM-138 LPM-138 Refactor Tui with composable modals and use canvas Refactors the TUI (dashboard + kanban) to use a shared, composable modal system with canvas-based overlay rendering, while consolidating styling into internal/style and expanding issue interactions (e.g., comments). Changes: Introduces a new pkg/tui/modal system (manager/stack + multiple modal types) and overlays modals using Lipgloss compositor layers. Refactors dashboard/kanban views and input handling to use the modal manager + focus manager instead of per-modal boolean state. Consolidates TUI styling by moving from pkg/tui/styles to internal/style and updates components to use the new style package; adds a shared footer renderer. --- internal/style/styles.go | 102 ++++- internal/utils/truncate/trunc.go | 43 ++ internal/utils/user/user.go | 19 + pkg/tui/components/footer.go | 48 +++ pkg/tui/components/header.go | 4 +- pkg/tui/components/helpbar.go | 10 +- pkg/tui/components/issue_detail.go | 50 +-- pkg/tui/components/issue_list.go | 16 +- pkg/tui/components/modals.go | 273 ------------- pkg/tui/modal/base.go | 102 +++++ pkg/tui/modal/confirm.go | 141 +++++++ pkg/tui/modal/focus.go | 143 +++++++ pkg/tui/modal/manager.go | 256 ++++++++++++ pkg/tui/modal/modal.go | 167 ++++++++ pkg/tui/modal/select.go | 215 ++++++++++ pkg/tui/modal/textarea.go | 177 ++++++++ pkg/tui/modal/textinput.go | 179 ++++++++ pkg/tui/msgs/msgs.go | 27 +- pkg/tui/styles/styles.go | 85 ---- pkg/tui/views/dashboard/keys.go | 120 +++--- pkg/tui/views/dashboard/model.go | 208 +++------- pkg/tui/views/dashboard/operations.go | 564 +++++++++++-------------- pkg/tui/views/dashboard/view.go | 190 +-------- pkg/tui/views/kanban/keys.go | 133 +++--- pkg/tui/views/kanban/model.go | 253 +++++------- pkg/tui/views/kanban/operations.go | 568 ++++++++++++-------------- pkg/tui/views/kanban/view.go | 128 ++---- 27 files changed, 2469 insertions(+), 1752 deletions(-) create mode 100644 internal/utils/truncate/trunc.go create mode 100644 internal/utils/user/user.go create mode 100644 pkg/tui/components/footer.go delete mode 100644 pkg/tui/components/modals.go create mode 100644 pkg/tui/modal/base.go create mode 100644 pkg/tui/modal/confirm.go create mode 100644 pkg/tui/modal/focus.go create mode 100644 pkg/tui/modal/manager.go create mode 100644 pkg/tui/modal/modal.go create mode 100644 pkg/tui/modal/select.go create mode 100644 pkg/tui/modal/textarea.go create mode 100644 pkg/tui/modal/textinput.go delete mode 100644 pkg/tui/styles/styles.go diff --git a/internal/style/styles.go b/internal/style/styles.go index 5e56d2f..a79d65e 100644 --- a/internal/style/styles.go +++ b/internal/style/styles.go @@ -2,30 +2,98 @@ package style import ( "charm.land/lipgloss/v2" -) - -// Color palette -var ( - PrimaryColor = lipgloss.Color("6") - SecondaryColor = lipgloss.Color("2") - AccentColor = lipgloss.Color("7") - TextColor = lipgloss.Color("15") - - BorderColor = lipgloss.Color("8") + "charm.land/lipgloss/v2/compat" ) var ( - AppStyle = lipgloss.NewStyle().Padding(1, 2).Foreground(TextColor) + Primary = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")} + Secondary = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")} + + Success = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")} + Warning = compat.AdaptiveColor{Light: lipgloss.Color("#F59E0B"), Dark: lipgloss.Color("#F59E0B")} + Error = compat.AdaptiveColor{Light: lipgloss.Color("#FE5F86"), Dark: lipgloss.Color("#FE5F86")} + + PrimaryText = compat.AdaptiveColor{Light: lipgloss.Color("#1A1A1A"), Dark: lipgloss.Color("#E0E0E0")} + SecondaryText = compat.AdaptiveColor{Light: lipgloss.Color("#666666"), Dark: lipgloss.Color("#999999")} + FaintText = compat.AdaptiveColor{Light: lipgloss.Color("#999999"), Dark: lipgloss.Color("#666666")} + + PrimaryBorder = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")} + SecondaryBorder = compat.AdaptiveColor{Light: lipgloss.Color("#CCCCCC"), Dark: lipgloss.Color("#444444")} + + SelectedBackground = compat.AdaptiveColor{Light: lipgloss.Color("#E8E8E8"), Dark: lipgloss.Color("#333333")} +) + +const ( + ListViewRatio = 52 // Percentage of total width allocated to the list view + LabelWidth = 14 + MarginBottomSmall = 1 +) + +var DefaultBorder = lipgloss.ThickBorder() + +var ( + HeaderStyle = lipgloss.NewStyle().Foreground(Primary).Padding(0, 1).Bold(true) + HeaderTitleStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true).Padding(0) +) + +var ContainerStyle = lipgloss.NewStyle(). + Border(DefaultBorder, true, false, false, false). + BorderForeground(SecondaryBorder). + Padding(1) + +var ModalContainerStyle = lipgloss.NewStyle(). + Border(DefaultBorder). + BorderForeground(PrimaryBorder). + Padding(2, 3) + +var DetailsContainerStyle = lipgloss.NewStyle(). + Border(DefaultBorder, true, false, false, true). + BorderForeground(SecondaryBorder). + Padding(1) + +var ( + RowStyle = lipgloss.NewStyle().MarginBottom(MarginBottomSmall) + TitleStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true) + LabelStyle = lipgloss.NewStyle().Foreground(SecondaryText) + ValueStyle = lipgloss.NewStyle().Foreground(PrimaryText) + IssueTypeStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true) ) var ( - DefaultBorder = lipgloss.NormalBorder() - BorderStyle = lipgloss.NewStyle().Border(DefaultBorder).BorderForeground(BorderColor) + FilterStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true).Padding(0, 1) + FilterInputStyle = lipgloss.NewStyle().Foreground(PrimaryText).Padding(0, 1) + FilterPromptStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true) ) +func StatusStyle(status string) lipgloss.Style { + style := lipgloss.NewStyle().Bold(true) + switch status { + case "open": + return style.Foreground(Secondary) + case "closed": + return style.Foreground(FaintText) + case "in_progress": + return style.Foreground(Warning) + case "blocked": + return style.Foreground(Error) + default: + return style.Foreground(SecondaryText) + } +} + +func HighlightKey(key string) string { + return lipgloss.NewStyle(). + Foreground(Primary). + Bold(true). + Padding(0, 1). + Render(key) +} + var ( - TitleStyle = lipgloss.NewStyle().Foreground(PrimaryColor).Bold(true) - TextStyle = lipgloss.NewStyle().Foreground(TextColor) - HelpStyle = lipgloss.NewStyle().Align(lipgloss.Center).Foreground(AccentColor) - ErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true) // Red color for errors + TextStyle = lipgloss.NewStyle().Foreground(PrimaryText) + BorderStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder()).BorderForeground(SecondaryBorder) + ErrorStyle = lipgloss.NewStyle().Foreground(Error).Bold(true) + HelpStyle = lipgloss.NewStyle().Align(lipgloss.Center).Foreground(Secondary) ) + +var SecondaryColor = lipgloss.Color("#02BA84") diff --git a/internal/utils/truncate/trunc.go b/internal/utils/truncate/trunc.go new file mode 100644 index 0000000..9b34f11 --- /dev/null +++ b/internal/utils/truncate/trunc.go @@ -0,0 +1,43 @@ +package truncate + +import "charm.land/lipgloss/v2" + +// TruncateToWidth trims the given text so that its rendered width does not +// exceed maxWidth. If truncation occurs and there is room, an ellipsis is +// appended to indicate that the text was shortened. +func TruncateToWidth(text string, maxWidth int) string { + if maxWidth <= 0 { + return "" + } + + if lipgloss.Width(text) <= maxWidth { + return text + } + + const ellipsis = "…" + ellipsisWidth := lipgloss.Width(ellipsis) + + if ellipsisWidth > maxWidth { + runes := []rune(text) + if len(runes) > 0 { + firstChar := string(runes[0]) + if lipgloss.Width(firstChar) <= maxWidth { + return firstChar + } + } + return "" + } + + runes := []rune(text) + lastSafe := 0 + for i := range runes { + candidate := string(runes[:i+1]) + if lipgloss.Width(candidate)+ellipsisWidth > maxWidth { + break + } + lastSafe = i + 1 + } + + current := string(runes[:lastSafe]) + return current + ellipsis +} diff --git a/internal/utils/user/user.go b/internal/utils/user/user.go new file mode 100644 index 0000000..16047f2 --- /dev/null +++ b/internal/utils/user/user.go @@ -0,0 +1,19 @@ +package user + +import ( + "os" + "os/user" +) + +func GetOsUsername() string { + if u, err := user.Current(); err == nil && u.Username != "" { + return u.Username + } + if s := os.Getenv("USER"); s != "" { + return s + } + if s := os.Getenv("USERNAME"); s != "" { + return s + } + return "user" +} diff --git a/pkg/tui/components/footer.go b/pkg/tui/components/footer.go new file mode 100644 index 0000000..10cde57 --- /dev/null +++ b/pkg/tui/components/footer.go @@ -0,0 +1,48 @@ +package components + +import ( + "charm.land/lipgloss/v2" + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/style" + "github.com/LazyBachelor/LazyPM/internal/utils/truncate" +) + +// RenderFooter renders the shared footer with the help bar and optional +// validation feedback message. +func RenderFooter(width int, helpBar *HelpBar, feedback models.ValidationFeedback) string { + feedbackStatus := feedback.Message + + // Ensure the feedback message does not exceed the total available width. + if feedback.Message != "" { + // Allocate at least 30% of width for feedback, but not more than 60% + feedbackWidth := max(width*3/10, min(width/2, 50)) + styledFeedback := style.ErrorStyle.Render(feedbackStatus + " [Press '?' for details]") + feedbackStatus = truncate.TruncateToWidth(styledFeedback, feedbackWidth) + + if helpBar.IsExpanded() && feedbackStatus != "" { + for _, check := range feedback.Checks { + var prefix string + if check.Valid { + prefix = "✅ " + } else { + prefix = "❌ " + } + + remainingWidth := max(width/2, 10) + styledCheckMsg := style.TextStyle.Render(check.Message) + truncatedMsg := truncate.TruncateToWidth(styledCheckMsg, remainingWidth) + + feedbackStatus += "\n" + prefix + truncatedMsg + } + } + } + + if feedbackStatus == "" { + return helpBar.View() + } + + helpWidth := max(width-lipgloss.Width(feedbackStatus), 0) + + helpBar.SetWidth(helpWidth) + return lipgloss.JoinHorizontal(lipgloss.Left, helpBar.View(), feedbackStatus) +} diff --git a/pkg/tui/components/header.go b/pkg/tui/components/header.go index c5d625b..6c646d2 100644 --- a/pkg/tui/components/header.go +++ b/pkg/tui/components/header.go @@ -2,7 +2,7 @@ package components import ( "charm.land/lipgloss/v2" - "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/LazyBachelor/LazyPM/internal/style" ) type Header struct { @@ -14,7 +14,7 @@ func NewHeader(title string) Header { } func (h Header) View(width int) string { - title := styles.HeaderTitleStyle.Render(h.Title) + title := style.HeaderTitleStyle.Render(h.Title) return lipgloss.PlaceHorizontal( width, diff --git a/pkg/tui/components/helpbar.go b/pkg/tui/components/helpbar.go index 85d004a..27ec2a7 100644 --- a/pkg/tui/components/helpbar.go +++ b/pkg/tui/components/helpbar.go @@ -4,7 +4,7 @@ import ( "strings" "charm.land/lipgloss/v2" - "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/LazyBachelor/LazyPM/internal/style" ) type ViewKind int @@ -66,7 +66,7 @@ func (h HelpBar) shortHelp() string { for _, item := range h.config.ShortItems { items = append( items, - styles.HighlightKey(item.Key)+item.Desc+" ", + style.HighlightKey(item.Key)+item.Desc+" ", ) } @@ -74,7 +74,7 @@ func (h HelpBar) shortHelp() string { return lipgloss.NewStyle(). Border(lipgloss.Border{Top: "─"}, true, false, false, false). - BorderForeground(styles.SecondaryBorder). + BorderForeground(style.SecondaryBorder). Padding(0, 1). Width(h.width). Render(content) @@ -91,7 +91,7 @@ func (h HelpBar) fullHelp() string { renderItem := func(item HelpItem) string { return cellStyle.Render( - keyStyle.Render(styles.HighlightKey(item.Key)) + " " + descStyle.Render(item.Desc), + keyStyle.Render(style.HighlightKey(item.Key)) + " " + descStyle.Render(item.Desc), ) } @@ -115,7 +115,7 @@ func (h HelpBar) fullHelp() string { content := lipgloss.JoinVertical(lipgloss.Left, result...) return lipgloss.NewStyle(). Border(lipgloss.Border{Top: "─"}, true, false, false, false). - BorderForeground(styles.SecondaryBorder). + BorderForeground(style.SecondaryBorder). Padding(0, 1). Width(h.width). Render(content) diff --git a/pkg/tui/components/issue_detail.go b/pkg/tui/components/issue_detail.go index abc7022..7e1ba92 100644 --- a/pkg/tui/components/issue_detail.go +++ b/pkg/tui/components/issue_detail.go @@ -6,7 +6,7 @@ import ( "charm.land/bubbles/v2/viewport" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/LazyBachelor/LazyPM/internal/style" ) type IssueDetail struct { @@ -46,20 +46,20 @@ func (i *IssueDetail) SetFocused(focused bool) { func (i *IssueDetail) refreshContent() { contentWidth := max(i.viewport.Width()-2, 1) - titleRow := styles.RowStyle.Render( - styles.TitleStyle.Render(i.issue.Title), + titleRow := style.RowStyle.Render( + style.TitleStyle.Render(i.issue.Title), ) - idRow := styles.RowStyle.Render( - styles.LabelStyle.Render("ID:") + styles.ValueStyle.Render(i.issue.ID), + idRow := style.RowStyle.Render( + style.LabelStyle.Render("ID:") + style.ValueStyle.Render(i.issue.ID), ) - typeRow := styles.RowStyle.Render( - styles.LabelStyle.Render("Type:") + styles.ValueStyle.Render(string(i.issue.IssueType)), + typeRow := style.RowStyle.Render( + style.LabelStyle.Render("Type:") + style.ValueStyle.Render(string(i.issue.IssueType)), ) - statusRow := styles.RowStyle.Render( - styles.LabelStyle.Render("Status:") + styles.StatusStyle(string(i.issue.Status)).Render(string(i.issue.Status)), + statusRow := style.RowStyle.Render( + style.LabelStyle.Render("Status:") + style.StatusStyle(string(i.issue.Status)).Render(string(i.issue.Status)), ) var closingReasonRow string @@ -70,23 +70,23 @@ func (i *IssueDetail) refreshContent() { } else { closingReason = string(i.issue.CloseReason) } - closingReasonRow = styles.RowStyle.Render( - styles.LabelStyle.Render("Close reason: ") + styles.ValueStyle.Render(closingReason)) + closingReasonRow = style.RowStyle.Render( + style.LabelStyle.Render("Close reason: ") + style.ValueStyle.Render(closingReason)) } - priorityRow := styles.RowStyle.Render( - styles.LabelStyle.Render("Priority:") + styles.ValueStyle.Render(PriorityCodeName(i.issue.Priority)), + priorityRow := style.RowStyle.Render( + style.LabelStyle.Render("Priority:") + style.ValueStyle.Render(PriorityCodeName(i.issue.Priority)), ) - assigneeRow := styles.RowStyle.Render( - styles.LabelStyle.Render("Assignee:") + styles.ValueStyle.Render(i.issue.Assignee), + assigneeRow := style.RowStyle.Render( + style.LabelStyle.Render("Assignee:") + style.ValueStyle.Render(i.issue.Assignee), ) - descLabel := styles.LabelStyle.Render("Description:") - descStyle := styles.ValueStyle.Width(contentWidth) + descLabel := style.LabelStyle.Render("Description:") + descStyle := style.ValueStyle.Width(contentWidth) descContent := descStyle.Render(i.issue.Description) - commentsLabel := styles.LabelStyle.MarginTop(1).Render("Comments:") + commentsLabel := style.LabelStyle.MarginTop(1).Render("Comments:") var parts []string parts = append(parts, titleRow, idRow, typeRow, statusRow, closingReasonRow, priorityRow, assigneeRow, descLabel, descContent, commentsLabel) @@ -106,14 +106,14 @@ func (i IssueDetail) View() string { vpHeight := i.viewport.Height() if i.focused { - return styles.DetailsContainerStyle. - BorderForeground(styles.PrimaryBorder). + return style.DetailsContainerStyle. + BorderForeground(style.PrimaryBorder). Width(vpWidth). Height(vpHeight). MaxHeight(vpHeight). Render(content) } - return styles.DetailsContainerStyle. + return style.DetailsContainerStyle. Width(vpWidth). Height(vpHeight). MaxHeight(vpHeight). @@ -133,12 +133,12 @@ func (i *IssueDetail) renderComments() []string { var parts []string if len(i.comments) == 0 { - parts = append(parts, styles.ValueStyle.Render("No comments yet.")) + parts = append(parts, style.ValueStyle.Render("No comments yet.")) } else { for _, c := range i.comments { - authorDate := lipgloss.NewStyle().Foreground(styles.Primary).Render(c.Author) + " " + - lipgloss.NewStyle().Foreground(styles.FaintText).Render(formatCommentTime(c.CreatedAt)) - commentTextStyle := styles.ValueStyle.Width(contentWidth) + authorDate := lipgloss.NewStyle().Foreground(style.Primary).Render(c.Author) + " " + + lipgloss.NewStyle().Foreground(style.FaintText).Render(formatCommentTime(c.CreatedAt)) + commentTextStyle := style.ValueStyle.Width(contentWidth) commentRow := lipgloss.JoinVertical(lipgloss.Left, authorDate, commentTextStyle.MarginLeft(1).Render(c.Text), diff --git a/pkg/tui/components/issue_list.go b/pkg/tui/components/issue_list.go index d82a16a..e306b4f 100644 --- a/pkg/tui/components/issue_list.go +++ b/pkg/tui/components/issue_list.go @@ -13,7 +13,7 @@ import ( "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/LazyBachelor/LazyPM/internal/style" "github.com/muesli/reflow/truncate" ) @@ -116,7 +116,7 @@ var priorityCodeNames = map[int]string{ func renderHeaders(cols []tableColumn) string { var parts []string - headerStyle := lipgloss.NewStyle().Foreground(styles.FaintText).Bold(true) + headerStyle := lipgloss.NewStyle().Foreground(style.FaintText).Bold(true) for _, col := range cols { colWidth := col.width @@ -302,14 +302,14 @@ func (l IssueList) renderResponsive() string { if l.list.FilterState() == list.Filtering { filterText := l.list.FilterInput.Value() - filterView := styles.FilterStyle.Render("🔍 " + filterText) + filterView := style.FilterStyle.Render("🔍 " + filterText) content = append(content, filterView) } itemsView := l.renderFilteredItems() content = append(content, header, itemsView) - return styles.ContainerStyle. + return style.ContainerStyle. Width(l.width). MaxWidth(l.width). MaxHeight(l.height). @@ -409,16 +409,16 @@ func renderRow(issue ListIssue, isSelected bool, cols []tableColumn) string { colWidth = 1 } - style := lipgloss.NewStyle().Width(int(colWidth)) + cellStyle := lipgloss.NewStyle().Width(int(colWidth)) if isSelected { - style = style.Background(styles.SelectedBackground).Bold(true) + cellStyle = cellStyle.Background(style.SelectedBackground).Bold(true) } if issue.Status == models.StatusClosed { - style = style.Strikethrough(true).Foreground(styles.FaintText) + cellStyle = cellStyle.Strikethrough(true).Foreground(style.FaintText) } truncated := truncate.StringWithTail(value, colWidth, "...") - parts = append(parts, style.Render(truncated)) + parts = append(parts, cellStyle.Render(truncated)) } return lipgloss.JoinHorizontal(lipgloss.Left, parts...) diff --git a/pkg/tui/components/modals.go b/pkg/tui/components/modals.go deleted file mode 100644 index b204001..0000000 --- a/pkg/tui/components/modals.go +++ /dev/null @@ -1,273 +0,0 @@ -package components - -import ( - "charm.land/lipgloss/v2" - "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/LazyBachelor/LazyPM/internal/style" - "github.com/LazyBachelor/LazyPM/pkg/tui/styles" -) - -// modalBoxWidth returns a clamped width for modal content. Never returns a value < 1. -func modalBoxWidth(maxWidth, width int) int { - if width < 5 { - return 1 - } - w := min(maxWidth, width-4) - if w < 1 { - return 1 - } - return w -} - -// components contains reusable TUI modal renderers for issue actions. - -func RenderEditTitle(width, height int, inputView string) string { - if width < 5 || height < 5 { - return "" - } - editBoxWidth := modalBoxWidth(60, width) - editContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Edit title (Enter to save, Esc to cancel):"), - inputView, - ) - editBox := styles.ContainerStyle. - Width(editBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(editContent) - return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, editBox) -} - -func RenderEditDescription(width, height int, inputView string) string { - if width < 5 || height < 5 { - return "" - } - editBoxWidth := modalBoxWidth(60, width) - editContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Edit description (Ctrl+S to save, Esc to cancel):"), - inputView, - ) - editBox := styles.ContainerStyle. - Width(editBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(editContent) - return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, editBox) -} - -func RenderCreateIssue(width, height int, inputView string) string { - if width < 5 || height < 5 { - return "" - } - createBoxWidth := modalBoxWidth(60, width) - createContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("New issue (Enter to create, Esc to cancel):"), - inputView, - ) - createBox := styles.ContainerStyle. - Width(createBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(createContent) - return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, createBox) -} - -func RenderConfirmDelete(width, height int, issueID string) string { - if width < 5 || height < 5 { - return "" - } - confirmContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Delete issue "+issueID+"?"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("Press y to delete, n or Esc to cancel"), - ) - confirmBoxWidth := modalBoxWidth(50, width) - confirmBox := styles.ContainerStyle. - Width(confirmBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(confirmContent) - return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, confirmBox) -} - -func RenderChooseStatus(width, height int, issueID string) string { - if width < 5 || height < 5 { - return "" - } - statusContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Change status for "+issueID+":"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("o = open i = in_progress b = blocked r = ready_to_sprint c = closed"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), - ) - statusBoxWidth := modalBoxWidth(50, width) - statusBox := styles.ContainerStyle. - Width(statusBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(statusContent) - return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, statusBox) -} - -func RenderChoosePriority(width, height int, issueID string) string { - if width < 5 || height < 5 { - return "" - } - priorityContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Change priority for "+issueID+":"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("0 = irrelevant 1 = low 2 = normal 3 = high 4 = critical"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), - ) - priorityBoxWidth := modalBoxWidth(60, width) - priorityBox := styles.ContainerStyle. - Width(priorityBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(priorityContent) - return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, priorityBox) -} - -func RenderEditAssignee(width, height int, inputView string) string { - if width < 5 || height < 5 { - return "" - } - editBoxWidth := modalBoxWidth(60, width) - editContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Edit assignee (Enter to save, Esc to cancel):"), - inputView, - ) - editBox := styles.ContainerStyle. - Width(editBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(editContent) - return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, editBox) -} - -func RenderChooseType(width, height int, issueID string) string { - if width < 5 || height < 5 { - return "" - } - typeContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Change type for "+issueID+":"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("b = bug f = feature t = task e = epic c = chore"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), - ) - typeBoxWidth := modalBoxWidth(65, width) - typeBox := styles.ContainerStyle. - Width(typeBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(typeContent) - return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, typeBox) -} - -// RenderModals wraps the common modal overlay logic used by different views. -// It returns either one of the modal overlays (edit title/description, create, -// confirm delete, choose status/priority/type) or the provided main view if -// no modal is active. -func RenderModals( - width, height int, - editingTitle bool, titleInputView string, - editingDescription bool, descriptionInputView string, - creatingIssue bool, createTitleInputView string, - confirmingDelete bool, deleteIssueID string, - choosingStatus bool, statusIssueID string, - choosingPriority bool, priorityIssueID string, - choosingType bool, typeIssueID string, - editingAssignee bool, assigneeInputView string, - mainView string, -) string { - if editingTitle { - return RenderEditTitle(width, height, titleInputView) - } - - if editingDescription { - return RenderEditDescription(width, height, descriptionInputView) - } - - if creatingIssue { - return RenderCreateIssue(width, height, createTitleInputView) - } - - if confirmingDelete { - return RenderConfirmDelete(width, height, deleteIssueID) - } - - if choosingStatus { - return RenderChooseStatus(width, height, statusIssueID) - } - - if choosingPriority { - return RenderChoosePriority(width, height, priorityIssueID) - } - - if choosingType { - return RenderChooseType(width, height, typeIssueID) - } - - if editingAssignee { - return RenderEditAssignee(width, height, assigneeInputView) - } - - return mainView -} - -// truncateToWidth trims the given text so that its rendered width does not -// exceed maxWidth. If truncation occurs and there is room, an ellipsis is -// appended to indicate that the text was shortened. -func truncateToWidth(text string, maxWidth int) string { - if maxWidth <= 0 { - return "" - } - - if lipgloss.Width(text) <= maxWidth { - return text - } - - const ellipsis = "…" - ellipsisWidth := lipgloss.Width(ellipsis) - if ellipsisWidth > maxWidth { - // Not enough space even for an ellipsis; return empty. - return "" - } - - runes := []rune(text) - current := "" - for _, r := range runes { - next := current + string(r) - if lipgloss.Width(next)+ellipsisWidth > maxWidth { - break - } - current = next - } - - return current + ellipsis -} - -// RenderFooter renders the shared footer with the help bar and optional -// validation feedback message. -func RenderFooter(width int, helpBar *HelpBar, feedback models.ValidationFeedback) string { - feedbackStatus := feedback.Message - - // Ensure the feedback message does not exceed the total available width. - if feedback.Message != "" { - feedbackStatus = truncateToWidth(style.ErrorStyle.Render(feedbackStatus+" [Press '?' for details]"), width) - - if helpBar.IsExpanded() && feedbackStatus != "" { - for _, check := range feedback.Checks { - var prefix string - if check.Valid { - prefix = "✅ " - } else { - prefix = "❌ " - } - - // Ensure each check line does not exceed the available width. - remainingWidth := max(width-lipgloss.Width(prefix), 0) - truncatedMsg := truncateToWidth(check.Message, remainingWidth) - - feedbackStatus += "\n" + prefix + truncatedMsg - } - } - } - - if feedbackStatus == "" { - return helpBar.View() - } - - helpWidth := max(width-lipgloss.Width(feedbackStatus), 0) - - helpBar.SetWidth(helpWidth) - return lipgloss.JoinHorizontal(lipgloss.Left, helpBar.View(), feedbackStatus) -} diff --git a/pkg/tui/modal/base.go b/pkg/tui/modal/base.go new file mode 100644 index 0000000..6efbe99 --- /dev/null +++ b/pkg/tui/modal/base.go @@ -0,0 +1,102 @@ +package modal + +import ( + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/LazyBachelor/LazyPM/internal/style" +) + +// BaseModal provides common functionality for all modals. +// Embed this struct to get default implementations of the Modal interface. +type BaseModal struct { + id string + modType ModalType + active bool + width int + height int +} + +// NewBaseModal creates a new base modal with the given properties +func NewBaseModal(id string, modType ModalType) BaseModal { + return BaseModal{ + id: id, + modType: modType, + width: 80, + height: 20, + } +} + +// ID returns the modal's unique identifier +func (b *BaseModal) ID() string { + return b.id +} + +// Type returns the modal type +func (b *BaseModal) Type() ModalType { + return b.modType +} + +// IsActive returns true if the modal is currently active +func (b *BaseModal) IsActive() bool { + return b.active +} + +// SetSize updates the modal dimensions +func (b *BaseModal) SetSize(width, height int) { + if width < 1 { + width = 1 + } + if height < 1 { + height = 1 + } + b.width = width + b.height = height +} + +// activate marks the modal as active +func (b *BaseModal) activate() tea.Cmd { + b.active = true + return nil +} + +// deactivate marks the modal as inactive +func (b *BaseModal) deactivate() { + b.active = false +} + +// Width returns the modal width +func (b *BaseModal) Width() int { + return b.width +} + +// Height returns the modal height +func (b *BaseModal) Height() int { + return b.height +} + +// ModalFrame renders content within a standard modal frame without full-screen placement +func ModalFrame(content string, width int) string { + if width < 5 { + return "" + } + + boxWidth := max(min(80, width-4), 1) + + return style.ModalContainerStyle. + Width(boxWidth). + Render(content) +} + +// ModalWithLabel renders a modal with a label and content +func ModalWithLabel(label, content string, width int) string { + if width < 5 { + return "" + } + + fullContent := lipgloss.JoinVertical(lipgloss.Left, + style.LabelStyle.Render(label), + content, + ) + + return ModalFrame(fullContent, width) +} diff --git a/pkg/tui/modal/confirm.go b/pkg/tui/modal/confirm.go new file mode 100644 index 0000000..20e9a8b --- /dev/null +++ b/pkg/tui/modal/confirm.go @@ -0,0 +1,141 @@ +package modal + +import ( + "slices" + + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/LazyBachelor/LazyPM/internal/style" +) + +// ConfirmResult is returned when a confirm modal completes +type ConfirmResult struct { + Confirmed bool +} + +// ConfirmModal is a modal for yes/no confirmations +// Suitable for: delete confirmations, discard changes, etc. +type ConfirmModal struct { + BaseModal + message string + yesKeys []string + noKeys []string + issueID string + width int + height int +} + +// ConfirmConfig configures a confirmation modal +type ConfirmConfig struct { + ID string + Message string + YesKeys []string + NoKeys []string + IssueID string + Width int + Height int +} + +// NewConfirmModal creates a new confirmation modal +func NewConfirmModal(cfg ConfirmConfig) *ConfirmModal { + if cfg.YesKeys == nil { + cfg.YesKeys = []string{"y", "Y"} + } + if cfg.NoKeys == nil { + cfg.NoKeys = []string{"n", "N", "esc"} + } + + mod := &ConfirmModal{ + BaseModal: NewBaseModal(cfg.ID, TypeConfirm), + message: cfg.Message, + yesKeys: cfg.YesKeys, + noKeys: cfg.NoKeys, + issueID: cfg.IssueID, + width: cfg.Width, + height: cfg.Height, + } + + if mod.width == 0 { + mod.width = 50 + } + if mod.height == 0 { + mod.height = 20 + } + + return mod +} + +// Activate prepares the modal +func (c *ConfirmModal) Activate() tea.Cmd { + c.BaseModal.activate() + return nil +} + +// Deactivate cleans up the modal +func (c *ConfirmModal) Deactivate() { + c.BaseModal.deactivate() +} + +// IssueID returns the associated issue ID +func (c *ConfirmModal) IssueID() string { + return c.issueID +} + +// Update handles input when the modal is active +func (c *ConfirmModal) Update(msg tea.Msg) (tea.Cmd, bool) { + if !c.IsActive() { + return nil, false + } + + switch msg := msg.(type) { + case tea.KeyPressMsg: + s := msg.String() + + // Check yes keys + if slices.Contains(c.yesKeys, s) { + c.Deactivate() + return func() tea.Msg { + return ModalCompletedMsg{ + ModalID: c.ID(), + Value: ConfirmResult{Confirmed: true}, + } + }, true + } + + // Check no/cancel keys + if slices.Contains(c.noKeys, s) { + c.Deactivate() + return func() tea.Msg { + return ModalCancelledMsg{ModalID: c.ID()} + }, true + } + } + + return nil, true +} + +// View renders the modal +func (c *ConfirmModal) View() string { + if c.width < 5 { + return "" + } + + boxWidth := max(min(50, c.width-4), 1) + + content := lipgloss.JoinVertical(lipgloss.Left, + style.LabelStyle.Render(c.message), + lipgloss.NewStyle().Foreground(style.FaintText). + Render("Press y to confirm, n or Esc to cancel"), + ) + + return style.ModalContainerStyle. + Width(boxWidth). + Render(content) +} + +// SetSize updates the modal dimensions +func (c *ConfirmModal) SetSize(width, height int) { + c.BaseModal.SetSize(width, height) + c.width = width + c.height = height +} diff --git a/pkg/tui/modal/focus.go b/pkg/tui/modal/focus.go new file mode 100644 index 0000000..bb35e1f --- /dev/null +++ b/pkg/tui/modal/focus.go @@ -0,0 +1,143 @@ +package modal + +// FocusArea represents a distinct focusable area in the UI +type FocusArea int + +const ( + FocusNone FocusArea = iota + FocusList + FocusDetail + FocusColumn1 + FocusColumn2 + FocusColumn3 + FocusColumn4 +) + +// FocusManager handles focus state across different UI areas. +// It provides a clean separation of focus concerns from modal state. +type FocusManager struct { + currentArea FocusArea + areas map[FocusArea]bool +} + +// NewFocusManager creates a new focus manager with all areas disabled by default +func NewFocusManager() *FocusManager { + return &FocusManager{ + currentArea: FocusNone, + areas: make(map[FocusArea]bool), + } +} + +// SetCurrent sets the currently focused area +func (f *FocusManager) SetCurrent(area FocusArea) { + f.currentArea = area +} + +// Current returns the currently focused area +func (f *FocusManager) Current() FocusArea { + return f.currentArea +} + +// IsFocused returns true if the given area is currently focused +func (f *FocusManager) IsFocused(area FocusArea) bool { + return f.currentArea == area +} + +// IsListFocused returns true if any list area is focused +func (f *FocusManager) IsListFocused() bool { + return f.currentArea == FocusList || + f.currentArea == FocusColumn1 || + f.currentArea == FocusColumn2 || + f.currentArea == FocusColumn3 || + f.currentArea == FocusColumn4 +} + +// IsDetailFocused returns true if the detail area is focused +func (f *FocusManager) IsDetailFocused() bool { + return f.currentArea == FocusDetail +} + +// EnableArea marks an area as available for focus +func (f *FocusManager) EnableArea(area FocusArea) { + f.areas[area] = true +} + +// DisableArea marks an area as unavailable for focus +func (f *FocusManager) DisableArea(area FocusArea) { + f.areas[area] = false + if f.currentArea == area { + f.currentArea = FocusNone + } +} + +// IsAreaEnabled returns true if the area is enabled +func (f *FocusManager) IsAreaEnabled(area FocusArea) bool { + return f.areas[area] +} + +// Next moves focus to the next enabled area +func (f *FocusManager) Next() { + areas := []FocusArea{FocusList, FocusDetail} + f.cycleFocus(areas) +} + +// Previous moves focus to the previous enabled area +func (f *FocusManager) Previous() { + areas := []FocusArea{FocusDetail, FocusList} + f.cycleFocus(areas) +} + +// NextColumn moves focus to the next column (for kanban) +func (f *FocusManager) NextColumn() { + areas := []FocusArea{FocusColumn1, FocusColumn2, FocusColumn3, FocusColumn4} + f.cycleFocus(areas) +} + +// PreviousColumn moves focus to the previous column (for kanban) +func (f *FocusManager) PreviousColumn() { + areas := []FocusArea{FocusColumn4, FocusColumn3, FocusColumn2, FocusColumn1} + f.cycleFocus(areas) +} + +// cycleFocus finds the next enabled area in the given order +func (f *FocusManager) cycleFocus(areas []FocusArea) { + // Find current position + startIdx := -1 + for i, area := range areas { + if area == f.currentArea { + startIdx = i + break + } + } + + // Search for next enabled area + for i := 1; i <= len(areas); i++ { + idx := (startIdx + i) % len(areas) + if idx < 0 { + idx += len(areas) + } + if f.areas[areas[idx]] { + f.currentArea = areas[idx] + return + } + } +} + +// ToggleDetail toggles between list and detail focus +func (f *FocusManager) ToggleDetail() { + if f.currentArea == FocusDetail { + f.currentArea = FocusList + } else { + f.currentArea = FocusDetail + } +} + +// Reset clears the current focus +func (f *FocusManager) Reset() { + f.currentArea = FocusNone +} + +// CanHandleKey returns true if the current focus area can handle keyboard input +func (f *FocusManager) CanHandleKey() bool { + return f.currentArea != FocusNone +} diff --git a/pkg/tui/modal/manager.go b/pkg/tui/modal/manager.go new file mode 100644 index 0000000..3250677 --- /dev/null +++ b/pkg/tui/modal/manager.go @@ -0,0 +1,256 @@ +package modal + +import ( + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" +) + +// Manager provides a clean API for managing modals in views. +// Views should embed this struct to get modal management capabilities. +type Manager struct { + stack *ModalStack + width int + height int +} + +// NewManager creates a new modal manager +func NewManager() *Manager { + return &Manager{ + stack: NewModalStack(), + } +} + +// SetSize updates the dimensions for modal rendering +func (m *Manager) SetSize(width, height int) { + m.width = width + m.height = height +} + +// ShowModal activates a pre-registered modal by ID +func (m *Manager) ShowModal(id string) tea.Cmd { + for _, modal := range m.stack.modals { + if modal.ID() == id { + return modal.Activate() + } + } + return nil +} + +// RegisterModal adds a modal to the manager's registry +func (m *Manager) RegisterModal(modal Modal) { + m.stack.Register(modal) +} + +// PushModal adds a modal to the stack and activates it +func (m *Manager) PushModal(modal Modal) tea.Cmd { + m.stack.Push(modal) + return modal.Activate() +} + +// PopModal removes the top modal from the stack +func (m *Manager) PopModal() Modal { + return m.stack.Pop() +} + +// CloseAll closes all active modals +func (m *Manager) CloseAll() { + m.stack.Clear() +} + +// IsModalActive returns true if any modal is currently active +func (m *Manager) IsModalActive() bool { + return m.stack.HasActiveModal() +} + +// ActiveModal returns the currently active modal +func (m *Manager) ActiveModal() Modal { + return m.stack.ActiveModal() +} + +// Update handles messages and routes them to the active modal +// Returns: (command, handled) - if handled is true, the view should stop processing this message +func (m *Manager) Update(msg tea.Msg) (tea.Cmd, bool) { + return m.stack.Update(msg) +} + +// View returns the rendered modal content (just the modal, not positioned) +func (m *Manager) View() string { + active := m.stack.ActiveModal() + if active == nil { + return "" + } + active.SetSize(m.width, m.height) + return active.View() +} + +// GetTextInputModal retrieves a TextInputModal by ID (if registered) +func (m *Manager) GetTextInputModal(id string) *TextInputModal { + for _, modal := range m.stack.modals { + if modal.ID() == id && modal.Type() == TypeTextInput { + if tim, ok := modal.(*TextInputModal); ok { + return tim + } + } + } + return nil +} + +// GetTextAreaModal retrieves a TextAreaModal by ID (if registered) +func (m *Manager) GetTextAreaModal(id string) *TextAreaModal { + for _, modal := range m.stack.modals { + if modal.ID() == id && modal.Type() == TypeTextArea { + if tam, ok := modal.(*TextAreaModal); ok { + return tam + } + } + } + return nil +} + +// GetConfirmModal retrieves a ConfirmModal by ID (if registered) +func (m *Manager) GetConfirmModal(id string) *ConfirmModal { + for _, modal := range m.stack.modals { + if modal.ID() == id && modal.Type() == TypeConfirm { + if cm, ok := modal.(*ConfirmModal); ok { + return cm + } + } + } + return nil +} + +// GetSelectModal retrieves a SelectModal by ID (if registered) +func (m *Manager) GetSelectModal(id string) *SelectModal { + for _, modal := range m.stack.modals { + if modal.ID() == id && modal.Type() == TypeSelect { + if sm, ok := modal.(*SelectModal); ok { + return sm + } + } + } + return nil +} + +// RenderWithMainView renders the main view with an overlaid modal using Canvas +// This allows the modal to appear on top without clearing the background +func (m *Manager) RenderWithMainView(mainView string) string { + if !m.IsModalActive() { + return mainView + } + + modalContent := m.View() + if modalContent == "" { + return mainView + } + + // Calculate centered position for the modal + modalWidth := lipgloss.Width(modalContent) + modalHeight := lipgloss.Height(modalContent) + + // Center the modal + x := max((m.width-modalWidth)/2, 0) + y := max((m.height-modalHeight)/2, 0) + + // Create layers: main view as base, modal on top with Z-index + mainLayer := lipgloss.NewLayer(mainView).X(0).Y(0).Z(0) + modalLayer := lipgloss.NewLayer(modalContent).X(x).Y(y).Z(1) + + // Create compositor with layers and render + compositor := lipgloss.NewCompositor(mainLayer, modalLayer) + return compositor.Render() +} + +// RegisterCommonModals registers the standard set of modals used across views. +// This helper reduces duplication between dashboard and kanban views. +func RegisterCommonModals(m *Manager) { + // Edit Title Modal + m.RegisterModal(NewTextInputModal(TextInputConfig{ + ID: ModalEditTitle, + Label: "Edit title (Enter to save, Esc to cancel):", + Placeholder: "Issue title...", + SaveKeys: []string{"enter"}, + CharLimit: 256, + InitialValue: "", + })) + + // Create Issue Modal + m.RegisterModal(NewTextInputModal(TextInputConfig{ + ID: ModalCreateIssue, + Label: "New issue (Enter to create, Esc to cancel):", + Placeholder: "New issue title...", + SaveKeys: []string{"enter"}, + CharLimit: 256, + })) + + // Edit Assignee Modal + m.RegisterModal(NewTextInputModal(TextInputConfig{ + ID: ModalEditAssignee, + Label: "Edit assignee (Enter to save, Esc to cancel):", + Placeholder: "Assignee name...", + SaveKeys: []string{"enter"}, + CharLimit: 64, + })) + + // Edit Description Modal + m.RegisterModal(NewTextAreaModal(TextAreaConfig{ + ID: ModalEditDescription, + Label: "Edit description (Ctrl+S to save, Esc to cancel):", + Placeholder: "Issue description...", + SaveKeys: []string{"ctrl+s"}, + InputHeight: 10, + })) + + // Add Comment Modal + m.RegisterModal(NewTextAreaModal(TextAreaConfig{ + ID: ModalAddComment, + Label: "Add comment (Ctrl+S to save, Esc to cancel):", + Placeholder: "Write your comment...", + SaveKeys: []string{"ctrl+s"}, + InputHeight: 8, + })) + + // Close Reason TextArea Modal + m.RegisterModal(NewTextAreaModal(TextAreaConfig{ + ID: ModalCloseReason, + Label: "Enter closing reason (Ctrl+S to save, Esc to cancel):", + Placeholder: "Enter closing reason...", + SaveKeys: []string{"ctrl+s"}, + InputHeight: 4, + })) + + // Delete Confirm Modal + m.RegisterModal(NewConfirmModal(ConfirmConfig{ + ID: ModalConfirmDelete, + Message: "Delete issue?", + YesKeys: []string{"y", "Y"}, + NoKeys: []string{"n", "N", "esc"}, + })) + + // Status Select Modal + m.RegisterModal(NewSelectModal(SelectConfig{ + ID: ModalSelectStatus, + Label: "Change status:", + Options: StatusOptions(), + })) + + // Close Reason Select Modal + m.RegisterModal(NewSelectModal(SelectConfig{ + ID: ModalSelectCloseReason, + Label: "Choose closing reason:", + Options: CloseReasonOptions(), + })) + + // Priority Select Modal + m.RegisterModal(NewSelectModal(SelectConfig{ + ID: ModalSelectPriority, + Label: "Change priority:", + Options: PriorityOptions(), + })) + + // Type Select Modal + m.RegisterModal(NewSelectModal(SelectConfig{ + ID: ModalSelectType, + Label: "Change type:", + Options: TypeOptions(), + })) +} diff --git a/pkg/tui/modal/modal.go b/pkg/tui/modal/modal.go new file mode 100644 index 0000000..0e3e3d6 --- /dev/null +++ b/pkg/tui/modal/modal.go @@ -0,0 +1,167 @@ +// Package modal provides a composable, interface-based modal system for TUI views. +// It enables separation of concerns between modal rendering, input handling, and focus management. +package modal + +import ( + tea "charm.land/bubbletea/v2" +) + +// Modal is the core interface that all modals must implement. +// It defines the contract for modal lifecycle, rendering, and input handling. +type Modal interface { + ID() string + Type() ModalType + IsActive() bool + Activate() tea.Cmd + Deactivate() + Update(msg tea.Msg) (tea.Cmd, bool) + View() string + SetSize(width, height int) +} + +// ModalType categorizes different modal behaviors +type ModalType int + +const ( + TypeTextInput ModalType = iota + TypeConfirm + TypeSelect + TypeTextArea + TypeCustom +) + +// ModalResult carries the output from a completed modal +type ModalResult struct { + ModalID string + Value interface{} // Type depends on the modal implementation + Err error +} + +// ModalCompletedMsg is sent when a modal completes successfully +type ModalCompletedMsg struct { + ModalID string + Value interface{} +} + +// ModalCancelledMsg is sent when a modal is cancelled +type ModalCancelledMsg struct { + ModalID string +} + +// Modal IDs used across the application +const ( + ModalEditTitle = "edit-title" + ModalCreateIssue = "create-issue" + ModalEditAssignee = "edit-assignee" + ModalEditDescription = "edit-description" + ModalAddComment = "add-comment" + ModalCloseReason = "close-reason-other" + ModalConfirmDelete = "confirm-delete" + ModalSelectStatus = "select-status" + ModalSelectCloseReason = "select-close-reason" + ModalSelectPriority = "select-priority" + ModalSelectType = "select-type" +) + +// ModalStack manages a stack of active modals with priority handling +type ModalStack struct { + modals []Modal +} + +// NewModalStack creates an empty modal stack +func NewModalStack() *ModalStack { + return &ModalStack{ + modals: make([]Modal, 0), + } +} + +// Push adds a modal to the top of the stack +func (s *ModalStack) Push(m Modal) { + s.modals = append(s.modals, m) +} + +// Pop removes and returns the top modal +func (s *ModalStack) Pop() Modal { + if len(s.modals) == 0 { + return nil + } + m := s.modals[len(s.modals)-1] + s.modals = s.modals[:len(s.modals)-1] + return m +} + +// Peek returns the top modal without removing it +func (s *ModalStack) Peek() Modal { + if len(s.modals) == 0 { + return nil + } + return s.modals[len(s.modals)-1] +} + +// ActiveModal returns the currently active modal (top of stack if active) +func (s *ModalStack) ActiveModal() Modal { + for i := len(s.modals) - 1; i >= 0; i-- { + if s.modals[i].IsActive() { + return s.modals[i] + } + } + return nil +} + +// HasActiveModal returns true if any modal in the stack is active +func (s *ModalStack) HasActiveModal() bool { + return s.ActiveModal() != nil +} + +// Clear deactivates and removes all modals +func (s *ModalStack) Clear() { + for _, m := range s.modals { + m.Deactivate() + } + s.modals = s.modals[:0] +} + +// Update routes messages to the active modal +// Returns handled=false for global messages like WindowSizeMsg so they pass through to the view +func (s *ModalStack) Update(msg tea.Msg) (tea.Cmd, bool) { + active := s.ActiveModal() + if active == nil { + return nil, false + } + + // Allow global quit key even when modal is active + if key, ok := msg.(tea.KeyPressMsg); ok { + if key.String() == "ctrl+c" { + return nil, false + } + } + + // WindowSizeMsg should pass through to the view even when modal is active + // This allows both the modal and the underlying view to resize + if _, ok := msg.(tea.WindowSizeMsg); ok { + cmd, _ := active.Update(msg) + return cmd, false + } + + return active.Update(msg) +} + +// View renders the active modal with proper framing +func (s *ModalStack) View(width, height int) string { + active := s.ActiveModal() + if active == nil { + return "" + } + active.SetSize(width, height) + return active.View() +} + +// Len returns the number of modals in the stack +func (s *ModalStack) Len() int { + return len(s.modals) +} + +// Register allows pre-registering modals for use +func (s *ModalStack) Register(modals ...Modal) { + s.modals = append(s.modals, modals...) +} diff --git a/pkg/tui/modal/select.go b/pkg/tui/modal/select.go new file mode 100644 index 0000000..7ad2e6a --- /dev/null +++ b/pkg/tui/modal/select.go @@ -0,0 +1,215 @@ +package modal + +import ( + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/LazyBachelor/LazyPM/internal/style" +) + +// SelectResult is returned when a select modal completes +type SelectResult struct { + SelectedKey string + SelectedValue string +} + +// SelectOption represents a selectable option +type SelectOption struct { + Key string // The key to press + Label string // Display label + Value string // The actual value to return +} + +// SelectModal is a modal for selecting from a list of options +// Suitable for: status selection, priority selection, type selection, etc. +type SelectModal struct { + BaseModal + label string + options []SelectOption + helpText string + cancelKey string + issueID string + width int + height int +} + +// SelectConfig configures a select modal +type SelectConfig struct { + ID string + Label string + Options []SelectOption + CancelKey string + IssueID string + Width int + Height int +} + +// NewSelectModal creates a new select modal +func NewSelectModal(cfg SelectConfig) *SelectModal { + if cfg.CancelKey == "" { + cfg.CancelKey = "esc" + } + + mod := &SelectModal{ + BaseModal: NewBaseModal(cfg.ID, TypeSelect), + label: cfg.Label, + options: cfg.Options, + cancelKey: cfg.CancelKey, + issueID: cfg.IssueID, + width: cfg.Width, + height: cfg.Height, + } + + // Build help text from options with styled keys in vertical layout + var parts []string + for _, opt := range cfg.Options { + styledKey := lipgloss.NewStyle().Foreground(style.Primary).Bold(true).Render(opt.Key) + styledLabel := lipgloss.NewStyle().Foreground(style.SecondaryText).Render(opt.Label) + option := lipgloss.NewStyle().Foreground(style.FaintText).Render(" ") + styledKey + lipgloss.NewStyle().Foreground(style.FaintText).Render(" → ") + styledLabel + parts = append(parts, option) + } + mod.helpText = lipgloss.JoinVertical(lipgloss.Left, parts...) + + if mod.width == 0 { + mod.width = 70 + } + if mod.height == 0 { + mod.height = 20 + } + + return mod +} + +// Activate prepares the modal +func (s *SelectModal) Activate() tea.Cmd { + s.BaseModal.activate() + return nil +} + +// Deactivate cleans up the modal +func (s *SelectModal) Deactivate() { + s.BaseModal.deactivate() +} + +// IssueID returns the associated issue ID +func (s *SelectModal) IssueID() string { + return s.issueID +} + +// Options returns the available options +func (s *SelectModal) Options() []SelectOption { + return s.options +} + +// Update handles input when the modal is active +func (s *SelectModal) Update(msg tea.Msg) (tea.Cmd, bool) { + if !s.IsActive() { + return nil, false + } + + switch msg := msg.(type) { + case tea.KeyPressMsg: + key := msg.String() + + // Check cancel key + if key == s.cancelKey { + s.Deactivate() + return func() tea.Msg { + return ModalCancelledMsg{ModalID: s.ID()} + }, true + } + + // Check option keys + for _, opt := range s.options { + if key == opt.Key { + s.Deactivate() + return func() tea.Msg { + return ModalCompletedMsg{ + ModalID: s.ID(), + Value: SelectResult{SelectedKey: opt.Key, SelectedValue: opt.Value}, + } + }, true + } + } + } + + // Consume all keys when modal is active to prevent leakage to underlying components + return nil, true +} + +// View renders the modal +func (s *SelectModal) View() string { + if s.width < 5 { + return "" + } + + boxWidth := max(min(70, s.width-4), 1) + + cancelText := lipgloss.NewStyle(). + Foreground(style.FaintText). + Render(s.cancelKey + " = cancel") + + content := lipgloss.JoinVertical(lipgloss.Left, + style.ValueStyle.Render(s.label), + "", + s.helpText, + "", + cancelText, + ) + + return style.ModalContainerStyle. + Width(boxWidth). + Render(content) +} + +// SetSize updates the modal dimensions +func (s *SelectModal) SetSize(width, height int) { + s.BaseModal.SetSize(width, height) + s.width = width + s.height = height +} + +// Predefined option sets for common use cases + +// StatusOptions returns options for status selection +func StatusOptions() []SelectOption { + return []SelectOption{ + {Key: "o", Label: "open", Value: "open"}, + {Key: "i", Label: "in_progress", Value: "in_progress"}, + {Key: "b", Label: "blocked", Value: "blocked"}, + {Key: "r", Label: "ready_to_sprint", Value: "ready_to_sprint"}, + {Key: "c", Label: "closing", Value: "closing"}, + } +} + +// PriorityOptions returns options for priority selection +func PriorityOptions() []SelectOption { + return []SelectOption{ + {Key: "0", Label: "irrelevant", Value: "0"}, + {Key: "1", Label: "low", Value: "1"}, + {Key: "2", Label: "normal", Value: "2"}, + {Key: "3", Label: "high", Value: "3"}, + {Key: "4", Label: "critical", Value: "4"}, + } +} + +// TypeOptions returns options for issue type selection +func TypeOptions() []SelectOption { + return []SelectOption{ + {Key: "b", Label: "bug", Value: "bug"}, + {Key: "f", Label: "feature", Value: "feature"}, + {Key: "t", Label: "task", Value: "task"}, + {Key: "e", Label: "epic", Value: "epic"}, + {Key: "c", Label: "chore", Value: "chore"}, + } +} + +// CloseReasonOptions returns options for close reason selection +func CloseReasonOptions() []SelectOption { + return []SelectOption{ + {Key: "c", Label: "Done", Value: "Done"}, + {Key: "d", Label: "Duplicate", Value: "Duplicate issue"}, + {Key: "w", Label: "Won't fix", Value: "Won't fix"}, + {Key: "o", Label: "Obsolete", Value: "Obsolete"}, + {Key: "h", Label: "Other", Value: "other"}, + } +} diff --git a/pkg/tui/modal/textarea.go b/pkg/tui/modal/textarea.go new file mode 100644 index 0000000..bb01d33 --- /dev/null +++ b/pkg/tui/modal/textarea.go @@ -0,0 +1,177 @@ +package modal + +import ( + "slices" + + "charm.land/bubbles/v2/textarea" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/LazyBachelor/LazyPM/internal/style" +) + +// TextAreaResult is returned when a text area modal completes +type TextAreaResult struct { + Value string +} + +// TextAreaModal is a modal for multi-line text input +// Suitable for: editing descriptions, adding comments, custom close reasons, etc. +type TextAreaModal struct { + BaseModal + input textarea.Model + label string + saveKeys []string + placeholder string + width int + height int + inputHeight int + issueID string +} + +// TextAreaConfig configures a text area modal +type TextAreaConfig struct { + ID string + Label string + Placeholder string + SaveKeys []string + InitialValue string + IssueID string + Width int + Height int + InputHeight int // Height of the textarea itself +} + +// NewTextAreaModal creates a new text area modal +func NewTextAreaModal(cfg TextAreaConfig) *TextAreaModal { + if cfg.SaveKeys == nil { + cfg.SaveKeys = []string{"ctrl+s"} + } + if cfg.InputHeight == 0 { + cfg.InputHeight = 8 + } + + ta := textarea.New() + ta.Placeholder = cfg.Placeholder + ta.SetValue(cfg.InitialValue) + + mod := &TextAreaModal{ + BaseModal: NewBaseModal(cfg.ID, TypeTextArea), + input: ta, + label: cfg.Label, + saveKeys: cfg.SaveKeys, + placeholder: cfg.Placeholder, + width: cfg.Width, + height: cfg.Height, + inputHeight: cfg.InputHeight, + issueID: cfg.IssueID, + } + + if mod.width == 0 { + mod.width = 60 + } + if mod.height == 0 { + mod.height = 20 + } + + return mod +} + +// Activate prepares the modal for input +func (t *TextAreaModal) Activate() tea.Cmd { + t.BaseModal.activate() + return t.input.Focus() +} + +// Deactivate cleans up the modal +func (t *TextAreaModal) Deactivate() { + t.BaseModal.deactivate() + t.input.Blur() +} + +// SetValue updates the textarea value +func (t *TextAreaModal) SetValue(value string) { + t.input.SetValue(value) +} + +// Value returns the current textarea value +func (t *TextAreaModal) Value() string { + return t.input.Value() +} + +// IssueID returns the associated issue ID +func (t *TextAreaModal) IssueID() string { + return t.issueID +} + +// Update handles input when the modal is active +func (t *TextAreaModal) Update(msg tea.Msg) (tea.Cmd, bool) { + if !t.IsActive() { + return nil, false + } + + switch msg := msg.(type) { + case tea.KeyPressMsg: + s := msg.String() + + // Check save keys + if slices.Contains(t.saveKeys, s) { + value := t.input.Value() + t.Deactivate() + return func() tea.Msg { + return ModalCompletedMsg{ + ModalID: t.ID(), + Value: TextAreaResult{Value: value}, + } + }, true + } + + // Cancel on escape + if s == "esc" { + t.Deactivate() + t.input.Blur() + t.input.Reset() + return func() tea.Msg { + return ModalCancelledMsg{ModalID: t.ID()} + }, true + } + } + + // Let the textarea handle the message + var cmd tea.Cmd + t.input, cmd = t.input.Update(msg) + // Always handle the message when modal is active to prevent keys leaking to list + return cmd, true +} + +// View renders the modal +func (t *TextAreaModal) View() string { + if t.width < 5 { + return "" + } + + boxWidth := max(min(60, t.width-4), 1) + + t.input.SetWidth(boxWidth - 2) + t.input.SetHeight(t.inputHeight) + + content := lipgloss.JoinVertical(lipgloss.Left, + style.LabelStyle.Render(t.label), + t.input.View(), + ) + + return style.ModalContainerStyle. + Width(boxWidth). + Render(content) +} + +// SetSize updates the modal dimensions +func (t *TextAreaModal) SetSize(width, height int) { + t.BaseModal.SetSize(width, height) + t.width = width + t.height = height +} + +// Reset clears the textarea +func (t *TextAreaModal) Reset() { + t.input.Reset() +} diff --git a/pkg/tui/modal/textinput.go b/pkg/tui/modal/textinput.go new file mode 100644 index 0000000..d5fc6d0 --- /dev/null +++ b/pkg/tui/modal/textinput.go @@ -0,0 +1,179 @@ +package modal + +import ( + "slices" + + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/LazyBachelor/LazyPM/internal/style" +) + +// TextInputResult is returned when a text input modal completes +type TextInputResult struct { + Value string +} + +// TextInputModal is a modal for single-line text input +// Suitable for: editing titles, editing assignees, creating issues +type TextInputModal struct { + BaseModal + input textinput.Model + label string + saveKeys []string + placeholder string + charLimit int + width int + height int + issueID string // Optional: for context +} + +// TextInputConfig configures a text input modal +type TextInputConfig struct { + ID string + Label string + Placeholder string + SaveKeys []string + CharLimit int + InitialValue string + IssueID string + Width int + Height int +} + +// NewTextInputModal creates a new text input modal +func NewTextInputModal(cfg TextInputConfig) *TextInputModal { + if cfg.SaveKeys == nil { + cfg.SaveKeys = []string{"enter"} + } + if cfg.CharLimit == 0 { + cfg.CharLimit = 256 + } + + ti := textinput.New() + ti.Placeholder = cfg.Placeholder + ti.CharLimit = cfg.CharLimit + ti.SetValue(cfg.InitialValue) + + mod := &TextInputModal{ + BaseModal: NewBaseModal(cfg.ID, TypeTextInput), + input: ti, + label: cfg.Label, + saveKeys: cfg.SaveKeys, + placeholder: cfg.Placeholder, + charLimit: cfg.CharLimit, + width: cfg.Width, + height: cfg.Height, + issueID: cfg.IssueID, + } + + if mod.width == 0 { + mod.width = 60 + } + if mod.height == 0 { + mod.height = 20 + } + + return mod +} + +// Activate prepares the modal for input +func (t *TextInputModal) Activate() tea.Cmd { + t.BaseModal.activate() + return t.input.Focus() +} + +// Deactivate cleans up the modal +func (t *TextInputModal) Deactivate() { + t.BaseModal.deactivate() + t.input.Blur() +} + +// SetValue updates the input value +func (t *TextInputModal) SetValue(value string) { + t.input.SetValue(value) +} + +// Value returns the current input value +func (t *TextInputModal) Value() string { + return t.input.Value() +} + +// IssueID returns the associated issue ID +func (t *TextInputModal) IssueID() string { + return t.issueID +} + +// Update handles input when the modal is active +func (t *TextInputModal) Update(msg tea.Msg) (tea.Cmd, bool) { + if !t.IsActive() { + return nil, false + } + + switch msg := msg.(type) { + case tea.KeyPressMsg: + s := msg.String() + + // Check save keys + if slices.Contains(t.saveKeys, s) { + value := t.input.Value() + t.Deactivate() + return func() tea.Msg { + return ModalCompletedMsg{ + ModalID: t.ID(), + Value: TextInputResult{Value: value}, + } + }, true + } + + // Cancel on escape + if s == "esc" { + t.Deactivate() + return func() tea.Msg { + return ModalCancelledMsg{ModalID: t.ID()} + }, true + } + } + + // Let the text input handle the message + var cmd tea.Cmd + t.input, cmd = t.input.Update(msg) + // Always handle the message when modal is active to prevent keys leaking to list + return cmd, true +} + +// View renders the modal +func (t *TextInputModal) View() string { + if t.width < 5 { + return "" + } + + boxWidth := min(60, t.width-4) + t.input.SetWidth(boxWidth - 2) + + content := lipgloss.JoinVertical(lipgloss.Left, + style.LabelStyle.Render(t.label), + t.input.View(), + ) + + return style.ModalContainerStyle. + Width(boxWidth). + Render(content) +} + +// SetSize updates the modal dimensions +func (t *TextInputModal) SetSize(width, height int) { + t.BaseModal.SetSize(width, height) + t.width = width + t.height = height +} + +// CursorEnd moves the cursor to the end of the input +func (t *TextInputModal) CursorEnd() { + t.input.CursorEnd() +} + +// Reset clears the input value +func (t *TextInputModal) Reset() { + t.input.Reset() +} diff --git a/pkg/tui/msgs/msgs.go b/pkg/tui/msgs/msgs.go index 40e1169..a241d98 100644 --- a/pkg/tui/msgs/msgs.go +++ b/pkg/tui/msgs/msgs.go @@ -10,35 +10,47 @@ import ( // Msg types used by both dashboard and kanban TUI views. type ( + SwitchToDashboardMsg struct{} + + SwitchToKanbanBoardMsg struct{} + + SelectIssueMsg struct{ IssueID string } + TitleUpdatedMsg struct { IssueID string Err error } + DescriptionUpdatedMsg struct { IssueID string Err error } + StatusUpdatedMsg struct { IssueID string Err error } + PriorityUpdatedMsg struct { IssueID string Err error } + TypeUpdatedMsg struct { IssueID string Err error } + AssigneeUpdatedMsg struct { IssueID string Err error } - SelectIssueMsg struct{ IssueID string } - CreatedMsg struct { + + CreatedMsg struct { Issue *models.Issue Err error } + DeletedMsg struct { IssueID string Err error @@ -50,11 +62,14 @@ type ( Err error } - // SwitchToDashboardMsg signals to switch to the main dashboard view. - SwitchToDashboardMsg struct{} + ModalCompletedMsg struct { + ModalID string + Result interface{} + } - // SwitchToKanbanBoardMsg signals to switch to the kanban board view. - SwitchToKanbanBoardMsg struct{} + ModalCancelledMsg struct { + ModalID string + } ) // UpdateIssueTitleCmd returns a command that updates an issue's title. diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go deleted file mode 100644 index 4f2af0f..0000000 --- a/pkg/tui/styles/styles.go +++ /dev/null @@ -1,85 +0,0 @@ -package styles - -import ( - "charm.land/lipgloss/v2" - "charm.land/lipgloss/v2/compat" -) - -var ( - Primary = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")} - Secondary = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")} - - Success = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")} - Warning = compat.AdaptiveColor{Light: lipgloss.Color("#F59E0B"), Dark: lipgloss.Color("#F59E0B")} - Error = compat.AdaptiveColor{Light: lipgloss.Color("#FE5F86"), Dark: lipgloss.Color("#FE5F86")} - - PrimaryText = compat.AdaptiveColor{Light: lipgloss.Color("#1A1A1A"), Dark: lipgloss.Color("#E0E0E0")} - SecondaryText = compat.AdaptiveColor{Light: lipgloss.Color("#666666"), Dark: lipgloss.Color("#999999")} - FaintText = compat.AdaptiveColor{Light: lipgloss.Color("#999999"), Dark: lipgloss.Color("#666666")} - - PrimaryBorder = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")} - SecondaryBorder = compat.AdaptiveColor{Light: lipgloss.Color("#CCCCCC"), Dark: lipgloss.Color("#444444")} - - SelectedBackground = compat.AdaptiveColor{Light: lipgloss.Color("#E8E8E8"), Dark: lipgloss.Color("#333333")} -) - -const ( - ListViewRatio = 52 // Percentage of total width allocated to the list view - LabelWidth = 14 - MarginBottomSmall = 1 -) - -var DefaultBorder = lipgloss.ThickBorder() - -var ( - HeaderStyle = lipgloss.NewStyle().Foreground(Primary).Padding(0, 1).Bold(true) - HeaderTitleStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true).Padding(0) -) - -var ContainerStyle = lipgloss.NewStyle(). - Border(DefaultBorder, true, false, false, false). - BorderForeground(SecondaryBorder). - Padding(1) - -var DetailsContainerStyle = lipgloss.NewStyle(). - Border(DefaultBorder, true, false, false, true). - BorderForeground(SecondaryBorder). - Padding(1) - -var ( - RowStyle = lipgloss.NewStyle().MarginBottom(MarginBottomSmall) - TitleStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true) - LabelStyle = lipgloss.NewStyle().Foreground(SecondaryText) - ValueStyle = lipgloss.NewStyle().Foreground(PrimaryText) - IssueTypeStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true) -) - -var ( - FilterStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true).Padding(0, 1) - FilterInputStyle = lipgloss.NewStyle().Foreground(PrimaryText).Padding(0, 1) - FilterPromptStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true) -) - -func StatusStyle(status string) lipgloss.Style { - style := lipgloss.NewStyle().Bold(true) - switch status { - case "open": - return style.Foreground(Secondary) - case "closed": - return style.Foreground(FaintText) - case "in_progress": - return style.Foreground(Warning) - case "blocked": - return style.Foreground(Error) - default: - return style.Foreground(SecondaryText) - } -} - -func HighlightKey(key string) string { - return lipgloss.NewStyle(). - Foreground(Primary). - Bold(true). - Padding(0, 1). - Render(key) -} diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 367a3ea..8cce7cc 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -7,7 +7,7 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" ) -type DashboardKeyMap struct { +type KeyMap struct { components.CommonKeyMap SwitchToKanbanBoard key.Binding Quit key.Binding @@ -26,7 +26,7 @@ type DashboardKeyMap struct { DeleteIssue key.Binding } -var defaultDashboardKeyMap = DashboardKeyMap{ +var defaultDashboardKeyMap = KeyMap{ CommonKeyMap: components.DefaultCommonKeyMap(), SwitchToKanbanBoard: key.NewBinding( key.WithKeys("v"), @@ -69,73 +69,85 @@ var defaultDashboardKeyMap = DashboardKeyMap{ ), } -func (d *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { +func (m *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { var cmd tea.Cmd switch { - case key.Matches(msg, d.keyMap.Help): - d.helpBar.ToggleHelp() - d.logAction("tui toggled help") - case key.Matches(msg, d.keyMap.Quit): - d.logAction("tui quit requested") + case m.notInModalMsgWithKey(msg, m.keyMap.Help): + m.helpBar.ToggleHelp() + m.logAction("tui toggled help") + + case m.notInModalMsgWithKey(msg, m.keyMap.Quit): + m.logAction("tui quit requested") return tea.Quit - case !d.IsInModal() && key.Matches(msg, d.keyMap.SwitchToKanbanBoard): + + case m.notInModalMsgWithKey(msg, m.keyMap.SwitchToKanbanBoard): return func() tea.Msg { return msgs.SwitchToKanbanBoardMsg{} } - case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollUp): - d.issueDetail.ScrollUp(1) - d.logAction("tui scrolled issue detail up") - case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown): - d.issueDetail.ScrollDown(1) - d.logAction("tui scrolled issue detail down") - case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.EditTitle): - if selected := d.issueList.SelectedItem(); selected.ID != "" { - d.startEditTitle(selected) - cmd = d.titleInput.Focus() - d.logAction("tui started editing issue title") + + case m.notInModalMsgWithKey(msg, m.keyMap.ScrollUp): + m.issueDetail.ScrollUp(1) + m.logAction("tui scrolled issue detail up") + + case m.notInModalMsgWithKey(msg, m.keyMap.ScrollDown): + m.issueDetail.ScrollDown(1) + m.logAction("tui scrolled issue detail down") + + case m.notInModalMsgWithKey(msg, m.keyMap.EditTitle): + if selected := m.issueList.SelectedItem(); selected.ID != "" { + cmd = m.startEditTitle(selected) + m.logAction("tui started editing issue title") } - case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.EditDescription): - if selected := d.issueList.SelectedItem(); selected.ID != "" { - d.startEditDescription(selected) - cmd = d.descriptionInput.Focus() - d.logAction("tui started editing issue description") + + case m.notInModalMsgWithKey(msg, m.keyMap.EditDescription): + if selected := m.issueList.SelectedItem(); selected.ID != "" { + cmd = m.startEditDescription(selected) + m.logAction("tui started editing issue description") } - case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangeStatus): - if selected := d.issueList.SelectedItem(); selected.ID != "" { - d.startChooseStatus(selected) - d.logAction("tui opened status picker") + + case m.notInModalMsgWithKey(msg, m.keyMap.ChangeStatus): + if selected := m.issueList.SelectedItem(); selected.ID != "" { + cmd = m.startChooseStatus(selected) + m.logAction("tui opened status picker") } - case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangePriority): - if selected := d.issueList.SelectedItem(); selected.ID != "" { - d.startChoosePriority(selected) - d.logAction("tui opened priority picker") + + case m.notInModalMsgWithKey(msg, m.keyMap.ChangePriority): + if selected := m.issueList.SelectedItem(); selected.ID != "" { + cmd = m.startChoosePriority(selected) + m.logAction("tui opened priority picker") } - case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangeType): - if selected := d.issueList.SelectedItem(); selected.ID != "" { - d.startChooseType(selected) - d.logAction("tui opened type picker") + + case m.notInModalMsgWithKey(msg, m.keyMap.ChangeType): + if selected := m.issueList.SelectedItem(); selected.ID != "" { + cmd = m.startChooseType(selected) + m.logAction("tui opened type picker") } - case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.ChangeAssignee): - if selected := d.issueList.SelectedItem(); selected.ID != "" { - d.startEditAssignee(selected) - cmd = d.assigneeInput.Focus() - d.logAction("tui started editing assignee") + + case m.notInModalMsgWithKey(msg, m.keyMap.ChangeAssignee): + if selected := m.issueList.SelectedItem(); selected.ID != "" { + cmd = m.startEditAssignee(selected) + m.logAction("tui started editing assignee") } - case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.AddComment): - if selected := d.issueList.SelectedItem(); selected.ID != "" { - d.startAddComment(selected) - cmd = d.commentInput.Focus() + + case m.notInModalMsgWithKey(msg, m.keyMap.AddComment): + if selected := m.issueList.SelectedItem(); selected.ID != "" { + cmd = m.startAddComment(selected) } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && !d.addingComment && key.Matches(msg, d.keyMap.AddIssue): - d.startCreateIssue() - cmd = d.createTitleInput.Focus() - d.logAction("tui started creating issue") - case !d.IsInModal() && !d.addingComment && key.Matches(msg, d.keyMap.DeleteIssue): - fl := d.issueList + + case m.notInModalMsgWithKey(msg, m.keyMap.AddIssue): + cmd = m.startCreateIssue() + m.logAction("tui started creating issue") + + case m.notInModalMsgWithKey(msg, m.keyMap.DeleteIssue): + fl := m.issueList if selected := fl.SelectedItem(); selected.ID != "" { - d.startConfirmDelete(selected.ID, fl.Index()) - d.logAction("tui opened delete confirmation") + cmd = m.startConfirmDelete(selected.ID, fl.Index()) + m.logAction("tui opened delete confirmation") } } return cmd } + +func (m *Model) notInModalMsgWithKey(msg tea.KeyPressMsg, keyBinding key.Binding) bool { + return !m.IsInModal() && key.Matches(msg, keyBinding) +} diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index a1bce80..4ac5f07 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -3,12 +3,11 @@ package dashboard import ( "context" - "charm.land/bubbles/v2/textarea" - "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" + "github.com/LazyBachelor/LazyPM/pkg/tui/modal" ) // Use shared types from components for consistency. @@ -20,102 +19,80 @@ type ( ) type Model struct { - header Header - issueList IssueList - issueDetail IssueDetail - closedIssueList IssueList - helpBar components.HelpBar - keyMap DashboardKeyMap - app *app.App - width int - height int - focusedWindow int // 0 = main (display issues), 1 = closed issues - focusedPaneMain int // 0 = list, 1 = detail - focusedPaneClosed int - editingTitle bool // true while we are editing a title - titleInput textinput.Model - editingIssueID string + header Header + issueList IssueList + issueDetail IssueDetail + closedIssueList IssueList + helpBar components.HelpBar + keyMap KeyMap + app *app.App + width int + height int - editingDescription bool // true while editing a description - descriptionInput textarea.Model - editingDescIssueID string - creatingIssue bool // true while creating a new issue - createTitleInput textinput.Model + // Modal and Focus management + modalManager *modal.Manager + focusManager *modal.FocusManager - confirmingDelete bool // true while confirming a delete - deleteConfirmID string - deleteConfirmIndex int + // Current issue being operated on + currentIssueID string + deleteIndex int - choosingStatus bool - statusIssueID string - choosingPriority bool - priorityIssueID string - choosingType bool - typeIssueID string - editingAssignee bool - assigneeInput textinput.Model - assigneeIssueID string - addingComment bool - commentInput textarea.Model - commentIssueID string - choosingCloseReason bool - closeReasonIssueID string - closingOtherReason bool - closeReasonInput textarea.Model - feedbackChan chan models.ValidationFeedback - quitChan chan bool - currentFeedback models.ValidationFeedback - showComplete bool - submitChan chan<- struct{} + feedbackChan chan models.ValidationFeedback + quitChan chan bool + currentFeedback models.ValidationFeedback + showComplete bool + submitChan chan<- struct{} } func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *Model { m := &Model{ - header: components.NewHeader("Project Manager Dashboard"), - keyMap: defaultDashboardKeyMap, - app: app, - width: 80, - height: 24, - focusedWindow: 0, - focusedPaneMain: 0, - focusedPaneClosed: 0, - feedbackChan: feedbackChan, - quitChan: quitChan, - submitChan: submitChan, + header: components.NewHeader("Project Manager Dashboard"), + keyMap: defaultDashboardKeyMap, + app: app, + width: 80, + height: 24, + feedbackChan: feedbackChan, + quitChan: quitChan, + submitChan: submitChan, + modalManager: modal.NewManager(), + focusManager: modal.NewFocusManager(), + deleteIndex: -1, } + // Setup lists allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) m.issueList = components.NewIssueListFromIssues(app, components.SortedIssues(allIssues), 0, 0) m.issueDetail = components.NewIssueDetail() m.helpBar = components.NewHelpBar(components.ViewIssues) - inputs := components.NewIssueInputs() - m.titleInput = inputs.Title - m.createTitleInput = inputs.CreateTitle - m.descriptionInput = inputs.Description - m.assigneeInput = inputs.Assignee + // Setup focus + m.focusManager.EnableArea(modal.FocusList) + m.focusManager.EnableArea(modal.FocusDetail) + m.focusManager.SetCurrent(modal.FocusList) - closeReasonTa := textarea.New() - closeReasonTa.Placeholder = "Enter closing reason..." - closeReasonTa.SetWidth(56) - closeReasonTa.SetHeight(4) - m.closeReasonInput = closeReasonTa - - commentTa := textarea.New() - commentTa.Placeholder = "Write your comment..." - commentTa.SetWidth(56) - commentTa.SetHeight(6) - m.commentInput = commentTa + // Register modals + m.registerModals() if selected := m.issueList.SelectedItem(); selected.ID != "" { m.setDetailIssueWithComments(selected.Issue) - } else if selected := m.closedIssueList.SelectedItem(); selected.ID != "" { - m.setDetailIssueWithComments(selected.Issue) } return m } +func (m *Model) Init() tea.Cmd { + if m.submitChan != nil { + m.submitChan <- struct{}{} + m.logAction("tui submitted validation") + } + return components.ListenForValidation(m.feedbackChan) +} + +// registerModals sets up all modals using the common registration helper +func (m *Model) registerModals() { + modal.RegisterCommonModals(m.modalManager) +} + // setDetailIssueWithComments sets the issue in the detail pane and loads its comments. func (m *Model) setDetailIssueWithComments(issue models.Issue) { m.issueDetail.SetIssue(issue) @@ -127,13 +104,6 @@ func (m *Model) setDetailIssueWithComments(issue models.Issue) { m.issueDetail.SetComments(comments) } -func (m *Model) startAddComment(selected ListIssue) { - m.addingComment = true - m.commentIssueID = selected.ID - m.commentInput.SetValue("") - m.commentInput.Reset() -} - func (m *Model) logAction(action string) { if m.app != nil { m.app.LogAction(models.EncodeActionEvent(models.ActionEvent{ @@ -144,7 +114,6 @@ func (m *Model) logAction(action string) { } // submitValidation sends a validation request to the submit channel. -// Call this after every successful user action that modifies issues. func (m *Model) submitValidation() { if m.submitChan != nil { select { @@ -155,80 +124,15 @@ func (m *Model) submitValidation() { } } -func (m *Model) startEditTitle(selected ListIssue) { - m.editingTitle = true - m.editingIssueID = selected.ID - m.titleInput.SetValue(selected.Issue.Title) - m.titleInput.CursorEnd() -} - -func (m *Model) startEditDescription(selected ListIssue) { - m.editingDescription = true - m.editingDescIssueID = selected.ID - m.descriptionInput.SetValue(selected.Issue.Description) - m.descriptionInput.CursorEnd() -} - -func (m *Model) startCreateIssue() { - m.creatingIssue = true - m.createTitleInput.SetValue("") - m.createTitleInput.Reset() -} - -func (m *Model) startConfirmDelete(issueID string, index int) { - m.confirmingDelete = true - m.deleteConfirmID = issueID - m.deleteConfirmIndex = index -} - -func (m *Model) startChooseStatus(selected ListIssue) { - m.choosingStatus = true - m.statusIssueID = selected.ID -} - -func (m *Model) startChoosePriority(selected ListIssue) { - m.choosingPriority = true - m.priorityIssueID = selected.ID -} - -func (m *Model) startChooseType(selected ListIssue) { - m.choosingType = true - m.typeIssueID = selected.ID -} - -func (m *Model) startEditAssignee(selected ListIssue) { - m.editingAssignee = true - m.assigneeIssueID = selected.ID - m.assigneeInput.SetValue(selected.Assignee) - m.assigneeInput.CursorEnd() -} - -func (m *Model) Init() tea.Cmd { - if m.submitChan != nil { - m.submitChan <- struct{}{} - m.logAction("tui submitted validation") - } - return components.ListenForValidation(m.feedbackChan) -} - -// IsInModal returns true when a modal (edit, create, delete confirm, choose status/priority/type) is active. +// IsInModal returns true when a modal is active func (m *Model) IsInModal() bool { - return m.editingTitle || m.creatingIssue || m.editingDescription || - m.choosingStatus || m.choosingPriority || m.confirmingDelete || - m.choosingType || m.editingAssignee || - m.choosingCloseReason || m.closingOtherReason + return m.modalManager.IsModalActive() } func (m *Model) IsFocusedOnList() bool { - if m.focusedWindow == 0 { - return m.focusedPaneMain == 0 - } - return m.focusedPaneClosed == 0 + return m.focusManager.IsListFocused() } func (m *Model) IsFocusedOnDetail() bool { - if m.focusedWindow == 0 { - return m.focusedPaneMain == 1 - } - return m.focusedPaneClosed == 1 + return m.focusManager.IsDetailFocused() } diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 22eaa31..b27d26d 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -2,33 +2,18 @@ package dashboard import ( "context" - "os" - "os/user" + "strconv" "charm.land/bubbles/v2/list" tea "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/utils/user" "github.com/LazyBachelor/LazyPM/pkg/tui/components" + "github.com/LazyBachelor/LazyPM/pkg/tui/modal" "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" ) -func defaultCommentAuthor() string { - if u, err := user.Current(); err == nil && u.Username != "" { - return u.Username - } - if s := os.Getenv("USER"); s != "" { - return s - } - if s := os.Getenv("USERNAME"); s != "" { - return s - } - return "user" -} - func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { - /* update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view - Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. - */ allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { return nil @@ -40,24 +25,228 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { break } } - return tea.Sequence(setItemsCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: issueID} }) } -// refreshAndSubmit refreshes the issue lists and submits validation. -// This is a wrapper that should be used after any successful user action. func (m *Model) refreshAndSubmit(issueID string) tea.Cmd { refreshCmd := m.refreshIssueListsAndSelectIssue(issueID) m.submitValidation() return refreshCmd } +func (m *Model) startEditTitle(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + titleModal := m.modalManager.GetTextInputModal(modal.ModalEditTitle) + if titleModal != nil { + titleModal.SetValue(selected.Issue.Title) + titleModal.CursorEnd() + return m.modalManager.ShowModal(modal.ModalEditTitle) + } + return nil +} + +func (m *Model) startEditDescription(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + descModal := m.modalManager.GetTextAreaModal(modal.ModalEditDescription) + if descModal != nil { + descModal.SetValue(selected.Issue.Description) + return m.modalManager.ShowModal(modal.ModalEditDescription) + } + return nil +} + +func (m *Model) startCreateIssue() tea.Cmd { + createModal := m.modalManager.GetTextInputModal(modal.ModalCreateIssue) + if createModal != nil { + createModal.Reset() + return m.modalManager.ShowModal(modal.ModalCreateIssue) + } + return nil +} + +func (m *Model) startConfirmDelete(issueID string, index int) tea.Cmd { + m.currentIssueID = issueID + m.deleteIndex = index + deleteModal := m.modalManager.GetConfirmModal(modal.ModalConfirmDelete) + if deleteModal != nil { + return m.modalManager.ShowModal(modal.ModalConfirmDelete) + } + return nil +} + +func (m *Model) startChooseStatus(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + return m.modalManager.ShowModal(modal.ModalSelectStatus) +} + +func (m *Model) startChoosePriority(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + return m.modalManager.ShowModal(modal.ModalSelectPriority) +} + +func (m *Model) startChooseType(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + return m.modalManager.ShowModal(modal.ModalSelectType) +} + +func (m *Model) startEditAssignee(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + assigneeModal := m.modalManager.GetTextInputModal(modal.ModalEditAssignee) + if assigneeModal != nil { + assigneeModal.SetValue(selected.Assignee) + assigneeModal.CursorEnd() + return m.modalManager.ShowModal(modal.ModalEditAssignee) + } + return nil +} + +func (m *Model) startAddComment(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + commentModal := m.modalManager.GetTextAreaModal(modal.ModalAddComment) + if commentModal != nil { + commentModal.Reset() + return m.modalManager.ShowModal(modal.ModalAddComment) + } + return nil +} + +// handleModalCompleted handles all modal completion messages +func (m *Model) handleModalCompleted(msg modal.ModalCompletedMsg) tea.Cmd { + switch msg.ModalID { + case modal.ModalEditTitle: + if r, ok := msg.Value.(modal.TextInputResult); ok { + m.logAction("tui submitted issue title edit") + cmd := msgs.UpdateIssueTitleCmd(m.app, m.currentIssueID, r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalCreateIssue: + if r, ok := msg.Value.(modal.TextInputResult); ok && r.Value != "" { + m.logAction("tui submitted new issue") + cmd := msgs.CreateIssueCmd(m.app, r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalEditAssignee: + if r, ok := msg.Value.(modal.TextInputResult); ok { + m.logAction("tui submitted assignee edit") + cmd := msgs.UpdateIssueAssigneeCmd(m.app, m.currentIssueID, r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalEditDescription: + if r, ok := msg.Value.(modal.TextAreaResult); ok { + m.logAction("tui submitted issue description edit") + cmd := msgs.UpdateIssueDescriptionCmd(m.app, m.currentIssueID, r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalAddComment: + if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" { + cmd := msgs.AddIssueCommentCmd(m.app, m.currentIssueID, user.GetOsUsername(), r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalCloseReason: + if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" { + m.logAction("tui submitted custom close reason") + cmd := msgs.CloseIssueCmd(m.app, m.currentIssueID, r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalConfirmDelete: + if r, ok := msg.Value.(modal.ConfirmResult); ok && r.Confirmed { + m.logAction("tui confirmed issue deletion") + idx := m.deleteIndex + issueID := m.currentIssueID + m.deleteIndex = -1 + m.currentIssueID = "" + cmd := msgs.DeleteIssueCmd(m.app, issueID, idx) + return func() tea.Msg { return cmd() } + } + case modal.ModalSelectStatus: + if r, ok := msg.Value.(modal.SelectResult); ok { + m.logAction("tui selected issue status") + if r.SelectedValue == "closing" { + return m.modalManager.ShowModal(modal.ModalSelectCloseReason) + } + cmd := msgs.UpdateIssueStatusCmd(m.app, m.currentIssueID, r.SelectedValue) + return func() tea.Msg { return cmd() } + } + case modal.ModalSelectCloseReason: + if r, ok := msg.Value.(modal.SelectResult); ok { + if r.SelectedValue == "other" { + return m.modalManager.ShowModal(modal.ModalCloseReason) + } + cmd := msgs.CloseIssueCmd(m.app, m.currentIssueID, r.SelectedValue) + return func() tea.Msg { return cmd() } + } + case modal.ModalSelectPriority: + if r, ok := msg.Value.(modal.SelectResult); ok { + m.logAction("tui selected issue priority") + priority, _ := strconv.Atoi(r.SelectedValue) + cmd := msgs.UpdateIssuePriorityCmd(m.app, m.currentIssueID, priority) + return func() tea.Msg { return cmd() } + } + case modal.ModalSelectType: + if r, ok := msg.Value.(modal.SelectResult); ok { + m.logAction("tui selected issue type") + issueType := models.IssueType(r.SelectedValue) + cmd := msgs.UpdateIssueTypeCmd(m.app, m.currentIssueID, issueType) + return func() tea.Msg { return cmd() } + } + } + return nil +} + +// handleModalCancelled handles all modal cancellation messages +func (m *Model) handleModalCancelled(msg modal.ModalCancelledMsg) { + switch msg.ModalID { + case modal.ModalEditTitle: + m.currentIssueID = "" + m.logAction("tui canceled issue title edit") + case modal.ModalCreateIssue: + m.logAction("tui canceled issue creation") + case modal.ModalEditAssignee: + m.currentIssueID = "" + m.logAction("tui canceled assignee edit") + case modal.ModalEditDescription: + m.currentIssueID = "" + m.logAction("tui canceled issue description edit") + case modal.ModalAddComment: + m.currentIssueID = "" + case modal.ModalCloseReason: + m.currentIssueID = "" + m.logAction("tui canceled close reason") + case modal.ModalConfirmDelete: + m.deleteIndex = -1 + m.currentIssueID = "" + m.logAction("tui canceled issue deletion") + case modal.ModalSelectStatus: + m.currentIssueID = "" + m.logAction("tui canceled status picker") + case modal.ModalSelectCloseReason: + m.currentIssueID = "" + m.logAction("tui canceled close reason") + case modal.ModalSelectPriority: + m.currentIssueID = "" + m.logAction("tui canceled priority picker") + case modal.ModalSelectType: + m.currentIssueID = "" + m.logAction("tui canceled type picker") + } +} + func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if cmd, handled := m.modalManager.Update(msg); handled { + return m, cmd + } + switch msg := msg.(type) { + case modal.ModalCompletedMsg: + return m, m.handleModalCompleted(msg) + + case modal.ModalCancelledMsg: + m.handleModalCancelled(msg) + return m, nil + case msgs.TitleUpdatedMsg: - m.editingTitle = false - m.editingIssueID = "" - m.titleInput.Blur() + m.modalManager.GetTextInputModal(modal.ModalEditTitle).Reset() + m.currentIssueID = "" if msg.Err != nil { m.logAction("tui failed to update issue title") return m, nil @@ -66,9 +255,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.refreshAndSubmit(msg.IssueID) case msgs.DescriptionUpdatedMsg: - m.editingDescription = false - m.editingDescIssueID = "" - m.descriptionInput.Blur() + m.modalManager.GetTextAreaModal(modal.ModalEditDescription).Reset() + m.currentIssueID = "" if msg.Err != nil { m.logAction("tui failed to update issue description") return m, nil @@ -77,8 +265,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.refreshAndSubmit(msg.IssueID) case msgs.StatusUpdatedMsg: - m.choosingStatus = false - m.statusIssueID = "" + m.currentIssueID = "" if msg.Err != nil { m.logAction("tui failed to update issue status") return m, nil @@ -87,8 +274,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.refreshAndSubmit(msg.IssueID) case msgs.PriorityUpdatedMsg: - m.choosingPriority = false - m.priorityIssueID = "" + m.currentIssueID = "" if msg.Err != nil { m.logAction("tui failed to update issue priority") return m, nil @@ -97,8 +283,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.refreshAndSubmit(msg.IssueID) case msgs.TypeUpdatedMsg: - m.choosingType = false - m.typeIssueID = "" + m.currentIssueID = "" if msg.Err != nil { m.logAction("tui failed to update issue type") return m, nil @@ -107,9 +292,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.refreshAndSubmit(msg.IssueID) case msgs.AssigneeUpdatedMsg: - m.editingAssignee = false - m.assigneeIssueID = "" - m.assigneeInput.Blur() + m.modalManager.GetTextInputModal(modal.ModalEditAssignee).Reset() + m.currentIssueID = "" if msg.Err != nil { m.logAction("tui failed to update issue assignee") return m, nil @@ -123,9 +307,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case msgs.CreatedMsg: - m.creatingIssue = false - m.createTitleInput.Blur() - m.createTitleInput.Reset() + m.modalManager.GetTextInputModal(modal.ModalCreateIssue).Reset() if msg.Err != nil || msg.Issue == nil { m.logAction("tui failed to create issue") return m, nil @@ -136,11 +318,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } setItemsCmd := m.issueList.SetIssues(components.SortedIssues(allIssues)) - // Determine the created issue from the refreshed list to ensure all fields (like ID) are populated. selectedIssue := msg.Issue if selectedIssue.ID == "" { for _, issue := range allIssues { - // Prefer an issue that matches the created issue's title when ID is not yet known. if issue.Title == msg.Issue.Title { selectedIssue = issue break @@ -152,19 +332,19 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.logAction("tui created issue") m.submitValidation() return m, tea.Sequence(setItemsCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} }) + case msgs.IssueCommentAddedMsg: - m.addingComment = false - m.commentIssueID = "" - m.commentInput.Blur() - m.commentInput.Reset() + m.modalManager.GetTextAreaModal(modal.ModalAddComment).Reset() + m.currentIssueID = "" if msg.Err != nil { return m, nil } m.submitValidation() return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + case msgs.DeletedMsg: - m.confirmingDelete = false - m.deleteConfirmID = "" + m.deleteIndex = -1 + m.currentIssueID = "" if msg.Err != nil { m.logAction("tui failed to delete issue") return m, nil @@ -174,297 +354,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case tea.KeyPressMsg: - if m.confirmingDelete { - switch msg.String() { - case "y", "Y": - m.logAction("tui confirmed issue deletion") - issueID := m.deleteConfirmID - idx := m.deleteConfirmIndex - m.confirmingDelete = false - m.deleteConfirmID = "" - return m, msgs.DeleteIssueCmd(m.app, issueID, idx) - case "n", "N", "esc": - m.logAction("tui canceled issue deletion") - m.confirmingDelete = false - m.deleteConfirmID = "" - return m, nil - } - } - - if m.choosingStatus { - switch msg.String() { - case "o": - m.logAction("tui selected issue status open") - issueID := m.statusIssueID - m.choosingStatus = false - m.statusIssueID = "" - return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusOpen)) - case "i": - m.logAction("tui selected issue status in_progress") - issueID := m.statusIssueID - m.choosingStatus = false - m.statusIssueID = "" - return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) - case "b": - m.logAction("tui selected issue status blocked") - issueID := m.statusIssueID - m.choosingStatus = false - m.statusIssueID = "" - return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusBlocked)) - case "r": - m.logAction("tui selected issue status ready_to_sprint") - issueID := m.statusIssueID - m.choosingStatus = false - m.statusIssueID = "" - return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) - case "c": - m.logAction("tui selected issue status closing") - issueID := m.statusIssueID - m.choosingStatus = false - m.statusIssueID = "" - m.choosingCloseReason = true - m.closeReasonIssueID = issueID - return m, nil - case "esc": - m.logAction("tui canceled status picker") - m.choosingStatus = false - m.statusIssueID = "" - return m, nil - } - } - - if m.choosingCloseReason { - var reason string - switch msg.String() { - case "d": - m.logAction("tui selected close reason done") - reason = "Done" - case "u": - m.logAction("tui selected close reason duplicate issue") - reason = "Duplicate issue" - case "w": - m.logAction("tui selected close reason won't fix") - reason = "Won't fix" - case "o": - m.logAction("tui selected close reason obsolete") - reason = "Obsolete" - case "h": - m.logAction("tui selected close reason other") - m.choosingCloseReason = false - m.closingOtherReason = true - m.closeReasonInput.SetValue("") - m.closeReasonInput.Focus() - return m, nil - case "esc": - m.logAction("tui canceled close reason picker") - m.choosingCloseReason = false - m.closeReasonIssueID = "" - return m, nil - } - - if reason != "" { - issueID := m.closeReasonIssueID - m.choosingCloseReason = false - m.closeReasonIssueID = "" - return m, msgs.CloseIssueCmd(m.app, issueID, reason) - } - } - - if m.closingOtherReason { - switch msg.String() { - case "enter", "ctrl+s": - reason := m.closeReasonInput.Value() - if reason != "" { - m.logAction("tui submitted custom close reason") - issueID := m.closeReasonIssueID - m.closingOtherReason = false - m.closeReasonIssueID = "" - m.closeReasonInput.Blur() - return m, msgs.CloseIssueCmd(m.app, issueID, reason) - } - case "esc": - m.logAction("tui canceled custom close reason") - m.closingOtherReason = false - m.closeReasonIssueID = "" - m.closeReasonInput.Blur() - return m, nil - } - var cmd tea.Cmd - m.closeReasonInput, cmd = m.closeReasonInput.Update(msg) - return m, cmd - } - - if m.choosingPriority { - switch msg.String() { - case "0", "1", "2", "3", "4": - m.logAction("tui selected issue priority") - issueID := m.priorityIssueID - priority := int(msg.String()[0] - '0') - m.choosingPriority = false - m.priorityIssueID = "" - return m, msgs.UpdateIssuePriorityCmd(m.app, issueID, priority) - case "esc": - m.logAction("tui canceled priority picker") - m.choosingPriority = false - m.priorityIssueID = "" - return m, nil - default: - return m, nil - } - } - - if m.choosingType { - switch msg.String() { - case "b": - m.logAction("tui selected issue type bug") - issueID := m.typeIssueID - m.choosingType = false - m.typeIssueID = "" - return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeBug) - case "f": - m.logAction("tui selected issue type feature") - issueID := m.typeIssueID - m.choosingType = false - m.typeIssueID = "" - return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeFeature) - case "t": - m.logAction("tui selected issue type task") - issueID := m.typeIssueID - m.choosingType = false - m.typeIssueID = "" - return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeTask) - case "e": - m.logAction("tui selected issue type epic") - issueID := m.typeIssueID - m.choosingType = false - m.typeIssueID = "" - return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeEpic) - case "c": - m.logAction("tui selected issue type chore") - issueID := m.typeIssueID - m.choosingType = false - m.typeIssueID = "" - return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeChore) - case "esc": - m.logAction("tui canceled type picker") - m.choosingType = false - m.typeIssueID = "" - return m, nil - default: - return m, nil - } - } - - if m.creatingIssue { - if msg.String() == "enter" { - title := m.createTitleInput.Value() - if title != "" { - m.logAction("tui submitted new issue") - return m, msgs.CreateIssueCmd(m.app, title) - } - } - if msg.String() == "esc" { - m.logAction("tui canceled issue creation") - m.creatingIssue = false - m.createTitleInput.Blur() - m.createTitleInput.Reset() - return m, nil - } - var cmd tea.Cmd - m.createTitleInput, cmd = m.createTitleInput.Update(msg) - return m, cmd - } - - if m.editingAssignee { - if msg.String() == "enter" { - assignee := m.assigneeInput.Value() - m.logAction("tui submitted assignee edit") - return m, msgs.UpdateIssueAssigneeCmd(m.app, m.assigneeIssueID, assignee) - } - if msg.String() == "esc" { - m.logAction("tui canceled assignee edit") - m.editingAssignee = false - m.assigneeIssueID = "" - m.assigneeInput.Blur() - return m, nil - } - var cmd tea.Cmd - m.assigneeInput, cmd = m.assigneeInput.Update(msg) - return m, cmd - } - - if m.editingTitle { - if msg.String() == "enter" { - newTitle := m.titleInput.Value() - if newTitle != "" { - m.logAction("tui submitted issue title edit") - return m, msgs.UpdateIssueTitleCmd(m.app, m.editingIssueID, newTitle) - } - } - if msg.String() == "esc" { - m.logAction("tui canceled issue title edit") - m.editingTitle = false - m.editingIssueID = "" - m.titleInput.Blur() - return m, nil - } - var cmd tea.Cmd - m.titleInput, cmd = m.titleInput.Update(msg) - return m, cmd - } - - if m.addingComment { - if msg.String() == "ctrl+s" || msg.String() == "enter" { - text := m.commentInput.Value() - if text != "" { - issueID := m.commentIssueID - m.addingComment = false - m.commentIssueID = "" - m.commentInput.Blur() - m.commentInput.Reset() - return m, msgs.AddIssueCommentCmd(m.app, issueID, defaultCommentAuthor(), text) - } - } - if msg.String() == "esc" { - m.addingComment = false - m.commentIssueID = "" - m.commentInput.Blur() - m.commentInput.Reset() - return m, nil - } - var cmd tea.Cmd - m.commentInput, cmd = m.commentInput.Update(msg) - return m, cmd - } - - if m.editingDescription { - if msg.String() == "ctrl+s" { - m.logAction("tui submitted issue description edit") - issueID := m.editingDescIssueID - newDesc := m.descriptionInput.Value() - m.editingDescription = false - m.editingDescIssueID = "" - m.descriptionInput.Blur() - return m, msgs.UpdateIssueDescriptionCmd(m.app, issueID, newDesc) - } - if msg.String() == "esc" { - m.logAction("tui canceled issue description edit") - m.editingDescription = false - m.editingDescIssueID = "" - m.descriptionInput.Blur() - return m, nil - } - var cmd tea.Cmd - m.descriptionInput, cmd = m.descriptionInput.Update(msg) - return m, cmd - } - if m.issueList.FilterState() == list.Filtering { cmd, _ := m.issueList.Update(msg) return m, cmd } - // On main dashboard, ESC does nothing; only q quits; like in lazybeads. if msg.String() == "esc" { return m, nil } @@ -473,6 +367,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if cmd != nil { return m, cmd } + case components.ValidationFeedbackMsg: m.currentFeedback = msg.Feedback if msg.Feedback.Success { @@ -484,10 +379,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height + m.modalManager.SetSize(msg.Width, msg.Height) return m, nil } - if m.focusedWindow == 0 { + if m.focusManager.IsFocused(modal.FocusList) { cmd, changed := m.issueList.Update(msg) if changed { if selected := m.issueList.SelectedItem(); selected.ID != "" { diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index 539b91e..8c4e15e 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -4,16 +4,17 @@ import ( tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" - "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/LazyBachelor/LazyPM/pkg/tui/modal" + "github.com/LazyBachelor/LazyPM/internal/style" ) func (m *Model) View() tea.View { if m.width == 0 || m.height == 0 { - // if there is no space just print a loading message return tea.NewView("Loading...") } m.helpBar.SetWidth(m.width) + m.modalManager.SetSize(m.width, m.height) header := m.header.View(m.width) headerHeight := m.header.Height() @@ -21,202 +22,27 @@ func (m *Model) View() tea.View { footer := components.RenderFooter(m.width, &m.helpBar, m.currentFeedback) footerHeight := lipgloss.Height(footer) - // To avoid layer overflow or clipping, the label heights are calculated and subtracted from the available height before calculating the list heights to avoid layout overflow or clipping. contentHeight := m.height - headerHeight - footerHeight - //mainLabel := styles.LabelStyle.Render("Display issues") - //closedLabel := styles.LabelStyle.Render("Closed issues") - //labelHeight := lipgloss.Height(mainLabel) + lipgloss.Height(closedLabel) availableForLists := contentHeight - halfHeight := max(availableForLists / 2, 1) + halfHeight := max(availableForLists/2, 1) totalContentWidth := m.width - 1 - listWidth := totalContentWidth * styles.ListViewRatio / 100 + listWidth := totalContentWidth * style.ListViewRatio / 100 detailWidth := totalContentWidth - listWidth m.issueList.SetSize(listWidth, halfHeight) - //m.closedIssueList.SetSize(listWidth, halfHeight) m.issueDetail.SetSize(detailWidth, contentHeight) - // Only highlight the focused list; unfocused list should not show selection highlight. - m.issueList.SetHighlightSelected(m.focusedWindow == 0 && m.focusedPaneMain == 0) - m.closedIssueList.SetHighlightSelected(m.focusedWindow == 1 && m.focusedPaneClosed == 0) + m.issueList.SetHighlightSelected(m.focusManager.IsFocused(modal.FocusList)) listView := m.issueList.View() - //closedListView := m.closedIssueList.View() detailView := m.issueDetail.View() - //if m.focusedWindow == 0 { - // mainLabel = lipgloss.NewStyle().Foreground(styles.Primary).Bold(true).Render("Display issues ▶") - //} else { - // closedLabel = lipgloss.NewStyle().Foreground(styles.Primary).Bold(true).Render("Closed issues ▶") - //} - - leftColumn := lipgloss.JoinVertical(lipgloss.Left, - //mainLabel, - listView, - //closedLabel, closedListView, - ) + leftColumn := lipgloss.JoinVertical(lipgloss.Left, listView) content := lipgloss.JoinHorizontal(lipgloss.Left, leftColumn, detailView) mainView := lipgloss.JoinVertical(lipgloss.Left, header, content, footer) - if m.editingAssignee { - editBoxWidth := min(60, m.width-4) - m.assigneeInput.SetWidth(editBoxWidth - 2) - editContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Edit assignee (Enter to save, Esc to cancel):"), - m.assigneeInput.View(), - ) - editBox := styles.ContainerStyle. - Width(editBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(editContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) - } - - if m.editingTitle { - editBoxWidth := min(60, m.width-4) - m.titleInput.SetWidth(editBoxWidth - 2) - editContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Edit title (Enter to save, Esc to cancel):"), - m.titleInput.View(), - ) - editBox := styles.ContainerStyle. - Width(editBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(editContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) - } - - if m.addingComment { - editBoxWidth := min(60, m.width-4) - m.commentInput.SetWidth(editBoxWidth - 2) - m.commentInput.SetHeight(8) - editContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Add comment for "+m.commentIssueID+" (Ctrl+S or Enter to save, Esc to cancel):"), - m.commentInput.View(), - ) - editBox := styles.ContainerStyle. - Width(editBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(editContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) - } - - if m.editingDescription { - editBoxWidth := min(60, m.width-4) - m.descriptionInput.SetWidth(editBoxWidth - 2) - m.descriptionInput.SetHeight(10) - editContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Edit description (Ctrl+S to save, Esc to cancel):"), - m.descriptionInput.View(), - ) - editBox := styles.ContainerStyle. - Width(editBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(editContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) - } - - if m.creatingIssue { - createBoxWidth := min(60, m.width-4) - m.createTitleInput.SetWidth(createBoxWidth - 2) - createContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("New issue (Enter to create, Esc to cancel):"), - m.createTitleInput.View(), - ) - createBox := styles.ContainerStyle. - Width(createBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(createContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, createBox)) - } - - if m.confirmingDelete { - confirmContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Delete issue "+m.deleteConfirmID+"?"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("Press y to delete, n or Esc to cancel"), - ) - confirmBoxWidth := min(50, m.width-4) - confirmBox := styles.ContainerStyle. - Width(confirmBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(confirmContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, confirmBox)) - } - - if m.choosingStatus { - statusContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Change status for "+m.statusIssueID+":"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("o = open i = in_progress r = ready_to_sprint c = closing (choose reason)"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), - ) - statusBoxWidth := min(50, m.width-4) - statusBox := styles.ContainerStyle. - Width(statusBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(statusContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, statusBox)) - } - - if m.choosingPriority { - priorityContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Change priority for "+m.priorityIssueID+":"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("0 = irrelevant 1 = low 2 = normal 3 = high 4 = critical"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), - ) - priorityBoxWidth := min(60, m.width-4) - priorityBox := styles.ContainerStyle. - Width(priorityBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(priorityContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, priorityBox)) - } - - if m.choosingCloseReason { - reasonContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Choose closing reason for "+m.closeReasonIssueID+":"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("d = Done u = Duplicate issue w = Won't fix o = Obsolete h = Other"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), - ) - reasonBoxWidth := min(70, m.width-4) - reasonBox := styles.ContainerStyle. - Width(reasonBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(reasonContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, reasonBox)) - } - - if m.closingOtherReason { - editBoxWidth := min(60, m.width-4) - m.closeReasonInput.SetWidth(editBoxWidth - 2) - m.closeReasonInput.SetHeight(4) - editContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Enter closing reason for "+m.closeReasonIssueID+" (Enter or Ctrl+S to save, Esc to cancel):"), - m.closeReasonInput.View(), - ) - editBox := styles.ContainerStyle. - Width(editBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(editContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) - } - - if m.choosingType { - typeContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Change type for "+m.typeIssueID+":"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("b = bug f = feature t = task e = epic c = chore"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), - ) - typeBoxWidth := min(65, m.width-4) - typeBox := styles.ContainerStyle. - Width(typeBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(typeContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, typeBox)) - } - - return tea.NewView(mainView) - + return tea.NewView(m.modalManager.RenderWithMainView(mainView)) } diff --git a/pkg/tui/views/kanban/keys.go b/pkg/tui/views/kanban/keys.go index f146489..85f8be8 100644 --- a/pkg/tui/views/kanban/keys.go +++ b/pkg/tui/views/kanban/keys.go @@ -7,7 +7,7 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" ) -type KanbanKeyMap struct { +type KeyMap struct { components.CommonKeyMap SwitchToDashboard key.Binding MoveColumnLeft key.Binding @@ -15,17 +15,14 @@ type KanbanKeyMap struct { MoveIssueLeft key.Binding MoveIssueRight key.Binding SubmitValidation key.Binding + AddComment key.Binding } -var defaultKanbanKeyMap = KanbanKeyMap{ +var defaultKanbanKeyMap = KeyMap{ CommonKeyMap: components.DefaultCommonKeyMap(), - SubmitValidation: key.NewBinding( - key.WithKeys("S"), - key.WithHelp("S", "submit validation"), - ), SwitchToDashboard: key.NewBinding( key.WithKeys("v"), - key.WithHelp("v", "dashboard 1"), + key.WithHelp("v", "dashboard"), ), MoveColumnLeft: key.NewBinding( key.WithKeys("h"), @@ -43,81 +40,87 @@ var defaultKanbanKeyMap = KanbanKeyMap{ key.WithKeys("right", "]"), key.WithHelp("→/]", "move issue right"), ), + AddComment: key.NewBinding( + key.WithKeys("c"), + key.WithHelp("c", "add comment"), + ), } -func (d *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { +func (m *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { var cmd tea.Cmd switch { - case key.Matches(msg, d.keyMap.SubmitValidation): - if d.submitChan != nil { - select { - case d.submitChan <- struct{}{}: - default: - } - } - case key.Matches(msg, d.keyMap.Help): - d.helpBar.ToggleHelp() - case key.Matches(msg, d.keyMap.Quit): + case m.notInModalMsgWithKey(msg, m.keyMap.Help): + m.helpBar.ToggleHelp() + + case m.notInModalMsgWithKey(msg, m.keyMap.Quit): return tea.Quit - case !d.IsInModal() && msg.String() == " ": - return func() tea.Msg { return nil } // consume space - case !d.IsInModal() && key.Matches(msg, d.keyMap.SwitchToDashboard): + + case m.notInModalMsgWithKey(msg, m.keyMap.SwitchToDashboard): return func() tea.Msg { return msgs.SwitchToDashboardMsg{} } - case !d.IsInModal() && key.Matches(msg, d.keyMap.MoveColumnLeft): - if d.focusedColumn > 0 { - d.focusedColumn-- - d.updateDetailFromSelection() + + case m.notInModalMsgWithKey(msg, m.keyMap.MoveColumnLeft): + m.focusManager.PreviousColumn() + m.updateDetailFromSelection() + + case m.notInModalMsgWithKey(msg, m.keyMap.MoveColumnRight): + m.focusManager.NextColumn() + m.updateDetailFromSelection() + + case m.notInModalMsgWithKey(msg, m.keyMap.MoveIssueRight): + cmd = m.moveIssue(+1) + + case m.notInModalMsgWithKey(msg, m.keyMap.MoveIssueLeft): + cmd = m.moveIssue(-1) + + case m.IsFocusedOnDetail() && key.Matches(msg, m.keyMap.ScrollUp): + m.issueDetail.ScrollUp(1) + + case m.notInModalMsgWithKey(msg, m.keyMap.ScrollDown): + m.issueDetail.ScrollDown(1) + + case m.notInModalMsgWithKey(msg, m.keyMap.EditTitle): + if selected := m.FocusedIssueList().SelectedItem(); selected.ID != "" { + cmd = m.startEditTitle(selected) } - case !d.IsInModal() && key.Matches(msg, d.keyMap.MoveColumnRight): - if d.focusedColumn < 3 { - d.focusedColumn++ - d.updateDetailFromSelection() + case m.notInModalMsgWithKey(msg, m.keyMap.EditDescription): + if selected := m.FocusedIssueList().SelectedItem(); selected.ID != "" { + cmd = m.startEditDescription(selected) } - case !d.IsInModal() && key.Matches(msg, d.keyMap.MoveIssueRight): - cmd = d.moveIssue(+1) - case !d.IsInModal() && key.Matches(msg, d.keyMap.MoveIssueLeft): - cmd = d.moveIssue(-1) - case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollUp): - d.issueDetail.ScrollUp(1) - case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown): - d.issueDetail.ScrollDown(1) - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.editingAssignee && key.Matches(msg, d.keyMap.EditTitle): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { - d.startEditTitle(selected) - cmd = d.titleInput.Focus() + case m.notInModalMsgWithKey(msg, m.keyMap.ChangeStatus): + if selected := m.FocusedIssueList().SelectedItem(); selected.ID != "" { + cmd = m.startChooseStatus(selected) } - case !d.IsInModal() && key.Matches(msg, d.keyMap.EditDescription): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { - d.startEditDescription(selected) - cmd = d.descriptionInput.Focus() + case m.notInModalMsgWithKey(msg, m.keyMap.ChangePriority): + if selected := m.FocusedIssueList().SelectedItem(); selected.ID != "" { + cmd = m.startChoosePriority(selected) } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangeStatus): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { - d.startChooseStatus(selected) + case m.notInModalMsgWithKey(msg, m.keyMap.ChangeType): + if selected := m.FocusedIssueList().SelectedItem(); selected.ID != "" { + cmd = m.startChooseType(selected) } - case !d.IsInModal() && key.Matches(msg, d.keyMap.ChangePriority): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { - d.startChoosePriority(selected) + case m.notInModalMsgWithKey(msg, m.keyMap.ChangeAssignee): + if selected := m.FocusedIssueList().SelectedItem(); selected.ID != "" { + cmd = m.startEditAssignee(selected) } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangeType): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { - d.startChooseType(selected) + case m.notInModalMsgWithKey(msg, m.keyMap.AddIssue): + cmd = m.startCreateIssue() + + case m.notInModalMsgWithKey(msg, m.keyMap.AddComment): + if selected := m.FocusedIssueList().SelectedItem(); selected.ID != "" { + cmd = m.startAddComment(selected) } - case !d.IsInModal() && key.Matches(msg, d.keyMap.ChangeAssignee): - if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { - d.startEditAssignee(selected) - cmd = d.assigneeInput.Focus() - } - case !d.IsInModal() && key.Matches(msg, d.keyMap.AddIssue): - d.startCreateIssue() - cmd = d.createTitleInput.Focus() - case !d.IsInModal() && key.Matches(msg, d.keyMap.DeleteIssue): - fl := d.FocusedIssueList() + + case m.notInModalMsgWithKey(msg, m.keyMap.DeleteIssue): + fl := m.FocusedIssueList() if selected := fl.SelectedItem(); selected.ID != "" { - d.startConfirmDelete(selected.ID, fl.Index()) + cmd = m.startConfirmDelete(selected.ID, fl.Index()) } } return cmd } + +func (m *Model) notInModalMsgWithKey(msg tea.KeyPressMsg, keyBinding key.Binding) bool { + return !m.IsInModal() && key.Matches(msg, keyBinding) +} diff --git a/pkg/tui/views/kanban/model.go b/pkg/tui/views/kanban/model.go index d1afc1d..5a84bda 100644 --- a/pkg/tui/views/kanban/model.go +++ b/pkg/tui/views/kanban/model.go @@ -3,12 +3,11 @@ package kanban import ( "context" - "charm.land/bubbles/v2/textarea" - "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" + "github.com/LazyBachelor/LazyPM/pkg/tui/modal" "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" ) @@ -27,61 +26,42 @@ type Model struct { doneList IssueList issueDetail IssueDetail helpBar components.HelpBar - keyMap KanbanKeyMap + keyMap KeyMap app *app.App width int height int - focusedColumn int // 0 = To Do, 1 = In Progress, 2 = Blocked, 3 = Done - focusOnDetail bool // true when detail pane is focused + // Modal and Focus management + modalManager *modal.Manager + focusManager *modal.FocusManager - editingTitle bool // true while we are editing a title - titleInput textinput.Model - editingIssueID string + // Current issue being operated on + currentIssueID string + deleteIndex int - editingDescription bool // true while editing a description - descriptionInput textarea.Model - editingDescIssueID string - creatingIssue bool // true while creating a new issue - createTitleInput textinput.Model - - confirmingDelete bool // true while confirming a delete - deleteConfirmID string - deleteConfirmIndex int - - choosingStatus bool // true while choosing a status - statusIssueID string - choosingPriority bool // true while choosing a priority - priorityIssueID string - choosingType bool // true while choosing a type - typeIssueID string - editingAssignee bool // true while editing assignee - assigneeInput textinput.Model - assigneeIssueID string - choosingCloseReason bool // true while choosing a close reason - closeReasonIssueID string - closingOtherReason bool // true while entering a custom close reason - closeReasonInput textarea.Model - feedbackChan chan models.ValidationFeedback - quitChan chan bool - submitChan chan<- struct{} - currentFeedback models.ValidationFeedback - showComplete bool + feedbackChan chan models.ValidationFeedback + quitChan chan bool + currentFeedback models.ValidationFeedback + showComplete bool + submitChan chan<- struct{} } func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *Model { m := &Model{ - header: components.NewHeader("Kanban Board"), - keyMap: defaultKanbanKeyMap, - app: app, - width: 80, - height: 24, - focusedColumn: 0, - focusOnDetail: false, - feedbackChan: feedbackChan, - quitChan: quitChan, - submitChan: submitChan, + header: components.NewHeader("Kanban Board"), + keyMap: defaultKanbanKeyMap, + app: app, + width: 80, + height: 24, + feedbackChan: feedbackChan, + quitChan: quitChan, + submitChan: submitChan, + modalManager: modal.NewManager(), + focusManager: modal.NewFocusManager(), + deleteIndex: -1, } + + // Setup lists m.issueDetail = components.NewIssueDetail() m.helpBar = components.NewHelpBar(components.ViewKanban) @@ -96,77 +76,33 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui m.blockedList = components.NewIssueListFromIssues(app, blockedIssues, 20, 10) m.doneList = components.NewIssueListFromIssues(app, doneIssues, 20, 10) - inputs := components.NewIssueInputs() - m.titleInput = inputs.Title - m.createTitleInput = inputs.CreateTitle - m.descriptionInput = inputs.Description - m.assigneeInput = inputs.Assignee + // Setup focus areas for kanban columns + m.focusManager.EnableArea(modal.FocusColumn1) + m.focusManager.EnableArea(modal.FocusColumn2) + m.focusManager.EnableArea(modal.FocusColumn3) + m.focusManager.EnableArea(modal.FocusColumn4) + m.focusManager.SetCurrent(modal.FocusColumn1) - closeReasonTa := textarea.New() - closeReasonTa.Placeholder = "Enter closing reason..." - closeReasonTa.SetWidth(56) - closeReasonTa.SetHeight(4) - m.closeReasonInput = closeReasonTa + // Register modals + m.registerModals() if selected := m.todoList.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) - } else if selected := m.inProgList.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) - } else if selected := m.blockedList.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) - } else if selected := m.doneList.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) + m.setDetailIssueWithComments(selected.Issue) } return m } -func (m *Model) startEditTitle(selected ListIssue) { - m.editingTitle = true - m.editingIssueID = selected.ID - m.titleInput.SetValue(selected.Issue.Title) - m.titleInput.CursorEnd() +func (m *Model) Init() tea.Cmd { + if m.submitChan != nil { + m.submitChan <- struct{}{} + m.logAction("tui submitted validation") + } + return components.ListenForValidation(m.feedbackChan) } -func (m *Model) startEditDescription(selected ListIssue) { - m.editingDescription = true - m.editingDescIssueID = selected.ID - m.descriptionInput.SetValue(selected.Issue.Description) - m.descriptionInput.CursorEnd() -} - -func (m *Model) startCreateIssue() { - m.creatingIssue = true - m.createTitleInput.SetValue("") - m.createTitleInput.Reset() -} - -func (m *Model) startConfirmDelete(issueID string, index int) { - m.confirmingDelete = true - m.deleteConfirmID = issueID - m.deleteConfirmIndex = index -} - -func (m *Model) startChooseStatus(selected ListIssue) { - m.choosingStatus = true - m.statusIssueID = selected.ID -} - -func (m *Model) startChoosePriority(selected ListIssue) { - m.choosingPriority = true - m.priorityIssueID = selected.ID -} - -func (m *Model) startChooseType(selected ListIssue) { - m.choosingType = true - m.typeIssueID = selected.ID -} - -func (m *Model) startEditAssignee(selected ListIssue) { - m.editingAssignee = true - m.assigneeIssueID = selected.ID - m.assigneeInput.SetValue(selected.Assignee) - m.assigneeInput.CursorEnd() +func (m *Model) registerModals() { + modal.RegisterCommonModals(m.modalManager) } func (m *Model) logAction(action string) { @@ -178,7 +114,6 @@ func (m *Model) logAction(action string) { } } -// submitValidation sends a validation request to the submit channel. func (m *Model) submitValidation() { if m.submitChan != nil { select { @@ -189,90 +124,76 @@ func (m *Model) submitValidation() { } } -func (m *Model) Init() tea.Cmd { - if m.submitChan != nil { - m.submitChan <- struct{}{} - m.logAction("tui submitted validation") - } - return components.ListenForValidation(m.feedbackChan) -} - -// IsInModal returns true when a modal (edit, create, delete confirm, choose status/priority/type) is active. func (m *Model) IsInModal() bool { - return m.editingTitle || m.creatingIssue || m.editingDescription || - m.choosingStatus || m.choosingPriority || m.confirmingDelete || - m.choosingType || m.editingAssignee || - m.choosingCloseReason || m.closingOtherReason + return m.modalManager.IsModalActive() } func (m *Model) IsFocusedOnList() bool { - return !m.focusOnDetail + return m.focusManager.IsListFocused() } func (m *Model) IsFocusedOnDetail() bool { - return m.focusOnDetail -} - -func (m *Model) FocusList() { - m.focusOnDetail = false - m.issueDetail.SetFocused(false) -} - -func (m *Model) FocusDetail() { - m.focusOnDetail = true - m.issueDetail.SetFocused(true) + return m.focusManager.IsDetailFocused() } func (m *Model) ToggleFocus() { - if m.IsFocusedOnList() { - m.FocusDetail() + if m.focusManager.IsDetailFocused() { + m.focusManager.SetCurrent(modal.FocusColumn1) + m.issueDetail.SetFocused(false) } else { - m.FocusList() + m.focusManager.SetCurrent(modal.FocusDetail) + m.issueDetail.SetFocused(true) } } func (m *Model) FocusedIssueList() *IssueList { - switch m.focusedColumn { - case 0: + switch m.focusManager.Current() { + case modal.FocusColumn1: return &m.todoList - case 1: + case modal.FocusColumn2: return &m.inProgList - case 2: + case modal.FocusColumn3: return &m.blockedList - case 3: + case modal.FocusColumn4: return &m.doneList default: return &m.todoList } } -// updateDetailFromSelection updates the detail pane based on the currently -// focused column's selected issue. func (m *Model) updateDetailFromSelection() { selected := m.FocusedIssueList().SelectedItem() if selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) + m.setDetailIssueWithComments(selected.Issue) } } -// statusForColumn maps a board column index to a Status. -func statusForColumn(col int) models.Status { +// setDetailIssueWithComments sets the issue in the detail pane and loads its comments. +func (m *Model) setDetailIssueWithComments(issue models.Issue) { + m.issueDetail.SetIssue(issue) + if issue.ID == "" { + m.issueDetail.SetComments(nil) + return + } + comments, _ := m.app.Issues.GetIssueComments(context.Background(), issue.ID) + m.issueDetail.SetComments(comments) +} + +func statusForColumn(col modal.FocusArea) models.Status { switch col { - case 0: + case modal.FocusColumn1: return models.StatusOpen - case 1: + case modal.FocusColumn2: return models.StatusInProgress - case 2: + case modal.FocusColumn3: return models.StatusBlocked - case 3: + case modal.FocusColumn4: return models.StatusClosed default: return models.StatusOpen } } -// moveIssue moves the currently selected issue in the focused column horizontally -// to an adjacent column by updating its status. func (m *Model) moveIssue(delta int) tea.Cmd { fl := m.FocusedIssueList() selected := fl.SelectedItem() @@ -280,8 +201,32 @@ func (m *Model) moveIssue(delta int) tea.Cmd { return nil } - newCol := m.focusedColumn + delta - if newCol < 0 || newCol > 3 { + currentCol := m.focusManager.Current() + var newCol modal.FocusArea + switch currentCol { + case modal.FocusColumn1: + if delta > 0 { + newCol = modal.FocusColumn2 + } + case modal.FocusColumn2: + if delta > 0 { + newCol = modal.FocusColumn3 + } else { + newCol = modal.FocusColumn1 + } + case modal.FocusColumn3: + if delta > 0 { + newCol = modal.FocusColumn4 + } else { + newCol = modal.FocusColumn2 + } + case modal.FocusColumn4: + if delta < 0 { + newCol = modal.FocusColumn3 + } + } + + if newCol == modal.FocusNone { return nil } diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index 2a9f790..7884f7f 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -2,16 +2,17 @@ package kanban import ( "context" + "strconv" "charm.land/bubbles/v2/list" - "charm.land/bubbletea/v2" + tea "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/utils/user" "github.com/LazyBachelor/LazyPM/pkg/tui/components" + "github.com/LazyBachelor/LazyPM/pkg/tui/modal" "github.com/LazyBachelor/LazyPM/pkg/tui/msgs" ) -// update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view -// Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { @@ -31,7 +32,7 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { var targetStatus models.Status for _, issue := range allIssues { if issue.ID == issueID { - m.issueDetail.SetIssue(*issue) + m.setDetailIssueWithComments(*issue) targetStatus = issue.Status break } @@ -39,85 +40,276 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { switch targetStatus { case models.StatusOpen: - m.focusedColumn = 0 + m.focusManager.SetCurrent(modal.FocusColumn1) case models.StatusInProgress: - m.focusedColumn = 1 + m.focusManager.SetCurrent(modal.FocusColumn2) case models.StatusBlocked: - m.focusedColumn = 2 + m.focusManager.SetCurrent(modal.FocusColumn3) case models.StatusClosed: - m.focusedColumn = 3 + m.focusManager.SetCurrent(modal.FocusColumn4) } - // Select the moved issue in its new column immediately so the highlight follows it. - m.todoList.SelectIssueID(issueID) - m.inProgList.SelectIssueID(issueID) - m.blockedList.SelectIssueID(issueID) - m.doneList.SelectIssueID(issueID) - - return tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd) + return tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { + return msgs.SelectIssueMsg{IssueID: issueID} + }) } -// refreshAndSubmit refreshes the issue lists and submits validation. -// This is a wrapper that should be used after any successful user action. func (m *Model) refreshAndSubmit(issueID string) tea.Cmd { refreshCmd := m.refreshIssueListsAndSelectIssue(issueID) m.submitValidation() return refreshCmd } +// Modal action handlers + +func (m *Model) startEditTitle(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + titleModal := m.modalManager.GetTextInputModal(modal.ModalEditTitle) + if titleModal != nil { + titleModal.SetValue(selected.Issue.Title) + titleModal.CursorEnd() + return m.modalManager.ShowModal(modal.ModalEditTitle) + } + return nil +} + +func (m *Model) startEditDescription(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + descModal := m.modalManager.GetTextAreaModal(modal.ModalEditDescription) + if descModal != nil { + descModal.SetValue(selected.Issue.Description) + return m.modalManager.ShowModal(modal.ModalEditDescription) + } + return nil +} + +func (m *Model) startCreateIssue() tea.Cmd { + createModal := m.modalManager.GetTextInputModal(modal.ModalCreateIssue) + if createModal != nil { + createModal.Reset() + return m.modalManager.ShowModal(modal.ModalCreateIssue) + } + return nil +} + +func (m *Model) startConfirmDelete(issueID string, index int) tea.Cmd { + m.currentIssueID = issueID + m.deleteIndex = index + deleteModal := m.modalManager.GetConfirmModal(modal.ModalConfirmDelete) + if deleteModal != nil { + return m.modalManager.ShowModal(modal.ModalConfirmDelete) + } + return nil +} + +func (m *Model) startChooseStatus(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + return m.modalManager.ShowModal(modal.ModalSelectStatus) +} + +func (m *Model) startChoosePriority(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + return m.modalManager.ShowModal(modal.ModalSelectPriority) +} + +func (m *Model) startChooseType(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + return m.modalManager.ShowModal(modal.ModalSelectType) +} + +func (m *Model) startEditAssignee(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + assigneeModal := m.modalManager.GetTextInputModal(modal.ModalEditAssignee) + if assigneeModal != nil { + assigneeModal.SetValue(selected.Assignee) + assigneeModal.CursorEnd() + return m.modalManager.ShowModal(modal.ModalEditAssignee) + } + return nil +} + +func (m *Model) startAddComment(selected ListIssue) tea.Cmd { + m.currentIssueID = selected.ID + commentModal := m.modalManager.GetTextAreaModal(modal.ModalAddComment) + if commentModal != nil { + commentModal.Reset() + return m.modalManager.ShowModal(modal.ModalAddComment) + } + return nil +} + +// handleModalCompleted handles all modal completion messages +func (m *Model) handleModalCompleted(msg modal.ModalCompletedMsg) tea.Cmd { + switch msg.ModalID { + case modal.ModalEditTitle: + if r, ok := msg.Value.(modal.TextInputResult); ok { + cmd := msgs.UpdateIssueTitleCmd(m.app, m.currentIssueID, r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalCreateIssue: + if r, ok := msg.Value.(modal.TextInputResult); ok && r.Value != "" { + cmd := msgs.CreateIssueCmd(m.app, r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalEditAssignee: + if r, ok := msg.Value.(modal.TextInputResult); ok { + cmd := msgs.UpdateIssueAssigneeCmd(m.app, m.currentIssueID, r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalEditDescription: + if r, ok := msg.Value.(modal.TextAreaResult); ok { + cmd := msgs.UpdateIssueDescriptionCmd(m.app, m.currentIssueID, r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalConfirmDelete: + if r, ok := msg.Value.(modal.ConfirmResult); ok && r.Confirmed { + idx := m.deleteIndex + issueID := m.currentIssueID + m.deleteIndex = -1 + m.currentIssueID = "" + cmd := msgs.DeleteIssueCmd(m.app, issueID, idx) + return func() tea.Msg { return cmd() } + } + case modal.ModalSelectStatus: + if r, ok := msg.Value.(modal.SelectResult); ok { + if r.SelectedValue == "closing" { + return m.modalManager.ShowModal(modal.ModalSelectCloseReason) + } + cmd := msgs.UpdateIssueStatusCmd(m.app, m.currentIssueID, r.SelectedValue) + return func() tea.Msg { return cmd() } + } + case modal.ModalSelectCloseReason: + if r, ok := msg.Value.(modal.SelectResult); ok { + if r.SelectedValue == "other" { + return m.modalManager.ShowModal(modal.ModalCloseReason) + } + cmd := msgs.CloseIssueCmd(m.app, m.currentIssueID, r.SelectedValue) + return func() tea.Msg { return cmd() } + } + case modal.ModalSelectPriority: + if r, ok := msg.Value.(modal.SelectResult); ok { + priority, err := strconv.Atoi(r.SelectedValue) + if err != nil { + return nil + } + cmd := msgs.UpdateIssuePriorityCmd(m.app, m.currentIssueID, priority) + return func() tea.Msg { return cmd() } + } + case modal.ModalSelectType: + if r, ok := msg.Value.(modal.SelectResult); ok { + issueType := models.IssueType(r.SelectedValue) + cmd := msgs.UpdateIssueTypeCmd(m.app, m.currentIssueID, issueType) + return func() tea.Msg { return cmd() } + } + case modal.ModalCloseReason: + if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" { + cmd := msgs.CloseIssueCmd(m.app, m.currentIssueID, r.Value) + return func() tea.Msg { return cmd() } + } + case modal.ModalAddComment: + if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" { + cmd := msgs.AddIssueCommentCmd(m.app, m.currentIssueID, user.GetOsUsername(), r.Value) + return func() tea.Msg { return cmd() } + } + } + return nil +} + +// handleModalCancelled handles all modal cancellation messages +func (m *Model) handleModalCancelled(msg modal.ModalCancelledMsg) { + switch msg.ModalID { + case modal.ModalEditTitle: + m.currentIssueID = "" + case modal.ModalCreateIssue: + // No cleanup needed + case modal.ModalEditAssignee: + m.currentIssueID = "" + case modal.ModalEditDescription: + m.currentIssueID = "" + case modal.ModalConfirmDelete: + m.deleteIndex = -1 + m.currentIssueID = "" + case modal.ModalSelectStatus: + m.currentIssueID = "" + case modal.ModalSelectCloseReason: + m.currentIssueID = "" + case modal.ModalSelectPriority: + m.currentIssueID = "" + case modal.ModalSelectType: + m.currentIssueID = "" + case modal.ModalCloseReason: + m.currentIssueID = "" + case modal.ModalAddComment: + m.currentIssueID = "" + } +} + func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if cmd, handled := m.modalManager.Update(msg); handled { + return m, cmd + } + switch msg := msg.(type) { + case modal.ModalCompletedMsg: + return m, m.handleModalCompleted(msg) + + case modal.ModalCancelledMsg: + m.handleModalCancelled(msg) + return m, nil case msgs.TitleUpdatedMsg: - m.editingTitle = false - m.editingIssueID = "" - m.titleInput.Blur() + m.modalManager.GetTextInputModal(modal.ModalEditTitle).Reset() + m.currentIssueID = "" if msg.Err != nil { return m, nil } return m, m.refreshAndSubmit(msg.IssueID) case msgs.DescriptionUpdatedMsg: - m.editingDescription = false - m.editingDescIssueID = "" - m.descriptionInput.Blur() + m.modalManager.GetTextAreaModal(modal.ModalEditDescription).Reset() + m.currentIssueID = "" if msg.Err != nil { return m, nil } return m, m.refreshAndSubmit(msg.IssueID) case msgs.StatusUpdatedMsg: - m.choosingStatus = false - m.statusIssueID = "" + m.currentIssueID = "" if msg.Err != nil { return m, nil } return m, m.refreshAndSubmit(msg.IssueID) case msgs.PriorityUpdatedMsg: - m.choosingPriority = false - m.priorityIssueID = "" + m.currentIssueID = "" if msg.Err != nil { return m, nil } return m, m.refreshAndSubmit(msg.IssueID) case msgs.TypeUpdatedMsg: - m.choosingType = false - m.typeIssueID = "" + m.currentIssueID = "" if msg.Err != nil { return m, nil } return m, m.refreshAndSubmit(msg.IssueID) case msgs.AssigneeUpdatedMsg: - m.editingAssignee = false - m.assigneeIssueID = "" - m.assigneeInput.Blur() + m.modalManager.GetTextInputModal(modal.ModalEditAssignee).Reset() + m.currentIssueID = "" if msg.Err != nil { return m, nil } return m, m.refreshAndSubmit(msg.IssueID) + case msgs.IssueCommentAddedMsg: + m.modalManager.GetTextAreaModal(modal.ModalAddComment).Reset() + m.currentIssueID = "" + if msg.Err != nil { + return m, nil + } + m.submitValidation() + return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) + case msgs.SelectIssueMsg: m.todoList.SelectIssueID(msg.IssueID) m.inProgList.SelectIssueID(msg.IssueID) @@ -126,9 +318,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case msgs.CreatedMsg: - m.creatingIssue = false - m.createTitleInput.Blur() - m.createTitleInput.Reset() + m.modalManager.GetTextInputModal(modal.ModalCreateIssue).Reset() if msg.Err != nil || msg.Issue == nil { return m, nil } @@ -147,11 +337,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { blockedCmd := m.blockedList.SetIssues(blockedIssues) doneCmd := m.doneList.SetIssues(doneIssues) - // Determine the created issue from the refreshed list to ensure all fields (like ID) are populated. selectedIssue := msg.Issue if selectedIssue.ID == "" { for _, issue := range allIssues { - // Prefer an issue that matches the created issue's title when ID is not yet known. if issue.Title == msg.Issue.Title { selectedIssue = issue break @@ -159,12 +347,15 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - m.issueDetail.SetIssue(*selectedIssue) + m.setDetailIssueWithComments(*selectedIssue) m.submitValidation() - return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} }) + return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { + return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} + }) + case msgs.DeletedMsg: - m.confirmingDelete = false - m.deleteConfirmID = "" + m.deleteIndex = -1 + m.currentIssueID = "" if msg.Err != nil { return m, nil } @@ -183,77 +374,74 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { blockedCmd := m.blockedList.SetIssues(blockedIssues) doneCmd := m.doneList.SetIssues(doneIssues) - // If there are no issues at all, clear the detail view and return. if len(todoIssues) == 0 && len(inProgIssues) == 0 && len(blockedIssues) == 0 && len(doneIssues) == 0 { - m.issueDetail.SetIssue(models.Issue{}) + m.setDetailIssueWithComments(models.Issue{}) m.submitValidation() return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd) } - // Determine which column to use for the next selection based on the current focus. var targetIssues []*models.Issue - switch m.focusedColumn { - case 0: + switch m.focusManager.Current() { + case modal.FocusColumn1: targetIssues = todoIssues if len(targetIssues) == 0 { if len(inProgIssues) > 0 { targetIssues = inProgIssues - m.focusedColumn = 1 + m.focusManager.SetCurrent(modal.FocusColumn2) } else if len(blockedIssues) > 0 { targetIssues = blockedIssues - m.focusedColumn = 2 + m.focusManager.SetCurrent(modal.FocusColumn3) } else if len(doneIssues) > 0 { targetIssues = doneIssues - m.focusedColumn = 3 + m.focusManager.SetCurrent(modal.FocusColumn4) } } - case 1: + case modal.FocusColumn2: targetIssues = inProgIssues if len(targetIssues) == 0 { if len(todoIssues) > 0 { targetIssues = todoIssues - m.focusedColumn = 0 + m.focusManager.SetCurrent(modal.FocusColumn1) } else if len(blockedIssues) > 0 { targetIssues = blockedIssues - m.focusedColumn = 2 + m.focusManager.SetCurrent(modal.FocusColumn3) } else if len(doneIssues) > 0 { targetIssues = doneIssues - m.focusedColumn = 3 + m.focusManager.SetCurrent(modal.FocusColumn4) } } - case 2: + case modal.FocusColumn3: targetIssues = blockedIssues if len(targetIssues) == 0 { if len(inProgIssues) > 0 { targetIssues = inProgIssues - m.focusedColumn = 1 + m.focusManager.SetCurrent(modal.FocusColumn2) } else if len(todoIssues) > 0 { targetIssues = todoIssues - m.focusedColumn = 0 + m.focusManager.SetCurrent(modal.FocusColumn1) } else if len(doneIssues) > 0 { targetIssues = doneIssues - m.focusedColumn = 3 + m.focusManager.SetCurrent(modal.FocusColumn4) } } - case 3: + case modal.FocusColumn4: targetIssues = doneIssues if len(targetIssues) == 0 { if len(blockedIssues) > 0 { targetIssues = blockedIssues - m.focusedColumn = 2 + m.focusManager.SetCurrent(modal.FocusColumn3) } else if len(inProgIssues) > 0 { targetIssues = inProgIssues - m.focusedColumn = 1 + m.focusManager.SetCurrent(modal.FocusColumn2) } else if len(todoIssues) > 0 { targetIssues = todoIssues - m.focusedColumn = 0 + m.focusManager.SetCurrent(modal.FocusColumn1) } } } - // Safety: if targetIssues is still empty here, just clear detail and return. if len(targetIssues) == 0 { - m.issueDetail.SetIssue(models.Issue{}) + m.setDetailIssueWithComments(models.Issue{}) m.submitValidation() return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd) } @@ -263,249 +451,19 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { newIndex = len(targetIssues) - 1 } selectedIssue := targetIssues[newIndex] - m.issueDetail.SetIssue(*selectedIssue) + m.setDetailIssueWithComments(*selectedIssue) m.submitValidation() return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} }) case tea.KeyPressMsg: - if m.confirmingDelete { - switch msg.String() { - case "y", "Y": - issueID := m.deleteConfirmID - idx := m.deleteConfirmIndex - m.confirmingDelete = false - m.deleteConfirmID = "" - return m, msgs.DeleteIssueCmd(m.app, issueID, idx) - case "n", "N", "esc": - m.confirmingDelete = false - m.deleteConfirmID = "" - return m, nil - } - } - - if m.choosingStatus { - switch msg.String() { - case "o": - issueID := m.statusIssueID - m.choosingStatus = false - m.statusIssueID = "" - return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusOpen)) - case "i": - issueID := m.statusIssueID - m.choosingStatus = false - m.statusIssueID = "" - return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) - case "b": - issueID := m.statusIssueID - m.choosingStatus = false - m.statusIssueID = "" - return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusBlocked)) - case "r": - issueID := m.statusIssueID - m.choosingStatus = false - m.statusIssueID = "" - return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) - case "c": - issueID := m.statusIssueID - m.choosingStatus = false - m.statusIssueID = "" - m.choosingCloseReason = true - m.closeReasonIssueID = issueID - return m, nil - case "esc": - m.choosingStatus = false - m.statusIssueID = "" - return m, nil - } - } - - if m.choosingCloseReason { - var reason string - switch msg.String() { - case "d": - reason = "Done" - case "u": - reason = "Duplicate issue" - case "w": - reason = "Won't fix" - case "o": - reason = "Obsolete" - case "h": - m.choosingCloseReason = false - m.closingOtherReason = true - m.closeReasonInput.SetValue("") - m.closeReasonInput.Focus() - return m, nil - case "esc": - m.choosingCloseReason = false - m.closeReasonIssueID = "" - return m, nil - } - - if reason != "" { - issueID := m.closeReasonIssueID - m.choosingCloseReason = false - m.closeReasonIssueID = "" - return m, msgs.CloseIssueCmd(m.app, issueID, reason) - } - } - - if m.closingOtherReason { - switch msg.String() { - case "enter", "ctrl+s": - reason := m.closeReasonInput.Value() - if reason != "" { - issueID := m.closeReasonIssueID - m.closingOtherReason = false - m.closeReasonIssueID = "" - m.closeReasonInput.Blur() - return m, msgs.CloseIssueCmd(m.app, issueID, reason) - } - case "esc": - m.closingOtherReason = false - m.closeReasonIssueID = "" - m.closeReasonInput.Blur() - return m, nil - } - var cmd tea.Cmd - m.closeReasonInput, cmd = m.closeReasonInput.Update(msg) + fl := m.FocusedIssueList() + if fl.FilterState() == list.Filtering { + cmd, _ := fl.Update(msg) return m, cmd } - if m.choosingPriority { - switch msg.String() { - case "0", "1", "2", "3", "4": - issueID := m.priorityIssueID - priority := int(msg.String()[0] - '0') - m.choosingPriority = false - m.priorityIssueID = "" - return m, msgs.UpdateIssuePriorityCmd(m.app, issueID, priority) - case "esc": - m.choosingPriority = false - m.priorityIssueID = "" - return m, nil - default: - return m, nil - } - } - - if m.choosingType { - switch msg.String() { - case "b": - issueID := m.typeIssueID - m.choosingType = false - m.typeIssueID = "" - return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeBug) - case "f": - issueID := m.typeIssueID - m.choosingType = false - m.typeIssueID = "" - return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeFeature) - case "t": - issueID := m.typeIssueID - m.choosingType = false - m.typeIssueID = "" - return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeTask) - case "e": - issueID := m.typeIssueID - m.choosingType = false - m.typeIssueID = "" - return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeEpic) - case "c": - issueID := m.typeIssueID - m.choosingType = false - m.typeIssueID = "" - return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeChore) - case "esc": - m.choosingType = false - m.typeIssueID = "" - return m, nil - default: - return m, nil - } - } - - if m.creatingIssue { - if msg.String() == "enter" { - title := m.createTitleInput.Value() - if title != "" { - return m, msgs.CreateIssueCmd(m.app, title) - } - } - if msg.String() == "esc" { - m.creatingIssue = false - m.createTitleInput.Blur() - m.createTitleInput.Reset() - return m, nil - } - var cmd tea.Cmd - m.createTitleInput, cmd = m.createTitleInput.Update(msg) - return m, cmd - } - - if m.editingAssignee { - if msg.String() == "enter" { - assignee := m.assigneeInput.Value() - return m, msgs.UpdateIssueAssigneeCmd(m.app, m.assigneeIssueID, assignee) - } - if msg.String() == "esc" { - m.editingAssignee = false - m.assigneeIssueID = "" - m.assigneeInput.Blur() - return m, nil - } - var cmd tea.Cmd - m.assigneeInput, cmd = m.assigneeInput.Update(msg) - return m, cmd - } - - if m.editingTitle { - if msg.String() == "enter" { - newTitle := m.titleInput.Value() - if newTitle != "" { - return m, msgs.UpdateIssueTitleCmd(m.app, m.editingIssueID, newTitle) - } - } - if msg.String() == "esc" { - m.editingTitle = false - m.editingIssueID = "" - m.titleInput.Blur() - return m, nil - } - var cmd tea.Cmd - m.titleInput, cmd = m.titleInput.Update(msg) - return m, cmd - } - - if m.editingDescription { - if msg.String() == "ctrl+s" { - issueID := m.editingDescIssueID - newDesc := m.descriptionInput.Value() - m.editingDescription = false - m.editingDescIssueID = "" - m.descriptionInput.Blur() - return m, msgs.UpdateIssueDescriptionCmd(m.app, issueID, newDesc) - } - if msg.String() == "esc" { - m.editingDescription = false - m.editingDescIssueID = "" - m.descriptionInput.Blur() - return m, nil - } - var cmd tea.Cmd - m.descriptionInput, cmd = m.descriptionInput.Update(msg) - return m, cmd - } - - focusedList := m.FocusedIssueList() - if focusedList.FilterState() == list.Filtering { - cmd, _ := focusedList.Update(msg) - return m, cmd - } - - // On main dashboard, ESC does nothing; only q quits; like in lazybeads. if msg.String() == "esc" { return m, nil } @@ -514,6 +472,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if cmd != nil { return m, cmd } + case components.ValidationFeedbackMsg: m.currentFeedback = msg.Feedback if msg.Feedback.Success { @@ -525,6 +484,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height + m.modalManager.SetSize(msg.Width, msg.Height) return m, nil } @@ -532,8 +492,20 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmd, changed := fl.Update(msg) if changed { if selected := fl.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) + m.setDetailIssueWithComments(selected.Issue) } } + + // Only propagate non-key messages to other lists (SetItems, etc.) + // Key messages should only affect the focused list + if _, isKeyMsg := msg.(tea.KeyPressMsg); !isKeyMsg { + // Update all lists to ensure they receive commands like SetItems + todoCmd, _ := m.todoList.Update(msg) + inProgCmd, _ := m.inProgList.Update(msg) + blockedCmd, _ := m.blockedList.Update(msg) + doneCmd, _ := m.doneList.Update(msg) + return m, tea.Sequence(cmd, todoCmd, inProgCmd, blockedCmd, doneCmd) + } + return m, cmd } diff --git a/pkg/tui/views/kanban/view.go b/pkg/tui/views/kanban/view.go index 4b0d38e..8303359 100644 --- a/pkg/tui/views/kanban/view.go +++ b/pkg/tui/views/kanban/view.go @@ -1,19 +1,20 @@ package kanban import ( - "charm.land/bubbletea/v2" + tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/pkg/tui/components" - "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/LazyBachelor/LazyPM/pkg/tui/modal" + "github.com/LazyBachelor/LazyPM/internal/style" ) func (m *Model) View() tea.View { if m.width == 0 || m.height == 0 { - // if there is no space just print a loading message return tea.NewView("Loading...") } m.helpBar.SetWidth(m.width) + m.modalManager.SetSize(m.width, m.height) header := m.header.View(m.width) headerHeight := m.header.Height() @@ -23,45 +24,40 @@ func (m *Model) View() tea.View { contentHeight := m.height - headerHeight - footerHeight totalContentWidth := m.width - 1 - colWidth := totalContentWidth / 4 - if colWidth < 20 { - colWidth = 20 + colWidth := max(totalContentWidth/4, 20) + + // Calculate initial list height (half of content height, minimum 5 rows) + listHeight := contentHeight / 2 + if listHeight < 5 { + listHeight = contentHeight } - // Leave some space for the detail view below the board. - boardHeight := contentHeight / 2 - if boardHeight < 5 { - boardHeight = contentHeight - } + m.todoList.SetSize(colWidth, listHeight-1) + m.inProgList.SetSize(colWidth, listHeight-1) + m.blockedList.SetSize(colWidth, listHeight-1) + m.doneList.SetSize(colWidth, listHeight-1) - m.todoList.SetSize(colWidth, boardHeight-1) - m.inProgList.SetSize(colWidth, boardHeight-1) - m.blockedList.SetSize(colWidth, boardHeight-1) - m.doneList.SetSize(colWidth, boardHeight-1) + // Only highlight the focused column's selected row + currentFocus := m.focusManager.Current() + m.todoList.SetHighlightSelected(currentFocus == modal.FocusColumn1) + m.inProgList.SetHighlightSelected(currentFocus == modal.FocusColumn2) + m.blockedList.SetHighlightSelected(currentFocus == modal.FocusColumn3) + m.doneList.SetHighlightSelected(currentFocus == modal.FocusColumn4) - // Only highlight the selected row in the focused column. - m.todoList.SetHighlightSelected(!m.focusOnDetail && m.focusedColumn == 0) - m.inProgList.SetHighlightSelected(!m.focusOnDetail && m.focusedColumn == 1) - m.blockedList.SetHighlightSelected(!m.focusOnDetail && m.focusedColumn == 2) - m.doneList.SetHighlightSelected(!m.focusOnDetail && m.focusedColumn == 3) + todoLabel := style.LabelStyle.Render("To Do") + inProgLabel := style.LabelStyle.Render("In Progress") + blockedLabel := style.LabelStyle.Render("Blocked") + doneLabel := style.LabelStyle.Render("Done") - // Detail view takes full width below the board. - m.issueDetail.SetSize(totalContentWidth, contentHeight-boardHeight) - - todoLabel := styles.LabelStyle.Render("To Do") - inProgLabel := styles.LabelStyle.Render("In Progress") - blockedLabel := styles.LabelStyle.Render("Blocked") - doneLabel := styles.LabelStyle.Render("Done") - - highlight := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true) - switch m.focusedColumn { - case 0: + highlight := lipgloss.NewStyle().Foreground(style.Primary).Bold(true) + switch currentFocus { + case modal.FocusColumn1: todoLabel = highlight.Render("To Do ▶") - case 1: + case modal.FocusColumn2: inProgLabel = highlight.Render("In Progress ▶") - case 2: + case modal.FocusColumn3: blockedLabel = highlight.Render("Blocked ▶") - case 3: + case modal.FocusColumn4: doneLabel = highlight.Render("Done ▶") } @@ -71,10 +67,13 @@ func (m *Model) View() tea.View { doneCol := lipgloss.JoinVertical(lipgloss.Left, doneLabel, m.doneList.View()) board := lipgloss.JoinHorizontal(lipgloss.Left, todoCol, inProgCol, blockedCol, doneCol) + boardHeight := lipgloss.Height(board) + + detailHeight := max(contentHeight-boardHeight, 5) + m.issueDetail.SetSize(totalContentWidth, detailHeight) + content := lipgloss.JoinVertical(lipgloss.Left, board, m.issueDetail.View()) - // Add spacer to lock footer to bottom of screen when content is shorter than available space - // This is to avoid having the footer floating above the bottom of the screen mainView := lipgloss.JoinVertical(lipgloss.Left, header, content, footer) mainViewHeight := lipgloss.Height(mainView) if mainViewHeight < m.height { @@ -83,60 +82,5 @@ func (m *Model) View() tea.View { mainView = lipgloss.JoinVertical(lipgloss.Left, header, content, spacer, footer) } - if m.choosingCloseReason { - reasonContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Choose closing reason for "+m.closeReasonIssueID+":"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("d = Done u = Duplicate issue w = Won't fix o = Obsolete h = Other"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), - ) - reasonBoxWidth := min(70, m.width-4) - reasonBox := styles.ContainerStyle. - Width(reasonBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(reasonContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, reasonBox)) - } - - if m.closingOtherReason { - editBoxWidth := min(60, m.width-4) - m.closeReasonInput.SetWidth(editBoxWidth - 2) - m.closeReasonInput.SetHeight(4) - editContent := lipgloss.JoinVertical(lipgloss.Left, - styles.LabelStyle.Render("Enter closing reason for "+m.closeReasonIssueID+" (Enter or Ctrl+S to save, Esc to cancel):"), - m.closeReasonInput.View(), - ) - editBox := styles.ContainerStyle. - Width(editBoxWidth). - BorderForeground(styles.PrimaryBorder). - Render(editContent) - return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)) - } - - return tea.NewView(components.RenderModals( - m.width, - m.height, - m.editingTitle, - m.titleInput.View(), - m.editingDescription, - m.descriptionInput.View(), - m.creatingIssue, - m.createTitleInput.View(), - m.confirmingDelete, - m.deleteConfirmID, - m.choosingStatus, - m.statusIssueID, - m.choosingPriority, - m.priorityIssueID, - m.choosingType, - m.typeIssueID, - m.editingAssignee, - m.assigneeInput.View(), - mainView, - )) - -} - -func (m *Model) footer() string { - // Kept for backwards compatibility; delegate to the shared helper. - return components.RenderFooter(m.width, &m.helpBar, m.currentFeedback) + return tea.NewView(m.modalManager.RenderWithMainView(mainView)) } From 2abddd5b54f5fd37a4edcec0bc0c56069a2636c7 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 16:33:56 +0100 Subject: [PATCH 57/73] fix from merge --- pkg/tui/components/issue_list.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/tui/components/issue_list.go b/pkg/tui/components/issue_list.go index 52e2cea..e306b4f 100644 --- a/pkg/tui/components/issue_list.go +++ b/pkg/tui/components/issue_list.go @@ -14,7 +14,6 @@ import ( "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/style" - "github.com/LazyBachelor/LazyPM/pkg/tui/styles" "github.com/muesli/reflow/truncate" ) From 14e64f73f2f2c03e06633b4d743e0a608e22d3df Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 17:11:51 +0100 Subject: [PATCH 58/73] fix close modal --- pkg/tui/components/helpbar.go | 4 ++++ pkg/tui/components/keymap.go | 2 +- pkg/tui/modal/confirm.go | 26 +++++++++++++------------- pkg/tui/modal/manager.go | 9 +++++++++ pkg/tui/modal/modal.go | 1 + pkg/tui/views/dashboard/keys.go | 9 ++------- pkg/tui/views/dashboard/operations.go | 17 ++++++++++++++++- pkg/tui/views/kanban/keys.go | 3 ++- pkg/tui/views/kanban/operations.go | 17 ++++++++++++++++- 9 files changed, 64 insertions(+), 24 deletions(-) diff --git a/pkg/tui/components/helpbar.go b/pkg/tui/components/helpbar.go index 27ec2a7..004b237 100644 --- a/pkg/tui/components/helpbar.go +++ b/pkg/tui/components/helpbar.go @@ -177,6 +177,7 @@ func helpBarConfig(view ViewKind) HelpBarConfig { {Key: "v", Desc: "kanban"}, {Key: "↑/k", Desc: "up"}, {Key: "↓/j", Desc: "down"}, + {Key: "/", Desc: "search"}, {Key: "a", Desc: "add"}, {Key: "c", Desc: "comment"}, {Key: "e/d/s/p/t/A", Desc: "edit"}, @@ -188,6 +189,7 @@ func helpBarConfig(view ViewKind) HelpBarConfig { {Key: "v", Desc: "kanban"}, {Key: "↑/k", Desc: "up"}, {Key: "↓/j", Desc: "down"}, + {Key: "/", Desc: "search"}, {Key: "a", Desc: "add issue"}, {Key: "c", Desc: "add comment"}, {Key: "x", Desc: "delete issue"}, @@ -208,6 +210,7 @@ func helpBarConfig(view ViewKind) HelpBarConfig { {Key: "v", Desc: "list view"}, {Key: "↑/k", Desc: "up"}, {Key: "↓/j", Desc: "down"}, + {Key: "/", Desc: "search"}, {Key: "pgup/pgdn", Desc: "page"}, {Key: "h/l", Desc: "column"}, {Key: "←/→", Desc: "move"}, @@ -223,6 +226,7 @@ func helpBarConfig(view ViewKind) HelpBarConfig { {Key: "←/→", Desc: "move issue"}, {Key: "↑/k", Desc: "up"}, {Key: "↓/j", Desc: "down"}, + {Key: "/", Desc: "search"}, {Key: "pgup", Desc: "page up"}, {Key: "pgdn", Desc: "page down"}, {Key: "a", Desc: "add issue"}, diff --git a/pkg/tui/components/keymap.go b/pkg/tui/components/keymap.go index c5bc96e..d5baf34 100644 --- a/pkg/tui/components/keymap.go +++ b/pkg/tui/components/keymap.go @@ -27,7 +27,7 @@ func DefaultCommonKeyMap() CommonKeyMap { key.WithHelp("?", "help"), ), Quit: key.NewBinding( - key.WithKeys("q", "ctrl+c"), + key.WithKeys("q"), key.WithHelp("q", "quit"), ), ScrollUp: key.NewBinding( diff --git a/pkg/tui/modal/confirm.go b/pkg/tui/modal/confirm.go index 20e9a8b..c9e2843 100644 --- a/pkg/tui/modal/confirm.go +++ b/pkg/tui/modal/confirm.go @@ -93,22 +93,22 @@ func (c *ConfirmModal) Update(msg tea.Msg) (tea.Cmd, bool) { // Check yes keys if slices.Contains(c.yesKeys, s) { - c.Deactivate() - return func() tea.Msg { - return ModalCompletedMsg{ - ModalID: c.ID(), - Value: ConfirmResult{Confirmed: true}, - } - }, true - } + c.Deactivate() + return func() tea.Msg { + return ModalCompletedMsg{ + ModalID: c.ID(), + Value: ConfirmResult{Confirmed: true}, + } + }, true + } // Check no/cancel keys if slices.Contains(c.noKeys, s) { - c.Deactivate() - return func() tea.Msg { - return ModalCancelledMsg{ModalID: c.ID()} - }, true - } + c.Deactivate() + return func() tea.Msg { + return ModalCancelledMsg{ModalID: c.ID()} + }, true + } } return nil, true diff --git a/pkg/tui/modal/manager.go b/pkg/tui/modal/manager.go index 3250677..f166d10 100644 --- a/pkg/tui/modal/manager.go +++ b/pkg/tui/modal/manager.go @@ -163,6 +163,15 @@ func (m *Manager) RenderWithMainView(mainView string) string { // RegisterCommonModals registers the standard set of modals used across views. // This helper reduces duplication between dashboard and kanban views. func RegisterCommonModals(m *Manager) { + + // Exit Confirm Modal + m.RegisterModal(NewConfirmModal(ConfirmConfig{ + ID: ModalConfirmExit, + Message: "Close the task and interface?", + YesKeys: []string{"y", "Y"}, + NoKeys: []string{"n", "N", "esc"}, + })) + // Edit Title Modal m.RegisterModal(NewTextInputModal(TextInputConfig{ ID: ModalEditTitle, diff --git a/pkg/tui/modal/modal.go b/pkg/tui/modal/modal.go index 0e3e3d6..151fc85 100644 --- a/pkg/tui/modal/modal.go +++ b/pkg/tui/modal/modal.go @@ -50,6 +50,7 @@ type ModalCancelledMsg struct { // Modal IDs used across the application const ( + ModalConfirmExit = "confirm-exit" ModalEditTitle = "edit-title" ModalCreateIssue = "create-issue" ModalEditAssignee = "edit-assignee" diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 8cce7cc..34187f4 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -10,11 +10,6 @@ import ( type KeyMap struct { components.CommonKeyMap SwitchToKanbanBoard key.Binding - Quit key.Binding - SelectIssue key.Binding - BackToList key.Binding - ScrollUp key.Binding - ScrollDown key.Binding EditTitle key.Binding EditDescription key.Binding ChangeStatus key.Binding @@ -78,8 +73,8 @@ func (m *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { m.logAction("tui toggled help") case m.notInModalMsgWithKey(msg, m.keyMap.Quit): - m.logAction("tui quit requested") - return tea.Quit + m.logAction("tui opened exit confirmation") + return m.startConfirmExit() case m.notInModalMsgWithKey(msg, m.keyMap.SwitchToKanbanBoard): return func() tea.Msg { return msgs.SwitchToKanbanBoardMsg{} } diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index b27d26d..8b72da6 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -34,6 +34,10 @@ func (m *Model) refreshAndSubmit(issueID string) tea.Cmd { return refreshCmd } +func (m *Model) startConfirmExit() tea.Cmd { + return m.modalManager.ShowModal(modal.ModalConfirmExit) +} + func (m *Model) startEditTitle(selected ListIssue) tea.Cmd { m.currentIssueID = selected.ID titleModal := m.modalManager.GetTextInputModal(modal.ModalEditTitle) @@ -113,6 +117,11 @@ func (m *Model) startAddComment(selected ListIssue) tea.Cmd { // handleModalCompleted handles all modal completion messages func (m *Model) handleModalCompleted(msg modal.ModalCompletedMsg) tea.Cmd { switch msg.ModalID { + case modal.ModalConfirmExit: + if r, ok := msg.Value.(modal.ConfirmResult); ok && r.Confirmed { + m.logAction("tui confirmed exit") + return tea.Quit + } case modal.ModalEditTitle: if r, ok := msg.Value.(modal.TextInputResult); ok { m.logAction("tui submitted issue title edit") @@ -196,6 +205,8 @@ func (m *Model) handleModalCompleted(msg modal.ModalCompletedMsg) tea.Cmd { // handleModalCancelled handles all modal cancellation messages func (m *Model) handleModalCancelled(msg modal.ModalCancelledMsg) { switch msg.ModalID { + case modal.ModalConfirmExit: + m.logAction("tui canceled exit") case modal.ModalEditTitle: m.currentIssueID = "" m.logAction("tui canceled issue title edit") @@ -354,6 +365,10 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case tea.KeyPressMsg: + if msg.String() == "ctrl+c" { + return m, tea.Quit + } + if m.issueList.FilterState() == list.Filtering { cmd, _ := m.issueList.Update(msg) return m, cmd @@ -364,7 +379,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } cmd := m.handleKeyPressMsg(msg) - if cmd != nil { + if cmd != nil || m.IsInModal() { return m, cmd } diff --git a/pkg/tui/views/kanban/keys.go b/pkg/tui/views/kanban/keys.go index 85f8be8..4017711 100644 --- a/pkg/tui/views/kanban/keys.go +++ b/pkg/tui/views/kanban/keys.go @@ -54,7 +54,8 @@ func (m *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { m.helpBar.ToggleHelp() case m.notInModalMsgWithKey(msg, m.keyMap.Quit): - return tea.Quit + m.logAction("tui opened exit confirmation") + return m.startConfirmExit() case m.notInModalMsgWithKey(msg, m.keyMap.SwitchToDashboard): return func() tea.Msg { return msgs.SwitchToDashboardMsg{} } diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index 7884f7f..5e767bb 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -62,6 +62,10 @@ func (m *Model) refreshAndSubmit(issueID string) tea.Cmd { // Modal action handlers +func (m *Model) startConfirmExit() tea.Cmd { + return m.modalManager.ShowModal(modal.ModalConfirmExit) +} + func (m *Model) startEditTitle(selected ListIssue) tea.Cmd { m.currentIssueID = selected.ID titleModal := m.modalManager.GetTextInputModal(modal.ModalEditTitle) @@ -141,6 +145,11 @@ func (m *Model) startAddComment(selected ListIssue) tea.Cmd { // handleModalCompleted handles all modal completion messages func (m *Model) handleModalCompleted(msg modal.ModalCompletedMsg) tea.Cmd { switch msg.ModalID { + case modal.ModalConfirmExit: + if r, ok := msg.Value.(modal.ConfirmResult); ok && r.Confirmed { + m.logAction("tui confirmed exit") + return tea.Quit + } case modal.ModalEditTitle: if r, ok := msg.Value.(modal.TextInputResult); ok { cmd := msgs.UpdateIssueTitleCmd(m.app, m.currentIssueID, r.Value) @@ -218,6 +227,8 @@ func (m *Model) handleModalCompleted(msg modal.ModalCompletedMsg) tea.Cmd { // handleModalCancelled handles all modal cancellation messages func (m *Model) handleModalCancelled(msg modal.ModalCancelledMsg) { switch msg.ModalID { + case modal.ModalConfirmExit: + m.logAction("tui canceled exit") case modal.ModalEditTitle: m.currentIssueID = "" case modal.ModalCreateIssue: @@ -458,6 +469,10 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) case tea.KeyPressMsg: + if msg.String() == "ctrl+c" { + return m, tea.Quit + } + fl := m.FocusedIssueList() if fl.FilterState() == list.Filtering { cmd, _ := fl.Update(msg) @@ -469,7 +484,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } cmd := m.handleKeyPressMsg(msg) - if cmd != nil { + if cmd != nil || m.IsInModal() { return m, cmd } From ce944085fe69f7563cae8e8992c439b755a4f697 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 20:53:14 +0100 Subject: [PATCH 59/73] make sure we check this in coding task --- cmd/pm/tasks/codingTask.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 8d5f2e7..4bb1a38 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -6,6 +6,7 @@ import ( "strings" "charm.land/huh/v2" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" ) @@ -141,7 +142,9 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { return expect.Fatal("Issue was deleted or could not be found") } - expect.Equal(issue.Assignee, "Me", "Issue Assignee") + if !expect.Equal(issue.Assignee, "Me", "Issue Assignee").Valid() { + return expect.ValidationFeedback + } if _, err := os.Stat("./code.txt"); os.IsNotExist(err) { expect.Fail("The code.txt file should exist on the desktop.") @@ -161,6 +164,11 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { } expect.Contains(code, "go.mongodb.org/mongo-driver v1.17.9", "MongoDB Driver version") + if !expect.Valid() { + return expect.ValidationFeedback + } + + expect.Equal(issue.Status, models.StatusClosed, "Issue Status") return expect.Complete() } From 515007bf387e06a2138a7a66f33c13ba3ab7852e Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 20:53:29 +0100 Subject: [PATCH 60/73] simplify dep task --- cmd/pm/tasks/dependencyManagement.go | 46 +++++++++------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/cmd/pm/tasks/dependencyManagement.go b/cmd/pm/tasks/dependencyManagement.go index 798d2e9..e71bdeb 100644 --- a/cmd/pm/tasks/dependencyManagement.go +++ b/cmd/pm/tasks/dependencyManagement.go @@ -14,13 +14,15 @@ const dependencyManagementDescription = `You are tasked with managing issue depe Several issues in your project have dependencies on other issues. You need to: -1. Find the 4 issues that mention dependencies in their detail description. For example: "Depends on Issue '123'". Set their status to "blocked". -2. Find the 2 foundational issues that are mentioned by the other issues. -3. Set priority of the 2 foundational issues to 3 (high). -4. Set status of the 2 foundational issues to in-progress. -5. Assign the 2 foundational issues to yourself as "Me". +1. Find 2 issues that mention dependencies in their detail description. + For example: "Depends on Issue '123'". Set their status to "blocked". -Resolving dependencies in the right order is critical for efficient team workflow.` +2. Find the issue that is mentioned by the other issues: + - Set priority to 3 (high). + - Set status to in-progress. + - Assign the to yourself as "Me". + +Resolving dependencies in the right order is critical for a efficient team` type DependencyManagementTask struct { done bool @@ -78,13 +80,6 @@ func (t *DependencyManagementTask) Setup(ctx context.Context) error { WithStatus(models.StatusOpen). WithIssueType(models.TypeTask). Build(), - NewIssueBuilder(). - WithTitle("Create home page for the website"). - WithDescription("Create a page for the website."). - WithPriority(2). - WithStatus(models.StatusOpen). - WithIssueType(models.TypeTask). - Build(), NewIssueBuilder(). WithTitle("Implement Authentication System"). WithDescription("Add login/logout functionality. Depends on 'Setup database connection' issue."). @@ -99,20 +94,6 @@ func (t *DependencyManagementTask) Setup(ctx context.Context) error { WithStatus(models.StatusOpen). WithIssueType(models.TypeTask). Build(), - NewIssueBuilder(). - WithTitle("Create user profile page"). - WithDescription("Frontend user profile page. Depends on 'Create home page for the website' issue."). - WithPriority(3). - WithStatus(models.StatusOpen). - WithIssueType(models.TypeTask). - Build(), - NewIssueBuilder(). - WithTitle("Create about page"). - WithDescription("Frontend about page. Depends on 'Create home page for the website' issue."). - WithPriority(2). - WithStatus(models.StatusOpen). - WithIssueType(models.TypeTask). - Build(), } return t.app.Issues.CreateIssues(ctx, t.depIssues, "") @@ -127,14 +108,18 @@ func (t *DependencyManagementTask) Validate(ctx context.Context) ValidationFeedb } for _, issue := range issues { - for _, depIssue := range t.depIssues[2:] { + for _, depIssue := range t.depIssues[1:] { if issue.Title == depIssue.Title { expect.Equal(issue.Status, models.StatusBlocked, fmt.Sprintf("%s status", issue.Title)) } } - - for _, foundationalIssue := range t.depIssues[:2] { + } + if !expect.Valid() { + return expect.ValidationFeedback + } + for _, issue := range issues { + for _, foundationalIssue := range t.depIssues[:1] { if issue.Title == foundationalIssue.Title { expect.Equal(issue.Priority, 3, fmt.Sprintf("%s priority", issue.Title)) @@ -144,7 +129,6 @@ func (t *DependencyManagementTask) Validate(ctx context.Context) ValidationFeedb fmt.Sprintf("%s status", issue.Title)) } } - } return expect.Complete() From ee2456c3fd617a4afb0d66ac2af87ac1bffd6073 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 20 Mar 2026 21:01:46 +0100 Subject: [PATCH 61/73] clear terminal between tasks and give users a message confirming they are done --- cmd/pm/runner.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/pm/runner.go b/cmd/pm/runner.go index a4de2c6..175b565 100644 --- a/cmd/pm/runner.go +++ b/cmd/pm/runner.go @@ -147,6 +147,10 @@ func runStartCmd(cmd *cobra.Command, args []string) error { if err := taskLoop(cmd.Context(), app, surveyTasks, interfaces); err != nil { return returnIfUserQuit(err, "task loop failed") } + + //clear the terminal after the survey is done + fmt.Println("\033[H\033[2J") + cmd.Println(style.TitleStyle.Render("Thank you for completing the survey! You are now finished and can safely close the terminal.")) return nil } @@ -188,6 +192,7 @@ func taskLoop(ctx context.Context, application *task.App, surveyTasks map[string return fmt.Errorf("failed to write task details: %w", err) } + fmt.Println("\033[H\033[2J") if err := runner.Run(ctx, t, selected, tasks.InterfaceToType(selected)); err != nil { return err } From 38ecf6f0af471a52b365850e9ed24b76faf49101 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 22 Mar 2026 14:02:19 +0100 Subject: [PATCH 62/73] (hotfix) make header title styling work again --- internal/style/styles.go | 2 +- pkg/tui/components/header.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/style/styles.go b/internal/style/styles.go index a79d65e..a041f5b 100644 --- a/internal/style/styles.go +++ b/internal/style/styles.go @@ -33,7 +33,7 @@ var DefaultBorder = lipgloss.ThickBorder() var ( HeaderStyle = lipgloss.NewStyle().Foreground(Primary).Padding(0, 1).Bold(true) - HeaderTitleStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true).Padding(0) + HeaderTitleStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true).PaddingRight(1) ) var ContainerStyle = lipgloss.NewStyle(). diff --git a/pkg/tui/components/header.go b/pkg/tui/components/header.go index 6c646d2..ec0663d 100644 --- a/pkg/tui/components/header.go +++ b/pkg/tui/components/header.go @@ -21,6 +21,7 @@ func (h Header) View(width int) string { lipgloss.Left, title, lipgloss.WithWhitespaceChars("─"), + lipgloss.WithWhitespaceStyle(style.TitleStyle), ) } From ca0b00292b9cf59c87b4ced858442568490354ca Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 22 Mar 2026 15:15:36 +0100 Subject: [PATCH 63/73] refine task ui alignment --- pkg/task/taskui.go | 108 +++++++++++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 28 deletions(-) diff --git a/pkg/task/taskui.go b/pkg/task/taskui.go index b3ef3e2..190c7ce 100644 --- a/pkg/task/taskui.go +++ b/pkg/task/taskui.go @@ -2,10 +2,9 @@ package task import ( "fmt" - "strings" "charm.land/bubbles/v2/key" - "charm.land/bubbletea/v2" + tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/style" @@ -38,7 +37,7 @@ var DefaultTaskKeys = TaskHelpKeys{ ), About: key.NewBinding( key.WithKeys("?"), - key.WithHelp("?", "Details about the interface"), + key.WithHelp("?", "Learn interface"), ), } @@ -75,46 +74,99 @@ func (m TaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m TaskModel) View() tea.View { if m.width < 55 || m.height < 16 { - content := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, - style.TextStyle.Render("Terminal too small.")) + content := lipgloss.Place( + m.width, + m.height, + lipgloss.Center, + lipgloss.Center, + style.TextStyle.Render("Terminal too small."), + ) v := tea.NewView(content) v.AltScreen = true return v } - boxWidth := min(m.width-10, 120) + boxWidth := min(m.width-8, 120) - detailsText := fmt.Sprintf("Interface Type: %s | Time to complete: %s | Difficulty: %s", m.InterfaceType, m.TimeToComplete, m.Difficulty) + cardStyle := style.BorderStyle.Padding(1, 2) + bodyWidth := boxWidth - cardStyle.GetHorizontalFrameSize() + cardStyle = cardStyle.Width(bodyWidth) - boxStyle := style.BorderStyle.Padding(2, 4).Width(boxWidth) + textWidth := min(bodyWidth, 64) - var b strings.Builder + titleWrap := lipgloss.NewStyle(). + Width(boxWidth). + Align(lipgloss.Center) - b.WriteString(style.TitleStyle.Render(m.Title)) - b.WriteString("\n") + helpWrap := lipgloss.NewStyle(). + Width(boxWidth). + Align(lipgloss.Center) - content := lipgloss.JoinVertical( - lipgloss.Center, - style.TextStyle.Render(m.Description), - "\n", - style.TextStyle.Foreground(style.SecondaryColor).Render(detailsText), - style.ErrorStyle.Render(m.interfaceHelpText()), - style.ErrorStyle.Render(m.getQuitHelpText()), - ) + descStyle := style.TextStyle. + Width(textWidth). + Align(lipgloss.Left) - if m.aboutVisible { - b.WriteString(boxStyle.Render(m.InterfaceDescription)) - } else { - b.WriteString(boxStyle.Render(content)) + detailsStyle := style.TextStyle. + Foreground(style.SecondaryText). + Width(textWidth). + Align(lipgloss.Center) + infoStyle := style.ErrorStyle. + Width(textWidth). + Align(lipgloss.Center) + + centerInCard := func(s string) string { + return lipgloss.PlaceHorizontal(bodyWidth, lipgloss.Center, s) } - b.WriteString("\n") + detailsText := fmt.Sprintf( + "Interface Type: %s | Time to complete: %s | Difficulty: %s", + m.InterfaceType, + m.TimeToComplete, + m.Difficulty, + ) - helpText := "Press " + m.keys.Start.Help().Key + " to start • " + m.keys.Quit.Help().Key + " to quit • " + m.keys.About.Help().Key + " " + m.keys.About.Help().Desc - b.WriteString(style.HelpStyle.Render(helpText)) + var body string + if m.aboutVisible { + about := style.TextStyle. + Width(textWidth). + Align(lipgloss.Left). + Render(m.InterfaceDescription) - final := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, b.String()) + body = centerInCard(about) + } else { + body = lipgloss.JoinVertical( + lipgloss.Left, + centerInCard(descStyle.Render(m.Description)), + "", + centerInCard(detailsStyle.Render(detailsText)), + centerInCard(infoStyle.Render(m.interfaceHelpText())), + centerInCard(infoStyle.Render(m.getQuitHelpText())), + ) + } + + card := cardStyle.Render(body) + + helpText := "Press " + m.keys.Start.Help().Key + " " + m.keys.Start.Help().Desc + + " • " + m.keys.Quit.Help().Key + " " + m.keys.Quit.Help().Desc + + " • " + m.keys.About.Help().Key + " " + m.keys.About.Help().Desc + + view := lipgloss.JoinVertical( + lipgloss.Center, + titleWrap.Render(style.TitleStyle.Render(m.Title)), + "", + card, + "", + helpWrap.Render(style.HelpStyle.Render(helpText)), + ) + + final := lipgloss.Place( + m.width, + m.height, + lipgloss.Center, + lipgloss.Center, + view, + ) v := tea.NewView(final) v.AltScreen = true @@ -137,7 +189,7 @@ func (m TaskModel) getQuitHelpText() string { func (m TaskModel) interfaceHelpText() string { switch m.InterfaceType { case models.InterfaceTypeWeb: - return "Press the button in the upper right corner to view task progress and completion criteria" + return "Press the button in the upper right corner to view task progress" case models.InterfaceTypeTUI: return "Press Shift+S to view task progress and completion criteria" case models.InterfaceTypeCLI, models.InterfaceTypeREPL: From aac74c425df44ac81d41e4b9506a149a0c90eda1 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 22 Mar 2026 15:15:55 +0100 Subject: [PATCH 64/73] use new style --- internal/style/styles.go | 57 +++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/internal/style/styles.go b/internal/style/styles.go index a041f5b..812eee3 100644 --- a/internal/style/styles.go +++ b/internal/style/styles.go @@ -6,21 +6,54 @@ import ( ) var ( - Primary = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")} - Secondary = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")} + Primary = compat.AdaptiveColor{ + Light: lipgloss.Color("6"), + Dark: lipgloss.Color("6"), + } + Secondary = compat.AdaptiveColor{ + Light: lipgloss.Color("2"), + Dark: lipgloss.Color("2"), + } - Success = compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")} - Warning = compat.AdaptiveColor{Light: lipgloss.Color("#F59E0B"), Dark: lipgloss.Color("#F59E0B")} - Error = compat.AdaptiveColor{Light: lipgloss.Color("#FE5F86"), Dark: lipgloss.Color("#FE5F86")} + Success = compat.AdaptiveColor{ + Light: lipgloss.Color("2"), + Dark: lipgloss.Color("2"), + } + Warning = compat.AdaptiveColor{ + Light: lipgloss.Color("3"), + Dark: lipgloss.Color("3"), + } + Error = compat.AdaptiveColor{ + Light: lipgloss.Color("9"), + Dark: lipgloss.Color("9"), + } - PrimaryText = compat.AdaptiveColor{Light: lipgloss.Color("#1A1A1A"), Dark: lipgloss.Color("#E0E0E0")} - SecondaryText = compat.AdaptiveColor{Light: lipgloss.Color("#666666"), Dark: lipgloss.Color("#999999")} - FaintText = compat.AdaptiveColor{Light: lipgloss.Color("#999999"), Dark: lipgloss.Color("#666666")} + PrimaryText = compat.AdaptiveColor{ + Light: lipgloss.Color("7"), + Dark: lipgloss.Color("7"), + } + SecondaryText = compat.AdaptiveColor{ + Light: lipgloss.Color("8"), + Dark: lipgloss.Color("8"), + } + FaintText = compat.AdaptiveColor{ + Light: lipgloss.Color("8"), + Dark: lipgloss.Color("8"), + } - PrimaryBorder = compat.AdaptiveColor{Light: lipgloss.Color("#5A56E0"), Dark: lipgloss.Color("#7571F9")} - SecondaryBorder = compat.AdaptiveColor{Light: lipgloss.Color("#CCCCCC"), Dark: lipgloss.Color("#444444")} + PrimaryBorder = compat.AdaptiveColor{ + Light: lipgloss.Color("8"), + Dark: lipgloss.Color("8"), + } + SecondaryBorder = compat.AdaptiveColor{ + Light: lipgloss.Color("8"), + Dark: lipgloss.Color("8"), + } - SelectedBackground = compat.AdaptiveColor{Light: lipgloss.Color("#E8E8E8"), Dark: lipgloss.Color("#333333")} + SelectedBackground = compat.AdaptiveColor{ + Light: lipgloss.Color("5"), + Dark: lipgloss.Color("5"), + } ) const ( @@ -95,5 +128,3 @@ var ( ErrorStyle = lipgloss.NewStyle().Foreground(Error).Bold(true) HelpStyle = lipgloss.NewStyle().Align(lipgloss.Center).Foreground(Secondary) ) - -var SecondaryColor = lipgloss.Color("#02BA84") From 11afdfd9b8543f59b1268a4731038502ea95ee70 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 22 Mar 2026 15:16:10 +0100 Subject: [PATCH 65/73] fix repl arrow bug --- go.mod | 6 +++--- go.sum | 3 +-- pkg/repl/repl.go | 14 +++++++------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index e5a75b7..dd0e8e8 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,14 @@ module github.com/LazyBachelor/LazyPM go 1.26.1 require ( + github.com/c-bata/go-prompt v0.2.6 github.com/go-git/go-git/v6 v6.0.0-20260317113930-fb0d09929504 github.com/joho/godotenv v1.5.1 github.com/muesli/reflow v0.3.0 github.com/spf13/pflag v1.0.10 github.com/steveyegge/beads v0.49.6 go.mongodb.org/mongo-driver/v2 v2.5.0 + golang.org/x/term v0.41.0 ) // Terminal dependencies @@ -18,9 +20,7 @@ require ( charm.land/fang/v2 v2.0.1 charm.land/huh/v2 v2.0.3 charm.land/lipgloss/v2 v2.0.2 - github.com/c-bata/go-prompt v0.2.6 github.com/spf13/cobra v1.10.2 - golang.org/x/term v0.41.0 ) // Web dependencies @@ -78,7 +78,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.21 // indirect - github.com/mattn/go-tty v0.0.7 // indirect + github.com/mattn/go-tty v0.0.3 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/mango v0.2.0 // indirect diff --git a/go.sum b/go.sum index 1dc87ce..66d3907 100644 --- a/go.sum +++ b/go.sum @@ -160,9 +160,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= -github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= -github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index abe2cb8..ce0f3f1 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -52,14 +52,11 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { r.currentFeedback = ValidationFeedback{} r.completionChan = make(chan struct{}, 1) - // Set terminal to raw mode to capture input properly in the REPL. - // This allows us to handle input character by character and provide a better user experience. - // We also ensure that the terminal state is restored when the REPL exits, even if an error occurs. + // Save terminal state to restore on exit (go-prompt v0.2.6 doesn't restore properly) oldState, err := term.GetState(int(os.Stdin.Fd())) - if err != nil { - return fmt.Errorf("failed to get terminal state: %w", err) + if err == nil { + defer term.Restore(int(os.Stdin.Fd()), oldState) } - defer term.Restore(int(os.Stdin.Fd()), oldState) // Initialize services for beads, config and stats. app, cleanup, err := app.New(ctx, config) @@ -114,7 +111,10 @@ replLoop: case <-r.completionChan: fmt.Println(style.TitleStyle.Render("Task completed successfully!")) fmt.Print("Press Enter to exit...") - term.Restore(int(os.Stdin.Fd()), oldState) + // Restore terminal before reading final input (go-prompt doesn't restore properly) + if oldState != nil { + term.Restore(int(os.Stdin.Fd()), oldState) + } reader.ReadString('\n') break replLoop } From ecabaf796ea139b7f592797e1dd8bb4e2a9f845f Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 22 Mar 2026 15:27:05 +0100 Subject: [PATCH 66/73] update task descriptions for better readability --- cmd/pm/tasks/backlogRefinement.go | 5 +++-- cmd/pm/tasks/codingTask.go | 12 ++++-------- cmd/pm/tasks/createIssue.go | 2 -- cmd/pm/tasks/dependencyManagement.go | 8 ++------ cmd/pm/tasks/gitTask.go | 10 +++++----- cmd/pm/tasks/issueReviewCleanup.go | 3 +-- cmd/pm/tasks/priorityManagement.go | 8 +++----- 7 files changed, 18 insertions(+), 30 deletions(-) diff --git a/cmd/pm/tasks/backlogRefinement.go b/cmd/pm/tasks/backlogRefinement.go index 30e66ae..e823ca8 100644 --- a/cmd/pm/tasks/backlogRefinement.go +++ b/cmd/pm/tasks/backlogRefinement.go @@ -11,10 +11,11 @@ import ( const backlogRefinementDescription = `You are tasked with backlog refinement. -The product backlog has become cluttered with old and unclear issues. You need to groom the backlog: +The product backlog has become cluttered with old and unclear issues. +You need to groom the backlog: 1. Go to the backlog. -2. Find two issues that got the same name or describe the same problem. +2. Find two issues that have the same name. 3. Open one of these issues. 4. Select "Close issue" 5. Choose "Duplicate issue" as closing reason. diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 4bb1a38..cac668a 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -10,17 +10,13 @@ import ( "github.com/LazyBachelor/LazyPM/internal/utils/check" ) -const codingDescription = `You are tasked with doing a chore in the codebase. - -This task will test your ability to read and understand instructions, change text, and save it to a file. - -The MongoDB Driver dependency in the file is outdated and needs to be updated to the latest version. -This is a common task for developers, and it requires attention to detail and the ability to follow instructions carefully. +const codingDescription = `You are tasked with doing a upgrading a dependency in the codebase. Your task: 1. Assign the given issue to yourself as 'Me'. -2. A file will appear in the current directory named "code.txt". - Open it and follow the instructions inside. And save the file after you are done. +2. A text file will appear in the this directory: + - Open it and follow the instructions inside. + - Save the file after you are done. 3. When you are done, mark this task as "Closed".` var textFileDescription = ` diff --git a/cmd/pm/tasks/createIssue.go b/cmd/pm/tasks/createIssue.go index 4893e8d..9bd2bb9 100644 --- a/cmd/pm/tasks/createIssue.go +++ b/cmd/pm/tasks/createIssue.go @@ -10,8 +10,6 @@ import ( const description = `You are tasked with creating a new issue in the project management system. -This task will test your ability to use the issue creation workflow effectively. - Your task: 1. Create a new issue with the title "My first Issue" 2. Add this detailed description "I need to do some coding" diff --git a/cmd/pm/tasks/dependencyManagement.go b/cmd/pm/tasks/dependencyManagement.go index e71bdeb..05ee072 100644 --- a/cmd/pm/tasks/dependencyManagement.go +++ b/cmd/pm/tasks/dependencyManagement.go @@ -14,15 +14,11 @@ const dependencyManagementDescription = `You are tasked with managing issue depe Several issues in your project have dependencies on other issues. You need to: -1. Find 2 issues that mention dependencies in their detail description. - For example: "Depends on Issue '123'". Set their status to "blocked". - +1. Find 2 issues that mention dependencies in their description. 2. Find the issue that is mentioned by the other issues: - Set priority to 3 (high). - Set status to in-progress. - - Assign the to yourself as "Me". - -Resolving dependencies in the right order is critical for a efficient team` + - Assign the to yourself as "Me".` type DependencyManagementTask struct { done bool diff --git a/cmd/pm/tasks/gitTask.go b/cmd/pm/tasks/gitTask.go index c19a329..e89941a 100644 --- a/cmd/pm/tasks/gitTask.go +++ b/cmd/pm/tasks/gitTask.go @@ -14,14 +14,14 @@ import ( const gitTaskDescription = `You are tasked with performing a Git operation. -This task will test your ability to use Git effectively within a project management workflow. -Your goal is to modify a file in a Git repository and commit the change. +Modify a file in a Git repository and commit the change. Your task: 1. Assign the given issue to yourself as 'Me'. -2. A folder called "task" is created in the project directory when you start this task. -3. Inside the folder you will find README.md. Edit this file and add something to it. - The file must be different from its original content. +2. A folder called "task" is created in this directory. +3. Inside the folder you will find README.md. + - Edit this file and add something to it. + - The file must be different from its original content. 4. Commit your change: - Open a terminal and change into the task folder. - Run "git add ." to stage the changes. diff --git a/cmd/pm/tasks/issueReviewCleanup.go b/cmd/pm/tasks/issueReviewCleanup.go index 194d760..1299f28 100644 --- a/cmd/pm/tasks/issueReviewCleanup.go +++ b/cmd/pm/tasks/issueReviewCleanup.go @@ -9,8 +9,7 @@ import ( const issueReviewCleanupDescription = `You are responsible for reviewing and maintaining the current project issues. -Using the system, complete the following steps: - +Complete the following steps: 1. Add a comment to two issues 2. Delete the issue titled "Delete this issue"` diff --git a/cmd/pm/tasks/priorityManagement.go b/cmd/pm/tasks/priorityManagement.go index 338bd6e..73461fb 100644 --- a/cmd/pm/tasks/priorityManagement.go +++ b/cmd/pm/tasks/priorityManagement.go @@ -12,15 +12,13 @@ import ( const priorityManagementDescription = `You are tasked with managing issue priorities. A critical production issue has been reported. - The database is not working properly and users are not able to connect and access their data. You need to rebalance the current sprint priorities: -1. A new issue has appeared in the list that needs urgent attention. - Change the database related issue's priority to 4 (critical). -2. Set the priority of the feature and chore issues in the list to 1 (low). -` +1. A new issue has appeared in the list that needs urgent attention: + - Change the database related issue's priority to 4. +2. Set the priority of the feature and chore issues in the list to 1.` type PriorityManagementTask struct { done bool From cdb3d1dcd9be83f72a85d80ad2e4db9f79a3992f Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 22 Mar 2026 17:12:22 +0100 Subject: [PATCH 67/73] might fix repl bugs --- pkg/repl/repl.go | 61 +++++++++++++++++++++++----------------------- pkg/task/runner.go | 8 ++++++ 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index ce0f3f1..331ebef 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -2,7 +2,6 @@ package repl import ( - "bufio" "context" "fmt" "os" @@ -52,7 +51,7 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { r.currentFeedback = ValidationFeedback{} r.completionChan = make(chan struct{}, 1) - // Save terminal state to restore on exit (go-prompt v0.2.6 doesn't restore properly) + // Save terminal state to restore on exit oldState, err := term.GetState(int(os.Stdin.Fd())) if err == nil { defer term.Restore(int(os.Stdin.Fd()), oldState) @@ -82,12 +81,20 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { go r.watchValidation() } + // Goroutine to inject newline when task completes to wake up blocked prompt + go func() { + for range r.completionChan { + if tty, err := os.OpenFile("/dev/tty", os.O_WRONLY, 0); err == nil { + tty.Write([]byte("\n")) + tty.Close() + } + } + }() + // history keeps track of command history. - // This enables navigating through previous commands. var history []string - // Start the REPL loop, which continues until the user types "exit" or "quit" or task completes. - reader := bufio.NewReader(os.Stdin) + // Start the REPL loop replLoop: for !r.exitRequested { // Check if we should exit before prompting (non-blocking check) @@ -95,27 +102,22 @@ replLoop: break } - // Prompt the user for input, and provide suggestions. - inputChan := make(chan string, 1) - go func(hist []string) { - inputChan <- prompt.Input( - PromptPrefix, - completer, - promptOptions(hist)..., - ) - }(history) + // Check if task completed before showing prompt + if r.taskCompleted { + fmt.Println("\nTask completed successfully!") + break replLoop + } - var input string - select { - case input = <-inputChan: - case <-r.completionChan: - fmt.Println(style.TitleStyle.Render("Task completed successfully!")) - fmt.Print("Press Enter to exit...") - // Restore terminal before reading final input (go-prompt doesn't restore properly) - if oldState != nil { - term.Restore(int(os.Stdin.Fd()), oldState) - } - reader.ReadString('\n') + // Prompt the user for input, and provide suggestions. + input := prompt.Input( + PromptPrefix, + completer, + promptOptions(history)..., + ) + + // Check if task completed while at prompt (will be true if newline was injected) + if r.taskCompleted { + fmt.Println("\nTask completed successfully!") break replLoop } @@ -124,8 +126,7 @@ replLoop: break } - // Check if task completed while waiting at prompt - // Trim whitespace from the input to ensure consistent command processing. + // Trim whitespace from the input input = strings.TrimSpace(input) // If the user types "exit" or "quit", break the loop and exit the REPL. @@ -139,7 +140,7 @@ replLoop: r.logAction("repl command: " + input) } - // Add the input to the history for future navigation. + // Add the input to the history history = append(history, input) output, err := r.execute(input) @@ -153,11 +154,11 @@ replLoop: } if err != nil { - // Show command output (even on error) in normal text style + // Show command output (even on error) if output != "" { fmt.Println(style.TextStyle.Render(output)) } - // Show error message in red if no output was captured + // Show error message in red if no output if output == "" { fmt.Println(style.ErrorStyle.Render(err.Error())) } diff --git a/pkg/task/runner.go b/pkg/task/runner.go index 5aeae15..6529939 100644 --- a/pkg/task/runner.go +++ b/pkg/task/runner.go @@ -4,9 +4,11 @@ import ( "context" "fmt" "log/slog" + "os" "charm.land/bubbletea/v2" "github.com/LazyBachelor/LazyPM/internal/models" + "golang.org/x/term" ) type App = models.App @@ -123,6 +125,12 @@ func (r *TaskRunner) Run(ctx context.Context, t Tasker, i Interface, iType Inter } } + if oldState, err := term.GetState(int(os.Stdin.Fd())); err == nil { + term.Restore(int(os.Stdin.Fd()), oldState) + } + + fmt.Print("\033[0m\033[?25h") + // Questionnaire if err := runQuestionnaire(t, iType, collector); err != nil { return err From 84c73b7b94e24ce396986dddaf2ed07a32ba7ab6 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 22 Mar 2026 18:29:29 +0100 Subject: [PATCH 68/73] upgrade go-prompt to dev branch as it has some bug fixes we need --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dd0e8e8..8fa7360 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/LazyBachelor/LazyPM go 1.26.1 require ( - github.com/c-bata/go-prompt v0.2.6 + github.com/c-bata/go-prompt v0.2.7-0.20250812090649-d000795a4f93 github.com/go-git/go-git/v6 v6.0.0-20260317113930-fb0d09929504 github.com/joho/godotenv v1.5.1 github.com/muesli/reflow v0.3.0 diff --git a/go.sum b/go.sum index 66d3907..0197cb6 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= -github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= -github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= +github.com/c-bata/go-prompt v0.2.7-0.20250812090649-d000795a4f93 h1:RUOY4RbqbKkoUUA7QEzfH7SvdjP4M8hpmRrJXcr7CVw= +github.com/c-bata/go-prompt v0.2.7-0.20250812090649-d000795a4f93/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= From a6d4144d8ca475be2256fb3cc2091f62fb63a42d Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 22 Mar 2026 19:09:15 +0100 Subject: [PATCH 69/73] simplify this --- pkg/repl/repl.go | 50 +++++++++++++++++------------------------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index 331ebef..2c7918e 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -28,11 +28,10 @@ Type 'status' to check task progress.` ) type REPL struct { - feedbackChan chan ValidationFeedback - quitChan chan bool - submitChan chan<- struct{} - completionChan chan struct{} - app *App + feedbackChan chan ValidationFeedback + quitChan chan bool + submitChan chan<- struct{} + app *App currentFeedback ValidationFeedback exitRequested bool @@ -49,7 +48,6 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { r.taskCompleted = false r.exitRequested = false r.currentFeedback = ValidationFeedback{} - r.completionChan = make(chan struct{}, 1) // Save terminal state to restore on exit oldState, err := term.GetState(int(os.Stdin.Fd())) @@ -81,33 +79,23 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { go r.watchValidation() } - // Goroutine to inject newline when task completes to wake up blocked prompt - go func() { - for range r.completionChan { - if tty, err := os.OpenFile("/dev/tty", os.O_WRONLY, 0); err == nil { - tty.Write([]byte("\n")) - tty.Close() - } - } - }() - // history keeps track of command history. var history []string // Start the REPL loop replLoop: - for !r.exitRequested { - // Check if we should exit before prompting (non-blocking check) - if r.exitRequested { - break - } - + for !r.exitRequested || r.taskCompleted { // Check if task completed before showing prompt if r.taskCompleted { - fmt.Println("\nTask completed successfully!") + fmt.Scanln() break replLoop } + // Check if we should exit before prompting + if r.exitRequested { + break + } + // Prompt the user for input, and provide suggestions. input := prompt.Input( PromptPrefix, @@ -117,12 +105,11 @@ replLoop: // Check if task completed while at prompt (will be true if newline was injected) if r.taskCompleted { - fmt.Println("\nTask completed successfully!") break replLoop } // Check again after prompt returns (in case validation completed while waiting) - if r.exitRequested { + if r.exitRequested && !r.taskCompleted { break } @@ -185,16 +172,15 @@ func (r *REPL) watchValidation() { } if feedback.Success { r.taskCompleted = true - if r.completionChan != nil { - select { - case r.completionChan <- struct{}{}: - default: - } - } + // Print completion message immediately + fmt.Fprintf(os.Stderr, "\n\nTASK COMPLETED\n%s\nPress Enter to exit...\n\n", feedback.Message) + os.Stderr.Sync() return } case <-r.quitChan: - r.exitRequested = true + if !r.taskCompleted { + r.exitRequested = true + } return } } From 06404708baa596be9df1c19de61c92e992e206ba Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 23 Mar 2026 10:59:19 +0100 Subject: [PATCH 70/73] rewrite interface description to be more usefull --- cmd/pm/tasks/base.go | 50 ++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/cmd/pm/tasks/base.go b/cmd/pm/tasks/base.go index 93f359f..525284f 100644 --- a/cmd/pm/tasks/base.go +++ b/cmd/pm/tasks/base.go @@ -57,31 +57,45 @@ func BaseDetails(interfaceType InterfaceType) TaskDetails { switch interfaceType { case InterfaceTypeREPL: - interfaceDesc = `What is a REPL Interface? + interfaceDesc = `About our REPL Interface? + - REPL stands for Read-Eval-Print Loop. It is an interactive programming environment that takes single user inputs (reads), executes them (eval), and returns the result to the user (print), then waits for the next input (loop). -- In this task, you will interact with the task through a REPL interface, which allows you to execute commands and receive immediate feedback in a command-line environment. -- You can type commands to perform actions related to the task, and the REPL will process those commands and provide responses based on your input. -- The REPL interface is designed to facilitate a more dynamic and interactive way of completing the task, allowing you to experiment and receive real-time feedback as you work through the task requirements. + +- In a traditional CLI, you would use the help command to see available commands and their descriptions. + In this REPL interface, you can also type 'pm help' to get a list of available commands and their descriptions. + +- You can also run shell commands directly from the REPL, like "git" or "ls". + +- When you type commands in this interface, suggestions will appear as you type, which you can select to auto-complete your command. + - Write "exit" to skip task during the survey.` case InterfaceTypeTUI: - interfaceDesc = `What is a TUI Interface? -- TUI stands for Text User Interface. It is a user interface that uses text-based elements to allow users to interact with the application. + interfaceDesc = `About our TUI Interface? + +- TUI stands for Terminal User Interface. It is a user interface that uses text-based elements to allow users to interact with the application. + - The main way to interact with a TUI is through keyboard inputs, where you can navigate through menus, select options, and input data using the keyboard. -- In this task, you will interact with the task through a TUI interface, which provides a more structured and visually organized way to complete the task using text-based menus, forms, and other interactive elements. -- The TUI interface is designed to enhance usability and provide a more engaging experience while working through the task requirements, allowing you to navigate through options and input information in a more intuitive way. + +- At the bottom of the interface you will find the help menu, which lists available keybinds. + +- The interface has parts: + - List View: This view shows the available issues in a list format, allowing you to browse through them and select one to work on. + - Kanban: This view allowtwos you to manage and track the progress of different of issues. + - Press "q" to quit task during the survey.` case InterfaceTypeWeb: - interfaceDesc = `What is a Web Interface? -- A Web Interface is a user interface that is accessed through a web browser. It allows users to interact with the application using graphical elements such as buttons, forms, and menus. -- In this task, you will interact with the task through a Web interface, which provides a more visually rich and user-friendly way to complete the task using a web-based platform. -- The Web interface is designed to enhance usability and provide a more engaging experience while working through the task requirements, allowing you to navigate through options and input information in a more intuitive way using a graphical interface. + interfaceDesc = `About our Web Interface? + +- A Web Interface is a user interface that is accessed through a web browser. + It allows users to interact with the application using graphical elements such as buttons, forms, and menus. + +- We designed this interface to only be interactive through mouse clicks. + +- The interface has parts: + - Issues: This view shows the available issues in a list format. + - Kanban: This view allows you to manage and track the progress of different issues. + - Press esc/q in the terminal to skip the task.` - case InterfaceTypeCLI: - interfaceDesc = `What is a CLI Interface? -- CLI stands for Command-Line Interface. It is a text-based interface where you interact with the application by typing commands. -- In this task, you will interact with the task through a CLI interface and execute commands directly in the terminal. -- Write "exit" to skip task during the survey.` - default: interfaceDesc = "Unknown Interface" } From d900a5fcd3a27c0c1f51769a7510030966796480 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:26:04 -0700 Subject: [PATCH 71/73] Merge pull request #75 from LazyBachelor/LPM-140 LPM-140 The Sprinting Update Adds sprint support across the web UI (board + issue detail), TUI kanban, CLI/REPL, and the Beads-backed storage layer. Changes: Introduces sprint entities in storage and exposes sprint operations via IssueService. Updates web board to support backlog vs. sprint columns, sprint selection, and sprint creation routes. Updates TUI kanban to show backlog + sprint columns and adds sprint selection/creation actions. --- cmd/pm/tasks/sprintPlanning.go | 72 +++-- internal/commands/issues/completion.go | 2 +- internal/commands/issues/create.go | 4 +- internal/commands/issues/list.go | 2 +- internal/commands/issues/sprint.go | 322 ++++++++++++++++++++ internal/commands/issues/update.go | 2 +- internal/models/app.go | 9 + internal/models/beads.go | 11 +- internal/storage/beads.go | 296 +++++++++++++++++- pkg/repl/suggestions.go | 183 ++++++++++-- pkg/tui/components/helpbar.go | 6 +- pkg/tui/components/issue_list.go | 6 +- pkg/tui/modal/focus.go | 26 +- pkg/tui/modal/manager.go | 7 + pkg/tui/modal/modal.go | 1 + pkg/tui/modal/select.go | 27 +- pkg/tui/views/kanban/keys.go | 16 + pkg/tui/views/kanban/model.go | 97 ++++-- pkg/tui/views/kanban/operations.go | 158 ++++++++-- pkg/tui/views/kanban/view.go | 40 ++- pkg/web/assets/css/styles.css | 2 +- pkg/web/assets/js/board-drag-drop.js | 42 ++- pkg/web/components/issue_detail.templ | 38 +-- pkg/web/components/issue_detail_templ.go | 44 +-- pkg/web/handler/dashboard.go | 88 +++++- pkg/web/handler/issues.go | 55 ++-- pkg/web/routes/boardview.templ | 113 ++++--- pkg/web/routes/boardview_templ.go | 365 ++++++++++++++--------- pkg/web/routes/dashboard.templ | 340 +++++++++++++++++---- pkg/web/routes/dashboard_templ.go | 175 ++++------- pkg/web/routes/issue_detail.templ | 325 ++++++++++++++++---- pkg/web/routes/issue_detail_templ.go | 273 ++++++++--------- pkg/web/server/routes.go | 2 + 33 files changed, 2378 insertions(+), 771 deletions(-) create mode 100644 internal/commands/issues/sprint.go diff --git a/cmd/pm/tasks/sprintPlanning.go b/cmd/pm/tasks/sprintPlanning.go index 7d4c87b..5d0db33 100644 --- a/cmd/pm/tasks/sprintPlanning.go +++ b/cmd/pm/tasks/sprintPlanning.go @@ -2,6 +2,7 @@ package tasks import ( "context" + "fmt" "charm.land/huh/v2" "github.com/LazyBachelor/LazyPM/internal/models" @@ -16,15 +17,16 @@ You will be given a list of issues with different priorities and dependencies. Your task: 1. Review the backlog issues 2. Select which issues to include in the sprint -3. Update issue statuses to move items into the sprint +3. Add issues to the sprint 4. Address any blocked or dependent issues 5. Prioritize high-priority items The goal is to create a realistic sprint plan that delivers value while respecting team capacity.` type SprintPlanningTask struct { - done bool - app *App + done bool + app *App + sprintNum int } func NewSprintPlanningTask(app *App) *SprintPlanningTask { @@ -70,32 +72,38 @@ func (t *SprintPlanningTask) Setup(ctx context.Context) error { return err } + sprintNum, err := t.app.Issues.AddSprint(ctx) + if err != nil { + return err + } + t.sprintNum = sprintNum + backlogIssues := []*models.Issue{ NewIssueBuilder(). WithTitle("Implement user authentication"). WithDescription("Add login/logout functionality. Priority: High"). - WithPriority(1). + WithPriority(4). WithStatus(models.StatusOpen). WithIssueType(models.TypeTask). Build(), NewIssueBuilder(). WithTitle("Design database schema"). WithDescription("Create tables for users and orders. Priority: High"). - WithPriority(1). + WithPriority(4). WithStatus(models.StatusOpen). WithIssueType(models.TypeTask). Build(), NewIssueBuilder(). WithTitle("Setup CI/CD pipeline"). WithDescription("Configure automated testing and deployment. Currently blocked by server setup"). - WithPriority(2). + WithPriority(3). WithStatus(models.StatusBlocked). WithIssueType(models.TypeTask). Build(), NewIssueBuilder(). WithTitle("Create API documentation"). WithDescription("Document all REST endpoints. Priority: Low"). - WithPriority(3). + WithPriority(1). WithStatus(models.StatusOpen). WithIssueType(models.TypeTask). Build(), @@ -114,36 +122,52 @@ func (t *SprintPlanningTask) Setup(ctx context.Context) error { func (t *SprintPlanningTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - issues, err := FetchIssues(ctx, t.app) - if err != nil { - return expect.ValidationFeedback + if t.sprintNum == 0 { + return expect.Fatal("No sprint was created. Create a sprint first.") } - // Sort by priority ascending (0 is highest priority). - sorted := make([]*models.Issue, len(issues)) - copy(sorted, issues) + sprintIssues, err := t.app.Issues.GetIssuesBySprint(ctx, t.sprintNum) + if err != nil { + return expect.Fatal("Could not fetch sprint issues") + } + + allIssues, err := FetchIssues(ctx, t.app) + if err != nil { + return expect.Fatal("Could not fetch issues") + } + + expect.Assert(len(sprintIssues) > 0, fmt.Sprintf("Sprint %d has no issues. Move issues from Backlog to the sprint.", t.sprintNum)) + + sorted := make([]*models.Issue, len(allIssues)) + copy(sorted, allIssues) for i := range sorted { for j := i + 1; j < len(sorted); j++ { - if sorted[j].Priority < sorted[i].Priority { + if sorted[j].Priority > sorted[i].Priority { sorted[i], sorted[j] = sorted[j], sorted[i] } } } - topN := min(len(sorted), 5) - top := sorted[:topN] + top3 := sorted[:min(len(sorted), 3)] + sprintIssueIDs := make(map[string]bool) + for _, issue := range sprintIssues { + sprintIssueIDs[issue.ID] = true + } - var plannedCount int - for _, issue := range top { - if issue.Status == models.StatusReadyToSprint || - issue.Status == models.StatusInProgress || - issue.Status == models.StatusClosed { - plannedCount++ + var topInSprint int + for _, issue := range top3 { + if sprintIssueIDs[issue.ID] { + topInSprint++ } } - expect.Assert(plannedCount >= 3, - "Expected at least 3 of the 5 highest-priority issues to be moved into 'ready_to_sprint', 'in_progress', or 'closed' for the sprint.") + expect.Assert(topInSprint >= 2, + fmt.Sprintf("Only %d of 3 high-priority issues in sprint.\n High priority: \n- %s \n- %s \n- %s", + topInSprint, top3[0].Title, top3[1].Title, top3[2].Title)) + + expect.Assert(len(sprintIssues) >= 3, + fmt.Sprintf("Sprint %d only has %d issues. Add at least %d more from backlog.", + t.sprintNum, len(sprintIssues), 3-len(sprintIssues))) return expect.Complete() } diff --git a/internal/commands/issues/completion.go b/internal/commands/issues/completion.go index 1ab4670..579e6b8 100644 --- a/internal/commands/issues/completion.go +++ b/internal/commands/issues/completion.go @@ -11,7 +11,7 @@ import ( // Variables for completion options and functions. var ( typeOptions = []string{"bug", "feature", "task", "chore"} - statusOptions = []string{"open", "closed", "in_progress", "blocked", "ready_to_sprint"} + statusOptions = []string{"open", "closed", "in_progress", "blocked"} priorityRange = []string{"0", "1", "2", "3", "4"} ) diff --git a/internal/commands/issues/create.go b/internal/commands/issues/create.go index 238e295..56a27da 100644 --- a/internal/commands/issues/create.go +++ b/internal/commands/issues/create.go @@ -26,7 +26,7 @@ var CreateCmd = &cobra.Command{ Example: createCmdExample, Args: cobra.MinimumNArgs(0), - Aliases: []string{"add"}, + Aliases: []string{"add", "new"}, RunE: runCreateCmd, } @@ -112,7 +112,7 @@ func runCreateInteractive() error { func init() { //CreateCmd.Flags().BoolVarP(&createFlags.interactive, "interactive", "i", false, "Create issue interactively") CreateCmd.Flags().StringVarP(&createFlags.description, "desc", "d", "", "Issue description") - CreateCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress, ready_to_sprint)") + CreateCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress)") CreateCmd.Flags().StringVarP(&createFlags.issueType, "type", "t", "task", "Issue type(bug, feature, task)") CreateCmd.Flags().IntVarP(&createFlags.priority, "priority", "p", 0, "Issue priority(0-4)") CreateCmd.Flags().StringVarP(&createFlags.assignee, "assignee", "a", "", "Issue assignee") diff --git a/internal/commands/issues/list.go b/internal/commands/issues/list.go index 46c0167..3f3f1c9 100644 --- a/internal/commands/issues/list.go +++ b/internal/commands/issues/list.go @@ -74,7 +74,7 @@ func runGetIssuesCmd(cmd *cobra.Command, args []string) error { func init() { ListCmd.Flags().StringVar(&listFlags.title, "title", "", "Filter issues by title") ListCmd.Flags().StringVarP(&listFlags.description, "desc", "d", "", "Filter issues by description") - ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress, ready_to_sprint)") + ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress)") ListCmd.Flags().StringVarP(&listFlags.issueType, "type", "t", "", "Filter issues by type (bug, feature, task)") ListCmd.Flags().IntVarP(&listFlags.priority, "priority", "p", 0, "Filter issues by priority (0-4)") ListCmd.Flags().StringVarP(&listFlags.assignee, "assignee", "a", "", "Filter issues by assignee") diff --git a/internal/commands/issues/sprint.go b/internal/commands/issues/sprint.go new file mode 100644 index 0000000..d89a59b --- /dev/null +++ b/internal/commands/issues/sprint.go @@ -0,0 +1,322 @@ +package issues + +import ( + "fmt" + "strconv" + + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/spf13/cobra" +) + +// sprintFlags holds the flag values for sprint commands +type sprintFlags struct { + sprintNum int + issueID string +} + +var sprintCmdFlags sprintFlags + +// SprintCmd represents the sprint management command +var SprintCmd = &cobra.Command{ + Use: "sprint", + Short: "Manage sprints", + Long: `Manage sprints - create, list, add/remove issues, and view sprint contents.`, +} + +// SprintListCmd lists all sprints +var SprintListCmd = &cobra.Command{ + Use: "list", + Short: "List all sprints", + Long: `List all sprints in the project.`, + Aliases: []string{"ls"}, + RunE: runSprintListCmd, +} + +// SprintCreateCmd creates a new sprint +var SprintCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new sprint", + Long: `Create a new sprint for organizing issues.`, + Aliases: []string{"new"}, + RunE: runSprintCreateCmd, +} + +// SprintIssuesCmd lists issues in a sprint +var SprintIssuesCmd = &cobra.Command{ + Use: "issues [sprint-num]", + Short: "List issues in a sprint", + Long: `List all issues assigned to a specific sprint. Use 'backlog' or omit to view the backlog.`, + Aliases: []string{"show", "view"}, + Args: cobra.MaximumNArgs(1), + RunE: runSprintIssuesCmd, +} + +// SprintAddCmd adds an issue to a sprint +var SprintAddCmd = &cobra.Command{ + Use: "add [issue-id] [sprint-num]", + Short: "Add an issue to a sprint", + Long: `Add an issue to a sprint. If sprint-num is omitted, adds to backlog.`, + Args: cobra.RangeArgs(1, 2), + RunE: runSprintAddCmd, + + ValidArgsFunction: completeIssues, +} + +// SprintRemoveCmd removes an issue from a sprint +var SprintRemoveCmd = &cobra.Command{ + Use: "remove [issue-id] [sprint-num]", + Short: "Remove an issue from a sprint", + Long: `Remove an issue from a sprint. If sprint-num is omitted, removes from backlog.`, + Aliases: []string{"rm"}, + Args: cobra.RangeArgs(1, 2), + RunE: runSprintRemoveCmd, + + ValidArgsFunction: completeIssues, +} + +// SprintBacklogCmd shows the backlog sprint +var SprintBacklogCmd = &cobra.Command{ + Use: "backlog", + Short: "Show backlog issues", + Long: `Show all issues in the backlog sprint.`, + RunE: runSprintBacklogCmd, +} + +// SprintDeleteCmd deletes a sprint +var SprintDeleteCmd = &cobra.Command{ + Use: "delete [sprint-num]", + Short: "Delete a sprint", + Long: `Delete a sprint by its number. Issues in the sprint will not be deleted.`, + Aliases: []string{"del"}, + Args: cobra.MaximumNArgs(1), + RunE: runSprintDeleteCmd, +} + +func runSprintListCmd(cmd *cobra.Command, args []string) error { + app := AppFromContext(cmd.Context()) + + sprints, err := app.Issues.GetSprints(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to get sprints: %w", err) + } + + if len(sprints) == 0 { + cmd.Println("No sprints found.") + return nil + } + + backlogNum, _ := app.Issues.GetBacklogSprint(cmd.Context()) + + cmd.Println("Sprints:") + for _, sprintNum := range sprints { + label := "" + if sprintNum == backlogNum { + label = " (backlog)" + } + cmd.Printf(" Sprint %d%s\n", sprintNum, label) + } + + return nil +} + +func runSprintCreateCmd(cmd *cobra.Command, args []string) error { + app := AppFromContext(cmd.Context()) + + sprintNum, err := app.Issues.AddSprint(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to create sprint: %w", err) + } + + cmd.Printf("Created sprint %d\n", sprintNum) + return nil +} + +func runSprintIssuesCmd(cmd *cobra.Command, args []string) error { + app := AppFromContext(cmd.Context()) + ctx := cmd.Context() + + var sprintNum int + var isBacklog bool + var err error + + if len(args) == 0 || args[0] == "backlog" { + sprintNum, err = app.Issues.GetBacklogSprint(ctx) + if err != nil { + return fmt.Errorf("failed to get backlog sprint: %w", err) + } + isBacklog = true + } else { + sprintNum, err = strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid sprint number: %s", args[0]) + } + isBacklog = false + } + + var issues []*models.Issue + if isBacklog { + issues, err = app.Issues.GetIssuesNotInAnySprint(ctx) + } else { + issues, err = app.Issues.GetIssuesBySprint(ctx, sprintNum) + } + if err != nil { + return fmt.Errorf("failed to get issues in sprint %d: %w", sprintNum, err) + } + + if isBacklog { + cmd.Printf("Backlog (%d issues):\n", len(issues)) + } else { + cmd.Printf("Sprint %d (%d issues):\n", sprintNum, len(issues)) + } + + if len(issues) == 0 { + cmd.Println(" No issues in this sprint.") + return nil + } + + issuesList := models.IssuesPtrToIssues(issues) + models.PrintIssues(issuesList) + + return nil +} + +func runSprintAddCmd(cmd *cobra.Command, args []string) error { + app := AppFromContext(cmd.Context()) + ctx := cmd.Context() + + issueID := args[0] + + var sprintNum int + var err error + + if len(args) == 1 { + sprintNum, err = app.Issues.GetBacklogSprint(ctx) + if err != nil { + return fmt.Errorf("failed to get backlog sprint: %w", err) + } + } else { + sprintNum, err = strconv.Atoi(args[1]) + if err != nil { + return fmt.Errorf("invalid sprint number: %s", args[1]) + } + } + + err = app.Issues.AddIssueToSprint(ctx, issueID, sprintNum) + if err != nil { + return fmt.Errorf("failed to add issue to sprint: %w", err) + } + + backlogNum, _ := app.Issues.GetBacklogSprint(ctx) + + if sprintNum == backlogNum { + cmd.Printf("Added issue %s to backlog\n", issueID) + } else { + cmd.Printf("Added issue %s to sprint %d\n", issueID, sprintNum) + } + + return nil +} + +func runSprintRemoveCmd(cmd *cobra.Command, args []string) error { + app := AppFromContext(cmd.Context()) + ctx := cmd.Context() + + issueID := args[0] + + var sprintNum int + var err error + + if len(args) == 1 { + sprintNum, err = app.Issues.GetBacklogSprint(ctx) + if err != nil { + return fmt.Errorf("failed to get backlog sprint: %w", err) + } + } else { + sprintNum, err = strconv.Atoi(args[1]) + if err != nil { + return fmt.Errorf("invalid sprint number: %s", args[1]) + } + } + + err = app.Issues.RemoveIssueFromSprint(ctx, issueID, sprintNum) + if err != nil { + return fmt.Errorf("failed to remove issue from sprint: %w", err) + } + + backlogNum, _ := app.Issues.GetBacklogSprint(ctx) + + if sprintNum == backlogNum { + cmd.Printf("Removed issue %s from backlog\n", issueID) + } else { + cmd.Printf("Removed issue %s from sprint %d\n", issueID, sprintNum) + } + + return nil +} + +func runSprintBacklogCmd(cmd *cobra.Command, args []string) error { + app := AppFromContext(cmd.Context()) + ctx := cmd.Context() + + issues, err := app.Issues.GetIssuesNotInAnySprint(ctx) + if err != nil { + return fmt.Errorf("failed to get backlog issues: %w", err) + } + + cmd.Printf("Backlog (%d issues):\n", len(issues)) + + if len(issues) == 0 { + cmd.Println(" No issues in backlog.") + return nil + } + + issuesList := models.IssuesPtrToIssues(issues) + models.PrintIssues(issuesList) + + return nil +} + +func runSprintDeleteCmd(cmd *cobra.Command, args []string) error { + app := AppFromContext(cmd.Context()) + ctx := cmd.Context() + + var sprintNum int + var err error + + if len(args) == 0 { + sprintNum, err = app.Issues.GetBacklogSprint(ctx) + if err != nil { + return fmt.Errorf("failed to get backlog sprint: %w", err) + } + } else { + sprintNum, err = strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("invalid sprint number: %s", args[0]) + } + } + + backlogNum, _ := app.Issues.GetBacklogSprint(ctx) + if sprintNum == backlogNum { + return fmt.Errorf("cannot delete the backlog sprint") + } + + err = app.Issues.RemoveSprint(ctx, sprintNum) + if err != nil { + return fmt.Errorf("failed to delete sprint: %w", err) + } + + cmd.Printf("Deleted sprint %d\n", sprintNum) + return nil +} + +func init() { + SprintCmd.AddCommand(SprintListCmd) + SprintCmd.AddCommand(SprintBacklogCmd) + SprintCmd.AddCommand(SprintCreateCmd) + SprintCmd.AddCommand(SprintIssuesCmd) + SprintCmd.AddCommand(SprintAddCmd) + SprintCmd.AddCommand(SprintRemoveCmd) + SprintCmd.AddCommand(SprintDeleteCmd) + + RootCmd.AddCommand(SprintCmd) +} diff --git a/internal/commands/issues/update.go b/internal/commands/issues/update.go index 3c2bb7a..791c7c0 100644 --- a/internal/commands/issues/update.go +++ b/internal/commands/issues/update.go @@ -66,7 +66,7 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { func init() { UpdateCmd.Flags().StringVar(&updateFlags.title, "title", "", "New issue title") UpdateCmd.Flags().StringVarP(&updateFlags.description, "desc", "d", "", "New issue description") - UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress, ready_to_sprint)") + UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress)") UpdateCmd.Flags().StringVarP(&updateFlags.issueType, "type", "t", "", "New issue type(bug, feature, task)") UpdateCmd.Flags().IntVarP(&updateFlags.priority, "priority", "p", 0, "New issue priority(0-4)") UpdateCmd.Flags().StringVarP(&updateFlags.assignee, "assignee", "a", "", "New issue assignee") diff --git a/internal/models/app.go b/internal/models/app.go index 3736d60..5c2db42 100644 --- a/internal/models/app.go +++ b/internal/models/app.go @@ -53,6 +53,15 @@ type IssueService interface { AddIssueComment(ctx context.Context, issueID, author, text string) (*Comment, error) GetIssueComments(ctx context.Context, issueID string) ([]*Comment, error) GetCommentCounts(ctx context.Context, issueIDs []string) (map[string]int, error) + + AddSprint(ctx context.Context) (int, error) + RemoveSprint(ctx context.Context, sprintNum int) error + GetSprints(ctx context.Context) ([]int, error) + GetBacklogSprint(ctx context.Context) (int, error) + GetIssuesBySprint(ctx context.Context, sprintNum int) ([]*Issue, error) + GetIssuesNotInAnySprint(ctx context.Context) ([]*Issue, error) + AddIssueToSprint(ctx context.Context, issueID string, sprintNum int) error + RemoveIssueFromSprint(ctx context.Context, issueID string, sprintNum int) error } type StatsService interface { diff --git a/internal/models/beads.go b/internal/models/beads.go index 624ed6d..6144371 100644 --- a/internal/models/beads.go +++ b/internal/models/beads.go @@ -35,12 +35,11 @@ type ( // Status constants const ( - StatusOpen = beads.StatusOpen - StatusInProgress = beads.StatusInProgress - StatusBlocked = beads.StatusBlocked - StatusDeferred = beads.StatusDeferred - StatusClosed = beads.StatusClosed - StatusReadyToSprint Status = "ready_to_sprint" + StatusOpen = beads.StatusOpen + StatusInProgress = beads.StatusInProgress + StatusBlocked = beads.StatusBlocked + StatusDeferred = beads.StatusDeferred + StatusClosed = beads.StatusClosed ) // IssueType constants diff --git a/internal/storage/beads.go b/internal/storage/beads.go index 294da1f..96d39ed 100644 --- a/internal/storage/beads.go +++ b/internal/storage/beads.go @@ -2,7 +2,10 @@ package storage import ( "context" + "database/sql" + "encoding/json" "fmt" + "slices" "github.com/LazyBachelor/LazyPM/internal/models" @@ -21,13 +24,71 @@ func NewBeadsIssueStorage(ctx context.Context, storage beads.Storage, prefix str } } - storage.SetConfig(ctx, "status.custom", "ready_to_sprint") + storage.UnderlyingDB().Exec(` + CREATE TABLE IF NOT EXISTS sprints ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + issues TEXT, + sprint_num INTEGER UNIQUE, + is_backlog BOOLEAN DEFAULT 0 + ); + `) + + backlogNum, err := getBacklogSprintNum(storage) + if err != nil { + _, err = storage.UnderlyingDB().Exec( + "INSERT INTO sprints (name, issues, sprint_num, is_backlog) VALUES (?, ?, 0, 1)", + "backlog", "[]", + ) + if err != nil { + return nil, fmt.Errorf("failed to create backlog sprint: %w", err) + } + storage.SetConfig(ctx, "backlog_sprint", "0") + } else { + storage.SetConfig(ctx, "backlog_sprint", fmt.Sprintf("%d", backlogNum)) + } return &BeadsService{ Storage: storage, }, nil } +func (s *BeadsService) CreateIssue(ctx context.Context, issue *models.Issue, actor string) error { + if err := s.Storage.CreateIssue(ctx, issue, actor); err != nil { + return err + } + + backlogNum, err := s.GetBacklogSprint(ctx) + if err != nil { + return err + } + + if err := s.AddIssueToSprint(ctx, issue.ID, backlogNum); err != nil { + return err + } + + return nil +} + +func (s *BeadsService) CreateIssues(ctx context.Context, issues []*models.Issue, actor string) error { + if err := s.Storage.CreateIssues(ctx, issues, actor); err != nil { + return err + } + + backlogNum, err := s.GetBacklogSprint(ctx) + if err != nil { + return nil + } + + for _, issue := range issues { + if err := s.AddIssueToSprint(ctx, issue.ID, backlogNum); err != nil { + continue + } + } + + return nil +} + func (s *BeadsService) AllIssues(ctx context.Context) ([]models.Issue, error) { issuesPtr, err := s.Storage.SearchIssues(ctx, "", models.IssueFilter{}) if err != nil { @@ -45,10 +106,241 @@ func (s *BeadsService) AllIssues(ctx context.Context) ([]models.Issue, error) { func (s *BeadsService) DeleteIssues() error { - var deleteIssues = "DELETE FROM issues;" + var deleteIssues = `DELETE FROM issues; + DELETE FROM sprints;` if _, err := s.UnderlyingDB().Exec(deleteIssues); err != nil { return err } return nil } + +func (s *BeadsService) AddSprint(ctx context.Context) (int, error) { + var addSprint = "INSERT INTO sprints (sprint_num, issues) VALUES ((SELECT IFNULL(MAX(sprint_num), 0) + 1 FROM sprints), '[]');" + + r, err := s.UnderlyingDB().Exec(addSprint) + if err != nil { + return 0, fmt.Errorf("failed to add sprint: %w", err) + } + + id, err := r.LastInsertId() + if err != nil { + return 0, fmt.Errorf("failed to get last insert id: %w", err) + } + + var sprintNum int + err = s.UnderlyingDB().QueryRow("SELECT sprint_num FROM sprints WHERE id = ?", id).Scan(&sprintNum) + if err != nil { + return 0, fmt.Errorf("failed to get sprint_num: %w", err) + } + + return sprintNum, nil +} + +func (s *BeadsService) RemoveSprint(ctx context.Context, sprintNum int) error { + result, err := s.UnderlyingDB().Exec("DELETE FROM sprints WHERE sprint_num = ?", sprintNum) + if err != nil { + return fmt.Errorf("failed to remove sprint: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to check rows affected: %w", err) + } + + if rowsAffected == 0 { + return fmt.Errorf("sprint %d not found", sprintNum) + } + + return nil +} + +func (s *BeadsService) GetSprints(ctx context.Context) ([]int, error) { + rows, err := s.UnderlyingDB().Query("SELECT sprint_num FROM sprints WHERE is_backlog = 0 ORDER BY sprint_num") + if err != nil { + return nil, fmt.Errorf("failed to get sprints: %w", err) + } + defer rows.Close() + + var sprints []int + for rows.Next() { + var sprintNum int + if err := rows.Scan(&sprintNum); err != nil { + return nil, fmt.Errorf("failed to scan sprint: %w", err) + } + sprints = append(sprints, sprintNum) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating sprints: %w", err) + } + + return sprints, nil +} + +func (s *BeadsService) GetIssuesBySprint(ctx context.Context, sprintNum int) ([]*models.Issue, error) { + var issuesJSON string + err := s.UnderlyingDB().QueryRow("SELECT issues FROM sprints WHERE sprint_num = ?", sprintNum).Scan(&issuesJSON) + if err != nil { + if err == sql.ErrNoRows { + return []*models.Issue{}, nil + } + return nil, fmt.Errorf("failed to get sprint issues: %w", err) + } + + var issueIDs []string + if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil { + return nil, fmt.Errorf("failed to unmarshal issues: %w", err) + } + + if len(issueIDs) == 0 { + return []*models.Issue{}, nil + } + + var issues []*models.Issue + for _, id := range issueIDs { + issue, err := s.Storage.GetIssue(ctx, id) + if err != nil { + // Skip issues that don't exist or can't be retrieved + continue + } + issues = append(issues, issue) + } + + return issues, nil +} + +func (s *BeadsService) AddIssueToSprint(ctx context.Context, issueID string, sprintNum int) error { + var issuesJSON string + err := s.UnderlyingDB().QueryRow("SELECT issues FROM sprints WHERE sprint_num = ?", sprintNum).Scan(&issuesJSON) + if err != nil { + if err == sql.ErrNoRows { + return fmt.Errorf("sprint %d not found", sprintNum) + } + return fmt.Errorf("failed to get sprint: %w", err) + } + + var issueIDs []string + if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil { + return fmt.Errorf("failed to unmarshal issues: %w", err) + } + + if slices.Contains(issueIDs, issueID) { + return nil + } + + issueIDs = append(issueIDs, issueID) + + updatedJSON, err := json.Marshal(issueIDs) + if err != nil { + return fmt.Errorf("failed to marshal issues: %w", err) + } + + _, err = s.UnderlyingDB().Exec("UPDATE sprints SET issues = ? WHERE sprint_num = ?", string(updatedJSON), sprintNum) + if err != nil { + return fmt.Errorf("failed to update sprint: %w", err) + } + + return nil +} + +func (s *BeadsService) RemoveIssueFromSprint(ctx context.Context, issueID string, sprintNum int) error { + var issuesJSON string + err := s.UnderlyingDB().QueryRow("SELECT issues FROM sprints WHERE sprint_num = ?", sprintNum).Scan(&issuesJSON) + if err != nil { + if err == sql.ErrNoRows { + return fmt.Errorf("sprint %d not found", sprintNum) + } + return fmt.Errorf("failed to get sprint: %w", err) + } + + var issueIDs []string + if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil { + return fmt.Errorf("failed to unmarshal issues: %w", err) + } + + found := false + var updatedIDs []string + for _, id := range issueIDs { + if id != issueID { + updatedIDs = append(updatedIDs, id) + } else { + found = true + } + } + + if !found { + return nil + } + + updatedJSON, err := json.Marshal(updatedIDs) + if err != nil { + return fmt.Errorf("failed to marshal issues: %w", err) + } + + _, err = s.UnderlyingDB().Exec("UPDATE sprints SET issues = ? WHERE sprint_num = ?", string(updatedJSON), sprintNum) + if err != nil { + return fmt.Errorf("failed to update sprint: %w", err) + } + + return nil +} + +func (s *BeadsService) GetBacklogSprint(ctx context.Context) (int, error) { + sprintNum, err := getBacklogSprintNum(s.Storage) + if err != nil { + if err == sql.ErrNoRows { + return 0, fmt.Errorf("backlog sprint not found") + } + return 0, fmt.Errorf("failed to get backlog sprint: %w", err) + } + return sprintNum, nil +} + +// GetIssuesNotInAnySprint returns issues that are only in the backlog +func (s *BeadsService) GetIssuesNotInAnySprint(ctx context.Context) ([]*models.Issue, error) { + allIssues, err := s.Storage.SearchIssues(ctx, "", models.IssueFilter{}) + if err != nil { + return nil, fmt.Errorf("failed to get all issues: %w", err) + } + + rows, err := s.UnderlyingDB().Query("SELECT sprint_num, issues FROM sprints WHERE is_backlog = 0") + if err != nil { + return nil, fmt.Errorf("failed to get sprint issues: %w", err) + } + defer rows.Close() + + issuesInSprints := make(map[string]bool) + for rows.Next() { + var sprintNum int + var issuesJSON string + if err := rows.Scan(&sprintNum, &issuesJSON); err != nil { + continue + } + var issueIDs []string + if err := json.Unmarshal([]byte(issuesJSON), &issueIDs); err != nil { + continue + } + for _, id := range issueIDs { + issuesInSprints[id] = true + } + } + + var backlogIssues []*models.Issue + for _, issue := range allIssues { + if !issuesInSprints[issue.ID] { + backlogIssues = append(backlogIssues, issue) + } + } + + return backlogIssues, nil +} + +func getBacklogSprintNum(storage beads.Storage) (int, error) { + var sprintNum int + err := storage.UnderlyingDB().QueryRow("SELECT sprint_num FROM sprints WHERE is_backlog = 1 LIMIT 1").Scan(&sprintNum) + if err != nil { + return 0, err + } + return sprintNum, nil +} diff --git a/pkg/repl/suggestions.go b/pkg/repl/suggestions.go index 4824a66..4715312 100644 --- a/pkg/repl/suggestions.go +++ b/pkg/repl/suggestions.go @@ -22,44 +22,74 @@ var rootSuggestions = []prompt.Suggest{ // commandSuggestions is a list of prompt suggestions for PM commands. var baseSuggestions = []prompt.Suggest{ {Text: "help", Description: "Show help information"}, + {Text: "sprint", Description: "Manage sprints"}, {Text: "create", Description: "Create a new issue with title"}, - {Text: "describe", Description: "Get issue details by ID"}, {Text: "list", Description: "List all issues"}, - {Text: "close", Description: "Close an issue by ID"}, + {Text: "read", Description: "Read issue details by ID"}, {Text: "update", Description: "Update an existing issue by ID"}, + {Text: "close", Description: "Close an issue by ID"}, {Text: "delete", Description: "Delete an issue by ID"}, {Text: "comment", Description: "Add a comment on an issue by ID"}, {Text: "comments", Description: "List comments on an issue by ID"}, + + {Text: "new", Description: "Alias for create command"}, + {Text: "ls", Description: "Alias for list command"}, + {Text: "search", Description: "Alias for list command"}, + {Text: "describe", Description: "Alias for read command"}, + {Text: "details", Description: "Alias for read command"}, + {Text: "edit", Description: "Alias for update command"}, + {Text: "rm", Description: "Alias for delete command"}, + {Text: "del", Description: "Alias for delete command"}, + {Text: "get", Description: "Alias for read command"}, } // createFlags is a list of prompt suggestions for the create command flags. var createFlags = []prompt.Suggest{ {Text: "--desc", Description: "Issue description"}, - {Text: "--status", Description: "Issue status (open, closed, in_progress)"}, - {Text: "--type", Description: "Issue type (bug, feature, task)"}, - {Text: "--priority", Description: "Issue priority (0-4)"}, + {Text: "--status", Description: "Issue status"}, + {Text: "--type", Description: "Issue type"}, + {Text: "--priority", Description: "Issue priority"}, {Text: "--assignee", Description: "Issue assignee"}, + + {Text: "-d", Description: "Issue description (short)"}, + {Text: "-s", Description: "Issue status (short)"}, + {Text: "-t", Description: "Issue type (short)"}, + {Text: "-p", Description: "Issue priority (short)"}, + {Text: "-a", Description: "Issue assignee (short)"}, } // updateFlags is a list of prompt suggestions for the update command flags. var updateFlags = []prompt.Suggest{ {Text: "--title", Description: "New issue title"}, {Text: "--desc", Description: "New issue description"}, - {Text: "--status", Description: "New issue status (open, closed, in_progress)"}, - {Text: "--type", Description: "New issue type (bug, feature, task)"}, - {Text: "--priority", Description: "New issue priority (0-4)"}, + {Text: "--status", Description: "New issue status"}, + {Text: "--type", Description: "New issue type"}, + {Text: "--priority", Description: "New issue priority"}, {Text: "--assignee", Description: "New issue assignee"}, + + {Text: "-t", Description: "New issue type (short)"}, + {Text: "-p", Description: "New issue priority (short)"}, + {Text: "-a", Description: "New issue assignee (short)"}, + {Text: "-d", Description: "New issue description (short)"}, + {Text: "-s", Description: "New issue status (short)"}, } // listFlags is a list of prompt suggestions for the list command flags. var listFlags = []prompt.Suggest{ {Text: "--title", Description: "Filter by title"}, {Text: "--desc", Description: "Filter by description"}, - {Text: "--status", Description: "Filter by status (open, closed, in_progress)"}, - {Text: "--type", Description: "Filter by type (bug, feature, task)"}, - {Text: "--priority", Description: "Filter by priority (0-4)"}, + {Text: "--status", Description: "Filter by status"}, + {Text: "--type", Description: "Filter by type"}, + {Text: "--priority", Description: "Filter by priority"}, {Text: "--assignee", Description: "Filter by assignee"}, {Text: "--limit", Description: "Limit number of results"}, + + {Text: "-s", Description: "Filter by status (short)"}, + {Text: "-t", Description: "Filter by type (short)"}, + {Text: "-p", Description: "Filter by priority (short)"}, + {Text: "-a", Description: "Filter by assignee (short)"}, + {Text: "-d", Description: "Filter by description (short)"}, + {Text: "-l", Description: "Limit number of results (short)"}, } var deleteFlags = []prompt.Suggest{ @@ -68,19 +98,37 @@ var deleteFlags = []prompt.Suggest{ } var commentFlags = []prompt.Suggest{ - {Text: "--message", Description: "Comment text (alternative to positional args)"}, - {Text: "-m", Description: "Comment text (short)"}, + {Text: "--message", Description: "Comment text"}, {Text: "--author", Description: "Author name for the comment"}, + + {Text: "-m", Description: "Comment text (short)"}, {Text: "-a", Description: "Author name (short)"}, } +// sprintSubcommands is a list of prompt suggestions for sprint subcommands. +var sprintSubcommands = []prompt.Suggest{ + {Text: "list", Description: "List all sprints"}, + {Text: "create", Description: "Create a new sprint"}, + {Text: "issues", Description: "List issues in a sprint"}, + {Text: "add", Description: "Add an issue to a sprint"}, + {Text: "remove", Description: "Remove an issue from a sprint"}, + {Text: "backlog", Description: "Show backlog issues"}, + {Text: "delete", Description: "Delete a sprint"}, + + {Text: "ls", Description: "Alias for list subcommand"}, + {Text: "new", Description: "Alias for create subcommand"}, + {Text: "show", Description: "Alias for issues subcommand"}, + {Text: "view", Description: "Alias for issues subcommand"}, + {Text: "rm", Description: "Alias for remove subcommand"}, + {Text: "del", Description: "Alias for delete subcommand"}, +} + // statusValues is a list of prompt suggestions for status types var statusValues = []prompt.Suggest{ {Text: "open", Description: "Open status"}, {Text: "closed", Description: "Closed status"}, {Text: "in_progress", Description: "In progress status"}, {Text: "blocked", Description: "Blocked status"}, - {Text: "ready_to_sprint", Description: "Ready to sprint status"}, } // typeValues is a list of prompt suggestions for issue types @@ -102,18 +150,20 @@ var priorityValues = []prompt.Suggest{ // isIDCommand maps command names to a boolean indicating whether they expect an issue ID as an argument. var isIDCommand = map[string]bool{ - "describe": true, - "delete": true, - "del": true, - "rm": true, - "remove": true, - "get": true, - "read": true, - "close": true, - "update": true, - "edit": true, - "comment": true, - "comments": true, + "describe": true, + "delete": true, + "del": true, + "rm": true, + "remove": true, + "get": true, + "read": true, + "close": true, + "update": true, + "edit": true, + "comment": true, + "comments": true, + "sprint-add": true, + "sprint-remove": true, } var commandFlags = map[string][]prompt.Suggest{ @@ -130,6 +180,7 @@ var commandFlags = map[string][]prompt.Suggest{ "remove": deleteFlags, "comment": commentFlags, "comments": nil, // no flags, just issue ID + "sprint": sprintSubcommands, } // commandSuggestions returns a list of prompt suggestions based on the current input words. @@ -148,12 +199,70 @@ func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest { return filterByPrefix(values, lastWord) } + if cmd == "sprint" { + switch len(words) { + case 1: + if strings.HasSuffix(text, " ") { + return sprintSubcommands + } + return nil + case 2: + subcommand := words[1] + if !strings.HasSuffix(text, " ") { + return filterByPrefix(sprintSubcommands, lastWord) + } + + switch subcommand { + case "list", "ls", "create", "new", "backlog": + return nil + case "issues", "show", "view", "delete", "del": + return sprintNumSuggestions("", subcommand != "delete" && subcommand != "del") + case "add", "remove", "rm": + return issueIDSuggestions("", true) + default: + return filterByPrefix(sprintSubcommands, subcommand) + } + case 3: + subcommand := words[1] + + switch subcommand { + case "issues", "show", "view", "delete", "del": + if !strings.HasSuffix(text, " ") { + return sprintNumSuggestions(lastWord, subcommand != "delete" && subcommand != "del") + } + return nil + case "add", "remove", "rm": + if !strings.HasSuffix(text, " ") { + return issueIDSuggestions(lastWord, true) + } + return sprintNumSuggestions("", true) + } + case 4: + subcommand := words[1] + + switch subcommand { + case "add", "remove", "rm": + if !strings.HasSuffix(text, " ") { + return sprintNumSuggestions(lastWord, true) + } + } + } + return nil + } + flags := commandFlags[cmd] if isIDCommand[cmd] { - if len(words) < 2 && !strings.HasPrefix(lastWord, "-") { - return issueIDSuggestions(lastWord, true) + if len(words) == 1 && strings.HasSuffix(text, " ") { + return issueIDSuggestions("", true) } + + if len(words) == 2 && + !strings.HasSuffix(text, " ") && + !strings.HasPrefix(words[1], "-") { + return issueIDSuggestions(words[1], true) + } + return filterByPrefix(flags, lastWord) } @@ -179,6 +288,24 @@ func issueIDSuggestions(partial string, hasCommand bool) []prompt.Suggest { return suggestions } +// sprintNumSuggestions returns a list of prompt suggestions for sprint numbers. +func sprintNumSuggestions(partial string, includeBacklog bool) []prompt.Suggest { + suggestions := []prompt.Suggest{ + {Text: "0", Description: "Backlog"}, + {Text: "1", Description: "Sprint 1"}, + {Text: "2", Description: "Sprint 2"}, + {Text: "3", Description: "Sprint 3"}, + {Text: "4", Description: "Sprint 4"}, + {Text: "5", Description: "Sprint 5"}, + } + + if !includeBacklog { + suggestions = suggestions[1:] + } + + return filterByPrefix(suggestions, partial) +} + // parseWords extracts the last and previous words from the input for flag suggestion logic. func parseWords(words []string, text string) (lastWord, prevWord string) { if len(words) > 0 && !strings.HasSuffix(text, " ") { diff --git a/pkg/tui/components/helpbar.go b/pkg/tui/components/helpbar.go index 004b237..6ae9969 100644 --- a/pkg/tui/components/helpbar.go +++ b/pkg/tui/components/helpbar.go @@ -211,11 +211,13 @@ func helpBarConfig(view ViewKind) HelpBarConfig { {Key: "↑/k", Desc: "up"}, {Key: "↓/j", Desc: "down"}, {Key: "/", Desc: "search"}, + {Key: "n", Desc: "new sprint"}, + {Key: "S", Desc: "select sprint"}, {Key: "pgup/pgdn", Desc: "page"}, {Key: "h/l", Desc: "column"}, {Key: "←/→", Desc: "move"}, {Key: "a", Desc: "add"}, - {Key: "e/d/s/p/t/A", Desc: "edit"}, + {Key: "e/d/s/p/t/A/", Desc: "edit"}, {Key: "x", Desc: "delete"}, {Key: "q", Desc: "quit"}, {Key: "?", Desc: "help"}, @@ -237,6 +239,8 @@ func helpBarConfig(view ViewKind) HelpBarConfig { {Key: "p", Desc: "change priority"}, {Key: "t", Desc: "change type"}, {Key: "A", Desc: "change assignee"}, + {Key: "S", Desc: "select sprint"}, + {Key: "n", Desc: "new sprint"}, {Key: "q", Desc: "quit"}, {Key: "?", Desc: "help"}, }, diff --git a/pkg/tui/components/issue_list.go b/pkg/tui/components/issue_list.go index e306b4f..0b3d0f9 100644 --- a/pkg/tui/components/issue_list.go +++ b/pkg/tui/components/issue_list.go @@ -247,8 +247,10 @@ func SortedIssues(issues []*models.Issue) []*models.Issue { func StatusOnly(issues []*models.Issue, status models.Status) []*models.Issue { out := make([]*models.Issue, 0, len(issues)) for _, issue := range issues { - if issue.Status == status || - (status == models.StatusOpen && issue.Status == models.StatusReadyToSprint) { + if issue == nil { + continue + } + if issue.Status == status { out = append(out, issue) } } diff --git a/pkg/tui/modal/focus.go b/pkg/tui/modal/focus.go index bb35e1f..5c131ba 100644 --- a/pkg/tui/modal/focus.go +++ b/pkg/tui/modal/focus.go @@ -7,6 +7,7 @@ const ( FocusNone FocusArea = iota FocusList FocusDetail + FocusColumn0 // Backlog FocusColumn1 FocusColumn2 FocusColumn3 @@ -46,12 +47,25 @@ func (f *FocusManager) IsFocused(area FocusArea) bool { // IsListFocused returns true if any list area is focused func (f *FocusManager) IsListFocused() bool { return f.currentArea == FocusList || + f.currentArea == FocusColumn0 || f.currentArea == FocusColumn1 || f.currentArea == FocusColumn2 || f.currentArea == FocusColumn3 || f.currentArea == FocusColumn4 } +// NextColumn moves focus to the next column (for kanban) +func (f *FocusManager) NextColumn() { + areas := []FocusArea{FocusColumn0, FocusColumn1, FocusColumn2, FocusColumn3, FocusColumn4} + f.cycleFocus(areas) +} + +// PreviousColumn moves focus to the previous column (for kanban) +func (f *FocusManager) PreviousColumn() { + areas := []FocusArea{FocusColumn4, FocusColumn3, FocusColumn2, FocusColumn1, FocusColumn0} + f.cycleFocus(areas) +} + // IsDetailFocused returns true if the detail area is focused func (f *FocusManager) IsDetailFocused() bool { return f.currentArea == FocusDetail @@ -87,18 +101,6 @@ func (f *FocusManager) Previous() { f.cycleFocus(areas) } -// NextColumn moves focus to the next column (for kanban) -func (f *FocusManager) NextColumn() { - areas := []FocusArea{FocusColumn1, FocusColumn2, FocusColumn3, FocusColumn4} - f.cycleFocus(areas) -} - -// PreviousColumn moves focus to the previous column (for kanban) -func (f *FocusManager) PreviousColumn() { - areas := []FocusArea{FocusColumn4, FocusColumn3, FocusColumn2, FocusColumn1} - f.cycleFocus(areas) -} - // cycleFocus finds the next enabled area in the given order func (f *FocusManager) cycleFocus(areas []FocusArea) { // Find current position diff --git a/pkg/tui/modal/manager.go b/pkg/tui/modal/manager.go index f166d10..b9b656a 100644 --- a/pkg/tui/modal/manager.go +++ b/pkg/tui/modal/manager.go @@ -262,4 +262,11 @@ func RegisterCommonModals(m *Manager) { Label: "Change type:", Options: TypeOptions(), })) + + // Sprint Select Modal + m.RegisterModal(NewSelectModal(SelectConfig{ + ID: ModalSelectSprint, + Label: "Select sprint:", + Options: []SelectOption{}, + })) } diff --git a/pkg/tui/modal/modal.go b/pkg/tui/modal/modal.go index 151fc85..ccf9765 100644 --- a/pkg/tui/modal/modal.go +++ b/pkg/tui/modal/modal.go @@ -62,6 +62,7 @@ const ( ModalSelectCloseReason = "select-close-reason" ModalSelectPriority = "select-priority" ModalSelectType = "select-type" + ModalSelectSprint = "select-sprint" ) // ModalStack manages a stack of active modals with priority handling diff --git a/pkg/tui/modal/select.go b/pkg/tui/modal/select.go index 7ad2e6a..0039812 100644 --- a/pkg/tui/modal/select.go +++ b/pkg/tui/modal/select.go @@ -1,6 +1,8 @@ package modal import ( + "fmt" + tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/LazyBachelor/LazyPM/internal/style" @@ -176,7 +178,6 @@ func StatusOptions() []SelectOption { {Key: "o", Label: "open", Value: "open"}, {Key: "i", Label: "in_progress", Value: "in_progress"}, {Key: "b", Label: "blocked", Value: "blocked"}, - {Key: "r", Label: "ready_to_sprint", Value: "ready_to_sprint"}, {Key: "c", Label: "closing", Value: "closing"}, } } @@ -213,3 +214,27 @@ func CloseReasonOptions() []SelectOption { {Key: "h", Label: "Other", Value: "other"}, } } + +// SprintOptions generates sprint selection options from available sprints +func SprintOptions(sprints []int, backlogNum int) []SelectOption { + options := make([]SelectOption, 0, len(sprints)) + for _, sprintNum := range sprints { + label := fmt.Sprintf("Sprint %d", sprintNum) + if sprintNum == backlogNum { + label = "Backlog" + } + + key := fmt.Sprintf("%d", sprintNum) + if sprintNum <= 9 { + key = fmt.Sprintf("%d", sprintNum) + } else { + continue + } + options = append(options, SelectOption{ + Key: key, + Label: label, + Value: fmt.Sprintf("%d", sprintNum), + }) + } + return options +} diff --git a/pkg/tui/views/kanban/keys.go b/pkg/tui/views/kanban/keys.go index 4017711..fbed166 100644 --- a/pkg/tui/views/kanban/keys.go +++ b/pkg/tui/views/kanban/keys.go @@ -16,6 +16,8 @@ type KeyMap struct { MoveIssueRight key.Binding SubmitValidation key.Binding AddComment key.Binding + SelectSprint key.Binding + CreateSprint key.Binding } var defaultKanbanKeyMap = KeyMap{ @@ -44,6 +46,14 @@ var defaultKanbanKeyMap = KeyMap{ key.WithKeys("c"), key.WithHelp("c", "add comment"), ), + SelectSprint: key.NewBinding( + key.WithKeys("S"), + key.WithHelp("S", "select sprint"), + ), + CreateSprint: key.NewBinding( + key.WithKeys("n"), + key.WithHelp("n", "new sprint"), + ), } func (m *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { @@ -117,6 +127,12 @@ func (m *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { if selected := fl.SelectedItem(); selected.ID != "" { cmd = m.startConfirmDelete(selected.ID, fl.Index()) } + + case m.notInModalMsgWithKey(msg, m.keyMap.SelectSprint): + cmd = m.startSelectSprint() + + case m.notInModalMsgWithKey(msg, m.keyMap.CreateSprint): + cmd = m.startCreateSprint() } return cmd diff --git a/pkg/tui/views/kanban/model.go b/pkg/tui/views/kanban/model.go index 5a84bda..f53ce16 100644 --- a/pkg/tui/views/kanban/model.go +++ b/pkg/tui/views/kanban/model.go @@ -20,6 +20,7 @@ type ( type Model struct { header Header + backlogList IssueList todoList IssueList inProgList IssueList blockedList IssueList @@ -31,6 +32,9 @@ type Model struct { width int height int + currentSprintNum int + backlogNum int + // Modal and Focus management modalManager *modal.Manager focusManager *modal.FocusManager @@ -61,15 +65,33 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui deleteIndex: -1, } - // Setup lists m.issueDetail = components.NewIssueDetail() m.helpBar = components.NewHelpBar(components.ViewKanban) - allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) - todoIssues := components.StatusOnly(allIssues, models.StatusOpen) - inProgIssues := components.StatusOnly(allIssues, models.StatusInProgress) - blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked) - doneIssues := components.StatusOnly(allIssues, models.StatusClosed) + ctx := context.Background() + backlogNum, _ := app.Issues.GetBacklogSprint(ctx) + m.backlogNum = backlogNum + + backlogIssues, _ := app.Issues.GetIssuesNotInAnySprint(ctx) + m.backlogList = components.NewIssueListFromIssues(app, backlogIssues, 20, 10) + + sprints, _ := app.Issues.GetSprints(ctx) + m.currentSprintNum = 0 + for _, sprintNum := range sprints { + if sprintNum != backlogNum { + m.currentSprintNum = sprintNum + break + } + } + + var sprintIssues []*models.Issue + if m.currentSprintNum > 0 { + sprintIssues, _ = app.Issues.GetIssuesBySprint(ctx, m.currentSprintNum) + } + todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen) + inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress) + blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked) + doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed) m.todoList = components.NewIssueListFromIssues(app, todoIssues, 20, 10) m.inProgList = components.NewIssueListFromIssues(app, inProgIssues, 20, 10) @@ -77,16 +99,17 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui m.doneList = components.NewIssueListFromIssues(app, doneIssues, 20, 10) // Setup focus areas for kanban columns + m.focusManager.EnableArea(modal.FocusColumn0) m.focusManager.EnableArea(modal.FocusColumn1) m.focusManager.EnableArea(modal.FocusColumn2) m.focusManager.EnableArea(modal.FocusColumn3) m.focusManager.EnableArea(modal.FocusColumn4) - m.focusManager.SetCurrent(modal.FocusColumn1) + m.focusManager.SetCurrent(modal.FocusColumn0) // Register modals m.registerModals() - if selected := m.todoList.SelectedItem(); selected.ID != "" { + if selected := m.backlogList.SelectedItem(); selected.ID != "" { m.setDetailIssueWithComments(selected.Issue) } @@ -138,7 +161,7 @@ func (m *Model) IsFocusedOnDetail() bool { func (m *Model) ToggleFocus() { if m.focusManager.IsDetailFocused() { - m.focusManager.SetCurrent(modal.FocusColumn1) + m.focusManager.SetCurrent(modal.FocusColumn0) m.issueDetail.SetFocused(false) } else { m.focusManager.SetCurrent(modal.FocusDetail) @@ -148,6 +171,8 @@ func (m *Model) ToggleFocus() { func (m *Model) FocusedIssueList() *IssueList { switch m.focusManager.Current() { + case modal.FocusColumn0: + return &m.backlogList case modal.FocusColumn1: return &m.todoList case modal.FocusColumn2: @@ -157,7 +182,7 @@ func (m *Model) FocusedIssueList() *IssueList { case modal.FocusColumn4: return &m.doneList default: - return &m.todoList + return &m.backlogList } } @@ -181,6 +206,8 @@ func (m *Model) setDetailIssueWithComments(issue models.Issue) { func statusForColumn(col modal.FocusArea) models.Status { switch col { + case modal.FocusColumn0: + return models.StatusOpen case modal.FocusColumn1: return models.StatusOpen case modal.FocusColumn2: @@ -204,25 +231,31 @@ func (m *Model) moveIssue(delta int) tea.Cmd { currentCol := m.focusManager.Current() var newCol modal.FocusArea switch currentCol { - case modal.FocusColumn1: + case modal.FocusColumn0: // Backlog if delta > 0 { - newCol = modal.FocusColumn2 + return m.moveIssueToSprint(selected.ID, m.currentSprintNum) } - case modal.FocusColumn2: + case modal.FocusColumn1: // To Do if delta > 0 { - newCol = modal.FocusColumn3 + newCol = modal.FocusColumn2 // To In Progress + } else if delta < 0 { + return m.moveIssueToBacklog(selected.ID) + } + case modal.FocusColumn2: // In Progress + if delta > 0 { + newCol = modal.FocusColumn3 // To Blocked } else { - newCol = modal.FocusColumn1 + newCol = modal.FocusColumn1 // To To Do } - case modal.FocusColumn3: + case modal.FocusColumn3: // Blocked if delta > 0 { - newCol = modal.FocusColumn4 + newCol = modal.FocusColumn4 // To Done } else { - newCol = modal.FocusColumn2 + newCol = modal.FocusColumn2 // To In Progress } - case modal.FocusColumn4: + case modal.FocusColumn4: // Done if delta < 0 { - newCol = modal.FocusColumn3 + newCol = modal.FocusColumn3 // To Blocked } } @@ -233,3 +266,27 @@ func (m *Model) moveIssue(delta int) tea.Cmd { newStatus := statusForColumn(newCol) return msgs.UpdateIssueStatusCmd(m.app, selected.ID, string(newStatus)) } + +// moveIssueToSprint moves an issue to a sprint +func (m *Model) moveIssueToSprint(issueID string, sprintNum int) tea.Cmd { + return func() tea.Msg { + err := m.app.Issues.AddIssueToSprint(context.Background(), issueID, sprintNum) + m.submitValidation() + if err != nil { + return m.refreshIssueListsAndSelectIssue(issueID)() + } + return m.refreshIssueListsAndSelectIssue(issueID)() + } +} + +// moveIssueToBacklog removes an issue from the current sprint (moves it to backlog) +func (m *Model) moveIssueToBacklog(issueID string) tea.Cmd { + return func() tea.Msg { + err := m.app.Issues.RemoveIssueFromSprint(context.Background(), issueID, m.currentSprintNum) + m.submitValidation() + if err != nil { + return m.refreshIssueListsAndSelectIssue(issueID)() + } + return m.refreshIssueListsAndSelectIssue(issueID)() + } +} diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index 5e767bb..d95c29d 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -2,6 +2,7 @@ package kanban import ( "context" + "fmt" "strconv" "charm.land/bubbles/v2/list" @@ -14,42 +15,63 @@ import ( ) func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { - allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) - if err != nil { - return nil - } + ctx := context.Background() - todoIssues := components.StatusOnly(allIssues, models.StatusOpen) - inProgIssues := components.StatusOnly(allIssues, models.StatusInProgress) - blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked) - doneIssues := components.StatusOnly(allIssues, models.StatusClosed) + backlogIssues, _ := m.app.Issues.GetIssuesNotInAnySprint(ctx) + backlogCmd := m.backlogList.SetIssues(backlogIssues) + + sprintIssues, _ := m.app.Issues.GetIssuesBySprint(ctx, m.currentSprintNum) + todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen) + inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress) + blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked) + doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed) todoCmd := m.todoList.SetIssues(todoIssues) inProgCmd := m.inProgList.SetIssues(inProgIssues) blockedCmd := m.blockedList.SetIssues(blockedIssues) doneCmd := m.doneList.SetIssues(doneIssues) + // Find the issue to determine which column it belongs to var targetStatus models.Status - for _, issue := range allIssues { + var inBacklog bool + for _, issue := range sprintIssues { + if issue == nil { + continue + } if issue.ID == issueID { - m.setDetailIssueWithComments(*issue) targetStatus = issue.Status + m.setDetailIssueWithComments(*issue) break } } + // Check if issue is in backlog + if targetStatus == "" { + for _, issue := range backlogIssues { + if issue == nil { + continue + } + if issue.ID == issueID { + inBacklog = true + m.setDetailIssueWithComments(*issue) + break + } + } + } - switch targetStatus { - case models.StatusOpen: + switch { + case inBacklog: + m.focusManager.SetCurrent(modal.FocusColumn0) + case targetStatus == models.StatusOpen: m.focusManager.SetCurrent(modal.FocusColumn1) - case models.StatusInProgress: + case targetStatus == models.StatusInProgress: m.focusManager.SetCurrent(modal.FocusColumn2) - case models.StatusBlocked: + case targetStatus == models.StatusBlocked: m.focusManager.SetCurrent(modal.FocusColumn3) - case models.StatusClosed: + case targetStatus == models.StatusClosed: m.focusManager.SetCurrent(modal.FocusColumn4) } - return tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { + return tea.Sequence(backlogCmd, todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: issueID} }) } @@ -60,6 +82,27 @@ func (m *Model) refreshAndSubmit(issueID string) tea.Cmd { return refreshCmd } +// refreshAllIssueLists refreshes all issue lists without selecting a specific issue +func (m *Model) refreshAllIssueLists() tea.Cmd { + ctx := context.Background() + + backlogIssues, _ := m.app.Issues.GetIssuesNotInAnySprint(ctx) + backlogCmd := m.backlogList.SetIssues(backlogIssues) + + sprintIssues, _ := m.app.Issues.GetIssuesBySprint(ctx, m.currentSprintNum) + todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen) + inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress) + blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked) + doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed) + + todoCmd := m.todoList.SetIssues(todoIssues) + inProgCmd := m.inProgList.SetIssues(inProgIssues) + blockedCmd := m.blockedList.SetIssues(blockedIssues) + doneCmd := m.doneList.SetIssues(doneIssues) + + return tea.Batch(backlogCmd, todoCmd, inProgCmd, blockedCmd, doneCmd) +} + // Modal action handlers func (m *Model) startConfirmExit() tea.Cmd { @@ -121,6 +164,58 @@ func (m *Model) startChooseType(selected ListIssue) tea.Cmd { return m.modalManager.ShowModal(modal.ModalSelectType) } +func (m *Model) startSelectSprint() tea.Cmd { + sprints, err := m.app.Issues.GetSprints(context.Background()) + if err != nil { + return nil + } + + options := make([]modal.SelectOption, 0, len(sprints)) + for _, sprintNum := range sprints { + if sprintNum == m.backlogNum { + continue + } + label := fmt.Sprintf("Sprint %d", sprintNum) + key := fmt.Sprintf("%d", sprintNum) + if sprintNum > 9 { + continue + } + options = append(options, modal.SelectOption{ + Key: key, + Label: label, + Value: fmt.Sprintf("%d", sprintNum), + }) + } + + if len(options) == 0 { + return nil + } + + sprintModal := modal.NewSelectModal(modal.SelectConfig{ + ID: "dynamic-sprint-select", + Label: "Select sprint to view:", + Options: options, + CancelKey: "esc", + }) + + return m.modalManager.PushModal(sprintModal) +} + +func (m *Model) startCreateSprint() tea.Cmd { + return tea.Sequence( + func() tea.Msg { + ctx := context.Background() + newSprintNum, err := m.app.Issues.AddSprint(ctx) + if err != nil { + return nil + } + m.currentSprintNum = newSprintNum + return nil + }, + m.refreshAllIssueLists(), + ) +} + func (m *Model) startEditAssignee(selected ListIssue) tea.Cmd { m.currentIssueID = selected.ID assigneeModal := m.modalManager.GetTextInputModal(modal.ModalEditAssignee) @@ -210,6 +305,15 @@ func (m *Model) handleModalCompleted(msg modal.ModalCompletedMsg) tea.Cmd { cmd := msgs.UpdateIssueTypeCmd(m.app, m.currentIssueID, issueType) return func() tea.Msg { return cmd() } } + case modal.ModalSelectSprint, "dynamic-sprint-select": + if r, ok := msg.Value.(modal.SelectResult); ok { + sprintNum, err := strconv.Atoi(r.SelectedValue) + if err != nil { + return nil + } + m.currentSprintNum = sprintNum + return m.refreshAllIssueLists() + } case modal.ModalCloseReason: if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" { cmd := msgs.CloseIssueCmd(m.app, m.currentIssueID, r.Value) @@ -248,6 +352,7 @@ func (m *Model) handleModalCancelled(msg modal.ModalCancelledMsg) { m.currentIssueID = "" case modal.ModalSelectType: m.currentIssueID = "" + case modal.ModalSelectSprint, "dynamic-sprint-select": case modal.ModalCloseReason: m.currentIssueID = "" case modal.ModalAddComment: @@ -322,6 +427,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case msgs.SelectIssueMsg: + m.backlogList.SelectIssueID(msg.IssueID) m.todoList.SelectIssueID(msg.IssueID) m.inProgList.SelectIssueID(msg.IssueID) m.blockedList.SelectIssueID(msg.IssueID) @@ -333,21 +439,23 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil || msg.Issue == nil { return m, nil } - allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) - if err != nil { - return m, nil - } - todoIssues := components.StatusOnly(allIssues, models.StatusOpen) - inProgIssues := components.StatusOnly(allIssues, models.StatusInProgress) - blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked) - doneIssues := components.StatusOnly(allIssues, models.StatusClosed) + backlogIssues, _ := m.app.Issues.GetIssuesNotInAnySprint(context.Background()) + backlogCmd := m.backlogList.SetIssues(backlogIssues) + + sprintIssues, _ := m.app.Issues.GetIssuesBySprint(context.Background(), m.currentSprintNum) + todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen) + inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress) + blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked) + doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed) todoCmd := m.todoList.SetIssues(todoIssues) inProgCmd := m.inProgList.SetIssues(inProgIssues) blockedCmd := m.blockedList.SetIssues(blockedIssues) doneCmd := m.doneList.SetIssues(doneIssues) + allIssues := append(backlogIssues, sprintIssues...) + selectedIssue := msg.Issue if selectedIssue.ID == "" { for _, issue := range allIssues { @@ -360,7 +468,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.setDetailIssueWithComments(*selectedIssue) m.submitValidation() - return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { + return m, tea.Sequence(backlogCmd, todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} }) diff --git a/pkg/tui/views/kanban/view.go b/pkg/tui/views/kanban/view.go index 8303359..f3b14a9 100644 --- a/pkg/tui/views/kanban/view.go +++ b/pkg/tui/views/kanban/view.go @@ -1,11 +1,13 @@ package kanban import ( + "fmt" + tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" + "github.com/LazyBachelor/LazyPM/internal/style" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/modal" - "github.com/LazyBachelor/LazyPM/internal/style" ) func (m *Model) View() tea.View { @@ -16,6 +18,14 @@ func (m *Model) View() tea.View { m.helpBar.SetWidth(m.width) m.modalManager.SetSize(m.width, m.height) + var sprintName string + if m.currentSprintNum > 0 { + sprintName = fmt.Sprintf("Sprint %d", m.currentSprintNum) + } else { + sprintName = "No Sprint" + } + + m.header = components.NewHeader(fmt.Sprintf("Kanban Board - %s", sprintName)) header := m.header.View(m.width) headerHeight := m.header.Height() @@ -24,49 +34,53 @@ func (m *Model) View() tea.View { contentHeight := m.height - headerHeight - footerHeight totalContentWidth := m.width - 1 - colWidth := max(totalContentWidth/4, 20) + colWidth := max(totalContentWidth/5, 16) - // Calculate initial list height (half of content height, minimum 5 rows) listHeight := contentHeight / 2 if listHeight < 5 { listHeight = contentHeight } + m.backlogList.SetSize(colWidth, listHeight-1) m.todoList.SetSize(colWidth, listHeight-1) m.inProgList.SetSize(colWidth, listHeight-1) m.blockedList.SetSize(colWidth, listHeight-1) m.doneList.SetSize(colWidth, listHeight-1) - // Only highlight the focused column's selected row currentFocus := m.focusManager.Current() + m.backlogList.SetHighlightSelected(currentFocus == modal.FocusColumn0) m.todoList.SetHighlightSelected(currentFocus == modal.FocusColumn1) m.inProgList.SetHighlightSelected(currentFocus == modal.FocusColumn2) m.blockedList.SetHighlightSelected(currentFocus == modal.FocusColumn3) m.doneList.SetHighlightSelected(currentFocus == modal.FocusColumn4) - todoLabel := style.LabelStyle.Render("To Do") - inProgLabel := style.LabelStyle.Render("In Progress") - blockedLabel := style.LabelStyle.Render("Blocked") - doneLabel := style.LabelStyle.Render("Done") + backlogLabel := style.LabelStyle.Render("Backlog") + todoLabel := style.LabelStyle.Render(fmt.Sprintf("%s - To Do", sprintName)) + inProgLabel := style.LabelStyle.Render(fmt.Sprintf("%s - In Progress", sprintName)) + blockedLabel := style.LabelStyle.Render(fmt.Sprintf("%s - Blocked", sprintName)) + doneLabel := style.LabelStyle.Render(fmt.Sprintf("%s - Done", sprintName)) highlight := lipgloss.NewStyle().Foreground(style.Primary).Bold(true) switch currentFocus { + case modal.FocusColumn0: + backlogLabel = highlight.Render("Backlog ▶") case modal.FocusColumn1: - todoLabel = highlight.Render("To Do ▶") + todoLabel = highlight.Render(fmt.Sprintf("%s - To Do ▶", sprintName)) case modal.FocusColumn2: - inProgLabel = highlight.Render("In Progress ▶") + inProgLabel = highlight.Render(fmt.Sprintf("%s - In Progress ▶", sprintName)) case modal.FocusColumn3: - blockedLabel = highlight.Render("Blocked ▶") + blockedLabel = highlight.Render(fmt.Sprintf("%s - Blocked ▶", sprintName)) case modal.FocusColumn4: - doneLabel = highlight.Render("Done ▶") + doneLabel = highlight.Render(fmt.Sprintf("%s - Done ▶", sprintName)) } + backlogCol := lipgloss.JoinVertical(lipgloss.Left, backlogLabel, m.backlogList.View()) todoCol := lipgloss.JoinVertical(lipgloss.Left, todoLabel, m.todoList.View()) inProgCol := lipgloss.JoinVertical(lipgloss.Left, inProgLabel, m.inProgList.View()) blockedCol := lipgloss.JoinVertical(lipgloss.Left, blockedLabel, m.blockedList.View()) doneCol := lipgloss.JoinVertical(lipgloss.Left, doneLabel, m.doneList.View()) - board := lipgloss.JoinHorizontal(lipgloss.Left, todoCol, inProgCol, blockedCol, doneCol) + board := lipgloss.JoinHorizontal(lipgloss.Left, backlogCol, todoCol, inProgCol, blockedCol, doneCol) boardHeight := lipgloss.Height(board) detailHeight := max(contentHeight-boardHeight, 5) diff --git a/pkg/web/assets/css/styles.css b/pkg/web/assets/css/styles.css index 2b9eab5..b905381 100644 --- a/pkg/web/assets/css/styles.css +++ b/pkg/web/assets/css/styles.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-gray-500:oklch(55.1% .027 264.364);--color-black:#000;--spacing:.25rem;--container-xs:20rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-lg:.5rem;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}@media (prefers-color-scheme:dark){:root:not([data-theme]){color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}}:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dark]:checked),[data-theme=dark]{color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=cupcake]:checked),[data-theme=cupcake]{color-scheme:light;--color-base-100:oklch(97.788% .004 56.375);--color-base-200:oklch(93.982% .007 61.449);--color-base-300:oklch(91.586% .006 53.44);--color-base-content:oklch(23.574% .066 313.189);--color-primary:oklch(85% .138 181.071);--color-primary-content:oklch(43% .078 188.216);--color-secondary:oklch(89% .061 343.231);--color-secondary-content:oklch(45% .187 3.815);--color-accent:oklch(90% .076 70.697);--color-accent-content:oklch(47% .157 37.304);--color-neutral:oklch(27% .006 286.033);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(68% .169 237.323);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(69% .17 162.48);--color-success-content:oklch(26% .051 172.552);--color-warning:oklch(79% .184 86.047);--color-warning-content:oklch(28% .066 53.813);--color-error:oklch(64% .246 16.439);--color-error-content:oklch(27% .105 12.094);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:0}:root:has(input.theme-controller[value=bumblebee]:checked),[data-theme=bumblebee]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(92% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(85% .199 91.936);--color-primary-content:oklch(42% .095 57.708);--color-secondary:oklch(75% .183 55.934);--color-secondary-content:oklch(40% .123 38.172);--color-accent:oklch(0% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(37% .01 67.558);--color-neutral-content:oklch(92% .003 48.717);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(39% .09 240.876);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(39% .141 25.723);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=emerald]:checked),[data-theme=emerald]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(35.519% .032 262.988);--color-primary:oklch(76.662% .135 153.45);--color-primary-content:oklch(33.387% .04 162.24);--color-secondary:oklch(61.302% .202 261.294);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(72.772% .149 33.2);--color-accent-content:oklch(0% 0 0);--color-neutral:oklch(35.519% .032 262.988);--color-neutral-content:oklch(98.462% .001 247.838);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=corporate]:checked),[data-theme=corporate]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(22.389% .031 278.072);--color-primary:oklch(58% .158 241.966);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(55% .046 257.417);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(60% .118 184.704);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(0% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(60% .126 221.723);--color-info-content:oklch(100% 0 0);--color-success:oklch(62% .194 149.214);--color-success-content:oklch(100% 0 0);--color-warning:oklch(85% .199 91.936);--color-warning-content:oklch(0% 0 0);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(0% 0 0);--radius-selector:.25rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=synthwave]:checked),[data-theme=synthwave]{color-scheme:dark;--color-base-100:oklch(15% .09 281.288);--color-base-200:oklch(20% .09 281.288);--color-base-300:oklch(25% .09 281.288);--color-base-content:oklch(78% .115 274.713);--color-primary:oklch(71% .202 349.761);--color-primary-content:oklch(28% .109 3.907);--color-secondary:oklch(82% .111 230.318);--color-secondary-content:oklch(29% .066 243.157);--color-accent:oklch(75% .183 55.934);--color-accent-content:oklch(26% .079 36.259);--color-neutral:oklch(45% .24 277.023);--color-neutral-content:oklch(87% .065 274.039);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(77% .152 181.912);--color-success-content:oklch(27% .046 192.524);--color-warning:oklch(90% .182 98.111);--color-warning-content:oklch(42% .095 57.708);--color-error:oklch(73.7% .121 32.639);--color-error-content:oklch(23.501% .096 290.329);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=retro]:checked),[data-theme=retro]{color-scheme:light;--color-base-100:oklch(91.637% .034 90.515);--color-base-200:oklch(88.272% .049 91.774);--color-base-300:oklch(84.133% .065 90.856);--color-base-content:oklch(41% .112 45.904);--color-primary:oklch(80% .114 19.571);--color-primary-content:oklch(39% .141 25.723);--color-secondary:oklch(92% .084 155.995);--color-secondary-content:oklch(44% .119 151.328);--color-accent:oklch(68% .162 75.834);--color-accent-content:oklch(41% .112 45.904);--color-neutral:oklch(44% .011 73.639);--color-neutral-content:oklch(86% .005 56.366);--color-info:oklch(58% .158 241.966);--color-info-content:oklch(96% .059 95.617);--color-success:oklch(51% .096 186.391);--color-success-content:oklch(96% .059 95.617);--color-warning:oklch(64% .222 41.116);--color-warning-content:oklch(96% .059 95.617);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(40% .123 38.172);--radius-selector:.25rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=cyberpunk]:checked),[data-theme=cyberpunk]{color-scheme:light;--color-base-100:oklch(94.51% .179 104.32);--color-base-200:oklch(91.51% .179 104.32);--color-base-300:oklch(85.51% .179 104.32);--color-base-content:oklch(0% 0 0);--color-primary:oklch(74.22% .209 6.35);--color-primary-content:oklch(14.844% .041 6.35);--color-secondary:oklch(83.33% .184 204.72);--color-secondary-content:oklch(16.666% .036 204.72);--color-accent:oklch(71.86% .217 310.43);--color-accent-content:oklch(14.372% .043 310.43);--color-neutral:oklch(23.04% .065 269.31);--color-neutral-content:oklch(94.51% .179 104.32);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:0rem;--radius-field:0rem;--radius-box:0rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=valentine]:checked),[data-theme=valentine]{color-scheme:light;--color-base-100:oklch(97% .014 343.198);--color-base-200:oklch(94% .028 342.258);--color-base-300:oklch(89% .061 343.231);--color-base-content:oklch(52% .223 3.958);--color-primary:oklch(65% .241 354.308);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(62% .265 303.9);--color-secondary-content:oklch(97% .014 308.299);--color-accent:oklch(82% .111 230.318);--color-accent-content:oklch(39% .09 240.876);--color-neutral:oklch(40% .153 2.432);--color-neutral-content:oklch(89% .061 343.231);--color-info:oklch(86% .127 207.078);--color-info-content:oklch(44% .11 240.79);--color-success:oklch(84% .143 164.978);--color-success-content:oklch(43% .095 166.913);--color-warning:oklch(75% .183 55.934);--color-warning-content:oklch(26% .079 36.259);--color-error:oklch(63% .237 25.331);--color-error-content:oklch(97% .013 17.38);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=halloween]:checked),[data-theme=halloween]{color-scheme:dark;--color-base-100:oklch(21% .006 56.043);--color-base-200:oklch(14% .004 49.25);--color-base-300:oklch(0% 0 0);--color-base-content:oklch(84.955% 0 0);--color-primary:oklch(77.48% .204 60.62);--color-primary-content:oklch(19.693% .004 196.779);--color-secondary:oklch(45.98% .248 305.03);--color-secondary-content:oklch(89.196% .049 305.03);--color-accent:oklch(64.8% .223 136.073);--color-accent-content:oklch(0% 0 0);--color-neutral:oklch(24.371% .046 65.681);--color-neutral-content:oklch(84.874% .009 65.681);--color-info:oklch(54.615% .215 262.88);--color-info-content:oklch(90.923% .043 262.88);--color-success:oklch(62.705% .169 149.213);--color-success-content:oklch(12.541% .033 149.213);--color-warning:oklch(66.584% .157 58.318);--color-warning-content:oklch(13.316% .031 58.318);--color-error:oklch(65.72% .199 27.33);--color-error-content:oklch(13.144% .039 27.33);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=garden]:checked),[data-theme=garden]{color-scheme:light;--color-base-100:oklch(92.951% .002 17.197);--color-base-200:oklch(86.445% .002 17.197);--color-base-300:oklch(79.938% .001 17.197);--color-base-content:oklch(16.961% .001 17.32);--color-primary:oklch(62.45% .278 3.836);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(48.495% .11 355.095);--color-secondary-content:oklch(89.699% .022 355.095);--color-accent:oklch(56.273% .054 154.39);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(24.155% .049 89.07);--color-neutral-content:oklch(92.951% .002 17.197);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=forest]:checked),[data-theme=forest]{color-scheme:dark;--color-base-100:oklch(20.84% .008 17.911);--color-base-200:oklch(18.522% .007 17.911);--color-base-300:oklch(16.203% .007 17.911);--color-base-content:oklch(83.768% .001 17.911);--color-primary:oklch(68.628% .185 148.958);--color-primary-content:oklch(0% 0 0);--color-secondary:oklch(69.776% .135 168.327);--color-secondary-content:oklch(13.955% .027 168.327);--color-accent:oklch(70.628% .119 185.713);--color-accent-content:oklch(14.125% .023 185.713);--color-neutral:oklch(30.698% .039 171.364);--color-neutral-content:oklch(86.139% .007 171.364);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=aqua]:checked),[data-theme=aqua]{color-scheme:dark;--color-base-100:oklch(37% .146 265.522);--color-base-200:oklch(28% .091 267.935);--color-base-300:oklch(22% .091 267.935);--color-base-content:oklch(90% .058 230.902);--color-primary:oklch(85.661% .144 198.645);--color-primary-content:oklch(40.124% .068 197.603);--color-secondary:oklch(60.682% .108 309.782);--color-secondary-content:oklch(96% .016 293.756);--color-accent:oklch(93.426% .102 94.555);--color-accent-content:oklch(18.685% .02 94.555);--color-neutral:oklch(27% .146 265.522);--color-neutral-content:oklch(80% .146 265.522);--color-info:oklch(54.615% .215 262.88);--color-info-content:oklch(90.923% .043 262.88);--color-success:oklch(62.705% .169 149.213);--color-success-content:oklch(12.541% .033 149.213);--color-warning:oklch(66.584% .157 58.318);--color-warning-content:oklch(27% .077 45.635);--color-error:oklch(73.95% .19 27.33);--color-error-content:oklch(14.79% .038 27.33);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=lofi]:checked),[data-theme=lofi]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(94% 0 0);--color-base-content:oklch(0% 0 0);--color-primary:oklch(15.906% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(21.455% .001 17.278);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(26.861% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(0% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(79.54% .103 205.9);--color-info-content:oklch(15.908% .02 205.9);--color-success:oklch(90.13% .153 164.14);--color-success-content:oklch(18.026% .03 164.14);--color-warning:oklch(88.37% .135 79.94);--color-warning-content:oklch(17.674% .027 79.94);--color-error:oklch(78.66% .15 28.47);--color-error-content:oklch(15.732% .03 28.47);--radius-selector:2rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=pastel]:checked),[data-theme=pastel]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98.462% .001 247.838);--color-base-300:oklch(92.462% .001 247.838);--color-base-content:oklch(20% 0 0);--color-primary:oklch(90% .063 306.703);--color-primary-content:oklch(49% .265 301.924);--color-secondary:oklch(89% .058 10.001);--color-secondary-content:oklch(51% .222 16.935);--color-accent:oklch(90% .093 164.15);--color-accent-content:oklch(50% .118 165.612);--color-neutral:oklch(55% .046 257.417);--color-neutral-content:oklch(92% .013 255.508);--color-info:oklch(86% .127 207.078);--color-info-content:oklch(52% .105 223.128);--color-success:oklch(87% .15 154.449);--color-success-content:oklch(52% .154 150.069);--color-warning:oklch(83% .128 66.29);--color-warning-content:oklch(55% .195 38.402);--color-error:oklch(80% .114 19.571);--color-error-content:oklch(50% .213 27.518);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:0;--noise:0}:root:has(input.theme-controller[value=fantasy]:checked),[data-theme=fantasy]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(27.807% .029 256.847);--color-primary:oklch(37.45% .189 325.02);--color-primary-content:oklch(87.49% .037 325.02);--color-secondary:oklch(53.92% .162 241.36);--color-secondary-content:oklch(90.784% .032 241.36);--color-accent:oklch(75.98% .204 56.72);--color-accent-content:oklch(15.196% .04 56.72);--color-neutral:oklch(27.807% .029 256.847);--color-neutral-content:oklch(85.561% .005 256.847);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=wireframe]:checked),[data-theme=wireframe]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(94% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(87% 0 0);--color-primary-content:oklch(26% 0 0);--color-secondary:oklch(87% 0 0);--color-secondary-content:oklch(26% 0 0);--color-accent:oklch(87% 0 0);--color-accent-content:oklch(26% 0 0);--color-neutral:oklch(87% 0 0);--color-neutral-content:oklch(26% 0 0);--color-info:oklch(44% .11 240.79);--color-info-content:oklch(90% .058 230.902);--color-success:oklch(43% .095 166.913);--color-success-content:oklch(90% .093 164.15);--color-warning:oklch(47% .137 46.201);--color-warning-content:oklch(92% .12 95.746);--color-error:oklch(44% .177 26.899);--color-error-content:oklch(88% .062 18.334);--radius-selector:0rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=black]:checked),[data-theme=black]{color-scheme:dark;--color-base-100:oklch(0% 0 0);--color-base-200:oklch(19% 0 0);--color-base-300:oklch(22% 0 0);--color-base-content:oklch(87.609% 0 0);--color-primary:oklch(35% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(35% 0 0);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(35% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(35% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(45.201% .313 264.052);--color-info-content:oklch(89.04% .062 264.052);--color-success:oklch(51.975% .176 142.495);--color-success-content:oklch(90.395% .035 142.495);--color-warning:oklch(96.798% .211 109.769);--color-warning-content:oklch(19.359% .042 109.769);--color-error:oklch(62.795% .257 29.233);--color-error-content:oklch(12.559% .051 29.233);--radius-selector:0rem;--radius-field:0rem;--radius-box:0rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=luxury]:checked),[data-theme=luxury]{color-scheme:dark;--color-base-100:oklch(14.076% .004 285.822);--color-base-200:oklch(20.219% .004 308.229);--color-base-300:oklch(23.219% .004 308.229);--color-base-content:oklch(75.687% .123 76.89);--color-primary:oklch(100% 0 0);--color-primary-content:oklch(20% 0 0);--color-secondary:oklch(27.581% .064 261.069);--color-secondary-content:oklch(85.516% .012 261.069);--color-accent:oklch(36.674% .051 338.825);--color-accent-content:oklch(87.334% .01 338.825);--color-neutral:oklch(24.27% .057 59.825);--color-neutral-content:oklch(93.203% .089 90.861);--color-info:oklch(79.061% .121 237.133);--color-info-content:oklch(15.812% .024 237.133);--color-success:oklch(78.119% .192 132.154);--color-success-content:oklch(15.623% .038 132.154);--color-warning:oklch(86.127% .136 102.891);--color-warning-content:oklch(17.225% .027 102.891);--color-error:oklch(71.753% .176 22.568);--color-error-content:oklch(14.35% .035 22.568);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dracula]:checked),[data-theme=dracula]{color-scheme:dark;--color-base-100:oklch(28.822% .022 277.508);--color-base-200:oklch(26.805% .02 277.508);--color-base-300:oklch(24.787% .019 277.508);--color-base-content:oklch(97.747% .007 106.545);--color-primary:oklch(75.461% .183 346.812);--color-primary-content:oklch(15.092% .036 346.812);--color-secondary:oklch(74.202% .148 301.883);--color-secondary-content:oklch(14.84% .029 301.883);--color-accent:oklch(83.392% .124 66.558);--color-accent-content:oklch(16.678% .024 66.558);--color-neutral:oklch(39.445% .032 275.524);--color-neutral-content:oklch(87.889% .006 275.524);--color-info:oklch(88.263% .093 212.846);--color-info-content:oklch(17.652% .018 212.846);--color-success:oklch(87.099% .219 148.024);--color-success-content:oklch(17.419% .043 148.024);--color-warning:oklch(95.533% .134 112.757);--color-warning-content:oklch(19.106% .026 112.757);--color-error:oklch(68.22% .206 24.43);--color-error-content:oklch(13.644% .041 24.43);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=cmyk]:checked),[data-theme=cmyk]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(95% 0 0);--color-base-300:oklch(90% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(71.772% .133 239.443);--color-primary-content:oklch(14.354% .026 239.443);--color-secondary:oklch(64.476% .202 359.339);--color-secondary-content:oklch(12.895% .04 359.339);--color-accent:oklch(94.228% .189 105.306);--color-accent-content:oklch(18.845% .037 105.306);--color-neutral:oklch(21.778% 0 0);--color-neutral-content:oklch(84.355% 0 0);--color-info:oklch(68.475% .094 217.284);--color-info-content:oklch(13.695% .018 217.284);--color-success:oklch(46.949% .162 321.406);--color-success-content:oklch(89.389% .032 321.406);--color-warning:oklch(71.236% .159 52.023);--color-warning-content:oklch(14.247% .031 52.023);--color-error:oklch(62.013% .208 28.717);--color-error-content:oklch(12.402% .041 28.717);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=autumn]:checked),[data-theme=autumn]{color-scheme:light;--color-base-100:oklch(95.814% 0 0);--color-base-200:oklch(89.107% 0 0);--color-base-300:oklch(82.4% 0 0);--color-base-content:oklch(19.162% 0 0);--color-primary:oklch(40.723% .161 17.53);--color-primary-content:oklch(88.144% .032 17.53);--color-secondary:oklch(61.676% .169 23.865);--color-secondary-content:oklch(12.335% .033 23.865);--color-accent:oklch(73.425% .094 60.729);--color-accent-content:oklch(14.685% .018 60.729);--color-neutral:oklch(54.367% .037 51.902);--color-neutral-content:oklch(90.873% .007 51.902);--color-info:oklch(69.224% .097 207.284);--color-info-content:oklch(13.844% .019 207.284);--color-success:oklch(60.995% .08 174.616);--color-success-content:oklch(12.199% .016 174.616);--color-warning:oklch(70.081% .164 56.844);--color-warning-content:oklch(14.016% .032 56.844);--color-error:oklch(53.07% .241 24.16);--color-error-content:oklch(90.614% .048 24.16);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=business]:checked),[data-theme=business]{color-scheme:dark;--color-base-100:oklch(24.353% 0 0);--color-base-200:oklch(22.648% 0 0);--color-base-300:oklch(20.944% 0 0);--color-base-content:oklch(84.87% 0 0);--color-primary:oklch(41.703% .099 251.473);--color-primary-content:oklch(88.34% .019 251.473);--color-secondary:oklch(64.092% .027 229.389);--color-secondary-content:oklch(12.818% .005 229.389);--color-accent:oklch(67.271% .167 35.791);--color-accent-content:oklch(13.454% .033 35.791);--color-neutral:oklch(27.441% .013 253.041);--color-neutral-content:oklch(85.488% .002 253.041);--color-info:oklch(62.616% .143 240.033);--color-info-content:oklch(12.523% .028 240.033);--color-success:oklch(70.226% .094 156.596);--color-success-content:oklch(14.045% .018 156.596);--color-warning:oklch(77.482% .115 81.519);--color-warning-content:oklch(15.496% .023 81.519);--color-error:oklch(51.61% .146 29.674);--color-error-content:oklch(90.322% .029 29.674);--radius-selector:0rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=acid]:checked),[data-theme=acid]{color-scheme:light;--color-base-100:oklch(98% 0 0);--color-base-200:oklch(95% 0 0);--color-base-300:oklch(91% 0 0);--color-base-content:oklch(0% 0 0);--color-primary:oklch(71.9% .357 330.759);--color-primary-content:oklch(14.38% .071 330.759);--color-secondary:oklch(73.37% .224 48.25);--color-secondary-content:oklch(14.674% .044 48.25);--color-accent:oklch(92.78% .264 122.962);--color-accent-content:oklch(18.556% .052 122.962);--color-neutral:oklch(21.31% .128 278.68);--color-neutral-content:oklch(84.262% .025 278.68);--color-info:oklch(60.72% .227 252.05);--color-info-content:oklch(12.144% .045 252.05);--color-success:oklch(85.72% .266 158.53);--color-success-content:oklch(17.144% .053 158.53);--color-warning:oklch(91.01% .212 100.5);--color-warning-content:oklch(18.202% .042 100.5);--color-error:oklch(64.84% .293 29.349);--color-error-content:oklch(12.968% .058 29.349);--radius-selector:1rem;--radius-field:1rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=lemonade]:checked),[data-theme=lemonade]{color-scheme:light;--color-base-100:oklch(98.71% .02 123.72);--color-base-200:oklch(91.8% .018 123.72);--color-base-300:oklch(84.89% .017 123.72);--color-base-content:oklch(19.742% .004 123.72);--color-primary:oklch(58.92% .199 134.6);--color-primary-content:oklch(11.784% .039 134.6);--color-secondary:oklch(77.75% .196 111.09);--color-secondary-content:oklch(15.55% .039 111.09);--color-accent:oklch(85.39% .201 100.73);--color-accent-content:oklch(17.078% .04 100.73);--color-neutral:oklch(30.98% .075 108.6);--color-neutral-content:oklch(86.196% .015 108.6);--color-info:oklch(86.19% .047 224.14);--color-info-content:oklch(17.238% .009 224.14);--color-success:oklch(86.19% .047 157.85);--color-success-content:oklch(17.238% .009 157.85);--color-warning:oklch(86.19% .047 102.15);--color-warning-content:oklch(17.238% .009 102.15);--color-error:oklch(86.19% .047 25.85);--color-error-content:oklch(17.238% .009 25.85);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=night]:checked),[data-theme=night]{color-scheme:dark;--color-base-100:oklch(20.768% .039 265.754);--color-base-200:oklch(19.314% .037 265.754);--color-base-300:oklch(17.86% .034 265.754);--color-base-content:oklch(84.153% .007 265.754);--color-primary:oklch(75.351% .138 232.661);--color-primary-content:oklch(15.07% .027 232.661);--color-secondary:oklch(68.011% .158 276.934);--color-secondary-content:oklch(13.602% .031 276.934);--color-accent:oklch(72.36% .176 350.048);--color-accent-content:oklch(14.472% .035 350.048);--color-neutral:oklch(27.949% .036 260.03);--color-neutral-content:oklch(85.589% .007 260.03);--color-info:oklch(68.455% .148 237.251);--color-info-content:oklch(0% 0 0);--color-success:oklch(78.452% .132 181.911);--color-success-content:oklch(15.69% .026 181.911);--color-warning:oklch(83.242% .139 82.95);--color-warning-content:oklch(16.648% .027 82.95);--color-error:oklch(71.785% .17 13.118);--color-error-content:oklch(14.357% .034 13.118);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=coffee]:checked),[data-theme=coffee]{color-scheme:dark;--color-base-100:oklch(24% .023 329.708);--color-base-200:oklch(21% .021 329.708);--color-base-300:oklch(16% .019 329.708);--color-base-content:oklch(72.354% .092 79.129);--color-primary:oklch(71.996% .123 62.756);--color-primary-content:oklch(14.399% .024 62.756);--color-secondary:oklch(34.465% .029 199.194);--color-secondary-content:oklch(86.893% .005 199.194);--color-accent:oklch(42.621% .074 224.389);--color-accent-content:oklch(88.524% .014 224.389);--color-neutral:oklch(16.51% .015 326.261);--color-neutral-content:oklch(83.302% .003 326.261);--color-info:oklch(79.49% .063 184.558);--color-info-content:oklch(15.898% .012 184.558);--color-success:oklch(74.722% .072 131.116);--color-success-content:oklch(14.944% .014 131.116);--color-warning:oklch(88.15% .14 87.722);--color-warning-content:oklch(17.63% .028 87.722);--color-error:oklch(77.318% .128 31.871);--color-error-content:oklch(15.463% .025 31.871);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=winter]:checked),[data-theme=winter]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97.466% .011 259.822);--color-base-300:oklch(93.268% .016 262.751);--color-base-content:oklch(41.886% .053 255.824);--color-primary:oklch(56.86% .255 257.57);--color-primary-content:oklch(91.372% .051 257.57);--color-secondary:oklch(42.551% .161 282.339);--color-secondary-content:oklch(88.51% .032 282.339);--color-accent:oklch(59.939% .191 335.171);--color-accent-content:oklch(11.988% .038 335.171);--color-neutral:oklch(19.616% .063 257.651);--color-neutral-content:oklch(83.923% .012 257.651);--color-info:oklch(88.127% .085 214.515);--color-info-content:oklch(17.625% .017 214.515);--color-success:oklch(80.494% .077 197.823);--color-success-content:oklch(16.098% .015 197.823);--color-warning:oklch(89.172% .045 71.47);--color-warning-content:oklch(17.834% .009 71.47);--color-error:oklch(73.092% .11 20.076);--color-error-content:oklch(14.618% .022 20.076);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=dim]:checked),[data-theme=dim]{color-scheme:dark;--color-base-100:oklch(30.857% .023 264.149);--color-base-200:oklch(28.036% .019 264.182);--color-base-300:oklch(26.346% .018 262.177);--color-base-content:oklch(82.901% .031 222.959);--color-primary:oklch(86.133% .141 139.549);--color-primary-content:oklch(17.226% .028 139.549);--color-secondary:oklch(73.375% .165 35.353);--color-secondary-content:oklch(14.675% .033 35.353);--color-accent:oklch(74.229% .133 311.379);--color-accent-content:oklch(14.845% .026 311.379);--color-neutral:oklch(24.731% .02 264.094);--color-neutral-content:oklch(82.901% .031 222.959);--color-info:oklch(86.078% .142 206.182);--color-info-content:oklch(17.215% .028 206.182);--color-success:oklch(86.171% .142 166.534);--color-success-content:oklch(17.234% .028 166.534);--color-warning:oklch(86.163% .142 94.818);--color-warning-content:oklch(17.232% .028 94.818);--color-error:oklch(82.418% .099 33.756);--color-error-content:oklch(16.483% .019 33.756);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=nord]:checked),[data-theme=nord]{color-scheme:light;--color-base-100:oklch(95.127% .007 260.731);--color-base-200:oklch(93.299% .01 261.788);--color-base-300:oklch(89.925% .016 262.749);--color-base-content:oklch(32.437% .022 264.182);--color-primary:oklch(59.435% .077 254.027);--color-primary-content:oklch(11.887% .015 254.027);--color-secondary:oklch(69.651% .059 248.687);--color-secondary-content:oklch(13.93% .011 248.687);--color-accent:oklch(77.464% .062 217.469);--color-accent-content:oklch(15.492% .012 217.469);--color-neutral:oklch(45.229% .035 264.131);--color-neutral-content:oklch(89.925% .016 262.749);--color-info:oklch(69.207% .062 332.664);--color-info-content:oklch(13.841% .012 332.664);--color-success:oklch(76.827% .074 131.063);--color-success-content:oklch(15.365% .014 131.063);--color-warning:oklch(85.486% .089 84.093);--color-warning-content:oklch(17.097% .017 84.093);--color-error:oklch(60.61% .12 15.341);--color-error-content:oklch(12.122% .024 15.341);--radius-selector:1rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=sunset]:checked),[data-theme=sunset]{color-scheme:dark;--color-base-100:oklch(22% .019 237.69);--color-base-200:oklch(20% .019 237.69);--color-base-300:oklch(18% .019 237.69);--color-base-content:oklch(77.383% .043 245.096);--color-primary:oklch(74.703% .158 39.947);--color-primary-content:oklch(14.94% .031 39.947);--color-secondary:oklch(72.537% .177 2.72);--color-secondary-content:oklch(14.507% .035 2.72);--color-accent:oklch(71.294% .166 299.844);--color-accent-content:oklch(14.258% .033 299.844);--color-neutral:oklch(26% .019 237.69);--color-neutral-content:oklch(70% .019 237.69);--color-info:oklch(85.559% .085 206.015);--color-info-content:oklch(17.111% .017 206.015);--color-success:oklch(85.56% .085 144.778);--color-success-content:oklch(17.112% .017 144.778);--color-warning:oklch(85.569% .084 74.427);--color-warning-content:oklch(17.113% .016 74.427);--color-error:oklch(85.511% .078 16.886);--color-error-content:oklch(17.102% .015 16.886);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=caramellatte]:checked),[data-theme=caramellatte]{color-scheme:light;--color-base-100:oklch(98% .016 73.684);--color-base-200:oklch(95% .038 75.164);--color-base-300:oklch(90% .076 70.697);--color-base-content:oklch(40% .123 38.172);--color-primary:oklch(0% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(22.45% .075 37.85);--color-secondary-content:oklch(90% .076 70.697);--color-accent:oklch(46.44% .111 37.85);--color-accent-content:oklch(90% .076 70.697);--color-neutral:oklch(55% .195 38.402);--color-neutral-content:oklch(98% .016 73.684);--color-info:oklch(42% .199 265.638);--color-info-content:oklch(90% .076 70.697);--color-success:oklch(43% .095 166.913);--color-success-content:oklch(90% .076 70.697);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(39% .141 25.723);--radius-selector:2rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:1}:root:has(input.theme-controller[value=abyss]:checked),[data-theme=abyss]{color-scheme:dark;--color-base-100:oklch(20% .08 209);--color-base-200:oklch(15% .08 209);--color-base-300:oklch(10% .08 209);--color-base-content:oklch(90% .076 70.697);--color-primary:oklch(92% .2653 125);--color-primary-content:oklch(50% .2653 125);--color-secondary:oklch(83.27% .0764 298.3);--color-secondary-content:oklch(43.27% .0764 298.3);--color-accent:oklch(43% 0 0);--color-accent-content:oklch(98% 0 0);--color-neutral:oklch(30% .08 209);--color-neutral-content:oklch(90% .076 70.697);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(79% .209 151.711);--color-success-content:oklch(26% .065 152.934);--color-warning:oklch(84.8% .1962 84.62);--color-warning-content:oklch(44.8% .1962 84.62);--color-error:oklch(65% .1985 24.22);--color-error-content:oklch(27% .1985 24.22);--radius-selector:2rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=silk]:checked),[data-theme=silk]{color-scheme:light;--color-base-100:oklch(97% .0035 67.78);--color-base-200:oklch(95% .0081 61.42);--color-base-300:oklch(90% .0081 61.42);--color-base-content:oklch(40% .0081 61.42);--color-primary:oklch(23.27% .0249 284.3);--color-primary-content:oklch(94.22% .2505 117.44);--color-secondary:oklch(23.27% .0249 284.3);--color-secondary-content:oklch(73.92% .2135 50.94);--color-accent:oklch(23.27% .0249 284.3);--color-accent-content:oklch(88.92% .2061 189.9);--color-neutral:oklch(20% 0 0);--color-neutral-content:oklch(80% .0081 61.42);--color-info:oklch(80.39% .1148 241.68);--color-info-content:oklch(30.39% .1148 241.68);--color-success:oklch(83.92% .0901 136.87);--color-success-content:oklch(23.92% .0901 136.87);--color-warning:oklch(83.92% .1085 80);--color-warning-content:oklch(43.92% .1085 80);--color-error:oklch(75.1% .1814 22.37);--color-error-content:oklch(35.1% .1814 22.37);--radius-selector:2rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:0}:root{--fx-noise:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E");scrollbar-color:currentColor #0000}@supports (color:color-mix(in lab, red, red)){:root{scrollbar-color:color-mix(in oklch, currentColor 35%, #0000) #0000}}@property --radialprogress{syntax:"";inherits:true;initial-value:0%}:root:not(span){overflow:var(--page-overflow)}:root{background:var(--page-scroll-bg,var(--root-bg));--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000), var(--root-bg,#0000)) var(--root-bg,#0000)}@supports (color:color-mix(in lab, red, red)){:root{--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000), var(--root-bg,#0000)) color-mix(in srgb, var(--root-bg,#0000), oklch(0% 0 0) calc(var(--page-has-backdrop,0) * 40%))}}:root{--page-scroll-transition-on:background-color .3s ease-out;transition:var(--page-scroll-transition);scrollbar-gutter:var(--page-scroll-gutter,unset);scrollbar-gutter:if(style(--page-has-scroll: 1): var(--page-scroll-gutter,unset) ; else: unset)}@keyframes set-page-has-scroll{0%,to{--page-has-scroll:1}}:root,[data-theme]{background:var(--page-scroll-bg,var(--root-bg));color:var(--color-base-content)}:where(:root,[data-theme]){--root-bg:var(--color-base-100)}}@layer components;@layer utilities{@layer daisyui.l1.l2.l3{.modal{pointer-events:none;visibility:hidden;width:100%;max-width:none;height:100%;max-height:none;color:inherit;transition:visibility .3s allow-discrete, background-color .3s ease-out, opacity .1s ease-out;overscroll-behavior:contain;z-index:999;scrollbar-gutter:auto;background-color:#0000;place-items:center;margin:0;padding:0;display:grid;position:fixed;inset:0;overflow:clip}.modal::backdrop{display:none}.tooltip{--tt-bg:var(--color-neutral);--tt-off:calc(100% + .5rem);--tt-tail:calc(100% + 1px + .25rem);display:inline-block;position:relative}.tooltip>.tooltip-content,.tooltip[data-tip]:before{border-radius:var(--radius-field);text-align:center;white-space:normal;max-width:20rem;color:var(--color-neutral-content);opacity:0;background-color:var(--tt-bg);pointer-events:none;z-index:2;--tw-content:attr(data-tip);content:var(--tw-content);width:max-content;padding-block:.25rem;padding-inline:.5rem;font-size:.875rem;line-height:1.25;position:absolute}.tooltip:after{opacity:0;background-color:var(--tt-bg);content:"";pointer-events:none;--mask-tooltip:url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A");width:.625rem;height:.25rem;-webkit-mask-position:-1px 0;mask-position:-1px 0;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-image:var(--mask-tooltip);-webkit-mask-image:var(--mask-tooltip);mask-image:var(--mask-tooltip);display:block;position:absolute}@media (prefers-reduced-motion:no-preference){.tooltip>.tooltip-content,.tooltip[data-tip]:before,.tooltip:after{transition:opacity .2s cubic-bezier(.4,0,.2,1) 75ms,transform .2s cubic-bezier(.4,0,.2,1) 75ms}}:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))>.tooltip-content,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))[data-tip]:before,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible)):after{opacity:1;--tt-pos:0rem}@media (prefers-reduced-motion:no-preference){:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))>.tooltip-content,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))[data-tip]:before,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible)):after{transition:opacity .2s cubic-bezier(.4,0,.2,1),transform .2s cubic-bezier(.4,0,.2,1)}}.tab{cursor:pointer;appearance:none;text-align:center;webkit-user-select:none;-webkit-user-select:none;user-select:none;flex-wrap:wrap;justify-content:center;align-items:center;display:inline-flex;position:relative}@media (hover:hover){.tab:hover{color:var(--color-base-content)}}.tab{--tab-p:.75rem;--tab-bg:var(--color-base-100);--tab-border-color:var(--color-base-300);--tab-radius-ss:0;--tab-radius-se:0;--tab-radius-es:0;--tab-radius-ee:0;--tab-order:0;--tab-radius-min:calc(.75rem - var(--border));--tab-radius-limit:min(var(--radius-field), var(--tab-radius-min));--tab-radius-grad:#0000 calc(69% - var(--border)), var(--tab-border-color) calc(69% - var(--border) + .25px), var(--tab-border-color) 69%, var(--tab-bg) calc(69% + .25px);order:var(--tab-order);height:var(--tab-height);padding-inline:var(--tab-p);border-color:#0000;font-size:.875rem}.tab:is(input[type=radio]){min-width:fit-content}.tab:is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.tab:is(label){position:relative}.tab:is(label) input{cursor:pointer;appearance:none;opacity:0;position:absolute;inset:0}:is(.tab:checked,.tab:is(label:has(:checked)),.tab:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]))+.tab-content{display:block}.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:color-mix(in oklab, var(--color-base-content) 50%, transparent)}}.tab:not(input):empty{cursor:default;flex-grow:1}.tab:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.tab:focus{outline-offset:2px;outline:2px solid #0000}}.tab:focus-visible,.tab:is(label:has(:checked:focus-visible)){outline-offset:-5px;outline:2px solid}.tab[disabled]{pointer-events:none;opacity:.4}.menu{--menu-active-fg:var(--color-neutral-content);--menu-active-bg:var(--color-neutral);flex-flow:column wrap;width:fit-content;padding:.5rem;font-size:.875rem;display:flex}.menu :where(li ul){white-space:nowrap;margin-inline-start:1rem;padding-inline-start:.5rem;position:relative}.menu :where(li ul):before{background-color:var(--color-base-content);opacity:.1;width:var(--border);content:"";inset-inline-start:0;position:absolute;top:.75rem;bottom:.75rem}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--radius-field);text-align:start;text-wrap:balance;-webkit-user-select:none;user-select:none;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;align-content:flex-start;align-items:center;gap:.5rem;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:grid}.menu :where(li>details>summary){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li>details>summary){outline-offset:2px;outline:2px solid #0000}}.menu :where(li>details>summary)::-webkit-details-marker{display:none}:is(.menu :where(li>details>summary),.menu :where(li>.menu-dropdown-toggle)):after{content:"";transform-origin:50%;pointer-events:none;justify-self:flex-end;width:.375rem;height:.375rem;transition-property:rotate,translate;transition-duration:.2s;display:block;translate:0 -1px;rotate:-135deg;box-shadow:inset 2px 2px}.menu details{interpolate-size:allow-keywords;overflow:hidden}.menu details::details-content{block-size:0}@media (prefers-reduced-motion:no-preference){.menu details::details-content{transition-behavior:allow-discrete;transition-property:block-size,content-visibility;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.menu details[open]::details-content{block-size:auto}.menu :where(li>details[open]>summary):after,.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after{translate:0 1px;rotate:45deg}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{color:var(--color-base-content);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){box-shadow:inset 0 1px oklch(0% 0 0/.01),inset 0 -1px oklch(100% 0 0/.01)}.menu :where(li:empty){background-color:var(--color-base-content);opacity:.1;height:1px;margin:.5rem 1rem}.menu :where(li){flex-flow:column wrap;flex-shrink:0;align-items:stretch;display:flex;position:relative}.menu :where(li) .badge{justify-self:flex-end}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{outline-offset:2px;outline:2px solid #0000}}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{color:var(--menu-active-fg);background-color:var(--menu-active-bg);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise)}:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):not(:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):active){box-shadow:0 2px calc(var(--depth) * 3px) -2px var(--menu-active-bg)}.menu :where(li).menu-disabled{pointer-events:none;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li).menu-disabled{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.menu .dropdown:focus-within .menu-dropdown-toggle:after{translate:0 1px;rotate:45deg}.menu .dropdown-content{margin-top:.5rem;padding:.5rem}.menu .dropdown-content:before{display:none}.dropdown{position-area:var(--anchor-v,bottom) var(--anchor-h,span-right);display:inline-block;position:relative}.dropdown>:not(:has(~[class*=dropdown-content])):focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.dropdown>:not(:has(~[class*=dropdown-content])):focus{outline-offset:2px;outline:2px solid #0000}}.dropdown .dropdown-content{position:absolute}.dropdown.dropdown-close .dropdown-content,.dropdown:not(details,.dropdown-open,.dropdown-hover:hover,:focus-within) .dropdown-content,.dropdown.dropdown-hover:not(:hover) [tabindex]:first-child:focus:not(:focus-visible)~.dropdown-content{transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover],.dropdown .dropdown-content{z-index:999}@media (prefers-reduced-motion:no-preference){.dropdown[popover],.dropdown .dropdown-content{transition-behavior:allow-discrete;transition-property:opacity,scale,display;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);animation:.2s dropdown}}@starting-style{.dropdown[popover],.dropdown .dropdown-content{opacity:0;scale:.95}}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within)>[tabindex]:first-child{pointer-events:none}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within) .dropdown-content,.dropdown:not(.dropdown-close).dropdown-hover:hover .dropdown-content{opacity:1;scale:1}.dropdown:is(details) summary::-webkit-details-marker{display:none}.dropdown:where([popover]){background:0 0}.dropdown[popover]{color:inherit;position:fixed}@supports not (position-area:bottom){.dropdown[popover]{margin:auto}.dropdown[popover].dropdown-close{transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover].dropdown-open:not(:popover-open){transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover]::backdrop{background-color:oklab(0% none none/.3)}}:is(.dropdown[popover].dropdown-close,.dropdown[popover]:not(.dropdown-open,:popover-open)){transform-origin:top;opacity:0;display:none;scale:.95}:where(.btn){width:unset}.btn{cursor:pointer;text-align:center;vertical-align:middle;outline-offset:2px;webkit-user-select:none;-webkit-user-select:none;user-select:none;padding-inline:var(--btn-p);color:var(--btn-fg);--tw-prose-links:var(--btn-fg);height:var(--size);font-size:var(--fontsize,.875rem);outline-color:var(--btn-color,var(--color-base-content));background-color:var(--btn-bg);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--btn-noise);border-width:var(--border);border-style:solid;border-color:var(--btn-border);text-shadow:0 .5px oklch(100% 0 0 / calc(var(--depth) * .15));touch-action:manipulation;box-shadow:0 .5px 0 .5px oklch(100% 0 0 / calc(var(--depth) * 6%)) inset, var(--btn-shadow);--size:calc(var(--size-field,.25rem) * 10);--btn-bg:var(--btn-color,var(--color-base-200));--btn-fg:var(--color-base-content);--btn-p:1rem;--btn-border:var(--btn-bg);border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-wrap:nowrap;flex-shrink:0;justify-content:center;align-items:center;gap:.375rem;font-weight:600;transition-property:color,background-color,border-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:inline-flex}@supports (color:color-mix(in lab, red, red)){.btn{--btn-border:color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%))}}.btn{--btn-shadow:0 3px 2px -2px var(--btn-bg), 0 4px 3px -2px var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn{--btn-shadow:0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000), 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000)}}.btn{--btn-noise:var(--fx-noise)}@media (hover:hover){.btn:hover{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:hover{--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}}.btn:focus-visible,.btn:has(:focus-visible){isolation:isolate;outline-width:2px;outline-style:solid}.btn:active:not(.btn-active){--btn-bg:var(--btn-color,var(--color-base-200));translate:0 .5px}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 5%)}}.btn:active:not(.btn-active){--btn-border:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-border:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}.btn:active:not(.btn-active){--btn-shadow:0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0)}.btn:is(input[type=checkbox],input[type=radio]){appearance:none}.btn:is(input[type=checkbox],input[type=radio])[aria-label]:after{--tw-content:attr(aria-label);content:var(--tw-content)}.btn:where(input:checked:not(.filter .btn)){--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content);isolation:isolate}.loading{pointer-events:none;aspect-ratio:1;vertical-align:middle;width:calc(var(--size-selector,.25rem) * 6);background-color:currentColor;display:inline-block;-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:100%;mask-size:100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.collapse{border-radius:var(--radius-box,1rem);isolation:isolate;grid-template-rows:max-content 0fr;grid-template-columns:minmax(0,1fr);width:100%;display:grid;position:relative;overflow:hidden}@media (prefers-reduced-motion:no-preference){.collapse{transition:grid-template-rows .2s}}.collapse>input:is([type=checkbox],[type=radio]){appearance:none;opacity:0;z-index:1;grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close)),.collapse:not(.collapse-close):has(>input:is([type=checkbox],[type=radio]):checked){grid-template-rows:max-content 1fr}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){content-visibility:visible;min-height:fit-content}@supports not (content-visibility:visible){.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){visibility:visible}}.collapse:focus-visible,.collapse:has(>input:is([type=checkbox],[type=radio]):focus-visible),.collapse:has(summary:focus-visible){outline-color:var(--color-base-content);outline-offset:2px;outline-width:2px;outline-style:solid}.collapse:not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-close)>input[type=radio]:not(:checked),.collapse:not(.collapse-close)>.collapse-title{cursor:pointer}:is(.collapse[tabindex]:focus:not(.collapse-close,.collapse[open]),.collapse[tabindex]:focus-within:not(.collapse-close,.collapse[open]))>.collapse-title{cursor:unset}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){padding-bottom:1rem}.collapse:is(details){width:100%}@media (prefers-reduced-motion:no-preference){.collapse:is(details)::details-content{transition:content-visibility .2s allow-discrete, visibility .2s allow-discrete, min-height .2s ease-out allow-discrete, padding .1s ease-out 20ms, background-color .2s ease-out, height .2s;interpolate-size:allow-keywords;height:0}.collapse:is(details):where([open])::details-content{height:auto}}.collapse:is(details) summary{display:block;position:relative}.collapse:is(details) summary::-webkit-details-marker{display:none}.collapse:is(details)>.collapse-content{content-visibility:visible}.collapse:is(details) summary{outline:none}.collapse-content{content-visibility:hidden;min-height:0;cursor:unset;grid-row-start:2;grid-column-start:1;padding-left:1rem;padding-right:1rem}@supports not (content-visibility:hidden){.collapse-content{visibility:hidden}}@media (prefers-reduced-motion:no-preference){.collapse-content{transition:content-visibility .2s allow-discrete, visibility .2s allow-discrete, min-height .2s ease-out allow-discrete, padding .1s ease-out 20ms, background-color .2s ease-out}}.validator-hint{visibility:hidden;margin-top:.5rem;font-size:.75rem}.validator:user-valid{--input-color:var(--color-success)}.validator:user-valid:focus{--input-color:var(--color-success)}.validator:user-valid:checked{--input-color:var(--color-success)}.validator:user-valid[aria-checked=true]{--input-color:var(--color-success)}.validator:user-valid:focus-within{--input-color:var(--color-success)}.validator:has(:user-valid){--input-color:var(--color-success)}.validator:has(:user-valid):focus{--input-color:var(--color-success)}.validator:has(:user-valid):checked{--input-color:var(--color-success)}.validator:has(:user-valid)[aria-checked=true]{--input-color:var(--color-success)}.validator:has(:user-valid):focus-within{--input-color:var(--color-success)}.validator:user-invalid{--input-color:var(--color-error)}.validator:user-invalid:focus{--input-color:var(--color-error)}.validator:user-invalid:checked{--input-color:var(--color-error)}.validator:user-invalid[aria-checked=true]{--input-color:var(--color-error)}.validator:user-invalid:focus-within{--input-color:var(--color-error)}.validator:user-invalid~.validator-hint{visibility:visible;color:var(--color-error)}.validator:has(:user-invalid){--input-color:var(--color-error)}.validator:has(:user-invalid):focus{--input-color:var(--color-error)}.validator:has(:user-invalid):checked{--input-color:var(--color-error)}.validator:has(:user-invalid)[aria-checked=true]{--input-color:var(--color-error)}.validator:has(:user-invalid):focus-within{--input-color:var(--color-error)}.validator:has(:user-invalid)~.validator-hint{visibility:visible;color:var(--color-error)}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))),:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):focus,:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):checked,:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))[aria-checked=true],:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):focus-within{--input-color:var(--color-error)}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))~.validator-hint{visibility:visible;color:var(--color-error)}.list{flex-direction:column;font-size:.875rem;display:flex}.list .list-row{--list-grid-cols:minmax(0, auto) 1fr;border-radius:var(--radius-box);word-break:break-word;grid-auto-flow:column;grid-template-columns:var(--list-grid-cols);gap:1rem;padding:1rem;display:grid;position:relative}:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{content:"";border-bottom:var(--border) solid;inset-inline:var(--radius-box);border-color:var(--color-base-content);position:absolute;bottom:0}@supports (color:color-mix(in lab, red, red)){:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{border-color:color-mix(in oklab, var(--color-base-content) 5%, transparent)}}.toast{translate:var(--toast-x,0) var(--toast-y,0);inset-inline:auto 1rem;background-color:#0000;flex-direction:column;gap:.5rem;width:max-content;max-width:calc(100vw - 2rem);display:flex;position:fixed;top:auto;bottom:1rem}@media (prefers-reduced-motion:no-preference){.toast>*{animation:.25s ease-out toast}}.toggle{border:var(--border) solid currentColor;color:var(--input-color);cursor:pointer;appearance:none;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)));padding:var(--toggle-p);flex-shrink:0;grid-template-columns:0fr 1fr 1fr;place-content:center;display:inline-grid;position:relative;box-shadow:inset 0 1px}@supports (color:color-mix(in lab, red, red)){.toggle{box-shadow:0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset}}.toggle{--input-color:var(--color-base-content);transition:color .3s,grid-template-columns .2s}@supports (color:color-mix(in lab, red, red)){.toggle{--input-color:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}.toggle{--toggle-p:calc(var(--size) * .125);--size:calc(var(--size-selector,.25rem) * 6);width:calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2);height:var(--size)}.toggle>*{z-index:1;cursor:pointer;appearance:none;background-color:#0000;border:none;grid-column:2/span 1;grid-row-start:1;height:100%;padding:.125rem;transition:opacity .2s,rotate .4s}.toggle>:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.toggle>:focus{outline-offset:2px;outline:2px solid #0000}}.toggle>:nth-child(2){color:var(--color-base-100);rotate:0deg}.toggle>:nth-child(3){color:var(--color-base-100);opacity:0;rotate:-15deg}.toggle:has(:checked)>:nth-child(2){opacity:0;rotate:15deg}.toggle:has(:checked)>:nth-child(3){opacity:1;rotate:0deg}.toggle:before{aspect-ratio:1;border-radius:var(--radius-selector);--tw-content:"";content:var(--tw-content);width:100%;height:100%;box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor;background-color:currentColor;grid-row-start:1;grid-column-start:2;transition:background-color .1s,translate .2s,inset-inline-start .2s;position:relative;inset-inline-start:0;translate:0}@supports (color:color-mix(in lab, red, red)){.toggle:before{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000)}}.toggle:before{background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise)}@media (forced-colors:active){.toggle:before{outline-style:var(--tw-outline-style);outline-offset:calc(1px * -1);outline-width:1px}}@media print{.toggle:before{outline-offset:-1rem;outline:.25rem solid}}.toggle:focus-visible,.toggle:has(:focus-visible){outline-offset:2px;outline:2px solid}.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked){background-color:var(--color-base-100);--input-color:var(--color-base-content);grid-template-columns:1fr 1fr 0fr}:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{background-color:currentColor}@starting-style{:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{opacity:0}}.toggle:indeterminate{grid-template-columns:.5fr 1fr .5fr}.toggle:disabled{cursor:not-allowed;opacity:.3}.toggle:disabled:before{border:var(--border) solid currentColor;background-color:#0000}.input{cursor:text;border:var(--border) solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;white-space:nowrap;width:clamp(3rem,20rem,100%);height:var(--size);font-size:max(var(--font-size,.875rem), .875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.5rem;padding-inline:.75rem;display:inline-flex;position:relative}@supports (color:color-mix(in lab, red, red)){.input{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.input{--size:calc(var(--size-field,.25rem) * 10);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.input:where(input){display:inline-flex}.input :where(input){appearance:none;background-color:#0000;border:none;width:100%;height:100%;display:inline-flex}.input :where(input):focus,.input :where(input):focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.input :where(input):focus,.input :where(input):focus-within{outline-offset:2px;outline:2px solid #0000}}.input :where(input[type=url]),.input :where(input[type=email]){direction:ltr}.input :where(input[type=date]){display:inline-flex}.input:focus,.input:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.input:focus,.input:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.input:focus,.input:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.input:focus,.input:focus-within{--font-size:1rem}}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{box-shadow:none}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.input[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input::-webkit-calendar-picker-indicator{position:absolute;inset-inline-end:.75em}.input:has(>input[type=date]) :where(input[type=date]){webkit-appearance:none;appearance:none;display:inline-flex}.input:has(>input[type=date]) input[type=date]::-webkit-calendar-picker-indicator{cursor:pointer;width:1em;height:1em;position:absolute;inset-inline-end:.75em}.table{border-collapse:separate;--tw-border-spacing-x:calc(.25rem * 0);--tw-border-spacing-y:calc(.25rem * 0);width:100%;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y);border-radius:var(--radius-box);text-align:left;font-size:.875rem;position:relative}.table:where(:dir(rtl),[dir=rtl],[dir=rtl] *){text-align:right}@media (hover:hover){:is(.table tr.row-hover,.table tr.row-hover:nth-child(2n)):hover{background-color:var(--color-base-200)}}.table :where(th,td){vertical-align:middle;padding-block:.75rem;padding-inline:1rem}.table :where(thead,tfoot){white-space:nowrap;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead,tfoot){color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.table :where(thead,tfoot){font-size:.875rem;font-weight:600}.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)}}.table :where(.table-pin-rows thead tr){z-index:1;background-color:var(--color-base-100);position:sticky;top:0}.table :where(.table-pin-rows tfoot tr){z-index:1;background-color:var(--color-base-100);position:sticky;bottom:0}.table :where(.table-pin-cols tr th){background-color:var(--color-base-100);position:sticky;left:0;right:0}.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)}}.steps{counter-reset:step;grid-auto-columns:1fr;grid-auto-flow:column;display:inline-grid;overflow:auto hidden}.steps .step{text-align:center;--step-bg:var(--color-base-300);--step-fg:var(--color-base-content);grid-template-rows:40px 1fr;grid-template-columns:auto;place-items:center;min-width:4rem;display:grid}.steps .step:before{width:100%;height:.5rem;color:var(--step-bg);background-color:var(--step-bg);content:"";border:1px solid;grid-row-start:1;grid-column-start:1;margin-inline-start:-100%;top:0}.steps .step>.step-icon,.steps .step:not(:has(.step-icon)):after{--tw-content:counter(step);content:var(--tw-content);counter-increment:step;z-index:1;color:var(--step-fg);background-color:var(--step-bg);border:1px solid var(--step-bg);border-radius:3.40282e38px;grid-row-start:1;grid-column-start:1;place-self:center;place-items:center;width:2rem;height:2rem;display:grid;position:relative}.steps .step:first-child:before{--tw-content:none;content:var(--tw-content)}.steps .step[data-content]:after{--tw-content:attr(data-content);content:var(--tw-content)}.range{appearance:none;webkit-appearance:none;--range-thumb:var(--color-base-100);--range-thumb-size:calc(var(--size-selector,.25rem) * 6);--range-progress:currentColor;--range-fill:1;--range-p:.25rem;--range-bg:currentColor}@supports (color:color-mix(in lab, red, red)){.range{--range-bg:color-mix(in oklab, currentColor 10%, #0000)}}.range{cursor:pointer;vertical-align:middle;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));width:clamp(3rem,20rem,100%);height:var(--range-thumb-size);background-color:#0000;border:none;overflow:hidden}[dir=rtl] .range{--range-dir:-1}.range:focus{outline:none}.range:focus-visible{outline-offset:2px;outline:2px solid}.range::-webkit-slider-runnable-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size) * .5)}@media (forced-colors:active){.range::-webkit-slider-runnable-track{border:1px solid}.range::-moz-range-track{border:1px solid}}.range::-webkit-slider-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));background-color:var(--range-thumb);height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p) solid;appearance:none;webkit-appearance:none;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill));position:relative;top:50%;transform:translateY(-50%)}@supports (color:color-mix(in lab, red, red)){.range::-webkit-slider-thumb{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill))}}.range::-moz-range-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size) * .5)}.range::-moz-range-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p) solid;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill));background-color:currentColor;position:relative;top:50%}@supports (color:color-mix(in lab, red, red)){.range::-moz-range-thumb{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill))}}.range:disabled{cursor:not-allowed;opacity:.3}.select{border:var(--border) solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);height:var(--size);touch-action:manipulation;white-space:nowrap;text-overflow:ellipsis;box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;background-image:linear-gradient(45deg,#0000 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,#0000 50%);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-repeat:no-repeat;background-size:4px 4px,4px 4px;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.375rem;padding-inline:.75rem 1.75rem;font-size:.875rem;display:inline-flex;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.select{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.select{border-color:var(--input-color);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.select{--size:calc(var(--size-field,.25rem) * 10)}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}[dir=rtl] .select::picker(select){translate:.5rem}[dir=rtl] .select select::picker(select){translate:.5rem}.select[multiple]{background-image:none;height:auto;padding-block:.75rem;padding-inline-end:.75rem;overflow:auto}.select select{appearance:none;width:calc(100% + 2.75rem);height:calc(100% - calc(var(--border) * 2));background:inherit;border-radius:inherit;border-style:none;align-items:center;margin-inline:-.75rem -1.75rem;padding-inline:.75rem 1.75rem}.select select:focus,.select select:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.select select:focus,.select select:focus-within{outline-offset:2px;outline:2px solid #0000}}.select select:not(:last-child){background-image:none;margin-inline-end:-1.375rem}.select:focus,.select:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.select:focus,.select:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.select:focus,.select:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.select:has(>select[disabled])>select[disabled]{cursor:not-allowed}@supports (appearance:base-select){.select,.select select{appearance:base-select}:is(.select,.select select)::picker(select){appearance:base-select}}:is(.select,.select select)::picker(select){color:inherit;border:var(--border) solid var(--color-base-200);border-radius:var(--radius-box);background-color:inherit;max-height:min(24rem,70dvh);box-shadow:0 2px calc(var(--depth) * 3px) -2px oklch(0% 0 0/.2);box-shadow:0 20px 25px -5px rgb(0 0 0/calc(var(--depth) * .1)), 0 8px 10px -6px rgb(0 0 0/calc(var(--depth) * .1));margin-block:.5rem;margin-inline:.5rem;padding:.5rem;translate:-.5rem}:is(.select,.select select)::picker-icon{display:none}:is(.select,.select select) optgroup{padding-top:.5em}:is(.select,.select select) optgroup option:first-child{margin-top:.5em}:is(.select,.select select) option{border-radius:var(--radius-field);white-space:normal;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{outline-offset:2px;outline:2px solid #0000}}:is(.select,.select select) option:not(:disabled):active{background-color:var(--color-neutral);color:var(--color-neutral-content);box-shadow:0 2px calc(var(--depth) * 3px) -2px var(--color-neutral)}.swap{cursor:pointer;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;place-content:center;display:inline-grid;position:relative}.swap input{appearance:none;border:none}.swap>*{grid-row-start:1;grid-column-start:1}@media (prefers-reduced-motion:no-preference){.swap>*{transition-property:transform,rotate,opacity;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.swap .swap-on,.swap .swap-indeterminate,.swap input:indeterminate~.swap-on,.swap input:is(:checked,:indeterminate)~.swap-off{opacity:0}.swap input:checked~.swap-on,.swap input:indeterminate~.swap-indeterminate{opacity:1;backface-visibility:visible}.collapse-title{grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out;position:relative}.checkbox{border:var(--border) solid var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox{border:var(--border) solid var(--input-color,color-mix(in oklab, var(--color-base-content) 20%, #0000))}}.checkbox{cursor:pointer;appearance:none;border-radius:var(--radius-selector);vertical-align:middle;color:var(--color-base-content);box-shadow:0 1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 0 #0000 inset, 0 0 #0000;--size:calc(var(--size-selector,.25rem) * 6);width:var(--size);height:var(--size);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);flex-shrink:0;padding:.25rem;transition:background-color .2s,box-shadow .2s;display:inline-block;position:relative}.checkbox:before{--tw-content:"";content:var(--tw-content);opacity:0;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,70% 80%,70% 100%);width:100%;height:100%;box-shadow:0px 3px 0 0px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;background-color:currentColor;font-size:1rem;line-height:.75;transition:clip-path .3s .1s,opacity .1s .1s,rotate .3s .1s,translate .3s .1s;display:block;rotate:45deg}.checkbox:focus-visible{outline:2px solid var(--input-color,currentColor);outline-offset:2px}.checkbox:checked,.checkbox[aria-checked=true]{background-color:var(--input-color,#0000);box-shadow:0 0 #0000 inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * .1))}:is(.checkbox:checked,.checkbox[aria-checked=true]):before{clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 0%,70% 0%,70% 100%);opacity:1}@media (forced-colors:active){:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:0deg}}@media print{:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:0deg}}.checkbox:indeterminate{background-color:var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox:indeterminate{background-color:var(--input-color,color-mix(in oklab, var(--color-base-content) 20%, #0000))}}.checkbox:indeterminate:before{opacity:1;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,80% 80%,80% 100%);translate:0 -35%;rotate:0deg}.radio{cursor:pointer;appearance:none;vertical-align:middle;border:var(--border) solid var(--input-color,currentColor);border-radius:3.40282e38px;flex-shrink:0;padding:.25rem;display:inline-block;position:relative}@supports (color:color-mix(in lab, red, red)){.radio{border:var(--border) solid var(--input-color,color-mix(in srgb, currentColor 20%, #0000))}}.radio{box-shadow:0 1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset;--size:calc(var(--size-selector,.25rem) * 6);width:var(--size);height:var(--size);color:var(--input-color,currentColor)}.radio:before{--tw-content:"";content:var(--tw-content);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);border-radius:3.40282e38px;width:100%;height:100%;display:block}.radio:focus-visible{outline:2px solid}.radio:checked,.radio[aria-checked=true]{background-color:var(--color-base-100);border-color:currentColor}@media (prefers-reduced-motion:no-preference){.radio:checked,.radio[aria-checked=true]{animation:.2s ease-out radio}}:is(.radio:checked,.radio[aria-checked=true]):before{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * .1));background-color:currentColor}@media (forced-colors:active){:is(.radio:checked,.radio[aria-checked=true]):before{outline-style:var(--tw-outline-style);outline-offset:calc(1px * -1);outline-width:1px}}@media print{:is(.radio:checked,.radio[aria-checked=true]):before{outline-offset:-1rem;outline:.25rem solid}}.rating{vertical-align:middle;display:inline-flex;position:relative}.rating input{appearance:none;border:none}.rating :where(*){background-color:var(--color-base-content);opacity:.2;border-radius:0;width:1.5rem;height:1.5rem}@media (prefers-reduced-motion:no-preference){.rating :where(*){animation:.25s ease-out rating}}.rating :where(*):is(input){cursor:pointer}.rating .rating-hidden{background-color:#0000;width:.5rem}.rating input[type=radio]:checked{background-image:none}.rating :checked,.rating [aria-checked=true],.rating [aria-current=true],.rating :has(~:checked,~[aria-checked=true],~[aria-current=true]){opacity:1}.rating :focus-visible{scale:1.1}@media (prefers-reduced-motion:no-preference){.rating :focus-visible{transition:scale .2s ease-out}}.rating :active:focus{animation:none;scale:1.1}.navbar{align-items:center;width:100%;min-height:4rem;padding:.5rem;display:flex}.card{border-radius:var(--radius-box);outline-offset:2px;outline:0 solid #0000;flex-direction:column;transition:outline .2s ease-in-out;display:flex;position:relative}.card:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.card:focus{outline-offset:2px;outline:2px solid #0000}}.card:focus-visible{outline-color:currentColor}.card :where(figure:first-child){border-start-start-radius:inherit;border-start-end-radius:inherit;border-end-end-radius:unset;border-end-start-radius:unset;overflow:hidden}.card :where(figure:last-child){border-start-start-radius:unset;border-start-end-radius:unset;border-end-end-radius:inherit;border-end-start-radius:inherit;overflow:hidden}.card figure{justify-content:center;align-items:center;display:flex}.card:has(>input:is(input[type=checkbox],input[type=radio])){cursor:pointer;-webkit-user-select:none;user-select:none}.card:has(>:checked){outline:2px solid}.stats{border-radius:var(--radius-box);grid-auto-flow:column;display:inline-grid;position:relative;overflow-x:auto}.progress{appearance:none;border-radius:var(--radius-box);background-color:currentColor;width:100%;height:.5rem;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.progress{background-color:color-mix(in oklab, currentcolor 20%, transparent)}}.progress{color:var(--color-base-content)}.progress:indeterminate{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%}@media (prefers-reduced-motion:no-preference){.progress:indeterminate{animation:5s ease-in-out infinite progress}}@supports ((-moz-appearance:none)){.progress:indeterminate::-moz-progress-bar{background-color:#0000}@media (prefers-reduced-motion:no-preference){.progress:indeterminate::-moz-progress-bar{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%;animation:5s ease-in-out infinite progress}}.progress::-moz-progress-bar{border-radius:var(--radius-box);background-color:currentColor}}@supports ((-webkit-appearance:none)){.progress::-webkit-progress-bar{border-radius:var(--radius-box);background-color:#0000}.progress::-webkit-progress-value{border-radius:var(--radius-box);background-color:currentColor}}.textarea{border:var(--border) solid #0000;appearance:none;border-radius:var(--radius-field);background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);min-height:5rem;font-size:max(var(--font-size,.875rem), .875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;flex-shrink:1;padding-block:.5rem;padding-inline:.75rem}@supports (color:color-mix(in lab, red, red)){.textarea{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.textarea{--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.textarea textarea{appearance:none;background-color:#0000;border:none}.textarea textarea:focus,.textarea textarea:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.textarea textarea:focus,.textarea textarea:focus-within{outline-offset:2px;outline:2px solid #0000}}.textarea:focus,.textarea:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.textarea:focus,.textarea:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.textarea:focus,.textarea:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.textarea:focus,.textarea:focus-within{--font-size:1rem}}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){box-shadow:none}.textarea:has(>textarea[disabled])>textarea[disabled]{cursor:not-allowed}.tab-content{order:var(--tabcontent-order);--tabcontent-radius-ss:var(--radius-box);--tabcontent-radius-se:var(--radius-box);--tabcontent-radius-es:var(--radius-box);--tabcontent-radius-ee:var(--radius-box);--tabcontent-order:1;width:100%;height:calc(100% - var(--tab-height) + var(--border));margin:var(--tabcontent-margin);border-color:#0000;border-width:var(--border);border-start-start-radius:var(--tabcontent-radius-ss);border-start-end-radius:var(--tabcontent-radius-se);border-end-end-radius:var(--tabcontent-radius-ee);border-end-start-radius:var(--tabcontent-radius-es);display:none}.modal-box{background-color:var(--color-base-100);border-top-left-radius:var(--modal-tl,var(--radius-box));border-top-right-radius:var(--modal-tr,var(--radius-box));border-bottom-left-radius:var(--modal-bl,var(--radius-box));border-bottom-right-radius:var(--modal-br,var(--radius-box));opacity:0;overscroll-behavior:contain;grid-row-start:1;grid-column-start:1;width:91.6667%;max-width:32rem;max-height:100vh;padding:1.5rem;transition:translate .3s ease-out,scale .3s ease-out,opacity .2s ease-out 50ms,box-shadow .3s ease-out;overflow-y:auto;scale:.95;box-shadow:0 25px 50px -12px oklch(0% 0 0/.25)}.divider{white-space:nowrap;height:1rem;margin:var(--divider-m,1rem 0);--divider-color:var(--color-base-content);flex-direction:row;align-self:stretch;align-items:center;display:flex}@supports (color:color-mix(in lab, red, red)){.divider{--divider-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.divider:before,.divider:after{content:"";background-color:var(--divider-color);flex-grow:1;width:100%;height:.125rem}@media print{.divider:before,.divider:after{border:.5px solid}}.divider:not(:empty){gap:1rem}.filter{flex-wrap:wrap;display:flex}.filter input[type=radio]{width:auto}.filter input{opacity:1;transition:margin .1s,opacity .3s,padding .3s,border-width .1s;overflow:hidden;scale:1}.filter input:not(:last-child){margin-inline-end:.25rem}.filter input.filter-reset{aspect-ratio:1}.filter input.filter-reset:after{--tw-content:"×";content:var(--tw-content)}.filter:not(:has(input:checked:not(.filter-reset))) .filter-reset,.filter:not(:has(input:checked:not(.filter-reset))) input[type=reset],.filter:has(input:checked:not(.filter-reset)) input:not(:checked,.filter-reset,input[type=reset]){opacity:0;border-width:0;width:0;margin-inline:0;padding-inline:0;scale:0}.label{white-space:nowrap;color:currentColor;align-items:center;gap:.375rem;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.label{color:color-mix(in oklab, currentcolor 60%, transparent)}}.label:has(input){cursor:pointer}.label:is(.input>*,.select>*){white-space:nowrap;height:calc(100% - .5rem);font-size:inherit;align-items:center;padding-inline:.75rem;display:flex}.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border) solid currentColor;margin-inline:-.75rem .75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border) solid color-mix(in oklab, currentColor 10%, #0000)}}.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border) solid currentColor;margin-inline:.75rem -.75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border) solid color-mix(in oklab, currentColor 10%, #0000)}}.fieldset-legend{color:var(--color-base-content);justify-content:space-between;align-items:center;gap:.5rem;margin-bottom:-.25rem;padding-block:.5rem;font-weight:600;display:flex}.status{aspect-ratio:1;border-radius:var(--radius-selector);background-color:var(--color-base-content);width:.5rem;height:.5rem;display:inline-block}@supports (color:color-mix(in lab, red, red)){.status{background-color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.status{vertical-align:middle;color:#0000004d;background-position:50%;background-repeat:no-repeat}@supports (color:color-mix(in lab, red, red)){.status{color:color-mix(in oklab, var(--color-black) 30%, transparent)}}.status{background-image:radial-gradient(circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * .5)), #0000);box-shadow:0 2px 3px -1px}@supports (color:color-mix(in lab, red, red)){.status{box-shadow:0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), #0000)}}.badge{border-radius:var(--radius-selector);vertical-align:middle;color:var(--badge-fg);border:var(--border) solid var(--badge-color,var(--color-base-200));background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);background-color:var(--badge-bg);--badge-bg:var(--badge-color,var(--color-base-100));--badge-fg:var(--color-base-content);--size:calc(var(--size-selector,.25rem) * 6);width:fit-content;height:var(--size);padding-inline:calc(var(--size) / 2 - var(--border));justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;display:inline-flex}.footer{grid-auto-flow:row;place-items:start;gap:2.5rem 1rem;width:100%;font-size:.875rem;line-height:1.25rem;display:grid}.footer>*{place-items:start;gap:.5rem;display:grid}.footer.footer-center{text-align:center;grid-auto-flow:column dense;place-items:center}.footer.footer-center>*{place-items:center}.navbar-end{justify-content:flex-end;align-items:center;width:50%;display:inline-flex}.navbar-start{justify-content:flex-start;align-items:center;width:50%;display:inline-flex}.card-body{padding:var(--card-p,1.5rem);font-size:var(--card-fs,.875rem);flex-direction:column;flex:auto;gap:.5rem;display:flex}.card-body :where(p){flex-grow:1}.navbar-center{flex-shrink:0;align-items:center;display:inline-flex}.fieldset-label{color:var(--color-base-content);align-items:center;gap:.375rem;display:flex}@supports (color:color-mix(in lab, red, red)){.fieldset-label{color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.fieldset-label:has(input){cursor:pointer}.alert{--alert-border-color:var(--color-base-200);border-radius:var(--radius-box);color:var(--color-base-content);background-color:var(--alert-color,var(--color-base-200));text-align:start;background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);box-shadow:0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * .08)) inset, 0 1px #000, 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * .08));border-style:solid;grid-template-columns:auto;grid-auto-flow:column;justify-content:start;place-items:center start;gap:1rem;padding-block:.75rem;padding-inline:1rem;font-size:.875rem;line-height:1.25rem;display:grid}@supports (color:color-mix(in lab, red, red)){.alert{box-shadow:0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * .08)) inset, 0 1px color-mix(in oklab, color-mix(in oklab, #000 20%, var(--alert-color,var(--color-base-200))) calc(var(--depth) * 20%), #0000), 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * .08))}}.alert:has(:nth-child(2)){grid-template-columns:auto minmax(auto,1fr)}.fieldset{grid-template-columns:1fr;grid-auto-rows:max-content;gap:.375rem;padding-block:.25rem;font-size:.75rem;display:grid}.card-title{font-size:var(--cardtitle-fs,1.125rem);align-items:center;gap:.5rem;font-weight:600;display:flex}.mask{vertical-align:middle;display:inline-block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.skeleton{border-radius:var(--radius-box);background-color:var(--color-base-300)}@media (prefers-reduced-motion:reduce){.skeleton{transition-duration:15s}}.skeleton{will-change:background-position;background-image:linear-gradient(105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100%);background-position-x:-50%;background-size:200%}@media (prefers-reduced-motion:no-preference){.skeleton{animation:1.8s ease-in-out infinite skeleton}}.link{cursor:pointer;text-decoration-line:underline}.link:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.link:focus{outline-offset:2px;outline:2px solid #0000}}.link:focus-visible{outline-offset:2px;outline:2px solid}.btn-error{--btn-color:var(--color-error);--btn-fg:var(--color-error-content)}.btn-primary{--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content)}}@layer daisyui.l1.l2{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{pointer-events:auto;visibility:visible;opacity:1;transition:visibility 0s allow-discrete, background-color .3s ease-out, opacity .1s ease-out;background-color:oklch(0% 0 0/.4)}:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal) .modal-box{opacity:1;translate:0;scale:1}:root:has(:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal)){--page-has-backdrop:1;--page-overflow:hidden;--page-scroll-bg:var(--page-scroll-bg-on);--page-scroll-gutter:stable;--page-scroll-transition:var(--page-scroll-transition-on);animation:forwards set-page-has-scroll;animation-timeline:scroll()}@starting-style{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{opacity:0}}.tooltip>.tooltip-content,.tooltip[data-tip]:before{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-off) 50%}.tooltip:after{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-tail) 50%}.btn:disabled:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.btn:disabled:not(.btn-link,.btn-ghost){box-shadow:none}.btn:disabled{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled{--btn-fg:color-mix(in oklch, var(--color-base-content) 20%, #0000)}}.btn[disabled]:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.btn[disabled]:not(.btn-link,.btn-ghost){box-shadow:none}.btn[disabled]{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]{--btn-fg:color-mix(in oklch, var(--color-base-content) 20%, #0000)}}@media (prefers-reduced-motion:no-preference){.collapse[open].collapse-arrow>.collapse-title:after,.collapse.collapse-open.collapse-arrow>.collapse-title:after{transform:translateY(-50%)rotate(225deg)}}.collapse.collapse-open.collapse-plus>.collapse-title:after{--tw-content:"−";content:var(--tw-content)}:is(.collapse[tabindex].collapse-arrow:focus:not(.collapse-close),.collapse.collapse-arrow[tabindex]:focus-within:not(.collapse-close))>.collapse-title:after,.collapse.collapse-arrow:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{transform:translateY(-50%)rotate(225deg)}.collapse[open].collapse-plus>.collapse-title:after,.collapse[tabindex].collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse.collapse-plus:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{--tw-content:"−";content:var(--tw-content)}.list .list-row:has(.list-col-grow:first-child){--list-grid-cols:1fr}.list .list-row:has(.list-col-grow:nth-child(2)){--list-grid-cols:minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(3)){--list-grid-cols:minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(4)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(5)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(6)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row>*{grid-row-start:1}.steps .step-neutral+.step-neutral:before,.steps .step-neutral:after,.steps .step-neutral>.step-icon{--step-bg:var(--color-neutral);--step-fg:var(--color-neutral-content)}.steps .step-primary+.step-primary:before,.steps .step-primary:after,.steps .step-primary>.step-icon{--step-bg:var(--color-primary);--step-fg:var(--color-primary-content)}.steps .step-secondary+.step-secondary:before,.steps .step-secondary:after,.steps .step-secondary>.step-icon{--step-bg:var(--color-secondary);--step-fg:var(--color-secondary-content)}.steps .step-accent+.step-accent:before,.steps .step-accent:after,.steps .step-accent>.step-icon{--step-bg:var(--color-accent);--step-fg:var(--color-accent-content)}.steps .step-info+.step-info:before,.steps .step-info:after,.steps .step-info>.step-icon{--step-bg:var(--color-info);--step-fg:var(--color-info-content)}.steps .step-success+.step-success:before,.steps .step-success:after,.steps .step-success>.step-icon{--step-bg:var(--color-success);--step-fg:var(--color-success-content)}.steps .step-warning+.step-warning:before,.steps .step-warning:after,.steps .step-warning>.step-icon{--step-bg:var(--color-warning);--step-fg:var(--color-warning-content)}.steps .step-error+.step-error:before,.steps .step-error:after,.steps .step-error>.step-icon{--step-bg:var(--color-error);--step-fg:var(--color-error-content)}.menu-vertical{flex-direction:column;display:inline-flex}.menu-vertical>li:not(.menu-title)>details>ul{background-color:revert-layer;border-radius:revert-layer;animation:revert-layer;box-shadow:revert-layer;margin-inline-start:1rem;margin-top:0;padding-block:0;padding-inline-end:0;transition:revert-layer;position:relative}.checkbox:disabled,.radio:disabled{cursor:not-allowed;opacity:.2}.rating.rating-xs :where(:not(.rating-hidden)){width:1rem;height:1rem}.rating.rating-sm :where(:not(.rating-hidden)){width:1.25rem;height:1.25rem}.rating.rating-md :where(:not(.rating-hidden)){width:1.5rem;height:1.5rem}.rating.rating-lg :where(:not(.rating-hidden)){width:1.75rem;height:1.75rem}.rating.rating-xl :where(:not(.rating-hidden)){width:2rem;height:2rem}:where(.navbar){position:relative}.tooltip-top>.tooltip-content,.tooltip-top[data-tip]:before{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-off) 50%}.tooltip-top:after{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-tail) 50%}.btn-active{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn-active{--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}.btn-active{--btn-shadow:0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0);isolation:isolate}.input-lg{--size:calc(var(--size-field,.25rem) * 12);font-size:max(var(--font-size,1.125rem), 1.125rem)}.input-lg[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input-md{--size:calc(var(--size-field,.25rem) * 10);font-size:max(var(--font-size,.875rem), .875rem)}.input-md[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input-sm{--size:calc(var(--size-field,.25rem) * 8);font-size:max(var(--font-size,.75rem), .75rem)}.input-sm[type=number]::-webkit-inner-spin-button{margin-block:-.5rem;margin-inline-end:-.75rem}.input-xl{--size:calc(var(--size-field,.25rem) * 14);font-size:max(var(--font-size,1.375rem), 1.375rem)}.input-xl[type=number]::-webkit-inner-spin-button{margin-block:-1rem;margin-inline-end:-.75rem}.input-xs{--size:calc(var(--size-field,.25rem) * 6);font-size:max(var(--font-size,.6875rem), .6875rem)}.input-xs[type=number]::-webkit-inner-spin-button{margin-block:-.25rem;margin-inline-end:-.75rem}.badge-ghost{border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content);background-image:none}.badge-outline{color:var(--badge-color);--badge-bg:#0000;background-image:none;border-color:currentColor}.select-lg{--size:calc(var(--size-field,.25rem) * 12);font-size:1.125rem}.select-lg option{padding-block:.375rem;padding-inline:1rem}.select-sm{--size:calc(var(--size-field,.25rem) * 8);font-size:.75rem}.select-sm option{padding-block:.25rem;padding-inline:.625rem}.select-xl{--size:calc(var(--size-field,.25rem) * 14);font-size:1.375rem}.select-xl option{padding-block:.375rem;padding-inline:1.25rem}.select-xs{--size:calc(var(--size-field,.25rem) * 6);font-size:.6875rem}.select-xs option{padding-block:.25rem;padding-inline:.5rem}.table-sm :not(thead,tfoot) tr{font-size:.75rem}.table-sm :where(th,td){padding-block:.5rem;padding-inline:.75rem}.badge-sm{--size:calc(var(--size-selector,.25rem) * 5);font-size:.75rem}.textarea-lg{font-size:max(var(--font-size,1.125rem), 1.125rem)}.textarea-sm{font-size:max(var(--font-size,.75rem), .75rem)}.textarea-xl{font-size:max(var(--font-size,1.375rem), 1.375rem)}.textarea-xs{font-size:max(var(--font-size,.6875rem), .6875rem)}.alert-error{color:var(--color-error-content);--alert-border-color:var(--color-error);--alert-color:var(--color-error)}.checkbox-primary{color:var(--color-primary-content);--input-color:var(--color-primary)}.link-primary{color:var(--color-primary)}@media (hover:hover){.link-primary:hover{color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.link-primary:hover{color:color-mix(in oklab, var(--color-primary) 80%, #000)}}}.btn-sm{--fontsize:.75rem;--btn-p:.75rem;--size:calc(var(--size-field,.25rem) * 8)}.btn-xs{--fontsize:.6875rem;--btn-p:.5rem;--size:calc(var(--size-field,.25rem) * 6)}.badge-error{--badge-color:var(--color-error);--badge-fg:var(--color-error-content)}.badge-info{--badge-color:var(--color-info);--badge-fg:var(--color-info-content)}.badge-primary{--badge-color:var(--color-primary);--badge-fg:var(--color-primary-content)}.badge-secondary{--badge-color:var(--color-secondary);--badge-fg:var(--color-secondary-content)}.badge-success{--badge-color:var(--color-success);--badge-fg:var(--color-success-content)}.badge-warning{--badge-color:var(--color-warning);--badge-fg:var(--color-warning-content)}.range-lg{--range-thumb-size:calc(var(--size-selector,.25rem) * 7)}.range-sm{--range-thumb-size:calc(var(--size-selector,.25rem) * 5)}.range-xl{--range-thumb-size:calc(var(--size-selector,.25rem) * 8)}.range-xs{--range-thumb-size:calc(var(--size-selector,.25rem) * 4)}.textarea-error,.textarea-error:focus,.textarea-error:focus-within{--input-color:var(--color-error)}}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse:not(td,tr,colgroup){visibility:revert-layer}.validator:user-invalid~.validator-hint{display:revert-layer}.validator:has(:user-invalid)~.validator-hint{display:revert-layer}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))~.validator-hint{display:revert-layer}.collapse{visibility:collapse}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.join{--join-ss:0;--join-se:0;--join-es:0;--join-ee:0;align-items:stretch;display:inline-flex}.join :where(.join-item){border-start-start-radius:var(--join-ss,0);border-start-end-radius:var(--join-se,0);border-end-end-radius:var(--join-ee,0);border-end-start-radius:var(--join-es,0)}.join :where(.join-item) *{--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.join>.join-item:where(:first-child),.join :first-child:not(:last-child) :where(.join-item){--join-ss:var(--radius-field);--join-se:0;--join-es:var(--radius-field);--join-ee:0}.join>.join-item:where(:last-child),.join :last-child:not(:first-child) :where(.join-item){--join-ss:0;--join-se:var(--radius-field);--join-es:0;--join-ee:var(--radius-field)}.join>.join-item:where(:only-child),.join :only-child :where(.join-item){--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.join>:where(:focus,:has(:focus)){z-index:1}@media (hover:hover){.join>:where(.btn:hover,:has(.btn:hover)){isolation:isolate}}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-2{margin:calc(var(--spacing) * 2)}.mx-auto{margin-inline:auto}.my-2{margin-block:calc(var(--spacing) * 2)}.join-item:where(:not(:first-child,:disabled,[disabled],.btn-disabled)){margin-block-start:0;margin-inline-start:calc(var(--border,1px) * -1)}.join-item:where(:is(:disabled,[disabled],.btn-disabled)){border-width:var(--border,1px) 0 var(--border,1px) var(--border,1px)}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows), 0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:oklab(21% -.00316127 -.0338527/.1);--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.mt-0{margin-top:calc(var(--spacing) * 0)}.mt-0\!{margin-top:calc(var(--spacing) * 0)!important}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-5{margin-right:calc(var(--spacing) * 5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.alert{border-width:var(--border);border-color:var(--alert-border-color,var(--color-base-200))}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:root .prose{--tw-prose-body:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-body:color-mix(in oklab, var(--color-base-content) 80%, #0000)}}:root .prose{--tw-prose-headings:var(--color-base-content);--tw-prose-lead:var(--color-base-content);--tw-prose-links:var(--color-base-content);--tw-prose-bold:var(--color-base-content);--tw-prose-counters:var(--color-base-content);--tw-prose-bullets:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-bullets:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-hr:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-hr:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-quotes:var(--color-base-content);--tw-prose-quote-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-quote-borders:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-captions:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-captions:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-code:var(--color-base-content);--tw-prose-pre-code:var(--color-neutral-content);--tw-prose-pre-bg:var(--color-neutral);--tw-prose-th-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-th-borders:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-td-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-td-borders:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-kbd:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-kbd:color-mix(in oklab, var(--color-base-content) 80%, #0000)}}:root .prose :where(code):not(pre>code){background-color:var(--color-base-200);border-radius:var(--radius-selector);border:var(--border) solid var(--color-base-300);font-weight:inherit;padding-block:.2em;padding-inline:.5em}:root .prose :where(code):not(pre>code):before,:root .prose :where(code):not(pre>code):after{display:none}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[calc\(100vh-2rem\)\]{max-height:calc(100vh - 2rem)}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-\[120px\]{min-height:120px}.min-h-dvh{min-height:100dvh}.min-h-full{min-height:100%}.w-4{width:calc(var(--spacing) * 4)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-7{max-width:calc(var(--spacing) * 7)}.max-w-60{max-width:calc(var(--spacing) * 60)}.max-w-lg{max-width:var(--container-lg)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-20{min-width:calc(var(--spacing) * 20)}.min-w-80{min-width:calc(var(--spacing) * 80)}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-base-200{border-color:var(--color-base-200)}.border-base-300{border-color:var(--color-base-300)}.bg-base-100{background-color:var(--color-base-100)}.bg-base-200{background-color:var(--color-base-200)}.bg-gray-500{background-color:var(--color-gray-500)}.bg-gray-500\/50{background-color:#6a728280}@supports (color:color-mix(in lab, red, red)){.bg-gray-500\/50{background-color:color-mix(in oklab, var(--color-gray-500) 50%, transparent)}}.bg-primary,.bg-primary\/10{background-color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.p-0{padding:calc(var(--spacing) * 0)}.p-0\!{padding:calc(var(--spacing) * 0)!important}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-4{padding-block:calc(var(--spacing) * 4)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-pre-wrap{white-space:pre-wrap}.text-base-content,.text-base-content\/50{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/50{color:color-mix(in oklab, var(--color-base-content) 50%, transparent)}}.text-base-content\/60{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/60{color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.text-base-content\/70{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/70{color:color-mix(in oklab, var(--color-base-content) 70%, transparent)}}.text-base-content\/80{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/80{color:color-mix(in oklab, var(--color-base-content) 80%, transparent)}}.text-error{color:var(--color-error)}.text-primary{color:var(--color-primary)}.text-primary-content{color:var(--color-primary-content)}.italic{font-style:italic}.prose :where(a.btn:not(.btn-link)):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-primary{--tw-ring-color:var(--color-primary)}@layer daisyui.l1{.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)){--btn-shadow:"";--btn-bg:#0000;--btn-border:#0000;--btn-noise:none}.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)):not(:disabled,[disabled],.btn-disabled){--btn-fg:var(--btn-color,currentColor);outline-color:currentColor}@media (hover:none){.btn-ghost:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color,currentColor);--btn-border:#0000;--btn-noise:none;outline-color:currentColor}}.btn-outline:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn),:disabled,[disabled],.btn-disabled){--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}@media (hover:none){.btn-outline:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;user-select:none}.ring-inset{--tw-ring-inset:inset}@media (hover:hover){.hover\:text-primary:hover{color:var(--color-primary)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.active\:cursor-grabbing:active{cursor:grabbing}@media not all and (min-width:770px){.max-\[770px\]\:hidden{display:none}}}[x-cloak]{display:none!important}@keyframes rating{0%,40%{filter:brightness(1.05)contrast(1.05);scale:1.1}}@keyframes dropdown{0%{opacity:0}}@keyframes radio{0%{padding:5px}50%{padding:3px}}@keyframes toast{0%{opacity:0;scale:.9}to{opacity:1;scale:1}}@keyframes rotator{89.9999%,to{--first-item-position:0 0%}90%,99.9999%{--first-item-position:0 calc(var(--items) * 100%)}to{translate:0 -100%}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}@keyframes menu{0%{opacity:0}}@keyframes progress{50%{background-position-x:-115%}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-gray-500:oklch(55.1% .027 264.364);--color-black:#000;--spacing:.25rem;--container-xs:20rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-lg:.5rem;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}@media (prefers-color-scheme:dark){:root:not([data-theme]){color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}}:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dark]:checked),[data-theme=dark]{color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=cupcake]:checked),[data-theme=cupcake]{color-scheme:light;--color-base-100:oklch(97.788% .004 56.375);--color-base-200:oklch(93.982% .007 61.449);--color-base-300:oklch(91.586% .006 53.44);--color-base-content:oklch(23.574% .066 313.189);--color-primary:oklch(85% .138 181.071);--color-primary-content:oklch(43% .078 188.216);--color-secondary:oklch(89% .061 343.231);--color-secondary-content:oklch(45% .187 3.815);--color-accent:oklch(90% .076 70.697);--color-accent-content:oklch(47% .157 37.304);--color-neutral:oklch(27% .006 286.033);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(68% .169 237.323);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(69% .17 162.48);--color-success-content:oklch(26% .051 172.552);--color-warning:oklch(79% .184 86.047);--color-warning-content:oklch(28% .066 53.813);--color-error:oklch(64% .246 16.439);--color-error-content:oklch(27% .105 12.094);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:0}:root:has(input.theme-controller[value=bumblebee]:checked),[data-theme=bumblebee]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(92% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(85% .199 91.936);--color-primary-content:oklch(42% .095 57.708);--color-secondary:oklch(75% .183 55.934);--color-secondary-content:oklch(40% .123 38.172);--color-accent:oklch(0% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(37% .01 67.558);--color-neutral-content:oklch(92% .003 48.717);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(39% .09 240.876);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(39% .141 25.723);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=emerald]:checked),[data-theme=emerald]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(35.519% .032 262.988);--color-primary:oklch(76.662% .135 153.45);--color-primary-content:oklch(33.387% .04 162.24);--color-secondary:oklch(61.302% .202 261.294);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(72.772% .149 33.2);--color-accent-content:oklch(0% 0 0);--color-neutral:oklch(35.519% .032 262.988);--color-neutral-content:oklch(98.462% .001 247.838);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=corporate]:checked),[data-theme=corporate]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(22.389% .031 278.072);--color-primary:oklch(58% .158 241.966);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(55% .046 257.417);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(60% .118 184.704);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(0% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(60% .126 221.723);--color-info-content:oklch(100% 0 0);--color-success:oklch(62% .194 149.214);--color-success-content:oklch(100% 0 0);--color-warning:oklch(85% .199 91.936);--color-warning-content:oklch(0% 0 0);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(0% 0 0);--radius-selector:.25rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=synthwave]:checked),[data-theme=synthwave]{color-scheme:dark;--color-base-100:oklch(15% .09 281.288);--color-base-200:oklch(20% .09 281.288);--color-base-300:oklch(25% .09 281.288);--color-base-content:oklch(78% .115 274.713);--color-primary:oklch(71% .202 349.761);--color-primary-content:oklch(28% .109 3.907);--color-secondary:oklch(82% .111 230.318);--color-secondary-content:oklch(29% .066 243.157);--color-accent:oklch(75% .183 55.934);--color-accent-content:oklch(26% .079 36.259);--color-neutral:oklch(45% .24 277.023);--color-neutral-content:oklch(87% .065 274.039);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(77% .152 181.912);--color-success-content:oklch(27% .046 192.524);--color-warning:oklch(90% .182 98.111);--color-warning-content:oklch(42% .095 57.708);--color-error:oklch(73.7% .121 32.639);--color-error-content:oklch(23.501% .096 290.329);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=retro]:checked),[data-theme=retro]{color-scheme:light;--color-base-100:oklch(91.637% .034 90.515);--color-base-200:oklch(88.272% .049 91.774);--color-base-300:oklch(84.133% .065 90.856);--color-base-content:oklch(41% .112 45.904);--color-primary:oklch(80% .114 19.571);--color-primary-content:oklch(39% .141 25.723);--color-secondary:oklch(92% .084 155.995);--color-secondary-content:oklch(44% .119 151.328);--color-accent:oklch(68% .162 75.834);--color-accent-content:oklch(41% .112 45.904);--color-neutral:oklch(44% .011 73.639);--color-neutral-content:oklch(86% .005 56.366);--color-info:oklch(58% .158 241.966);--color-info-content:oklch(96% .059 95.617);--color-success:oklch(51% .096 186.391);--color-success-content:oklch(96% .059 95.617);--color-warning:oklch(64% .222 41.116);--color-warning-content:oklch(96% .059 95.617);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(40% .123 38.172);--radius-selector:.25rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=cyberpunk]:checked),[data-theme=cyberpunk]{color-scheme:light;--color-base-100:oklch(94.51% .179 104.32);--color-base-200:oklch(91.51% .179 104.32);--color-base-300:oklch(85.51% .179 104.32);--color-base-content:oklch(0% 0 0);--color-primary:oklch(74.22% .209 6.35);--color-primary-content:oklch(14.844% .041 6.35);--color-secondary:oklch(83.33% .184 204.72);--color-secondary-content:oklch(16.666% .036 204.72);--color-accent:oklch(71.86% .217 310.43);--color-accent-content:oklch(14.372% .043 310.43);--color-neutral:oklch(23.04% .065 269.31);--color-neutral-content:oklch(94.51% .179 104.32);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:0rem;--radius-field:0rem;--radius-box:0rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=valentine]:checked),[data-theme=valentine]{color-scheme:light;--color-base-100:oklch(97% .014 343.198);--color-base-200:oklch(94% .028 342.258);--color-base-300:oklch(89% .061 343.231);--color-base-content:oklch(52% .223 3.958);--color-primary:oklch(65% .241 354.308);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(62% .265 303.9);--color-secondary-content:oklch(97% .014 308.299);--color-accent:oklch(82% .111 230.318);--color-accent-content:oklch(39% .09 240.876);--color-neutral:oklch(40% .153 2.432);--color-neutral-content:oklch(89% .061 343.231);--color-info:oklch(86% .127 207.078);--color-info-content:oklch(44% .11 240.79);--color-success:oklch(84% .143 164.978);--color-success-content:oklch(43% .095 166.913);--color-warning:oklch(75% .183 55.934);--color-warning-content:oklch(26% .079 36.259);--color-error:oklch(63% .237 25.331);--color-error-content:oklch(97% .013 17.38);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=halloween]:checked),[data-theme=halloween]{color-scheme:dark;--color-base-100:oklch(21% .006 56.043);--color-base-200:oklch(14% .004 49.25);--color-base-300:oklch(0% 0 0);--color-base-content:oklch(84.955% 0 0);--color-primary:oklch(77.48% .204 60.62);--color-primary-content:oklch(19.693% .004 196.779);--color-secondary:oklch(45.98% .248 305.03);--color-secondary-content:oklch(89.196% .049 305.03);--color-accent:oklch(64.8% .223 136.073);--color-accent-content:oklch(0% 0 0);--color-neutral:oklch(24.371% .046 65.681);--color-neutral-content:oklch(84.874% .009 65.681);--color-info:oklch(54.615% .215 262.88);--color-info-content:oklch(90.923% .043 262.88);--color-success:oklch(62.705% .169 149.213);--color-success-content:oklch(12.541% .033 149.213);--color-warning:oklch(66.584% .157 58.318);--color-warning-content:oklch(13.316% .031 58.318);--color-error:oklch(65.72% .199 27.33);--color-error-content:oklch(13.144% .039 27.33);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=garden]:checked),[data-theme=garden]{color-scheme:light;--color-base-100:oklch(92.951% .002 17.197);--color-base-200:oklch(86.445% .002 17.197);--color-base-300:oklch(79.938% .001 17.197);--color-base-content:oklch(16.961% .001 17.32);--color-primary:oklch(62.45% .278 3.836);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(48.495% .11 355.095);--color-secondary-content:oklch(89.699% .022 355.095);--color-accent:oklch(56.273% .054 154.39);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(24.155% .049 89.07);--color-neutral-content:oklch(92.951% .002 17.197);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=forest]:checked),[data-theme=forest]{color-scheme:dark;--color-base-100:oklch(20.84% .008 17.911);--color-base-200:oklch(18.522% .007 17.911);--color-base-300:oklch(16.203% .007 17.911);--color-base-content:oklch(83.768% .001 17.911);--color-primary:oklch(68.628% .185 148.958);--color-primary-content:oklch(0% 0 0);--color-secondary:oklch(69.776% .135 168.327);--color-secondary-content:oklch(13.955% .027 168.327);--color-accent:oklch(70.628% .119 185.713);--color-accent-content:oklch(14.125% .023 185.713);--color-neutral:oklch(30.698% .039 171.364);--color-neutral-content:oklch(86.139% .007 171.364);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=aqua]:checked),[data-theme=aqua]{color-scheme:dark;--color-base-100:oklch(37% .146 265.522);--color-base-200:oklch(28% .091 267.935);--color-base-300:oklch(22% .091 267.935);--color-base-content:oklch(90% .058 230.902);--color-primary:oklch(85.661% .144 198.645);--color-primary-content:oklch(40.124% .068 197.603);--color-secondary:oklch(60.682% .108 309.782);--color-secondary-content:oklch(96% .016 293.756);--color-accent:oklch(93.426% .102 94.555);--color-accent-content:oklch(18.685% .02 94.555);--color-neutral:oklch(27% .146 265.522);--color-neutral-content:oklch(80% .146 265.522);--color-info:oklch(54.615% .215 262.88);--color-info-content:oklch(90.923% .043 262.88);--color-success:oklch(62.705% .169 149.213);--color-success-content:oklch(12.541% .033 149.213);--color-warning:oklch(66.584% .157 58.318);--color-warning-content:oklch(27% .077 45.635);--color-error:oklch(73.95% .19 27.33);--color-error-content:oklch(14.79% .038 27.33);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=lofi]:checked),[data-theme=lofi]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(94% 0 0);--color-base-content:oklch(0% 0 0);--color-primary:oklch(15.906% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(21.455% .001 17.278);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(26.861% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(0% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(79.54% .103 205.9);--color-info-content:oklch(15.908% .02 205.9);--color-success:oklch(90.13% .153 164.14);--color-success-content:oklch(18.026% .03 164.14);--color-warning:oklch(88.37% .135 79.94);--color-warning-content:oklch(17.674% .027 79.94);--color-error:oklch(78.66% .15 28.47);--color-error-content:oklch(15.732% .03 28.47);--radius-selector:2rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=pastel]:checked),[data-theme=pastel]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98.462% .001 247.838);--color-base-300:oklch(92.462% .001 247.838);--color-base-content:oklch(20% 0 0);--color-primary:oklch(90% .063 306.703);--color-primary-content:oklch(49% .265 301.924);--color-secondary:oklch(89% .058 10.001);--color-secondary-content:oklch(51% .222 16.935);--color-accent:oklch(90% .093 164.15);--color-accent-content:oklch(50% .118 165.612);--color-neutral:oklch(55% .046 257.417);--color-neutral-content:oklch(92% .013 255.508);--color-info:oklch(86% .127 207.078);--color-info-content:oklch(52% .105 223.128);--color-success:oklch(87% .15 154.449);--color-success-content:oklch(52% .154 150.069);--color-warning:oklch(83% .128 66.29);--color-warning-content:oklch(55% .195 38.402);--color-error:oklch(80% .114 19.571);--color-error-content:oklch(50% .213 27.518);--radius-selector:1rem;--radius-field:2rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:0;--noise:0}:root:has(input.theme-controller[value=fantasy]:checked),[data-theme=fantasy]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(93% 0 0);--color-base-300:oklch(86% 0 0);--color-base-content:oklch(27.807% .029 256.847);--color-primary:oklch(37.45% .189 325.02);--color-primary-content:oklch(87.49% .037 325.02);--color-secondary:oklch(53.92% .162 241.36);--color-secondary-content:oklch(90.784% .032 241.36);--color-accent:oklch(75.98% .204 56.72);--color-accent-content:oklch(15.196% .04 56.72);--color-neutral:oklch(27.807% .029 256.847);--color-neutral-content:oklch(85.561% .005 256.847);--color-info:oklch(72.06% .191 231.6);--color-info-content:oklch(0% 0 0);--color-success:oklch(64.8% .15 160);--color-success-content:oklch(0% 0 0);--color-warning:oklch(84.71% .199 83.87);--color-warning-content:oklch(0% 0 0);--color-error:oklch(71.76% .221 22.18);--color-error-content:oklch(0% 0 0);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=wireframe]:checked),[data-theme=wireframe]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97% 0 0);--color-base-300:oklch(94% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(87% 0 0);--color-primary-content:oklch(26% 0 0);--color-secondary:oklch(87% 0 0);--color-secondary-content:oklch(26% 0 0);--color-accent:oklch(87% 0 0);--color-accent-content:oklch(26% 0 0);--color-neutral:oklch(87% 0 0);--color-neutral-content:oklch(26% 0 0);--color-info:oklch(44% .11 240.79);--color-info-content:oklch(90% .058 230.902);--color-success:oklch(43% .095 166.913);--color-success-content:oklch(90% .093 164.15);--color-warning:oklch(47% .137 46.201);--color-warning-content:oklch(92% .12 95.746);--color-error:oklch(44% .177 26.899);--color-error-content:oklch(88% .062 18.334);--radius-selector:0rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=black]:checked),[data-theme=black]{color-scheme:dark;--color-base-100:oklch(0% 0 0);--color-base-200:oklch(19% 0 0);--color-base-300:oklch(22% 0 0);--color-base-content:oklch(87.609% 0 0);--color-primary:oklch(35% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(35% 0 0);--color-secondary-content:oklch(100% 0 0);--color-accent:oklch(35% 0 0);--color-accent-content:oklch(100% 0 0);--color-neutral:oklch(35% 0 0);--color-neutral-content:oklch(100% 0 0);--color-info:oklch(45.201% .313 264.052);--color-info-content:oklch(89.04% .062 264.052);--color-success:oklch(51.975% .176 142.495);--color-success-content:oklch(90.395% .035 142.495);--color-warning:oklch(96.798% .211 109.769);--color-warning-content:oklch(19.359% .042 109.769);--color-error:oklch(62.795% .257 29.233);--color-error-content:oklch(12.559% .051 29.233);--radius-selector:0rem;--radius-field:0rem;--radius-box:0rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=luxury]:checked),[data-theme=luxury]{color-scheme:dark;--color-base-100:oklch(14.076% .004 285.822);--color-base-200:oklch(20.219% .004 308.229);--color-base-300:oklch(23.219% .004 308.229);--color-base-content:oklch(75.687% .123 76.89);--color-primary:oklch(100% 0 0);--color-primary-content:oklch(20% 0 0);--color-secondary:oklch(27.581% .064 261.069);--color-secondary-content:oklch(85.516% .012 261.069);--color-accent:oklch(36.674% .051 338.825);--color-accent-content:oklch(87.334% .01 338.825);--color-neutral:oklch(24.27% .057 59.825);--color-neutral-content:oklch(93.203% .089 90.861);--color-info:oklch(79.061% .121 237.133);--color-info-content:oklch(15.812% .024 237.133);--color-success:oklch(78.119% .192 132.154);--color-success-content:oklch(15.623% .038 132.154);--color-warning:oklch(86.127% .136 102.891);--color-warning-content:oklch(17.225% .027 102.891);--color-error:oklch(71.753% .176 22.568);--color-error-content:oklch(14.35% .035 22.568);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dracula]:checked),[data-theme=dracula]{color-scheme:dark;--color-base-100:oklch(28.822% .022 277.508);--color-base-200:oklch(26.805% .02 277.508);--color-base-300:oklch(24.787% .019 277.508);--color-base-content:oklch(97.747% .007 106.545);--color-primary:oklch(75.461% .183 346.812);--color-primary-content:oklch(15.092% .036 346.812);--color-secondary:oklch(74.202% .148 301.883);--color-secondary-content:oklch(14.84% .029 301.883);--color-accent:oklch(83.392% .124 66.558);--color-accent-content:oklch(16.678% .024 66.558);--color-neutral:oklch(39.445% .032 275.524);--color-neutral-content:oklch(87.889% .006 275.524);--color-info:oklch(88.263% .093 212.846);--color-info-content:oklch(17.652% .018 212.846);--color-success:oklch(87.099% .219 148.024);--color-success-content:oklch(17.419% .043 148.024);--color-warning:oklch(95.533% .134 112.757);--color-warning-content:oklch(19.106% .026 112.757);--color-error:oklch(68.22% .206 24.43);--color-error-content:oklch(13.644% .041 24.43);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=cmyk]:checked),[data-theme=cmyk]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(95% 0 0);--color-base-300:oklch(90% 0 0);--color-base-content:oklch(20% 0 0);--color-primary:oklch(71.772% .133 239.443);--color-primary-content:oklch(14.354% .026 239.443);--color-secondary:oklch(64.476% .202 359.339);--color-secondary-content:oklch(12.895% .04 359.339);--color-accent:oklch(94.228% .189 105.306);--color-accent-content:oklch(18.845% .037 105.306);--color-neutral:oklch(21.778% 0 0);--color-neutral-content:oklch(84.355% 0 0);--color-info:oklch(68.475% .094 217.284);--color-info-content:oklch(13.695% .018 217.284);--color-success:oklch(46.949% .162 321.406);--color-success-content:oklch(89.389% .032 321.406);--color-warning:oklch(71.236% .159 52.023);--color-warning-content:oklch(14.247% .031 52.023);--color-error:oklch(62.013% .208 28.717);--color-error-content:oklch(12.402% .041 28.717);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=autumn]:checked),[data-theme=autumn]{color-scheme:light;--color-base-100:oklch(95.814% 0 0);--color-base-200:oklch(89.107% 0 0);--color-base-300:oklch(82.4% 0 0);--color-base-content:oklch(19.162% 0 0);--color-primary:oklch(40.723% .161 17.53);--color-primary-content:oklch(88.144% .032 17.53);--color-secondary:oklch(61.676% .169 23.865);--color-secondary-content:oklch(12.335% .033 23.865);--color-accent:oklch(73.425% .094 60.729);--color-accent-content:oklch(14.685% .018 60.729);--color-neutral:oklch(54.367% .037 51.902);--color-neutral-content:oklch(90.873% .007 51.902);--color-info:oklch(69.224% .097 207.284);--color-info-content:oklch(13.844% .019 207.284);--color-success:oklch(60.995% .08 174.616);--color-success-content:oklch(12.199% .016 174.616);--color-warning:oklch(70.081% .164 56.844);--color-warning-content:oklch(14.016% .032 56.844);--color-error:oklch(53.07% .241 24.16);--color-error-content:oklch(90.614% .048 24.16);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=business]:checked),[data-theme=business]{color-scheme:dark;--color-base-100:oklch(24.353% 0 0);--color-base-200:oklch(22.648% 0 0);--color-base-300:oklch(20.944% 0 0);--color-base-content:oklch(84.87% 0 0);--color-primary:oklch(41.703% .099 251.473);--color-primary-content:oklch(88.34% .019 251.473);--color-secondary:oklch(64.092% .027 229.389);--color-secondary-content:oklch(12.818% .005 229.389);--color-accent:oklch(67.271% .167 35.791);--color-accent-content:oklch(13.454% .033 35.791);--color-neutral:oklch(27.441% .013 253.041);--color-neutral-content:oklch(85.488% .002 253.041);--color-info:oklch(62.616% .143 240.033);--color-info-content:oklch(12.523% .028 240.033);--color-success:oklch(70.226% .094 156.596);--color-success-content:oklch(14.045% .018 156.596);--color-warning:oklch(77.482% .115 81.519);--color-warning-content:oklch(15.496% .023 81.519);--color-error:oklch(51.61% .146 29.674);--color-error-content:oklch(90.322% .029 29.674);--radius-selector:0rem;--radius-field:.25rem;--radius-box:.25rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=acid]:checked),[data-theme=acid]{color-scheme:light;--color-base-100:oklch(98% 0 0);--color-base-200:oklch(95% 0 0);--color-base-300:oklch(91% 0 0);--color-base-content:oklch(0% 0 0);--color-primary:oklch(71.9% .357 330.759);--color-primary-content:oklch(14.38% .071 330.759);--color-secondary:oklch(73.37% .224 48.25);--color-secondary-content:oklch(14.674% .044 48.25);--color-accent:oklch(92.78% .264 122.962);--color-accent-content:oklch(18.556% .052 122.962);--color-neutral:oklch(21.31% .128 278.68);--color-neutral-content:oklch(84.262% .025 278.68);--color-info:oklch(60.72% .227 252.05);--color-info-content:oklch(12.144% .045 252.05);--color-success:oklch(85.72% .266 158.53);--color-success-content:oklch(17.144% .053 158.53);--color-warning:oklch(91.01% .212 100.5);--color-warning-content:oklch(18.202% .042 100.5);--color-error:oklch(64.84% .293 29.349);--color-error-content:oklch(12.968% .058 29.349);--radius-selector:1rem;--radius-field:1rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=lemonade]:checked),[data-theme=lemonade]{color-scheme:light;--color-base-100:oklch(98.71% .02 123.72);--color-base-200:oklch(91.8% .018 123.72);--color-base-300:oklch(84.89% .017 123.72);--color-base-content:oklch(19.742% .004 123.72);--color-primary:oklch(58.92% .199 134.6);--color-primary-content:oklch(11.784% .039 134.6);--color-secondary:oklch(77.75% .196 111.09);--color-secondary-content:oklch(15.55% .039 111.09);--color-accent:oklch(85.39% .201 100.73);--color-accent-content:oklch(17.078% .04 100.73);--color-neutral:oklch(30.98% .075 108.6);--color-neutral-content:oklch(86.196% .015 108.6);--color-info:oklch(86.19% .047 224.14);--color-info-content:oklch(17.238% .009 224.14);--color-success:oklch(86.19% .047 157.85);--color-success-content:oklch(17.238% .009 157.85);--color-warning:oklch(86.19% .047 102.15);--color-warning-content:oklch(17.238% .009 102.15);--color-error:oklch(86.19% .047 25.85);--color-error-content:oklch(17.238% .009 25.85);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=night]:checked),[data-theme=night]{color-scheme:dark;--color-base-100:oklch(20.768% .039 265.754);--color-base-200:oklch(19.314% .037 265.754);--color-base-300:oklch(17.86% .034 265.754);--color-base-content:oklch(84.153% .007 265.754);--color-primary:oklch(75.351% .138 232.661);--color-primary-content:oklch(15.07% .027 232.661);--color-secondary:oklch(68.011% .158 276.934);--color-secondary-content:oklch(13.602% .031 276.934);--color-accent:oklch(72.36% .176 350.048);--color-accent-content:oklch(14.472% .035 350.048);--color-neutral:oklch(27.949% .036 260.03);--color-neutral-content:oklch(85.589% .007 260.03);--color-info:oklch(68.455% .148 237.251);--color-info-content:oklch(0% 0 0);--color-success:oklch(78.452% .132 181.911);--color-success-content:oklch(15.69% .026 181.911);--color-warning:oklch(83.242% .139 82.95);--color-warning-content:oklch(16.648% .027 82.95);--color-error:oklch(71.785% .17 13.118);--color-error-content:oklch(14.357% .034 13.118);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=coffee]:checked),[data-theme=coffee]{color-scheme:dark;--color-base-100:oklch(24% .023 329.708);--color-base-200:oklch(21% .021 329.708);--color-base-300:oklch(16% .019 329.708);--color-base-content:oklch(72.354% .092 79.129);--color-primary:oklch(71.996% .123 62.756);--color-primary-content:oklch(14.399% .024 62.756);--color-secondary:oklch(34.465% .029 199.194);--color-secondary-content:oklch(86.893% .005 199.194);--color-accent:oklch(42.621% .074 224.389);--color-accent-content:oklch(88.524% .014 224.389);--color-neutral:oklch(16.51% .015 326.261);--color-neutral-content:oklch(83.302% .003 326.261);--color-info:oklch(79.49% .063 184.558);--color-info-content:oklch(15.898% .012 184.558);--color-success:oklch(74.722% .072 131.116);--color-success-content:oklch(14.944% .014 131.116);--color-warning:oklch(88.15% .14 87.722);--color-warning-content:oklch(17.63% .028 87.722);--color-error:oklch(77.318% .128 31.871);--color-error-content:oklch(15.463% .025 31.871);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=winter]:checked),[data-theme=winter]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(97.466% .011 259.822);--color-base-300:oklch(93.268% .016 262.751);--color-base-content:oklch(41.886% .053 255.824);--color-primary:oklch(56.86% .255 257.57);--color-primary-content:oklch(91.372% .051 257.57);--color-secondary:oklch(42.551% .161 282.339);--color-secondary-content:oklch(88.51% .032 282.339);--color-accent:oklch(59.939% .191 335.171);--color-accent-content:oklch(11.988% .038 335.171);--color-neutral:oklch(19.616% .063 257.651);--color-neutral-content:oklch(83.923% .012 257.651);--color-info:oklch(88.127% .085 214.515);--color-info-content:oklch(17.625% .017 214.515);--color-success:oklch(80.494% .077 197.823);--color-success-content:oklch(16.098% .015 197.823);--color-warning:oklch(89.172% .045 71.47);--color-warning-content:oklch(17.834% .009 71.47);--color-error:oklch(73.092% .11 20.076);--color-error-content:oklch(14.618% .022 20.076);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=dim]:checked),[data-theme=dim]{color-scheme:dark;--color-base-100:oklch(30.857% .023 264.149);--color-base-200:oklch(28.036% .019 264.182);--color-base-300:oklch(26.346% .018 262.177);--color-base-content:oklch(82.901% .031 222.959);--color-primary:oklch(86.133% .141 139.549);--color-primary-content:oklch(17.226% .028 139.549);--color-secondary:oklch(73.375% .165 35.353);--color-secondary-content:oklch(14.675% .033 35.353);--color-accent:oklch(74.229% .133 311.379);--color-accent-content:oklch(14.845% .026 311.379);--color-neutral:oklch(24.731% .02 264.094);--color-neutral-content:oklch(82.901% .031 222.959);--color-info:oklch(86.078% .142 206.182);--color-info-content:oklch(17.215% .028 206.182);--color-success:oklch(86.171% .142 166.534);--color-success-content:oklch(17.234% .028 166.534);--color-warning:oklch(86.163% .142 94.818);--color-warning-content:oklch(17.232% .028 94.818);--color-error:oklch(82.418% .099 33.756);--color-error-content:oklch(16.483% .019 33.756);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=nord]:checked),[data-theme=nord]{color-scheme:light;--color-base-100:oklch(95.127% .007 260.731);--color-base-200:oklch(93.299% .01 261.788);--color-base-300:oklch(89.925% .016 262.749);--color-base-content:oklch(32.437% .022 264.182);--color-primary:oklch(59.435% .077 254.027);--color-primary-content:oklch(11.887% .015 254.027);--color-secondary:oklch(69.651% .059 248.687);--color-secondary-content:oklch(13.93% .011 248.687);--color-accent:oklch(77.464% .062 217.469);--color-accent-content:oklch(15.492% .012 217.469);--color-neutral:oklch(45.229% .035 264.131);--color-neutral-content:oklch(89.925% .016 262.749);--color-info:oklch(69.207% .062 332.664);--color-info-content:oklch(13.841% .012 332.664);--color-success:oklch(76.827% .074 131.063);--color-success-content:oklch(15.365% .014 131.063);--color-warning:oklch(85.486% .089 84.093);--color-warning-content:oklch(17.097% .017 84.093);--color-error:oklch(60.61% .12 15.341);--color-error-content:oklch(12.122% .024 15.341);--radius-selector:1rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=sunset]:checked),[data-theme=sunset]{color-scheme:dark;--color-base-100:oklch(22% .019 237.69);--color-base-200:oklch(20% .019 237.69);--color-base-300:oklch(18% .019 237.69);--color-base-content:oklch(77.383% .043 245.096);--color-primary:oklch(74.703% .158 39.947);--color-primary-content:oklch(14.94% .031 39.947);--color-secondary:oklch(72.537% .177 2.72);--color-secondary-content:oklch(14.507% .035 2.72);--color-accent:oklch(71.294% .166 299.844);--color-accent-content:oklch(14.258% .033 299.844);--color-neutral:oklch(26% .019 237.69);--color-neutral-content:oklch(70% .019 237.69);--color-info:oklch(85.559% .085 206.015);--color-info-content:oklch(17.111% .017 206.015);--color-success:oklch(85.56% .085 144.778);--color-success-content:oklch(17.112% .017 144.778);--color-warning:oklch(85.569% .084 74.427);--color-warning-content:oklch(17.113% .016 74.427);--color-error:oklch(85.511% .078 16.886);--color-error-content:oklch(17.102% .015 16.886);--radius-selector:1rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:0;--noise:0}:root:has(input.theme-controller[value=caramellatte]:checked),[data-theme=caramellatte]{color-scheme:light;--color-base-100:oklch(98% .016 73.684);--color-base-200:oklch(95% .038 75.164);--color-base-300:oklch(90% .076 70.697);--color-base-content:oklch(40% .123 38.172);--color-primary:oklch(0% 0 0);--color-primary-content:oklch(100% 0 0);--color-secondary:oklch(22.45% .075 37.85);--color-secondary-content:oklch(90% .076 70.697);--color-accent:oklch(46.44% .111 37.85);--color-accent-content:oklch(90% .076 70.697);--color-neutral:oklch(55% .195 38.402);--color-neutral-content:oklch(98% .016 73.684);--color-info:oklch(42% .199 265.638);--color-info-content:oklch(90% .076 70.697);--color-success:oklch(43% .095 166.913);--color-success-content:oklch(90% .076 70.697);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(70% .191 22.216);--color-error-content:oklch(39% .141 25.723);--radius-selector:2rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:1}:root:has(input.theme-controller[value=abyss]:checked),[data-theme=abyss]{color-scheme:dark;--color-base-100:oklch(20% .08 209);--color-base-200:oklch(15% .08 209);--color-base-300:oklch(10% .08 209);--color-base-content:oklch(90% .076 70.697);--color-primary:oklch(92% .2653 125);--color-primary-content:oklch(50% .2653 125);--color-secondary:oklch(83.27% .0764 298.3);--color-secondary-content:oklch(43.27% .0764 298.3);--color-accent:oklch(43% 0 0);--color-accent-content:oklch(98% 0 0);--color-neutral:oklch(30% .08 209);--color-neutral-content:oklch(90% .076 70.697);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(79% .209 151.711);--color-success-content:oklch(26% .065 152.934);--color-warning:oklch(84.8% .1962 84.62);--color-warning-content:oklch(44.8% .1962 84.62);--color-error:oklch(65% .1985 24.22);--color-error-content:oklch(27% .1985 24.22);--radius-selector:2rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=silk]:checked),[data-theme=silk]{color-scheme:light;--color-base-100:oklch(97% .0035 67.78);--color-base-200:oklch(95% .0081 61.42);--color-base-300:oklch(90% .0081 61.42);--color-base-content:oklch(40% .0081 61.42);--color-primary:oklch(23.27% .0249 284.3);--color-primary-content:oklch(94.22% .2505 117.44);--color-secondary:oklch(23.27% .0249 284.3);--color-secondary-content:oklch(73.92% .2135 50.94);--color-accent:oklch(23.27% .0249 284.3);--color-accent-content:oklch(88.92% .2061 189.9);--color-neutral:oklch(20% 0 0);--color-neutral-content:oklch(80% .0081 61.42);--color-info:oklch(80.39% .1148 241.68);--color-info-content:oklch(30.39% .1148 241.68);--color-success:oklch(83.92% .0901 136.87);--color-success-content:oklch(23.92% .0901 136.87);--color-warning:oklch(83.92% .1085 80);--color-warning-content:oklch(43.92% .1085 80);--color-error:oklch(75.1% .1814 22.37);--color-error-content:oklch(35.1% .1814 22.37);--radius-selector:2rem;--radius-field:.5rem;--radius-box:1rem;--size-selector:.25rem;--size-field:.25rem;--border:2px;--depth:1;--noise:0}:root{--fx-noise:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E");scrollbar-color:currentColor #0000}@supports (color:color-mix(in lab, red, red)){:root{scrollbar-color:color-mix(in oklch, currentColor 35%, #0000) #0000}}@property --radialprogress{syntax:"";inherits:true;initial-value:0%}:root:not(span){overflow:var(--page-overflow)}:root{background:var(--page-scroll-bg,var(--root-bg));--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000), var(--root-bg,#0000)) var(--root-bg,#0000)}@supports (color:color-mix(in lab, red, red)){:root{--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000), var(--root-bg,#0000)) color-mix(in srgb, var(--root-bg,#0000), oklch(0% 0 0) calc(var(--page-has-backdrop,0) * 40%))}}:root{--page-scroll-transition-on:background-color .3s ease-out;transition:var(--page-scroll-transition);scrollbar-gutter:var(--page-scroll-gutter,unset);scrollbar-gutter:if(style(--page-has-scroll: 1): var(--page-scroll-gutter,unset) ; else: unset)}@keyframes set-page-has-scroll{0%,to{--page-has-scroll:1}}:root,[data-theme]{background:var(--page-scroll-bg,var(--root-bg));color:var(--color-base-content)}:where(:root,[data-theme]){--root-bg:var(--color-base-100)}}@layer components;@layer utilities{@layer daisyui.l1.l2.l3{.diff{webkit-user-select:none;-webkit-user-select:none;user-select:none;direction:ltr;grid-template-rows:1fr 1.8rem 1fr;grid-template-columns:auto 1fr;width:100%;display:grid;position:relative;overflow:hidden;container-type:inline-size}.diff:focus-visible,.diff:has(.diff-item-1:focus-visible),.diff:focus-visible{outline-style:var(--tw-outline-style);outline-offset:1px;outline-width:2px;outline-color:var(--color-base-content)}.diff:focus-visible .diff-resizer{min-width:95cqi;max-width:95cqi}.diff:has(.diff-item-1:focus-visible){outline-style:var(--tw-outline-style);outline-offset:1px;outline-width:2px}.diff:has(.diff-item-1:focus-visible) .diff-resizer{min-width:5cqi;max-width:5cqi}@supports (-webkit-overflow-scrolling:touch) and (overflow:-webkit-paged-x){.diff:focus .diff-resizer{min-width:5cqi;max-width:5cqi}.diff:has(.diff-item-1:focus) .diff-resizer{min-width:95cqi;max-width:95cqi}}.modal{pointer-events:none;visibility:hidden;width:100%;max-width:none;height:100%;max-height:none;color:inherit;transition:visibility .3s allow-discrete, background-color .3s ease-out, opacity .1s ease-out;overscroll-behavior:contain;z-index:999;scrollbar-gutter:auto;background-color:#0000;place-items:center;margin:0;padding:0;display:grid;position:fixed;inset:0;overflow:clip}.modal::backdrop{display:none}.fab{pointer-events:none;z-index:999;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));white-space:nowrap;inset-inline-end:1rem;flex-direction:column-reverse;align-items:flex-end;gap:.5rem;display:flex;position:fixed;bottom:1rem}.fab>*{pointer-events:auto;align-items:center;gap:.5rem;display:flex}.fab>:hover,.fab>:has(:focus-visible){z-index:1}.fab>[tabindex]:first-child{transition-property:opacity,visibility,rotate;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:grid;position:relative}.fab .fab-close,.fab .fab-main-action{inset-inline-end:0;position:absolute;bottom:0}:is(.fab:focus-within:has(.fab-close),.fab:focus-within:has(.fab-main-action))>[tabindex]{opacity:0;rotate:90deg}.fab:focus-within>[tabindex]:first-child{pointer-events:none}.fab:focus-within>:nth-child(n+2){visibility:visible;--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:1}.fab>:nth-child(n+2){visibility:hidden;--tw-scale-x:80%;--tw-scale-y:80%;--tw-scale-z:80%;scale:var(--tw-scale-x) var(--tw-scale-y);opacity:0;transition-property:opacity,scale,visibility;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.fab>:nth-child(n+2).fab-main-action,.fab>:nth-child(n+2).fab-close{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.fab>:nth-child(3){transition-delay:30ms}.fab>:nth-child(4){transition-delay:60ms}.fab>:nth-child(5){transition-delay:90ms}.fab>:nth-child(6){transition-delay:.12s}.tooltip{--tt-bg:var(--color-neutral);--tt-off:calc(100% + .5rem);--tt-tail:calc(100% + 1px + .25rem);display:inline-block;position:relative}.tooltip>.tooltip-content,.tooltip[data-tip]:before{border-radius:var(--radius-field);text-align:center;white-space:normal;max-width:20rem;color:var(--color-neutral-content);opacity:0;background-color:var(--tt-bg);pointer-events:none;z-index:2;--tw-content:attr(data-tip);content:var(--tw-content);width:max-content;padding-block:.25rem;padding-inline:.5rem;font-size:.875rem;line-height:1.25;position:absolute}.tooltip:after{opacity:0;background-color:var(--tt-bg);content:"";pointer-events:none;--mask-tooltip:url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A");width:.625rem;height:.25rem;-webkit-mask-position:-1px 0;mask-position:-1px 0;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-image:var(--mask-tooltip);-webkit-mask-image:var(--mask-tooltip);mask-image:var(--mask-tooltip);display:block;position:absolute}@media (prefers-reduced-motion:no-preference){.tooltip>.tooltip-content,.tooltip[data-tip]:before,.tooltip:after{transition:opacity .2s cubic-bezier(.4,0,.2,1) 75ms,transform .2s cubic-bezier(.4,0,.2,1) 75ms}}:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))>.tooltip-content,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))[data-tip]:before,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible)):after{opacity:1;--tt-pos:0rem}@media (prefers-reduced-motion:no-preference){:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))>.tooltip-content,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))[data-tip]:before,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible)):after{transition:opacity .2s cubic-bezier(.4,0,.2,1),transform .2s cubic-bezier(.4,0,.2,1)}}.tab{cursor:pointer;appearance:none;text-align:center;webkit-user-select:none;-webkit-user-select:none;user-select:none;flex-wrap:wrap;justify-content:center;align-items:center;display:inline-flex;position:relative}@media (hover:hover){.tab:hover{color:var(--color-base-content)}}.tab{--tab-p:.75rem;--tab-bg:var(--color-base-100);--tab-border-color:var(--color-base-300);--tab-radius-ss:0;--tab-radius-se:0;--tab-radius-es:0;--tab-radius-ee:0;--tab-order:0;--tab-radius-min:calc(.75rem - var(--border));--tab-radius-limit:min(var(--radius-field), var(--tab-radius-min));--tab-radius-grad:#0000 calc(69% - var(--border)), var(--tab-border-color) calc(69% - var(--border) + .25px), var(--tab-border-color) 69%, var(--tab-bg) calc(69% + .25px);order:var(--tab-order);height:var(--tab-height);padding-inline:var(--tab-p);border-color:#0000;font-size:.875rem}.tab:is(input[type=radio]){min-width:fit-content}.tab:is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.tab:is(label){position:relative}.tab:is(label) input{cursor:pointer;appearance:none;opacity:0;position:absolute;inset:0}:is(.tab:checked,.tab:is(label:has(:checked)),.tab:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]))+.tab-content{display:block}.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:color-mix(in oklab, var(--color-base-content) 50%, transparent)}}.tab:not(input):empty{cursor:default;flex-grow:1}.tab:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.tab:focus{outline-offset:2px;outline:2px solid #0000}}.tab:focus-visible,.tab:is(label:has(:checked:focus-visible)){outline-offset:-5px;outline:2px solid}.tab[disabled]{pointer-events:none;opacity:.4}.menu{--menu-active-fg:var(--color-neutral-content);--menu-active-bg:var(--color-neutral);flex-flow:column wrap;width:fit-content;padding:.5rem;font-size:.875rem;display:flex}.menu :where(li ul){white-space:nowrap;margin-inline-start:1rem;padding-inline-start:.5rem;position:relative}.menu :where(li ul):before{background-color:var(--color-base-content);opacity:.1;width:var(--border);content:"";inset-inline-start:0;position:absolute;top:.75rem;bottom:.75rem}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--radius-field);text-align:start;text-wrap:balance;-webkit-user-select:none;user-select:none;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;align-content:flex-start;align-items:center;gap:.5rem;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:grid}.menu :where(li>details>summary){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li>details>summary){outline-offset:2px;outline:2px solid #0000}}.menu :where(li>details>summary)::-webkit-details-marker{display:none}:is(.menu :where(li>details>summary),.menu :where(li>.menu-dropdown-toggle)):after{content:"";transform-origin:50%;pointer-events:none;justify-self:flex-end;width:.375rem;height:.375rem;transition-property:rotate,translate;transition-duration:.2s;display:block;translate:0 -1px;rotate:-135deg;box-shadow:inset 2px 2px}.menu details{interpolate-size:allow-keywords;overflow:hidden}.menu details::details-content{block-size:0}@media (prefers-reduced-motion:no-preference){.menu details::details-content{transition-behavior:allow-discrete;transition-property:block-size,content-visibility;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.menu details[open]::details-content{block-size:auto}.menu :where(li>details[open]>summary):after,.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after{translate:0 1px;rotate:45deg}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{color:var(--color-base-content);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn).menu-focus,.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title),li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(.menu-active,:active,.btn):focus-visible{outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){outline-offset:2px;outline:2px solid #0000}}.menu :where(li:not(.menu-title,.disabled)>:not(ul,details,.menu-title):not(.menu-active,:active,.btn):hover,li:not(.menu-title,.disabled)>details>summary:not(.menu-title):not(.menu-active,:active,.btn):hover){box-shadow:inset 0 1px oklch(0% 0 0/.01),inset 0 -1px oklch(100% 0 0/.01)}.menu :where(li:empty){background-color:var(--color-base-content);opacity:.1;height:1px;margin:.5rem 1rem}.menu :where(li){flex-flow:column wrap;flex-shrink:0;align-items:stretch;display:flex;position:relative}.menu :where(li) .badge{justify-self:flex-end}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{outline-offset:2px;outline:2px solid #0000}}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{color:var(--menu-active-fg);background-color:var(--menu-active-bg);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise)}:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):not(:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):active){box-shadow:0 2px calc(var(--depth) * 3px) -2px var(--menu-active-bg)}.menu :where(li).menu-disabled{pointer-events:none;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li).menu-disabled{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.menu .dropdown:focus-within .menu-dropdown-toggle:after{translate:0 1px;rotate:45deg}.menu .dropdown-content{margin-top:.5rem;padding:.5rem}.menu .dropdown-content:before{display:none}.dropdown{position-area:var(--anchor-v,bottom) var(--anchor-h,span-right);display:inline-block;position:relative}.dropdown>:not(:has(~[class*=dropdown-content])):focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.dropdown>:not(:has(~[class*=dropdown-content])):focus{outline-offset:2px;outline:2px solid #0000}}.dropdown .dropdown-content{position:absolute}.dropdown.dropdown-close .dropdown-content,.dropdown:not(details,.dropdown-open,.dropdown-hover:hover,:focus-within) .dropdown-content,.dropdown.dropdown-hover:not(:hover) [tabindex]:first-child:focus:not(:focus-visible)~.dropdown-content{transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover],.dropdown .dropdown-content{z-index:999}@media (prefers-reduced-motion:no-preference){.dropdown[popover],.dropdown .dropdown-content{transition-behavior:allow-discrete;transition-property:opacity,scale,display;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);animation:.2s dropdown}}@starting-style{.dropdown[popover],.dropdown .dropdown-content{opacity:0;scale:.95}}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within)>[tabindex]:first-child{pointer-events:none}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within) .dropdown-content,.dropdown:not(.dropdown-close).dropdown-hover:hover .dropdown-content{opacity:1;scale:1}.dropdown:is(details) summary::-webkit-details-marker{display:none}.dropdown:where([popover]){background:0 0}.dropdown[popover]{color:inherit;position:fixed}@supports not (position-area:bottom){.dropdown[popover]{margin:auto}.dropdown[popover].dropdown-close{transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover].dropdown-open:not(:popover-open){transform-origin:top;opacity:0;display:none;scale:.95}.dropdown[popover]::backdrop{background-color:oklab(0% none none/.3)}}:is(.dropdown[popover].dropdown-close,.dropdown[popover]:not(.dropdown-open,:popover-open)){transform-origin:top;opacity:0;display:none;scale:.95}:where(.btn){width:unset}.btn{cursor:pointer;text-align:center;vertical-align:middle;outline-offset:2px;webkit-user-select:none;-webkit-user-select:none;user-select:none;padding-inline:var(--btn-p);color:var(--btn-fg);--tw-prose-links:var(--btn-fg);height:var(--size);font-size:var(--fontsize,.875rem);outline-color:var(--btn-color,var(--color-base-content));background-color:var(--btn-bg);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--btn-noise);border-width:var(--border);border-style:solid;border-color:var(--btn-border);text-shadow:0 .5px oklch(100% 0 0 / calc(var(--depth) * .15));touch-action:manipulation;box-shadow:0 .5px 0 .5px oklch(100% 0 0 / calc(var(--depth) * 6%)) inset, var(--btn-shadow);--size:calc(var(--size-field,.25rem) * 10);--btn-bg:var(--btn-color,var(--color-base-200));--btn-fg:var(--color-base-content);--btn-p:1rem;--btn-border:var(--btn-bg);border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-wrap:nowrap;flex-shrink:0;justify-content:center;align-items:center;gap:.375rem;font-weight:600;transition-property:color,background-color,border-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:inline-flex}@supports (color:color-mix(in lab, red, red)){.btn{--btn-border:color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%))}}.btn{--btn-shadow:0 3px 2px -2px var(--btn-bg), 0 4px 3px -2px var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn{--btn-shadow:0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000), 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000)}}.btn{--btn-noise:var(--fx-noise)}@media (hover:hover){.btn:hover{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:hover{--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}}.btn:focus-visible,.btn:has(:focus-visible){isolation:isolate;outline-width:2px;outline-style:solid}.btn:active:not(.btn-active){--btn-bg:var(--btn-color,var(--color-base-200));translate:0 .5px}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 5%)}}.btn:active:not(.btn-active){--btn-border:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-border:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}.btn:active:not(.btn-active){--btn-shadow:0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0)}.btn:is(input[type=checkbox],input[type=radio]){appearance:none}.btn:is(input[type=checkbox],input[type=radio])[aria-label]:after{--tw-content:attr(aria-label);content:var(--tw-content)}.btn:where(input:checked:not(.filter .btn)){--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content);isolation:isolate}.loading{pointer-events:none;aspect-ratio:1;vertical-align:middle;width:calc(var(--size-selector,.25rem) * 6);background-color:currentColor;display:inline-block;-webkit-mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:100%;mask-size:100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.collapse{border-radius:var(--radius-box,1rem);isolation:isolate;grid-template-rows:max-content 0fr;grid-template-columns:minmax(0,1fr);width:100%;display:grid;position:relative;overflow:hidden}@media (prefers-reduced-motion:no-preference){.collapse{transition:grid-template-rows .2s}}.collapse>input:is([type=checkbox],[type=radio]){appearance:none;opacity:0;z-index:1;grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close)),.collapse:not(.collapse-close):has(>input:is([type=checkbox],[type=radio]):checked){grid-template-rows:max-content 1fr}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){content-visibility:visible;min-height:fit-content}@supports not (content-visibility:visible){.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){visibility:visible}}.collapse:focus-visible,.collapse:has(>input:is([type=checkbox],[type=radio]):focus-visible),.collapse:has(summary:focus-visible){outline-color:var(--color-base-content);outline-offset:2px;outline-width:2px;outline-style:solid}.collapse:not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-close)>input[type=radio]:not(:checked),.collapse:not(.collapse-close)>.collapse-title{cursor:pointer}:is(.collapse[tabindex]:focus:not(.collapse-close,.collapse[open]),.collapse[tabindex]:focus-within:not(.collapse-close,.collapse[open]))>.collapse-title{cursor:unset}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){padding-bottom:1rem}.collapse:is(details){width:100%}@media (prefers-reduced-motion:no-preference){.collapse:is(details)::details-content{transition:content-visibility .2s allow-discrete, visibility .2s allow-discrete, min-height .2s ease-out allow-discrete, padding .1s ease-out 20ms, background-color .2s ease-out, height .2s;interpolate-size:allow-keywords;height:0}.collapse:is(details):where([open])::details-content{height:auto}}.collapse:is(details) summary{display:block;position:relative}.collapse:is(details) summary::-webkit-details-marker{display:none}.collapse:is(details)>.collapse-content{content-visibility:visible}.collapse:is(details) summary{outline:none}.collapse-content{content-visibility:hidden;min-height:0;cursor:unset;grid-row-start:2;grid-column-start:1;padding-left:1rem;padding-right:1rem}@supports not (content-visibility:hidden){.collapse-content{visibility:hidden}}@media (prefers-reduced-motion:no-preference){.collapse-content{transition:content-visibility .2s allow-discrete, visibility .2s allow-discrete, min-height .2s ease-out allow-discrete, padding .1s ease-out 20ms, background-color .2s ease-out}}.validator-hint{visibility:hidden;margin-top:.5rem;font-size:.75rem}.validator:user-valid{--input-color:var(--color-success)}.validator:user-valid:focus{--input-color:var(--color-success)}.validator:user-valid:checked{--input-color:var(--color-success)}.validator:user-valid[aria-checked=true]{--input-color:var(--color-success)}.validator:user-valid:focus-within{--input-color:var(--color-success)}.validator:has(:user-valid){--input-color:var(--color-success)}.validator:has(:user-valid):focus{--input-color:var(--color-success)}.validator:has(:user-valid):checked{--input-color:var(--color-success)}.validator:has(:user-valid)[aria-checked=true]{--input-color:var(--color-success)}.validator:has(:user-valid):focus-within{--input-color:var(--color-success)}.validator:user-invalid{--input-color:var(--color-error)}.validator:user-invalid:focus{--input-color:var(--color-error)}.validator:user-invalid:checked{--input-color:var(--color-error)}.validator:user-invalid[aria-checked=true]{--input-color:var(--color-error)}.validator:user-invalid:focus-within{--input-color:var(--color-error)}.validator:user-invalid~.validator-hint{visibility:visible;color:var(--color-error)}.validator:has(:user-invalid){--input-color:var(--color-error)}.validator:has(:user-invalid):focus{--input-color:var(--color-error)}.validator:has(:user-invalid):checked{--input-color:var(--color-error)}.validator:has(:user-invalid)[aria-checked=true]{--input-color:var(--color-error)}.validator:has(:user-invalid):focus-within{--input-color:var(--color-error)}.validator:has(:user-invalid)~.validator-hint{visibility:visible;color:var(--color-error)}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))),:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):focus,:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):checked,:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))[aria-checked=true],:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):focus-within{--input-color:var(--color-error)}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))~.validator-hint{visibility:visible;color:var(--color-error)}.list{flex-direction:column;font-size:.875rem;display:flex}.list .list-row{--list-grid-cols:minmax(0, auto) 1fr;border-radius:var(--radius-box);word-break:break-word;grid-auto-flow:column;grid-template-columns:var(--list-grid-cols);gap:1rem;padding:1rem;display:grid;position:relative}:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{content:"";border-bottom:var(--border) solid;inset-inline:var(--radius-box);border-color:var(--color-base-content);position:absolute;bottom:0}@supports (color:color-mix(in lab, red, red)){:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{border-color:color-mix(in oklab, var(--color-base-content) 5%, transparent)}}.toast{translate:var(--toast-x,0) var(--toast-y,0);inset-inline:auto 1rem;background-color:#0000;flex-direction:column;gap:.5rem;width:max-content;max-width:calc(100vw - 2rem);display:flex;position:fixed;top:auto;bottom:1rem}@media (prefers-reduced-motion:no-preference){.toast>*{animation:.25s ease-out toast}}.toggle{border:var(--border) solid currentColor;color:var(--input-color);cursor:pointer;appearance:none;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)));padding:var(--toggle-p);flex-shrink:0;grid-template-columns:0fr 1fr 1fr;place-content:center;display:inline-grid;position:relative;box-shadow:inset 0 1px}@supports (color:color-mix(in lab, red, red)){.toggle{box-shadow:0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset}}.toggle{--input-color:var(--color-base-content);transition:color .3s,grid-template-columns .2s}@supports (color:color-mix(in lab, red, red)){.toggle{--input-color:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}.toggle{--toggle-p:calc(var(--size) * .125);--size:calc(var(--size-selector,.25rem) * 6);width:calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2);height:var(--size)}.toggle>*{z-index:1;cursor:pointer;appearance:none;background-color:#0000;border:none;grid-column:2/span 1;grid-row-start:1;height:100%;padding:.125rem;transition:opacity .2s,rotate .4s}.toggle>:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.toggle>:focus{outline-offset:2px;outline:2px solid #0000}}.toggle>:nth-child(2){color:var(--color-base-100);rotate:0deg}.toggle>:nth-child(3){color:var(--color-base-100);opacity:0;rotate:-15deg}.toggle:has(:checked)>:nth-child(2){opacity:0;rotate:15deg}.toggle:has(:checked)>:nth-child(3){opacity:1;rotate:0deg}.toggle:before{aspect-ratio:1;border-radius:var(--radius-selector);--tw-content:"";content:var(--tw-content);width:100%;height:100%;box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor;background-color:currentColor;grid-row-start:1;grid-column-start:2;transition:background-color .1s,translate .2s,inset-inline-start .2s;position:relative;inset-inline-start:0;translate:0}@supports (color:color-mix(in lab, red, red)){.toggle:before{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000)}}.toggle:before{background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise)}@media (forced-colors:active){.toggle:before{outline-style:var(--tw-outline-style);outline-offset:calc(1px * -1);outline-width:1px}}@media print{.toggle:before{outline-offset:-1rem;outline:.25rem solid}}.toggle:focus-visible,.toggle:has(:focus-visible){outline-offset:2px;outline:2px solid}.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked){background-color:var(--color-base-100);--input-color:var(--color-base-content);grid-template-columns:1fr 1fr 0fr}:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{background-color:currentColor}@starting-style{:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{opacity:0}}.toggle:indeterminate{grid-template-columns:.5fr 1fr .5fr}.toggle:disabled{cursor:not-allowed;opacity:.3}.toggle:disabled:before{border:var(--border) solid currentColor;background-color:#0000}.input{cursor:text;border:var(--border) solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;white-space:nowrap;width:clamp(3rem,20rem,100%);height:var(--size);font-size:max(var(--font-size,.875rem), .875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.5rem;padding-inline:.75rem;display:inline-flex;position:relative}@supports (color:color-mix(in lab, red, red)){.input{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.input{--size:calc(var(--size-field,.25rem) * 10);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.input:where(input){display:inline-flex}.input :where(input){appearance:none;background-color:#0000;border:none;width:100%;height:100%;display:inline-flex}.input :where(input):focus,.input :where(input):focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.input :where(input):focus,.input :where(input):focus-within{outline-offset:2px;outline:2px solid #0000}}.input :where(input[type=url]),.input :where(input[type=email]){direction:ltr}.input :where(input[type=date]){display:inline-flex}.input:focus,.input:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.input:focus,.input:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.input:focus,.input:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.input:focus,.input:focus-within{--font-size:1rem}}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{box-shadow:none}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.input[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input::-webkit-calendar-picker-indicator{position:absolute;inset-inline-end:.75em}.input:has(>input[type=date]) :where(input[type=date]){webkit-appearance:none;appearance:none;display:inline-flex}.input:has(>input[type=date]) input[type=date]::-webkit-calendar-picker-indicator{cursor:pointer;width:1em;height:1em;position:absolute;inset-inline-end:.75em}.indicator{width:max-content;display:inline-flex;position:relative}.indicator :where(.indicator-item){z-index:1;white-space:nowrap;top:var(--indicator-t,0);bottom:var(--indicator-b,auto);left:var(--indicator-s,auto);right:var(--indicator-e,0);translate:var(--indicator-x,50%) var(--indicator-y,-50%);position:absolute}.table{border-collapse:separate;--tw-border-spacing-x:calc(.25rem * 0);--tw-border-spacing-y:calc(.25rem * 0);width:100%;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y);border-radius:var(--radius-box);text-align:left;font-size:.875rem;position:relative}.table:where(:dir(rtl),[dir=rtl],[dir=rtl] *){text-align:right}@media (hover:hover){:is(.table tr.row-hover,.table tr.row-hover:nth-child(2n)):hover{background-color:var(--color-base-200)}}.table :where(th,td){vertical-align:middle;padding-block:.75rem;padding-inline:1rem}.table :where(thead,tfoot){white-space:nowrap;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead,tfoot){color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.table :where(thead,tfoot){font-size:.875rem;font-weight:600}.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)}}.table :where(.table-pin-rows thead tr){z-index:1;background-color:var(--color-base-100);position:sticky;top:0}.table :where(.table-pin-rows tfoot tr){z-index:1;background-color:var(--color-base-100);position:sticky;bottom:0}.table :where(.table-pin-cols tr th){background-color:var(--color-base-100);position:sticky;left:0;right:0}.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000)}}.steps{counter-reset:step;grid-auto-columns:1fr;grid-auto-flow:column;display:inline-grid;overflow:auto hidden}.steps .step{text-align:center;--step-bg:var(--color-base-300);--step-fg:var(--color-base-content);grid-template-rows:40px 1fr;grid-template-columns:auto;place-items:center;min-width:4rem;display:grid}.steps .step:before{width:100%;height:.5rem;color:var(--step-bg);background-color:var(--step-bg);content:"";border:1px solid;grid-row-start:1;grid-column-start:1;margin-inline-start:-100%;top:0}.steps .step>.step-icon,.steps .step:not(:has(.step-icon)):after{--tw-content:counter(step);content:var(--tw-content);counter-increment:step;z-index:1;color:var(--step-fg);background-color:var(--step-bg);border:1px solid var(--step-bg);border-radius:3.40282e38px;grid-row-start:1;grid-column-start:1;place-self:center;place-items:center;width:2rem;height:2rem;display:grid;position:relative}.steps .step:first-child:before{--tw-content:none;content:var(--tw-content)}.steps .step[data-content]:after{--tw-content:attr(data-content);content:var(--tw-content)}.range{appearance:none;webkit-appearance:none;--range-thumb:var(--color-base-100);--range-thumb-size:calc(var(--size-selector,.25rem) * 6);--range-progress:currentColor;--range-fill:1;--range-p:.25rem;--range-bg:currentColor}@supports (color:color-mix(in lab, red, red)){.range{--range-bg:color-mix(in oklab, currentColor 10%, #0000)}}.range{cursor:pointer;vertical-align:middle;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));width:clamp(3rem,20rem,100%);height:var(--range-thumb-size);background-color:#0000;border:none;overflow:hidden}[dir=rtl] .range{--range-dir:-1}.range:focus{outline:none}.range:focus-visible{outline-offset:2px;outline:2px solid}.range::-webkit-slider-runnable-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size) * .5)}@media (forced-colors:active){.range::-webkit-slider-runnable-track{border:1px solid}.range::-moz-range-track{border:1px solid}}.range::-webkit-slider-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));background-color:var(--range-thumb);height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p) solid;appearance:none;webkit-appearance:none;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill));position:relative;top:50%;transform:translateY(-50%)}@supports (color:color-mix(in lab, red, red)){.range::-webkit-slider-thumb{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill))}}.range::-moz-range-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size) * .5)}.range::-moz-range-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p) solid;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px currentColor, 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill));background-color:currentColor;position:relative;top:50%}@supports (color:color-mix(in lab, red, red)){.range::-moz-range-thumb{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir,1) * -100cqw) - (var(--range-dir,1) * var(--range-thumb-size) / 2)) 0 0 calc(100cqw * var(--range-fill))}}.range:disabled{cursor:not-allowed;opacity:.3}.diff-resizer{isolation:isolate;z-index:2;resize:horizontal;opacity:0;cursor:ew-resize;transform-origin:100% 100%;clip-path:inset(calc(100% - .75rem) 0 0 calc(100% - .75rem));grid-row-start:2;grid-column-start:1;width:50cqi;min-width:1rem;max-width:calc(100cqi - 1rem);height:.75rem;transition:min-width .3s ease-out,max-width .3s ease-out;position:relative;overflow:hidden;transform:scaleY(5)translate(.32rem,50%)}.select{border:var(--border) solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);height:var(--size);touch-action:manipulation;white-space:nowrap;text-overflow:ellipsis;box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;background-image:linear-gradient(45deg,#0000 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,#0000 50%);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-repeat:no-repeat;background-size:4px 4px,4px 4px;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.375rem;padding-inline:.75rem 1.75rem;font-size:.875rem;display:inline-flex;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.select{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.select{border-color:var(--input-color);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.select{--size:calc(var(--size-field,.25rem) * 10)}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}[dir=rtl] .select::picker(select){translate:.5rem}[dir=rtl] .select select::picker(select){translate:.5rem}.select[multiple]{background-image:none;height:auto;padding-block:.75rem;padding-inline-end:.75rem;overflow:auto}.select select{appearance:none;width:calc(100% + 2.75rem);height:calc(100% - calc(var(--border) * 2));background:inherit;border-radius:inherit;border-style:none;align-items:center;margin-inline:-.75rem -1.75rem;padding-inline:.75rem 1.75rem}.select select:focus,.select select:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.select select:focus,.select select:focus-within{outline-offset:2px;outline:2px solid #0000}}.select select:not(:last-child){background-image:none;margin-inline-end:-1.375rem}.select:focus,.select:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.select:focus,.select:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.select:focus,.select:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.select:has(>select[disabled])>select[disabled]{cursor:not-allowed}@supports (appearance:base-select){.select,.select select{appearance:base-select}:is(.select,.select select)::picker(select){appearance:base-select}}:is(.select,.select select)::picker(select){color:inherit;border:var(--border) solid var(--color-base-200);border-radius:var(--radius-box);background-color:inherit;max-height:min(24rem,70dvh);box-shadow:0 2px calc(var(--depth) * 3px) -2px oklch(0% 0 0/.2);box-shadow:0 20px 25px -5px rgb(0 0 0/calc(var(--depth) * .1)), 0 8px 10px -6px rgb(0 0 0/calc(var(--depth) * .1));margin-block:.5rem;margin-inline:.5rem;padding:.5rem;translate:-.5rem}:is(.select,.select select)::picker-icon{display:none}:is(.select,.select select) optgroup{padding-top:.5em}:is(.select,.select select) optgroup option:first-child{margin-top:.5em}:is(.select,.select select) option{border-radius:var(--radius-field);white-space:normal;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{outline-offset:2px;outline:2px solid #0000}}:is(.select,.select select) option:not(:disabled):active{background-color:var(--color-neutral);color:var(--color-neutral-content);box-shadow:0 2px calc(var(--depth) * 3px) -2px var(--color-neutral)}.swap{cursor:pointer;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;place-content:center;display:inline-grid;position:relative}.swap input{appearance:none;border:none}.swap>*{grid-row-start:1;grid-column-start:1}@media (prefers-reduced-motion:no-preference){.swap>*{transition-property:transform,rotate,opacity;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.swap .swap-on,.swap .swap-indeterminate,.swap input:indeterminate~.swap-on,.swap input:is(:checked,:indeterminate)~.swap-off{opacity:0}.swap input:checked~.swap-on,.swap input:indeterminate~.swap-indeterminate{opacity:1;backface-visibility:visible}.collapse-title{grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out;position:relative}.checkbox{border:var(--border) solid var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox{border:var(--border) solid var(--input-color,color-mix(in oklab, var(--color-base-content) 20%, #0000))}}.checkbox{cursor:pointer;appearance:none;border-radius:var(--radius-selector);vertical-align:middle;color:var(--color-base-content);box-shadow:0 1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 0 #0000 inset, 0 0 #0000;--size:calc(var(--size-selector,.25rem) * 6);width:var(--size);height:var(--size);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);flex-shrink:0;padding:.25rem;transition:background-color .2s,box-shadow .2s;display:inline-block;position:relative}.checkbox:before{--tw-content:"";content:var(--tw-content);opacity:0;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,70% 80%,70% 100%);width:100%;height:100%;box-shadow:0px 3px 0 0px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;background-color:currentColor;font-size:1rem;line-height:.75;transition:clip-path .3s .1s,opacity .1s .1s,rotate .3s .1s,translate .3s .1s;display:block;rotate:45deg}.checkbox:focus-visible{outline:2px solid var(--input-color,currentColor);outline-offset:2px}.checkbox:checked,.checkbox[aria-checked=true]{background-color:var(--input-color,#0000);box-shadow:0 0 #0000 inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * .1))}:is(.checkbox:checked,.checkbox[aria-checked=true]):before{clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 0%,70% 0%,70% 100%);opacity:1}@media (forced-colors:active){:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:0deg}}@media print{:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:0deg}}.checkbox:indeterminate{background-color:var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox:indeterminate{background-color:var(--input-color,color-mix(in oklab, var(--color-base-content) 20%, #0000))}}.checkbox:indeterminate:before{opacity:1;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,80% 80%,80% 100%);translate:0 -35%;rotate:0deg}.radio{cursor:pointer;appearance:none;vertical-align:middle;border:var(--border) solid var(--input-color,currentColor);border-radius:3.40282e38px;flex-shrink:0;padding:.25rem;display:inline-block;position:relative}@supports (color:color-mix(in lab, red, red)){.radio{border:var(--border) solid var(--input-color,color-mix(in srgb, currentColor 20%, #0000))}}.radio{box-shadow:0 1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset;--size:calc(var(--size-selector,.25rem) * 6);width:var(--size);height:var(--size);color:var(--input-color,currentColor)}.radio:before{--tw-content:"";content:var(--tw-content);background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);border-radius:3.40282e38px;width:100%;height:100%;display:block}.radio:focus-visible{outline:2px solid}.radio:checked,.radio[aria-checked=true]{background-color:var(--color-base-100);border-color:currentColor}@media (prefers-reduced-motion:no-preference){.radio:checked,.radio[aria-checked=true]{animation:.2s ease-out radio}}:is(.radio:checked,.radio[aria-checked=true]):before{box-shadow:0 -1px oklch(0% 0 0 / calc(var(--depth) * .1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * .1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * .1));background-color:currentColor}@media (forced-colors:active){:is(.radio:checked,.radio[aria-checked=true]):before{outline-style:var(--tw-outline-style);outline-offset:calc(1px * -1);outline-width:1px}}@media print{:is(.radio:checked,.radio[aria-checked=true]):before{outline-offset:-1rem;outline:.25rem solid}}.rating{vertical-align:middle;display:inline-flex;position:relative}.rating input{appearance:none;border:none}.rating :where(*){background-color:var(--color-base-content);opacity:.2;border-radius:0;width:1.5rem;height:1.5rem}@media (prefers-reduced-motion:no-preference){.rating :where(*){animation:.25s ease-out rating}}.rating :where(*):is(input){cursor:pointer}.rating .rating-hidden{background-color:#0000;width:.5rem}.rating input[type=radio]:checked{background-image:none}.rating :checked,.rating [aria-checked=true],.rating [aria-current=true],.rating :has(~:checked,~[aria-checked=true],~[aria-current=true]){opacity:1}.rating :focus-visible{scale:1.1}@media (prefers-reduced-motion:no-preference){.rating :focus-visible{transition:scale .2s ease-out}}.rating :active:focus{animation:none;scale:1.1}.navbar{align-items:center;width:100%;min-height:4rem;padding:.5rem;display:flex}.card{border-radius:var(--radius-box);outline-offset:2px;outline:0 solid #0000;flex-direction:column;transition:outline .2s ease-in-out;display:flex;position:relative}.card:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.card:focus{outline-offset:2px;outline:2px solid #0000}}.card:focus-visible{outline-color:currentColor}.card :where(figure:first-child){border-start-start-radius:inherit;border-start-end-radius:inherit;border-end-end-radius:unset;border-end-start-radius:unset;overflow:hidden}.card :where(figure:last-child){border-start-start-radius:unset;border-start-end-radius:unset;border-end-end-radius:inherit;border-end-start-radius:inherit;overflow:hidden}.card figure{justify-content:center;align-items:center;display:flex}.card:has(>input:is(input[type=checkbox],input[type=radio])){cursor:pointer;-webkit-user-select:none;user-select:none}.card:has(>:checked){outline:2px solid}.stats{border-radius:var(--radius-box);grid-auto-flow:column;display:inline-grid;position:relative;overflow-x:auto}.progress{appearance:none;border-radius:var(--radius-box);background-color:currentColor;width:100%;height:.5rem;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.progress{background-color:color-mix(in oklab, currentcolor 20%, transparent)}}.progress{color:var(--color-base-content)}.progress:indeterminate{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%}@media (prefers-reduced-motion:no-preference){.progress:indeterminate{animation:5s ease-in-out infinite progress}}@supports ((-moz-appearance:none)){.progress:indeterminate::-moz-progress-bar{background-color:#0000}@media (prefers-reduced-motion:no-preference){.progress:indeterminate::-moz-progress-bar{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%;animation:5s ease-in-out infinite progress}}.progress::-moz-progress-bar{border-radius:var(--radius-box);background-color:currentColor}}@supports ((-webkit-appearance:none)){.progress::-webkit-progress-bar{border-radius:var(--radius-box);background-color:#0000}.progress::-webkit-progress-value{border-radius:var(--radius-box);background-color:currentColor}}.textarea{border:var(--border) solid #0000;appearance:none;border-radius:var(--radius-field);background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);min-height:5rem;font-size:max(var(--font-size,.875rem), .875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset;flex-shrink:1;padding-block:.5rem;padding-inline:.75rem}@supports (color:color-mix(in lab, red, red)){.textarea{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * .1)) inset}}.textarea{--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea{--input-color:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}.textarea textarea{appearance:none;background-color:#0000;border:none}.textarea textarea:focus,.textarea textarea:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.textarea textarea:focus,.textarea textarea:focus-within{outline-offset:2px;outline:2px solid #0000}}.textarea:focus,.textarea:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.textarea:focus,.textarea:focus-within{box-shadow:0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000)}}.textarea:focus,.textarea:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.textarea:focus,.textarea:focus-within{--font-size:1rem}}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){color:color-mix(in oklab, var(--color-base-content) 40%, transparent)}}:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder{color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){box-shadow:none}.textarea:has(>textarea[disabled])>textarea[disabled]{cursor:not-allowed}.stack{grid-template-rows:3px 4px 1fr 4px 3px;grid-template-columns:3px 4px 1fr 4px 3px;display:inline-grid}.stack>*{width:100%;height:100%}.stack>:nth-child(n+2){opacity:.7;width:100%}.stack>:nth-child(2){z-index:2;opacity:.9}.stack>:first-child{z-index:3;width:100%}.tab-content{order:var(--tabcontent-order);--tabcontent-radius-ss:var(--radius-box);--tabcontent-radius-se:var(--radius-box);--tabcontent-radius-es:var(--radius-box);--tabcontent-radius-ee:var(--radius-box);--tabcontent-order:1;width:100%;height:calc(100% - var(--tab-height) + var(--border));margin:var(--tabcontent-margin);border-color:#0000;border-width:var(--border);border-start-start-radius:var(--tabcontent-radius-ss);border-start-end-radius:var(--tabcontent-radius-se);border-end-end-radius:var(--tabcontent-radius-ee);border-end-start-radius:var(--tabcontent-radius-es);display:none}.modal-box{background-color:var(--color-base-100);border-top-left-radius:var(--modal-tl,var(--radius-box));border-top-right-radius:var(--modal-tr,var(--radius-box));border-bottom-left-radius:var(--modal-bl,var(--radius-box));border-bottom-right-radius:var(--modal-br,var(--radius-box));opacity:0;overscroll-behavior:contain;grid-row-start:1;grid-column-start:1;width:91.6667%;max-width:32rem;max-height:100vh;padding:1.5rem;transition:translate .3s ease-out,scale .3s ease-out,opacity .2s ease-out 50ms,box-shadow .3s ease-out;overflow-y:auto;scale:.95;box-shadow:0 25px 50px -12px oklch(0% 0 0/.25)}.divider{white-space:nowrap;height:1rem;margin:var(--divider-m,1rem 0);--divider-color:var(--color-base-content);flex-direction:row;align-self:stretch;align-items:center;display:flex}@supports (color:color-mix(in lab, red, red)){.divider{--divider-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.divider:before,.divider:after{content:"";background-color:var(--divider-color);flex-grow:1;width:100%;height:.125rem}@media print{.divider:before,.divider:after{border:.5px solid}}.divider:not(:empty){gap:1rem}.filter{flex-wrap:wrap;display:flex}.filter input[type=radio]{width:auto}.filter input{opacity:1;transition:margin .1s,opacity .3s,padding .3s,border-width .1s;overflow:hidden;scale:1}.filter input:not(:last-child){margin-inline-end:.25rem}.filter input.filter-reset{aspect-ratio:1}.filter input.filter-reset:after{--tw-content:"×";content:var(--tw-content)}.filter:not(:has(input:checked:not(.filter-reset))) .filter-reset,.filter:not(:has(input:checked:not(.filter-reset))) input[type=reset],.filter:has(input:checked:not(.filter-reset)) input:not(:checked,.filter-reset,input[type=reset]){opacity:0;border-width:0;width:0;margin-inline:0;padding-inline:0;scale:0}.label{white-space:nowrap;color:currentColor;align-items:center;gap:.375rem;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.label{color:color-mix(in oklab, currentcolor 60%, transparent)}}.label:has(input){cursor:pointer}.label:is(.input>*,.select>*){white-space:nowrap;height:calc(100% - .5rem);font-size:inherit;align-items:center;padding-inline:.75rem;display:flex}.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border) solid currentColor;margin-inline:-.75rem .75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border) solid color-mix(in oklab, currentColor 10%, #0000)}}.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border) solid currentColor;margin-inline:.75rem -.75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border) solid color-mix(in oklab, currentColor 10%, #0000)}}.fieldset-legend{color:var(--color-base-content);justify-content:space-between;align-items:center;gap:.5rem;margin-bottom:-.25rem;padding-block:.5rem;font-weight:600;display:flex}.status{aspect-ratio:1;border-radius:var(--radius-selector);background-color:var(--color-base-content);width:.5rem;height:.5rem;display:inline-block}@supports (color:color-mix(in lab, red, red)){.status{background-color:color-mix(in oklab, var(--color-base-content) 20%, transparent)}}.status{vertical-align:middle;color:#0000004d;background-position:50%;background-repeat:no-repeat}@supports (color:color-mix(in lab, red, red)){.status{color:color-mix(in oklab, var(--color-black) 30%, transparent)}}.status{background-image:radial-gradient(circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * .5)), #0000);box-shadow:0 2px 3px -1px}@supports (color:color-mix(in lab, red, red)){.status{box-shadow:0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), #0000)}}.badge{border-radius:var(--radius-selector);vertical-align:middle;color:var(--badge-fg);border:var(--border) solid var(--badge-color,var(--color-base-200));background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);background-color:var(--badge-bg);--badge-bg:var(--badge-color,var(--color-base-100));--badge-fg:var(--color-base-content);--size:calc(var(--size-selector,.25rem) * 6);width:fit-content;height:var(--size);padding-inline:calc(var(--size) / 2 - var(--border));justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;display:inline-flex}.kbd{border-radius:var(--radius-field);background-color:var(--color-base-200);vertical-align:middle;border:var(--border) solid var(--color-base-content);justify-content:center;align-items:center;padding-inline:.5em;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.kbd{border:var(--border) solid color-mix(in srgb, var(--color-base-content) 20%, #0000)}}.kbd{border-bottom:calc(var(--border) + 1px) solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.kbd{border-bottom:calc(var(--border) + 1px) solid color-mix(in srgb, var(--color-base-content) 20%, #0000)}}.kbd{--size:calc(var(--size-selector,.25rem) * 6);height:var(--size);min-width:var(--size);font-size:.875rem}.footer{grid-auto-flow:row;place-items:start;gap:2.5rem 1rem;width:100%;font-size:.875rem;line-height:1.25rem;display:grid}.footer>*{place-items:start;gap:.5rem;display:grid}.footer.footer-center{text-align:center;grid-auto-flow:column dense;place-items:center}.footer.footer-center>*{place-items:center}.stat{grid-template-columns:repeat(1,1fr);column-gap:1rem;width:100%;padding-block:1rem;padding-inline:1.5rem;display:inline-grid}.stat:not(:last-child){border-inline-end:var(--border) dashed currentColor}@supports (color:color-mix(in lab, red, red)){.stat:not(:last-child){border-inline-end:var(--border) dashed color-mix(in oklab, currentColor 10%, #0000)}}.stat:not(:last-child){border-block-end:none}.navbar-end{justify-content:flex-end;align-items:center;width:50%;display:inline-flex}.navbar-start{justify-content:flex-start;align-items:center;width:50%;display:inline-flex}.card-body{padding:var(--card-p,1.5rem);font-size:var(--card-fs,.875rem);flex-direction:column;flex:auto;gap:.5rem;display:flex}.card-body :where(p){flex-grow:1}.navbar-center{flex-shrink:0;align-items:center;display:inline-flex}.fieldset-label{color:var(--color-base-content);align-items:center;gap:.375rem;display:flex}@supports (color:color-mix(in lab, red, red)){.fieldset-label{color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.fieldset-label:has(input){cursor:pointer}.alert{--alert-border-color:var(--color-base-200);border-radius:var(--radius-box);color:var(--color-base-content);background-color:var(--alert-color,var(--color-base-200));text-align:start;background-size:auto, calc(var(--noise) * 100%);background-image:none, var(--fx-noise);box-shadow:0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * .08)) inset, 0 1px #000, 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * .08));border-style:solid;grid-template-columns:auto;grid-auto-flow:column;justify-content:start;place-items:center start;gap:1rem;padding-block:.75rem;padding-inline:1rem;font-size:.875rem;line-height:1.25rem;display:grid}@supports (color:color-mix(in lab, red, red)){.alert{box-shadow:0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * .08)) inset, 0 1px color-mix(in oklab, color-mix(in oklab, #000 20%, var(--alert-color,var(--color-base-200))) calc(var(--depth) * 20%), #0000), 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * .08))}}.alert:has(:nth-child(2)){grid-template-columns:auto minmax(auto,1fr)}.fieldset{grid-template-columns:1fr;grid-auto-rows:max-content;gap:.375rem;padding-block:.25rem;font-size:.75rem;display:grid}.card-actions{flex-wrap:wrap;align-items:flex-start;gap:.5rem;display:flex}.card-title{font-size:var(--cardtitle-fs,1.125rem);align-items:center;gap:.5rem;font-weight:600;display:flex}.mask{vertical-align:middle;display:inline-block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.skeleton{border-radius:var(--radius-box);background-color:var(--color-base-300)}@media (prefers-reduced-motion:reduce){.skeleton{transition-duration:15s}}.skeleton{will-change:background-position;background-image:linear-gradient(105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100%);background-position-x:-50%;background-size:200%}@media (prefers-reduced-motion:no-preference){.skeleton{animation:1.8s ease-in-out infinite skeleton}}.link{cursor:pointer;text-decoration-line:underline}.link:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.link:focus{outline-offset:2px;outline:2px solid #0000}}.link:focus-visible{outline-offset:2px;outline:2px solid}.btn-error{--btn-color:var(--color-error);--btn-fg:var(--color-error-content)}.btn-primary{--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content)}}@layer daisyui.l1.l2{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{pointer-events:auto;visibility:visible;opacity:1;transition:visibility 0s allow-discrete, background-color .3s ease-out, opacity .1s ease-out;background-color:oklch(0% 0 0/.4)}:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal) .modal-box{opacity:1;translate:0;scale:1}:root:has(:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal)){--page-has-backdrop:1;--page-overflow:hidden;--page-scroll-bg:var(--page-scroll-bg-on);--page-scroll-gutter:stable;--page-scroll-transition:var(--page-scroll-transition-on);animation:forwards set-page-has-scroll;animation-timeline:scroll()}@starting-style{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{opacity:0}}.tooltip>.tooltip-content,.tooltip[data-tip]:before{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-off) 50%}.tooltip:after{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-tail) 50%}.btn:disabled:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.btn:disabled:not(.btn-link,.btn-ghost){box-shadow:none}.btn:disabled{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled{--btn-fg:color-mix(in oklch, var(--color-base-content) 20%, #0000)}}.btn[disabled]:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab, var(--color-base-content) 10%, transparent)}}.btn[disabled]:not(.btn-link,.btn-ghost){box-shadow:none}.btn[disabled]{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]{--btn-fg:color-mix(in oklch, var(--color-base-content) 20%, #0000)}}@media (prefers-reduced-motion:no-preference){.collapse[open].collapse-arrow>.collapse-title:after,.collapse.collapse-open.collapse-arrow>.collapse-title:after{transform:translateY(-50%)rotate(225deg)}}.collapse.collapse-open.collapse-plus>.collapse-title:after{--tw-content:"−";content:var(--tw-content)}:is(.collapse[tabindex].collapse-arrow:focus:not(.collapse-close),.collapse.collapse-arrow[tabindex]:focus-within:not(.collapse-close))>.collapse-title:after,.collapse.collapse-arrow:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{transform:translateY(-50%)rotate(225deg)}.collapse[open].collapse-plus>.collapse-title:after,.collapse[tabindex].collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse.collapse-plus:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{--tw-content:"−";content:var(--tw-content)}.list .list-row:has(.list-col-grow:first-child){--list-grid-cols:1fr}.list .list-row:has(.list-col-grow:nth-child(2)){--list-grid-cols:minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(3)){--list-grid-cols:minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(4)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(5)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row:has(.list-col-grow:nth-child(6)){--list-grid-cols:minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr}.list .list-row>*{grid-row-start:1}.steps .step-neutral+.step-neutral:before,.steps .step-neutral:after,.steps .step-neutral>.step-icon{--step-bg:var(--color-neutral);--step-fg:var(--color-neutral-content)}.steps .step-primary+.step-primary:before,.steps .step-primary:after,.steps .step-primary>.step-icon{--step-bg:var(--color-primary);--step-fg:var(--color-primary-content)}.steps .step-secondary+.step-secondary:before,.steps .step-secondary:after,.steps .step-secondary>.step-icon{--step-bg:var(--color-secondary);--step-fg:var(--color-secondary-content)}.steps .step-accent+.step-accent:before,.steps .step-accent:after,.steps .step-accent>.step-icon{--step-bg:var(--color-accent);--step-fg:var(--color-accent-content)}.steps .step-info+.step-info:before,.steps .step-info:after,.steps .step-info>.step-icon{--step-bg:var(--color-info);--step-fg:var(--color-info-content)}.steps .step-success+.step-success:before,.steps .step-success:after,.steps .step-success>.step-icon{--step-bg:var(--color-success);--step-fg:var(--color-success-content)}.steps .step-warning+.step-warning:before,.steps .step-warning:after,.steps .step-warning>.step-icon{--step-bg:var(--color-warning);--step-fg:var(--color-warning-content)}.steps .step-error+.step-error:before,.steps .step-error:after,.steps .step-error>.step-icon{--step-bg:var(--color-error);--step-fg:var(--color-error-content)}.menu-vertical{flex-direction:column;display:inline-flex}.menu-vertical>li:not(.menu-title)>details>ul{background-color:revert-layer;border-radius:revert-layer;animation:revert-layer;box-shadow:revert-layer;margin-inline-start:1rem;margin-top:0;padding-block:0;padding-inline-end:0;transition:revert-layer;position:relative}.checkbox:disabled,.radio:disabled{cursor:not-allowed;opacity:.2}.rating.rating-xs :where(:not(.rating-hidden)){width:1rem;height:1rem}.rating.rating-sm :where(:not(.rating-hidden)){width:1.25rem;height:1.25rem}.rating.rating-md :where(:not(.rating-hidden)){width:1.5rem;height:1.5rem}.rating.rating-lg :where(:not(.rating-hidden)){width:1.75rem;height:1.75rem}.rating.rating-xl :where(:not(.rating-hidden)){width:2rem;height:2rem}:where(.navbar){position:relative}.tooltip-top>.tooltip-content,.tooltip-top[data-tip]:before{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-off) 50%}.tooltip-top:after{transform:translateX(-50%) translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-tail) 50%}.btn-active{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn-active{--btn-bg:color-mix(in oklab, var(--btn-color,var(--color-base-200)), #000 7%)}}.btn-active{--btn-shadow:0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0);isolation:isolate}:is(.stack,.stack.stack-bottom)>*{grid-area:3/3/6/4}:is(.stack,.stack.stack-bottom)>:nth-child(2){grid-area:2/2/5/5}:is(.stack,.stack.stack-bottom)>:first-child{grid-area:1/1/4/6}.stack.stack-top>*{grid-area:1/3/4/4}.stack.stack-top>:nth-child(2){grid-area:2/2/5/5}.stack.stack-top>:first-child{grid-area:3/1/6/6}.stack.stack-start>*{grid-area:3/1/4/4}.stack.stack-start>:nth-child(2){grid-area:2/2/5/5}.stack.stack-start>:first-child{grid-area:1/3/6/6}.stack.stack-end>*{grid-area:3/3/4/6}.stack.stack-end>:nth-child(2){grid-area:2/2/5/5}.stack.stack-end>:first-child{grid-area:1/1/6/4}.input-lg{--size:calc(var(--size-field,.25rem) * 12);font-size:max(var(--font-size,1.125rem), 1.125rem)}.input-lg[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input-md{--size:calc(var(--size-field,.25rem) * 10);font-size:max(var(--font-size,.875rem), .875rem)}.input-md[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input-sm{--size:calc(var(--size-field,.25rem) * 8);font-size:max(var(--font-size,.75rem), .75rem)}.input-sm[type=number]::-webkit-inner-spin-button{margin-block:-.5rem;margin-inline-end:-.75rem}.input-xl{--size:calc(var(--size-field,.25rem) * 14);font-size:max(var(--font-size,1.375rem), 1.375rem)}.input-xl[type=number]::-webkit-inner-spin-button{margin-block:-1rem;margin-inline-end:-.75rem}.input-xs{--size:calc(var(--size-field,.25rem) * 6);font-size:max(var(--font-size,.6875rem), .6875rem)}.input-xs[type=number]::-webkit-inner-spin-button{margin-block:-.25rem;margin-inline-end:-.75rem}.badge-ghost{border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content);background-image:none}.badge-outline{color:var(--badge-color);--badge-bg:#0000;background-image:none;border-color:currentColor}.table-zebra tbody tr:where(:nth-child(2n)),.table-zebra tbody tr:where(:nth-child(2n)) :where(.table-pin-cols tr th){background-color:var(--color-base-200)}@media (hover:hover){:is(.table-zebra tbody tr.row-hover,.table-zebra tbody tr.row-hover:where(:nth-child(2n))):hover{background-color:var(--color-base-300)}}.select-lg{--size:calc(var(--size-field,.25rem) * 12);font-size:1.125rem}.select-lg option{padding-block:.375rem;padding-inline:1rem}.select-sm{--size:calc(var(--size-field,.25rem) * 8);font-size:.75rem}.select-sm option{padding-block:.25rem;padding-inline:.625rem}.select-xl{--size:calc(var(--size-field,.25rem) * 14);font-size:1.375rem}.select-xl option{padding-block:.375rem;padding-inline:1.25rem}.select-xs{--size:calc(var(--size-field,.25rem) * 6);font-size:.6875rem}.select-xs option{padding-block:.25rem;padding-inline:.5rem}.table-sm :not(thead,tfoot) tr{font-size:.75rem}.table-sm :where(th,td){padding-block:.5rem;padding-inline:.75rem}.badge-sm{--size:calc(var(--size-selector,.25rem) * 5);font-size:.75rem}.textarea-lg{font-size:max(var(--font-size,1.125rem), 1.125rem)}.textarea-sm{font-size:max(var(--font-size,.75rem), .75rem)}.textarea-xl{font-size:max(var(--font-size,1.375rem), 1.375rem)}.textarea-xs{font-size:max(var(--font-size,.6875rem), .6875rem)}.alert-error{color:var(--color-error-content);--alert-border-color:var(--color-error);--alert-color:var(--color-error)}.checkbox-primary{color:var(--color-primary-content);--input-color:var(--color-primary)}.link-primary{color:var(--color-primary)}@media (hover:hover){.link-primary:hover{color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.link-primary:hover{color:color-mix(in oklab, var(--color-primary) 80%, #000)}}}.btn-sm{--fontsize:.75rem;--btn-p:.75rem;--size:calc(var(--size-field,.25rem) * 8)}.btn-xs{--fontsize:.6875rem;--btn-p:.5rem;--size:calc(var(--size-field,.25rem) * 6)}.badge-error{--badge-color:var(--color-error);--badge-fg:var(--color-error-content)}.badge-info{--badge-color:var(--color-info);--badge-fg:var(--color-info-content)}.badge-primary{--badge-color:var(--color-primary);--badge-fg:var(--color-primary-content)}.badge-secondary{--badge-color:var(--color-secondary);--badge-fg:var(--color-secondary-content)}.badge-success{--badge-color:var(--color-success);--badge-fg:var(--color-success-content)}.badge-warning{--badge-color:var(--color-warning);--badge-fg:var(--color-warning-content)}.range-lg{--range-thumb-size:calc(var(--size-selector,.25rem) * 7)}.range-sm{--range-thumb-size:calc(var(--size-selector,.25rem) * 5)}.range-xl{--range-thumb-size:calc(var(--size-selector,.25rem) * 8)}.range-xs{--range-thumb-size:calc(var(--size-selector,.25rem) * 4)}.textarea-error,.textarea-error:focus,.textarea-error:focus-within{--input-color:var(--color-error)}}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse:not(td,tr,colgroup){visibility:revert-layer}.validator:user-invalid~.validator-hint{display:revert-layer}.validator:has(:user-invalid)~.validator-hint{display:revert-layer}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))~.validator-hint{display:revert-layer}.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-1{top:calc(var(--spacing) * 1)}.top-1\/2{top:50%}.left-1{left:calc(var(--spacing) * 1)}.left-1\/2{left:50%}.join{--join-ss:0;--join-se:0;--join-es:0;--join-ee:0;align-items:stretch;display:inline-flex}.join :where(.join-item){border-start-start-radius:var(--join-ss,0);border-start-end-radius:var(--join-se,0);border-end-end-radius:var(--join-ee,0);border-end-start-radius:var(--join-es,0)}.join :where(.join-item) *{--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.join>.join-item:where(:first-child),.join :first-child:not(:last-child) :where(.join-item){--join-ss:var(--radius-field);--join-se:0;--join-es:var(--radius-field);--join-ee:0}.join>.join-item:where(:last-child),.join :last-child:not(:first-child) :where(.join-item){--join-ss:0;--join-se:var(--radius-field);--join-es:0;--join-ee:var(--radius-field)}.join>.join-item:where(:only-child),.join :only-child :where(.join-item){--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.join>:where(:focus,:has(:focus)){z-index:1}@media (hover:hover){.join>:where(.btn:hover,:has(.btn:hover)){isolation:isolate}}.z-3{z-index:3}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-1{margin:calc(var(--spacing) * 1)}.m-2{margin:calc(var(--spacing) * 2)}.m-3{margin:calc(var(--spacing) * 3)}.mx-auto{margin-inline:auto}.my-2{margin-block:calc(var(--spacing) * 2)}.join-item:where(:not(:first-child,:disabled,[disabled],.btn-disabled)){margin-block-start:0;margin-inline-start:calc(var(--border,1px) * -1)}.join-item:where(:is(:disabled,[disabled],.btn-disabled)){border-width:var(--border,1px) 0 var(--border,1px) var(--border,1px)}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows), 0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:oklab(21% -.00316127 -.0338527/.1);--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.mt-0{margin-top:calc(var(--spacing) * 0)}.mt-0\!{margin-top:calc(var(--spacing) * 0)!important}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-5{margin-right:calc(var(--spacing) * 5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.kbd{box-shadow:none}.alert{border-width:var(--border);border-color:var(--alert-border-color,var(--color-base-200))}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:root .prose{--tw-prose-body:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-body:color-mix(in oklab, var(--color-base-content) 80%, #0000)}}:root .prose{--tw-prose-headings:var(--color-base-content);--tw-prose-lead:var(--color-base-content);--tw-prose-links:var(--color-base-content);--tw-prose-bold:var(--color-base-content);--tw-prose-counters:var(--color-base-content);--tw-prose-bullets:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-bullets:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-hr:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-hr:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-quotes:var(--color-base-content);--tw-prose-quote-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-quote-borders:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-captions:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-captions:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-code:var(--color-base-content);--tw-prose-pre-code:var(--color-neutral-content);--tw-prose-pre-bg:var(--color-neutral);--tw-prose-th-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-th-borders:color-mix(in oklab, var(--color-base-content) 50%, #0000)}}:root .prose{--tw-prose-td-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-td-borders:color-mix(in oklab, var(--color-base-content) 20%, #0000)}}:root .prose{--tw-prose-kbd:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-kbd:color-mix(in oklab, var(--color-base-content) 80%, #0000)}}:root .prose :where(code):not(pre>code){background-color:var(--color-base-200);border-radius:var(--radius-selector);border:var(--border) solid var(--color-base-300);font-weight:inherit;padding-block:.2em;padding-inline:.5em}:root .prose :where(code):not(pre>code):before,:root .prose :where(code):not(pre>code):after{display:none}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-1{height:calc(var(--spacing) * 1)}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-16{height:calc(var(--spacing) * 16)}.h-full{height:100%}.h-screen{height:100vh}.max-h-\[calc\(100vh-2rem\)\]{max-height:calc(100vh - 2rem)}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-25{min-height:calc(var(--spacing) * 25)}.min-h-30{min-height:calc(var(--spacing) * 30)}.min-h-37{min-height:calc(var(--spacing) * 37)}.min-h-37\.5{min-height:calc(var(--spacing) * 37.5)}.min-h-\[20px\]{min-height:20px}.min-h-\[100px\]{min-height:100px}.min-h-\[120px\]{min-height:120px}.min-h-\[150px\]{min-height:150px}.min-h-full{min-height:100%}.w-0{width:calc(var(--spacing) * 0)}.w-0\.5{width:calc(var(--spacing) * .5)}.w-1{width:calc(var(--spacing) * 1)}.w-4{width:calc(var(--spacing) * 4)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-7{max-width:calc(var(--spacing) * 7)}.max-w-60{max-width:calc(var(--spacing) * 60)}.max-w-lg{max-width:var(--container-lg)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-20{min-width:calc(var(--spacing) * 20)}.min-w-80{min-width:calc(var(--spacing) * 80)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-1{--tw-translate-x:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-col-resize{cursor:col-resize}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-base-200{border-color:var(--color-base-200)}.border-base-300{border-color:var(--color-base-300)}.bg-base-100{background-color:var(--color-base-100)}.bg-base-200{background-color:var(--color-base-200)}.bg-base-300{background-color:var(--color-base-300)}.bg-base-content,.bg-base-content\/30{background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.bg-base-content\/30{background-color:color-mix(in oklab, var(--color-base-content) 30%, transparent)}}.bg-gray-500{background-color:var(--color-gray-500)}.bg-gray-500\/50{background-color:#6a728280}@supports (color:color-mix(in lab, red, red)){.bg-gray-500\/50{background-color:color-mix(in oklab, var(--color-gray-500) 50%, transparent)}}.bg-primary,.bg-primary\/10{background-color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.p-0{padding:calc(var(--spacing) * 0)}.p-0\!{padding:calc(var(--spacing) * 0)!important}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-base-content,.text-base-content\/50{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/50{color:color-mix(in oklab, var(--color-base-content) 50%, transparent)}}.text-base-content\/60{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/60{color:color-mix(in oklab, var(--color-base-content) 60%, transparent)}}.text-base-content\/70{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/70{color:color-mix(in oklab, var(--color-base-content) 70%, transparent)}}.text-base-content\/80{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/80{color:color-mix(in oklab, var(--color-base-content) 80%, transparent)}}.text-error{color:var(--color-error)}.text-primary{color:var(--color-primary)}.text-primary-content{color:var(--color-primary-content)}.italic{font-style:italic}.prose :where(a.btn:not(.btn-link)):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-primary{--tw-ring-color:var(--color-primary)}@layer daisyui.l1{.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)){--btn-shadow:"";--btn-bg:#0000;--btn-border:#0000;--btn-noise:none}.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)):not(:disabled,[disabled],.btn-disabled){--btn-fg:var(--btn-color,currentColor);outline-color:currentColor}@media (hover:none){.btn-ghost:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color,currentColor);--btn-border:#0000;--btn-noise:none;outline-color:currentColor}}.btn-outline:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn),:disabled,[disabled],.btn-disabled){--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}@media (hover:none){.btn-outline:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color);--btn-border:var(--btn-color);--btn-noise:none}}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;user-select:none}.ring-inset{--tw-ring-inset:inset}@media (hover:hover){.hover\:bg-base-300:hover{background-color:var(--color-base-300)}.hover\:bg-primary:hover{background-color:var(--color-primary)}.hover\:text-primary:hover{color:var(--color-primary)}.hover\:opacity-80:hover{opacity:.8}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@layer daisyui.l1.l2{.hover\:badge-primary:hover{--badge-color:var(--color-primary);--badge-fg:var(--color-primary-content)}}}.active\:cursor-grabbing:active{cursor:grabbing}@media not all and (min-width:770px){.max-\[770px\]\:hidden{display:none}}@media (min-width:48rem){.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:64rem){.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:inline-flex{display:inline-flex}.lg\:w-5\/12{width:41.6667%}.lg\:w-7\/12{width:58.3333%}.lg\:w-auto{width:auto}.lg\:flex-1{flex:1}.lg\:flex-row{flex-direction:row}.lg\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.lg\:border-base-200{border-color:var(--color-base-200)}.lg\:p-6{padding:calc(var(--spacing) * 6)}}@media (min-width:80rem){.xl\:w-1\/3{width:33.3333%}.xl\:w-2\/3{width:66.6667%}}}[x-cloak]{display:none!important}@keyframes rating{0%,40%{filter:brightness(1.05)contrast(1.05);scale:1.1}}@keyframes dropdown{0%{opacity:0}}@keyframes radio{0%{padding:5px}50%{padding:3px}}@keyframes toast{0%{opacity:0;scale:.9}to{opacity:1;scale:1}}@keyframes rotator{89.9999%,to{--first-item-position:0 0%}90%,99.9999%{--first-item-position:0 calc(var(--items) * 100%)}to{translate:0 -100%}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}@keyframes menu{0%{opacity:0}}@keyframes progress{50%{background-position-x:-115%}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false} \ No newline at end of file diff --git a/pkg/web/assets/js/board-drag-drop.js b/pkg/web/assets/js/board-drag-drop.js index fba3bad..e642a32 100644 --- a/pkg/web/assets/js/board-drag-drop.js +++ b/pkg/web/assets/js/board-drag-drop.js @@ -6,7 +6,14 @@ if (e.target.closest("button") || e.target.closest("a")) return; const card = e.target.closest(".board-card"); if (!card) return; + + const column = card.closest(".board-column"); + const sourceStatus = column?.dataset?.status || ""; + const sourceSprint = column?.dataset?.sprint || ""; + e.dataTransfer.setData("text/plain", card.dataset.issueId); + e.dataTransfer.setData("source-status", sourceStatus); + e.dataTransfer.setData("source-sprint", sourceSprint); e.dataTransfer.effectAllowed = "move"; card.classList.add("opacity-50"); }); @@ -35,12 +42,45 @@ if (!zone) return; e.preventDefault(); zone.classList.remove("ring-2", "ring-primary", "ring-inset"); + const issueId = e.dataTransfer.getData("text/plain"); + const sourceStatus = e.dataTransfer.getData("source-status"); + const sourceSprint = e.dataTransfer.getData("source-sprint"); const newStatus = zone.dataset.status; + const targetSprint = zone.dataset.sprint; + if (!issueId || !newStatus) return; + let sourceSprintNum = 0; + if (sourceSprint && sourceSprint.startsWith("Sprint ")) { + sourceSprintNum = parseInt(sourceSprint.replace("Sprint ", ""), 10); + } + + let targetSprintNum = 0; + if (targetSprint && targetSprint.startsWith("Sprint ")) { + targetSprintNum = parseInt(targetSprint.replace("Sprint ", ""), 10); + } + const formData = new URLSearchParams(); - formData.append("status", newStatus); + + let issueStatus = newStatus; + if (newStatus === "todo" || newStatus === "backlog") { + issueStatus = "open"; + } else if (newStatus === "done") { + issueStatus = "closed"; + } + + formData.append("status", issueStatus); + + if (sourceStatus === "backlog" && newStatus !== "backlog" && targetSprintNum > 0) { + formData.append("add_to_sprint", targetSprintNum.toString()); + } + else if (sourceStatus !== "backlog" && newStatus === "backlog" && sourceSprintNum > 0) { + formData.append("remove_from_sprint", sourceSprintNum.toString()); + } + else if (sourceStatus !== "backlog" && newStatus !== "backlog" && targetSprintNum > 0) { + formData.append("add_to_sprint", targetSprintNum.toString()); + } fetch("/issues/" + issueId + "?from=board", { method: "PATCH", diff --git a/pkg/web/components/issue_detail.templ b/pkg/web/components/issue_detail.templ index 04f101a..502b848 100644 --- a/pkg/web/components/issue_detail.templ +++ b/pkg/web/components/issue_detail.templ @@ -57,52 +57,52 @@ templ IssueDetail(props IssueDetailProps) { templ StatusBadge(status models.Status) { switch status { case "open": - Open + Open case "in_progress": - In Progress + In Progress case "ready_to_sprint": - Ready to sprint + Ready to sprint case "closed": - Closed + Closed case "blocked": - Blocked + Blocked case "deferred": - Deferred + Deferred default: - { string(status) } + { string(status) } } } templ TypeBadge(issueType models.IssueType) { switch issueType { case "bug": - Bug + Bug case "feature": - Feature + Feature case "task": - Task + Task case "chore": - Chore + Chore case "epic": - Epic + Epic default: - { string(issueType) } + { string(issueType) } } } templ PriorityBadge(priority int) { switch priority { case 0: - Irrelevant + Irrelevant case 1: - Low + Low case 2: - Normal + Normal case 3: - High + High case 4: - Critical + Critical default: - { fmt.Sprintf("P%d", priority) } + { fmt.Sprintf("P%d", priority) } } } diff --git a/pkg/web/components/issue_detail_templ.go b/pkg/web/components/issue_detail_templ.go index c405ee5..ccee021 100644 --- a/pkg/web/components/issue_detail_templ.go +++ b/pkg/web/components/issue_detail_templ.go @@ -202,44 +202,44 @@ func StatusBadge(status models.Status) templ.Component { ctx = templ.ClearChildren(ctx) switch status { case "open": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Open") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Open") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "in_progress": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "In Progress") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "In Progress") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "ready_to_sprint": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Ready to sprint") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Ready to sprint") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "closed": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Closed") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Closed") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "blocked": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Blocked") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Blocked") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "deferred": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "Deferred") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "Deferred") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(string(status)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 72, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 72, Col: 78} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -277,39 +277,39 @@ func TypeBadge(issueType models.IssueType) templ.Component { ctx = templ.ClearChildren(ctx) switch issueType { case "bug": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Bug") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Bug") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "feature": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Feature") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Feature") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "task": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Task") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Task") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "chore": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Chore") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Chore") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "epic": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Epic") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Epic") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(string(issueType)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 89, Col: 65} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 89, Col: 83} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -347,39 +347,39 @@ func PriorityBadge(priority int) templ.Component { ctx = templ.ClearChildren(ctx) switch priority { case 0: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Irrelevant") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Irrelevant") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 1: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "Low") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "Low") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 2: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "Normal") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "Normal") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 3: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "High") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "High") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 4: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Critical") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Critical") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", priority)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 106, Col: 74} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 106, Col: 92} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { diff --git a/pkg/web/handler/dashboard.go b/pkg/web/handler/dashboard.go index 6184afd..2059b11 100644 --- a/pkg/web/handler/dashboard.go +++ b/pkg/web/handler/dashboard.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "net/http" "sort" "strings" @@ -21,24 +22,72 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { return } - // Sort issues by highest priority first (4 -> 0) sort.Slice(issues, func(i, j int) bool { return issues[i].Priority > issues[j].Priority }) - // Check if board view is requested - isBoardView := r.URL.Query().Get("board") == "true" + isBoardView := r.URL.Query().Get("board") == "true" || strings.HasPrefix(r.URL.Path, "/board/") if isBoardView { + ctx := r.Context() + + backlogIssues, err := app.Issues.GetIssuesNotInAnySprint(ctx) + if err != nil { + backlogIssues = []*models.Issue{} + } + + sort.Slice(backlogIssues, func(i, j int) bool { + return backlogIssues[i].Priority > backlogIssues[j].Priority + }) + + sprints, err := app.Issues.GetSprints(ctx) + if err != nil { + sprints = []int{} + } + + currentSprint := 0 + sprintParam := r.URL.Query().Get("sprint") + if sprintParam != "" { + fmt.Sscanf(sprintParam, "%d", ¤tSprint) + } + + if currentSprint == 0 && len(sprints) > 0 { + currentSprint = sprints[0] + } + + var sprintIssues []*models.Issue + if currentSprint > 0 { + sprintIssues, err = app.Issues.GetIssuesBySprint(ctx, currentSprint) + if err != nil { + sprintIssues = []*models.Issue{} + } + } + + sort.Slice(sprintIssues, func(i, j int) bool { + return sprintIssues[i].Priority > sprintIssues[j].Priority + }) + boardProps := routes.BoardViewProps{ - BaseURL: "/?board=true", - QueryParam: query, - Issues: issues, - EmptyText: "No issues found", + BaseURL: "/?board=true", + QueryParam: query, + Issues: issues, + BacklogIssues: backlogIssues, + SprintIssues: sprintIssues, + CurrentSprint: currentSprint, + Sprints: sprints, + EmptyText: "No issues found", } if hx.IsHxRequest() { - routes.BoardViewContent(boardProps).Render(r.Context(), w) + if r.URL.Query().Get("board") != "true" { + w.Header().Set("HX-Push-Url", fmt.Sprintf("/?board=true&sprint=%d", currentSprint)) + } + + if strings.HasPrefix(r.URL.Path, "/board/") { + routes.BoardColumns(boardProps).Render(r.Context(), w) + } else { + routes.BoardViewContent(boardProps).Render(r.Context(), w) + } return } @@ -46,7 +95,6 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { return } - // Regular dashboard view var selectedIssue *models.Issue if len(issues) > 0 { selectedIssue = issues[0] @@ -62,6 +110,13 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { } } + if selectedIssue != nil { + comments, err := app.Issues.GetIssueComments(r.Context(), selectedIssue.ID) + if err == nil && comments != nil { + selectedIssue.Comments = comments + } + } + isPartial := r.URL.Query().Get("partial") == "true" if hx.IsHxRequest() && isPartial { routes.DashboardIssueList(issues, selectedIssueID).Render(r.Context(), w) @@ -83,3 +138,18 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { routes.Dashboard(props).Render(r.Context(), w) } + +func CreateSprintHandler(w http.ResponseWriter, r *http.Request) { + app := App(r) + ctx := r.Context() + + sprintNum, err := app.Issues.AddSprint(ctx) + if err != nil { + http.Error(w, "Failed to create sprint", http.StatusInternalServerError) + return + } + + redirectURL := fmt.Sprintf("/?board=true&sprint=%d", sprintNum) + w.Header().Set("HX-Redirect", redirectURL) + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index 83dff1f..f90760e 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -2,8 +2,10 @@ package handler import ( "context" + "fmt" "html" "net/http" + "net/url" "strings" "github.com/LazyBachelor/LazyPM/internal/models" @@ -17,7 +19,7 @@ const commentsKey = "comments" type IssueForm struct { Title string `form:"title" validate:"required,max=255"` Description string `form:"description" validate:"max=2000"` - Status models.Status `form:"status" validate:"required,oneof=open in_progress blocked ready_to_sprint closed"` + Status models.Status `form:"status" validate:"required,oneof=open in_progress blocked closed"` IssueType models.IssueType `form:"issue_type" validate:"required,oneof=task bug feature chore"` Priority int `form:"priority" validate:"gte=0,lte=4"` } @@ -25,10 +27,11 @@ type IssueForm struct { type UpdateIssueForm struct { Title *string `form:"title" validate:"omitempty,max=255"` Description *string `form:"description" validate:"omitempty,max=2000"` - Status *models.Status `form:"status" validate:"omitempty,oneof=open in_progress blocked ready_to_sprint closed"` + Status *models.Status `form:"status" validate:"omitempty,oneof=open in_progress blocked closed"` CloseReason *string `form:"close_reason" validate:"omitempty,max=2000"` IssueType *models.IssueType `form:"issue_type" validate:"omitempty,oneof=task bug feature chore"` Priority *int `form:"priority" validate:"omitempty,gte=0,lte=4"` + Assignee *string `form:"assignee" validate:"omitempty,max=100"` } func CreateIssue(w http.ResponseWriter, r *http.Request) { @@ -122,20 +125,8 @@ func GetIssue(w http.ResponseWriter, r *http.Request) { comments := r.Context().Value(commentsKey).([]*models.Comment) hx := HTMX(r) - from := r.URL.Query().Get("from") - detailProps := routes.IssueDetailProps{ - Issue: issue, - Comments: comments, - From: from, - } - - if !hx.IsHxRequest() && strings.Contains(r.Header.Get("Accept"), "text/html") { - routes.IssueDetail(detailProps).Render(r.Context(), w) - return - } - - if hx.IsHxRequest() { - routes.IssueDetailContent(detailProps).Render(r.Context(), w) + if strings.Contains(r.Header.Get("Accept"), "text/html") && !hx.IsHxRequest() { + routes.IssueDetailPage(issue, comments).Render(r.Context(), w) return } @@ -170,6 +161,24 @@ func UpdateIssue(w http.ResponseWriter, r *http.Request) { return } + ctx := r.Context() + addToSprint := r.FormValue("add_to_sprint") + removeFromSprint := r.FormValue("remove_from_sprint") + + if addToSprint != "" { + sprintNum := 0 + fmt.Sscanf(addToSprint, "%d", &sprintNum) + if sprintNum > 0 { + app.Issues.AddIssueToSprint(ctx, issue.ID, sprintNum) + } + } else if removeFromSprint != "" { + sprintNum := 0 + fmt.Sscanf(removeFromSprint, "%d", &sprintNum) + if sprintNum > 0 { + app.Issues.RemoveIssueFromSprint(ctx, issue.ID, sprintNum) + } + } + issue, err = app.Issues.GetIssue(r.Context(), issue.ID) if err != nil { http.Error(w, "Failed to retrieve updated issue", http.StatusInternalServerError) @@ -177,13 +186,20 @@ func UpdateIssue(w http.ResponseWriter, r *http.Request) { } if hx.IsHxRequest() { - // Check if 'from' parameter says board view from := r.URL.Query().Get("from") referer := r.Header.Get("Referer") boardView := from == "board" || strings.Contains(referer, "board=true") if boardView { - w.Header().Set("HX-Redirect", "/?board=true") + redirectURL := "/?board=true" + if strings.Contains(referer, "sprint=") { + if parsedURL, err := url.Parse(referer); err == nil { + if sprintParam := parsedURL.Query().Get("sprint"); sprintParam != "" { + redirectURL = redirectURL + "&sprint=" + sprintParam + } + } + } + w.Header().Set("HX-Redirect", redirectURL) } else { w.Header().Set("HX-Redirect", "/?selected-issue="+issue.ID) } @@ -329,5 +345,8 @@ func (f *UpdateIssueForm) toChanges() map[string]any { if f.Priority != nil { changes["priority"] = *f.Priority } + if f.Assignee != nil { + changes["assignee"] = *f.Assignee + } return changes } diff --git a/pkg/web/routes/boardview.templ b/pkg/web/routes/boardview.templ index e2924f9..2beb51c 100644 --- a/pkg/web/routes/boardview.templ +++ b/pkg/web/routes/boardview.templ @@ -7,10 +7,14 @@ import ( ) type BoardViewProps struct { - Issues []*models.Issue - BaseURL string - QueryParam string - EmptyText string + Issues []*models.Issue + BacklogIssues []*models.Issue + SprintIssues []*models.Issue + CurrentSprint int + Sprints []int + BaseURL string + QueryParam string + EmptyText string } templ BoardView(props BoardViewProps) { @@ -22,13 +26,6 @@ templ BoardView(props BoardViewProps) { templ BoardViewContent(props BoardViewProps) {
-
- @components.SearchForm(components.SearchFormProps{ - RootURL: props.BaseURL, - SearchQuery: props.QueryParam, - Target: "#board-container", - }) -
@@ -50,53 +47,86 @@ templ BoardViewContent(props BoardViewProps) { New Issue
+
+ Sprint: + + +
-
- @BoardColumns(props.Issues) +
+ @BoardColumns(props)
} -templ BoardColumns(issues []*models.Issue) { +templ BoardColumns(props BoardViewProps) { {{ - openIssues := []*models.Issue{} + backlogIssues := props.BacklogIssues + + todoIssues := []*models.Issue{} inProgressIssues := []*models.Issue{} blockedIssues := []*models.Issue{} - readyToSprintIssues := []*models.Issue{} - closedIssues := []*models.Issue{} + doneIssues := []*models.Issue{} - for _, issue := range issues { + for _, issue := range props.SprintIssues { switch issue.Status { case "open": - openIssues = append(openIssues, issue) + todoIssues = append(todoIssues, issue) case "in_progress": inProgressIssues = append(inProgressIssues, issue) case "blocked": blockedIssues = append(blockedIssues, issue) - case "ready_to_sprint": - readyToSprintIssues = append(readyToSprintIssues, issue) case "closed": - closedIssues = append(closedIssues, issue) + doneIssues = append(doneIssues, issue) } } + + sprintName := "No Sprint" + if props.CurrentSprint > 0 { + sprintName = fmt.Sprintf("Sprint %d", props.CurrentSprint) + } }}
- @BoardColumn("Open", "open", openIssues, "badge-info") - @BoardColumn("In Progress", "in_progress", inProgressIssues, "badge-warning") - @BoardColumn("Blocked", "blocked", blockedIssues, "badge-error") - @BoardColumn("Ready to sprint", "ready_to_sprint", readyToSprintIssues, "badge-primary") - @BoardColumn("Closed", "closed", closedIssues, "badge-success") + @BoardColumn("Backlog", "backlog", backlogIssues, "badge-ghost", "") + @BoardColumn(fmt.Sprintf("%s - To Do", sprintName), "todo", todoIssues, "badge-info", sprintName) + @BoardColumn(fmt.Sprintf("%s - In Progress", sprintName), "in_progress", inProgressIssues, "badge-warning", sprintName) + @BoardColumn(fmt.Sprintf("%s - Blocked", sprintName), "blocked", blockedIssues, "badge-error", sprintName) + @BoardColumn(fmt.Sprintf("%s - Done", sprintName), "done", doneIssues, "badge-success", sprintName)
} -templ BoardColumn(title string, status string, issues []*models.Issue, badgeClass string) { -
+templ BoardColumn(title string, status string, issues []*models.Issue, badgeClass string, sprintName string) { +

{ title }

{ fmt.Sprintf("%d", len(issues)) }
-
+
if len(issues) == 0 {
Drop issues here
} @@ -116,19 +146,16 @@ templ BoardCard(issue *models.Issue) {

{ issue.Title }

-
- +
+ + + + +
Sprint:
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + templ_7745c5c3_Err = BoardColumns(props).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = BoardColumns(props.Issues).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -117,7 +184,7 @@ func BoardViewContent(props BoardViewProps) templ.Component { }) } -func BoardColumns(issues []*models.Issue) templ.Component { +func BoardColumns(props BoardViewProps) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -133,56 +200,60 @@ func BoardColumns(issues []*models.Issue) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var4 := templ.GetChildren(ctx) - if templ_7745c5c3_Var4 == nil { - templ_7745c5c3_Var4 = templ.NopComponent + templ_7745c5c3_Var8 := templ.GetChildren(ctx) + if templ_7745c5c3_Var8 == nil { + templ_7745c5c3_Var8 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - openIssues := []*models.Issue{} + backlogIssues := props.BacklogIssues + + todoIssues := []*models.Issue{} inProgressIssues := []*models.Issue{} blockedIssues := []*models.Issue{} - readyToSprintIssues := []*models.Issue{} - closedIssues := []*models.Issue{} + doneIssues := []*models.Issue{} - for _, issue := range issues { + for _, issue := range props.SprintIssues { switch issue.Status { case "open": - openIssues = append(openIssues, issue) + todoIssues = append(todoIssues, issue) case "in_progress": inProgressIssues = append(inProgressIssues, issue) case "blocked": blockedIssues = append(blockedIssues, issue) - case "ready_to_sprint": - readyToSprintIssues = append(readyToSprintIssues, issue) case "closed": - closedIssues = append(closedIssues, issue) + doneIssues = append(doneIssues, issue) } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + + sprintName := "No Sprint" + if props.CurrentSprint > 0 { + sprintName = fmt.Sprintf("Sprint %d", props.CurrentSprint) + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = BoardColumn("Open", "open", openIssues, "badge-info").Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = BoardColumn("Backlog", "backlog", backlogIssues, "badge-ghost", "").Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = BoardColumn("In Progress", "in_progress", inProgressIssues, "badge-warning").Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = BoardColumn(fmt.Sprintf("%s - To Do", sprintName), "todo", todoIssues, "badge-info", sprintName).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = BoardColumn("Blocked", "blocked", blockedIssues, "badge-error").Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = BoardColumn(fmt.Sprintf("%s - In Progress", sprintName), "in_progress", inProgressIssues, "badge-warning", sprintName).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = BoardColumn("Ready to sprint", "ready_to_sprint", readyToSprintIssues, "badge-primary").Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = BoardColumn(fmt.Sprintf("%s - Blocked", sprintName), "blocked", blockedIssues, "badge-error", sprintName).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = BoardColumn("Closed", "closed", closedIssues, "badge-success").Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = BoardColumn(fmt.Sprintf("%s - Done", sprintName), "done", doneIssues, "badge-success", sprintName).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -190,7 +261,7 @@ func BoardColumns(issues []*models.Issue) templ.Component { }) } -func BoardColumn(title string, status string, issues []*models.Issue, badgeClass string) templ.Component { +func BoardColumn(title string, status string, issues []*models.Issue, badgeClass string, sprintName string) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -206,91 +277,117 @@ func BoardColumn(title string, status string, issues []*models.Issue, badgeClass }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var5 := templ.GetChildren(ctx) - if templ_7745c5c3_Var5 == nil { - templ_7745c5c3_Var5 = templ.NopComponent + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 96, Col: 44} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var8 = []any{"badge", badgeClass} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 126, Col: 44} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 = []any{"badge", badgeClass} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(issues))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 127, Col: 71} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(issues) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
Drop issues here
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Drop issues here
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -301,7 +398,7 @@ func BoardColumn(title string, status string, issues []*models.Issue, badgeClass return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -325,77 +422,77 @@ func BoardCard(issue *models.Issue) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var12 := templ.GetChildren(ctx) - if templ_7745c5c3_Var12 == nil { - templ_7745c5c3_Var12 = templ.NopComponent + templ_7745c5c3_Var18 := templ.GetChildren(ctx) + if templ_7745c5c3_Var18 == nil { + templ_7745c5c3_Var18 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 118, Col: 80} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 148, Col: 80} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" title=\"Edit issue\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 146, Col: 65} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 173, Col: 65} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -407,30 +504,30 @@ func BoardCard(issue *models.Issue) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if issue.Description != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Description) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 151, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 178, Col: 81} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/routes/dashboard.templ b/pkg/web/routes/dashboard.templ index db55c44..42328ef 100644 --- a/pkg/web/routes/dashboard.templ +++ b/pkg/web/routes/dashboard.templ @@ -1,6 +1,7 @@ package routes import ( + "fmt" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/components" "github.com/LazyBachelor/LazyPM/pkg/web/components/base" @@ -21,17 +22,14 @@ templ Dashboard(props DashboardProps) { } templ DashboardContent(props DashboardProps) { -
+ {{ + issuesJSON, _ := templ.JSONString(props.Issues) + xData := fmt.Sprintf(`{ selectedId: '%s', issues: %s }`, props.SelectedIssue.ID, issuesJSON) + }} +
-
- @components.SearchForm(components.SearchFormProps{ - RootURL: props.BaseURL, - SearchQuery: props.QueryParam, - Target: "#issue-list-container", - }) -
-
+
-
-
- if props.SelectedIssue != nil { - - - } +
+
-
-
+
+
@DashboardIssueList(props.Issues, props.SelectedIssue.ID)
-
- if props.SelectedIssue != nil { -
- @components.IssueDetail(components.IssueDetailProps{ - Issue: props.SelectedIssue, - }) +
+
+
+
+
+
+
@@ -116,23 +363,14 @@ templ DashboardIssueRows(issues []*models.Issue, selectedID string) { No issues } for _, issue := range issues { - {{ - var active string - if selectedID == issue.ID { - active = "bg-primary/10" - } - }} { issue.ID } - { issue.Title } + i.id === '%s')?.title || %q`, issue.ID, issue.Title) }> @components.StatusBadge(issue.Status) diff --git a/pkg/web/routes/dashboard_templ.go b/pkg/web/routes/dashboard_templ.go index 48edd39..6586d67 100644 --- a/pkg/web/routes/dashboard_templ.go +++ b/pkg/web/routes/dashboard_templ.go @@ -9,6 +9,7 @@ import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import ( + "fmt" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/components" "github.com/LazyBachelor/LazyPM/pkg/web/components/base" @@ -90,55 +91,22 @@ func DashboardContent(props DashboardProps) templ.Component { templ_7745c5c3_Var3 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + issuesJSON, _ := templ.JSONString(props.Issues) + xData := fmt.Sprintf(`{ selectedId: '%s', issues: %s }`, props.SelectedIssue.ID, issuesJSON) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if props.SelectedIssue != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -146,27 +114,7 @@ func DashboardContent(props DashboardProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if props.SelectedIssue != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = components.IssueDetail(components.IssueDetailProps{ - Issue: props.SelectedIssue, - }).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -190,12 +138,12 @@ func DashboardIssueList(issues []*models.Issue, selectedID string) templ.Compone }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var6 := templ.GetChildren(ctx) - if templ_7745c5c3_Var6 == nil { - templ_7745c5c3_Var6 = templ.NopComponent + templ_7745c5c3_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -215,7 +163,7 @@ func DashboardIssueList(issues []*models.Issue, selectedID string) templ.Compone if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -239,93 +187,84 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var7 := templ.GetChildren(ctx) - if templ_7745c5c3_Var7 == nil { - templ_7745c5c3_Var7 = templ.NopComponent + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if len(issues) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "No issues") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "No issues") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } for _, issue := range issues { - var active string - if selectedID == issue.ID { - active = "bg-primary/10" - } - var templ_7745c5c3_Var8 = []any{"hover cursor-pointer min-h-full w-full select-none", active} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(`{"selected-issue": "` + issue.ID + `"}`) + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 130, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 372, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" hx-swap=\"innerHTML\" data-issue-id=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " i.id === '%s')?.title || %q`, issue.ID, issue.Title)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 132, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 373, Col: 124} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 134, Col: 43} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 135, Col: 46} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -333,7 +272,7 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -341,7 +280,7 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -349,7 +288,7 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/routes/issue_detail.templ b/pkg/web/routes/issue_detail.templ index 5706792..9a1808a 100644 --- a/pkg/web/routes/issue_detail.templ +++ b/pkg/web/routes/issue_detail.templ @@ -1,77 +1,272 @@ package routes import ( + "fmt" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/components" ) -type IssueDetailProps struct { - Issue *models.Issue - Comments []*models.Comment - From string // "board" when navigating from board view -} - -type IssueDetailModalProps struct { - Issue *models.Issue - Comments []*models.Comment - From string -} - -templ IssueDetailContent(props IssueDetailProps) { +templ IssueDetailPage(issue *models.Issue, comments []*models.Comment) { {{ - backURL := "/" - editURL := "/issues/" + props.Issue.ID + "/edit" - if props.From == "board" { - backURL = "/?board=true" - editURL += "?from=board" - } + issueJSON, _ := templ.JSONString(issue) + xData := fmt.Sprintf(`{ + issue: %s, + editing: null, + newComment: '', + get statusClass() { + const classes = { + 'open': 'badge-info', + 'in_progress': 'badge-warning', + 'blocked': 'badge-error', + 'closed': 'badge-success' + } + return classes[this.issue.status] || 'badge-ghost' + }, + get priorityLabel() { + const labels = ['Irrelevant', 'Low', 'Normal', 'High', 'Critical'] + return labels[this.issue.priority] || 'P' + this.issue.priority + }, + async saveField(field, value) { + const formData = new URLSearchParams() + formData.append(field, value || '') + const response = await fetch('/issues/' + this.issue.id, { + method: 'PATCH', + body: formData, + headers: { 'HX-Request': 'true' } + }) + this.editing = null + // Check if server wants to redirect back to list + const redirectUrl = response.headers.get('HX-Redirect') + if (redirectUrl) { + window.location.href = redirectUrl + } + }, + async addComment() { + if (!this.newComment.trim()) return + const author = this.issue.created_by || 'Anonymous' + const formData = new URLSearchParams() + formData.append('text', this.newComment) + formData.append('author', author) + await fetch('/issues/' + this.issue.id + '/comments', { + method: 'POST', + body: formData, + headers: { 'HX-Request': 'true' } + }) + this.newComment = '' + window.location.reload() + } + }`, issueJSON) }} -
- -
- @components.IssueDetail(components.IssueDetailProps{Issue: props.Issue}) -
- @components.CommentSection(components.CommentSectionProps{ - IssueID: props.Issue.ID, - Comments: props.Comments, - }) -
-} - -templ IssueDetail(props IssueDetailProps) { @BaseLayout() { - @IssueDetailContent(props) +
+ +
+
+
+ + +
+
+
+ + + + + + +
+
+

Description

+ + +
+
+
+ Created +

+

+
+
+ Updated +

+
+
+ + +
+
+
+
+
+

Comments

+
+
+ +
+ +
+
+
+
+ for _, comment := range comments { +
+
+
+ { comment.Author } + { comment.CreatedAt.Format("Jan 2, 2006 15:04") } +
+

{ comment.Text }

+
+
+ } +
+
+
} } - -templ IssueDetailModalContent(props IssueDetailModalProps) { -
-
- @components.IssueDetail(components.IssueDetailProps{Issue: props.Issue}) -
- @components.CommentSection(components.CommentSectionProps{ - IssueID: props.Issue.ID, - Comments: props.Comments, - }) -
-} diff --git a/pkg/web/routes/issue_detail_templ.go b/pkg/web/routes/issue_detail_templ.go index 9328d69..df92981 100644 --- a/pkg/web/routes/issue_detail_templ.go +++ b/pkg/web/routes/issue_detail_templ.go @@ -9,23 +9,12 @@ import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import ( + "fmt" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/components" ) -type IssueDetailProps struct { - Issue *models.Issue - Comments []*models.Comment - From string // "board" when navigating from board view -} - -type IssueDetailModalProps struct { - Issue *models.Issue - Comments []*models.Comment - From string -} - -func IssueDetailContent(props IssueDetailProps) templ.Component { +func IssueDetailPage(issue *models.Issue, comments []*models.Comment) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -46,95 +35,55 @@ func IssueDetailContent(props IssueDetailProps) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - backURL := "/" - editURL := "/issues/" + props.Issue.ID + "/edit" - if props.From == "board" { - backURL = "/?board=true" - editURL += "?from=board" - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = components.IssueDetail(components.IssueDetailProps{Issue: props.Issue}).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = components.CommentSection(components.CommentSectionProps{ - IssueID: props.Issue.ID, - Comments: props.Comments, - }).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func IssueDetail(props IssueDetailProps) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr + issueJSON, _ := templ.JSONString(issue) + xData := fmt.Sprintf(`{ + issue: %s, + editing: null, + newComment: '', + get statusClass() { + const classes = { + 'open': 'badge-info', + 'in_progress': 'badge-warning', + 'blocked': 'badge-error', + 'closed': 'badge-success' } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var4 := templ.GetChildren(ctx) - if templ_7745c5c3_Var4 == nil { - templ_7745c5c3_Var4 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + return classes[this.issue.status] || 'badge-ghost' + }, + get priorityLabel() { + const labels = ['Irrelevant', 'Low', 'Normal', 'High', 'Critical'] + return labels[this.issue.priority] || 'P' + this.issue.priority + }, + async saveField(field, value) { + const formData = new URLSearchParams() + formData.append(field, value || '') + const response = await fetch('/issues/' + this.issue.id, { + method: 'PATCH', + body: formData, + headers: { 'HX-Request': 'true' } + }) + this.editing = null + // Check if server wants to redirect back to list + const redirectUrl = response.headers.get('HX-Redirect') + if (redirectUrl) { + window.location.href = redirectUrl + } + }, + async addComment() { + if (!this.newComment.trim()) return + const author = this.issue.created_by || 'Anonymous' + const formData = new URLSearchParams() + formData.append('text', this.newComment) + formData.append('author', author) + await fetch('/issues/' + this.issue.id + '/comments', { + method: 'POST', + body: formData, + headers: { 'HX-Request': 'true' } + }) + this.newComment = '' + window.location.reload() + } + }`, issueJSON) + templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -146,61 +95,83 @@ func IssueDetail(props IssueDetailProps) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = IssueDetailContent(props).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Description

Created

Updated

Comments

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, comment := range comments { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(comment.Author) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 261, Col: 61} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(comment.CreatedAt.Format("Jan 2, 2006 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 262, Col: 89} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(comment.Text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 264, Col: 61} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func IssueDetailModalContent(props IssueDetailModalProps) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var6 := templ.GetChildren(ctx) - if templ_7745c5c3_Var6 == nil { - templ_7745c5c3_Var6 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = components.IssueDetail(components.IssueDetailProps{Issue: props.Issue}).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = components.CommentSection(components.CommentSectionProps{ - IssueID: props.Issue.ID, - Comments: props.Comments, - }).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + templ_7745c5c3_Err = BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/server/routes.go b/pkg/web/server/routes.go index 2c4b916..8063123 100644 --- a/pkg/web/server/routes.go +++ b/pkg/web/server/routes.go @@ -34,6 +34,8 @@ func (s *Server) RegisterRoutes(assets embed.FS) http.Handler { s.handleAssets(r, assets) r.Get("/", handler.DashboardHandler) + r.Get("/board/sprint", handler.DashboardHandler) + r.Post("/board/sprint/new", handler.CreateSprintHandler) r.Get("/status", handler.HandleTaskStatus) r.Get("/status/modal", handler.HandleTaskStatusModal) From 6763aeb6ea8fb643c5976bf90139facdb3698fdf Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 23 Mar 2026 19:41:56 +0100 Subject: [PATCH 72/73] use equal fold to make the test case-insensitive --- internal/utils/check/expect.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/utils/check/expect.go b/internal/utils/check/expect.go index fdb410f..0f404d2 100644 --- a/internal/utils/check/expect.go +++ b/internal/utils/check/expect.go @@ -76,7 +76,7 @@ func (e *Expector) Fail(message string) *Expector { func (e *Expector) NotEmptyAndEqual(val, expected string, message string) *Expector { if val == "" { return e.Fail(fmt.Sprintf("%s is empty", message)) - } else if val != expected { + } else if !strings.EqualFold(val, expected) { return e.Fail(fmt.Sprintf(`%s expected "%v", got "%v"`, message, expected, val)) } return e.Pass(message + " is correct") @@ -115,7 +115,6 @@ func (e *Expector) Contains(s, substr, message string) *Expector { return e.Pass(fmt.Sprintf(`%s contains "%v"`, message, substr)) } - func (e *Expector) NotContains(s, substr, message string) *Expector { check := NewCheck(message, !strings.Contains(s, substr)) e.Checks = append(e.Checks, check) From 1eaedecff85ccc17ff3379d407a4e6fa6217a49f Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 23 Mar 2026 20:03:01 +0100 Subject: [PATCH 73/73] simplify coding task --- cmd/pm/tasks/codingTask.go | 60 +++++++++----------------------------- internal/storage/beads.go | 12 ++++---- 2 files changed, 18 insertions(+), 54 deletions(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index cac668a..a1fc9f2 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -10,58 +10,26 @@ import ( "github.com/LazyBachelor/LazyPM/internal/utils/check" ) -const codingDescription = `You are tasked with doing a upgrading a dependency in the codebase. +const codingDescription = `You are tasked with fixing a logical error in the code. Your task: -1. Assign the given issue to yourself as 'Me'. -2. A text file will appear in the this directory: +1. A text file will appear in the this directory: - Open it and follow the instructions inside. - Save the file after you are done. -3. When you are done, mark this task as "Closed".` +2. When you are done, mark this task as "Closed".` var textFileDescription = ` # Instructions for the coding task -Please upgrade the MongoDB Driver dependency in the go.mod file to the latest version. -It should be v1.17.9. After you are done, save the file and mark the task as completed. +There is a major logical error in this code, you need to fix it. +Change the function logic so that it correctly adds two numbers together instead of subtracting them. ############################################################` var code = ` -require ( - charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 - github.com/go-git/go-git/v6 v6.0.0-20260222090600-424e9964d3a3 - github.com/muesli/reflow v0.3.0 - github.com/steveyegge/beads v0.49.6 - go.mongodb.org/mongo-driver v1.17.8 - go.mongodb.org/mongo-driver/v2 v2.5.0 -) - -require ( - github.com/c-bata/go-prompt v0.2.6 - github.com/charmbracelet/bubbles v0.21.1 - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/fang v0.4.4 - github.com/charmbracelet/huh v0.8.0 - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 - github.com/spf13/cobra v1.10.2 - golang.org/x/term v0.40.0 -) - -require ( - github.com/NYTimes/gziphandler v1.1.1 - github.com/a-h/templ v0.3.977 - github.com/donseba/go-htmx v1.12.1 - github.com/go-chi/chi/v5 v5.2.5 - github.com/go-playground/form/v4 v4.3.0 - github.com/go-playground/validator/v10 v10.30.1 - github.com/rs/cors v1.11.1 -) - -tool ( - github.com/a-h/templ/cmd/templ - github.com/haatos/goshipit/cmd/gsi -) +function Add(a, b int) int { + return a - b +} ` var textFileContent = codingDescription + textFileDescription + "\n" + code @@ -120,8 +88,10 @@ func (t *CodingTask) Setup(ctx context.Context) error { } t.issue = NewIssueBuilder(). - WithTitle("Upgrade MongoDB Driver"). - WithPriority(4).WithDescription(codingDescription). + WithTitle("Fix major error"). + WithDescription(codingDescription). + WithStatus(models.StatusInProgress). + WithPriority(4). Build() return t.app.Issues.CreateIssue(ctx, t.issue, "") @@ -138,10 +108,6 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { return expect.Fatal("Issue was deleted or could not be found") } - if !expect.Equal(issue.Assignee, "Me", "Issue Assignee").Valid() { - return expect.ValidationFeedback - } - if _, err := os.Stat("./code.txt"); os.IsNotExist(err) { expect.Fail("The code.txt file should exist on the desktop.") return expect.ValidationFeedback @@ -159,7 +125,7 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { return expect.ValidationFeedback } - expect.Contains(code, "go.mongodb.org/mongo-driver v1.17.9", "MongoDB Driver version") + expect.Contains(code, "a + b", "Function logic") if !expect.Valid() { return expect.ValidationFeedback } diff --git a/internal/storage/beads.go b/internal/storage/beads.go index 96d39ed..922c184 100644 --- a/internal/storage/beads.go +++ b/internal/storage/beads.go @@ -77,7 +77,7 @@ func (s *BeadsService) CreateIssues(ctx context.Context, issues []*models.Issue, backlogNum, err := s.GetBacklogSprint(ctx) if err != nil { - return nil + return err } for _, issue := range issues { @@ -105,14 +105,12 @@ func (s *BeadsService) AllIssues(ctx context.Context) ([]models.Issue, error) { } func (s *BeadsService) DeleteIssues() error { - - var deleteIssues = `DELETE FROM issues; - DELETE FROM sprints;` - - if _, err := s.UnderlyingDB().Exec(deleteIssues); err != nil { + _, err := s.UnderlyingDB().Exec("DELETE FROM issues; DELETE FROM sprints;") + if err != nil { return err } - return nil + _, err = s.UnderlyingDB().Exec("INSERT INTO sprints (name, issues, sprint_num, is_backlog) VALUES ('backlog', '[]', 0, 1)") + return err } func (s *BeadsService) AddSprint(ctx context.Context) (int, error) {