create-nextblock 0.14.2 → 0.14.5

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 (68) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/[slug]/page.tsx +7 -2
  3. package/templates/nextblock-template/app/[slug]/page.utils.ts +8 -3
  4. package/templates/nextblock-template/app/actions/postActions.ts +3 -0
  5. package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -0
  6. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +83 -3
  7. package/templates/nextblock-template/app/actions/visualEditingActions.ts +34 -14
  8. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +45 -0
  9. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +736 -117
  10. package/templates/nextblock-template/app/api/view/route.ts +114 -0
  11. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +1 -2
  12. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +2 -2
  13. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +17 -22
  14. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +13 -19
  15. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +45 -34
  16. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +10 -0
  17. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -0
  18. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +528 -0
  19. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +33 -17
  20. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +19 -7
  21. package/templates/nextblock-template/app/cms/pages/actions.ts +17 -10
  22. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +7 -29
  23. package/templates/nextblock-template/app/cms/pages/page.tsx +6 -19
  24. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +42 -18
  25. package/templates/nextblock-template/app/cms/posts/actions.ts +16 -27
  26. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +3 -60
  27. package/templates/nextblock-template/app/cms/posts/page.tsx +6 -13
  28. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +63 -9
  29. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +66 -32
  30. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -285
  31. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -0
  32. package/templates/nextblock-template/app/cms/revisions/service.ts +549 -471
  33. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -132
  34. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -0
  35. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -0
  36. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -24
  37. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -0
  38. package/templates/nextblock-template/app/layout.tsx +49 -0
  39. package/templates/nextblock-template/app/lib/sitemap-utils.ts +6 -4
  40. package/templates/nextblock-template/app/lib/ucp/server.ts +4 -1
  41. package/templates/nextblock-template/app/page.tsx +6 -3
  42. package/templates/nextblock-template/app/product/[slug]/page.tsx +27 -3
  43. package/templates/nextblock-template/app/providers.tsx +16 -4
  44. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +7 -10
  45. package/templates/nextblock-template/components/theme-icon.tsx +78 -0
  46. package/templates/nextblock-template/components/theme-switcher.tsx +85 -90
  47. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +4 -1
  48. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -0
  49. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -0
  50. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +7 -2
  51. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +114 -0
  52. package/templates/nextblock-template/lib/blocks/blockColors.ts +132 -0
  53. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +17 -1
  54. package/templates/nextblock-template/lib/cms-transfer/server.ts +13 -0
  55. package/templates/nextblock-template/lib/full-backup/server.ts +1 -0
  56. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -0
  57. package/templates/nextblock-template/lib/search/server.ts +3 -0
  58. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +102 -87
  59. package/templates/nextblock-template/lib/themes/buildThemeCss.test.ts +163 -0
  60. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -0
  61. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -0
  62. package/templates/nextblock-template/lib/themes/tokens.ts +143 -0
  63. package/templates/nextblock-template/lib/visual-editing/mutations.ts +4 -1
  64. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +46 -1
  65. package/templates/nextblock-template/next-env.d.ts +2 -2
  66. package/templates/nextblock-template/package.json +1 -1
  67. package/templates/nextblock-template/scripts/verify-site-themes.ts +63 -0
  68. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,132 @@
