bible270 1.8.4 → 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: a0203626f9a4f8ad023af249e14e69405dff650d48e0e2dc41a71e31cc27daaa
4
- data.tar.gz: 5965d52ad7378dd14fd58f12ea5f7ef3a3789c396db9e7a62c971969af624364
3
+ metadata.gz: 251f07d9a39f657788b627258c3cf78a260f651be7e998267a3cd7028cd6e172
4
+ data.tar.gz: 49fc0b718a2d8625d18ba357dff2af2891de59a8daf4a113a566737f2afd6fe6
5
5
  SHA512:
6
- metadata.gz: b5c0aca831d6336909d96124c9fdb7f4be01858e0fd5d35b72ba13415f3ad9ca19ee2c38329d03f8cec52b10506ec9a98d911a4dede76ac14d6ff7f14a01da91
7
- data.tar.gz: '0803ce48711261b5282cac7a0877c747cd7069f906fc356b5440020661a81ecffd19a417253a56b7915dd866754321cda958c1f93dc2a2a451c8e97e6302d679'
6
+ metadata.gz: cb3c3b3246e339330a180f670af37a8d558a01c7a7b4a9fb71087aecf77ecc01508fbcb329e389339a8531dd7f08a18e9b6b37a05a3d1f9bb945d72629521502
7
+ data.tar.gz: 83023e4b01da0ce5a7b152801a5b6de52b1409b3becb172e11ab05b69dad6a35bc0095e486c446ffaae076ee97ad0a5d886a813f5d68bb698e37d9c6400f485a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,9 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
7
7
  ### Changed
8
8
 
9
9
  - keep Proverbs 7 whole and divide Proverbs 10 and 16 into four readings
10
+ - show likes and replies on My Progress and reader pages, with admin edit links on reader pages
11
+ - show each reader's pace beside their completed-day count for admins on Community
12
+ - add edit, like, and reply controls to reflections on Admin reader pages
10
13
 
11
14
  **[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.8.1...main)**
12
15
 
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):
@@ -97,7 +97,7 @@ module Bible270
97
97
  @comment_notifications_available = comment_notifications_available_for?(@reader)
98
98
  @completion_dove_preference_available = completion_dove_available_for(@reader)
99
99
  @days_completed = @reader.days_completed
100
- @comments = @reader.comments.order(created_at: :desc).limit(50)
100
+ @comments = @reader.comments.includes(likes: :reader).order(created_at: :desc).limit(50)
101
101
  end
102
102
 
103
103
  # ---- reflections ------------------------------------------------------
@@ -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
@@ -17,7 +17,7 @@ module Bible270
17
17
  def progress
18
18
  @reader = current_reader
19
19
  # Defaults keep the open visitor page simple and make every view input explicit.
20
- @recent_comments = @reader ? @reader.comments.recent.limit(5) : Comment.none
20
+ @recent_comments = @reader ? recent_reflections(@reader, limit: 5) : []
21
21
  @partial_day_tasks = []
22
22
  @remaining_partial_days = 0
23
23
  return unless @reader
@@ -34,7 +34,7 @@ module Bible270
34
34
  redirect_to(community_path, alert: 'Reader not found.') and return unless @reader
35
35
 
36
36
  @days_completed = @reader.days_completed
37
- @recent_comments = @reader.comments.recent.limit(20)
37
+ @recent_comments = recent_reflections(@reader, limit: 20)
38
38
  end
39
39
 
40
40
  def mention_suggestions
@@ -43,5 +43,16 @@ module Bible270
43
43
  response.headers['Cache-Control'] = 'private, no-store'
44
44
  render json: { suggestions: Reader.mention_suggestions(params[:q], except: current_reader) }
45
45
  end
46
+
47
+ private
48
+
49
+ def recent_reflections(reader, limit:)
50
+ comments = reader.comments.recent
51
+ .includes(:reader, { likes: :reader }, { replies: [:reader, { likes: :reader }] })
52
+ .limit(limit).to_a
53
+ ids = comments.map(&:id)
54
+ # A reader's own replies already appear beneath any listed parent reflection.
55
+ comments.reject { |comment| ids.include?(comment.parent_id) }
56
+ end
46
57
  end
47
58
  end
@@ -150,7 +150,14 @@ module Bible270
150
150
  def edited? = updated_at - created_at > 1
151
151
 
152
152
  # Visible replies, oldest first: a conversation reads forwards.
