@camox/cli 0.16.0 → 0.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camox/cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "bin": {
5
5
  "camox": "./dist/index.mjs"
6
6
  },
@@ -26,7 +26,7 @@
26
26
  "@types/node": "^24.12.2",
27
27
  "@typescript/native-preview": "7.0.0-dev.20260412.1",
28
28
  "vite-plus": "latest",
29
- "@camox/api-contract": "0.16.0"
29
+ "@camox/api-contract": "0.17.0"
30
30
  },
31
31
  "nx": {
32
32
  "tags": [
@@ -6,7 +6,7 @@
6
6
  "tailwind": {
7
7
  "config": "",
8
8
  "css": "src/styles.css",
9
- "baseColor": "gray",
9
+ "baseColor": "zinc",
10
10
  "cssVariables": true,
11
11
  "prefix": ""
12
12
  },
@@ -18,5 +18,8 @@
18
18
  "lib": "@/lib",
19
19
  "hooks": "@/hooks"
20
20
  },
21
+ "rtl": false,
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
21
24
  "registries": {}
22
25
  }
@@ -13,6 +13,8 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@base-ui/react": "^1.4.0",
16
+ "@fontsource-variable/inter": "^5.2.8",
17
+ "@fontsource-variable/noto-serif": "^5.2.9",
16
18
  "@tailwindcss/vite": "^4.2.2",
17
19
  "@tanstack/react-query": "^5.99.0",
18
20
  "@tanstack/react-router": "^1.168.18",
@@ -26,7 +28,7 @@
26
28
  "nitro": "3.0.260311-beta",
27
29
  "react": "^19.2.5",
28
30
  "react-dom": "^19.2.5",
29
- "shadcn": "^4.3.1",
31
+ "shadcn": "^4.6.0",
30
32
  "tailwind-merge": "^3.5.0",
31
33
  "tailwindcss": "^4.0.6"
32
34
  },
