package webui import ( "strconv" "strings" "kjol/wasmruntime" "kjol/wasmruntime/vdom" ) // Port of web/kit/Forms.tsx. Reactive accessors collapse to plain values, and // value/onChange model as a plain `Value string` field + `OnChange func(string)` // callback. The advanced combobox/multi-select controls in the TSX rely on // solid-js/web Portals + floating-ui measurement + refs + document listeners, // none of which exist in the neutral runtime; those are approximated here (see // the NOTE comments) with static, CSS-positioned structure and signal-free // open/toggle wiring. Genuinely browser-only controls are omitted (see bottom). // // Naming: the TSX exports are all `Form…`-prefixed, so none collide with the // vdom tag builders Form/Input/Label/Table; they are kept as-is. The exported // data table US_STATES becomes USStates (Go casing). Unexported helpers/consts // are `form`-prefixed per the package convention. // -- shared Tailwind class strings (verbatim from Forms.tsx) ------------------- 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 dark:text-red-400 text-ss mt-1" const formSuccessCls = "block text-green-600 text-ss mt-1" const formInputGroupCls = "flex flex-row items-stretch w-full text-sm" 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-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-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-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-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-ink-muted text-center" const formDropdownNoResultsDark = "p-2 text-sm text-text-on-dark-muted text-center" 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 ----------------------------------------------------- // formControlH is the fixed control height (small variant is shorter). func formControlH(small bool) string { if small { return "h-[30px]" } return "h-[38px]" } // 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-line-strong focus:outline-sky-500" if onDark { borderNormal = "border-border-on-dark focus:outline-sky-500" } switch { case errMsg != "": return "border-red-500 focus:outline-red-500" case successMsg != "": return "border-green-500 focus:outline-green-500" default: return borderNormal } } func formInputCls(small bool, errMsg, successMsg, extra string, onDark bool) string { base := formInputBase if onDark { base = formInputBaseDark } pad := "p-2" if small { pad = "p-1" } return cx(base, formControlH(small), pad, formFieldBorder(errMsg, successMsg, onDark), extra) } // formTextareaCls shares the input look but opts out of the fixed control height // so the textarea can grow with its content. func formTextareaCls(small bool, errMsg, extra string, onDark bool) string { base := formInputBase if onDark { base = formInputBaseDark } pad := "p-2" if small { pad = "p-1" } return cx(base, pad, formFieldBorder(errMsg, "", onDark), extra) } func formPrefixCls(small bool, errMsg string, onDark bool) string { 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" } border := borderNormal if errMsg != "" { border = "border-red-500" } pad := "p-2" if small { pad = "p-1 text-sm" } return cx(base, border, pad) } func formTriggerCls(small, onDark bool) string { base := formTriggerBase if onDark { base = formTriggerBaseDark } pad := "p-2" if small { pad = "p-1" } return cx(base, formControlH(small), pad) } // ComboboxFieldWidth values sizing a combobox/multi-select field. const ( ComboboxWidthFill = "fill" ComboboxWidthGrow = "grow" ComboboxWidthNarrow = "narrow" ComboboxWidthDefault = "default" ComboboxWidthWide = "wide" ComboboxWidthAuto = "auto" ) var formComboboxWidthCls = map[string]string{ "fill": "w-full min-w-0 max-w-full", "grow": "w-full min-w-[8rem] max-w-[16rem]", "narrow": "w-full min-w-[7rem] max-w-[11rem]", "default": "w-full min-w-[9rem] max-w-[14rem]", "wide": "w-full min-w-[10rem] max-w-[20rem]", "auto": "w-auto min-w-[8rem] max-w-full", } func formComboboxRootCls(class, fieldWidth string) string { w := formComboboxWidthCls[fieldWidth] if w == "" { w = formComboboxWidthCls["fill"] } return cx("relative", w, class) } // formValidationSpans renders the trailing error/success message spans. func formValidationSpans(errMsg, successMsg string) []vdom.Mod { var out []vdom.Mod if errMsg != "" { out = append(out, vdom.Span(vdom.Attr("class", formErrorCls), vdom.Text(errMsg))) } if successMsg != "" { out = append(out, vdom.Span(vdom.Attr("class", formSuccessCls), vdom.Text(successMsg))) } return out } // -- shared types -------------------------------------------------------------- // FormSelectOption is one