153
- def visible_replies = replies.approved.order(created_at: :asc)
153
+ def visible_replies
154
+ if replies.loaded?
155
+ replies.select(&:approved?).sort_by { |reply| [reply.created_at, reply.id] }
156
+ else
157
+ replies.approved.order(created_at: :asc, id: :asc)
158
+ end
159
+ end
160
+
154
161
  def moderated? = moderated_at.present?
155
162
 
156
163
  private
@@ -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
@@ -643,6 +657,17 @@ module Bible270
643
657
  Plan.day_for(Bible270.today, effective_start_date)
644
658
  end
645
659
 
660
+ def progress_status(completed_days: days_completed)
661
+ today = calendar_day
662
+ return 'undated' unless today
663
+
664
+ difference = completed_days - today
665
+ return 'on track' if difference.zero?
666
+
667
+ amount = difference.abs
668
+ "#{amount} #{amount == 1 ? 'day' : 'days'} #{difference.positive? ? 'ahead' : 'behind'}"
669
+ end
670
+
646
671
  # Raw, unclamped — lets callers distinguish "not started yet" / "finished".
647
672
  # The plan day that today actually is, or nil when today falls outside the
648
673
  # plan's window. calendar_day clamps, so before the start date it reports day
@@ -115,7 +115,8 @@
115
115
 
116
116
 
117
117
  <div class="b270-panel">
118
- <%= render "bible270/shared/reflections", comments: @comments, moderate: true %>
118
+ <%= render "bible270/shared/interaction_status" %>
119
+ <%= render "bible270/shared/reflections", comments: @comments, moderate: true, editable: true, actions: true %>
119
120
  </div>
120
121
 
121
122
  <div class="b270-panel">
@@ -172,3 +173,4 @@
172
173
  </div>
173
174
 
174
175
  <p style="margin-top:18px"><%= link_to '← All readers', admin_path %></p>
176
+ <%= render "bible270/shared/interaction_ui" %>
@@ -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,
@@ -14,6 +14,7 @@
14
14
  joined <%= Bible270.local_time(r.created_at).strftime("%b %-d %Y") %><% if @show_admin_progress %>
15
15
  · <%= link_to "#{@days_completed[r.id]} of #{Bible270::Plan::DAYS} days complete",
16
16
  admin_reader_path(r), class: "b270-admin-progress" %>
17
+ · <%= r.progress_status(completed_days: @days_completed[r.id].to_i) %>
17
18
  <% end %>
18
19
  </div>
19
20
  </div>
@@ -12,8 +12,10 @@
12
12
 
13
13
  <% if @reader %>
14
14
  <div class="b270-panel">
15
+ <%= render "bible270/shared/interaction_status" %>
15
16
  <%= render "bible270/shared/reflections", comments: @recent_comments,
16
- title: "Your recent reflections", editable: true,
17
+ title: "Your recent reflections", editable: true, conversations: true,
17
18
  empty_text: "You have not written any reflections yet." %>
18
19
  </div>
20
+ <%= render "bible270/shared/interaction_ui" %>
19
21
  <% end %>
@@ -16,8 +16,11 @@
16
16
  </div>
17
17
 
18
18
  <div class="b270-panel">
19
+ <%= render "bible270/shared/interaction_status" %>
19
20
  <%= render "bible270/shared/reflections", comments: @recent_comments,
20
- title: "Their reflections", empty_text: "No reflections yet." %>
21
+ title: "Their reflections", empty_text: "No reflections yet.",
22
+ conversations: true, editable: Bible270.config.admin?(current_reader) %>
21
23
  </div>
22
24
 
23
25
  <%= render "bible270/shared/day_index", reader: @reader, expanded: true %>
26
+ <%= render "bible270/shared/interaction_ui" %>
@@ -1,4 +1,4 @@
1
- <%# locals: (comments:, title: "Their reflections", moderate: false, editable: false, empty_text: "None yet.") %>
1
+ <%# locals: (comments:, title: "Their reflections", moderate: false, editable: false, actions: false, conversations: false, reply: false, empty_text: "None yet.") %>
2
2
  <%#
3
3
  A reader's reflections. Three views showed near-identical markup: the admin
4
4
  page, a reader's public page, and the progress page, differing only in whether
@@ -11,17 +11,21 @@
11
11
  No ERB tags in this comment: the first close-tag would end it and the rest
12
12
  would become live template code.
13
13
  %>
14
- <h2 class="b270-subhead"><%= title %></h2>
14
+ <% if title.present? %><h2 class="b270-subhead"><%= title %></h2><% end %>
15
15
 
16
16
  <% if comments.any? %>
