showcase-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ //= link showcase.js
2
+ //= link_tree ../builds/ .css
@@ -0,0 +1,28 @@
1
+ window.customElements.define("showcase-sample", class extends HTMLElement {
2
+ connectedCallback() {
3
+ this.events.forEach((name) => this.addEventListener(name, this.emit))
4
+ }
5
+
6
+ disconnectedCallback() {
7
+ this.events.forEach(this.removeEventListener)
8
+ }
9
+
10
+ emit(event) {
11
+ console.log({ originator: this, event })
12
+ this.relay(event)
13
+ }
14
+
15
+ relay({ type, detail }) {
16
+ const node = document.createElement("div")
17
+ node.innerHTML = JSON.stringify({ type, detail })
18
+ this.relayTarget?.appendChild(node)
19
+ }
20
+
21
+ get relayTarget() {
22
+ return this.querySelector("[data-showcase-sample-target='relay']")
23
+ }
24
+
25
+ get events() {
26
+ return JSON.parse(this.getAttribute("events")) || []
27
+ }
28
+ })
@@ -0,0 +1,3 @@
1
+ class Showcase::ApplicationController < ActionController::Base
2
+ layout "showcase"
3
+ end
@@ -0,0 +1,8 @@
1
+ class Showcase::PagesController < Showcase::ApplicationController
2
+ def index
3
+ end
4
+
5
+ def show
6
+ @page = Showcase::Path.new(params[:id]).page_for view_context
7
+ end
8
+ end
@@ -0,0 +1,51 @@
1
+ class Showcase::Page::Options
2
+ include Enumerable
3
+
4
+ def initialize(view_context)
5
+ @view_context = view_context
6
+ @options = []
7
+ @order = [:name, :required, :type, :default, :description]
8
+ end
9
+ delegate :empty?, to: :@options
10
+
11
+ def required(*arguments, **keywords, &block)
12
+ option(*arguments, **keywords, required: true, &block)
13
+ end
14
+
15
+ def optional(*arguments, **keywords, &block)
16
+ option(*arguments, **keywords, required: false, &block)
17
+ end
18
+
19
+ DEFAULT_OMITTED = Object.new
20
+
21
+ def option(name, description = nil, required: false, type: nil, default: DEFAULT_OMITTED, **options, &block)
22
+ description ||= @view_context.capture(&block).remove(/^\s+/).html_safe if block
23
+
24
+ type ||= type_from_default(default)
25
+ default = default == DEFAULT_OMITTED ? nil : default.inspect
26
+
27
+ @options << options.with_defaults(name: name, default: default, type: type, description: description, required: required)
28
+ end
29
+
30
+ def headers
31
+ @headers ||= @order | @options.flat_map(&:keys).uniq.sort
32
+ end
33
+
34
+ def each(&block)
35
+ @options.each do |option|
36
+ yield headers.index_with { option[_1] }
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def type_from_default(default)
43
+ case default
44
+ when DEFAULT_OMITTED then String
45
+ when true, false then "Boolean"
46
+ when nil then "nil"
47
+ else
48
+ default.class
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,43 @@
1
+ class Showcase::Page::Sample
2
+ attr_reader :name, :id, :events, :details
3
+ attr_reader :source
4
+
5
+ def initialize(view_context, name, description: nil, id: name.parameterize, events: nil, **details)
6
+ @view_context = view_context
7
+ @name, @id, @details = name, id, details
8
+ @events = Array(events)
9
+ description description if description
10
+ end
11
+
12
+ def description(content = nil, &block)
13
+ @description = content || @view_context.capture(&block) if content || block_given?
14
+ @description
15
+ end
16
+
17
+ def collect(&block)
18
+ preview(&block)
19
+ extract(&block)
20
+ end
21
+
22
+ def preview(&block)
23
+ block_given? ? @preview = @view_context.capture(&block) : @preview
24
+ end
25
+
26
+ def extract(&block)
27
+ @source = Showcase.sample_renderer.call \
28
+ extract_block_lines_via_matched_indentation_from(*block.source_location)
29
+ end
30
+
31
+ private
32
+
33
+ def extract_block_lines_via_matched_indentation_from(file, starting_index)
34
+ first_line, *lines = File.readlines(file).from(starting_index - 1)
35
+
36
+ indentation = first_line.match(/^\s+(?=<%)/).to_s
37
+ matcher = /^#{indentation}\S/
38
+
39
+ index = lines.index { _1.match?(matcher) }
40
+ lines.slice!(index..) if index
41
+ lines
42
+ end
43
+ end
@@ -0,0 +1,45 @@
1
+ class Showcase::Page
2
+ autoload :Sample, "showcase/page/sample"
3
+ autoload :Options, "showcase/page/options"
4
+
5
+ attr_reader :id, :badges, :samples
6
+
7
+ def initialize(view_context, id:, title: nil)
8
+ @view_context, @id = view_context, id
9
+ @badges, @samples = [], []
10
+ title title
11
+ end
12
+
13
+ def title(content = nil)
14
+ @title = content if content
15
+ @title
16
+ end
17
+
18
+ def description(content = nil, &block)
19
+ @description = content || @view_context.capture(&block) if content || block_given?
20
+ @description
21
+ end
22
+
23
+ def badge(*badges)
24
+ @badges.concat badges
25
+ end
26
+
27
+ def sample(name, **options, &block)
28
+ @samples << sample = Sample.new(@view_context, name, **options)
29
+
30
+ if block.arity.zero?
31
+ sample.collect(&block)
32
+ else
33
+ @view_context.capture(sample, &block)
34
+ end
35
+ end
36
+
37
+ def options
38
+ @options ||= Options.new(@view_context).tap { yield _1 if block_given? }
39
+ end
40
+
41
+ def render_template
42
+ @view_context.render template: id, prefixes: [Showcase.templates_path], locals: { showcase: self }
43
+ nil
44
+ end
45
+ end
@@ -0,0 +1,30 @@
1
+ class Showcase::Path
2
+ class Tree < Struct.new(:id, :paths)
3
+ def name
4
+ root? ? "Pages" : id
5
+ end
6
+
7
+ def root?
8
+ id == "."
9
+ end
10
+ end
11
+
12
+ def self.tree
13
+ all.group_by(&:dirname).map { Tree.new _1, _2 }
14
+ end
15
+
16
+ def self.all
17
+ Showcase.filenames.map { new _1 }.sort_by!(&:id)
18
+ end
19
+
20
+ attr_reader :id, :dirname, :basename
21
+
22
+ def initialize(path)
23
+ @id = path.split(".").first
24
+ @dirname, @basename = File.split(@id)
25
+ end
26
+
27
+ def page_for(view_context)
28
+ Showcase::Page.new(view_context, id: id, title: basename.titleize).tap(&:render_template)
29
+ end
30
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Showcase</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+
7
+ <%= stylesheet_link_tag "application", "showcase" %>
8
+ <%= javascript_include_tag "application", "showcase" %>
9
+ </head>
10
+
11
+ <body>
12
+ <%= render "showcase/root" %>
13
+ </body>
14
+ </html>
@@ -0,0 +1,13 @@
1
+ <main class="flex flex-wrap">
2
+ <section class="grid grid-cols-12 w-full">
3
+ <nav class="col-span-3 xl:col-span-2 py-5 h-full border-r">
4
+ <h1 class="font-black text-2xl py-2 pl-4 cursor-pointer"><%= link_to "Showcase", root_url %></h1>
5
+
6
+ <%= render partial: "showcase/path/tree", collection: Showcase::Path.tree %>
7
+ </nav>
8
+
9
+ <section class="col-span-9 xl:col-span-10 w-full min-h-screen p-12 pt-7">
10
+ <%= yield %>
11
+ </section>
12
+ </section>
13
+ </main>
@@ -0,0 +1,27 @@
1
+ <section class="w-full overflow-x-auto">
2
+ <h2 class="text-xl font-semibold mb-2">Options</h2>
3
+
4
+ <table class="table border-collapse border border-gray-200">
5
+ <thead>
6
+ <tr class="bg-slate-50">
7
+ <% options.headers.each do |header| %>
8
+ <th class="px-4 py-2"><%= header.to_s.humanize %></th>
9
+ <% end %>
10
+ </tr>
11
+ </thead>
12
+
13
+ <tbody>
14
+ <% options.each do |option| %>
15
+ <tr class="border-t border-gray-200">
16
+ <% option.each do |key, value| %>
17
+ <% if key == :required %>
18
+ <td class="p-4"><%= tag.input type: :checkbox, checked: value, disabled: true if value %></td>
19
+ <% else %>
20
+ <td class="p-4"><%= tag.pre value %></td>
21
+ <% end %>
22
+ <% end %>
23
+ </tr>
24
+ <% end %>
25
+ </tbody>
26
+ </table>
27
+ </section>
@@ -0,0 +1,26 @@
1
+ <div class="space-y-8">
2
+ <section>
3
+ <% if page.title %>
4
+ <div class="flex items-center space-x-2 mb-2">
5
+ <h2 class="font-semibold text-3xl"><%= page.title %></h2>
6
+
7
+ <%# TODO: Insert default badge support here. %>
8
+ </div>
9
+ <% end %>
10
+
11
+ <% if page.description %>
12
+ <p class="font-normal text-base"><%= page.description %></p>
13
+ <% end %>
14
+ </section>
15
+
16
+ <% if page.samples.any? %>
17
+ <section>
18
+ <h2 class="font-semibold text-xl mb-2">Samples</h2>
19
+ <%= render partial: "showcase/pages/sample", collection: page.samples %>
20
+ </section>
21
+ <% end %>
22
+
23
+ <% if options = page.options.presence %>
24
+ <%= render "showcase/pages/options", options: options %>
25
+ <% end %>
26
+ </div>
@@ -0,0 +1,37 @@
1
+ <section class="mb-4 border border-gray-200 rounded-md">
2
+ <showcase-sample id="<%= sample.id %>" events="<%= sample.events %>">
3
+ <header class="bg-slate-100/50">
4
+ <h3 class="px-4 py-2 font-medium text-base md:text-lg leading-snug truncate"><%= link_to sample.name, "##{sample.id}" %></h3>
5
+
6
+ <% if sample.description %>
7
+ <p class="px-4 py-2 text-base"><%= sample.description %></p>
8
+ <% end %>
9
+ </header>
10
+
11
+ <% if sample.preview %>
12
+ <section class="px-4 py-2 border border-gray-200 border-0 border-b">
13
+ <%= sample.preview %>
14
+ </section>
15
+ <% end %>
16
+
17
+ <% if sample.source %>
18
+ <details>
19
+ <summary class="px-4 py-2 hover:bg-indigo-50 cursor-pointer select-none">View Source</summary>
20
+
21
+ <section class="px-4 py-2 relative overflow-hidden hover:select-all">
22
+ <%= sample.source %>
23
+ </section>
24
+ </details>
25
+ <% end %>
26
+
27
+ <% if sample.events.any? %>
28
+ <section class="px-4 py-2 font-small bg-slate-50">
29
+ <h4 class="mb-2 font-medium text-base">JavaScript Events</h4>
30
+
31
+ <div class="overflow-scroll max-h-20">
32
+ <pre data-showcase-sample-target="relay"></pre>
33
+ </div>
34
+ </section>
35
+ <% end %>
36
+ </showcase-sample>
37
+ </section>
@@ -0,0 +1,33 @@
1
+ <article class="space-y-4 font-normal text-base">
2
+ <p class="font-normal text-xl">👋 Welcome to <span class="italic">Showcase</span> — the UI Pattern Library!</p>
3
+
4
+ <section class="space-y-4">
5
+ <h2 class="font-semibold text-2xl">What is this thing?</h2>
6
+ <p>This resource is intended to be a hub for all-things UI for your developers, with the goal of <span class="italic font-semibold">sharing knowledge</span>, <span class="italic font-semibold">promoting reuse of existing code</span>, and <span class="italic font-semibold">ensuring consistency</span> across your application.</p>
7
+ </section>
8
+
9
+ <section class="space-y-4">
10
+ <h2 class="font-semibold text-2xl">How do I use it?</h2>
11
+ <p>On each page, you can add a series of usage examples for each component, style, or pattern. In the samples blocks, you'll see a live preview, as well as the source used to produce the example.</p>
12
+ <p>At the bottom of most pages, you will see a table, listing any options for configuring the partial or component.</p>
13
+ </section>
14
+
15
+ <section class="space-y-4">
16
+ <h2 class="font-semibold text-2xl">But I don't see the thing I need 🤔</h2>
17
+ <p>If you don't see the pattern or component here, that doesn't mean it doesn't aleady exist or can't be created with some combination of existing building blocks.</p>
18
+ </section>
19
+
20
+ <section class="space-y-4">
21
+ <h2 class="font-semibold text-2xl">I have questions, who do I reach out to?</h2>
22
+ <p>If you need help or have questions, please reach out to <%#= Showcase.links.support %>.</p>
23
+ </section>
24
+
25
+ <section class="space-y-4">
26
+ <h2 class="font-semibold text-2xl">Additional resources</h2>
27
+ <ul class="list-none">
28
+ <li>
29
+ <%= link_to "Bullet Train field partials documentation", "https://bullettrain.co/docs/field-partials", target: "_blank" %>
30
+ </li>
31
+ </ul>
32
+ </section>
33
+ </article>
@@ -0,0 +1 @@
1
+ <%= render "showcase/pages/page", page: @page %>
@@ -0,0 +1,9 @@
1
+ <details open class="flex flex-col">
2
+ <%= tag.summary tree.name.titleize, class: "hover:bg-indigo-50 font-medium text-black text-base py-2 pl-4 cursor-pointer" %>
3
+
4
+ <% tree.paths.each do |path| %>
5
+ <article class="hover:bg-indigo-50 <%= "bg-indigo-50" if path.id == params[:id] %>">
6
+ <%= link_to path.basename.titleize, page_path(path.id), class: "inline-block py-2 px-8 w-full" %>
7
+ </article>
8
+ <% end %>
9
+ </details>
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ Showcase::Engine.routes.draw do
2
+ get "pages/*id", to: "pages#show", as: :page
3
+ root to: "pages#index"
4
+ end
@@ -0,0 +1,22 @@
1
+ const defaultTheme = require('tailwindcss/defaultTheme')
2
+
3
+ module.exports = {
4
+ content: [
5
+ './public/*.html',
6
+ './app/helpers/**/*.rb',
7
+ './app/javascript/**/*.js',
8
+ './app/views/**/*.{erb,haml,html,slim}'
9
+ ],
10
+ theme: {
11
+ extend: {
12
+ fontFamily: {
13
+ sans: defaultTheme.fontFamily.sans,
14
+ },
15
+ },
16
+ },
17
+ plugins: [
18
+ require('@tailwindcss/forms'),
19
+ require('@tailwindcss/aspect-ratio'),
20
+ require('@tailwindcss/typography'),
21
+ ]
22
+ }
@@ -0,0 +1,9 @@
1
+ module Showcase
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Showcase
4
+
5
+ initializer "showcase.assets" do |app|
6
+ app.config.assets.precompile += %w[showcase_manifest]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Showcase
2
+ VERSION = "0.1.0"
3
+ end
data/lib/showcase.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "showcase/version"
2
+ require "showcase/engine"
3
+
4
+ module Showcase
5
+ singleton_class.attr_accessor :templates_directory_prefix, :sample_renderer
6
+ @templates_directory_prefix = ""
7
+ @sample_renderer = ->(lines) { lines.join }
8
+
9
+ def self.templates_path
10
+ @templates_path ||= File.join(templates_directory_prefix, "showcase/pages/templates").delete_prefix("/")
11
+ end
12
+
13
+ def self.filenames
14
+ Dir.glob("**/*.*", base: Rails.root.join("app/views", templates_path))
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :showcase do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: showcase-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Pence
8
+ - Kasper Timm Hansen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2022-12-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: 6.1.0
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: 6.1.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: tailwindcss-rails
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ description:
43
+ email:
44
+ - hey@kaspth.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - Rakefile
52
+ - app/assets/builds/showcase.css
53
+ - app/assets/config/showcase_manifest.js
54
+ - app/assets/javascripts/showcase.js
55
+ - app/controllers/showcase/application_controller.rb
56
+ - app/controllers/showcase/pages_controller.rb
57
+ - app/models/showcase/page.rb
58
+ - app/models/showcase/page/options.rb
59
+ - app/models/showcase/page/sample.rb
60
+ - app/models/showcase/path.rb
61
+ - app/views/layouts/showcase.html.erb
62
+ - app/views/showcase/_root.html.erb
63
+ - app/views/showcase/pages/_options.html.erb
64
+ - app/views/showcase/pages/_page.html.erb
65
+ - app/views/showcase/pages/_sample.html.erb
66
+ - app/views/showcase/pages/index.html.erb
67
+ - app/views/showcase/pages/show.html.erb
68
+ - app/views/showcase/path/_tree.html.erb
69
+ - config/routes.rb
70
+ - config/tailwind.config.js
71
+ - lib/showcase.rb
72
+ - lib/showcase/engine.rb
73
+ - lib/showcase/version.rb
74
+ - lib/tasks/showcase_tasks.rake
75
+ homepage: https://github.com/kaspth/showcase
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ homepage_uri: https://github.com/kaspth/showcase
80
+ source_code_uri: https://github.com/kaspth/showcase
81
+ changelog_uri: https://github.com/kaspth/showcase/blob/main/CHANGELOG.md
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.3.24
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Showcase helps you show off and document your partials, components, view
101
+ helpers and Stimulus controllers.
102
+ test_files: []