mirror of
https://github.com/juanfont/headscale.git
synced 2025-11-20 01:40:21 -05:00
hscontrol/templates: refactor to use CSS classes and embedded files
Refactor template system to use go:embed for external assets and CSS classes for styling instead of inline styles: - general.go: Add go:embed directives for style.css and headscale.svg, replace inline styles with CSS classes (H1, H2, H3, P, etc.), add mdTypesetBody wrapper with Material for MkDocs styling - apple.go, oidc_callback.go, register_web.go, windows.go: Update to use new CSS-based helper functions (H1, H2, P, etc.) and mdTypesetBody for consistent layout This separates content from presentation, making templates easier to maintain and update. All styling is now centralized in style.css with Material for MkDocs design system.
This commit is contained in:
committed by
Kristoffer Dalby
parent
285c4e46a9
commit
3ed1067a95
@@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(`<svg id="logo" width="146" height="51" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2;" viewBox="0 0 1280 640">
|
||||
<path d="M.08 0v-.736h.068v.3C.203-.509.27-.545.347-.545c.029 0 .055.005.079.015.024.01.045.025.062.045.017.02.031.045.041.075.009.03.014.065.014.105V0H.475v-.289C.475-.352.464-.4.443-.433.422-.466.385-.483.334-.483c-.027 0-.052.006-.075.017C.236-.455.216-.439.2-.419c-.017.02-.029.044-.038.072-.009.028-.014.059-.014.093V0H.08Z" style="fill: #f8b5cb; fill-rule: nonzero" transform="translate(32.92220721 521.8022953) scale(235.3092)"/>
|
||||
<path d="M.051-.264c0-.036.007-.071.02-.105.013-.034.031-.064.055-.09.023-.026.052-.047.086-.063.033-.015.071-.023.112-.023.039 0 .076.007.109.021.033.014.062.033.087.058.025.025.044.054.058.088.014.035.021.072.021.113v.005H.121c.001.031.007.059.018.084.01.025.024.047.042.065.018.019.04.033.065.043.025.01.052.015.082.015.026 0 .049-.003.069-.01.02-.007.038-.016.054-.028C.466-.102.48-.115.492-.13c.011-.015.022-.03.032-.046l.057.03C.556-.097.522-.058.48-.03.437-.001.387.013.328.013.284.013.245.006.21-.01.175-.024.146-.045.123-.07.1-.095.082-.125.07-.159.057-.192.051-.227.051-.264ZM.128-.32h.396C.51-.375.485-.416.449-.441.412-.466.371-.479.325-.479c-.048 0-.089.013-.123.039-.034.026-.059.066-.074.12Z" style="fill: #8d8d8d; fill-rule: nonzero" transform="translate(177.16674681 521.8022953) scale(235.3092)"/>
|
||||
<path d="M.051-.267c0-.038.007-.074.021-.108.014-.033.033-.063.058-.088.025-.025.054-.045.087-.06.033-.015.069-.022.108-.022.043 0 .083.009.119.027.035.019.066.047.093.084v-.097h.067V0H.537v-.091C.508-.056.475-.029.44-.013.404.005.365.013.323.013.284.013.248.006.215-.01.182-.024.153-.045.129-.071.104-.096.085-.126.072-.16.058-.193.051-.229.051-.267Zm.279.218c.027 0 .054-.005.079-.015.025-.01.048-.024.068-.043.019-.018.035-.04.047-.067.012-.027.018-.056.018-.089 0-.031-.005-.059-.016-.086C.515-.375.501-.398.482-.417.462-.436.44-.452.415-.463.389-.474.361-.479.331-.479c-.031 0-.059.006-.084.017C.221-.45.199-.434.18-.415c-.019.02-.033.043-.043.068-.011.026-.016.053-.016.082 0 .029.005.056.016.082.011.026.025.049.044.069.019.02.041.036.066.047.025.012.053.018.083.018Z" style="fill: #8d8d8d; fill-rule: nonzero" transform="translate(327.76463481 521.8022953) scale(235.3092)"/>
|
||||
<path d="M.051-.267c0-.038.007-.074.021-.108.014-.033.033-.063.058-.088.025-.025.054-.045.087-.06.033-.015.069-.022.108-.022.043 0 .083.009.119.027.035.019.066.047.093.084v-.302h.068V0H.537v-.091C.508-.056.475-.029.44-.013.404.005.365.013.323.013.284.013.248.006.215-.01.182-.024.153-.045.129-.071.104-.096.085-.126.072-.16.058-.193.051-.229.051-.267Zm.279.218c.027 0 .054-.005.079-.015.025-.01.048-.024.068-.043.019-.018.035-.04.047-.067.011-.027.017-.056.017-.089 0-.031-.005-.059-.016-.086C.514-.375.5-.398.481-.417.462-.436.439-.452.414-.463.389-.474.361-.479.331-.479c-.031 0-.059.006-.084.017C.221-.45.199-.434.18-.415c-.019.02-.033.043-.043.068-.011.026-.016.053-.016.082 0 .029.005.056.016.082.011.026.025.049.044.069.019.02.041.036.066.047.025.012.053.018.083.018Z" style="fill: #8d8d8d; fill-rule: nonzero" transform="translate(488.71612761 521.8022953) scale(235.3092)"/>
|
||||
<path d="m.034-.062.043-.049c.017.019.035.034.054.044.018.01.037.015.057.015.013 0 .026-.002.038-.007.011-.004.021-.01.031-.018.009-.008.016-.017.021-.028.005-.011.008-.022.008-.035 0-.019-.005-.034-.014-.047C.263-.199.248-.21.229-.221.205-.234.183-.247.162-.259.14-.271.122-.284.107-.298.092-.311.08-.327.071-.344.062-.361.058-.381.058-.404c0-.021.004-.04.012-.058.007-.016.018-.031.031-.044.013-.013.028-.022.046-.029.018-.007.037-.01.057-.01.029 0 .056.006.079.019s.045.031.068.053l-.044.045C.291-.443.275-.456.258-.465.241-.474.221-.479.2-.479c-.022 0-.041.007-.056.02C.128-.445.12-.428.12-.408c0 .019.006.035.017.048.011.013.027.026.048.037.027.015.05.028.071.04.021.013.038.026.052.039.014.013.025.028.032.044.007.016.011.035.011.057 0 .021-.004.041-.011.059-.008.019-.019.036-.033.05-.014.015-.031.026-.05.035C.237.01.215.014.191.014c-.03 0-.059-.006-.086-.02C.077-.019.053-.037.034-.062Z" style="fill: #8d8d8d; fill-rule: nonzero" transform="translate(649.90292961 521.8022953) scale(235.3092)"/>
|
||||
<path d="M.051-.266c0-.04.007-.077.022-.111.014-.034.034-.063.059-.089.025-.025.054-.044.089-.058.035-.014.072-.021.113-.021.051 0 .098.01.139.03.041.021.075.049.1.085l-.05.043C.498-.418.47-.441.439-.456.408-.471.372-.479.331-.479c-.03 0-.058.005-.083.016C.222-.452.2-.436.181-.418.162-.399.148-.376.137-.35c-.011.026-.016.054-.016.084 0 .031.005.06.016.086.011.027.025.049.044.068.019.019.041.034.067.044.025.011.053.016.084.016.077 0 .141-.03.191-.09l.051.04c-.028.036-.062.064-.103.085C.43.004.384.014.332.014.291.014.254.007.219-.008.184-.022.155-.042.13-.067.105-.092.086-.121.072-.156.058-.19.051-.227.051-.266Z" style="fill: #8d8d8d; fill-rule: nonzero" transform="translate(741.20289921 521.8022953) scale(235.3092)"/>
|
||||
<path d="M.051-.267c0-.038.007-.074.021-.108.014-.033.033-.063.058-.088.025-.025.054-.045.087-.06.033-.015.069-.022.108-.022.043 0 .083.009.119.027.035.019.066.047.093.084v-.097h.067V0H.537v-.091C.508-.056.475-.029.44-.013.404.005.365.013.323.013.284.013.248.006.215-.01.182-.024.153-.045.129-.071.104-.096.085-.126.072-.16.058-.193.051-.229.051-.267Zm.279.218c.027 0 .054-.005.079-.015.025-.01.048-.024.068-.043.019-.018.035-.04.047-.067.012-.027.018-.056.018-.089 0-.031-.005-.059-.016-.086C.515-.375.501-.398.482-.417.462-.436.44-.452.415-.463.389-.474.361-.479.331-.479c-.031 0-.059.006-.084.017C.221-.45.199-.434.18-.415c-.019.02-.033.043-.043.068-.011.026-.016.053-.016.082 0 .029.005.056.016.082.011.026.025.049.044.069.019.02.041.036.066.047.025.012.053.018.083.018Z" style="fill: #8d8d8d; fill-rule: nonzero" transform="translate(884.27089281 521.8022953) scale(235.3092)"/>
|
||||
<path d="M.066-.736h.068V0H.066z" style="fill: #8d8d8d; fill-rule: nonzero" transform="translate(1045.22238561 521.8022953) scale(235.3092)"/>
|
||||
<path d="M.051-.264c0-.036.007-.071.02-.105.013-.034.031-.064.055-.09.023-.026.052-.047.086-.063.033-.015.071-.023.112-.023.039 0 .076.007.109.021.033.014.062.033.087.058.025.025.044.054.058.088.014.035.021.072.021.113v.005H.121c.001.031.007.059.018.084.01.025.024.047.042.065.018.019.04.033.065.043.025.01.052.015.082.015.026 0 .049-.003.069-.01.02-.007.038-.016.054-.028C.466-.102.48-.115.492-.13c.011-.015.022-.03.032-.046l.057.03C.556-.097.522-.058.48-.03.437-.001.387.013.328.013.284.013.245.006.21-.01.175-.024.146-.045.123-.07.1-.095.082-.125.07-.159.057-.192.051-.227.051-.264ZM.128-.32h.396C.51-.375.485-.416.449-.441.412-.466.371-.479.325-.479c-.048 0-.089.013-.123.039-.034.026-.059.066-.074.12Z" style="fill: #8d8d8d; fill-rule: nonzero" transform="translate(1092.28422561 521.8022953) scale(235.3092)"/>
|
||||
<circle cx="141.023" cy="338.36" r="117.472" style="fill: #f8b5cb" transform="matrix(.581302 0 0 .58613 40.06479894 12.59842153)"/>
|
||||
<circle cx="352.014" cy="268.302" r="33.095" style="fill: #a2a2a2" transform="matrix(.59308 0 0 .58289 32.39345942 21.2386)"/>
|
||||
<circle cx="352.014" cy="268.302" r="33.095" style="fill: #a2a2a2" transform="matrix(.59308 0 0 .58289 32.39345942 88.80371146)"/>
|
||||
<circle cx="352.014" cy="268.302" r="33.095" style="fill: #a2a2a2" transform="matrix(.59308 0 0 .58289 120.7528627 88.80371146)"/>
|
||||
<circle cx="352.014" cy="268.302" r="33.095" style="fill: #a2a2a2" transform="matrix(.59308 0 0 .58289 120.99825939 21.2386)"/>
|
||||
<circle cx="805.557" cy="336.915" r="118.199" style="fill: #8d8d8d" transform="matrix(.5782 0 0 .58289 36.19871106 15.26642564)"/>
|
||||
<circle cx="805.557" cy="336.915" r="118.199" style="fill: #8d8d8d" transform="matrix(.5782 0 0 .58289 183.24041937 15.26642564)"/>
|
||||
<path d="M680.282 124.808h-68.093v390.325h68.081v-28.23H640V153.228h40.282v-28.42Z" style="fill: #303030" transform="translate(34.2345 21.2386) scale(.58289)"/>
|
||||
<path d="M680.282 124.808h-68.093v390.325h68.081v-28.23H640V153.228h40.282v-28.42Z" style="fill: #303030" transform="matrix(-.58289 0 0 .58289 1116.7719791 21.2386)"/>
|
||||
</svg>`)
|
||||
}
|
||||
|
||||
// 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(`<svg id="checkbox" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512">
|
||||
return elem.Raw(`<svg id="checkbox" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 512 512">
|
||||
<path d="M256 32C132.3 32 32 132.3 32 256s100.3 224 224 224 224-100.3 224-224S379.7 32 256 32zm114.9 149.1L231.8 359.6c-1.1 1.1-2.9 3.5-5.1 3.5-2.3 0-3.8-1.6-5.1-2.9-1.3-1.3-78.9-75.9-78.9-75.9l-1.5-1.5c-.6-.9-1.1-2-1.1-3.2 0-1.2.5-2.3 1.1-3.2.4-.4.7-.7 1.1-1.2 7.7-8.1 23.3-24.5 24.3-25.5 1.3-1.3 2.4-3 4.8-3 2.5 0 4.1 2.1 5.3 3.3 1.2 1.2 45 43.3 45 43.3l111.3-143c1-.8 2.2-1.4 3.5-1.4 1.3 0 2.5.5 3.5 1.3l30.6 24.1c.8 1 1.3 2.2 1.3 3.5.1 1.3-.4 2.4-1 3.3z"></path>
|
||||
</svg>`)
|
||||
}
|
||||
|
||||
// externalLinkIcon returns the external link SVG icon as raw HTML
|
||||
func externalLinkIcon() elem.Node {
|
||||
return elem.Raw(`<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.307 1H11.5a.5.5 0 1 1 0-1h3a.499.499 0 0 1 .5.65V3.5a.5.5 0 1 1-1 0V1.72l-1.793 1.774a.5.5 0 0 1-.713-.701L13.307 1zM12 14V8a.5.5 0 1 1 1 0v6.5a.5.5 0 0 1-.5.5H.563a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 .5-.5H8a.5.5 0 0 1 0 1H1v12h11zM4 6a.5.5 0 0 1 0-1h3a.5.5 0 0 1 0 1H4zm0 2.5a.5.5 0 0 1 0-1h5a.5.5 0 0 1 0 1H4zM4 11a.5.5 0 1 1 0-1h5a.5.5 0 1 1 0 1H4z"/>
|
||||
</svg>`)
|
||||
}
|
||||
|
||||
// oidcCallbackStyles returns the CSS styles for the OIDC callback page
|
||||
func oidcCallbackStyles() *elem.Element {
|
||||
return elem.Style(nil, elem.Text(`
|
||||
body {
|
||||
font-size: 14px;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
"Roboto",
|
||||
"Oxygen",
|
||||
"Ubuntu",
|
||||
"Cantarell",
|
||||
"Fira Sans",
|
||||
"Droid Sans",
|
||||
"Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: #fdfdfe;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
#logo {
|
||||
display: block;
|
||||
margin-left: -20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
min-width: 40vw;
|
||||
background: #fafdfa;
|
||||
border: 1px solid #c6e9c9;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 16px 16px 12px;
|
||||
position: relative;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.message #checkbox {
|
||||
fill: #2eb039;
|
||||
}
|
||||
|
||||
.message .message-title {
|
||||
color: #1e7125;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.message .message-body {
|
||||
border: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.message p {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #17421b;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
color: #1563ff;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
a svg {
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
.icon {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
height: 21px;
|
||||
width: 21px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 17.5px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h1 + p {
|
||||
margin: 8px 0 16px 0;
|
||||
}
|
||||
`))
|
||||
}
|
||||
|
||||
// OIDCCallback renders the OIDC authentication success callback page
|
||||
// OIDCCallback renders the OIDC authentication success callback page.
|
||||
func OIDCCallback(user, verb string) *elem.Element {
|
||||
return elem.Html(attrs.Props{attrs.Lang: "en"},
|
||||
elem.Head(nil,
|
||||
elem.Meta(attrs.Props{attrs.Charset: "UTF-8"}),
|
||||
elem.Meta(attrs.Props{
|
||||
attrs.HTTPequiv: "X-UA-Compatible",
|
||||
attrs.Content: "IE=edge",
|
||||
}),
|
||||
elem.Meta(attrs.Props{
|
||||
attrs.Name: "viewport",
|
||||
attrs.Content: "width=device-width, initial-scale=1.0",
|
||||
}),
|
||||
elem.Title(nil, elem.Text("Headscale Authentication Succeeded")),
|
||||
oidcCallbackStyles(),
|
||||
// Success message box
|
||||
successBox := elem.Div(attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.Display: "flex",
|
||||
styles.AlignItems: "center",
|
||||
styles.Gap: spaceM,
|
||||
styles.Padding: spaceL,
|
||||
styles.BackgroundColor: colorSuccessLight,
|
||||
styles.Border: "1px solid " + colorSuccess,
|
||||
styles.BorderRadius: "0.5rem",
|
||||
styles.MarginBottom: spaceXL,
|
||||
}.ToInline(),
|
||||
},
|
||||
checkboxIcon(),
|
||||
elem.Div(nil,
|
||||
elem.Strong(attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.Display: "block",
|
||||
styles.Color: colorSuccess,
|
||||
styles.FontSize: fontSizeH3,
|
||||
styles.MarginBottom: spaceXS,
|
||||
}.ToInline(),
|
||||
}, elem.Text("Signed in successfully")),
|
||||
elem.P(attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.Margin: "0",
|
||||
styles.Color: colorTextPrimary,
|
||||
styles.FontSize: fontSizeBase,
|
||||
}.ToInline(),
|
||||
}, elem.Text(verb), elem.Text(" as "), elem.Strong(nil, elem.Text(user)), elem.Text(". You can now close this window.")),
|
||||
),
|
||||
elem.Body(attrs.Props{"translate": "no"},
|
||||
elem.Div(attrs.Props{attrs.Class: "container"},
|
||||
elem.Div(nil,
|
||||
headscaleLogo(),
|
||||
elem.Div(attrs.Props{attrs.Class: "message is-success"},
|
||||
checkboxIcon(),
|
||||
elem.Div(attrs.Props{attrs.Class: "message-content"},
|
||||
elem.Div(attrs.Props{attrs.Class: "message-title"},
|
||||
elem.Text("Signed in via your OIDC provider"),
|
||||
),
|
||||
elem.P(attrs.Props{attrs.Class: "message-body"},
|
||||
elem.Text(verb),
|
||||
elem.Text(" as "),
|
||||
elem.Text(user),
|
||||
elem.Text(", you can now close this window."),
|
||||
),
|
||||
),
|
||||
),
|
||||
elem.Hr(nil),
|
||||
elem.H1(nil, elem.Text("Not sure how to get started?")),
|
||||
elem.P(attrs.Props{attrs.Class: "learn"},
|
||||
elem.Text("Check out beginner and advanced guides on, or read more in the documentation."),
|
||||
),
|
||||
elem.A(attrs.Props{
|
||||
attrs.Href: "https://github.com/juanfont/headscale/tree/main/docs",
|
||||
attrs.Rel: "noreferrer noopener",
|
||||
attrs.Target: "_blank",
|
||||
},
|
||||
elem.Span(attrs.Props{attrs.Class: "icon"},
|
||||
externalLinkIcon(),
|
||||
),
|
||||
elem.Text("View the headscale documentation"),
|
||||
),
|
||||
elem.A(attrs.Props{
|
||||
attrs.Href: "https://tailscale.com/kb/",
|
||||
attrs.Rel: "noreferrer noopener",
|
||||
attrs.Target: "_blank",
|
||||
},
|
||||
elem.Span(attrs.Props{attrs.Class: "icon"},
|
||||
externalLinkIcon(),
|
||||
),
|
||||
elem.Text("View the tailscale documentation"),
|
||||
),
|
||||
)
|
||||
|
||||
return HtmlStructure(
|
||||
elem.Title(nil, elem.Text("Headscale Authentication Succeeded")),
|
||||
mdTypesetBody(
|
||||
headscaleLogo(),
|
||||
successBox,
|
||||
H2(elem.Text("Getting started")),
|
||||
P(elem.Text("Check out the documentation to learn more about headscale and Tailscale:")),
|
||||
Ul(
|
||||
elem.Li(nil,
|
||||
externalLink("https://github.com/juanfont/headscale/tree/main/docs", "Headscale documentation"),
|
||||
),
|
||||
elem.Li(nil,
|
||||
externalLink("https://tailscale.com/kb/", "Tailscale knowledge base"),
|
||||
),
|
||||
),
|
||||
pageFooter(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,30 +4,18 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/chasefleming/elem-go"
|
||||
"github.com/chasefleming/elem-go/attrs"
|
||||
"github.com/chasefleming/elem-go/styles"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
)
|
||||
|
||||
var codeStyleRegisterWebAPI = styles.Props{
|
||||
styles.Display: "block",
|
||||
styles.Padding: "20px",
|
||||
styles.Border: "1px solid #bbb",
|
||||
styles.BackgroundColor: "#eee",
|
||||
}
|
||||
|
||||
func RegisterWeb(registrationID types.RegistrationID) *elem.Element {
|
||||
return HtmlStructure(
|
||||
elem.Title(nil, elem.Text("Registration - Headscale")),
|
||||
elem.Body(attrs.Props{
|
||||
attrs.Style: bodyStyle.ToInline(),
|
||||
},
|
||||
headerOne("headscale"),
|
||||
headerTwo("Machine registration"),
|
||||
elem.P(nil, elem.Text("Run the command below in the headscale server to add this machine to your network: ")),
|
||||
elem.Code(attrs.Props{attrs.Style: codeStyleRegisterWebAPI.ToInline()},
|
||||
elem.Text(fmt.Sprintf("headscale nodes register --key %s --user USERNAME", registrationID.String())),
|
||||
),
|
||||
mdTypesetBody(
|
||||
headscaleLogo(),
|
||||
H1(elem.Text("Machine registration")),
|
||||
P(elem.Text("Run the command below in the headscale server to add this machine to your network:")),
|
||||
Pre(PreCode(fmt.Sprintf("headscale nodes register --key %s --user USERNAME", registrationID.String()))),
|
||||
pageFooter(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package templates
|
||||
|
||||
import (
|
||||
"github.com/chasefleming/elem-go"
|
||||
"github.com/chasefleming/elem-go/attrs"
|
||||
)
|
||||
|
||||
func Windows(url string) *elem.Element {
|
||||
@@ -10,28 +9,19 @@ func Windows(url string) *elem.Element {
|
||||
elem.Title(nil,
|
||||
elem.Text("headscale - Windows"),
|
||||
),
|
||||
elem.Body(attrs.Props{
|
||||
attrs.Style: bodyStyle.ToInline(),
|
||||
},
|
||||
headerOne("headscale: Windows configuration"),
|
||||
elem.P(nil,
|
||||
mdTypesetBody(
|
||||
headscaleLogo(),
|
||||
H1(elem.Text("Windows configuration")),
|
||||
P(
|
||||
elem.Text("Download "),
|
||||
elem.A(attrs.Props{
|
||||
attrs.Href: "https://tailscale.com/download/windows",
|
||||
attrs.Rel: "noreferrer noopener",
|
||||
attrs.Target: "_blank",
|
||||
},
|
||||
elem.Text("Tailscale for Windows ")),
|
||||
elem.Text("and install it."),
|
||||
externalLink("https://tailscale.com/download/windows", "Tailscale for Windows"),
|
||||
elem.Text(" and install it."),
|
||||
),
|
||||
elem.P(nil,
|
||||
elem.Text("Open a Command Prompt or Powershell and use Tailscale's login command to connect with headscale: "),
|
||||
),
|
||||
elem.Pre(nil,
|
||||
elem.Code(nil,
|
||||
elem.Text("tailscale login --login-server "+url),
|
||||
),
|
||||
P(
|
||||
elem.Text("Open a Command Prompt or PowerShell and use Tailscale's login command to connect with headscale:"),
|
||||
),
|
||||
Pre(PreCode("tailscale login --login-server "+url)),
|
||||
pageFooter(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user