recourse 5.12.2 → 6.0.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: 6dde202165a7bfa0c497a0ae06b118b3108536014fe7b439c6ef0f658f2bb295
4
- data.tar.gz: 17d8781fda77d53d5bdf01199146342be707f634b955259a4499969c0341fdd6
3
+ metadata.gz: 635ceebc330e4dabee8a5fc81f7e176039803b5b78373ffe9cb82ded4b4b2730
4
+ data.tar.gz: a2a57c8f1a521bd395a40a527b1ed823151df466867c618c58646aaa4de90de3
5
5
  SHA512:
6
- metadata.gz: adf3b5711fd9327cde802204c2d0a3a69c8943b602e133ba66863106a621719c46dc507706de2bfedc42116e5f97dcf751fa8a333c772c8b71fa747cb4e7d087
7
- data.tar.gz: 41d333d13e8e3d5645b3314c8ef2ab61137fae5cf381e4197c7598893e45c188e624a5eafb3dd1fc813e39adf504448357726bf4c9b1028e663f37f63e2d8e49
6
+ metadata.gz: 9ac64fa98077faa73d5a8f0d65bb041ffe447d7843f85724141c33d6d067570e927eeed245a8d95b2eda578327b0fae176fd281941dbb29638a5c4cbe118a8c5
7
+ data.tar.gz: ebae99455b03cf6861838da467a79f561ed159eb76be1b7112ab350a47c05d0fa9c3178476021106f3158583d7234ae86465cfc3c069d4caf3da2a096593ed96
data/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 6.0.0 - 2026-09-19
11
+
12
+ * [Change] These pages are drawn from bh
13
+
14
+ They were drawn from a package named after one company's design system, pinned by hand in
15
+ `Recourse::BUNDLE` and fetched from a CDN. They are drawn from `bh` now, which is the
16
+ Bootstrap layer under that package and is a dependency of this gem, so the version in the
17
+ lock file is the version on the page and nothing is fetched across a network.
18
+
19
+ `Recourse::BUNDLE` is gone. `Recourse.assets` stays, defaulting to bh's own engine, for a
20
+ host serving those three folders from somewhere else.
21
+
22
+ The host's `recourses/head` partial is drawn last in the head rather than before the
23
+ script, so a stylesheet of the host's outranks the rules above it and a module of its own
24
+ runs after the application this one starts.
25
+
26
+ * [Change] The flash is bh's toasts
27
+
28
+ `recourses/_flash` and `FLASH_THEMES` are gone: the markup was bh's, written twice. What
29
+ is this gem's own stays — the row a write landed on, and the link spliced into the words
30
+ a message says — and rides on `toasts` through its `data:` and its block.
31
+
32
+ * [Feature] Every tab wears its model's icon
33
+
34
+ A tab earned one only where the parent kept a `has_many` of that name, so a nested index
35
+ reached through a join, and every singular resource, read as words alone beside tabs that
36
+ had pictures. The icon is now looked up from the route, which is the same question the
37
+ sidebar and the crumbs already ask.
38
+
39
+
10
40
  ## 5.6.2 - 2026-09-18
11
41
 
12
42
  * [Fix] A kept table notices a count that changed
data/README.md CHANGED
@@ -10,16 +10,16 @@ gem install recourse
10
10
 
11
11
  ```ruby
12
12
  # Gemfile
13
- gem 'recourse', '~> 5.0'
13
+ gem 'recourse', '~> 6.0'
14
14
  ```
15
15
 
16
- `~> 5.0` follows Semantic Versioning: `bundle update` takes every 5.x and never a
16
+ `~> 6.0` follows Semantic Versioning: `bundle update` takes every 6.x and never a
17
17
  breaking change. Rails 8.1 and Ruby 3.2 are the minimum; the pages need Turbo, which
18
18
  `turbo-rails` brings, and nothing else in the host. Everything a page is styled and
19
19
  scripted by — Bootstrap 6, its icons, Turbo, Stimulus and the controllers behind these
20
- screens — is linked from https://design.houseaccount.com, so a host without an asset
21
- pipeline gets the same screens, and a host with a Content Security Policy allows that
22
- origin for `style-src`, `script-src` and `font-src`.
20
+ screens — is `bh`, which this gem depends on and whose engine serves them at `/bh/`, so a
21
+ host without an asset pipeline gets the same screens and nothing is fetched across a
22
+ network.
23
23
 
