diff --git a/README.md b/README.md index 7e509de..aa8d170 100644 --- a/README.md +++ b/README.md @@ -91,9 +91,9 @@ class Views::Docs::Pages::Installation < DocsUI::Page def lead = "Add the gem and render your first component." def content - render DocsUI::Section.new("Add the gem") do - render DocsUI::Prose.new { p { "Components are plain Ruby classes." } } - render DocsUI::Code.new(<<~RUBY, filename: "Gemfile") + DocsUI::Section("Add the gem") do + prose { p { "Components are plain Ruby classes." } } + DocsUI::Code(<<~RUBY, filename: "Gemfile") gem "docs-kit" RUBY end @@ -101,6 +101,37 @@ class Views::Docs::Pages::Installation < DocsUI::Page end ``` +`DocsUI::Page` includes the kit, so inside `#content` you call the components +directly — `DocsUI::Section(...)`, `DocsUI::Code(...)` — no `render … .new`. + +### The authoring convention + +One rule covers the whole kit: **the primary argument is positional; modifiers +are keyword arguments.** + +```ruby +DocsUI::Header("Installation", eyebrow: "Guide") # title positional +DocsUI::Section("Add the gem", id: "add", description: …) # title positional +DocsUI::Code(source, lexer: :ruby, filename: "Gemfile") # source positional +``` + +For the two wrappers that take **no** positional argument — prose and a +multi-language example — `DocsUI::Page` gives you lowercase helpers so a block +needs no parens: + +```ruby +prose { p { "Hand-authored prose." } } # → DocsUI::Prose +example { |ex| ex.code(:ruby) { source } } # → DocsUI::Example +md(<<~'MD') # → DocsUI::Markdown + A block of **Markdown**. +MD +``` + +The kit forms `DocsUI::Prose() { … }` / `DocsUI::Example() { … }` still work — +they just need the empty `()`, because a bare `DocsUI::Prose do … end` parses as +a constant reference (a Ruby `SyntaxError`). The lowercase helpers sidestep that +entirely, so they're the everyday path. + ## Authoring with Markdown Prose is the most-written content type — and the noisiest to hand-build from @@ -138,9 +169,8 @@ hand-written `DocsUI::Code`; an unknown fence language falls back to plaintext. Two things to know: -- **`md` is a lowercase method, so `md <<~MD … MD` needs no parens** — unlike - `DocsUI::Prose()` / `DocsUI::Example()`, which take a block and so require the - empty-parens form. +- **`md` is a lowercase page helper (like `prose`/`example`), so `md <<~MD … MD` + needs no parens** — see [the authoring convention](#the-authoring-convention). - **Use a single-quoted heredoc, `<<~'MD'`.** Then `#{…}` in your prose is literal text (Phlex escapes author text — no `html_safe`, no interpolation). diff --git a/app/components/docs_ui/header.rb b/app/components/docs_ui/header.rb index 133814c..be3441e 100644 --- a/app/components/docs_ui/header.rb +++ b/app/components/docs_ui/header.rb @@ -4,12 +4,18 @@ module DocsUI # A doc page header: an optional eyebrow (kicker), the title, and a lead # paragraph. Gives every doc page a consistent masthead. # - # render DocsUI::Header.new(title: "Installation", eyebrow: "Guide") do + # render DocsUI::Header.new("Installation", eyebrow: "Guide") do # plain "Add the gem and render your first component." # end + # + # The primary argument (the title) is positional, matching Section/Code and the + # kit-wide convention. The legacy `title:` kwarg still works so existing sites + # keep rendering unchanged; the positional wins if both are given. class Header < Phlex::HTML - def initialize(title:, eyebrow: nil) - @title = title + # Positional title (the convention), with a silent `title:` kwarg fallback for + # sites that still pass it by keyword. Positional wins when both are given. + def initialize(title = nil, eyebrow: nil, **opts) + @title = title || opts[:title] @eyebrow = eyebrow end diff --git a/app/components/docs_ui/page.rb b/app/components/docs_ui/page.rb index 1f9ff6d..8d362d8 100644 --- a/app/components/docs_ui/page.rb +++ b/app/components/docs_ui/page.rb @@ -19,6 +19,9 @@ class Page < Phlex::HTML # Authored pages subclass this, so include the kit here: a page body can call # DocsUI::Section(...) / DocsUI::Code(...) directly, no render ... .new. include DocsUI + # The lowercase, block-friendly authoring helpers (md/prose/example) — the + # friction-free path that never trips the parens-with-blocks gotcha. + include DocsUI::PageHelpers class << self def title(value = nil) @@ -46,7 +49,7 @@ def view_template a(href: root_path, class: "link link-hover text-sm opacity-70") { "← Home" } end - render DocsUI::Header.new(title: self.class.title, eyebrow: self.class.eyebrow) do + render DocsUI::Header.new(self.class.title, eyebrow: self.class.eyebrow) do plain lead if lead end @@ -54,14 +57,9 @@ def view_template end end - # Render a block of GFM Markdown as Prose-styled prose (see DocsUI::Markdown). - # A lowercase method + heredoc sidesteps the parens-with-blocks gotcha: - # md <<~'MD' - # Write **prose** as Markdown. Single-quoted heredoc so #{} stays literal. - # MD - def md(source) - render DocsUI::Markdown.new(source) - end + # The lowercase authoring helpers md/prose/example come from DocsUI::PageHelpers + # (included above) — the parens-free path that never hits the constant-reference + # SyntaxError. The kit forms (DocsUI::Prose(), DocsUI::Example()) stay valid too. # Override in subclasses for the lead paragraph (optional). def lead = nil diff --git a/app/components/docs_ui/page_helpers.rb b/app/components/docs_ui/page_helpers.rb new file mode 100644 index 0000000..4711b17 --- /dev/null +++ b/app/components/docs_ui/page_helpers.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module DocsUI + # Lowercase, block-friendly authoring helpers mixed into DocsUI::Page. They + # exist so the everyday page body never trips the Ruby parens-with-blocks trap: + # a lowercase method call takes a block WITHOUT parens, so `prose do … end` is + # unambiguously a method call (the bare `DocsUI::Prose do … end` kit form parses + # as a constant reference — a SyntaxError). The kit forms stay valid; these are + # the friction-free path. + # + # Extracted from Page so they can be unit-tested against a bare Phlex host: + # Page itself includes Phlex::Rails::Helpers::Routes (a live Rails view context) + # and cannot load in the standalone suite. + module PageHelpers + # Render a block of GFM Markdown as Prose-styled prose (see DocsUI::Markdown). + # A lowercase method + heredoc sidesteps the parens-with-blocks gotcha: + # md <<~'MD' + # Write **prose** as Markdown. Single-quoted heredoc so #{} stays literal. + # MD + def md(source) + render DocsUI::Markdown.new(source) + end + + # Render hand-authored prose in a DocsUI::Prose wrapper. Lowercase, so it + # takes the block without parens: `prose do p { "…" } end`. + def prose(&) + render DocsUI::Prose.new(&) + end + + # Render a multi-language code group (DocsUI::Example). Lowercase, so it takes + # the block without parens: `example do |ex| ex.code(:ruby) { … } end`. + def example(&) + render DocsUI::Example.new(&) + end + end +end diff --git a/docs/app/views/docs/pages/authoring.rb b/docs/app/views/docs/pages/authoring.rb index 47b9c30..2229ed7 100644 --- a/docs/app/views/docs/pages/authoring.rb +++ b/docs/app/views/docs/pages/authoring.rb @@ -3,9 +3,8 @@ module Views module Docs module Pages - # How to write a documentation page: a Phlex class, a registry entry, and - # the DocsUI building blocks. Also covers the parens-with-blocks gotcha and - # the automatic "On this page" TOC. +# 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" @@ -16,7 +15,6 @@ def content page_is_a_class_section register_section building_blocks_section - parens_gotcha_section toc_section end @@ -39,7 +37,7 @@ def lead = "One sentence that sits under the page title." def content DocsUI::Section("First steps", description: "What this section covers.") do - DocsUI::Prose() do + prose do p { "Hand-authored prose with consistent reading rhythm." } end @@ -56,7 +54,7 @@ def content end RUBY - DocsUI::Prose() do + prose do p do code { "title" } plain " names the page, " @@ -79,7 +77,7 @@ def content def register_section DocsUI::Section("Register the page", description: "Add an entry so it appears in the nav and resolves at /docs/.") do - DocsUI::Prose() do + prose do p do plain "A page shows up once it has a row in the " code { "Doc" } @@ -116,48 +114,47 @@ def building_blocks_section render PropTable.new( [ "Helper", "Use for" ], [ - [ "DocsUI::Section", "an anchored subsection with a heading (+ optional description)" ], - [ "DocsUI::Prose()", "hand-authored prose (needs parens with a block)" ], - [ "DocsUI::Code", "a syntax-highlighted code block" ], - [ "DocsUI::Example()", "multi-language tabbed code" ], - [ "DocsUI::Callout", "note / tip / warning boxes" ] + [ "DocsUI::Section(title)", "an anchored subsection with a heading (+ optional description)" ], + [ "md(source)", "a block of GFM Markdown, styled like Prose" ], + [ "prose { … }", "hand-authored prose (p/ul/code) in a reading-rhythm wrapper" ], + [ "DocsUI::Code(source)", "a syntax-highlighted code block" ], + [ "example { |ex| … }", "multi-language tabbed code" ], + [ "DocsUI::Callout(level)", "note / tip / warning boxes" ] ] ) - end - end - - def parens_gotcha_section - DocsUI::Section("Gotcha: parens with blocks", - description: "The one syntax rule that bites everyone.") do - DocsUI::Callout(:warning) do - "DocsUI::Prose and DocsUI::Example take no positional args, so with a block you MUST write " \ - "DocsUI::Prose() do … end. The bare form parses as a constant reference — a Ruby SyntaxError." - end - - DocsUI::Code(<<~RUBY) - # Wrong — SyntaxError: `do` block reads as a constant reference. - DocsUI::Prose do - p { "..." } - end - - # Right — the parens make it a method call that takes the block. - DocsUI::Prose() do - p { "..." } - end - RUBY - DocsUI::Prose() do + prose do p do - code { "DocsUI::Section" } + plain "The primary argument is always positional — " + code { "Section(\"Title\")" } plain ", " - code { "DocsUI::Code" } - plain ", and " - code { "DocsUI::Callout" } - plain " already take arguments, so their parens are never optional — the gotcha is only " - code { "Prose" } - plain " and " - code { "Example" } - plain "." + code { "Code(source)" } + plain ", " + code { "Header(\"Title\")" } + plain " — with modifiers as keywords (" + code { "description:" } + plain ", " + code { "eyebrow:" } + plain ")." + end + p do + plain "For the wrappers that take no argument, use the lowercase page helpers " + code { "prose" } + plain " / " + code { "example" } + plain " (and " + code { "md" } + plain " for Markdown). A lowercase method takes a block without parens, so " + code { "prose do … end" } + plain " just works. The kit forms " + code { "DocsUI::Prose()" } + plain " / " + code { "DocsUI::Example()" } + plain " stay valid — they only need the empty " + code { "()" } + plain " because a bare " + code { "DocsUI::Prose do" } + plain " parses as a constant reference (a SyntaxError)." end end end @@ -166,7 +163,7 @@ def parens_gotcha_section def toc_section DocsUI::Section("The \"On this page\" TOC", description: "Built for you from your section headings.") do - DocsUI::Prose() do + prose do p do plain "Every " code { "DocsUI::Section" } @@ -187,7 +184,7 @@ class Views::Docs::Pages::Guide < DocsUI::Page end RUBY - DocsUI::Prose() do + prose do p do plain "See the " a(href: "/docs/on-this-page") { "On this page" } diff --git a/docs/app/views/docs/pages/components.rb b/docs/app/views/docs/pages/components.rb index efc0016..df00b50 100644 --- a/docs/app/views/docs/pages/components.rb +++ b/docs/app/views/docs/pages/components.rb @@ -33,7 +33,7 @@ def content def shell_section DocsUI::Section("Shell", description: "The whole HTML document you're looking at right now.") do - DocsUI::Prose() do + prose do p do code { "DocsUI::Shell" } plain " is the top-level page: the topbar (brand + " @@ -62,7 +62,7 @@ def shell_section def page_section DocsUI::Section("Page", description: "The base class you subclass for every docs page — including this one.") do - DocsUI::Prose() do + prose do p do plain "Subclass " code { "DocsUI::Page" } @@ -88,7 +88,7 @@ class Views::Docs::Pages::Guide < DocsUI::Page on_page :toggle # :panel | :toggle | :sidebar | false def lead = "One-sentence summary." - def content = DocsUI::Section("Hello") { DocsUI::Prose() { p { "..." } } } + def content = DocsUI::Section("Hello") { prose { p { "..." } } } end RUBY render PropTable.new( @@ -106,7 +106,7 @@ def content = DocsUI::Section("Hello") { DocsUI::Prose() { p { "..." } } } def header_section DocsUI::Section("Header", description: "The masthead: eyebrow + h1 + optional lead.") do - DocsUI::Prose() do + prose do p do plain "The block at the top of this page — kicker, heading, summary — is a " code { "DocsUI::Header" } @@ -122,14 +122,14 @@ def header_section end end DocsUI::Code(<<~RUBY) - DocsUI::Header(title: "My guide", eyebrow: "Reference") do + DocsUI::Header("My guide", eyebrow: "Reference") do plain "An optional lead paragraph." end RUBY render PropTable.new( [ "Arg", "Type", "Default", "Description" ], [ - [ "title", "String", "—", "The h1 text." ], + [ "title", "String (positional)", "—", "The h1 text. Legacy title: kwarg still accepted." ], [ "eyebrow", "String, nil", "nil", "Small kicker above the h1." ], [ "block", "Phlex block", "nil", "Optional lead paragraph rendered under the h1." ] ] @@ -141,7 +141,7 @@ def header_section def section_section DocsUI::Section("Section", description: "An anchored section wrapper — this description is its description: arg.") do - DocsUI::Prose() do + prose do p do plain "Every block on this page is a " code { "DocsUI::Section" } @@ -160,7 +160,7 @@ def section_section end DocsUI::Code(<<~RUBY) DocsUI::Section("Getting started", id: "start", description: "Read me first.") do - DocsUI::Prose() { p { "Section body." } } + prose { p { "Section body." } } end RUBY render PropTable.new( @@ -176,7 +176,7 @@ def section_section def prose_section DocsUI::Section("Prose", description: "A typographic wrapper for hand-authored HTML.") do - DocsUI::Prose() do + prose do p { "Prose gives hand-authored text a consistent reading rhythm without a typography plugin." } ul do li { "lists," } @@ -184,19 +184,25 @@ def prose_section li { "links — all styled." } end end - DocsUI::Prose() { p { "The call that produced the block above:" } } + prose { p { "The call that produced the block above:" } } DocsUI::Code(<<~RUBY) - DocsUI::Prose() do + prose do p { "Prose gives hand-authored text a consistent reading rhythm." } ul { li { "lists," }; li { "inline code," }; li { "links." } } end RUBY - DocsUI::Callout(:warning) do - plain "A kit call with a block needs parens: " - code { "DocsUI::Prose() do" } - plain ". Bare " + DocsUI::Callout(:tip) do + plain "On a " + code { "DocsUI::Page" } + plain " use the lowercase " + code { "prose do … end" } + plain " helper — a method call, no parens needed. The kit form " + code { "DocsUI::Prose() do … end" } + plain " also works, but bare " code { "DocsUI::Prose do" } - plain " is a Ruby SyntaxError." + plain " is a SyntaxError, so it needs the empty " + code { "()" } + plain "." end render PropTable.new( [ "Arg", "Type", "Default", "Description" ], @@ -214,7 +220,7 @@ class User < ApplicationRecord has_many :posts end RUBY - DocsUI::Prose() { p { "The call that produced the block above:" } } + prose { p { "The call that produced the block above:" } } DocsUI::Code(%(DocsUI::Code(source, lexer: :ruby, filename: "app/models/user.rb"))) render PropTable.new( [ "Arg", "Type", "Default", "Description" ], @@ -229,7 +235,7 @@ class User < ApplicationRecord def example_section DocsUI::Section("Example", description: "Multi-language tabbed code with a sticky, global language choice.") do - DocsUI::Example() do |ex| + example do |ex| ex.code(:ruby, filename: "client.rb") do %(Anthropic::Client.new.messages.create(model: "claude-opus-4-8", messages: msgs)) end @@ -237,9 +243,9 @@ def example_section %(anthropic.Anthropic().messages.create(model="claude-opus-4-8", messages=msgs)) end end - DocsUI::Prose() { p { "The call that produced the tabs above:" } } + prose { p { "The call that produced the tabs above:" } } DocsUI::Code(<<~RUBY) - DocsUI::Example() do |ex| + example do |ex| ex.code(:ruby, filename: "client.rb") { ruby_source } ex.code(:python, filename: "client.py") { python_source } end @@ -261,7 +267,7 @@ def callout_section DocsUI::Callout(:note) { "This is a note callout." } DocsUI::Callout(:tip) { "A tip callout — for handy asides." } DocsUI::Callout(:warning) { "A warning callout — for gotchas." } - DocsUI::Prose() { p { "The calls that produced the boxes above:" } } + prose { p { "The calls that produced the boxes above:" } } DocsUI::Code(<<~RUBY) DocsUI::Callout(:note) { "This is a note callout." } DocsUI::Callout(:tip) { "A tip callout." } @@ -285,7 +291,7 @@ def icon_section DocsUI::Icon("book-open", class: "size-6") DocsUI::Icon("paintbrush", class: "size-6") end - DocsUI::Prose() { p { "The calls that produced the icons above:" } } + prose { p { "The calls that produced the icons above:" } } DocsUI::Code(<<~RUBY) DocsUI::Icon("rocket", class: "size-6") DocsUI::Icon("book-open", class: "size-6") @@ -308,7 +314,7 @@ def icon_section def on_this_page_section DocsUI::Section("OnThisPage", description: "The auto-TOC — the panel Shell renders from your on_page setting.") do - DocsUI::Prose() do + prose do p do plain "The TOC is built from the page's " code { "Section" } @@ -336,7 +342,7 @@ class Views::Docs::Pages::Api < DocsUI::Page def sidebar_section DocsUI::Section("Sidebar", description: "The left nav — built from your config, rendered by Shell.") do - DocsUI::Prose() do + prose do p do plain "The sidebar is driven entirely by " code { "DocsKit.configuration.nav" } @@ -363,7 +369,7 @@ def sidebar_section def theme_switcher_section DocsUI::Section("ThemeSwitcher", description: "The theme dropdown — built from your config, rendered by Shell.") do - DocsUI::Prose() do + prose do p do plain "The dropdown in the topbar is a " code { "DocsUI::ThemeSwitcher" } diff --git a/docs/app/views/docs/pages/configuration.rb b/docs/app/views/docs/pages/configuration.rb index a4d39db..904bc0b 100644 --- a/docs/app/views/docs/pages/configuration.rb +++ b/docs/app/views/docs/pages/configuration.rb @@ -20,7 +20,7 @@ def content def configure_section DocsUI::Section("DocsKit.configure", description: "Set it once; the shared chrome reads it everywhere.") do - DocsUI::Prose() do + prose do p do plain "Everything that differs between sites lives here — " code { "brand" } @@ -87,7 +87,7 @@ def all_options_section def sidebar_nav_section DocsUI::Section("The sidebar nav", description: "A callable that maps your registry into the shared sidebar shape.") do - DocsUI::Prose() do + prose do p do plain "Set " code { "c.nav" } @@ -126,7 +126,7 @@ def sidebar_nav_section def themes_section DocsUI::Section("Themes", description: "The theme list is the contract between config and CSS.") do - DocsUI::Prose() do + prose do p do plain "The values in " code { "c.themes" } diff --git a/docs/app/views/docs/pages/deploy.rb b/docs/app/views/docs/pages/deploy.rb index 749ff8f..4f5ea7a 100644 --- a/docs/app/views/docs/pages/deploy.rb +++ b/docs/app/views/docs/pages/deploy.rb @@ -11,7 +11,7 @@ def lead = "One reusable workflow deploys every docs-kit site to Kamal + GHCR." def content DocsUI::Section("Scaffolded for you") do - DocsUI::Prose() do + prose do p do plain "The CLI writes the whole deploy: " code { "config/deploy.yml" } @@ -30,7 +30,7 @@ def content end DocsUI::Section("The reusable workflow") do - DocsUI::Prose() do + prose do p do plain "Build and deploy live " strong { "once" } @@ -61,7 +61,7 @@ def content end DocsUI::Section("Naming", description: "Use the repo name.") do - DocsUI::Prose() do + prose do p do plain "Set " code { "image" } @@ -92,7 +92,7 @@ def content [ "DEPLOY_DOMAIN", "The public host kamal-proxy routes." ] ] ) - DocsUI::Prose() do + prose do p do plain "Add these to a " code { "docs" } diff --git a/docs/app/views/docs/pages/installation.rb b/docs/app/views/docs/pages/installation.rb index 0a049ef..e194001 100644 --- a/docs/app/views/docs/pages/installation.rb +++ b/docs/app/views/docs/pages/installation.rb @@ -111,7 +111,7 @@ def requirements_section # authoring styles to prove Prose stays fully supported alongside md. def verify_section DocsUI::Section("Verify") do - DocsUI::Prose() do + prose do p do plain "Boot the app with " code { "bin/dev" } diff --git a/docs/app/views/docs/pages/languages.rb b/docs/app/views/docs/pages/languages.rb index b3d4afd..6359430 100644 --- a/docs/app/views/docs/pages/languages.rb +++ b/docs/app/views/docs/pages/languages.rb @@ -22,7 +22,7 @@ def content def multi_language_section DocsUI::Section("Multi-language examples", description: "Pick a tab — every example on the page follows.") do - DocsUI::Prose() do + prose do p do code { "DocsUI::Example" } plain " renders one example in several languages with tabs. The choice is a " @@ -34,7 +34,7 @@ def multi_language_section end end - DocsUI::Example() do |ex| + example do |ex| ex.code(:ruby, filename: "client.rb") do <<~RUBY client = Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"]) @@ -70,10 +70,10 @@ def multi_language_section end end - DocsUI::Prose() { p { "Author it by handing each language a code block:" } } + prose { p { "Author it by handing each language a code block:" } } DocsUI::Code(<<~RUBY) - DocsUI::Example() do |ex| + example do |ex| ex.code(:ruby, filename: "client.rb") { ruby_source } ex.code(:python, filename: "client.py") { python_source } ex.code(:javascript) { js_source } @@ -106,7 +106,7 @@ def hello, do: IO.puts("elixir works") def config_section DocsUI::Section("Configuring languages") do - DocsUI::Prose() do + prose do p do plain "Pass any Rouge lexer name to " code { "lexer:" } @@ -125,7 +125,7 @@ def config_section end RUBY - DocsUI::Prose() do + prose do p do plain "Now " code { "DocsUI::Code(src, lexer: :curl)" } diff --git a/docs/app/views/docs/pages/on_this_page.rb b/docs/app/views/docs/pages/on_this_page.rb index b977aba..aabe0ba 100644 --- a/docs/app/views/docs/pages/on_this_page.rb +++ b/docs/app/views/docs/pages/on_this_page.rb @@ -24,7 +24,7 @@ def content def automatic_toc_section DocsUI::Section("Automatic TOC") do - DocsUI::Prose() do + prose do p do plain "docs-kit builds an " strong { "On this page" } @@ -60,7 +60,7 @@ def default_section c.on_page_default = :panel end RUBY - DocsUI::Prose() do + prose do p do plain "Sets the placement for " strong { "every" } @@ -82,7 +82,7 @@ def content end end RUBY - DocsUI::Prose() do + prose do p do plain "Declare " code { "on_page" } @@ -104,7 +104,7 @@ def content def how_it_works_section DocsUI::Section("How it works") do - DocsUI::Prose() do + prose do p do plain "The TOC is pure client-side. The docs-nav Stimulus controller reads " code { "section[id]" } diff --git a/docs/app/views/docs/pages/overview.rb b/docs/app/views/docs/pages/overview.rb index 8471fcd..589613a 100644 --- a/docs/app/views/docs/pages/overview.rb +++ b/docs/app/views/docs/pages/overview.rb @@ -21,7 +21,7 @@ def content def what_is_section DocsUI::Section("What is docs-kit", description: "A gem, not a template.") do - DocsUI::Prose() do + prose do p do strong { "docs-kit" } plain " is a Ruby gem that gives you the shared chrome for a Rails " @@ -42,7 +42,7 @@ def what_is_section def mental_model_section DocsUI::Section("The mental model", description: "Configure the chrome; don't re-author it.") do - DocsUI::Prose() do + prose do p do plain "The chrome — " code { "Shell" } @@ -78,7 +78,7 @@ def mental_model_section def what_you_get_section DocsUI::Section("What you get", description: "Everything below ships in the box.") do - DocsUI::Prose() do + prose do ul do li do strong { "Shared shell + responsive sidebar" } @@ -120,7 +120,7 @@ def what_you_get_section def next_steps_section DocsUI::Section("Next steps") do - DocsUI::Prose() do + prose do p do plain "Start with " strong { "Installation" } diff --git a/docs/app/views/docs/pages/styling.rb b/docs/app/views/docs/pages/styling.rb index 806cb89..3d9f532 100644 --- a/docs/app/views/docs/pages/styling.rb +++ b/docs/app/views/docs/pages/styling.rb @@ -11,7 +11,7 @@ def lead = "Each site builds its own Tailwind + daisyUI stylesheet so the chrome def content DocsUI::Section("The canonical build", description: "docs-kit ships no compiled CSS — you build it.") do - DocsUI::Prose() do + prose do p do plain "docs-kit ships " strong { "no compiled CSS" } @@ -38,7 +38,7 @@ def content end DocsUI::Section("application.tailwind.css", description: "Your Tailwind entry point wires up daisyUI and the sources.") do - DocsUI::Prose() do + prose do p do plain "The " code { "themes:" } @@ -66,7 +66,7 @@ def content end DocsUI::Section("Adding a theme") do - DocsUI::Prose() do + prose do p { "Themes come from daisyUI. To add one:" } ol do li do @@ -102,7 +102,7 @@ def content end DocsUI::Section("Custom styles") do - DocsUI::Prose() do + prose do p do plain "Add your own CSS below the imports in " code { "application.tailwind.css" } diff --git a/spec/docs_ui/header_spec.rb b/spec/docs_ui/header_spec.rb new file mode 100644 index 0000000..1483d55 --- /dev/null +++ b/spec/docs_ui/header_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +RSpec.describe DocsUI::Header do + # Render the Header inside a host Phlex component so the (optional) lead block + # runs in a Phlex context, as it would on a real page. + def render_header(*args, lead: nil, **kwargs) + header_args = args + header_kwargs = kwargs + lead_text = lead + Class.new(Phlex::HTML) do + define_method(:view_template) do + if lead_text + render DocsUI::Header.new(*header_args, **header_kwargs) { plain lead_text } + else + render DocsUI::Header.new(*header_args, **header_kwargs) + end + end + end.new.call + end + + it "renders the title from a positional argument" do + html = render_header("Installation") + + expect(html).to include("Installation") + end + + it "renders the eyebrow kicker above the title" do + html = render_header("Installation", eyebrow: "Guide") + + expect(html).to include(">Guide") + expect(html.index(">Guide")).to be < html.index(">Installation") + end + + it "renders the block as the lead paragraph under the h1" do + html = render_header("Installation", lead: "Add the gem.") + + expect(html).to include("Add the gem.") + expect(html.index(">Installation")).to be < html.index("Add the gem.") + end + + # Backwards compatibility: existing sites (the gem Page base + both consumer + # sites) pass the title as a kwarg. That MUST keep working, silently. + context "when the title is passed as the legacy title: kwarg" do + it "renders the same h1 as the positional form" do + positional = render_header("Installation") + legacy = render_header(title: "Installation") + + expect(legacy).to include(">Installation") + expect(legacy).to eq(positional) + end + + it "still honors the eyebrow kwarg alongside title:" do + html = render_header(title: "Installation", eyebrow: "Guide") + + expect(html).to include(">Guide") + expect(html).to include(">Installation") + end + end + + # The convention: primary arg is positional. If a caller mixes both forms, + # the positional wins (it is the documented primary path). + context "when both a positional title and a title: kwarg are given" do + it "the positional title wins" do + html = render_header("Positional", title: "Kwarg") + + expect(html).to include(">Positional") + expect(html).not_to include(">Kwarg") + end + end +end diff --git a/spec/docs_ui/markdown_spec.rb b/spec/docs_ui/markdown_spec.rb index ceb405f..43084a4 100644 --- a/spec/docs_ui/markdown_spec.rb +++ b/spec/docs_ui/markdown_spec.rb @@ -171,17 +171,17 @@ def render_md(source) expect(html).not_to include("one") + # It is the Prose wrapper (its typographic child-selector classes), not a + # bare div. + expect(html).to include("[&_p]:my-4") + end + end + + describe "#example" do + it "renders an Example (multi-language tabs) from a parens-free block" do + html = render_body do + # `example` here is the PageHelpers method under test, not RSpec's example. + example do |ex| # rubocop:disable RSpec/NoExpectationExample + ex.code(:ruby, filename: "client.rb") { "Anthropic.new" } + ex.code(:python, filename: "client.py") { "anthropic.Client()" } + end + end + + expect(html).to include('data-testid="code-lang-ruby"') + expect(html).to include('data-testid="code-lang-python"') + expect(html.scan('data-docs-nav-target="codePanel"').size).to eq(2) + end + end + + describe "#md" do + it "renders Prose-styled Markdown from a page body" do + html = render_body { md("A **markdown** paragraph.") } + + expect(html).to include("text-base-content/80") # the Prose wrapper classes + expect(html).to include("markdown") + end + end +end