Address review: drop render_ prefixes, extract RelativeTime, use Pathname
- Rename the Home view partials to drop the render_ prefix; name them by
  role (topbar, recent_assets, save_panel, dock, ...) since header/main
  would otherwise shadow Phlex's element methods.
- Move relative-time formatting out of the view into a standalone,
  unit-tested Domus::RelativeTime.
- Build icon paths with Pathname instead of File, matching app.rb.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147qRctyhELxJgGs3nHizEM
change
commit 15855cdbb6f71b86203a5f07e0457c82554fa05c
author Claude <noreply@anthropic.com>
date
parent 99250d9c
diff --git a/lib/relative_time.rb b/lib/relative_time.rb
new file mode 100644
index 0000000..e169732
--- /dev/null
+++ b/lib/relative_time.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Domus
+  # Formats a timestamp as a compact, archival relative age —
+  # "now", "5m", "3h", "2d", "1w", "4mo", "2y".
+  module RelativeTime
+    module_function
+
+    def format(at, now: Time.now)
+      return "" unless at
+
+      seconds = (now - at).to_i
+      return "now" if seconds < 60
+
+      minutes = seconds / 60
+      return "#{minutes}m" if minutes < 60
+
+      hours = minutes / 60
+      return "#{hours}h" if hours < 24
+
+      days = hours / 24
+      return "#{days}d" if days < 7
+
+      weeks = days / 7
+      return "#{weeks}w" if days < 30
+
+      months = days / 30
+      return "#{months}mo" if months < 12
+
+      "#{days / 365}y"
+    end
+  end
+end
diff --git a/lib/views/home.rb b/lib/views/home.rb
index 2d0374d..5237e97 100644
--- a/lib/views/home.rb
+++ b/lib/views/home.rb
@@ -3,13 +3,14 @@
 require "phlex"
 require_relative "icons"
 require_relative "capture_form"
+require_relative "../relative_time"
 
 module Domus
   module Views
-    # The home page — the archive's front door. Capture actions live in the
-    # header (and a thumb-reachable dock on small screens) and open the
-    # picker in place: the whole page is one captureApp() Alpine component,
-    # so choosing a file swaps the recent-asset list for the save form.
+    # The home page — the archive's front door. Capture actions live in a
+    # thumb-reachable dock and open the picker in place: the whole page is one
+    # captureApp() Alpine component, so choosing a file swaps the recent-asset
+    # list for the save form.
     class Home < Phlex::HTML
       include Icons
 
@@ -31,10 +32,10 @@ module Domus
           end
           body do
             div(class: "page", "x-data": "captureApp()") do
-              render_header
-              render_main
-              render_capture
-              render_dock
+              topbar
+              recent_assets
+              save_panel
+              dock
             end
           end
         end
@@ -42,7 +43,7 @@ module Domus
 
       private
 
-      def render_header
+      def topbar
         header(class: "topbar") do
           a(href: "/", class: "logo") do
             span(class: "logo-mark")
@@ -55,7 +56,7 @@ module Domus
       # captureApp() chooses for the viewport (upload on desktop, camera on
       # mobile), and the caret opens a drop-up menu with the alternate.
       # Both labels are rendered and toggled by media query.
