@ecomcx/nexus-theme-sdk 0.0.0-stage → 1.24.1

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 (52) hide show
  1. package/dist/api.d.ts +92 -0
  2. package/dist/api.js +149 -0
  3. package/dist/auth-forms.d.ts +83 -0
  4. package/dist/auth-forms.js +206 -0
  5. package/dist/auth-pages.d.ts +6 -0
  6. package/dist/auth-pages.js +33 -0
  7. package/dist/auth-shared.d.ts +14 -0
  8. package/dist/auth-shared.js +7 -0
  9. package/dist/auth.d.ts +16 -0
  10. package/dist/auth.js +48 -0
  11. package/dist/client.d.ts +102 -0
  12. package/dist/client.js +191 -0
  13. package/dist/components.d.ts +68 -0
  14. package/dist/components.js +154 -0
  15. package/dist/content.d.ts +25 -0
  16. package/dist/content.js +59 -0
  17. package/dist/dynamic.d.ts +9 -0
  18. package/dist/dynamic.js +128 -0
  19. package/dist/i18n.d.ts +11 -0
  20. package/dist/i18n.js +104 -0
  21. package/dist/image.d.ts +24 -0
  22. package/dist/image.js +37 -0
  23. package/dist/index.d.ts +19 -0
  24. package/dist/index.js +21 -0
  25. package/dist/locale.d.ts +9 -0
  26. package/dist/locale.js +20 -0
  27. package/dist/metadata.d.ts +3 -0
  28. package/dist/metadata.js +26 -0
  29. package/dist/page-canvas.d.ts +4 -0
  30. package/dist/page-canvas.js +7 -0
  31. package/dist/page-full-width.d.ts +3 -0
  32. package/dist/page-full-width.js +6 -0
  33. package/dist/plugin-form-client.d.ts +27 -0
  34. package/dist/plugin-form-client.js +55 -0
  35. package/dist/plugin-form.d.ts +15 -0
  36. package/dist/plugin-form.js +25 -0
  37. package/dist/preferences.d.ts +9 -0
  38. package/dist/preferences.js +17 -0
  39. package/dist/rich.d.ts +66 -0
  40. package/dist/rich.js +388 -0
  41. package/dist/runtime.d.ts +58 -0
  42. package/dist/runtime.js +238 -0
  43. package/dist/social-icons.d.ts +5 -0
  44. package/dist/social-icons.js +21 -0
  45. package/dist/template-blocks.d.ts +3 -0
  46. package/dist/template-blocks.js +11 -0
  47. package/dist/types.d.ts +475 -0
  48. package/dist/types.js +1 -0
  49. package/dist/utils.d.ts +8 -0
  50. package/dist/utils.js +29 -0
  51. package/package.json +62 -4
  52. package/README.md +0 -3
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { ExtensionBoundary, PostPasswordForm } from "./client.js";
3
+ import { applyFilters, findBlock, findRegion, findShortcode, getActiveExtensions } from "./runtime.js";
4
+ import { getPattern, getSite, getTemplates } from "./api.js";
5
+ import { getT } from "./i18n.js";
6
+ import { renderDynamic } from "./dynamic.js";
7
+ import { renderContent } from "./rich.js";
8
+ import { cx } from "./utils.js";
9
+ /**
10
+ * Render nội dung bài viết (định dạng của CMS — @ecomcx/nexus-content; phần vẽ ở rich.tsx, không phụ thuộc trình soạn).
11
+ * Luôn có class `ecx-content` (giao diện nền trong @ecomcx/nexus-content/style.css, theme ghi đè được); `className` thay cho `entry-content`.
12
+ */
13
+ export async function PostContent({ post, content, className }) {
14
+ const site = await getSite();
15
+ const doc = content ?? post?.content;
16
+ if (!content && post?.passwordProtected && !post.content) {
17
+ const t = await getT();
18
+ return _jsx("div", { className: cx(className ?? "entry-content", "ecx-content"), children: _jsx(PostPasswordForm, { postId: post.id, labels: { intro: t("password.intro"), label: t("password.label"), submit: t("password.submit"), wrong: t("password.wrong") } }) });
19
+ }
20
+ if (!doc?.blocks?.length)
21
+ return null;
22
+ const rendered = await renderContent(doc, await contentContext(site, post), (blocks) => applyFilters("post.blocks", blocks, { site, post }, site));
23
+ return _jsx("div", { className: cx(className ?? "entry-content", "ecx-content"), children: rendered });
24
+ }
25
+ /**
26
+ * Vùng của theme: header, footer (vùng chuẩn của core — nên luôn bọc) và vùng hybrid khai báo ở theme.json › regions.
27
+ * Thứ tự: plugin thay vùng (definePlugin → regions) → bản sửa trong Trình dựng / `parts/{name}.json` → `children` (bản code của theme).
28
+ * `<EditableRegion name="header" data={data}><Header /></EditableRegion>`
29
+ */
30
+ export async function EditableRegion({ name, data, className, children }) {
31
+ const doc = (await getTemplates().catch(() => null))?.parts[name];
32
+ const site = await getSite();
33
+ const own = doc?.blocks?.length
34
+ ? _jsx("div", { className: cx(`ecx-region ecx-region-${name}`, "ecx-content", className), children: await renderContent(doc, await contentContext(site, data?.post, data)) })
35
+ : _jsx(_Fragment, { children: children });
36
+ return _jsx(_Fragment, { children: await regionOverride(name, own, site, data) });
37
+ }
38
+ /** Plugin thay vùng: component nhận bản sẽ vẽ (`children`), lỗi chỉ ẩn vùng đó. */
39
+ async function regionOverride(name, fallback, site, data) {
40
+ const Region = await findRegion(name, site);
41
+ if (!Region)
42
+ return fallback;
43
+ return _jsx(ExtensionBoundary, { name: `Region ${name}`, children: _jsx(Region, { name: name, site: site, data: data ?? null, children: fallback }) });
44
+ }
45
+ /** Ngữ cảnh vẽ nội dung dùng chung cho bài viết và template khối. */
46
+ export async function contentContext(site, post, data) {
47
+ const t = await getT();
48
+ const blocks = (await getActiveExtensions(site)).flatMap((d) => d.blockTypes ?? []);
49
+ return {
50
+ site, post, data, blocks,
51
+ resolve: { shortcode: (name) => findShortcode(name, site), block: (type) => findBlock(type, site) },
52
+ labels: { download: t("file.download"), toc: t("toc.title") },
53
+ boundary: (name, key, el) => _jsx(ExtensionBoundary, { name: name, children: el }, key),
54
+ dynamic: renderDynamic,
55
+ parts: async (slug) => (await getTemplates().catch(() => null))?.parts[slug] ?? null,
56
+ region: (name, fallback) => regionOverride(name, fallback, site, data),
57
+ pattern: (ref) => getPattern(ref),
58
+ };
59
+ }
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from "react";
2
+ import { type RenderContext, type RenderNode } from "./rich.js";
3
+ type Children = (ctx: RenderContext, nodes?: RenderNode[]) => Promise<ReactNode[]>;
4
+ /**
5
+ * Khối động (@ecomcx/nexus-content DYNAMIC_NODES): vẽ theo ngữ cảnh — website, trang đang xem (`ctx.data`), bài trong vòng lặp (`ctx.post`).
6
+ * Ngoài template (nội dung bài) không có `ctx.data`: `postContent` không vẽ (tránh lặp vô hạn), `query` luôn tự lấy bài.
7
+ */
8
+ export declare function renderDynamic(node: RenderNode, key: string, ctx: RenderContext, children: Children): Promise<ReactNode>;
9
+ export {};
@@ -0,0 +1,128 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Fragment } from "react";
3
+ import { getPosts } from "./api.js";
4
+ import { Link } from "./client.js";
5
+ import { Comments, FeaturedImage, LanguageSwitcher, Menu, Pagination, PostDate, SearchForm, TermLinks } from "./components.js";
6
+ import { getT } from "./i18n.js";
7
+ import { localeHref } from "./locale.js";
8
+ import { renderContent } from "./rich.js";
9
+ import { cx } from "./utils.js";
10
+ const align = (a) => (a.textAlign && a.textAlign !== "left" ? { textAlign: a.textAlign } : undefined);
11
+ const ASPECT = { "16/9": "16 / 9", "4/3": "4 / 3", "1/1": "1 / 1", "3/4": "3 / 4" };
12
+ /**
13
+ * Khối động (@ecomcx/nexus-content DYNAMIC_NODES): vẽ theo ngữ cảnh — website, trang đang xem (`ctx.data`), bài trong vòng lặp (`ctx.post`).
14
+ * Ngoài template (nội dung bài) không có `ctx.data`: `postContent` không vẽ (tránh lặp vô hạn), `query` luôn tự lấy bài.
15
+ */
16
+ export async function renderDynamic(node, key, ctx, children) {
17
+ const a = node.attrs ?? {};
18
+ const site = ctx.site;
19
+ const post = ctx.post ?? ctx.data?.post;
20
+ switch (node.type) {
21
+ case "siteTitle": {
22
+ const Tag = (a.level > 0 ? `h${Math.min(6, a.level)}` : "p");
23
+ const title = a.link === false ? site.title : _jsx(Link, { href: localeHref(site, "/"), rel: "home", children: site.title });
24
+ return _jsx(Tag, { className: "ecx-site-title", style: align(a), children: title }, key);
25
+ }
26
+ case "siteLogo": {
27
+ const logo = site.themeSettings?.logo;
28
+ if (!logo?.url)
29
+ return null;
30
+ const img = _jsx("img", { src: logo.url, alt: site.title, width: a.width ?? 120, style: { height: "auto" } });
31
+ return _jsx("div", { className: "ecx-site-logo", children: a.link === false ? img : _jsx(Link, { href: localeHref(site, "/"), rel: "home", children: img }) }, key);
32
+ }
33
+ case "siteTagline":
34
+ return site.tagline ? _jsx("p", { className: "ecx-site-tagline", style: align(a), children: site.tagline }, key) : null;
35
+ case "navigation": {
36
+ const location = String(a.location ?? "primary");
37
+ const menu = _jsx(Menu, { location: location, className: "ecx-navigation-menu" });
38
+ const t = await getT();
39
+ return (_jsxs("div", { className: cx("ecx-navigation", `ecx-navigation-${a.orientation ?? "horizontal"}`, a.mobile !== false && "has-mobile"), children: [_jsx("div", { className: "ecx-navigation-items", children: menu }), a.mobile !== false && _jsxs("details", { className: "ecx-navigation-toggle", children: [_jsx("summary", { "aria-label": t("navigation.menu"), children: "\u2630" }), menu] })] }, key));
40
+ }
41
+ case "search":
42
+ return _jsx("div", { className: "ecx-search", children: _jsx(SearchForm, { defaultValue: ctx.data?.search, placeholder: a.placeholder, buttonLabel: a.buttonText }) }, key);
43
+ case "languageSwitcher":
44
+ return _jsx(LanguageSwitcher, { className: "ecx-language-switcher" }, key);
45
+ case "copyright":
46
+ return _jsxs("p", { className: "ecx-copyright", style: align(a), children: ["\u00A9 ", new Date().getFullYear(), " ", a.text || site.title] }, key);
47
+ case "postTitle": {
48
+ if (!post)
49
+ return null;
50
+ const Tag = `h${Math.min(6, Math.max(1, a.level ?? 1))}`;
51
+ return _jsx(Tag, { className: "ecx-post-title", style: align(a), children: a.link ? _jsx(Link, { href: post.link, children: post.title }) : post.title }, key);
52
+ }
53
+ case "postContent": {
54
+ if (!post || !ctx.data)
55
+ return null;
56
+ const { PostContent } = await import("./content.js");
57
+ return _jsx(PostContent, { post: post }, key);
58
+ }
59
+ case "postExcerpt":
60
+ if (!post?.excerpt)
61
+ return null;
62
+ return _jsxs("p", { className: "ecx-post-excerpt", children: [post.excerpt, a.moreText && _jsxs(_Fragment, { children: [" ", _jsx(Link, { href: post.link, className: "ecx-more-link", children: a.moreText })] })] }, key);
63
+ case "featuredImage": {
64
+ if (!post?.featuredMedia)
65
+ return null;
66
+ const ratio = ASPECT[a.aspect];
67
+ return _jsx("div", { className: "ecx-featured-image", style: ratio ? { aspectRatio: ratio, overflow: "hidden" } : undefined, children: _jsx(FeaturedImage, { media: post.featuredMedia, size: a.size ?? "large", link: a.link ? post.link : undefined }) }, key);
68
+ }
69
+ case "postDate":
70
+ return post ? _jsx("p", { className: "ecx-post-date", style: align(a), children: _jsx(PostDate, { post: post, site: site }) }, key) : null;
71
+ case "postAuthor":
72
+ if (!post?.author)
73
+ return null;
74
+ return _jsxs("p", { className: "ecx-post-author", children: [a.avatar && post.author.avatarUrl && _jsx("img", { src: post.author.avatarUrl, alt: "", width: 32, height: 32, style: { borderRadius: "50%", verticalAlign: "middle", marginRight: 8 } }), _jsx(Link, { href: localeHref(site, `/author/${post.author.slug}`), rel: "author", children: post.author.name })] }, key);
75
+ case "postTerms": {
76
+ const terms = post?.terms?.[a.taxonomy ?? "category"];
77
+ return terms?.length ? _jsx("p", { className: "ecx-post-terms", children: _jsx(TermLinks, { terms: terms, separator: a.separator ?? ", " }) }, key) : null;
78
+ }
79
+ case "comments":
80
+ return post && ctx.data?.post?.id === post.id ? _jsx(Comments, { post: post }, key) : null;
81
+ case "query": {
82
+ let posts;
83
+ let data;
84
+ if (a.inherit !== false && ctx.data?.posts) {
85
+ posts = ctx.data.posts;
86
+ data = ctx.data;
87
+ }
88
+ else {
89
+ const taxonomy = a.category ? "category" : a.tag ? "post_tag" : undefined;
90
+ const res = await getPosts({ type: a.postType ?? "post", perPage: a.perPage ?? 10, taxonomy, term: a.category || a.tag || undefined,
91
+ orderBy: a.orderBy ?? "date", order: a.order ?? "desc" }).catch(() => null);
92
+ posts = res?.items ?? [];
93
+ }
94
+ return _jsx("div", { className: "ecx-query", children: await children({ ...ctx, query: { posts, data } }) }, key);
95
+ }
96
+ case "postTemplate": {
97
+ const posts = ctx.query?.posts ?? [];
98
+ if (!posts.length)
99
+ return null;
100
+ const grid = a.layout === "grid";
101
+ const style = grid ? { display: "grid", gap: "1.5rem", gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${Math.floor(60 / (a.columns ?? 3))}rem), 1fr))`, listStyle: "none", padding: 0 } : { listStyle: "none", padding: 0 };
102
+ const items = await Promise.all(posts.map(async (p) => _jsx("li", { className: "ecx-post", children: await children({ ...ctx, post: p }) }, p.id)));
103
+ return _jsx("ul", { className: cx("ecx-post-template", grid && "is-grid"), style: style, children: items }, key);
104
+ }
105
+ case "pagination":
106
+ return ctx.query?.data ? _jsx(Pagination, { data: ctx.query.data }, key) : null;
107
+ case "queryEmpty":
108
+ return ctx.query && !ctx.query.posts.length ? _jsx("div", { className: "ecx-query-empty", children: await children(ctx) }, key) : null;
109
+ case "syncedPattern": {
110
+ if (typeof a.ref !== "number" || !ctx.pattern || (ctx.partDepth ?? 0) >= 4)
111
+ return null;
112
+ const doc = await ctx.pattern(a.ref);
113
+ if (!doc)
114
+ return null;
115
+ return _jsx(Fragment, { children: await renderContent(doc, { ...ctx, partDepth: (ctx.partDepth ?? 0) + 1 }) }, key);
116
+ }
117
+ case "templatePart": {
118
+ if (!a.slug || !ctx.parts || (ctx.partDepth ?? 0) >= 4)
119
+ return null;
120
+ const doc = await ctx.parts(String(a.slug));
121
+ const Tag = (["header", "footer", "aside", "section"].includes(a.tagName) ? a.tagName : "div");
122
+ const part = doc ? _jsx(Tag, { className: cx("ecx-template-part", `ecx-template-part-${a.slug}`), children: await renderContent(doc, { ...ctx, partDepth: (ctx.partDepth ?? 0) + 1 }) }) : null;
123
+ const el = ctx.region ? await ctx.region(String(a.slug), part) : part;
124
+ return el == null ? null : _jsx(Fragment, { children: el }, key);
125
+ }
126
+ }
127
+ return null;
128
+ }
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** Chữ mặc định của các component trong SDK (theme có thể ghi đè trong locales/{code}.json). */
2
+ export declare const BUILTIN: Record<string, Record<string, string>>;
3
+ export type Translator = ((key: string, vars?: Record<string, string | number>) => string) & {
4
+ locale: string;
5
+ };
6
+ /**
7
+ * Hàm dịch chữ cho ngôn ngữ hiện tại. Thông điệp theo ICU MessageFormat: biến `{name}`, số nhiều
8
+ * `{count, plural, one {# comment} other {# comments}}`, chọn `{gender, select, …}`.
9
+ * Thứ tự tra: locales của theme (con → cha) → chữ có sẵn của SDK; ngôn ngữ hiện tại → mặc định → tiếng Anh → chính key.
10
+ */
11
+ export declare const getT: () => Promise<Translator>;
package/dist/i18n.js ADDED
@@ -0,0 +1,104 @@
1
+ import { IntlMessageFormat } from "intl-messageformat";
2
+ import { cache } from "react";
3
+ import { getSite } from "./api.js";
4
+ import { getActiveTheme } from "./runtime.js";
5
+ /** Chữ mặc định của các component trong SDK (theme có thể ghi đè trong locales/{code}.json). */
6
+ export const BUILTIN = {
7
+ vi: {
8
+ "search.placeholder": "Tìm kiếm…", "search.button": "Tìm", "search.label": "Từ khoá", "navigation.menu": "Menu", "toc.title": "Mục lục",
9
+ "pagination.prev": "« Trước", "pagination.next": "Sau »", "pagination.label": "Phân trang",
10
+ "comments.title": "Bình luận", "comments.closed": "Bình luận đã đóng.",
11
+ "comments.form.title": "Để lại bình luận", "comments.form.content": "Bình luận", "comments.form.name": "Tên",
12
+ "comments.form.email": "Email", "comments.form.website": "Website", "comments.form.submit": "Gửi bình luận",
13
+ "comments.form.sending": "Đang gửi…", "comments.form.approved": "Cảm ơn bạn! Bình luận đã được đăng (tải lại trang để xem).",
14
+ "comments.form.pending": "Cảm ơn bạn! Bình luận đang chờ duyệt.", "comments.form.error": "Không gửi được bình luận",
15
+ "comments.more": "Xem thêm bình luận", "comments.loading": "Đang tải…",
16
+ "password.intro": "Nội dung này được bảo vệ bằng mật khẩu. Nhập mật khẩu để xem:", "password.label": "Mật khẩu",
17
+ "password.submit": "Xem nội dung", "password.wrong": "Mật khẩu không đúng",
18
+ "archives.month": "Tháng {month}/{year}", "language.label": "Ngôn ngữ",
19
+ "auth.login": "Tên đăng nhập hoặc email", "auth.password": "Mật khẩu", "auth.submitLogin": "Đăng nhập", "auth.code": "Mã xác thực (6 số)",
20
+ "auth.verify": "Xác nhận", "auth.userName": "Tên đăng nhập", "auth.email": "Email", "auth.displayName": "Tên hiển thị",
21
+ "auth.submitRegister": "Tạo tài khoản", "auth.submitForgot": "Gửi liên kết đặt lại", "auth.newPassword": "Mật khẩu mới (tối thiểu 10 ký tự)",
22
+ "auth.submitReset": "Đặt mật khẩu", "auth.sending": "Đang xử lý…", "auth.error": "Có lỗi xảy ra, vui lòng thử lại", "auth.logout": "Đăng xuất",
23
+ "auth.noAccount": "Chưa có tài khoản?", "auth.register": "Đăng ký", "auth.forgot": "Quên mật khẩu?", "auth.haveAccount": "Đã có tài khoản?",
24
+ "auth.login2": "Đăng nhập", "auth.registerClosed": "Website hiện không mở đăng ký thành viên.",
25
+ "auth.missingToken": "Liên kết không hợp lệ — hãy yêu cầu đặt lại mật khẩu lần nữa.", "auth.signedInAs": "Bạn đang đăng nhập với tên {name}.",
26
+ "file.download": "Tải tệp", "form.submit": "Gửi", "form.sending": "Đang gửi…", "form.error": "Không gửi được, vui lòng thử lại",
27
+ },
28
+ en: {
29
+ "search.placeholder": "Search…", "search.button": "Search", "search.label": "Keyword", "navigation.menu": "Menu", "toc.title": "Contents",
30
+ "pagination.prev": "« Previous", "pagination.next": "Next »", "pagination.label": "Pagination",
31
+ "comments.title": "Comments", "comments.closed": "Comments are closed.",
32
+ "comments.form.title": "Leave a comment", "comments.form.content": "Comment", "comments.form.name": "Name",
33
+ "comments.form.email": "Email", "comments.form.website": "Website", "comments.form.submit": "Post comment",
34
+ "comments.form.sending": "Sending…", "comments.form.approved": "Thanks! Your comment is published (reload to see it).",
35
+ "comments.form.pending": "Thanks! Your comment is awaiting moderation.", "comments.form.error": "Could not post your comment",
36
+ "comments.more": "Load more comments", "comments.loading": "Loading…",
37
+ "password.intro": "This content is password protected. Enter the password to view it:", "password.label": "Password",
38
+ "password.submit": "Enter", "password.wrong": "Incorrect password",
39
+ "archives.month": "{month}/{year}", "language.label": "Language",
40
+ "auth.login": "Username or email", "auth.password": "Password", "auth.submitLogin": "Log in", "auth.code": "Authentication code (6 digits)",
41
+ "auth.verify": "Verify", "auth.userName": "Username", "auth.email": "Email", "auth.displayName": "Display name",
42
+ "auth.submitRegister": "Create account", "auth.submitForgot": "Send reset link", "auth.newPassword": "New password (at least 10 characters)",
43
+ "auth.submitReset": "Set password", "auth.sending": "Working…", "auth.error": "Something went wrong, please try again", "auth.logout": "Log out",
44
+ "auth.noAccount": "No account yet?", "auth.register": "Register", "auth.forgot": "Forgot password?", "auth.haveAccount": "Already have an account?",
45
+ "auth.login2": "Log in", "auth.registerClosed": "Registration is currently closed.",
46
+ "auth.missingToken": "Invalid link — please request a new password reset.", "auth.signedInAs": "You are logged in as {name}.",
47
+ "file.download": "Download", "form.submit": "Send", "form.sending": "Sending…", "form.error": "Could not send, please try again",
48
+ },
49
+ };
50
+ /**
51
+ * Hàm dịch chữ cho ngôn ngữ hiện tại. Thông điệp theo ICU MessageFormat: biến `{name}`, số nhiều
52
+ * `{count, plural, one {# comment} other {# comments}}`, chọn `{gender, select, …}`.
53
+ * Thứ tự tra: locales của theme (con → cha) → chữ có sẵn của SDK; ngôn ngữ hiện tại → mặc định → tiếng Anh → chính key.
54
+ */
55
+ export const getT = cache(async () => {
56
+ const site = await getSite();
57
+ const theme = await getActiveTheme(site);
58
+ const order = [...new Set([site.locale, site.defaultLocale, "en"])];
59
+ const dicts = [];
60
+ for (const code of order) {
61
+ for (const t of theme.chain) {
62
+ const loader = t.locales?.[code];
63
+ if (loader) {
64
+ try {
65
+ dicts.push({ code, dict: (await loader()).default });
66
+ }
67
+ catch { /* bỏ qua */ }
68
+ }
69
+ }
70
+ if (BUILTIN[code])
71
+ dicts.push({ code, dict: BUILTIN[code] });
72
+ }
73
+ const compiled = new Map();
74
+ const t = ((key, vars) => {
75
+ const hit = dicts.find((d) => d.dict[key] !== undefined);
76
+ if (!hit)
77
+ return key;
78
+ const src = hit.dict[key];
79
+ if (!src.includes("{") && !src.includes("'"))
80
+ return src;
81
+ const id = `${hit.code}\0${key}`;
82
+ if (!compiled.has(id)) {
83
+ try {
84
+ compiled.set(id, new IntlMessageFormat(src, hit.code, undefined, { ignoreTag: true }));
85
+ }
86
+ catch {
87
+ compiled.set(id, null);
88
+ }
89
+ }
90
+ const f = compiled.get(id);
91
+ try {
92
+ if (f)
93
+ return String(f.format(vars));
94
+ }
95
+ catch { /* thiếu biến → thay thô như bản cũ */ }
96
+ let s = src;
97
+ if (vars)
98
+ for (const [k, v] of Object.entries(vars))
99
+ s = s.replaceAll(`{${k}}`, String(v));
100
+ return s;
101
+ });
102
+ t.locale = site.locale;
103
+ return t;
104
+ });
@@ -0,0 +1,24 @@
1
+ import type { CSSProperties } from "react";
2
+ import type { Media, MediaSize } from "./types.js";
3
+ /**
4
+ * Các cỡ ảnh dùng cho srcset: cỡ được chọn + mọi cỡ cùng tỉ lệ (không bị cắt) nhỏ hơn,
5
+ * để trình duyệt tự chọn file vừa đủ cho màn hình.
6
+ */
7
+ export declare function imageCandidates(media: Media, size?: string): MediaSize[];
8
+ export interface ImgProps {
9
+ media?: Media | null;
10
+ /** Cỡ lớn nhất cần dùng: thumbnail, medium, medium_large, large, 1536x1536, 2048x2048 hoặc cỡ theme tự khai báo. */
11
+ size?: string;
12
+ /** Thuộc tính sizes của HTML, vd "(max-width: 768px) 100vw, 50vw". Mặc định: rộng tối đa bằng cỡ ảnh. */
13
+ sizes?: string;
14
+ className?: string;
15
+ alt?: string;
16
+ /** Ảnh đầu trang (LCP): tải ngay, ưu tiên cao. */
17
+ priority?: boolean;
18
+ style?: CSSProperties;
19
+ }
20
+ /**
21
+ * Ảnh responsive: &lt;picture&gt; với WebP + ảnh gốc, srcset nhiều cỡ, width/height chống nhảy layout,
22
+ * object-position theo điểm lấy nét chọn trong thư viện media.
23
+ */
24
+ export declare function Img({ media, size, sizes, className, alt, priority, style }: ImgProps): import("react").JSX.Element | null;
package/dist/image.js ADDED
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./utils.js";
3
+ const ratio = (w, h) => (w && h ? w / h : 0);
4
+ /**
5
+ * Các cỡ ảnh dùng cho srcset: cỡ được chọn + mọi cỡ cùng tỉ lệ (không bị cắt) nhỏ hơn,
6
+ * để trình duyệt tự chọn file vừa đủ cho màn hình.
7
+ */
8
+ export function imageCandidates(media, size = "large") {
9
+ const chosen = media.sizes[size];
10
+ const full = { url: media.url, width: media.width ?? 0, height: media.height ?? 0, webpUrl: media.webpUrl ?? undefined };
11
+ const target = chosen ?? full;
12
+ const r = ratio(target.width, target.height);
13
+ const all = [...Object.values(media.sizes), ...(chosen ? [] : [full])];
14
+ const same = all.filter((s) => s.width <= target.width && Math.abs(ratio(s.width, s.height) - r) < 0.02);
15
+ if (!same.includes(target))
16
+ same.push(target);
17
+ return same.filter((s) => s.width > 0).sort((a, b) => a.width - b.width);
18
+ }
19
+ /**
20
+ * Ảnh responsive: &lt;picture&gt; với WebP + ảnh gốc, srcset nhiều cỡ, width/height chống nhảy layout,
21
+ * object-position theo điểm lấy nét chọn trong thư viện media.
22
+ */
23
+ export function Img({ media, size = "large", sizes, className, alt, priority, style }) {
24
+ if (!media)
25
+ return null;
26
+ const list = imageCandidates(media, size);
27
+ const main = list[list.length - 1] ?? { url: media.url, width: media.width ?? 0, height: media.height ?? 0 };
28
+ const srcSet = list.map((s) => `${s.url} ${s.width}w`).join(", ");
29
+ const webp = list.every((s) => s.webpUrl) ? list.map((s) => `${s.webpUrl} ${s.width}w`).join(", ") : null;
30
+ const sz = sizes ?? (main.width ? `(max-width: ${main.width}px) 100vw, ${main.width}px` : undefined);
31
+ const focal = media.focalX != null && media.focalY != null
32
+ ? { objectPosition: `${Math.round(media.focalX * 100)}% ${Math.round(media.focalY * 100)}%` } : undefined;
33
+ const img = (_jsx("img", { src: main.url, srcSet: list.length > 1 ? srcSet : undefined, sizes: list.length > 1 ? sz : undefined, width: main.width || undefined, height: main.height || undefined, alt: alt ?? media.alt ?? "", loading: priority ? "eager" : "lazy", decoding: "async", fetchPriority: priority ? "high" : undefined, className: className, style: { ...focal, ...style } }));
34
+ if (!webp || media.mimeType === "image/webp")
35
+ return img;
36
+ return _jsxs("picture", { className: cx("cms-picture"), children: [_jsx("source", { type: "image/webp", srcSet: webp, sizes: sz }), img] });
37
+ }
@@ -0,0 +1,19 @@
1
+ export * from "./types.js";
2
+ export * from "./api.js";
3
+ export * from "./utils.js";
4
+ export { definePlugin, getPlugin, hasPlugin, applyFilters, doAction, getSlotItems, getSlotComponents, getActiveTheme, getActiveExtensions, BUILTIN_PAGE_TEMPLATES, findShortcode, findWidget, findBlock, findRoute, findRegion } from "./runtime.js";
5
+ export { PostContent, EditableRegion } from "./content.js";
6
+ export { Img, imageCandidates, type ImgProps } from "./image.js";
7
+ export { Menu, WidgetArea, Slot, SearchForm, Pagination, FeaturedImage, PostDate, TermLinks, Comments, LanguageSwitcher } from "./components.js";
8
+ export { getT, BUILTIN as BUILTIN_STRINGS, type Translator } from "./i18n.js";
9
+ export { getRequestLocale, localeHref, LOCALE_HEADER } from "./locale.js";
10
+ export { CommentForm, CommentList, CommentsMore, ExtensionBoundary, PostPasswordForm, Link, isInternalHref, type LinkProps } from "./client.js";
11
+ export { getPreference } from "./preferences.js";
12
+ export { getCurrentUser, getAccessToken, requireUser, userCan, safeRedirect, ACCESS_COOKIE, REFRESH_COOKIE, type CurrentUser } from "./auth.js";
13
+ export { LoginForm, RegisterForm, ForgotPasswordForm, ResetPasswordForm, LogoutButton, useCurrentUser, AUTH_LABELS, AUTH_PATHS, type AuthLabels, type AuthPaths } from "./auth-forms.js";
14
+ export { buildDocumentMetadata } from "./metadata.js";
15
+ /** Lấy giá trị Customizer của theme đang active. */
16
+ export declare function getThemeSettings(): Promise<import("./types.js").JsonObject>;
17
+ export { PluginForm } from "./plugin-form.js";
18
+ export type { PluginFormField } from "./plugin-form-client.js";
19
+ export { defineBlock, type BlockDefinition, type BlockDefinitionInput, type BlockAttrSpec } from "@ecomcx/nexus-content";
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ export * from "./types.js";
2
+ export * from "./api.js";
3
+ export * from "./utils.js";
4
+ export { definePlugin, getPlugin, hasPlugin, applyFilters, doAction, getSlotItems, getSlotComponents, getActiveTheme, getActiveExtensions, BUILTIN_PAGE_TEMPLATES, findShortcode, findWidget, findBlock, findRoute, findRegion } from "./runtime.js";
5
+ export { PostContent, EditableRegion } from "./content.js";
6
+ export { Img, imageCandidates } from "./image.js";
7
+ export { Menu, WidgetArea, Slot, SearchForm, Pagination, FeaturedImage, PostDate, TermLinks, Comments, LanguageSwitcher } from "./components.js";
8
+ export { getT, BUILTIN as BUILTIN_STRINGS } from "./i18n.js";
9
+ export { getRequestLocale, localeHref, LOCALE_HEADER } from "./locale.js";
10
+ export { CommentForm, CommentList, CommentsMore, ExtensionBoundary, PostPasswordForm, Link, isInternalHref } from "./client.js";
11
+ export { getPreference } from "./preferences.js";
12
+ export { getCurrentUser, getAccessToken, requireUser, userCan, safeRedirect, ACCESS_COOKIE, REFRESH_COOKIE } from "./auth.js";
13
+ export { LoginForm, RegisterForm, ForgotPasswordForm, ResetPasswordForm, LogoutButton, useCurrentUser, AUTH_LABELS, AUTH_PATHS } from "./auth-forms.js";
14
+ export { buildDocumentMetadata } from "./metadata.js";
15
+ /** Lấy giá trị Customizer của theme đang active. */
16
+ export async function getThemeSettings() {
17
+ const { getSite } = await import("./api.js");
18
+ return (await getSite()).themeSettings;
19
+ }
20
+ export { PluginForm } from "./plugin-form.js";
21
+ export { defineBlock } from "@ecomcx/nexus-content";
@@ -0,0 +1,9 @@
1
+ /** Header do proxy.ts của frontend gắn vào request: mã ngôn ngữ lấy từ tiền tố URL (/en/...). */
2
+ export declare const LOCALE_HEADER = "x-cms-locale";
3
+ /** Ngôn ngữ của request hiện tại (undefined = ngôn ngữ mặc định của site). */
4
+ export declare const getRequestLocale: () => Promise<string | undefined>;
5
+ /** Đường dẫn có tiền tố ngôn ngữ: localeHref(site, "/author/an") → "/en/author/an" khi đang ở tiếng Anh. */
6
+ export declare function localeHref(site: {
7
+ locale: string;
8
+ defaultLocale: string;
9
+ }, path: string): string;
package/dist/locale.js ADDED
@@ -0,0 +1,20 @@
1
+ import { cache } from "react";
2
+ /** Header do proxy.ts của frontend gắn vào request: mã ngôn ngữ lấy từ tiền tố URL (/en/...). */
3
+ export const LOCALE_HEADER = "x-cms-locale";
4
+ /** Ngôn ngữ của request hiện tại (undefined = ngôn ngữ mặc định của site). */
5
+ export const getRequestLocale = cache(async () => {
6
+ try {
7
+ const { headers } = await import("next/headers");
8
+ return (await headers()).get(LOCALE_HEADER) ?? undefined;
9
+ }
10
+ catch {
11
+ return undefined; // ngoài request (build, route tĩnh)
12
+ }
13
+ });
14
+ /** Đường dẫn có tiền tố ngôn ngữ: localeHref(site, "/author/an") → "/en/author/an" khi đang ở tiếng Anh. */
15
+ export function localeHref(site, path) {
16
+ const p = path.startsWith("/") ? path : `/${path}`;
17
+ if (site.locale === site.defaultLocale)
18
+ return p;
19
+ return p === "/" ? `/${site.locale}` : `/${site.locale}${p}`;
20
+ }
@@ -0,0 +1,3 @@
1
+ import type { DocumentMetadata, ResolveResult, Site } from "./types.js";
2
+ /** Metadata mặc định cho trang + cho plugin (vd SEO) sửa qua filter "document.metadata". */
3
+ export declare function buildDocumentMetadata(data: ResolveResult, site: Site): Promise<DocumentMetadata>;
@@ -0,0 +1,26 @@
1
+ import { applyFilters } from "./runtime.js";
2
+ /** Metadata mặc định cho trang + cho plugin (vd SEO) sửa qua filter "document.metadata". */
3
+ export async function buildDocumentMetadata(data, site) {
4
+ const isHome = data.kind === "home" || data.kind === "front-page";
5
+ const base = {
6
+ title: isHome ? `${site.title}${site.tagline ? ` – ${site.tagline}` : ""}` : `${data.title} – ${site.title}`,
7
+ description: data.post?.excerpt || (data.term?.description || undefined) || site.tagline,
8
+ canonical: data.post?.link ?? (data.basePath || undefined),
9
+ alternates: Object.fromEntries((data.alternates ?? []).map((a) => [a.locale, a.link])),
10
+ openGraph: {
11
+ title: data.post?.title ?? data.title,
12
+ description: data.post?.excerpt || site.tagline,
13
+ image: data.post?.featuredMedia?.sizes?.large?.url ?? data.post?.featuredMedia?.url,
14
+ type: data.kind === "single" ? "article" : "website",
15
+ },
16
+ };
17
+ if (data.kind === "404" || data.kind === "search" || data.noIndex)
18
+ base.robots = "noindex";
19
+ if (site.indexable === false)
20
+ base.robots = "noindex, nofollow";
21
+ const meta = await applyFilters("document.metadata", base, { site, data }, site);
22
+ // Trang core/plugin khai báo noindex (đăng nhập, tài khoản, giỏ hàng) — plugin SEO không được mở index
23
+ if (data.noIndex || site.indexable === false)
24
+ meta.robots = site.indexable === false ? "noindex, nofollow" : "noindex, follow";
25
+ return meta;
26
+ }
@@ -0,0 +1,4 @@
1
+ import type { TemplateProps } from "./types.js";
2
+ /** Template "canvas": không có header/footer của theme, chỉ nội dung trang (trang dựng bằng khối). */
3
+ export default function Canvas({ data }: TemplateProps): import("react").JSX.Element;
4
+ export declare const layout = false;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PostContent } from "./content.js";
3
+ /** Template "canvas": không có header/footer của theme, chỉ nội dung trang (trang dựng bằng khối). */
4
+ export default function Canvas({ data }) {
5
+ return _jsx("main", { className: "ecx-canvas", children: _jsx(PostContent, { post: data.post }) });
6
+ }
7
+ export const layout = false;
@@ -0,0 +1,3 @@
1
+ import type { TemplateProps } from "./types.js";
2
+ /** Template "full-width": giữ header/footer của theme, nội dung trải hết chiều ngang màn hình. */
3
+ export default function FullWidth({ data }: TemplateProps): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PostContent } from "./content.js";
3
+ /** Template "full-width": giữ header/footer của theme, nội dung trải hết chiều ngang màn hình. */
4
+ export default function FullWidth({ data }) {
5
+ return _jsx("div", { className: "ecx-full-width", children: _jsx(PostContent, { post: data.post }) });
6
+ }
@@ -0,0 +1,27 @@
1
+ export interface PluginFormField {
2
+ key: string;
3
+ type: string;
4
+ label: string;
5
+ required: boolean;
6
+ maxLength?: number | null;
7
+ minLength?: number | null;
8
+ min?: string | null;
9
+ max?: string | null;
10
+ options?: string[] | null;
11
+ pattern?: string | null;
12
+ default?: unknown;
13
+ }
14
+ export interface PluginFormLabels {
15
+ submit: string;
16
+ sending: string;
17
+ error: string;
18
+ }
19
+ /** Phần chạy trên trình duyệt của <PluginForm>: ô nhập, gửi, hiện lỗi theo từng ô (422) và lời cảm ơn. */
20
+ export declare function PluginFormClient({ plugin, table, fields, labels, successMessage, className }: {
21
+ plugin: string;
22
+ table: string;
23
+ fields: PluginFormField[];
24
+ labels: PluginFormLabels;
25
+ successMessage?: string | null;
26
+ className?: string;
27
+ }): import("react").JSX.Element;
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ /** Phần chạy trên trình duyệt của <PluginForm>: ô nhập, gửi, hiện lỗi theo từng ô (422) và lời cảm ơn. */
5
+ export function PluginFormClient({ plugin, table, fields, labels, successMessage, className }) {
6
+ const [errors, setErrors] = useState({});
7
+ const [state, setState] = useState({ status: "idle" });
8
+ async function submit(e) {
9
+ e.preventDefault();
10
+ const form = e.currentTarget;
11
+ const data = new FormData(form);
12
+ const body = {};
13
+ for (const f of fields) {
14
+ const v = data.get(f.key);
15
+ if (f.type === "boolean")
16
+ body[f.key] = data.has(f.key);
17
+ else if (v === null || v === "")
18
+ body[f.key] = null;
19
+ else if (f.type === "number")
20
+ body[f.key] = Number(v);
21
+ else if (f.type === "datetime")
22
+ body[f.key] = new Date(String(v)).toISOString();
23
+ else
24
+ body[f.key] = String(v);
25
+ }
26
+ setState({ status: "sending" });
27
+ setErrors({});
28
+ try {
29
+ const res = await fetch(`/api/cms/plugins/${plugin}/data/${table}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
30
+ const json = await res.json().catch(() => ({}));
31
+ if (!res.ok) {
32
+ setErrors(json.errors ?? {});
33
+ setState({ status: "error", message: json.title ?? labels.error });
34
+ return;
35
+ }
36
+ form.reset();
37
+ setState({ status: "done", message: json.message ?? successMessage ?? "" });
38
+ }
39
+ catch {
40
+ setState({ status: "error", message: labels.error });
41
+ }
42
+ }
43
+ if (state.status === "done")
44
+ return _jsx("p", { className: "ecx-form-success form-success", role: "status", children: state.message });
45
+ return (_jsxs("form", { className: ["ecx-form cms-form", className].filter(Boolean).join(" "), onSubmit: submit, noValidate: true, children: [fields.map((f) => {
46
+ const id = `cms-form-${plugin}-${table}-${f.key}`;
47
+ const err = errors[f.key];
48
+ const common = { id, name: f.key, required: f.required, "aria-invalid": err ? true : undefined, defaultValue: f.default == null ? undefined : String(f.default) };
49
+ return (_jsxs("p", { className: `ecx-form-field ecx-form-${f.type} cms-form-field cms-form-${f.type}`, children: [f.type === "boolean"
50
+ ? _jsxs("label", { htmlFor: id, children: [_jsx("input", { type: "checkbox", id: id, name: f.key, defaultChecked: f.default === true }), " ", f.label] })
51
+ : _jsxs("label", { htmlFor: id, children: [f.label, f.required && _jsx("span", { "aria-hidden": "true", children: " *" })] }), f.type === "textarea" ? _jsx("textarea", { rows: 5, maxLength: f.maxLength ?? undefined, ...common })
52
+ : f.type === "select" ? (_jsxs("select", { ...common, children: [!f.required && _jsx("option", { value: "", children: "\u2014" }), (f.options ?? []).map((o) => _jsx("option", { value: o, children: o }, o))] }))
53
+ : f.type !== "boolean" && _jsx("input", { ...common, type: { email: "email", tel: "tel", url: "url", number: "number", date: "date", time: "time", datetime: "datetime-local" }[f.type] ?? "text", maxLength: f.maxLength ?? undefined, minLength: f.minLength ?? undefined, min: f.min ?? undefined, max: f.max ?? undefined, pattern: f.pattern ?? undefined }), err && _jsx("span", { className: "ecx-form-error form-error", children: err.join(" ") })] }, f.key));
54
+ }), state.status === "error" && _jsx("p", { className: "ecx-form-error form-error", role: "alert", children: state.message }), _jsx("button", { type: "submit", disabled: state.status === "sending", children: state.status === "sending" ? labels.sending : labels.submit })] }));
55
+ }
@@ -0,0 +1,15 @@
1
+ import type { ShortcodeProps } from "./types.js";
2
+ /**
3
+ * Form tự sinh cho bảng EF của plugin có [PublicCreate] (form liên hệ, đặt lịch, đăng ký…):
4
+ * <PluginForm plugin="lien-he" table="messages" /> — hoặc shortcode [cms-form plugin="lien-he" table="messages"]
5
+ * Ô nhập, nhãn, ràng buộc lấy từ kiểu dữ liệu C# của plugin; máy chủ kiểm tra lại và giới hạn số lần gửi.
6
+ * Class CSS: cms-form, cms-form-field, cms-form-{kiểu}, form-error, form-success.
7
+ */
8
+ export declare function PluginForm({ plugin, table, submitLabel, className }: {
9
+ plugin: string;
10
+ table: string;
11
+ submitLabel?: string;
12
+ className?: string;
13
+ }): Promise<import("react").JSX.Element | null>;
14
+ /** Shortcode có sẵn: [cms-form plugin="lien-he" table="messages" submit="Gửi liên hệ" class="…"]. */
15
+ export declare function CmsFormShortcode({ attrs }: ShortcodeProps): import("react").JSX.Element;