Add landing page for kjol, documentation
This commit is contained in:
@@ -7,6 +7,14 @@ import (
|
||||
ui "kjol/webui"
|
||||
)
|
||||
|
||||
// orElse is a fallback for an empty string.
|
||||
func orElse(s, fallback string) string {
|
||||
if s == "" {
|
||||
return fallback
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// row is a flex/grid container helper (appends *VNode children as Mods).
|
||||
func row(class string, children ...*VNode) *VNode {
|
||||
mods := []Mod{Attr("class", class)}
|
||||
@@ -16,19 +24,17 @@ func row(class string, children ...*VNode) *VNode {
|
||||
return Div(mods...)
|
||||
}
|
||||
|
||||
// kitSection wraps a labeled demo block in a card.
|
||||
// kitSection is one labelled block of the gallery — a live demo panel, so that what you
|
||||
// are looking at is unmistakably the component running rather than a picture of it.
|
||||
func kitSection(title string, body ...*VNode) *VNode {
|
||||
return ui.Card("",
|
||||
ui.CardHeader("", Text(title)),
|
||||
row("flex flex-col gap-4", body...),
|
||||
)
|
||||
return demo(title, row("flex flex-col gap-4", body...))
|
||||
}
|
||||
|
||||
func ptRow(name, plan string, status *VNode) *VNode {
|
||||
td := func(cls string, c *VNode) *VNode { return El("td", Attr("class", "px-3 py-2 text-sm "+cls), c) }
|
||||
return El("tr",
|
||||
td("text-neutral-800", Text(name)),
|
||||
td("text-neutral-600", Text(plan)),
|
||||
td("text-ink", Text(name)),
|
||||
td("text-ink-soft", Text(plan)),
|
||||
El("td", Attr("class", "px-3 py-2 text-sm text-right"), status),
|
||||
)
|
||||
}
|
||||
@@ -67,14 +73,53 @@ func KitPage(d Deps) func() *VNode {
|
||||
tip := ui.NewHoverTooltip(ui.PlacementTop, "")
|
||||
skills := ui.NewMultiSelect(ui.DropdownOptions{})
|
||||
|
||||
// The controls the first port left out, now that the host API can carry them.
|
||||
taxID := NewSignal("")
|
||||
rate := NewSignal("")
|
||||
signed := NewSignal("")
|
||||
picked := NewSignal("")
|
||||
tags := NewSignal([]string{"go"})
|
||||
|
||||
pad := ui.NewSignaturePad(ui.SignaturePadOptions{
|
||||
OnChange: func(svg string) { signed.Set(svg) },
|
||||
})
|
||||
// The search is the caller's: the component knows how to debounce, order and render,
|
||||
// and nothing at all about where options come from. Here it is a local slice; in an
|
||||
// app it would be a fetch.
|
||||
people := ui.NewAsyncCombobox(ui.AsyncComboboxOptions{
|
||||
MinChars: 2,
|
||||
Search: func(q string, done func([]ui.FormSelectOption)) {
|
||||
var out []ui.FormSelectOption
|
||||
for _, row := range employees() {
|
||||
p, ok := row.(Employee)
|
||||
if ok && strings.Contains(strings.ToLower(p.Name), strings.ToLower(q)) {
|
||||
out = append(out, ui.FormSelectOption{Value: p.Email, Label: p.Name})
|
||||
}
|
||||
}
|
||||
done(out)
|
||||
},
|
||||
})
|
||||
tagPicker := ui.NewMultiSelectTrigger(ui.DropdownOptions{})
|
||||
|
||||
return func() *VNode {
|
||||
return Div(Attr("class", "space-y-8"),
|
||||
Div(
|
||||
H2(Attr("class", "text-2xl font-semibold tracking-tight text-text-heading"), Text("UI Kit")),
|
||||
P(Attr("class", "mt-1 text-neutral-500"),
|
||||
Text("The kjol/webui components, ported from the Solid.js kit and styled with Tailwind. "+
|
||||
"Interactive components are driven by signals; overlays and floating elements render "+
|
||||
"in their static form (see the note at the bottom).")),
|
||||
return docPage("Components", "UI kit",
|
||||
"kjol/webui is the component library: buttons, badges, forms, tabs, alerts, cards, tables. "+
|
||||
"It is a Go port of the Solid.js kit the applications used before, styled with the same "+
|
||||
"Tailwind utilities — so the two can be swapped for one another a screen at a time.",
|
||||
|
||||
docSection("using", "Using a component",
|
||||
prose("Components are functions taking a props struct. There is no class hierarchy and nothing "+
|
||||
"to register: a component is a value, so you can build one, store it, pass it around, and "+
|
||||
"the compiler will tell you when you get it wrong."),
|
||||
code("app/kit.go", kitSnippet),
|
||||
note("Styling is Tailwind, compiled from your Go",
|
||||
"The Tailwind engine scans .go files for class names, because that is where the markup is. "+
|
||||
"There is no JavaScript build in this example at all — the CSS is compiled by a Go "+
|
||||
"program from Go source."),
|
||||
),
|
||||
|
||||
docSection("gallery", "The gallery",
|
||||
prose("Everything below is running. Click it."),
|
||||
),
|
||||
|
||||
kitSection("Buttons",
|
||||
@@ -124,9 +169,9 @@ func KitPage(d Deps) func() *VNode {
|
||||
kitSection("Tabs",
|
||||
ui.TabGroup(ui.TabGroupProps{
|
||||
Items: []ui.TabItem{
|
||||
{Title: "Overview", Content: P(Attr("class", "pt-3 text-sm text-neutral-600"), Text("The overview panel."))},
|
||||
{Title: "Details", Content: P(Attr("class", "pt-3 text-sm text-neutral-600"), Text("The details panel."))},
|
||||
{Title: "Activity", Badge: 3, Content: P(Attr("class", "pt-3 text-sm text-neutral-600"), Text("The activity panel (3 new)."))},
|
||||
{Title: "Overview", Content: P(Attr("class", "pt-3 text-sm text-ink-soft"), Text("The overview panel."))},
|
||||
{Title: "Details", Content: P(Attr("class", "pt-3 text-sm text-ink-soft"), Text("The details panel."))},
|
||||
{Title: "Activity", Badge: 3, Content: P(Attr("class", "pt-3 text-sm text-ink-soft"), Text("The activity panel (3 new)."))},
|
||||
},
|
||||
ActiveIndex: tab.Get(),
|
||||
OnTabChange: func(i int) { tab.Set(i) },
|
||||
@@ -135,9 +180,9 @@ func KitPage(d Deps) func() *VNode {
|
||||
|
||||
kitSection("Accordion",
|
||||
ui.SingleAccordion([]ui.AccordionItemData{
|
||||
{Title: "What is gowasm?", Content: P(Attr("class", "text-sm text-neutral-600"), Text("A tiny Go→WebAssembly UI engine."))},
|
||||
{Title: "Is it isomorphic?", Content: P(Attr("class", "text-sm text-neutral-600"), Text("Yes — the same Go renders on the server (SSR) and hydrates on the client."))},
|
||||
{Title: "How is it styled?", Content: P(Attr("class", "text-sm text-neutral-600"), Text("Tailwind utility classes, compiled by kjol's native Tailwind engine."))},
|
||||
{Title: "What is Kjol Web?", Content: P(Attr("class", "text-sm text-ink-soft"), Text("kjol's Go→WebAssembly UI engine."))},
|
||||
{Title: "Is it isomorphic?", Content: P(Attr("class", "text-sm text-ink-soft"), Text("Yes — the same Go renders on the server (SSR) and hydrates on the client."))},
|
||||
{Title: "How is it styled?", Content: P(Attr("class", "text-sm text-ink-soft"), Text("Tailwind utility classes, compiled by kjol's native Tailwind engine."))},
|
||||
}, acc.Get(), func(i int) { acc.Set(i) }),
|
||||
),
|
||||
|
||||
@@ -166,11 +211,68 @@ func KitPage(d Deps) func() *VNode {
|
||||
OnChange: func(v []string) { langs.Set(v) },
|
||||
})),
|
||||
),
|
||||
P(Attr("class", "text-xs text-neutral-500"),
|
||||
P(Attr("class", "text-xs text-ink-muted"),
|
||||
Text("Live: name=\""+name.Get()+"\" email=\""+email.Get()+"\" plan=\""+plan.Get()+
|
||||
"\" languages="+strings.Join(langs.Get(), ","))),
|
||||
),
|
||||
|
||||
kitSection("Masked inputs",
|
||||
row("grid gap-4 sm:grid-cols-2",
|
||||
row("flex flex-col gap-1", ui.FormLabel(ui.FormLabelProps{}, Text("Tax ID")),
|
||||
// The mask is a pure function of the string, applied on every keystroke.
|
||||
// It must be idempotent — it is fed its own output — or the field
|
||||
// corrupts itself as you type.
|
||||
ui.FormInput(ui.FormInputProps{
|
||||
Value: taxID.Get(),
|
||||
Placeholder: "12-3456789",
|
||||
OnInput: func(v string) { taxID.Set(ui.MaskTaxID(v)) },
|
||||
})),
|
||||
row("flex flex-col gap-1", ui.FormLabel(ui.FormLabelProps{}, Text("Rate")),
|
||||
ui.FormInput(ui.FormInputProps{
|
||||
Value: rate.Get(),
|
||||
Placeholder: "5.25",
|
||||
OnInput: func(v string) { rate.Set(ui.MaskRate(v)) },
|
||||
})),
|
||||
),
|
||||
P(Attr("class", "text-xs text-ink-muted"),
|
||||
Text("Type letters, extra dots, leading zeros — the mask takes what it can use.")),
|
||||
),
|
||||
|
||||
kitSection("Async combobox",
|
||||
row("max-w-sm",
|
||||
people.Render(ui.FormAsyncComboboxProps{
|
||||
Placeholder: "Search people…",
|
||||
OnSelect: func(o ui.FormSelectOption) { picked.Set(o.Label + " <" + o.Value + ">") },
|
||||
}),
|
||||
),
|
||||
P(Attr("class", "text-xs text-ink-muted"),
|
||||
Text("Two characters before it asks; 200 ms after you stop typing. A response for a "+
|
||||
"query you have already typed past is discarded rather than shown. Picked: "+
|
||||
orElse(picked.Get(), "nothing yet"))),
|
||||
),
|
||||
|
||||
kitSection("Multi-select behind your own trigger",
|
||||
tagPicker.Render(ui.FormMultiSelectTriggerProps{
|
||||
Trigger: ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Small: true,
|
||||
Icon: "filter", Text: "Tags (" + itoa(len(tags.Get())) + ")"}),
|
||||
Options: languageOptions(),
|
||||
Value: tags.Get(),
|
||||
Searchable: true,
|
||||
ShowSelectAll: true,
|
||||
OnChange: func(v []string) { tags.Set(v) },
|
||||
}),
|
||||
P(Attr("class", "text-xs text-ink-muted"),
|
||||
Text("Same selection model as the field above; only the thing you click on differs.")),
|
||||
),
|
||||
|
||||
kitSection("Signature pad",
|
||||
pad.Render(ui.SignaturePadProps{}),
|
||||
P(Attr("class", "text-xs text-ink-muted"),
|
||||
Text("Draw in it. It is an SVG, not a canvas — so the markup you are looking at IS the "+
|
||||
"value the caller gets ("+itoa(len(signed.Get()))+" bytes), and a stored signature "+
|
||||
"renders on the server.")),
|
||||
),
|
||||
|
||||
kitSection("Table",
|
||||
ui.PrettyTable(
|
||||
[]ui.PrettyTableColumn{
|
||||
@@ -214,16 +316,40 @@ func KitPage(d Deps) func() *VNode {
|
||||
modal.Render(ui.ModalProps{
|
||||
Header: H3(Attr("class", "text-lg font-semibold text-text-heading"), Text("Example modal")),
|
||||
},
|
||||
P(Attr("class", "text-neutral-600"),
|
||||
P(Attr("class", "text-ink-soft"),
|
||||
Text("Portaled to document.body, so it is not clipped by any ancestor. Escape closes "+
|
||||
"the topmost modal; the backdrop click closes too.")),
|
||||
),
|
||||
),
|
||||
|
||||
ui.Alert(ui.AlertGreen, "About this page",
|
||||
Text("Menus, tooltips, modals and dropdowns are now really measured: they are portaled to "+
|
||||
"document.body, positioned from getBoundingClientRect against the viewport, and they "+
|
||||
"flip and shift to stay on screen. Resize the window or scroll while one is open.")),
|
||||
docSection("more", "Where to go next",
|
||||
prose("The floating components on this page — the menu, the tooltip, the modal, the "+
|
||||
"multi-select — are the shallow end. Overlays covers how they are positioned, and what "+
|
||||
"happens when one would open off the edge of the screen."),
|
||||
apiTable(
|
||||
apiRow{"ui.Button / ui.Badge / ui.Alert", "The presentational set. Props structs, no state."},
|
||||
apiRow{"ui.FormInput / FormSelect / FormCombobox", "Inputs. Value in, OnChange out — the caller owns the state."},
|
||||
apiRow{"ui.NewMultiSelect", "A controller: checkboxed rows, pills that collapse to \"N items selected\" when they stop fitting."},
|
||||
apiRow{"ui.Tabs / ui.Accordion / ui.Card", "Layout and disclosure."},
|
||||
apiRow{"ui.RegisterIcon", "Add your own icons. The kit ships a small set; the app brings the rest."},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const kitSnippet = `// A component is a function taking a props struct.
|
||||
ui.Button(ui.ButtonProps{
|
||||
Color: ui.ButtonPrimary,
|
||||
Icon: "check",
|
||||
Text: "Save",
|
||||
OnClick: func() { toaster.Success("Saved.") },
|
||||
})
|
||||
|
||||
// Inputs are controlled: the caller owns the state.
|
||||
name := NewSignal("")
|
||||
|
||||
ui.FormInput(ui.FormInputProps{
|
||||
Value: name.Get(),
|
||||
OnInput: func(v string) { name.Set(v) }, // a write re-renders
|
||||
})`
|
||||
|
||||
Reference in New Issue
Block a user