mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-23 09:08:44 +02:00
Replace hardcoded Go color constants with var(--hs-*) and var(--md-*) CSS custom properties in externalLink, orDivider, card, warningBox, downloadButton, and pageFooter. This ensures all components follow the dark mode theme automatically. Also switch pageFooter from div to semantic footer element and simplify externalLink by letting CSS handle link styling. Updates juanfont/headscale#3182
561 lines
21 KiB
Go
561 lines
21 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 // rgba(0,0,0,0.87) - Body text
|
|
colorTextSecondary = "#0000008a" //nolint:unused // rgba(0,0,0,0.54) - Headings (--md-default-fg-color--light)
|
|
colorTextTertiary = "#00000052" //nolint:unused // rgba(0,0,0,0.32) - Lighter text
|
|
colorTextLightest = "#00000012" //nolint:unused // rgba(0,0,0,0.07) - Lightest text
|
|
|
|
// Code colors - from --md-code-* CSS variables.
|
|
colorCodeFg = "#36464e" //nolint:unused // Code text color (--md-code-fg-color)
|
|
colorCodeBg = "#f5f5f5" //nolint:unused // Code background (--md-code-bg-color)
|
|
|
|
// Border colors.
|
|
colorBorderLight = "#e5e7eb" //nolint:unused // Light borders
|
|
colorBorderMedium = "#d1d5db" //nolint:unused // Medium borders
|
|
|
|
// Background colors.
|
|
colorBackgroundPage = "#ffffff" //nolint:unused // Page background
|
|
colorBackgroundCard = "#ffffff" //nolint:unused // Card/content background
|
|
|
|
// Accent colors - from --md-primary/accent-fg-color.
|
|
colorPrimaryAccent = "#4051b5" //nolint:unused // Primary accent (links)
|
|
colorAccent = "#526cfe" //nolint:unused // Secondary accent
|
|
|
|
// Success colors.
|
|
colorSuccess = "#059669" //nolint:unused // Success states
|
|
colorSuccessLight = "#d1fae5" //nolint:unused // Success backgrounds
|
|
|
|
// Error colors.
|
|
colorError = "#dc2626" //nolint:unused // Error states (red-600)
|
|
colorErrorLight = "#fee2e2" //nolint:unused // Error backgrounds (red-100)
|
|
)
|
|
|
|
// 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 // 4px - Tight spacing
|
|
spaceS = "0.5rem" //nolint:unused // 8px - Small spacing
|
|
spaceM = "1rem" //nolint:unused // 16px - Medium spacing (base)
|
|
spaceL = "1.5rem" //nolint:unused // 24px - Large spacing
|
|
spaceXL = "2rem" //nolint:unused // 32px - Extra large spacing
|
|
space2XL = "3rem" //nolint:unused // 48px - 2x extra large spacing
|
|
space3XL = "4rem" //nolint:unused // 64px - 3x extra large spacing
|
|
)
|
|
|
|
// 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 // 12.8px - Base text (.md-typeset)
|
|
fontSizeH1 = "2em" //nolint:unused // 2x base - Main headings
|
|
fontSizeH2 = "1.5625em" //nolint:unused // 1.5625x base - Section headings
|
|
fontSizeH3 = "1.25em" //nolint:unused // 1.25x base - Subsection headings
|
|
fontSizeSmall = "0.8em" //nolint:unused // 0.8x base - Small text
|
|
fontSizeCode = "0.85em" //nolint:unused // 0.85x base - Inline code
|
|
|
|
// Line heights - from .md-typeset CSS rules.
|
|
lineHeightBase = "1.6" //nolint:unused // Body text (.md-typeset)
|
|
lineHeightH1 = "1.3" //nolint:unused // H1 headings
|
|
lineHeightH2 = "1.4" //nolint:unused // H2 headings
|
|
lineHeightH3 = "1.5" //nolint:unused // H3 headings
|
|
lineHeightCode = "1.4" //nolint:unused // Code blocks (pre)
|
|
)
|
|
|
|
// 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 // Reserved for future use in Phase 4.
|
|
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 // Reserved for future use in Phase 4.
|
|
func card(title string, children ...elem.Node) *elem.Element {
|
|
cardContent := children
|
|
if title != "" {
|
|
cardContent = append([]elem.Node{
|
|
elem.H3(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.MarginTop: "0",
|
|
styles.MarginBottom: spaceM,
|
|
}.ToInline(),
|
|
}, elem.Text(title)),
|
|
}, children...)
|
|
}
|
|
|
|
return elem.Div(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Background: "var(--hs-bg)",
|
|
styles.Border: "1px solid var(--hs-border)",
|
|
styles.BorderRadius: "0.5rem",
|
|
styles.Padding: "clamp(1rem, 3vw, 1.5rem)",
|
|
styles.MarginBottom: spaceL,
|
|
styles.BoxShadow: "0 1px 3px rgba(0,0,0,0.1)",
|
|
}.ToInline(),
|
|
}, cardContent...)
|
|
}
|
|
|
|
// Code Block Component
|
|
// EXTRACTED FROM: .md-typeset pre CSS rules
|
|
// Exact styling from Material for MkDocs documentation.
|
|
//
|
|
//nolint:unused // Used across apple.go, windows.go, register_web.go templates.
|
|
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 // Used in general.go for mdTypesetBody.
|
|
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 // Used across templates for main headings.
|
|
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 // Used across templates for section headings.
|
|
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 // Used across templates for subsection headings.
|
|
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 // Used for consistent paragraph spacing.
|
|
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 // Used for numbered instruction lists.
|
|
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 // Used for bullet point lists.
|
|
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 // Used for text links.
|
|
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 // Used for inline code snippets.
|
|
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 // Reserved for future inline code usage.
|
|
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 // Used in apple.go template.
|
|
func orDivider() *elem.Element {
|
|
lineStyle := styles.Props{
|
|
styles.Flex: "1",
|
|
styles.Height: "1px",
|
|
styles.BackgroundColor: "var(--hs-border)",
|
|
}.ToInline()
|
|
|
|
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: lineStyle}),
|
|
elem.Strong(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Color: "var(--md-default-fg-color--light)",
|
|
styles.FontSize: fontSizeBase,
|
|
styles.FontWeight: "500",
|
|
"text-transform": "uppercase",
|
|
"letter-spacing": "0.05em",
|
|
}.ToInline(),
|
|
}, elem.Text("or")),
|
|
elem.Div(attrs.Props{attrs.Style: lineStyle}),
|
|
)
|
|
}
|
|
|
|
// successBox creates a green success feedback box with a checkmark icon.
|
|
// The heading is displayed as bold green text, and children are rendered below it.
|
|
// Pairs with warningBox for consistent feedback styling.
|
|
//
|
|
//nolint:unused // Used in auth_success.go template.
|
|
func successBox(heading string, children ...elem.Node) *elem.Element {
|
|
return elem.Div(
|
|
attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Display: "flex",
|
|
styles.AlignItems: "center",
|
|
styles.Gap: spaceM,
|
|
styles.Padding: spaceL,
|
|
styles.BackgroundColor: "var(--hs-success-bg)",
|
|
styles.Border: "1px solid var(--hs-success)",
|
|
styles.BorderRadius: "0.5rem",
|
|
styles.MarginBottom: spaceXL,
|
|
}.ToInline(),
|
|
attrs.Role: "status",
|
|
"aria-live": "polite",
|
|
},
|
|
checkboxIcon(),
|
|
elem.Div(nil,
|
|
append([]elem.Node{
|
|
elem.Strong(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Display: "block",
|
|
styles.Color: "var(--hs-success)",
|
|
styles.FontSize: fontSizeH3,
|
|
styles.FontWeight: "700",
|
|
styles.MarginBottom: spaceXS,
|
|
}.ToInline(),
|
|
}, elem.Text(heading)),
|
|
}, children...)...,
|
|
),
|
|
)
|
|
}
|
|
|
|
// 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="48" height="48" viewBox="0 0 512 512" style="flex-shrink:0">
|
|
<path fill="currentColor" 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>`)
|
|
}
|
|
|
|
// errorBox creates a red error feedback box with an X-circle icon.
|
|
// The heading is displayed as bold red text, and children are rendered below it.
|
|
// Pairs with successBox for consistent feedback styling.
|
|
//
|
|
//nolint:unused // Used in auth_error.go template.
|
|
func errorBox(heading string, children ...elem.Node) *elem.Element {
|
|
return elem.Div(
|
|
attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Display: "flex",
|
|
styles.AlignItems: "center",
|
|
styles.Gap: spaceM,
|
|
styles.Padding: spaceL,
|
|
styles.BackgroundColor: "var(--hs-error-bg)",
|
|
styles.Border: "1px solid var(--hs-error)",
|
|
styles.BorderRadius: "0.5rem",
|
|
styles.MarginBottom: spaceXL,
|
|
}.ToInline(),
|
|
attrs.Role: "alert",
|
|
"aria-live": "assertive",
|
|
},
|
|
errorIcon(),
|
|
elem.Div(nil,
|
|
append([]elem.Node{
|
|
elem.Strong(attrs.Props{
|
|
attrs.Style: styles.Props{
|
|
styles.Display: "block",
|
|
styles.Color: "var(--hs-error)",
|
|
styles.FontSize: fontSizeH3,
|
|
styles.FontWeight: "700",
|
|
styles.MarginBottom: spaceXS,
|
|
}.ToInline(),
|
|
}, elem.Text(heading)),
|
|
}, children...)...,
|
|
),
|
|
)
|
|
}
|
|
|
|
// errorIcon returns the error X-circle SVG icon as raw HTML.
|
|
func errorIcon() elem.Node {
|
|
return elem.Raw(`<svg id="error-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 512 512" style="flex-shrink:0">
|
|
<path fill="currentColor" d="M256 32C132.3 32 32 132.3 32 256s100.3 224 224 224 224-100.3 224-224S379.7 32 256 32zm97.7 272.7c6.2 6.2 6.2 16.4 0 22.6-3.1 3.1-7.2 4.7-11.3 4.7s-8.2-1.6-11.3-4.7L256 251.3l-75.1 76c-3.1 3.1-7.2 4.7-11.3 4.7s-8.2-1.6-11.3-4.7c-6.2-6.2-6.2-16.4 0-22.6l75.1-76-75.1-76c-6.2-6.2-6.2-16.4 0-22.6s16.4-6.2 22.6 0l75.1 76 75.1-76c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6L278.6 229l75.1 75.7z"></path>
|
|
</svg>`)
|
|
}
|
|
|
|
// warningBox creates a warning message box with icon and content.
|
|
//
|
|
//nolint:unused // Used in apple.go template.
|
|
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: "var(--hs-warning-bg)",
|
|
styles.Border: "1px solid var(--hs-warning-border)",
|
|
styles.BorderRadius: "0.5rem",
|
|
styles.MarginTop: spaceL,
|
|
styles.MarginBottom: spaceL,
|
|
}.ToInline(),
|
|
attrs.Role: "note",
|
|
},
|
|
elem.Raw(`<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--hs-warning-border)" 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: "var(--hs-warning-text)",
|
|
styles.FontSize: fontSizeH3,
|
|
styles.MarginBottom: spaceXS,
|
|
}.ToInline(),
|
|
}, elem.Text(title)),
|
|
elem.Div(nil, elem.Text(message)),
|
|
),
|
|
)
|
|
}
|
|
|
|
// downloadButton creates a nice button-style link for downloads.
|
|
//
|
|
//nolint:unused // Used in apple.go template.
|
|
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-flex",
|
|
styles.AlignItems: "center",
|
|
styles.Padding: "0.75rem 1.5rem",
|
|
styles.BackgroundColor: "var(--md-primary-fg-color)",
|
|
styles.Color: "#ffffff",
|
|
styles.TextDecoration: "none",
|
|
styles.BorderRadius: "0.375rem",
|
|
styles.FontWeight: "500",
|
|
styles.Transition: "background-color 150ms ease-out",
|
|
styles.MarginRight: spaceM,
|
|
styles.MarginBottom: spaceM,
|
|
"min-height": "44px",
|
|
}.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 // Used in apple.go, oidc_callback.go templates.
|
|
func externalLink(href, text string) *elem.Element {
|
|
return elem.A(attrs.Props{
|
|
attrs.Href: href,
|
|
attrs.Rel: "noreferrer noopener",
|
|
attrs.Target: "_blank",
|
|
}, elem.Text(text))
|
|
}
|
|
|
|
// Instruction Step Component
|
|
// For numbered instruction lists with consistent formatting.
|
|
//
|
|
//nolint:unused // Reserved for future use in Phase 4.
|
|
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 // Reserved for future use in Phase 4.
|
|
func statusMessage(message string, isSuccess bool) *elem.Element {
|
|
bgColor := colorSuccessLight
|
|
textColor := colorSuccess
|
|
|
|
if !isSuccess {
|
|
bgColor = colorErrorLight
|
|
textColor = colorError
|
|
}
|
|
|
|
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))
|
|
}
|