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.
- package/package.json +1 -1
- package/templates/nextblock-template/app/[slug]/page.tsx +7 -2
- package/templates/nextblock-template/app/[slug]/page.utils.ts +8 -3
- package/templates/nextblock-template/app/actions/postActions.ts +3 -0
- package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -0
- package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +83 -3
- package/templates/nextblock-template/app/actions/visualEditingActions.ts +34 -14
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +45 -0
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +736 -117
- package/templates/nextblock-template/app/api/view/route.ts +114 -0
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +1 -2
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +2 -2
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +17 -22
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +13 -19
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +45 -34
- package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +10 -0
- package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -0
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +528 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +33 -17
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +19 -7
- package/templates/nextblock-template/app/cms/pages/actions.ts +17 -10
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +7 -29
- package/templates/nextblock-template/app/cms/pages/page.tsx +6 -19
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +42 -18
- package/templates/nextblock-template/app/cms/posts/actions.ts +16 -27
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +3 -60
- package/templates/nextblock-template/app/cms/posts/page.tsx +6 -13
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +63 -9
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +66 -32
- package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -285
- package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +549 -471
- package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -132
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -0
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -0
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -24
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -0
- package/templates/nextblock-template/app/layout.tsx +49 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +6 -4
- package/templates/nextblock-template/app/lib/ucp/server.ts +4 -1
- package/templates/nextblock-template/app/page.tsx +6 -3
- package/templates/nextblock-template/app/product/[slug]/page.tsx +27 -3
- package/templates/nextblock-template/app/providers.tsx +16 -4
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +7 -10
- package/templates/nextblock-template/components/theme-icon.tsx +78 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +85 -90
- package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +4 -1
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -0
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -0
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +7 -2
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +114 -0
- package/templates/nextblock-template/lib/blocks/blockColors.ts +132 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +17 -1
- package/templates/nextblock-template/lib/cms-transfer/server.ts +13 -0
- package/templates/nextblock-template/lib/full-backup/server.ts +1 -0
- package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -0
- package/templates/nextblock-template/lib/search/server.ts +3 -0
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +102 -87
- package/templates/nextblock-template/lib/themes/buildThemeCss.test.ts +163 -0
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -0
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -0
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -0
- package/templates/nextblock-template/lib/visual-editing/mutations.ts +4 -1
- package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +46 -1
- package/templates/nextblock-template/next-env.d.ts +2 -2
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/scripts/verify-site-themes.ts +63 -0
- 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:
|
|
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
|
|