@kahitsan/ksui 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ import { Show, type JSX } from "solid-js";
2
+
3
+ /** Horizontal alignment for the whole heading block. */
4
+ export type SectionHeadingAlign = "left" | "center" | "right";
5
+
6
+ /** Heading level rendered for the title element. */
7
+ export type SectionHeadingLevel = "h1" | "h2" | "h3";
8
+
9
+ export interface SectionHeadingProps {
10
+ /**
11
+ * Title content. Accept JSX so a caller can apply its own brand styling
12
+ * (e.g. a gradient span) on part of the title without this component
13
+ * carrying any site-specific CSS.
14
+ */
15
+ title: JSX.Element;
16
+ /** Small uppercase, wide-tracked label above the title (the "eyebrow"). */
17
+ kicker?: string;
18
+ /** Supporting copy below the title. Accepts JSX for richer content. */
19
+ subtitle?: JSX.Element;
20
+ /**
21
+ * Show a short underline accent bar below the title. The bar inherits the
22
+ * accent color; override with `accentClass`.
23
+ */
24
+ accent?: boolean;
25
+ /** Tailwind class controlling the accent bar color. */
26
+ accentClass?: string;
27
+ /** Tailwind class controlling the kicker color. */
28
+ kickerClass?: string;
29
+ /** Tailwind class controlling the title color. */
30
+ titleClass?: string;
31
+ /** Tailwind class controlling the subtitle color. */
32
+ subtitleClass?: string;
33
+ /** Block alignment. Defaults to "left". */
34
+ align?: SectionHeadingAlign;
35
+ /** Title element tag. Defaults to "h2". */
36
+ as?: SectionHeadingLevel;
37
+ /** Extra classes on the outer wrapper. */
38
+ class?: string;
39
+ }
40
+
41
+ const ALIGN_WRAP: Record<SectionHeadingAlign, string> = {
42
+ left: "text-left items-start",
43
+ center: "text-center items-center",
44
+ right: "text-right items-end",
45
+ };
46
+
47
+ // Per-level title sizing. Domain-free defaults; callers override via titleClass.
48
+ const TITLE_SIZE: Record<SectionHeadingLevel, string> = {
49
+ h1: "text-3xl md:text-4xl lg:text-6xl font-bold tracking-tight",
50
+ h2: "text-2xl md:text-3xl lg:text-4xl font-bold",
51
+ h3: "text-xl md:text-2xl font-bold",
52
+ };
53
+
54
+ /**
55
+ * A recurring section header block: an optional uppercase kicker, a title
56
+ * (any JSX, so the caller can apply brand-specific styling), an optional
57
+ * underline accent bar, and optional subtitle copy.
58
+ *
59
+ * Presentational only. No site copy, no domain coupling — every piece of text
60
+ * and color comes from props.
61
+ */
62
+ export default function SectionHeading(props: SectionHeadingProps): JSX.Element {
63
+ const align = () => props.align ?? "left";
64
+ const level = () => props.as ?? "h2";
65
+
66
+ const Title = (titleProps: { class: string; children: JSX.Element }): JSX.Element => {
67
+ const tag = level();
68
+ if (tag === "h1") return <h1 class={titleProps.class}>{titleProps.children}</h1>;
69
+ if (tag === "h3") return <h3 class={titleProps.class}>{titleProps.children}</h3>;
70
+ return <h2 class={titleProps.class}>{titleProps.children}</h2>;
71
+ };
72
+
73
+ return (
74
+ <div class={`flex flex-col ${ALIGN_WRAP[align()]} ${props.class ?? ""}`}>
75
+ <Show when={props.kicker}>
76
+ <div
77
+ class={`text-xs font-bold tracking-[0.3em] uppercase mb-2 ${
78
+ props.kickerClass ?? "text-amber-500"
79
+ }`}
80
+ >
81
+ {props.kicker}
82
+ </div>
83
+ </Show>
84
+
85
+ <Title class={`${TITLE_SIZE[level()]} ${props.titleClass ?? "text-white"}`}>
86
+ {props.title}
87
+ </Title>
88
+
89
+ <Show when={props.accent}>
90
+ <div class={`w-20 h-1 mt-4 rounded-full ${props.accentClass ?? "bg-amber-500"}`} />
91
+ </Show>
92
+
93
+ <Show when={props.subtitle}>
94
+ <p class={`mt-4 text-base md:text-lg max-w-2xl ${props.subtitleClass ?? "text-zinc-400"}`}>
95
+ {props.subtitle}
96
+ </p>
97
+ </Show>
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,92 @@
1
+ import { For, type Component, type JSX } from "solid-js";
2
+
3
+ /** Icon component shape: any component that accepts a numeric `size`
4
+ * (lucide-solid icons satisfy this, as does any custom SVG component). */
5
+ export type SocialIcon = Component<{ size?: number }>;
6
+
7
+ export interface SocialLink {
8
+ /** Destination URL. Opened in a new tab with rel="noopener noreferrer". */
9
+ href: string;
10
+ /** Icon component rendered inside the button (e.g. a lucide-solid icon). */
11
+ icon: SocialIcon;
12
+ /** Accessible label, used for `aria-label` and `title`. */
13
+ label: string;
14
+ }
15
+
16
+ /** Button outline shape. `round` is a full circle; `clip` cuts the
17
+ * top-right corner with a clip-path (the angular brand variant). */
18
+ export type SocialLinksShape = "round" | "clip";
19
+
20
+ export interface SocialLinksBarProps {
21
+ /** The links to render. The caller owns the URLs and icons; nothing
22
+ * domain-specific lives in the component. */
23
+ links: SocialLink[];
24
+ /** Button outline shape. Defaults to `round`. */
25
+ shape?: SocialLinksShape;
26
+ /** Pixel size of each square button. Defaults to 40 (`clip` defaults to 48). */
27
+ buttonSize?: number;
28
+ /** Pixel size of the icon. Defaults to ~45% of the button size. */
29
+ iconSize?: number;
30
+ /** Extra classes on the wrapping `<nav>`. */
31
+ class?: string;
32
+ testId?: string;
33
+ }
34
+
35
+ // Runtime-injected clip-path utility. ksui ships no sidecar CSS (the package
36
+ // exports only ./src), so the one rule the `clip` shape needs is emitted once
37
+ // as a <style> tag rather than imported from a sibling .css file.
38
+ const CLIP_STYLE_ID = "ksui-social-clip-style";
39
+ const CLIP_STYLE = `.ksui-social-clip{clip-path:polygon(0 0,calc(100% - 8px) 0,100% 8px,100% 100%,0 100%);}`;
40
+
41
+ // Inject the clip-path rule once per document, matching the SSR-safe
42
+ // getElementById-dedupe pattern used by Button/ThemeToggle. A module-level
43
+ // boolean would be non-reentrant across SSR requests (the worker reuses the
44
+ // module), so the style must be keyed off the document, not module state.
45
+ function ensureSocialClipStyle(): void {
46
+ if (typeof document === "undefined") return;
47
+ if (document.getElementById(CLIP_STYLE_ID)) return;
48
+ const el = document.createElement("style");
49
+ el.id = CLIP_STYLE_ID;
50
+ el.textContent = CLIP_STYLE;
51
+ document.head.appendChild(el);
52
+ }
53
+
54
+ /**
55
+ * A horizontal row of accessible, round (or clip-cornered) icon buttons that
56
+ * link out to external profiles. Each button opens its href in a new tab with
57
+ * a safe `rel`, carries an `aria-label`, and renders the caller-supplied icon.
58
+ *
59
+ * Domain-free: the specific URLs, icons, and labels all come from `links`.
60
+ */
61
+ export default function SocialLinksBar(props: SocialLinksBarProps): JSX.Element {
62
+ ensureSocialClipStyle();
63
+ const shape = () => props.shape ?? "round";
64
+ const btn = () => props.buttonSize ?? (shape() === "clip" ? 48 : 40);
65
+ const icon = () => props.iconSize ?? Math.round(btn() * 0.45);
66
+ const shapeClass = () =>
67
+ shape() === "clip" ? "ksui-social-clip" : "rounded-full";
68
+
69
+ return (
70
+ <nav
71
+ data-testid={props.testId}
72
+ class={`flex gap-4 ${props.class ?? ""}`}
73
+ aria-label="Social links"
74
+ >
75
+ <For each={props.links}>
76
+ {(link) => (
77
+ <a
78
+ href={link.href}
79
+ target="_blank"
80
+ rel="noopener noreferrer"
81
+ title={link.label}
82
+ aria-label={link.label}
83
+ class={`flex items-center justify-center bg-zinc-800 text-zinc-400 transition-all hover:bg-amber-500 hover:text-black ${shapeClass()}`}
84
+ style={{ width: `${btn()}px`, height: `${btn()}px` }}
85
+ >
86
+ {link.icon({ size: icon() })}
87
+ </a>
88
+ )}
89
+ </For>
90
+ </nav>
91
+ );
92
+ }
@@ -0,0 +1,110 @@
1
+ import { Show, type JSX } from "solid-js";
2
+
3
+ export type StatusIndicatorTone = "success" | "neutral" | "warning" | "danger" | "info";
4
+
5
+ interface ToneClass {
6
+ /** Filled dot color. */
7
+ dot: string;
8
+ /** Label text color. */
9
+ text: string;
10
+ /** Box-shadow glow used when the dot pulses (an arbitrary Tailwind value). */
11
+ glow: string;
12
+ }
13
+
14
+ // Module-private tone palette. The caller maps its own availability/state
15
+ // (online/offline, open/closed, healthy/degraded) to one of these tones and
16
+ // passes a plain label; nothing domain-specific leaks into this atom.
17
+ const TONE_CLASS: Record<StatusIndicatorTone, ToneClass> = {
18
+ success: {
19
+ dot: "bg-emerald-400",
20
+ text: "text-white",
21
+ glow: "shadow-[0_0_10px_rgba(52,211,153,0.6)]",
22
+ },
23
+ neutral: {
24
+ dot: "bg-zinc-400",
25
+ text: "text-zinc-400",
26
+ glow: "shadow-[0_0_10px_rgba(161,161,170,0.5)]",
27
+ },
28
+ warning: {
29
+ dot: "bg-amber-400",
30
+ text: "text-amber-300",
31
+ glow: "shadow-[0_0_10px_rgba(251,191,36,0.6)]",
32
+ },
33
+ danger: {
34
+ dot: "bg-red-400",
35
+ text: "text-red-300",
36
+ glow: "shadow-[0_0_10px_rgba(248,113,113,0.6)]",
37
+ },
38
+ info: {
39
+ dot: "bg-blue-400",
40
+ text: "text-blue-300",
41
+ glow: "shadow-[0_0_10px_rgba(96,165,250,0.6)]",
42
+ },
43
+ };
44
+
45
+ export interface StatusIndicatorProps {
46
+ /** Text shown beside the dot (the caller's own label). */
47
+ label: string;
48
+ /** Domain-free tone selector. The caller maps its enum to one of these. */
49
+ tone?: StatusIndicatorTone;
50
+ /**
51
+ * Convenience boolean: when provided, overrides `tone` with `success` (true)
52
+ * or `danger` (false). Lets a caller bind a plain availability flag.
53
+ */
54
+ online?: boolean;
55
+ /** Animate the dot with a pulse + glow (good for a "live" indicator). */
56
+ pulse?: boolean;
57
+ /** Render the label uppercase with wide tracking (the marquee/chip style). */
58
+ uppercase?: boolean;
59
+ /** Optional small caption above the label (e.g. a category line). */
60
+ caption?: string;
61
+ /** Caption text color class; defaults to an amber accent. */
62
+ captionClass?: string;
63
+ /** Extra classes on the wrapper. */
64
+ class?: string;
65
+ testId?: string;
66
+ }
67
+
68
+ // Single availability-indicator atom. A filled, optionally pulsing colored dot
69
+ // next to a label. Distinct from StatusPill (a bordered tinted text chip) — this
70
+ // is the animated "live status" dot. The caller owns the domain → tone mapping
71
+ // and the label text.
72
+ export default function StatusIndicator(props: StatusIndicatorProps): JSX.Element {
73
+ const tone = (): StatusIndicatorTone =>
74
+ props.online === undefined ? (props.tone ?? "neutral") : props.online ? "success" : "danger";
75
+ const tc = () => TONE_CLASS[tone()];
76
+
77
+ return (
78
+ <div
79
+ data-testid={props.testId}
80
+ class={`flex items-center gap-3 ${props.class ?? ""}`}
81
+ >
82
+ <span
83
+ aria-hidden="true"
84
+ class={`h-3 w-3 shrink-0 rounded-full ${tc().dot} ${
85
+ props.pulse ? `animate-pulse ${tc().glow}` : ""
86
+ }`}
87
+ />
88
+ <div>
89
+ <Show when={props.caption}>
90
+ <div
91
+ class={`text-xs font-bold uppercase tracking-widest ${
92
+ props.captionClass ?? "text-amber-400"
93
+ }`}
94
+ >
95
+ {props.caption}
96
+ </div>
97
+ </Show>
98
+ <div
99
+ class={`${tc().text} ${
100
+ props.uppercase
101
+ ? "text-xs font-bold uppercase tracking-widest"
102
+ : "text-sm font-bold"
103
+ }`}
104
+ >
105
+ {props.label}
106
+ </div>
107
+ </div>
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,112 @@
1
+ // Source: kahitsan-web src/components/Header.tsx (ThemeToggle, lines 30-45)
2
+ // + its CSS in src/assets/css/button.css (.ks-theme-toggle*).
3
+ // Extracted as a domain-free, controlled primitive: the theme state is lifted
4
+ // to props (`value` + `onToggle`) so it carries no dependency on kahitsan's
5
+ // ~/lib/theme. Only solid-js + lucide-solid.
6
+ //
7
+ // ksui ships no sibling .css and no sidecar CSS in the published package, so
8
+ // the custom track/thumb/icon styles (transitions, the sliding ::after thumb)
9
+ // are injected once at runtime via a <style> tag — the same pattern ProgressBar
10
+ // uses — and referenced by plain, unscoped class names.
11
+
12
+ import type { JSX } from "solid-js";
13
+ import { splitProps } from "solid-js";
14
+ import Sun from "lucide-solid/icons/sun";
15
+ import Moon from "lucide-solid/icons/moon";
16
+
17
+ const THEME_TOGGLE_STYLE_ID = "ks-theme-toggle-inline-style";
18
+ const THEME_TOGGLE_CSS = `
19
+ .ks-theme-toggle { background: none; border: none; padding: 2px; cursor: pointer; display: flex; align-items: center; }
20
+ .ks-theme-toggle-track { position: relative; display: flex; align-items: center; justify-content: space-between; width: 52px; height: 28px; border-radius: 14px; background-color: #3f3f46; padding: 0 2px; transition: background-color 0.3s ease; }
21
+ .ks-theme-toggle-track[data-active] { background-color: #d3c5ac; }
22
+ .ks-theme-toggle-icon { position: relative; z-index: 2; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; pointer-events: none; transition: color 0.3s ease; }
23
+ .ks-theme-toggle-track::after { content: ''; position: absolute; left: 2px; top: 2px; width: 24px; height: 24px; border-radius: 50%; background-color: #52525b; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease, background-color 0.3s ease; z-index: 1; }
24
+ .ks-theme-toggle-track[data-active]::after { transform: translateX(24px); background-color: #ffffff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); }
25
+ .ks-theme-toggle-icon-moon { color: #fbbf24; }
26
+ .ks-theme-toggle-icon-sun { color: rgba(255, 255, 255, 0.3); }
27
+ .ks-theme-toggle-track[data-active] .ks-theme-toggle-icon-moon { color: rgba(120, 90, 0, 0.3); }
28
+ .ks-theme-toggle-track[data-active] .ks-theme-toggle-icon-sun { color: #785a00; }
29
+ .ks-theme-toggle:hover .ks-theme-toggle-track { background-color: #52525b; }
30
+ .ks-theme-toggle:hover .ks-theme-toggle-track[data-active] { background-color: #c9b99a; }
31
+ @media (prefers-reduced-motion: reduce) { .ks-theme-toggle-track, .ks-theme-toggle-track::after, .ks-theme-toggle-icon { transition: none; } }
32
+ `;
33
+
34
+ function ensureThemeToggleStyle(): void {
35
+ if (typeof document === "undefined") return;
36
+ if (document.getElementById(THEME_TOGGLE_STYLE_ID)) return;
37
+ const el = document.createElement("style");
38
+ el.id = THEME_TOGGLE_STYLE_ID;
39
+ el.textContent = THEME_TOGGLE_CSS;
40
+ document.head.appendChild(el);
41
+ }
42
+
43
+ export type ThemeToggleValue = "dark" | "light";
44
+
45
+ export interface ThemeToggleProps
46
+ extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, "value" | "onToggle"> {
47
+ /** Current theme. `"light"` slides the thumb right and activates the sun. */
48
+ value: ThemeToggleValue;
49
+ /** Called with the opposite theme when the toggle is clicked. */
50
+ onToggle: (next: ThemeToggleValue) => void;
51
+ /**
52
+ * Accessible label. Receives the theme the click will switch TO so the
53
+ * default reads e.g. "Switch to light mode". Override for non-English UIs.
54
+ */
55
+ ariaLabel?: (next: ThemeToggleValue) => string;
56
+ /** Optional test hook (data-testid). */
57
+ testId?: string;
58
+ }
59
+
60
+ function cn(...classes: Array<string | undefined | null | false>): string {
61
+ return classes.filter(Boolean).join(" ");
62
+ }
63
+
64
+ const defaultAriaLabel = (next: ThemeToggleValue): string => `Switch to ${next} mode`;
65
+
66
+ /**
67
+ * A controlled sliding sun/moon track toggle. Domain-free: it owns no theme
68
+ * state and applies no theme — it renders `value` and reports the intended
69
+ * next value through `onToggle`. The parent owns the theme source of truth.
70
+ */
71
+ export default function ThemeToggle(props: ThemeToggleProps): JSX.Element {
72
+ ensureThemeToggleStyle();
73
+
74
+ const [local, rest] = splitProps(props, [
75
+ "value",
76
+ "onToggle",
77
+ "ariaLabel",
78
+ "testId",
79
+ "class",
80
+ "onClick",
81
+ ]);
82
+
83
+ const isLight = (): boolean => local.value === "light";
84
+ const next = (): ThemeToggleValue => (isLight() ? "dark" : "light");
85
+ const label = (): string => (local.ariaLabel ?? defaultAriaLabel)(next());
86
+
87
+ const handleClick: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (event) => {
88
+ local.onToggle(next());
89
+ if (typeof local.onClick === "function") local.onClick(event);
90
+ };
91
+
92
+ return (
93
+ <button
94
+ type="button"
95
+ class={cn("ks-theme-toggle", local.class)}
96
+ aria-label={label()}
97
+ title={label()}
98
+ data-testid={local.testId}
99
+ onClick={handleClick}
100
+ {...rest}
101
+ >
102
+ <span class="ks-theme-toggle-track" data-active={isLight() ? "" : undefined}>
103
+ <span class="ks-theme-toggle-icon ks-theme-toggle-icon-moon">
104
+ <Moon size={12} />
105
+ </span>
106
+ <span class="ks-theme-toggle-icon ks-theme-toggle-icon-sun">
107
+ <Sun size={12} />
108
+ </span>
109
+ </span>
110
+ </button>
111
+ );
112
+ }
@@ -0,0 +1,130 @@
1
+ // Centered 404 / empty-state panel: an optional logo slot, a large display
2
+ // title, a heading, a message, and a default "go back" action button. It is a
3
+ // fully prop-driven, domain-free primitive — every piece of copy defaults to a
4
+ // generic 404 string but is overridable, and nothing about any specific site
5
+ // is hard-coded.
6
+ //
7
+ // Unlike the host-owned Button, this is a standalone primitive: it imports only
8
+ // solid-js and lucide-solid, never `@kserp/host-ui` or a router. The default
9
+ // action renders a plain, self-styled button. Navigation is left to the caller
10
+ // via `onButtonClick` (or `href`, which renders an anchor instead). Callers who
11
+ // want the host Button can pass it through the `action` slot, which fully
12
+ // replaces the built-in button.
13
+
14
+ import type { JSX } from "solid-js";
15
+ import { Show, splitProps } from "solid-js";
16
+ import { ArrowLeft } from "lucide-solid";
17
+
18
+ export interface NotFoundProps
19
+ extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "title"> {
20
+ /** Large display title. Defaults to "404". Pass "" to hide it entirely. */
21
+ title?: string;
22
+ /** Secondary heading under the title. Defaults to "Page Not Found". */
23
+ heading?: string;
24
+ /** Supporting message under the heading. */
25
+ message?: string;
26
+ /** Label for the default action button. Defaults to "Go Back Home". */
27
+ buttonText?: string;
28
+ /**
29
+ * When set, the default action renders as an anchor pointing here instead of
30
+ * a button. Ignored when `onButtonClick` or `action` is provided.
31
+ */
32
+ href?: string;
33
+ /** Click handler for the default action button. */
34
+ onButtonClick?: () => void;
35
+ /** Optional element rendered above the title (e.g. a logo or icon). */
36
+ logo?: JSX.Element;
37
+ /** Hide the action entirely. */
38
+ hideButton?: boolean;
39
+ /**
40
+ * Replace the built-in action with a custom element (e.g. the host Button).
41
+ * When provided, `buttonText`, `href`, and `onButtonClick` are ignored.
42
+ */
43
+ action?: JSX.Element;
44
+ /** Test id applied to the outer container. */
45
+ testId?: string;
46
+ }
47
+
48
+ export default function NotFound(props: NotFoundProps): JSX.Element {
49
+ const [local, others] = splitProps(props, [
50
+ "title",
51
+ "heading",
52
+ "message",
53
+ "buttonText",
54
+ "href",
55
+ "onButtonClick",
56
+ "logo",
57
+ "hideButton",
58
+ "action",
59
+ "testId",
60
+ "class",
61
+ ]);
62
+
63
+ const showTitle = () => local.title !== "";
64
+ const buttonClass =
65
+ "inline-flex items-center justify-center gap-2 rounded-md border " +
66
+ "border-amber-600/60 bg-amber-600/20 px-5 py-2.5 text-sm font-semibold " +
67
+ "text-amber-400 transition-colors hover:border-amber-500 " +
68
+ "hover:bg-amber-600/30 focus:outline-none focus-visible:ring-2 " +
69
+ "focus-visible:ring-amber-500/60";
70
+
71
+ return (
72
+ <div
73
+ class={
74
+ "flex items-center justify-center min-h-screen" +
75
+ (local.class ? " " + local.class : "")
76
+ }
77
+ data-testid={local.testId}
78
+ {...others}
79
+ >
80
+ <div class="text-center">
81
+ <Show when={local.logo}>
82
+ <div class="flex items-center justify-center mx-auto mb-6">{local.logo}</div>
83
+ </Show>
84
+
85
+ <Show when={showTitle()}>
86
+ <h1 class="text-4xl md:text-6xl font-bold text-amber-500 mb-4">
87
+ {local.title || "404"}
88
+ </h1>
89
+ </Show>
90
+
91
+ <h2 class="text-xl md:text-2xl font-bold text-white mb-3">
92
+ {local.heading || "Page Not Found"}
93
+ </h2>
94
+
95
+ <p class="text-sm text-zinc-400 max-w-md mb-8">
96
+ {local.message ||
97
+ "The page you're looking for doesn't exist or has been moved."}
98
+ </p>
99
+
100
+ <Show when={!local.hideButton}>
101
+ <Show
102
+ when={local.action}
103
+ fallback={
104
+ <Show
105
+ when={local.href && !local.onButtonClick}
106
+ fallback={
107
+ <button
108
+ type="button"
109
+ class={buttonClass}
110
+ onClick={() => local.onButtonClick?.()}
111
+ >
112
+ <ArrowLeft size={16} aria-hidden="true" />
113
+ {local.buttonText || "Go Back Home"}
114
+ </button>
115
+ }
116
+ >
117
+ <a href={local.href} class={buttonClass}>
118
+ <ArrowLeft size={16} aria-hidden="true" />
119
+ {local.buttonText || "Go Back Home"}
120
+ </a>
121
+ </Show>
122
+ }
123
+ >
124
+ {local.action}
125
+ </Show>
126
+ </Show>
127
+ </div>
128
+ </div>
129
+ );
130
+ }