@janbox/storefront-builder
v4.0.1
Published
Standalone storefront builder library extracted from craft-layers-portal
Downloads
701
Readme
@janbox/storefront-builder
Thư viện visual page builder độc lập, xây dựng trên CraftJS. Hỗ trợ kéo-thả tạo storefront với giao diện editor đầy đủ, 33 node component có sẵn và responsive design theo breakpoint.
Cài đặt
pnpm add @janbox/storefront-builderPeer dependencies
pnpm add react@^19 react-dom@^19 @janbox/storefront-ui@>=2Package Exports
| Import path | Nội dung |
| --- | --- |
| @janbox/storefront-builder | Node components (33 nodes) |
| @janbox/storefront-builder/editor | Giao diện editor đầy đủ (Editor component + toolbars) |
| @janbox/storefront-builder/templates | Element groups và section templates có sẵn |
| @janbox/storefront-builder/theme | Design tokens (palette, text scale) dùng trong builder |
| @janbox/storefront-builder/style.css | CSS bundle (bắt buộc import) |
Quick Start — Full Editor
Component Editor render giao diện visual builder hoàn chỉnh: header toolbar, sidebar thư viện component, canvas kéo-thả.
import '@janbox/storefront-builder/style.css';
import { Editor } from '@janbox/storefront-builder/editor';
import { HERO_SECTION_GROUP, FAQS_SECTION_GROUP } from '@janbox/storefront-builder/templates';
import type { Theme } from '@janbox/storefront-ui/theme';
const myTheme: Theme = {
palette: {
primary: {
500: '#fa8c16',
600: '#d46b08',
// ...
},
// ...
},
typography: {
base: { fontSize: '1rem', lineHeight: '1.5rem' },
// ...
},
};
export default function BuilderPage() {
return (
<Editor
theme={myTheme}
handlers={{
upload: async (file) => URL.createObjectURL(file),
submit: async (data) => {
await savePage(data);
},
close: () => navigateBack(),
saveTheme: async (theme) => {
await api.saveTheme(theme); // optional: enables theme editing
},
}}
insert={{
groups: [HERO_SECTION_GROUP, FAQS_SECTION_GROUP],
}}
/>
);
}Editor chiếm toàn bộ viewport (100vw × 100vh), nên đặt ở một route/page riêng.
Chi tiết đầy đủ về
Editorxem tại editor/lib/README.md.
Quick Start — Render Only (không có editor UI)
Dùng Composer + Canvas để render một trang đã lưu, không cần giao diện editor. Đây là cách dùng cho phía storefront hiển thị cho người mua.
Canvas hỗ trợ hai chế độ render:
- Truyền
data— ưu tiên render từ serialized data (JSON từ editor) - Truyền
children— nếu không truyềndata, Canvas sẽ render children như JSX thông thường
Render từ serialized data
import '@janbox/storefront-builder/style.css';
import { Composer, Canvas } from '@janbox/storefront-builder';
import type { SerializedNodes } from '@janbox/storefront-builder';
const savedData: SerializedNodes = { /* JSON từ editor */ };
export default function StorefrontPage() {
return (
<Composer>
<Canvas data={savedData} />
</Composer>
);
}data nhận SerializedNodes object hoặc JSON string. Khi truyền data, Canvas sẽ bỏ qua children và render hoàn toàn từ dữ liệu đã serialize.
Nếu trang có node cần dữ liệu ngoài (sản phẩm, danh mục, ...), truyền thêm prop nodeResources cho Composer — xem Node resources.
Render từ children (không có data)
import '@janbox/storefront-builder/style.css';
import { Composer, Canvas, RootNode, BoxNode, HeadingNode } from '@janbox/storefront-builder';
export default function StorefrontPage() {
return (
<Composer>
<Canvas>
<RootNode>
<BoxNode>
<HeadingNode text="Hello World" />
</BoxNode>
</RootNode>
</Canvas>
</Composer>
);
}Khi không truyền data, Canvas sử dụng children làm cây node mặc định để render.
Khái niệm cơ bản
Nodes
Mỗi phần tử trên canvas là một node — React component được đăng ký với CraftJS. Thư viện có 33 node có sẵn:
| Nhóm | Nodes |
| --- | --- |
| Layout | RootNode, Box, Flexbox, FlexItem, Grid, Cell |
| Typography | Text, Paragraph, Heading |
| Media | Image, Video, Icon |
| Navigation | Link, Button |
| Lists | UnorderedList, ListItem |
| Tabs | Tabs, TabList, Tab, TabContent, TabPanel |
| Accordion | Accordion, AccordionGroup, AccordionSummary, AccordionContent |
| Carousel | Swiper, SwiperSlide |
| Marquee | Marquee, MarqueeItem |
| Utilities | CountdownTimer |
| Internal | UnknownNode (fallback cho node không resolve được) |
Tất cả đều được export từ @janbox/storefront-builder.
Responsive Props
Tất cả style prop đều nhận plain value hoặc responsive object theo breakpoint:
// Breakpoints: xs (390px), sm (768px), md (1280px), lg (1680px)
// Plain value (áp dụng tại mọi breakpoint)
{ fontSize: '16px' }
// Responsive object (xs là base; các key khác override theo hướng tăng dần)
{ fontSize: { xs: '14px', md: '18px' } }Breakpoint hiện tại được điều khiển qua query param ?screen=xs|sm|md|lg.
Serialized State
Trạng thái canvas là plain JSON (SerializedNodes). Dùng useComposer() để đọc và serialize:
import { useComposer } from '@janbox/storefront-builder';
function SaveButton() {
const { query } = useComposer();
const handleSave = () => {
const json = query.serialize(); // JSON string dạng SerializedNodes
// lưu vào backend
};
return <button onClick={handleSave}>Lưu</button>;
}Templates
Templates là các nhóm kéo-thả có sẵn trong sidebar editor.
Sidebar Basics được tích hợp sẵn trong editor và chứa toàn bộ element cơ bản. Consumer không cần import hay truyền các nhóm này.
Section Groups
import {
HERO_SECTION_GROUP,
FAQS_SECTION_GROUP,
GUARANTEE_SECTION_GROUP,
} from '@janbox/storefront-builder/templates';Truyền vào Editor qua prop insert.groups:
<Editor
theme={myTheme}
handlers={{
upload: async () => '',
submit: () => {},
close: () => {},
}}
insert={{
groups: [HERO_SECTION_GROUP, FAQS_SECTION_GROUP],
}}
/>Custom Nodes
1. Tạo node component
// my-badge.node.tsx
import { defineNode, useNode } from '@janbox/storefront-builder';
type MyBadgeProps = { label: string; color?: string };
export const MyBadge = ({ label, color = '#fa8c16' }: MyBadgeProps) => {
const { connectors } = useNode();
return (
<span
ref={(el) => el && connectors.connect(el)}
style={{ background: color, padding: '2px 8px', borderRadius: 4 }}
>
{label}
</span>
);
};
defineNode(MyBadge, {
resolved: 'MyBadge',
isCanvas: false,
info: { displayName: 'Badge' },
defaultProps: { label: 'Badge', color: '#fa8c16' },
});2. Truyền resolver vào editor
import { MyBadge } from './my-badge.node';
<Editor
theme={myTheme}
handlers={{ upload: async () => '', submit: () => {}, close: () => {} }}
resolver={{ MyBadge }}
insert={{ groups: [] }}
/>3. Thêm toolbar (tùy chọn)
// my-badge.toolbar.tsx
import { useNodeProps } from '@janbox/storefront-builder';
export function MyBadgeToolbar() {
const { nodeProps, setNodeProps } = useNodeProps<MyBadgeProps>();
return (
<div>
<label>Label</label>
<input
value={nodeProps.label}
onChange={(e) => setNodeProps({ label: e.target.value })}
/>
</div>
);
}Đăng ký toolbar trong defineNode:
defineNode(MyBadge, {
resolved: 'MyBadge',
info: { displayName: 'Badge' },
related: { inspector: MyBadgeToolbar },
});Hooks
Tất cả hooks phải được gọi bên trong cây <Composer>.
useComposer(collector?)
Wrapper nâng cao của CraftJS useEditor. Bổ sung thêm actions.duplicate() và actions.move().
const { query, actions } = useComposer();
// Serialize trạng thái canvas hiện tại
const json = query.serialize();
// Duplicate một node
actions.duplicate(nodeId);
// Di chuyển một node
actions.move({ nodeId, sourceIndex: 0, destinationIndex: 2 });useNodeProps<P>(options?)
Đọc và ghi props của node hiện tại (hoặc node chỉ định qua options.nodeId). Hỗ trợ cập nhật responsive.
const { nodeProps, setNodeProps, setNodeResponsiveProps } = useNodeProps<MyProps>();
// Set prop thường
setNodeProps({ color: '#ff0000' });
// Set responsive prop cho breakpoint cụ thể
setNodeResponsiveProps({ fontSize: '18px' }, 'md');useNode()
Truy cập trực tiếp CraftJS node context (re-export từ @craftjs/core).
const { id, connectors, actions } = useNode();useNodeResources(selector?)
Đọc dữ liệu ngoài mà consumer cấp cho Composer/Editor. Nhận selector giống useComposer:
// chọn một resource
const products = useNodeResources((r) => r.products);
// chọn nhiều
const { products, categories } = useNodeResources((r) => ({
products: r.products,
categories: r.categories,
}));
// không selector → toàn bộ map
const resources = useNodeResources();Mọi resource đều optional, nên giá trị trả về có thể là undefined — node cần tự xử lý trường hợp consumer không cấp. Xem Node resources.
Node resources
Một số node cần dữ liệu mà builder không tự có (danh sách sản phẩm, danh mục, ...). Cơ chế nodeResources cho phép consumer cấp dữ liệu đó một lần, còn node nào cần thì tự đọc.
1. Consumer cấp dữ liệu
<Editor theme={myTheme} handlers={handlers} nodeResources={{ products }} />
// hoặc ở chế độ render-only
<Composer nodeResources={{ products }}>
<Canvas data={savedData} />
</Composer>Dữ liệu được truyền một lần lúc mount và giữ nguyên trong suốt phiên làm việc.
2. Node khai báo resource nó cần
NodeResources là interface mở rộng được bằng declaration merging. Node khai báo key nó cần, kèm kiểu dữ liệu:
// khai báo trong package (node nội bộ)
declare module '~/packages/builder/hooks' {
interface NodeResources {
products?: Product[];
}
}
// khai báo từ app consumer
declare module '@janbox/storefront-builder' {
interface NodeResources {
products?: Product[];
}
}Lưu ý: khai báo
products?:hayproducts:đều được — consumer luôn chỉ cần cấp những resource họ dùng, và giá trị node đọc ra luôn là optional.
3. Node đọc dữ liệu
import { useNodeResources } from '@janbox/storefront-builder';
export const ProductListNode = () => {
const products = useNodeResources((r) => r.products);
if (!products?.length) {
return <EmptyState />;
}
return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
};Selector chỉ dùng để lấy đúng phần cần thiết — nó không memo hoá theo kết quả, nên component sẽ re-render khi nodeResources đổi reference bất kể selector chọn gì. Với dữ liệu tĩnh truyền một lần thì điều này không ảnh hưởng.
Gọi ngoài cây <Composer> sẽ trả về object rỗng, không throw.
Theme Editor
Sidebar tích hợp sẵn tab Theme cho phép xem và chỉnh sửa palette. Tính năng chỉnh sửa được kích hoạt khi truyền handlers.saveTheme:
<Editor
theme={myTheme}
handlers={{
// ...
saveTheme: async (theme) => {
await api.updateTheme(theme);
},
}}
/>- Không truyền
saveTheme→ tab Theme chỉ hiển thị palette (view only) - Truyền
saveTheme→ hiện color picker cho primary/secondary + nút Save/Cancel - Cancel hoặc rời tab → reset về theme ban đầu
- Save → gọi
saveTheme(theme)async, có loading state
Stylesheet
Import stylesheet một lần ở entry của app:
import '@janbox/storefront-builder/style.css';Theme Shape
Prop theme của Editor tuân theo kiểu Theme của @janbox/storefront-ui:
type Theme = {
palette: {
primary: Record<100 | 200 | 300 | 400 | 500 | 600 | 700 | 800, string>;
secondary: Record<...>;
red, orange, yellow, green, blue, violet, neutral: Record<...>;
background: { subtle: string; default: string; emphasis: string };
surface: { default: string };
border: { default: string };
};
typography: Record<'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl', {
fontSize: string;
lineHeight: string;
}>;
};Builder Theme (Design Tokens)
Ngoài theme của @janbox/storefront-ui (truyền vào Editor), builder cũng export bộ design tokens nội bộ dùng cho styling:
import { palette, text, theme } from '@janbox/storefront-builder/theme';palette
Bảng màu nội bộ của builder — dùng cho UI editor và có thể tái sử dụng trong custom toolbar/component:
palette.primary['5s'] // '#597ef7'
palette.ink['6s'] // '#333333'
palette.red['4s'] // '#ff7875'text
Text scale (font size + line height) theo các cấp từ xs đến 9xl:
text.sm // { fontSize: '0.875rem', lineHeight: '1.25rem' }
text.base // { fontSize: '1rem', lineHeight: '1.5rem' }
text['2xl'] // { fontSize: '1.5rem', lineHeight: '2rem' }theme
Object tổng hợp { palette, text } — tiện khi cần truyền cả hai:
import { theme } from '@janbox/storefront-builder/theme';
theme.palette.blue['6s'] // '#0f62fe'
theme.text.lg // { fontSize: '1.125rem', lineHeight: '1.75rem' }Lưu ý: Đây là design tokens của builder UI, khác với prop
theme(kiểuThemecủa@janbox/storefront-ui) truyền vàoEditorđể theming storefront.
TypeScript
Package đi kèm đầy đủ file .d.ts. Các type chính:
import type {
SerializedNodes, // JSON state của canvas
NodeId, // string định danh node
NodeTree, // cây node
Resolver, // map resolvedName → component
NodeConfig, // config truyền vào defineNode()
NodeResources, // interface resources (mở rộng bằng declaration merging)
} from '@janbox/storefront-builder';Node Reference
Chi tiết props, usage và rules cho từng node:
Layout
- RootNode — container gốc của canvas
- BoxNode — block container
- FlexboxNode — flex container
- FlexItemNode — flex item
- GridNode — CSS grid container
- CellNode — grid cell
Typography
- TextNode — inline/block text
- HeadingNode — h1–h6 heading
- ParagraphNode — paragraph
Media
Navigation
- LinkNode — anchor link
- ButtonNode — CTA button
Lists
- UnorderedListNode — danh sách không thứ tự
- ListItemNode — item trong danh sách
Accordion
- AccordionGroupNode — accordion container
- AccordionNode — accordion item
- AccordionSummaryNode — accordion header
- AccordionContentNode — accordion content
Tabs
- TabsNode — tabs container
- TabListNode — container chứa tab headers
- TabNode — tab header button
- TabContentNode — container chứa tab panels
- TabPanelNode — nội dung tab panel
Carousel
- SwiperNode — carousel container
- SwiperSlideNode — carousel slide
Marquee
- MarqueeNode — auto-scrolling ticker
- MarqueeItemNode — ticker item
Utilities
- CountdownTimerNode — đồng hồ đếm ngược
Internal
- UnknownNode — fallback cho node không resolve được
Phát triển Monorepo
# Cài đặt dependencies
pnpm install
# Chạy demo app (React Router 7)
pnpm dev
# Build thư viện
pnpm build
# Type check
pnpm typecheckYêu cầu Node.js >= 20 và pnpm.
