docs-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/LICENSE.txt +21 -0
- data/README.md +939 -0
- data/app/components/docs_ui/brand_mark.rb +88 -0
- data/app/components/docs_ui/callout.rb +37 -0
- data/app/components/docs_ui/code.rb +123 -0
- data/app/components/docs_ui/endpoint.rb +44 -0
- data/app/components/docs_ui/error_table.rb +72 -0
- data/app/components/docs_ui/example.rb +102 -0
- data/app/components/docs_ui/field_table.rb +46 -0
- data/app/components/docs_ui/header.rb +30 -0
- data/app/components/docs_ui/icon.rb +65 -0
- data/app/components/docs_ui/json_response.rb +46 -0
- data/app/components/docs_ui/markdown.rb +187 -0
- data/app/components/docs_ui/markdown_action.rb +45 -0
- data/app/components/docs_ui/on_this_page.rb +104 -0
- data/app/components/docs_ui/open_api_operation.rb +126 -0
- data/app/components/docs_ui/page.rb +83 -0
- data/app/components/docs_ui/page_helpers.rb +52 -0
- data/app/components/docs_ui/prop_table.rb +43 -0
- data/app/components/docs_ui/prose.rb +30 -0
- data/app/components/docs_ui/request_example.rb +85 -0
- data/app/components/docs_ui/search_box.rb +106 -0
- data/app/components/docs_ui/search_results.rb +95 -0
- data/app/components/docs_ui/section.rb +94 -0
- data/app/components/docs_ui/shell.rb +161 -0
- data/app/components/docs_ui/sidebar.rb +106 -0
- data/app/components/docs_ui/table.rb +64 -0
- data/app/components/docs_ui/theme_switcher.rb +46 -0
- data/app/components/docs_ui/topbar_links.rb +42 -0
- data/app/controllers/docs_kit/llms_controller.rb +76 -0
- data/app/controllers/docs_kit/mcp_controller.rb +60 -0
- data/app/controllers/docs_kit/search_controller.rb +72 -0
- data/app/javascript/docs_kit/controllers/docs_nav_controller.js +619 -0
- data/config/importmap.rb +15 -0
- data/config/rubocop/docs_kit.yml +24 -0
- data/exe/docs-kit +80 -0
- data/lib/docs-kit.rb +5 -0
- data/lib/docs_kit/api_client.rb +52 -0
- data/lib/docs_kit/api_request.rb +66 -0
- data/lib/docs_kit/api_templates.rb +92 -0
- data/lib/docs_kit/configuration.rb +485 -0
- data/lib/docs_kit/controller.rb +47 -0
- data/lib/docs_kit/engine.rb +49 -0
- data/lib/docs_kit/llms_text.rb +105 -0
- data/lib/docs_kit/markdown_export/blocks.rb +160 -0
- data/lib/docs_kit/markdown_export/inline.rb +95 -0
- data/lib/docs_kit/markdown_export/table.rb +53 -0
- data/lib/docs_kit/markdown_export.rb +92 -0
- data/lib/docs_kit/mcp_server.rb +128 -0
- data/lib/docs_kit/mcp_tools.rb +118 -0
- data/lib/docs_kit/nav_item.rb +22 -0
- data/lib/docs_kit/open_api/document.rb +91 -0
- data/lib/docs_kit/open_api/operation.rb +213 -0
- data/lib/docs_kit/open_api/schema.rb +178 -0
- data/lib/docs_kit/open_api.rb +55 -0
- data/lib/docs_kit/registry.rb +152 -0
- data/lib/docs_kit/rubocop.rb +19 -0
- data/lib/docs_kit/search_hit.rb +28 -0
- data/lib/docs_kit/search_index/snippet.rb +65 -0
- data/lib/docs_kit/search_index.rb +169 -0
- data/lib/docs_kit/shortcut.rb +99 -0
- data/lib/docs_kit/templates/new_site.rb +175 -0
- data/lib/docs_kit/topbar_link.rb +39 -0
- data/lib/docs_kit/version.rb +5 -0
- data/lib/docs_kit.rb +72 -0
- data/lib/generators/docs_kit/install/USAGE +15 -0
- data/lib/generators/docs_kit/install/install_generator.rb +447 -0
- data/lib/generators/docs_kit/install/sync_report.rb +64 -0
- data/lib/generators/docs_kit/install/templates/agents_md.erb +105 -0
- data/lib/generators/docs_kit/install/templates/application.tailwind.css.erb +39 -0
- data/lib/generators/docs_kit/install/templates/build-css +34 -0
- data/lib/generators/docs_kit/install/templates/build_css.rake +13 -0
- data/lib/generators/docs_kit/install/templates/doc.rb.erb +17 -0
- data/lib/generators/docs_kit/install/templates/docs_controller.rb.erb +14 -0
- data/lib/generators/docs_kit/install/templates/docs_kit.rb.erb +91 -0
- data/lib/generators/docs_kit/install/templates/installation_page.rb.erb +37 -0
- data/lib/generators/docs_kit/install/templates/landing.rb.erb +25 -0
- data/lib/generators/docs_kit/install/templates/landings_controller.rb.erb +7 -0
- data/lib/generators/docs_kit/install/templates/phlex.rb.erb +14 -0
- data/lib/generators/docs_kit/install/templates/rails_icons.rb.erb +12 -0
- data/lib/generators/docs_kit/install/templates/skill.md.erb +88 -0
- data/lib/generators/docs_kit/page/USAGE +26 -0
- data/lib/generators/docs_kit/page/page_generator.rb +127 -0
- data/lib/generators/docs_kit/page/templates/page.rb.erb +21 -0
- data/lib/rubocop/cop/docs_kit/escaped_interpolation_in_heredoc.rb +119 -0
- data/lib/rubocop/cop/docs_kit/render_component_preferred.rb +123 -0
- metadata +253 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# Renders a brand/social logo as inline SVG, or falls through to a lucide icon.
|
|
5
|
+
#
|
|
6
|
+
# render DocsUI::BrandMark.new(:github, class: "size-5", label: "GitHub")
|
|
7
|
+
#
|
|
8
|
+
# lucide (the kit's synced icon set) ships NO brand logos — it dropped its brand
|
|
9
|
+
# icons, and rails_icons' brand library (simple_icons) is gone from the current
|
|
10
|
+
# `icons` gem. So the kit ships its own small, curated set of developer/social
|
|
11
|
+
# marks (BRANDS below), each an official Simple-Icons 24×24 single-path glyph.
|
|
12
|
+
#
|
|
13
|
+
# A token that IS a shipped brand (:github, :discord, …) renders that inline
|
|
14
|
+
# <svg>. Any other token is treated as a lucide icon name and delegated to
|
|
15
|
+
# DocsUI::Icon — so `icon: "book-open"` in a topbar link still works, and the
|
|
16
|
+
# brand set stays additive. The SVG is authored by the kit (a frozen constant),
|
|
17
|
+
# so `raw(safe(...))` is trusted markup, not config free text.
|
|
18
|
+
class BrandMark < Phlex::HTML
|
|
19
|
+
# token => official Simple-Icons path data (viewBox 0 0 24 24, fill rule
|
|
20
|
+
# nonzero). Sourced verbatim from simpleicons.org; each is a single <path d>.
|
|
21
|
+
# The path strings are inherently one long line each — never reflow them.
|
|
22
|
+
# rubocop:disable Layout/LineLength
|
|
23
|
+
BRANDS = {
|
|
24
|
+
github: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12",
|
|
25
|
+
gitlab: "m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z",
|
|
26
|
+
discord: "M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z",
|
|
27
|
+
x: "M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z",
|
|
28
|
+
rubygems: "M7.81 7.9l-2.97 2.95 7.19 7.18 2.96-2.95 4.22-4.23-2.96-2.96v-.01H7.8zM12 0L1.53 6v12L12 24l10.47-6V6L12 0zm8.47 16.85L12 21.73l-8.47-4.88V7.12L12 2.24l8.47 4.88v9.73z",
|
|
29
|
+
bluesky: "M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026",
|
|
30
|
+
mastodon: "M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z",
|
|
31
|
+
slack: "M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z",
|
|
32
|
+
whatsapp: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z",
|
|
33
|
+
telegram: "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z",
|
|
34
|
+
linkedin: "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z",
|
|
35
|
+
youtube: "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z",
|
|
36
|
+
reddit: "M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z",
|
|
37
|
+
stackoverflow: "M15.725 0l-1.72 1.277 6.39 8.588 1.716-1.277L15.725 0zm-3.94 3.418l-1.369 1.644 8.225 6.85 1.369-1.644-8.225-6.85zm-3.15 4.465l-.905 1.94 9.702 4.517.904-1.94-9.701-4.517zm-1.85 4.86l-.44 2.093 10.473 2.201.44-2.092-10.473-2.203zM1.89 15.47V24h19.19v-8.53h-2.133v6.397H4.021v-6.396H1.89zm4.265 2.133v2.13h10.66v-2.13H6.154Z"
|
|
38
|
+
}.freeze
|
|
39
|
+
# rubocop:enable Layout/LineLength
|
|
40
|
+
|
|
41
|
+
# True when `token` names a shipped brand mark (symbol or string), false for a
|
|
42
|
+
# lucide name, nil, or a blank string. The Shell uses this to decide whether a
|
|
43
|
+
# topbar link's icon renders as a brand <svg> or a lucide glyph.
|
|
44
|
+
def self.brand?(token)
|
|
45
|
+
return false if token.nil? || token.to_s.empty?
|
|
46
|
+
|
|
47
|
+
BRANDS.key?(token.to_sym)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# icon: a brand key (:github, …) or a lucide icon name.
|
|
51
|
+
# label: the accessible name — rendered as the SVG <title> (brand) so a
|
|
52
|
+
# screen reader announces "GitHub", never an empty graphic.
|
|
53
|
+
def initialize(icon, label: nil, **attributes)
|
|
54
|
+
@icon = icon
|
|
55
|
+
@label = label
|
|
56
|
+
@attributes = attributes
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def view_template
|
|
60
|
+
if self.class.brand?(@icon)
|
|
61
|
+
brand_svg
|
|
62
|
+
else
|
|
63
|
+
# Not a shipped brand — treat the token as a lucide icon name. Threads the
|
|
64
|
+
# same attributes (class, etc.); DocsUI::Icon degrades to nothing if the
|
|
65
|
+
# name isn't synced, exactly as elsewhere in the chrome.
|
|
66
|
+
render DocsUI::Icon.new(@icon.to_s, **@attributes)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# The inline brand <svg>. The path comes from the frozen BRANDS constant
|
|
73
|
+
# (kit-authored, never config free text), so raw(safe(...)) is trusted markup.
|
|
74
|
+
def brand_svg
|
|
75
|
+
svg(
|
|
76
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
77
|
+
viewBox: "0 0 24 24",
|
|
78
|
+
fill: "currentColor",
|
|
79
|
+
role: "img",
|
|
80
|
+
aria_label: @label,
|
|
81
|
+
**@attributes
|
|
82
|
+
) do
|
|
83
|
+
title { @label } if @label
|
|
84
|
+
raw(safe(%(<path d="#{BRANDS[@icon.to_sym]}"/>)))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# A callout box (note / tip / warning) for the docs. daisyUI alert styling + a
|
|
5
|
+
# lucide icon per level.
|
|
6
|
+
#
|
|
7
|
+
# render DocsUI::Callout.new(:warning) { "Restart the server after…" }
|
|
8
|
+
class Callout < Phlex::HTML
|
|
9
|
+
LEVELS = {
|
|
10
|
+
note: { klass: "alert-info", icon: "info" },
|
|
11
|
+
tip: { klass: "alert-success", icon: "lightbulb" },
|
|
12
|
+
warning: { klass: "alert-warning", icon: "triangle-alert" }
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def initialize(level = :note, title: nil)
|
|
16
|
+
# Normalize an unknown level to :note so both the styling and the
|
|
17
|
+
# data-md-callout export hint agree (never a bogus level name leaking out).
|
|
18
|
+
@level = LEVELS.key?(level) ? level : :note
|
|
19
|
+
@config = LEVELS[@level]
|
|
20
|
+
@title = title
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def view_template(&)
|
|
24
|
+
# data-md-callout carries the level (note/tip/warning) so
|
|
25
|
+
# DocsKit::MarkdownExport renders `> **Tip:** …` without reverse-engineering
|
|
26
|
+
# the level from the alert-* class.
|
|
27
|
+
div(class: "not-prose alert #{@config[:klass]} my-4 items-start", role: "note",
|
|
28
|
+
data: { md_callout: @level }) do
|
|
29
|
+
render DocsUI::Icon.new(@config[:icon], class: "size-5 shrink-0")
|
|
30
|
+
div do
|
|
31
|
+
div(class: "font-semibold") { @title } if @title
|
|
32
|
+
div(class: "text-sm", &)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rouge"
|
|
4
|
+
|
|
5
|
+
module DocsUI
|
|
6
|
+
# A syntax-highlighted code block for docs and demo panels. Rouge does the
|
|
7
|
+
# highlighting; an optional filename/label sits in a title bar like a real docs
|
|
8
|
+
# code sample. Self-contained: it injects its own Rouge theme CSS so no separate
|
|
9
|
+
# stylesheet asset is required.
|
|
10
|
+
#
|
|
11
|
+
# render DocsUI::Code.new(ruby_source) # ruby, no title
|
|
12
|
+
# render DocsUI::Code.new(py, lexer: :python, filename: "a.py") # any language
|
|
13
|
+
#
|
|
14
|
+
# Any language Rouge knows (~200 lexers) works by its name or alias — python,
|
|
15
|
+
# go, rust, elixir, kotlin, swift, json, dockerfile, ... — no allowlist. Add
|
|
16
|
+
# friendly lexer aliases via DocsKit.configure (code_lexer_aliases). An unknown
|
|
17
|
+
# language falls back to plaintext (never raises). (Tab labels are a
|
|
18
|
+
# DocsUI::Example concern — set via code_language_labels, not here; Code has no
|
|
19
|
+
# label, only a filename.)
|
|
20
|
+
class Code < Phlex::HTML
|
|
21
|
+
include Phlex::Rails::Helpers::ContentSecurityPolicyNonce
|
|
22
|
+
|
|
23
|
+
FORMATTER = Rouge::Formatters::HTML.new
|
|
24
|
+
|
|
25
|
+
def initialize(source, lexer: :ruby, filename: nil)
|
|
26
|
+
@source = source.to_s.strip
|
|
27
|
+
@lexer = lexer
|
|
28
|
+
@filename = filename
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def view_template
|
|
32
|
+
# Nonce the inline theme CSS so it survives a nonce-based style-src. Off a
|
|
33
|
+
# request there is no nonce (see #csp_nonce) and Phlex omits a nil-valued
|
|
34
|
+
# attribute, so the no-nonce markup is unchanged.
|
|
35
|
+
style(nonce: csp_nonce) { highlight_css }
|
|
36
|
+
resolved = lexer
|
|
37
|
+
div(class: "not-prose my-4 overflow-hidden rounded-box border border-base-300 bg-base-300/40") do
|
|
38
|
+
title_bar if @filename
|
|
39
|
+
# data-md-lang carries the RESOLVED Rouge tag (ruby/python/plaintext/…) so
|
|
40
|
+
# DocsKit::MarkdownExport emits a ```lang fence without re-resolving the
|
|
41
|
+
# language. It's the real lexer tag, not the requested alias.
|
|
42
|
+
div(class: "code-highlight overflow-x-auto p-4 text-sm leading-relaxed", data: { md_lang: resolved.tag }) do
|
|
43
|
+
pre { raw(safe(FORMATTER.format(resolved.lex(@source)))) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# The request's CSP nonce, or nil when there's no Rails view context (an
|
|
51
|
+
# isolated Phlex render, or a host that doesn't nonce style-src). The
|
|
52
|
+
# phlex-rails value helper delegates to view_context, which raises without
|
|
53
|
+
# one, so guard on its presence — a nil result makes Phlex omit the
|
|
54
|
+
# attribute, keeping the un-nonced markup unchanged.
|
|
55
|
+
def csp_nonce = view_context && content_security_policy_nonce
|
|
56
|
+
|
|
57
|
+
def title_bar
|
|
58
|
+
# data-md-skip: the title bar is chrome. MarkdownExport strips it whole
|
|
59
|
+
# before the visitor runs, so the filename never leaks into the .md twin as
|
|
60
|
+
# a stray line above the fence. The visible HTML is unaffected (DROP_SELECTOR
|
|
61
|
+
# is applied only inside #to_md).
|
|
62
|
+
div(class: "flex items-center gap-2 border-b border-base-300 bg-base-300/60 px-4 py-2",
|
|
63
|
+
data: { md_skip: true }) do
|
|
64
|
+
render DocsUI::Icon.new("file-code", class: "size-3.5 opacity-60")
|
|
65
|
+
span(class: "font-mono text-xs opacity-70") { @filename }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Resolve @lexer to a Rouge lexer instance. Order: an explicit Rouge::Lexer
|
|
70
|
+
# class/instance passed through; a configured friendly alias; Rouge's own
|
|
71
|
+
# registry (name/alias); then the configured fallback (plaintext).
|
|
72
|
+
def lexer
|
|
73
|
+
explicit_lexer || (find_lexer(@lexer.to_s) || Rouge::Lexers::PlainText).new
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A Rouge::Lexer instance passed directly (class or instance), else nil.
|
|
77
|
+
def explicit_lexer
|
|
78
|
+
return @lexer if @lexer.is_a?(Rouge::Lexer)
|
|
79
|
+
return @lexer.new if @lexer.is_a?(Class) && @lexer < Rouge::Lexer
|
|
80
|
+
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Find a lexer CLASS by name: configured alias → Rouge registry → fallback.
|
|
85
|
+
def find_lexer(name)
|
|
86
|
+
config = DocsKit.configuration
|
|
87
|
+
aliased = config.lexer_aliases[name.to_sym]
|
|
88
|
+
(aliased && Rouge::Lexer.find(aliased.to_s)) ||
|
|
89
|
+
Rouge::Lexer.find(name) ||
|
|
90
|
+
Rouge::Lexer.find(config.code_lexer_fallback.to_s)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Static Rouge theme CSS — no user input. Phlex safe(), not html_safe.
|
|
94
|
+
#
|
|
95
|
+
# The base (light) theme is emitted un-scoped so it applies to every theme.
|
|
96
|
+
# When a dark theme is configured (config.code_theme_dark), its CSS is
|
|
97
|
+
# additionally emitted scoped under [data-theme=X] .code-highlight for each
|
|
98
|
+
# shipped dark theme, so daisyUI's more-specific [data-theme] selector wins
|
|
99
|
+
# and code blocks restyle with the switcher — CSS-only, no JS, no flash.
|
|
100
|
+
# With no dark theme configured this reduces to the original single-theme
|
|
101
|
+
# output byte-for-byte (backwards compatible).
|
|
102
|
+
def highlight_css
|
|
103
|
+
theme = DocsKit.configuration.code_theme_class
|
|
104
|
+
raw(safe(<<~CSS))
|
|
105
|
+
#{theme.render(scope: '.code-highlight')}#{dark_highlight_css}
|
|
106
|
+
.code-highlight pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
|
|
107
|
+
CSS
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The dark theme's CSS, one block per shipped dark theme, each scoped under
|
|
111
|
+
# [data-theme=X] .code-highlight. Empty string when no dark theme is
|
|
112
|
+
# configured (or no shipped theme is dark) so #highlight_css is unchanged.
|
|
113
|
+
def dark_highlight_css
|
|
114
|
+
config = DocsKit.configuration
|
|
115
|
+
dark = config.code_theme_dark_class
|
|
116
|
+
return "" if dark.nil?
|
|
117
|
+
|
|
118
|
+
config.dark_themes_shipped.map do |name|
|
|
119
|
+
"\n#{dark.render(scope: "[data-theme=#{name}] .code-highlight")}"
|
|
120
|
+
end.join
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# An HTTP endpoint reference line — a method badge followed by the path — in the
|
|
5
|
+
# kit's daisyUI look. This is the `code(class: "badge …")` lambda every API page
|
|
6
|
+
# was hand-rolling; compose it instead.
|
|
7
|
+
#
|
|
8
|
+
# render DocsUI::Endpoint.new(:post, "/v1/messages")
|
|
9
|
+
# # => POST /v1/messages (POST as a primary badge, path monospace)
|
|
10
|
+
#
|
|
11
|
+
# It renders INLINE (no block wrapper), so it drops straight into a Section
|
|
12
|
+
# description or a run of prose:
|
|
13
|
+
#
|
|
14
|
+
# DocsUI::Section("Create a message", description: DocsUI::Endpoint.new(:post, "/v1/messages"))
|
|
15
|
+
#
|
|
16
|
+
# The verb → badge-colour map is an explicit frozen Hash of LITERAL class
|
|
17
|
+
# strings so the Tailwind scan (which reads the gem's Ruby) sees every badge
|
|
18
|
+
# class and generates it. An unknown verb falls back to a neutral badge and
|
|
19
|
+
# never raises — a typo degrades gracefully rather than blowing up a render.
|
|
20
|
+
class Endpoint < Phlex::HTML
|
|
21
|
+
# Each value is a single literal string (not interpolated) so Tailwind's
|
|
22
|
+
# source scan generates the colour. Keep these literal — see Critical Rule 6.
|
|
23
|
+
BADGE_CLASSES = {
|
|
24
|
+
"GET" => "badge badge-sm badge-success",
|
|
25
|
+
"POST" => "badge badge-sm badge-primary",
|
|
26
|
+
"PUT" => "badge badge-sm badge-warning",
|
|
27
|
+
"PATCH" => "badge badge-sm badge-warning",
|
|
28
|
+
"DELETE" => "badge badge-sm badge-error"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
NEUTRAL_BADGE = "badge badge-sm badge-neutral"
|
|
32
|
+
|
|
33
|
+
def initialize(method, path)
|
|
34
|
+
@method = method.to_s.upcase
|
|
35
|
+
@path = path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def view_template
|
|
39
|
+
code(class: BADGE_CLASSES.fetch(@method, NEUTRAL_BADGE)) { plain @method }
|
|
40
|
+
whitespace
|
|
41
|
+
code { plain @path }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# An error reference table for an API endpoint — a keyword-schema preset over
|
|
5
|
+
# DocsUI::Table. Each error is a Hash:
|
|
6
|
+
#
|
|
7
|
+
# render DocsUI::ErrorTable.new(
|
|
8
|
+
# [
|
|
9
|
+
# { scenario: "Missing or invalid API key", status: "401", type: "authentication_error" },
|
|
10
|
+
# { scenario: "Non-HTTPS URL", status: "422", type: "validation_error", param: "url" },
|
|
11
|
+
# ]
|
|
12
|
+
# )
|
|
13
|
+
#
|
|
14
|
+
# Columns: Scenario / Status / Type (auto code-styled) / Param (auto code-styled).
|
|
15
|
+
# The Param column is shown only when at least one error names a param — an
|
|
16
|
+
# endpoint whose errors are all param-free renders a clean three-column table.
|
|
17
|
+
# When the column IS shown, a param-free row gets the canonical em-dash `—`.
|
|
18
|
+
#
|
|
19
|
+
# `type:` is optional — OpenAPI has no canonical error-type field, so an
|
|
20
|
+
# OpenAPI-derived error may carry none. A row without a (present) type gets the
|
|
21
|
+
# em-dash `—` in the Type column, not an empty <code>; a row with `type:`
|
|
22
|
+
# renders it code-styled exactly as before.
|
|
23
|
+
class ErrorTable < Phlex::HTML
|
|
24
|
+
BASE_HEADERS = %w[Scenario Status Type].freeze
|
|
25
|
+
PARAM_HEADER = "Param"
|
|
26
|
+
|
|
27
|
+
# Shared with FieldTable's canonical "no value" placeholder.
|
|
28
|
+
NO_PARAM = "—"
|
|
29
|
+
|
|
30
|
+
def initialize(errors)
|
|
31
|
+
@errors = errors
|
|
32
|
+
@with_param = errors.any? { |error| present_param?(error[:param]) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def view_template
|
|
36
|
+
render DocsUI::Table.new(headers, @errors.map { |error| row(error) })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def headers
|
|
42
|
+
@with_param ? [*BASE_HEADERS, PARAM_HEADER] : BASE_HEADERS
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def row(error)
|
|
46
|
+
cells = [
|
|
47
|
+
error.fetch(:scenario),
|
|
48
|
+
error.fetch(:status),
|
|
49
|
+
type_cell(error)
|
|
50
|
+
]
|
|
51
|
+
cells << param_cell(error) if @with_param
|
|
52
|
+
cells
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The Type cell: code-styled when present, else the em-dash placeholder. A
|
|
56
|
+
# blank string counts as absent (same rule as #param_cell / #present_param?).
|
|
57
|
+
def type_cell(error)
|
|
58
|
+
type = error[:type]
|
|
59
|
+
present_param?(type) ? [:code, type] : NO_PARAM
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def param_cell(error)
|
|
63
|
+
param = error[:param]
|
|
64
|
+
present_param?(param) ? [:code, param] : NO_PARAM
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A blank string is not a param — it flips no column and gets the em-dash.
|
|
68
|
+
def present_param?(param)
|
|
69
|
+
!param.nil? && !param.to_s.strip.empty?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# A multi-language code group: the same example shown in several languages, with
|
|
5
|
+
# tabs to switch. The chosen language is a GLOBAL sticky preference (localStorage
|
|
6
|
+
# via the docs-nav controller) — pick Ruby once and every code group on this and
|
|
7
|
+
# future pages shows Ruby, falling back to an available language when a group
|
|
8
|
+
# doesn't have the chosen one.
|
|
9
|
+
#
|
|
10
|
+
# render DocsUI::Example.new do |ex|
|
|
11
|
+
# ex.code(:ruby, filename: "client.rb") do
|
|
12
|
+
# <<~RUBY
|
|
13
|
+
# Anthropic.messages.create(model: "claude-opus-4-8", ...)
|
|
14
|
+
# RUBY
|
|
15
|
+
# end
|
|
16
|
+
# ex.code(:python, filename: "client.py") do
|
|
17
|
+
# <<~PY
|
|
18
|
+
# client.messages.create(model="claude-opus-4-8", ...)
|
|
19
|
+
# PY
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# With one snippet it degrades to a plain DocsUI::Code (no tabs). With JS off the
|
|
24
|
+
# first language shows and the rest are visible below it (progressive
|
|
25
|
+
# enhancement — no content is hidden without JS).
|
|
26
|
+
class Example < Phlex::HTML
|
|
27
|
+
def initialize
|
|
28
|
+
@snippets = []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Collect one language's snippet. `lang` is the language token (e.g. :ruby,
|
|
32
|
+
# :python, :go) — Docs::Code resolves it against Rouge's full registry + the
|
|
33
|
+
# configured aliases, so any language works. The tab label comes from the
|
|
34
|
+
# configured language_labels (else the token capitalized), or an explicit
|
|
35
|
+
# `label:` override (used by RequestExample so a client carries its own tab
|
|
36
|
+
# name). filename/lexer are optional; lexer defaults to the language token.
|
|
37
|
+
# The block returns the source.
|
|
38
|
+
def code(lang, filename: nil, lexer: nil, label: nil)
|
|
39
|
+
token = lang.to_sym
|
|
40
|
+
@snippets << {
|
|
41
|
+
lang: token,
|
|
42
|
+
label: label || DocsKit.configuration.language_labels.fetch(token, token.to_s.capitalize),
|
|
43
|
+
filename: filename,
|
|
44
|
+
lexer: lexer || token,
|
|
45
|
+
source: yield.to_s
|
|
46
|
+
}
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def view_template
|
|
51
|
+
yield self if block_given?
|
|
52
|
+
return if @snippets.empty?
|
|
53
|
+
return render_single if @snippets.one?
|
|
54
|
+
|
|
55
|
+
div(
|
|
56
|
+
class: "not-prose my-4",
|
|
57
|
+
data: { docs_nav_target: "codeGroup" }
|
|
58
|
+
) do
|
|
59
|
+
language_tabs
|
|
60
|
+
snippet_panels
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def render_single
|
|
67
|
+
snippet = @snippets.first
|
|
68
|
+
render DocsUI::Code.new(snippet[:source], lexer: snippet[:lexer], filename: snippet[:filename])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def language_tabs
|
|
72
|
+
div(role: "tablist", class: "tabs tabs-box w-fit mb-2") do
|
|
73
|
+
@snippets.each do |snippet|
|
|
74
|
+
button(
|
|
75
|
+
role: "tab",
|
|
76
|
+
class: "tab",
|
|
77
|
+
data: {
|
|
78
|
+
docs_nav_target: "codeTab",
|
|
79
|
+
docs_nav_lang_param: snippet[:lang],
|
|
80
|
+
action: "docs-nav#selectLanguage",
|
|
81
|
+
testid: "code-lang-#{snippet[:lang]}"
|
|
82
|
+
}
|
|
83
|
+
) { snippet[:label] }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def snippet_panels
|
|
89
|
+
@snippets.each do |snippet|
|
|
90
|
+
div(
|
|
91
|
+
data: {
|
|
92
|
+
docs_nav_target: "codePanel",
|
|
93
|
+
docs_nav_lang_param: snippet[:lang],
|
|
94
|
+
lang: snippet[:lang]
|
|
95
|
+
}
|
|
96
|
+
) do
|
|
97
|
+
render DocsUI::Code.new(snippet[:source], lexer: snippet[:lexer], filename: snippet[:filename])
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# A parameter/field reference table for an API object or request body — a
|
|
5
|
+
# keyword-schema preset over DocsUI::Table. Each field is a Hash:
|
|
6
|
+
#
|
|
7
|
+
# render DocsUI::FieldTable.new(
|
|
8
|
+
# [
|
|
9
|
+
# { name: "url", type: "string", required: true, description: "HTTPS destination URL." },
|
|
10
|
+
# { name: "description", type: "string", description: "Optional internal label." },
|
|
11
|
+
# { name: "events", type: "array", required: true, description: [:md, "e.g. `payment_link.paid`"] },
|
|
12
|
+
# ]
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# Columns: Name (auto code-styled) / Type / Required (✓ or the canonical em-dash
|
|
16
|
+
# `—`) / Description. `required:` defaults to false. The description cell follows
|
|
17
|
+
# DocsUI::Table's convention — a plain String is escaped text, `[:code, "x"]` is
|
|
18
|
+
# inline code, `[:md, "…"]` is inline Markdown.
|
|
19
|
+
class FieldTable < Phlex::HTML
|
|
20
|
+
HEADERS = %w[Name Type Required Description].freeze
|
|
21
|
+
|
|
22
|
+
# The ONE canonical "no value" placeholder across the whole kit — never the
|
|
23
|
+
# ASCII hyphen "-", never a bare "—" typed ad hoc in a page.
|
|
24
|
+
REQUIRED_YES = "✓"
|
|
25
|
+
REQUIRED_NO = "—"
|
|
26
|
+
|
|
27
|
+
def initialize(fields)
|
|
28
|
+
@fields = fields
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def view_template
|
|
32
|
+
render DocsUI::Table.new(HEADERS, @fields.map { |field| row(field) })
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def row(field)
|
|
38
|
+
[
|
|
39
|
+
[:code, field.fetch(:name)],
|
|
40
|
+
field.fetch(:type),
|
|
41
|
+
field.fetch(:required, false) ? REQUIRED_YES : REQUIRED_NO,
|
|
42
|
+
field.fetch(:description, REQUIRED_NO)
|
|
43
|
+
]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# A doc page header: an optional eyebrow (kicker), the title, and a lead
|
|
5
|
+
# paragraph. Gives every doc page a consistent masthead.
|
|
6
|
+
#
|
|
7
|
+
# render DocsUI::Header.new("Installation", eyebrow: "Guide") do
|
|
8
|
+
# plain "Add the gem and render your first component."
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# The primary argument (the title) is positional, matching Section/Code and the
|
|
12
|
+
# kit-wide convention. The legacy `title:` kwarg still works so existing sites
|
|
13
|
+
# keep rendering unchanged; the positional wins if both are given.
|
|
14
|
+
class Header < Phlex::HTML
|
|
15
|
+
# Positional title (the convention), with a silent `title:` kwarg fallback for
|
|
16
|
+
# sites that still pass it by keyword. Positional wins when both are given.
|
|
17
|
+
def initialize(title = nil, eyebrow: nil, **opts)
|
|
18
|
+
@title = title || opts[:title]
|
|
19
|
+
@eyebrow = eyebrow
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def view_template(&block)
|
|
23
|
+
header(class: "mb-8 border-b border-base-300 pb-6") do
|
|
24
|
+
div(class: "mb-2 text-xs font-semibold uppercase tracking-wider text-primary") { @eyebrow } if @eyebrow
|
|
25
|
+
h1(class: "text-3xl font-bold tracking-tight md:text-4xl") { @title }
|
|
26
|
+
p(class: "mt-3 text-lg text-base-content/70", &block) if block
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocsUI
|
|
4
|
+
# Renders a synced lucide icon as inline SVG via rails_icons. Thin Phlex wrapper.
|
|
5
|
+
#
|
|
6
|
+
# render DocsUI::Icon.new("search", class: "size-4")
|
|
7
|
+
#
|
|
8
|
+
# A missing icon name falls back to a question-mark glyph outside development,
|
|
9
|
+
# rather than raising, so a typo never takes down a docs page in production.
|
|
10
|
+
class Icon < Phlex::HTML
|
|
11
|
+
MISSING_ICON = "circle-question-mark"
|
|
12
|
+
|
|
13
|
+
def initialize(name, **attributes)
|
|
14
|
+
@name = name
|
|
15
|
+
@attributes = attributes
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def view_template
|
|
19
|
+
# rails_icons is a Railtie gem; outside a Rails host (isolated Phlex tests)
|
|
20
|
+
# it isn't loaded, and in a not-yet-fully-set-up app its default library may
|
|
21
|
+
# be unconfigured. Either way, render nothing rather than take down the page
|
|
22
|
+
# — icons are chrome, not content. In development we still raise so a real
|
|
23
|
+
# misconfiguration is visible.
|
|
24
|
+
return unless defined?(::Icons::Icon) && rails_icons_library
|
|
25
|
+
|
|
26
|
+
svg = svg_for(@name, **@attributes)
|
|
27
|
+
raw(safe(svg)) if svg
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def svg_for(name, **arguments)
|
|
33
|
+
::Icons::Icon.new(
|
|
34
|
+
name: name.to_s.dasherize,
|
|
35
|
+
library: rails_icons_library,
|
|
36
|
+
variant: nil,
|
|
37
|
+
arguments: arguments
|
|
38
|
+
).svg
|
|
39
|
+
rescue ::Icons::IconNotFound
|
|
40
|
+
raise if local_env?
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
::Icons::Icon.new(name: MISSING_ICON, library: rails_icons_library, variant: nil, arguments: arguments).svg
|
|
44
|
+
rescue StandardError
|
|
45
|
+
nil # even the fallback glyph isn't synced — render nothing.
|
|
46
|
+
end
|
|
47
|
+
rescue StandardError
|
|
48
|
+
# Library misconfigured / icon set not synced. Surface it in dev; elsewhere
|
|
49
|
+
# degrade to no icon rather than 500 the whole docs page.
|
|
50
|
+
raise if local_env?
|
|
51
|
+
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def rails_icons_library
|
|
56
|
+
DocsKit.configuration.icon_library || ::RailsIcons.configuration.default_library
|
|
57
|
+
rescue StandardError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def local_env?
|
|
62
|
+
defined?(Rails) && Rails.env.local?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module DocsUI
|
|
6
|
+
# A pretty-printed JSON response block. Give it a Ruby Hash (deep-stringified and
|
|
7
|
+
# JSON.pretty_generate'd) or a pre-formatted String; it renders a DocsUI::Code
|
|
8
|
+
# with the json lexer and a filename title bar. Kills the hand-rolled
|
|
9
|
+
# deep_stringify + JSON.pretty_generate every API page was copy-pasting.
|
|
10
|
+
#
|
|
11
|
+
# render DocsUI::JsonResponse.new({ id: "obj_1", status: "active" })
|
|
12
|
+
# render DocsUI::JsonResponse.new(raw_json_string, filename: "webhook.json")
|
|
13
|
+
#
|
|
14
|
+
# A Hash with symbol keys renders as real JSON (string keys, no :symbol / =>
|
|
15
|
+
# leaking through). A String is passed through verbatim (already formatted).
|
|
16
|
+
class JsonResponse < Phlex::HTML
|
|
17
|
+
def initialize(body, filename: "response.json")
|
|
18
|
+
@body = body
|
|
19
|
+
@filename = filename
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def view_template
|
|
23
|
+
render DocsUI::Code.new(json_source, lexer: :json, filename: @filename)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# The JSON string to highlight: a String passes through; a Hash/Array is
|
|
29
|
+
# deep-stringified then pretty-generated so it reads like an API response.
|
|
30
|
+
def json_source
|
|
31
|
+
return @body if @body.is_a?(String)
|
|
32
|
+
|
|
33
|
+
JSON.pretty_generate(deep_stringify(@body))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Recursively stringify keys and symbol values so the output is real JSON.
|
|
37
|
+
def deep_stringify(value)
|
|
38
|
+
case value
|
|
39
|
+
when Hash then value.to_h { |k, v| [k.to_s, deep_stringify(v)] }
|
|
40
|
+
when Array then value.map { |v| deep_stringify(v) }
|
|
41
|
+
when Symbol then value.to_s
|
|
42
|
+
else value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|