diff --git a/AGENTS.md b/AGENTS.md index 88d96d0..6290c9a 100644 --- a/AGENTS.md +++ b/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 diff --git a/src/ui/form.cljc b/src/ui/form.cljc index 1777d8f..6fb9028 100644 --- a/src/ui/form.cljc +++ b/src/ui/form.cljc @@ -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 ────────────────────────────────────────────────────── diff --git a/src/ui/form.css b/src/ui/form.css index f6ac6ce..a34ea7e 100644 --- a/src/ui/form.css +++ b/src/ui/form.css @@ -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 ──────────────────────────────────────────── */ diff --git a/test/ui/form_test.clj b/test/ui/form_test.clj index d356f95..ff96380 100644 --- a/test/ui/form_test.clj +++ b/test/ui/form_test.clj @@ -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])]