# @janbox/storefront-builder

Thư viện visual page builder độc lập, xây dựng trên [CraftJS](https://craft.js.org/). 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

```bash
pnpm add @janbox/storefront-builder
```

### Peer dependencies

```bash
pnpm add react@^19 react-dom@^19 @janbox/storefront-ui@>=2
```

## Package 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ả.

```tsx
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ề `Editor` xem tại [editor/lib/README.md](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:

1. **Truyền `data`** — ưu tiên render từ serialized data (JSON từ editor)
2. **Truyền `children`** — nếu không truyền `data`, Canvas sẽ render children như JSX thông thường

### Render từ serialized data

```tsx
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](#node-resources).

### Render từ children (không có data)

```tsx
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:

```ts
// 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:

```tsx
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

```ts
import {
  HERO_SECTION_GROUP,
  FAQS_SECTION_GROUP,
  GUARANTEE_SECTION_GROUP,
} from '@janbox/storefront-builder/templates';
```

Truyền vào `Editor` qua prop `insert.groups`:

```tsx
<Editor
  theme={myTheme}
  handlers={{
    upload: async () => '',
    submit: () => {},
    close: () => {},
  }}
  insert={{
    groups: [HERO_SECTION_GROUP, FAQS_SECTION_GROUP],
  }}
/>
```

---

## Custom Nodes

### 1. Tạo node component

```tsx
// 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

```tsx
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)

```tsx
// 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`:

```ts
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()`.

```ts
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.

```ts
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`).

```ts
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`:

```ts
// 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).

---

## 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

```tsx
<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.

**Resource có sẵn: `link`**

`LinkNode` đọc component render link từ `nodeResources.link.component` (mặc định thẻ `<a>`):

```tsx
<Composer nodeResources={{ link: { component: AppLink } }}>
  <Canvas data={savedData} />
</Composer>
```

Xem [`lib/link/README.md`](dist/lib/link/README.md#link-component).

### 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:

```ts
// khai báo trong package (node nội bộ)
// đường dẫn tương đối từ file node tới module khai báo NodeResources;
// ví dụ trong repo: lib/link/link.node/types.ts
declare module '../../../components/composer/helpers' {
  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?:` hay `products:` đề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

```tsx
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`:

```tsx
<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:

```ts
import '@janbox/storefront-builder/style.css';
```

---

## Theme Shape

Prop `theme` của `Editor` tuân theo kiểu `Theme` của `@janbox/storefront-ui`:

```ts
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:

```ts
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:

```ts
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`:

```ts
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:

```ts
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ểu `Theme` của `@janbox/storefront-ui`) truyền vào `Editor` để theming storefront.

---

## TypeScript

Package đi kèm đầy đủ file `.d.ts`. Các type chính:

```ts
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](dist/lib/root/README.md) — container gốc của canvas
- [BoxNode](dist/lib/box/README.md) — block container
- [FlexboxNode](dist/lib/flexbox/README.md) — flex container
- [FlexItemNode](dist/lib/flex-item/README.md) — flex item
- [GridNode](dist/lib/grid/README.md) — CSS grid container
- [CellNode](dist/lib/cell/README.md) — grid cell

### Typography

- [TextNode](dist/lib/text/README.md) — inline/block text
- [HeadingNode](dist/lib/heading/README.md) — h1–h6 heading
- [ParagraphNode](dist/lib/paragraph/README.md) — paragraph

### Media

- [ImageNode](dist/lib/image/README.md) — hình ảnh
- [VideoNode](dist/lib/video/README.md) — video player
- [IconNode](dist/lib/icon/README.md) — inline SVG icon

### Navigation

- [LinkNode](dist/lib/link/README.md) — anchor link
- [ButtonNode](dist/lib/button/README.md) — CTA button

### Lists

- [UnorderedListNode](dist/lib/unordered-list/README.md) — danh sách không thứ tự
- [ListItemNode](dist/lib/list-item/README.md) — item trong danh sách

### Accordion

- [AccordionGroupNode](dist/lib/accordion-group/README.md) — accordion container
- [AccordionNode](dist/lib/accordion/README.md) — accordion item
- [AccordionSummaryNode](dist/lib/accordion-summary/README.md) — accordion header
- [AccordionContentNode](dist/lib/accordion-content/README.md) — accordion content

### Tabs

- [TabsNode](dist/lib/tabs/README.md) — tabs container
- [TabListNode](dist/lib/tab-list/README.md) — container chứa tab headers
- [TabNode](dist/lib/tab/README.md) — tab header button
- [TabContentNode](dist/lib/tab-content/README.md) — container chứa tab panels
- [TabPanelNode](dist/lib/tab-panel/README.md) — nội dung tab panel

### Carousel

- [SwiperNode](dist/lib/swiper/README.md) — carousel container
- [SwiperSlideNode](dist/lib/swiper-slide/README.md) — carousel slide

### Marquee

- [MarqueeNode](dist/lib/marquee/README.md) — auto-scrolling ticker
- [MarqueeItemNode](dist/lib/marquee-item/README.md) — ticker item

### Utilities

- [CountdownTimerNode](dist/lib/countdown-timer/README.md) — đồng hồ đếm ngược

### Internal

- [UnknownNode](dist/lib/unknown/README.md) — fallback cho node không resolve được

---

## Phát triển Monorepo

```bash
# Cài đặt dependencies
pnpm install

# Chạy demo app (React Router 7)
pnpm dev

# Build thư viện
pnpm build

# Type check
pnpm typecheck
```

Yêu cầu Node.js >= 20 và pnpm.
