@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 +2 -2
- package/templates/default/components.json +4 -1
- package/templates/default/package.json +3 -1
- package/templates/default/src/camox/blocks/faq.tsx +1 -1
- package/templates/default/src/camox/blocks/footer.tsx +11 -12
- package/templates/default/src/camox/blocks/hero.tsx +58 -21
- package/templates/default/src/camox/blocks/navbar.tsx +63 -28
- package/templates/default/src/camox/blocks/statistics.tsx +44 -44
- package/templates/default/src/camox/blocks/testimonial.tsx +4 -4
- package/templates/default/src/camox/blocks/youtube-video.tsx +158 -0
- package/templates/default/src/camox/layouts/default.tsx +3 -1
- package/templates/default/src/lib/utils.ts +2 -3
- package/templates/default/src/styles.css +85 -59
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camox/cli",
|
|
3
|
-
"version": "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.
|
|
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": "
|
|
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.
|
|
31
|
+
"shadcn": "^4.6.0",
|
|
30
32
|
"tailwind-merge": "^3.5.0",
|
|
31
33
|
"tailwindcss": "^4.0.6"
|
|
32
34
|
},
|
|
@@ -11,11 +11,11 @@ const footer = createBlock({
|
|
|
11
11
|
links: Type.RepeatableItem({
|
|
12
12
|
content: {
|
|
13
13
|
link: Type.Link({
|
|
14
|
-
default: { text: "
|
|
14
|
+
default: { text: "Footer link", href: "#", newTab: false },
|
|
15
15
|
title: "Link",
|
|
16
16
|
}),
|
|
17
17
|
},
|
|
18
|
-
minItems:
|
|
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-
|
|
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
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
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">© {new Date().getFullYear()}</div>
|
|
38
|
+
</div>
|
|
36
39
|
|
|
37
|
-
<div className="flex flex-wrap items-center
|
|
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
|
-
© {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: "
|
|
14
|
+
default: "Let's get going on {{projectName}}",
|
|
14
15
|
title: "Title",
|
|
15
16
|
}),
|
|
16
17
|
description: Type.String({
|
|
17
|
-
default: "
|
|
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
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
35
|
-
title: "
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
</
|
|
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-
|
|
48
|
-
<div className="container mx-auto
|
|
49
|
-
<div className="
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<div className="flex
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
</
|
|
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
|
-
|
|
91
|
-
</
|
|
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: "
|
|
14
|
+
author: Type.String({ default: "Kai Doe", title: "Author" }),
|
|
15
15
|
title: Type.String({ default: "Senior Developer", title: "Title" }),
|
|
16
|
-
company: Type.String({ default: "
|
|
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-
|
|
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
|
|
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:
|
|
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(
|
|
18
|
-
--foreground: oklch(0
|
|
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
|
|
19
|
+
--card-foreground: oklch(0 0 0);
|
|
21
20
|
--popover: oklch(1 0 0);
|
|
22
|
-
--popover-foreground: oklch(0
|
|
23
|
-
--primary: oklch(0.
|
|
24
|
-
--primary-foreground: oklch(0.
|
|
25
|
-
--secondary: oklch(0.
|
|
26
|
-
--secondary-foreground: oklch(0.
|
|
27
|
-
--muted: oklch(0.
|
|
28
|
-
--muted-foreground: oklch(0.
|
|
29
|
-
--accent: oklch(0.
|
|
30
|
-
--accent-foreground: oklch(0.
|
|
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.
|
|
34
|
-
--input: oklch(0.
|
|
35
|
-
--ring: oklch(0.
|
|
36
|
-
--chart-1: oklch(0.
|
|
37
|
-
--chart-2: oklch(0.
|
|
38
|
-
--chart-3: oklch(0.
|
|
39
|
-
--chart-4: oklch(0.
|
|
40
|
-
--chart-5: oklch(0.
|
|
41
|
-
--radius: 0.
|
|
42
|
-
--sidebar: oklch(0.
|
|
43
|
-
--sidebar-foreground: oklch(0
|
|
44
|
-
--sidebar-primary: oklch(0.
|
|
45
|
-
--sidebar-primary-foreground: oklch(0.
|
|
46
|
-
--sidebar-accent: oklch(0.
|
|
47
|
-
--sidebar-accent-foreground: oklch(0.
|
|
48
|
-
--sidebar-border: oklch(0.
|
|
49
|
-
--sidebar-ring: oklch(0.
|
|
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.
|
|
54
|
-
--foreground: oklch(0.985 0
|
|
55
|
-
--card: oklch(0.
|
|
56
|
-
--card-foreground: oklch(0.985 0
|
|
57
|
-
--popover: oklch(0.
|
|
58
|
-
--popover-foreground: oklch(0.985 0
|
|
59
|
-
--primary: oklch(0.
|
|
60
|
-
--primary-foreground: oklch(0.
|
|
61
|
-
--secondary: oklch(0.
|
|
62
|
-
--secondary-foreground: oklch(0.985 0
|
|
63
|
-
--muted: oklch(0.
|
|
64
|
-
--muted-foreground: oklch(0.
|
|
65
|
-
--accent: oklch(0.
|
|
66
|
-
--accent-foreground: oklch(0.985 0
|
|
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.
|
|
72
|
-
--chart-1: oklch(0.
|
|
73
|
-
--chart-2: oklch(0.
|
|
74
|
-
--chart-3: oklch(0.
|
|
75
|
-
--chart-4: oklch(0.
|
|
76
|
-
--chart-5: oklch(0.
|
|
77
|
-
--sidebar: oklch(0.
|
|
78
|
-
--sidebar-foreground: oklch(0.985 0
|
|
79
|
-
--sidebar-primary: oklch(0.
|
|
80
|
-
--sidebar-primary-foreground: oklch(0.
|
|
81
|
-
--sidebar-accent: oklch(0.
|
|
82
|
-
--sidebar-accent-foreground: oklch(0.985 0
|
|
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.
|
|
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
|
}
|