From 85bfd439dc40753d2d9024e36657cbfde8f3af04 Mon Sep 17 00:00:00 2001 From: codeGlaze Date: Thu, 15 Oct 2026 03:27:55 -0400 Subject: [PATCH 1/4] ui forms: Repeat the nav and add buttons at the bottom of long forms - Character builder: the Back / Next bar shows above and below the page. - Homebrew builders (option-traits): the add button repeats below a non-empty traits list, on every builder that uses it. - Selection builder: Add Option repeats below a non-empty options list. --- src/cljs/orcpub/character_builder.cljs | 49 +++++++++++++++----------- src/cljs/orcpub/dnd/e5/views.cljs | 37 ++++++++++++++----- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/src/cljs/orcpub/character_builder.cljs b/src/cljs/orcpub/character_builder.cljs index 3c8e9907b..4abb8e14c 100644 --- a/src/cljs/orcpub/character_builder.cljs +++ b/src/cljs/orcpub/character_builder.cljs @@ -1890,6 +1890,31 @@ (defn sorted-selection-set [selections] (into (sorted-set-by compare-paths) selections)) +(defn page-nav-bar + "Back / page title / Next bar for the character builder; wraps at either end. + Args: page-index, the current page index; page-name, the title shown. + Returns hiccup. Rendered above and below the page's options." + [page-index page-name] + [:div.flex.justify-cont-s-b.p-t-5.p-10.align-items-t + [:button.form-button.p-5-10.m-r-5 + {:on-click + (fn [_] + (dispatch [:set-page (let [prev (dec page-index)] + (if (neg? prev) + (dec (count pages)) + prev))]))} + "Back"] + [:div.flex-grow-1 + [:h3.f-w-b.f-s-20.t-a-c page-name]] + [:button.form-button.p-5-10.m-l-5 + {:on-click + (fn [_] + (dispatch [:set-page (let [next (inc page-index)] + (if (>= next (count pages)) + 0 + next))]))} + "Next"]]) + (defn new-options-column [num-columns] (let [character @(subscribe [:character]) built-template @(subscribe [:built-template]) @@ -1906,25 +1931,7 @@ [:div.w-100-p [:div#options-column.b-1.b-rad-5 [section-tabs available-selections built-template character page-index] - [:div.flex.justify-cont-s-b.p-t-5.p-10.align-items-t - [:button.form-button.p-5-10.m-r-5 - {:on-click - (fn [_] - (dispatch [:set-page (let [prev (dec page-index)] - (if (neg? prev) - (dec (count pages)) - prev))]))} - "Back"] - [:div.flex-grow-1 - [:h3.f-w-b.f-s-20.t-a-c (:name page)]] - [:button.form-button.p-5-10.m-l-5 - {:on-click - (fn [_] - (dispatch [:set-page (let [next (inc page-index)] - (if (>= next (count pages)) - 0 - next))]))} - "Next"]] + [page-nav-bar page-index (:name page)] (let [ui-fn-selections (mapcat (fn [{:keys [key group? ui-fn]}] (if group? @@ -1993,7 +2000,9 @@ selection num-columns remaining)]))) - sorted-selections)))])])]])) + sorted-selections)))])]) + ;; Repeated below the page so a long page needs no scroll back up. + [page-nav-bar page-index (:name page)]]])) (defn set-random-name diff --git a/src/cljs/orcpub/dnd/e5/views.cljs b/src/cljs/orcpub/dnd/e5/views.cljs index 544707945..b21538f04 100644 --- a/src/cljs/orcpub/dnd/e5/views.cljs +++ b/src/cljs/orcpub/dnd/e5/views.cljs @@ -6498,6 +6498,15 @@ #(dispatch [toggle-map-prop-event kw key])]]) @(subscribe [::mi/custom-and-standard-weapons]))))])]) +(defn add-trait-button + "The add button of a builder's traits list. + Args: add-trait-event, the event vector to dispatch; button-title, the label + or nil for \"add feature / trait\". Returns hiccup." + [add-trait-event button-title] + [:button.form-button.m-l-5 + {:on-click (make-event-handler add-trait-event)} + (or button-title "add feature / trait")]) + (defn option-traits [option add-trait-event edit-trait-name-event @@ -6509,9 +6518,7 @@ [:div.p-t-10.p-b-10.f-w-b.flex.justify-cont-s-b.align-items-c [:div.f-s-24.f-w-b.m-b-10 (or title "Features/Traits")] [:div - [:button.form-button.m-l-5 - {:on-click (make-event-handler add-trait-event)} - (or button-title "add feature / trait")]]] + [add-trait-button add-trait-event button-title]]] [:div (if (seq (:traits option)) (doall @@ -6559,7 +6566,11 @@ {:on-click #(dispatch [add-trait-event])} [:span "There are currently no features/traits, click "] [:span.orange.underline "here"] - [:span " or on the button above to add one."]])]]) + [:span " or on the button above to add one."]])] + ;; Repeated below a non-empty list so a long one needs no scroll back up. + (when (seq (:traits option)) + [:div.p-t-10.flex.justify-cont-end + [add-trait-button add-trait-event button-title]])]) (defn option-saving-throw-advantages [option toggle-map-prop-event] [:div.m-b-20 @@ -8102,6 +8113,14 @@ freqs (frequencies (remove nil? keys))] (set (map first (filter #(> (val %) 1) freqs))))) +(defn add-option-button + "The selection builder's Add Option button; dispatches ::selections/add-option. + No args. Returns hiccup." + [] + [:button.form-button + {:on-click #(dispatch [::selections/add-option])} + "Add Option"]) + (defn selection-builder [] (let [selection @(subscribe [::selections/builder-item]) options (:options selection) @@ -8132,9 +8151,7 @@ [:div [:div.flex.justify-cont-s-b [:div.f-s-24.f-w-b "Options"] - [:button.form-button - {:on-click #(dispatch [::selections/add-option])} - "Add Option"]] + [add-option-button]] ;; Summary warning for duplicate names (when has-dupes? [:div.p-10.m-b-10.red @@ -8189,7 +8206,11 @@ [textarea-field {:value description :on-change #(dispatch [::selections/set-selection-path-prop [:options i :description] %])}]]])) - options))]]])) + options))] + ;; Repeated below a non-empty list so a long one needs no scroll back up. + (when (seq options) + [:div.p-t-10.flex.justify-cont-end + [add-option-button]])]])) (defn language-builder [] (let [language @(subscribe [::langs/builder-item])] From 7fa19d461fc2a4110b789dd98f1b35cb5ffeadbe Mon Sep 17 00:00:00 2001 From: codeGlaze Date: Thu, 15 Oct 2026 09:29:15 -0400 Subject: [PATCH 2/4] ui forms: The bottom Back and Next bring the new page's top into view They change the page, then scroll the builder to its top below the sticky header and move focus to the page title, so a long page does not leave the user at the bottom of the next one. The top bar is unchanged. --- src/cljs/orcpub/character_builder.cljs | 55 +++++++++++++++----------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/cljs/orcpub/character_builder.cljs b/src/cljs/orcpub/character_builder.cljs index 4abb8e14c..159362865 100644 --- a/src/cljs/orcpub/character_builder.cljs +++ b/src/cljs/orcpub/character_builder.cljs @@ -1890,30 +1890,37 @@ (defn sorted-selection-set [selections] (into (sorted-set-by compare-paths) selections)) +(defn- show-page-top! + "After the next render, scrolls the builder's page to its top, just below the sticky header, + and moves focus to the page title so a screen reader starts there. No args; returns nil." + [] + (js/requestAnimationFrame + (fn [] + (when-let [col (.getElementById js/document "options-column")] + (let [header (or (some-> (.querySelector js/document ".sticky-header") .-offsetHeight) 0)] + (.scrollTo js/window 0 (- (+ (.-scrollY js/window) (.-top (.getBoundingClientRect col))) header)) + (some-> (.getElementById js/document "builder-page-title") (.focus #js {:preventScroll true}))))))) + (defn page-nav-bar "Back / page title / Next bar for the character builder; wraps at either end. - Args: page-index, the current page index; page-name, the title shown. - Returns hiccup. Rendered above and below the page's options." - [page-index page-name] - [:div.flex.justify-cont-s-b.p-t-5.p-10.align-items-t - [:button.form-button.p-5-10.m-r-5 - {:on-click - (fn [_] - (dispatch [:set-page (let [prev (dec page-index)] - (if (neg? prev) - (dec (count pages)) - prev))]))} - "Back"] - [:div.flex-grow-1 - [:h3.f-w-b.f-s-20.t-a-c page-name]] - [:button.form-button.p-5-10.m-l-5 - {:on-click - (fn [_] - (dispatch [:set-page (let [next (inc page-index)] - (if (>= next (count pages)) - 0 - next))]))} - "Next"]]) + Args: page-index, the current page index; page-name, the title shown; bottom?, true for the + copy below the page, whose buttons also bring the new page's top into view. + Returns hiccup." + [page-index page-name bottom?] + (let [go (fn [i] (dispatch [:set-page i]) (when bottom? (show-page-top!)))] + [:div.flex.justify-cont-s-b.p-t-5.p-10.align-items-t + [:button.form-button.p-5-10.m-r-5 + {:on-click (fn [_] (go (let [prev (dec page-index)] + (if (neg? prev) (dec (count pages)) prev))))} + "Back"] + [:div.flex-grow-1 + (if bottom? + [:h3.f-w-b.f-s-20.t-a-c page-name] + [:h3#builder-page-title.f-w-b.f-s-20.t-a-c {:tab-index -1} page-name])] + [:button.form-button.p-5-10.m-l-5 + {:on-click (fn [_] (go (let [next (inc page-index)] + (if (>= next (count pages)) 0 next))))} + "Next"]])) (defn new-options-column [num-columns] (let [character @(subscribe [:character]) @@ -1931,7 +1938,7 @@ [:div.w-100-p [:div#options-column.b-1.b-rad-5 [section-tabs available-selections built-template character page-index] - [page-nav-bar page-index (:name page)] + [page-nav-bar page-index (:name page) false] (let [ui-fn-selections (mapcat (fn [{:keys [key group? ui-fn]}] (if group? @@ -2002,7 +2009,7 @@ remaining)]))) sorted-selections)))])]) ;; Repeated below the page so a long page needs no scroll back up. - [page-nav-bar page-index (:name page)]]])) + [page-nav-bar page-index (:name page) true]]])) (defn set-random-name From a5513d0eddc19d892345cd10ad1a107e4efd7dbc Mon Sep 17 00:00:00 2001 From: codeGlaze Date: Sat, 17 Oct 2026 21:52:29 -0400 Subject: [PATCH 3/4] changelog: Bottom buttons on long builder pages, folded into Summer Patch --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c28c8e512..10673876c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ Homebrew can be moved, switched off and repaired instead of lost, and a characte - **Characters:** the builder shows choices waiting on a higher level beside the level picker, and says when each comes back. - **Characters:** with more than one class, a "1st" badge marks the starting class; tap another class's badge to make it the starting class, and choices that only apply to the starting class follow. - **Characters:** a Choices list in the builder shows every choice a character stores, including ones whose homebrew isn't loaded in this browser, and lets those be cleared. +- **Characters:** the character builder's Back and Next repeat at the bottom of each page, and open the next page at its top. +- **Homebrew:** the add buttons on homebrew builders repeat below a long list of traits or options. ### Changed From bec1135f58899c850de1aee1cdb8f0ad10779dcf Mon Sep 17 00:00:00 2001 From: codeGlaze Date: Sat, 17 Oct 2026 22:31:30 -0400 Subject: [PATCH 4/4] ui forms: The bottom Back and Next change the page before scrolling to it --- src/cljs/orcpub/character_builder.cljs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/cljs/orcpub/character_builder.cljs b/src/cljs/orcpub/character_builder.cljs index 599a65a34..bdb688d62 100644 --- a/src/cljs/orcpub/character_builder.cljs +++ b/src/cljs/orcpub/character_builder.cljs @@ -2067,9 +2067,10 @@ (defn- show-page-top! "After the next render, scrolls the builder's page to its top, just below the sticky header, - and moves focus to the page title so a screen reader starts there. No args; returns nil." + and moves focus to the page title so a screen reader starts there. No args; returns nil. + GOTCHA: the page change must already be in the db (dispatch-sync), or this measures the old page." [] - (js/requestAnimationFrame + (r/after-render (fn [] (when-let [col (.getElementById js/document "options-column")] (let [header (or (some-> (.querySelector js/document ".sticky-header") .-offsetHeight) 0)] @@ -2082,7 +2083,11 @@ copy below the page, whose buttons also bring the new page's top into view. Returns hiccup." [page-index page-name bottom?] - (let [go (fn [i] (dispatch [:set-page i]) (when bottom? (show-page-top!)))] + (let [go (fn [i] + (if bottom? + (do (dispatch-sync [:set-page i]) + (show-page-top!)) + (dispatch [:set-page i])))] [:div.flex.justify-cont-s-b.p-t-5.p-10.align-items-t [:button.form-button.p-5-10.m-r-5 {:on-click (fn [_] (go (let [prev (dec page-index)]