24
24
  ## Step 1. Edit your routes
25
25
 
@@ -344,20 +344,18 @@ The dummy app under `test/dummy` runs on SQLite; the gem names no adapter.
344
344
 
345
345
  ### Reading an unpublished bundle
346
346
 
347
- The pages link `houseaccount` from a CDN, pinned to `Recourse::BUNDLE`, so a stylesheet or
348
- a Stimulus controller that is not published yet cannot be read on a page. Two lines change
349
- that — one in this Gemfile, kept out of it because CI checks out this repo alone:
347
+ The pages link bh's own files, so a stylesheet or a Stimulus controller that is not
348
+ published yet is read by pointing the Gemfile at a checkout of it beside this one:
350
349
 
351
350
  ```bash
352
- echo "gem 'houseaccount', path: '../design'" >> Gemfile && bundle install
353
- cd test/dummy && bin/rails db:migrate && HOUSE_ASSETS=1 bin/rails server
351
+ echo "gem 'bh', path: '../bh'" >> Gemfile && bundle install
352
+ cd test/dummy && bin/rails db:migrate && bin/rails server
354
353
  ```
355
354
 
356
- `HOUSE_ASSETS` is what loads that gem and sets `Recourse.assets = ''`, so the page asks
357
- this app for `/css`, `/js` and `/theme` and the gem's engine answers them out of its own
358
- `public/`. Behind a variable because what a deployed app links is what the suite asserts,
359
- and because the dummy is also the stand-in for a host with no such gem at all. The dummy's
360
- development database is migrated on its own, which the line above is why.
355
+ A host serving those three folders from somewhere else a CDN, a bundle of its own
356
+ carrying bh's layer says so in `Recourse.assets`, and that is the whole of what it
357
+ writes. The dummy's development database is migrated on its own, which the line above is
358
+ why.
361
359
 
362
360
  ## License
363
361
 
@@ -16,18 +16,10 @@
16
16
 
17
17
  <%= yield :head %>
18
18
 
19
- <%# Everything the page is styled by, from the one place every HouseAccount app draws
20
- on: Bootstrap 6, its icons and the house's own rules for this gem's markup. Served
21
- from the `houseaccount` package on a CDN, pinned to a version — `Recourse.assets`
22
- is where, and `Recourse::BUNDLE` is which — so a change there reaches these pages
23
- when somebody moves that line and not before. Two connections opened before the
24
- parser finds either tag: the stylesheet's plain one and the CORS one a module
25
- script and a font are fetched over. They name the CDN outright, being a hint
26
- about it and nothing more, so a host serving the bundle itself simply never uses
27
- them. -%>
28
- <link rel='preconnect' href='https://cdn.jsdelivr.net'>
29
- <link rel='preconnect' href='https://cdn.jsdelivr.net' crossorigin>
30
- <link rel='stylesheet' href='<%= Recourse.asset 'css/houseaccount.css' %>'>
19
+ <%# Everything the page is styled by: Bootstrap 6 and bh's rules for this gem's
20
+ markup, served by bh's own engine. `Recourse.assets` is where, and a host
21
+ serving those files from somewhere else says so there. -%>
22
+ <link rel='stylesheet' href='<%= Recourse.asset 'css/bh.css' %>'>
31
23
  <%# The one thing these screens set that the house's stylesheet does not: system faces
32
24
  at a compact size, for pages dense with figures. 14px as rem, so a browser's own
33
25
  text-size setting still scales it. After the stylesheet, since both are `:root`. -%>
@@ -43,15 +35,17 @@
43
35
  <%= render 'recourses/scheme' %>
44
36
  <%= primary_color_style %>
45
37
 
46
- <%# The rest of the head is the host's: whatever it puts in a `recourses/head` partial
47
- — the icons its tab wears, say — is drawn here, from the host's own views or from a
48
- gem it loads, and nothing is guessed at where nobody has written one. -%>
49
- <%= render 'recourses/head' if lookup_context.exists? 'recourses/head', [], true %>
50
-
51
38
  <%# Turbo, Bootstrap and a Stimulus application with every controller this markup
52
39
  names, started once under `window.Stimulus`. In the head, so a controller exists
