keystone_ui 0.33.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af9ebb3caf69815bf6272cf2f67028d1f4e7a98b44f5e8fb0c70654a54b84303
4
- data.tar.gz: a0465bf4038d273df4d29aca184443640f113a73260842c7590e12d040557a18
3
+ metadata.gz: c2f3f4d0784444fc8cf7acde02c493fa2829dbcca367d1af4a852338b6f42212
4
+ data.tar.gz: 366951b7d1e19d053229c9c509db6555a8de379442e7bf0d11dfb9637992e012
5
5
  SHA512:
6
- metadata.gz: 986eb9e5c25dfafa87579cec226b9a7ec3511478a0a402158e2a5279f290311e3e6d513a2f082f331d01e5f2d0fd73463f7bf108a401276530963a2cecbec977
7
- data.tar.gz: 1a0c99faa3fd4c3c03863adf0a467cd53de595367af6cf015c6ffe6afab5e2b31b7713110341b26aad4dd11b6ecd626146020a9aef9549b7c6e600886e383cfa
6
+ metadata.gz: 8f8064c88fc4ce2937ec84bff38ee5a60eab0fe03380995af9204fc2ccc58d82ec7c5cf89203ceb991088b0e0e06c6aa1b9d4106b251b099ea86e8303a686d85
7
+ data.tar.gz: d9453549349d298e249bab69ee78ea54d70a09c1cbf989c9aaba21c307c8525b985708f4645e3374bb99e72457954cf9c5d39c4a5f71ab7ecbe6c0e4f490f17b
@@ -1,7 +1,7 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ["menu"]
4
+ static targets = ["menu", "option"]
5
5
  static values = { saveUrl: String }
6
6
 