17
17
  <% comments.each do |comment| %>
18
- <div class="b270-comment">
18
+ <div class="b270-comment <%= 'b270-reply' if reply %>" id="comment-<%= comment.id %>">
19
+ <% if reply %><%= b270_avatar(comment.reader, size: 36) %><% end %>
19
20
  <div style="flex:1">
20
21
  <div class="b270-cmeta">
22
+ <% if reply %>
23
+ <strong><%= link_to comment.reader.display_name, reader_path(comment.reader) %></strong> ·
24
+ <% end %>
21
25
  <%= link_to "Day #{comment.day}", day_path(comment.day) %>
22
26
  · <%= b270_date(comment.created_at) %>
23
27
  <% if moderate && comment.hidden? %><span class="b270-badge">Hidden</span><% end %>
24
- <% if editable && b270_can_edit?(comment) %>
28
+ <% if editable && !comment.hidden? && b270_can_edit?(comment) %>
25
29
  <%= link_to "edit", day_path(comment.day, edit: comment.id, anchor: "comment-#{comment.id}"),
26
30
  class: "b270-cedit" %>
27
31
  <% end %>
@@ -29,6 +33,27 @@
29
33
 
30
34
  <div class="b270-cbody"><%= b270_comment_body(comment) %></div>
31
35
 
36
+ <% if (actions || conversations) && !comment.hidden? %>
37
+ <div class="b270-cactions">
38
+ <%= render "bible270/likes/likes", comment: comment %>
39
+ <% if !comment.reply? && signed_in? && comment.reader_id != current_reader.id %>
40
+ <%= link_to "Reply", day_path(comment.day, reply_to: comment.id, anchor: "new_comment_form"),
41
+ class: "b270-linkbtn" %>
42
+ <% end %>
43
+ </div>
44
+ <% end %>
45
+ <% if conversations %>
46
+ <% unless comment.reply? %>
47
+ <div class="b270-replies" id="replies-<%= comment.id %>">
48
+ <% replies = comment.visible_replies %>
49
+ <% if replies.any? %>
50
+ <%= render "bible270/shared/reflections", comments: replies, title: nil,
51
+ conversations: true, reply: true, editable: editable %>
52
+ <% end %>
53
+ </div>
54
+ <% end %>
55
+ <% end %>
56
+
32
57
  <% if moderate %>
33
58
  <div style="display:flex;gap:10px;margin-top:6px">
34
59
  <% if comment.hidden? %>
@@ -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 */
@@ -239,6 +282,7 @@
239
282
  .b270-mention{color:var(--teal);font-weight:600;text-decoration:none}
240
283
  .b270-mention:hover{text-decoration:underline}
241
284
  .b270-cactions{display:flex;align-items:center;gap:12px;margin-top:6px;flex-wrap:wrap}
285
+ .b270-cactions .b270-linkbtn{margin:0}
242
286
  .b270-likes{display:inline-flex;align-items:center;gap:3px;position:relative}
243
287
  .b270-heart{font-size:13px;line-height:1;cursor:default}
244
288
  /* The single heart is also the toggle for signed-in readers. It stays red
@@ -280,8 +324,8 @@
280
324
  .b270-comment.b270-editing{background:rgba(165,129,44,.05);border-radius:10px;padding:12px}
281
325
  .b270-editing textarea{min-height:64px}
282
326
  .b270-cdel{font-size:12px;color:var(--oxblood);background:none;border:none;cursor:pointer;padding:0;margin-left:8px}
283
- .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}
284
- .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}
285
329
  .b270-form textarea[data-b270-autosize=false]{min-height:114px}
286
330
  .b270-broadcast-audience{margin:8px 0}
287
331
  .b270-editor-utilities[hidden]{display:none}
@@ -301,7 +345,7 @@
301
345
  .b270-format-bold{font-weight:700}
302
346
  .b270-format-italic{font-style:italic;font-weight:600}
303
347
  .b270-markdown-editor[data-b270-formatting-open=true] textarea{border-radius:0 0 10px 10px}
304
- .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)}
305
349
  .b270-markdown-preview[hidden]{display:none}
306
350
  .b270-markdown-preview>.b270-hint{margin:0}
307
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}
@@ -315,15 +359,16 @@
315
359
  .b270-mention-option:hover,.b270-mention-option[aria-selected=true]{background:var(--paper);color:var(--gold)}
316
360
  .b270-draft-status{min-height:16px;margin:4px 0 -4px;font-size:11.5px;color:var(--faint)}
317
361
  .b270-form .row{display:flex;align-items:center;gap:10px;margin-top:10px}
318
- .b270-form .row select{max-width:100%}
319
- .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}
320
- .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)}
321
366
 
322
367
  /* community */