@@ -37,7 +37,7 @@ const faq = createBlock({
37
37
 
38
38
  function FaqComponent() {
39
39
  return (
40
- <section className="py-24">
40
+ <section className="py-12 sm:py-16 md:py-20">
41
41
  <div className="mx-auto max-w-2xl px-4">
42
42
  <Accordion>
43
43
  <faq.Repeater name="items">
@@ -11,11 +11,11 @@ const footer = createBlock({
11
11
  links: Type.RepeatableItem({
12
12
  content: {
13
13
  link: Type.Link({
14
- default: { text: "Link", href: "#", newTab: false },
14
+ default: { text: "Footer link", href: "#", newTab: false },
15
15
  title: "Link",
16
16
  }),
17
17
  },
18
- minItems: 1,
18
+ minItems: 2,
19
19
  maxItems: 12,
20
20
  title: "Links",
21
21
  toMarkdown: (c) => [c.link],
@@ -27,14 +27,17 @@ const footer = createBlock({
27
27
 
28
28
  function FooterComponent() {
29
29
  return (
30
- <footer className="dark bg-background py-12">
30
+ <footer className="dark bg-background border-border border-t py-4">
31
31
  <div className="container mx-auto px-4">
32
- <div className="flex flex-col items-center gap-6 sm:flex-row sm:justify-between">
33
- <footer.Field name="title">
34
- {(props) => <div {...props} className="text-foreground text-lg font-bold" />}
35
- </footer.Field>
32
+ <div className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:gap-x-6 sm:gap-y-2">
33
+ <div className="flex items-center gap-2">
34
+ <footer.Field name="title">
35
+ {(props) => <div {...props} className="text-foreground text-sm font-bold" />}
36
+ </footer.Field>
37
+ <div className="text-muted-foreground text-sm">&copy; {new Date().getFullYear()}</div>
38
+ </div>
36
39
 
37
- <div className="flex flex-wrap items-center gap-4">
40
+ <div className="flex flex-col items-start gap-4 sm:ml-auto sm:flex-row sm:flex-wrap sm:items-center sm:justify-end">
38
41
  <footer.Repeater name="links">
39
42
  {(linkItem) => (
40
43
  <linkItem.Link name="link">
@@ -49,10 +52,6 @@ function FooterComponent() {
49
52
  </footer.Repeater>
50
53
  </div>
51
54
  </div>
52
-
53
- <div className="text-muted-foreground mt-8 text-center text-sm">
54
- &copy; {new Date().getFullYear()} All rights reserved.
55
- </div>
56
55
  </div>
57
56
  </footer>
58
57
  );
@@ -2,6 +2,7 @@ import { Link } from "@tanstack/react-router";
2
2
  import { Type, createBlock } from "camox/createBlock";
3
3
 
4
4
  import { Button } from "@/components/ui/button";
5
+ import { cn } from "@/lib/utils";
5
6
 
6
7
  const hero = createBlock({
7
8
  id: "hero",
@@ -10,42 +11,78 @@ const hero = createBlock({
10
11
  "Use this block as the main landing section at the top of a page. It should capture attention immediately with a clear value proposition.",
11
12
  content: {
12
13
  title: Type.String({
13
- default: "Welcome to {{projectName}}",
14
+ default: "Let's get going on {{projectName}}",
14
15
  title: "Title",
15
16
  }),
16
17
  description: Type.String({
17
- default: "Build something amazing with Camox.",
18
+ default: "Press ⌘+Enter to access Camox Studio and edit content.",
18
19
  maxLength: 280,
19
20
  title: "Description",
20
21
  }),
21
22
  cta: Type.Link({
22
- default: { text: "Get Started", href: "/", newTab: false },
23
+ default: { text: "Get started", href: "/", newTab: false },
23
24
  title: "CTA",
24
25
  }),
26
+ illustration: Type.Image({
27
+ title: "Illustration",
28
+ }),
29
+ },
30
+ settings: {
31
+ withIllustration: Type.Boolean({
32
+ default: true,
33
+ title: "With illustration",
34
+ }),
35
+ theme: Type.Enum({
36
+ default: "dark",
37
+ options: { light: "Light", dark: "Dark" },
38
+ title: "Theme",
39
+ }),
25
40
  },
26
41
  component: HeroComponent,
27
- toMarkdown: (c) => [`# ${c.title}`, c.description, c.cta],
42
+ toMarkdown: (c, s) => [`# ${c.title}`, c.description, s.withIllustration(c.illustration), c.cta],
28
43
  });
29
44
 
30
45
  function HeroComponent() {
46
+ const withIllustration = hero.useSetting("withIllustration");
47
+ const theme = hero.useSetting("theme");
48
+
31
49
  return (
32
- <section className="flex flex-col items-center justify-center py-32">
33
- <div className="container mx-auto px-4">
34
- <div className="mx-auto max-w-3xl text-center">
35
- <hero.Field name="title">
36
- {(props) => (
37
- <h1
38
- {...props}
39
- className="text-foreground mb-6 text-5xl font-bold tracking-tight sm:text-6xl"
40
- />
41
- )}
42
- </hero.Field>
43
- <hero.Field name="description">
44
- {(props) => <p {...props} className="text-muted-foreground mb-10 text-xl" />}
45
- </hero.Field>
46
- <hero.Link name="cta">
47
- {(props) => <Button size="lg" nativeButton={false} render={<Link {...props} />} />}
48
- </hero.Link>
50
+ <section
51
+ className={cn(
52
+ theme === "dark" ? "dark" : "light",
53
+ "bg-background py-16 sm:py-24 md:py-28",
54
+ !withIllustration && "flex flex-col items-center justify-center",
55
+ )}
56
+ >
57
+ <div className="container mx-auto">
58
+ <div
59
+ className={cn(
60
+ withIllustration
61
+ ? "grid items-center gap-12 lg:grid-cols-[1fr_auto]"
62
+ : "mx-auto max-w-3xl text-center",
63
+ )}
64
+ >
65
+ <div className={cn(withIllustration && "text-left")}>
66
+ <hero.Field name="title">
67
+ {(props) => (
68
+ <h1
69
+ {...props}
70
+ className="text-foreground mb-6 text-4xl font-bold tracking-tight sm:text-6xl"
71
+ />
72
+ )}
73
+ </hero.Field>
74
+ <hero.Field name="description">
75
+ {(props) => <p {...props} className="text-muted-foreground mb-10 text-xl" />}
76
+ </hero.Field>
77
+ <hero.Link name="cta">
78
+ {(props) => <Button size="lg" nativeButton={false} render={<Link {...props} />} />}
79
+ </hero.Link>
80
+ </div>
81
+ {withIllustration && (
82
+ <hero.Image name="illustration">
83
+ {(props) => <img {...props} className="h-auto w-full max-w-md rounded-lg" />}
84
+ </hero.Image>
85
+ )}
49
86
  </div>
50
87
  </div>
51
88
  </section>
@@ -1,5 +1,6 @@
1
1
  import { Link } from "@tanstack/react-router";
2
2
  import { Type, createBlock } from "camox/createBlock";
3
+ import type { ReactElement } from "react";
3
4
 
4
5
  import { Button } from "@/components/ui/button";
5
6
 
@@ -31,44 +32,78 @@ const navbar = createBlock({
31
32
  toMarkdown: (c) => [c.link],
32
33
  }),
33
34
  cta: Type.Link({
34
- default: { text: "Get Started", href: "#", newTab: false },
35
- title: "CTA",
35
+ default: { text: "Get started", href: "#", newTab: false },
36
+ title: "Call to action",
37
+ }),
38
+ },
39
+ settings: {
40
+ sticky: Type.Boolean({
41
+ default: true,
42
+ title: "Sticky",
36
43
  }),
37
44
  },
38
45
  component: NavbarComponent,
39
46
  toMarkdown: (c) => [c.title, c.links, c.cta],
40
47
  });
41
48
 
42
- function NavbarComponent() {
43
- return (
44
- <nav className="dark bg-background border-border border-b">
45
- <div className="container mx-auto px-4">
46
- <div className="flex h-16 items-center justify-between">
47
- <navbar.Link name="title">
48
- {(props) => <Link {...props} className="text-foreground text-xl font-bold" />}
49
- </navbar.Link>
49
+ function NavbarComponent(): ReactElement {
50
+ const sticky = navbar.useSetting("sticky");
51
+
52
+ const innerContent = (
53
+ <div className="container mx-auto px-4">
54
+ <div className="flex h-16 items-center justify-between">
55
+ <navbar.Link name="title">
56
+ {(props) => <Link {...props} className="text-foreground text-lg font-bold" />}
57
+ </navbar.Link>
50
58
 
51
- <div className="flex items-center gap-6">
52
- <navbar.Repeater name="links">
53
- {(linkItem) => (
54
- <linkItem.Link name="link">
55
- {(props) => (
56
- <Link
57
- {...props}
58
- className="text-muted-foreground hover:text-foreground text-sm transition-colors"
59
- />
60
- )}
61
- </linkItem.Link>
62
- )}
63
- </navbar.Repeater>
59
+ <div className="flex items-center gap-6">
60
+ <navbar.Repeater name="links">
61
+ {(linkItem) => (
62
+ <linkItem.Link name="link">
63
+ {(props) => (
64
+ <Link
65
+ {...props}
66
+ className="text-muted-foreground hover:text-foreground text-sm transition-colors"
67
+ />
68
+ )}
69
+ </linkItem.Link>
70
+ )}
71
+ </navbar.Repeater>
64
72
 
65
- <navbar.Link name="cta">
66
- {(props) => <Button size="sm" nativeButton={false} render={<Link {...props} />} />}
67
- </navbar.Link>
68
- </div>
73
+ <navbar.Link name="cta">
74
+ {(props) => (
75
+ <Button
76
+ size="sm"
77
+ variant="outline"
78
+ className="text-foreground"
79
+ nativeButton={false}
80
+ render={<Link {...props} />}
81
+ />
82
+ )}
83
+ </navbar.Link>
69
84
  </div>
70
85
  </div>
71
- </nav>
86
+ </div>
87
+ );
88
+
89
+ if (!sticky) {
90
+ return <nav className="dark bg-background border-border border-b">{innerContent}</nav>;
91
+ }
92
+
93
+ return (
94
+ <>
95
+ <div aria-hidden className="dark bg-background h-16 border-b border-transparent" />
96
+ <navbar.Detached>
97
+ {(props) => (
98
+ <nav
99
+ {...props}
100
+ className="dark bg-background border-border fixed top-0 right-0 left-0 z-50 border-b"
101
+ >
102
+ {innerContent}
103
+ </nav>
104
+ )}
105
+ </navbar.Detached>
106
+ </>
72
107
  );
73
108
  }
74
109
 
@@ -44,51 +44,51 @@ const statistics = createBlock({
44
44
 
45
45
  function StatisticsComponent() {
46
46
  return (
47
- <section className="dark bg-background py-24">
48
- <div className="container mx-auto px-4">
49
- <div className="mx-auto max-w-6xl">
50
- <div className="mb-16">
51
- <statistics.Field name="title">
52
- {(props) => (
53
- <div
54
- {...props}
55
- className="text-primary mb-4 text-sm font-semibold tracking-wider uppercase"
56
- />
57
- )}
58
- </statistics.Field>
59
- <statistics.Field name="subtitle">
60
- {(props) => (
61
- <h2 {...props} className="text-foreground mb-6 text-4xl font-bold sm:text-5xl" />
62
- )}
63
- </statistics.Field>
64
- <statistics.Field name="description">
65
- {(props) => (
66
- <p {...props} className="text-muted-foreground max-w-3xl text-lg leading-relaxed" />
67
- )}
68
- </statistics.Field>
69
- </div>
70
-
71
- <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-4">
72
- <statistics.Repeater name="statistics">
73
- {(stat) => (
74
- <div className="flex gap-3">
75
- <div className="w-0.5 bg-linear-to-b from-teal-400 to-blue-500" />
76
- <div className="flex flex-col">
77
- <stat.Field name="number">
78
- {(props) => (
79
- <div {...props} className="text-foreground mb-2 text-4xl font-bold" />
80
- )}
81
- </stat.Field>
82
- <stat.Field name="label">
83
- {(props) => (
84
- <p {...props} className="text-muted-foreground text-sm leading-relaxed" />
85
- )}
86
- </stat.Field>
87
- </div>
47
+ <section className="dark bg-background py-12 sm:py-16 md:py-20">
48
+ <div className="container mx-auto">
49
+ <div className="mb-16">
50
+ <statistics.Field name="title">
51
+ {(props) => (
52
+ <div
53
+ {...props}
54
+ className="text-accent-foreground mb-4 text-sm font-semibold tracking-wider uppercase"
55
+ />
56
+ )}
57
+ </statistics.Field>
58
+ <statistics.Field name="subtitle">
59
+ {(props) => (
60
+ <h2 {...props} className="text-foreground mb-6 text-3xl font-bold sm:text-5xl" />
61
+ )}
62
+ </statistics.Field>
63
+ <statistics.Field name="description">
64
+ {(props) => (
65
+ <p {...props} className="text-muted-foreground max-w-3xl text-lg leading-relaxed" />
66
+ )}
67
+ </statistics.Field>
68
+ </div>
69
+ <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-4">
70
+ <statistics.Repeater name="statistics">
71
+ {(stat) => (
72
+ <div className="flex gap-3">
73
+ <div className="from-chart-1 to-chart-2 w-0.5 bg-linear-to-b" />
74
+ <div className="flex flex-col">
75
+ <stat.Field name="number">
76
+ {(props) => (
77
+ <div
78
+ {...props}
79
+ className="text-foreground mb-2 text-3xl font-bold sm:text-4xl"
80
+ />
81
+ )}
82
+ </stat.Field>
83
+ <stat.Field name="label">
84
+ {(props) => (
85
+ <p {...props} className="text-muted-foreground text-sm leading-relaxed" />
86
+ )}
87
+ </stat.Field>
88
88
  </div>
89
- )}
90
- </statistics.Repeater>
91
- </div>
89
+ </div>
90
+ )}
91
+ </statistics.Repeater>
92
92
  </div>
93
93
  </div>
94
94
  </section>
@@ -11,9 +11,9 @@ const testimonial = createBlock({
11
11
  "This platform has transformed how we build and manage our website. The developer experience is exceptional.",
12
12
  title: "Quote",
13
13
  }),
14
- author: Type.String({ default: "Sarah Chen", title: "Author" }),
14
+ author: Type.String({ default: "Kai Doe", title: "Author" }),
15
15
  title: Type.String({ default: "Senior Developer", title: "Title" }),
16
- company: Type.String({ default: "TechCorp", title: "Company" }),
16
+ company: Type.String({ default: "E Corp", title: "Company" }),
17
17
  },
18
18
  component: TestimonialComponent,
19
19
  toMarkdown: (c) => [`> ${c.quote}`, `— ${c.author}, ${c.title}, ${c.company}`],
@@ -21,14 +21,14 @@ const testimonial = createBlock({
21
21
 
22
22
  function TestimonialComponent() {
23
23
  return (
24
- <section className="bg-background py-24">
24
+ <section className="bg-background py-12 sm:py-16 md:py-24">
25
25
  <div className="container mx-auto px-4">
26
26
  <div className="mx-auto max-w-4xl text-center">
27
27
  <testimonial.Field name="quote">
28
28
  {(props) => (
29
29
  <blockquote
30
30
  {...props}
31
- className="text-foreground mb-8 text-2xl leading-relaxed font-medium sm:text-3xl"
31
+ className="text-foreground mb-8 text-xl leading-relaxed font-medium sm:text-3xl"
32
32
  >
33
33
  "{props.children}"
34
34
  </blockquote>
@@ -0,0 +1,158 @@
1
+ import { Type, createBlock } from "camox/createBlock";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const youtubeVideo = createBlock({
6
+ id: "youtube-video",
7
+ title: "YouTube Video",
8
+ description:
9
+ "Embeds a YouTube video. Use this block to display a single YouTube video on a page. Don't try to guess the URL, use a web search tool to find a specific video URL instead.",
10
+ content: {
11
+ url: Type.Embed({
12
+ pattern:
13
+ "https:\\/\\/(www\\.)?(youtube\\.com\\/(watch\\?v=|embed\\/|shorts\\/)|youtu\\.be\\/).+",
14
+ default: "https://www.youtube.com/watch?v=-W_nFlIAWFM",
15
+ title: "YouTube URL",
16
+ }),
17
+ },
18
+ settings: {
19
+ fullWidth: Type.Boolean({
20
+ default: false,
21
+ title: "Full Width",
22
+ }),
23
+ theme: Type.Enum({
24
+ options: {
25
+ light: "Light",
26
+ dark: "Dark",
27
+ },
28
+ default: "light",
29
+ title: "Theme",
30
+ }),
31
+ autoplay: Type.Boolean({
32
+ default: false,
33
+ title: "Autoplay",
34
+ }),
35
+ mute: Type.Boolean({
36
+ default: false,
37
+ title: "Mute",
38
+ }),
39
+ controls: Type.Boolean({
40
+ default: true,
41
+ title: "Controls",
42
+ }),
43
+ showCaptions: Type.Boolean({
44
+ default: false,
45
+ title: "Show Captions",
46
+ }),
47
+ rel: Type.Boolean({
48
+ default: false,
49
+ title: "Related Videos",
50
+ }),
51
+ fullscreen: Type.Boolean({
52
+ default: true,
53
+ title: "Fullscreen",
54
+ }),
55
+ },
56
+ component: YouTubeVideoComponent,
57
+ toMarkdown: (c) => [c.url],
58
+ });
59
+
60
+ function extractVideoId(url: string): string | null {
61
+ const shortMatch = url.match(/youtu\.be\/([^?&]+)/);
62
+ if (shortMatch) return shortMatch[1];
63
+
64
+ const shortsMatch = url.match(/youtube\.com\/shorts\/([^?&]+)/);
65
+ if (shortsMatch) return shortsMatch[1];
66
+
67
+ const watchMatch = url.match(/[?&]v=([^&]+)/);
68
+ if (watchMatch) return watchMatch[1];
69
+
70
+ const embedMatch = url.match(/youtube\.com\/embed\/([^?&]+)/);
71
+ if (embedMatch) return embedMatch[1];
72
+
73
+ return null;
74
+ }
75
+
76
+ interface YouTubeParams {
77
+ autoplay: boolean;
78
+ mute: boolean;
79
+ controls: boolean;
80
+ showCaptions: boolean;
81
+ rel: boolean;
82
+ fullscreen: boolean;
83
+ }
84
+
85
+ function getYouTubeEmbedUrl(url: string, params: YouTubeParams): string {
86
+ const videoId = extractVideoId(url);
87
+ if (!videoId) return url;
88
+
89
+ const searchParams = new URLSearchParams();
90
+
91
+ if (params.autoplay) searchParams.set("autoplay", "1");
92
+ if (params.mute) searchParams.set("mute", "1");
93
+ if (!params.controls) searchParams.set("controls", "0");
94
+ if (params.showCaptions) searchParams.set("cc_load_policy", "1");
95
+ if (!params.rel) searchParams.set("rel", "0");
96
+ if (!params.fullscreen) searchParams.set("fs", "0");
97
+ searchParams.set("disablekb", "1");
98
+
99
+ const query = searchParams.toString();
100
+ return `https://www.youtube.com/embed/${videoId}${query ? `?${query}` : ""}`;
101
+ }
102
+
103
+ function YouTubeVideoComponent() {
104
+ const fullWidth = youtubeVideo.useSetting("fullWidth");
105
+ const theme = youtubeVideo.useSetting("theme");
106
+ const autoplay = youtubeVideo.useSetting("autoplay");
107
+ const mute = youtubeVideo.useSetting("mute");
108
+ const controls = youtubeVideo.useSetting("controls");
109
+ const showCaptions = youtubeVideo.useSetting("showCaptions");
110
+ const rel = youtubeVideo.useSetting("rel");
111
+ const fullscreen = youtubeVideo.useSetting("fullscreen");
112
+
113
+ const params: YouTubeParams = {
114
+ autoplay,
115
+ mute,
116
+ controls,
117
+ showCaptions,
118
+ rel,
119
+ fullscreen,
120
+ };
121
+
122
+ return (
123
+ <section className={cn(theme === "dark" ? "dark" : "light")}>
124
+ <div className={cn("bg-background", !fullWidth && "py-12")}>
125
+ <div className={cn(!fullWidth && "container mx-auto px-4")}>
126
+ <youtubeVideo.Embed name="url">
127
+ {(_props, { url }) => (
128
+ <div
129
+ className={cn(
130
+ "relative w-full",
131
+ !fullWidth && "overflow-hidden rounded-lg shadow-lg",
132
+ )}
133
+ style={{ paddingBottom: "56.25%" }}
134
+ >
135
+ <iframe
136
+ src={getYouTubeEmbedUrl(url, params)}
137
+ title="YouTube video"
138
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
139
+ allowFullScreen={fullscreen}
140
+ style={{
141
+ position: "absolute",
142
+ top: 0,
143
+ left: 0,
144
+ width: "100%",
145
+ height: "100%",
146
+ border: 0,
147
+ }}
148
+ />
149
+ </div>
150
+ )}
151
+ </youtubeVideo.Embed>
152
+ </div>
153
+ </div>
154
+ </section>
155
+ );
156
+ }
157
+
158
+ export { youtubeVideo as block };
@@ -1,9 +1,11 @@
1
1
  import { createLayout } from "camox/createLayout";
2
2
 
3
+ import { block as faqBlock } from "../blocks/faq";
3
4
  import { block as footerBlock } from "../blocks/footer";
4
5
  import { block as heroBlock } from "../blocks/hero";
5
6
  import { block as navbarBlock } from "../blocks/navbar";
6
7
  import { block as statisticsBlock } from "../blocks/statistics";
8
+ import { block as testimonialBlock } from "../blocks/testimonial";
7
9
 
8
10
  const defaultLayout = createLayout({
9
11
  id: "default",
@@ -12,7 +14,7 @@ const defaultLayout = createLayout({
12
14
  blocks: {
13
15
  before: [navbarBlock],
14
16
  after: [footerBlock],
15
- initial: [heroBlock, statisticsBlock],
17
+ initial: [heroBlock, testimonialBlock, statisticsBlock, faqBlock],
16
18
  },
17
19
  component: DefaultLayout,
18
20
  buildMetaTitle: ({ pageMetaTitle, projectName }) => `${pageMetaTitle} | ${projectName}`,
@@ -1,7 +1,6 @@
1
- import type { ClassValue } from "clsx";
2
- import { clsx } from "clsx";
1
+ import { clsx, type ClassValue } from "clsx";
3
2
  import { twMerge } from "tailwind-merge";
4
3
 
5
- export function cn(...inputs: Array<ClassValue>) {
4
+ export function cn(...inputs: ClassValue[]) {
6
5
  return twMerge(clsx(inputs));
7
6
  }
@@ -1,87 +1,86 @@
1
1
  @import "tailwindcss";
2
2
  @import "tw-animate-css";
3
3
  @import "shadcn/tailwind.css";
4
+ @import "@fontsource-variable/inter";
5
+ @import "@fontsource-variable/noto-serif";
4
6
 
5
7
  @custom-variant dark (&:is(.dark *));
6
8
 
7
9
  body {
8
10
  @apply m-0;
9
- font-family:
10
- -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
11
- "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
12
11
  -webkit-font-smoothing: antialiased;
13
12
  -moz-osx-font-smoothing: grayscale;
14
13
  }
15
14
 
16
15
  :root {
17
- --background: oklch(1 0 0);
18
- --foreground: oklch(0.13 0.028 261.692);
16
+ --background: oklch(0.99 0.01 260);
17
+ --foreground: oklch(0 0 0);
19
18
  --card: oklch(1 0 0);
20
- --card-foreground: oklch(0.13 0.028 261.692);
19
+ --card-foreground: oklch(0 0 0);
21
20
  --popover: oklch(1 0 0);
22
- --popover-foreground: oklch(0.13 0.028 261.692);
23
- --primary: oklch(0.21 0.034 264.665);
24
- --primary-foreground: oklch(0.985 0.002 247.839);
25
- --secondary: oklch(0.967 0.003 264.542);
26
- --secondary-foreground: oklch(0.21 0.034 264.665);
27
- --muted: oklch(0.967 0.003 264.542);
28
- --muted-foreground: oklch(0.551 0.027 264.364);
29
- --accent: oklch(0.967 0.003 264.542);
30
- --accent-foreground: oklch(0.21 0.034 264.665);
21
+ --popover-foreground: oklch(0 0 0);
22
+ --primary: oklch(0.623 0.214 259.815);
23
+ --primary-foreground: oklch(0.97 0.014 254.604);
24
+ --secondary: oklch(0.96 0.02 260);
25
+ --secondary-foreground: oklch(0.22 0.05 260);
26
+ --muted: oklch(0.96 0.02 260);
27
+ --muted-foreground: oklch(0.55 0.05 260);
28
+ --accent: oklch(0.96 0.02 260);
29
+ --accent-foreground: oklch(0.22 0.05 260);
31
30
  --destructive: oklch(0.577 0.245 27.325);
32
31
  --destructive-foreground: oklch(0.577 0.245 27.325);
33
- --border: oklch(0.928 0.006 264.531);
34
- --input: oklch(0.928 0.006 264.531);
35
- --ring: oklch(0.707 0.022 261.325);
36
- --chart-1: oklch(0.646 0.222 41.116);
37
- --chart-2: oklch(0.6 0.118 184.704);
38
- --chart-3: oklch(0.398 0.07 227.392);
39
- --chart-4: oklch(0.828 0.189 84.429);
40
- --chart-5: oklch(0.769 0.188 70.08);
41
- --radius: 0.4rem;
42
- --sidebar: oklch(0.985 0.002 247.839);
43
- --sidebar-foreground: oklch(0.13 0.028 261.692);
44
- --sidebar-primary: oklch(0.21 0.034 264.665);
45
- --sidebar-primary-foreground: oklch(0.985 0.002 247.839);
46
- --sidebar-accent: oklch(0.967 0.003 264.542);
47
- --sidebar-accent-foreground: oklch(0.21 0.034 264.665);
48
- --sidebar-border: oklch(0.928 0.006 264.531);
49
- --sidebar-ring: oklch(0.707 0.022 261.325);
32
+ --border: oklch(0.92 0.02 260);
33
+ --input: oklch(0.92 0.02 260);
34
+ --ring: oklch(0.7 0.05 260);
35
+ --chart-1: oklch(0.871 0.02 260);
36
+ --chart-2: oklch(0.62 0.19 260);
37
+ --chart-3: oklch(0.5 0.12 260);
38
+ --chart-4: oklch(0.4 0.08 260);
39
+ --chart-5: oklch(0.3 0.05 260);
40
+ --radius: 0.625rem;
41
+ --sidebar: oklch(0.99 0.01 260);
42
+ --sidebar-foreground: oklch(0 0 0);
43
+ --sidebar-primary: oklch(0.62 0.19 260);
44
+ --sidebar-primary-foreground: oklch(0.984 0.019 200.873);
45
+ --sidebar-accent: oklch(0.96 0.02 260);
46
+ --sidebar-accent-foreground: oklch(0.22 0.05 260);
47
+ --sidebar-border: oklch(0.92 0.02 260);
48
+ --sidebar-ring: oklch(0.7 0.05 260);
50
49
  }
51
50
 
52
51
  .dark {
53
- --background: oklch(0.13 0.028 261.692);
54
- --foreground: oklch(0.985 0.002 247.839);
55
- --card: oklch(0.21 0.034 264.665);
56
- --card-foreground: oklch(0.985 0.002 247.839);
57
- --popover: oklch(0.21 0.034 264.665);
58
- --popover-foreground: oklch(0.985 0.002 247.839);
59
- --primary: oklch(0.928 0.006 264.531);
60
- --primary-foreground: oklch(0.21 0.034 264.665);
61
- --secondary: oklch(0.278 0.033 256.848);
62
- --secondary-foreground: oklch(0.985 0.002 247.839);
63
- --muted: oklch(0.278 0.033 256.848);
64
- --muted-foreground: oklch(0.707 0.022 261.325);
65
- --accent: oklch(0.278 0.033 256.848);
66
- --accent-foreground: oklch(0.985 0.002 247.839);
52
+ --background: oklch(0.17 0.05 260);
53
+ --foreground: oklch(0.985 0 0);
54
+ --card: oklch(0.22 0.05 260);
55
+ --card-foreground: oklch(0.985 0 0);
56
+ --popover: oklch(0.22 0.05 260);
57
+ --popover-foreground: oklch(0.985 0 0);
58
+ --primary: oklch(0.623 0.214 259.815);
59
+ --primary-foreground: oklch(0.97 0.014 254.604);
60
+ --secondary: oklch(0.3 0.05 260);
61
+ --secondary-foreground: oklch(0.985 0 0);
62
+ --muted: oklch(0.3 0.05 260);
63
+ --muted-foreground: oklch(0.72 0.03 260);
64
+ --accent: oklch(0.3 0.05 260);
65
+ --accent-foreground: oklch(0.985 0 0);
67
66
  --destructive: oklch(0.704 0.191 22.216);
68
67
  --destructive-foreground: oklch(0.637 0.237 25.331);
69
68
  --border: oklch(1 0 0 / 10%);
70
69
  --input: oklch(1 0 0 / 15%);
71
- --ring: oklch(0.551 0.027 264.364);
72
- --chart-1: oklch(0.488 0.243 264.376);
73
- --chart-2: oklch(0.696 0.17 162.48);
74
- --chart-3: oklch(0.769 0.188 70.08);
75
- --chart-4: oklch(0.627 0.265 303.9);
76
- --chart-5: oklch(0.645 0.246 16.439);
77
- --sidebar: oklch(0.21 0.034 264.665);
78
- --sidebar-foreground: oklch(0.985 0.002 247.839);
79
- --sidebar-primary: oklch(0.488 0.243 264.376);
80
- --sidebar-primary-foreground: oklch(0.985 0.002 247.839);
81
- --sidebar-accent: oklch(0.278 0.033 256.848);
82
- --sidebar-accent-foreground: oklch(0.985 0.002 247.839);
70
+ --ring: oklch(0.55 0.05 260);
71
+ --chart-1: oklch(0.871 0.02 260);
72
+ --chart-2: oklch(0.62 0.19 260);
73
+ --chart-3: oklch(0.5 0.12 260);
74
+ --chart-4: oklch(0.4 0.08 260);
75
+ --chart-5: oklch(0.3 0.05 260);
76
+ --sidebar: oklch(0.22 0.05 260);
77
+ --sidebar-foreground: oklch(0.985 0 0);
78
+ --sidebar-primary: oklch(0.62 0.19 260);
79
+ --sidebar-primary-foreground: oklch(0.984 0.019 200.873);
80
+ --sidebar-accent: oklch(0.3 0.05 260);
81
+ --sidebar-accent-foreground: oklch(0.985 0 0);
83
82
  --sidebar-border: oklch(1 0 0 / 10%);
84
- --sidebar-ring: oklch(0.551 0.027 264.364);
83
+ --sidebar-ring: oklch(0.55 0.05 260);
85
84
  }
86
85
 
87
86
  @theme inline {
@@ -121,6 +120,11 @@ body {
121
120
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
122
121
  --color-sidebar-border: var(--sidebar-border);
123
122
  --color-sidebar-ring: var(--sidebar-ring);
123
+ --font-sans: "Inter Variable", sans-serif;
124
+ --font-heading: "Inter Variable", sans-serif;
125
+ --radius-2xl: calc(var(--radius) * 1.8);
126
+ --radius-3xl: calc(var(--radius) * 2.2);
127
+ --radius-4xl: calc(var(--radius) * 2.6);
124
128
  }
125
129
 
126
130
  @layer base {
@@ -133,4 +137,26 @@ body {
133
137
  a:focus-visible {
134
138
  @apply border-ring ring-ring/50 rounded-md ring-3 outline-none;
135
139
  }
140
+ html {
141
+ @apply font-sans;
142
+ font-size: 14px;
143
+ @media (width >= theme(--breakpoint-sm)) {
144
+ font-size: 15px;
145
+ }
146
+ @media (width >= theme(--breakpoint-md)) {
147
+ font-size: 16px;
148
+ }
149
+ }
150
+ }
151
+
152
+ @utility container {
153
+ margin-inline: auto;
154
+ padding-inline: 0.5rem;
155
+ max-width: 80rem;
156
+ @media (width >= theme(--breakpoint-sm)) {
157
+ padding-inline: 1rem;
158
+ }
159
+ @media (width >= theme(--breakpoint-md)) {
160
+ padding-inline: 2rem;
161
+ }
136
162
  }