From c15caff48c2d7437e8e75f1ad4ca5a88c7831238 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 13 Apr 2026 08:44:41 +0000 Subject: [PATCH] templates: add error box component and error page template Add errorBox() and errorIcon() to the design system, mirroring the existing successBox()/checkboxIcon() pattern with red error styling. Extract error color constants from the inline values in statusMessage(). Add AuthError() template that renders a styled HTML error page using the same HtmlStructure/mdTypesetBody/logo/footer as all other browser-facing pages. Updates juanfont/headscale#3182 --- hscontrol/templates/auth_error.go | 41 ++++++++++++++++++++++++++ hscontrol/templates/design.go | 49 +++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 hscontrol/templates/auth_error.go diff --git a/hscontrol/templates/auth_error.go b/hscontrol/templates/auth_error.go new file mode 100644 index 00000000..7b48974c --- /dev/null +++ b/hscontrol/templates/auth_error.go @@ -0,0 +1,41 @@ +package templates + +import ( + "github.com/chasefleming/elem-go" +) + +// AuthErrorResult contains the text content for an error page shown +// to users in their browser when a browser-facing operation fails +// (OIDC callback, SSH check, registration confirmation, etc.). +type AuthErrorResult struct { + // Title is the browser tab / page title, + // e.g. "Headscale - Error". + Title string + + // Heading is the bold red text inside the error box, + // e.g. "Forbidden". + Heading string + + // Message is the actionable user-facing message shown below + // the heading, e.g. "You are not authorized. Please contact + // your administrator." + Message string +} + +// AuthError renders a styled error page for browser-facing failures. +// The caller controls every user-visible string via [AuthErrorResult]. +func AuthError(result AuthErrorResult) *elem.Element { + box := errorBox( + result.Heading, + elem.Text(result.Message), + ) + + return HtmlStructure( + elem.Title(nil, elem.Text(result.Title)), + mdTypesetBody( + headscaleLogo(), + box, + pageFooter(), + ), + ) +} diff --git a/hscontrol/templates/design.go b/hscontrol/templates/design.go index 221eaf11..eb2d46ca 100644 --- a/hscontrol/templates/design.go +++ b/hscontrol/templates/design.go @@ -39,6 +39,10 @@ const ( // 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 @@ -406,6 +410,47 @@ func checkboxIcon() elem.Node { `) } +// 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: colorErrorLight, + styles.Border: "1px solid " + colorError, + styles.BorderRadius: "0.5rem", + styles.MarginBottom: spaceXL, + }.ToInline(), + }, + errorIcon(), + elem.Div(nil, + append([]elem.Node{ + elem.Strong(attrs.Props{ + attrs.Style: styles.Props{ + styles.Display: "block", + styles.Color: colorError, + styles.FontSize: fontSizeH3, + 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(``) +} + // warningBox creates a warning message box with icon and content. // //nolint:unused // Used in apple.go template. @@ -504,8 +549,8 @@ func statusMessage(message string, isSuccess bool) *elem.Element { textColor := colorSuccess if !isSuccess { - bgColor = "#fee2e2" // red-100 - textColor = "#dc2626" // red-600 + bgColor = colorErrorLight + textColor = colorError } return elem.Div(attrs.Props{