bible270 1.8.5 → 1.9.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: 37c1e22aede65482ba3d194eb1d2e74efde387475c1f264c30496e6330d3666c
4
- data.tar.gz: 298306a3422ce83b4f68b2a8e8acaa16b956c30475a7d9d36afd88a76f086450
3
+ metadata.gz: 251f07d9a39f657788b627258c3cf78a260f651be7e998267a3cd7028cd6e172
4
+ data.tar.gz: 49fc0b718a2d8625d18ba357dff2af2891de59a8daf4a113a566737f2afd6fe6
5
5
  SHA512:
6
- metadata.gz: 471fff64f3f002999a00c5785bdf975aa5c5ed4723c9c107e7e738161f4b4740b7f73c29067f0aa22a5bdb3d0c5cf818b5e3ebcb27b9b89898a7e1a1340b0abf
7
- data.tar.gz: b68eddc3274715999dde7df1df9928f4b0899e45323f18b91ba9e7d17781c81ad02df129ac52fcb09427fce8e8466d87f3b51be211c37fbbf30ad4fef8f15b15
6
+ metadata.gz: cb3c3b3246e339330a180f670af37a8d558a01c7a7b4a9fb71087aecf77ecc01508fbcb329e389339a8531dd7f08a18e9b6b37a05a3d1f9bb945d72629521502
7
+ data.tar.gz: 83023e4b01da0ce5a7b152801a5b6de52b1409b3becb172e11ab05b69dad6a35bc0095e486c446ffaae076ee97ad0a5d886a813f5d68bb698e37d9c6400f485a
data/README.md CHANGED
@@ -368,6 +368,24 @@ Psalm 35: []
368
368
  Psalm 78: [20, 39, 55]