53
40
  before the body's first `data-controller`. -%>
54
- <script type='module' src='<%= Recourse.asset 'js/houseaccount.js' %>'></script>
41
+ <script type='module' src='<%= Recourse.asset 'js/bh.js' %>'></script>
42
+
43
+ <%# The rest of the head is the host's: whatever it puts in a `recourses/head` partial
44
+ — the icons its tab wears, a layer of its own over this one — is drawn here, from
45
+ the host's own views or from a gem it loads, and nothing is guessed at where nobody
46
+ has written one. Last, so a stylesheet of the host's outranks the rules above it and
47
+ a module of its own runs after the application this one started. -%>
48
+ <%= render 'recourses/head' if lookup_context.exists? 'recourses/head', [], true %>
55
49
  </head>
56
50
 
57
51
  <%# The reader's zone is told to the server here rather than from a page of its
@@ -83,6 +77,6 @@
83
77
  <%= yield %>
84
78
  </main>
85
79
 
86
- <%= render 'recourses/flash' %>
80
+ <%= toasts(data: written_data) { |key, message| written_message key, message } %>
87
81
  </body>
88
82
  </html>
@@ -5,7 +5,7 @@
5
5
  select is named with `[]`, so the browser submits every pick the way Ransack reads a
6
6
  list predicate. -%>
7
7
  <%= tag.select name: (multiple ? "#{name}[]" : name), id: id, multiple: multiple, required: required,
8
- class: class_names('form-select', 'form-select-sm': small, 'is-invalid': invalid),
8
+ class: class_names('form-control', 'form-control-sm': small, 'is-invalid': invalid),
9
9
  aria: { label: aria_label, describedby: described },
