Add space X and space Y to components
This commit is contained in:
parent
b84eff7887
commit
2e52daa81d
6 changed files with 1192 additions and 0 deletions
13
docs/design-system/spacing.html
Normal file
13
docs/design-system/spacing.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Spacing</title>
|
||||
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./spacing.html" class="catalog-nav-link is-active">Spacing</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Spacing</h1><p>Fixed horizontal and vertical spacer primitives for composing gaps between UI components.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Horizontal spacing</h2><p>Use SpaceX to insert fixed horizontal gaps between inline or row-aligned components.</p></div><div class="catalog-example-preview"><div class="catalog-spacing-row"><button type="button" class="ui-button ui-button-solid ui-button-neutral ui-button-md">Précédent</button><span class="ui-space-x ui-space-x-lg" aria-hidden="true"></span><button type="button" class="ui-button ui-button-solid ui-button-default ui-button-md">Suivant</button></div></div><pre class="catalog-example-snippet"><code>@ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG})</code></pre></section><section class="catalog-example"><div class="catalog-example-copy"><h2>Vertical spacing</h2><p>Use SpaceY to insert fixed vertical gaps between stacked blocks.</p></div><div class="catalog-example-preview"><div class="catalog-spacing-column"><section class="ui-card"><div class="ui-card-body">Bloc 1</div></section><div class="ui-space-y ui-space-y-md" aria-hidden="true"></div><section class="ui-card"><div class="ui-card-body">Bloc 2</div></section></div></div><pre class="catalog-example-snippet"><code>@ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD})</code></pre></section></div></main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,444 @@
|
|||
# Go Backend Spacing Primitives Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add fixed-size `SpaceX` and `SpaceY` primitives to the Go backend UI library, document them in the catalog, and cover them with direct UI and design-system tests.
|
||||
|
||||
**Architecture:** Implement spacing as two presentational templ primitives backed by a shared `SpacingStep` enum and helper normalization functions. Keep the component API explicit, keep layout behavior in CSS classes rather than inline styles, and update the catalog with inner wrappers so the preview container's existing `gap` does not distort the examples.
|
||||
|
||||
**Tech Stack:** Go, templ, generated templ Go files, static CSS, catalog page generation via `cmd/designsystem`, Go `testing`
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
**Create**
|
||||
|
||||
- `go-backend/internal/web/ui/space.templ`
|
||||
- New `SpaceProps`, `SpaceX`, and `SpaceY` templ primitives.
|
||||
|
||||
**Modify**
|
||||
|
||||
- `go-backend/internal/web/ui/variants.go`
|
||||
- Add `SpacingStep` constants plus `normalizedSpacingStep`, `spaceXClass`, and `spaceYClass`.
|
||||
- `go-backend/internal/web/ui/space_templ.go`
|
||||
- Generated templ output for the new spacing primitive.
|
||||
- `go-backend/internal/web/ui/ui_test.go`
|
||||
- Add direct markup tests for default and explicit spacing steps.
|
||||
- `go-backend/internal/web/ui/catalog/examples.go`
|
||||
- Add catalog examples that render real `SpaceX` and `SpaceY` markup inside zero-gap wrappers.
|
||||
- `go-backend/internal/web/ui/catalog/pages.go`
|
||||
- Register the new `spacing` catalog page.
|
||||
- `go-backend/internal/web/ui/catalog/catalog_test.go`
|
||||
- Add page registration and rendered markup assertions for the spacing page.
|
||||
- `go-backend/cmd/designsystem/main_test.go`
|
||||
- Expect `spacing.html` in generated output.
|
||||
- `go-backend/static/styles.css`
|
||||
- Add spacing utility classes and, if needed, tiny catalog helper wrappers for zero-gap preview rows/columns.
|
||||
- `docs/design-system/index.html`
|
||||
- Generated design-system index with the spacing page link.
|
||||
- `docs/design-system/tokens.html`
|
||||
- Generated page with updated catalog navigation.
|
||||
- `docs/design-system/buttons.html`
|
||||
- Generated page with updated catalog navigation.
|
||||
- `docs/design-system/badges.html`
|
||||
- Generated page with updated catalog navigation.
|
||||
- `docs/design-system/icon-buttons.html`
|
||||
- Generated page with updated catalog navigation.
|
||||
- `docs/design-system/inputs.html`
|
||||
- Generated page with updated catalog navigation.
|
||||
- `docs/design-system/form-fields.html`
|
||||
- Generated page with updated catalog navigation.
|
||||
- `docs/design-system/modals.html`
|
||||
- Generated page with updated catalog navigation.
|
||||
- `docs/design-system/tables.html`
|
||||
- Generated page with updated catalog navigation.
|
||||
- `docs/design-system/empty-states.html`
|
||||
- Generated page with updated catalog navigation.
|
||||
- `docs/design-system/cards.html`
|
||||
- Generated page with updated catalog navigation.
|
||||
- `docs/design-system/spacing.html`
|
||||
- New generated design-system page for spacing.
|
||||
|
||||
**Commands / Generated Artifacts**
|
||||
|
||||
- `cd go-backend && just generate`
|
||||
- Regenerates `internal/web/ui/space_templ.go` and refreshes CSS build outputs.
|
||||
- `cd go-backend && just design-system`
|
||||
- Regenerates `docs/design-system/*.html`.
|
||||
- `cd go-backend && go test ./internal/web/ui ./internal/web/ui/catalog ./cmd/designsystem -count=1`
|
||||
- `cd go-backend && go test ./internal/web/... ./cmd/designsystem -count=1`
|
||||
|
||||
## Task 1: Lock Down Spacing Behavior With Failing Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-backend/internal/web/ui/ui_test.go`
|
||||
- Modify: `go-backend/internal/web/ui/catalog/catalog_test.go`
|
||||
- Modify: `go-backend/cmd/designsystem/main_test.go`
|
||||
|
||||
- [ ] **Step 1: Add failing direct UI tests for spacing markup**
|
||||
|
||||
Add two tests to `go-backend/internal/web/ui/ui_test.go` that lock down the default `md` size and an explicit `xl` size.
|
||||
|
||||
```go
|
||||
func TestSpaceXRendersDefaultMediumMarkup(t *testing.T) {
|
||||
component := SpaceX(SpaceProps{})
|
||||
|
||||
html := renderToString(t, component)
|
||||
|
||||
for _, want := range []string{
|
||||
`aria-hidden="true"`,
|
||||
`class="ui-space-x ui-space-x-md"`,
|
||||
} {
|
||||
if !strings.Contains(html, want) {
|
||||
t.Fatalf("expected %q in %q", want, html)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpaceYRendersExplicitExtraLargeMarkup(t *testing.T) {
|
||||
component := SpaceY(SpaceProps{Size: SpacingStepXL})
|
||||
|
||||
html := renderToString(t, component)
|
||||
|
||||
for _, want := range []string{
|
||||
`aria-hidden="true"`,
|
||||
`class="ui-space-y ui-space-y-xl"`,
|
||||
} {
|
||||
if !strings.Contains(html, want) {
|
||||
t.Fatalf("expected %q in %q", want, html)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Extend stylesheet coverage assertions for the new classes**
|
||||
|
||||
In `TestSharedSemanticClassesExistInStylesheet`, add the spacing classes that must exist after implementation:
|
||||
|
||||
```go
|
||||
for _, want := range []string{
|
||||
`.ui-space-x-md`,
|
||||
`.ui-space-y-md`,
|
||||
} {
|
||||
if !strings.Contains(css, want) {
|
||||
t.Fatalf("expected stylesheet to contain %q", want)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add failing catalog tests for the new page and generated site output**
|
||||
|
||||
Update `go-backend/internal/web/ui/catalog/catalog_test.go` and `go-backend/cmd/designsystem/main_test.go`.
|
||||
|
||||
```go
|
||||
if _, ok := FindPage("spacing"); !ok {
|
||||
t.Fatalf("expected catalog page %q", "spacing")
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
{slug: "spacing", want: []string{`ui-space-x`, `ui-space-y`}},
|
||||
```
|
||||
|
||||
```go
|
||||
for _, name := range []string{
|
||||
"spacing.html",
|
||||
} {
|
||||
path := filepath.Join(outputDir, name)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("expected generated file %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the focused tests to verify they fail**
|
||||
|
||||
Run: `cd go-backend && go test ./internal/web/ui ./internal/web/ui/catalog ./cmd/designsystem -count=1`
|
||||
|
||||
Expected: FAIL with undefined identifiers such as `SpaceX`, `SpaceY`, `SpaceProps`, and missing `spacing` page expectations.
|
||||
|
||||
- [ ] **Step 5: Commit the failing test expectations**
|
||||
|
||||
```bash
|
||||
git add go-backend/internal/web/ui/ui_test.go go-backend/internal/web/ui/catalog/catalog_test.go go-backend/cmd/designsystem/main_test.go
|
||||
git commit -m "test: define spacing primitive expectations"
|
||||
```
|
||||
|
||||
## Task 2: Implement The Spacing Primitive And CSS Contract
|
||||
|
||||
**Files:**
|
||||
- Create: `go-backend/internal/web/ui/space.templ`
|
||||
- Modify: `go-backend/internal/web/ui/variants.go`
|
||||
- Modify: `go-backend/internal/web/ui/space_templ.go`
|
||||
- Modify: `go-backend/internal/web/ui/ui_test.go`
|
||||
- Modify: `go-backend/static/styles.css`
|
||||
|
||||
- [ ] **Step 1: Add the spacing type and helper functions**
|
||||
|
||||
Update `go-backend/internal/web/ui/variants.go` with a small shared scale and deterministic normalization:
|
||||
|
||||
```go
|
||||
type SpacingStep string
|
||||
|
||||
const (
|
||||
SpacingStepXS SpacingStep = "xs"
|
||||
SpacingStepSM SpacingStep = "sm"
|
||||
SpacingStepMD SpacingStep = "md"
|
||||
SpacingStepLG SpacingStep = "lg"
|
||||
SpacingStepXL SpacingStep = "xl"
|
||||
)
|
||||
|
||||
func normalizedSpacingStep(step SpacingStep) SpacingStep {
|
||||
switch step {
|
||||
case SpacingStepXS, SpacingStepSM, SpacingStepLG, SpacingStepXL:
|
||||
return step
|
||||
default:
|
||||
return SpacingStepMD
|
||||
}
|
||||
}
|
||||
|
||||
func spaceXClass(step SpacingStep) string {
|
||||
return "ui-space-x ui-space-x-" + string(normalizedSpacingStep(step))
|
||||
}
|
||||
|
||||
func spaceYClass(step SpacingStep) string {
|
||||
return "ui-space-y ui-space-y-" + string(normalizedSpacingStep(step))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create the templ primitive**
|
||||
|
||||
Create `go-backend/internal/web/ui/space.templ`:
|
||||
|
||||
```go
|
||||
package ui
|
||||
|
||||
type SpaceProps struct {
|
||||
Size SpacingStep
|
||||
}
|
||||
|
||||
templ SpaceX(props SpaceProps) {
|
||||
<span class={ spaceXClass(props.Size) } aria-hidden="true"></span>
|
||||
}
|
||||
|
||||
templ SpaceY(props SpaceProps) {
|
||||
<div class={ spaceYClass(props.Size) } aria-hidden="true"></div>
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the CSS classes for base behavior and all spacing steps**
|
||||
|
||||
Append the spacing contract to `go-backend/static/styles.css` near the other `ui-` primitives:
|
||||
|
||||
```css
|
||||
.ui-space-x {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ui-space-y {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ui-space-x-xs { width: 0.25rem; }
|
||||
.ui-space-x-sm { width: 0.5rem; }
|
||||
.ui-space-x-md { width: 0.75rem; }
|
||||
.ui-space-x-lg { width: 1rem; }
|
||||
.ui-space-x-xl { width: 1.5rem; }
|
||||
|
||||
.ui-space-y-xs { height: 0.25rem; }
|
||||
.ui-space-y-sm { height: 0.5rem; }
|
||||
.ui-space-y-md { height: 0.75rem; }
|
||||
.ui-space-y-lg { height: 1rem; }
|
||||
.ui-space-y-xl { height: 1.5rem; }
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Regenerate templ output**
|
||||
|
||||
Run: `cd go-backend && just generate`
|
||||
|
||||
Expected: PASS with an updated `internal/web/ui/space_templ.go` and rebuilt static CSS assets.
|
||||
|
||||
- [ ] **Step 5: Run the focused component tests to verify they pass**
|
||||
|
||||
Run: `cd go-backend && go test ./internal/web/ui -run 'TestSpace|TestSharedSemanticClassesExistInStylesheet' -count=1`
|
||||
|
||||
Expected: PASS with the new spacing tests green.
|
||||
|
||||
- [ ] **Step 6: Commit the primitive implementation**
|
||||
|
||||
```bash
|
||||
git add go-backend/internal/web/ui/variants.go go-backend/internal/web/ui/space.templ go-backend/internal/web/ui/space_templ.go go-backend/internal/web/ui/ui_test.go go-backend/static/styles.css
|
||||
git commit -m "feat: add spacing primitives"
|
||||
```
|
||||
|
||||
## Task 3: Register The Catalog Page And Refresh Generated Docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-backend/internal/web/ui/catalog/examples.go`
|
||||
- Modify: `go-backend/internal/web/ui/catalog/pages.go`
|
||||
- Modify: `go-backend/internal/web/ui/catalog/catalog_test.go`
|
||||
- Modify: `go-backend/cmd/designsystem/main_test.go`
|
||||
- Modify: `go-backend/static/styles.css`
|
||||
- Modify: `docs/design-system/index.html`
|
||||
- Modify: `docs/design-system/spacing.html`
|
||||
|
||||
- [ ] **Step 1: Register the new catalog page**
|
||||
|
||||
Update `go-backend/internal/web/ui/catalog/pages.go`:
|
||||
|
||||
```go
|
||||
{
|
||||
Slug: "spacing",
|
||||
Title: "Spacing",
|
||||
Description: "Fixed horizontal and vertical spacer primitives for composing gaps between UI components.",
|
||||
Examples: spacingExamples(),
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add real spacing examples with zero-gap inner wrappers**
|
||||
|
||||
Add `spacingExamples()` to `go-backend/internal/web/ui/catalog/examples.go`. Use inner wrappers because `catalog-example-preview` already applies `gap: 0.75rem`.
|
||||
|
||||
```go
|
||||
func spacingExamples() []Example {
|
||||
return []Example{
|
||||
{
|
||||
Title: "Horizontal spacing",
|
||||
Description: "Use SpaceX to insert fixed horizontal gaps between inline or flex-row components.",
|
||||
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
|
||||
if _, err := io.WriteString(w, `<div class="catalog-spacing-row">`); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ui.Button(ui.ButtonProps{Label: "Précédent", Variant: ui.ButtonVariantNeutral, Size: ui.SizeMD, Type: "button"}).Render(ctx, w); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG}).Render(ctx, w); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ui.Button(ui.ButtonProps{Label: "Suivant", Variant: ui.ButtonVariantDefault, Size: ui.SizeMD, Type: "button"}).Render(ctx, w); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := io.WriteString(w, `</div>`)
|
||||
return err
|
||||
}),
|
||||
Snippet: `@ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG})`,
|
||||
},
|
||||
{
|
||||
Title: "Vertical spacing",
|
||||
Description: "Use SpaceY to insert fixed vertical gaps between stacked blocks.",
|
||||
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
|
||||
if _, err := io.WriteString(w, `<div class="catalog-spacing-column">`); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ui.Card(ui.CardProps{Body: textComponent("Bloc 1")}).Render(ctx, w); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD}).Render(ctx, w); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ui.Card(ui.CardProps{Body: textComponent("Bloc 2")}).Render(ctx, w); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := io.WriteString(w, `</div>`)
|
||||
return err
|
||||
}),
|
||||
Snippet: `@ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD})`,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add tiny catalog-only helper wrappers**
|
||||
|
||||
Append these helpers to `go-backend/static/styles.css` so the previews show only spacer-controlled gaps:
|
||||
|
||||
```css
|
||||
.catalog-spacing-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.catalog-spacing-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Regenerate the design-system site**
|
||||
|
||||
Run: `cd go-backend && just design-system`
|
||||
|
||||
Expected: PASS with refreshed `docs/design-system/index.html` and a new `docs/design-system/spacing.html`.
|
||||
|
||||
- [ ] **Step 5: Run catalog and design-system tests**
|
||||
|
||||
Run: `cd go-backend && go test ./internal/web/ui/catalog ./cmd/designsystem -count=1`
|
||||
|
||||
Expected: PASS with the spacing page present in both rendered catalog output and generated static site output.
|
||||
|
||||
- [ ] **Step 6: Commit the catalog integration**
|
||||
|
||||
```bash
|
||||
git add go-backend/internal/web/ui/catalog/examples.go go-backend/internal/web/ui/catalog/pages.go go-backend/internal/web/ui/catalog/catalog_test.go go-backend/cmd/designsystem/main_test.go go-backend/static/styles.css docs/design-system/*.html
|
||||
git commit -m "feat: document spacing primitives"
|
||||
```
|
||||
|
||||
## Task 4: Final Verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-backend/internal/web/ui/space.templ`
|
||||
- Modify: `go-backend/internal/web/ui/variants.go`
|
||||
- Modify: `go-backend/internal/web/ui/catalog/examples.go`
|
||||
- Modify: `go-backend/static/styles.css`
|
||||
|
||||
- [ ] **Step 1: Run the full relevant test suite**
|
||||
|
||||
Run: `cd go-backend && go test ./internal/web/... ./cmd/designsystem -count=1`
|
||||
|
||||
Expected: PASS with:
|
||||
|
||||
- `xtablo-backend/internal/web/ui`
|
||||
- `xtablo-backend/internal/web/ui/catalog`
|
||||
- `xtablo-backend/cmd/designsystem`
|
||||
|
||||
all green.
|
||||
|
||||
- [ ] **Step 2: Inspect the final diff for spacing-only scope**
|
||||
|
||||
Run: `git diff --stat -- go-backend/internal/web/ui/space.templ go-backend/internal/web/ui/space_templ.go go-backend/internal/web/ui/variants.go go-backend/internal/web/ui/ui_test.go go-backend/internal/web/ui/catalog/examples.go go-backend/internal/web/ui/catalog/pages.go go-backend/internal/web/ui/catalog/catalog_test.go go-backend/cmd/designsystem/main_test.go go-backend/static/styles.css docs/design-system`
|
||||
|
||||
Expected: only spacing primitive, catalog, generated docs, and test files are included.
|
||||
|
||||
- [ ] **Step 3: Create the final verification commit if any follow-up polish was needed**
|
||||
|
||||
```bash
|
||||
git add go-backend/internal/web/ui/space.templ go-backend/internal/web/ui/space_templ.go go-backend/internal/web/ui/variants.go go-backend/internal/web/ui/ui_test.go go-backend/internal/web/ui/catalog/examples.go go-backend/internal/web/ui/catalog/pages.go go-backend/internal/web/ui/catalog/catalog_test.go go-backend/cmd/designsystem/main_test.go go-backend/static/styles.css docs/design-system/*.html
|
||||
git commit -m "chore: verify spacing primitives release"
|
||||
```
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage check**
|
||||
|
||||
- `SpaceX` and `SpaceY`: covered in Task 2
|
||||
- `SpacingStep` scale and `md` fallback: covered in Task 2 plus Task 1 tests
|
||||
- stylesheet classes: covered in Task 2
|
||||
- catalog page and examples: covered in Task 3
|
||||
- design-system generated output: covered in Task 3 and Task 4
|
||||
- no app call-site migrations: respected by file scope
|
||||
|
||||
**Placeholder scan**
|
||||
|
||||
- No `TODO`, `TBD`, or “appropriate handling” placeholders remain.
|
||||
- All commands are explicit.
|
||||
- All code-changing steps include concrete snippets.
|
||||
|
||||
**Type consistency**
|
||||
|
||||
- Shared type names are consistent throughout: `SpacingStep`, `SpaceProps`, `SpaceX`, `SpaceY`
|
||||
- Class names are consistent throughout: `ui-space-x`, `ui-space-y`, `ui-space-x-{step}`, `ui-space-y-{step}`
|
||||
522
docs/superpowers/plans/2026-05-10-go-backend-tablo-edit-color.md
Normal file
522
docs/superpowers/plans/2026-05-10-go-backend-tablo-edit-color.md
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
# Go Backend Tablo Edit And Color Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add Go backend `/tablos` edit support with an edit icon beside delete, persisted hex colors, create validation for color, and an edit modal that includes both a hex input and a synced native color picker.
|
||||
|
||||
**Architecture:** Extend the existing `tablos` vertical slice in place. Persist `color` through the schema, sqlc, repository, handlers, and view models; add a small modal-state expansion so HTMX can render either the create modal or a per-tablo edit modal; keep status behavior untouched and validate `color` server-side as a canonical `#RRGGBB` string.
|
||||
|
||||
**Tech Stack:** Go, chi, templ, HTMX, PostgreSQL, pgx, sqlc, Go `net/http` tests, Tailwind-generated static CSS
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
**Modify**
|
||||
|
||||
- `go-backend/router.go`
|
||||
- Register the edit-open and update routes for `/tablos/{tabloID}`.
|
||||
- `go-backend/internal/db/schema.sql`
|
||||
- Add the new `color` column for persisted tablos.
|
||||
- `go-backend/internal/db/queries.sql`
|
||||
- Extend create/list queries with `color` and add an update query.
|
||||
- `go-backend/internal/db/repository.go`
|
||||
- Pass `color` through sqlc calls and map it back into the domain record.
|
||||
- `go-backend/internal/db/sqlc/models.go`
|
||||
- Generated sqlc model updates for the new column.
|
||||
- `go-backend/internal/db/sqlc/querier.go`
|
||||
- Generated sqlc interface updates for the new query.
|
||||
- `go-backend/internal/db/sqlc/queries.sql.go`
|
||||
- Generated sqlc query bindings for create/list/update.
|
||||
- `go-backend/internal/tablos/model.go`
|
||||
- Add `Color` to the record and create/update inputs.
|
||||
- `go-backend/internal/web/handlers/auth.go`
|
||||
- Extend the repository interface with the update method if it is declared here.
|
||||
- `go-backend/internal/web/handlers/tablos.go`
|
||||
- Add hex validation, modal state handling, edit-open handler, update handler, and in-memory repository support.
|
||||
- `go-backend/internal/web/handlers/tablos_test.go`
|
||||
- Add repository and handler coverage for color persistence, modal rendering, and update behavior.
|
||||
- `go-backend/internal/web/views/tablos_view.go`
|
||||
- Expand page and card view models with color, modal kind, edit URLs, and form values.
|
||||
- `go-backend/internal/web/views/tablos.templ`
|
||||
- Add edit buttons, render color-driven styles, add create color field, and add the edit modal with hex input plus native color picker.
|
||||
- `go-backend/internal/web/views/tablos_templ.go`
|
||||
- Generated templ output after updating `tablos.templ`.
|
||||
|
||||
**Commands / Generated Artifacts**
|
||||
|
||||
- `cd go-backend && just generate`
|
||||
- Regenerates:
|
||||
- `internal/db/sqlc/*.go`
|
||||
- `internal/web/views/*_templ.go`
|
||||
- `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos' -v`
|
||||
- `cd go-backend && go test ./...`
|
||||
|
||||
## Chunk 1: Lock Down Tests And Domain Inputs
|
||||
|
||||
### Task 1: Add failing tests for color-aware create and edit behavior
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-backend/internal/web/handlers/tablos_test.go`
|
||||
- Modify: `go-backend/internal/tablos/model.go`
|
||||
|
||||
- [ ] **Step 1: Add failing in-memory repository assertions for `Color`**
|
||||
|
||||
Add tests or extend existing ones so repository fixtures assert that `CreateTabloInput` now accepts `Color` and that the returned/listed `TabloRecord` includes it.
|
||||
|
||||
```go
|
||||
created, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||
OwnerID: user.ID,
|
||||
Name: "Roadmap",
|
||||
Color: "#3B82F6",
|
||||
Status: TabloStatusTodo,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create tablo: %v", err)
|
||||
}
|
||||
if created.Color != "#3B82F6" {
|
||||
t.Fatalf("expected color to persist, got %q", created.Color)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add failing handler tests for create validation and edit modal rendering**
|
||||
|
||||
Add tests covering:
|
||||
|
||||
- create rejects missing color
|
||||
- create rejects invalid color such as `#0af`
|
||||
- edit modal renders current `name`
|
||||
- edit modal renders current hex `color`
|
||||
- edit modal renders a native color picker input with the same value
|
||||
- update rejects another user's tablo
|
||||
|
||||
```go
|
||||
if !strings.Contains(body, `type="color"`) {
|
||||
t.Fatalf("expected edit modal to render a color picker, got %q", body)
|
||||
}
|
||||
if !strings.Contains(body, `value="#3B82F6"`) {
|
||||
t.Fatalf("expected edit modal to use the current color, got %q", body)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run handler tests to verify they fail**
|
||||
|
||||
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos' -v`
|
||||
|
||||
Expected: FAIL because `Color`, edit handlers, update behavior, and modal markup do not exist yet.
|
||||
|
||||
- [ ] **Step 4: Add domain input fields needed by the tests**
|
||||
|
||||
Update `go-backend/internal/tablos/model.go` so the types can express the feature:
|
||||
|
||||
```go
|
||||
type Record struct {
|
||||
ID uuid.UUID
|
||||
OwnerID uuid.UUID
|
||||
Name string
|
||||
Color string
|
||||
Status Status
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type CreateInput struct {
|
||||
OwnerID uuid.UUID
|
||||
Name string
|
||||
Color string
|
||||
Status Status
|
||||
}
|
||||
|
||||
type UpdateInput struct {
|
||||
ID uuid.UUID
|
||||
OwnerID uuid.UUID
|
||||
Name string
|
||||
Color string
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Re-run handler tests to confirm the remaining failures are implementation failures, not compile-shape failures**
|
||||
|
||||
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos' -v`
|
||||
|
||||
Expected: FAIL, but now due to missing repository/handler/view behavior rather than missing domain fields.
|
||||
|
||||
- [ ] **Step 6: Commit the test-and-domain scaffold**
|
||||
|
||||
```bash
|
||||
git add go-backend/internal/tablos/model.go go-backend/internal/web/handlers/tablos_test.go
|
||||
git commit -m "test: define tablo color and edit behavior"
|
||||
```
|
||||
|
||||
## Chunk 2: Persist Color And Update Repository Paths
|
||||
|
||||
### Task 2: Add `color` to schema, sqlc, and repository behavior
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-backend/internal/db/schema.sql`
|
||||
- Modify: `go-backend/internal/db/queries.sql`
|
||||
- Modify: `go-backend/internal/db/repository.go`
|
||||
- Modify: `go-backend/internal/db/sqlc/models.go`
|
||||
- Modify: `go-backend/internal/db/sqlc/querier.go`
|
||||
- Modify: `go-backend/internal/db/sqlc/queries.sql.go`
|
||||
- Modify: `go-backend/internal/web/handlers/auth.go`
|
||||
- Modify: `go-backend/internal/web/handlers/tablos.go`
|
||||
|
||||
- [ ] **Step 1: Add the database column to the schema**
|
||||
|
||||
Update the `public.tablos` table definition:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS public.tablos (
|
||||
id uuid PRIMARY KEY,
|
||||
owner_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
color text NOT NULL,
|
||||
status text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz NULL
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Extend sqlc queries and add an update statement**
|
||||
|
||||
Update `go-backend/internal/db/queries.sql`:
|
||||
|
||||
```sql
|
||||
-- name: CreateTablo :one
|
||||
INSERT INTO public.tablos (
|
||||
id, owner_id, name, color, status, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, now(), now()
|
||||
)
|
||||
RETURNING id, owner_id, name, color, status, created_at, updated_at, deleted_at;
|
||||
|
||||
-- name: ListTablos :many
|
||||
SELECT id, owner_id, name, color, status, created_at, updated_at, deleted_at
|
||||
FROM public.tablos
|
||||
...
|
||||
|
||||
-- name: UpdateTablo :execrows
|
||||
UPDATE public.tablos
|
||||
SET name = $3, color = $4, updated_at = now()
|
||||
WHERE id = $1
|
||||
AND owner_id = $2
|
||||
AND deleted_at IS NULL;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Regenerate generated files**
|
||||
|
||||
Run: `cd go-backend && just generate`
|
||||
|
||||
Expected: PASS with regenerated `internal/db/sqlc/*.go` and `internal/web/views/tablos_templ.go`.
|
||||
|
||||
- [ ] **Step 4: Thread `color` through repository and repository interface code**
|
||||
|
||||
Update:
|
||||
|
||||
- `go-backend/internal/db/repository.go`
|
||||
- `go-backend/internal/web/handlers/auth.go`
|
||||
- `go-backend/internal/web/handlers/tablos.go`
|
||||
|
||||
Representative implementation:
|
||||
|
||||
```go
|
||||
row, err := r.queries.CreateTablo(ctx, sqlcdb.CreateTabloParams{
|
||||
ID: uuid.New(),
|
||||
OwnerID: input.OwnerID,
|
||||
Name: strings.TrimSpace(input.Name),
|
||||
Color: input.Color,
|
||||
Status: string(input.Status),
|
||||
})
|
||||
```
|
||||
|
||||
and:
|
||||
|
||||
```go
|
||||
func (r *PostgresAuthRepository) UpdateTablo(ctx context.Context, input tablomodel.UpdateInput) error {
|
||||
rows, err := r.queries.UpdateTablo(ctx, sqlcdb.UpdateTabloParams{
|
||||
ID: input.ID,
|
||||
OwnerID: input.OwnerID,
|
||||
Name: strings.TrimSpace(input.Name),
|
||||
Color: input.Color,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows == 0 {
|
||||
return tablomodel.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update the in-memory repository implementation**
|
||||
|
||||
Add `Color` storage in `CreateTablo` and implement `UpdateTablo` in the in-memory repository so handler tests can pass without Postgres.
|
||||
|
||||
```go
|
||||
tablo.Color = input.Color
|
||||
...
|
||||
existing.Name = strings.TrimSpace(input.Name)
|
||||
existing.Color = input.Color
|
||||
existing.UpdatedAt = time.Now().UTC()
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run handler tests to verify repository-backed failures are cleared**
|
||||
|
||||
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos' -v`
|
||||
|
||||
Expected: FAIL only on handler/view/modal behavior that still needs implementation.
|
||||
|
||||
- [ ] **Step 7: Commit persistence changes**
|
||||
|
||||
```bash
|
||||
git add go-backend/internal/db/schema.sql go-backend/internal/db/queries.sql go-backend/internal/db/repository.go go-backend/internal/db/sqlc go-backend/internal/web/handlers/auth.go go-backend/internal/web/handlers/tablos.go
|
||||
git commit -m "feat: persist go-backend tablo colors"
|
||||
```
|
||||
|
||||
## Chunk 3: Add Edit Handlers, Validation, And Modal State
|
||||
|
||||
### Task 3: Implement hex validation and edit/update request handling
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-backend/router.go`
|
||||
- Modify: `go-backend/internal/web/handlers/auth.go`
|
||||
- Modify: `go-backend/internal/web/handlers/tablos.go`
|
||||
- Modify: `go-backend/internal/web/views/tablos_view.go`
|
||||
|
||||
- [ ] **Step 1: Register the new routes**
|
||||
|
||||
Update `go-backend/router.go`:
|
||||
|
||||
```go
|
||||
mux.Get("/tablos/{tabloID}/edit", authHandler.GetEditTabloModal())
|
||||
mux.Post("/tablos/{tabloID}", authHandler.PostTabloUpdate())
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add validation helpers in `tablos.go`**
|
||||
|
||||
Add a strict hex validator and normalize accepted values:
|
||||
|
||||
```go
|
||||
var tabloColorPattern = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`)
|
||||
|
||||
func normalizeTabloColor(raw string) (string, bool) {
|
||||
color := strings.TrimSpace(raw)
|
||||
if !tabloColorPattern.MatchString(color) {
|
||||
return "", false
|
||||
}
|
||||
return strings.ToUpper(color), true
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Expand modal state beyond `ModalOpen bool`**
|
||||
|
||||
Refactor page state and view model so the page can represent:
|
||||
|
||||
- no modal
|
||||
- create modal
|
||||
- edit modal
|
||||
|
||||
Suggested shape:
|
||||
|
||||
```go
|
||||
type TabloModalKind string
|
||||
|
||||
const (
|
||||
TabloModalNone TabloModalKind = ""
|
||||
TabloModalCreate TabloModalKind = "create"
|
||||
TabloModalEdit TabloModalKind = "edit"
|
||||
)
|
||||
```
|
||||
|
||||
Carry through:
|
||||
|
||||
- active modal kind
|
||||
- form name
|
||||
- form color
|
||||
- editing tablo ID
|
||||
- error message
|
||||
|
||||
- [ ] **Step 4: Update create handling to require `color`**
|
||||
|
||||
In `PostTablos()`:
|
||||
|
||||
- parse `name` and `color`
|
||||
- reject empty/invalid `color` with `422`
|
||||
- keep the create modal open on validation failure
|
||||
- pass normalized hex color into `CreateTabloInput`
|
||||
|
||||
- [ ] **Step 5: Implement edit-open and update handlers**
|
||||
|
||||
Add:
|
||||
|
||||
- `GetEditTabloModal() http.HandlerFunc`
|
||||
- `PostTabloUpdate() http.HandlerFunc`
|
||||
|
||||
Behavior:
|
||||
|
||||
- parse `tabloID`
|
||||
- find the owner-visible tablo from `ListTablos(...)` or add a narrow lookup helper if needed
|
||||
- prefill modal values from the existing record
|
||||
- on update, validate `name` and `color`
|
||||
- call `UpdateTablo(...)`
|
||||
- re-render `/tablos` with the current `view`, `q`, and `status`
|
||||
|
||||
- [ ] **Step 6: Expose edit URLs and modal helpers from the view model**
|
||||
|
||||
Update `go-backend/internal/web/views/tablos_view.go` so it can generate:
|
||||
|
||||
- `CreateModalHref()`
|
||||
- `EditModalHref(tabloID string)`
|
||||
- `CloseModalHref()`
|
||||
- card/list-level edit request URLs preserving current query state
|
||||
|
||||
- [ ] **Step 7: Run handler tests to verify only templ rendering remains**
|
||||
|
||||
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos' -v`
|
||||
|
||||
Expected: FAIL only where rendered markup still lacks the new buttons/fields or where templ generation has not been refreshed yet.
|
||||
|
||||
- [ ] **Step 8: Commit handler and view-model logic**
|
||||
|
||||
```bash
|
||||
git add go-backend/router.go go-backend/internal/web/handlers/tablos.go go-backend/internal/web/views/tablos_view.go
|
||||
git commit -m "feat: add go-backend tablo edit handlers"
|
||||
```
|
||||
|
||||
## Chunk 4: Render Edit UI, Color Picker, And Final Verification
|
||||
|
||||
### Task 4: Update templ views for edit action, color input, and color picker
|
||||
|
||||
**Files:**
|
||||
- Modify: `go-backend/internal/web/views/tablos.templ`
|
||||
- Modify: `go-backend/internal/web/views/tablos_templ.go`
|
||||
- Modify: `go-backend/internal/web/handlers/tablos_test.go`
|
||||
|
||||
- [ ] **Step 1: Add edit icon buttons before delete in grid and list views**
|
||||
|
||||
In `go-backend/internal/web/views/tablos.templ`, add an edit button component immediately before the delete button in:
|
||||
|
||||
- `TabloGridCardWithAttrs`
|
||||
- `TabloListRow`
|
||||
|
||||
Representative shape:
|
||||
|
||||
```templ
|
||||
@ui.IconButton(ui.IconButtonProps{
|
||||
Label: "Modifier le projet",
|
||||
Icon: "pencil",
|
||||
Variant: ui.IconButtonVariantGhost,
|
||||
Type: "button",
|
||||
Attrs: templ.Attributes{
|
||||
"hx-get": tablo.EditRequestURL,
|
||||
"hx-target": "#app-main-content",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-push-url": "true",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Render stored colors in card/list accents**
|
||||
|
||||
Replace or bypass accent-token-only rendering where needed so the card uses the persisted hex color directly.
|
||||
|
||||
Representative shape:
|
||||
|
||||
```templ
|
||||
<div class="project-avatar" style={ "background-color: " + tablo.Color }>
|
||||
```
|
||||
|
||||
and:
|
||||
|
||||
```templ
|
||||
<div class="project-progress-bar" style={ "width: " + tablo.ProgressLabel + "; background-color: " + tablo.Color }>
|
||||
```
|
||||
|
||||
Keep this narrow; do not refactor unrelated styles.
|
||||
|
||||
- [ ] **Step 3: Extend the create modal with a hex color input**
|
||||
|
||||
Add a `Couleur` field to `CreateTabloModalBody` using the shared form primitives, with placeholder/example text like `#3B82F6`.
|
||||
|
||||
- [ ] **Step 4: Add the edit modal with both controls**
|
||||
|
||||
Add an edit modal body that renders:
|
||||
|
||||
- `Nom du projet`
|
||||
- a hex text input
|
||||
- a native color picker input
|
||||
|
||||
Use the same `vm.FormColor` source for both values.
|
||||
|
||||
Representative shape:
|
||||
|
||||
```templ
|
||||
@ui.FormField(ui.FormFieldProps{
|
||||
Label: "Couleur",
|
||||
For: "tablo-color",
|
||||
Field: templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
||||
return templ.Join(
|
||||
ui.Input(ui.InputProps{
|
||||
ID: "tablo-color",
|
||||
Name: "color",
|
||||
Value: vm.FormColor,
|
||||
Placeholder: "#3B82F6",
|
||||
Type: "text",
|
||||
}),
|
||||
templ.Raw(`<input type="color" name="color_picker" value="` + vm.FormColor + `" class="..." oninput="this.form.color.value=this.value">`),
|
||||
).Render(ctx, w)
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
If inline JS is undesirable inside templ, use the smallest equivalent attribute approach already accepted by the codebase. Keep the submission source canonical: the posted `color` field must still carry the validated hex value.
|
||||
|
||||
- [ ] **Step 5: Switch modal rendering by modal kind and regenerate templ**
|
||||
|
||||
Update `TablosPageContent` so it renders:
|
||||
|
||||
- create modal when modal kind is `create`
|
||||
- edit modal when modal kind is `edit`
|
||||
|
||||
Run: `cd go-backend && just generate`
|
||||
|
||||
Expected: PASS with regenerated `go-backend/internal/web/views/tablos_templ.go`.
|
||||
|
||||
- [ ] **Step 6: Run the focused handler suite**
|
||||
|
||||
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos' -v`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Run the full Go backend suite**
|
||||
|
||||
Run: `cd go-backend && go test ./...`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 8: Commit the UI and final verification**
|
||||
|
||||
```bash
|
||||
git add go-backend/internal/web/views/tablos.templ go-backend/internal/web/views/tablos_templ.go go-backend/internal/web/handlers/tablos_test.go
|
||||
git commit -m "feat: add go-backend tablo edit modal"
|
||||
```
|
||||
|
||||
## Final Verification Checklist
|
||||
|
||||
- [ ] `cd go-backend && just generate`
|
||||
- [ ] `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos' -v`
|
||||
- [ ] `cd go-backend && go test ./...`
|
||||
- [ ] Manually verify in the browser via `cd go-backend && just dev`:
|
||||
- create modal requires a valid hex color
|
||||
- grid cards show edit to the left of delete
|
||||
- list rows show edit to the left of delete
|
||||
- edit modal preloads current name and color
|
||||
- edit modal shows both hex input and color picker
|
||||
- changing the picker updates the submitted hex value
|
||||
- successful save refreshes `/tablos` and closes the modal
|
||||
|
||||
Plan complete and saved to `docs/superpowers/plans/2026-05-10-go-backend-tablo-edit-color.md`. Ready to execute?
|
||||
13
go-backend/internal/web/ui/space.templ
Normal file
13
go-backend/internal/web/ui/space.templ
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package ui
|
||||
|
||||
type SpaceProps struct {
|
||||
Size SpacingStep
|
||||
}
|
||||
|
||||
templ SpaceX(props SpaceProps) {
|
||||
<span class={ spaceXClass(props.Size) } aria-hidden="true"></span>
|
||||
}
|
||||
|
||||
templ SpaceY(props SpaceProps) {
|
||||
<div class={ spaceYClass(props.Size) } aria-hidden="true"></div>
|
||||
}
|
||||
109
go-backend/internal/web/ui/space_templ.go
Normal file
109
go-backend/internal/web/ui/space_templ.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1001
|
||||
package ui
|
||||
|
||||
//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"
|
||||
|
||||
type SpaceProps struct {
|
||||
Size SpacingStep
|
||||
}
|
||||
|
||||
func SpaceX(props SpaceProps) 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)
|
||||
var templ_7745c5c3_Var2 = []any{spaceXClass(props.Size)}
|
||||
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, 1, "<span class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/space.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" aria-hidden=\"true\"></span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func SpaceY(props SpaceProps) 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)
|
||||
var templ_7745c5c3_Var5 = []any{spaceYClass(props.Size)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/space.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, 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, 4, "\" aria-hidden=\"true\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
91
go-backend/internal/web/views/dashboard_components_test.go
Normal file
91
go-backend/internal/web/views/dashboard_components_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/google/uuid"
|
||||
tablomodel "xtablo-backend/internal/tablos"
|
||||
)
|
||||
|
||||
func TestOverviewProjectsFromTablosCarriesColorAndEditURL(t *testing.T) {
|
||||
record := tablomodel.Record{
|
||||
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
||||
Name: "Palette",
|
||||
Color: "#3B82F6",
|
||||
Status: tablomodel.StatusTodo,
|
||||
CreatedAt: time.Date(2026, time.May, 10, 9, 0, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
projects := OverviewProjectsFromTablos([]tablomodel.Record{record})
|
||||
if len(projects) != 1 {
|
||||
t.Fatalf("expected one project, got %d", len(projects))
|
||||
}
|
||||
|
||||
project := projects[0]
|
||||
if project.Color != "#3B82F6" {
|
||||
t.Fatalf("expected color to be preserved, got %q", project.Color)
|
||||
}
|
||||
if project.EditRequestURL != "/tablos/11111111-1111-1111-1111-111111111111/edit" {
|
||||
t.Fatalf("expected edit request url to be set, got %q", project.EditRequestURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverviewProjectsSectionRendersColorAndEditAction(t *testing.T) {
|
||||
record := tablomodel.Record{
|
||||
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
|
||||
Name: "Palette",
|
||||
Color: "#3B82F6",
|
||||
Status: tablomodel.StatusTodo,
|
||||
CreatedAt: time.Date(2026, time.May, 10, 9, 0, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
html := renderViewToString(t, OverviewProjectsSection(OverviewProjectsFromTablos([]tablomodel.Record{record})))
|
||||
|
||||
for _, want := range []string{
|
||||
`style="--project-color:#3B82F6;"`,
|
||||
`aria-label="Modifier le projet"`,
|
||||
`hx-get="/tablos/11111111-1111-1111-1111-111111111111/edit"`,
|
||||
} {
|
||||
if !strings.Contains(html, want) {
|
||||
t.Fatalf("expected %q in %q", want, html)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTabloListRowDoesNotRenderSpacerBetweenEditAndDelete(t *testing.T) {
|
||||
component := TabloListRow(TabloCardView{
|
||||
ID: "11111111-1111-1111-1111-111111111111",
|
||||
Name: "Palette",
|
||||
Color: "#3B82F6",
|
||||
StatusLabel: "À faire",
|
||||
StatusTone: "info",
|
||||
Progress: 0,
|
||||
ProgressLabel: "0%",
|
||||
CreatedAtLabel: "10 mai 2026",
|
||||
DeleteRequestURL: "/tablos/11111111-1111-1111-1111-111111111111",
|
||||
EditRequestURL: "/tablos/11111111-1111-1111-1111-111111111111/edit",
|
||||
IconKind: "bolt",
|
||||
Initial: "P",
|
||||
})
|
||||
|
||||
html := renderViewToString(t, component)
|
||||
|
||||
if strings.Contains(html, `ui-space-x`) {
|
||||
t.Fatalf("expected no spacer markup in list row actions, got %q", html)
|
||||
}
|
||||
}
|
||||
|
||||
func renderViewToString(t *testing.T, component templ.Component) string {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := component.Render(context.Background(), &buf); err != nil {
|
||||
t.Fatalf("render component: %v", err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
Loading…
Reference in a new issue