diff --git a/hscontrol/templates/apple.go b/hscontrol/templates/apple.go index 84928ed5..3b120069 100644 --- a/hscontrol/templates/apple.go +++ b/hscontrol/templates/apple.go @@ -5,48 +5,43 @@ import ( "github.com/chasefleming/elem-go" "github.com/chasefleming/elem-go/attrs" + "github.com/chasefleming/elem-go/styles" ) func Apple(url string) *elem.Element { return HtmlStructure( elem.Title(nil, elem.Text("headscale - Apple")), - elem.Body(attrs.Props{ - attrs.Style: bodyStyle.ToInline(), - }, - headerOne("headscale: iOS configuration"), - headerTwo("GUI"), - elem.Ol(nil, + mdTypesetBody( + headscaleLogo(), + H1(elem.Text("iOS configuration")), + H2(elem.Text("GUI")), + Ol( elem.Li( nil, elem.Text("Install the official Tailscale iOS client from the "), - elem.A( - attrs.Props{ - attrs.Href: "https://apps.apple.com/app/tailscale/id1470499037", - }, - elem.Text("App store"), - ), + externalLink("https://apps.apple.com/app/tailscale/id1470499037", "App Store"), ), elem.Li( nil, - elem.Text("Open the Tailscale app"), + elem.Text("Open the "), + elem.Strong(nil, elem.Text("Tailscale")), + elem.Text(" app"), ), elem.Li( nil, - elem.Text(`Click the account icon in the top-right corner and select "Log in…".`), + elem.Text("Click the account icon in the top-right corner and select "), + elem.Strong(nil, elem.Text("Log in…")), ), elem.Li( nil, - elem.Text(`Tap the top-right options menu button and select "Use custom coordination server".`), + elem.Text("Tap the top-right options menu button and select "), + elem.Strong(nil, elem.Text("Use custom coordination server")), ), elem.Li( nil, - elem.Text( - fmt.Sprintf( - `Enter your instance URL: "%s"`, - url, - ), - ), + elem.Text("Enter your instance URL: "), + Code(elem.Text(url)), ), elem.Li( nil, @@ -55,65 +50,50 @@ func Apple(url string) *elem.Element { ), ), ), - headerOne("headscale: macOS configuration"), - headerTwo("Command line"), - elem.P(nil, + H1(elem.Text("macOS configuration")), + H2(elem.Text("Command line")), + P( elem.Text("Use Tailscale's login command to add your profile:"), ), - elem.Pre(nil, - elem.Code(nil, - elem.Text("tailscale login --login-server "+url), - ), - ), - headerTwo("GUI"), - elem.Ol(nil, + Pre(PreCode("tailscale login --login-server "+url)), + H2(elem.Text("GUI")), + Ol( elem.Li( nil, - elem.Text( - "Option + Click the Tailscale icon in the menu and hover over the Debug menu", - ), + elem.Text("Option + Click the "), + elem.Strong(nil, elem.Text("Tailscale")), + elem.Text(" icon in the menu and hover over the "), + elem.Strong(nil, elem.Text("Debug")), + elem.Text(" menu"), ), elem.Li(nil, - elem.Text(`Under "Custom Login Server", select "Add Account..."`), + elem.Text("Under "), + elem.Strong(nil, elem.Text("Custom Login Server")), + elem.Text(", select "), + elem.Strong(nil, elem.Text("Add Account...")), ), elem.Li( nil, - elem.Text( - fmt.Sprintf( - `Enter "%s" of the headscale instance and press "Add Account"`, - url, - ), - ), + elem.Text("Enter "), + Code(elem.Text(url)), + elem.Text(" of the headscale instance and press "), + elem.Strong(nil, elem.Text("Add Account")), ), elem.Li(nil, - elem.Text(`Follow the login procedure in the browser`), + elem.Text("Follow the login procedure in the browser"), ), ), - headerTwo("Profiles"), - elem.P( - nil, + H2(elem.Text("Profiles")), + P( elem.Text( "Headscale can be set to the default server by installing a Headscale configuration profile:", ), ), - elem.P( - nil, - elem.A( - attrs.Props{ - attrs.Href: "/apple/macos-app-store", - attrs.Download: "headscale_macos.mobileconfig", - }, - elem.Text("macOS AppStore profile "), - ), - elem.A( - attrs.Props{ - attrs.Href: "/apple/macos-standalone", - attrs.Download: "headscale_macos.mobileconfig", - }, - elem.Text("macOS Standalone profile"), - ), + elem.Div(attrs.Props{attrs.Style: styles.Props{styles.MarginTop: spaceL, styles.MarginBottom: spaceL}.ToInline()}, + downloadButton("/apple/macos-app-store", "macOS AppStore profile"), + downloadButton("/apple/macos-standalone", "macOS Standalone profile"), ), - elem.Ol(nil, + Ol( elem.Li( nil, elem.Text( @@ -121,105 +101,82 @@ func Apple(url string) *elem.Element { ), ), elem.Li(nil, - elem.Text(`Open System Preferences and go to "Profiles"`), + elem.Text("Open "), + elem.Strong(nil, elem.Text("System Preferences")), + elem.Text(" and go to "), + elem.Strong(nil, elem.Text("Profiles")), ), elem.Li(nil, - elem.Text(`Find and install the Headscale profile`), + elem.Text("Find and install the "), + elem.Strong(nil, elem.Text("Headscale")), + elem.Text(" profile"), ), elem.Li(nil, - elem.Text(`Restart Tailscale.app and log in`), + elem.Text("Restart "), + elem.Strong(nil, elem.Text("Tailscale.app")), + elem.Text(" and log in"), ), ), - elem.P(nil, elem.Text("Or")), - elem.P( - nil, + orDivider(), + P( elem.Text( - "Use your terminal to configure the default setting for Tailscale by issuing:", + "Use your terminal to configure the default setting for Tailscale by issuing one of the following commands:", ), ), - elem.Ul(nil, - elem.Li(nil, - elem.Text(`for app store client:`), - elem.Code( - nil, - elem.Text( - "defaults write io.tailscale.ipn.macos ControlURL "+url, - ), - ), - ), - elem.Li(nil, - elem.Text(`for standalone client:`), - elem.Code( - nil, - elem.Text( - "defaults write io.tailscale.ipn.macsys ControlURL "+url, - ), - ), - ), + P(elem.Text("For app store client:")), + Pre(PreCode("defaults write io.tailscale.ipn.macos ControlURL "+url)), + P(elem.Text("For standalone client:")), + Pre(PreCode("defaults write io.tailscale.ipn.macsys ControlURL "+url)), + P( + elem.Text("Restart "), + elem.Strong(nil, elem.Text("Tailscale.app")), + elem.Text(" and log in."), ), - elem.P(nil, - elem.Text("Restart Tailscale.app and log in."), - ), - headerThree("Caution"), - elem.P( - nil, - elem.Text( - "You should always download and inspect the profile before installing it:", - ), - ), - elem.Ul(nil, - elem.Li(nil, - elem.Text(`for app store client: `), - elem.Code(nil, - elem.Text(fmt.Sprintf(`curl %s/apple/macos-app-store`, url)), - ), - ), - elem.Li(nil, - elem.Text(`for standalone client: `), - elem.Code(nil, - elem.Text(fmt.Sprintf(`curl %s/apple/macos-standalone`, url)), - ), - ), - ), - headerOne("headscale: tvOS configuration"), - headerTwo("GUI"), - elem.Ol(nil, + warningBox("Caution", "You should always download and inspect the profile before installing it."), + P(elem.Text("For app store client:")), + Pre(PreCode(fmt.Sprintf(`curl %s/apple/macos-app-store`, url))), + P(elem.Text("For standalone client:")), + Pre(PreCode(fmt.Sprintf(`curl %s/apple/macos-standalone`, url))), + H1(elem.Text("tvOS configuration")), + H2(elem.Text("GUI")), + Ol( elem.Li( nil, elem.Text("Install the official Tailscale tvOS client from the "), - elem.A( - attrs.Props{ - attrs.Href: "https://apps.apple.com/app/tailscale/id1470499037", - }, - elem.Text("App store"), - ), + externalLink("https://apps.apple.com/app/tailscale/id1470499037", "App Store"), ), elem.Li( nil, - elem.Text( - "Open Settings (the Apple tvOS settings) > Apps > Tailscale", - ), + elem.Text("Open "), + elem.Strong(nil, elem.Text("Settings")), + elem.Text(" (the Apple tvOS settings) > "), + elem.Strong(nil, elem.Text("Apps")), + elem.Text(" > "), + elem.Strong(nil, elem.Text("Tailscale")), ), elem.Li( nil, - elem.Text( - fmt.Sprintf( - `Enter "%s" under "ALTERNATE COORDINATION SERVER URL"`, - url, - ), - ), + elem.Text("Enter "), + Code(elem.Text(url)), + elem.Text(" under "), + elem.Strong(nil, elem.Text("ALTERNATE COORDINATION SERVER URL")), ), elem.Li(nil, - elem.Text("Return to the tvOS Home screen"), + elem.Text("Return to the tvOS "), + elem.Strong(nil, elem.Text("Home")), + elem.Text(" screen"), ), elem.Li(nil, - elem.Text("Open Tailscale"), + elem.Text("Open "), + elem.Strong(nil, elem.Text("Tailscale")), ), elem.Li(nil, - elem.Text(`Select "Install VPN configuration"`), + elem.Text("Select "), + elem.Strong(nil, elem.Text("Install VPN configuration")), ), elem.Li(nil, - elem.Text(`Select "Allow"`), + elem.Text("Select "), + elem.Strong(nil, elem.Text("Allow")), ), elem.Li(nil, elem.Text("Scan the QR code and follow the login procedure"), @@ -228,6 +185,7 @@ func Apple(url string) *elem.Element { elem.Text("Headscale should now be working on your tvOS device"), ), ), + pageFooter(), ), ) } diff --git a/hscontrol/templates/general.go b/hscontrol/templates/general.go index a6051d58..2d3ea791 100644 --- a/hscontrol/templates/general.go +++ b/hscontrol/templates/general.go @@ -1,46 +1,176 @@ package templates import ( + _ "embed" + "github.com/chasefleming/elem-go" "github.com/chasefleming/elem-go/attrs" "github.com/chasefleming/elem-go/styles" ) -// bodyStyle provides consistent body styling across all templates with -// a centered, readable layout and appropriate spacing. -var bodyStyle = styles.Props{ - styles.Margin: "40px auto", - styles.MaxWidth: "800px", - styles.LineHeight: "1.5", - styles.FontSize: "16px", - styles.Color: "#444", - styles.Padding: "0 10px", - styles.FontFamily: "sans-serif", +//go:embed style.css +var headscaleCSS string + +//go:embed headscale.svg +var headscaleSVG string + + + +// mdTypesetBody creates a body element with md-typeset styling +// that matches the official Headscale documentation design. +// Uses CSS classes with styles defined in headscaleCSS. +func mdTypesetBody(children ...elem.Node) *elem.Element { + return elem.Body(attrs.Props{ + attrs.Style: styles.Props{ + styles.MinHeight: "100vh", + styles.Display: "flex", + styles.FlexDirection: "column", + styles.AlignItems: "center", + styles.BackgroundColor: "#ffffff", + styles.Padding: "3rem 1.5rem", + }.ToInline(), + "translate": "no", + }, + elem.Div(attrs.Props{ + attrs.Class: "md-typeset", + attrs.Style: styles.Props{ + styles.MaxWidth: "min(800px, 90vw)", + styles.Width: "100%", + }.ToInline(), + }, children...), + ) } -// headerStyle provides consistent header styling with improved line height -var headerStyle = styles.Props{ - styles.LineHeight: "1.2", +// Styled Element Wrappers +// These functions wrap elem-go elements using CSS classes. +// Styling is handled by the CSS in headscaleCSS. + +// H1 creates a H1 element styled by .md-typeset h1 +func H1(children ...elem.Node) *elem.Element { + return elem.H1(nil, children...) } -// headerOne creates a level 1 heading with consistent styling +// H2 creates a H2 element styled by .md-typeset h2 +func H2(children ...elem.Node) *elem.Element { + return elem.H2(nil, children...) +} + +// H3 creates a H3 element styled by .md-typeset h3 +func H3(children ...elem.Node) *elem.Element { + return elem.H3(nil, children...) +} + +// P creates a paragraph element styled by .md-typeset p +func P(children ...elem.Node) *elem.Element { + return elem.P(nil, children...) +} + +// Ol creates an ordered list element styled by .md-typeset ol +func Ol(children ...elem.Node) *elem.Element { + return elem.Ol(nil, children...) +} + +// Ul creates an unordered list element styled by .md-typeset ul +func Ul(children ...elem.Node) *elem.Element { + return elem.Ul(nil, children...) +} + +// A creates a link element styled by .md-typeset a +func A(href string, children ...elem.Node) *elem.Element { + return elem.A(attrs.Props{attrs.Href: href}, children...) +} + +// Code creates an inline code element styled by .md-typeset code +func Code(children ...elem.Node) *elem.Element { + return elem.Code(nil, children...) +} + +// Pre creates a preformatted text block styled by .md-typeset pre +func Pre(children ...elem.Node) *elem.Element { + return elem.Pre(nil, children...) +} + +// PreCode creates a code block inside Pre styled by .md-typeset pre > code +func PreCode(code string) *elem.Element { + return elem.Code(nil, elem.Text(code)) +} + +// Deprecated: use H1, H2, H3 instead func headerOne(text string) *elem.Element { - return elem.H1(attrs.Props{attrs.Style: headerStyle.ToInline()}, elem.Text(text)) + return H1(elem.Text(text)) } -// headerTwo creates a level 2 heading with consistent styling +// Deprecated: use H1, H2, H3 instead func headerTwo(text string) *elem.Element { - return elem.H2(attrs.Props{attrs.Style: headerStyle.ToInline()}, elem.Text(text)) + return H2(elem.Text(text)) } -// headerThree creates a level 3 heading with consistent styling +// Deprecated: use H1, H2, H3 instead func headerThree(text string) *elem.Element { - return elem.H3(attrs.Props{attrs.Style: headerStyle.ToInline()}, elem.Text(text)) + return H3(elem.Text(text)) +} + +// contentContainer wraps page content with proper width. +// Content inside is left-aligned by default. +func contentContainer(children ...elem.Node) *elem.Element { + containerStyle := styles.Props{ + styles.MaxWidth: "720px", + styles.Width: "100%", + styles.Display: "flex", + styles.FlexDirection: "column", + styles.AlignItems: "flex-start", // Left-align all children + } + + return elem.Div(attrs.Props{attrs.Style: containerStyle.ToInline()}, children...) +} + +// headscaleLogo returns the Headscale SVG logo for consistent branding across all pages. +// The logo is styled by the .headscale-logo CSS class. +func headscaleLogo() elem.Node { + // Return the embedded SVG as-is + return elem.Raw(headscaleSVG) +} + +// pageFooter creates a consistent footer for all pages. +func pageFooter() *elem.Element { + footerStyle := styles.Props{ + styles.MarginTop: space3XL, + styles.TextAlign: "center", + styles.FontSize: fontSizeSmall, + styles.Color: colorTextSecondary, + styles.LineHeight: lineHeightBase, + } + + linkStyle := styles.Props{ + styles.Color: colorTextSecondary, + styles.TextDecoration: "underline", + } + + return elem.Div(attrs.Props{attrs.Style: footerStyle.ToInline()}, + elem.Text("Powered by "), + elem.A(attrs.Props{ + attrs.Href: "https://github.com/juanfont/headscale", + attrs.Rel: "noreferrer noopener", + attrs.Target: "_blank", + attrs.Style: linkStyle.ToInline(), + }, elem.Text("Headscale")), + ) +} + +// listStyle provides consistent styling for ordered and unordered lists +// EXTRACTED FROM: .md-typeset ol, .md-typeset ul CSS rules +var listStyle = styles.Props{ + styles.LineHeight: lineHeightBase, // 1.6 - From .md-typeset + styles.MarginTop: "1em", // From CSS: margin-top: 1em + styles.MarginBottom: "1em", // From CSS: margin-bottom: 1em + styles.PaddingLeft: "clamp(1.5rem, 5vw, 2.5rem)", // Responsive indentation } // HtmlStructure creates a complete HTML document structure with proper meta tags // and semantic HTML5 structure. The head and body elements are passed as parameters // to allow for customization of each page. +// Styling is provided via a CSS stylesheet (Material for MkDocs design system) with +// minimal inline styles for layout and positioning. func HtmlStructure(head, body *elem.Element) *elem.Element { return elem.Html(attrs.Props{attrs.Lang: "en"}, elem.Head(nil, @@ -56,9 +186,21 @@ func HtmlStructure(head, body *elem.Element) *elem.Element { attrs.Content: "width=device-width, initial-scale=1.0", }), elem.Link(attrs.Props{ - attrs.Rel: "icon", + attrs.Rel: "icon", attrs.Href: "/favicon.ico", }), + // Google Fonts for Roboto and Roboto Mono + elem.Link(attrs.Props{ + attrs.Rel: "preconnect", + attrs.Href: "https://fonts.gstatic.com", + "crossorigin": "", + }), + elem.Link(attrs.Props{ + attrs.Rel: "stylesheet", + attrs.Href: "https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=Roboto+Mono:wght@400;700&display=swap", + }), + // Material for MkDocs CSS styles + elem.Style(attrs.Props{attrs.Type: "text/css"}, elem.Raw(headscaleCSS)), head, ), body, diff --git a/hscontrol/templates/oidc_callback.go b/hscontrol/templates/oidc_callback.go index 97462956..2b68b703 100644 --- a/hscontrol/templates/oidc_callback.go +++ b/hscontrol/templates/oidc_callback.go @@ -3,221 +3,67 @@ package templates import ( "github.com/chasefleming/elem-go" "github.com/chasefleming/elem-go/attrs" + "github.com/chasefleming/elem-go/styles" ) -// headscaleLogo returns the Headscale SVG logo as raw HTML -func headscaleLogo() elem.Node { - return elem.Raw(``) -} - -// checkboxIcon returns the success checkbox SVG icon as raw HTML +// checkboxIcon returns the success checkbox SVG icon as raw HTML. func checkboxIcon() elem.Node { - return elem.Raw(`