")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -111,33 +111,4 @@ func SignupPage() templ.Component {
})
}
-func HomePage(displayName string, email string) templ.Component {
- return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
- templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
- if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
- return templ_7745c5c3_CtxErr
- }
- templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
- if !templ_7745c5c3_IsBuffer {
- defer func() {
- templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err == nil {
- templ_7745c5c3_Err = templ_7745c5c3_BufErr
- }
- }()
- }
- ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var4 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var4 == nil {
- templ_7745c5c3_Var4 = templ.NopComponent
- }
- ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = DashboardPage("/", OverviewMainContent(displayName, email)).Render(ctx, templ_7745c5c3_Buffer)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- return nil
- })
-}
-
var _ = templruntime.GeneratedTemplate
diff --git a/go-backend/internal/web/views/tablos.templ b/go-backend/internal/web/views/tablos.templ
new file mode 100644
index 0000000..13d6c32
--- /dev/null
+++ b/go-backend/internal/web/views/tablos.templ
@@ -0,0 +1,289 @@
+package views
+
+import "xtablo-backend/internal/web/ui"
+
+templ TablosPageContent(vm TablosPageViewModel) {
+
+
+
Mes Projets
+ @ui.Button(ui.ButtonProps{
+ Label: "Nouveau projet",
+ Variant: ui.ButtonVariantPrimary,
+ Size: ui.SizeMD,
+ Type: "button",
+ Icon: "plus",
+ Attrs: templ.Attributes{
+ "hx-get": vm.CreateModalHref(),
+ "hx-target": "#app-main-content",
+ "hx-swap": "outerHTML",
+ "hx-push-url": "true",
+ },
+ })
+
+
+
+
+
+ @StatusPill(vm, "all", "Tous")
+ @StatusPill(vm, "todo", "Pas commencé")
+ @StatusPill(vm, "in_progress", "En cours")
+ @StatusPill(vm, "done", "Terminé")
+
+
+ if vm.HasTablos() {
+ if vm.IsGridView() {
+
+ for _, tablo := range vm.Tablos {
+ @TabloGridCard(tablo)
+ }
+
+ } else {
+
+ @ui.Table(ui.TableProps{
+ Head: TabloListHead(),
+ Body: TabloListBody(vm.Tablos),
+ })
+
+ }
+ } else {
+ @ui.EmptyState(ui.EmptyStateProps{
+ Title: "Aucun projet trouvé",
+ Description: "Créez votre premier projet",
+ Icon: ui.UIIcon("grid3x3"),
+ Action: ui.Button(ui.ButtonProps{
+ Label: "Nouveau projet",
+ Variant: ui.ButtonVariantPrimary,
+ Size: ui.SizeMD,
+ Type: "button",
+ Icon: "plus",
+ Attrs: templ.Attributes{
+ "hx-get": vm.CreateModalHref(),
+ "hx-target": "#app-main-content",
+ "hx-swap": "outerHTML",
+ "hx-push-url": "true",
+ },
+ }),
+ })
+ }
+ if vm.ModalOpen {
+ @CreateTabloModal(vm)
+ }
+
+}
+
+templ StatusPill(vm TablosPageViewModel, status string, label string) {
+
+ if status == "all" {
+
+ @ActionIcon("filter")
+
+ }
+ { label }
+
+}
+
+templ BorderlessDeleteButton(deleteRequestURL string) {
+ @ui.IconButton(ui.IconButtonProps{
+ Label: "Supprimer le projet",
+ Icon: "trash",
+ Variant: ui.IconButtonVariantDangerGhost,
+ Type: "button",
+ Attrs: templ.Attributes{
+ "hx-delete": deleteRequestURL,
+ "hx-target": "#app-main-content",
+ "hx-swap": "outerHTML",
+ "hx-confirm": "Supprimer ce projet ?",
+ },
+ })
+}
+
+templ TabloGridCard(tablo TabloCardView) {
+
+
+ @ui.Badge(ui.BadgeProps{
+ Label: tablo.StatusLabel,
+ Variant: badgeVariantForTone(tablo.StatusTone),
+ })
+ @BorderlessDeleteButton(tablo.DeleteRequestURL)
+
+
+
+ { tablo.Initial }
+
+
{ tablo.Name }
+
+
+ @ActionIcon("calendar")
+ { tablo.CardDateLabel }
+
+
+
+ Progression:
+ { tablo.ProgressLabel }
+
+
+
+
+}
+
+templ TabloListRow(tablo TabloCardView) {
+
+
+
+ svg]:w-4 [&>svg]:h-4 " + tablo.IconBgClass + " " + tablo.IconFgClass }>
+ @ActionIcon(tablo.IconKind)
+
+ { tablo.Name }
+
+ |
+
+ @ui.Badge(ui.BadgeProps{
+ Label: tablo.StatusLabel,
+ Variant: badgeVariantForTone(tablo.StatusTone),
+ })
+ |
+
+
+ @ActionIcon("calendar")
+ { tablo.CreatedAtLabel }
+
+ |
+
+
+
+ { tablo.ProgressLabel }
+
+ |
+
+ @BorderlessDeleteButton(tablo.DeleteRequestURL)
+ |
+
+}
+
+templ CreateTabloModal(vm TablosPageViewModel) {
+ @ui.Modal(ui.ModalProps{
+ Title: "Nouveau projet",
+ Body: CreateTabloModalBody(vm),
+ })
+}
+
+templ TabloListHead() {
+
+ | Projet |
+ Statut |
+ Créé le |
+ Progression |
+ |
+
+}
+
+templ TabloListBody(tablos []TabloCardView) {
+ for _, tablo := range tablos {
+ @TabloListRow(tablo)
+ }
+}
+
+templ CreateTabloModalBody(vm TablosPageViewModel) {
+
+}
diff --git a/go-backend/internal/web/views/tablos_templ.go b/go-backend/internal/web/views/tablos_templ.go
new file mode 100644
index 0000000..f88b800
--- /dev/null
+++ b/go-backend/internal/web/views/tablos_templ.go
@@ -0,0 +1,992 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.1001
+package views
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import "xtablo-backend/internal/web/ui"
+
+func TablosPageContent(vm TablosPageViewModel) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Mes Projets
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = ui.Button(ui.ButtonProps{
+ Label: "Nouveau projet",
+ Variant: ui.ButtonVariantPrimary,
+ Size: ui.SizeMD,
+ Type: "button",
+ Icon: "plus",
+ Attrs: templ.Attributes{
+ "hx-get": vm.CreateModalHref(),
+ "hx-target": "#app-main-content",
+ "hx-swap": "outerHTML",
+ "hx-push-url": "true",
+ },
+ }).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var2 = []any{gridToggleClass(vm.IsGridView())}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = ActionIcon("grid3x3").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " Vue en grille ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 = []any{listToggleClass(vm.IsGridView())}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = ActionIcon("list").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " Vue en liste")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = StatusPill(vm, "all", "Tous").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = StatusPill(vm, "todo", "Pas commencé").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = StatusPill(vm, "in_progress", "En cours").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = StatusPill(vm, "done", "Terminé").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if vm.HasTablos() {
+ if vm.IsGridView() {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, tablo := range vm.Tablos {
+ templ_7745c5c3_Err = TabloGridCard(tablo).Render(ctx, templ_7745c5c3_Buffer)
+ 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
+ }
+ } else {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = ui.Table(ui.TableProps{
+ Head: TabloListHead(),
+ Body: TabloListBody(vm.Tablos),
+ }).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ } else {
+ templ_7745c5c3_Err = ui.EmptyState(ui.EmptyStateProps{
+ Title: "Aucun projet trouvé",
+ Description: "Créez votre premier projet",
+ Icon: ui.UIIcon("grid3x3"),
+ Action: ui.Button(ui.ButtonProps{
+ Label: "Nouveau projet",
+ Variant: ui.ButtonVariantPrimary,
+ Size: ui.SizeMD,
+ Type: "button",
+ Icon: "plus",
+ Attrs: templ.Attributes{
+ "hx-get": vm.CreateModalHref(),
+ "hx-target": "#app-main-content",
+ "hx-swap": "outerHTML",
+ "hx-push-url": "true",
+ },
+ }),
+ }).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if vm.ModalOpen {
+ templ_7745c5c3_Err = CreateTabloModal(vm).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func StatusPill(vm TablosPageViewModel, status string, label string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var14 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var14 == nil {
+ templ_7745c5c3_Var14 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ var templ_7745c5c3_Var15 = []any{statusPillClass(vm.Status == status)}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if status == "all" {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = ActionIcon("filter").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ var templ_7745c5c3_Var19 string
+ templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(label)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 135, Col: 9}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func BorderlessDeleteButton(deleteRequestURL string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var20 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var20 == nil {
+ templ_7745c5c3_Var20 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = ui.IconButton(ui.IconButtonProps{
+ Label: "Supprimer le projet",
+ Icon: "trash",
+ Variant: ui.IconButtonVariantDangerGhost,
+ Type: "button",
+ Attrs: templ.Attributes{
+ "hx-delete": deleteRequestURL,
+ "hx-target": "#app-main-content",
+ "hx-swap": "outerHTML",
+ "hx-confirm": "Supprimer ce projet ?",
+ },
+ }).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func TabloGridCard(tablo TabloCardView) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var21 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var21 == nil {
+ templ_7745c5c3_Var21 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = ui.Badge(ui.BadgeProps{
+ Label: tablo.StatusLabel,
+ Variant: badgeVariantForTone(tablo.StatusTone),
+ }).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = BorderlessDeleteButton(tablo.DeleteRequestURL).Render(ctx, templ_7745c5c3_Buffer)
+ 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
+ }
+ var templ_7745c5c3_Var22 = []any{"project-avatar " + projectAccentClass(tablo.Accent)}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var24 string
+ templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Initial)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 165, Col: 25}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var25 string
+ templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 167, Col: 19}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = ActionIcon("calendar").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var26 string
+ templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CardDateLabel)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 171, Col: 30}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
Progression: ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var27 string
+ templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 176, Col: 33}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var28 = []any{"project-progress-bar " + projectAccentClass(tablo.Accent)}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var28...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func TabloListRow(tablo TabloCardView) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var31 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var31 == nil {
+ templ_7745c5c3_Var31 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var32 = []any{"w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden [&>svg]:w-4 [&>svg]:h-4 " + tablo.IconBgClass + " " + tablo.IconFgClass}
+ templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var32...)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = ActionIcon(tablo.IconKind).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var34 string
+ templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 192, Col: 84}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = ui.Badge(ui.BadgeProps{
+ Label: tablo.StatusLabel,
+ Variant: badgeVariantForTone(tablo.StatusTone),
+ }).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " | svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = ActionIcon("calendar").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var35 string
+ templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CreatedAtLabel)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 204, Col: 26}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var37 string
+ templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 212, Col: 109}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " | ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = BorderlessDeleteButton(tablo.DeleteRequestURL).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " |
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func CreateTabloModal(vm TablosPageViewModel) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var38 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var38 == nil {
+ templ_7745c5c3_Var38 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = ui.Modal(ui.ModalProps{
+ Title: "Nouveau projet",
+ Body: CreateTabloModalBody(vm),
+ }).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func TabloListHead() templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var39 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var39 == nil {
+ templ_7745c5c3_Var39 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
| Projet | Statut | Créé le | Progression | |
|---|
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func TabloListBody(tablos []TabloCardView) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var40 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var40 == nil {
+ templ_7745c5c3_Var40 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ for _, tablo := range tablos {
+ templ_7745c5c3_Err = TabloListRow(tablo).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ return nil
+ })
+}
+
+func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var41 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var41 == nil {
+ templ_7745c5c3_Var41 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/go-backend/internal/web/views/tablos_view.go b/go-backend/internal/web/views/tablos_view.go
new file mode 100644
index 0000000..111dce8
--- /dev/null
+++ b/go-backend/internal/web/views/tablos_view.go
@@ -0,0 +1,161 @@
+package views
+
+import (
+ "fmt"
+ "net/url"
+ "strings"
+
+ "xtablo-backend/internal/web/ui"
+)
+
+type TabloCardView struct {
+ ID string
+ Name string
+ Status string
+ StatusLabel string
+ StatusClass string
+ StatusTone string
+ Progress int
+ CreatedAtLabel string
+ CardDateLabel string
+ ProgressLabel string
+ DeleteURL string
+ DeleteRequestURL string
+ IconKind string
+ IconBgClass string
+ IconFgClass string
+ Accent string
+ Initial string
+}
+
+type TablosPageViewModel struct {
+ DisplayName string
+ View string
+ Query string
+ Status string
+ ModalOpen bool
+ FormName string
+ ErrorMessage string
+ Tablos []TabloCardView
+}
+
+func NewTablosPageViewModel(displayName string, view string, query string, status string, modalOpen bool, formName string, errorMessage string, tablos []TabloCardView) TablosPageViewModel {
+ return TablosPageViewModel{
+ DisplayName: displayName,
+ View: normalizedView(view),
+ Query: strings.TrimSpace(query),
+ Status: normalizedStatus(status),
+ ModalOpen: modalOpen,
+ FormName: strings.TrimSpace(formName),
+ ErrorMessage: strings.TrimSpace(errorMessage),
+ Tablos: tablos,
+ }
+}
+
+func (vm TablosPageViewModel) IsGridView() bool {
+ return vm.View != "list"
+}
+
+func (vm TablosPageViewModel) HasTablos() bool {
+ return len(vm.Tablos) > 0
+}
+
+func (vm TablosPageViewModel) StatusHref(status string) string {
+ values := vm.baseValues()
+ values.Set("status", normalizedStatus(status))
+ return "/tablos?" + values.Encode()
+}
+
+func (vm TablosPageViewModel) ViewHref(view string) string {
+ values := vm.baseValues()
+ values.Set("view", normalizedView(view))
+ return "/tablos?" + values.Encode()
+}
+
+func (vm TablosPageViewModel) SearchHref() string {
+ return "/tablos"
+}
+
+func (vm TablosPageViewModel) HiddenStateFields() map[string]string {
+ return map[string]string{
+ "view": vm.View,
+ "status": vm.Status,
+ "q": vm.Query,
+ }
+}
+
+func (vm TablosPageViewModel) SearchValues() string {
+ return fmt.Sprintf("view=%s&status=%s", vm.View, vm.Status)
+}
+
+func (vm TablosPageViewModel) CreateModalHref() string {
+ values := vm.baseValues()
+ values.Set("modal", "create")
+ return "/tablos?" + values.Encode()
+}
+
+func (vm TablosPageViewModel) CloseModalHref() string {
+ values := vm.baseValues()
+ return "/tablos?" + values.Encode()
+}
+
+func (vm TablosPageViewModel) HasSearch() bool {
+ return vm.Query != ""
+}
+
+func normalizedView(view string) string {
+ if view == "list" {
+ return "list"
+ }
+ return "grid"
+}
+
+func normalizedStatus(status string) string {
+ switch status {
+ case "todo", "in_progress", "done":
+ return status
+ default:
+ return "all"
+ }
+}
+
+func (vm TablosPageViewModel) baseValues() url.Values {
+ values := url.Values{}
+ values.Set("view", vm.View)
+ values.Set("status", vm.Status)
+ if vm.Query != "" {
+ values.Set("q", vm.Query)
+ }
+ return values
+}
+
+func gridToggleClass(active bool) string {
+ if active {
+ return "flex items-center gap-2 pb-3 border-b-2 transition-colors border-purple-600 text-purple-600 dark:border-purple-400 dark:text-purple-400 font-semibold"
+ }
+ return "flex items-center gap-2 pb-3 border-b-2 transition-colors border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
+}
+
+func listToggleClass(gridActive bool) string {
+ return gridToggleClass(!gridActive)
+}
+
+func statusPillClass(active bool) string {
+ if active {
+ return "flex items-center gap-1.5 px-4 py-2.5 border rounded-[8px] font-medium text-sm transition-colors border-purple-600 bg-purple-50 dark:bg-purple-950/30 text-purple-600 dark:text-purple-400"
+ }
+ return "flex items-center gap-1.5 px-4 py-2.5 border rounded-[8px] font-medium text-sm transition-colors border-[#EAECF0] dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300"
+}
+
+func badgeVariantForTone(tone string) ui.BadgeVariant {
+ switch tone {
+ case "warning":
+ return ui.BadgeVariantWarning
+ case "success":
+ return ui.BadgeVariantSuccess
+ case "danger":
+ return ui.BadgeVariantDanger
+ default:
+ return ui.BadgeVariantInfo
+ }
+}
diff --git a/go-backend/justfile b/go-backend/justfile
index 97bc6ea..2c0c7ec 100644
--- a/go-backend/justfile
+++ b/go-backend/justfile
@@ -2,6 +2,8 @@ set shell := ["bash", "-cu"]
database_url := "postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable"
compose_config_dir := ".podman-compose"
+tailwind_input := "tailwind.input.css"
+tailwind_output := "static/tailwind.css"
default:
@just --list
@@ -40,9 +42,17 @@ db-logs: machine-up compose-config
DOCKER_CONFIG="$PWD/{{compose_config_dir}}" podman compose logs -f postgres
generate:
+ pnpm exec tailwindcss -i {{tailwind_input}} -o {{tailwind_output}} --cwd .
go run github.com/a-h/templ/cmd/templ@latest generate
go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate
+design-system:
+ just generate
+ go run ./cmd/designsystem
+
+css-watch:
+ pnpm exec tailwindcss -i {{tailwind_input}} -o {{tailwind_output}} --cwd . --watch
+
fmt:
gofmt -w .
@@ -50,12 +60,15 @@ test:
go test ./...
build:
+ pnpm exec tailwindcss -i {{tailwind_input}} -o {{tailwind_output}} --cwd .
go build ./...
check: generate test build
dev: db-up
+ pnpm exec tailwindcss -i {{tailwind_input}} -o {{tailwind_output}} --cwd .
DATABASE_URL='{{database_url}}' air -c .air.toml
run: db-up
+ pnpm exec tailwindcss -i {{tailwind_input}} -o {{tailwind_output}} --cwd .
DATABASE_URL='{{database_url}}' go run .
diff --git a/go-backend/package.json b/go-backend/package.json
new file mode 100644
index 0000000..bab9545
--- /dev/null
+++ b/go-backend/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@xtablo/go-backend",
+ "private": true,
+ "version": "0.0.0",
+ "packageManager": "pnpm@10.19.0",
+ "devDependencies": {
+ "@tailwindcss/cli": "4.1.15",
+ "tailwindcss": "4.1.15"
+ }
+}
diff --git a/go-backend/router.go b/go-backend/router.go
index 999b974..95c5d10 100644
--- a/go-backend/router.go
+++ b/go-backend/router.go
@@ -37,6 +37,8 @@ func newRouterWithHandler(authHandler *handlers.AuthHandler) http.Handler {
mux.Get("/chat", authHandler.GetChatPage())
mux.Get("/files", authHandler.GetFilesPage())
mux.Get("/feedback", authHandler.GetFeedbackPage())
+ mux.Post("/tablos", authHandler.PostTablos())
+ mux.Delete("/tablos/{tabloID}", authHandler.DeleteTablo())
mux.Get("/login", authHandler.GetLoginPage())
mux.Get("/signup", authHandler.GetSignupPage())
mux.Post("/login", authHandler.PostLogin())
diff --git a/go-backend/router_test.go b/go-backend/router_test.go
index f720c68..20a5eac 100644
--- a/go-backend/router_test.go
+++ b/go-backend/router_test.go
@@ -1,9 +1,11 @@
package main
import (
+ "context"
"net/http"
"net/http/httptest"
"net/url"
+ "os"
"strings"
"testing"
@@ -56,6 +58,7 @@ func TestLoginPageRenders(t *testing.T) {
"Se connecter à Xtablo",
`hx-post="/login"`,
"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js",
+ `href="/static/tailwind.css"`,
`href="/pwa-icons/favicon-32x32.png"`,
`href="/pwa-icons/favicon-16x16.png"`,
`href="/pwa-icons/apple-touch-icon-180x180.png"`,
@@ -72,6 +75,30 @@ func TestLoginPageRenders(t *testing.T) {
}
}
+func TestTailwindStylesheetIsServed(t *testing.T) {
+ req := httptest.NewRequest(http.MethodGet, "/static/tailwind.css", nil)
+ rec := httptest.NewRecorder()
+
+ router := newTestRouter()
+ router.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected status 200, got %d", rec.Code)
+ }
+
+ body := rec.Body.String()
+ for _, want := range []string{
+ ".text-2xl",
+ ".grid-cols-1",
+ ".whitespace-nowrap",
+ ".justify-end",
+ } {
+ if !strings.Contains(body, want) {
+ t.Fatalf("expected tailwind.css to contain %q", want)
+ }
+ }
+}
+
func TestSignupPageRenders(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/signup", nil)
rec := httptest.NewRecorder()
@@ -239,6 +266,175 @@ func TestTasksPageRendersFullDashboardPage(t *testing.T) {
}
}
+func TestHomePageProjectsUseSharedTabloGridCardWithDeleteAction(t *testing.T) {
+ repo := handlers.NewInMemoryAuthRepository()
+ authUser, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
+ if err != nil {
+ t.Fatalf("expected demo user, got error: %v", err)
+ }
+ if _, err := repo.CreateTablo(context.Background(), handlers.CreateTabloInput{
+ OwnerID: authUser.ID,
+ Name: "Hello",
+ Status: handlers.TabloStatusInProgress,
+ }); err != nil {
+ t.Fatalf("expected tablo creation to succeed, got error: %v", err)
+ }
+
+ form := url.Values{}
+ form.Set("email", "demo@xtablo.com")
+ form.Set("password", "xtablo-demo")
+
+ loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
+ loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ loginRec := httptest.NewRecorder()
+
+ router := newRouterWithHandler(handlers.NewAuthHandler(repo))
+ router.ServeHTTP(loginRec, loginReq)
+
+ sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
+ if sessionCookie == nil {
+ t.Fatalf("expected session cookie to be set")
+ }
+
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
+ req.AddCookie(sessionCookie)
+ rec := httptest.NewRecorder()
+ router.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected status 200, got %d", rec.Code)
+ }
+
+ body := rec.Body.String()
+ for _, want := range []string{
+ `class="project-card"`,
+ `class="project-date-row"`,
+ `hx-delete="/tablos/`,
+ } {
+ if !strings.Contains(body, want) {
+ t.Fatalf("expected home page to contain %q", want)
+ }
+ }
+}
+
+func TestHomePageProjectsCollapseAfterSixByDefault(t *testing.T) {
+ repo := handlers.NewInMemoryAuthRepository()
+ authUser, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
+ if err != nil {
+ t.Fatalf("expected demo user, got error: %v", err)
+ }
+ for i := 0; i < 8; i++ {
+ if _, err := repo.CreateTablo(context.Background(), handlers.CreateTabloInput{
+ OwnerID: authUser.ID,
+ Name: "Project " + string(rune('A'+i)),
+ Status: handlers.TabloStatusTodo,
+ }); err != nil {
+ t.Fatalf("expected tablo creation to succeed, got error: %v", err)
+ }
+ }
+
+ form := url.Values{}
+ form.Set("email", "demo@xtablo.com")
+ form.Set("password", "xtablo-demo")
+
+ loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
+ loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ loginRec := httptest.NewRecorder()
+
+ router := newRouterWithHandler(handlers.NewAuthHandler(repo))
+ router.ServeHTTP(loginRec, loginReq)
+
+ sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
+ if sessionCookie == nil {
+ t.Fatalf("expected session cookie to be set")
+ }
+
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
+ req.AddCookie(sessionCookie)
+ rec := httptest.NewRecorder()
+ router.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected status 200, got %d", rec.Code)
+ }
+
+ body := rec.Body.String()
+ if count := strings.Count(body, `class="project-card"`); count != 6 {
+ t.Fatalf("expected 6 visible project cards by default, got %d", count)
+ }
+ for _, want := range []string{
+ `id="overview-projects-section"`,
+ `Voir 2 de plus`,
+ `hx-get="/?show_projects=all"`,
+ `hx-target="#overview-projects-section"`,
+ } {
+ if !strings.Contains(body, want) {
+ t.Fatalf("expected home page to contain %q", want)
+ }
+ }
+}
+
+func TestHomePageProjectsExpandViaHTMXSectionSwap(t *testing.T) {
+ repo := handlers.NewInMemoryAuthRepository()
+ authUser, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
+ if err != nil {
+ t.Fatalf("expected demo user, got error: %v", err)
+ }
+ for i := 0; i < 8; i++ {
+ if _, err := repo.CreateTablo(context.Background(), handlers.CreateTabloInput{
+ OwnerID: authUser.ID,
+ Name: "Project " + string(rune('A'+i)),
+ Status: handlers.TabloStatusTodo,
+ }); err != nil {
+ t.Fatalf("expected tablo creation to succeed, got error: %v", err)
+ }
+ }
+
+ form := url.Values{}
+ form.Set("email", "demo@xtablo.com")
+ form.Set("password", "xtablo-demo")
+
+ loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
+ loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ loginRec := httptest.NewRecorder()
+
+ router := newRouterWithHandler(handlers.NewAuthHandler(repo))
+ router.ServeHTTP(loginRec, loginReq)
+
+ sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
+ if sessionCookie == nil {
+ t.Fatalf("expected session cookie to be set")
+ }
+
+ req := httptest.NewRequest(http.MethodGet, "/?show_projects=all", nil)
+ req.Header.Set("HX-Request", "true")
+ req.Header.Set("HX-Target", "section#overview-projects-section")
+ req.AddCookie(sessionCookie)
+ rec := httptest.NewRecorder()
+ router.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected status 200, got %d", rec.Code)
+ }
+
+ body := rec.Body.String()
+ if count := strings.Count(body, `class="project-card"`); count != 8 {
+ t.Fatalf("expected 8 visible project cards after expansion, got %d", count)
+ }
+ if !strings.Contains(body, `id="overview-projects-section"`) {
+ t.Fatalf("expected section swap root in response, got %q", body)
+ }
+ if strings.Contains(body, `id="app-main-content"`) {
+ t.Fatalf("expected projects section response, got main content swap %q", body)
+ }
+ if strings.Contains(body, `Voir 2 de plus`) {
+ t.Fatalf("expected see more button to disappear after expansion, got %q", body)
+ }
+ if strings.Contains(body, `class="sidebar-nav-shell"`) {
+ t.Fatalf("expected projects section swap to avoid rerendering the full dashboard shell")
+ }
+}
+
func TestTasksPageReturnsHTMXMainContentSwap(t *testing.T) {
form := url.Values{}
form.Set("email", "demo@xtablo.com")
@@ -283,6 +479,116 @@ func TestTasksPageReturnsHTMXMainContentSwap(t *testing.T) {
}
}
+func TestTablosPageRendersFullDashboardPage(t *testing.T) {
+ form := url.Values{}
+ form.Set("email", "demo@xtablo.com")
+ form.Set("password", "xtablo-demo")
+
+ loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
+ loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ loginRec := httptest.NewRecorder()
+
+ router := newTestRouter()
+ router.ServeHTTP(loginRec, loginReq)
+
+ sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
+ if sessionCookie == nil {
+ t.Fatalf("expected session cookie to be set")
+ }
+
+ req := httptest.NewRequest(http.MethodGet, "/tablos", nil)
+ req.AddCookie(sessionCookie)
+ rec := httptest.NewRecorder()
+ router.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected status 200, got %d", rec.Code)
+ }
+
+ body := rec.Body.String()
+ for _, want := range []string{
+ `class="sidebar-nav-shell"`,
+ `id="app-main-content" class="flex-1 overflow-auto"`,
+ "Mes Projets",
+ "Nouveau projet",
+ "Vue en grille",
+ "Rechercher...",
+ } {
+ if !strings.Contains(body, want) {
+ t.Fatalf("expected tablos page to contain %q", want)
+ }
+ }
+}
+
+func TestTablosPageReturnsHTMXMainContentSwap(t *testing.T) {
+ form := url.Values{}
+ form.Set("email", "demo@xtablo.com")
+ form.Set("password", "xtablo-demo")
+
+ loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
+ loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ loginRec := httptest.NewRecorder()
+
+ router := newTestRouter()
+ router.ServeHTTP(loginRec, loginReq)
+
+ sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
+ if sessionCookie == nil {
+ t.Fatalf("expected session cookie to be set")
+ }
+
+ req := httptest.NewRequest(http.MethodGet, "/tablos?view=list&status=all", nil)
+ req.Header.Set("HX-Request", "true")
+ req.AddCookie(sessionCookie)
+ rec := httptest.NewRecorder()
+ router.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected status 200, got %d", rec.Code)
+ }
+
+ body := rec.Body.String()
+ for _, want := range []string{
+ `id="app-main-content"`,
+ `hx-swap-oob="outerHTML"`,
+ `id="sidebar-nav-tablos"`,
+ "Mes Projets",
+ "Vue en liste",
+ } {
+ if !strings.Contains(body, want) {
+ t.Fatalf("expected HTMX tablos response to contain %q", want)
+ }
+ }
+ if strings.Contains(body, `class="sidebar-nav-shell"`) {
+ t.Fatalf("expected HTMX tablos response to avoid rerendering the full sidebar")
+ }
+}
+
+func TestTablosPageUtilityStylesExist(t *testing.T) {
+ content, err := os.ReadFile("static/tailwind.css")
+ if err != nil {
+ t.Fatalf("read tailwind.css: %v", err)
+ }
+
+ css := string(content)
+ for _, want := range []string{
+ ".flex-1",
+ ".overflow-auto",
+ ".text-2xl",
+ ".bg-purple-600",
+ ".grid-cols-1",
+ ".rounded-xl",
+ ".md\\:flex-row",
+ ".sm\\:grid-cols-2",
+ ".lg\\:grid-cols-3",
+ ".xl\\:grid-cols-4",
+ } {
+ if !strings.Contains(css, want) {
+ t.Fatalf("expected tailwind.css to contain utility %q", want)
+ }
+ }
+}
+
func TestSignupCreatesUserSessionAndRedirects(t *testing.T) {
form := url.Values{}
form.Set("email", "new@xtablo.com")
diff --git a/go-backend/static/styles.css b/go-backend/static/styles.css
index 38689b5..0ced644 100644
--- a/go-backend/static/styles.css
+++ b/go-backend/static/styles.css
@@ -1017,19 +1017,510 @@ input {
color: #16a34a;
}
-.project-delete-button {
+.ui-button {
+ align-items: center;
+ border: 0;
+ border-radius: 0.75rem;
+ cursor: pointer;
+ display: inline-flex;
+ font-weight: 600;
+ gap: 0.5rem;
+ justify-content: center;
+ line-height: 1;
+ min-height: 44px;
+ text-decoration: none;
+ transition:
+ background-color 0.2s ease,
+ color 0.2s ease,
+ box-shadow 0.2s ease,
+ opacity 0.2s ease;
+}
+
+.ui-button-icon,
+.ui-button-icon svg {
+ height: 1rem;
+ width: 1rem;
+}
+
+.ui-button:focus-visible,
+.ui-icon-button:focus-visible,
+.borderless-icon-button:focus-visible {
+ box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.2);
+ outline: none;
+}
+
+.ui-button-sm {
+ font-size: 0.875rem;
+ min-height: 40px;
+ padding: 0.625rem 0.9rem;
+}
+
+.ui-button-md {
+ font-size: 0.95rem;
+ padding: 0.75rem 1.1rem;
+}
+
+.ui-button-lg {
+ font-size: 1rem;
+ padding: 0.9rem 1.25rem;
+}
+
+.ui-button-primary {
+ background: var(--secondary);
+ color: #fff;
+}
+
+.ui-button-primary:hover {
+ background: #6d28d9;
+}
+
+.ui-button-secondary {
+ background: #f3f4f6;
+ color: #111827;
+}
+
+.ui-button-secondary:hover {
+ background: #e5e7eb;
+}
+
+.ui-button-ghost {
+ background: transparent;
+ color: #4b5563;
+}
+
+.ui-button-ghost:hover {
+ background: #f9fafb;
+ color: #111827;
+}
+
+.ui-button-danger {
+ background: #dc2626;
+ color: #fff;
+}
+
+.ui-button-danger:hover {
+ background: #b91c1c;
+}
+
+.ui-badge {
+ border: 1px solid transparent;
+ border-radius: 999px;
+ display: inline-flex;
+ font-size: 0.75rem;
+ font-weight: 600;
+ line-height: 1.2;
+ padding: 0.3rem 0.75rem;
+}
+
+.ui-badge-info {
+ background: #eff6ff;
+ border-color: #bfdbfe;
+ color: #2563eb;
+}
+
+.ui-badge-warning {
+ background: #fff4e2;
+ border-color: #db9729;
+ color: #db9729;
+}
+
+.ui-badge-success {
+ background: #ecfdf3;
+ border-color: #bbf7d0;
+ color: #16a34a;
+}
+
+.ui-badge-danger {
+ background: #fef2f2;
+ border-color: #fecaca;
+ color: #dc2626;
+}
+
+.ui-input,
+.ui-textarea {
+ appearance: none;
+ background: #fff;
+ border: 1px solid #eaecf0;
+ border-radius: 0.75rem;
+ color: #111827;
+ font: inherit;
+ line-height: 1.4;
+ width: 100%;
+}
+
+.ui-input {
+ min-height: 44px;
+ padding: 0.75rem 0.95rem;
+}
+
+.ui-textarea {
+ min-height: 7rem;
+ padding: 0.85rem 0.95rem;
+ resize: vertical;
+}
+
+.ui-input::placeholder,
+.ui-textarea::placeholder {
+ color: #9ca3af;
+}
+
+.ui-input:focus,
+.ui-textarea:focus {
+ border-color: #8b5cf6;
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
+ outline: none;
+}
+
+.ui-form-field {
+ display: grid;
+ gap: 0.5rem;
+}
+
+.ui-form-label {
+ color: #111827;
+ font-size: 0.95rem;
+ font-weight: 600;
+}
+
+.ui-form-hint {
+ color: #6b7280;
+ font-size: 0.875rem;
+ margin: 0;
+}
+
+.ui-form-error {
+ color: #dc2626;
+ font-size: 0.875rem;
+ margin: 0;
+}
+
+.ui-card {
+ background: #fff;
+ border: 1px solid #eaecf0;
+ border-radius: 1rem;
+ box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
+}
+
+.ui-card-header,
+.ui-card-body,
+.ui-card-footer {
+ padding: 1.25rem 1.5rem;
+}
+
+.ui-card-header,
+.ui-card-footer {
+ border-color: #eaecf0;
+}
+
+.ui-card-header {
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+}
+
+.ui-card-footer {
+ border-top-style: solid;
+ border-top-width: 1px;
+}
+
+.ui-table-shell {
+ overflow-x: auto;
+ width: 100%;
+}
+
+.ui-table {
+ border-collapse: collapse;
+ min-width: 100%;
+ width: 100%;
+}
+
+.ui-empty-state {
+ align-items: center;
+ border: 1px dashed #d0d5dd;
+ border-radius: 1rem;
+ color: #6b7280;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ justify-content: center;
+ padding: 3rem 1.5rem;
+ text-align: center;
+}
+
+.ui-empty-state-title {
+ color: #111827;
+ font-size: 1.125rem;
+ font-weight: 700;
+ margin: 0;
+}
+
+.ui-empty-state-icon {
+ align-items: center;
+ background: #f3f4f6;
+ border-radius: 999px;
+ color: #9ca3af;
+ display: inline-flex;
+ height: 4rem;
+ justify-content: center;
+ width: 4rem;
+}
+
+.ui-empty-state-icon svg {
+ height: 2rem;
+ width: 2rem;
+}
+
+.ui-empty-state-description {
+ margin: 0;
+ max-width: 32rem;
+}
+
+.catalog-page {
+ margin: 0 auto;
+ max-width: 72rem;
+ padding: 3rem 1.5rem 4rem;
+}
+
+.catalog-nav {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.catalog-home-link,
+.catalog-nav-link {
+ border-radius: 999px;
+ color: #6b7280;
+ display: inline-flex;
+ font-size: 0.9rem;
+ font-weight: 600;
+ padding: 0.55rem 0.9rem;
+ transition:
+ background-color 0.2s ease,
+ color 0.2s ease;
+}
+
+.catalog-home-link:hover,
+.catalog-nav-link:hover {
+ background: #f3f4f6;
+ color: #111827;
+}
+
+.catalog-nav-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.catalog-nav-link.is-active {
+ background: #ede9fe;
+ color: #6d28d9;
+}
+
+.catalog-page-header {
+ margin-bottom: 2rem;
+}
+
+.catalog-page-header h1 {
+ color: #111827;
+ font-size: 2.25rem;
+ line-height: 1.1;
+ margin: 0 0 0.75rem;
+}
+
+.catalog-page-header p {
+ color: #6b7280;
+ margin: 0;
+ max-width: 42rem;
+}
+
+.catalog-eyebrow {
+ color: #7c3aed !important;
+ font-size: 0.8rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ margin-bottom: 0.75rem !important;
+ text-transform: uppercase;
+}
+
+.catalog-example-list,
+.catalog-page-list {
+ display: grid;
+ gap: 1.25rem;
+}
+
+.catalog-example,
+.catalog-page-link-card {
+ background: #fff;
+ border: 1px solid #eaecf0;
+ border-radius: 1rem;
+ box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05);
+ padding: 1.5rem;
+}
+
+.catalog-page-link-card {
+ display: block;
+}
+
+.catalog-example-copy h2,
+.catalog-page-link-card h2 {
+ color: #111827;
+ font-size: 1.125rem;
+ margin: 0 0 0.5rem;
+}
+
+.catalog-example-copy p,
+.catalog-page-link-card p {
+ color: #6b7280;
+ margin: 0;
+}
+
+.catalog-example-preview {
+ align-items: flex-start;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-top: 1rem;
+}
+
+.catalog-inline {
+ display: inline-flex;
+}
+
+.catalog-example-snippet {
+ background: #111827;
+ border-radius: 0.875rem;
+ color: #f9fafb;
+ margin: 1rem 0 0;
+ overflow-x: auto;
+ padding: 1rem;
+}
+
+.catalog-example-snippet code {
+ font-family:
+ ui-monospace,
+ SFMono-Regular,
+ "SF Mono",
+ Menlo,
+ Monaco,
+ Consolas,
+ "Liberation Mono",
+ monospace;
+ font-size: 0.875rem;
+}
+
+.catalog-page-link {
+ color: #7c3aed !important;
+ font-family:
+ ui-monospace,
+ SFMono-Regular,
+ "SF Mono",
+ Menlo,
+ Monaco,
+ Consolas,
+ "Liberation Mono",
+ monospace;
+ font-size: 0.875rem;
+ margin-top: 1rem !important;
+}
+
+.ui-icon-button {
+ align-items: center;
+ appearance: none;
background: transparent;
border: 0;
+ border-radius: 0.5rem;
+ color: #6b7280;
+ cursor: pointer;
+ display: inline-flex;
+ justify-content: center;
+ min-height: 44px;
+ min-width: 44px;
+ padding: 0.5rem;
+ transition:
+ background-color 0.2s ease,
+ color 0.2s ease;
+}
+
+.ui-icon-button:hover {
+ background: #f9fafb;
+ color: #111827;
+}
+
+.ui-modal-backdrop {
+ align-items: center;
+ background: rgba(17, 24, 39, 0.52);
+ display: flex;
+ inset: 0;
+ justify-content: center;
+ padding: 1rem;
+ position: fixed;
+ z-index: 40;
+}
+
+.ui-modal-panel {
+ background: #fff;
+ border: 1px solid #eaecf0;
+ border-radius: 1rem;
+ box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);
+ max-width: 32rem;
+ width: min(100%, 32rem);
+}
+
+.ui-modal-header,
+.ui-modal-body,
+.ui-modal-actions {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+}
+
+.ui-modal-header {
+ border-bottom: 1px solid #eaecf0;
+ padding-bottom: 1rem;
+ padding-top: 1.25rem;
+}
+
+.ui-modal-header h2 {
+ color: #111827;
+ font-size: 1.125rem;
+ font-weight: 700;
+ margin: 0;
+}
+
+.ui-modal-body {
+ padding-bottom: 1.25rem;
+ padding-top: 1.25rem;
+}
+
+.ui-modal-actions {
+ border-top: 1px solid #eaecf0;
+ display: flex;
+ gap: 0.75rem;
+ justify-content: flex-end;
+ padding-bottom: 1rem;
+ padding-top: 1rem;
+}
+
+.borderless-icon-button {
+ background: transparent;
+ border: 0;
+ box-shadow: none;
+ appearance: none;
color: #9ca3af;
cursor: pointer;
+ outline: none;
+}
+
+.project-card-top .borderless-icon-button {
padding: 0;
}
-.project-delete-button:hover {
+.project-card-top .borderless-icon-button:hover {
color: #ef4444;
}
-.project-delete-button svg,
+.borderless-icon-button svg,
.project-date-row svg,
.overview-more-button svg,
.tasks-add-button svg,
@@ -1038,6 +1529,22 @@ input {
width: 1rem;
}
+td.text-right .borderless-icon-button {
+ align-items: center;
+ border-radius: 0.25rem;
+ color: #9ca3af;
+ display: inline-flex;
+ justify-content: center;
+ min-height: 44px;
+ min-width: 44px;
+ padding: 0.5rem;
+ transition: color 0.2s;
+}
+
+td.text-right .borderless-icon-button:hover {
+ color: #ef4444;
+}
+
.project-card-title-row {
align-items: center;
display: flex;
diff --git a/go-backend/static/tailwind.css b/go-backend/static/tailwind.css
new file mode 100644
index 0000000..6314adb
--- /dev/null
+++ b/go-backend/static/tailwind.css
@@ -0,0 +1,833 @@
+/*! tailwindcss v4.1.15 | MIT License | https://tailwindcss.com */
+@layer properties;
+:root, :host {
+ --color-red-50: oklch(97.1% 0.013 17.38);
+ --color-red-200: oklch(88.5% 0.062 18.334);
+ --color-red-700: oklch(50.5% 0.213 27.518);
+ --color-green-50: oklch(98.2% 0.018 155.826);
+ --color-green-200: oklch(92.5% 0.084 155.995);
+ --color-green-400: oklch(79.2% 0.209 151.711);
+ --color-green-500: oklch(72.3% 0.219 149.579);
+ --color-green-600: oklch(62.7% 0.194 149.214);
+ --color-green-800: oklch(44.8% 0.119 151.328);
+ --color-green-950: oklch(26.6% 0.065 152.934);
+ --color-cyan-500: oklch(71.5% 0.143 215.221);
+ --color-blue-50: oklch(97% 0.014 254.604);
+ --color-blue-200: oklch(88.2% 0.059 254.128);
+ --color-blue-400: oklch(70.7% 0.165 254.624);
+ --color-blue-500: oklch(62.3% 0.214 259.815);
+ --color-blue-600: oklch(54.6% 0.245 262.881);
+ --color-blue-800: oklch(42.4% 0.199 265.638);
+ --color-blue-950: oklch(28.2% 0.091 267.935);
+ --color-purple-50: oklch(97.7% 0.014 308.299);
+ --color-purple-400: oklch(71.4% 0.203 305.504);
+ --color-purple-500: oklch(62.7% 0.265 303.9);
+ --color-purple-600: oklch(55.8% 0.288 302.321);
+ --color-purple-950: oklch(29.1% 0.149 302.717);
+ --color-gray-50: oklch(98.5% 0.002 247.839);
+ --color-gray-100: oklch(96.7% 0.003 264.542);
+ --color-gray-200: oklch(92.8% 0.006 264.531);
+ --color-gray-300: oklch(87.2% 0.01 258.338);
+ --color-gray-400: oklch(70.7% 0.022 261.325);
+ --color-gray-500: oklch(55.1% 0.027 264.364);
+ --color-gray-600: oklch(44.6% 0.03 256.802);
+ --color-gray-700: oklch(37.3% 0.034 259.733);
+ --color-gray-800: oklch(27.8% 0.033 256.848);
+ --color-gray-900: oklch(21% 0.034 264.665);
+ --color-white: #fff;
+ --spacing: 0.25rem;
+ --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-2xl: 1.5rem;
+ --text-2xl--line-height: calc(2 / 1.5);
+ --font-weight-medium: 500;
+ --font-weight-semibold: 600;
+ --font-weight-bold: 700;
+ --tracking-wider: 0.05em;
+ --radius-lg: 0.5rem;
+ --radius-xl: 0.75rem;
+ --default-transition-duration: 150ms;
+ --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+.pointer-events-none {
+ pointer-events: none;
+}
+.visible {
+ visibility: visible;
+}
+.absolute {
+ position: absolute;
+}
+.relative {
+ position: relative;
+}
+.static {
+ position: static;
+}
+.top-1\/2 {
+ top: calc(1/2 * 100%);
+}
+.left-3 {
+ left: calc(var(--spacing) * 3);
+}
+.isolate {
+ isolation: isolate;
+}
+.-mx-4 {
+ margin-inline: calc(var(--spacing) * -4);
+}
+.mb-1 {
+ margin-bottom: calc(var(--spacing) * 1);
+}
+.mb-6 {
+ margin-bottom: calc(var(--spacing) * 6);
+}
+.mb-8 {
+ margin-bottom: calc(var(--spacing) * 8);
+}
+.flex {
+ display: flex;
+}
+.grid {
+ display: grid;
+}
+.hidden {
+ display: none;
+}
+.inline {
+ display: inline;
+}
+.table {
+ display: table;
+}
+.size-10 {
+ width: calc(var(--spacing) * 10);
+ height: calc(var(--spacing) * 10);
+}
+.size-11 {
+ width: calc(var(--spacing) * 11);
+ height: calc(var(--spacing) * 11);
+}
+.size-12 {
+ width: calc(var(--spacing) * 12);
+ height: calc(var(--spacing) * 12);
+}
+.size-13 {
+ width: calc(var(--spacing) * 13);
+ height: calc(var(--spacing) * 13);
+}
+.size-14 {
+ width: calc(var(--spacing) * 14);
+ height: calc(var(--spacing) * 14);
+}
+.size-15 {
+ width: calc(var(--spacing) * 15);
+ height: calc(var(--spacing) * 15);
+}
+.size-16 {
+ width: calc(var(--spacing) * 16);
+ height: calc(var(--spacing) * 16);
+}
+.size-18 {
+ width: calc(var(--spacing) * 18);
+ height: calc(var(--spacing) * 18);
+}
+.size-20 {
+ width: calc(var(--spacing) * 20);
+ height: calc(var(--spacing) * 20);
+}
+.h-2 {
+ height: calc(var(--spacing) * 2);
+}
+.h-4 {
+ height: calc(var(--spacing) * 4);
+}
+.h-5 {
+ height: calc(var(--spacing) * 5);
+}
+.h-8 {
+ height: calc(var(--spacing) * 8);
+}
+.w-4 {
+ width: calc(var(--spacing) * 4);
+}
+.w-5 {
+ width: calc(var(--spacing) * 5);
+}
+.w-8 {
+ width: calc(var(--spacing) * 8);
+}
+.w-12 {
+ width: calc(var(--spacing) * 12);
+}
+.w-full {
+ width: 100%;
+}
+.min-w-\[80px\] {
+ min-width: 80px;
+}
+.flex-1 {
+ flex: 1;
+}
+.shrink-0 {
+ flex-shrink: 0;
+}
+.-translate-y-1\/2 {
+ --tw-translate-y: calc(calc(1/2 * 100%) * -1);
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+}
+.cursor-pointer {
+ cursor: pointer;
+}
+.grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+.flex-col {
+ flex-direction: column;
+}
+.flex-wrap {
+ flex-wrap: wrap;
+}
+.items-center {
+ align-items: center;
+}
+.justify-center {
+ justify-content: center;
+}
+.justify-end {
+ justify-content: flex-end;
+}
+.gap-1\.5 {
+ gap: calc(var(--spacing) * 1.5);
+}
+.gap-2 {
+ gap: calc(var(--spacing) * 2);
+}
+.gap-3 {
+ gap: calc(var(--spacing) * 3);
+}
+.gap-4 {
+ gap: calc(var(--spacing) * 4);
+}
+.gap-6 {
+ gap: calc(var(--spacing) * 6);
+}
+.truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.overflow-auto {
+ overflow: auto;
+}
+.overflow-hidden {
+ overflow: hidden;
+}
+.overflow-x-auto {
+ overflow-x: auto;
+}
+.rounded-\[8px\] {
+ border-radius: 8px;
+}
+.rounded-full {
+ border-radius: calc(infinity * 1px);
+}
+.rounded-lg {
+ border-radius: var(--radius-lg);
+}
+.rounded-xl {
+ border-radius: var(--radius-xl);
+}
+.border {
+ border-style: var(--tw-border-style);
+ border-width: 1px;
+}
+.border-t {
+ border-top-style: var(--tw-border-style);
+ border-top-width: 1px;
+}
+.border-b {
+ border-bottom-style: var(--tw-border-style);
+ border-bottom-width: 1px;
+}
+.border-b-2 {
+ border-bottom-style: var(--tw-border-style);
+ border-bottom-width: 2px;
+}
+.border-\[\#DB9729\] {
+ border-color: #DB9729;
+}
+.border-\[\#EAECF0\] {
+ border-color: #EAECF0;
+}
+.border-blue-200 {
+ border-color: var(--color-blue-200);
+}
+.border-green-200 {
+ border-color: var(--color-green-200);
+}
+.border-purple-600 {
+ border-color: var(--color-purple-600);
+}
+.border-red-200 {
+ border-color: var(--color-red-200);
+}
+.border-transparent {
+ border-color: transparent;
+}
+.bg-\[\#FFF4E2\] {
+ background-color: #FFF4E2;
+}
+.bg-blue-50 {
+ background-color: var(--color-blue-50);
+}
+.bg-blue-500 {
+ background-color: var(--color-blue-500);
+}
+.bg-cyan-500 {
+ background-color: var(--color-cyan-500);
+}
+.bg-gray-50 {
+ background-color: var(--color-gray-50);
+}
+.bg-gray-200 {
+ background-color: var(--color-gray-200);
+}
+.bg-green-50 {
+ background-color: var(--color-green-50);
+}
+.bg-green-500 {
+ background-color: var(--color-green-500);
+}
+.bg-purple-50 {
+ background-color: var(--color-purple-50);
+}
+.bg-purple-500 {
+ background-color: var(--color-purple-500);
+}
+.bg-purple-600 {
+ background-color: var(--color-purple-600);
+}
+.bg-red-50 {
+ background-color: var(--color-red-50);
+}
+.bg-white {
+ background-color: var(--color-white);
+}
+.px-4 {
+ padding-inline: calc(var(--spacing) * 4);
+}
+.px-6 {
+ padding-inline: calc(var(--spacing) * 6);
+}
+.py-2\.5 {
+ padding-block: calc(var(--spacing) * 2.5);
+}
+.py-3 {
+ padding-block: calc(var(--spacing) * 3);
+}
+.py-4 {
+ padding-block: calc(var(--spacing) * 4);
+}
+.pt-8 {
+ padding-top: calc(var(--spacing) * 8);
+}
+.pr-4 {
+ padding-right: calc(var(--spacing) * 4);
+}
+.pb-3 {
+ padding-bottom: calc(var(--spacing) * 3);
+}
+.pb-6 {
+ padding-bottom: calc(var(--spacing) * 6);
+}
+.pl-10 {
+ padding-left: calc(var(--spacing) * 10);
+}
+.text-left {
+ text-align: left;
+}
+.text-right {
+ text-align: right;
+}
+.text-2xl {
+ font-size: var(--text-2xl);
+ line-height: var(--tw-leading, var(--text-2xl--line-height));
+}
+.text-sm {
+ font-size: var(--text-sm);
+ line-height: var(--tw-leading, var(--text-sm--line-height));
+}
+.text-xs {
+ font-size: var(--text-xs);
+ line-height: var(--tw-leading, var(--text-xs--line-height));
+}
+.font-bold {
+ --tw-font-weight: var(--font-weight-bold);
+ font-weight: var(--font-weight-bold);
+}
+.font-medium {
+ --tw-font-weight: var(--font-weight-medium);
+ font-weight: var(--font-weight-medium);
+}
+.font-semibold {
+ --tw-font-weight: var(--font-weight-semibold);
+ font-weight: var(--font-weight-semibold);
+}
+.tracking-wider {
+ --tw-tracking: var(--tracking-wider);
+ letter-spacing: var(--tracking-wider);
+}
+.whitespace-nowrap {
+ white-space: nowrap;
+}
+.text-\[\#DB9729\] {
+ color: #DB9729;
+}
+.text-blue-600 {
+ color: var(--color-blue-600);
+}
+.text-gray-400 {
+ color: var(--color-gray-400);
+}
+.text-gray-500 {
+ color: var(--color-gray-500);
+}
+.text-gray-600 {
+ color: var(--color-gray-600);
+}
+.text-gray-700 {
+ color: var(--color-gray-700);
+}
+.text-gray-900 {
+ color: var(--color-gray-900);
+}
+.text-green-600 {
+ color: var(--color-green-600);
+}
+.text-purple-600 {
+ color: var(--color-purple-600);
+}
+.text-red-700 {
+ color: var(--color-red-700);
+}
+.text-white {
+ color: var(--color-white);
+}
+.uppercase {
+ text-transform: uppercase;
+}
+.placeholder-gray-400 {
+ &::placeholder {
+ color: var(--color-gray-400);
+ }
+}
+.filter {
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
+}
+.transition-all {
+ transition-property: all;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+}
+.transition-colors {
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+}
+.hover\:bg-gray-50 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-gray-50);
+ }
+ }
+}
+.hover\:text-gray-700 {
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-gray-700);
+ }
+ }
+}
+.focus\:ring-2 {
+ &:focus {
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
+}
+.focus\:ring-purple-500 {
+ &:focus {
+ --tw-ring-color: var(--color-purple-500);
+ }
+}
+.focus\:outline-none {
+ &:focus {
+ --tw-outline-style: none;
+ outline-style: none;
+ }
+}
+.sm\:mx-0 {
+ @media (width >= 40rem) {
+ margin-inline: calc(var(--spacing) * 0);
+ }
+}
+.sm\:grid-cols-2 {
+ @media (width >= 40rem) {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+.sm\:gap-6 {
+ @media (width >= 40rem) {
+ gap: calc(var(--spacing) * 6);
+ }
+}
+.md\:w-\[350px\] {
+ @media (width >= 48rem) {
+ width: 350px;
+ }
+}
+.md\:flex-row {
+ @media (width >= 48rem) {
+ flex-direction: row;
+ }
+}
+.md\:items-center {
+ @media (width >= 48rem) {
+ align-items: center;
+ }
+}
+.md\:justify-between {
+ @media (width >= 48rem) {
+ justify-content: space-between;
+ }
+}
+.lg\:grid-cols-3 {
+ @media (width >= 64rem) {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+}
+.xl\:grid-cols-4 {
+ @media (width >= 80rem) {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ }
+}
+.dark\:border-blue-800 {
+ &:is(.dark *) {
+ border-color: var(--color-blue-800);
+ }
+}
+.dark\:border-gray-700 {
+ &:is(.dark *) {
+ border-color: var(--color-gray-700);
+ }
+}
+.dark\:border-green-800 {
+ &:is(.dark *) {
+ border-color: var(--color-green-800);
+ }
+}
+.dark\:border-purple-400 {
+ &:is(.dark *) {
+ border-color: var(--color-purple-400);
+ }
+}
+.dark\:bg-blue-950\/30 {
+ &:is(.dark *) {
+ background-color: color-mix(in srgb, oklch(28.2% 0.091 267.935) 30%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ & {
+ background-color: color-mix(in oklab, var(--color-blue-950) 30%, transparent);
+ }
+ }
+ }
+}
+.dark\:bg-gray-700 {
+ &:is(.dark *) {
+ background-color: var(--color-gray-700);
+ }
+}
+.dark\:bg-gray-800 {
+ &:is(.dark *) {
+ background-color: var(--color-gray-800);
+ }
+}
+.dark\:bg-gray-800\/80 {
+ &:is(.dark *) {
+ background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 80%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ & {
+ background-color: color-mix(in oklab, var(--color-gray-800) 80%, transparent);
+ }
+ }
+ }
+}
+.dark\:bg-green-950\/30 {
+ &:is(.dark *) {
+ background-color: color-mix(in srgb, oklch(26.6% 0.065 152.934) 30%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ & {
+ background-color: color-mix(in oklab, var(--color-green-950) 30%, transparent);
+ }
+ }
+ }
+}
+.dark\:bg-purple-950\/30 {
+ &:is(.dark *) {
+ background-color: color-mix(in srgb, oklch(29.1% 0.149 302.717) 30%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ & {
+ background-color: color-mix(in oklab, var(--color-purple-950) 30%, transparent);
+ }
+ }
+ }
+}
+.dark\:text-blue-400 {
+ &:is(.dark *) {
+ color: var(--color-blue-400);
+ }
+}
+.dark\:text-gray-100 {
+ &:is(.dark *) {
+ color: var(--color-gray-100);
+ }
+}
+.dark\:text-gray-300 {
+ &:is(.dark *) {
+ color: var(--color-gray-300);
+ }
+}
+.dark\:text-gray-400 {
+ &:is(.dark *) {
+ color: var(--color-gray-400);
+ }
+}
+.dark\:text-green-400 {
+ &:is(.dark *) {
+ color: var(--color-green-400);
+ }
+}
+.dark\:text-purple-400 {
+ &:is(.dark *) {
+ color: var(--color-purple-400);
+ }
+}
+.dark\:hover\:bg-gray-800 {
+ &:is(.dark *) {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-gray-800);
+ }
+ }
+ }
+}
+.dark\:hover\:text-gray-200 {
+ &:is(.dark *) {
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-gray-200);
+ }
+ }
+ }
+}
+.\[\&\>svg\]\:h-4 {
+ &>svg {
+ height: calc(var(--spacing) * 4);
+ }
+}
+.\[\&\>svg\]\:w-4 {
+ &>svg {
+ width: calc(var(--spacing) * 4);
+ }
+}
+.\[\&\>svg\]\:shrink-0 {
+ &>svg {
+ flex-shrink: 0;
+ }
+}
+@property --tw-translate-x {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-translate-y {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-translate-z {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-border-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+}
+@property --tw-font-weight {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-tracking {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-blur {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-brightness {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-contrast {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-grayscale {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-hue-rotate {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-invert {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-opacity {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-saturate {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-sepia {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-drop-shadow {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-drop-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-drop-shadow-alpha {
+ syntax: "
";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-drop-shadow-size {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-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;
+}
+@layer properties {
+ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
+ *, ::before, ::after, ::backdrop {
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-translate-z: 0;
+ --tw-border-style: solid;
+ --tw-font-weight: initial;
+ --tw-tracking: initial;
+ --tw-blur: initial;
+ --tw-brightness: initial;
+ --tw-contrast: initial;
+ --tw-grayscale: initial;
+ --tw-hue-rotate: initial;
+ --tw-invert: initial;
+ --tw-opacity: initial;
+ --tw-saturate: initial;
+ --tw-sepia: initial;
+ --tw-drop-shadow: initial;
+ --tw-drop-shadow-color: initial;
+ --tw-drop-shadow-alpha: 100%;
+ --tw-drop-shadow-size: initial;
+ --tw-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;
+ }
+ }
+}
diff --git a/go-backend/tailwind.input.css b/go-backend/tailwind.input.css
new file mode 100644
index 0000000..00077a6
--- /dev/null
+++ b/go-backend/tailwind.input.css
@@ -0,0 +1,28 @@
+@import "tailwindcss/theme.css";
+@import "tailwindcss/utilities.css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme {
+ --color-surface: #ffffff;
+ --color-surface-muted: #f9fafb;
+ --color-text-strong: #111827;
+ --color-text-muted: #6b7280;
+ --color-border-subtle: #eaecf0;
+ --color-primary: #7c3aed;
+ --color-primary-strong: #6d28d9;
+ --color-danger: #dc2626;
+ --color-danger-strong: #b91c1c;
+ --color-warning-bg: #fff4e2;
+ --color-warning-fg: #db9729;
+ --color-warning-border: #db9729;
+ --color-info-bg: #eff6ff;
+ --color-info-fg: #2563eb;
+ --color-info-border: #bfdbfe;
+ --color-success-bg: #ecfdf3;
+ --color-success-fg: #16a34a;
+ --color-success-border: #bbf7d0;
+}
+
+@source "./internal/web/views/**/*.templ";
+@source "./internal/web/ui/**/*.templ";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5b8c7a5..dcfb60d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -720,6 +720,15 @@ importers:
specifier: ^4.24.3
version: 4.44.0(@cloudflare/workers-types@4.20260411.1)
+ go-backend:
+ devDependencies:
+ '@tailwindcss/cli':
+ specifier: 4.1.15
+ version: 4.1.15
+ tailwindcss:
+ specifier: 4.1.15
+ version: 4.1.15
+
packages/auth-ui:
dependencies:
'@xtablo/shared':
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index f8f575a..41169ed 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,4 +1,4 @@
packages:
- 'apps/*'
+ - 'go-backend'
- 'packages/*'
-