Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 75 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,31 @@ DocsKit.configure do |c|
c.title_suffix = "phlex-reactive"
c.themes = %w[dark light synthwave retro cyberpunk dracula night nord sunset]
c.version_badge = -> { "v#{Phlex::Reactive::VERSION}" } # optional
c.nav = lambda do
{
"Demos" => Demo.grouped.transform_values { |demos|
demos.map { |d| DocsKit::NavItem.new(href: "/demos/#{d.slug}", label: d.title, icon: d.icon) }
},
"Docs" => Doc.all.select(&:view_class).group_by(&:group).transform_values { |docs|
docs.map { |d| DocsKit::NavItem.new(href: "/docs/#{d.slug}", label: d.title) }
}
}
end

# The sidebar derives from your registries — one heading → one registry.
c.nav_registries = { "Docs" => Doc }
end
```

The nav is **derived from the registry**, so you never hand-write it. Each
registry maps a heading to its authored pages (`Doc.nav_items`); a page that
isn't written yet is skipped, so there are no dead links. Register a page with
one line (see [Add a page](#add-a-page)) and it appears in the sidebar.

### Custom nav (advanced)

Sites that interleave several registries under a heading, or need custom
subgroups, set an explicit `c.nav` lambda instead — it wins over
`nav_registries`:

```ruby
c.nav = lambda do
{
"Demos" => Demo.grouped.transform_values { |demos|
demos.map { |d| DocsKit::NavItem.new(href: "/demos/#{d.slug}", label: d.title, icon: d.icon) }
},
"Docs" => Doc.nav_items
}
end
```

Expand All @@ -83,17 +98,47 @@ def show = render_page(Views::Docs::Pages::Installation.new)
view context, so CSRF, `dom_id`, url helpers, and the reactive token signer all
work inside components.

A page composes the shell + kit:
### Add a page

One command scaffolds the page class **and** its registry line, both derived
from the title:

```bash
rails g docs_kit:page "Getting Started" --group=Guide
```

That writes `app/views/docs/pages/getting_started.rb` (a `DocsUI::Page` subclass
with a starter Markdown section) and injects `page "Getting Started", group:
"Guide"` into your `Doc` registry — so the page is routed and in the sidebar the
moment you write its content. Every derivation is overridable:

```bash
rails g docs_kit:page "OAuth" --group=Guide --slug=auth --view=OauthGuide
rails g docs_kit:page "Metrics" --group=Reference --eyebrow="Advanced"
rails g docs_kit:page "Guides Intro" --group=Guide --registry=Guide # a differently-named registry
```

Re-running is idempotent (no duplicate registry line, no clobbered file). If your
registry still uses the legacy hash `entries [...]` form, the generator writes
the page but prints the entry for you to add by hand instead of corrupting it.

#### Under the hood

A page is a `DocsUI::Page` subclass — the generator just writes this for you:

```ruby
class Views::Docs::Pages::Installation < DocsUI::Page
title "Installation"
# app/views/docs/pages/getting_started.rb — Zeitwerk resolves the compact
# reference through the directory-implied namespaces (no nested modules).
class Views::Docs::Pages::GettingStarted < DocsUI::Page
title "Getting Started"
eyebrow "Guide"
def lead = "Add the gem and render your first component."

def content
DocsUI::Section("Add the gem") do
prose { p { "Components are plain Ruby classes." } }
md <<~'MD'
Components are plain Ruby classes.
MD
DocsUI::Code(<<~RUBY, filename: "Gemfile")
gem "docs-kit"
RUBY
Expand All @@ -102,6 +147,19 @@ class Views::Docs::Pages::Installation < DocsUI::Page
end
```

…plus one line in the registry (`view_namespace` lets it derive the class):

```ruby
# app/models/doc.rb
class Doc
extend DocsKit::Registry
path_prefix "/docs"
view_namespace "Views::Docs::Pages"

page "Getting Started", group: "Guide" # slug "getting-started", view "GettingStarted"
end
```

`DocsUI::Page` includes the kit, so inside `#content` you call the components
directly — `DocsUI::Section(...)`, `DocsUI::Code(...)` — no `render … .new`.

