willamette 0.5.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +37 -0
  3. data/.rubocop.yml +54 -0
  4. data/CHANGELOG.md +14 -0
  5. data/CODE_OF_CONDUCT.md +92 -0
  6. data/Gemfile +15 -0
  7. data/Gemfile.lock +250 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +3 -0
  10. data/Rakefile +11 -0
  11. data/automations/components.automation.rb +123 -0
  12. data/automations/frontend.automation.rb +30 -0
  13. data/automations/layouts.automation.rb +122 -0
  14. data/bridgetown.automation.rb +7 -0
  15. data/components/willamette/back_to_top.css +35 -0
  16. data/components/willamette/back_to_top.js +32 -0
  17. data/components/willamette/code_element.css +10 -0
  18. data/components/willamette/code_element.js +49 -0
  19. data/components/willamette/header_navbar.dsd.css +19 -0
  20. data/components/willamette/header_navbar.rb +20 -0
  21. data/components/willamette/holy_grail_layout.dsd.css +81 -0
  22. data/components/willamette/holy_grail_layout.rb +17 -0
  23. data/components/willamette/pagination.erb +12 -0
  24. data/components/willamette/pagination.rb +7 -0
  25. data/components/willamette/post_item.css +102 -0
  26. data/components/willamette/post_item.rb +90 -0
  27. data/components/willamette/previous_next.erb +12 -0
  28. data/components/willamette/previous_next.rb +7 -0
  29. data/components/willamette/search_dialog.rb +18 -0
  30. data/components/willamette/search_dialog_element.js +90 -0
  31. data/content/search.erb +29 -0
  32. data/content/willamette/style-guide.md +93 -0
  33. data/layouts/willamette/default.erb +44 -0
  34. data/lib/willamette/builders/inspectors.rb +28 -0
  35. data/lib/willamette/builders/toc.rb +11 -0
  36. data/lib/willamette/locales/en.yml +22 -0
  37. data/lib/willamette/strategies/link.rb +20 -0
  38. data/lib/willamette/strategies/sidebar.rb +83 -0
  39. data/lib/willamette/version.rb +5 -0
  40. data/lib/willamette.rb +83 -0
  41. data/package-lock.json +303 -0
  42. data/package.json +22 -0
  43. data/setup.automation.rb +14 -0
  44. data/willamette.gemspec +30 -0
  45. metadata +143 -0
