saasaloy 0.1.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.
Files changed (69) hide show
  1. package/dist/index.js +8455 -0
  2. package/dist/index.js.map +1 -0
  3. package/package.json +69 -0
  4. package/schemas/manifest.schema.json +107 -0
  5. package/schemas/registry-item.schema.json +336 -0
  6. package/schemas/saasaloy-lock.schema.json +86 -0
  7. package/schemas/saasaloy.schema.json +42 -0
  8. package/templates/base/AGENTS.md +450 -0
  9. package/templates/base/CLAUDE.md +1 -0
  10. package/templates/base/DESIGN.md +209 -0
  11. package/templates/base/README.md +71 -0
  12. package/templates/base/_agents/skills/saasaloy-design/SKILL.md +198 -0
  13. package/templates/base/_agents/skills/saasaloy-landing-copy/SKILL.md +395 -0
  14. package/templates/base/_agents/skills/saasaloy-setup/SKILL.md +273 -0
  15. package/templates/base/_gitignore +32 -0
  16. package/templates/base/_husky/commit-msg +1 -0
  17. package/templates/base/_husky/pre-commit +1 -0
  18. package/templates/base/_prettierignore +31 -0
  19. package/templates/base/_saasaloy-base.json +9 -0
  20. package/templates/base/apps/web/astro.config.mjs +61 -0
  21. package/templates/base/apps/web/package.json +30 -0
  22. package/templates/base/apps/web/public/favicon.svg +4 -0
  23. package/templates/base/apps/web/src/layouts/Layout.astro +52 -0
  24. package/templates/base/apps/web/src/pages/404.astro +24 -0
  25. package/templates/base/apps/web/src/pages/500.astro +33 -0
  26. package/templates/base/apps/web/src/pages/index.astro +61 -0
  27. package/templates/base/apps/web/src/pages/privacy.astro +15 -0
  28. package/templates/base/apps/web/src/pages/terms.astro +14 -0
  29. package/templates/base/apps/web/tsconfig.json +11 -0
  30. package/templates/base/apps/web/wrangler.jsonc +27 -0
  31. package/templates/base/commitlint.config.js +9 -0
  32. package/templates/base/lint-staged.config.js +17 -0
  33. package/templates/base/oxlint.config.mjs +155 -0
  34. package/templates/base/package.json +44 -0
  35. package/templates/base/packages/tsconfig/base.json +17 -0
  36. package/templates/base/packages/tsconfig/package.json +18 -0
  37. package/templates/base/packages/ui/components.json +19 -0
  38. package/templates/base/packages/ui/package.json +39 -0
  39. package/templates/base/packages/ui/src/blocks/cta.tsx +82 -0
  40. package/templates/base/packages/ui/src/blocks/error-state.tsx +144 -0
  41. package/templates/base/packages/ui/src/blocks/faq.tsx +64 -0
  42. package/templates/base/packages/ui/src/blocks/feature-grid.tsx +185 -0
  43. package/templates/base/packages/ui/src/blocks/footer.tsx +99 -0
  44. package/templates/base/packages/ui/src/blocks/hero.tsx +84 -0
  45. package/templates/base/packages/ui/src/blocks/navbar.tsx +159 -0
  46. package/templates/base/packages/ui/src/blocks/pricing-table.tsx +175 -0
  47. package/templates/base/packages/ui/src/blocks/theme-toggle.tsx +51 -0
  48. package/templates/base/packages/ui/src/components/accordion.tsx +78 -0
  49. package/templates/base/packages/ui/src/components/badge.tsx +53 -0
  50. package/templates/base/packages/ui/src/components/button.tsx +59 -0
  51. package/templates/base/packages/ui/src/components/card.tsx +103 -0
  52. package/templates/base/packages/ui/src/components/input.tsx +20 -0
  53. package/templates/base/packages/ui/src/components/label.tsx +18 -0
  54. package/templates/base/packages/ui/src/components/separator.tsx +23 -0
  55. package/templates/base/packages/ui/src/containers/README.md +11 -0
  56. package/templates/base/packages/ui/src/content/errors.ts +58 -0
  57. package/templates/base/packages/ui/src/content/landing.ts +304 -0
  58. package/templates/base/packages/ui/src/index.ts +8 -0
  59. package/templates/base/packages/ui/src/lib/interpolate.ts +31 -0
  60. package/templates/base/packages/ui/src/lib/sentinel.ts +11 -0
  61. package/templates/base/packages/ui/src/lib/theme.ts +167 -0
  62. package/templates/base/packages/ui/src/lib/utils.ts +11 -0
  63. package/templates/base/packages/ui/src/styles/globals.css +164 -0
  64. package/templates/base/packages/ui/tsconfig.json +7 -0
  65. package/templates/base/pnpm-workspace.yaml +23 -0
  66. package/templates/base/prettier.config.js +10 -0
  67. package/templates/base/saasaloy.json +8 -0
  68. package/templates/base/stylelint.config.js +46 -0
  69. package/templates/base/turbo.json +20 -0