Expand Down Expand Up @@ -206,6 +264,9 @@ rails g rails_icons:sync --library=lucide
bun install && bun run build:css
```

Then add pages one command at a time — `rails g docs_kit:page "Title"
--group=Guide` (see [Add a page](#add-a-page)).

## Deploy a new docs site

The build + deploy is defined **once** in this gem's reusable workflow
Expand Down
48 changes: 18 additions & 30 deletions docs/app/models/doc.rb
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
# frozen_string_literal: true

# In-memory registry of the reference docs. Each entry maps a URL slug to its
# title, sidebar group, and the Phlex page class that renders it. Add a page by
# adding an entry here and a class under app/views/docs/pages/.
# In-memory registry of the reference docs. One line per page — slug and view
# derive from the title (both overridable), and the sidebar nav derives from
# this registry with zero extra code (see config/initializers/docs_kit.rb's
# `nav_registries`). Add a page with `rails g docs_kit:page "Title" --group=…`,
# which appends the `page` line here and writes the class under
# app/views/docs/pages/.
#
# Uses DocsKit::Registry for the shared all/from_slug/grouped API.
# Uses DocsKit::Registry for the shared all/from_slug/grouped/nav_items API.
class Doc
extend DocsKit::Registry
path_prefix "/docs"
view_namespace "Views::Docs::Pages"

entries [
{ slug: "overview", title: "Overview", group: "Getting started", view: "Overview" },
{ slug: "installation", title: "Installation", group: "Getting started", view: "Installation" },
{ slug: "configuration", title: "Configuration", group: "Getting started", view: "Configuration" },
{ slug: "authoring", title: "Authoring pages", group: "Getting started", view: "Authoring" },
{ slug: "styling", title: "Styling & CSS", group: "Getting started", view: "Styling" },
{ slug: "components", title: "Components", group: "Reference", view: "Components" },
{ slug: "languages", title: "Code languages", group: "Reference", view: "Languages" },
{ slug: "on-this-page", title: "On this page", group: "Reference", view: "OnThisPage" },
{ slug: "deploy", title: "Deploy", group: "Reference", view: "Deploy" }
]

attr_reader :slug, :title, :group, :view_name

def initialize(entry)
@slug = entry[:slug]
@title = entry[:title]
@group = entry[:group]
@view_name = entry[:view]
end

# The hand-authored Phlex page class (nil until the class exists — the sidebar
# only links docs whose page is written, so no dead links).
def view_class
"Views::Docs::Pages::#{view_name}".safe_constantize
end
page "Overview", group: "Getting started"
page "Installation", group: "Getting started"
page "Configuration", group: "Getting started"
page "Authoring pages", group: "Getting started", slug: "authoring", view: "Authoring"
page "Styling & CSS", group: "Getting started", slug: "styling", view: "Styling"
page "Components", group: "Reference"
page "Code languages", group: "Reference", slug: "languages", view: "Languages"
page "On this page", group: "Reference"
page "Deploy", group: "Reference"
end
112 changes: 69 additions & 43 deletions docs/app/views/docs/pages/authoring.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
module Views
module Docs
module Pages
# How to write a documentation page: a Phlex class, a registry entry, and
# How to write a documentation page: a Phlex class, a registry entry, and
# the DocsUI building blocks, plus the automatic "On this page" TOC.
class Authoring < DocsUI::Page
title "Authoring pages"
eyebrow "Getting started"

def lead = "Write a page as a Phlex class, register it, and the shell, masthead, and TOC come free."
def lead = "One command scaffolds a page — the class and its registry line. Then write content; the shell, masthead, and TOC come free."

def content
one_command_section
page_is_a_class_section
register_section
building_blocks_section
Expand All @@ -20,35 +21,60 @@ def content

private

def one_command_section
DocsUI::Section("One command",
description: "rails g docs_kit:page writes the class AND registers it — both derived from the title.") do
DocsUI::Code(<<~SHELL, lexer: :shell)
rails g docs_kit:page "Getting Started" --group=Guide
SHELL

md <<~'MD'
That writes `app/views/docs/pages/getting_started.rb` (slug
`getting-started`, class `GettingStarted`) and injects
`page "Getting Started", group: "Guide"` into the `Doc` registry, so
the page is routed and in the sidebar the moment you fill in
`#content`. Every derivation is overridable:

- `--slug=auth` — the URL slug,
- `--view=OauthGuide` — the class basename,
- `--eyebrow="Advanced"` — the eyebrow (defaults to the group),
- `--registry=Guide` — a differently-named registry class.

