From 90dd69a019c6186146a3fbabc258dbb55c7c4e45 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Sun, 8 Feb 2026 15:03:45 +0100
Subject: [PATCH 01/46] add introduction to the survey add lib to read toml
files embed fs for reading toml files add form for introduction add form for
choosing interface
---
cmd/survey.go | 44 ----------------------
cmd/survey/assets/intro.toml | 10 +++++
cmd/survey/forms/intro.go | 56 ++++++++++++++++++++++++++++
cmd/survey/survey.go | 72 ++++++++++++++++++++++++++++++++++++
go.mod | 1 +
go.sum | 2 +
6 files changed, 141 insertions(+), 44 deletions(-)
delete mode 100644 cmd/survey.go
create mode 100644 cmd/survey/assets/intro.toml
create mode 100644 cmd/survey/forms/intro.go
create mode 100644 cmd/survey/survey.go
diff --git a/cmd/survey.go b/cmd/survey.go
deleted file mode 100644
index a6a9922..0000000
--- a/cmd/survey.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "os"
-
- "github.com/LazyBachelor/LazyPM/pkg"
- "github.com/LazyBachelor/LazyPM/pkg/cli"
- "github.com/LazyBachelor/LazyPM/pkg/cli/repl"
- "github.com/LazyBachelor/LazyPM/pkg/tui"
- "github.com/LazyBachelor/LazyPM/pkg/web"
-)
-
-func main() {
- config := pkg.SurveyConfig{
- RootCmd: "pm",
- WebAddress: "localhost:8080",
- IssuePrefix: "pm",
- BeadsDBPath: "./.pm/db.db",
- StatisticsStoragePath: "./.pm/stats.json",
- }
-
- ctx := context.Background()
- var err error
-
- switch os.Args[1] {
- case "tui":
- err = tui.Run(ctx, config)
- case "cli":
- err = cli.RunWithArgs(ctx, config, os.Args[2:])
- case "repl":
- err = repl.RunREPL(ctx, config)
- case "web":
- err = web.Run(ctx, config)
- default:
- err = fmt.Errorf("unknown command: %s", os.Args[1])
- }
-
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error: %v\n", err)
- os.Exit(1)
- }
-}
diff --git a/cmd/survey/assets/intro.toml b/cmd/survey/assets/intro.toml
new file mode 100644
index 0000000..845895b
--- /dev/null
+++ b/cmd/survey/assets/intro.toml
@@ -0,0 +1,10 @@
+Title = "Welcome to the PM CLI Survey!"
+Description = "Thank you for taking the time to participate in our survey. Your feedback is crucial in helping us improve the PM CLI and make it a more effective tool for project management. Please read the following information about the PM CLI before proceeding with the survey."
+
+[About]
+Title = "About the PM CLI"
+Description = "YO PM CLI is a tool designed to help you manage your projects more efficiently. It provides features such as task management, issue tracking, and team collaboration, all through a simple command-line interface."
+
+[Disclaimer]
+Title = "Disclaimer"
+Description = "Please note that the PM CLI is currently in its early stages of development. We are actively seeking feedback to improve the tool, so your input is highly valuable to us. By participating in this survey, you agree to provide honest and constructive feedback to help shape the future of the PM CLI."
\ No newline at end of file
diff --git a/cmd/survey/forms/intro.go b/cmd/survey/forms/intro.go
new file mode 100644
index 0000000..fd4be96
--- /dev/null
+++ b/cmd/survey/forms/intro.go
@@ -0,0 +1,56 @@
+package forms
+
+import (
+ "embed"
+ "fmt"
+
+ "github.com/BurntSushi/toml"
+ "github.com/charmbracelet/huh"
+)
+
+type Introduction struct {
+ Title string
+ Description string
+ About struct {
+ Title string
+ Description string
+ }
+ Disclaimer struct {
+ Title string
+ Description string
+ }
+}
+
+func NewIntroduction(fs embed.FS) (Introduction, error) {
+ var intro Introduction
+ if _, err := toml.DecodeFS(fs, "assets/intro.toml", &intro); err != nil {
+ return Introduction{}, err
+ }
+
+ return intro, nil
+}
+
+func (i Introduction) Run() error {
+ fmt.Print("\033[H\033[2J")
+
+ form := huh.NewForm(
+ huh.NewGroup(
+ huh.NewNote().
+ Title(i.Title).
+ Description(i.Description),
+ ),
+ huh.NewGroup(
+ huh.NewNote().
+ Title(i.About.Title).
+ Description(i.About.Description),
+ ),
+ huh.NewGroup(
+ huh.NewNote().
+ Title(i.Disclaimer.Title).
+ Description(i.Disclaimer.Description),
+ ),
+ ).WithLayout(huh.LayoutStack).
+ WithTheme(huh.ThemeBase16())
+
+ return form.Run()
+}
diff --git a/cmd/survey/survey.go b/cmd/survey/survey.go
new file mode 100644
index 0000000..71cd91f
--- /dev/null
+++ b/cmd/survey/survey.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+ "context"
+ "embed"
+ "fmt"
+ "os"
+
+ "github.com/LazyBachelor/LazyPM/cmd/survey/forms"
+ "github.com/LazyBachelor/LazyPM/pkg"
+ "github.com/LazyBachelor/LazyPM/pkg/cli/repl"
+ "github.com/LazyBachelor/LazyPM/pkg/tui"
+ "github.com/LazyBachelor/LazyPM/pkg/web"
+ "github.com/charmbracelet/huh"
+)
+
+//go:embed assets/*
+var assetsFS embed.FS
+
+func main() {
+ intro, err := forms.NewIntroduction(assetsFS)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error loading introduction: %v\n", err)
+ os.Exit(1)
+ }
+
+ if err := intro.Run(); err != nil {
+ fmt.Fprintf(os.Stderr, "Error running introduction: %v\n", err)
+ os.Exit(1)
+ }
+
+ var selected string
+ prompt := huh.NewSelect[string]().Options(
+ huh.NewOption("Start CLI in REPL Mode", "repl"),
+ huh.NewOption("Start Web Interface", "web"),
+ huh.NewOption("Start TUI Interface", "tui"),
+ ).Value(&selected).WithTheme(huh.ThemeBase16())
+
+ if err := prompt.Run(); err != nil {
+ fmt.Fprintf(os.Stderr, "Error running prompt: %v\n", err)
+ os.Exit(1)
+ }
+
+ config := pkg.SurveyConfig{
+ RootCmd: "pm",
+ IssuePrefix: "pm",
+ BeadsDBPath: "./.pm/db.db",
+ StatisticsStoragePath: "./.pm/stats.json",
+ WebAddress: "localhost:8080",
+ }
+
+ ctx := context.Background()
+ switch selected {
+ case "repl":
+ fmt.Println("Starting CLI in REPL mode...")
+ err = repl.RunREPL(ctx, config)
+ case "web":
+ fmt.Println("Starting Web Interface...")
+ err = web.Run(ctx, config)
+ case "tui":
+ fmt.Println("Starting TUI Interface...")
+ err = tui.Run(ctx, config)
+ default:
+ fmt.Println("Invalid selection. Exiting.")
+ }
+
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error running selected interface: %v\n", err)
+ os.Exit(1)
+ }
+
+}
diff --git a/go.mod b/go.mod
index 04c7cad..fad1402 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/LazyBachelor/LazyPM
go 1.25.6
require (
+ github.com/BurntSushi/toml v1.6.0
github.com/Dicklesworthstone/beads_viewer v0.14.3
github.com/NYTimes/gziphandler v1.1.1
github.com/a-h/templ v0.3.977
diff --git a/go.sum b/go.sum
index dba30f8..14f3663 100644
--- a/go.sum
+++ b/go.sum
@@ -5,6 +5,8 @@ git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dk
git.sr.ht/~sbinet/gg v0.7.0 h1:YmNf7YKd7diDMTPm86hZa1EM3pbkOyD/zzjl0LZUdNM=
git.sr.ht/~sbinet/gg v0.7.0/go.mod h1:VYeli15tpMM4EvqlivlVbbyvWZlOU+EZn4XZmfBGUdM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Dicklesworthstone/beads_viewer v0.14.3 h1:DxeICQESYvrH+8cn/Pymlb7l9Xm5XL6BTyi6KWiY3H8=
github.com/Dicklesworthstone/beads_viewer v0.14.3/go.mod h1:5oEV2h+PVmBTdQpOijnlWMlG/qi+zheXbgOUlqKmWKI=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
From 0b840f87d8a36975a83d54e76f7cdd65870e9436 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 19:56:26 +0100
Subject: [PATCH 02/46] remove survery intro, will find better way
---
cmd/survey/assets/intro.toml | 10 -------
cmd/survey/forms/intro.go | 56 ------------------------------------
2 files changed, 66 deletions(-)
delete mode 100644 cmd/survey/assets/intro.toml
delete mode 100644 cmd/survey/forms/intro.go
diff --git a/cmd/survey/assets/intro.toml b/cmd/survey/assets/intro.toml
deleted file mode 100644
index 845895b..0000000
--- a/cmd/survey/assets/intro.toml
+++ /dev/null
@@ -1,10 +0,0 @@
-Title = "Welcome to the PM CLI Survey!"
-Description = "Thank you for taking the time to participate in our survey. Your feedback is crucial in helping us improve the PM CLI and make it a more effective tool for project management. Please read the following information about the PM CLI before proceeding with the survey."
-
-[About]
-Title = "About the PM CLI"
-Description = "YO PM CLI is a tool designed to help you manage your projects more efficiently. It provides features such as task management, issue tracking, and team collaboration, all through a simple command-line interface."
-
-[Disclaimer]
-Title = "Disclaimer"
-Description = "Please note that the PM CLI is currently in its early stages of development. We are actively seeking feedback to improve the tool, so your input is highly valuable to us. By participating in this survey, you agree to provide honest and constructive feedback to help shape the future of the PM CLI."
\ No newline at end of file
diff --git a/cmd/survey/forms/intro.go b/cmd/survey/forms/intro.go
deleted file mode 100644
index fd4be96..0000000
--- a/cmd/survey/forms/intro.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package forms
-
-import (
- "embed"
- "fmt"
-
- "github.com/BurntSushi/toml"
- "github.com/charmbracelet/huh"
-)
-
-type Introduction struct {
- Title string
- Description string
- About struct {
- Title string
- Description string
- }
- Disclaimer struct {
- Title string
- Description string
- }
-}
-
-func NewIntroduction(fs embed.FS) (Introduction, error) {
- var intro Introduction
- if _, err := toml.DecodeFS(fs, "assets/intro.toml", &intro); err != nil {
- return Introduction{}, err
- }
-
- return intro, nil
-}
-
-func (i Introduction) Run() error {
- fmt.Print("\033[H\033[2J")
-
- form := huh.NewForm(
- huh.NewGroup(
- huh.NewNote().
- Title(i.Title).
- Description(i.Description),
- ),
- huh.NewGroup(
- huh.NewNote().
- Title(i.About.Title).
- Description(i.About.Description),
- ),
- huh.NewGroup(
- huh.NewNote().
- Title(i.Disclaimer.Title).
- Description(i.Disclaimer.Description),
- ),
- ).WithLayout(huh.LayoutStack).
- WithTheme(huh.ThemeBase16())
-
- return form.Run()
-}
From 08ded5384cff5f317b6fc20fa8538287c4488dea Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 19:57:52 +0100
Subject: [PATCH 03/46] change signature of interfaces to fit Interface
interface
---
pkg/cli/cli.go | 10 ++++++++--
pkg/cli/repl/repl.go | 8 +++++++-
pkg/tui/tui.go | 18 +++++++++++++-----
pkg/web/web.go | 8 +++++++-
4 files changed, 35 insertions(+), 9 deletions(-)
diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go
index c3f515c..27dd22a 100644
--- a/pkg/cli/cli.go
+++ b/pkg/cli/cli.go
@@ -11,8 +11,14 @@ import (
// CLIConfig is an alias for service.Config, used to configure the CLI.
type CLIConfig = service.Config
+type CLI struct{}
+
+func NewCli() *CLI {
+ return &CLI{}
+}
+
// Run initializes the services and executes the CLI commands.
-func Run(ctx context.Context, config CLIConfig) error {
+func (c *CLI) Run(ctx context.Context, config CLIConfig) error {
svc, cleanup, err := service.NewServices(ctx, config)
if err != nil {
return err
@@ -30,7 +36,7 @@ func Run(ctx context.Context, config CLIConfig) error {
}
// RunWithArgs initializes the services and executes the CLI commands with the provided arguments.
-func RunWithArgs(ctx context.Context, config CLIConfig, args []string) error {
+func (c *CLI) RunWithArgs(ctx context.Context, config CLIConfig, args []string) error {
svc, cleanup, err := service.NewServices(ctx, config)
if err != nil {
return err
diff --git a/pkg/cli/repl/repl.go b/pkg/cli/repl/repl.go
index cba9569..1e9ffbd 100644
--- a/pkg/cli/repl/repl.go
+++ b/pkg/cli/repl/repl.go
@@ -22,8 +22,14 @@ You can also run shell commands directly. Type 'exit' or 'quit' to leave.`
ReplTitle = "Welcome to Project Management CLI! " + ReplHelp
)
+type REPL struct{}
+
+func NewRepl() *REPL {
+ return &REPL{}
+}
+
// RunREPL starts the interactive Read-Eval-Print Loop for the PM CLI.
-func RunREPL(ctx context.Context, config cli.CLIConfig) error {
+func (r *REPL) Run(ctx context.Context, config cli.CLIConfig) error {
// 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.
diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go
index b0feda5..7a26ed7 100644
--- a/pkg/tui/tui.go
+++ b/pkg/tui/tui.go
@@ -10,16 +10,24 @@ import (
type TUIConfig = service.Config
-func Run(ctx context.Context, config TUIConfig) (tea.Model, error) {
+type Tui struct{}
+
+func NewTui() *Tui {
+ return &Tui{}
+}
+
+func (t *Tui) Run(ctx context.Context, config TUIConfig) error {
svc, cleanup, err := service.NewServices(ctx, config)
if err != nil {
- return nil, err
+ return nil
}
defer cleanup()
- app := tea.NewProgram(views.NewDashboardView(svc),
- tea.WithAltScreen(), tea.WithMouseAllMotion())
+ if _, err := tea.NewProgram(views.NewDashboardView(svc),
+ tea.WithAltScreen(), tea.WithMouseAllMotion()).Run(); err != nil {
+ return err
+ }
- return app.Run()
+ return nil
}
diff --git a/pkg/web/web.go b/pkg/web/web.go
index 0670da8..f38215c 100644
--- a/pkg/web/web.go
+++ b/pkg/web/web.go
@@ -11,10 +11,16 @@ import (
type WebConfig = service.Config
+type Web struct{}
+
+func NewWeb() *Web {
+ return &Web{}
+}
+
//go:embed assets/*
var assets embed.FS
-func Run(ctx context.Context, config WebConfig) error {
+func (w Web) Run(ctx context.Context, config WebConfig) error {
svc, cleanup, err := service.NewServices(ctx, config)
if err != nil {
return err
From f9fe03a45135d77784f209634a2188e12c99a406 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 19:58:23 +0100
Subject: [PATCH 04/46] add delete issues for deleting all issues add db for
interactig directly with db
---
internal/service/service.go | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/internal/service/service.go b/internal/service/service.go
index f36730e..8210ae7 100644
--- a/internal/service/service.go
+++ b/internal/service/service.go
@@ -2,6 +2,7 @@ package service
import (
"context"
+ "database/sql"
"fmt"
"os"
"time"
@@ -24,6 +25,7 @@ type Config struct {
type Services struct {
Config Config
+ DB *sql.DB
Beads *BeadsService
Statistics *StatisticsService
}
@@ -36,6 +38,12 @@ func NewServices(ctx context.Context, config Config) (*Services, func(), error)
os.Exit(0)
}
+ db, err := sql.Open("sqlite3", config.BeadsDBPath)
+ if err != nil {
+ return nil, nil, err
+ }
+ cleanupFuncs = append(cleanupFuncs, func() { db.Close() })
+
store, err := beads.NewSQLiteStorage(ctx, config.BeadsDBPath)
if err != nil {
return nil, nil, err
@@ -59,6 +67,7 @@ func NewServices(ctx context.Context, config Config) (*Services, func(), error)
}
return &Services{
+ DB: db,
Beads: beadsSvc,
Statistics: statSvc,
Config: config,
@@ -92,3 +101,13 @@ func initialized(beadsPath string) bool {
}
return true
}
+
+func (s *Services) DeleteIssues() error {
+
+ var deleteIssues = "DELETE FROM issues;"
+
+ if _, err := s.DB.Exec(deleteIssues); err != nil {
+ return err
+ }
+ return nil
+}
From ba4394ecdca1990537e37646ca7d928bdfef87fa Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 19:59:05 +0100
Subject: [PATCH 05/46] construct structs
---
cmd/pm/main.go | 2 ++
cmd/tui/main.go | 4 +++-
cmd/web/main.go | 7 +++++--
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/cmd/pm/main.go b/cmd/pm/main.go
index 5ad70e1..d067ef4 100644
--- a/cmd/pm/main.go
+++ b/cmd/pm/main.go
@@ -14,6 +14,8 @@ func main() {
StatisticsStoragePath: "./.pm/stats.json",
}
+ cli := cli.NewCli()
+
if err := cli.Run(context.Background(), config); err != nil {
return
}
diff --git a/cmd/tui/main.go b/cmd/tui/main.go
index f53021f..824e6aa 100644
--- a/cmd/tui/main.go
+++ b/cmd/tui/main.go
@@ -13,7 +13,9 @@ func main() {
IssuePrefix: "pm",
}
- if _, err := tui.Run(context.Background(), config); err != nil {
+ tui := tui.NewTui()
+
+ if err := tui.Run(context.Background(), config); err != nil {
panic(err)
}
}
diff --git a/cmd/web/main.go b/cmd/web/main.go
index bf8542b..1cb7518 100644
--- a/cmd/web/main.go
+++ b/cmd/web/main.go
@@ -1,14 +1,17 @@
package main
import (
- "github.com/LazyBachelor/LazyPM/internal/service"
- "github.com/LazyBachelor/LazyPM/pkg/web"
"context"
"fmt"
"os"
+
+ "github.com/LazyBachelor/LazyPM/internal/service"
+ "github.com/LazyBachelor/LazyPM/pkg/web"
)
func main() {
+ web := web.NewWeb()
+
config := service.Config{
WebAddress: "localhost:8080",
BeadsDBPath: "./.pm/db.db",
From 5d97ef1fe7772902271c8f3b98cb07625693c26d Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 20:38:28 +0100
Subject: [PATCH 06/46] remove test main
---
main.go | 53 -----------------------------------------------------
1 file changed, 53 deletions(-)
delete mode 100644 main.go
diff --git a/main.go b/main.go
deleted file mode 100644
index 25d2bc4..0000000
--- a/main.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "os"
-
- "github.com/LazyBachelor/LazyPM/internal/models"
- "github.com/LazyBachelor/LazyPM/internal/service"
-)
-
-func main() {
- ctx := context.Background()
-
- config := service.Config{
- IssuePrefix: "pm",
- BeadsDBPath: "./.pm/db.db",
- StatisticsStoragePath: "./.pm/stats.json",
- }
- svc, cleanup, err := service.NewServices(ctx, config)
- checkErr(err)
-
- defer cleanup()
-
- issue := &models.Issue{
- IssueType: models.TypeTask,
- Title: "Sample Issue",
- Description: "This is a sample issue created for testing.",
- Status: models.StatusOpen,
- }
-
- err = svc.Beads.CreateIssue(ctx, issue, "")
- checkErr(err)
-
- fetchedIssues, err := svc.Beads.SearchIssues(ctx, "", models.IssueFilter{})
- checkErr(err)
-
- for _, iss := range fetchedIssues {
- fmt.Printf("Issue ID: %s, Title: %s, Status: %s\n", iss.ID, iss.Title, iss.Status)
- }
-
- stats, err := svc.Statistics.GetStatistics()
- checkErr(err)
-
- fmt.Printf("\nStatistics: %v\n", stats)
-}
-
-func checkErr(err error) {
- if err != nil {
- fmt.Println("Error:", err)
- os.Exit(1)
- }
-}
From ae13ccfdab3eb6874842c57fba8b1e37b0ef8469 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 20:38:46 +0100
Subject: [PATCH 07/46] update deps
---
go.mod | 3 +--
go.sum | 2 --
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/go.mod b/go.mod
index 458060d..521c31d 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/LazyBachelor/LazyPM
go 1.25.6
require (
- github.com/BurntSushi/toml v1.6.0
+ charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410
github.com/google/uuid v1.6.0
github.com/muesli/reflow v0.3.0
github.com/steveyegge/beads v0.49.6
@@ -29,7 +29,6 @@ require (
)
require (
- charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // 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
diff --git a/go.sum b/go.sum
index 6d444a0..d774a58 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,5 @@
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=
-github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
-github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
From da8ab0833ea3c5d1a837c5ceb10e501381fa3c84 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 20:39:00 +0100
Subject: [PATCH 08/46] delete survey pkg
---
pkg/survey.go | 19 -------------------
1 file changed, 19 deletions(-)
delete mode 100644 pkg/survey.go
diff --git a/pkg/survey.go b/pkg/survey.go
deleted file mode 100644
index 49636e1..0000000
--- a/pkg/survey.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package pkg
-
-import (
- "context"
-
- "github.com/LazyBachelor/LazyPM/internal/service"
-)
-
-type SurveyConfig = service.Config
-
-func Run(ctx context.Context, config SurveyConfig) error {
- _, cleanup, err := service.NewServices(ctx, config)
- if err != nil {
- return err
- }
- defer cleanup()
-
- return nil
-}
From 183f13895b127b7e6dd0dfad509beb4a2946aab7 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 20:40:55 +0100
Subject: [PATCH 09/46] add task system and task model for viewing task details
---
cmd/survey/tasks/runner.go | 22 ++++++++++++
cmd/survey/tasks/task.go | 45 +++++++++++++++++++++++++
cmd/survey/tasks/types.go | 29 ++++++++++++++++
cmd/survey/ui/help.go | 27 +++++++++++++++
cmd/survey/ui/style.go | 14 ++++++++
cmd/survey/ui/task.go | 69 ++++++++++++++++++++++++++++++++++++++
cmd/survey/ui/types.go | 28 ++++++++++++++++
7 files changed, 234 insertions(+)
create mode 100644 cmd/survey/tasks/runner.go
create mode 100644 cmd/survey/tasks/task.go
create mode 100644 cmd/survey/tasks/types.go
create mode 100644 cmd/survey/ui/help.go
create mode 100644 cmd/survey/ui/style.go
create mode 100644 cmd/survey/ui/task.go
create mode 100644 cmd/survey/ui/types.go
diff --git a/cmd/survey/tasks/runner.go b/cmd/survey/tasks/runner.go
new file mode 100644
index 0000000..e407efe
--- /dev/null
+++ b/cmd/survey/tasks/runner.go
@@ -0,0 +1,22 @@
+package tasks
+
+import (
+ "context"
+
+ "github.com/LazyBachelor/LazyPM/internal/service"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+func (t *Task) IntroduceTask() error {
+ _, err := tea.NewProgram(t.aboutScreen, tea.WithAltScreen()).Run()
+ return err
+}
+
+func (t *Task) StartInterface(ctx context.Context, cfg service.Config) error {
+ return t.interfaceType.Run(ctx, cfg)
+}
+
+func (t *Task) StartQuestionnaire() error {
+ _, err := tea.NewProgram(t.questionnaire, tea.WithAltScreen()).Run()
+ return err
+}
diff --git a/cmd/survey/tasks/task.go b/cmd/survey/tasks/task.go
new file mode 100644
index 0000000..71b54eb
--- /dev/null
+++ b/cmd/survey/tasks/task.go
@@ -0,0 +1,45 @@
+// task.go
+package tasks
+
+import (
+ "context"
+ "errors"
+
+ "github.com/LazyBachelor/LazyPM/internal/service"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+func NewTask(interfaceType Interface, aboutScreen tea.Model, questionnaire tea.Model) *Task {
+ return &Task{
+ interfaceType: interfaceType,
+ aboutScreen: aboutScreen,
+ questionnaire: questionnaire,
+ }
+}
+
+func (t *Task) SetValidateFunc(fn ValidateFunc) {
+ t.validateFunc = fn
+}
+
+func (t *Task) SetDbStateFunc(fn DbStateFunc) {
+ t.dbStateFunc = fn
+}
+
+func (t *Task) SetInterface(interfaceType Interface) {
+ t.interfaceType = interfaceType
+}
+
+func (t *Task) Validate(ctx context.Context, svc *service.Services) (bool, error) {
+ if t.validateFunc == nil {
+ return false, errors.New("validateFunc is not set")
+ }
+ return t.validateFunc(ctx, svc)
+}
+
+func (t *Task) MigrateToTask(ctx context.Context, svc *service.Services, task *Task) error {
+ t = task
+ if t.dbStateFunc == nil {
+ return errors.New("dbStateFunc is not set")
+ }
+ return t.dbStateFunc(ctx, svc)
+}
diff --git a/cmd/survey/tasks/types.go b/cmd/survey/tasks/types.go
new file mode 100644
index 0000000..bfc9e87
--- /dev/null
+++ b/cmd/survey/tasks/types.go
@@ -0,0 +1,29 @@
+package tasks
+
+import (
+ "context"
+
+ "github.com/LazyBachelor/LazyPM/internal/service"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+type Interface interface {
+ Run(context.Context, service.Config) error
+}
+
+type ValidateFunc func(context.Context, *service.Services) (ok bool, err error)
+type DbStateFunc func(context.Context, *service.Services) error
+
+type Task struct {
+ interfaceType Interface
+ aboutScreen tea.Model
+ questionnaire tea.Model
+
+ validateFunc ValidateFunc
+ dbStateFunc DbStateFunc
+}
+
+type TaskList struct {
+ Todo []*Task
+ Done []*Task
+}
diff --git a/cmd/survey/ui/help.go b/cmd/survey/ui/help.go
new file mode 100644
index 0000000..15b58a6
--- /dev/null
+++ b/cmd/survey/ui/help.go
@@ -0,0 +1,27 @@
+package ui
+
+import "github.com/charmbracelet/bubbles/key"
+
+type TaskHelpKeys struct {
+ Quit key.Binding
+ Continue key.Binding
+}
+
+var DefaultTaskKeys = TaskHelpKeys{
+ Quit: key.NewBinding(
+ key.WithKeys("q", "ctrl+c"),
+ key.WithHelp("q", "Quit"),
+ ),
+ Continue: key.NewBinding(
+ key.WithKeys(" "),
+ key.WithHelp("space", "Continue"),
+ ),
+}
+
+func (h TaskHelpKeys) ShortHelp() []key.Binding {
+ return []key.Binding{h.Continue, h.Quit}
+}
+
+func (h TaskHelpKeys) FullHelp() [][]key.Binding {
+ return [][]key.Binding{{h.Continue, h.Quit}}
+}
diff --git a/cmd/survey/ui/style.go b/cmd/survey/ui/style.go
new file mode 100644
index 0000000..9a2b4f4
--- /dev/null
+++ b/cmd/survey/ui/style.go
@@ -0,0 +1,14 @@
+package ui
+
+import (
+ "github.com/charmbracelet/huh"
+ "github.com/charmbracelet/lipgloss"
+)
+
+func theme() *huh.Theme {
+ theme := huh.ThemeBase16()
+
+ theme.Focused.Base = lipgloss.NewStyle().Align(lipgloss.Center)
+
+ return theme
+}
diff --git a/cmd/survey/ui/task.go b/cmd/survey/ui/task.go
new file mode 100644
index 0000000..18c4011
--- /dev/null
+++ b/cmd/survey/ui/task.go
@@ -0,0 +1,69 @@
+package ui
+
+import (
+ "charm.land/lipgloss/v2"
+ "github.com/charmbracelet/bubbles/help"
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+func NewTaskModel(details TaskDetails) TaskModel {
+ return TaskModel{
+ TaskDetails: details,
+ keys: DefaultTaskKeys,
+ help: help.New(),
+ }
+}
+
+func (m TaskModel) Init() tea.Cmd {
+ return nil
+}
+
+func (t TaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ t.SetSize(msg.Width, msg.Height)
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, t.keys.Quit):
+ return t, tea.Quit
+ case key.Matches(msg, t.keys.Continue):
+ return t, tea.Quit
+ }
+ }
+ return t, nil
+}
+
+func (m TaskModel) View() string {
+ padding := 3
+
+ header := lipgloss.NewStyle().
+ PaddingTop(padding).Width(m.width).Align(lipgloss.Center).
+ Bold(true).Render(m.Title)
+
+ headerHeight := lipgloss.Height(header)
+
+ helpView := lipgloss.NewStyle().
+ PaddingBottom(padding).
+ Width(m.width).Align(lipgloss.Center).
+ Render(m.help.View(m.keys))
+
+ helpHeigh := lipgloss.Height(helpView)
+
+ details := lipgloss.NewStyle().Align(lipgloss.Center).
+ Width(m.width).PaddingBottom(1).Render("Time to complete:", m.TimeToComplete, "Difficulty:", m.Difficulty)
+
+ detailsHeight := lipgloss.Height(details)
+
+ content := lipgloss.NewStyle().
+ Width(m.width).Height(m.height-headerHeight-helpHeigh-detailsHeight).
+ Align(lipgloss.Center, lipgloss.Center).
+ Render(m.Description)
+
+ return lipgloss.JoinVertical(lipgloss.Top, header, content, details, helpView)
+
+}
+
+func (m *TaskModel) SetSize(width, height int) {
+ m.width, m.height = width, height
+}
diff --git a/cmd/survey/ui/types.go b/cmd/survey/ui/types.go
new file mode 100644
index 0000000..ec382be
--- /dev/null
+++ b/cmd/survey/ui/types.go
@@ -0,0 +1,28 @@
+package ui
+
+import (
+ "github.com/charmbracelet/bubbles/help"
+ "github.com/charmbracelet/huh"
+)
+
+type TaskDetails struct {
+ Title string
+ Description string
+ TimeToComplete string
+ Difficulty string
+}
+
+type TaskModel struct {
+ TaskDetails
+ keys TaskHelpKeys
+ help help.Model
+ width, height int
+}
+
+type Questions []*huh.Group
+
+type QuestionnaireModel struct {
+ Questions
+ form *huh.Form
+ width, height int
+}
From 964e9234be459a6b72bc921cf6766d76729b857b Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 20:41:19 +0100
Subject: [PATCH 10/46] add questionare model for survey after task
---
cmd/survey/ui/questionare.go | 53 ++++++++++++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
create mode 100644 cmd/survey/ui/questionare.go
diff --git a/cmd/survey/ui/questionare.go b/cmd/survey/ui/questionare.go
new file mode 100644
index 0000000..9849a60
--- /dev/null
+++ b/cmd/survey/ui/questionare.go
@@ -0,0 +1,53 @@
+package ui
+
+import (
+ "charm.land/lipgloss/v2"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/huh"
+)
+
+func NewQuestionnaireModel(questions Questions) *QuestionnaireModel {
+ form := huh.NewForm(questions...).
+ WithTheme(theme()).WithLayout(huh.LayoutGrid(1, 1))
+
+ return &QuestionnaireModel{
+ Questions: questions,
+ form: form,
+ }
+}
+
+func (q *QuestionnaireModel) Init() tea.Cmd {
+ return q.form.Init()
+}
+
+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:
+ switch msg.String() {
+ case "q", "ctrl+c":
+ return q, tea.Quit
+ }
+ }
+
+ form, cmd := q.form.Update(msg)
+ if f, ok := form.(*huh.Form); ok {
+ q.form = f
+ }
+ return q, cmd
+}
+
+func (q *QuestionnaireModel) View() string {
+ form := lipgloss.NewStyle().
+ Width(q.width).Align(lipgloss.Center).
+ Render(q.form.View())
+
+ return lipgloss.Place(
+ q.width, q.height, lipgloss.Center, lipgloss.Center, form,
+ )
+}
+
+func (q *QuestionnaireModel) SetSize(width, height int) {
+ q.width, q.height = width, height
+}
From e74c784b5dfc1bbda58a255b58f5e73dd6667f11 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 20:41:47 +0100
Subject: [PATCH 11/46] use new tasks and questionare with interfaces add a
create issue task WIP
---
cmd/survey/survey.go | 83 +++++++++++++++------------------
cmd/survey/tasks/createIssue.go | 74 +++++++++++++++++++++++++++++
2 files changed, 111 insertions(+), 46 deletions(-)
create mode 100644 cmd/survey/tasks/createIssue.go
diff --git a/cmd/survey/survey.go b/cmd/survey/survey.go
index bc58ef9..e689b5d 100644
--- a/cmd/survey/survey.go
+++ b/cmd/survey/survey.go
@@ -2,71 +2,62 @@ package main
import (
"context"
- "embed"
"fmt"
"os"
- "github.com/LazyBachelor/LazyPM/cmd/survey/forms"
- "github.com/LazyBachelor/LazyPM/pkg"
+ "github.com/LazyBachelor/LazyPM/cmd/survey/tasks"
+ "github.com/LazyBachelor/LazyPM/internal/service"
"github.com/LazyBachelor/LazyPM/pkg/cli/repl"
"github.com/LazyBachelor/LazyPM/pkg/tui"
"github.com/LazyBachelor/LazyPM/pkg/web"
- "github.com/charmbracelet/huh"
)
-//go:embed assets/*
-var assetsFS embed.FS
-
func main() {
- intro, err := forms.NewIntroduction(assetsFS)
+ ctx := context.Background()
+
+ svc, close, err := initializeServices(ctx)
if err != nil {
- fmt.Fprintf(os.Stderr, "Error loading introduction: %v\n", err)
- os.Exit(1)
+ fatal("Failed to initialize services: %v\n", err)
+ }
+ defer close()
+
+ tui := tui.NewTui()
+ web := web.NewWeb()
+ repl := repl.NewRepl()
+
+ createIssueTask := tasks.NewCreateIssueTask(web)
+
+ task := createIssueTask
+
+ task.SetInterface(tui)
+ task.SetInterface(repl)
+
+ task.MigrateToTask(ctx, svc, task)
+
+ if err := task.IntroduceTask(); err != nil {
+ fatal("Failed to introduce task: %v\n", err)
}
- if err := intro.Run(); err != nil {
- fmt.Fprintf(os.Stderr, "Error running introduction: %v\n", err)
- os.Exit(1)
+ if err := task.StartInterface(ctx, svc.Config); err != nil {
+ fatal("Failed to start interface: %v\n", err)
}
- var selected string
- prompt := huh.NewSelect[string]().Options(
- huh.NewOption("Start CLI in REPL Mode", "repl"),
- huh.NewOption("Start Web Interface", "web"),
- huh.NewOption("Start TUI Interface", "tui"),
- ).Value(&selected).WithTheme(huh.ThemeBase16())
-
- if err := prompt.Run(); err != nil {
- fmt.Fprintf(os.Stderr, "Error running prompt: %v\n", err)
- os.Exit(1)
+ if err := task.StartQuestionnaire(); err != nil {
+ fatal("Failed to start questionnaire: %v\n", err)
}
+}
- config := pkg.SurveyConfig{
- RootCmd: "pm",
+func initializeServices(ctx context.Context) (*service.Services, func(), error) {
+ config := service.Config{
IssuePrefix: "pm",
BeadsDBPath: "./.pm/db.db",
StatisticsStoragePath: "./.pm/stats.json",
WebAddress: "localhost:8080",
}
-
- ctx := context.Background()
- switch selected {
- case "repl":
- fmt.Println("Starting CLI in REPL mode...")
- err = repl.RunREPL(ctx, config)
- case "web":
- fmt.Println("Starting Web Interface...")
- err = web.Run(ctx, config)
- case "tui":
- fmt.Println("Starting TUI Interface...")
- _, err = tui.Run(ctx, config)
- default:
- fmt.Println("Invalid selection. Exiting.")
- }
-
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error running selected interface: %v\n", err)
- os.Exit(1)
- }
-
+ return service.NewServices(ctx, config)
+}
+
+func fatal(format string, args ...interface{}) {
+ fmt.Fprintf(os.Stderr, format, args...)
+ os.Exit(1)
}
diff --git a/cmd/survey/tasks/createIssue.go b/cmd/survey/tasks/createIssue.go
new file mode 100644
index 0000000..4932a77
--- /dev/null
+++ b/cmd/survey/tasks/createIssue.go
@@ -0,0 +1,74 @@
+package tasks
+
+import (
+ "context"
+ "errors"
+
+ "github.com/LazyBachelor/LazyPM/cmd/survey/ui"
+ "github.com/LazyBachelor/LazyPM/internal/models"
+ "github.com/LazyBachelor/LazyPM/internal/service"
+ "github.com/charmbracelet/huh"
+)
+
+func NewCreateIssueTask(interfaceType Interface) *Task {
+ aboutScreen := ui.NewTaskModel(createIssueDetails())
+ questionare := ui.NewQuestionnaireModel(createIssueQuestionare())
+
+ task := NewTask(interfaceType, aboutScreen, questionare)
+ task.SetDbStateFunc(createIssueDbState)
+ task.SetValidateFunc(createIssueValidate)
+
+ return task
+}
+
+func createIssueDetails() ui.TaskDetails {
+ return ui.TaskDetails{
+ Title: "Create Issue Task",
+ Description: `Description for create issue task`,
+ TimeToComplete: "15m",
+ Difficulty: "Hard",
+ }
+}
+
+func createIssueQuestionare() ui.Questions {
+ return ui.Questions{
+ huh.NewGroup(
+ huh.NewConfirm().Title("Was this good"),
+ ),
+ huh.NewGroup(
+ huh.NewSelect[int]().Options(
+ huh.NewOption("Very good", 1),
+ huh.NewOption("Very Bad", 2),
+ ).Title("How good was it?"),
+ ),
+ }
+}
+
+func createIssueDbState(ctx context.Context, svc *service.Services) error {
+ if err := svc.DeleteIssues(); err != nil {
+ return err
+ }
+
+ issues := []*models.Issue{
+ {Title: "Test Issue", Description: "Long Description", IssueType: models.TypeBug, Status: models.StatusBlocked},
+ }
+
+ if err := svc.Beads.CreateIssues(ctx, issues, "actor"); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func createIssueValidate(ctx context.Context, svc *service.Services) (ok bool, errorMsg error) {
+ issues, err := svc.Beads.SearchIssues(ctx, "", models.IssueFilter{})
+ if err != nil {
+ return false, err
+ }
+
+ if len(issues) == 0 {
+ return false, errors.New("No issues found. Please create an issue to proceed.")
+ }
+
+ return true, nil
+}
From fe5494eaee044cb4d23b1e47b9097bb26d323299 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 22:05:25 +0100
Subject: [PATCH 12/46] format time and difficulty better use m
---
cmd/survey/ui/task.go | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/cmd/survey/ui/task.go b/cmd/survey/ui/task.go
index 18c4011..5e5314e 100644
--- a/cmd/survey/ui/task.go
+++ b/cmd/survey/ui/task.go
@@ -1,6 +1,8 @@
package ui
import (
+ "fmt"
+
"charm.land/lipgloss/v2"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
@@ -19,19 +21,19 @@ func (m TaskModel) Init() tea.Cmd {
return nil
}
-func (t TaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m TaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
- t.SetSize(msg.Width, msg.Height)
+ m.SetSize(msg.Width, msg.Height)
case tea.KeyMsg:
switch {
- case key.Matches(msg, t.keys.Quit):
- return t, tea.Quit
- case key.Matches(msg, t.keys.Continue):
- return t, tea.Quit
+ case key.Matches(msg, m.keys.Quit):
+ return m, tea.Quit
+ case key.Matches(msg, m.keys.Continue):
+ return m, tea.Quit
}
}
- return t, nil
+ return m, nil
}
func (m TaskModel) View() string {
@@ -50,8 +52,9 @@ func (m TaskModel) View() string {
helpHeigh := lipgloss.Height(helpView)
+ detailsText := fmt.Sprintf("Time to complete: %s | Difficulty: %s", m.TimeToComplete, m.Difficulty)
details := lipgloss.NewStyle().Align(lipgloss.Center).
- Width(m.width).PaddingBottom(1).Render("Time to complete:", m.TimeToComplete, "Difficulty:", m.Difficulty)
+ Width(m.width).PaddingBottom(1).Render(detailsText)
detailsHeight := lipgloss.Height(details)
From 18eb113f71c7c0231c4290e5923748ebb4ca7f12 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 22:05:44 +0100
Subject: [PATCH 13/46] add questionare
---
cmd/survey/ui/{questionare.go => questionnaire.go} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename cmd/survey/ui/{questionare.go => questionnaire.go} (100%)
diff --git a/cmd/survey/ui/questionare.go b/cmd/survey/ui/questionnaire.go
similarity index 100%
rename from cmd/survey/ui/questionare.go
rename to cmd/survey/ui/questionnaire.go
From 7dc6420854000dc1e0c8b44466e41813cc21db73 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 22:06:10 +0100
Subject: [PATCH 14/46] move around types for better understanding
---
cmd/survey/tasks/types.go | 20 ++++----------------
1 file changed, 4 insertions(+), 16 deletions(-)
diff --git a/cmd/survey/tasks/types.go b/cmd/survey/tasks/types.go
index bfc9e87..74af23c 100644
--- a/cmd/survey/tasks/types.go
+++ b/cmd/survey/tasks/types.go
@@ -4,26 +4,14 @@ import (
"context"
"github.com/LazyBachelor/LazyPM/internal/service"
- tea "github.com/charmbracelet/bubbletea"
)
+type TaskConfig = service.Config
+
type Interface interface {
- Run(context.Context, service.Config) error
+ Run(context.Context, TaskConfig) error
}
+type ConfigFunc func() TaskConfig
type ValidateFunc func(context.Context, *service.Services) (ok bool, err error)
type DbStateFunc func(context.Context, *service.Services) error
-
-type Task struct {
- interfaceType Interface
- aboutScreen tea.Model
- questionnaire tea.Model
-
- validateFunc ValidateFunc
- dbStateFunc DbStateFunc
-}
-
-type TaskList struct {
- Todo []*Task
- Done []*Task
-}
From 412b2fe0c2b7da008061124630e9737295569c25 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 22:06:48 +0100
Subject: [PATCH 15/46] add config to tasks use fmt.Errorf
---
cmd/survey/tasks/task.go | 46 +++++++++++++++++++++++++---------------
1 file changed, 29 insertions(+), 17 deletions(-)
diff --git a/cmd/survey/tasks/task.go b/cmd/survey/tasks/task.go
index 71b54eb..9e6553b 100644
--- a/cmd/survey/tasks/task.go
+++ b/cmd/survey/tasks/task.go
@@ -3,43 +3,55 @@ package tasks
import (
"context"
- "errors"
+ "fmt"
"github.com/LazyBachelor/LazyPM/internal/service"
tea "github.com/charmbracelet/bubbletea"
)
-func NewTask(interfaceType Interface, aboutScreen tea.Model, questionnaire tea.Model) *Task {
+type Task struct {
+ Config TaskConfig
+ interfaceType Interface
+ aboutScreen tea.Model
+ questionnaire tea.Model
+
+ validateFunc ValidateFunc
+ dbStateFunc DbStateFunc
+}
+
+func NewTask(aboutScreen tea.Model, questionnaire tea.Model) *Task {
return &Task{
- interfaceType: interfaceType,
aboutScreen: aboutScreen,
questionnaire: questionnaire,
}
}
-func (t *Task) SetValidateFunc(fn ValidateFunc) {
- t.validateFunc = fn
-}
-
-func (t *Task) SetDbStateFunc(fn DbStateFunc) {
- t.dbStateFunc = fn
+func (t *Task) SetConfigFunc(fn ConfigFunc) {
+ t.Config = fn()
}
func (t *Task) SetInterface(interfaceType Interface) {
t.interfaceType = interfaceType
}
-func (t *Task) Validate(ctx context.Context, svc *service.Services) (bool, error) {
- if t.validateFunc == nil {
- return false, errors.New("validateFunc is not set")
- }
- return t.validateFunc(ctx, svc)
+func (t *Task) SetDbStateFunc(fn DbStateFunc) {
+ t.dbStateFunc = fn
}
-func (t *Task) MigrateToTask(ctx context.Context, svc *service.Services, task *Task) error {
- t = task
+func (t *Task) SetValidateFunc(fn ValidateFunc) {
+ t.validateFunc = fn
+}
+
+func (t *Task) Initialize(ctx context.Context, svc *service.Services) error {
if t.dbStateFunc == nil {
- return errors.New("dbStateFunc is not set")
+ return fmt.Errorf("dbStateFunc is not set")
}
return t.dbStateFunc(ctx, svc)
}
+
+func (t *Task) Validate(ctx context.Context, svc *service.Services) (bool, error) {
+ if t.validateFunc == nil {
+ return false, fmt.Errorf("validateFunc is not set")
+ }
+ return t.validateFunc(ctx, svc)
+}
From cbfbb6a0bbc25cc74d2d57514af4c45723bc205a Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 22:07:03 +0100
Subject: [PATCH 16/46] add init file for better separation
---
cmd/survey/init.go | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
create mode 100644 cmd/survey/init.go
diff --git a/cmd/survey/init.go b/cmd/survey/init.go
new file mode 100644
index 0000000..84f7609
--- /dev/null
+++ b/cmd/survey/init.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+ "context"
+
+ "github.com/LazyBachelor/LazyPM/cmd/survey/tasks"
+ "github.com/LazyBachelor/LazyPM/internal/service"
+ "github.com/LazyBachelor/LazyPM/pkg/cli/repl"
+ "github.com/LazyBachelor/LazyPM/pkg/tui"
+ "github.com/LazyBachelor/LazyPM/pkg/web"
+)
+
+func initializeServices(ctx context.Context) (*service.Services, func(), error) {
+ config := service.Config{
+ IssuePrefix: "pm",
+ BeadsDBPath: "./.pm/db.db",
+ StatisticsStoragePath: "./.pm/stats.json",
+ WebAddress: "localhost:8080",
+ }
+ return service.NewServices(ctx, config)
+}
+
+func initTasks() []*tasks.Task {
+ return []*tasks.Task{
+ tasks.NewCreateIssueTask(),
+ tasks.NewCreateIssueTask(),
+ }
+}
+
+func initInterfaces() []tasks.Interface {
+ return []tasks.Interface{repl.NewRepl(), tui.NewTui(), web.NewWeb()}
+}
From 0e9bbbc3bd7f429d01b87ea21277588d808030d8 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 22:07:15 +0100
Subject: [PATCH 17/46] reformat
---
cmd/survey/survey.go | 84 ++++++++++++++++++++++++--------------------
1 file changed, 45 insertions(+), 39 deletions(-)
diff --git a/cmd/survey/survey.go b/cmd/survey/survey.go
index e689b5d..d913507 100644
--- a/cmd/survey/survey.go
+++ b/cmd/survey/survey.go
@@ -3,61 +3,67 @@ package main
import (
"context"
"fmt"
- "os"
+ "log"
+ "math/rand"
+ "time"
"github.com/LazyBachelor/LazyPM/cmd/survey/tasks"
"github.com/LazyBachelor/LazyPM/internal/service"
- "github.com/LazyBachelor/LazyPM/pkg/cli/repl"
- "github.com/LazyBachelor/LazyPM/pkg/tui"
- "github.com/LazyBachelor/LazyPM/pkg/web"
)
func main() {
+ rand.Seed(time.Now().UnixNano())
ctx := context.Background()
svc, close, err := initializeServices(ctx)
if err != nil {
- fatal("Failed to initialize services: %v\n", err)
+ log.Fatalf("Failed to initialize services: %v\n", err)
}
defer close()
- tui := tui.NewTui()
- web := web.NewWeb()
- repl := repl.NewRepl()
+ tasks := initTasks()
+ interfaces := initInterfaces()
- createIssueTask := tasks.NewCreateIssueTask(web)
-
- task := createIssueTask
-
- task.SetInterface(tui)
- task.SetInterface(repl)
-
- task.MigrateToTask(ctx, svc, task)
-
- if err := task.IntroduceTask(); err != nil {
- fatal("Failed to introduce task: %v\n", err)
- }
-
- if err := task.StartInterface(ctx, svc.Config); err != nil {
- fatal("Failed to start interface: %v\n", err)
- }
-
- if err := task.StartQuestionnaire(); err != nil {
- fatal("Failed to start questionnaire: %v\n", err)
+ if err := taskLoop(ctx, svc, tasks, interfaces); err != nil {
+ log.Fatalf("Task loop failed: %v\n", err)
}
}
-func initializeServices(ctx context.Context) (*service.Services, func(), error) {
- config := service.Config{
- IssuePrefix: "pm",
- BeadsDBPath: "./.pm/db.db",
- StatisticsStoragePath: "./.pm/stats.json",
- WebAddress: "localhost:8080",
- }
- return service.NewServices(ctx, config)
-}
+func taskLoop(ctx context.Context, svc *service.Services, tasks []*tasks.Task, interfaces []tasks.Interface) error {
+ interfaceIndex := rand.Int() % len(interfaces)
-func fatal(format string, args ...interface{}) {
- fmt.Fprintf(os.Stderr, format, args...)
- os.Exit(1)
+ for _, task := range tasks {
+
+ task.SetInterface(interfaces[interfaceIndex])
+
+ if err := task.Initialize(ctx, svc); err != nil {
+ return fmt.Errorf("Failed to initialize task: %w", err)
+ }
+
+ if err := task.IntroduceTask(); err != nil {
+ return fmt.Errorf("failed to display task introduction screen: %w", err)
+ }
+
+ if err := task.StartInterface(ctx, task.Config); err != nil {
+ return fmt.Errorf("failed to start task interface: %w", err)
+ }
+
+ ok, err := task.Validate(ctx, svc)
+ if err != nil {
+ return fmt.Errorf("validation error: %w", err)
+ }
+ if !ok {
+ return fmt.Errorf("task validation failed: task did not meet requirements")
+ }
+
+ if err := task.StartQuestionnaire(); err != nil {
+ return fmt.Errorf("failed to start questionnaire: %w", err)
+ }
+
+ interfaceIndex++
+ if interfaceIndex >= len(interfaces) {
+ interfaceIndex = 0
+ }
+ }
+ return nil
}
From 93c523d4905662320b210d64d9b8cd3260d3f038 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 22:07:29 +0100
Subject: [PATCH 18/46] add config and reformat
---
cmd/survey/tasks/createIssue.go | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/cmd/survey/tasks/createIssue.go b/cmd/survey/tasks/createIssue.go
index 4932a77..98fbb76 100644
--- a/cmd/survey/tasks/createIssue.go
+++ b/cmd/survey/tasks/createIssue.go
@@ -10,27 +10,36 @@ import (
"github.com/charmbracelet/huh"
)
-func NewCreateIssueTask(interfaceType Interface) *Task {
+func NewCreateIssueTask() *Task {
aboutScreen := ui.NewTaskModel(createIssueDetails())
- questionare := ui.NewQuestionnaireModel(createIssueQuestionare())
+ questionnaire := ui.NewQuestionnaireModel(createIssueQuestionnaire())
- task := NewTask(interfaceType, aboutScreen, questionare)
+ task := NewTask(aboutScreen, questionnaire)
+ task.SetConfigFunc(createIssueConfig)
task.SetDbStateFunc(createIssueDbState)
task.SetValidateFunc(createIssueValidate)
-
return task
}
+func createIssueConfig() TaskConfig {
+ return TaskConfig{
+ IssuePrefix: "pm",
+ BeadsDBPath: "./.pm/db.db",
+ StatisticsStoragePath: "./.pm/task-1-stats.json",
+ WebAddress: "localhost:8080",
+ }
+}
+
func createIssueDetails() ui.TaskDetails {
return ui.TaskDetails{
Title: "Create Issue Task",
- Description: `Description for create issue task`,
+ Description: "Create a new issue in the project management system to test the issue creation workflow.",
TimeToComplete: "15m",
Difficulty: "Hard",
}
}
-func createIssueQuestionare() ui.Questions {
+func createIssueQuestionnaire() ui.Questions {
return ui.Questions{
huh.NewGroup(
huh.NewConfirm().Title("Was this good"),
From d670ec0dc22d1c67279e9e22a9d0718771f06b60 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Thu, 12 Feb 2026 22:07:43 +0100
Subject: [PATCH 19/46] check if nil
---
cmd/survey/tasks/runner.go | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/cmd/survey/tasks/runner.go b/cmd/survey/tasks/runner.go
index e407efe..2e8c89f 100644
--- a/cmd/survey/tasks/runner.go
+++ b/cmd/survey/tasks/runner.go
@@ -2,21 +2,32 @@ package tasks
import (
"context"
+ "fmt"
"github.com/LazyBachelor/LazyPM/internal/service"
tea "github.com/charmbracelet/bubbletea"
)
func (t *Task) IntroduceTask() error {
+ if t.aboutScreen == nil {
+ return fmt.Errorf("aboutScreen is not set")
+ }
_, err := tea.NewProgram(t.aboutScreen, tea.WithAltScreen()).Run()
return err
}
func (t *Task) StartInterface(ctx context.Context, cfg service.Config) error {
+ if t.interfaceType == nil {
+ return fmt.Errorf("interfaceType is not set")
+ }
+
return t.interfaceType.Run(ctx, cfg)
}
func (t *Task) StartQuestionnaire() error {
+ if t.questionnaire == nil {
+ return fmt.Errorf("questionnaire is not set")
+ }
_, err := tea.NewProgram(t.questionnaire, tea.WithAltScreen()).Run()
return err
}
From 0bbe5ed3757a163054fb12a64c6a579e27662a0a Mon Sep 17 00:00:00 2001
From: Robin Olsen <129996395+Telikz@users.noreply.github.com>
Date: Thu, 12 Feb 2026 23:58:06 +0100
Subject: [PATCH 20/46] Update pkg/cli/repl/repl.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
pkg/cli/repl/repl.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/cli/repl/repl.go b/pkg/cli/repl/repl.go
index 1e9ffbd..5f0faa8 100644
--- a/pkg/cli/repl/repl.go
+++ b/pkg/cli/repl/repl.go
@@ -28,7 +28,7 @@ func NewRepl() *REPL {
return &REPL{}
}
-// RunREPL starts the interactive Read-Eval-Print Loop for the PM CLI.
+// Run starts the interactive Read-Eval-Print Loop for the PM CLI.
func (r *REPL) Run(ctx context.Context, config cli.CLIConfig) error {
// 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.
From 6fc888347a2f1bed653b544966ad0e67c98222e5 Mon Sep 17 00:00:00 2001
From: Robin Olsen <129996395+Telikz@users.noreply.github.com>
Date: Thu, 12 Feb 2026 23:58:24 +0100
Subject: [PATCH 21/46] Update cmd/survey/tasks/createIssue.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
cmd/survey/tasks/createIssue.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/survey/tasks/createIssue.go b/cmd/survey/tasks/createIssue.go
index 98fbb76..9f0d11d 100644
--- a/cmd/survey/tasks/createIssue.go
+++ b/cmd/survey/tasks/createIssue.go
@@ -76,7 +76,7 @@ func createIssueValidate(ctx context.Context, svc *service.Services) (ok bool, e
}
if len(issues) == 0 {
- return false, errors.New("No issues found. Please create an issue to proceed.")
+ return false, errors.New("no issues found. Please create an issue to proceed.")
}
return true, nil
From d12146462f3849cf2cf40718e55d66c6c1693297 Mon Sep 17 00:00:00 2001
From: Robin Olsen <129996395+Telikz@users.noreply.github.com>
Date: Thu, 12 Feb 2026 23:58:36 +0100
Subject: [PATCH 22/46] Update cmd/survey/survey.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
cmd/survey/survey.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/survey/survey.go b/cmd/survey/survey.go
index d913507..74ac98a 100644
--- a/cmd/survey/survey.go
+++ b/cmd/survey/survey.go
@@ -37,7 +37,7 @@ func taskLoop(ctx context.Context, svc *service.Services, tasks []*tasks.Task, i
task.SetInterface(interfaces[interfaceIndex])
if err := task.Initialize(ctx, svc); err != nil {
- return fmt.Errorf("Failed to initialize task: %w", err)
+ return fmt.Errorf("failed to initialize task: %w", err)
}
if err := task.IntroduceTask(); err != nil {
From fdfe4aa62034c1646309cd563df7c223f8cb0b04 Mon Sep 17 00:00:00 2001
From: Robin Olsen <129996395+Telikz@users.noreply.github.com>
Date: Thu, 12 Feb 2026 23:59:12 +0100
Subject: [PATCH 23/46] Update cmd/survey/survey.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
cmd/survey/survey.go | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/cmd/survey/survey.go b/cmd/survey/survey.go
index 74ac98a..a270790 100644
--- a/cmd/survey/survey.go
+++ b/cmd/survey/survey.go
@@ -21,18 +21,18 @@ func main() {
}
defer close()
- tasks := initTasks()
+ surveyTasks := initTasks()
interfaces := initInterfaces()
- if err := taskLoop(ctx, svc, tasks, interfaces); err != nil {
+ if err := taskLoop(ctx, svc, surveyTasks, interfaces); err != nil {
log.Fatalf("Task loop failed: %v\n", err)
}
}
-func taskLoop(ctx context.Context, svc *service.Services, tasks []*tasks.Task, interfaces []tasks.Interface) error {
+func taskLoop(ctx context.Context, svc *service.Services, surveyTasks []*tasks.Task, interfaces []tasks.Interface) error {
interfaceIndex := rand.Int() % len(interfaces)
- for _, task := range tasks {
+ for _, task := range surveyTasks {
task.SetInterface(interfaces[interfaceIndex])
From fc5156a0ad8d5be35f60c1ebad2794220380d744 Mon Sep 17 00:00:00 2001
From: Robin Olsen <129996395+Telikz@users.noreply.github.com>
Date: Fri, 13 Feb 2026 00:00:05 +0100
Subject: [PATCH 24/46] Update pkg/tui/tui.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
pkg/tui/tui.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go
index 7a26ed7..f9a704d 100644
--- a/pkg/tui/tui.go
+++ b/pkg/tui/tui.go
@@ -19,7 +19,7 @@ func NewTui() *Tui {
func (t *Tui) Run(ctx context.Context, config TUIConfig) error {
svc, cleanup, err := service.NewServices(ctx, config)
if err != nil {
- return nil
+ return err
}
defer cleanup()
From e994c890f460f7b5f53a276ae3af968507c9c9b2 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 10:13:41 +0100
Subject: [PATCH 25/46] refactor survery cmd move package files to pkg
---
cmd/survey/init.go | 9 +++++----
cmd/survey/survey.go | 6 ++----
cmd/survey/tasks/createIssue.go | 11 ++++++-----
cmd/survey/ui/style.go | 14 --------------
{cmd/survey/tasks => pkg/task}/runner.go | 5 ++---
{cmd/survey/tasks => pkg/task}/task.go | 3 +--
{cmd/survey/tasks => pkg/task}/types.go | 2 +-
{cmd/survey => pkg/task}/ui/help.go | 2 +-
{cmd/survey => pkg/task}/ui/questionnaire.go | 5 +++--
{cmd/survey => pkg/task}/ui/task.go | 2 +-
{cmd/survey => pkg/task}/ui/types.go | 2 +-
11 files changed, 23 insertions(+), 38 deletions(-)
delete mode 100644 cmd/survey/ui/style.go
rename {cmd/survey/tasks => pkg/task}/runner.go (80%)
rename {cmd/survey/tasks => pkg/task}/task.go (97%)
rename {cmd/survey/tasks => pkg/task}/types.go (96%)
rename {cmd/survey => pkg/task}/ui/help.go (97%)
rename {cmd/survey => pkg/task}/ui/questionnaire.go (88%)
rename {cmd/survey => pkg/task}/ui/task.go (99%)
rename {cmd/survey => pkg/task}/ui/types.go (96%)
diff --git a/cmd/survey/init.go b/cmd/survey/init.go
index 84f7609..772af6f 100644
--- a/cmd/survey/init.go
+++ b/cmd/survey/init.go
@@ -6,6 +6,7 @@ import (
"github.com/LazyBachelor/LazyPM/cmd/survey/tasks"
"github.com/LazyBachelor/LazyPM/internal/service"
"github.com/LazyBachelor/LazyPM/pkg/cli/repl"
+ "github.com/LazyBachelor/LazyPM/pkg/task"
"github.com/LazyBachelor/LazyPM/pkg/tui"
"github.com/LazyBachelor/LazyPM/pkg/web"
)
@@ -20,13 +21,13 @@ func initializeServices(ctx context.Context) (*service.Services, func(), error)
return service.NewServices(ctx, config)
}
-func initTasks() []*tasks.Task {
- return []*tasks.Task{
+func initTasks() []*task.Task {
+ return []*task.Task{
tasks.NewCreateIssueTask(),
tasks.NewCreateIssueTask(),
}
}
-func initInterfaces() []tasks.Interface {
- return []tasks.Interface{repl.NewRepl(), tui.NewTui(), web.NewWeb()}
+func initInterfaces() []task.Interface {
+ return []task.Interface{repl.NewRepl(), tui.NewTui(), web.NewWeb()}
}
diff --git a/cmd/survey/survey.go b/cmd/survey/survey.go
index a270790..64e36e2 100644
--- a/cmd/survey/survey.go
+++ b/cmd/survey/survey.go
@@ -5,14 +5,12 @@ import (
"fmt"
"log"
"math/rand"
- "time"
- "github.com/LazyBachelor/LazyPM/cmd/survey/tasks"
"github.com/LazyBachelor/LazyPM/internal/service"
+ "github.com/LazyBachelor/LazyPM/pkg/task"
)
func main() {
- rand.Seed(time.Now().UnixNano())
ctx := context.Background()
svc, close, err := initializeServices(ctx)
@@ -29,7 +27,7 @@ func main() {
}
}
-func taskLoop(ctx context.Context, svc *service.Services, surveyTasks []*tasks.Task, interfaces []tasks.Interface) error {
+func taskLoop(ctx context.Context, svc *service.Services, surveyTasks []*task.Task, interfaces []task.Interface) error {
interfaceIndex := rand.Int() % len(interfaces)
for _, task := range surveyTasks {
diff --git a/cmd/survey/tasks/createIssue.go b/cmd/survey/tasks/createIssue.go
index 9f0d11d..9a9a2d4 100644
--- a/cmd/survey/tasks/createIssue.go
+++ b/cmd/survey/tasks/createIssue.go
@@ -4,25 +4,26 @@ import (
"context"
"errors"
- "github.com/LazyBachelor/LazyPM/cmd/survey/ui"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/LazyBachelor/LazyPM/internal/service"
+ "github.com/LazyBachelor/LazyPM/pkg/task"
+ ui "github.com/LazyBachelor/LazyPM/pkg/task/ui"
"github.com/charmbracelet/huh"
)
-func NewCreateIssueTask() *Task {
+func NewCreateIssueTask() *task.Task {
aboutScreen := ui.NewTaskModel(createIssueDetails())
questionnaire := ui.NewQuestionnaireModel(createIssueQuestionnaire())
- task := NewTask(aboutScreen, questionnaire)
+ task := task.NewTask(aboutScreen, questionnaire)
task.SetConfigFunc(createIssueConfig)
task.SetDbStateFunc(createIssueDbState)
task.SetValidateFunc(createIssueValidate)
return task
}
-func createIssueConfig() TaskConfig {
- return TaskConfig{
+func createIssueConfig() task.TaskConfig {
+ return task.TaskConfig{
IssuePrefix: "pm",
BeadsDBPath: "./.pm/db.db",
StatisticsStoragePath: "./.pm/task-1-stats.json",
diff --git a/cmd/survey/ui/style.go b/cmd/survey/ui/style.go
deleted file mode 100644
index 9a2b4f4..0000000
--- a/cmd/survey/ui/style.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package ui
-
-import (
- "github.com/charmbracelet/huh"
- "github.com/charmbracelet/lipgloss"
-)
-
-func theme() *huh.Theme {
- theme := huh.ThemeBase16()
-
- theme.Focused.Base = lipgloss.NewStyle().Align(lipgloss.Center)
-
- return theme
-}
diff --git a/cmd/survey/tasks/runner.go b/pkg/task/runner.go
similarity index 80%
rename from cmd/survey/tasks/runner.go
rename to pkg/task/runner.go
index 2e8c89f..9ad8b0a 100644
--- a/cmd/survey/tasks/runner.go
+++ b/pkg/task/runner.go
@@ -1,10 +1,9 @@
-package tasks
+package task
import (
"context"
"fmt"
- "github.com/LazyBachelor/LazyPM/internal/service"
tea "github.com/charmbracelet/bubbletea"
)
@@ -16,7 +15,7 @@ func (t *Task) IntroduceTask() error {
return err
}
-func (t *Task) StartInterface(ctx context.Context, cfg service.Config) error {
+func (t *Task) StartInterface(ctx context.Context, cfg TaskConfig) error {
if t.interfaceType == nil {
return fmt.Errorf("interfaceType is not set")
}
diff --git a/cmd/survey/tasks/task.go b/pkg/task/task.go
similarity index 97%
rename from cmd/survey/tasks/task.go
rename to pkg/task/task.go
index 9e6553b..2a9e548 100644
--- a/cmd/survey/tasks/task.go
+++ b/pkg/task/task.go
@@ -1,5 +1,4 @@
-// task.go
-package tasks
+package task
import (
"context"
diff --git a/cmd/survey/tasks/types.go b/pkg/task/types.go
similarity index 96%
rename from cmd/survey/tasks/types.go
rename to pkg/task/types.go
index 74af23c..90427d5 100644
--- a/cmd/survey/tasks/types.go
+++ b/pkg/task/types.go
@@ -1,4 +1,4 @@
-package tasks
+package task
import (
"context"
diff --git a/cmd/survey/ui/help.go b/pkg/task/ui/help.go
similarity index 97%
rename from cmd/survey/ui/help.go
rename to pkg/task/ui/help.go
index 15b58a6..ebc31e4 100644
--- a/cmd/survey/ui/help.go
+++ b/pkg/task/ui/help.go
@@ -1,4 +1,4 @@
-package ui
+package taskui
import "github.com/charmbracelet/bubbles/key"
diff --git a/cmd/survey/ui/questionnaire.go b/pkg/task/ui/questionnaire.go
similarity index 88%
rename from cmd/survey/ui/questionnaire.go
rename to pkg/task/ui/questionnaire.go
index 9849a60..e7dc01d 100644
--- a/cmd/survey/ui/questionnaire.go
+++ b/pkg/task/ui/questionnaire.go
@@ -1,14 +1,15 @@
-package ui
+package taskui
import (
"charm.land/lipgloss/v2"
+ "github.com/LazyBachelor/LazyPM/internal/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
)
func NewQuestionnaireModel(questions Questions) *QuestionnaireModel {
form := huh.NewForm(questions...).
- WithTheme(theme()).WithLayout(huh.LayoutGrid(1, 1))
+ WithTheme(style.HuhCenterTheme()).WithLayout(huh.LayoutGrid(1, 1))
return &QuestionnaireModel{
Questions: questions,
diff --git a/cmd/survey/ui/task.go b/pkg/task/ui/task.go
similarity index 99%
rename from cmd/survey/ui/task.go
rename to pkg/task/ui/task.go
index 5e5314e..81da4e6 100644
--- a/cmd/survey/ui/task.go
+++ b/pkg/task/ui/task.go
@@ -1,4 +1,4 @@
-package ui
+package taskui
import (
"fmt"
diff --git a/cmd/survey/ui/types.go b/pkg/task/ui/types.go
similarity index 96%
rename from cmd/survey/ui/types.go
rename to pkg/task/ui/types.go
index ec382be..dc6a47e 100644
--- a/cmd/survey/ui/types.go
+++ b/pkg/task/ui/types.go
@@ -1,4 +1,4 @@
-package ui
+package taskui
import (
"github.com/charmbracelet/bubbles/help"
From b1a72a5a2ef092a1bfa4b18bc8d8bd015fcdaedc Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 10:14:05 +0100
Subject: [PATCH 26/46] add global style package
---
internal/style/styles.go | 28 ++++++++++++++++++++++++++++
internal/style/themes.go | 14 ++++++++++++++
2 files changed, 42 insertions(+)
create mode 100644 internal/style/styles.go
create mode 100644 internal/style/themes.go
diff --git a/internal/style/styles.go b/internal/style/styles.go
new file mode 100644
index 0000000..7dd47c8
--- /dev/null
+++ b/internal/style/styles.go
@@ -0,0 +1,28 @@
+package style
+
+import "github.com/charmbracelet/lipgloss"
+
+// Color palette
+var (
+ PrimaryColor = lipgloss.AdaptiveColor{Light: "#007acc", Dark: "#1e90ff"}
+ SecondaryColor = lipgloss.AdaptiveColor{Light: "#ff6f61", Dark: "#ff6347"}
+ AccentColor = lipgloss.AdaptiveColor{Light: "#6a5acd", Dark: "#9370db"}
+ Background = lipgloss.AdaptiveColor{Light: "#ffffff", Dark: "#1e1e1e"}
+ TextColor = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#ffffff"}
+)
+
+var (
+ AppStyle = lipgloss.NewStyle().Padding(1, 2).Background(Background).Foreground(TextColor)
+)
+
+var (
+ DefaultBorder = lipgloss.NormalBorder()
+ BorderStyle = lipgloss.NewStyle().Border(DefaultBorder).BorderForeground(PrimaryColor)
+)
+
+var (
+ TitleStyle = lipgloss.NewStyle().Foreground(PrimaryColor).Bold(true)
+ DescriptionStyle = lipgloss.NewStyle().Foreground(TextColor).Italic(true)
+ DetailStyle = lipgloss.NewStyle().Foreground(SecondaryColor)
+ HelpStyle = lipgloss.NewStyle().Foreground(AccentColor)
+)
diff --git a/internal/style/themes.go b/internal/style/themes.go
new file mode 100644
index 0000000..c6861bd
--- /dev/null
+++ b/internal/style/themes.go
@@ -0,0 +1,14 @@
+package style
+
+import (
+ "github.com/charmbracelet/huh"
+ "github.com/charmbracelet/lipgloss"
+)
+
+func HuhCenterTheme() *huh.Theme {
+ theme := huh.ThemeBase16()
+
+ theme.Focused.Base = lipgloss.NewStyle().Align(lipgloss.Center)
+
+ return theme
+}
From 400a989454d1f0f826ff536cb3fcacdbe6c39a55 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 10:16:43 +0100
Subject: [PATCH 27/46] fix typo
---
pkg/task/ui/task.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/task/ui/task.go b/pkg/task/ui/task.go
index 81da4e6..c416ad3 100644
--- a/pkg/task/ui/task.go
+++ b/pkg/task/ui/task.go
@@ -50,7 +50,7 @@ func (m TaskModel) View() string {
Width(m.width).Align(lipgloss.Center).
Render(m.help.View(m.keys))
- helpHeigh := lipgloss.Height(helpView)
+ helpHeight := lipgloss.Height(helpView)
detailsText := fmt.Sprintf("Time to complete: %s | Difficulty: %s", m.TimeToComplete, m.Difficulty)
details := lipgloss.NewStyle().Align(lipgloss.Center).
@@ -59,7 +59,7 @@ func (m TaskModel) View() string {
detailsHeight := lipgloss.Height(details)
content := lipgloss.NewStyle().
- Width(m.width).Height(m.height-headerHeight-helpHeigh-detailsHeight).
+ Width(m.width).Height(m.height-headerHeight-helpHeight-detailsHeight).
Align(lipgloss.Center, lipgloss.Center).
Render(m.Description)
From c3181e0f777eedbfbc47edcec6fbe1e77df7d359 Mon Sep 17 00:00:00 2001
From: Robin Olsen <129996395+Telikz@users.noreply.github.com>
Date: Fri, 13 Feb 2026 10:23:57 +0100
Subject: [PATCH 28/46] Update cmd/survey/init.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
cmd/survey/init.go | 1 -
1 file changed, 1 deletion(-)
diff --git a/cmd/survey/init.go b/cmd/survey/init.go
index 772af6f..533976b 100644
--- a/cmd/survey/init.go
+++ b/cmd/survey/init.go
@@ -24,7 +24,6 @@ func initializeServices(ctx context.Context) (*service.Services, func(), error)
func initTasks() []*task.Task {
return []*task.Task{
tasks.NewCreateIssueTask(),
- tasks.NewCreateIssueTask(),
}
}
From f396ae5b37b280c6b229d108832ba92ac9617db7 Mon Sep 17 00:00:00 2001
From: Robin Olsen <129996395+Telikz@users.noreply.github.com>
Date: Fri, 13 Feb 2026 10:25:52 +0100
Subject: [PATCH 29/46] Update pkg/web/web.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
pkg/web/web.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/web/web.go b/pkg/web/web.go
index f38215c..8c85f36 100644
--- a/pkg/web/web.go
+++ b/pkg/web/web.go
@@ -20,7 +20,7 @@ func NewWeb() *Web {
//go:embed assets/*
var assets embed.FS
-func (w Web) Run(ctx context.Context, config WebConfig) error {
+func (w *Web) Run(ctx context.Context, config WebConfig) error {
svc, cleanup, err := service.NewServices(ctx, config)
if err != nil {
return err
From 54c0845f2848a92cb1eb686fdfc015f3f22dea9f Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 13:38:54 +0100
Subject: [PATCH 30/46] handles routes in routes
---
pkg/web/handler/handler.go | 21 ------------------
pkg/web/handler/pages.go | 44 --------------------------------------
2 files changed, 65 deletions(-)
delete mode 100644 pkg/web/handler/handler.go
delete mode 100644 pkg/web/handler/pages.go
diff --git a/pkg/web/handler/handler.go b/pkg/web/handler/handler.go
deleted file mode 100644
index 597333c..0000000
--- a/pkg/web/handler/handler.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package handler
-
-import (
- "net/http"
-
- "github.com/LazyBachelor/LazyPM/internal/service"
-)
-
-type Route struct {
- Pattern string
- Handler http.Handler
-}
-
-func GetRoutes(svc *service.Services) []Route {
- var routes []Route
-
- routes = append(routes, PagesRoutes(svc)...)
- routes = append(routes, IssuesRoutes(svc)...)
-
- return routes
-}
diff --git a/pkg/web/handler/pages.go b/pkg/web/handler/pages.go
deleted file mode 100644
index 82c6c63..0000000
--- a/pkg/web/handler/pages.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package handler
-
-import (
- "net/http"
-
- "github.com/LazyBachelor/LazyPM/internal/service"
- "github.com/LazyBachelor/LazyPM/pkg/web/components"
- "github.com/LazyBachelor/LazyPM/pkg/web/routes"
-)
-
-func PagesRoutes(svc *service.Services) []Route {
- return []Route{
- {Pattern: "/", Handler: IndexHandler(svc)},
- }
-}
-
-func IndexHandler(svc *service.Services) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
-
- if r.URL.Path != "/" {
- handleNotFound(w, r)
- return
- }
-
- issues, err := svc.Beads.AllIssues(r.Context())
-
- if err != nil {
- http.Error(w, "Failed to retrieve issues",
- http.StatusInternalServerError)
- return
- }
-
- props := routes.IndexProps{
- IssueTable: components.IssueTableProps{
- Issues: issues,
- },
- }
- routes.Index(props).Render(r.Context(), w)
- }
-}
-
-func handleNotFound(w http.ResponseWriter, _ *http.Request) {
- http.Error(w, "Page not found", http.StatusNotFound)
-}
From f13d361a7c049f332c33e4634998a8024f163496 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 13:39:11 +0100
Subject: [PATCH 31/46] use chi as router
---
pkg/web/server/routes.go | 40 ++++++++++++++++++++++++++++------------
1 file changed, 28 insertions(+), 12 deletions(-)
diff --git a/pkg/web/server/routes.go b/pkg/web/server/routes.go
index 0911f81..a3f43b3 100644
--- a/pkg/web/server/routes.go
+++ b/pkg/web/server/routes.go
@@ -8,29 +8,45 @@ import (
"github.com/LazyBachelor/LazyPM/pkg/web/handler"
"github.com/NYTimes/gziphandler"
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/chi/v5/middleware"
"github.com/rs/cors"
)
func (s *Server) RegisterRoutes(assets embed.FS) http.Handler {
- mux := http.NewServeMux()
+ r := chi.NewRouter()
- s.handleAssets(mux, assets)
+ r.Use(middleware.Logger)
+ r.Use(middleware.Recoverer)
+ r.Use(cors.AllowAll().Handler)
+ r.Use(middleware.CleanPath)
- for _, route := range handler.GetRoutes(s.Services) {
- mux.Handle(route.Pattern, route.Handler)
- }
+ r.Use(handler.HTMXMiddleware)
+ r.Use(handler.ServicesMiddleware(s.Services))
- handler := cors.Default().Handler(mux)
+ s.handleAssets(r, assets)
- return gziphandler.GzipHandler(handler)
+ r.Get("/", handler.IndexHandler)
+
+ r.Route("/issues", func(r chi.Router) {
+ r.Get("/", handler.ListIssues)
+ r.Post("/", handler.CreateIssue)
+
+ r.Route("/{id}", func(r chi.Router) {
+ r.Use(handler.IssueCtx)
+ r.Get("/", handler.GetIssue)
+ r.Put("/", handler.UpdateIssue)
+ r.Delete("/", handler.DeleteIssue)
+ })
+ })
+ return gziphandler.GzipHandler(r)
}
-func (s *Server) handleAssets(mux *http.ServeMux, assets embed.FS) {
- mux.Handle("/assets/",
- http.StripPrefix("/assets/",
- http.FileServer(http.Dir("pkg/web/assets"))))
+func (s *Server) handleAssets(r chi.Router, assets embed.FS) {
+ r.Handle("/assets/*", http.StripPrefix("/assets/",
+ http.FileServer(http.Dir("pkg/web/assets"))))
- mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
+ r.HandleFunc("GET /robots.txt", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
http.ServeContent(w, r, "robots.txt", time.Now(), strings.NewReader("User-agent: *\nAllow: /"))
})
From b4fb4ad0666e6758ca0c74059f3bdf7bc60527df Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 13:39:32 +0100
Subject: [PATCH 32/46] add services and htmx middleware
---
pkg/web/handler/context.go | 42 ++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
create mode 100644 pkg/web/handler/context.go
diff --git a/pkg/web/handler/context.go b/pkg/web/handler/context.go
new file mode 100644
index 0000000..bdb1b51
--- /dev/null
+++ b/pkg/web/handler/context.go
@@ -0,0 +1,42 @@
+package handler
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/LazyBachelor/LazyPM/internal/service"
+ "github.com/donseba/go-htmx"
+)
+
+type contextKey string
+
+const (
+ servicesKey contextKey = "services"
+ htmxKey contextKey = "htmx"
+)
+
+func ServicesMiddleware(svc *service.Services) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ctx := context.WithValue(r.Context(), servicesKey, svc)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+ }
+}
+
+func HTMXMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ htmxInstance := htmx.New()
+ handler := htmxInstance.NewHandler(w, r)
+ ctx := context.WithValue(r.Context(), htmxKey, handler)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+}
+
+func Services(r *http.Request) *service.Services {
+ return r.Context().Value(servicesKey).(*service.Services)
+}
+
+func HTMX(r *http.Request) *htmx.Handler {
+ return r.Context().Value(htmxKey).(*htmx.Handler)
+}
From 8728b321abac752fb5f0b058ebf25f61c520b5b1 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 13:40:00 +0100
Subject: [PATCH 33/46] rename to index and use new middlewares and chi
---
pkg/web/handler/index.go | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 pkg/web/handler/index.go
diff --git a/pkg/web/handler/index.go b/pkg/web/handler/index.go
new file mode 100644
index 0000000..6cec171
--- /dev/null
+++ b/pkg/web/handler/index.go
@@ -0,0 +1,26 @@
+package handler
+
+import (
+ "net/http"
+
+ "github.com/LazyBachelor/LazyPM/pkg/web/components"
+ "github.com/LazyBachelor/LazyPM/pkg/web/routes"
+)
+
+func IndexHandler(w http.ResponseWriter, r *http.Request) {
+ svc := Services(r)
+
+ issues, err := svc.Beads.AllIssues(r.Context())
+ if err != nil {
+ http.Error(w, "failed to retrieve issues", http.StatusInternalServerError)
+ return
+ }
+
+ props := routes.IndexProps{
+ IssueTable: components.IssueTableProps{
+ Issues: issues,
+ },
+ }
+
+ routes.Index(props).Render(r.Context(), w)
+}
From 029b9cbf69c232bb463807fe1a3200901144e6bc Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 13:40:16 +0100
Subject: [PATCH 34/46] make generic form parser
---
pkg/web/handler/forms.go | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 pkg/web/handler/forms.go
diff --git a/pkg/web/handler/forms.go b/pkg/web/handler/forms.go
new file mode 100644
index 0000000..960464a
--- /dev/null
+++ b/pkg/web/handler/forms.go
@@ -0,0 +1,30 @@
+package handler
+
+import (
+ "net/http"
+
+ "github.com/go-playground/form/v4"
+ "github.com/go-playground/validator/v10"
+)
+
+var (
+ decoder = form.NewDecoder()
+ validate = validator.New(validator.WithRequiredStructEnabled())
+)
+
+func ParseForm[T any](r *http.Request) (*T, error) {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
+
+ var data T
+ if err := decoder.Decode(&data, r.PostForm); err != nil {
+ return nil, err
+ }
+
+ return &data, nil
+}
+
+func ValidateForm[T any](data *T) error {
+ return validate.Struct(data)
+}
From 912bbdbacdcadb0244600be2f5ab0e74d814b23b Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 13:40:30 +0100
Subject: [PATCH 35/46] add issues endpoints
---
pkg/web/handler/issues.go | 170 ++++++++++++++++++++++++++------------
1 file changed, 116 insertions(+), 54 deletions(-)
diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go
index bbe2fe0..a9bb23a 100644
--- a/pkg/web/handler/issues.go
+++ b/pkg/web/handler/issues.go
@@ -1,72 +1,134 @@
package handler
import (
- "encoding/json"
- "fmt"
+ "context"
"net/http"
"github.com/LazyBachelor/LazyPM/internal/models"
- "github.com/LazyBachelor/LazyPM/internal/service"
+ "github.com/go-chi/chi/v5"
)
-func IssuesRoutes(svc *service.Services) []Route {
- return []Route{
- {Pattern: "/issues", Handler: GetAllIssues(svc)},
- {Pattern: "POST /create-issue", Handler: CreateIssue(svc)},
+type IssueForm struct {
+ Title string `form:"title" validate:"required,max=255"`
+ Description string `form:"description" validate:"required,max=2000"`
+ Status models.Status `form:"status" validate:"required,oneof=open in_progress closed"`
+ IssueType models.IssueType `form:"issue_type" validate:"required,oneof=task bug feature chore"`
+ Priority int `form:"priority" validate:"gte=0,lte=4"`
+}
+
+func (f *IssueForm) ToIssue() models.Issue {
+ return models.Issue{
+ Title: f.Title,
+ Description: f.Description,
+ Status: f.Status,
+ IssueType: f.IssueType,
+ Priority: f.Priority,
}
}
-func CreateIssue(svc *service.Services) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
+func CreateIssue(w http.ResponseWriter, r *http.Request) {
+ svc := Services(r)
+ hx := HTMX(r)
- // 1. Parse Form instead of JSON
- if err := r.ParseForm(); err != nil {
- http.Error(w, "Failed to parse form", http.StatusBadRequest)
- return
- }
-
- // 2. Map form values to your struct manually
- // (Or use a library like 'gorilla/schema')
- issue := models.Issue{
- Title: r.FormValue("title"),
- Description: r.FormValue("description"),
- Status: models.Status(r.FormValue("status")),
- IssueType: models.IssueType(r.FormValue("issue_type")),
- }
-
- err := svc.Beads.CreateIssue(r.Context(), &issue, "")
- if err != nil {
- http.Error(w, "Failed to create issue: "+err.Error(), http.StatusInternalServerError)
- return
- }
-
- // 3. HTMX usually expects HTML back, not JSON
- w.Header().Set("Content-Type", "text/html")
- fmt.Fprintf(w, "Created issue: %s
", issue.Title)
+ form, err := ParseForm[IssueForm](r)
+ if err != nil {
+ http.Error(w, "Failed to parse form", http.StatusBadRequest)
+ return
}
+
+ if err := ValidateForm(form); err != nil {
+ w.WriteHeader(http.StatusUnprocessableEntity)
+ if hx.IsHxRequest() {
+ hx.WriteString("Please fix the form errors
")
+ } else {
+ hx.WriteJSON(map[string]interface{}{"error": err.Error()})
+ }
+ return
+ }
+
+ issue := form.ToIssue()
+ if err := svc.Beads.CreateIssue(r.Context(), &issue, ""); err != nil {
+ http.Error(w, "Failed to create issue: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ if hx.IsHxRequest() {
+ hx.WriteString("Issue created successfully
")
+ return
+ }
+
+ hx.WriteJSON(map[string]any{
+ "title": issue.Title,
+ "status": issue.Status,
+ })
}
-func GetAllIssues(svc *service.Services) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- issues, err := svc.Beads.AllIssues(r.Context())
+func ListIssues(w http.ResponseWriter, r *http.Request) {
+ svc := Services(r)
+ hx := HTMX(r)
- if err != nil {
- http.Error(w, "Failed to retrieve issues", http.StatusInternalServerError)
- return
- }
-
- jsonData, err := json.Marshal(issues)
-
- if err != nil {
- http.Error(w, "Failed to marshal issues", http.StatusInternalServerError)
- return
- }
-
- w.Header().Set("Content-Type", "application/json")
- w.Write(jsonData)
+ issues, err := svc.Beads.AllIssues(r.Context())
+ if err != nil {
+ http.Error(w, "Failed to retrieve issues", http.StatusInternalServerError)
+ return
}
+
+ hx.WriteJSON(issues)
+}
+
+func IssueCtx(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ svc := Services(r)
+
+ id := chi.URLParam(r, "id")
+ issue, err := svc.Beads.GetIssue(r.Context(), id)
+ if err != nil {
+ http.Error(w, "Issue not found", http.StatusNotFound)
+ return
+ }
+
+ ctx := context.WithValue(r.Context(), "issue", issue)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+
+}
+
+func GetIssue(w http.ResponseWriter, r *http.Request) {
+ issue := r.Context().Value("issue").(*models.Issue)
+ hx := HTMX(r)
+
+ hx.WriteJSON(issue)
+}
+
+func UpdateIssue(w http.ResponseWriter, r *http.Request) {
+ issue := r.Context().Value("issue").(*models.Issue)
+ svc := Services(r)
+ hx := HTMX(r)
+
+ changes := make(map[string]any)
+
+ if err := svc.Beads.UpdateIssue(r.Context(), issue.ID, changes, ""); err != nil {
+ http.Error(w, "Failed to update issue", http.StatusInternalServerError)
+ return
+ }
+
+ issue, err := svc.Beads.GetIssue(r.Context(), issue.ID)
+ if err != nil {
+ http.Error(w, "Failed to retrieve updated issue", http.StatusInternalServerError)
+ return
+ }
+
+ hx.WriteJSON(issue)
+}
+
+func DeleteIssue(w http.ResponseWriter, r *http.Request) {
+ issue := r.Context().Value("issue").(*models.Issue)
+
+ svc := Services(r)
+ if err := svc.Beads.DeleteIssue(r.Context(), issue.ID); err != nil {
+ http.Error(w, "Failed to delete issue", http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
}
From cc1f84a4bda02cbafbf93b78d849aaf8bb846643 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 13:40:41 +0100
Subject: [PATCH 36/46] uprate deps
---
go.mod | 9 +++++++++
go.sum | 18 ++++++++++++++++++
2 files changed, 27 insertions(+)
diff --git a/go.mod b/go.mod
index 521c31d..40e1777 100644
--- a/go.mod
+++ b/go.mod
@@ -47,13 +47,21 @@ require (
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/donseba/go-htmx v1.12.1 // indirect
github.com/dustin/go-humanize v1.0.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/go-chi/chi/v5 v5.2.5 // indirect
+ github.com/go-playground/form/v4 v4.3.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320 // indirect
github.com/inconshreveable/mousetrap v1.1.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
@@ -84,6 +92,7 @@ require (
github.com/tetratelabs/wazero v1.11.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/crypto v0.48.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
diff --git a/go.sum b/go.sum
index d774a58..021bd89 100644
--- a/go.sum
+++ b/go.sum
@@ -70,6 +70,8 @@ github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/donseba/go-htmx v1.12.1 h1:ZO9TWLyZYN3KL2s/N3ZasCf/B3dmX1xzmZoIkJIT+C0=
+github.com/donseba/go-htmx v1.12.1/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -80,6 +82,18 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
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/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-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk=
+github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
+github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -96,6 +110,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -187,6 +203,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
+golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
From aaf8eacf3cd15233386fefa8333d158b37413d50 Mon Sep 17 00:00:00 2001
From: Robin Olsen
Date: Fri, 13 Feb 2026 13:41:24 +0100
Subject: [PATCH 37/46] add some margins to the index page resize form
---
pkg/web/assets/css/styles.css | 1238 ++++++++++++++++++++++++
pkg/web/components/base/input.templ | 3 +-
pkg/web/components/base/input_templ.go | 27 +-
pkg/web/components/base/range.templ | 2 +-
pkg/web/components/base/range_templ.go | 203 +---
pkg/web/components/issue.templ | 5 +-
pkg/web/components/issue_templ.go | 15 +-
pkg/web/routes/index.templ | 12 +-
pkg/web/routes/index_templ.go | 12 +-
9 files changed, 1286 insertions(+), 231 deletions(-)
diff --git a/pkg/web/assets/css/styles.css b/pkg/web/assets/css/styles.css
index f70a7c6..ccc6fa8 100644
--- a/pkg/web/assets/css/styles.css
+++ b/pkg/web/assets/css/styles.css
@@ -7,12 +7,31 @@
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
+ --color-green-100: oklch(96.2% 0.044 156.743);
+ --color-green-800: oklch(44.8% 0.119 151.328);
+ --color-teal-700: oklch(51.1% 0.096 186.391);
+ --color-gray-500: oklch(55.1% 0.027 264.364);
+ --color-gray-600: oklch(44.6% 0.03 256.802);
+ --color-gray-800: oklch(27.8% 0.033 256.848);
--color-black: #000;
+ --color-white: #fff;
--spacing: 0.25rem;
+ --container-xs: 20rem;
+ --container-lg: 32rem;
+ --text-xs: 0.75rem;
+ --text-xs--line-height: calc(1 / 0.75);
+ --text-sm: 0.875rem;
+ --text-sm--line-height: calc(1.25 / 0.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-semibold: 600;
+ --font-weight-bold: 700;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--default-transition-duration: 150ms;
@@ -170,6 +189,305 @@
}
}
@layer utilities {
+ .diff {
+ @layer daisyui.l1.l2.l3 {
+ position: relative;
+ display: grid;
+ width: 100%;
+ overflow: hidden;
+ webkit-user-select: none;
+ user-select: none;
+ grid-template-rows: 1fr 1.8rem 1fr;
+ direction: ltr;
+ container-type: inline-size;
+ grid-template-columns: auto 1fr;
+ &:focus-visible, &:has(.diff-item-1:focus-visible) {
+ outline-style: var(--tw-outline-style);
+ outline-width: 2px;
+ outline-offset: 1px;
+ outline-color: var(--color-base-content);
+ }
+ &:focus-visible {
+ outline-style: var(--tw-outline-style);
+ outline-width: 2px;
+ outline-offset: 1px;
+ outline-color: var(--color-base-content);
+ .diff-resizer {
+ min-width: 95cqi;
+ max-width: 95cqi;
+ }
+ }
+ &:has(.diff-item-1:focus-visible) {
+ outline-style: var(--tw-outline-style);
+ outline-width: 2px;
+ outline-offset: 1px;
+ .diff-resizer {
+ min-width: 5cqi;
+ max-width: 5cqi;
+ }
+ }
+ @supports (-webkit-overflow-scrolling: touch) and (overflow: -webkit-paged-x) {
+ &:focus {
+ .diff-resizer {
+ min-width: 5cqi;
+ max-width: 5cqi;
+ }
+ }
+ &:has(.diff-item-1:focus) {
+ .diff-resizer {
+ min-width: 95cqi;
+ max-width: 95cqi;
+ }
+ }
+ }
+ }
+ }
+ .fab {
+ @layer daisyui.l1.l2.l3 {
+ pointer-events: none;
+ position: fixed;
+ inset-inline-end: calc(0.25rem * 4);
+ bottom: calc(0.25rem * 4);
+ z-index: 999;
+ display: flex;
+ flex-direction: column-reverse;
+ align-items: flex-end;
+ gap: calc(0.25rem * 2);
+ font-size: var(--text-sm);
+ line-height: var(--tw-leading, var(--text-sm--line-height));
+ white-space: nowrap;
+ > * {
+ pointer-events: auto;
+ display: flex;
+ align-items: center;
+ gap: calc(0.25rem * 2);
+ &:hover, &:has(:focus-visible) {
+ z-index: 1;
+ }
+ }
+ > [tabindex] {
+ &:first-child {
+ position: relative;
+ display: grid;
+ transition-property: opacity, visibility, rotate;
+ transition-duration: 0.2s;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ }
+ .fab-close {
+ position: absolute;
+ inset-inline-end: calc(0.25rem * 0);
+ bottom: calc(0.25rem * 0);
+ }
+ .fab-main-action {
+ position: absolute;
+ inset-inline-end: calc(0.25rem * 0);
+ bottom: calc(0.25rem * 0);
+ }
+ &:focus-within {
+ &:has(.fab-close), &:has(.fab-main-action) {
+ > [tabindex] {
+ rotate: 90deg;
+ opacity: 0%;
+ }
+ }
+ > [tabindex]:first-child {
+ pointer-events: none;
+ }
+ > :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: 100%;
+ }
+ }
+ > :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: 0.2s;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ &.fab-main-action, &.fab-close {
+ --tw-scale-x: 100%;
+ --tw-scale-y: 100%;
+ --tw-scale-z: 100%;
+ scale: var(--tw-scale-x) var(--tw-scale-y);
+ }
+ }
+ > :nth-child(3) {
+ transition-delay: 30ms;
+ }
+ > :nth-child(4) {
+ transition-delay: 60ms;
+ }
+ > :nth-child(5) {
+ transition-delay: 90ms;
+ }
+ > :nth-child(6) {
+ transition-delay: 120ms;
+ }
+ }
+ }
+ .tooltip {
+ @layer daisyui.l1.l2.l3 {
+ position: relative;
+ display: inline-block;
+ --tt-bg: var(--color-neutral);
+ --tt-off: calc(100% + 0.5rem);
+ --tt-tail: calc(100% + 1px + 0.25rem);
+ & > .tooltip-content, &[data-tip]:before {
+ position: absolute;
+ max-width: 20rem;
+ border-radius: var(--radius-field);
+ padding-inline: calc(0.25rem * 2);
+ padding-block: calc(0.25rem * 1);
+ text-align: center;
+ white-space: normal;
+ color: var(--color-neutral-content);
+ opacity: 0%;
+ font-size: 0.875rem;
+ line-height: 1.25;
+ background-color: var(--tt-bg);
+ width: max-content;
+ pointer-events: none;
+ z-index: 2;
+ --tw-content: attr(data-tip);
+ content: var(--tw-content);
+ }
+ &:after {
+ opacity: 0%;
+ background-color: var(--tt-bg);
+ content: "";
+ pointer-events: none;
+ width: 0.625rem;
+ height: 0.25rem;
+ display: block;
+ position: absolute;
+ mask-repeat: no-repeat;
+ mask-position: -1px 0;
+ --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");
+ mask-image: var(--mask-tooltip);
+ }
+ @media (prefers-reduced-motion: no-preference) {
+ & > .tooltip-content, &[data-tip]:before, &:after {
+ transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms;
+ }
+ }
+ &:is([data-tip]:not([data-tip=""]), :has(.tooltip-content:not(:empty))) {
+ &.tooltip-open, &:hover, &:has(:focus-visible) {
+ & > .tooltip-content, &[data-tip]:before, &:after {
+ opacity: 100%;
+ --tt-pos: 0rem;
+ @media (prefers-reduced-motion: no-preference) {
+ transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s;
+ }
+ }
+ }
+ }
+ }
+ @layer daisyui.l1.l2 {
+ > .tooltip-content, &[data-tip]:before {
+ transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem));
+ inset: auto auto var(--tt-off) 50%;
+ }
+ &:after {
+ transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem));
+ inset: auto auto var(--tt-tail) 50%;
+ }
+ }
+ }
+ .tab {
+ @layer daisyui.l1.l2.l3 {
+ position: relative;
+ display: inline-flex;
+ cursor: pointer;
+ appearance: none;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ webkit-user-select: none;
+ user-select: none;
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-base-content);
+ }
+ }
+ --tab-p: 0.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(0.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) + 0.25px),
+ var(--tab-border-color) 69%,
+ var(--tab-bg) calc(69% + 0.25px);
+ border-color: #0000;
+ order: var(--tab-order);
+ height: var(--tab-height);
+ font-size: 0.875rem;
+ padding-inline: var(--tab-p);
+ &:is(input[type="radio"]) {
+ min-width: fit-content;
+ &:after {
+ --tw-content: attr(aria-label);
+ content: var(--tw-content);
+ }
+ }
+ &:is(label) {
+ position: relative;
+ input {
+ position: absolute;
+ inset: calc(0.25rem * 0);
+ cursor: pointer;
+ appearance: none;
+ opacity: 0%;
+ }
+ }
+ &:checked, &:is(label:has(:checked)), &:is(.tab-active, [aria-selected="true"], [aria-current="true"], [aria-current="page"]) {
+ & + .tab-content {
+ display: block;
+ }
+ }
+ &: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)) {
+ color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
+ }
+ }
+ &:not(input):empty {
+ flex-grow: 1;
+ cursor: default;
+ }
+ &:focus {
+ --tw-outline-style: none;
+ outline-style: none;
+ @media (forced-colors: active) {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ }
+ }
+ &:focus-visible, &:is(label:has(:checked:focus-visible)) {
+ outline: 2px solid currentColor;
+ outline-offset: -5px;
+ }
+ &[disabled] {
+ pointer-events: none;
+ opacity: 40%;
+ }
+ }
+ }
.menu {
@layer daisyui.l1.l2.l3 {
display: flex;
@@ -610,6 +928,20 @@
}
}
}
+ .loading {
+ @layer daisyui.l1.l2.l3 {
+ pointer-events: none;
+ display: inline-block;
+ aspect-ratio: 1 / 1;
+ background-color: currentcolor;
+ vertical-align: middle;
+ width: calc(var(--size-selector, 0.25rem) * 6);
+ mask-size: 100%;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ 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");
+ }
+ }
.collapse {
&:not(td, tr, colgroup) {
visibility: revert-layer;
@@ -776,6 +1108,36 @@
}
}
}
+ .validator-hint {
+ @layer daisyui.l1.l2.l3 {
+ visibility: hidden;
+ margin-top: calc(0.25rem * 2);
+ font-size: 0.75rem;
+ }
+ }
+ .validator {
+ @layer daisyui.l1.l2.l3 {
+ &:user-valid, &:has(:user-valid) {
+ &, &:focus, &:checked, &[aria-checked="true"], &:focus-within {
+ --input-color: var(--color-success);
+ }
+ }
+ &:user-invalid, &:has(:user-invalid), &[aria-invalid]:not([aria-invalid="false"]), &:has([aria-invalid]:not([aria-invalid="false"])) {
+ &, &:focus, &:checked, &[aria-checked="true"], &:focus-within {
+ --input-color: var(--color-error);
+ }
+ & ~ .validator-hint {
+ visibility: visible;
+ color: var(--color-error);
+ }
+ }
+ }
+ &:user-invalid, &:has(:user-invalid), &[aria-invalid]:not([aria-invalid="false"]), &:has([aria-invalid]:not([aria-invalid="false"])) {
+ & ~ .validator-hint {
+ display: revert-layer;
+ }
+ }
+ }
.collapse-open {
@layer daisyui.l1.l2 {
grid-template-rows: max-content 1fr;
@@ -795,6 +1157,65 @@
.visible {
visibility: visible;
}
+ .list {
+ @layer daisyui.l1.l2.l3 {
+ display: flex;
+ flex-direction: column;
+ font-size: 0.875rem;
+ .list-row {
+ --list-grid-cols: minmax(0, auto) 1fr;
+ position: relative;
+ display: grid;
+ grid-auto-flow: column;
+ gap: calc(0.25rem * 4);
+ border-radius: var(--radius-box);
+ padding: calc(0.25rem * 4);
+ word-break: break-word;
+ grid-template-columns: var(--list-grid-cols);
+ }
+ & > :not(:last-child) {
+ &.list-row, .list-row {
+ &:after {
+ content: "";
+ border-bottom: var(--border) solid;
+ inset-inline: var(--radius-box);
+ position: absolute;
+ bottom: calc(0.25rem * 0);
+ border-color: var(--color-base-content);
+ @supports (color: color-mix(in lab, red, red)) {
+ border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent);
+ }
+ }
+ }
+ }
+ }
+ @layer daisyui.l1.l2 {
+ .list-row {
+ &:has(.list-col-grow:nth-child(1)) {
+ --list-grid-cols: 1fr;
+ }
+ &:has(.list-col-grow:nth-child(2)) {
+ --list-grid-cols: minmax(0, auto) 1fr;
+ }
+ &:has(.list-col-grow:nth-child(3)) {
+ --list-grid-cols: minmax(0, auto) minmax(0, auto) 1fr;
+ }
+ &:has(.list-col-grow:nth-child(4)) {
+ --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr;
+ }
+ &:has(.list-col-grow:nth-child(5)) {
+ --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr;
+ }
+ &: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;
+ }
+ > * {
+ grid-row-start: 1;
+ }
+ }
+ }
+ }
.toast {
@layer daisyui.l1.l2.l3 {
position: fixed;
@@ -938,6 +1359,23 @@
}
}
}
+ .indicator {
+ @layer daisyui.l1.l2.l3 {
+ position: relative;
+ display: inline-flex;
+ width: max-content;
+ :where(.indicator-item) {
+ z-index: 1;
+ position: absolute;
+ 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%);
+ }
+ }
+ }
.table {
@layer daisyui.l1.l2.l3 {
font-size: 0.875rem;
@@ -1102,6 +1540,27 @@
}
}
}
+ .diff-resizer {
+ @layer daisyui.l1.l2.l3 {
+ position: relative;
+ isolation: isolate;
+ z-index: 2;
+ grid-column-start: 1;
+ grid-row-start: 2;
+ height: calc(0.25rem * 3);
+ width: 50cqi;
+ max-width: calc(100cqi - 1rem);
+ min-width: 1rem;
+ resize: horizontal;
+ overflow: hidden;
+ opacity: 0%;
+ transform: scaleY(5) translate(0.32rem, 50%);
+ cursor: ew-resize;
+ transform-origin: 100% 100%;
+ clip-path: inset(calc(100% - 0.75rem) 0 0 calc(100% - 0.75rem));
+ transition: min-width 0.3s ease-out, max-width 0.3s ease-out;
+ }
+ }
.select {
@layer daisyui.l1.l2.l3 {
border: var(--border) solid #0000;
@@ -1539,6 +1998,15 @@
}
}
}
+ .stats {
+ @layer daisyui.l1.l2.l3 {
+ position: relative;
+ display: inline-grid;
+ grid-auto-flow: column;
+ overflow-x: auto;
+ border-radius: var(--radius-box);
+ }
+ }
.progress {
@layer daisyui.l1.l2.l3 {
position: relative;
@@ -1589,9 +2057,97 @@
}
}
}
+ .absolute {
+ position: absolute;
+ }
+ .relative {
+ position: relative;
+ }
.static {
position: static;
}
+ .tooltip-top {
+ @layer daisyui.l1.l2 {
+ > .tooltip-content, &[data-tip]:before {
+ transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem));
+ inset: auto auto var(--tt-off) 50%;
+ }
+ &:after {
+ transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem));
+ inset: auto auto var(--tt-tail) 50%;
+ }
+ }
+ }
+ .join {
+ display: inline-flex;
+ align-items: stretch;
+ --join-ss: 0;
+ --join-se: 0;
+ --join-es: 0;
+ --join-ee: 0;
+ :where(.join-item) {
+ border-start-start-radius: var(--join-ss, 0);
+ border-start-end-radius: var(--join-se, 0);
+ border-end-start-radius: var(--join-es, 0);
+ border-end-end-radius: var(--join-ee, 0);
+ * {
+ --join-ss: var(--radius-field);
+ --join-se: var(--radius-field);
+ --join-es: var(--radius-field);
+ --join-ee: var(--radius-field);
+ }
+ }
+ > .join-item:where(:first-child) {
+ --join-ss: var(--radius-field);
+ --join-se: 0;
+ --join-es: var(--radius-field);
+ --join-ee: 0;
+ }
+ :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-item:where(:last-child) {
+ --join-ss: 0;
+ --join-se: var(--radius-field);
+ --join-es: 0;
+ --join-ee: var(--radius-field);
+ }
+ :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-item:where(:only-child) {
+ --join-ss: var(--radius-field);
+ --join-se: var(--radius-field);
+ --join-es: var(--radius-field);
+ --join-ee: var(--radius-field);
+ }
+ :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);
+ }
+ }
+ > :where(:focus, :has(:focus)) {
+ z-index: 1;
+ }
+ @media (hover: hover) {
+ > :where(.btn:hover, :has(.btn:hover)) {
+ isolation: isolate;
+ }
+ }
+ }
.textarea {
@layer daisyui.l1.l2.l3 {
border: var(--border) solid #0000;
@@ -1667,6 +2223,110 @@
}
}
}
+ .stack {
+ @layer daisyui.l1.l2.l3 {
+ display: inline-grid;
+ grid-template-columns: 3px 4px 1fr 4px 3px;
+ grid-template-rows: 3px 4px 1fr 4px 3px;
+ & > * {
+ height: 100%;
+ width: 100%;
+ &:nth-child(n + 2) {
+ width: 100%;
+ opacity: 70%;
+ }
+ &:nth-child(2) {
+ z-index: 2;
+ opacity: 90%;
+ }
+ &:nth-child(1) {
+ z-index: 3;
+ width: 100%;
+ }
+ }
+ }
+ @layer daisyui.l1.l2 {
+ &, &.stack-bottom {
+ > * {
+ grid-column: 3 / 4;
+ grid-row: 3 / 6;
+ &:nth-child(2) {
+ grid-column: 2 / 5;
+ grid-row: 2 / 5;
+ }
+ &:nth-child(1) {
+ grid-column: 1 / 6;
+ grid-row: 1 / 4;
+ }
+ }
+ }
+ &.stack-top {
+ > * {
+ grid-column: 3 / 4;
+ grid-row: 1 / 4;
+ &:nth-child(2) {
+ grid-column: 2 / 5;
+ grid-row: 2 / 5;
+ }
+ &:nth-child(1) {
+ grid-column: 1 / 6;
+ grid-row: 3 / 6;
+ }
+ }
+ }
+ &.stack-start {
+ > * {
+ grid-column: 1 / 4;
+ grid-row: 3 / 4;
+ &:nth-child(2) {
+ grid-column: 2 / 5;
+ grid-row: 2 / 5;
+ }
+ &:nth-child(1) {
+ grid-column: 3 / 6;
+ grid-row: 1 / 6;
+ }
+ }
+ }
+ &.stack-end {
+ > * {
+ grid-column: 3 / 6;
+ grid-row: 3 / 4;
+ &:nth-child(2) {
+ grid-column: 2 / 5;
+ grid-row: 2 / 5;
+ }
+ &:nth-child(1) {
+ grid-column: 1 / 4;
+ grid-row: 1 / 6;
+ }
+ }
+ }
+ }
+ }
+ .tab-content {
+ @layer daisyui.l1.l2.l3 {
+ order: var(--tabcontent-order);
+ display: none;
+ border-color: transparent;
+ --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-width: var(--border);
+ border-start-start-radius: var(--tabcontent-radius-ss);
+ border-start-end-radius: var(--tabcontent-radius-se);
+ border-end-start-radius: var(--tabcontent-radius-es);
+ border-end-end-radius: var(--tabcontent-radius-ee);
+ }
+ }
+ .m-5 {
+ margin: calc(var(--spacing) * 5);
+ }
.filter {
@layer daisyui.l1.l2.l3 {
display: flex;
@@ -1712,6 +2372,126 @@
}
}
}
+ .mx-auto {
+ margin-inline: auto;
+ }
+ .input-lg {
+ @layer daisyui.l1.l2 {
+ --size: calc(var(--size-field, 0.25rem) * 12);
+ font-size: max(var(--font-size, 1.125rem), 1.125rem);
+ &[type="number"] {
+ &::-webkit-inner-spin-button {
+ margin-block: calc(0.25rem * -3);
+ margin-inline-end: calc(0.25rem * -3);
+ }
+ }
+ }
+ }
+ .input-sm {
+ @layer daisyui.l1.l2 {
+ --size: calc(var(--size-field, 0.25rem) * 8);
+ font-size: max(var(--font-size, 0.75rem), 0.75rem);
+ &[type="number"] {
+ &::-webkit-inner-spin-button {
+ margin-block: calc(0.25rem * -2);
+ margin-inline-end: calc(0.25rem * -3);
+ }
+ }
+ }
+ }
+ .input-xl {
+ @layer daisyui.l1.l2 {
+ --size: calc(var(--size-field, 0.25rem) * 14);
+ font-size: max(var(--font-size, 1.375rem), 1.375rem);
+ &[type="number"] {
+ &::-webkit-inner-spin-button {
+ margin-block: calc(0.25rem * -4);
+ margin-inline-end: calc(0.25rem * -3);
+ }
+ }
+ }
+ }
+ .input-xs {
+ @layer daisyui.l1.l2 {
+ --size: calc(var(--size-field, 0.25rem) * 6);
+ font-size: max(var(--font-size, 0.6875rem), 0.6875rem);
+ &[type="number"] {
+ &::-webkit-inner-spin-button {
+ margin-block: calc(0.25rem * -1);
+ margin-inline-end: calc(0.25rem * -3);
+ }
+ }
+ }
+ }
+ .label {
+ @layer daisyui.l1.l2.l3 {
+ display: inline-flex;
+ align-items: center;
+ gap: calc(0.25rem * 1.5);
+ white-space: nowrap;
+ color: currentcolor;
+ @supports (color: color-mix(in lab, red, red)) {
+ color: color-mix(in oklab, currentcolor 60%, transparent);
+ }
+ &:has(input) {
+ cursor: pointer;
+ }
+ &:is(.input > *, .select > *) {
+ display: flex;
+ height: calc(100% - 0.5rem);
+ align-items: center;
+ padding-inline: calc(0.25rem * 3);
+ white-space: nowrap;
+ font-size: inherit;
+ &:first-child {
+ margin-inline-start: calc(0.25rem * -3);
+ margin-inline-end: calc(0.25rem * 3);
+ border-inline-end: var(--border) solid currentColor;
+ @supports (color: color-mix(in lab, red, red)) {
+ border-inline-end: var(--border) solid color-mix(in oklab, currentColor 10%, #0000);
+ }
+ }
+ &:last-child {
+ margin-inline-start: calc(0.25rem * 3);
+ margin-inline-end: calc(0.25rem * -3);
+ border-inline-start: var(--border) solid currentColor;
+ @supports (color: color-mix(in lab, red, red)) {
+ border-inline-start: var(--border) solid color-mix(in oklab, currentColor 10%, #0000);
+ }
+ }
+ }
+ }
+ }
+ .mt-0 {
+ margin-top: calc(var(--spacing) * 0);
+ }
+ .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);
+ }
+ .fieldset-legend {
+ @layer daisyui.l1.l2.l3 {
+ margin-bottom: calc(0.25rem * -1);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: calc(0.25rem * 2);
+ padding-block: calc(0.25rem * 2);
+ color: var(--color-base-content);
+ font-weight: 600;
+ }
+ }
+ .mb-4 {
+ margin-bottom: calc(var(--spacing) * 4);
+ }
+ .mb-12 {
+ margin-bottom: calc(var(--spacing) * 12);
+ }
.status {
@layer daisyui.l1.l2.l3 {
display: inline-block;
@@ -1759,6 +2539,103 @@
padding-inline: calc(var(--size) / 2 - var(--border));
}
}
+ .kbd {
+ box-shadow: none;
+ @layer daisyui.l1.l2.l3 {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-field);
+ background-color: var(--color-base-200);
+ vertical-align: middle;
+ padding-inline: 0.5em;
+ border: var(--border) solid var(--color-base-content);
+ @supports (color: color-mix(in lab, red, red)) {
+ border: var(--border) solid color-mix(in srgb, var(--color-base-content) 20%, #0000);
+ }
+ border-bottom: calc(var(--border) + 1px) solid var(--color-base-content);
+ @supports (color: color-mix(in lab, red, red)) {
+ border-bottom: calc(var(--border) + 1px) solid color-mix(in srgb, var(--color-base-content) 20%, #0000);
+ }
+ --size: calc(var(--size-selector, 0.25rem) * 6);
+ font-size: 0.875rem;
+ height: var(--size);
+ min-width: var(--size);
+ }
+ }
+ .stat {
+ @layer daisyui.l1.l2.l3 {
+ display: inline-grid;
+ width: 100%;
+ column-gap: calc(0.25rem * 4);
+ padding-inline: calc(0.25rem * 6);
+ padding-block: calc(0.25rem * 4);
+ grid-template-columns: repeat(1, 1fr);
+ &:not(:last-child) {
+ border-inline-end: var(--border) dashed currentColor;
+ @supports (color: color-mix(in lab, red, red)) {
+ border-inline-end: var(--border) dashed color-mix(in oklab, currentColor 10%, #0000);
+ }
+ border-block-end: none;
+ }
+ }
+ }
+ .fieldset-label {
+ @layer daisyui.l1.l2.l3 {
+ display: flex;
+ align-items: center;
+ gap: calc(0.25rem * 1.5);
+ color: var(--color-base-content);
+ @supports (color: color-mix(in lab, red, red)) {
+ color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
+ }
+ &:has(input) {
+ cursor: pointer;
+ }
+ }
+ }
+ .alert {
+ border-width: var(--border);
+ border-color: var(--alert-border-color, var(--color-base-200));
+ @layer daisyui.l1.l2.l3 {
+ border-style: solid;
+ --alert-border-color: var(--color-base-200);
+ display: grid;
+ align-items: center;
+ gap: calc(0.25rem * 4);
+ border-radius: var(--radius-box);
+ padding-inline: calc(0.25rem * 4);
+ padding-block: calc(0.25rem * 3);
+ color: var(--color-base-content);
+ background-color: var(--alert-color, var(--color-base-200));
+ justify-content: start;
+ justify-items: start;
+ grid-auto-flow: column;
+ grid-template-columns: auto;
+ text-align: start;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ 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) * 0.08)) inset, 0 1px #000, 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * 0.08));
+ @supports (color: color-mix(in lab, red, red)) {
+ box-shadow: 0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * 0.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) * 0.08));
+ }
+ &:has(:nth-child(2)) {
+ grid-template-columns: auto minmax(auto, 1fr);
+ }
+ }
+ }
+ .fieldset {
+ @layer daisyui.l1.l2.l3 {
+ display: grid;
+ gap: calc(0.25rem * 1.5);
+ padding-block: calc(0.25rem * 1);
+ font-size: 0.75rem;
+ grid-template-columns: 1fr;
+ grid-auto-rows: max-content;
+ }
+ }
.mask {
@layer daisyui.l1.l2.l3 {
display: inline-block;
@@ -1768,15 +2645,48 @@
mask-position: center;
}
}
+ .block {
+ display: block;
+ }
+ .flex {
+ display: flex;
+ }
+ .hidden {
+ display: none;
+ }
.inline {
display: inline;
}
.table {
display: table;
}
+ .h-6 {
+ height: calc(var(--spacing) * 6);
+ }
+ .w-full {
+ width: 100%;
+ }
+ .max-w-7 {
+ max-width: calc(var(--spacing) * 7);
+ }
+ .max-w-lg {
+ max-width: var(--container-lg);
+ }
+ .max-w-xs {
+ max-width: var(--container-xs);
+ }
+ .flex-1 {
+ flex: 1;
+ }
.flex-shrink {
flex-shrink: 1;
}
+ .flex-grow {
+ flex-grow: 1;
+ }
+ .grow {
+ flex-grow: 1;
+ }
.border-collapse {
border-collapse: collapse;
}
@@ -1820,30 +2730,227 @@
.resize {
resize: both;
}
+ .flex-col {
+ flex-direction: column;
+ }
.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;
+ }
+ .gap-2 {
+ gap: calc(var(--spacing) * 2);
+ }
+ .gap-6 {
+ gap: calc(var(--spacing) * 6);
+ }
+ .space-y-1 {
+ :where(& > :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)));
+ }
+ }
+ .space-y-2 {
+ :where(& > :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)));
+ }
+ }
+ .space-x-1 {
+ :where(& > :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 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .overflow-x-auto {
+ overflow-x: auto;
+ }
+ .rounded {
+ border-radius: 0.25rem;
+ }
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
+ .bg-green-100 {
+ background-color: var(--color-green-100);
+ }
+ .bg-white {
+ background-color: var(--color-white);
+ }
.mask-repeat {
mask-repeat: repeat;
}
+ .p-0 {
+ padding: calc(var(--spacing) * 0);
+ }
+ .p-0\! {
+ padding: calc(var(--spacing) * 0) !important;
+ }
+ .p-4 {
+ padding: calc(var(--spacing) * 4);
+ }
+ .select-lg {
+ @layer daisyui.l1.l2 {
+ --size: calc(var(--size-field, 0.25rem) * 12);
+ font-size: 1.125rem;
+ option {
+ padding-inline: calc(0.25rem * 4);
+ padding-block: calc(0.25rem * 1.5);
+ }
+ }
+ }
+ .select-sm {
+ @layer daisyui.l1.l2 {
+ --size: calc(var(--size-field, 0.25rem) * 8);
+ font-size: 0.75rem;
+ option {
+ padding-inline: calc(0.25rem * 2.5);
+ padding-block: calc(0.25rem * 1);
+ }
+ }
+ }
+ .select-xl {
+ @layer daisyui.l1.l2 {
+ --size: calc(var(--size-field, 0.25rem) * 14);
+ font-size: 1.375rem;
+ option {
+ padding-inline: calc(0.25rem * 5);
+ padding-block: calc(0.25rem * 1.5);
+ }
+ }
+ }
+ .select-xs {
+ @layer daisyui.l1.l2 {
+ --size: calc(var(--size-field, 0.25rem) * 6);
+ font-size: 0.6875rem;
+ option {
+ padding-inline: calc(0.25rem * 2);
+ padding-block: calc(0.25rem * 1);
+ }
+ }
+ }
+ .px-4 {
+ padding-inline: calc(var(--spacing) * 4);
+ }
+ .py-1 {
+ padding-block: calc(var(--spacing) * 1);
+ }
+ .py-2 {
+ padding-block: calc(var(--spacing) * 2);
+ }
.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));
+ }
+ .textarea-lg {
+ @layer daisyui.l1.l2 {
+ font-size: max(var(--font-size, 1.125rem), 1.125rem);
+ }
+ }
+ .textarea-sm {
+ @layer daisyui.l1.l2 {
+ font-size: max(var(--font-size, 0.75rem), 0.75rem);
+ }
+ }
+ .textarea-xl {
+ @layer daisyui.l1.l2 {
+ font-size: max(var(--font-size, 1.375rem), 1.375rem);
+ }
+ }
+ .textarea-xs {
+ @layer daisyui.l1.l2 {
+ font-size: max(var(--font-size, 0.6875rem), 0.6875rem);
+ }
+ }
+ .font-semibold {
+ --tw-font-weight: var(--font-weight-semibold);
+ font-weight: var(--font-weight-semibold);
+ }
.text-wrap {
text-wrap: wrap;
}
+ .alert-error {
+ @layer daisyui.l1.l2 {
+ color: var(--color-error-content);
+ --alert-border-color: var(--color-error);
+ --alert-color: var(--color-error);
+ }
+ }
+ .alert-success {
+ @layer daisyui.l1.l2 {
+ color: var(--color-success-content);
+ --alert-border-color: var(--color-success);
+ --alert-color: var(--color-success);
+ }
+ }
+ .text-error {
+ color: var(--color-error);
+ }
+ .text-gray-500 {
+ color: var(--color-gray-500);
+ }
+ .text-gray-600 {
+ color: var(--color-gray-600);
+ }
+ .text-gray-800 {
+ color: var(--color-gray-800);
+ }
+ .text-green-800 {
+ color: var(--color-green-800);
+ }
+ .text-primary {
+ color: var(--color-primary);
+ }
.underline {
text-decoration-line: underline;
}
+ .shadow {
+ --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
+ .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,);
}
@@ -1866,6 +2973,41 @@
--btn-fg: var(--color-primary-content);
}
}
+ .range-lg {
+ @layer daisyui.l1.l2 {
+ --range-thumb-size: calc(var(--size-selector, 0.25rem) * 7);
+ }
+ }
+ .range-sm {
+ @layer daisyui.l1.l2 {
+ --range-thumb-size: calc(var(--size-selector, 0.25rem) * 5);
+ }
+ }
+ .range-xl {
+ @layer daisyui.l1.l2 {
+ --range-thumb-size: calc(var(--size-selector, 0.25rem) * 8);
+ }
+ }
+ .range-xs {
+ @layer daisyui.l1.l2 {
+ --range-thumb-size: calc(var(--size-selector, 0.25rem) * 4);
+ }
+ }
+ .textarea-error {
+ @layer daisyui.l1.l2 {
+ &, &:focus, &:focus-within {
+ --input-color: var(--color-error);
+ }
+ }
+ }
+ .hover\:shadow {
+ &:hover {
+ @media (hover: hover) {
+ --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
+ }
+ }
}
@layer base {
:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] {
@@ -2234,11 +3376,90 @@
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: 0px;
+}
+@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-outline-style {
syntax: "*";
inherits: false;
@@ -2309,7 +3530,24 @@
--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-outline-style: solid;
--tw-blur: initial;
--tw-brightness: initial;
diff --git a/pkg/web/components/base/input.templ b/pkg/web/components/base/input.templ
index 87c5049..f23657e 100644
--- a/pkg/web/components/base/input.templ
+++ b/pkg/web/components/base/input.templ
@@ -6,6 +6,7 @@ type InputProps struct {
Label string
Type string // defaults to "text"
Value string
+ Class string
Placeholder string
Description string
Error string
@@ -37,7 +38,7 @@ templ Input(props InputProps) {
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "")
+ if props.ValidatorHint != "" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var16 string
+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(props.ValidatorHint)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/input.templ`, Line: 87, Col: 80}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/pkg/web/components/base/range.templ b/pkg/web/components/base/range.templ
index 7366e87..b22a39f 100644
--- a/pkg/web/components/base/range.templ
+++ b/pkg/web/components/base/range.templ
@@ -17,6 +17,7 @@ type RangeProps struct {
templ Range(props RangeProps) {
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" step=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var13 string
+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", props.Step))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 48, Col: 40}
+ }
+ _, 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, 13, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/pkg/web/components/base/select.templ b/pkg/web/components/base/select.templ
index 9a14c81..6dc7088 100644
--- a/pkg/web/components/base/select.templ
+++ b/pkg/web/components/base/select.templ
@@ -33,10 +33,9 @@ templ Select(props SelectProps) {
templ.KV("select-lg", props.Size == "lg"),
templ.KV("select-xl", props.Size == "xl"),
}
- if props.ID != "" {
- id={ props.ID }
- }
+ id={ props.Name }
name={ props.Name }
+ aria-label={ props.Label }
if props.Required {
required
}
diff --git a/pkg/web/components/base/select_templ.go b/pkg/web/components/base/select_templ.go
index 121dbe8..aab85ee 100644
--- a/pkg/web/components/base/select_templ.go
+++ b/pkg/web/components/base/select_templ.go
@@ -95,48 +95,51 @@ func Select(props SelectProps) templ.Component {
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, 5, "\" id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- if props.ID != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " id=\"")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- var templ_7745c5c3_Var5 string
- templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.ID)
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/select.templ`, Line: 37, Col: 17}
- }
- _, 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, 7, "\"")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Name)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/select.templ`, Line: 36, Col: 18}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " name=\"")
+ _, 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, "\" name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/select.templ`, Line: 39, Col: 20}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/select.templ`, Line: 37, Col: 20}
}
_, 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, 9, "\"")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" aria-label=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/select.templ`, Line: 38, Col: 27}
+ }
+ _, 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
}
if props.Required {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " required")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " required")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -145,7 +148,7 @@ func Select(props SelectProps) templ.Component {
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
}
@@ -153,30 +156,30 @@ func Select(props SelectProps) 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, 11, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if props.Description != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var7 string
- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(props.Description)
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(props.Description)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/select.templ`, Line: 48, Col: 42}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/select.templ`, Line: 47, Col: 42}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ _, 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, 14, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "")
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, 14, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -200,55 +203,55 @@ func SelectOptions(options []SelectOption) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var8 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var8 == nil {
- templ_7745c5c3_Var8 = templ.NopComponent
+ templ_7745c5c3_Var9 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var9 == nil {
+ templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
for i := range options {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var11 string
+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(options[i].Label)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/select.templ`, Line: 63, Col: 21}
+ }
+ _, 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, 20, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/pkg/web/components/base/textarea.templ b/pkg/web/components/base/textarea.templ
index f065a4f..ce501c7 100644
--- a/pkg/web/components/base/textarea.templ
+++ b/pkg/web/components/base/textarea.templ
@@ -24,10 +24,9 @@ templ Textarea(props TextareaProps) {
}