Module: Studio::AtTimeHelper
- Defined in:
- app/helpers/studio/at_time_helper.rb
Overview
The "at" format — the shared primitive for stamping WHEN something happened.
A relative stamp ("Shipped less than a minute ago") answers how long ago and then stops. It never tells the reader what the clock said, so it cannot be placed against the rest of a day. The "at" format answers the other question, and it answers it in the reader's own time:
· The clock is 12-hour with a single-letter meridiem — "3:53p".
· The date appears ONLY when the stamp is not today. Something that happened
minutes ago needs no date; last week's is ambiguous without it. The year
joins only when it differs, so "Aug 10, 3:53p" stays short all year.
· A country flag TRAILS the clock when the reader's timezone sits outside the
US. Inside the US there is no flag at all — the flag carries signal only
because it is unusual, and one that fired on every stamp would carry none.
· The relative phrase is not thrown away; it moves to the hover title, beside
the full local stamp and its zone name.
WHO OWNS WHAT. The server renders the app-timezone form as the pre-hydration
and no-JS fallback, and never renders a flag — it cannot know where the reader
is sitting. studio/_at_time_script re-stamps the clock to the VIEWER's local
time and adds the flag from the browser's IANA zone. That split is the whole
design: the flag is a fact about the reader, so only the reader's machine may
assert it.
HOSTS: render studio/at_time_script ONCE per page, near the END of the layout
body (its first pass runs synchronously, so rendering it in head finds zero
stamps), then use at_time_tag anywhere. Recipe in the README's UI Primitives
section. Without the script the stamps still render — in the app's timezone,
with no flag — so a host that forgets it degrades to the old behavior rather
than breaking. Specimen: /admin/style → Tricks → Time stamps.
Constant Summary collapse
- MONTH_DAY =
"%b %-d"
Instance Method Summary collapse
-
#at_clock(time) ⇒ Object
"3:53p" / "11:07a" — 12-hour clock, no leading zero, single-letter meridiem, no space.
-
#at_date(time, now: Time.current) ⇒ Object
The date half of an "at" stamp, or nil when the stamp falls on
now's date — today's stamps carry no date. -
#at_stamp_text(time, now: Time.current) ⇒ Object
The visible text of an "at" stamp: "3:53p" today, "Aug 10, 3:53p" otherwise.
-
#at_stamp_title(time, now: Time.current) ⇒ Object
The hover title: the relative phrase this format replaced, then the full local stamp with its zone.
-
#at_time_tag(time, prefix: "at", now: Time.current, css_class: nil) ⇒ Object
The primitive itself.
Instance Method Details
#at_clock(time) ⇒ Object
"3:53p" / "11:07a" — 12-hour clock, no leading zero, single-letter meridiem, no space. Zones the time into the app zone first, so a bare UTC timestamp and an already-zoned one render the same. nil-safe.
40 41 42 43 44 45 |
# File 'app/helpers/studio/at_time_helper.rb', line 40 def at_clock(time) return nil if time.blank? local = time.in_time_zone "#{local.strftime('%-l:%M')}#{local.hour < 12 ? 'a' : 'p'}" end |
#at_date(time, now: Time.current) ⇒ Object
The date half of an "at" stamp, or nil when the stamp falls on now's date —
today's stamps carry no date. "Aug 10" within the current year, "Aug 10 2025"
outside it. now is injectable so the boundary is testable.
50 51 52 53 54 55 56 57 58 59 60 |
# File 'app/helpers/studio/at_time_helper.rb', line 50 def at_date(time, now: Time.current) return nil if time.blank? local = time.in_time_zone today = now.in_time_zone return nil if local.to_date == today.to_date return local.strftime(MONTH_DAY) if local.year == today.year "#{local.strftime(MONTH_DAY)} #{local.year}" end |
#at_stamp_text(time, now: Time.current) ⇒ Object
The visible text of an "at" stamp: "3:53p" today, "Aug 10, 3:53p" otherwise. The JS half builds the identical string from the viewer's clock — change one and change the other, or the value flickers on hydration.
65 66 67 68 69 70 |
# File 'app/helpers/studio/at_time_helper.rb', line 65 def at_stamp_text(time, now: Time.current) return nil if time.blank? date = at_date(time, now: now) date ? "#{date}, #{at_clock(time)}" : at_clock(time) end |
#at_stamp_title(time, now: Time.current) ⇒ Object
The hover title: the relative phrase this format replaced, then the full local stamp with its zone. Server-side that zone is the app's; the script rewrites the whole title in the viewer's.
now is measured against, not decoration: time_ago_in_words reads the real
clock, so a caller that injected now used to get a title from a DIFFERENT
moment than the label right beside it.
79 80 81 82 83 84 |
# File 'app/helpers/studio/at_time_helper.rb', line 79 def at_stamp_title(time, now: Time.current) return nil if time.blank? local = time.in_time_zone "#{distance_of_time_in_words(local, now)} ago · #{local.strftime('%a, %b %-d, %Y, %-l:%M %p %Z')}" end |
#at_time_tag(time, prefix: "at", now: Time.current, css_class: nil) ⇒ Object
The primitive itself. Renders a
at_time_tag(release.shipped_at) => "at 3:53p"
at_time_tag(task.created_at, prefix: nil) => "3:53p"
nil for a blank time, so a caller can <%= at_time_tag(t) %> unguarded.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'app/helpers/studio/at_time_helper.rb', line 93 def at_time_tag(time, prefix: "at", now: Time.current, css_class: nil) return nil if time.blank? # One reading of "no prefix" for BOTH halves. `.presence` folds nil, "" and # false to nil, and the data attribute is then omitted rather than carrying a # stringified value — `prefix: false` used to render bare server-side and # hydrate to "false 3:53p", because the client trusts the attribute it is given. prefix = prefix.presence local = time.in_time_zone text = at_stamp_text(local, now: now) text = "#{prefix} #{text}" if prefix tag.time(datetime: local.iso8601, title: at_stamp_title(local, now: now), class: ["whitespace-nowrap", css_class].compact.join(" "), data: { at_stamp: "", at_epoch: local.to_i, at_prefix: prefix }) do # No whitespace between the two slots: the gap is `ml-2` on the flag, which # collapses with the flag itself when the reader is inside the US. safe_join([ tag.span(text, data: { at_text: "" }), tag.span("", class: "ml-2", hidden: true, data: { at_flag: "" }) ]) end end |