|
| 1 | +package docs |
| 2 | + |
| 3 | +// This file holds the in-app rendering path: when the composition root wires |
| 4 | +// EnableInApp (dashboard auth on), a signed-in visitor's doc page renders as a |
| 5 | +// fragment inside the dashboard chrome instead of the standalone public shell in |
| 6 | +// render.go. The fragment keeps the classic doc layout (grouped TOC rail + prose |
| 7 | +// column) and carries its own scoped <style> block; it is kept in its own file |
| 8 | +// because template literals count toward the 500-line checklen budget. |
| 9 | + |
| 10 | +import ( |
| 11 | + "bytes" |
| 12 | + "html/template" |
| 13 | + "log/slog" |
| 14 | + "net/http" |
| 15 | +) |
| 16 | + |
| 17 | +// EnableInApp wires the two collaborators the in-app rendering path needs: |
| 18 | +// authed soft-checks the session cookie (no redirect), and render writes a |
| 19 | +// trusted HTML fragment inside the authenticated dashboard chrome. The |
| 20 | +// composition root calls this only when an auth layer exists; until then Render |
| 21 | +// keeps the standalone shell for every request, so the community build (no |
| 22 | +// control plane) is untouched. |
| 23 | +func (h *Handler) EnableInApp(authed func(*http.Request) bool, render func(http.ResponseWriter, *http.Request, string, template.HTML)) { |
| 24 | + h.authed = authed |
| 25 | + h.renderApp = render |
| 26 | +} |
| 27 | + |
| 28 | +// appData is the in-app fragment's view model: the rendered prose body and the |
| 29 | +// same grouped, active-lit table of contents the standalone shell shows. |
| 30 | +type appData struct { |
| 31 | + Body template.HTML |
| 32 | + Groups []navGroup |
| 33 | +} |
| 34 | + |
| 35 | +// renderInApp executes the fragment template and hands the result to the |
| 36 | +// injected dashboard renderer, which wraps it in the sidebar + top-bar chrome |
| 37 | +// with the Documentation nav item lit. A template failure logs and 500s (nothing |
| 38 | +// has been written yet, so the error page is still deliverable). |
| 39 | +func (h *Handler) renderInApp(w http.ResponseWriter, r *http.Request, title string, body template.HTML) { |
| 40 | + var buf bytes.Buffer |
| 41 | + if err := appTmpl.Execute(&buf, appData{Body: body, Groups: h.groups(r.URL.Path)}); err != nil { |
| 42 | + h.log.Error("docs: execute in-app fragment", slog.Any("err", err)) |
| 43 | + http.Error(w, "documentation unavailable", http.StatusInternalServerError) |
| 44 | + return |
| 45 | + } |
| 46 | + h.renderApp(w, r, title, template.HTML(buf.String())) //nolint:gosec // template-produced, autoescaped output |
| 47 | +} |
| 48 | + |
| 49 | +// appTmpl is the in-app doc fragment: the two-column layout (grouped TOC rail + |
| 50 | +// prose column) dropped into the dashboard's content area. No <html>/<head> and |
| 51 | +// no scripts — the dashboard CSP allows inline styles but no inline JS. Every |
| 52 | +// class is doc- prefixed so nothing can collide with the dashboard chrome |
| 53 | +// classes, and the colours come from the CSS variables the dashboard head |
| 54 | +// defines (--panel, --line, --txt*, --accent, --blue, --mono, --ui), so the |
| 55 | +// fragment inherits the dashboard theme instead of restating it. The prose rules |
| 56 | +// mirror the standalone shell's (render.go) so a page reads identically in both |
| 57 | +// chromes. |
| 58 | +var appTmpl = template.Must(template.New("docapp").Parse(`<style> |
| 59 | +.doc-wrap{display:grid;grid-template-columns:240px minmax(0,1fr);align-items:start;} |
| 60 | +.doc-rail{border-right:1px solid var(--line);padding:2px 14px 2px 0;} |
| 61 | +.doc-grp{font:700 10px var(--mono);color:var(--txt-3);letter-spacing:1px;text-transform:uppercase;margin:20px 6px 8px;} |
| 62 | +.doc-grp:first-child{margin-top:0;} |
| 63 | +.doc-lnk{display:block;padding:7px 10px;border-radius:8px;font:600 13px var(--ui);color:var(--txt-2);} |
| 64 | +.doc-lnk:hover{background:var(--panel-2);color:var(--txt);} |
| 65 | +.doc-lnk.on{background:rgba(180,241,74,.10);color:var(--txt);} |
| 66 | +.doc-prose{max-width:760px;padding:0 34px;} |
| 67 | +.doc-prose h1{font:800 32px/1.15 var(--ui);letter-spacing:-.5px;margin:0 0 24px;} |
| 68 | +.doc-prose h2{font:700 22px/1.2 var(--ui);letter-spacing:-.3px;margin:44px 0 14px;padding-top:14px;border-top:1px solid var(--line-soft);} |
| 69 | +.doc-prose h3{font:700 16px var(--ui);margin:28px 0 10px;} |
| 70 | +.doc-prose p{font:400 15px/1.7 var(--ui);color:var(--txt);margin:0 0 16px;} |
| 71 | +.doc-prose li{font:400 15px/1.7 var(--ui);color:var(--txt);margin:6px 0;} |
| 72 | +.doc-prose ul,.doc-prose ol{padding-left:22px;margin:0 0 16px;} |
| 73 | +.doc-prose a{color:var(--blue);} |
| 74 | +.doc-prose a:hover{text-decoration:underline;} |
| 75 | +.doc-prose strong{color:var(--txt);font-weight:700;} |
| 76 | +.doc-prose code{font:500 13px var(--mono);background:var(--panel-3);color:#D8E4C0;padding:2px 6px;border-radius:5px;} |
| 77 | +.doc-prose pre{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;overflow-x:auto;margin:0 0 18px;} |
| 78 | +.doc-prose pre code{background:none;padding:0;color:var(--txt);font-size:13px;line-height:1.6;} |
| 79 | +.doc-prose blockquote{margin:0 0 18px;padding:12px 18px;border-left:3px solid var(--accent);background:var(--panel-2);border-radius:0 10px 10px 0;color:var(--txt-2);} |
| 80 | +.doc-prose blockquote p{margin:0;font-size:14px;} |
| 81 | +.doc-prose table{width:100%;border-collapse:collapse;margin:0 0 20px;font-size:13.5px;} |
| 82 | +.doc-prose th{text-align:left;font:600 11px var(--mono);color:var(--txt-3);letter-spacing:.5px;text-transform:uppercase;padding:10px 14px;border-bottom:1px solid var(--line);background:var(--panel-2);} |
| 83 | +.doc-prose td{padding:11px 14px;border-bottom:1px solid var(--line-soft);color:var(--txt-2);vertical-align:top;} |
| 84 | +.doc-prose td code{font-size:12px;} |
| 85 | +.doc-prose hr{border:none;border-top:1px solid var(--line);margin:32px 0;} |
| 86 | +.doc-prose h2 a,.doc-prose h3 a{color:inherit;} |
| 87 | +@media(max-width:980px){ |
| 88 | + .doc-wrap{grid-template-columns:1fr;} |
| 89 | + .doc-rail{border-right:none;border-bottom:1px solid var(--line);padding:0 0 16px;margin-bottom:20px;} |
| 90 | + .doc-prose{padding:0;} |
| 91 | +} |
| 92 | +</style> |
| 93 | +<div class="doc-wrap"> |
| 94 | + <aside class="doc-rail"> |
| 95 | + {{range .Groups}} |
| 96 | + <div class="doc-grp">{{.Group}}</div> |
| 97 | + {{range .Items}}<a class="doc-lnk{{if .Active}} on{{end}}" href="{{.Href}}">{{.Title}}</a>{{end}} |
| 98 | + {{end}} |
| 99 | + </aside> |
| 100 | + <article class="doc-prose"> |
| 101 | + {{.Body}} |
| 102 | + </article> |
| 103 | +</div>`)) |
0 commit comments