@navneet_25/tempjs 2.0.0 → 3.0.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 (90) hide show
  1. package/README.md +66 -18
  2. package/cli/brand-manager.js +183 -96
  3. package/cli/config.js +9 -1
  4. package/cli/doctor.js +393 -0
  5. package/cli/fetch-core-modules.js +130 -0
  6. package/cli/index.js +141 -10
  7. package/cli/info.js +28 -1
  8. package/cli/init-options.js +209 -0
  9. package/cli/module-manager.js +168 -0
  10. package/cli/parse-args.js +21 -0
  11. package/cli/update.js +15 -0
  12. package/cli/version-manager.js +518 -0
  13. package/package.json +27 -4
  14. package/packages/core/modules/README.md +72 -0
  15. package/packages/core/modules/blog-compose/app/admin/components/blog-compose-form.ts +44 -0
  16. package/packages/core/modules/blog-compose/app/admin/hooks/useBlogPosts.ts +103 -0
  17. package/packages/core/modules/blog-compose/app/admin/panels/BlogComposePanel.tsx +388 -0
  18. package/packages/core/modules/blog-compose/app/api/blog-posts/[id]/route.ts +29 -0
  19. package/packages/core/modules/blog-compose/app/api/blog-posts/route.ts +21 -0
  20. package/packages/core/modules/blog-compose/app/blog/[slug]/page.tsx +79 -0
  21. package/packages/core/modules/blog-compose/app/blog/layout.tsx +22 -0
  22. package/packages/core/modules/blog-compose/app/blog/page.tsx +102 -0
  23. package/packages/core/modules/blog-compose/app/components/BlogSection.tsx +74 -0
  24. package/packages/core/modules/blog-compose/app/components/BlogShell.tsx +36 -0
  25. package/packages/core/modules/blog-compose/app/components/BlogSidebar.tsx +112 -0
  26. package/packages/core/modules/blog-compose/lib/blog/compose/annotate-text.tsx +103 -0
  27. package/packages/core/modules/blog-compose/lib/blog/compose/blocks/media-blocks.tsx +97 -0
  28. package/packages/core/modules/blog-compose/lib/blog/compose/blocks/text-blocks.tsx +115 -0
  29. package/packages/core/modules/blog-compose/lib/blog/compose/index.ts +21 -0
  30. package/packages/core/modules/blog-compose/lib/blog/compose/interpreter.tsx +55 -0
  31. package/packages/core/modules/blog-compose/lib/blog/compose/registry.ts +276 -0
  32. package/packages/core/modules/blog-compose/lib/blog/compose/types.ts +128 -0
  33. package/packages/core/modules/blog-compose/lib/blog/register-sitemap.ts +18 -0
  34. package/packages/core/modules/blog-compose/lib/controllers/BlogComposeController.ts +2 -0
  35. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.controller.ts +78 -0
  36. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.repository.ts +78 -0
  37. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.service.ts +72 -0
  38. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.types.ts +20 -0
  39. package/packages/core/modules/blog-compose/lib/features/blog-compose/index.ts +7 -0
  40. package/packages/core/modules/blog-compose/prisma/blog-compose.prisma +13 -0
  41. package/packages/core/modules/enquiry-modal/app/components/EnquiryModal.tsx +308 -0
  42. package/packages/core/modules/footer/app/components/Footer.tsx +127 -0
  43. package/packages/core/modules/gallery/app/admin/components/GalleryFormModal.tsx +132 -0
  44. package/packages/core/modules/gallery/app/admin/components/GalleryList.tsx +142 -0
  45. package/packages/core/modules/gallery/app/admin/hooks/useGallery.ts +72 -0
  46. package/packages/core/modules/gallery/app/api/gallery/[id]/route.ts +29 -0
  47. package/packages/core/modules/gallery/app/api/gallery/route.ts +22 -0
  48. package/packages/core/modules/gallery/app/components/GallerySection.tsx +92 -0
  49. package/packages/core/modules/gallery/app/gallery/layout.tsx +12 -0
  50. package/packages/core/modules/gallery/app/gallery/page.tsx +179 -0
  51. package/packages/core/modules/gallery/lib/controllers/GalleryController.ts +2 -0
  52. package/packages/core/modules/gallery/lib/features/gallery/gallery.controller.ts +78 -0
  53. package/packages/core/modules/gallery/lib/features/gallery/gallery.repository.ts +55 -0
  54. package/packages/core/modules/gallery/lib/features/gallery/gallery.service.ts +43 -0
  55. package/packages/core/modules/gallery/lib/features/gallery/gallery.types.ts +17 -0
  56. package/packages/core/modules/gallery/lib/features/gallery/index.ts +4 -0
  57. package/packages/core/modules/gallery/prisma/gallery.prisma +11 -0
  58. package/packages/core/modules/hero-simple/app/components/Hero.tsx +160 -0
  59. package/packages/core/modules/legal-pages/app/components/LegalDocumentPage.tsx +61 -0
  60. package/packages/core/modules/legal-pages/app/privacy-policy/page.tsx +19 -0
  61. package/packages/core/modules/legal-pages/app/terms-and-conditions/page.tsx +19 -0
  62. package/packages/core/modules/reviews/app/admin/components/ReviewFormModal.tsx +110 -0
  63. package/packages/core/modules/reviews/app/admin/components/ReviewsList.tsx +116 -0
  64. package/packages/core/modules/reviews/app/admin/hooks/useReviews.ts +70 -0
  65. package/packages/core/modules/reviews/app/api/reviews/[id]/route.ts +29 -0
  66. package/packages/core/modules/reviews/app/api/reviews/route.ts +17 -0
  67. package/packages/core/modules/reviews/app/components/ReviewsSection.tsx +100 -0
  68. package/packages/core/modules/reviews/lib/controllers/ReviewController.ts +2 -0
  69. package/packages/core/modules/reviews/lib/features/reviews/index.ts +3 -0
  70. package/packages/core/modules/reviews/lib/features/reviews/review.controller.ts +64 -0
  71. package/packages/core/modules/reviews/lib/features/reviews/review.repository.ts +50 -0
  72. package/packages/core/modules/reviews/lib/features/reviews/review.service.ts +38 -0
  73. package/packages/core/modules/reviews/prisma/reviews.prisma +9 -0
  74. package/packages/core/modules/seo/app/components/SiteJsonLd.tsx +16 -0
  75. package/packages/core/modules/seo/app/robots.ts +30 -0
  76. package/packages/core/modules/seo/app/sitemap.ts +9 -0
  77. package/packages/core/modules/seo/lib/seo/index.ts +5 -0
  78. package/packages/core/modules/seo/lib/seo/json-ld.ts +128 -0
  79. package/packages/core/modules/seo/lib/seo/metadata.ts +65 -0
  80. package/packages/core/modules/seo/lib/seo/sitemap.ts +82 -0
  81. package/packages/core/modules/seo/lib/seo/types.ts +30 -0
  82. package/packages/core/modules/seo/lib/seo/urls.ts +27 -0
  83. package/packages/core/modules/theme-modes/app/components/ThemeModeInit.tsx +24 -0
  84. package/packages/core/modules/theme-modes/app/components/ThemeModeToggle.tsx +58 -0
  85. package/packages/core/modules.json +140 -0
  86. package/packages/core/prisma/schema.prisma +33 -0
  87. package/scripts/module-installer-core.mjs +657 -0
  88. package/scripts/template-modules-admin.mjs +366 -0
  89. package/scripts/template-modules-home.mjs +120 -0
  90. package/templates.json +163 -8
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from "react";
4
+ import Link from "next/link";
5
+ import { ArrowLeft, Calendar } from "lucide-react";
6
+ import { SITE } from "@/constants";
7
+
8
+ interface BlogListItem {
9
+ id: string;
10
+ slug: string;
11
+ title: string;
12
+ excerpt?: string | null;
13
+ coverImage?: string | null;
14
+ publishedAt?: string | null;
15
+ createdAt: string;
16
+ }
17
+
18
+ const withSidebar = SITE.features.blogSidebar;
19
+
20
+ export default function BlogIndexPage() {
21
+ const [posts, setPosts] = useState<BlogListItem[]>([]);
22
+ const [loading, setLoading] = useState(true);
23
+
24
+ useEffect(() => {
25
+ async function load() {
26
+ try {
27
+ const res = await fetch("/api/blog-posts");
28
+ const json = await res.json();
29
+ if (json.success && Array.isArray(json.data)) {
30
+ setPosts(json.data);
31
+ }
32
+ } catch (err) {
33
+ console.warn("Blog index: failed to load", err);
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }
38
+ load();
39
+ }, []);
40
+
41
+ const listContent = (
42
+ <>
43
+ <p className="text-text-muted text-sm mb-10">{SITE.blog.pageSubtitle}</p>
44
+ {loading ? (
45
+ <p className="text-text-muted text-center py-16">Loading articles…</p>
46
+ ) : posts.length === 0 ? (
47
+ <p className="text-center text-text-muted py-20">{SITE.blog.emptyMessage}</p>
48
+ ) : (
49
+ <ul className="space-y-8">
50
+ {posts.map((post) => (
51
+ <li key={post.id}>
52
+ <Link
53
+ href={`/blog/${post.slug}`}
54
+ className="block group rounded-2xl border border-primary/10 bg-bg-card p-6 hover:border-primary/25 hover:shadow-md transition-all"
55
+ >
56
+ <h2 className="text-xl font-bold text-primary group-hover:text-primary-hover font-serif">
57
+ {post.title}
58
+ </h2>
59
+ {post.excerpt && (
60
+ <p className="mt-2 text-sm text-text-muted line-clamp-2">{post.excerpt}</p>
61
+ )}
62
+ <p className="mt-3 flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-text-muted">
63
+ <Calendar className="w-3 h-3" />
64
+ {post.publishedAt
65
+ ? new Date(post.publishedAt).toLocaleDateString()
66
+ : new Date(post.createdAt).toLocaleDateString()}
67
+ </p>
68
+ </Link>
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ )}
73
+ </>
74
+ );
75
+
76
+ if (withSidebar) {
77
+ return (
78
+ <div>
79
+ <h1 className="text-2xl font-bold font-serif text-primary mb-2">{SITE.blog.pageTitle}</h1>
80
+ {listContent}
81
+ </div>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <div className="min-h-screen bg-bg-main">
87
+ <header className="border-b border-primary/10 bg-white/80 backdrop-blur-md sticky top-0 z-30">
88
+ <div className="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
89
+ <Link
90
+ href="/"
91
+ className="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:text-primary-hover"
92
+ >
93
+ <ArrowLeft className="w-4 h-4" />
94
+ {SITE.navigation.backToHome}
95
+ </Link>
96
+ <h1 className="text-lg font-bold font-serif text-primary">{SITE.blog.pageTitle}</h1>
97
+ </div>
98
+ </header>
99
+ <main className="max-w-4xl mx-auto px-6 py-12">{listContent}</main>
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from "react";
4
+ import Link from "next/link";
5
+ import { SITE } from "@/constants";
6
+
7
+ interface BlogCard {
8
+ id: string;
9
+ slug: string;
10
+ title: string;
11
+ excerpt?: string | null;
12
+ }
13
+
14
+ export default function BlogSection() {
15
+ const [posts, setPosts] = useState<BlogCard[]>([]);
16
+
17
+ useEffect(() => {
18
+ async function load() {
19
+ try {
20
+ const res = await fetch("/api/blog-posts");
21
+ const json = await res.json();
22
+ if (json.success && Array.isArray(json.data)) {
23
+ setPosts(json.data.slice(0, SITE.blog.homepageLimit));
24
+ }
25
+ } catch (err) {
26
+ console.warn("BlogSection: failed to load", err);
27
+ }
28
+ }
29
+ load();
30
+ }, []);
31
+
32
+ if (posts.length === 0) return null;
33
+
34
+ return (
35
+ <section className="py-16 px-6 md:px-12 bg-bg-light border-t border-primary/10">
36
+ <div className="max-w-6xl mx-auto">
37
+ <div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4 mb-10">
38
+ <div>
39
+ <span className="text-[10px] font-bold uppercase tracking-widest text-accent-gold-dark">
40
+ {SITE.blog.sectionEyebrow}
41
+ </span>
42
+ <h2 className="text-2xl md:text-3xl font-bold font-serif text-primary mt-1">
43
+ {SITE.blog.sectionTitle}
44
+ </h2>
45
+ {SITE.blog.sectionSubtitle && (
46
+ <p className="text-sm text-text-muted mt-2 max-w-xl">{SITE.blog.sectionSubtitle}</p>
47
+ )}
48
+ </div>
49
+ <Link
50
+ href="/blog"
51
+ className="text-xs font-bold uppercase tracking-widest text-primary hover:text-primary-hover"
52
+ >
53
+ {SITE.blog.viewAllLabel}
54
+ </Link>
55
+ </div>
56
+
57
+ <div className="grid md:grid-cols-3 gap-6">
58
+ {posts.map((post) => (
59
+ <Link
60
+ key={post.id}
61
+ href={`/blog/${post.slug}`}
62
+ className="block rounded-2xl border border-primary/10 bg-bg-card p-5 hover:border-primary/25 hover:shadow-md transition-all"
63
+ >
64
+ <h3 className="font-bold text-primary font-serif line-clamp-2">{post.title}</h3>
65
+ {post.excerpt && (
66
+ <p className="mt-2 text-sm text-text-muted line-clamp-3">{post.excerpt}</p>
67
+ )}
68
+ </Link>
69
+ ))}
70
+ </div>
71
+ </div>
72
+ </section>
73
+ );
74
+ }
@@ -0,0 +1,36 @@
1
+ import Link from "next/link";
2
+ import { ArrowLeft } from "lucide-react";
3
+ import { SITE } from "@/constants";
4
+ import BlogSidebar, { type BlogSidebarPost } from "./BlogSidebar";
5
+
6
+ export default function BlogShell({
7
+ posts,
8
+ children,
9
+ }: {
10
+ posts: BlogSidebarPost[];
11
+ children: React.ReactNode;
12
+ }) {
13
+ return (
14
+ <div className="min-h-screen bg-bg-main">
15
+ <header className="border-b border-primary/10 bg-white/80 backdrop-blur-md sticky top-0 z-30">
16
+ <div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between gap-4">
17
+ <Link
18
+ href="/"
19
+ className="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:text-primary-hover"
20
+ >
21
+ <ArrowLeft className="w-4 h-4" />
22
+ {SITE.navigation.backToHome}
23
+ </Link>
24
+ <span className="text-xs font-bold uppercase tracking-widest text-text-muted truncate">
25
+ {SITE.blog.pageTitle}
26
+ </span>
27
+ </div>
28
+ </header>
29
+
30
+ <div className="max-w-6xl mx-auto px-6 py-8 flex gap-10">
31
+ <BlogSidebar posts={posts} />
32
+ <main className="flex-1 min-w-0 max-w-3xl">{children}</main>
33
+ </div>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+ import { useEffect, useState } from "react";
6
+ import { SITE } from "@/constants";
7
+
8
+ export interface BlogSidebarPost {
9
+ slug: string;
10
+ title: string;
11
+ }
12
+
13
+ export default function BlogSidebar({ posts }: { posts: BlogSidebarPost[] }) {
14
+ const pathname = usePathname();
15
+ const [activeHash, setActiveHash] = useState("");
16
+ const [tocItems, setTocItems] = useState<{ id: string; text: string }[]>([]);
17
+
18
+ useEffect(() => {
19
+ const headings = Array.from(document.querySelectorAll(".compose-blog h2[id]"));
20
+ setTocItems(
21
+ headings.map((el) => ({ id: el.id, text: el.textContent?.trim() || el.id }))
22
+ );
23
+
24
+ const handleScroll = () => {
25
+ if (headings.length === 0) return;
26
+ let current = "";
27
+ for (const el of headings) {
28
+ const rect = el.getBoundingClientRect();
29
+ if (rect.top <= 120) {
30
+ current = `#${el.id}`;
31
+ }
32
+ }
33
+ setActiveHash(current || `#${headings[0].id}`);
34
+ };
35
+
36
+ window.addEventListener("scroll", handleScroll, { passive: true });
37
+ handleScroll();
38
+ return () => window.removeEventListener("scroll", handleScroll);
39
+ }, [pathname]);
40
+
41
+ const showToc =
42
+ pathname.startsWith("/blog/") &&
43
+ pathname !== "/blog/playground" &&
44
+ tocItems.length > 0;
45
+
46
+ return (
47
+ <nav className="w-56 shrink-0 sticky top-8 h-fit max-h-[calc(100vh-8rem)] overflow-y-auto pr-2">
48
+ <p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-3">
49
+ {SITE.blog.sidebarTitle}
50
+ </p>
51
+ <ul className="space-y-1">
52
+ {posts.map((post) => {
53
+ const href = `/blog/${post.slug}`;
54
+ const isActive = pathname === href;
55
+ return (
56
+ <li key={post.slug}>
57
+ <Link
58
+ href={href}
59
+ className={`block rounded-lg px-3 py-2 text-sm transition-colors ${
60
+ isActive
61
+ ? "bg-primary/10 text-primary font-bold"
62
+ : "text-text-muted hover:bg-bg-light hover:text-primary"
63
+ }`}
64
+ >
65
+ {post.title}
66
+ </Link>
67
+ </li>
68
+ );
69
+ })}
70
+ <li>
71
+ <Link
72
+ href="/blog"
73
+ className={`block rounded-lg px-3 py-2 text-sm transition-colors ${
74
+ pathname === "/blog"
75
+ ? "bg-primary/10 text-primary font-bold"
76
+ : "text-text-muted hover:bg-bg-light hover:text-primary"
77
+ }`}
78
+ >
79
+ {SITE.blog.viewAllLabel}
80
+ </Link>
81
+ </li>
82
+ </ul>
83
+
84
+ {showToc && (
85
+ <div className="mt-6 border-t border-primary/10 pt-4">
86
+ <p className="text-[10px] font-bold uppercase tracking-widest text-text-muted mb-2">
87
+ On this page
88
+ </p>
89
+ <ul className="ml-2 pl-3 border-l border-primary/15 space-y-1">
90
+ {tocItems.map((item) => {
91
+ const isSubActive = activeHash === `#${item.id}`;
92
+ return (
93
+ <li key={item.id}>
94
+ <a
95
+ href={`#${item.id}`}
96
+ className={`block py-1 text-xs transition-colors ${
97
+ isSubActive
98
+ ? "text-primary font-bold"
99
+ : "text-text-muted hover:text-primary"
100
+ }`}
101
+ >
102
+ {item.text}
103
+ </a>
104
+ </li>
105
+ );
106
+ })}
107
+ </ul>
108
+ </div>
109
+ )}
110
+ </nav>
111
+ );
112
+ }
@@ -0,0 +1,103 @@
1
+ import React from "react";
2
+
3
+ export interface TextAnnotation {
4
+ highlight?: string[];
5
+ underline?: string[];
6
+ }
7
+
8
+ interface Segment {
9
+ text: string;
10
+ highlight?: boolean;
11
+ underline?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Split plain text into segments with highlight/underline flags.
16
+ * Matches are case-sensitive; longer phrases are preferred first to reduce overlap issues.
17
+ */
18
+ function buildSegments(text: string, annotations: TextAnnotation): Segment[] {
19
+ const phrases: { phrase: string; highlight?: boolean; underline?: boolean }[] = [];
20
+ for (const p of annotations.highlight ?? []) {
21
+ if (p.trim()) phrases.push({ phrase: p, highlight: true });
22
+ }
23
+ for (const p of annotations.underline ?? []) {
24
+ if (p.trim()) phrases.push({ phrase: p, underline: true });
25
+ }
26
+ if (phrases.length === 0) return [{ text }];
27
+
28
+ phrases.sort((a, b) => b.phrase.length - a.phrase.length);
29
+
30
+ type Match = { start: number; end: number; highlight?: boolean; underline?: boolean };
31
+ const matches: Match[] = [];
32
+
33
+ for (const { phrase, highlight, underline } of phrases) {
34
+ let from = 0;
35
+ while (from < text.length) {
36
+ const idx = text.indexOf(phrase, from);
37
+ if (idx === -1) break;
38
+ matches.push({ start: idx, end: idx + phrase.length, highlight, underline });
39
+ from = idx + phrase.length;
40
+ }
41
+ }
42
+
43
+ if (matches.length === 0) return [{ text }];
44
+
45
+ matches.sort((a, b) => a.start - b.start);
46
+
47
+ const segments: Segment[] = [];
48
+ let cursor = 0;
49
+
50
+ for (const m of matches) {
51
+ if (m.start < cursor) continue;
52
+ if (m.start > cursor) {
53
+ segments.push({ text: text.slice(cursor, m.start) });
54
+ }
55
+ segments.push({
56
+ text: text.slice(m.start, m.end),
57
+ highlight: m.highlight,
58
+ underline: m.underline,
59
+ });
60
+ cursor = m.end;
61
+ }
62
+
63
+ if (cursor < text.length) {
64
+ segments.push({ text: text.slice(cursor) });
65
+ }
66
+
67
+ return segments.length > 0 ? segments : [{ text }];
68
+ }
69
+
70
+ export function AnnotatedText({
71
+ text,
72
+ highlight,
73
+ underline,
74
+ className,
75
+ }: {
76
+ text: string;
77
+ highlight?: string[];
78
+ underline?: string[];
79
+ className?: string;
80
+ }) {
81
+ const segments = buildSegments(text, { highlight, underline });
82
+
83
+ return (
84
+ <span className={className}>
85
+ {segments.map((seg, i) => {
86
+ if (!seg.highlight && !seg.underline) {
87
+ return <span key={i}>{seg.text}</span>;
88
+ }
89
+ return (
90
+ <span
91
+ key={i}
92
+ className={[
93
+ seg.highlight ? "bg-accent-gold-light/80 text-text-main px-0.5 rounded" : "",
94
+ seg.underline ? "underline decoration-accent-gold decoration-2 underline-offset-2" : "",
95
+ ].join(" ")}
96
+ >
97
+ {seg.text}
98
+ </span>
99
+ );
100
+ })}
101
+ </span>
102
+ );
103
+ }
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { ComposeCodeBlock, ComposeCommandBlock, ComposeCalloutBlock, ComposeImageBlock } from "../types";
5
+
6
+ export function ComposeCode({ block }: { block: ComposeCodeBlock }) {
7
+ return (
8
+ <div className="my-6 rounded-xl border border-primary/10 bg-bg-light overflow-hidden">
9
+ {block.language && block.language !== "text" && (
10
+ <div className="px-4 py-2 border-b border-primary/10 text-[10px] font-bold uppercase tracking-widest text-text-muted">
11
+ {block.language}
12
+ </div>
13
+ )}
14
+ <pre
15
+ className={[
16
+ "px-4 py-3 text-sm overflow-x-auto font-mono text-text-main leading-relaxed",
17
+ block.showLineNumbers ? "pl-8" : "",
18
+ ].join(" ")}
19
+ >
20
+ <code>{block.code}</code>
21
+ </pre>
22
+ </div>
23
+ );
24
+ }
25
+
26
+ export function ComposeCommand({ block }: { block: ComposeCommandBlock }) {
27
+ const [copied, setCopied] = useState(false);
28
+
29
+ async function copy() {
30
+ try {
31
+ await navigator.clipboard.writeText(block.command);
32
+ setCopied(true);
33
+ setTimeout(() => setCopied(false), 2000);
34
+ } catch {
35
+ /* ignore */
36
+ }
37
+ }
38
+
39
+ return (
40
+ <div className="my-6 rounded-xl border border-primary/10 bg-bg-card shadow-sm overflow-hidden">
41
+ <div className="flex items-center justify-between gap-2 border-b border-primary/10 px-4 py-2 bg-bg-light">
42
+ <span className="text-xs font-semibold uppercase tracking-wide text-text-muted">
43
+ {block.title || "Command"}
44
+ </span>
45
+ <button
46
+ type="button"
47
+ onClick={copy}
48
+ className="rounded-md px-3 py-1 text-xs font-bold bg-primary text-white hover:opacity-90 cursor-pointer"
49
+ >
50
+ {copied ? "Copied!" : "Copy"}
51
+ </button>
52
+ </div>
53
+ {block.description && (
54
+ <p className="px-4 py-2 text-sm text-text-muted border-b border-primary/5">
55
+ {block.description}
56
+ </p>
57
+ )}
58
+ <pre className="px-4 py-3 text-sm font-mono text-text-main overflow-x-auto whitespace-pre-wrap">
59
+ <code>{block.command}</code>
60
+ </pre>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ export function ComposeCallout({ block }: { block: ComposeCalloutBlock }) {
66
+ const styles = {
67
+ info: "bg-primary/5 border-primary/20 text-text-main",
68
+ tip: "bg-emerald-50 border-emerald-200 text-emerald-900",
69
+ warning: "bg-amber-50 border-amber-200 text-amber-900",
70
+ }[block.variant];
71
+
72
+ return (
73
+ <div className={`my-6 rounded-xl border p-4 ${styles}`}>
74
+ {block.title && <p className="font-bold text-sm mb-1">{block.title}</p>}
75
+ <p className="text-sm leading-relaxed">{block.text}</p>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export function ComposeDivider() {
81
+ return <hr className="my-10 border-t border-primary/10" />;
82
+ }
83
+
84
+ export function ComposeImage({ block }: { block: ComposeImageBlock }) {
85
+ return (
86
+ <figure className="my-8">
87
+ <img
88
+ src={block.src}
89
+ alt={block.alt}
90
+ className="w-full rounded-xl border border-primary/10 shadow-sm"
91
+ />
92
+ {block.caption && (
93
+ <figcaption className="mt-2 text-center text-sm text-text-muted">{block.caption}</figcaption>
94
+ )}
95
+ </figure>
96
+ );
97
+ }
@@ -0,0 +1,115 @@
1
+ import type { ComposeHeadingBlock } from "../types";
2
+ import { AnnotatedText } from "../annotate-text";
3
+ import type { ComposeListBlock, ComposeQuoteBlock } from "../types";
4
+ import { slugify } from "@/lib/utils/slugify";
5
+
6
+ export function ComposeHeading({ block }: { block: ComposeHeadingBlock }) {
7
+ const base = "text-text-main tracking-tight";
8
+
9
+ if (block.level === "title") {
10
+ return (
11
+ <h1
12
+ className={[
13
+ base,
14
+ "text-3xl md:text-4xl font-bold font-serif mb-6",
15
+ block.variant === "accent" ? "text-primary" : "",
16
+ block.variant === "underline-bar"
17
+ ? "border-b-4 border-accent-gold pb-3"
18
+ : "",
19
+ ].join(" ")}
20
+ >
21
+ {block.text}
22
+ </h1>
23
+ );
24
+ }
25
+
26
+ if (block.level === "h3") {
27
+ return (
28
+ <h3
29
+ className={[
30
+ base,
31
+ "text-lg font-semibold mt-8 mb-3",
32
+ block.variant === "serif" ? "font-serif" : "",
33
+ block.variant === "accent" ? "text-primary" : "",
34
+ ].join(" ")}
35
+ >
36
+ {block.text}
37
+ </h3>
38
+ );
39
+ }
40
+
41
+ return (
42
+ <h2
43
+ id={slugify(block.text)}
44
+ className={[
45
+ base,
46
+ "text-2xl font-bold mt-10 mb-4",
47
+ block.variant === "serif" ? "font-serif" : "",
48
+ block.variant === "accent" ? "text-primary" : "",
49
+ block.variant === "underline-bar"
50
+ ? "border-b border-primary/20 pb-2"
51
+ : "",
52
+ ].join(" ")}
53
+ >
54
+ {block.text}
55
+ </h2>
56
+ );
57
+ }
58
+
59
+ export function ComposeExplain({
60
+ block,
61
+ }: {
62
+ block: import("../types").ComposeExplainBlock;
63
+ }) {
64
+ const variantClass =
65
+ block.variant === "lead"
66
+ ? "text-lg leading-relaxed text-text-main"
67
+ : block.variant === "muted"
68
+ ? "text-sm text-text-muted"
69
+ : "text-base leading-relaxed text-text-muted";
70
+
71
+ return (
72
+ <p className={`my-4 ${variantClass}`}>
73
+ <AnnotatedText
74
+ text={block.text}
75
+ highlight={block.highlight}
76
+ underline={block.underline}
77
+ />
78
+ </p>
79
+ );
80
+ }
81
+
82
+ export function ComposeParagraph({
83
+ block,
84
+ }: {
85
+ block: import("../types").ComposeParagraphBlock;
86
+ }) {
87
+ return <p className="my-4 text-text-muted leading-relaxed">{block.text}</p>;
88
+ }
89
+
90
+ export function ComposeList({ block }: { block: ComposeListBlock }) {
91
+ const Tag = block.style === "number" ? "ol" : "ul";
92
+ const listClass =
93
+ block.style === "number"
94
+ ? "my-4 list-decimal pl-6 space-y-2 text-text-muted"
95
+ : "my-4 list-disc pl-6 space-y-2 text-text-muted";
96
+
97
+ return (
98
+ <Tag className={listClass}>
99
+ {block.items.map((item, i) => (
100
+ <li key={i} className="leading-relaxed">{item}</li>
101
+ ))}
102
+ </Tag>
103
+ );
104
+ }
105
+
106
+ export function ComposeQuote({ block }: { block: ComposeQuoteBlock }) {
107
+ return (
108
+ <blockquote className="my-6 border-l-4 border-accent-gold pl-4 py-1">
109
+ <p className="text-lg font-serif text-text-main leading-relaxed italic">{block.text}</p>
110
+ {block.attribution && (
111
+ <footer className="mt-2 text-sm text-text-muted not-italic">— {block.attribution}</footer>
112
+ )}
113
+ </blockquote>
114
+ );
115
+ }
@@ -0,0 +1,21 @@
1
+ export {
2
+ COMPOSE_ENGINE_ID,
3
+ createEmptyComposeDocument,
4
+ isComposeDocument,
5
+ type ComposeBlock,
6
+ type ComposeBlockType,
7
+ type ComposeDocument,
8
+ type BlogPostMeta,
9
+ type BlogPostWithContent,
10
+ } from "./types";
11
+
12
+ export {
13
+ COMPOSE_BLOCK_REGISTRY,
14
+ COMPOSE_BLOCK_TYPES,
15
+ getComposeBlockDefinition,
16
+ type ComposeBlockDefinition,
17
+ type ComposeFieldDef,
18
+ } from "./registry";
19
+
20
+ export { AnnotatedText } from "./annotate-text";
21
+ export { ComposeBlogRenderer, ComposeBlockView } from "./interpreter";