323
368
  .b270-admin-reader-tools{display:flex;align-items:flex-end;justify-content:flex-end;gap:8px;margin:0 0 10px;flex-wrap:wrap}
324
369
  .b270-admin-reader-sort{display:flex;align-items:flex-end;gap:8px}
325
370
  .b270-admin-reader-sort label{display:grid;gap:3px;font-size:11.5px;color:var(--muted)}
326
- .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}
327
372
  .b270 .b270-admin-reader-sort .b270-btn,.b270 .b270-reader-stats-open{margin:0;padding:8px 13px;font-size:12.5px}
328
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)}
329
374
  .b270-reader-stats-dialog::backdrop{background:rgba(35,31,24,.55)}
@@ -332,7 +377,7 @@
332
377
  .b270-reader-stats-head .b270-subhead{margin:0}
333
378
  .b270-reader-stats-head form{margin:0}
334
379
  .b270-reader-stats-head .b270-linkbtn{margin:0;color:var(--muted)}
335
- .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}
336
381
  .b270-list{background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
337
382
  .b270-li{display:flex;align-items:center;gap:14px;padding:14px 18px}
338
383
  .b270-li+.b270-li{border-top:1px solid var(--line-soft)}
@@ -348,13 +393,13 @@
348
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}
349
394
  .b270-cell:hover{border-color:var(--gold);color:var(--gold)}
350
395
  .b270-cell.partial{background:linear-gradient(135deg,var(--card) 50%,rgba(165,129,44,.35) 50%);color:var(--muted)}
351
- .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}
352
397
  /* Days that have reflections carry a dot and nothing else: the fill stays
353
398
  free to mean reading progress, so the two facts never compete. On a finished
354
- 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. */
355
400
  .b270-cell.talk{position:relative}
356
401
  .b270-cell.talk::after{content:"";position:absolute;top:3px;right:3px;width:7px;height:7px;border-radius:50%;background:var(--oxblood)}
357
- .b270-cell.talk.complete::after{background:#f4efe6}
402
+ .b270-cell.talk.complete::after{background:var(--complete-dot)}
358
403
  .b270-cell.now{box-shadow:0 0 0 2px var(--gold)}
359
404
 
360
405
  /* On the Reflections page each thread carries the day it belongs to, since
@@ -364,7 +409,7 @@
364
409
  .b270-reflection-count{margin:3px 0 0;font-size:12px;color:var(--faint)}
365
410
  .b270-reflection-filters{display:flex;align-items:flex-end;justify-content:flex-end;gap:8px;flex-wrap:wrap}
366
411
  .b270-reflection-filters label{display:grid;gap:3px;font-size:11.5px;color:var(--muted)}
367
- .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}
368
413
  .b270 .b270-reflection-filters .b270-btn{margin:0;padding:8px 13px;font-size:12.5px}
369
414
  .b270 .b270-filter-clear{align-self:center;font-size:12px;color:var(--muted)}
370
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?
@@ -28,14 +28,14 @@ module Bible270
28
28
  Row.new(
29
29
  name: reader.display_name,
30
30
  days_read: days_read,
31
- status: status(reader, days_read),
31
+ status: reader.progress_status(completed_days: days_read),
32
32
  last_activity: last_activity_date(reader)
33
33
  )
34
34
  end
35
35
  end
36
36
 
37
37
  def status_for(reader)
38
- status(reader, completed_days.fetch(reader.id, 0))
38
+ reader.progress_status(completed_days: completed_days.fetch(reader.id, 0))
39
39
  end
40
40
 
41
41
  def empty? = rows.empty?
@@ -83,16 +83,5 @@ module Bible270
83
83
  occurred_at = recent_activities[reader.id]&.occurred_at
84
84
  occurred_at ? Bible270.local_time(occurred_at).strftime('%b %-d, %Y') : '—'
85
85
  end
86
-
87
- def status(reader, days_read)
88
- calendar_day = reader.calendar_day
89
- return 'undated' unless calendar_day
90
-
91
- difference = days_read - calendar_day
92
- return 'on track' if difference.zero?
93
-
94
- amount = difference.abs
95
- "#{amount} #{amount == 1 ? 'day' : 'days'} #{difference.positive? ? 'ahead' : 'behind'}"
96
- end
97
86
  end
98
87
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '1.8.4'
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.4
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