Skip to content

Commit ae0b35c

Browse files
committed
Add docs site
1 parent 6e43cd3 commit ae0b35c

30 files changed

Lines changed: 6301 additions & 31 deletions

lib/tasks/website.rake

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: true
2+
3+
namespace :website do
4+
WEBSITE_DIR = File.expand_path("../../website", __dir__)
5+
6+
desc "Render demo components and write HTML/source fragments into the docs site"
7+
task :demos do
8+
require File.expand_path("../../test/dummy/config/environment", __dir__)
9+
require "fileutils"
10+
11+
out = File.join(WEBSITE_DIR, "_includes", "demos")
12+
FileUtils.mkdir_p(out)
13+
14+
demos = [
15+
{
16+
slug: "release_card",
17+
title: "Deploy dashboard release card",
18+
component: Dashboard::ReleaseCardComponent,
19+
args: {release_id: 1, name: "API Gateway", version: "2.4.1", environment: :production, status: :deployed},
20+
# The release card emits a sibling card and a third "pending" card so
21+
# the demo shows the dynamic `status:` class colours side by side.
22+
siblings: [
23+
{release_id: 2, name: "Auth Service", version: "1.9.0", environment: :staging, status: :pending},
24+
{release_id: 3, name: "Web Frontend", version: "3.0.0-rc1", environment: :preview, status: :failed}
25+
],
26+
source_path: "test/dummy/app/components/dashboard/release_card_component.rb"
27+
}
28+
]
29+
30+
demos.each do |demo|
31+
html = Vident::StableId.with_sequence_generator(seed: "vident-docs-#{demo[:slug]}") do
32+
rendered = demo[:component].new(**demo[:args]).call
33+
Array(demo[:siblings]).each do |sibling_args|
34+
rendered += demo[:component].new(**sibling_args).call
35+
end
36+
rendered
37+
end
38+
# Strip the development-only "Before ..." HTML comment that the dummy
39+
# ApplicationComponent injects so the embedded fragment stays clean.
40+
html = html.gsub(/<!--\s*Before [^>]*?-->/, "").strip
41+
42+
source = File.read(File.expand_path("../../#{demo[:source_path]}", __dir__))
43+
44+
File.write(File.join(out, "#{demo[:slug]}_rendered.html"), html + "\n")
45+
File.write(File.join(out, "#{demo[:slug]}_source.rb"), source)
46+
File.write(File.join(out, "#{demo[:slug]}_html.html"), pretty_html(html))
47+
puts " rendered #{demo[:slug]}#{html.bytesize} bytes"
48+
end
49+
50+
puts "Wrote demos to #{out}"
51+
end
52+
53+
desc "Build the documentation website"
54+
task build: :demos do
55+
Dir.chdir(WEBSITE_DIR) do
56+
sh "bundle install"
57+
sh "bundle exec jekyll build"
58+
end
59+
end
60+
61+
desc "Serve the documentation website locally with live reload"
62+
task serve: :demos do
63+
Dir.chdir(WEBSITE_DIR) do
64+
sh "bundle install"
65+
sh "bundle exec jekyll serve --livereload"
66+
end
67+
end
68+
69+
desc "Clean the documentation website build"
70+
task :clean do
71+
Dir.chdir(WEBSITE_DIR) do
72+
sh "bundle exec jekyll clean"
73+
end
74+
end
75+
76+
# Pretty-prints the rendered fragment for the "Raw HTML" tab. Nokogiri
77+
# escapes `>` inside attribute values to `&gt;` (correct HTML5, but ugly to
78+
# read), so we unescape that back — the result is still valid HTML and
79+
# matches what a developer would expect to see in their source.
80+
def pretty_html(html)
81+
require "nokogiri"
82+
Nokogiri::HTML5.fragment(html).to_xhtml(indent: 2).gsub("&gt;", ">")
83+
end
84+
end

test/dummy/app/components/dashboard/release_card_component.rb

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,18 @@
22

33
module Dashboard
44
class ReleaseCardComponent < ApplicationComponent
5-
# Lock the stimulus identifier to match the V1 component's, so the
6-
# existing `app_components/dashboard/release_card_component_controller.js`
7-
# still resolves against this V2 component without duplication.
8-
# Emitted `data-controller="dashboard--release-card-component"`.
9-
class << self
10-
def stimulus_identifier_path = "dashboard/release_card_component"
11-
end
12-
135
prop :release_id, Integer
146
prop :name, String
157
prop :version, String
168
prop :environment, _Union(:production, :staging, :preview), default: :staging
179
prop :status, _Union(:pending, :deployed, :failed), default: :pending
1810

19-
# `stimulus_outlet_host:` is inherited from Vident::Component. Passing
20-
# the parent page at render time causes this card to call
21-
# `host.add_stimulus_outlets(self)` in after_initialize, which wires a
22-
# `data-dashboard--page-component-dashboard--release-card-component-outlet`
23-
# attribute onto the host's root element.
24-
2511
stimulus do
26-
# Mirrors the :release_id / :name / :status props straight through as
27-
# Stimulus values. No second declaration needed — the Resolver reads
28-
# the `@ivar` current value at render time.
2912
values_from_props :release_id, :name, :status
3013