-      def render_capture_control
+      def capture_control
         div(class: "capture", "@click.outside": "menuOpen = false") do
           div(class: "split") do
             button(
@@ -97,7 +98,7 @@ module Domus
         end
       end
 
-      def render_main
+      def recent_assets
         main(class: "wrap", "x-show": "state === 'capture'") do
           section do
             div(class: "sec-h") do
@@ -105,15 +106,15 @@ module Domus
               span(class: "sub") { plain "#{@total} tracked" } if @total.positive?
             end
             if @assets.empty?
-              render_empty
+              empty_state
             else
-              render_archive
+              archive
             end
           end
         end
       end
 
-      def render_archive
+      def archive
         div(class: "archive") do
           @assets.each do |asset|
             div(class: "entry") do
@@ -121,20 +122,20 @@ module Domus
               div(class: "body") do
                 div(class: "nm") { plain asset[:name] }
               end
-              span(class: "time") { plain relative_time(asset[:created_at]) }
+              span(class: "time") { plain RelativeTime.format(asset[:created_at]) }
             end
           end
         end
       end
 
-      def render_empty
+      def empty_state
         div(class: "archive empty") do
           p(class: "empty-line") { plain "Nothing tracked yet." }
         end
       end
 
       # The capture save step, shown once a photo or file has been chosen.
-      def render_capture
+      def save_panel
         div(class: "content", "x-show": "state === 'saved'", "x-cloak": true) do
           div(class: "card") do
             render CaptureForm.new
@@ -145,36 +146,11 @@ module Domus
       # The capture front door: a dock fixed to the bottom of the viewport on
       # every screen, so the primary action stays reachable and out of the
       # header.
-      def render_dock
+      def dock
         div(class: "dock", "x-show": "state === 'capture'") do
-          render_capture_control
+          capture_control
         end
       end
-
-      # Compact, archival relative time — "now", "5m", "3h", "2d", "1w", "4mo".
-      def relative_time(at)
-        return "" unless at
-
-        seconds = (Time.now - at).to_i
-        return "now" if seconds < 60
-
-        minutes = seconds / 60
-        return "#{minutes}m" if minutes < 60
-
-        hours = minutes / 60
-        return "#{hours}h" if hours < 24
-
-        days = hours / 24
-        return "#{days}d" if days < 7
-
-        weeks = days / 7
-        return "#{weeks}w" if days < 30
-
-        months = days / 30
-        return "#{months}mo" if months < 12
-
-        "#{days / 365}y"
-      end
     end
   end
 end
diff --git a/lib/views/icons.rb b/lib/views/icons.rb
index 7d86983..35636e5 100644
--- a/lib/views/icons.rb
+++ b/lib/views/icons.rb
@@ -1,15 +1,17 @@
 # frozen_string_literal: true
 
+require "pathname"
+
 module Domus
   module Views
     # Inlines the SVG files from public/icons so they inherit currentColor
     # and can be sized and recoloured with CSS. Mixed into the Phlex views
     # that draw icons.
     module Icons
-      ICONS_DIR = File.expand_path("../../public/icons", __dir__)
+      ICONS_DIR = Pathname(__dir__).join("../../public/icons").expand_path
 
       ICONS = Hash.new do |cache, name|
-        cache[name] = File.read(File.join(ICONS_DIR, "#{name}.svg")).freeze
+        cache[name] = ICONS_DIR.join("#{name}.svg").read.freeze
       end
 
       def icon(name)
diff --git a/test/test_relative_time.rb b/test/test_relative_time.rb
new file mode 100644
index 0000000..729434c
--- /dev/null
+++ b/test/test_relative_time.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+require_relative "../lib/relative_time"
+
+class TestRelativeTime < Minitest::Test
+  def fmt(seconds_ago)
+    now = Time.now
+    Domus::RelativeTime.format(now - seconds_ago, now: now)
+  end
+
+  def test_blank_for_nil
+    assert_equal "", Domus::RelativeTime.format(nil)
+  end
+
+  def test_now_under_a_minute
+    assert_equal "now", fmt(30)
+  end
+
+  def test_minutes
+    assert_equal "5m", fmt(5 * 60)
+  end
+
+  def test_hours
+    assert_equal "3h", fmt(3 * 60 * 60)
+  end
+
+  def test_days
+    assert_equal "2d", fmt(2 * 86_400)
+  end
+
+  def test_weeks
+    assert_equal "1w", fmt(8 * 86_400)
+  end
+
+  def test_months
+    assert_equal "2mo", fmt(70 * 86_400)
+  end
+
+  def test_years
+    assert_equal "1y", fmt(400 * 86_400)
+  end
+end