shoreditch 1.1.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.
@@ -0,0 +1,25 @@
1
+ <section class="sd-comments">
2
+ <h2>Comments</h2>
3
+
4
+ <%# data-theme follows the OS preference on load; shoreditch.js re-syncs it
5
+ through Giscus's postMessage API when the reader uses the theme toggle,
6
+ so the comment thread does not stay light on a dark page. %>
7
+ <script
8
+ src="https://giscus.app/client.js"
9
+ data-repo="<%= repo %>"
10
+ data-repo-id="<%= repo_id %>"
11
+ data-category="<%= category %>"
12
+ data-category-id="<%= category_id %>"
13
+ data-mapping="pathname"
14
+ data-strict="1"
15
+ data-reactions-enabled="1"
16
+ data-emit-metadata="0"
17
+ data-input-position="bottom"
18
+ data-theme="preferred_color_scheme"
19
+ data-lang="en"
20
+ crossorigin="anonymous"
21
+ async
22
+ ></script>
23
+
24
+ <noscript>Comments need JavaScript. They are also readable as GitHub Discussions on the repository.</noscript>
25
+ </section>
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The comments slot.
4
+ #
5
+ # The theme ships Giscus (comments stored as GitHub Discussions) as the one
6
+ # provider it knows how to configure, but nothing here is Giscus-specific from
7
+ # a consuming site's point of view: a site that wants something else overrides
8
+ # this component by putting its own `components/shoreditch/comments.erb` in
9
+ # place. Site files take precedence over the gem's source manifest, so no fork
10
+ # and no layout copy is needed.
11
+ #
12
+ # Off unless configured. Set in config/initializers.rb:
13
+ #
14
+ # init :shoreditch do
15
+ # comments({
16
+ # repo: "you/your-repo", repo_id: "R_...",
17
+ # category: "Comments", category_id: "DIC_...",
18
+ # })
19
+ # end
20
+ #
21
+ # The four values come from https://giscus.app after enabling Discussions on
22
+ # the repository. A single post opts out with `comments: false` in front matter.
23
+ class Shoreditch::Comments < Bridgetown::Component
24
+ def initialize(site:, resource:)
25
+ @site = site
26
+ @resource = resource
27
+ end
28
+
29
+ attr_reader :site, :resource
30
+
31
+ def render?
32
+ resource.data.comments != false && settings.is_a?(Hash) && required_keys_present?
33
+ end
34
+
35
+ def settings
36
+ site.config.shoreditch[:comments]
37
+ end
38
+
39
+ # Giscus silently renders nothing if any of these are missing, which is a
40
+ # miserable thing to debug, so treat a partial config as "off".
41
+ def required_keys_present?
42
+ %i[repo repo_id category category_id].all? { |key| settings[key].to_s.strip != "" }
43
+ end
44
+
45
+ def repo = settings[:repo].to_s
46
+ def repo_id = settings[:repo_id].to_s
47
+ def category = settings[:category].to_s
48
+ def category_id = settings[:category_id].to_s
49
+ end
@@ -0,0 +1,52 @@
1
+ <ul class="sd-details">
2
+ <% links.each do |key, icon_name, href, text| %>
3
+ <li data-network="<%= key %>">
4
+ <a href="<%= href %>" rel="me noopener">
5
+ <svg class="sd-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
6
+ <path d="<%= icon(icon_name) %>" />
7
+ </svg>
8
+ <%= text %>
9
+ </a>
10
+ </li>
11
+ <% end %>
12
+
13
+ <% if phone != "" %>
14
+ <li data-network="phone">
15
+ <a href="tel:<%= phone.gsub(/[^0-9+]/, "") %>">
16
+ <svg class="sd-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
17
+ <path d="<%= icon(:phone) %>" />
18
+ </svg>
19
+ <%= phone %>
20
+ </a>
21
+ </li>
22
+ <% end %>
23
+
24
+ <% if email? %>
25
+ <%# The two halves are only joined in the browser — see shoreditch.js. %>
26
+ <li data-network="email">
27
+ <a
28
+ class="sd-email"
29
+ href="#"
30
+ hidden
31
+ data-sd-email-user="<%= email_parts.first %>"
32
+ data-sd-email-host="<%= email_parts.last %>"
33
+ >
34
+ <svg class="sd-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
35
+ <path d="<%= icon(:email) %>" />
36
+ </svg>
37
+ Email
38
+ </a>
39
+ </li>
40
+ <% end %>
41
+
42
+ <% if file_url != "" %>
43
+ <li data-network="file">
44
+ <a href="<%= file_url %>" rel="noopener">
45
+ <svg class="sd-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
46
+ <path d="<%= icon(:file) %>" />
47
+ </svg>
48
+ <%= file_link_text %>
49
+ </a>
50
+ </li>
51
+ <% end %>
52
+ </ul>
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The contact panel: how to reach the author, and where else they are.
4
+ #
5
+ # Restored from the Jekyll theme's `_includes/details.html`, with two changes.
6
+ # Icons are inlined from the vendored Simple Icons set rather than pulled from
7
+ # a Font Awesome CDN kit on every page load, and the markup is a definition of
8
+ # links rather than a list of hard-coded network blocks, so adding a network is
9
+ # one entry in ENTRIES.
10
+ #
11
+ # Everything is driven by `author` in src/_data/site_metadata.yml. A page opts
12
+ # in with `include_details: true` in its front matter — off elsewhere, since a
13
+ # blog post has no use for a phone number.
14
+ class Shoreditch::Details < Bridgetown::Component
15
+ # network key => [icon, url template, label]. `%s` takes the handle.
16
+ ENTRIES = [
17
+ [:github, :github, "https://github.com/%s", "%s"],
18
+ [:mastodon, :mastodon, "%s", "Mastodon"],
19
+ [:linkedin, :link, "https://linkedin.com/in/%s", "%s"],
20
+ [:twitter, :x, "https://x.com/%s", "@%s"],
21
+ [:instagram, :instagram, "https://instagram.com/%s", "%s"],
22
+ [:reddit, :reddit, "https://reddit.com/user/%s", "%s"],
23
+ [:youtube, :youtube, "%s", "YouTube"],
24
+ [:tiktok, :tiktok, "https://tiktok.com/@%s", "%s"],
25
+ [:lastfm, :lastfm, "https://last.fm/user/%s", "%s"],
26
+ [:deviantart, :deviantart, "https://deviantart.com/%s", "%s"],
27
+ [:artstation, :artstation, "https://artstation.com/%s", "%s"],
28
+ ].freeze
29
+
30
+ def initialize(site:, resource:)
31
+ @site = site
32
+ @resource = resource
33
+ end
34
+
35
+ attr_reader :site, :resource
36
+
37
+ # Not `author.present?`: site.metadata is a HashWithDotAccess::Hash, which
38
+ # routes unknown methods to key lookups, so `present?` returns nil for a
39
+ # missing "present" key rather than calling ActiveSupport. Only methods Hash
40
+ # genuinely defines are safe to call on it.
41
+ def render?
42
+ resource.data.include_details == true && author.is_a?(Hash) && !author.empty?
43
+ end
44
+
45
+ def author
46
+ site.metadata.author
47
+ end
48
+
49
+ # [key, icon, href, text] for each network the site actually filled in. The
50
+ # key and the icon are separate because they diverge: LinkedIn is styled as
51
+ # linkedin but drawn with the generic `link` mark.
52
+ def links
53
+ ENTRIES.filter_map do |key, icon, url, label|
54
+ handle = author[key.to_s].to_s.strip
55
+ next if handle.empty?
56
+
57
+ [key, icon, format(url, handle), format(label, handle)]
58
+ end
59
+ end
60
+
61
+ def phone = author["phone"].to_s.strip
62
+
63
+ def file_url = author["file_url"].to_s.strip
64
+
65
+ def file_link_text
66
+ text = author["file_link_text"].to_s.strip
67
+ text.empty? ? "Download" : text
68
+ end
69
+
70
+ # Kept split across two metadata keys, as the Jekyll version had it, so the
71
+ # address never appears whole in the HTML for a crawler to lift. Assembled by
72
+ # shoreditch.js. Without JavaScript the address is simply not shown, which is
73
+ # the trade the original made too.
74
+ def email_parts
75
+ [author["email_1"].to_s.strip, author["email_2"].to_s.strip]
76
+ end
77
+
78
+ def email? = email_parts.none?(&:empty?)
79
+
80
+ def icon(name)
81
+ Shoreditch::ICONS[name]
82
+ end
83
+ end
@@ -0,0 +1,12 @@
1
+ <% if favicon %>
2
+ <link rel="shortcut icon" href="<%= favicon %>" />
3
+ <% end %>
4
+ <% apple_icons.each do |size, path| %>
5
+ <link rel="apple-touch-icon" sizes="<%= size %>" href="<%= path %>" />
6
+ <% end %>
7
+ <% png_icons.each do |size, path| %>
8
+ <link rel="icon" type="image/png" sizes="<%= size %>" href="<%= path %>" />
9
+ <% end %>
10
+ <% if manifest %>
11
+ <link rel="manifest" href="<%= manifest %>" />
12
+ <% end %>
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Favicon and touch-icon links.
4
+ #
5
+ # Only emitted for files the site actually has. The Jekyll theme hard-coded the
6
+ # full apple-touch-icon set, which meant every site using the theme requested a
7
+ # dozen icons whether or not they existed.
8
+ class Shoreditch::HeadIcons < Bridgetown::Component
9
+ APPLE_SIZES = %w[57x57 60x60 72x72 76x76 114x114 120x120 144x144 152x152 180x180].freeze
10
+ PNG_SIZES = %w[16x16 32x32 96x96 192x192].freeze
11
+
12
+ def initialize(site:, prefix: "")
13
+ @site = site
14
+ @prefix = prefix
15
+ end
16
+
17
+ def apple_icons
18
+ APPLE_SIZES.filter_map do |size|
19
+ path = "#{@prefix}/apple-icon-#{size}.png"
20
+ [size, path] if exists?(path)
21
+ end
22
+ end
23
+
24
+ def png_icons
25
+ PNG_SIZES.filter_map do |size|
26
+ # The 192px icon is named for Android; the rest are plain favicons.
27
+ name = size == "192x192" ? "android-icon-192x192" : "favicon-#{size}"
28
+ path = "#{@prefix}/#{name}.png"
29
+ [size, path] if exists?(path)
30
+ end
31
+ end
32
+
33
+ def favicon
34
+ path = "#{@prefix}/favicon.ico"
35
+ path if exists?(path)
36
+ end
37
+
38
+ def manifest
39
+ path = "#{@prefix}/manifest.json"
40
+ path if exists?(path)
41
+ end
42
+
43
+ private
44
+
45
+ def exists?(path)
46
+ File.exist?(File.join(@site.config.source, path))
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ <article class="sd-post-summary">
2
+ <% if thumbnail %>
3
+ <a class="sd-post-thumbnail" href="<%= post.relative_url %>" tabindex="-1" aria-hidden="true">
4
+ <img src="<%= thumbnail %>" alt="" loading="lazy" />
5
+ </a>
6
+ <% end %>
7
+
8
+ <div class="sd-post-detail">
9
+ <p class="sd-post-dates">
10
+ <time datetime="<%= date_attr %>"><%= date_text %></time>
11
+ </p>
12
+ <h2><a href="<%= post.relative_url %>"><%= post.data.title %></a></h2>
13
+ <%# Wrapped so the excerpt can be clamped and flattened as a unit: the
14
+ summary is whatever block the post opens with, which may be a
15
+ blockquote or a {:.message}, and an index preview has to read the same
16
+ whichever it is. %>
17
+ <div class="sd-post-excerpt"><%= summary %></div>
18
+ </div>
19
+ </article>
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # One entry in an index listing — used by both the home page and tag pages.
4
+ class Shoreditch::PostSummary < Bridgetown::Component
5
+ def initialize(post:)
6
+ @post = post
7
+ end
8
+
9
+ attr_reader :post
10
+
11
+ def thumbnail
12
+ post.data.thumbnail
13
+ end
14
+
15
+ def date_text
16
+ post.date.strftime("%-d %B %Y")
17
+ end
18
+
19
+ def date_attr
20
+ post.date.strftime("%Y-%m-%d")
21
+ end
22
+
23
+ # Bridgetown's own summary is the first *source line*. The demo's markdown is
24
+ # hard-wrapped at 80 columns, so that cut three of four excerpts off
25
+ # mid-clause and left the paragraph unclosed. An explicit <!--more--> marker
26
+ # wins; otherwise take the first complete paragraph.
27
+ def summary
28
+ html = post.content.to_s
29
+ excerpt =
30
+ if (marker = html.index("<!--more-->"))
31
+ html[0...marker]
32
+ else
33
+ html[%r{\A.*?</p>}m] || html
34
+ end
35
+ excerpt.strip.html_safe
36
+ end
37
+ end
@@ -0,0 +1,64 @@
1
+ <aside class="sd-sidebar">
2
+ <div class="sd-logo-area">
3
+ <% if logo_source %>
4
+ <a href="<%= site.metadata.logo_link || "/" %>">
5
+ <img
6
+ class="sd-logo"
7
+ data-shape="<%= logo_shape %>"
8
+ data-flashy="<%= flashy_logo? %>"
9
+ src="<%= logo_source %>"
10
+ alt="<%= site.metadata.title %>"
11
+ />
12
+ </a>
13
+ <% end %>
14
+
15
+ <% if logo_legend %>
16
+ <span class="sd-logo-legend" data-shape="<%= logo_legend_shape %>"><%= logo_legend %></span>
17
+ <% end %>
18
+
19
+ <%# `include_sticky: false` in front matter drops the site identity and the
20
+ navigation, leaving the logo and badge to introduce a person instead. %>
21
+ <% if sticky? %>
22
+ <div class="sd-site-identity">
23
+ <p class="sd-site-title"><a href="/"><%= site.metadata.title %></a></p>
24
+ <% if site.metadata.tagline %>
25
+ <p class="sd-site-tagline"><%= site.metadata.tagline %></p>
26
+ <% end %>
27
+ </div>
28
+ <% end %>
29
+ </div>
30
+
31
+ <%# The contact panel belongs with the identity above it, not with the
32
+ navigation — on a CV the logo, name and contact details read as one block. %>
33
+ <%= render Shoreditch::Details.new(site: site, resource: resource) %>
34
+
35
+ <%# Everything from here sits at the foot of the column: the identity stays
36
+ at the top where the eye starts, the menu and the footer drop to the
37
+ bottom. One auto margin on this wrapper does it, which is why the nav and
38
+ footer are grouped rather than each pushed down separately — two auto
39
+ margins would split the free space and strand the nav in the middle. %>
40
+ <div class="sd-sidebar-bottom">
41
+ <% if sticky? && nav_pages.any? %>
42
+ <nav class="sd-nav" aria-label="Main">
43
+ <% nav_pages.each do |page| %>
44
+ <%# The attribute is always present and only its value varies, because
45
+ ERB escapes interpolated output — injecting the whole attribute
46
+ rendered it as aria-current=&quot;page&quot; and the
47
+ a[aria-current="page"] rule never matched. "false" is the value
48
+ ARIA defines for "not the current item". %>
49
+ <a
50
+ href="<%= page.relative_url %>"
51
+ aria-current="<%= current?(page) ? "page" : "false" %>"
52
+ ><%= page.data.title %></a>
53
+ <% end %>
54
+ </nav>
55
+ <% end %>
56
+
57
+ <div class="sd-sidebar-footer">
58
+ <button class="sd-theme-toggle" type="button" data-shoreditch-theme-toggle>
59
+ Switch theme
60
+ </button>
61
+ <p>&copy; <%= Time.now.year %> <%= site.metadata.author&.name || site.metadata.title %></p>
62
+ </div>
63
+ </div>
64
+ </aside>
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The fixed left column: logo, site identity, navigation and footer.
4
+ #
5
+ # Navigation is drawn from pages that opt in with `nav_order` in their front
6
+ # matter, so a site controls the menu from its content rather than from theme
7
+ # configuration. Pages with `exclude: true` never appear.
8
+ class Shoreditch::Sidebar < Bridgetown::Component
9
+ def initialize(site:, resource:)
10
+ @site = site
11
+ @resource = resource
12
+ end
13
+
14
+ # Components get no implicit access to the site, so it is passed in and
15
+ # exposed for the template.
16
+ attr_reader :site
17
+
18
+ def options
19
+ @site.config.shoreditch
20
+ end
21
+
22
+ def nav_pages
23
+ @site.collections.pages.resources
24
+ .reject { |page| page.data.exclude }
25
+ .select { |page| page.data.nav_order }
26
+ .sort_by { |page| [page.data.nav_order, page.data.title.to_s] }
27
+ end
28
+
29
+ def current?(page)
30
+ page.relative_url == @resource.relative_url
31
+ end
32
+
33
+ attr_reader :resource
34
+
35
+ def logo_source
36
+ return nil if @resource.data.include_logo == false
37
+
38
+ @resource.data.logo_location || @site.metadata.logo
39
+ end
40
+
41
+ # `include_sticky: false` drops the site title, tagline and navigation. The
42
+ # CV page used it so its sidebar introduced a person rather than a site.
43
+ def sticky?
44
+ @resource.data.include_sticky != false
45
+ end
46
+
47
+ # A page can override the logo's shape, as the Jekyll version allowed.
48
+ def logo_shape
49
+ @resource.data.logo_shape || options[:logo_shape]
50
+ end
51
+
52
+ # "round" or "straight" — the badge under the logo, shaped independently.
53
+ def logo_legend_shape
54
+ @resource.data.logo_legend_shape || options[:logo_legend_shape] || "round"
55
+ end
56
+
57
+ def flashy_logo?
58
+ @resource.data.flashy_logo == true
59
+ end
60
+
61
+ def logo_legend
62
+ # A page can override the badge, or suppress it by setting it to false.
63
+ return @resource.data.logo_legend if @resource.data.key?("logo_legend")
64
+
65
+ options[:logo_legend]
66
+ end
67
+ end