refactor(form): replace inline error text with tooltip icon

Instead of rendering error messages as `<small class="form-error">`
below inputs, errors now display as a circle-x icon inside the input
area. Hovering the icon shows the error text in a danger-styled tooltip.

- Wrap children in `.form-field-control` when error is present
- Use existing tooltip + icon components for the error indicator
- Add CSS for positioning, padding-right offset, and danger color overrides
- Update tests to match new structure
- Add pre-commit tmux server check instructions to AGENTS.md
This commit is contained in:
Florian Schroedl
2026-03-05 14:01:26 +01:00
parent d5473b1bbf
commit b52361ebf1
4 changed files with 105 additions and 17 deletions

View File

@@ -1,5 +1,7 @@
(ns ui.form
(:require [clojure.string :as str]))
(:require [clojure.string :as str]
[ui.icon :as icon]
[ui.tooltip :as tooltip]))
;; In squint, keywords are strings — name is identity
#?(:squint (defn- kw-name [s] s)
@@ -19,6 +21,12 @@
[opts]
(str/join " " (form-field-class-list opts)))
(defn- error-icon
"Render a circle-x icon wrapped in a tooltip showing the error text."
[error]
(tooltip/tooltip {:text error :class "form-error-icon"}
(icon/icon {:icon-name :circle-x :size :sm})))
(defn form-field
"Render a form field wrapper with label, hint, and error support.
@@ -37,9 +45,11 @@
(cond-> (if label
[[:label {:class "form-label"} label]]
[])
true (into children)
hint (conj [:small {:class "form-hint"} hint])
error (conj [:small {:class "form-error"} error]))))
true (into (if error
[(into [:div {:class "form-field-control"}]
(conj (vec children) (error-icon error)))]
children))
hint (conj [:small {:class "form-hint"} hint]))))
:cljs
(let [cls (form-field-class-list {:error error})
@@ -49,9 +59,11 @@
(cond-> (if label
[[:label {:class ["form-label"]} label]]
[])
true (into children)
hint (conj [:small {:class ["form-hint"]} hint])
error (conj [:small {:class ["form-error"]} error]))))
true (into (if error
[(into [:div {:class ["form-field-control"]}]
(conj (vec children) (error-icon error)))]
children))
hint (conj [:small {:class ["form-hint"]} hint]))))
:clj
(let [classes (cond-> (form-field-classes {:error error})
@@ -61,9 +73,11 @@
(cond-> (if label
[[:label {:class "form-label"} label]]
[])
true (into children)
hint (conj [:small {:class "form-hint"} hint])
error (conj [:small {:class "form-error"} error]))))))
true (into (if error
[(into [:div {:class "form-field-control"}]
(conj (vec children) (error-icon error)))]
children))
hint (conj [:small {:class "form-hint"} hint]))))))
;; ── Text input ──────────────────────────────────────────────────────

View File

@@ -240,16 +240,63 @@
cursor: not-allowed;
}
/* ── Hint & Error text ─────────────────────────────────────────── */
/* ── Hint text ─────────────────────────────────────────────────── */
.form-hint {
font-size: var(--font-xs);
color: var(--fg-2);
}
.form-error {
font-size: var(--font-xs);
/* ── Error icon with tooltip ───────────────────────────────────── */
.form-field-control {
position: relative;
}
.form-field-control > .form-input,
.form-field-control > .form-textarea,
.form-field-control > .form-select {
padding-right: var(--size-8);
}
.tooltip.form-error-icon {
position: absolute;
right: var(--size-2);
top: 0;
bottom: 0;
display: flex;
align-items: center;
color: var(--danger);
cursor: pointer;
z-index: 1;
}
.tooltip.form-error-icon::after {
background: color-mix(in srgb, var(--danger) 18%, var(--bg-1));
color: var(--fg-0);
border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
white-space: normal;
min-width: 12rem;
max-width: 20rem;
line-height: 1.4;
left: auto;
right: 0;
transform: translateY(4px);
}
.tooltip.form-error-icon:hover::after {
transform: translateY(0);
}
.tooltip.form-error-icon::before {
border-top-color: color-mix(in srgb, var(--danger) 18%, var(--bg-1));
left: auto;
right: var(--size-1);
transform: translateY(4px);
}
.tooltip.form-error-icon:hover::before {
transform: translateY(0);
}
/* ── Fieldset & Legend ──────────────────────────────────────────── */