Add landing page for kjol, documentation

This commit is contained in:
2026-07-13 16:51:21 -04:00
parent 5230bd6702
commit fec8ef4a3e
54 changed files with 3529 additions and 547 deletions

View File

@@ -48,12 +48,12 @@ func tableColumns() []ui.AutoTableColumn {
Key: "name", DisplayName: "Name", Sortable: true, SortIdentifier: "Name",
CSV: true, CSVValue: func(r any) string { return emp(r).Name },
// No Toggleable: the name is what identifies a row, so it cannot be hidden.
Cell: func(r any) *VNode { return ui.AutoTableTdLeft("text-neutral-800", Text(emp(r).Name)) },
Cell: func(r any) *VNode { return ui.AutoTableTdLeft("text-ink", Text(emp(r).Name)) },
},
{
Key: "email", DisplayName: "Email", Sortable: true, SortIdentifier: "Email",
Toggleable: true, CSV: true, CSVValue: func(r any) string { return emp(r).Email },
Cell: func(r any) *VNode { return ui.AutoTableTdLeft("text-neutral-500", Text(emp(r).Email)) },
Cell: func(r any) *VNode { return ui.AutoTableTdLeft("text-ink-muted", Text(emp(r).Email)) },
},
{
Key: "team", DisplayName: "Team", Sortable: true, SortIdentifier: "Team",
@@ -162,7 +162,7 @@ func newEmployeeTable(highlight *Signal[string]) *ui.AutoTableState {
Accordion: true,
RowKey: func(r any) string { return emp(r).Email },
AccordionContent: func(r any) *VNode {
return P(Attr("class", "px-4 py-2 text-sm text-neutral-600"), Text(emp(r).Note))
return P(Attr("class", "px-4 py-2 text-sm text-ink-soft"), Text(emp(r).Note))
},
Columns: ui.AutoTableColumnOptions{
@@ -202,19 +202,37 @@ func TablePage(d Deps) func() *VNode {
}
return ui.AutoTablePDFHeader{
Title: "Employees",
Subtitle: "Exported from the gowasm example",
Subtitle: "Exported from the Kjol Web example",
ShowDate: true,
Orientation: orientation,
}
}
return func() *VNode {
return Div(Attr("class", "space-y-6"),
Div(
H2(Attr("class", "text-2xl font-semibold tracking-tight text-text-heading"), Text("AutoTable")),
P(Attr("class", "mt-1 text-neutral-500"),
Text("Filtering, sorting, pagination, expandable rows and column management — all in Go. "+
"Drag a header to reorder, drag its right edge to resize; both persist across reloads.")),
return docPage("Components", "AutoTable",
"A table that filters, sorts, pages, reorders, resizes, computes and exports — configured with "+
"a column list and a slice of rows. Everything a user changes about it is theirs and persists; "+
"everything it exports is what they filtered, not what happened to be on screen.",
docSection("defining", "Defining one",
prose("A column says how to read a field, how to sort it, and how to render it. The state object "+
"is a CONTROLLER: build it once, alongside your signals — never inside the render, which "+
"would hand it fresh refs and a fresh idea of which page it was on every frame."),
code("app/table.go", tableSnippet),
note("The server renders a skeleton, on purpose",
"The layout — column order, widths, what is hidden, the calculated columns — lives in the "+
"browser's localStorage, which the server cannot read. So the server ships a skeleton "+
"rather than the DEFAULT table: a user who had reordered their columns would otherwise "+
"watch them rearrange themselves the moment the WebAssembly booted."),
),
docSection("try-it", "Try it",
prose("Search matches name or email. Sort by Salary and it parses the currency, so $980 sorts "+
"below $1,200.50. Unhide Rank and sort that: \"Item 2\" comes before \"Item 10\", because "+
"numbers inside text are compared as numbers. Drag a header to reorder it, drag its right "+
"edge to resize — reload the page and both are still where you left them."),
prose("Filter it, then export. You get every matching row across every page, in the column order "+
"you dragged them into, with the calculated columns computed per row."),
),
table.Render(
@@ -270,7 +288,7 @@ func TablePage(d Deps) func() *VNode {
// Highlight + auto-page-jump: Radia is on page 3 by default, and the table
// pages itself to wherever she actually is once filters and sorting move her.
row("flex flex-wrap items-center gap-2",
row("mt-4 flex flex-wrap items-center gap-2",
ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Small: true,
Text: "Find Radia Perlman",
OnClick: func() { highlight.Set("radia@example.com") }}),
@@ -279,12 +297,59 @@ func TablePage(d Deps) func() *VNode {
OnClick: func() { highlight.Set("") }}),
),
ui.Alert(ui.AlertBlue, "What to try",
Text("Search (it matches name OR email); pick a status; select several teams. Sort by Salary — "+
"it parses the currency, so $980 sorts below $1,200.50. Unhide Rank and sort it: 'Item 2' "+
"comes before 'Item 10'. Click a row to expand it. Drag a header to reorder, drag its right "+
"edge to resize — both survive a reload. Filter the table, then export: you get every "+
"matching row, not just this page. 'Find Radia' jumps to whichever page she is on.")),
docSection("calculated", "Calculated columns",
prose("The toolbar's calculator builds new columns at runtime, in two modes. Basic picks a "+
"function and the columns it combines ACROSS each row — sum of Salary and Bonus, per "+
"person. Advanced writes a formula, with insert menus for columns, functions and constants: "+
"([Salary] + [Bonus]) * 12."),
prose("A summary row is the other axis: it aggregates ONE column DOWN the filtered rows and "+
"prints the result in the footer. Confusing the two is the classic bug here — a column that "+
"aggregates down shows every row the same number, and it looks plausible enough to ship."),
codeLang("formulas", "syntax", formulaSnippet),
note("Compiled as you type",
"The formula is parsed and evaluated against the real first row while you write it, so a "+
"typo shows up as an error under the box — not as a column of dashes discovered later."),
),
docSection("export", "Export",
prose("CSV and PDF are written in Go, standard library only — the PDF writer builds its own "+
"xref table and embeds Helvetica metrics. Export takes the FILTERED rows, the VISIBLE "+
"columns, in the user's order, including whatever they calculated."),
apiTable(
apiRow{"NewAutoTableState", "Build the controller: the columns, and where to persist the layout."},
apiRow{".SetRows", "Hand it the data. It filters, sorts and pages from there."},
apiRow{".RestoreLayout", "Read the saved layout and reveal the table over its skeleton. Call it once, on the client."},
apiRow{".FilteredRows / .ExportColumns", "What the user selected, and what they can see — the inputs to any export."},
apiRow{"ExportCSV / ExportPDF", "Write the bytes. DownloadCSV / DownloadPDF / PrintPDF do it and hand them to the browser."},
),
),
)
}
}
const tableSnippet = `// A controller: built ONCE, next to your signals — never inside the render.
table := ui.NewAutoTableState([]ui.AutoTableColumn{
{DisplayName: "Name", SortIdentifier: "Name", Sortable: true,
Cell: func(r any) *VNode { return Text(r.(Employee).Name) }},
{DisplayName: "Salary", SortIdentifier: "Salary", Sortable: true,
SortType: ui.SortTypeNumeric, // parses the currency: $980 < $1,200.50
Cell: func(r any) *VNode { return Text(money(r.(Employee).Salary)) }},
{DisplayName: "Rank", HiddenByDefault: true},
}, ui.AutoTableStateOptions{
PerPage: 5,
Columns: ui.AutoTableColumnOptions{
StorageKey: "employees", // order, widths, visibility — the user's, and persisted
},
})
table.SetRows(employees())`
const formulaSnippet = `A COLUMN combines operands ACROSS one row:
sum[Salary, Bonus] -> 1200.50 + 150.00 = 1350.50 (per person)
([Salary] + [Bonus]) * 12 -> the annualised figure
SUM({Salary:1:ROW()}) -> a running total, down the rows
A SUMMARY ROW aggregates ONE column DOWN the filtered rows:
avg[Salary] -> one number, printed in the footer`