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.
- package/dist/index.js +8455 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/schemas/manifest.schema.json +107 -0
- package/schemas/registry-item.schema.json +336 -0
- package/schemas/saasaloy-lock.schema.json +86 -0
- package/schemas/saasaloy.schema.json +42 -0
- package/templates/base/AGENTS.md +450 -0
- package/templates/base/CLAUDE.md +1 -0
- package/templates/base/DESIGN.md +209 -0
- package/templates/base/README.md +71 -0
- package/templates/base/_agents/skills/saasaloy-design/SKILL.md +198 -0
- package/templates/base/_agents/skills/saasaloy-landing-copy/SKILL.md +395 -0
- package/templates/base/_agents/skills/saasaloy-setup/SKILL.md +273 -0
- package/templates/base/_gitignore +32 -0
- package/templates/base/_husky/commit-msg +1 -0
- package/templates/base/_husky/pre-commit +1 -0
- package/templates/base/_prettierignore +31 -0
- package/templates/base/_saasaloy-base.json +9 -0
- package/templates/base/apps/web/astro.config.mjs +61 -0
- package/templates/base/apps/web/package.json +30 -0
- package/templates/base/apps/web/public/favicon.svg +4 -0
- package/templates/base/apps/web/src/layouts/Layout.astro +52 -0
- package/templates/base/apps/web/src/pages/404.astro +24 -0
- package/templates/base/apps/web/src/pages/500.astro +33 -0
- package/templates/base/apps/web/src/pages/index.astro +61 -0
- package/templates/base/apps/web/src/pages/privacy.astro +15 -0
- package/templates/base/apps/web/src/pages/terms.astro +14 -0
- package/templates/base/apps/web/tsconfig.json +11 -0
- package/templates/base/apps/web/wrangler.jsonc +27 -0
- package/templates/base/commitlint.config.js +9 -0
- package/templates/base/lint-staged.config.js +17 -0
- package/templates/base/oxlint.config.mjs +155 -0
- package/templates/base/package.json +44 -0
- package/templates/base/packages/tsconfig/base.json +17 -0
- package/templates/base/packages/tsconfig/package.json +18 -0
- package/templates/base/packages/ui/components.json +19 -0
- package/templates/base/packages/ui/package.json +39 -0
- package/templates/base/packages/ui/src/blocks/cta.tsx +82 -0
- package/templates/base/packages/ui/src/blocks/error-state.tsx +144 -0
- package/templates/base/packages/ui/src/blocks/faq.tsx +64 -0
- package/templates/base/packages/ui/src/blocks/feature-grid.tsx +185 -0
- package/templates/base/packages/ui/src/blocks/footer.tsx +99 -0
- package/templates/base/packages/ui/src/blocks/hero.tsx +84 -0
- package/templates/base/packages/ui/src/blocks/navbar.tsx +159 -0
- package/templates/base/packages/ui/src/blocks/pricing-table.tsx +175 -0
- package/templates/base/packages/ui/src/blocks/theme-toggle.tsx +51 -0
- package/templates/base/packages/ui/src/components/accordion.tsx +78 -0
- package/templates/base/packages/ui/src/components/badge.tsx +53 -0
- package/templates/base/packages/ui/src/components/button.tsx +59 -0
- package/templates/base/packages/ui/src/components/card.tsx +103 -0
- package/templates/base/packages/ui/src/components/input.tsx +20 -0
- package/templates/base/packages/ui/src/components/label.tsx +18 -0
- package/templates/base/packages/ui/src/components/separator.tsx +23 -0
- package/templates/base/packages/ui/src/containers/README.md +11 -0
- package/templates/base/packages/ui/src/content/errors.ts +58 -0
- package/templates/base/packages/ui/src/content/landing.ts +304 -0
- package/templates/base/packages/ui/src/index.ts +8 -0
- package/templates/base/packages/ui/src/lib/interpolate.ts +31 -0
- package/templates/base/packages/ui/src/lib/sentinel.ts +11 -0
- package/templates/base/packages/ui/src/lib/theme.ts +167 -0
- package/templates/base/packages/ui/src/lib/utils.ts +11 -0
- package/templates/base/packages/ui/src/styles/globals.css +164 -0
- package/templates/base/packages/ui/tsconfig.json +7 -0
- package/templates/base/pnpm-workspace.yaml +23 -0
- package/templates/base/prettier.config.js +10 -0
- package/templates/base/saasaloy.json +8 -0
- package/templates/base/stylelint.config.js +46 -0
- 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
|
+
};
|