studio-engine 0.86.0 → 0.87.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 +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +1 -1
- data/app/helpers/studio/site_footer_helper.rb +9 -6
- data/app/views/style/_site_footer.html.erb +4 -2
- data/docs/SITE_FOOTER.md +91 -12
- data/lib/studio/site_footer.rb +71 -8
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +56 -10
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3bc2b4fbaacbbcb3895cdcc95f4caa6421df1cc1cf16c9e53d8bdd5aa19f3a7
|
|
4
|
+
data.tar.gz: b453a06ef41d2bd56b23a56e2f80bc29f19bd793702dbf23e6b1f7236f0cfb37
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f88b17d347d21f137b61a91d0e1d0e70c67e3b464e36f1f0de7bb198798c914fd9a09af6a567639cae488ae67aabebdbac811c6a2a55f1ba02f9bdada74bb8bc
|
|
7
|
+
data.tar.gz: 975c4a433ea0dac45d70a683afc799984ee027c27a33e6cf823467cc545f01dda4575ef6623efd830b4bdf486cb4a29b53cee7e0886b441681a30af3b7f4ff38
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.87.0 — 2026-10-04
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **The site footer is on by default. An unset `config.site_footer` now renders
|
|
12
|
+
a footer wherever `studio_site_footer` is called**; before, it rendered
|
|
13
|
+
nothing. The default footer is built from what the engine already knows: the
|
|
14
|
+
app's name (the site identity's title, else `config.app_name`), its footer or
|
|
15
|
+
navbar logo, the © line, and a Privacy Policy and a Terms of Service link for
|
|
16
|
+
each of the routes named `privacy` and `terms` that the host has (a GET route
|
|
17
|
+
that needs no arguments; anything else is not linked). It has no address, no
|
|
18
|
+
map, no phone, no email, no tagline, no social row and no link columns, and
|
|
19
|
+
names no Leaflet. The rule for where it shows is unchanged. **An app that
|
|
20
|
+
sets `config.site_footer` to a Hash or a callable renders exactly what it did**,
|
|
21
|
+
and an app that does not call the helper is unaffected. A callable that
|
|
22
|
+
answers `nil` still declines the footer for that request.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`config.site_footer = false` turns the footer off.** It is the one opt-out,
|
|
27
|
+
and it is distinct from unset (`nil`), which is the default footer.
|
|
28
|
+
`Studio.site_footer_enabled?` and `Studio.site_footer_declared?` read the
|
|
29
|
+
three states.
|
|
30
|
+
- **`config.site_footer_address`: the location, as one setting.** A Hash of the
|
|
31
|
+
facts' `address:` keys (`street:`, `city_line:`, `lat:`, `lng:`, and the
|
|
32
|
+
optional `zoom:` and `directions_url:`) gives the footer its Location band,
|
|
33
|
+
and its map when both coordinates are present, without writing the facts
|
|
34
|
+
callable. It applies to the default footer and to an app's own facts that
|
|
35
|
+
carry no address. An address in the app's own facts wins, whole, and
|
|
36
|
+
`address: false` there shows none.
|
|
37
|
+
- **docs/NEW_APP_SETUP.md has a Site Footer step (§ 10)**, and its layout ends
|
|
38
|
+
with `<%= studio_site_footer %>`. The sections after it moved down by one.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- `address: false` in the footer's facts shows no address instead of raising.
|
|
43
|
+
|
|
7
44
|
## 0.86.0 — 2026-10-03
|
|
8
45
|
|
|
9
46
|
### Added
|
data/README.md
CHANGED
|
@@ -41,7 +41,7 @@ resolved.
|
|
|
41
41
|
- **Site identity and link previews**: `Studio::SiteIdentity` holds the app's title, description and image, edited at `/admin/link_preview` beside a live unfurl card and read anywhere through `Studio.site_identity`. Every page unfurls with it unless it calls `link_preview image:, title:, description:`, and `Studio::LinkPreviewBots` serves preview fetchers a slim page under iMessage's 1 MiB limit. Adopt with `bin/rails g studio:site_identity`. See [`docs/LINK_PREVIEW.md`](docs/LINK_PREVIEW.md).
|
|
42
42
|
- **Public user page**: `/u/:username` shows a user's avatar and username (nothing else), and unfurls with their avatar, falling back to the site image. Link any username to it with `link_to_user_profile(user)` or `studio_user_profile_path(user)`. Opt in with `config.draw_public_user_routes = true`. See [`docs/PUBLIC_USER_PAGE.md`](docs/PUBLIC_USER_PAGE.md).
|
|
43
43
|
- **Transactional emails**: `Studio::EmailCatalog` — every email an app sends, its type, a live preview, and its banner — plus the shared `/admin/emails` page. Every app inherits the standard emails and their artwork on day one, and can register its own workflows and upload its own banners. See [Transactional emails](#transactional-emails).
|
|
44
|
-
- **Site footer**: `<%= studio_site_footer %>` in the layout renders the public site's footer
|
|
44
|
+
- **Site footer**: `<%= studio_site_footer %>` in the layout renders the public site's footer. It is on by default: with nothing configured it prints the app's name, logo and the legal links the app has routes for, and no address. `config.site_footer` declares the app's own facts (brand, social profiles, link columns, an address with a live map, legal links), every part optional; `config.site_footer_address` adds a location in one setting; `config.site_footer = false` turns the footer off. Leaflet is vendored and served by the engine. See [`docs/SITE_FOOTER.md`](docs/SITE_FOOTER.md).
|
|
45
45
|
- **Booking**: a simple scheduler on Google Calendar appointment schedules, with or without the footer. With `config.booking_url`, `studio_booking_frame` embeds Google's booking page, `studio_booking_link` opens it in a popup, and `config.draw_booking_routes = true` draws `/schedule` (or name the app's own page with `config.booking_path`). `config.booking_crop` crops the frame to the slot picker at rest, from numbers `bin/booking-crop-measure` reads off the app's own schedule. See [`docs/BOOKING.md`](docs/BOOKING.md).
|
|
46
46
|
|
|
47
47
|
## Configuration
|
|
@@ -10,15 +10,18 @@ module Studio
|
|
|
10
10
|
# <%= studio_booking_popup %> the dialog those links open
|
|
11
11
|
# <%= studio_footer_map %> the map alone, e.g. on a contact page
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
# (
|
|
15
|
-
#
|
|
13
|
+
# THE FOOTER IS ON BY DEFAULT: with Studio.site_footer unset, the layout line
|
|
14
|
+
# renders the default footer (the site name, the logo, and the legal links the
|
|
15
|
+
# host has routes for), and `config.site_footer = false` turns it off. The
|
|
16
|
+
# booking helpers render NOTHING without Studio.booking_url, and the map
|
|
17
|
+
# nothing without an address, so a layout or a shared view can call any of
|
|
18
|
+
# them before an app has declared anything.
|
|
16
19
|
#
|
|
17
20
|
# Prefixed names for the reason Studio::GeoHelper gives: every helper module is
|
|
18
21
|
# included into every view.
|
|
19
22
|
module SiteFooterHelper
|
|
20
|
-
# The resolved facts (Studio::SiteFooter.resolve)
|
|
21
|
-
# request.
|
|
23
|
+
# The resolved facts (Studio::SiteFooter.resolve): the app's own, else the
|
|
24
|
+
# default footer's. nil when the footer is off. Resolved once per request.
|
|
22
25
|
#
|
|
23
26
|
# A callable that RAISES must not take every page down with it, because the
|
|
24
27
|
# footer is in the layout. In production the error is logged once per process
|
|
@@ -41,7 +44,7 @@ module Studio
|
|
|
41
44
|
# Where the footer shows: Studio.site_footer_visible, asked of this view. By
|
|
42
45
|
# default that is every page for a visitor and, for a signed-in viewer, only
|
|
43
46
|
# the controllers in Studio.site_footer_controllers (plus the booking page).
|
|
44
|
-
# Always false for an app that
|
|
47
|
+
# Always false for an app that turned the footer off.
|
|
45
48
|
def studio_show_site_footer?
|
|
46
49
|
return false if studio_site_footer_facts.nil?
|
|
47
50
|
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
whose box runs from 200 to 600), cropped by the same arithmetic the real
|
|
13
13
|
wrapper uses (Studio::Booking.crop). No real schedule's numbers are here. %>
|
|
14
14
|
<%
|
|
15
|
-
|
|
15
|
+
# The app's OWN facts only. An app on the default footer (nothing declared)
|
|
16
|
+
# sees the sample, which shows every kind of row the default does not have.
|
|
17
|
+
own_facts = Studio.site_footer_declared? ? studio_site_footer_facts : nil
|
|
16
18
|
sample = {
|
|
17
19
|
name: Studio.site_identity[:title], tagline: "What this app is, in a line",
|
|
18
20
|
email: "team@example.com",
|
|
@@ -42,7 +44,7 @@
|
|
|
42
44
|
</p>
|
|
43
45
|
<p class="text-muted text-sm" data-site-footer-status>
|
|
44
46
|
This app:
|
|
45
|
-
footer <strong class="text-body"><%= own_facts ? "declared" : "not declared" %></strong> ·
|
|
47
|
+
footer <strong class="text-body"><%= own_facts ? "declared" : (Studio.site_footer_enabled? ? "default (not declared)" : "off") %></strong> ·
|
|
46
48
|
booking <strong class="text-body"><%= booking ? "on" : "off" %></strong> ·
|
|
47
49
|
booking page <strong class="text-body"><%= Studio.draw_booking_routes ? "drawn" : "not drawn" %></strong> ·
|
|
48
50
|
frame <strong class="text-body"><%= own_crop ? "cropped at rest" : "whole at rest" %></strong>
|
data/docs/SITE_FOOTER.md
CHANGED
|
@@ -14,6 +14,79 @@ studio-engine | grep docs/` finds them. The repository copy is
|
|
|
14
14
|
|
|
15
15
|
## Adopt it
|
|
16
16
|
|
|
17
|
+
The app owns its layout, so the footer is one line the app writes. The engine
|
|
18
|
+
never injects it.
|
|
19
|
+
|
|
20
|
+
```erb
|
|
21
|
+
<%# app/views/layouts/application.html.erb, at the end of <body> %>
|
|
22
|
+
<%= studio_site_footer %>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**The footer is on by default.** That line, with nothing configured, renders
|
|
26
|
+
the default footer. `config.site_footer` has three states:
|
|
27
|
+
|
|
28
|
+
| `config.site_footer` | What `studio_site_footer` renders |
|
|
29
|
+
|----------------------|-----------------------------------|
|
|
30
|
+
| unset (`nil`) | The default footer |
|
|
31
|
+
| a Hash, or a callable receiving the view | The app's own facts ([Your own facts](#your-own-facts)) |
|
|
32
|
+
| `false` | Nothing: the opt-out |
|
|
33
|
+
|
|
34
|
+
### The default footer
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
# config/initializers/studio.rb
|
|
38
|
+
Studio.configure do |config|
|
|
39
|
+
config.app_name = "Example Co" # nothing about the footer: it is already on
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
It prints only what the engine knows to be true of the app:
|
|
44
|
+
|
|
45
|
+
- **The name**, as the wordmark and the © line: the site identity's title
|
|
46
|
+
(`Studio.site_identity[:title]`), which is `config.app_name` until the
|
|
47
|
+
operator or `config.site_title` says otherwise.
|
|
48
|
+
- **The logo**: the app's "Footer Logo" in `config.theme_logos`, else its
|
|
49
|
+
navbar logo. None configured, none shown.
|
|
50
|
+
- **A legal line**, for the routes the host has: "Privacy Policy" when it has a
|
|
51
|
+
route named `privacy`, "Terms of Service" when it has one named `terms`
|
|
52
|
+
(`get "privacy", to: "legal#privacy", as: :privacy`). A route is linked only
|
|
53
|
+
when a GET reaches it and its path needs no arguments. A host with neither
|
|
54
|
+
gets the © line alone.
|
|
55
|
+
|
|
56
|
+
It has no address, no map, no phone, no email, no tagline, no social row and no
|
|
57
|
+
link columns, and the page names no Leaflet. The engine cannot know those, and
|
|
58
|
+
a guessed link is a dead one. For any of them, add a location (below) or
|
|
59
|
+
declare [your own facts](#your-own-facts).
|
|
60
|
+
|
|
61
|
+
### A location, in one setting
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
Studio.configure do |config|
|
|
65
|
+
config.site_footer_address = { street: "123 Example St", city_line: "Washington, DC 20024",
|
|
66
|
+
lat: 38.8894, lng: -77.0352 }
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That adds the Location band, and the map when `lat:` and `lng:` are both there,
|
|
71
|
+
to the default footer. The keys are the facts' `address:` keys
|
|
72
|
+
([The address and the map](#the-address-and-the-map)). It also gives a location
|
|
73
|
+
to an app's own facts that carry none. **An address in the app's own facts
|
|
74
|
+
wins**, whole, and `address: false` there shows none.
|
|
75
|
+
|
|
76
|
+
### Turning it off
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
Studio.configure do |config|
|
|
80
|
+
config.site_footer = false # no footer on any page; unset (nil) is the default footer
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
To keep the footer off some pages rather than all of them, narrow
|
|
85
|
+
[where it shows](#where-it-shows) instead. A facts callable that answers `nil`
|
|
86
|
+
for a request renders no footer for that request.
|
|
87
|
+
|
|
88
|
+
### Your own facts
|
|
89
|
+
|
|
17
90
|
```ruby
|
|
18
91
|
# config/initializers/studio.rb
|
|
19
92
|
Studio.configure do |config|
|
|
@@ -39,11 +112,6 @@ Studio.configure do |config|
|
|
|
39
112
|
end
|
|
40
113
|
```
|
|
41
114
|
|
|
42
|
-
```erb
|
|
43
|
-
<%# app/views/layouts/application.html.erb, at the end of <body> %>
|
|
44
|
-
<%= studio_site_footer %>
|
|
45
|
-
```
|
|
46
|
-
|
|
47
115
|
That is the whole adoption. Nothing is copied into the app's `public/`, and
|
|
48
116
|
nothing is added to its asset manifest or its Tailwind build.
|
|
49
117
|
|
|
@@ -78,6 +146,10 @@ end
|
|
|
78
146
|
<%= studio_site_footer %>
|
|
79
147
|
```
|
|
80
148
|
|
|
149
|
+
Leave `config.site_footer` unset and the same app gets
|
|
150
|
+
[the default footer](#the-default-footer), its name read from `config.app_name`
|
|
151
|
+
since there is no site-identity table to ask.
|
|
152
|
+
|
|
81
153
|
Do not call `Studio.routes(self)`: everything it draws (sign-in, admin, error
|
|
82
154
|
logs) needs a database. The app also needs an asset pipeline (Propshaft or
|
|
83
155
|
Sprockets), as every Rails 8 app has, and a mapping of the theme tokens onto its
|
|
@@ -107,8 +179,9 @@ development and with `eager_load = true`.
|
|
|
107
179
|
## The facts
|
|
108
180
|
|
|
109
181
|
`config.site_footer` is a Hash, or a callable that receives the view (so it can
|
|
110
|
-
use route helpers). `nil
|
|
111
|
-
`studio_site_footer` renders nothing
|
|
182
|
+
use route helpers). Unset (`nil`), the facts are the default footer's; `false`
|
|
183
|
+
means the app has no footer and `studio_site_footer` renders nothing
|
|
184
|
+
([Adopt it](#adopt-it)).
|
|
112
185
|
|
|
113
186
|
Every key is optional. A missing key removes its part of the footer.
|
|
114
187
|
|
|
@@ -121,7 +194,7 @@ Every key is optional. A missing key removes its part of the footer.
|
|
|
121
194
|
| `home_path` | Where the logo and wordmark link | `/` |
|
|
122
195
|
| `tagline` | A line under the wordmark | No line |
|
|
123
196
|
| `email` | A `mailto:` link under the tagline | No line. (To list the email in a column instead, write it as a column link.) |
|
|
124
|
-
| `address` | The Location band, and the map |
|
|
197
|
+
| `address` | The Location band, and the map | `config.site_footer_address`; with neither, no Location band, no map, and Leaflet is never requested. `address: false` shows none even with the setting |
|
|
125
198
|
| `social` | A row of round icons | No row |
|
|
126
199
|
| `columns` | Link columns under bold headings. A column may carry a width hint: `[ "Contact", links, { width: 1.5 } ]` | Brand only |
|
|
127
200
|
| `legal` | The centred line above the © | The © alone |
|
|
@@ -356,14 +429,19 @@ link. Those rules are more specific than the engine's and still apply.
|
|
|
356
429
|
|
|
357
430
|
## Tests
|
|
358
431
|
|
|
359
|
-
- `test/lib/studio/site_footer_test.rb`: the facts' rules
|
|
432
|
+
- `test/lib/studio/site_footer_test.rb`: the facts' rules, the default facts, the
|
|
433
|
+
address setting's precedence, and the visibility rule.
|
|
360
434
|
- `test/lib/vendored_leaflet_test.rb`: Leaflet is vendored, precompiled, and
|
|
361
435
|
addressed through the asset pipeline.
|
|
362
436
|
- `test/integration/site_footer_test.rb`: what a host renders, with and without
|
|
363
|
-
each fact, signed in and out
|
|
437
|
+
each fact, signed in and out; the default footer with nothing set, the
|
|
438
|
+
`false` opt-out, `config.site_footer_address` on the default footer and
|
|
439
|
+
against an app's own address, and the default legal links with and without
|
|
440
|
+
the host's routes.
|
|
364
441
|
- `test/integration/footer_only_consumer_test.rb`: an app with no ActiveRecord
|
|
365
442
|
boots (development and eager-loaded), renders the footer with no Leaflet, and
|
|
366
|
-
runs `studio_engine:install:migrations` to a clean exit
|
|
443
|
+
runs `studio_engine:install:migrations` to a clean exit; with nothing set it
|
|
444
|
+
renders the default footer.
|
|
367
445
|
- `test/lib/site_footer_link_contrast_test.rb`: the link ink clears 4.5:1 on the
|
|
368
446
|
band, on the default tokens, in both themes.
|
|
369
447
|
- `e2e/site_footer.spec.js`: the map's script and the footer's layout, in a
|
|
@@ -372,4 +450,5 @@ link. Those rules are more specific than the engine's and still apply.
|
|
|
372
450
|
line inside its column at each, and the line heights. These lab pages load Turbo, so the map's remount after a Turbo visit is
|
|
373
451
|
covered.
|
|
374
452
|
|
|
375
|
-
`/admin/style` shows the footer under Tricks, with this app's facts or a
|
|
453
|
+
`/admin/style` shows the footer under Tricks, with this app's own facts, or a
|
|
454
|
+
sample when it has declared none (the default footer or `false`).
|
data/lib/studio/site_footer.rb
CHANGED
|
@@ -7,7 +7,8 @@ require "cgi"
|
|
|
7
7
|
# booting the dummy app. See docs/SITE_FOOTER.md.
|
|
8
8
|
#
|
|
9
9
|
# The declared value is a Hash, or a callable that receives the view (for route
|
|
10
|
-
# helpers).
|
|
10
|
+
# helpers). Unset (nil) is the DEFAULT footer (`default_facts`), and `false` is
|
|
11
|
+
# no footer. EVERY KEY IS OPTIONAL, and a missing key removes its part of the
|
|
11
12
|
# footer rather than printing a placeholder:
|
|
12
13
|
#
|
|
13
14
|
# config.site_footer = ->(view) {
|
|
@@ -105,21 +106,73 @@ module Studio
|
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
def validate!(declared)
|
|
108
|
-
return if declared.nil? || declared.is_a?(Hash) || declared.respond_to?(:call)
|
|
109
|
+
return if declared.nil? || declared == false || declared.is_a?(Hash) || declared.respond_to?(:call)
|
|
109
110
|
|
|
110
111
|
raise ArgumentError,
|
|
111
|
-
"Studio.site_footer must be nil, a Hash, or a
|
|
112
|
+
"Studio.site_footer must be nil (the default footer), false (no footer), a Hash, or a " \
|
|
113
|
+
"callable receiving the view (got #{declared.class}). See docs/SITE_FOOTER.md."
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_address!(declared)
|
|
117
|
+
return if declared.nil? || declared == false || declared.is_a?(Hash)
|
|
118
|
+
|
|
119
|
+
raise ArgumentError,
|
|
120
|
+
"Studio.site_footer_address must be nil or a Hash of street:, city_line:, lat:, lng: " \
|
|
112
121
|
"(got #{declared.class}). See docs/SITE_FOOTER.md."
|
|
113
122
|
end
|
|
114
123
|
|
|
115
|
-
#
|
|
124
|
+
# THE DEFAULT FOOTER'S FACTS: what an app that sets nothing gets. Only what
|
|
125
|
+
# is known to be true of the host: a legal line linking the routes in
|
|
126
|
+
# DEFAULT_LEGAL that the host really has. The name and the logo come from
|
|
127
|
+
# `resolve`'s own fallbacks. No address, no email, no tagline, no social row
|
|
128
|
+
# and no link columns: the engine cannot know them, and a guessed link is a
|
|
129
|
+
# dead one.
|
|
130
|
+
#
|
|
131
|
+
# routes: the host's route set (Rails.application.routes), or nil to ask
|
|
132
|
+
# the view alone.
|
|
133
|
+
DEFAULT_LEGAL = [["Privacy Policy", :privacy], ["Terms of Service", :terms]].freeze
|
|
134
|
+
|
|
135
|
+
def default_facts(view, routes: nil)
|
|
136
|
+
legal = DEFAULT_LEGAL.filter_map do |label, route_name|
|
|
137
|
+
path = host_path(view, route_name, routes)
|
|
138
|
+
[label, path] unless path.nil?
|
|
139
|
+
end
|
|
140
|
+
{ legal: legal }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The path of the host's route `name`, or nil unless the view can build it
|
|
144
|
+
# with no arguments and (when the route set is given) a GET reaches it.
|
|
145
|
+
def host_path(view, name, routes = nil)
|
|
146
|
+
helper = :"#{name}_path"
|
|
147
|
+
return nil unless view.respond_to?(helper)
|
|
148
|
+
return nil unless routes.nil? || get_route?(routes, name)
|
|
149
|
+
|
|
150
|
+
text(view.public_send(helper))
|
|
151
|
+
rescue StandardError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def get_route?(routes, name)
|
|
156
|
+
route = routes.named_routes[name]
|
|
157
|
+
return false if route.nil? || route.required_parts.any?
|
|
158
|
+
|
|
159
|
+
verbs = route.verb.to_s
|
|
160
|
+
verbs.empty? || verbs.split("|").include?("GET")
|
|
161
|
+
rescue StandardError
|
|
162
|
+
false
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The normalized facts, or nil when there is no footer (the declared value,
|
|
166
|
+
# or what its callable answered, is nil or false).
|
|
116
167
|
#
|
|
117
168
|
# name: the site name the engine resolved (the site identity's title)
|
|
118
169
|
# logo: the logo the engine resolved (the navbar logo), or nil
|
|
119
170
|
# booking_path: the booking page's path (Studio.booking_path_for), or nil
|
|
120
|
-
|
|
171
|
+
# address: Studio.site_footer_address: the address of facts that
|
|
172
|
+
# carry none of their own
|
|
173
|
+
def resolve(declared, view, name: nil, logo: nil, booking_path: nil, address: nil)
|
|
121
174
|
raw = declared.respond_to?(:call) ? declared.call(view) : declared
|
|
122
|
-
return nil if raw.nil?
|
|
175
|
+
return nil if raw.nil? || raw == false
|
|
123
176
|
|
|
124
177
|
facts = symbolize(raw)
|
|
125
178
|
site_name = text(facts[:name]) || text(name)
|
|
@@ -133,7 +186,7 @@ module Studio
|
|
|
133
186
|
home_path: safe_href(facts[:home_path], "home_path") || "/",
|
|
134
187
|
tagline: text(facts[:tagline]),
|
|
135
188
|
email: text(facts[:email]),
|
|
136
|
-
address:
|
|
189
|
+
address: address_for(facts, address),
|
|
137
190
|
social: Array(facts[:social]).filter_map { |row| social(row) },
|
|
138
191
|
columns: Array(facts[:columns]).filter_map { |row| column(row, booking_path) },
|
|
139
192
|
legal: Array(facts[:legal]).filter_map { |row| link(row, booking_path) },
|
|
@@ -205,7 +258,7 @@ module Studio
|
|
|
205
258
|
# no address at all: no Location band and no map.
|
|
206
259
|
def address(facts)
|
|
207
260
|
declared = facts[:address]
|
|
208
|
-
source = declared.nil? ? facts : symbolize(declared)
|
|
261
|
+
source = declared.nil? || declared == false ? facts : symbolize(declared)
|
|
209
262
|
street = text(source[:street])
|
|
210
263
|
city_line = text(source[:city_line])
|
|
211
264
|
return nil if street.nil? && city_line.nil?
|
|
@@ -228,6 +281,16 @@ module Studio
|
|
|
228
281
|
}
|
|
229
282
|
end
|
|
230
283
|
|
|
284
|
+
# The facts' own address, else the configured one (Studio.site_footer_address).
|
|
285
|
+
# The facts win: an address of their own is used whole, and an `address:` key
|
|
286
|
+
# they wrote, even nil or false, is their answer.
|
|
287
|
+
def address_for(facts, configured)
|
|
288
|
+
own = address(facts)
|
|
289
|
+
return own if !own.nil? || facts.key?(:address) || configured.nil? || configured == false
|
|
290
|
+
|
|
291
|
+
address(symbolize(configured))
|
|
292
|
+
end
|
|
293
|
+
|
|
231
294
|
def coordinate(value)
|
|
232
295
|
return nil if value.nil? || value.to_s.strip.empty?
|
|
233
296
|
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -582,10 +582,16 @@ module Studio
|
|
|
582
582
|
# primitives: Google Calendar's appointment page inline, in a popup, and on a
|
|
583
583
|
# page. See docs/SITE_FOOTER.md and docs/BOOKING.md.
|
|
584
584
|
|
|
585
|
-
# The footer's facts
|
|
586
|
-
#
|
|
587
|
-
#
|
|
588
|
-
#
|
|
585
|
+
# The footer's facts. THE FOOTER IS ON BY DEFAULT: left unset (nil), the
|
|
586
|
+
# layout line `studio_site_footer` renders the DEFAULT footer, built from what
|
|
587
|
+
# the engine already knows (the site name, the footer or navbar logo, and a
|
|
588
|
+
# legal line linking the host's `privacy` and `terms` routes when it has
|
|
589
|
+
# them). No address, no map, no email, no link columns.
|
|
590
|
+
#
|
|
591
|
+
# nil (unset) the default footer
|
|
592
|
+
# false NO footer: the one opt-out
|
|
593
|
+
# a Hash, or a callable receiving the view: the app's own facts. Every key
|
|
594
|
+
# is optional; shape and rules: lib/studio/site_footer.rb.
|
|
589
595
|
#
|
|
590
596
|
# config.site_footer = ->(view) {
|
|
591
597
|
# { tagline: "Everything, by example",
|
|
@@ -601,6 +607,35 @@ module Studio
|
|
|
601
607
|
@@site_footer = declared
|
|
602
608
|
end
|
|
603
609
|
|
|
610
|
+
# True unless the app turned the footer off (`config.site_footer = false`).
|
|
611
|
+
def self.site_footer_enabled?
|
|
612
|
+
site_footer != false
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# True when the app wrote its own facts, as against taking the default footer
|
|
616
|
+
# (unset) or none (false).
|
|
617
|
+
def self.site_footer_declared?
|
|
618
|
+
!site_footer.nil? && site_footer != false
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# THE LOCATION, AS ONE SETTING: the address the footer prints in its Location
|
|
622
|
+
# band, and maps when it carries lat: and lng:. nil (the default) is no
|
|
623
|
+
# location. It is how an app on the default footer adds one without writing
|
|
624
|
+
# the facts callable:
|
|
625
|
+
#
|
|
626
|
+
# config.site_footer_address = { street: "123 Example St", city_line: "Washington, DC 20024",
|
|
627
|
+
# lat: 38.8894, lng: -77.0352 }
|
|
628
|
+
#
|
|
629
|
+
# The keys are the facts' `address:` keys (docs/SITE_FOOTER.md). An app's own
|
|
630
|
+
# facts WIN: when they carry an address this is not read, and `address: false`
|
|
631
|
+
# there shows none.
|
|
632
|
+
mattr_reader :site_footer_address, default: nil
|
|
633
|
+
|
|
634
|
+
def self.site_footer_address=(declared)
|
|
635
|
+
SiteFooter.validate_address!(declared)
|
|
636
|
+
@@site_footer_address = declared || nil
|
|
637
|
+
end
|
|
638
|
+
|
|
604
639
|
# Controllers whose pages keep the footer for a SIGNED-IN viewer, by
|
|
605
640
|
# controller name ("landing") or path ("admin/reports"). A visitor gets the
|
|
606
641
|
# footer on every page; a signed-in viewer gets it only here, because every
|
|
@@ -1207,14 +1242,25 @@ module Studio
|
|
|
1207
1242
|
end
|
|
1208
1243
|
|
|
1209
1244
|
# The site footer's facts resolved for a view (lib/studio/site_footer.rb), or
|
|
1210
|
-
# nil when the app
|
|
1211
|
-
#
|
|
1212
|
-
#
|
|
1245
|
+
# nil when the app turned the footer off (or its own callable answered nil).
|
|
1246
|
+
# Unset, the facts are the default footer's. The name defaults to the site
|
|
1247
|
+
# identity's title and the logo to the navbar logo, so an app that has set
|
|
1248
|
+
# those up says them once; Studio.site_footer_address is the address of any
|
|
1249
|
+
# facts that carry none.
|
|
1213
1250
|
def self.site_footer_for(view)
|
|
1214
|
-
return nil
|
|
1251
|
+
return nil unless site_footer_enabled?
|
|
1252
|
+
|
|
1253
|
+
declared = site_footer.nil? ? SiteFooter.default_facts(view, routes: host_route_set) : site_footer
|
|
1254
|
+
SiteFooter.resolve(declared, view, name: site_identity[:title], logo: logo_for("Footer Logo"),
|
|
1255
|
+
booking_path: booking_path_for(view), address: site_footer_address)
|
|
1256
|
+
end
|
|
1257
|
+
|
|
1258
|
+
# The host's route set, for the default footer's legal links. nil outside a
|
|
1259
|
+
# booted Rails app.
|
|
1260
|
+
def self.host_route_set
|
|
1261
|
+
return nil unless defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
|
|
1215
1262
|
|
|
1216
|
-
|
|
1217
|
-
booking_path: booking_path_for(view))
|
|
1263
|
+
::Rails.application.routes
|
|
1218
1264
|
end
|
|
1219
1265
|
|
|
1220
1266
|
# Navbar links resolved for a view context (lib/studio/navbar_links.rb).
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.87.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|