Improve index tables

Stretch inputs to column width in place of fixed sizing.
Set minimum column widths for columns with form inputs.
Simplify table operations by removing `thead` and adding sentinel.
Manage empty table display with CSS only.
This commit is contained in:
2026-10-17 05:13:39 +02:00
parent 2c845f3528
commit 9c51887024
19 changed files with 108 additions and 106 deletions
+30 -6
View File
@@ -24,6 +24,7 @@
* *
* NOTE: style in a modular way, similar to how CSS @scope would be used, * NOTE: style in a modular way, similar to how CSS @scope would be used,
* to make transition easier once @scope is widely available. */ * to make transition easier once @scope is widely available. */
/* TODO: change :root -> <html> ? */
:root { :root {
--color-focus-gray: #f3f3f3; --color-focus-gray: #f3f3f3;
--color-border-gray: #dddddd; --color-border-gray: #dddddd;
@@ -171,6 +172,7 @@ table svg:not(:only-child) {
width: 1.25em; width: 1.25em;
} }
input:focus-visible, input:focus-visible,
/* TODO: select:focus-visible is redundant with -within? */
select:focus-visible, select:focus-visible,
select:focus-within, select:focus-within,
/* TODO: how to achieve `summary:focus-within` for `::details-content`? */ /* TODO: how to achieve `summary:focus-within` for `::details-content`? */
@@ -301,6 +303,7 @@ header {
grid-area: header; grid-area: header;
} }
/* TODO: change .navigation -> <nav> ? */
.navigation { .navigation {
display: flex; display: flex;
grid-area: nav; grid-area: nav;
@@ -463,20 +466,27 @@ header {
.items-table { .items-table {
border-spacing: 0; border: none;
border: 1px solid var(--color-border-gray);
border-radius: 0.25em; border-radius: 0.25em;
border-spacing: 0;
font-size: 0.85rem; font-size: 0.85rem;
text-align: left; text-align: left;
} }
.items-table thead { .items-table:has(tbody > tr > td) {
font-size: 0.8rem; border: 1px solid var(--color-border-gray);
} }
.items-table thead, .items-table:has(tbody > tr > td) caption {
.items-table tbody tr:hover { display: none;
}
.items-table tbody:not(:has(tr > td)){
visibility: collapse;
}
.items-table th,
.items-table tr:where(:hover) {
background-color: var(--color-focus-gray); background-color: var(--color-focus-gray);
} }
.items-table th { .items-table th {
font-size: 0.8rem;
padding: 0.75em 0 0.75em 1em; padding: 0.75em 0 0.75em 1em;
text-align: center; text-align: center;
} }
@@ -560,6 +570,20 @@ header {
.hmin50 { .hmin50 {
min-width: 50%; min-width: 50%;
} }
.hminnumber {
min-width: 5em;
}
.hmintext {
min-width: 6em;
}
.hstretch {
/* `width` conflicts with <input>/<textarea> size defined by [size]/[cols]
* attributes. It's best to not use these attributes (e.g. set them to 1) when
* setting width through stylesheet. */
/* NOTE: `field-sizing` can be considered after upgrade do FF >=152 */
width: -moz-available;
width: stretch;
}
.italic { .italic {
color: var(--color-gray); color: var(--color-gray);
font-style: italic; font-style: italic;
+5 -1
View File
@@ -92,6 +92,10 @@ module ApplicationHelper
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{selector}(method, options = {}) def #{selector}(method, options = {})
options[:maxlength] ||= object.type_for_attribute(method).limit options[:maxlength] ||= object.type_for_attribute(method).limit
# If [cols]/[size] is not set, minimum field width is controlled with
# stylesheet. To prevent browser estimating missing [cols]/[size]
# based on e.g. [maxlength], set it to 1.
options[:#{selector == :text_area ? :cols : :size}] ||= 1
super super
end end
RUBY_EVAL RUBY_EVAL
@@ -103,7 +107,7 @@ module ApplicationHelper
options[:min] ||= Float::MIN_15.to_scientific options[:min] ||= Float::MIN_15.to_scientific
options[:max] ||= Float::MAX_15.to_scientific options[:max] ||= Float::MAX_15.to_scientific
options[:oninput] = 'numberValidate(event)' options[:oninput] = 'numberValidate(event)'
options[:size] ||= 6 options[:size] ||= options[:class]&.split&.include?('hstretch') ? 1 : 6
options[:step] ||= :any options[:step] ||= :any
options[:value] = object.public_send("#{method}_before_type_cast") options[:value] = object.public_send("#{method}_before_type_cast")
end end
+2 -1
View File
@@ -19,7 +19,8 @@
<details id="quantity_select" class="center hexpand" open <details id="quantity_select" class="center hexpand" open
onkeydown="detailsProcessKey(event)"> onkeydown="detailsProcessKey(event)">
<summary autofocus> <summary autofocus>
<!-- TODO: Set content with CSS when span empty to avoid duplication --> <%# TODO: Set content with CSS when span empty to avoid duplication; or
change to disabled input with prompt %>
<span data-prompt="<%= t('.select_quantity') %>"> <span data-prompt="<%= t('.select_quantity') %>">
<%= t('.select_quantity') %> <%= t('.select_quantity') %>
</span> </span>
+6 -9
View File
@@ -3,17 +3,14 @@
data: {link: link, form: form_tag, hidden_row: hidden_row} do %> data: {link: link, form: form_tag, hidden_row: hidden_row} do %>
<td style="--depth:<%= @quantity.depth %>"> <td style="--depth:<%= @quantity.depth %>">
<%= form.text_field :name, required: true, autofocus: true, size: 20 %> <%= form.text_field :name, autofocus: true, class: 'hstretch', required: true %>
</td> <td><%= form.text_area :description, class: 'hstretch', escape: false, rows: 1 %>
<td>
<%= form.text_area :description, cols: 30, rows: 1, escape: false %>
</td>
<td class="flex"> <td class="flex">
<%= form.button %> <%= form.button %>
<%= image_link_to t(:cancel), "close-outline", quantities_path, class: 'dangerous', <%= image_link_to t(:cancel), "close-outline", quantities_path,
name: :cancel, onclick: render_turbo_stream('form_close', {row: row}) %> class: 'dangerous', name: :cancel,
</td> onclick: render_turbo_stream('form_close', {row: row}) %>
<td></td> <td>
<% end %> <% end %>
<% end %> <% end %>
+1 -3
View File
@@ -1,7 +1,5 @@
<%= turbo_stream.close_form dom_target(:new, @quantity.parent || :quantity) %> <%= turbo_stream.close_form dom_target(:new, @quantity.parent || :quantity) %>
<%= turbo_stream.remove :no_items %>
<% @ancestors.each do |ancestor| %> <% @ancestors.each do |ancestor| %>
<%= turbo_stream.replace ancestor %> <%= turbo_stream.replace ancestor %>
<% end %> <% end %>
<%= @before ? turbo_stream.before(@before, @quantity) : <%= turbo_stream.before @before || :endmark, @quantity %>
turbo_stream.append(:quantities, @quantity) %>
@@ -2,4 +2,3 @@
<%= turbo_stream.replace ancestor %> <%= turbo_stream.replace ancestor %>
<% end %> <% end %>
<%= turbo_stream.remove @quantity %> <%= turbo_stream.remove @quantity %>
<%= turbo_stream.append(:quantities, render_no_items) if current_user.quantities.empty? %>
+1 -2
View File
@@ -3,11 +3,10 @@
link: nil, link: nil,
form_tag: dom_target(:edit, @quantity, :form)} %> form_tag: dom_target(:edit, @quantity, :form)} %>
<%= turbo_stream.append :quantity_form do %> <%= turbo_stream.append_all 'body' do %>
<%- tabular_form_with model: @quantity, html: {id: ids[:form_tag]} do %> <%- tabular_form_with model: @quantity, html: {id: ids[:form_tag]} do %>
<% end %> <% end %>
<% end %> <% end %>
<%= turbo_stream.hide ids[:hidden_row] %> <%= turbo_stream.hide ids[:hidden_row] %>
<%= turbo_stream.remove ids[:row] %>
<%= turbo_stream.after @quantity, partial: 'form', locals: ids -%> <%= turbo_stream.after @quantity, partial: 'form', locals: ids -%>
+19 -21
View File
@@ -3,31 +3,29 @@
<%= image_link_to t('.new_quantity'), 'plus-outline', new_quantity_path, <%= image_link_to t('.new_quantity'), 'plus-outline', new_quantity_path,
id: :new_quantity_link, onclick: 'this.blur();', data: {turbo_stream: true} %> id: :new_quantity_link, onclick: 'this.blur();', data: {turbo_stream: true} %>
<% end %> <% end %>
<%#= image_link_to t('.import_quantities'), 'download-outline', default_quantities_path, <%#= image_link_to t('.import_quantities'), 'download-outline',
class: 'tools-area' %> default_quantities_path, class: 'tools-area' %>
</div> </div>
<%# TODO: remove? form can be inserted directly, e.g. at the end of index %>
<%= tag.div class: 'main-area', id: :quantity_form %>
<table class="main-area items-table"> <table class="main-area items-table">
<thead> <caption class="hint"><%= t '.no_items' %></caption>
<tbody>
<tr> <tr>
<th><%= Quantity.human_attribute_name(:name) %></th> <th class="hmintext"><%= Quantity.human_attribute_name(:name) %>
<th class="hexpand"><%= Quantity.human_attribute_name(:description) %></th> <th class="hexpand"><%= Quantity.human_attribute_name(:description) %>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<th><%= t :actions %></th> <th><%= t :actions %>
<th></th> <th>
<tr id="quantity_" hidden
ondragover="dragOver(event)" ondrop="drop(event)"
ondragenter="dragEnter(event)" ondragleave="dragLeave(event)"
data-drop-id="quantity_"
data-drop-id-param="<%= field_name(:quantity, :parent_id) %>">
<th colspan="4"><%= t '.top_level_drop' %>
<% end %> <% end %>
</tr>
<%= tag.tr id: "quantity_", hidden: true, <%= render @quantities %>
ondragover: "dragOver(event)", ondrop: "drop(event)", <tr id="endmark" hidden>
ondragenter: "dragEnter(event)", ondragleave: "dragLeave(event)",
data: {drop_id: "quantity_", drop_id_param: "quantity[parent_id]"} do %>
<th colspan="4"><%= t '.top_level_drop' %></th>
<% end %>
</thead>
<tbody id="quantities">
<%= render(@quantities) || render_no_items %>
</tbody>
</table> </table>
+2 -7
View File
@@ -6,15 +6,10 @@
<%= turbo_stream.disable ids[:link] -%> <%= turbo_stream.disable ids[:link] -%>
<%= turbo_stream.append :quantity_form do %> <%= turbo_stream.append_all 'body' do %>
<%- tabular_form_with model: @quantity, html: {id: ids[:form_tag]} do |form| %> <%- tabular_form_with model: @quantity, html: {id: ids[:form_tag]} do |form| %>
<%= form.hidden_field :parent_id if @quantity.parent_id? %> <%= form.hidden_field :parent_id if @quantity.parent_id? %>
<% end %> <% end %>
<% end %> <% end %>
<% if @quantity.parent_id? %> <%= turbo_stream.after @quantity.parent || :quantity_, partial: 'form', locals: ids %>
<%= turbo_stream.remove ids[:row] %>
<%= turbo_stream.after @quantity.parent, partial: 'form', locals: ids %>
<% else %>
<%= turbo_stream.prepend :quantities, partial: 'form', locals: ids %>
<% end %>
@@ -1,9 +1,7 @@
<% @self_and_progenies.each do |q| %>
<%= turbo_stream.remove q %>
<% end %>
<% @previous_ancestors.union(@ancestors).each do |ancestor| %> <% @previous_ancestors.union(@ancestors).each do |ancestor| %>
<%= turbo_stream.replace ancestor %> <%= turbo_stream.replace ancestor %>
<% end %> <% end %>
<% @self_and_progenies.each do |q| %> <% @self_and_progenies.each do |quantity| %>
<%= @before ? turbo_stream.before(@before, q) : turbo_stream.append(:quantities, q) %> <%= turbo_stream.remove quantity %>
<%= turbo_stream.before @before || :endmark, quantity %>
<% end %> <% end %>
+6 -9
View File
@@ -3,20 +3,17 @@
data: {link: link, form: form_tag, hidden_row: hidden_row} do %> data: {link: link, form: form_tag, hidden_row: hidden_row} do %>
<td style="--depth:<%= @unit.base_id? ? 1 : 0 %>"> <td style="--depth:<%= @unit.base_id? ? 1 : 0 %>">
<%= form.text_field :symbol, required: true, autofocus: true, size: 12 %> <%= form.text_field :symbol, autofocus: true, class: 'hstretch', required: true %>
</td>
<td> <td>
<%= form.text_area :description, cols: 30, rows: 1, escape: false %> <% if @unit.base_id? %>
</td> <%= form.number_field :multiplier, class: 'hstretch', required: true %>
<td> <% end %>
<%= form.number_field :multiplier, required: true if @unit.base_id? %> <td><%= form.text_area :description, class: 'hstretch', escape: false, rows: 1 %>
</td>
<td class="flex"> <td class="flex">
<%= form.button %> <%= form.button %>
<%= image_link_to t(:cancel), "close-outline", units_path, class: 'dangerous', <%= image_link_to t(:cancel), "close-outline", units_path, class: 'dangerous',
name: :cancel, onclick: render_turbo_stream('form_close', {row: row}) %> name: :cancel, onclick: render_turbo_stream('form_close', {row: row}) %>
</td> <td>
<td></td>
<% end %> <% end %>
<% end %> <% end %>
+1 -1
View File
@@ -9,9 +9,9 @@
<td style="--depth:<%= unit.base_id? ? 1 : 0 %>"> <td style="--depth:<%= unit.base_id? ? 1 : 0 %>">
<%= link_to unit, edit_unit_path(unit), class: 'link', onclick: 'this.blur();', <%= link_to unit, edit_unit_path(unit), class: 'link', onclick: 'this.blur();',
data: {turbo_stream: true} %> data: {turbo_stream: true} %>
<td><%= unit.description %>
<%# Closing <td> tag required to avoid whitespace inclusion for testing. %> <%# Closing <td> tag required to avoid whitespace inclusion for testing. %>
<td class="ralign"><%= unit.multiplier.to_html if unit.base_id? %></td> <td class="ralign"><%= unit.multiplier.to_html if unit.base_id? %></td>
<td><%= unit.description %>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<td class="flex"> <td class="flex">
+1 -2
View File
@@ -1,4 +1,3 @@
<%= turbo_stream.close_form dom_target(:new, @unit.base || :unit) %> <%= turbo_stream.close_form dom_target(:new, @unit.base || :unit) %>
<%= turbo_stream.remove :no_items %>
<%= turbo_stream.replace @unit.base if @unit.base_id? %> <%= turbo_stream.replace @unit.base if @unit.base_id? %>
<%= @before ? turbo_stream.before(@before, @unit) : turbo_stream.append(:units, @unit) %> <%= turbo_stream.before @before || :endmark, @unit %>
-1
View File
@@ -1,3 +1,2 @@
<%= turbo_stream.replace @unit.base if @unit.base_id? %> <%= turbo_stream.replace @unit.base if @unit.base_id? %>
<%= turbo_stream.remove @unit %> <%= turbo_stream.remove @unit %>
<%= turbo_stream.append(:units, render_no_items) if current_user.units.empty? %>
+1 -2
View File
@@ -3,11 +3,10 @@
link: nil, link: nil,
form_tag: dom_target(:edit, @unit, :form)} %> form_tag: dom_target(:edit, @unit, :form)} %>
<%= turbo_stream.append :unit_form do %> <%= turbo_stream.append_all 'body' do %>
<%- tabular_form_with model: @unit, html: {id: ids[:form_tag]} do %> <%- tabular_form_with model: @unit, html: {id: ids[:form_tag]} do %>
<% end %> <% end %>
<% end %> <% end %>
<%= turbo_stream.hide ids[:hidden_row] %> <%= turbo_stream.hide ids[:hidden_row] %>
<%= turbo_stream.remove ids[:row] %>
<%= turbo_stream.after @unit, partial: 'form', locals: ids -%> <%= turbo_stream.after @unit, partial: 'form', locals: ids -%>
+19 -20
View File
@@ -7,28 +7,27 @@
class: 'tools-area' %> class: 'tools-area' %>
</div> </div>
<%# TODO: remove? form can be inserted directly, e.g. at the end of index %>
<%= tag.div id: :unit_form %>
<table class="main-area items-table"> <table class="main-area items-table">
<thead> <caption class="hint"><%= t '.no_items' %></caption>
<tbody>
<tr> <tr>
<th><%= Unit.human_attribute_name(:symbol) %></th> <th class="hmintext"><%= Unit.human_attribute_name(:symbol) %>
<th class="hexpand"><%= Unit.human_attribute_name(:description) %></th> <th class="hminnumber"><%= Unit.human_attribute_name(:multiplier) %>
<th><%= Unit.human_attribute_name(:multiplier) %></th> <th class="hexpand"><%= Unit.human_attribute_name(:description) %>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<th><%= t :actions %></th> <th><%= t :actions %>
<th></th> <th>
<tr id="unit_" hidden
ondragover="dragOver(event)" ondrop="drop(event)"
ondragenter="dragEnter(event)" ondragleave="dragLeave(event)"
data-drop-id="unit_" data-drop-id-param="<%= field_name(:unit, :base_id) %>">
<th colspan="5"><%= t '.top_level_drop' %>
<% end %> <% end %>
</tr>
<%= tag.tr id: "unit_", hidden: true, <%= render @units %>
ondragover: "dragOver(event)", ondrop: "drop(event)", <%# Hidden rows with IDs allow simplier row insertion by using only
ondragenter: "dragEnter(event)", ondragleave: "dragLeave(event)", turbo_stream #after/#before instead of #after + #prepend/#before + #append. %>
data: {drop_id: "unit_", drop_id_param: "unit[base_id]"} do %> <tr id="endmark" hidden>
<th colspan="5"><%= t '.top_level_drop' %></th>
<% end %>
</thead>
<tbody id="units">
<%= render(@units) || render_no_items %>
</tbody>
</table> </table>
+2 -7
View File
@@ -6,15 +6,10 @@
<%= turbo_stream.disable ids[:link] -%> <%= turbo_stream.disable ids[:link] -%>
<%= turbo_stream.append :unit_form do %> <%= turbo_stream.append_all 'body' do %>
<%- tabular_form_with model: @unit, html: {id: ids[:form_tag]} do |form| %> <%- tabular_form_with model: @unit, html: {id: ids[:form_tag]} do |form| %>
<%= form.hidden_field :base_id if @unit.base_id? %> <%= form.hidden_field :base_id if @unit.base_id? %>
<% end %> <% end %>
<% end %> <% end %>
<% if @unit.base_id? %> <%= turbo_stream.after @unit.base || :unit_, partial: 'form', locals: ids %>
<%= turbo_stream.remove ids[:row] %>
<%= turbo_stream.after @unit.base, partial: 'form', locals: ids %>
<% else %>
<%= turbo_stream.prepend :units, partial: 'form', locals: ids %>
<% end %>
+4 -3
View File
@@ -1,4 +1,5 @@
<% [@previous_base, @unit.base].uniq.compact.each do |unit| %>
<%= turbo_stream.replace unit %>
<% end %>
<%= turbo_stream.remove @unit %> <%= turbo_stream.remove @unit %>
<%= turbo_stream.replace @previous_base if @previous_base %> <%= turbo_stream.before @before || :endmark, @unit %>
<%= turbo_stream.replace @unit.base if @unit.base_id? && (@previous_base&.id != @unit.base_id) %>
<%= @before ? turbo_stream.before(@before, @unit) : turbo_stream.append(:units, @unit) %>
+3 -3
View File
@@ -108,12 +108,12 @@ en:
new_measurement: Add measurement new_measurement: Add measurement
quantities: quantities:
navigation: Quantities navigation: Quantities
no_items: There are no configured quantities. You can Add some or Import from defaults.
quantity: quantity:
new_subquantity: Child new_subquantity: Child
destroy: Delete destroy: Delete
index: index:
new_quantity: Add quantity new_quantity: Add quantity
no_items: There are no configured quantities. You can Add some or Import from defaults.
top_level_drop: Drop here to reposition into top-level quantity top_level_drop: Drop here to reposition into top-level quantity
create: create:
success: Created new quantity "%{quantity}" success: Created new quantity "%{quantity}"
@@ -123,13 +123,13 @@ en:
success: Deleted quantity "%{quantity}" success: Deleted quantity "%{quantity}"
units: units:
navigation: Units navigation: Units
no_items: There are no configured units. You can Add some or Import from defaults.
unit: unit:
new_subunit: Subunit new_subunit: Subunit
destroy: Delete destroy: Delete
index: index:
new_unit: Add unit
import_units: Import import_units: Import
new_unit: Add unit
no_items: There are no configured units. You can Add some or Import from defaults.
top_level_drop: Drop here to reposition into top-level unit top_level_drop: Drop here to reposition into top-level unit
create: create:
success: Created new unit "%{unit}" success: Created new unit "%{unit}"