31-
# Proc evaluated in the component instance, so it sees @status. The DSL
32-
# emits a `data-<controller>-status-class="..."` attribute on the root
33-
# which Stimulus exposes as `this.statusClasses` in the JS controller.
34-
# `class_list_for_stimulus_classes(:status)` below inlines the same
35-
# resolved value into the `class=` attribute for the first render.
14+
# Procs run in the component instance at render time, so they see
15+
# `@status`. `class_list_for_stimulus_classes(:status)` inlines the
16+
# same value into `class=` for the first paint.
3617
classes status: -> {
3718
case @status
3819
when :deployed then "border-green-500 bg-green-50"
@@ -41,9 +22,6 @@ def stimulus_identifier_path = "dashboard/release_card_component"
4122
end
4223
}
4324

44-
# Fluent builder: reads left-to-right as "the `select` method fires
45-
# on the `click` event". Emits `click->implied#select`. Equivalent
46-
# V1-style shorthand `actions [:click, :select]` still works.
4725
action(:select).on(:click)
4826
end
4927

@@ -64,12 +42,9 @@ def view_template
6442
p(class: "mt-3 text-xs uppercase tracking-wide text-gray-500") { @status.to_s }
6543

6644
div(class: "mt-3 flex gap-2") do
67-
# Demo of Stimulus params: one `apply` handler for both buttons, with
68-
# `stimulus_params: { kind: ... }` on the element so the handler reads
69-
# `event.params.kind` to tell which button fired. In a real app you'd
70-
# probably just keep separate `promote` / `cancel` handlers — this
71-
# intentional "one dispatch switch" shape is a bit RPC-ish and is
72-
# here to show what the params feature looks like.
45+
# Both buttons share an `apply` handler; the controller reads
46+
# `event.params.kind` to tell them apart, matching each button's
47+
# `stimulus_params:` declaration.
7348
card.child_element(
7449
:button,
7550
stimulus_action: [:click, :apply],

website/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
_site/
2+
.jekyll-cache/
3+
.jekyll-metadata
4+
.sass-cache/
5+
Gemfile.lock
6+
vendor/

website/Gemfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
source "https://rubygems.org"
2+
3+
# Jekyll site generator. The vitepress theme requires Jekyll 4.3+ and Ruby 3.1+.
4+
gem "jekyll", "~> 4.4"
5+
gem "jekyll-vitepress-theme"
6+
7+
group :jekyll_plugins do
8+
gem "jekyll-feed", "~> 0.17"
9+
gem "jekyll-sitemap", "~> 1.4"
10+
end
11+
12+
# Windows / JRuby zoneinfo support — kept for parity with the encoded_id site.
13+
platforms :mingw, :x64_mingw, :mswin, :jruby do
14+
gem "tzinfo", ">= 1", "< 3"
15+
gem "tzinfo-data"
16+
end
17+
18+
gem "wdm", "~> 0.1", platforms: [:mingw, :x64_mingw, :mswin]
19+
gem "http_parser.rb", "~> 0.6.0", platforms: [:jruby]

website/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Vident Documentation Website
2+
3+
Static documentation site built with Jekyll and the
4+
[jekyll-vitepress-theme](https://github.com/crmne/jekyll-vitepress-theme).
5+
The site deploys to Cloudflare Pages from `main` (see
6+
`.github/workflows/website.yml`).
7+
8+
## Local development
9+
10+
From the repository root:
11+
12+
```bash
13+
# Build the site (also re-renders the live demo HTML / sources)
14+
bundle exec rake website:demos website:build
15+
16+
# Serve with live reload at http://localhost:4000
17+
bundle exec rake website:serve
18+
19+
# Clean generated output
20+
bundle exec rake website:clean
21+
```
22+
23+
Or, manually inside `website/`:
24+
25+
```bash
26+
cd website
27+
bundle install
28+
bundle exec jekyll serve
29+
```
30+
31+
## How the live demo on the landing page works
32+
33+
The landing page leads with a working component (`PhlexGreeters::GreeterVidentComponent`
34+
from the dummy app). Because Cloudflare Pages serves static HTML only, we
35+
pre-render the example at build time:
36+
37+
- `rake website:demos` renders the Phlex component to HTML and writes the
38+
fragment, the Vident source, and a pretty-printed copy of the rendered
39+
HTML into `website/_includes/demos/`.
40+
- `website/assets/js/demo.js` ships a tiny Stimulus bundle (loaded from a CDN)
41+
that registers the controllers under the same identifiers the rendered
42+
components use, so the demo is interactive in the browser without a server.
43+
44+
If you change the demo component, re-run `rake website:demos` to refresh the
45+
embedded fragments.

website/_config.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
title: Vident
2+
tagline: Type-safe Rails components with first-class Stimulus
3+
description: >-
4+
Vident builds typed, Stimulus-wired components for Rails on top of Phlex
5+
or ViewComponent. A single declarative DSL replaces the data-attribute
6+
boilerplate, with intelligent Tailwind class merging and component caching.
7+
url: "https://vident.dev"
8+
baseurl: ""
9+
github_username: stevegeek
10+
github_repo: vident
11+
12+
theme: jekyll-vitepress-theme
13+
plugins:
14+
- jekyll-vitepress-theme
15+
- jekyll-feed
16+
- jekyll-sitemap
17+
18+
markdown: kramdown
19+
kramdown:
20+
input: GFM
21+
syntax_highlighter: rouge
22+
permalink: pretty
23+
24+
# Collections power the sidebar groups. Each `_<name>/` folder becomes a group;
25+
# pages are ordered by `nav_order` frontmatter inside each group.
26+
collections:
27+
introduction:
28+
output: true
29+
permalink: /introduction/:path/
30+
guides:
31+
output: true
32+
permalink: /guides/:path/
33+
reference:
34+
output: true
35+
permalink: /reference/:path/
36+
37+
defaults:
38+
- scope: { path: "", type: "introduction" }
39+
values: { layout: "default" }
40+
- scope: { path: "", type: "guides" }
41+
values: { layout: "default" }
42+
- scope: { path: "", type: "reference" }
43+
values: { layout: "default" }
44+
45+
jekyll_vitepress:
46+
branding:
47+
site_title: Vident
48+
logo: /assets/img/vident-logo.svg
49+
syntax:
50+
light_theme: github
51+
dark_theme: github.dark
52+
edit_link:
53+
pattern: "https://github.com/stevegeek/vident/edit/main/website/:path"
54+
text: Edit this page on GitHub
55+
footer:
56+
message: Released under the MIT License.
57+
copyright: "Copyright © 2023–{{ 'now' | date: '%Y' }} Stephen Ierodiaconou"
58+
# Light mode leads with the logo's red; dark mode swaps to its yellow so
59+
# links/CTAs stay legible on a dark background.
60+
tokens:
61+
light:
62+
"--vp-c-brand-1": "#cf3a13"
63+
"--vp-c-brand-2": "#b13110"
64+
"--vp-c-brand-3": "#92290d"
65+
dark:
66+
"--vp-c-brand-1": "#fbbf24"
67+
"--vp-c-brand-2": "#f59e0b"
68+
"--vp-c-brand-3": "#d97706"
69+
70+
exclude:
71+
- Gemfile
72+
- Gemfile.lock
73+
- vendor/
74+
- node_modules/
75+
- README.md

website/_data/navigation.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
- title: Introduction
2+
url: /introduction/getting-started/
3+
collections: [introduction]
4+
- title: Guides
5+
url: /guides/components/
6+
collections: [guides]
7+
- title: Reference
8+
url: /reference/stimulus-dsl/
9+
collections: [reference]
10+
- title: Changelog
11+
url: https://github.com/stevegeek/vident/blob/main/CHANGELOG.md

website/_data/sidebar.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
- title: Introduction
2+
collection: introduction
3+
- title: Guides
4+
collection: guides
5+
- title: Reference
6+
collection: reference

website/_data/social_links.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
- icon: github
2+
link: https://github.com/stevegeek/vident
3+
label: Vident on GitHub
4+
- icon: rubygems
5+
link: https://rubygems.org/gems/vident
6+
label: Vident on RubyGems

website/_guides/caching.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
title: Component caching
3+
nav_order: 5
4+
---
5+
6+
# Component caching
7+
8+
Vident exposes a `cache_component` helper on both the Phlex and
9+
ViewComponent adapters. It's a thin wrapper around Rails' fragment
10+
caching that scopes the cache key to the component, so expensive renders
11+
become a single cache lookup once warmed.
12+
13+
```ruby
14+
class ProductCardComponent < Vident::ViewComponent::Base
15+
prop :product, Product
16+
17+
def call
18+
cache_component(@product) do
19+
root_element do |card|
20+
# …expensive markup…
21+
end
22+
end
23+
end
24+
end
25+
```
26+
27+
The cache key is composed from the component class, its props (or an
28+
explicit dependency you pass in), and the request-scoped ID seed, so:
29+
30+
- Two requests rendering the same component with the same props share a
31+
cache entry.
32+
- Within a single request, repeated renders of a cached fragment keep
33+
consistent element IDs (the seed makes them deterministic), so
34+
Stimulus controllers wire up correctly to the rehydrated HTML.
35+
36+
### Tips
37+
38+
- Pass an explicit cache dependency (`cache_component([@product, @user])`)
39+
when the component reads things outside its props.
40+
- Stimulus values that read from procs evaluate **before** the cache lookup,
41+
so they're stable across cache hits.
42+
- For per-user output that should not be cached together, include the user
43+
(or their role) in the cache key.
44+
45+
For the broader cache key story and the seeding rationale, see
46+
[Element IDs and seeding](/reference/element-ids/).

0 commit comments

Comments
 (0)