From bd1a664caa09456c5c2a8f10551a74d82d6508f2 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 31 Jan 2026 17:22:09 +0100 Subject: [PATCH] Measurement form based on select-styled
--- app/assets/images/pictograms/chevron-down.svg | 1 + .../images/pictograms/pencil-outline.svg | 1 + app/assets/stylesheets/application.css | 123 +++++++++++-- app/controllers/measurements_controller.rb | 42 +---- app/controllers/readouts_controller.rb | 39 ++++ app/helpers/application_helper.rb | 11 +- app/helpers/quantities_helper.rb | 9 + app/javascript/application.js | 173 +++++++++++++----- app/models/measurement.rb | 3 + app/models/quantity.rb | 5 + app/views/measurements/_form.html.erb | 58 +++--- app/views/measurements/_form_close.html.erb | 4 +- app/views/measurements/_form_frame.html.erb | 16 -- app/views/measurements/_form_repath.html.erb | 7 - .../measurements/discard.turbo_stream.erb | 2 - app/views/measurements/index.html.erb | 28 ++- app/views/measurements/new.turbo_stream.erb | 10 +- app/views/quantities/_form.html.erb | 2 +- app/views/quantities/index.html.erb | 3 +- app/views/readouts/_form.html.erb | 25 +++ app/views/readouts/_form_repath.html.erb | 7 + app/views/readouts/discard.turbo_stream.erb | 4 + app/views/readouts/new.turbo_stream.erb | 8 + app/views/units/_form.html.erb | 2 +- config/initializers/core_ext.rb | 1 + .../initializers/turbo_streams_tag_builder.rb | 12 ++ config/locales/en.yml | 11 +- config/routes.rb | 6 +- lib/core_ext/array_delete_bang.rb | 10 + 29 files changed, 433 insertions(+), 190 deletions(-) create mode 100644 app/assets/images/pictograms/chevron-down.svg create mode 100644 app/assets/images/pictograms/pencil-outline.svg create mode 100644 app/controllers/readouts_controller.rb create mode 100644 app/models/measurement.rb delete mode 100644 app/views/measurements/_form_frame.html.erb delete mode 100644 app/views/measurements/_form_repath.html.erb delete mode 100644 app/views/measurements/discard.turbo_stream.erb create mode 100644 app/views/readouts/_form.html.erb create mode 100644 app/views/readouts/_form_repath.html.erb create mode 100644 app/views/readouts/discard.turbo_stream.erb create mode 100644 app/views/readouts/new.turbo_stream.erb create mode 100644 lib/core_ext/array_delete_bang.rb diff --git a/app/assets/images/pictograms/chevron-down.svg b/app/assets/images/pictograms/chevron-down.svg new file mode 100644 index 0000000..369236e --- /dev/null +++ b/app/assets/images/pictograms/chevron-down.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/pictograms/pencil-outline.svg b/app/assets/images/pictograms/pencil-outline.svg new file mode 100644 index 0000000..eb103c8 --- /dev/null +++ b/app/assets/images/pictograms/pencil-outline.svg @@ -0,0 +1 @@ + diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6fe64c8..4a93d84 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -49,6 +49,7 @@ /* TODO: collapse gaps around empty rows (`topside`) once possible + * with grid-collapse property and remove alternative grid-template * https://github.com/w3c/csswg-drafts/issues/5813 */ body { display: grid; @@ -56,20 +57,27 @@ body { grid-template-areas: "header header header" "nav nav nav" - "leftempty topside rightempty" + "leftside topside rightside" "leftside main rightside"; - grid-template-columns: 1fr auto 1fr; - grid-template-rows: repeat(4, auto); + grid-template-columns: 1fr minmax(max-content, 2fr) 1fr; font-family: system-ui; margin: 0.4em; } +body:not(:has(.topside)) { + grid-template-areas: + "header header header" + "nav nav nav" + "leftside main rightside"; +} button, +details, input, select, textarea { background-color: inherit; font: inherit; } +details, input, select { text-align: inherit; @@ -86,9 +94,10 @@ input[type=submit] { text-decoration: none; white-space: nowrap; } +/* [hidden] submit controls cannot have `display` set as it makes them visible */ .button, -button, -input[type=submit], +button:not([hidden]), +input[type=submit]:not([hidden]), .tab { align-items: center; color: var(--color-gray); @@ -105,11 +114,13 @@ input[type=submit] { } input:not([type=submit]):not([type=checkbox]), select, +summary, textarea { padding: 0.2em 0.4em; } .button, button, +details, input, select, textarea { @@ -134,6 +145,9 @@ button > svg:not(:last-child) { fieldset { padding: 0.4em; } +fieldset:not(:has(input, select, textarea)) { + display: none; +} legend { color: var(--color-gray); display: flex; @@ -191,6 +205,7 @@ input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } +details:hover, input:hover, select:hover, textarea:hover { @@ -203,9 +218,11 @@ textarea:invalid { border-color: var(--color-red); outline: solid 1px var(--color-red); } +details:hover, select:hover { cursor: pointer; } +details:focus-visible, input:focus-visible, select:focus-within, select:focus-visible, @@ -417,10 +434,10 @@ table.items td.link a::after { table.items td:first-child { padding-inline-start: calc(1em + var(--depth) * 0.8em); } -table.items td:has(input, textarea) { +table.items td:has(input, select, textarea) { padding-inline-start: calc(0.6em - 0.9px); } -table.items td:first-child:has(input, textarea) { +table.items td:first-child:has(input, select, textarea) { padding-inline-start: calc(0.6em + var(--depth) * 0.8em - 0.9px); } table.items th:last-child { @@ -484,12 +501,6 @@ table.items td:not(:first-child), color: var(--color-table-gray); fill: var(--color-table-gray); } -table.items td.hint { - color: var(--color-table-gray); - font-style: italic; - font-size: 0.8rem; - padding: 1em; -} table.items svg { height: 1.2rem; vertical-align: middle; @@ -555,16 +566,34 @@ form table.items td:first-child { .extendedright { margin-right: auto; } +.hexpand { + width: 100%; +} .hflex { display: flex; gap: 0.8em; } +.hflex.reverse { + flex-direction: row-reverse; +} +.hflex.centered { + justify-content: center; +} +.hint { + color: var(--color-table-gray); + font-style: italic; + font-size: 0.9rem; + text-align: center; +} .vflex { display: flex; gap: 0.8em; flex-direction: column; } [disabled] { +/* label:has(input[disabled]) { + * TODO: disabled checkbox blue square focus removal; disabled label styling + * */ border-color: var(--color-border-gray) !important; color: var(--color-border-gray) !important; cursor: not-allowed; @@ -574,3 +603,71 @@ form table.items td:first-child { .unwrappable { white-space: nowrap; } + + +details { + align-content: center; + position: relative; +} +summary { + align-items: center; + color: var(--color-gray); + display: flex; + gap: 0.2em; + height: 100%; + white-space: nowrap; +} +summary::before { + background-color: #000; + content: ""; + height: 1em; + mask-image: url('pictograms/chevron-down.svg'); + mask-size: cover; + width: 1em; +} +summary:has(.button) { + padding-block: 0; + padding-inline-end: 0; +} +summary .button { + border: 1px var(--color-border-gray); + border-radius: 0; + border-style: none none none solid; + height: 100%; +} +summary span { + width: 100%; +} +details[open] summary::before { + transform: rotate(180deg); +} +summary::marker { + padding-left: 0.25em; +} +/* TODO: use ::details-content ? */ +details[open] ul { + background: white; + border: solid 1px var(--color-border-gray); + border-radius: 0.25em; + box-sizing: content-box; + box-shadow: 1px 1px 3px var(--color-border-gray); + margin: 0; + margin-left: -0.9px; + padding-left: 0; + position: absolute; + width: 100%; +} +li:hover { + background-color: var(--color-focus-gray); +} +li label { + align-items: center; + display: flex; + line-height: 1.4em; +} +li input[type=checkbox] { + margin: 0 0.25em; +} +li::marker { + content: ''; +} diff --git a/app/controllers/measurements_controller.rb b/app/controllers/measurements_controller.rb index 307aac8..ebe6622 100644 --- a/app/controllers/measurements_controller.rb +++ b/app/controllers/measurements_controller.rb @@ -1,36 +1,11 @@ class MeasurementsController < ApplicationController - before_action :find_quantity, only: [:new, :discard] - before_action :find_prev_quantities, only: [:new, :discard] - def index - @quantities = current_user.quantities.ordered + @measurements = [] + #@measurements = current_user.units.ordered.includes(:base, :subunits) end def new - quantities = - case params[:scope] - when 'children' - @quantity.subquantities - when 'subtree' - @quantity.progenies - else - [@quantity] - end - quantities -= @prev_quantities - @readouts = current_user.readouts.build(quantities.map { |q| {quantity: q} }) - - @units = current_user.units.ordered - - all_quantities = @prev_quantities + quantities - @common_ancestor = current_user.quantities - .common_ancestors(all_quantities.map(&:parent_id)).first - end - - def discard - @prev_quantities -= [@quantity] - - @common_ancestor = current_user.quantities - .common_ancestors(@prev_quantities.map(&:parent_id)).first + @quantities = current_user.quantities.ordered end def create @@ -38,15 +13,4 @@ class MeasurementsController < ApplicationController def destroy end - - private - - def find_quantity - @quantity = current_user.quantities.find_by!(id: params[:id]) - end - - def find_prev_quantities - prev_quantity_ids = params[:readouts]&.map { |r| r[:quantity_id] } || [] - @prev_quantities = current_user.quantities.find(prev_quantity_ids) - end end diff --git a/app/controllers/readouts_controller.rb b/app/controllers/readouts_controller.rb new file mode 100644 index 0000000..e47f271 --- /dev/null +++ b/app/controllers/readouts_controller.rb @@ -0,0 +1,39 @@ +class ReadoutsController < ApplicationController + before_action :find_quantities, only: [:new] + before_action :find_quantity, only: [:discard] + before_action :find_prev_quantities, only: [:new, :discard] + + def new + @quantities -= @prev_quantities + # TODO: raise ParameterInvalid if new_quantities.empty? + @readouts = current_user.readouts.build(@quantities.map { |q| {quantity: q} }) + + @user_units = current_user.units.ordered + + quantities = @prev_quantities + @quantities + @superquantity = current_user.quantities + .common_ancestors(quantities.map(&:parent_id)).first + end + + def discard + @prev_quantities -= [@quantity] + + @superquantity = current_user.quantities + .common_ancestors(@prev_quantities.map(&:parent_id)).first + end + + private + + def find_quantities + @quantities = current_user.quantities.find(params[:quantity]) + end + + def find_quantity + @quantity = current_user.quantities.find_by!(id: params[:id]) + end + + def find_prev_quantities + prev_quantity_ids = params[:readouts]&.map { |r| r[:quantity_id] } || [] + @prev_quantities = current_user.quantities.find(prev_quantity_ids) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1fce8a0..ebdd181 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -75,12 +75,11 @@ module ApplicationHelper end def number_field(method, options = {}) - value = object.public_send(method) - if value.is_a?(BigDecimal) - options[:value] = value.to_scientific - type = object.class.type_for_attribute(method) - options[:step] ||= BigDecimal(10).power(-type.scale) - options[:max] ||= BigDecimal(10).power(type.precision - type.scale) - + attr_type = object.type_for_attribute(method) + if attr_type.type == :decimal + options[:value] = object.public_send(method)&.to_scientific + options[:step] ||= BigDecimal(10).power(-attr_type.scale) + options[:max] ||= BigDecimal(10).power(attr_type.precision - attr_type.scale) - options[:step] options[:min] = options[:min] == :step ? options[:step] : options[:min] options[:min] ||= -options[:max] diff --git a/app/helpers/quantities_helper.rb b/app/helpers/quantities_helper.rb index 9e6aa8b..2bb64b9 100644 --- a/app/helpers/quantities_helper.rb +++ b/app/helpers/quantities_helper.rb @@ -1,2 +1,11 @@ module QuantitiesHelper + def quantities_check_boxes + # Closing
on focusout event depends on relatedTarget for internal + # actions being non-null. To ensure this, all top-layer elements of + # ::details-content must accept focus, e.g.