1
+ import type { CSSProperties } from 'react';
2
+
3
+ /**
4
+ * Text-colour model shared by the block renderers and the CMS previews.
5
+ *
6
+ * A colour is stored as EITHER a theme token keyword (`"primary"`) — which keeps
7
+ * following the active theme and dark mode — OR a literal CSS colour emitted by
8
+ * the colour picker (`"#FF8800"`, `"rgba(...)"`, `"hsl(...)"`). Tokens render as
9
+ * a static Tailwind class; literals render as an inline style.
10
+ *
11
+ * Keep this the single source of truth: HeadingBlockRenderer, EditableBlock and
12
+ * ColumnEditor previously each carried their own map and had drifted apart
13
+ * (`muted` rendered as the near-invisible `text-muted` on the live site while
14
+ * both previews used `text-muted-foreground`).
15
+ */
16
+
17
+ export const TEXT_COLOR_TOKENS = [
18
+ 'foreground',
19
+ 'primary',
20
+ 'secondary',
21
+ 'accent',
22
+ 'muted',
23
+ 'destructive',
24
+ 'background',
25
+ ] as const;
26
+
27
+ export type TextColorToken = (typeof TEXT_COLOR_TOKENS)[number];
28
+
29
+ /**
30
+ * Token -> Tailwind class.
31
+ *
32
+ * These must be written as complete literals, never interpolated. Tailwind v4
33
+ * ignores the `safelist` key in the legacy JS config (libs/ui/tailwind.config.js
34
+ * still carries one, but v4 never reads it) — a class exists only if the scanner
35
+ * finds the whole string in a source file. Listing them here is what guarantees
36
+ * they are emitted.
37
+ */
38
+ export const TEXT_COLOR_CLASSES: Record<TextColorToken, string> = {
39
+ foreground: 'text-foreground',
40
+ primary: 'text-primary',
41
+ secondary: 'text-secondary',
42
+ accent: 'text-accent',
43
+ // `--muted` is a near-white surface colour; the readable pair is its foreground.
44
+ muted: 'text-muted-foreground',
45
+ destructive: 'text-destructive',
46
+ background: 'text-background',
47
+ };
48
+
49
+ /** Token -> CSS colour, for painting swatches in the editor. */
50
+ export const TEXT_COLOR_SWATCHES: Record<TextColorToken, string> = {
51
+ foreground: 'hsl(var(--foreground))',
52
+ primary: 'hsl(var(--primary))',
53
+ secondary: 'hsl(var(--secondary))',
54
+ accent: 'hsl(var(--accent))',
55
+ muted: 'hsl(var(--muted-foreground))',
56
+ destructive: 'hsl(var(--destructive))',
57
+ background: 'hsl(var(--background))',
58
+ };
59
+
60
+ export const TEXT_COLOR_LABELS: Record<TextColorToken, string> = {
61
+ foreground: 'Default text',
62
+ primary: 'Primary',
63
+ secondary: 'Secondary',
64
+ accent: 'Accent',
65
+ muted: 'Muted',
66
+ destructive: 'Destructive',
67
+ background: 'Background',
68
+ };
69
+
70
+ /** Token options in the shape `ColorField` expects. */
71
+ export const TEXT_COLOR_TOKEN_OPTIONS = TEXT_COLOR_TOKENS.map((token) => ({
72
+ value: token,
73
+ label: TEXT_COLOR_LABELS[token],
74
+ cssColor: TEXT_COLOR_SWATCHES[token],
75
+ }));
76
+
77
+ /**
78
+ * Accepts what the colour picker can emit. Deliberately permissive on the inner
79
+ * grammar — the picker only produces well-formed values, and being strict here
80
+ * would reject legitimate colours authored by hand or by Cortex AI.
81
+ */
82
+ export const CSS_COLOR_PATTERN =
83
+ /^(#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})|rgba?\([^)]+\)|hsla?\([^)]+\))$/i;
84
+
85
+ export function isTextColorToken(value: unknown): value is TextColorToken {
86
+ return typeof value === 'string' && (TEXT_COLOR_TOKENS as readonly string[]).includes(value);
87
+ }
88
+
89
+ export function isCustomCssColor(value: unknown): value is string {
90
+ return typeof value === 'string' && CSS_COLOR_PATTERN.test(value.trim());
91
+ }
92
+
93
+ export interface ResolvedTextColor {
94
+ className?: string;
95
+ style?: CSSProperties;
96
+ }
97
+
98
+ /**
99
+ * Turn a stored colour into render props. Unknown values resolve to `{}` so a
100
+ * stale or hand-edited value degrades to the inherited colour rather than
101
+ * emitting a bogus Tailwind class.
102
+ */
103
+ export function resolveTextColor(value: string | null | undefined): ResolvedTextColor {
104
+ if (!value) return {};
105
+ const trimmed = value.trim();
106
+ if (isTextColorToken(trimmed)) {
107
+ return { className: TEXT_COLOR_CLASSES[trimmed] };
108
+ }
109
+ if (isCustomCssColor(trimmed)) {
110
+ return { style: { color: trimmed } };
111
+ }
112
+ return {};
113
+ }
114
+
115
+ /**
116
+ * Text alignment, also as literals. `text-justify` currently only survives
117
+ * because the string happens to appear in a regex in libs/editor/src/lib/kit.ts;
118
+ * pinning it here removes that accidental dependency.
119
+ */
120
+ export const TEXT_ALIGN_CLASSES = {
121
+ left: 'text-left',
122
+ center: 'text-center',
123
+ right: 'text-right',
124
+ justify: 'text-justify',
125
+ } as const;
126
+
127
+ export type TextAlign = keyof typeof TEXT_ALIGN_CLASSES;
128
+
129
+ export function resolveTextAlign(value: string | null | undefined): string | undefined {
130
+ if (!value) return undefined;
131
+ return TEXT_ALIGN_CLASSES[value as TextAlign];
132
+ }
@@ -2,6 +2,7 @@ import { z } from '../zod-config';
2
2
  import { TestimonialBlockConfig, TestimonialBlockContent } from '../../components/blocks/TestimonialBlock';
3
3
  import { ProductGridBlockSchema, ProductGridBlockContent, FeaturedProductBlockSchema, FeaturedProductBlockContent, CartBlockSchema, CartBlockContent, CheckoutBlockSchema, CheckoutBlockContent, ProductDetailsBlockSchema, ProductDetailsBlockContent } from './ecommerce-block-schemas';
4
4
  import { availableBlockTypes, type BlockType } from './blockTypes';
5
+ import { CSS_COLOR_PATTERN, TEXT_COLOR_TOKENS } from './blockColors';
5
6
  export { availableBlockTypes, type BlockType } from './blockTypes';
6
7
 
7
8
  /**
@@ -19,11 +20,26 @@ export const TextBlockSchema = z.object({
19
20
  });
20
21
  export type TextBlockContent = z.infer<typeof TextBlockSchema>;
21
22
 
23
+ /**
24
+ * A text colour is either a theme token (keeps following the theme and dark
25
+ * mode) or a literal CSS colour from the colour picker. Existing rows only ever
26
+ * hold tokens, so widening the union is backward compatible.
27
+ * See `./blockColors` for the token -> class mapping used at render time.
28
+ */
29
+ export const TextColorSchema = z
30
+ .union([
31
+ z.enum(TEXT_COLOR_TOKENS),
32
+ z.string().regex(CSS_COLOR_PATTERN, 'Must be a hex, rgb(a) or hsl(a) colour'),
33
+ ])
34
+ .describe(
35
+ `A theme token (${TEXT_COLOR_TOKENS.join(', ')}) or a custom CSS colour such as "#FF8800", "rgba(255,136,0,0.8)" or "hsl(28, 100%, 50%)"`,
36
+ );
37
+
22
38
  export const HeadingBlockSchema = z.object({
23
39
  level: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5), z.literal(6)]).describe('Heading level (1-6)'),
