recourse 7.10.0 → 7.11.1

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: a5ba4a5b982928c2ee9ea80e400a2d95c9b59102224902787ea3b3d6b9f4c9a8
4
- data.tar.gz: 0d44134228230f5a15f4bc7545df435407518a074ae34299605d90294a93a4bd
3
+ metadata.gz: 5d5b8233bcbb5c24c4df425ab9c5a43f5db42baf5f5801b29ec118ce792871bc
4
+ data.tar.gz: 7c5a0bd68a7d5c22d752b2a8fdb2667d0291b6d6bbfbc376218cb5186fa5a2ed
5
5
  SHA512:
6
- metadata.gz: 7db938770e4ecf06d930a89b965cc9790b2596b6c889be4f0a77cb0fff553739978408c22706da03abb4209964b17f6875b3c6703335e0bc9744d70c213768a1
7
- data.tar.gz: b6a11dad45cae5cac88c42890b8551fd2f8a6c527c554672e7fe07221af251c1dbb1b59b35facc89b3c802ff57e1aec6ae01878d49e6424fe2b8d8b772eb85a2
6
+ metadata.gz: 67e9c27e1c7489a189e9a001b1c3d191f68ef480555a06f701d68d561bbc8c97f66de6eb307939e6c1bb980fc7afd5ec1b9195f48289e28cc3450e0148de8e59
7
+ data.tar.gz: b0415018f1b632c3da2d61e1a77e8b96aafced9fcdbac997a4cb316821f30311805f7ed2e91e6e93d2a270ccbffae119394835f93d072becf9395f18aa159147
data/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## 7.11.1 - 2026-09-26
11
+
12
+ * [FIX] A tooltip in the sidebar's foot closes when its icon is clicked
13
+
14
+ Needs bh 6.10.1, which closes an icon's tooltip on a click on it or on its button. The moon
15
+ and the sun swap places on a click, and the tooltip of the one that went was left open and
16
+ put in the page's corner.
17
+
18
+ * [FIX] The moon and the sun say `Dark` and `Light`
19
+
20
+ The words the phone shows beside them, and no longer `Dark theme` and `Light theme`. The
21
+ `recourse.dark_theme` and `recourse.light_theme` words 7.10.0 added are gone: a host that
22
+ translated them translates `recourse.darken` and `recourse.lighten`, which are the same words.
23
+
24
+ ## 7.11.0 - 2026-09-26
25
+
26
+ * [FEATURE] A host can say which pages are the same for everybody, and they are sent to be kept
27
+
28
+ `Recourse.public_pages = -> { !signed_in? }` runs in the controller for every request. A GET
29
+ it approves is sent `Cache-Control: public` with `max-age`, `s-maxage`, `stale-while-revalidate`
30
+ and `stale-if-error` (`Recourse.public_cache` sets the seconds), and `Vary: Turbo-Frame`. It is
31
+ drawn once for everybody, so none of what the reader's own browser tells the server is in
32
+ it: not their zone, not their page size, not their density (the layout widens the page in
33
+ the browser instead), no form token and no way out. `Recourse.public_version = -> { … }` gives
34
+ such a page an ETag, so a CDN or a browser asks whether it stands and is told `304`. A frame's
35
+ request is never public: the same address answers a page.
36
+
10
37
  ## 7.10.0 - 2026-09-26
11
38
 
12
39
  * [FEATURE] The sidebar's foot says what each icon does
@@ -8,7 +8,12 @@ module Recourse
8
8
  # a stranger can write, and an unchecked one is `?limit=100000` by another route.
9
9
  # Which is also why pagy's `max_limit` stays unset: the query string is still not
10
10
  # asked, and this is the one thing that is.
11
+ #
12
+ # A page drawn once for everybody has the first size, whatever this reader chose: the
13
+ # switch under the table redraws the table alone, which is not that page.
11
14
  def recourse_limit
15
+ return Recourse::LIMITS.first if public_page?
16
+
12
17
  limit = cookies[Recourse::LIMIT_STORAGE].to_i
13
18
 
14
19
  Recourse::LIMITS.include?(limit) ? limit : Recourse::LIMITS.first