10
10
  data: { controller: 'combobox', combobox_placeholder_value: placeholder,
11
11
  combobox_more_value: t('recourse.more'), combobox_all_value: all,
@@ -1,28 +1,19 @@
1
- # Reopened for where the house bundle these pages are drawn from is served from.
1
+ # Reopened for where the design layer these pages are drawn from is served from.
2
2
  module Recourse
3
- # The release of `houseaccount` they are drawn from, named once — the layout links its
4
- # stylesheet and its script, and the sidebar's toggle swaps a palette out of the same
5
- # release. A published version is never written again, so the bytes behind these URLs
6
- # cannot move under us, and a change to the bundle reaches these pages when somebody
7
- # moves this line and not before.
8
- BUNDLE = '0.17.0'
9
-
10
3
  class << self
11
- # Where that release is served from, with no trailing slash. jsdelivr's copy of the
12
- # published package by default, which is what every deployed app reads and the one
13
- # place all five of them draw from.
4
+ # Where bh's built files answer from, with no trailing slash. Its own engine by
5
+ # default, which every app holding the gem already serves, so nothing is fetched
6
+ # across a network and no version is written down twice: the gem in the lock file
7
+ # is the one on the page.
14
8
  #
15
- # A host that sets it to `''` serves the bundle itself, out of the `houseaccount`
16
- # gem: its engine answers `/css`, `/js` and `/theme` from its own `public/`. That is
17
- # how the bundle is worked on — a stylesheet or a controller can be tried on a real
18
- # page before the version carrying it is published — and it is the whole of what
19
- # such a host writes.
9
+ # A host serving those files from somewhere else a CDN, a bundle of its own that
10
+ # carries bh's layer points this at it, and that is the whole of what it writes.
20
11
  attr_accessor :assets
21
12
  end
22
13
 
23
- @assets = "https://cdn.jsdelivr.net/npm/houseaccount@#{BUNDLE}/public"
14
+ @assets = Bh::Engine::PREFIX.chomp '/'
24
15
 
25
- # One file of the bundle.
16
+ # One file of the design layer.
26
17
  def self.asset(path) = "#{assets}/#{path}"
27
18
 
28
19
  # And the folder one palette is read from, which three places ask for: the layout
@@ -81,11 +81,11 @@ module Recourse
81
81
  def destroy_button(record)
82
82
  path = url_for controller: "/#{destroy_action_path}", action: :destroy, id: record
83
83
 
84
- button_to icon_tag(ICONS[:destroy], class: 'fg-danger'), path,
85
- method: :delete, class: 'btn btn-sm btn-link btn-icon p-0',
86
- aria: { label: t('recourse.delete', model: resource_name) },
87
- form: { data: { turbo_frame: '_top',
88
- turbo_confirm: destroy_warning(record), } }
84
+ confirm_button_to icon_tag(ICONS[:destroy], class: 'fg-danger'), path,
85
+ confirm: destroy_warning(record), method: :delete,
86
+ class: 'btn btn-sm btn-link btn-icon p-0',
87
+ aria: { label: t('recourse.delete', model: resource_name) },
88
+ form: { data: { turbo_frame: '_top' } }
89
89
  end
90
90
  end
91
91
  end
@@ -43,7 +43,8 @@ module Recourse
43
43
  word = Recourse.model_title association.klass, count: value, lower: true
44
44
 
45
45
  safe_join [
46
- tag.span(count, class: 'recourse-counter-figure', data: tooltip_on_top(title)),
46
+ tag.span(count, class: 'recourse-counter-figure',
47
+ data: tooltip_data(title, placement: 'top')),
47
48
  tag.span("#{count} #{word}", class: 'recourse-counter-word'),
48
49
  ]
49
50
  end
@@ -18,12 +18,10 @@ module Recourse
18
18
  path = destroy_resource_path record
19
19
  return unless path
20
20
 
21
- options = {
22
- method: :delete, class: 'btn btn-sm btn-solid theme-danger ms-3',
23
- form_class: 'd-inline-block', data: { turbo_confirm: destroy_warning(record) },
24
- }
25
-
26
- button_to t('recourse.delete', model: resource_name), path, **options
21
+ confirm_button_to t('recourse.delete', model: resource_name), path,
22
+ confirm: destroy_warning(record), method: :delete,
23
+ class: 'btn btn-sm btn-solid theme-danger ms-3',
24
+ form_class: 'd-inline-block'
27
25
  end
28
26
 
29
27
  # What deleting this record takes with it, counted a level down and no further:
@@ -1,24 +1,16 @@
1
1
  module Recourse
2
2
  module Helpers
3
- # What a flash message is drawn as: its toast's theme, the row it marks, and the
4
- # words it leads somewhere.
3
+ # What a flash message is drawn as: the row it marks, and the words it leads somewhere.
5
4
  module Flashes
6
- # Bootstrap theme for each flash key, so a notice and an alert read apart.
7
- FLASH_THEMES = { 'notice' => 'theme-success', 'alert' => 'theme-danger' }
8
-
9
5
  private
10
6
 
11
- # Theme for one flash entry, falling back to a neutral one for a host's key.
12
- def flash_theme(key)
13
- FLASH_THEMES.fetch key.to_s, 'theme-primary'
14
- end
15
-
16
7
  # What marks the row a write just landed on, and nothing at all on a page no write
17
8
  # brought about — which is every page but the one after a create or an update.
18
9
  def written_data
19
10
  written = flash[Recourse::WRITTEN]
11
+ return {} if written.blank?
20
12
 
21
- { controller: 'written', written_row_value: written['row'] } if written.present?
13
+ { controller: 'written', written_row_value: written['row'] }
22
14
  end
23
15
 
24
16
  # The message with the record it names led to that record's page, where a write
@@ -20,13 +20,7 @@ module Recourse
20
20
  # an icon alone says nothing to one, and given the tooltip that says the same
21
21
  # word to everyone else.
22
22
  def icon_heading(concept, title, **)
23
- icon_tag concept, label: title, role: :img, data: tooltip_on_top(title), **
24
- end
25
-
26
- def tooltip_on_top(title)
27
- # `bs_title` is what Bootstrap's tooltip reads, and the controller is what
28
- # makes one: Bootstrap never wires a tooltip on its own.
29
- { controller: 'tooltip', bs_placement: 'top', bs_title: title }
23
+ icon_tag concept, label: title, role: :img, data: tooltip_data(title, placement: 'top'), **
30
24
  end
31
25
  end
32
26
  end
@@ -14,7 +14,7 @@ module Recourse
14
14
  association = nested_association record, name if action == :index
15
15
  return association_tab_label record, association, namespace if association
16
16
 
17
- routed_tab_name name, namespace, action
17
+ tab_label_for Recourse.known_icon(name), routed_tab_name(name, namespace, action)
18
18
  end
19
19
 
20
20
  def nested_association(record, name)
@@ -36,15 +36,19 @@ module Recourse
36
36
  [lead.presence&.upcase_first, title].compact.join ' '
37
37
  end
38
38
 
39
- # The icon is the counted model's own, whatever leads the words beside it. The
40
- # words are wrapped where a picture or a figure stands beside them, so a phone,
41
- # which has room for neither, keeps the picture and the figure and drops the words.
42
- # A figure and its words share one element: the link lays its children out with a
43
- # gap, and two of them would stand a gap and a space apart.
39
+ # The icon is the counted model's own, whatever leads the words beside it.
44
40
  def association_tab_label(record, association, namespace)
45
41
  icon = Recourse.known_model_icon association.klass
46
42
  count = tab_count record, association
47
- words = tab_words association, namespace, count
43
+
44
+ tab_label_for icon, tab_words(association, namespace, count), count
45
+ end
46
+
47
+ # The words with whatever stands beside them. The words are wrapped where a picture
48
+ # or a figure does, so a phone, which has room for neither, keeps the picture and the
49
+ # figure and drops the words. A figure and its words share one element: the link lays
50
+ # its children out with a gap, and two of them would stand a gap and a space apart.
51
+ def tab_label_for(icon, words, count = nil)
48
52
  words = tag.span words, class: 'recourse-tab-word' if icon || count
49
53
  words = tag.span safe_join([count, words], ' ') if count
50
54
 
@@ -1,4 +1,4 @@
1
1
  module Recourse
2
2
  # Version of the gem, read by the gemspec and by hosts checking compatibility.
3
- VERSION = '5.12.2'
3
+ VERSION = '6.0.0'
4
4
  end
data/lib/recourse.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'bh'
1
2
  require 'pagy'
2
3
  # Before searchable.rb, so its `extend` lands ahead of Ransack's own defaults.
3
4
  require 'ransack'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recourse
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.12.2
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - claudiob
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '8.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: bh
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '6.7'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '6.7'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: pagy
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -144,7 +158,6 @@ files:
144
158
  - app/views/recourses/_color.html.erb
145
159
  - app/views/recourses/_combobox.html.erb
146
160
  - app/views/recourses/_fields.html.erb
147
- - app/views/recourses/_flash.html.erb
148
161
  - app/views/recourses/_footer.html.erb
149
162
  - app/views/recourses/_form.html.erb
150
163
  - app/views/recourses/_map.html.erb
@@ -1,27 +0,0 @@
1
- <% if flash.any? -%>
2
- <%# `data-turbo-temporary`: a toast born visible would otherwise replay from the
3
- page snapshot on every Back, announcing a save that happened a page ago. -%>
4
- <%# The row a write landed on rides here rather than on a toast: this exists once
5
- per page, `hidden.bs.toast` bubbles up to it from whichever toast hides, and
6
- `data-turbo-temporary` already takes the whole thing away on a Back. The same
7
- key carries the record's words and its page, which the notice below links. -%>
8
- <div class='toast-container position-fixed bottom-0 end-0 p-3' data-turbo-temporary
9
- <%= tag.attributes data: written_data %>>
10
- <%# Every key here becomes a toast, whoever invented it — so the one carrying the
11
- written row, which is data rather than a message, is taken out first. -%>
12
- <% flash.to_hash.except(Recourse::WRITTEN).each do |key, message| -%>
13
- <%# `fade show` from the server: visible at first paint, before any JavaScript.
14
- The `toast` controller owns the timed hide, and hovering or focusing holds
15
- the toast open while someone reads it or aims for the X. -%>
16
- <div class='toast fade show <%= flash_theme key %>' role='alert' aria-live='assertive' aria-atomic='true'
17
- data-controller='toast'
18
- data-action='mouseenter->toast#stopTimer mouseleave->toast#startTimer focusin->toast#stopTimer focusout->toast#startTimer'>
19
- <div class='toast-header border-0'>
20
- <span class='me-auto'><%= written_message key, message %></span>
21
- <button type='button' class='btn-close' data-bs-dismiss='toast' aria-label='Close'></button>
22
- </div>
23
- <div class='toast-body d-none'></div>
24
- </div>
25
- <% end -%>
26
- </div>
27
- <% end -%>