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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ A `DocsUI::` Phlex kit, configured once per site:
| `DocsUI::Page` | Base class for a hand-authored doc page; renders inside `DocsUI::Shell`. |
| `DocsUI::Header` / `Section` / `Prose` / `Callout` | The page-authoring kit. |
| `DocsUI::Markdown` | GFM Markdown island — prose as Markdown, styled like `Prose`, fenced code through Rouge. |
| `DocsUI::Table` / `PropTable` | Reference tables — generic headers+rows, and a name/type/default/description preset. |
| `DocsUI::Example` | Base for a live example with `method_source`-extracted source. |

Plus `DocsKit::Registry` (in-memory docs registry mixin), `DocsKit::NavItem`
Expand Down
24 changes: 23 additions & 1 deletion app/components/docs_ui/markdown.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,24 @@ class Markdown < Phlex::HTML
TABLE_WRAPPER = "not-prose my-4 overflow-x-auto rounded-box border border-base-300"
TABLE_CLASSES = "table table-sm table-zebra"

def initialize(source)
# Render source as INLINE markdown: no Prose wrapper div, and a single
# top-level paragraph is unwrapped so its inline children (strong/em/code/
# link) sit directly in the surrounding element. Used for a [:md, "…"] table
# cell — the cell's <td> is the container, so a block <p>/typography div would
# be wrong there.
def self.inline(source) = new(source, inline: true)

def initialize(source, inline: false)
# commonmarker v2 raises unless the text is UTF-8. Author heredocs already
# are, but nil.to_s / a US-ASCII string would crash the render — normalize
# at the boundary so any input parses.
@source = source.to_s.encode(Encoding::UTF_8)
@inline = inline
end

def view_template
return visit_inline(document) if @inline

div(class: CLASSES) { visit(document) }
end

Expand All @@ -51,6 +61,18 @@ def document
Commonmarker.parse(@source)
end

# Inline render: unwrap each top-level paragraph to its inline children (no
# <p>), so `[:md, "a **note**"]` becomes `a <strong>note</strong>` inside the
# cell rather than a block paragraph. Non-paragraph blocks (a stray list) still
# render as themselves. Adjacent blocks get a joining space so unwrapping never
# fuses text ("one" + "two" → "one two", not "onetwo").
def visit_inline(node)
node.each_with_index do |child, i|
whitespace unless i.zero?
child.type == :paragraph ? visit_children(child) : visit(child)
end
end

# Emit each child of a node in order.
def visit_children(node)
node.each { |child| visit(child) }
Expand Down
43 changes: 43 additions & 0 deletions app/components/docs_ui/prop_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

module DocsUI
# A props/options/params reference table: name · type · default · description,
# with the first column (the name) auto code-styled. A thin preset over
# DocsUI::Table — same cell conventions, same markup, no duplication.
#
# render DocsUI::PropTable.new(
# [
# ["brand", "String", '"Docs"', "Topbar + sidebar heading."],
# ["themes", "Array", "%w[dark light]", "ThemeSwitcher options."],
# ]
# )
#
# The default headers are Option/Type/Default/Description; pass `headers:` to
# override (e.g. `%w[Arg Type Default Description]` for a component's args). Cell
# values follow DocsUI::Table's convention (String / [:code, "x"] / [:md, "…"]);
# the first cell of each row is wrapped in <code> automatically unless it's
# already a special-cell pair.
class PropTable < Phlex::HTML
DEFAULT_HEADERS = %w[Option Type Default Description].freeze

def initialize(rows, headers: DEFAULT_HEADERS)
@rows = rows.map { |cells| code_first_column(cells) }
@headers = headers
end

def view_template
render DocsUI::Table.new(@headers, @rows)
end

private

# Auto-code-style the name column. A plain String first cell becomes a
# [:code, …] cell; a cell that's already a typed pair ([:code, …]/[:md, …]) is
# left as the author wrote it.
def code_first_column(cells)
first, *rest = cells
first = [:code, first] unless first.is_a?(Array)
[first, *rest]
end
end
end
61 changes: 61 additions & 0 deletions app/components/docs_ui/table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

module DocsUI
# A generic reference table — headers + rows — in the kit's daisyUI look (a
# `table table-sm table-zebra` inside a `rounded-box` border, `not-prose` so the
# surrounding Prose typography doesn't restyle it). This is the piece every docs
# site was hand-rolling; compose it, don't write raw `table`/`tr`/`td` markup.
#
# render DocsUI::Table.new(
# ["Option", "Type", "Default", "Description"],
# [
# ["brand", "String", '"Docs"', "Topbar + sidebar heading."],
# ["themes", [:code, "%w[dark light]"], "—", "ThemeSwitcher options."],
# ]
# )
#
# Cell values (the same convention the dogfood PropTable proved):
#
# * String → plain text (Phlex-escaped; HTML in it is inert, never live)
# * [:code, "x"] → inline <code> (for a type, a default literal, an identifier)
# * [:md, "…"] → inline GFM through DocsUI::Markdown (bold/links/inline code),
# opt-in so a plain String that merely *looks* like markdown
# is never surprise-parsed.
#
# PropTable is a thin preset over this (name/type/default/description, first
# column auto-code-styled).
class Table < Phlex::HTML
WRAPPER = "not-prose my-4 overflow-x-auto rounded-box border border-base-300"
TABLE = "table table-sm table-zebra"

def initialize(headers, rows)
@headers = headers
@rows = rows
end

def view_template
div(class: WRAPPER) do
table(class: TABLE) do
thead do
tr { @headers.each { |header| th(class: "whitespace-nowrap") { plain header.to_s } } }
end
tbody do
@rows.each { |cells| tr { cells.each { |cell| td { render_cell(cell) } } } }
end
end
end
end

private

# Dispatch a cell by its shape. A [type, value] pair selects an inline
# renderer; anything else is plain, Phlex-escaped text.
def render_cell(cell)
case cell
in [:code, value] then code(class: "text-sm") { plain value.to_s }
in [:md, value] then render DocsUI::Markdown.inline(value.to_s)
else plain cell.to_s
end
end
end
end
6 changes: 3 additions & 3 deletions docs/app/views/docs/pages/authoring.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,16 @@ class Doc
def building_blocks_section
DocsUI::Section("The building blocks",
description: "The DocsUI kit you compose inside #content.") do
render PropTable.new(
[ "Helper", "Use for" ],
render DocsUI::PropTable.new(
[
[ "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" ]
]
],
headers: [ "Helper", "Use for" ]
)

prose do
Expand Down
Loading
Loading