369
369
  ```
370
370
 
371
+ ## Display preference
372
+
373
+ Readers can choose **Light**, **Dark**, or **System** under **Profile → Display**, then save.
374
+ The preference is stored with their account and applies throughout Bible270 on every device.
375
+ System is the default for existing and new readers, follows the device's current appearance,
376
+ and responds to changes without reloading. Signed-out visitors also use System.
377
+ The completion dove retains its bright colors in either theme.
378
+
379
+ Upgrading hosts must install and run migration `20260101000023`:
380
+
381
+ ```bash
382
+ bin/rails bible270:install:migrations
383
+ bin/rails db:migrate
384
+ ```
385
+
386
+ Custom engine layouts must render the shared styles and set `data-display-mode` on the
387
+ `.b270` element to `current_reader&.effective_display_mode || 'system'` to honor the preference.
388
+
371
389
  ## Start dates
372
390
 
373
391
  The plan runs **undated** (day numbers only) or **dated** (days mapped onto a calendar):
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- # A reader editing their own name — the one thing about themselves they can
5
- # change. Everything else about a reader comes from how they signed in.
4
+ # A reader editing their own details and preferences.
6
5
  class ProfilesController < ApplicationController
7
6
  before_action :require_signed_in_reader
8
7
 
9
8
  def edit
10
9
  @names = current_reader.suggested_names
10
+ @display_mode = current_reader.effective_display_mode
11
11
  load_comment_notification_state
12
12
  load_daily_reminder_state
13
13
  end
@@ -16,7 +16,8 @@ module Bible270
16
16
  @comment_notifications_available = comment_notifications_available?
17
17
  @comment_notification_level = requested_comment_notification_level
18
18
  @daily_reminders_available = daily_reminders_available?
19
- problems = [comment_notification_problem].compact
19
+ @display_mode = params.fetch(:display_mode, current_reader.effective_display_mode).to_s
20
+ problems = [comment_notification_problem, display_mode_problem].compact
20
21
  problems << 'both a first and last name' unless current_reader.update_names(params[:first_name],
21
22
  params[:last_name])
22
23
  if params[:bible_version].present? && !current_reader.update_bible_version(params[:bible_version])
@@ -35,6 +36,7 @@ module Bible270
35
36
  end
36
37
 
37
38
  if problems.empty?
39
+ save_display_mode
38
40
  current_reader.update_comment_notification_level!(@comment_notification_level) if @comment_notifications_available
39
41
  if @daily_reminders_available
40
42
  current_reader.update!(daily_reminders: requested_daily_reminders,
@@ -57,6 +59,17 @@ module Bible270
57
59
 
58
60
  private
59
61
 
62
+ def display_mode_problem
63
+ return unless Reader.display_mode_column?
64
+ return if Reader::DISPLAY_MODES.include?(@display_mode)
65
+
66
+ 'a display preference from the list'
67
+ end
68
+
69
+ def save_display_mode
70
+ current_reader.update!(display_mode: @display_mode) if Reader.display_mode_column?
71
+ end
72
+
60
73
  def load_comment_notification_state
61
74
  @comment_notifications_available = comment_notifications_available?
62
75
  @comment_notification_level = current_reader.comment_notification_level
@@ -27,6 +27,8 @@ module Bible270
27
27
 
28
28
  PASSAGE_SOURCES = %w[bible_com bible_gateway blue_letter_bible].freeze
29
29
  DEFAULT_PASSAGE_SOURCE = 'bible_com'
30
+ DISPLAY_MODES = %w[light dark system].freeze
31
+ DEFAULT_DISPLAY_MODE = 'system'
30
32
  DAILY_REMINDER_TIME_FORMAT = %r{\A(?:[01]\d|2[0-3]):[0-5]\d\z}
31
33
  DAILY_REMINDER_MINUTES = %w[00 15 30 45].freeze
32
34
  DAILY_REMINDER_COLUMNS = %w[daily_reminders daily_reminder_time last_daily_reminder_sent_on].freeze
@@ -39,6 +41,7 @@ module Bible270
39
41
  validates :display_name, presence: true
40
42
  validates :uid, uniqueness: { scope: :provider }, allow_nil: true
41
43
  validates :passage_source, inclusion: { in: PASSAGE_SOURCES }
44
+ validates :display_mode, inclusion: { in: DISPLAY_MODES }, if: -> { has_attribute?(:display_mode) }
42
45
  validates :daily_reminder_time,
43
46
  format: { with: DAILY_REMINDER_TIME_FORMAT, message: 'must use 24-hour HH:MM format' },
44
47
  if: -> { self.class.daily_reminder_columns? }
@@ -55,6 +58,17 @@ module Bible270
55
58
  false
56
59
  end
57
60
 
61
+ def self.display_mode_column?
62
+ column_names.include?('display_mode')
63
+ rescue ActiveRecord::StatementInvalid
64
+ false
65
+ end
66
+
67
+ def effective_display_mode
68
+ value = self[:display_mode] if has_attribute?(:display_mode)
69
+ DISPLAY_MODES.include?(value) ? value : DEFAULT_DISPLAY_MODE
70
+ end
71
+
58
72
  def self.valid_daily_reminder_time?(value)
59
73
  value.to_s.match?(DAILY_REMINDER_TIME_FORMAT)
60
74
  end
@@ -38,6 +38,20 @@
38
38
  </label>
39
39
  <%= render "bible270/shared/passage_source", reader: current_reader %>
40
40
 
41
+ <% if Bible270::Reader.display_mode_column? %>
42
+ <label class="b270-field">
43
+ <span>Display</span>
44
+ <%= select_tag :display_mode,
45
+ options_for_select([["Light", "light"], ["Dark", "dark"], ["System", "system"]], @display_mode),
46
+ class: "b270-select", aria: { describedby: "b270-display-hint" } %>
47
+ <span id="b270-display-hint" class="b270-hint" style="margin:0">System follows your device's light or dark setting.</span>
48
+ </label>
49
+ <% else %>
50
+ <p class="b270-flash alert" style="margin:0">
51
+ Display settings are temporarily unavailable while this installation finishes a database update.
52
+ </p>
53
+ <% end %>
54
+
41
55
  <% if @comment_notifications_available %>
42
56
  <%= render "bible270/shared/comment_notification_choices",
43
57
  level: @comment_notification_level,
@@ -3,9 +3,52 @@
3
3
  --paper:#ece6da; --card:#f4efe6; --ink:#231f18; --muted:#5d564c; --faint:#6e665b;
4
4
  --line:#d7cfbf; --line-soft:#e2dbcd; --teal:#2f6a67; --oxblood:#8a2f3b; --gold:#80631d;
5
5
  --gold-lite:#80631d; --shadow:0 1px 0 rgba(35,31,24,.04),0 10px 30px -18px rgba(35,31,24,.35);
6
+ --control:#fff; --control-line:#d7cfbf; --task-surface:rgba(255,255,255,.35); --button-hover:#3a352b;
7
+ --on-accent:#fff; --complete-ink:#eef3f2; --complete-dot:#f4efe6;
8
+ --button-surface:#231f18; --button-ink:#ece6da;
9
+ --teal-fill:#2f6a67; --oxblood-fill:#8a2f3b; --gold-fill:#80631d;
10
+ --teal-mark:#2f6a67; --oxblood-mark:#8a2f3b; --gold-mark:#80631d;
11
+ --alert-surface:#f6e4e2; --alert-ink:#7a2b28; --alert-line:#e6c3bf;
12
+ --notice-surface:#e7efe6; --notice-ink:#2f5d34; --notice-line:#c6dcc6;
13
+ color-scheme:light;
6
14
  margin:0; background:radial-gradient(120% 80% at 100% 0%,rgba(165,129,44,.06),transparent 55%),var(--paper);
7
15
  color:var(--ink); font-family:"Inter",system-ui,sans-serif; line-height:1.2; -webkit-font-smoothing:antialiased;
8
16
  }
17
+ /* Explicit light keeps native controls light even when the OS is dark.
18
+ Only system mode follows the media query, including live OS changes. */
19
+ .b270[data-display-mode="light"]{color-scheme:light}
20
+ .b270[data-display-mode="dark"]{
21
+ color-scheme:dark;
22
+ background-image:none;
23
+ --control-line:#827c6e;
24
+ --paper:#39311f; --card:#322714; --ink:#ccc9bd; --muted:#b7b09f; --faint:#a9a28f;
25
+ --line:#42484a; --line-soft:#4b422d; --teal:#80b5ad; --oxblood:#d19199; --gold:#d6be79;
26
+ --gold-lite:#695116; --shadow:0 1px 0 rgba(0,0,0,.1);
27
+ --control:#181b1b; --task-surface:rgba(255,255,255,.025); --button-hover:#292316;
28
+ --on-accent:#ded8c5; --complete-ink:#ded8c5; --complete-dot:#ded8c5;
29
+ --button-surface:#201c12; --button-ink:#ded8c5;
30
+ --teal-fill:#2f5955; --oxblood-fill:#702731; --gold-fill:#695116;
31
+ --teal-mark:#409594; --oxblood-mark:#a73d50; --gold-mark:#b18b2b;
32
+ --alert-surface:#42282a; --alert-ink:#ffc0b8; --alert-line:#a66d68;
33
+ --notice-surface:#25382a; --notice-ink:#b6dfb6; --notice-line:#6d956e;
34
+ }
35
+ @media (prefers-color-scheme:dark){
36
+ .b270[data-display-mode="system"]{
37
+ color-scheme:dark;
38
+ background-image:none;
39
+ --control-line:#827c6e;
40
+ --paper:#39311f; --card:#322714; --ink:#ccc9bd; --muted:#b7b09f; --faint:#a9a28f;
41
+ --line:#42484a; --line-soft:#4b422d; --teal:#80b5ad; --oxblood:#d19199; --gold:#d6be79;
42
+ --gold-lite:#695116; --shadow:0 1px 0 rgba(0,0,0,.1);
43
+ --control:#181b1b; --task-surface:rgba(255,255,255,.025); --button-hover:#292316;
44
+ --on-accent:#ded8c5; --complete-ink:#ded8c5; --complete-dot:#ded8c5;
45
+ --button-surface:#201c12; --button-ink:#ded8c5;
46
+ --teal-fill:#2f5955; --oxblood-fill:#702731; --gold-fill:#695116;
47
+ --teal-mark:#409594; --oxblood-mark:#a73d50; --gold-mark:#b18b2b;
48
+ --alert-surface:#42282a; --alert-ink:#ffc0b8; --alert-line:#a66d68;
49
+ --notice-surface:#25382a; --notice-ink:#b6dfb6; --notice-line:#6d956e;
50
+ }
51
+ }
9
52
  .b270 *{box-sizing:border-box}
10
53
  /* Blanket anchor colour. NB: this is (0,1,1), so any single-class rule
11
54
  colouring an <a> (.b270-btn, .b270-cell, .b270-arrow ...) must be
@@ -38,7 +81,7 @@
38
81
  .b270-avatarrow input[type=file]{font:inherit;font-size:13px;max-width:100%}
39
82
  .b270-emailform{display:grid;gap:12px;max-width:340px;margin:12px 0 0}
40
83
  .b270-field{display:grid;gap:5px;font-size:12.5px;color:var(--muted)}
41
- .b270-field input{font:inherit;font-size:15px;padding:10px 12px;border:1px solid var(--line);border-radius:9px;background:#fff;color:var(--ink);width:100%}
84
+ .b270-field input{font:inherit;font-size:15px;padding:10px 12px;border:1px solid var(--control-line);border-radius:9px;background:var(--control);color:var(--ink);width:100%}
42
85
  .b270-field input:focus{outline:none;border-color:var(--gold);box-shadow:0 0 0 3px rgba(165,129,44,.13)}
43
86
  .b270-notification-field{margin:0;padding:0;border:0}
44
87
  .b270-notification-field legend{padding:0;margin:0 0 3px}
@@ -63,8 +106,8 @@
63
106
  .b270-time-control:active .b270-reminder-tooltip{visibility:visible;opacity:1}
64
107
  .b270-field .b270-reminder-optin input[type=checkbox]{width:auto;margin:0;flex:none}
65
108
  .b270-time-control{grid-template-columns:48px 5px 48px;align-items:center;gap:2px}
66
- .b270-field .b270-time-select{width:48px;height:30px;margin:0;padding:4px 2px;border:1px solid var(--line);
67
- border-radius:7px;background:#fff;color:var(--ink);font:12px/16px system-ui,sans-serif;
109
+ .b270-field .b270-time-select{width:48px;height:30px;margin:0;padding:4px 2px;border:1px solid var(--control-line);
110
+ border-radius:7px;background:var(--control);color:var(--ink);font:12px/16px system-ui,sans-serif;
68
111
  font-variant-numeric:tabular-nums;text-align:center}
69
112
  .b270-time-select:focus{outline:none;border-color:var(--gold);box-shadow:0 0 0 3px rgba(165,129,44,.13)}
70
113
  .b270-time-separator{display:grid;place-items:center;height:30px;font:12px/30px system-ui,sans-serif;color:var(--muted)}
@@ -72,19 +115,19 @@
72
115
  .b270-or{display:flex;align-items:center;gap:12px;margin:22px 0 18px;color:var(--faint);font-size:12px;letter-spacing:.1em;text-transform:uppercase}
73
116
  .b270-or::before,.b270-or::after{content:"";flex:1;height:1px;background:var(--line-soft)}
74
117
  .b270-providers{display:grid;gap:10px;max-width:340px}
75
- .b270 .b270-provider{display:block;width:100%;text-align:center;border:1px solid var(--line);background:#fff;color:var(--ink);border-radius:10px;padding:12px 16px;font:inherit;font-weight:600;font-size:14.5px;cursor:pointer;transition:border-color .15s,transform .05s}
118
+ .b270 .b270-provider{display:block;width:100%;text-align:center;border:1px solid var(--line);background:var(--control);color:var(--ink);border-radius:10px;padding:12px 16px;font:inherit;font-weight:600;font-size:14.5px;cursor:pointer;transition:border-color .15s,transform .05s}
76
119
  .b270-provider:hover{border-color:var(--gold);color:var(--gold)}
77
120
  .b270-provider:active{transform:translateY(1px)}
78
121
  .b270 .b270-signinbtn{border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:8px;padding:7px 13px;font:inherit;font-weight:600;font-size:13px;cursor:pointer}
79
122
  .b270-signinbtn:hover{border-color:var(--gold);color:var(--gold)}
80
123
  .b270-signin-hint a{color:var(--gold);font-weight:600}
81
124
  .b270-flash{border-radius:10px;padding:11px 15px;margin-bottom:18px;font-size:14px}
82
- .b270-flash.alert{background:#f6e4e2;color:#7a2b28;border:1px solid #e6c3bf}
83
- .b270-flash.notice{background:#e7efe6;color:#2f5d34;border:1px solid #c6dcc6}
125
+ .b270-flash.alert{background:var(--alert-surface);color:var(--alert-ink);border:1px solid var(--alert-line)}
126
+ .b270-flash.notice{background:var(--notice-surface);color:var(--notice-ink);border:1px solid var(--notice-line)}
84
127
  .b270-interaction-status{margin:8px 0 12px;padding:8px 11px;border-radius:8px;background:rgba(47,106,103,.08);color:var(--teal);font-size:12.5px}
85
128
  .b270-interaction-status:empty{display:none}
86
129
  .b270-interaction-status[data-state="pending"]{color:var(--muted)}
87
- .b270-interaction-status[data-state="error"]{background:#f6e4e2;color:#7a2b28}
130
+ .b270-interaction-status[data-state="error"]{background:var(--alert-surface);color:var(--alert-ink)}
88
131
  .b270 form[aria-busy="true"] [data-b270-disabled-by-submit]{cursor:wait;opacity:.55}
89
132
 
90
133
  .b270-eyebrow{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);font-weight:600;margin:0 0 10px}
@@ -103,7 +146,7 @@
103
146
 
104
147
 
105
148
  .b270-task-list{list-style:none;margin:0;padding:0;display:grid;gap:8px}
106
- .b270-task{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:10px 12px;border:1px solid var(--line-soft);border-radius:9px;background:rgba(255,255,255,.35)}
149
+ .b270-task{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:10px 12px;border:1px solid var(--line-soft);border-radius:9px;background:var(--task-surface)}
107
150
  .b270-task-body{display:grid;gap:3px;min-width:0}
108
151
  .b270-task-body strong{font-size:14px}
109
152
  .b270-task-body span{color:var(--muted);font-size:12px;line-height:1.4}
@@ -118,7 +161,7 @@
118
161
  .b270-startline{margin:0;font-size:13.5px;color:var(--muted)}
119
162
  .b270-startline strong{color:var(--ink)}
120
163
  .b270-startform{display:flex;align-items:flex-end;gap:10px;margin-top:10px;flex-wrap:wrap}
121
- .b270-startform input[type=date]{font:inherit;font-size:14px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink)}
164
+ .b270-startform input[type=date]{font:inherit;font-size:14px;padding:8px 10px;border:1px solid var(--control-line);border-radius:8px;background:var(--control);color:var(--ink)}
122
165
  .b270-linkbtn{background:none;border:none;padding:0;margin-top:8px;font:inherit;font-size:12.5px;color:var(--oxblood);cursor:pointer;text-decoration:underline}
123
166
  .b270-hint{margin:8px 0 0;font-size:12px;color:var(--faint);max-width:111ch}
124
167
  .b270-tracks{margin-top:16px;display:grid;gap:11px}
@@ -139,7 +182,7 @@
139
182
  .b270-dayhead .k{display:flex;align-items:center;flex-wrap:wrap;gap:9px;font-family:"Spectral",serif;font-size:24px;font-weight:600;line-height:1}
140
183
  .b270-dayhead .date{display:flex;align-items:center;flex-wrap:wrap;gap:9px;color:var(--faint);font-size:13px;margin-top:4px}
141
184
  .b270-dayhead .b270-badge{margin-left:0}
142
- .b270-badge{display:inline-block;vertical-align:middle;line-height:1;font-size:10.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:#fff;background:var(--gold-lite);padding:5px 8px 4px;border-radius:20px;margin-left:9px}
185
+ .b270-badge{display:inline-block;vertical-align:middle;line-height:1;font-size:10.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--on-accent);background:var(--gold-lite);padding:5px 8px 4px;border-radius:20px;margin-left:9px}
143
186
 
144
187
  .b270-day-complete{display:flex;align-items:center;gap:14px;margin:0 0 14px;padding:11px 13px;
145
188
  border:1px solid var(--line);border-radius:10px;background:var(--card);box-shadow:var(--shadow)}
@@ -168,15 +211,15 @@
168
211
  .b270-readings{background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
169
212
  .b270-reading{display:flex;gap:15px;align-items:center;padding:16px 20px;border-left:4px solid transparent}
170
213
  .b270-reading+.b270-reading{border-top:1px solid var(--line-soft)}
171
- .b270-reading.ot{border-left-color:var(--teal)} .b270-reading.nt{border-left-color:var(--oxblood)} .b270-reading.pp{border-left-color:var(--gold)}
214
+ .b270-reading.ot{border-left-color:var(--teal-mark)} .b270-reading.nt{border-left-color:var(--oxblood-mark)} .b270-reading.pp{border-left-color:var(--gold-mark)}
172
215
  .b270-reading.rest{opacity:.65}
173
216
  .b270-toggle{border:none;background:none;padding:0;cursor:pointer}
174
217
  .b270-toggle form{margin:0}
175
- .b270-check{width:26px;height:26px;border-radius:8px;border:2px solid var(--line);flex:none;display:grid;place-items:center;background:#fff;transition:background-color .18s,border-color .18s,opacity .18s}
176
- .b270-check svg{width:15px;height:15px;stroke:#fff;stroke-width:3;fill:none;stroke-linecap:round;stroke-linejoin:round;opacity:0}
177
- .b270-reading.done.ot .b270-check{background:var(--teal);border-color:var(--teal)}
178
- .b270-reading.done.nt .b270-check{background:var(--oxblood);border-color:var(--oxblood)}
179
- .b270-reading.done.pp .b270-check{background:var(--gold);border-color:var(--gold)}
218
+ .b270-check{width:26px;height:26px;border-radius:8px;border:2px solid var(--control-line);flex:none;display:grid;place-items:center;background:var(--control);transition:background-color .18s,border-color .18s,opacity .18s}
219
+ .b270-check svg{width:15px;height:15px;stroke:var(--on-accent);stroke-width:3;fill:none;stroke-linecap:round;stroke-linejoin:round;opacity:0}
220
+ .b270-reading.done.ot .b270-check{background:var(--teal-fill);border-color:var(--teal-mark)}
221
+ .b270-reading.done.nt .b270-check{background:var(--oxblood-fill);border-color:var(--oxblood-mark)}
222
+ .b270-reading.done.pp .b270-check{background:var(--gold-fill);border-color:var(--gold-mark)}
180
223
  .b270-reading.done .b270-check svg{opacity:1}
181
224
  .b270-rbody{flex:1;min-width:0}
182
225
  .b270-cat{font-size:11px;letter-spacing:.13em;text-transform:uppercase;color:var(--faint);font-weight:600}
@@ -195,16 +238,16 @@
195
238
  /* A ticked chapter looks exactly like a ticked single reading: the box fills
196
239
  in the track's colour and the tick appears. No strikethrough — the other
197
240
  tracks don't strike their references either. */
198
- .b270-reading.ot .b270-part.done .b270-check{background:var(--teal);border-color:var(--teal)}
199
- .b270-reading.nt .b270-part.done .b270-check{background:var(--oxblood);border-color:var(--oxblood)}
200
- .b270-reading.pp .b270-part.done .b270-check{background:var(--gold);border-color:var(--gold)}
241
+ .b270-reading.ot .b270-part.done .b270-check{background:var(--teal-fill);border-color:var(--teal-mark)}
242
+ .b270-reading.nt .b270-part.done .b270-check{background:var(--oxblood-fill);border-color:var(--oxblood-mark)}
243
+ .b270-reading.pp .b270-part.done .b270-check{background:var(--gold-fill);border-color:var(--gold-mark)}
201
244
  .b270-part.done .b270-check svg{opacity:1}
202
245
  .b270-parts-meta{margin-top:10px;font-size:12.5px;color:var(--faint)}
203
246
  .b270-version{font-size:11.5px;color:var(--faint);font-weight:400;letter-spacing:.02em;white-space:nowrap}
204
- .b270-select{font:inherit;font-size:14px;padding:10px 30px 10px 12px;border:1px solid var(--line);border-radius:9px;background:#fff;color:var(--ink);width:100%;max-width:100%;text-overflow:ellipsis}
247
+ .b270-select{font:inherit;font-size:14px;padding:10px 30px 10px 12px;border:1px solid var(--control-line);border-radius:9px;background:var(--control);color:var(--ink);width:100%;max-width:100%;text-overflow:ellipsis}
205
248
  .b270-source-choices{display:flex;align-items:center;justify-content:space-between;gap:4px;flex-wrap:nowrap;width:100%;margin:8px 0 2px;color:var(--muted);font-size:clamp(10.5px,3.4vw,12.5px)}
206
249
  .b270-source-choice{display:inline-flex;align-items:center;gap:4px;cursor:pointer;white-space:nowrap}
207
- .b270-source-choice input{appearance:none;width:14px;height:14px;margin:0;border:1px solid var(--line);border-radius:3px;background:#fff;display:grid;place-content:center;cursor:pointer;flex:none}
250
+ .b270-source-choice input{appearance:none;width:14px;height:14px;margin:0;border:1px solid var(--control-line);border-radius:3px;background:var(--control);display:grid;place-content:center;cursor:pointer;flex:none}
208
251
  .b270-source-choice input::before{content:"";width:9px;height:9px;transform:scale(0);transition:transform .1s;clip-path:polygon(14% 44%,0 59%,38% 100%,100% 18%,82% 0,37% 65%);background:var(--paper)}
209
252
  .b270-source-choice input:checked{background:var(--gold);border-color:var(--gold)}
210
253
  .b270-source-choice input:checked::before{transform:scale(1)}
@@ -219,7 +262,7 @@
219
262
 
220
263
  /* completers */
221
264
  .b270-completers{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:16px;font-size:13px;color:var(--muted)}
222
- .b270-avatar{border-radius:50%;object-fit:cover;display:inline-block;text-align:center;background:var(--teal);color:#fff;font-size:12px;font-weight:600}
265
+ .b270-avatar{border-radius:50%;object-fit:cover;display:inline-block;text-align:center;background:var(--teal-fill);color:var(--on-accent);font-size:12px;font-weight:600}
223
266
  .b270-avatar-fallback{border-radius:50%}
224
267
 
225
268
  /* comments */
@@ -281,8 +324,8 @@
281
324
  .b270-comment.b270-editing{background:rgba(165,129,44,.05);border-radius:10px;padding:12px}
282
325
  .b270-editing textarea{min-height:64px}
283
326
  .b270-cdel{font-size:12px;color:var(--oxblood);background:none;border:none;cursor:pointer;padding:0;margin-left:8px}
284
- .b270-input{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:9px;background:#fff;font:inherit;font-size:14px;margin-bottom:8px}
285
- .b270-form textarea{width:100%;font:inherit;font-size:15px;border:1px solid var(--line);border-radius:10px;padding:11px 13px;background:#fff;resize:vertical;min-height:76px}
327
+ .b270-input{width:100%;padding:9px 11px;border:1px solid var(--control-line);border-radius:9px;background:var(--control);color:var(--ink);font:inherit;font-size:14px;margin-bottom:8px}
328
+ .b270-form textarea{width:100%;font:inherit;font-size:15px;border:1px solid var(--control-line);border-radius:10px;padding:11px 13px;background:var(--control);color:var(--ink);resize:vertical;min-height:76px}
286
329
  .b270-form textarea[data-b270-autosize=false]{min-height:114px}
287
330
  .b270-broadcast-audience{margin:8px 0}
288
331
  .b270-editor-utilities[hidden]{display:none}
@@ -302,7 +345,7 @@
302
345
  .b270-format-bold{font-weight:700}
303
346
  .b270-format-italic{font-style:italic;font-weight:600}
304
347
  .b270-markdown-editor[data-b270-formatting-open=true] textarea{border-radius:0 0 10px 10px}
305
- .b270-markdown-preview{min-height:76px;margin:0;padding:11px 13px;border:1px solid var(--line);border-radius:10px;background:#fff;color:var(--ink)}
348
+ .b270-markdown-preview{min-height:76px;margin:0;padding:11px 13px;border:1px solid var(--control-line);border-radius:10px;background:var(--control);color:var(--ink)}
306
349
  .b270-markdown-preview[hidden]{display:none}
307
350
  .b270-markdown-preview>.b270-hint{margin:0}
308
351
  .b270-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}
@@ -316,15 +359,16 @@
316
359
  .b270-mention-option:hover,.b270-mention-option[aria-selected=true]{background:var(--paper);color:var(--gold)}
317
360
  .b270-draft-status{min-height:16px;margin:4px 0 -4px;font-size:11.5px;color:var(--faint)}
318
361
  .b270-form .row{display:flex;align-items:center;gap:10px;margin-top:10px}
319
- .b270-form .row select{max-width:100%}
320
- .b270 .b270-btn{border:none;border-radius:10px;padding:8px 16px;margin-top:4px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;background:var(--ink);color:var(--paper);text-decoration:none;display:inline-block}
321
- .b270-btn:hover{background:#3a352b}
362
+ /* Override the reflection track selector's inline background and border color. */
363
+ .b270-form .row select{max-width:100%;background:var(--control)!important;border-color:var(--control-line)!important;color:var(--ink)}
364
+ .b270 .b270-btn{border:none;border-radius:10px;padding:8px 16px;margin-top:4px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;background:var(--button-surface);color:var(--button-ink);text-decoration:none;display:inline-block}
365
+ .b270 .b270-btn:hover{background:var(--button-hover)}
322
366
 
323
367
  /* community */
324
368
  .b270-admin-reader-tools{display:flex;align-items:flex-end;justify-content:flex-end;gap:8px;margin:0 0 10px;flex-wrap:wrap}
325
369
  .b270-admin-reader-sort{display:flex;align-items:flex-end;gap:8px}
326
370
  .b270-admin-reader-sort label{display:grid;gap:3px;font-size:11.5px;color:var(--muted)}
327
- .b270-admin-reader-sort select{min-width:170px;padding:7px 25px 7px 9px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);font:inherit;font-size:12.5px}
371
+ .b270-admin-reader-sort select{min-width:170px;padding:7px 25px 7px 9px;border:1px solid var(--control-line);border-radius:8px;background:var(--control);color:var(--ink);font:inherit;font-size:12.5px}
328
372
  .b270 .b270-admin-reader-sort .b270-btn,.b270 .b270-reader-stats-open{margin:0;padding:8px 13px;font-size:12.5px}
329
373
  .b270-reader-stats-dialog{width:min(680px,calc(100vw - 28px));max-width:none;padding:0;border:1px solid var(--line);border-radius:14px;background:var(--card);color:var(--ink);box-shadow:0 18px 60px rgba(35,31,24,.35)}
330
374
  .b270-reader-stats-dialog::backdrop{background:rgba(35,31,24,.55)}
@@ -333,7 +377,7 @@
333
377
  .b270-reader-stats-head .b270-subhead{margin:0}
334
378
  .b270-reader-stats-head form{margin:0}
335
379
  .b270-reader-stats-head .b270-linkbtn{margin:0;color:var(--muted)}
336
- .b270-reader-stats-dialog pre{max-height:65vh;overflow:auto;margin:0;padding:12px;border:1px solid var(--line-soft);border-radius:9px;background:#fff;color:var(--ink);font:12.5px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
380
+ .b270-reader-stats-dialog pre{max-height:65vh;overflow:auto;margin:0;padding:12px;border:1px solid var(--line-soft);border-radius:9px;background:var(--control);color:var(--ink);font:12.5px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
337
381
  .b270-list{background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
338
382
  .b270-li{display:flex;align-items:center;gap:14px;padding:14px 18px}
339
383
  .b270-li+.b270-li{border-top:1px solid var(--line-soft)}
@@ -349,13 +393,13 @@
349
393
  .b270 .b270-cell{aspect-ratio:1;border-radius:6px;border:1px solid var(--line);background:var(--card);font-size:10.5px;color:var(--faint);display:grid;place-items:center;text-decoration:none;font-variant-numeric:tabular-nums}
350
394
  .b270-cell:hover{border-color:var(--gold);color:var(--gold)}
351
395
  .b270-cell.partial{background:linear-gradient(135deg,var(--card) 50%,rgba(165,129,44,.35) 50%);color:var(--muted)}
352
- .b270-cell.complete{background:var(--teal);border-color:var(--teal);color:#eef3f2;font-weight:600}
396
+ .b270-cell.complete{background:var(--teal-fill);border-color:var(--teal-mark);color:var(--complete-ink);font-weight:600}
353
397
  /* Days that have reflections carry a dot and nothing else: the fill stays
354
398
  free to mean reading progress, so the two facts never compete. On a finished
355
- square the dot goes pale, since oxblood on teal is hard to pick out. */
399
+ square the dot contrasts with teal, since oxblood on teal is hard to pick out. */
356
400
  .b270-cell.talk{position:relative}
357
401
  .b270-cell.talk::after{content:"";position:absolute;top:3px;right:3px;width:7px;height:7px;border-radius:50%;background:var(--oxblood)}
358
- .b270-cell.talk.complete::after{background:#f4efe6}
402
+ .b270-cell.talk.complete::after{background:var(--complete-dot)}
359
403
  .b270-cell.now{box-shadow:0 0 0 2px var(--gold)}
360
404
 
361
405
  /* On the Reflections page each thread carries the day it belongs to, since
@@ -365,7 +409,7 @@
365
409
  .b270-reflection-count{margin:3px 0 0;font-size:12px;color:var(--faint)}
366
410
  .b270-reflection-filters{display:flex;align-items:flex-end;justify-content:flex-end;gap:8px;flex-wrap:wrap}
367
411
  .b270-reflection-filters label{display:grid;gap:3px;font-size:11.5px;color:var(--muted)}
368
- .b270-reflection-filters select{min-width:112px;max-width:180px;padding:7px 25px 7px 9px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);font:inherit;font-size:12.5px}
412
+ .b270-reflection-filters select{min-width:112px;max-width:180px;padding:7px 25px 7px 9px;border:1px solid var(--control-line);border-radius:8px;background:var(--control);color:var(--ink);font:inherit;font-size:12.5px}
369
413
  .b270 .b270-reflection-filters .b270-btn{margin:0;padding:8px 13px;font-size:12.5px}
370
414
  .b270 .b270-filter-clear{align-self:center;font-size:12px;color:var(--muted)}
371
415
  .b270-thread{margin-bottom:8px}
@@ -13,7 +13,7 @@
13
13
  <%= render "bible270/shared/styles" %>
14
14
  <%= yield :head %>
15
15
  </head>
16
- <body class="b270">
16
+ <body class="b270" data-display-mode="<%= current_reader&.effective_display_mode || Bible270::Reader::DEFAULT_DISPLAY_MODE %>">
17
17
  <div class="b270-wrap">
18
18
  <%= render "bible270/shared/header" %>
19
19
  <% if flash[:alert] %><div class="b270-flash alert"><%= flash[:alert] %></div><% end %>
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDisplayModeToBible270Readers < ActiveRecord::Migration[7.0]
4
+ def change
5
+ add_column :bible270_readers, :display_mode, :string, null: false, default: 'system'
6
+ end
7
+ end
@@ -32,6 +32,7 @@ module Bible270
32
32
  'AddCompletionDoveDisabledToBible270Readers' => :check_completion_dove_disabled,
33
33
  'AddBodyFormatToBible270Comments' => :check_comment_body_format,
34
34
  'ChangeAllCommentNoticesDefaultToTrue' => :check_all_comment_notices_default,
35
+ 'AddDisplayModeToBible270Readers' => :check_display_mode,
35
36
  'CreateActiveStorageTables' => :check_active_storage
36
37
  }.freeze
37
38
 
@@ -222,6 +223,16 @@ module Bible270
222
223
  missing
223
224
  end
224
225
 
226
+ def check_display_mode
227
+ missing = table_missing(:bible270_readers, columns: %i[display_mode])
228
+ return missing if missing.any?
229
+
230
+ column = connection.columns(:bible270_readers).find { |candidate| candidate.name == 'display_mode' }
231
+ missing << 'column bible270_readers.display_mode default system' unless column&.default == 'system'
232
+ missing << 'column bible270_readers.display_mode null false' unless column && !column.null
233
+ missing
234
+ end
235
+
225
236
  def check_all_comment_notices_default
226
237
  missing = table_missing(:bible270_readers, columns: %i[notify_on_all_comments])
227
238
  return missing if missing.any?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '1.8.5'
4
+ VERSION = '1.9.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bible270
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.5
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew vonderLuft
@@ -217,6 +217,7 @@ files:
217
217
  - db/migrate/20260101000020_add_completion_dove_disabled_to_bible270_readers.rb
218
218
  - db/migrate/20260101000021_add_body_format_to_bible270_comments.rb
219
219
  - db/migrate/20260101000022_change_all_comment_notices_default_to_true.rb
220
+ - db/migrate/20260101000023_add_display_mode_to_bible270_readers.rb
220
221
  - lib/bible270.rb
221
222
  - lib/bible270/avatars.rb
222
223
  - lib/bible270/comment_formatter.rb