@@ -0,0 +1,90 @@
1
+ const { default: _default } = require("hotkeys-js")
2
+
3
+ class SearchDialogElement extends HTMLElement {
4
+ static {
5
+ customElements.define("wll-search-dialog", this)
6
+ }
7
+
8
+ connectedCallback() {
9
+ setTimeout(() => {
10
+ this.querySelector(":scope > wa-dialog").addEventListener("wa-show", this)
11
+ this.querySelector(":scope > wa-dialog").addEventListener("wa-after-hide", this)
12
+ })
13
+ }
14
+
15
+ handleEvent(event) {
16
+ if (event.type === "wa-show") {
17
+ this.handleShow()
18
+ } else if (event.type == "wa-after-hide") {
19
+ this.handleHide()
20
+ }
21
+ }
22
+
23
+ async handleShow() {
24
+ const html = await (await fetch("/search")).text()
25
+ this.querySelector("wll-dialog-inner").replaceWith(Document.parseHTMLUnsafe(html).querySelector("wll-search-page"))
26
+ setTimeout(() => this.querySelector("wa-input").focus()) // queue task to focus, otherwise control isn't loaded or something
27
+ const source = this.querySelector("script").textContent
28
+ this.querySelector("script").remove()
29
+ const newScript = document.createElement("script")
30
+ newScript.type = "module"
31
+ newScript.textContent = source
32
+ document.body.append(newScript)
33
+ }
34
+
35
+ handleHide() {
36
+ this.querySelector("wll-search-page").replaceWith(document.createElement("wll-dialog-inner"))
37
+ }
38
+ }
39
+
40
+ customElements.define("wll-search-page", class extends HTMLElement {
41
+ connectedCallback() {
42
+ this.addEventListener("input", this)
43
+ this.addEventListener("keydown", (event) => {
44
+ if (event.key === "ArrowDown") {
45
+ if (document.activeElement.localName === "wa-input") {
46
+ event.preventDefault()
47
+ this.querySelector("a")?.focus()
48
+ } else if (document.activeElement.localName === "a") {
49
+ event.preventDefault()
50
+ document.activeElement.closest("li").nextElementSibling?.querySelector("a")?.focus()
51
+ }
52
+ } else if (event.key === "ArrowUp") {
53
+ if (document.activeElement.localName === "a") {
54
+ event.preventDefault()
55
+ const previousItem = document.activeElement.closest("li").previousElementSibling
56
+ if (previousItem) {
57
+ previousItem.querySelector("a").focus()
58
+ } else {
59
+ this.querySelector("wa-input").focus()
60
+ }
61
+ }
62
+ }
63
+ })
64
+ this.tmpl = this.querySelector("template#search-result-template")
65
+ }
66
+
67
+ async handleEvent(event) {
68
+ console.log(event.target.value)
69
+ const resultsList = this.querySelector("wll-search-results > ul")
70
+ const search = await pagefind.debouncedSearch(event.target.value)
71
+ if (search === null) {
72
+ // a more recent search call has been made, nothing to do
73
+ } else {
74
+ resultsList.replaceChildren()
75
+ if (search.results.length > 0) {
76
+ search.results.forEach(async (result) => {
77
+ const data = await result.data()
78
+ const tmplInstance = this.tmpl.content.cloneNode(true)
79
+ tmplInstance.querySelector("[data-title]").innerHTML = `<a href="${data.url}">${data.meta.title}</a>`
80
+ tmplInstance.querySelector("[data-excerpt").innerHTML = data.excerpt
81
+ this.querySelector("wll-search-results > ul").append(tmplInstance)
82
+ })
83
+ } else {
84
+ resultsList.setHTMLUnsafe(`
85
+ <li><p><wa-icon name="search"></wa-icon> <em>No results could be found.</em></p></li>
86
+ `)
87
+ }
88
+ }
89
+ }
90
+ })
@@ -0,0 +1,29 @@
1
+ ---
2
+ layout: page
3
+ title: Search
4
+ ---
5
+
6
+ <wll-search-page>
7
+ <!-- input type="search" / -->
8
+ <wa-input size="large" placeholder="<%= t("labels.search") %>…" pill>
9
+ <wa-icon name="search" slot="start"></wa-icon>
10
+ </wa-input>
11
+
12
+ <wll-search-results>
13
+ <ul>
14
+ </ul>
15
+ </wll-search-results>
16
+
17
+ <template id="search-result-template">
18
+ <li>
19
+ <p data-title></p>
20
+ <p data-excerpt></p>
21
+ </li>
22
+ </template>
23
+
24
+ <script type="module">
25
+ const pagefind = await import("/pagefind/pagefind.js")
26
+ pagefind.init()
27
+ window.pagefind = pagefind
28
+ </script>
29
+ </wll-search-page>
@@ -0,0 +1,93 @@
1
+ ---
2
+ layout: page
3
+ title: This is the Willamette Theme
4
+ ---
5
+
6
+ <style>
7
+ .swatch {
8
+ wa-copy-button::part(button) {
9
+ background: var(--color);
10
+ border-radius: var(--wa-border-radius-m);
11
+ padding: 1rem;
12
+ }
13
+ }
14
+ </style>
15
+
16
+ <wa-callout variant="brand">
17
+ <wa-icon slot="icon" name="circle-check" variant="regular"></wa-icon>
18
+ Welcome to your new home on the Internet!
19
+ </wa-callout>
20
+
21
+ <hr />
22
+
23
+ <wa-color-picker label="Chose your brand color:" size="large" oninput='
24
+ document.documentElement.style.setProperty("--wll-color-brand", event.target.value)
25
+ this.querySelector("output").textContent = `--wll-color-brand: ${event.target.value};`
26
+ this.querySelector("wa-copy-button").slot = "hint"
27
+ '>
28
+ <output id="color-hint" slot="hint"></output>
29
+ <wa-copy-button from="color-hint"></wa-copy-button>
30
+ </wa-color-picker>
31
+
32
+ <hr />
33
+
34
+ ## Brand Color Scale
35
+
36
+ <ul class="color-group" style="display: flex; flex-wrap: wrap; list-style: none">
37
+ <li class="color-preview">
38
+ <div class="color swatch" style="--color: var(--wa-color-brand-95)">
39
+ <wa-copy-button value="--wa-color-brand-95" copy-label="--wa-color-brand-95"></wa-copy-button>
40
+ </div>
41
+ <small>95</small>
42
+ </li><li class="color-preview">
43
+ <div class="color swatch" style="--color: var(--wa-color-brand-90)">
44
+ <wa-copy-button value="--wa-color-brand-90" copy-label="--wa-color-brand-90"></wa-copy-button>
45
+ </div>
46
+ <small>90</small>
47
+ </li><li class="color-preview">
48
+ <div class="color swatch" style="--color: var(--wa-color-brand-80)">
49
+ <wa-copy-button value="--wa-color-brand-80" copy-label="--wa-color-brand-80"></wa-copy-button>
50
+ </div>
51
+ <small>80</small>
52
+ </li><li class="color-preview">
53
+ <div class="color swatch" style="--color: var(--wa-color-brand-70)">
54
+ <wa-copy-button value="--wa-color-brand-70" copy-label="--wa-color-brand-70"></wa-copy-button>
55
+ </div>
56
+ <small>70</small>
57
+ </li><li class="color-preview">
58
+ <div class="color swatch" style="--color: var(--wa-color-brand-60)">
59
+ <wa-copy-button value="--wa-color-brand-60" copy-label="--wa-color-brand-60"></wa-copy-button>
60
+ </div>
61
+ <small>60</small>
62
+ </li><li class="color-preview">
63
+ <div class="color swatch" style="--color: var(--wa-color-brand-50)">
64
+ <wa-copy-button value="--wa-color-brand-50" copy-label="--wa-color-brand-50"></wa-copy-button>
65
+ </div>
66
+ <small>50</small>
67
+ </li><li class="color-preview">
68
+ <div class="color swatch" style="--color: var(--wa-color-brand-40)">
69
+ <wa-copy-button value="--wa-color-brand-40" copy-label="--wa-color-brand-40"></wa-copy-button>
70
+ </div>
71
+ <small>40</small>
72
+ </li><li class="color-preview">
73
+ <div class="color swatch" style="--color: var(--wa-color-brand-30)">
74
+ <wa-copy-button value="--wa-color-brand-30" copy-label="--wa-color-brand-30"></wa-copy-button>
75
+ </div>
76
+ <small>30</small>
77
+ </li><li class="color-preview">
78
+ <div class="color swatch" style="--color: var(--wa-color-brand-20)">
79
+ <wa-copy-button value="--wa-color-brand-20" copy-label="--wa-color-brand-20"></wa-copy-button>
80
+ </div>
81
+ <small>20</small>
82
+ </li><li class="color-preview">
83
+ <div class="color swatch" style="--color: var(--wa-color-brand-10)">
84
+ <wa-copy-button value="--wa-color-brand-10" copy-label="--wa-color-brand-10"></wa-copy-button>
85
+ </div>
86
+ <small>10</small>
87
+ </li><li class="color-preview">
88
+ <div class="color swatch" style="--color: var(--wa-color-brand-05)">
89
+ <wa-copy-button value="--wa-color-brand-05" copy-label="--wa-color-brand-05"></wa-copy-button>
90
+ </div>
91
+ <small>05</small>
92
+ </li>
93
+ </ul>
@@ -0,0 +1,44 @@
1
+ <!doctype html>
2
+ <html lang="<%= site.locale %>" class="wa-palette-default">
3
+ <head>
4
+ <%= render "head", metadata: site.metadata, title: data.title %>
5
+ </head>
6
+ <body class="OFF:wll-grid-lines OFF:wll-code-dark <%= data.layout %> <%= data.page_class %>">
7
+ <%= dsd do %>
8
+ <%= render Willamette::HolyGrailLayout.new %>
9
+ <% end %>
10
+ <%= render Shared::Navbar.new(metadata: site.metadata, resource:) %>
11
+
12
+ <main slot="content" id="content">
13
+ <div hidden class="show-for-tablet" style="position: absolute; right: var(--wa-space-m)"><button type="button" class="wa-outlined" onclick="document.body.classList.toggle('sidebar-end-open')"><wa-icon name="list"></wa-icon></button></div>
14
+
15
+ <main-contents <%= "data-pagefind-body" unless data.exclude_from_pagefind %>>
16
+ <%= yield %>
17
+ </main-contents>
18
+
19
+ <wll-back-to-top>
20
+ <button><wa-icon name="circle-chevron-up"></wa-icon></button>
21
+ </wll-back-to-top>
22
+ </main>
23
+
24
+ <aside slot="sidebar-start">
25
+ <%= render Shared::Sidebar.new(metadata: site.metadata, resource:) %>
26
+ </aside>
27
+
28
+ <aside slot="sidebar-end" hidden>
29
+ <!-- TODO: this should be in a shared component -->
30
+ <div class="show-for-tablet" style="position: absolute; right: var(--wa-space-m)"><button type="button" class="wa-outlined" onclick="document.body.classList.toggle('sidebar-end-open')"><wa-icon name="close"></wa-icon></button></div>
31
+ <h2><%= t "documentation.contents" %></h2>
32
+
33
+ <%= slotted :toc %>
34
+
35
+ <hr />
36
+
37
+ <!-- TOC will be relocated here -->
38
+ </aside>
39
+
40
+ <%= render "footer", metadata: site.metadata %>
41
+ <a slot="skip-to-content" class="skip-to-content-link" href="#content"><%= t "content.skip_to_content" %></a>
42
+ <%= render Willamette::SearchDialog.new %>
43
+ </body>
44
+ </html>
@@ -0,0 +1,28 @@
1
+ module Willamette
2
+ class Builders::Inspectors < Bridgetown::Builder
3
+ def build # rubocop:disable Metrics/CyclomaticComplexity
4
+ inspect_html do |document|
5
+ document.query_selector_all("a").each do |anchor|
6
+ next if anchor[:target]
7
+
8
+ unless anchor[:href]&.starts_with?("http") && !anchor[:href]&.include?(site.config.url)
9
+ next
10
+ end
11
+
12
+ anchor[:target] = "_blank"
13
+ end
14
+ end
15
+
16
+ inspect_html do |document|
17
+ document.query_selector_all("article h2[id], article h3[id]").each do |heading|
18
+ heading << document.create_text_node(" ")
19
+ heading << document.create_element(
20
+ "a", "#",
21
+ href: "##{heading[:id]}",
22
+ class: "heading-anchor"
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ module Willamette
2
+ class Builders::Toc < Bridgetown::Builder
3
+ def build
4
+ helper :toc do |&block|
5
+ helpers.slot(:toc, &block)
6
+
7
+ "* toc\n{:toc}\n".html_safe
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ en:
2
+ content:
3
+ featured_post_image: featured post image
4
+ skip_to_content: Skip to content
5
+ posts:
6
+ more_posts: More Posts
7
+ newer_posts: Newer Posts
8
+ labels:
9
+ close: Close
10
+ email_address: Email Address
11
+ first_name: First Name (Optional)
12
+ next: Next
13
+ previous: Previous
14
+ search: Search
15
+ subscribe: Subscribe
16
+ documentation:
17
+ contents: Contents
18
+ explore: Explore
19
+ marketing:
20
+ follow: Follow Us
21
+ subscribe: Subscribe
22
+ see_also: See Also
@@ -0,0 +1,20 @@
1
+ module Willamette
2
+ # Contain logic for rendering
3
+ Strategies::Link = Data.define(:depth, :current, :icon, :icon_family, :title, :url)
4
+
5
+ def self.link(depth: 1, current: false, icon: nil, icon_family: nil, **) # rubocop:disable Metrics/ParameterLists
6
+ Strategies::Link.new(depth:, current:, icon:, icon_family:, **)
7
+ end
8
+
9
+ def self.links_for(base_resource)
10
+ base_resource.collection.map do |resource|
11
+ depth = resource.relative_path.each_filename.count - 1
12
+ Willamette.link(
13
+ current: resource == base_resource,
14
+ title: resource.data.title,
15
+ url: resource.relative_url,
16
+ depth:
17
+ )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,83 @@
1
+ module Willamette
2
+ # Contain logic for rendering
3
+ # rubocop:disable Metrics/ParameterLists
4
+ class Strategies::Sidebar
5
+ attr_reader :description
6
+
7
+ # Use the `sidebar` module method on `Willamette`
8
+ def initialize(
9
+ component, description:, explore_links:, follow_links:, subscribe:, see_also_links:
10
+ )
11
+ @component = component
12
+ @description = description
13
+ @explore_links = explore_links || []
14
+ @follow_links = follow_links || []
15
+ @subscribe = subscribe
16
+ @see_also_links = see_also_links || []
17
+ end
18
+
19
+ def explore? = !@explore_links.empty?
20
+
21
+ def explore_links = links_html(@explore_links)
22
+
23
+ def description? = @description && !explore?
24
+
25
+ def follow? = !@follow_links.empty?
26
+
27
+ def follow_links = links_html(@follow_links)
28
+
29
+ def subscribe? = @subscribe
30
+
31
+ def see_also? = !@see_also_links.empty?
32
+
33
+ def see_also_links = links_html(@see_also_links)
34
+
35
+ def default_link_icon(folder: false)
36
+ folder ? "folder-open" : "file-lines"
37
+ end
38
+
39
+ private
40
+
41
+ def links_html(links)
42
+ doc = Nokolexbor::HTML("<ul></ul>")
43
+ list = doc.at_css("ul")
44
+ links.each_with_index do |link, index|
45
+ list << link_node(doc:, link:, folder: (links[index + 1]&.depth || 1) > link.depth)
46
+ end
47
+ list.to_html.html_safe
48
+ end
49
+
50
+ def link_node(doc:, link:, folder: false) # rubocop:disable Metrics/AbcSize
51
+ item = doc.create_element("li")
52
+ item[:"item-depth"] = link.depth
53
+ anchor = doc.create_element("a")
54
+ anchor[:href] = @component.relative_url(link.url)
55
+ anchor[:"aria-current"] = "page" if link.current
56
+
57
+ icon = doc.create_element("wa-icon")
58
+ icon[:name] = link.icon || default_link_icon(folder:)
59
+ icon[:family] = link.icon_family if link.icon_family
60
+ anchor << icon
61
+
62
+ anchor << doc.create_text_node(" ")
63
+ anchor << doc.create_text_node(link.title)
64
+
65
+ item << anchor
66
+ item
67
+ end
68
+ end
69
+
70
+ def self.sidebar(
71
+ component,
72
+ description:,
73
+ explore_links: nil,
74
+ follow_links: nil,
75
+ subscribe: false,
76
+ see_also_links: nil
77
+ )
78
+ Willamette::Strategies::Sidebar.new(
79
+ component, description:, explore_links:, follow_links:, subscribe:, see_also_links:
80
+ )
81
+ end
82
+ # rubocop:enable Metrics/ParameterLists
83
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Willamette
4
+ VERSION = "0.5.0"
5
+ end
data/lib/willamette.rb ADDED
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bridgetown"
4
+
5
+ module Willamette
6
+ module Builders
7
+ end
8
+
9
+ module Strategies
10
+ end
11
+ end
12
+
13
+ require "willamette/builders/inspectors"
14
+ require "willamette/builders/toc"
15
+ require "willamette/strategies/link"
16
+ require "willamette/strategies/sidebar"
17
+
18
+ # use `<wll-code>` element instead of `<div>`
19
+ Kramdown::Converter::Html.class_eval do
20
+ alias_method :__old_convert_codeblock, :convert_codeblock
21
+
22
+ def convert_codeblock(elem, indent)
23
+ output = __old_convert_codeblock(elem, indent)
24
+ return output unless @options[:syntax_highlighter]
25
+
26
+ output.sub(%r!^(\s*)<div!, "\\1<wll-code").sub("</div>\n", "</wll-code>\n")
27
+ end
28
+ end
29
+
30
+ # @param config [Bridgetown::Configuration::ConfigurationDSL]
31
+ Bridgetown.initializer :willamette do |config, docs_url_segment: "docs"|
32
+ # Add code here which will run when a site includes
33
+ # `init :willamette`
34
+ # in its configuration
35
+
36
+ # Add default configuration data:
37
+ config.willamette ||= {}
38
+ config.willamette.my_setting ||= 123
39
+
40
+ # Register your builders:
41
+ config.builder Willamette::Builders::Inspectors
42
+ config.builder Willamette::Builders::Toc
43
+ config.builder :WillametteRegisterPermalinkPlaceholders do
44
+ def build
45
+ permalink_placeholder :unordered_path do |resource|
46
+ {
47
+ raw_value:
48
+ placeholder_processors[:path].(resource)[:raw_value].gsub(%r!(^|/)[0-9]+_!, "\\1"),
49
+ }
50
+ end
51
+ end
52
+ end
53
+
54
+ config.collections do
55
+ docs do
56
+ output true
57
+ permalink "/:locale/#{docs_url_segment ? "#{docs_url_segment}/" : ""}:unordered_path/"
58
+ name "Documentation"
59
+ end
60
+ end
61
+
62
+ config.keep_files << "pagefind"
63
+
64
+ # You can optionally supply a source manifest:
65
+ config.source_manifest(
66
+ origin: Willamette,
67
+ components: File.expand_path("../components", __dir__),
68
+ layouts: File.expand_path("../layouts", __dir__),
69
+ content: File.expand_path("../content", __dir__)
70
+ )
71
+
72
+ I18n.load_path += Gem.find_files_from_load_path("willamette/locales/en.*")
73
+
74
+ skip_pagefind_write = false
75
+ config.hook :site, :post_write do
76
+ next if config.fast_refresh && skip_pagefind_write
77
+
78
+ skip_pagefind_write = true
79
+
80
+ `rm -rf output/pagefind && npx --yes pagefind --site output`
81
+ Bridgetown.logger.info "Pagefind:", "Wrote search index"
82
+ end
83
+ end