Re-running is idempotent, and a legacy hash-`entries` registry is
left untouched (the generator prints the entry to add by hand).
MD

DocsUI::Callout(:tip) do
"The rest of this page is what the generator produces — the shape to reach for when you hand-write or edit a page."
end
end
end

def page_is_a_class_section
DocsUI::Section("A page is a Phlex class",
description: "Subclass DocsUI::Page, declare its metadata, fill in #content.") do
DocsUI::Code(<<~RUBY, filename: "app/views/docs/pages/guide.rb")
# frozen_string_literal: true

module Views
module Docs
module Pages
class Guide < DocsUI::Page
title "Guide"
eyebrow "Getting started"

def lead = "One sentence that sits under the page title."

def content
DocsUI::Section("First steps", description: "What this section covers.") do
prose do
p { "Hand-authored prose with consistent reading rhythm." }
end

DocsUI::Code(<<~SOURCE, filename: "config/routes.rb")
Rails.application.routes.draw do
mount DocsKit::Engine, at: "/docs"
end
SOURCE
end
# Compact class reference — Zeitwerk resolves it through the
# directory-implied namespaces, so no nested-module ceremony.
class Views::Docs::Pages::Guide < DocsUI::Page
title "Guide"
eyebrow "Getting started"

def lead = "One sentence that sits under the page title."

def content
DocsUI::Section("First steps", description: "What this section covers.") do
md <<~'MD'
Prose written as Markdown, styled with the reading rhythm.
MD

DocsUI::Code(<<~SOURCE, filename: "config/routes.rb")
Rails.application.routes.draw do
mount DocsKit::Engine, at: "/docs"
end
end
SOURCE
end
end
end
Expand Down Expand Up @@ -76,34 +102,34 @@ def content

def register_section
DocsUI::Section("Register the page",
description: "Add an entry so it appears in the nav and resolves at /docs/<slug>.") do
prose do
p do
plain "A page shows up once it has a row in the "
code { "Doc" }
plain " registry. The "
code { "view:" }
plain " maps to your class name under "
code { "Views::Docs::Pages" }
plain "; the "
code { "group:" }
plain " sets its sidebar heading."
end
end
description: "One line in the Doc registry — slug and view derive from the title.") do
md <<~'MD'
A page shows up once it has a `page` line in the `Doc` registry.
`slug` and `view` derive from the title (both overridable per line),
and `group:` sets its sidebar heading. The generator injects this
line for you.
MD

DocsUI::Code(<<~RUBY, filename: "app/models/doc.rb")
class Doc
extend DocsKit::Registry
path_prefix "/docs"
view_namespace "Views::Docs::Pages"

entries [
{ slug: "overview", title: "Overview", group: "Getting started", view: "Overview" },
{ slug: "guide", title: "Guide", group: "Getting started", view: "Guide" }
]
page "Overview", group: "Getting started"
page "Guide", group: "Getting started"
# overrides win: page "OAuth", group: "Guide", slug: "auth", view: "OauthGuide"
end
RUBY

md <<~'MD'
The sidebar derives from the registry — set
`c.nav_registries = { "Docs" => Doc }` in the initializer and never
hand-write a nav lambda again.
MD

DocsUI::Callout(:note) do
"The sidebar only links a page whose class exists, so an entry without its class yet is a no-op — no dead links."
"The sidebar only links a page whose class exists, so a page line without its class yet is a no-op — no dead links."
end
end
end
Expand Down
12 changes: 5 additions & 7 deletions docs/config/initializers/docs_kit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@
# c.code_lexer_aliases = { curl: "console" }
# c.code_language_labels = { elixir: "Elixir" }

# The sidebar nav: an ordered { "Heading" => { "Subgroup" => [NavItem] } }.
c.nav = lambda do
docs = Doc.all.select(&:view_class).group_by(&:group).transform_values do |items|
items.map { |d| DocsKit::NavItem.new(href: "/docs/#{d.slug}", label: d.title) }
end
{ "Docs" => docs }.reject { |_, v| v.empty? }
end
# The sidebar nav derives from the registry — one heading → one registry.
# Each registry's authored pages become NavItems automatically (an unwritten
# page is skipped, so no dead links). For bespoke nav (interleaved
# registries, custom subgroups) set a `c.nav` lambda instead; it wins.
c.nav_registries = { "Docs" => Doc }
end
end
Loading
Loading