mirror of
https://github.com/juanfont/headscale.git
synced 2026-01-23 02:24:10 +00:00
Disable the thelper linter which triggers on inline test closures in table-driven tests. These closures are intentionally not standalone helpers and don't benefit from t.Helper(). Also remove explanatory comments from nolint directives throughout the codebase as they add noise without providing significant value.
482 lines
16 KiB
Go
482 lines
16 KiB
Go
package templates
|
|
|
|
import (
|
|
elem "github.com/chasefleming/elem-go"
|
|
"github.com/chasefleming/elem-go/attrs"
|
|
"github.com/chasefleming/elem-go/styles"
|
|
)
|
|
|
|
// Design System Constants
|
|
// These constants define the visual language for all Headscale HTML templates.
|
|
// They ensure consistency across all pages and make it easy to maintain and update the design.
|
|
|
|
// Color System
|
|
// EXTRACTED FROM: https://headscale.net/stable/assets/stylesheets/main.342714a4.min.css
|
|
// Material for MkDocs design system - exact values from official docs.
|
|
const (
|
|
// Text colors - from --md-default-fg-color CSS variables.
|
|
colorTextPrimary = "#000000de" //nolint:unused
|
|
colorTextSecondary = "#0000008a" //nolint:unused
|
|
colorTextTertiary = "#00000052" //nolint:unused
|
|
colorTextLightest = "#00000012" //nolint:unused
|
|
|
|
// Code colors - from --md-code-* CSS variables.
|
|
colorCodeFg = "#36464e" //nolint:unused
|
|
colorCodeBg = "#f5f5f5" //nolint:unused
|
|
|
|
// Border colors.
|
|
colorBorderLight = "#e5e7eb" //nolint:unused
|
|
colorBorderMedium = "#d1d5db" //nolint:unused
|
|
|
|
// Background colors.
|
|
colorBackgroundPage = "#ffffff" //nolint:unused
|
|
colorBackgroundCard = "#ffffff" //nolint:unused
|
|
|
|
// Accent colors - from --md-primary/accent-fg-color.
|
|
colorPrimaryAccent = "#4051b5" //nolint:unused
|
|
colorAccent = "#526cfe" //nolint:unused
|
|
|
|
// Success colors.
|
|
colorSuccess = "#059669" //nolint:unused
|
|
colorSuccessLight = "#d1fae5" //nolint:unused
|
|
)
|
|
|
|
// Spacing System
|
|
// Based on 4px/8px base unit for consistent rhythm.
|
|
// Uses rem units for scalability with user font size preferences.
|
|
const (
|
|
spaceXS = "0.25rem" //nolint:unused
|
|
spaceS = "0.5rem" //nolint:unused
|
|
spaceM = "1rem" //nolint:unused
|
|
spaceL = "1.5rem" //nolint:unused
|
|
spaceXL = "2rem" //nolint:unused
|
|
space2XL = "3rem" //nolint:unused
|
|
space3XL = "4rem" //nolint:unused
|
|
)
|
|
|
|
// Typography System
|
|
// EXTRACTED FROM: https://headscale.net/stable/assets/stylesheets/main.342714a4.min.css
|
|
// Material for MkDocs typography - exact values from .md-typeset CSS.
|
|
const (
|
|
// Font families - from CSS custom properties.
|
|
fontFamilySystem = `"Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif` //nolint:unused
|
|
fontFamilyCode = `"Roboto Mono", "SF Mono", Monaco, "Cascadia Code", Consolas, "Courier New", monospace` //nolint:unused
|
|
|
|
// Font sizes - from .md-typeset CSS rules.
|
|
fontSizeBase = "0.8rem" //nolint:unused
|
|
fontSizeH1 = "2em" //nolint:unused
|
|
fontSizeH2 = "1.5625em" //nolint:unused
|
|
fontSizeH3 = "1.25em" //nolint:unused
|
|
fontSizeSmall = "0.8em" //nolint:unused
|
|
fontSizeCode = "0.85em" //nolint:unused
|
|
|
|
// Line heights - from .md-typeset CSS rules.
|
|
lineHeightBase = "1.6" //nolint:unused
|
|
lineHeightH1 = "1.3" //nolint:unused
|
|
lineHeightH2 = "1.4" //nolint:unused
|
|
lineHeightH3 = "1.5" //nolint:unused
|
|
lineHeightCode = "1.4" //nolint:unused
|
|
)
|
|
|
|
// Responsive Container Component
|
|
// Creates a centered container with responsive padding and max-width.
|
|
// Mobile-first approach: starts at 100% width with padding, constrains on larger screens.
|
|
//
|
|
//nolint:unused
|
|
func responsiveContainer(children ...elem.Node) *elem.Element {
|
|
return elem.Div(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Width: "100%",
|
|
styles.MaxWidth: "min(800px, 90vw)", // Responsive: 90% of viewport or 800px max
|
|
styles.Margin: "0 auto", // Center horizontally
|
|
styles.Padding: "clamp(1rem, 5vw, 2.5rem)", // Fluid padding: 16px to 40px
|
|
}.ToInline(),
|
|
}, children...)
|
|
}
|
|
|
|
// Card Component
|
|
// Reusable card for grouping related content with visual separation.
|
|
// Parameters:
|
|
// - title: Optional title for the card (empty string for no title)
|
|
// - children: Content elements to display in the card
|
|
//
|
|
//nolint:unused
|
|
func card(title string, children ...elem.Node) *elem.Element {
|
|
cardContent := children
|
|
if title != "" {
|
|
// Prepend title as H3 if provided
|
|
cardContent = append([]elem.Node{
|
|
elem.H3(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.MarginTop: "0",
|
|
styles.MarginBottom: spaceM,
|
|
styles.FontSize: fontSizeH3,
|
|
styles.LineHeight: lineHeightH3, // 1.5 - H3 line height
|
|
styles.Color: colorTextSecondary,
|
|
}.ToInline(),
|
|
}, elem.Text(title)),
|
|
}, children...)
|
|
}
|
|
|
|
return elem.Div(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Background: colorBackgroundCard,
|
|
styles.Border: "1px solid " + colorBorderLight,
|
|
styles.BorderRadius: "0.5rem", // 8px rounded corners
|
|
styles.Padding: "clamp(1rem, 3vw, 1.5rem)", // Responsive padding
|
|
styles.MarginBottom: spaceL,
|
|
styles.BoxShadow: "0 1px 3px rgba(0,0,0,0.1)", // Subtle shadow
|
|
}.ToInline(),
|
|
}, cardContent...)
|
|
}
|
|
|
|
// Code Block Component
|
|
// EXTRACTED FROM: .md-typeset pre CSS rules
|
|
// Exact styling from Material for MkDocs documentation.
|
|
//
|
|
//nolint:unused
|
|
func codeBlock(code string) *elem.Element {
|
|
return elem.Pre(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Display: "block",
|
|
styles.Padding: "0.77em 1.18em", // From .md-typeset pre
|
|
styles.Border: "none", // No border in original
|
|
styles.BorderRadius: "0.1rem", // From .md-typeset code
|
|
styles.BackgroundColor: colorCodeBg, // #f5f5f5
|
|
styles.FontFamily: fontFamilyCode, // Roboto Mono
|
|
styles.FontSize: fontSizeCode, // 0.85em
|
|
styles.LineHeight: lineHeightCode, // 1.4
|
|
styles.OverflowX: "auto", // Horizontal scroll
|
|
"overflow-wrap": "break-word", // Word wrapping
|
|
"word-wrap": "break-word", // Legacy support
|
|
styles.WhiteSpace: "pre-wrap", // Preserve whitespace
|
|
styles.MarginTop: spaceM, // 1em
|
|
styles.MarginBottom: spaceM, // 1em
|
|
styles.Color: colorCodeFg, // #36464e
|
|
styles.BoxShadow: "none", // No shadow in original
|
|
}.ToInline(),
|
|
},
|
|
elem.Code(nil, elem.Text(code)),
|
|
)
|
|
}
|
|
|
|
// Base Typeset Styles
|
|
// Returns inline styles for the main content container that matches .md-typeset.
|
|
// EXTRACTED FROM: .md-typeset CSS rule from Material for MkDocs.
|
|
//
|
|
//nolint:unused
|
|
func baseTypesetStyles() styles.Props {
|
|
return styles.Props{
|
|
styles.FontSize: fontSizeBase, // 0.8rem
|
|
styles.LineHeight: lineHeightBase, // 1.6
|
|
styles.Color: colorTextPrimary,
|
|
styles.FontFamily: fontFamilySystem,
|
|
"overflow-wrap": "break-word",
|
|
styles.TextAlign: "left",
|
|
}
|
|
}
|
|
|
|
// H1 Styles
|
|
// Returns inline styles for H1 headings that match .md-typeset h1.
|
|
// EXTRACTED FROM: .md-typeset h1 CSS rule from Material for MkDocs.
|
|
//
|
|
//nolint:unused
|
|
func h1Styles() styles.Props {
|
|
return styles.Props{
|
|
styles.Color: colorTextSecondary, // rgba(0, 0, 0, 0.54)
|
|
styles.FontSize: fontSizeH1, // 2em
|
|
styles.LineHeight: lineHeightH1, // 1.3
|
|
styles.Margin: "0 0 1.25em",
|
|
styles.FontWeight: "300",
|
|
"letter-spacing": "-0.01em",
|
|
styles.FontFamily: fontFamilySystem, // Roboto
|
|
"overflow-wrap": "break-word",
|
|
}
|
|
}
|
|
|
|
// H2 Styles
|
|
// Returns inline styles for H2 headings that match .md-typeset h2.
|
|
// EXTRACTED FROM: .md-typeset h2 CSS rule from Material for MkDocs.
|
|
//
|
|
//nolint:unused
|
|
func h2Styles() styles.Props {
|
|
return styles.Props{
|
|
styles.FontSize: fontSizeH2, // 1.5625em
|
|
styles.LineHeight: lineHeightH2, // 1.4
|
|
styles.Margin: "1.6em 0 0.64em",
|
|
styles.FontWeight: "300",
|
|
"letter-spacing": "-0.01em",
|
|
styles.Color: colorTextSecondary, // rgba(0, 0, 0, 0.54)
|
|
styles.FontFamily: fontFamilySystem, // Roboto
|
|
"overflow-wrap": "break-word",
|
|
}
|
|
}
|
|
|
|
// H3 Styles
|
|
// Returns inline styles for H3 headings that match .md-typeset h3.
|
|
// EXTRACTED FROM: .md-typeset h3 CSS rule from Material for MkDocs.
|
|
//
|
|
//nolint:unused
|
|
func h3Styles() styles.Props {
|
|
return styles.Props{
|
|
styles.FontSize: fontSizeH3, // 1.25em
|
|
styles.LineHeight: lineHeightH3, // 1.5
|
|
styles.Margin: "1.6em 0 0.8em",
|
|
styles.FontWeight: "400",
|
|
"letter-spacing": "-0.01em",
|
|
styles.Color: colorTextSecondary, // rgba(0, 0, 0, 0.54)
|
|
styles.FontFamily: fontFamilySystem, // Roboto
|
|
"overflow-wrap": "break-word",
|
|
}
|
|
}
|
|
|
|
// Paragraph Styles
|
|
// Returns inline styles for paragraphs that match .md-typeset p.
|
|
// EXTRACTED FROM: .md-typeset p CSS rule from Material for MkDocs.
|
|
//
|
|
//nolint:unused
|
|
func paragraphStyles() styles.Props {
|
|
return styles.Props{
|
|
styles.Margin: "1em 0",
|
|
styles.FontFamily: fontFamilySystem, // Roboto
|
|
styles.FontSize: fontSizeBase, // 0.8rem - inherited from .md-typeset
|
|
styles.LineHeight: lineHeightBase, // 1.6 - inherited from .md-typeset
|
|
styles.Color: colorTextPrimary, // rgba(0, 0, 0, 0.87)
|
|
"overflow-wrap": "break-word",
|
|
}
|
|
}
|
|
|
|
// Ordered List Styles
|
|
// Returns inline styles for ordered lists that match .md-typeset ol.
|
|
// EXTRACTED FROM: .md-typeset ol CSS rule from Material for MkDocs.
|
|
//
|
|
//nolint:unused
|
|
func orderedListStyles() styles.Props {
|
|
return styles.Props{
|
|
styles.MarginBottom: "1em",
|
|
styles.MarginTop: "1em",
|
|
styles.PaddingLeft: "2em",
|
|
styles.FontFamily: fontFamilySystem, // Roboto - inherited from .md-typeset
|
|
styles.FontSize: fontSizeBase, // 0.8rem - inherited from .md-typeset
|
|
styles.LineHeight: lineHeightBase, // 1.6 - inherited from .md-typeset
|
|
styles.Color: colorTextPrimary, // rgba(0, 0, 0, 0.87) - inherited from .md-typeset
|
|
"overflow-wrap": "break-word",
|
|
}
|
|
}
|
|
|
|
// Unordered List Styles
|
|
// Returns inline styles for unordered lists that match .md-typeset ul.
|
|
// EXTRACTED FROM: .md-typeset ul CSS rule from Material for MkDocs.
|
|
//
|
|
//nolint:unused
|
|
func unorderedListStyles() styles.Props {
|
|
return styles.Props{
|
|
styles.MarginBottom: "1em",
|
|
styles.MarginTop: "1em",
|
|
styles.PaddingLeft: "2em",
|
|
styles.FontFamily: fontFamilySystem, // Roboto - inherited from .md-typeset
|
|
styles.FontSize: fontSizeBase, // 0.8rem - inherited from .md-typeset
|
|
styles.LineHeight: lineHeightBase, // 1.6 - inherited from .md-typeset
|
|
styles.Color: colorTextPrimary, // rgba(0, 0, 0, 0.87) - inherited from .md-typeset
|
|
"overflow-wrap": "break-word",
|
|
}
|
|
}
|
|
|
|
// Link Styles
|
|
// Returns inline styles for links that match .md-typeset a.
|
|
// EXTRACTED FROM: .md-typeset a CSS rule from Material for MkDocs.
|
|
// Note: Hover states cannot be implemented with inline styles.
|
|
//
|
|
//nolint:unused
|
|
func linkStyles() styles.Props {
|
|
return styles.Props{
|
|
styles.Color: colorPrimaryAccent, // #4051b5 - var(--md-primary-fg-color)
|
|
styles.TextDecoration: "none",
|
|
"word-break": "break-word",
|
|
styles.FontFamily: fontFamilySystem, // Roboto - inherited from .md-typeset
|
|
}
|
|
}
|
|
|
|
// Inline Code Styles (updated)
|
|
// Returns inline styles for inline code that matches .md-typeset code.
|
|
// EXTRACTED FROM: .md-typeset code CSS rule from Material for MkDocs.
|
|
//
|
|
//nolint:unused
|
|
func inlineCodeStyles() styles.Props {
|
|
return styles.Props{
|
|
styles.BackgroundColor: colorCodeBg, // #f5f5f5
|
|
styles.Color: colorCodeFg, // #36464e
|
|
styles.BorderRadius: "0.1rem",
|
|
styles.FontSize: fontSizeCode, // 0.85em
|
|
styles.FontFamily: fontFamilyCode, // Roboto Mono
|
|
styles.Padding: "0 0.2941176471em",
|
|
"word-break": "break-word",
|
|
}
|
|
}
|
|
|
|
// Inline Code Component
|
|
// For inline code snippets within text.
|
|
//
|
|
//nolint:unused
|
|
func inlineCode(code string) *elem.Element {
|
|
return elem.Code(attrs.Props{
|
|
attrs.Style: inlineCodeStyles().ToInline(),
|
|
}, elem.Text(code))
|
|
}
|
|
|
|
// orDivider creates a visual "or" divider between sections.
|
|
// Styled with lines on either side for better visual separation.
|
|
//
|
|
//nolint:unused
|
|
func orDivider() *elem.Element {
|
|
return elem.Div(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Display: "flex",
|
|
styles.AlignItems: "center",
|
|
styles.Gap: spaceM,
|
|
styles.MarginTop: space2XL,
|
|
styles.MarginBottom: space2XL,
|
|
styles.Width: "100%",
|
|
}.ToInline(),
|
|
},
|
|
elem.Div(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Flex: "1",
|
|
styles.Height: "1px",
|
|
styles.BackgroundColor: colorBorderLight,
|
|
}.ToInline(),
|
|
}),
|
|
elem.Strong(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Color: colorTextSecondary,
|
|
styles.FontSize: fontSizeBase,
|
|
styles.FontWeight: "500",
|
|
"text-transform": "uppercase",
|
|
"letter-spacing": "0.05em",
|
|
}.ToInline(),
|
|
}, elem.Text("or")),
|
|
elem.Div(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Flex: "1",
|
|
styles.Height: "1px",
|
|
styles.BackgroundColor: colorBorderLight,
|
|
}.ToInline(),
|
|
}),
|
|
)
|
|
}
|
|
|
|
// warningBox creates a warning message box with icon and content.
|
|
//
|
|
//nolint:unused
|
|
func warningBox(title, message string) *elem.Element {
|
|
return elem.Div(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Display: "flex",
|
|
styles.AlignItems: "flex-start",
|
|
styles.Gap: spaceM,
|
|
styles.Padding: spaceL,
|
|
styles.BackgroundColor: "#fef3c7", // yellow-100
|
|
styles.Border: "1px solid #f59e0b", // yellow-500
|
|
styles.BorderRadius: "0.5rem",
|
|
styles.MarginTop: spaceL,
|
|
styles.MarginBottom: spaceL,
|
|
}.ToInline(),
|
|
},
|
|
elem.Raw(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0; margin-top: 2px;"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`),
|
|
elem.Div(nil,
|
|
elem.Strong(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Display: "block",
|
|
styles.Color: "#92400e", // yellow-800
|
|
styles.FontSize: fontSizeH3,
|
|
styles.MarginBottom: spaceXS,
|
|
}.ToInline(),
|
|
}, elem.Text(title)),
|
|
elem.Div(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Color: colorTextPrimary,
|
|
styles.FontSize: fontSizeBase,
|
|
}.ToInline(),
|
|
}, elem.Text(message)),
|
|
),
|
|
)
|
|
}
|
|
|
|
// downloadButton creates a nice button-style link for downloads.
|
|
//
|
|
//nolint:unused
|
|
func downloadButton(href, text string) *elem.Element {
|
|
return elem.A(attrs.Props{
|
|
attrs.Href: href,
|
|
attrs.Download: "headscale_macos.mobileconfig",
|
|
attrs.Style: styles.Props{
|
|
styles.Display: "inline-block",
|
|
styles.Padding: "0.75rem 1.5rem",
|
|
styles.BackgroundColor: "#3b82f6", // blue-500
|
|
styles.Color: "#ffffff",
|
|
styles.TextDecoration: "none",
|
|
styles.BorderRadius: "0.5rem",
|
|
styles.FontWeight: "500",
|
|
styles.Transition: "background-color 0.2s",
|
|
styles.MarginRight: spaceM,
|
|
styles.MarginBottom: spaceM,
|
|
}.ToInline(),
|
|
}, elem.Text(text))
|
|
}
|
|
|
|
// External Link Component
|
|
// Creates a link with proper security attributes for external URLs.
|
|
// Automatically adds rel="noreferrer noopener" and target="_blank".
|
|
//
|
|
//nolint:unused
|
|
func externalLink(href, text string) *elem.Element {
|
|
return elem.A(attrs.Props{
|
|
attrs.Href: href,
|
|
attrs.Rel: "noreferrer noopener",
|
|
attrs.Target: "_blank",
|
|
attrs.Style: styles.Props{
|
|
styles.Color: colorPrimaryAccent, // #4051b5 - base link color
|
|
styles.TextDecoration: "none",
|
|
}.ToInline(),
|
|
}, elem.Text(text))
|
|
}
|
|
|
|
// Instruction Step Component
|
|
// For numbered instruction lists with consistent formatting.
|
|
//
|
|
//nolint:unused
|
|
func instructionStep(_ int, text string) *elem.Element {
|
|
return elem.Li(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.MarginBottom: spaceS,
|
|
styles.LineHeight: lineHeightBase,
|
|
}.ToInline(),
|
|
}, elem.Text(text))
|
|
}
|
|
|
|
// Status Message Component
|
|
// For displaying success/error/info messages with appropriate styling.
|
|
//
|
|
//nolint:unused
|
|
func statusMessage(message string, isSuccess bool) *elem.Element {
|
|
bgColor := colorSuccessLight
|
|
textColor := colorSuccess
|
|
|
|
if !isSuccess {
|
|
bgColor = "#fee2e2" // red-100
|
|
textColor = "#dc2626" // red-600
|
|
}
|
|
|
|
return elem.Div(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Padding: spaceM,
|
|
styles.BackgroundColor: bgColor,
|
|
styles.Color: textColor,
|
|
styles.BorderRadius: "0.5rem",
|
|
styles.Border: "1px solid " + textColor,
|
|
styles.MarginBottom: spaceL,
|
|
styles.FontSize: fontSizeBase,
|
|
styles.LineHeight: lineHeightBase,
|
|
}.ToInline(),
|
|
}, elem.Text(message))
|
|
}
|