@@ -0,0 +1,45 @@
1
+ module Recourse
2
+ # A page the host says is the same for everybody is sent for everybody to keep: a CDN
3
+ # in front of the host serves it for a day, and asks the host only when it must.
4
+ module PublicCaching
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :cache_for_everybody
9
+ helper_method :public_page?
10
+ end
11
+
12
+ private
13
+
14
+ # Whether this page is drawn once for everybody. Only a read: a page that changes
15
+ # something is nobody else's, and a frame's request is answered with the frame alone,
16
+ # which the same address answers with a whole page. Asked once per request, since the
17
+ # host's answer may be a query and the layout, the zone and the paging all ask.
18
+ def public_page?
19
+ return @public_page if defined? @public_page
20
+
21
+ @public_page = Recourse.public_pages.present? && (request.get? || request.head?) &&
22
+ !turbo_frame_request? && instance_exec(&Recourse.public_pages)
23
+ end
24
+
25
+ def cache_for_everybody
26
+ return unless public_page?
27
+
28
+ response.cache_control.merge! public_cache_control
29
+ response.headers['Vary'] = [response.headers['Vary'], 'Turbo-Frame'].compact_blank.join ', '
30
+ version = Recourse.public_version
31
+ fresh_when etag: instance_exec(&version) if version
32
+ end
33
+
34
+ # `s-maxage` is the CDN's alone: the reader's browser keeps the page for `max-age`.
35
+ def public_cache_control
36
+ keep = Recourse.public_cache
37
+
38
+ {
39
+ public: true, max_age: keep[:max_age], extras: ["s-maxage=#{keep[:shared_max_age]}"],
40
+ stale_while_revalidate: keep[:stale_while_revalidate],
41
+ stale_if_error: keep[:stale_if_error],
42
+ }
43
+ end
44
+ end
45
+ end
@@ -20,7 +20,11 @@ module Recourse
20
20
  # Nil where the browser has not said, or said something no zone answers to — and
21
21
  # `Time.zone = nil` falls back to the host's own setting, so an absent cookie and a
22
22
  # forged one both leave the page exactly as it is drawn today.
23
+ # And nil for a page drawn once for everybody, whose times are the host's own: this
24
+ # reader's zone is no business of the next reader's page.
23
25
  def recourse_zone
26
+ return if public_page?
27
+
24
28
  name = cookies[Recourse::ZONE_STORAGE]
25
29
 
26
30
  ActiveSupport::TimeZone[name] if name.present?
@@ -5,8 +5,8 @@ module Recourse
5
5
  class BaseController < ApplicationController
6
6
  include Pagy::Method, Positioned, AttachmentResolution, AttachmentWriting,
7
7
  Landing, Paging, ListResolution, ParameterResolution, ParentNaming,
8
- ParentResolution, ReferenceResolution, ResourceResolution, Retrieving,
9
- Weeks, Zoning
8
+ ParentResolution, PublicCaching, ReferenceResolution, ResourceResolution,
9
+ Retrieving, Weeks, Zoning
10
10
 
11
11
  helper Helpers
12
12
 
@@ -11,7 +11,8 @@
11
11
  never has to be remembered here. -%>
12
12
  <meta name='turbo-refresh-method' content='morph'>
13
13
  <meta name='turbo-refresh-scroll' content='preserve'>
14
- <%= csrf_meta_tags %>
14
+ <%# No token on a page everybody is shown: the next reader would submit this one's. -%>
15
+ <%= csrf_meta_tags unless public_page? %>
15
16
  <%= csp_meta_tag %>
16
17
 
17
18
  <%= yield :head %>
@@ -52,6 +53,9 @@
52
53
  own: every screen the gem draws is one to read against their clock. -%>
53
54
  <body class='<%= class_names 'recourse-shell', 'recourse-expanded': expanded? %>'
54
55
  data-controller='timezone' data-timezone-storage-value='<%= Recourse::ZONE_STORAGE %>'>