@@ -0,0 +1,51 @@
1
+ import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
2
+
3
+ import { Button } from "@repo/ui/components/button";
4
+ import { THEME_LABELS } from "@repo/ui/lib/theme";
5
+ import { cn } from "@repo/ui/lib/utils";
6
+
7
+ // The light/dark/system control — and the one block that is deliberately INERT. There is
8
+ // no onClick here and no state: the button is chrome, and every behaviour it appears to
9
+ // have comes from THEME_INIT_SCRIPT (packages/ui/src/lib/theme.ts), which the host
10
+ // document inlines in its <head>:
11
+ //
12
+ // - Astro apps/web/src/layouts/Layout.astro, `<script is:inline set:html={…} />`
13
+ // - Vite a `transformIndexHtml` plugin injecting the same constant at `head-prepend`
14
+ //
15
+ // Drop this block into a document that does NOT inline that script and it renders
16
+ // nothing at all — which is the point, not a bug. `data-theme` on <html> is set by the
17
+ // script and by nothing else, so it doubles as the JavaScript-present marker: the CSS
18
+ // below keeps the button hidden until the attribute exists. With JS off the control is
19
+ // absent rather than dead, and because the attribute lands pre-paint (before this button
20
+ // is even parsed) there is no reveal-on-mount flicker to pay for.
21
+ //
22
+ // That is what buys the landing page ZERO additional JavaScript for theme switching:
23
+ // index.astro renders this with no `client:*` directive. Adding one would ship React for
24
+ // a button that does not use it.
25
+ //
26
+ // The `in-data-[theme=…]` variants compile to `:where([data-theme=…]) &` — an ancestor
27
+ // match, so the icon swap is pure CSS off <html>. They live here rather than in
28
+ // globals.css so the block travels to another app self-contained.
29
+
30
+ export interface ThemeToggleProps {
31
+ /** Positioning and chrome for the host page — the block itself is position-neutral. */
32
+ className?: string;
33
+ }
34
+
35
+ export function ThemeToggle({ className }: ThemeToggleProps) {
36
+ return (
37
+ <Button
38
+ variant="ghost"
39
+ size="icon-sm"
40
+ data-theme-toggle=""
41
+ // The script rewrites this the moment the DOM is ready; the value here is the
42
+ // truthful one for a first-time visitor, whose stored theme is unset.
43
+ aria-label={THEME_LABELS.system}
44
+ className={cn("hidden in-data-theme:inline-flex", className)}
45
+ >
46
+ <SunIcon className="hidden in-data-[theme=light]:block" />
47
+ <MoonIcon className="hidden in-data-[theme=dark]:block" />
48
+ <MonitorIcon className="hidden in-data-[theme=system]:block" />
49
+ </Button>
50
+ );
51
+ }
@@ -0,0 +1,78 @@
1
+ import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
2
+
3
+ import { cn } from "@repo/ui/lib/utils";
4
+ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
5
+
6
+ function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
7
+ return (
8
+ <AccordionPrimitive.Root
9
+ data-slot="accordion"
10
+ className={cn("flex w-full flex-col", className)}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
17
+ return (
18
+ <AccordionPrimitive.Item
19
+ data-slot="accordion-item"
20
+ className={cn("not-last:border-b", className)}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ function AccordionTrigger({
27
+ className,
28
+ children,
29
+ ...props
30
+ }: AccordionPrimitive.Trigger.Props) {
31
+ return (
32
+ <AccordionPrimitive.Header className="flex">
33
+ <AccordionPrimitive.Trigger
34
+ data-slot="accordion-trigger"
35
+ className={cn(
36
+ "group/accordion-trigger focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-3 aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ {children}
42
+ <ChevronDownIcon
43
+ data-slot="accordion-trigger-icon"
44
+ className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
45
+ />
46
+ <ChevronUpIcon
47
+ data-slot="accordion-trigger-icon"
48
+ className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
49
+ />
50
+ </AccordionPrimitive.Trigger>
51
+ </AccordionPrimitive.Header>
52
+ );
53
+ }
54
+
55
+ function AccordionContent({
56
+ className,
57
+ children,
58
+ ...props
59
+ }: AccordionPrimitive.Panel.Props) {
60
+ return (
61
+ <AccordionPrimitive.Panel
62
+ data-slot="accordion-content"
63
+ className="data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden text-sm"
64
+ {...props}
65
+ >
66
+ <div
67
+ className={cn(
68
+ "[&_a]:hover:text-foreground h-(--accordion-panel-height) pt-0 pb-2.5 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4",
69
+ className
70
+ )}
71
+ >
72
+ {children}
73
+ </div>
74
+ </AccordionPrimitive.Panel>
75
+ );
76
+ }
77
+
78
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,53 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props";
2
+ import { useRender } from "@base-ui/react/use-render";
3
+ import { cva } from "class-variance-authority";
4
+ import type { VariantProps } from "class-variance-authority";
5
+
6
+ import { cn } from "@repo/ui/lib/utils";
7
+
8
+ const badgeVariants = cva(
9
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
10
+ {
11
+ defaultVariants: {
12
+ variant: "default",
13
+ },
14
+ variants: {
15
+ variant: {
16
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
17
+ destructive:
18
+ "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
19
+ ghost:
20
+ "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ outline:
23
+ "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
24
+ secondary:
25
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
26
+ },
27
+ },
28
+ }
29
+ );
30
+
31
+ function Badge({
32
+ className,
33
+ variant = "default",
34
+ render,
35
+ ...props
36
+ }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
37
+ return useRender({
38
+ defaultTagName: "span",
39
+ props: mergeProps<"span">(
40
+ {
41
+ className: cn(badgeVariants({ variant }), className),
42
+ },
43
+ props
44
+ ),
45
+ render,
46
+ state: {
47
+ slot: "badge",
48
+ variant,
49
+ },
50
+ });
51
+ }
52
+
53
+ export { Badge, badgeVariants };
@@ -0,0 +1,59 @@
1
+ import { Button as ButtonPrimitive } from "@base-ui/react/button";
2
+ import { cva } from "class-variance-authority";
3
+ import type { VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@repo/ui/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ defaultVariants: {
11
+ size: "default",
12
+ variant: "default",
13
+ },
14
+ variants: {
15
+ size: {
16
+ default:
17
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
18
+ icon: "size-8",
19
+ "icon-lg": "size-9",
20
+ "icon-sm":
21
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
22
+ "icon-xs":
23
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
24
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
25
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
26
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
27
+ },
28
+ variant: {
29
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
30
+ destructive:
31
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
32
+ ghost:
33
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
34
+ link: "text-primary underline-offset-4 hover:underline",
35
+ outline:
36
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
37
+ secondary:
38
+ "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
39
+ },
40
+ },
41
+ }
42
+ );
43
+
44
+ function Button({
45
+ className,
46
+ variant = "default",
47
+ size = "default",
48
+ ...props
49
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
50
+ return (
51
+ <ButtonPrimitive
52
+ data-slot="button"
53
+ className={cn(buttonVariants({ className, size, variant }))}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ export { Button, buttonVariants };
@@ -0,0 +1,103 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@repo/ui/lib/utils";
4
+
5
+ function Card({
6
+ className,
7
+ size = "default",
8
+ ...props
9
+ }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
10
+ return (
11
+ <div
12
+ data-slot="card"
13
+ data-size={size}
14
+ className={cn(
15
+ "group/card bg-card text-card-foreground ring-foreground/10 flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl py-(--card-spacing) text-sm ring-1 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
24
+ return (
25
+ <div
26
+ data-slot="card-header"
27
+ className={cn(
28
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
37
+ return (
38
+ <div
39
+ data-slot="card-title"
40
+ className={cn(
41
+ "text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+
49
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
50
+ return (
51
+ <div
52
+ data-slot="card-description"
53
+ className={cn("text-muted-foreground text-sm", className)}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div
62
+ data-slot="card-action"
63
+ className={cn(
64
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ );
70
+ }
71
+
72
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="card-content"
76
+ className={cn("px-(--card-spacing)", className)}
77
+ {...props}
78
+ />
79
+ );
80
+ }
81
+
82
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="card-footer"
86
+ className={cn(
87
+ "bg-muted/50 flex items-center rounded-b-xl border-t p-(--card-spacing)",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ export {
96
+ Card,
97
+ CardHeader,
98
+ CardFooter,
99
+ CardTitle,
100
+ CardAction,
101
+ CardDescription,
102
+ CardContent,
103
+ };
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ import { Input as InputPrimitive } from "@base-ui/react/input";
3
+
4
+ import { cn } from "@repo/ui/lib/utils";
5
+
6
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
7
+ return (
8
+ <InputPrimitive
9
+ type={type}
10
+ data-slot="input"
11
+ className={cn(
12
+ "border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ );
18
+ }
19
+
20
+ export { Input };
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@repo/ui/lib/utils";
4
+
5
+ function Label({ className, ...props }: React.ComponentProps<"label">) {
6
+ return (
7
+ <label
8
+ data-slot="label"
9
+ className={cn(
10
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ export { Label };
@@ -0,0 +1,23 @@
1
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
2
+
3
+ import { cn } from "@repo/ui/lib/utils";
4
+
5
+ function Separator({
6
+ className,
7
+ orientation = "horizontal",
8
+ ...props
9
+ }: SeparatorPrimitive.Props) {
10
+ return (
11
+ <SeparatorPrimitive
12
+ data-slot="separator"
13
+ orientation={orientation}
14
+ className={cn(
15
+ "bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ export { Separator };
@@ -0,0 +1,11 @@
1
+ # `src/containers/`
2
+
3
+ Containers are the third UI layer. A container binds a shared store or cross-island state, then wires the data and the callbacks into pure blocks. Keep them thin — the visual weight stays in the block.
4
+
5
+ The base ships none, so this folder holds only this note. Delete the note when you add your first container.
6
+
7
+ Rules (the full version is in the repository's `AGENTS.md`):
8
+
9
+ - A container imports a client-state store from `@repo/ui/lib/*`, subscribes to it, and renders blocks. It owns no markup a block could own.
10
+ - No IO anywhere in `@repo/ui` — no fetch, no persistence, no auth. The app that owns the data hydrates the store.
11
+ - One container, one file, one component export. Reach it by subpath: `import { CartSummary } from "@repo/ui/containers/cart-summary";`.
@@ -0,0 +1,58 @@
1
+ // Every word an error screen shows, in one file.
2
+ //
3
+ // The words the landing page shows live next door in ./landing.ts, and that file's
4
+ // preamble states the five shape rules — max three levels, stable ids over positions,
5
+ // single-brace `{token}` placeholders, no runtime concatenation, user-visible strings
6
+ // only. They apply here unchanged; read them there rather than trusting a summary.
7
+ //
8
+ // WHY THIS IS A SECOND FILE. A copy pass rewrites the marketing voice and never reaches
9
+ // an error screen, so keeping these strings out of `landing.*` means a rewrite cannot
10
+ // leave a 404 page speaking in the old voice. A translation pass walks both files.
11
+ //
12
+ // ONE NAMESPACE, THREE CASES, and the split is by what failed, not by which app failed:
13
+ //
14
+ // notFound Nothing lives at this address. The web app's 404 page and the admin
15
+ // app's not-found screen both read it.
16
+ // renderFailure The app was reached, and drawing the screen threw. The admin app's
17
+ // error boundary reads it, and it is the only case with a retry that
18
+ // can actually work — the router re-renders in place.
19
+ // serverFailure The server failed while producing the page. The web app's 500 page
20
+ // reads it.
21
+ //
22
+ // The `code` values are display labels, not a claim about the HTTP status the response
23
+ // carried. The admin not-found screen shows "404" over a soft 200, by decision.
24
+ //
25
+ // No `{siteName}` token appears below, deliberately. An error screen is the one surface
26
+ // where naming the product adds nothing: the reader already knows where they are, and a
27
+ // brand name in a failure message reads as deflection.
28
+
29
+ /** Copy for the three error screens ../blocks/error-state.tsx renders. */
30
+ export const errors = {
31
+ notFound: {
32
+ code: "404",
33
+ title: "This page does not exist",
34
+ description:
35
+ "The address may have a typo, or the page may have moved since the link was written. Nothing is lost — start again from the home page.",
36
+ homeLabel: "Back to home",
37
+ },
38
+
39
+ renderFailure: {
40
+ code: "500",
41
+ title: "This screen stopped working",
42
+ description:
43
+ "Something failed while drawing this page. Trying again often clears it; if it does not, go back to the home page and take another route in.",
44
+ retryLabel: "Try again",
45
+ homeLabel: "Back to home",
46
+ },
47
+
48
+ serverFailure: {
49
+ code: "500",
50
+ title: "Something went wrong on our side",
51
+ description:
52
+ "The page could not be produced. This is not something you did. Try again in a moment.",
53
+ // No `retryLabel`. The 500 page is prerendered, so a retry button would reload the
54
+ // same static file and could not re-run whatever failed; src/pages/500.astro says so
55
+ // at length. `renderFailure` keeps its retry because the router re-renders in place.
56
+ homeLabel: "Back to home",
57
+ },
58
+ };