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:
21
AGENTS.md
21
AGENTS.md
@@ -190,9 +190,28 @@ Add the component to all three dev targets so it renders in the visual test page
|
||||
```sh
|
||||
bb build-theme # Regenerate CSS with new component styles
|
||||
bb test # All tests pass
|
||||
bb dev-hiccup # Visual check
|
||||
```
|
||||
|
||||
### 6. Check running dev servers before committing — CRITICAL
|
||||
|
||||
A tmux session `ui-dev` runs all three dev servers (`bb dev-all`). **Always check every pane for compile errors before committing:**
|
||||
|
||||
```sh
|
||||
# List panes, then check each for errors
|
||||
tmux list-panes -t ui-dev -F "#{pane_index}: #{pane_current_command}"
|
||||
for i in $(tmux list-panes -t ui-dev -F "#{pane_index}"); do
|
||||
echo "=== pane $i ==="
|
||||
tmux capture-pane -t "ui-dev:bash.$i" -p -S -30 | grep -v '^$' | tail -10
|
||||
done
|
||||
```
|
||||
|
||||
Look for:
|
||||
- **shadow-cljs** (Replicant): `Build failure`, warnings, or `CompilerException`
|
||||
- **Vite/Squint**: `ERROR`, `SyntaxError`, or failed imports
|
||||
- **Hiccup** (Babashka): stack traces or `Exception`
|
||||
|
||||
Do **not** commit if any pane shows errors. Fix them first.
|
||||
|
||||
## Theme System
|
||||
|
||||
### Token naming
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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 ──────────────────────────────────────────── */
|
||||
|
||||
@@ -32,11 +32,19 @@
|
||||
(is (some #(and (vector? %) (= :small (first %)) (= "form-hint" (get-in % [1 :class])))
|
||||
result))))
|
||||
|
||||
(testing "renders error text"
|
||||
(testing "renders error icon with tooltip"
|
||||
(let [result (form/form-field {:label "Email" :error "Invalid email"} [:input])]
|
||||
(is (= "form-field form-field--error" (get-in result [1 :class])))
|
||||
(is (some #(and (vector? %) (= :small (first %)) (= "form-error" (get-in % [1 :class])))
|
||||
result))))
|
||||
;; Children wrapped in form-field-control
|
||||
(let [control (nth result 3)]
|
||||
(is (= :div (first control)))
|
||||
(is (= "form-field-control" (get-in control [1 :class])))
|
||||
;; Contains the child input
|
||||
(is (= :input (first (nth control 2))))
|
||||
;; Contains the tooltip with error text
|
||||
(let [tip (nth control 3)]
|
||||
(is (= :span (first tip)))
|
||||
(is (= "Invalid email" (get-in tip [1 :data-tooltip])))))))
|
||||
|
||||
(testing "no label renders without label element"
|
||||
(let [result (form/form-field {} [:input])]
|
||||
|
||||
Reference in New Issue
Block a user