24
40
  text_content: z.string().describe('The text content of the heading'),
25
41
  textAlign: z.enum(['left', 'center', 'right', 'justify']).optional().describe('Text alignment'),
26
- textColor: z.enum(['primary', 'secondary', 'accent', 'muted', 'destructive', 'background']).optional().describe('Color of the heading text'),
42
+ textColor: TextColorSchema.optional().describe('Color of the heading text'),
27
43
  });
28
44
  export type HeadingBlockContent = z.infer<typeof HeadingBlockSchema>;
29
45
 
@@ -22,10 +22,12 @@ import {
22
22
  import {
23
23
  createPageRevision,
24
24
  createPostRevision,
25
+ createProductRevision,
25
26
  } from "../../app/cms/revisions/service";
26
27
  import {
27
28
  getFullPageContent,
28
29
  getFullPostContent,
30
+ getFullProductContent,
29
31
  type FullPageContent,
30
32
  type FullPostContent,
31
33
  } from "../../app/cms/revisions/utils";
@@ -1385,6 +1387,12 @@ async function applyProductImport(params: {
1385
1387
  throw new Error(`Failed to save product draft from row ${item.rowNumber}: ${error.message}`);
1386
1388
  }
1387
1389
  } else {
1390
+ // Live mode writes the product row and its blocks directly, so it has to record a
1391
+ // revision itself — exactly as applyContentImport does for pages and posts. Without
1392
+ // this the content changes underneath the revision chain, and the next diff taken
1393
+ // against it would replay onto a document that no longer matches.
1394
+ const previousContent = await getFullProductContent(productId);
1395
+
1388
1396
  const { error } = await auth.supabase
1389
1397
  .from("products")
1390
1398
  .update(toLiveProductPayload(item.meta) as any)
@@ -1407,6 +1415,11 @@ async function applyProductImport(params: {
1407
1415
  await syncCategoriesForTranslationGroup(auth.supabase as any, productId, item.categoryIds);
1408
1416
  }
1409
1417
  await (auth.supabase as any).from("product_drafts").delete().eq("product_id", productId);
1418
+
1419
+ const nextContent = await getFullProductContent(productId);
1420
+ if (previousContent && nextContent) {
1421
+ await createProductRevision(productId, auth.userId, previousContent, nextContent);
1422
+ }
1410
1423
  }
1411
1424
 
1412
1425
  revalidatePath("/cms/products");
@@ -638,6 +638,7 @@ async function rewriteMediaBaseUrls(params: {
638
638
  { table: "products", idColumn: "id", columns: ["description_json", "metadata"] },
639
639
  { table: "page_revisions", idColumn: "id", columns: ["content"] },
640
640
  { table: "post_revisions", idColumn: "id", columns: ["content"] },
641
+ { table: "product_revisions", idColumn: "id", columns: ["content"] },
641
642
  { table: "site_settings", idColumn: "key", columns: ["value"] },
642
643
  { table: "translations", idColumn: "key", columns: ["translations"] },
643
644
  ];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Build a link to the `/api/view` entry point used by the CMS "Preview" and
3
+ * "View Live" buttons.
4
+ *
5
+ * Always pass the content's own language. The public site resolves language from
6
+ * a cookie, not the URL, so a link without `lang` renders in whatever language
7
+ * the editor happens to be browsing in — which is how opening a French page
8
+ * landed you on the English one. See `app/api/view/route.ts`.
9
+ */
10
+ export function buildViewUrl(options: {
11
+ /** Root-relative public path, e.g. "/about" or "/article/hello". */
12
+ path: string;
13
+ /** The content row's language code, e.g. "fr". */
14
+ languageCode?: string | null;
15
+ /** True to enter Live Draft Mode (preview unpublished content). */
16
+ draft?: boolean;
17
+ }): string {
18
+ const params = new URLSearchParams({ path: options.path });
19
+ if (options.languageCode) {
20
+ params.set("lang", options.languageCode);
21
+ }
22
+ if (options.draft) {
23
+ params.set("draft", "1");
24
+ }
25
+ return `/api/view?${params.toString()}`;
26
+ }
@@ -1,6 +1,7 @@
1
1
  import 'server-only';
2
2
 
3
3
  import { getSsgSupabaseClient, verifyPackageOnline } from '@nextblock-cms/db/server';
4
+ import { buildPublishedAtOrFilter } from '@nextblock-cms/utils';
4
5
  import { resolveMediaUrl } from '../media/resolveMediaUrl';
5
6
  import { getHomepageTranslationGroupId } from '../../app/lib/homepage';
6
7
  import type {
@@ -348,6 +349,7 @@ async function fetchPages(languageId: number | null): Promise<SearchCandidate[]>
348
349
  `
349
350
  )
350
351
  .eq('status', 'published')
352
+ .or(buildPublishedAtOrFilter())
351
353
  .order('updated_at', { ascending: false })
352
354
  .limit(CANDIDATE_LIMIT);
353
355
 
@@ -487,6 +489,7 @@ async function fetchProducts(languageId: number | null): Promise<SearchCandidate
487
489
  `
488
490
  )
489
491
  .eq('status', 'active')
492
+ .or(buildPublishedAtOrFilter())
490
493
  .order('updated_at', { ascending: false })
491
494
  .limit(CANDIDATE_LIMIT);
492
495