56
+ <%# What the reader's browser says, put back before anything is painted, on a page the
57
+ server drew for everybody: the words beside the sidebar's icons. -%>
58
+ <%= render 'recourses/density' if public_page? %>
55
59
  <%# First in the page, since a phone reads it first: the row of icons at the top,
56
60
  above the trail. From 768px the layout's grid seats it beside the content instead,
57
61
  under the navbar, whatever order it was written in. -%>
@@ -0,0 +1,10 @@
1
+ <%# The density a reader chose last time, back before the page is painted, as the server was
2
+ not told: a page drawn once for everybody is compact, and this widens it. Classic and
3
+ inline for the reason the scheme is: a module runs after the first paint. -%>
4
+ <script>
5
+ (function () {
6
+ if (/(?:^|; )<%= Recourse::DENSITY_STORAGE %>=expanded(?:;|$)/.test(document.cookie)) {
7
+ document.body.classList.add('recourse-expanded')
8
+ }
9
+ })()
10
+ </script>
@@ -41,11 +41,11 @@
41
41
  <%= tag.button type: 'button', class: 'nav-link border-0 bg-transparent',
42
42
  data: scheme_data do %>
43
43
  <span class='recourse-scheme-light'>
44
- <%= tag.i class: 'bi bi-moon-fill', data: tooltip_data(t('recourse.dark_theme'), placement: 'right') %>
44
+ <%= tag.i class: 'bi bi-moon-fill', data: tooltip_data(t('recourse.darken'), placement: 'right') %>
45
45
  <span class='recourse-foot-word'><%= t 'recourse.darken' %></span>
46
46
  </span>
47
47
  <span class='recourse-scheme-dark'>
48
- <%= tag.i class: 'bi bi-sun-fill', data: tooltip_data(t('recourse.light_theme'), placement: 'right') %>
48
+ <%= tag.i class: 'bi bi-sun-fill', data: tooltip_data(t('recourse.lighten'), placement: 'right') %>
49
49
  <span class='recourse-foot-word'><%= t 'recourse.lighten' %></span>
50
50
  </span>
51
51
  <% end %>
@@ -60,7 +60,9 @@
60
60
  <span class='recourse-foot-word'><%= t 'recourse.enter' %></span>
61
61
  <% end %>
62
62
  <% end %>
63
- <% if exit? %>
63
+ <%# Nobody in particular is signed in on a page drawn once for everybody, and the button
64
+ carries a token that is theirs alone. -%>
65
+ <% if exit? && !public_page? %>
64
66
  <%= button_to exit_path, method: :delete, class: 'nav-link border-0 bg-transparent',
65
67
  form: { data: { turbo: false } } do %>
66
68
  <%= tag.i class: 'bi bi-box-arrow-right', data: tooltip_data(exit_tooltip, placement: 'right') %>
@@ -47,7 +47,6 @@ en:
47
47
  create: Create
48
48
  created: "%{record} was created."
49
49
  created_error: "%{model} could not be created."
50
- dark_theme: Dark theme
51
50
  darken: Dark
52
51
  delete: Delete %{model}
53
52
  deleted: "%{model} was deleted."
@@ -70,7 +69,6 @@ en:
70
69
  items:
71
70
  one: 1 item
72
71
  other: "%{count} items"
73
- light_theme: Light theme
74
72
  lighten: Light
75
73
  missing_model: You declared `recourses :%{name}` in your routes file, but this app
76
74
  has no %{model} model.
@@ -5,8 +5,9 @@ module Recourse
5
5
  private
6
6
 
7
7
  # Whether the reader asked for the words: the cookie the arrows write, read back so
8
- # the page arrives expanded rather than compact and then widened by a script.
9
- def expanded? = cookies[Recourse::DENSITY_STORAGE] == 'expanded'
8
+ # the page arrives expanded rather than compact and then widened by a script. Not on a
9
+ # page drawn once for everybody, which the layout's own script widens in the browser.
10
+ def expanded? = !public_page? && cookies[Recourse::DENSITY_STORAGE] == 'expanded'
10
11
 
11
12
  # What the arrows need to switch: the class they toggle and the cookie they write.
12
13
  def density_data