7
7
  connect() {
@@ -15,33 +15,92 @@ export default class extends Controller {
15
15
 
16
16
  toggle(event) {
17
17
  event.stopPropagation()
18
- this.menuTarget.classList.toggle("hidden")
18
+ if (this.menuTarget.classList.contains("hidden")) {
19
+ this.menuTarget.classList.remove("hidden")
20
+ } else {
21
+ this.hideMenu()
22
+ }
19
23
  }
20
24
 
21
25
  close(event) {
22
26
  if (!this.element.contains(event.target)) {
23
- this.menuTarget.classList.add("hidden")
27
+ this.hideMenu()
24
28
  }
25
29
  }
26
30
 
27
- save() {
28
- const allCheckboxes = this.element.querySelectorAll("input[type=checkbox]")
29
- const hiddenColumns = Array.from(allCheckboxes)
30
- .filter(cb => !cb.checked)
31
- .map(cb => cb.value)
32
-
33
- if (this.hasSaveUrlValue) {
34
- const token = document.querySelector('meta[name="csrf-token"]')?.content
35
- fetch(this.saveUrlValue, {
36
- method: "PATCH",
37
- headers: {
38
- "Content-Type": "application/json",
39
- "X-CSRF-Token": token
40
- },
41
- body: JSON.stringify({ hidden_columns: hiddenColumns })
42
- }).then(() => {
43
- Turbo.visit(window.location.href, { action: "replace" })
44
- })
45
- }
31
+ hideMenu() {
32
+ this.menuTarget.classList.add("hidden")
33
+ if (!this.changed) return
34
+
35
+ this.changed = false
36
+ this.send()
37
+ }
38
+
39
+ mark(event) {
40
+ const option = this.optionFor(event)
41
+ option.querySelector("label").classList.toggle("ks-menu-option-hidden", !this.checkboxIn(option).checked)
42
+ this.changed = true
43
+ }
44
+
45
+ optionFor(event) {
46
+ return event.currentTarget.closest('[data-column-picker-target="option"]')
47
+ }
48
+
49
+ moveUp(event) {
50
+ const option = this.optionFor(event)
51
+ const previous = option.previousElementSibling
52
+ if (!previous) return
53
+
54
+ option.parentNode.insertBefore(option, previous)
55
+ this.moved()
56
+ }
57
+
58
+ moveDown(event) {
59
+ const option = this.optionFor(event)
60
+ const next = option.nextElementSibling
61
+ if (!next) return
62
+
63
+ option.parentNode.insertBefore(next, option)
64
+ this.moved()
65
+ }
66
+
67
+ moved() {
68
+ this.changed = true
69
+ const options = this.optionTargets
70
+ options.forEach((option, index) => {
71
+ option.querySelector('[data-action="click->column-picker#moveUp"]').disabled = index === 0
72
+ option.querySelector('[data-action="click->column-picker#moveDown"]').disabled = index === options.length - 1
73
+ })
74
+ }
75
+
76
+ columnOrder() {
77
+ return this.optionTargets.map(option => this.checkboxIn(option).value)
78
+ }
79
+
80
+ hiddenColumns() {
81
+ return this.optionTargets
82
+ .map(option => this.checkboxIn(option))
83
+ .filter(checkbox => !checkbox.checked)
84
+ .map(checkbox => checkbox.value)
85
+ }
86
+
87
+ checkboxIn(option) {
88
+ return option.querySelector("input[type=checkbox]")
89
+ }
90
+
91
+ send() {
92
+ if (!this.hasSaveUrlValue) return
93
+
94
+ const token = document.querySelector('meta[name="csrf-token"]')?.content
95
+ fetch(this.saveUrlValue, {
96
+ method: "PATCH",
97
+ headers: {
98
+ "Content-Type": "application/json",
99
+ "X-CSRF-Token": token
100
+ },
101
+ body: JSON.stringify({ hidden_columns: this.hiddenColumns(), column_order: this.columnOrder() })
102
+ }).then(() => {
103
+ Turbo.visit(window.location.href, { action: "replace" })
104
+ })
46
105
  }
47
106
  }
@@ -3,7 +3,7 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class BreadcrumbsComponent < ViewComponent::Base
6
- CLASSES = "ks-mobile-header-back hidden lg:block text-sm"
6
+ CLASSES = "ks-mobile-header-back ks-breadcrumbs hidden lg:block text-sm"
7
7
  SEPARATOR = "›"
8
8
 
9
9
  def initialize(trail:, current: nil)
@@ -6,15 +6,27 @@
6
6
  Columns
7
7
  </button>
8
8
  <div data-column-picker-target="menu" class="<%= MENU_CLASSES %>">
9
- <% hideable_columns.each do |col| %>
10
- <label class="<%= OPTION_CLASSES %>">
11
- <input type="checkbox"
12
- value="<%= col.key %>"
13
- <%= "checked" unless hidden?(col.key) %>
14
- data-action="change->column-picker#save"
15
- class="<%= CHECKBOX_CLASSES %>">
16
- <%= col.header_text %>
17
- </label>
9
+ <% hideable_columns.each_with_index do |col, index| %>
10
+ <div class="<%= OPTION_ROW_CLASSES %>" data-column-picker-target="option">
11
+ <label class="<%= option_classes(col.key) %>">
12
+ <input type="checkbox"
13
+ value="<%= col.key %>"
14
+ <%= "checked" unless hidden?(col.key) %>
15
+ data-action="change->column-picker#mark"
16
+ class="<%= CHECKBOX_CLASSES %>">
17
+ <%= col.header_text %>
18
+ </label>
19
+ <button type="button"
20
+ data-action="click->column-picker#moveUp"
21
+ aria-label="Move up"
22
+ <%= "disabled" if index.zero? %>
23
+ class="<%= MOVE_BUTTON_CLASSES %>">&uarr;</button>
24
+ <button type="button"
25
+ data-action="click->column-picker#moveDown"
26
+ aria-label="Move down"
27
+ <%= "disabled" if index == hideable_columns.size - 1 %>
28
+ class="<%= MOVE_BUTTON_CLASSES %>">&darr;</button>
29
+ </div>
18
30
  <% end %>
19
31
  </div>
20
32
  </div>
@@ -8,6 +8,9 @@ module Keystone
8
8
  MENU_CLASSES = "ks-menu absolute right-0 z-10 w-56 hidden"
9
9
  OPTION_CLASSES = "ks-menu-option flex items-center text-sm cursor-pointer"
10
10
  CHECKBOX_CLASSES = "ks-menu-checkbox"
11
+ OPTION_HIDDEN_CLASSES = "ks-menu-option-hidden"
12
+ OPTION_ROW_CLASSES = "flex items-center"
13
+ MOVE_BUTTON_CLASSES = "ks-menu-move text-sm"
11
14
 
12
15
  COLUMNS_ICON = <<~SVG.freeze
13
16
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
@@ -30,6 +33,10 @@ module Keystone
30
33
  def hidden?(key)
31
34
  @hidden_keys.include?(key.to_sym)
32
35
  end
36
+
37
+ def option_classes(key)
38
+ hidden?(key) ? "#{OPTION_CLASSES} #{OPTION_HIDDEN_CLASSES}" : OPTION_CLASSES
39
+ end
33
40
  end
34
41
  end
35
42
  end
@@ -1,5 +1,7 @@
1
1
  <% if column_picker %>
2
- <%= render column_picker %>
2
+ <div class="<%= Keystone::Ui::DataTableComponent::TOOLBAR_CLASSES %>">
3
+ <%= render column_picker %>
4
+ </div>
3
5
  <% end %>
4
6
  <div class="<%= Keystone::Ui::DataTableComponent::WRAPPER_CLASSES %>">
5
7
  <table class="relative min-w-full divide-y divide-gray-300 dark:divide-white/15">
@@ -7,6 +7,7 @@ module Keystone
7
7
  BODY_CLASSES = "ks-table-body"
8
8
  HEAD_CLASSES = "ks-table-head"
9
9
  WRAPPER_CLASSES = "ks-table overflow-x-auto"
10
+ TOOLBAR_CLASSES = "ks-table-toolbar flex justify-end"
10
11
  HEADER_CLASSES_FIRST = "ks-table-header ks-table-header-first text-left text-sm"
11
12
  HEADER_CLASSES_MIDDLE = "ks-table-header ks-table-header-middle text-left text-sm"
12
13
  HEADER_CLASSES_LAST = "ks-table-header ks-table-header-last text-right text-sm"
@@ -147,10 +148,19 @@ module Keystone
147
148
  return unless saved
148
149
 
149
150
  @hidden_columns = saved[:value].to_h.fetch("hidden_columns", @hidden_columns)
151
+ @all_columns = ordered_columns(saved[:value].to_h["column_order"])
150
152
  @columns = visible_columns(@hidden_columns)
151
153
  @save_url = saved[:save_url]
152
154
  end
153
155
 
156
+ def ordered_columns(column_order)
157
+ return @all_columns unless column_order
158
+
159
+ positions = Array(column_order).map(&:to_sym).each_with_index.to_h
160
+ hideable = @all_columns.select(&:hideable?).sort_by.with_index { |col, index| [ positions.fetch(col.key, positions.size + index) ] }
161
+ @all_columns.map { |col| col.hideable? ? hideable.shift : col }
162
+ end
163
+
154
164
  def visible_columns(hidden_columns)
155
165
  hidden_keys = Array(hidden_columns).map(&:to_sym).to_set
156
166
  @all_columns.reject { |col| col.hideable? && hidden_keys.include?(col.key) }
@@ -3,7 +3,7 @@
3
3
  module Keystone
4
4
  module Ui
5
5
  class DesktopBackLinkComponent < ViewComponent::Base
6
- CLASSES = "ks-mobile-header-back hidden lg:inline-flex items-center"
6
+ CLASSES = "ks-mobile-header-back ks-page-back hidden lg:inline-flex items-center"
7
7
  LABEL = "Back"
8
8
 
9
9
  def initialize(url:)
@@ -2,11 +2,12 @@
2
2
  <% content_for :form_page_title, @title %>
3
3
  <% content_for :form_page_back_url, @back_url %>
4
4
 
5
- <% if @trail %>
6
- <% content_for :keystone_page_header, render(Keystone::Ui::BreadcrumbsComponent.new(trail: @trail, current: @title)) %>
7
- <% else %>
5
+ <% unless top_level? %>
8
6
  <% content_for :keystone_page_header, render(Keystone::Ui::DesktopBackLinkComponent.new(url: @back_url)) %>
9
7
  <% end %>
8
+ <% if @trail&.any? %>
9
+ <% content_for :keystone_page_header, render(Keystone::Ui::BreadcrumbsComponent.new(trail: @trail, current: @title)) %>
10
+ <% end %>
10
11
 
11
12
  <% content_for :keystone_page_header do %>
12
13
  <div class="<%= DESKTOP_WRAPPER_CLASSES %>">
@@ -17,10 +17,14 @@ module Keystone
17
17
  def before_render
18
18
  @trail ||= KeystoneUi.configuration.supplied_trail(helpers)
19
19
  @back_url ||= @trail&.last&.last
20
- raise KeystoneUi::MissingBackLink, @title unless @back_url
20
+ raise KeystoneUi::MissingBackLink, @title unless @back_url || top_level?
21
21
  raise KeystoneUi::IncompleteTrail, @title if @trail&.any? { |label, href| label.blank? || href.blank? }
22
22
  end
23
23
 
24
+ def top_level?
25
+ @trail == []
26
+ end
27
+
24
28
  def subtitle?
25
29
  !@subtitle.nil?
26
30
  end
@@ -1,6 +1,8 @@
1
1
  <div class="<%= WRAPPER_CLASSES %>">
2
- <%= link_to @back_url, class: BACK_LINK_CLASSES, aria: { label: "Back" } do %>
3
- <%= BACK_ICON.html_safe %>
2
+ <% if @back_url %>
3
+ <%= link_to @back_url, class: BACK_LINK_CLASSES, aria: { label: "Back" } do %>
4
+ <%= BACK_ICON.html_safe %>
5
+ <% end %>
4
6
  <% end %>
5
7
  </div>
6
8
  <span class="<%= TITLE_CLASSES %>">
@@ -3,8 +3,9 @@
3
3
  <% content_for :show_page_back_url, @back_url %>
4
4
  <% content_for :show_page_subtitle, @subtitle if @subtitle.present? %>
5
5
 
6
- <% if @trail %>
7
- <% content_for :keystone_page_header, render(Keystone::Ui::BreadcrumbsComponent.new(trail: @trail, current: @title)) %>
8
- <% else %>
6
+ <% unless top_level? %>
9
7
  <% content_for :keystone_page_header, render(Keystone::Ui::DesktopBackLinkComponent.new(url: @back_url)) %>
10
8
  <% end %>
9
+ <% if @trail&.any? %>
10
+ <% content_for :keystone_page_header, render(Keystone::Ui::BreadcrumbsComponent.new(trail: @trail, current: @title)) %>
11
+ <% end %>
@@ -17,10 +17,14 @@ module Keystone
17
17
  def before_render
18
18
  @trail ||= KeystoneUi.configuration.supplied_trail(helpers)
19
19
  @back_url ||= @trail&.last&.last
20
- raise KeystoneUi::MissingBackLink, @title unless @back_url
20
+ raise KeystoneUi::MissingBackLink, @title unless @back_url || top_level?
21
21
  raise KeystoneUi::IncompleteTrail, @title if @trail&.any? { |label, href| label.blank? || href.blank? }
22
22
  end
23
23
 
24
+ def top_level?
25
+ @trail == []
26
+ end
27
+
24
28
  def subtitle?
25
29
  !@subtitle.nil?
26
30
  end
@@ -3,7 +3,7 @@
3
3
  module KeystoneUi
4
4
  class MissingBackLink < StandardError
5
5
  def initialize(title)
6
- super(%(The page "#{title}" has no Back link. Pass back_url: or trail:, or supply a trail for it through config.trail_supplier.))
6
+ super(%(The page "#{title}" has no Back link. Pass back_url: or trail:, or supply a trail for it through config.trail_supplier. A nav tab's own page passes trail: [].))
7
7
  end
8
8
  end
9
9
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KeystoneUi
4
- VERSION = "0.33.1"
4
+ VERSION = "0.35.0"
5
5
  end
@@ -43,8 +43,9 @@ outer element. See Conventions before using it.
43
43
  takes a block. The outer wrapper for a screen. `max_width:` `:sm` `:md` `:lg`
44
44
  `:xl` `:full`; `padding:` `:standard` or `:none`; `top_offset:` `:sm` `:md`
45
45
  `:lg` `:xl` to clear a fixed navbar. When `ui_form_page` or `ui_show_page` was
46
- called earlier on the same screen, `ui_page` renders their back link or
47
- breadcrumbs, and the form page's title, at its top, above the block.
46
+ called earlier on the same screen, `ui_page` renders their "Back" link, their
47
+ breadcrumbs, and the form page's title, in that order, at its top, above the
48
+ block.
48
49
  - `ui_section(title: nil, subtitle: nil, action: nil, menu: [], spacing: :md, class: nil)`
49
50
  — takes a block. A titled block of content with an optional right-aligned
50
51
  link. `action:` is `{ label:, href: }`; `spacing:` `:sm` `:md` `:lg`.
@@ -73,25 +74,29 @@ outer element. See Conventions before using it.
73
74
  the title and back URL so the navbar can render mobile header context, and
74
75
  hands `ui_page` a "Back" link to `back_url` shown from `lg:` up, then the
75
76
  title and subtitle shown from `md:` up. Call it before `ui_page`, outside
76
- `ui_page`'s block, or none of that appears. Passing `trail:` replaces the
77
- "Back" link with breadcrumbs: the trail's links followed by `title` unlinked,
78
- as `ui_breadcrumbs` renders them. With no `trail:`, it uses the trail the app
79
- supplies for the current request, if the app supplies one. With no
80
- `back_url:`, the back URL is the `href` of the trail's last link. A `trail:`
81
- or `back_url:` passed here always wins over the supplied ones. With no
82
- `back_url:` and no trail from either place, or a trail that is empty or whose
83
- last link has no `href`, rendering raises `KeystoneUi::MissingBackLink`, naming
84
- the page's title. A trail with any link whose label or `href` is `nil` or
85
- blank raises `KeystoneUi::IncompleteTrail`, naming the page's title, even when
86
- `back_url:` is passed.
77
+ `ui_page`'s block, or none of that appears. Passing a non-empty `trail:` adds
78
+ breadcrumbs under the "Back" link: the trail's links followed by `title`
79
+ unlinked, as `ui_breadcrumbs` renders them, shown from `lg:` up. With no
80
+ `trail:`, it uses the trail the app supplies for the current request, if the
81
+ app supplies one. With no `back_url:`, the back URL is the `href` of the
82
+ trail's last link. A `trail:` or `back_url:` passed here always wins over the
83
+ supplied ones. `trail: []` marks the page a navigation tab opens directly: it
84
+ shows no "Back" link and no breadcrumbs, publishes no back URL when no
85
+ `back_url:` is passed, so the mobile header shows no back arrow, and raises
86
+ nothing. With no `back_url:` and no trail from either place, or a trail whose
87
+ last link has no `href`, rendering raises `KeystoneUi::MissingBackLink`,
88
+ naming the page's title. A trail with any link whose label or `href` is `nil`
89
+ or blank raises `KeystoneUi::IncompleteTrail`, naming the page's title, even
90
+ when `back_url:` is passed.
87
91
  - `ui_show_page(title:, back_url: nil, subtitle: nil, trail: nil)` — the shell
88
92
  marker for a detail screen. It renders nothing where it is called. It
89
93
  publishes the title, subtitle, and back URL for the navbar, and hands
90
94
  `ui_page` a "Back" link to `back_url` shown from `lg:` up. Call it before
91
95
  `ui_page`, outside `ui_page`'s block. It shows no title, so put
92
96
  `ui_page_header` inside the `ui_page` block for the desktop title. `trail:`
93
- and `back_url:` work as they do on `ui_form_page`, including the supplied
94
- trail, the fallback to the trail's last link, and the
97
+ and `back_url:` work as they do on `ui_form_page`, including the breadcrumbs
98
+ under the "Back" link, the supplied trail, the fallback to the trail's last
99
+ link, `trail: []` for a navigation tab's own page, and the
95
100
  `KeystoneUi::MissingBackLink` and `KeystoneUi::IncompleteTrail` errors, and
96
101
  the breadcrumbs end with `title` unlinked.
97
102
  - `ui_breadcrumbs(trail:, current: nil)` — a line of links shown only from `lg:`
@@ -116,7 +121,8 @@ outer element. See Conventions before using it.
116
121
  `icon:` is a raw SVG string.
117
122
  - `ui_mobile_header(title:, back_url:, subtitle: nil)` — a back chevron plus
118
123
  centered title for mobile; hidden above `lg:`. Place it in the navbar's
119
- `mobile_left` slot.
124
+ `mobile_left` slot. `back_url:` must be passed, and `nil` renders the title
125
+ with no back chevron.
120
126
  - `ui_action_menu` — no keywords, takes a block. An ellipsis (⋯) button that
121
127
  opens a dropdown of actions, shown at every screen size. Fill the block with
122
128
  `ui_action_menu_item` calls.
@@ -227,21 +233,40 @@ outer element. See Conventions before using it.
227
233
  key that lists `"hidden_columns"`, the hideable columns in that list replace
228
234
  the ones passed in `hidden_columns:`, and a saved empty list shows every
229
235
  column. When the saved value is `nil` or lists no `"hidden_columns"`, the
230
- table keeps the columns `hidden_columns:` hides. Whenever the supplier
231
- returns a save address, the table renders a "Columns" menu above itself that
232
- saves to it, including for a person with nothing saved yet. When the
236
+ table keeps the columns `hidden_columns:` hides. When the saved value lists
237
+ `"column_order"`, an array of column keys, the hideable columns render in
238
+ that order, followed by any hideable columns the list leaves out in the order
239
+ they were declared. Columns that are not hideable keep their declared place,
240
+ and the hideable ones fill the remaining places. With no `"column_order"` the
241
+ columns keep their declared order. Whenever the supplier
242
+ returns a save address, the table renders a "Columns" menu in a row above
243
+ itself, aligned right, that saves to it as `ui_column_picker` does, including
244
+ for a person with nothing saved yet. When the
233
245
  supplier returns nothing, when no supplier is set, or when no `key:` is
234
246
  passed, the table renders from `hidden_columns:` with no Columns menu. In
235
247
  every case `hidden_columns:` is the table's default layout.
236
248
  - `Keystone::Ui::Column.new(key, header_text, mobile_hidden: false, sortable: false, hideable: false)`
237
249
  — a column with per-column options, for when a `{ key: "Label" }` hash is not
238
250
  enough. `mobile_hidden:` hides the column below `sm:`; `sortable:` opts it into
239
- sort headers; `hideable:` lets the column picker hide it.
251
+ sort headers; `hideable:` lets the Columns menu hide it and move it, and a
252
+ saved `"column_order"` place it.
240
253
  - `ui_column_picker(columns:, hidden_columns: [], save_url: nil)` — a "Columns"
241
- dropdown of checkboxes for every `hideable` column. Pass it the same columns
242
- and hidden keys as the table. On toggle it sends `PATCH save_url` with JSON
243
- `{ "hidden_columns": ["key", ...] }` and a `X-CSRF-Token` header, then reloads
244
- the page. The app must provide that endpoint and persist the list. A table
254
+ dropdown with one row per `hideable` column, in the order `columns:` lists
255
+ them. Each row has a checkbox and an up and a down button that move the
256
+ column one place; the first row's up button and the last row's down button
257
+ are disabled, and after a move the disabled buttons follow the new first and
258
+ last rows. A hidden column's name renders greyed, and unticking a box greys
259
+ its name at once. Pass it the same columns, in the order the table shows
260
+ them, and the same hidden keys as the table. Ticking, unticking and moving
261
+ change only the open menu and send nothing. When the menu closes, by its
262
+ Columns button or by a click outside it, it sends one `PATCH save_url` with
263
+ JSON `{ "hidden_columns": ["key", ...], "column_order": ["key", ...] }` and a
264
+ `X-CSRF-Token` header, then reloads the page. A menu closed with nothing
265
+ changed sends nothing. `column_order` lists every hideable column's key in
266
+ the menu's order when it closes. With no `save_url:` it sends nothing. The app must provide that endpoint and persist
267
+ both lists. The picker does not reorder the table: beside a table without
268
+ `key:`, the app must pass the table and the picker its columns in the saved
269
+ order itself. A table
245
270
  given `key:` renders its own Columns menu when the supplier gives a save
246
271
  address, so never add `ui_column_picker` beside such a table.
247
272
 
@@ -439,8 +464,11 @@ outer element. See Conventions before using it.
439
464
  detail screen without `ui_page` shows no desktop "Back" link or form title.
440
465
  Those shells supply the desktop "Back" link themselves, so never add a
441
466
  second back link or button to those screens. Whether a screen
442
- shows that "Back" link or breadcrumbs, and which parent screens the trail
443
- names, is the developer's choice, so ask before passing `trail:`. Check
467
+ shows breadcrumbs under its "Back" link, and which parent screens the trail
468
+ names, is the developer's choice, so ask before passing `trail:`. A screen
469
+ a navigation tab opens directly, such as a bottom tab's own page, passes
470
+ `trail: []` and no `back_url:`, so it shows no "Back" link and no back
471
+ arrow; which screens those are is the developer's choice, so ask. Check
444
472
  first whether the app's Keystone UI initializer sets a `trail_supplier`,
445
473
  which supplies a trail for every request: if it
446
474
  does, a screen whose supplied trail is right passes neither `trail:` nor
@@ -518,11 +546,16 @@ outer element. See Conventions before using it.
518
546
  For a table whose hideable columns a user should be able to choose and keep,
519
547
  check whether the app's Keystone UI initializer sets a
520
548
  `preference_supplier`. If it does, pass `key:` and the default
521
- `hidden_columns:`, and add no `ui_column_picker`. Which key names the table
522
- and which columns it hides by default are the developer's choice, so ask
523
- rather than pick. If the app sets no supplier, either use `ui_column_picker`
549
+ `hidden_columns:`, and add no `ui_column_picker`. The order the columns are
550
+ declared in is the default order, and only columns declared
551
+ `hideable: true` can be hidden or moved from the Columns menu. Which key
552
+ names the table, which columns are hideable, and which it hides by default
553
+ are the developer's choice, so ask rather than pick. If the app sets no supplier, either use `ui_column_picker`
524
554
  with an endpoint the app owns, as in step 7, or hand setting up a supplier to
525
- `keystone_ui-install`, and ask the developer which.
555
+ `keystone_ui-install`, and ask the developer which. If a Columns menu shows
556
+ no greyed name for a hidden column, the app's keystone_ui-styles version is
557
+ older than 0.11.0, so stop and hand that part to `keystone_ui-install`, and
558
+ add no classes to fix it.
526
559
 
527
560
  7. Wire up anything that posts back. Several helpers render controls whose
528
561
  endpoints the app must own — the column picker's save URL, the pipeline's box
@@ -26,21 +26,24 @@ component updates every page that uses it. It is mobile-first — components shi
26
26
  separate mobile and desktop treatments (a bottom tab bar and mobile header on
27
27
  small screens, a full navigation bar from the `lg:` breakpoint up), because
28
28
  these apps are often viewed in a native webview. The form and show page shells
29
- follow the same split for going back: the mobile header carries the back link on
29
+ follow the same split for going back: the mobile header carries the back arrow on
30
30
  small screens, and from `lg:` up the shell shows a "Back" link. Given a trail of
31
- earlier pages instead, the shell shows breadcrumbs in place of that link, ending
32
- with the page's own title. A page that passes no trail gets the one the app or
31
+ earlier pages, the shell shows breadcrumbs under that "Back" link, ending with
32
+ the page's own title. A page that passes no trail gets the one the app or
33
33
  another gem supplies for the request, if any, and a page that passes no back
34
34
  link goes back to that trail's last link. A trail or back link the page passes
35
35
  itself always wins. Supplying a trail is set up through the install local.
36
36
 
37
- Every form and show page must end up with a Back link. A page with no back
38
- link, no trail and no supplied trail raises `KeystoneUi::MissingBackLink` when it
39
- renders, and the message names the page's title. A trail with a link missing
40
- its label or its address raises `KeystoneUi::IncompleteTrail`, also naming the
41
- page. That link or those breadcrumbs, and the form page's title, appear at the
42
- top of the page container, inside its width and padding, so a page using either
43
- shell also uses the page container.
37
+ A nav tab's own page has nowhere to go back to, and it says so by passing an
38
+ empty trail. Such a page shows no "Back" link and no breadcrumbs, and its mobile
39
+ header shows no back arrow. Every other form and show page must end up with a
40
+ Back link. A page with no back link, no trail and no supplied trail raises
41
+ `KeystoneUi::MissingBackLink` when it renders, and the message names the page's
42
+ title and says an empty trail marks a nav tab's page. A trail with a link
43
+ missing its label or its address raises `KeystoneUi::IncompleteTrail`, also
44
+ naming the page. The "Back" link, the breadcrumbs and the form page's title
45
+ appear at the top of the page container, inside its width and padding, so a
46
+ page using either shell also uses the page container.
44
47
 
45
48
  ## Interface
46
49
 
@@ -131,9 +134,20 @@ holds the catalog.
131
134
  the table's own call hides are its default layout. A saved layout that lists
132
135
  columns to hide replaces that default, and a saved empty list shows every
133
136
  column. When nothing is saved, or the saved layout lists no columns to hide,
134
- the table keeps its default. When the supplier gives an address to save to,
135
- a "Columns" menu appears above the table and saves the user's choice there,
136
- even before anything has been saved.
137
+ the table keeps its default. A saved layout can also list an order for the
138
+ columns. Only hideable columns move: they fill the places hideable columns
139
+ held in the table's own call, in the saved order, and any the order leaves
140
+ out follow the ones it names. Every other column keeps its place, and a
141
+ layout with no order keeps the order the columns were declared in. When the
142
+ supplier gives an address to save to, a "Columns" menu appears above the
143
+ table, at its right, and saves the user's choice there, even before anything
144
+ has been saved. The menu lists the hideable columns in the order the table
145
+ shows them, each with a box to show or hide it and buttons to move it up or
146
+ down. An unticked column's name is greyed at once. Ticking boxes and moving
147
+ columns sends nothing while the menu is open. Closing the menu, with its
148
+ Columns button or by clicking anywhere outside it, saves the hidden columns
149
+ and the order together once, then reloads the page. Closing it with nothing
150
+ changed saves nothing.
137
151
  A table with no key, or an app with no supplier, renders from its own call
138
152
  alone. Setting up the supplier belongs to the install local.
139
153
  - **Suggestions.** A form field can carry a list of suggested values. The
@@ -26,8 +26,9 @@ built on ViewComponent; hook it in before building any screen with those helpers
26
26
  `look_supplier` (which registered look a page gets),
27
27
  `trail_supplier` (a callable that supplies the breadcrumb trail for form and
28
28
  show pages that pass none, and their Back link when they pass none either),
29
- `preference_supplier` (a callable that supplies the saved column layout of a
30
- data table given a `key:`, and the address its Columns menu saves to),
29
+ `preference_supplier` (a callable that supplies the saved hidden columns and
30
+ column order of a data table given a `key:`, and the address its Columns menu
31
+ saves to),
31
32
  and the `tailwind_imports` and `tailwind_sources` lists (extra CSS files and
32
33
  scan paths added to the Tailwind build).
33
34
  - `keystone_theme_attributes` — a layout helper placed inside the `<html>` tag.
@@ -42,7 +43,7 @@ built on ViewComponent; hook it in before building any screen with those helpers
42
43
  ## How to use it
43
44
 
44
45
  1. Confirm the prerequisites: Ruby >= 3.2 and **tailwindcss-rails v4+** in the
45
- host app. The gem brings ViewComponent and keystone_ui-styles 0.10.0 or later with it.
46
+ host app. The gem brings ViewComponent and keystone_ui-styles 0.11.0 or later with it.
46
47
  Tailwind does not have to be initialized first, because the generator creates
47
48
  the stylesheet if it is missing.
48
49
 
@@ -212,26 +213,31 @@ built on ViewComponent; hook it in before building any screen with those helpers
212
213
  name for the request. A name that is not registered, or `nil`, leaves the
213
214
  page on the default look.
214
215
  - `trail_supplier` — a callable that receives the view and returns the
215
- breadcrumb trail as an array of `[label, href]` pairs, or `nil`. A form or
216
- show page that passes no trail of its own shows the supplied one on `lg:`
217
- screens, ending with the page's title, in place of the desktop "Back" link.
218
- A page that passes no Back link goes back to the trail's last link. A page
219
- that passes its own trail or Back link keeps it. With no supplier, or a
220
- `nil` return, the page shows its Back link. Return `nil`, not an empty
221
- array, for a page with no trail. An empty array replaces the page's Back
222
- link with breadcrumbs holding only its title, and on a page that passes
223
- no Back link it raises `KeystoneUi::MissingBackLink`.
224
- - Every form and show page must end up with a Back link. A page that passes
225
- no Back link and no trail, and gets no trail from the supplier, raises
226
- `KeystoneUi::MissingBackLink` naming the page's title when it renders.
227
- So does a page whose trail's last link has no address.
228
- - Every link the supplier returns needs both a label and an address. A trail
229
- with a blank label or address raises `KeystoneUi::IncompleteTrail` naming
230
- the page's title.
231
- - Ask the developer which code in the app knows each page's trail before
232
- writing the callable. If some form or show pages pass no `back_url:`, ask
233
- where their Back link should come from before enabling the supplier or
234
- leaving it out.
216
+ breadcrumb trail as an array of `[label, href]` pairs, an empty array, or
217
+ `nil`. It is asked only by a form or show page that passes no `trail:`.
218
+ A page that passes its own trail, including an empty one, keeps it.
219
+ - On `lg:` screens a form or show page shows a "Back" link, and under it the
220
+ breadcrumbs ending with the page's title when the trail has any links. A
221
+ page that passes no `back_url:` goes back to the trail's last link. With
222
+ no supplier, or a `nil` return, the page shows only its Back link.
223
+ - An empty trail marks a nav tab's own page, which has nowhere to go back
224
+ to. The page shows no Back link and no breadcrumbs, and raises nothing.
225
+ Return `[]` from the supplier for those
226
+ pages, and `nil` for a page whose Back link comes from its own
227
+ `back_url:`.
228
+ - Every other form and show page must end up with a Back link. A page that
229
+ passes no `back_url:`, and has no trail or a trail whose last link has no
230
+ address, raises `KeystoneUi::MissingBackLink` naming the page's title when
231
+ it renders. The message tells the developer to pass `back_url:` or
232
+ `trail:`, to supply a trail through `trail_supplier`, or to pass
233
+ `trail: []` on a nav tab's own page.
234
+ - Every link in a trail needs both a label and an address. A trail with a
235
+ blank label or address raises `KeystoneUi::IncompleteTrail` naming the
236
+ page's title.
237
+ - Ask the developer which code in the app knows each page's trail, and which
238
+ pages are nav tabs' own pages, before writing the callable. If some form
239
+ or show pages pass no `back_url:`, ask where their Back link should come
240
+ from before enabling the supplier or leaving it out.
235
241
  - `preference_supplier` — a callable that receives the view and a data
236
242
  table's key and returns `{ value:, save_url: }`, or `nil` when the person
237
243
  can neither see nor save a layout for that key. It is asked only for
@@ -245,15 +251,33 @@ built on ViewComponent; hook it in before building any screen with those helpers
245
251
  - When `value` is `nil`, or has no `"hidden_columns"` string key, the table
246
252
  keeps the hidden columns its own call passes. A symbol key is not read, so
247
253
  `{ hidden_columns: [...] }` also keeps them.
254
+ - `value` may also hold a list of hideable column names under the string
255
+ key `"column_order"`, such as
256
+ `{ "hidden_columns" => ["sku"], "column_order" => ["price", "sku"] }`.
257
+ The table renders its hideable columns in that order, in the places
258
+ hideable columns hold in its own call, then any hideable columns the list
259
+ leaves out in the order they were declared. Columns that are not hideable
260
+ keep their place, and names that match no hideable column are ignored.
261
+ With no `"column_order"` string key the columns keep their declared order.
248
262
  - When nothing is saved yet for a person who may save a layout, return
249
263
  `{ value: nil, save_url: }`. The table then shows its own default layout
250
264
  with the Columns menu, so the person can save a first layout.
251
- - When `save_url` is present, the table shows a Columns menu above itself,
252
- with the currently hidden columns unchecked.
253
- Toggling a column sends a `PATCH` to `save_url` with the JSON body
254
- `{"hidden_columns": [...]}` and the page's CSRF token, then reloads the
255
- page. The host must have a route and action at that address that store the
256
- list for that key. With no `save_url`, the saved layout applies and no menu
265
+ - When `save_url` is present, the table shows a Columns button above its
266
+ right edge. The button opens a menu listing the hideable columns in the
267
+ order the table shows them, with the currently hidden columns unchecked,
268
+ and gives each column an up and a down button.
269
+ - Ticking, unticking and moving columns change only the menu while it is
270
+ open. When the menu is closed, by its Columns button or by a click
271
+ outside it, it sends one `PATCH` to `save_url` if anything was changed
272
+ while it was open, then reloads the page. A menu closed with no change
273
+ sends nothing.
274
+ - The `PATCH` carries the page's CSRF token and the JSON body
275
+ `{"hidden_columns": [...], "column_order": [...]}`. `column_order` lists
276
+ every hideable column's name in the menu's order.
277
+ - The host must have a route and action at `save_url` that store both lists
278
+ for that key. The supplier must return them in `value` under the same
279
+ string keys, `"hidden_columns"` and `"column_order"`, or the saved order
280
+ is not applied. With no `save_url`, the saved layout applies and no menu
257
281
  is shown.
258
282
  - A companion preferences gem may set this supplier for the app. Ask the
259
283
  developer whether the app uses one, or which code stores each user's table
@@ -125,6 +125,7 @@ sources:
125
125
  - app/components/keystone/ui/action_menu_component.rb
126
126
  - app/components/keystone/ui/action_menu_item_component.rb
127
127
  - app/components/keystone/ui/mobile_header_component.rb
128
+ - app/components/keystone/ui/mobile_header_component.html.erb
128
129
  - app/components/keystone/ui/modal_component.rb
129
130
  - app/components/keystone/ui/multi_select_component.rb
130
131
  - app/components/keystone/ui/nav_dropdown_component.rb
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keystone_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.33.1
4
+ version: 0.35.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 0.10.0
18
+ version: 0.12.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 0.10.0
25
+ version: 0.12.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: view_component
28
28
  requirement: !ruby/object:Gem::Requirement