package app import ( "strings" . "kjol/vdom" 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)} for _, c := range children { mods = append(mods, c) } return Div(mods...) } // 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 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-ink", Text(name)), td("text-ink-soft", Text(plan)), El("td", Attr("class", "px-3 py-2 text-sm text-right"), status), ) } // languageOptions is deliberately longer than the pill limit, so the multi-select // demonstrates both ways it collapses: past 3 selections it says "N items selected" // outright, and below that it still collapses if the pills are too wide for the field. func languageOptions() []ui.FormSelectOption { return []ui.FormSelectOption{ {Value: "go", Label: "Go"}, {Value: "rust", Label: "Rust"}, {Value: "ts", Label: "TypeScript"}, {Value: "python", Label: "Python"}, {Value: "kotlin", Label: "Kotlin"}, {Value: "swift", Label: "Swift"}, } } //gowasm:page /wasm/kit layout=app func KitPage(d Deps) func() *VNode { // Interactive demos own their state via signals (a write re-renders). tab := NewSignal(0) acc := NewSignal(0) notify := NewSignal(true) span := NewSignal("week") name := NewSignal("") email := NewSignal("") plan := NewSignal("pro") langs := NewSignal([]string{"go"}) // Floating components are CONTROLLERS: they own refs, timers and open state, so // they are built once here — never inside the render closure below, which would // rebuild them (and lose their state) on every frame. modal := ui.NewModal(ui.ModalOptions{Size: ui.ModalMedium}) menu := ui.NewMenu(ui.MenuOptions{Placement: ui.PlacementBottomStart}) 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 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", row("flex flex-wrap items-center gap-2", ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Primary"}), ui.Button(ui.ButtonProps{Color: ui.ButtonGreen, Text: "Green"}), ui.Button(ui.ButtonProps{Color: ui.ButtonRed, Text: "Red"}), ui.Button(ui.ButtonProps{Color: ui.ButtonBlue, Text: "Blue"}), ui.Button(ui.ButtonProps{Color: ui.ButtonNeutral, Text: "Neutral"}), ), row("flex flex-wrap items-center gap-2", ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Outline: true, Text: "Outline"}), ui.Button(ui.ButtonProps{Color: ui.ButtonRed, Outline: true, Text: "Danger"}), ui.Button(ui.ButtonProps{Color: ui.ButtonGreen, Small: true, Icon: "check", Text: "Small + icon"}), ui.Button(ui.ButtonProps{Color: ui.ButtonNeutral, Icon: "plus"}), ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Disabled", Disabled: true}), ), ), kitSection("Badges", row("flex flex-wrap items-center gap-2", ui.Badge(ui.BadgeProps{Color: ui.BadgeGreen}, Text("active")), ui.Badge(ui.BadgeProps{Color: ui.BadgeRed}, Text("failed")), ui.Badge(ui.BadgeProps{Color: ui.BadgeBlue}, Text("info")), ui.Badge(ui.BadgeProps{Color: ui.BadgeAmber, Pill: true}, Text("pending")), ui.Badge(ui.BadgeProps{Color: ui.BadgeNeutral}, Text("default")), ui.Badge(ui.BadgeProps{Color: ui.BadgeMuted}, Text("muted")), ), ), kitSection("Alerts", ui.Alert(ui.AlertBlue, "Heads up", Text("An informational message with a header.")), ui.Alert(ui.AlertGreen, "", Text("A success alert without a header.")), ui.Alert(ui.AlertYellow, "Warning", Text("Something needs your attention.")), ui.Alert(ui.AlertRed, "Error", Text("Something went wrong.")), ), kitSection("Toggles & segmented control", ui.ToggleSwitch(notify.Get(), func(v bool) { notify.Set(v) }, "Email notifications", "Send me product updates", false, ""), ui.SegmentedButtons([]ui.SegmentedButtonOption{ {Value: "day", Label: "Day"}, {Value: "week", Label: "Week"}, {Value: "month", Label: "Month"}, }, span.Get(), func(v string) { span.Set(v) }, false, "max-w-xs"), ), kitSection("Tabs", ui.TabGroup(ui.TabGroupProps{ Items: []ui.TabItem{ {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) }, }), ), kitSection("Accordion", ui.SingleAccordion([]ui.AccordionItemData{ {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) }), ), kitSection("Forms", row("grid gap-4 sm:grid-cols-3", row("flex flex-col gap-1", ui.FormLabel(ui.FormLabelProps{}, Text("Name")), ui.FormInput(ui.FormInputProps{Value: name.Get(), Placeholder: "Ada Lovelace", OnInput: func(v string) { name.Set(v) }})), row("flex flex-col gap-1", ui.FormLabel(ui.FormLabelProps{}, Text("Email")), ui.FormEmailInput(ui.FormInputProps{Value: email.Get(), Placeholder: "ada@example.com", OnInput: func(v string) { email.Set(v) }}, true)), row("flex flex-col gap-1", ui.FormLabel(ui.FormLabelProps{}, Text("Plan")), ui.FormSelect(ui.FormSelectProps{Value: plan.Get(), OnChange: func(v string) { plan.Set(v) }}, ui.FormOption("free", "Free", false), ui.FormOption("pro", "Pro", false), ui.FormOption("enterprise", "Enterprise", false))), // A multi-select. Its rows carry checkboxes, and the field shows the // selection as removable pills — until they stop fitting, at which point // it collapses to "N items selected". Tick a few and watch it flip. row("flex flex-col gap-1", ui.FormLabel(ui.FormLabelProps{}, Text("Languages")), skills.Render(ui.FormMultiSelectProps{ Options: languageOptions(), Value: langs.Get(), Placeholder: "Pick a few", Searchable: true, ShowSelectAll: true, OnChange: func(v []string) { langs.Set(v) }, })), ), 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{ {DisplayName: "Name"}, {DisplayName: "Plan"}, {DisplayName: "Status", DisplayPosition: ui.PrettyTableColRight}, }, ui.PrettyTableOptions{Hover: true, Alternate: true, SurroundingBorder: true, HeaderBorderY: true}, ptRow("Ada Lovelace", "Pro", ui.Badge(ui.BadgeProps{Color: ui.BadgeGreen}, Text("active"))), ptRow("Alan Turing", "Free", ui.Badge(ui.BadgeProps{Color: ui.BadgeNeutral}, Text("trial"))), ptRow("Grace Hopper", "Enterprise", ui.Badge(ui.BadgeProps{Color: ui.BadgeBlue}, Text("invited"))), ), ), kitSection("Overlays (measured, portaled)", row("flex flex-wrap items-center gap-4", ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Open modal", OnClick: modal.Open}), // The menu measures itself against the viewport: drag the window // narrow, or scroll it to the bottom, and it flips/shifts to stay on // screen. Items close the menu themselves — no callback plumbing. menu.TriggerFunc(ui.MenuTriggerProps{Tag: "div"}, func(open bool) *VNode { caret := " ▾" if open { caret = " ▴" } return ui.Button(ui.ButtonProps{Color: ui.ButtonWhite, Text: "Menu" + caret}) }), menu.Content("", menu.Item(ui.MenuItemProps{Icon: "check"}, Text("Profile")), menu.Item(ui.MenuItemProps{}, Text("Settings")), ui.MenuDivider(""), menu.Item(ui.MenuItemProps{}, Text("Sign out")), ), // The tooltip's arrow tracks the trigger even when the panel gets // shifted away from it near a viewport edge. tip.Render(Span(Text("A measured tooltip — try it near the window edge")), ui.Button(ui.ButtonProps{Color: ui.ButtonLightNeutral, Text: "Hover me"})), ), modal.Render(ui.ModalProps{ Header: H3(Attr("class", "text-lg font-semibold text-text-heading"), Text("Example modal")), }, 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.")), ), ), 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 })`