Add landing page for kjol, documentation
This commit is contained in:
@@ -23,28 +23,28 @@ import (
|
||||
|
||||
// -- shared Tailwind class strings (verbatim from Forms.tsx) -------------------
|
||||
|
||||
const formInputBase = "bg-white block w-full border rounded-default shadow-xs text-sm focus:outline-2 focus:outline-offset-1 disabled:bg-neutral-100 disabled:cursor-not-allowed"
|
||||
const formInputBase = "bg-surface block w-full border rounded-default shadow-xs text-sm focus:outline-2 focus:outline-offset-1 disabled:bg-surface-raised disabled:cursor-not-allowed"
|
||||
const formInputBaseDark = "bg-dark text-text-on-dark placeholder:text-text-on-dark-faint block w-full border rounded-default shadow-xs text-sm focus:outline-2 focus:outline-offset-1 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
|
||||
const formErrorCls = "block text-red-600 text-xs mt-1"
|
||||
const formErrorCls = "block text-red-600 dark:text-red-400 text-xs mt-1"
|
||||
const formSuccessCls = "block text-green-600 text-xs mt-1"
|
||||
const formInputGroupCls = "flex flex-row items-stretch w-full text-sm"
|
||||
|
||||
const formTriggerBase = "bg-white border border-neutral-300 rounded-default shadow-xs text-sm w-full text-left flex items-center justify-between gap-2 cursor-pointer disabled:bg-neutral-100 disabled:cursor-not-allowed"
|
||||
const formTriggerBase = "bg-surface border border-line-strong rounded-default shadow-xs text-sm w-full text-left flex items-center justify-between gap-2 cursor-pointer disabled:bg-surface-raised disabled:cursor-not-allowed"
|
||||
const formTriggerBaseDark = "bg-dark-raised border border-border-on-dark text-text-on-dark rounded-default shadow-xs text-sm w-full text-left flex items-center justify-between gap-2 cursor-pointer hover:border-border-on-dark-hover disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
|
||||
const formDropdown = "bg-white border border-neutral-300 rounded-default shadow-lg max-h-60 overflow-auto"
|
||||
const formDropdown = "bg-surface border border-line-strong rounded-default shadow-lg max-h-60 overflow-auto"
|
||||
const formDropdownDark = "bg-dark-raised border border-border-on-dark rounded-default shadow-lg max-h-60 overflow-auto"
|
||||
const formDropdownSearchWrap = "sticky top-0 bg-white border-b border-neutral-200 p-2"
|
||||
const formDropdownSearchWrap = "sticky top-0 bg-surface border-b border-line p-2"
|
||||
const formDropdownSearchWrapDark = "sticky top-0 bg-dark-raised border-b border-border-on-dark p-2"
|
||||
const formDropdownSearchInput = "w-full bg-white border border-neutral-300 rounded-default shadow-xs text-sm p-1 focus:outline-2 focus:outline-sky-500 focus:outline-offset-1"
|
||||
const formDropdownSearchInput = "w-full bg-surface border border-line-strong rounded-default shadow-xs text-sm p-1 focus:outline-2 focus:outline-sky-500 focus:outline-offset-1"
|
||||
const formDropdownSearchInputDark = "w-full bg-dark border border-border-on-dark text-text-on-dark placeholder:text-text-on-dark-faint rounded-default shadow-xs text-sm p-1 focus:outline-2 focus:outline-sky-500 focus:outline-offset-1"
|
||||
const formDropdownOption = "w-full text-left p-2 text-sm cursor-pointer flex items-center gap-2 bg-transparent border-none hover:bg-neutral-100 disabled:text-neutral-400 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
const formDropdownOption = "w-full text-left p-2 text-sm cursor-pointer flex items-center gap-2 bg-transparent border-none hover:bg-surface-raised disabled:text-ink-faint disabled:cursor-not-allowed whitespace-nowrap"
|
||||
const formDropdownOptionDark = "w-full text-left p-2 text-sm cursor-pointer flex items-center gap-2 bg-transparent border-none text-text-on-dark hover:bg-white/5 disabled:text-text-on-dark-faint disabled:cursor-not-allowed whitespace-nowrap"
|
||||
const formDropdownNoResults = "p-2 text-sm text-neutral-500 text-center"
|
||||
const formDropdownNoResults = "p-2 text-sm text-ink-muted text-center"
|
||||
const formDropdownNoResultsDark = "p-2 text-sm text-text-on-dark-muted text-center"
|
||||
const formSelectAllWrap = "border-b border-neutral-200"
|
||||
const formSelectAllBtn = "w-full text-left p-2 text-sm cursor-pointer text-neutral-600 font-medium bg-transparent border-none hover:bg-neutral-100"
|
||||
const formSelectAllWrap = "border-b border-line"
|
||||
const formSelectAllBtn = "w-full text-left p-2 text-sm cursor-pointer text-ink-soft font-medium bg-transparent border-none hover:bg-surface-raised"
|
||||
|
||||
// -- shared class builders -----------------------------------------------------
|
||||
|
||||
@@ -59,7 +59,7 @@ func formControlH(small bool) string {
|
||||
// formFieldBorder picks the border/focus-outline color: error > success > normal.
|
||||
// error/success are the message strings; non-empty means "present" (truthy).
|
||||
func formFieldBorder(errMsg, successMsg string, onDark bool) string {
|
||||
borderNormal := "border-neutral-300 focus:outline-sky-500"
|
||||
borderNormal := "border-line-strong focus:outline-sky-500"
|
||||
if onDark {
|
||||
borderNormal = "border-border-on-dark focus:outline-sky-500"
|
||||
}
|
||||
@@ -100,8 +100,8 @@ func formTextareaCls(small bool, errMsg, extra string, onDark bool) string {
|
||||
}
|
||||
|
||||
func formPrefixCls(small bool, errMsg string, onDark bool) string {
|
||||
base := "bg-neutral-100 border shadow-xs border-r-0 flex items-center shrink-0 rounded-l-default"
|
||||
borderNormal := "border-neutral-300"
|
||||
base := "bg-surface-raised border shadow-xs border-r-0 flex items-center shrink-0 rounded-l-default"
|
||||
borderNormal := "border-line-strong"
|
||||
if onDark {
|
||||
base = "bg-dark-raised text-text-on-dark-muted border shadow-xs border-r-0 flex items-center shrink-0 rounded-l-default"
|
||||
borderNormal = "border-border-on-dark"
|
||||
@@ -662,7 +662,7 @@ func FormLabel(p FormLabelProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
if p.Inline {
|
||||
display = "inline"
|
||||
}
|
||||
color := "text-neutral-700"
|
||||
color := "text-ink-soft"
|
||||
if p.OnDark {
|
||||
color = "text-text-on-dark"
|
||||
}
|
||||
@@ -685,8 +685,8 @@ func FormFileInput(p FormInputProps) *vdom.VNode {
|
||||
filePad = "file:py-[2px] file:px-3"
|
||||
}
|
||||
class := cx(formInputBase,
|
||||
"p-1 border-neutral-300 focus:outline-sky-500 cursor-pointer",
|
||||
"file:ml-1 file:mr-2 file:bg-neutral-100 file:border file:border-neutral-300 file:rounded-default file:shadow-xs file:text-sm file:cursor-pointer file:hover:bg-neutral-200",
|
||||
"p-1 border-line-strong focus:outline-sky-500 cursor-pointer",
|
||||
"file:ml-1 file:mr-2 file:bg-surface-raised file:border file:border-line-strong file:rounded-default file:shadow-xs file:text-sm file:cursor-pointer file:hover:bg-surface-strong",
|
||||
filePad, p.Class)
|
||||
mods := []vdom.Mod{
|
||||
vdom.Attr("type", "file"),
|
||||
@@ -717,8 +717,8 @@ func FormSpacer() *vdom.VNode { return vdom.Div(vdom.Attr("class", "mb-3")) }
|
||||
// FormFieldset wraps children in a bordered <fieldset> with an optional legend.
|
||||
func FormFieldset(legend, class string, children ...*vdom.VNode) *vdom.VNode {
|
||||
mods := []vdom.Mod{
|
||||
vdom.Attr("class", cx("border border-neutral-300 rounded-default py-3 px-4", class)),
|
||||
vdom.Legend(vdom.Attr("class", "px-2 text-sm font-medium text-neutral-600"), vdom.Text(legend)),
|
||||
vdom.Attr("class", cx("border border-line-strong rounded-default py-3 px-4", class)),
|
||||
vdom.Legend(vdom.Attr("class", "px-2 text-sm font-medium text-ink-soft"), vdom.Text(legend)),
|
||||
}
|
||||
mods = kids(mods, children)
|
||||
return vdom.Fieldset(mods...)
|
||||
@@ -954,7 +954,7 @@ func (d *dropdown) optionButton(opt FormSelectOption, idx int, selected, onDark,
|
||||
cls = cx(cls, "py-1.5 px-2")
|
||||
}
|
||||
if idx == d.active.Get() {
|
||||
cls = cx(cls, "bg-neutral-100")
|
||||
cls = cx(cls, "bg-surface-raised")
|
||||
}
|
||||
|
||||
mods := []vdom.Mod{
|
||||
@@ -1073,7 +1073,7 @@ func (c *Combobox) Render(p FormComboboxProps) *vdom.VNode {
|
||||
}
|
||||
|
||||
display := pick(p.Placeholder, "Select an option")
|
||||
placeholderCls := "text-neutral-500"
|
||||
placeholderCls := "text-ink-muted"
|
||||
if p.OnDark {
|
||||
placeholderCls = "text-text-on-dark-muted"
|
||||
}
|
||||
@@ -1082,7 +1082,7 @@ func (c *Combobox) Render(p FormComboboxProps) *vdom.VNode {
|
||||
placeholderCls = ""
|
||||
}
|
||||
|
||||
chevronCls := "text-neutral-400"
|
||||
chevronCls := "text-ink-faint"
|
||||
if p.OnDark {
|
||||
chevronCls = "text-text-on-dark-muted"
|
||||
}
|
||||
@@ -1178,6 +1178,12 @@ type FormMultiSelectProps struct {
|
||||
Small bool
|
||||
Class string
|
||||
FieldWidth string
|
||||
|
||||
// CollapsedLabel names the selection once the pills no longer fit. It defaults to
|
||||
// "N items selected", which is right for a list of things and wrong for anything
|
||||
// that is not — a column picker reading "5 items selected" tells you the count and
|
||||
// hides what the count is OF.
|
||||
CollapsedLabel func(n int) string
|
||||
}
|
||||
|
||||
// Render draws the field and its panel.
|
||||
@@ -1211,7 +1217,7 @@ func (m *MultiSelect) Render(p FormMultiSelectProps) *vdom.VNode {
|
||||
AriaHasPopup: "listbox",
|
||||
},
|
||||
vdom.Div(face...),
|
||||
IconInline(chevron, 16, "text-neutral-400"),
|
||||
IconInline(chevron, 16, "text-ink-faint"),
|
||||
)
|
||||
if p.Disabled {
|
||||
trigger.Attrs["disabled"] = "disabled"
|
||||
@@ -1227,27 +1233,7 @@ func (m *MultiSelect) Render(p FormMultiSelectProps) *vdom.VNode {
|
||||
// user is looking at and asked for; quietly selecting the hidden ones too would be
|
||||
// a nasty surprise.
|
||||
if p.ShowSelectAll && len(filtered) > 0 {
|
||||
all := formAllSelected(filtered, p.Value)
|
||||
label := "Select All"
|
||||
if all {
|
||||
label = "Deselect All"
|
||||
}
|
||||
mods := []vdom.Mod{vdom.Attr("type", "button"), vdom.Attr("class", formSelectAllBtn), vdom.Text(label)}
|
||||
if p.OnChange != nil {
|
||||
oc, cur, opts, allNow := p.OnChange, p.Value, filtered, all
|
||||
mods = append(mods, vdom.On(vdom.EVENT_CLICK, func() {
|
||||
if allNow {
|
||||
next := cur
|
||||
for _, o := range opts {
|
||||
next = formRemoveStr(next, o.Value)
|
||||
}
|
||||
oc(next)
|
||||
return
|
||||
}
|
||||
oc(formSelectAllValues(opts, cur))
|
||||
}))
|
||||
}
|
||||
rows = append(rows, vdom.Button(mods...))
|
||||
rows = append(rows, m.selectAllButton(filtered, p.Value, p.OnChange))
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
@@ -1276,6 +1262,34 @@ func (m *MultiSelect) Render(p FormMultiSelectProps) *vdom.VNode {
|
||||
)
|
||||
}
|
||||
|
||||
// selectAllButton is the row at the top of the panel.
|
||||
//
|
||||
// It acts on what is VISIBLE. Selecting all of a filtered list is what the user is
|
||||
// looking at and asked for; quietly selecting the hidden ones too would be a nasty
|
||||
// surprise, and one they would not discover until they saved.
|
||||
func (m *MultiSelect) selectAllButton(filtered []FormSelectOption, value []string, onChange func([]string)) *vdom.VNode {
|
||||
all := formAllSelected(filtered, value)
|
||||
label := "Select All"
|
||||
if all {
|
||||
label = "Deselect All"
|
||||
}
|
||||
mods := []vdom.Mod{vdom.Attr("type", "button"), vdom.Attr("class", formSelectAllBtn), vdom.Text(label)}
|
||||
if onChange != nil {
|
||||
mods = append(mods, vdom.On(vdom.EVENT_CLICK, func() {
|
||||
if all {
|
||||
next := value
|
||||
for _, o := range filtered {
|
||||
next = formRemoveStr(next, o.Value)
|
||||
}
|
||||
onChange(next)
|
||||
return
|
||||
}
|
||||
onChange(formSelectAllValues(filtered, value))
|
||||
}))
|
||||
}
|
||||
return vdom.Button(mods...)
|
||||
}
|
||||
|
||||
// triggerFace is what the closed field shows: a placeholder, a row of removable pills,
|
||||
// or "N items selected" when the pills will not fit.
|
||||
//
|
||||
@@ -1286,7 +1300,7 @@ func (m *MultiSelect) Render(p FormMultiSelectProps) *vdom.VNode {
|
||||
func (m *MultiSelect) triggerFace(p FormMultiSelectProps, selected []FormSelectOption, maxTags int) []vdom.Mod {
|
||||
if len(selected) == 0 {
|
||||
return []vdom.Mod{vdom.Span(
|
||||
vdom.Attr("class", "text-neutral-500 truncate"),
|
||||
vdom.Attr("class", "text-ink-muted truncate"),
|
||||
vdom.Text(pick(p.Placeholder, "Select options")),
|
||||
)}
|
||||
}
|
||||
@@ -1315,7 +1329,7 @@ func (m *MultiSelect) triggerFace(p FormMultiSelectProps, selected []FormSelectO
|
||||
ov, label := opt.Value, opt.Label
|
||||
remove := []vdom.Mod{
|
||||
vdom.Attr("type", "button"),
|
||||
vdom.Attr("class", "bg-transparent border-none p-0 cursor-pointer flex items-center hover:text-neutral-900"),
|
||||
vdom.Attr("class", "bg-transparent border-none p-0 cursor-pointer flex items-center hover:text-ink"),
|
||||
vdom.Attr("aria-label", "Remove "+label),
|
||||
IconInline("xmark", 10, ""),
|
||||
}
|
||||
@@ -1334,10 +1348,14 @@ func (m *MultiSelect) triggerFace(p FormMultiSelectProps, selected []FormSelectO
|
||||
))
|
||||
}
|
||||
|
||||
label := itemsSelected
|
||||
if p.CollapsedLabel != nil {
|
||||
label = p.CollapsedLabel
|
||||
}
|
||||
count := vdom.Span(
|
||||
vdom.WithRef(m.countRef),
|
||||
vdom.Attr("class", faceCls("truncate", !tooMany)),
|
||||
vdom.Text(itemsSelected(len(selected))),
|
||||
vdom.Text(label(len(selected))),
|
||||
)
|
||||
|
||||
m.collapseFace(tooMany)
|
||||
@@ -1390,7 +1408,7 @@ func itemsSelected(n int) string {
|
||||
return strconv.Itoa(n) + " items selected"
|
||||
}
|
||||
|
||||
const formMultiSelectTag = "inline-flex items-center gap-0.5 rounded-default bg-neutral-200 py-0.5 text-neutral-700 whitespace-nowrap leading-none"
|
||||
const formMultiSelectTag = "inline-flex items-center gap-0.5 rounded-default bg-surface-strong py-0.5 text-ink-soft whitespace-nowrap leading-none"
|
||||
|
||||
func truncateRunes(s string, max int) string {
|
||||
if max <= 0 {
|
||||
@@ -1543,18 +1561,15 @@ var formTimezones = []FormSelectOption{
|
||||
{Value: "Pacific/Honolulu", Label: "Hawaii"},
|
||||
}
|
||||
|
||||
// -- intentionally omitted (no neutral-runtime equivalent) ---------------------
|
||||
// -- the rest of the TSX kit ---------------------------------------------------
|
||||
//
|
||||
// NOTE: the following TSX exports are omitted. They depend on capabilities the
|
||||
// neutral vdom runtime does not provide, and have no meaningful static form:
|
||||
// These four were once listed here as unportable — "no neutral-runtime equivalent".
|
||||
// That was true before the host API existed. It is not true now, and they all live in
|
||||
// forms_async.go and signaturepad.go:
|
||||
//
|
||||
// - FormSignaturePad — freehand drawing on a <canvas> 2D context with
|
||||
// mouse/touch tracking and SVG serialization.
|
||||
// - FormAsyncCombobox — debounced async option loading via Promises/timers.
|
||||
// - FormMultiSelectTrigger — a custom-trigger multi-select popover (Portal-free
|
||||
// but still driven by document listeners + refs); its
|
||||
// selection model is already covered by FormMultiSelect.
|
||||
// - handleTaxIdInput / handleRateInput — standalone oninput handlers that mutate
|
||||
// a DOM element in place; with no component consumer in
|
||||
// this file and no DOM handle in handlers, they don't
|
||||
// port. (Their masking logic mirrors formClean* above.)
|
||||
// - SignaturePad — draws into an SVG rather than a canvas, so the thing you
|
||||
// sign IS the value the caller stores.
|
||||
// - AsyncCombobox — debounced, and it discards out-of-order responses.
|
||||
// - MultiSelectTrigger — MultiSelect behind a trigger of your own.
|
||||
// - MaskTaxID / MaskRate — the TSX's oninput handlers, as pure functions of the
|
||||
// string. Testable, reusable, and they run on the server.
|
||||
|
||||
Reference in New Issue
Block a user