@@ -0,0 +1,36 @@
1
+ # Reopened for what a host says about the pages a shared cache may keep for everybody.
2
+ module Recourse
3
+ # How long each keeper may hold a page a host called cacheable: the reader's browser
4
+ # (`max_age`), a CDN in front of the host (`shared_max_age`), and for how long past
5
+ # that either may show an old page while a new one is fetched (`stale_while_revalidate`)
6
+ # or when the host does not answer (`stale_if_error`). In seconds.
7
+ PUBLIC_CACHE = {
8
+ max_age: 300, shared_max_age: 86_400, stale_while_revalidate: 86_400, stale_if_error: 604_800,
9
+ }.freeze
10
+
11
+ class << self
12
+ # A Proc answering, for one request, whether its page is the same for everybody: run
13
+ # in the controller, so `request` and whatever the host knows of who is reading are
14
+ # there to ask. Nil, as it is until a host says, keeps every page private to the
15
+ # reader, as before. A page it approves is sent with `public` cache headers, and is
16
+ # drawn with none of what a reader's own browser tells the server — their zone, their
17
+ # page size, their density, a form's token — since that page will be shown to others.
18
+ attr_accessor :public_pages
19
+
20
+ # A Proc answering what a public page's ETag is made of, in the controller: whatever
21
+ # changes when the page would, and nothing that changes when it would not. A browser or
22
+ # a CDN then asks whether it still stands and is told `304`, which costs a
23
+ # `SELECT` and no render. Nil sends no ETag of the gem's own.
24
+ attr_accessor :public_version
25
+
26
+ # The numbers PUBLIC_CACHE holds, or a host's own where it has said.
27
+ attr_writer :public_cache
28
+
29
+ # @return [Hash{Symbol => Integer}] the seconds each keeper keeps a public page for.
30
+ def public_cache = PUBLIC_CACHE.merge(@public_cache || {})
31
+ end
32
+
33
+ @public_pages = nil
34
+ @public_version = nil
35
+ @public_cache = nil
36
+ end
@@ -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 = '7.10.0'
3
+ VERSION = '7.11.1'
4
4
  end
data/lib/recourse.rb CHANGED
@@ -25,6 +25,7 @@ require_relative 'recourse/maps'
25
25
  require_relative 'recourse/orders'
26
26
  require_relative 'recourse/positions'
27
27
  require_relative 'recourse/positioning'
28
+ require_relative 'recourse/public_caches'
28
29
  require_relative 'recourse/writes'
29
30
  require_relative 'recourse/zones'
30
31
  require_relative 'recourse/positionable'
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: 7.10.0
4
+ version: 7.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - claudiob
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '6.10'
46
+ version: 6.10.1
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '6.10'
53
+ version: 6.10.1
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: pagy
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -144,6 +144,7 @@ files:
144
144
  - app/controllers/concerns/recourse/parent_naming.rb
145
145
  - app/controllers/concerns/recourse/parent_resolution.rb
146
146
  - app/controllers/concerns/recourse/positioned.rb
147
+ - app/controllers/concerns/recourse/public_caching.rb
147
148
  - app/controllers/concerns/recourse/reference_resolution.rb
148
149
  - app/controllers/concerns/recourse/resource_resolution.rb
149
150
  - app/controllers/concerns/recourse/retrieving.rb
@@ -159,6 +160,7 @@ files:
159
160
  - app/views/recourses/_card.html.erb
160
161
  - app/views/recourses/_color.html.erb
161
162
  - app/views/recourses/_combobox.html.erb
163
+ - app/views/recourses/_density.html.erb
162
164
  - app/views/recourses/_fields.html.erb
163
165
  - app/views/recourses/_footer.html.erb
164
166
  - app/views/recourses/_form.html.erb
@@ -254,6 +256,7 @@ files:
254
256
  - lib/recourse/positionable.rb
255
257
  - lib/recourse/positioning.rb
256
258
  - lib/recourse/positions.rb
259
+ - lib/recourse/public_caches.rb
257
260
  - lib/recourse/recoursive.rb
258
261
  - lib/recourse/registry.rb
259
262
  - lib/recourse/routes.rb