Áp dụng cho api_version: "2026-01".
Extension Worker UI (block, action modal/fullscreen, trang render: "worker") chỉ dựng giao diện bằng các element pos-* dưới đây. POS vẽ mỗi element bằng component thật của POS, nên giao diện luôn đồng bộ với POS.
Trang iframe không dùng danh sách này; dùng HTML/React tự do, có thể kèm @pancake-pos/ui để giao diện giống POS.
Quy tắc chung
- Chỉ dùng element trong danh sách. Thẻ HTML thường (
div,span,img,table,script…) và element lạ không được vẽ; extension báo lỗi trong nhật ký của app. - Text chỉ hiển thị khi nằm trong
pos-text,pos-heading,pos-badge,pos-link,pos-button,pos-banner. - Không có
style,className, HTML thô. Màu sắc, khoảng cách, font theo POS. - URL (
href,src) chỉ chấp nhậnhttps://. - Sự kiện truyền giá trị qua
event.detail. Trong JSX dùng thuộc tínhonClick,onChange… như component thông thường. Với JS thuần, tên sự kiện là phần sauonviết thường:onPageChange→addEventListener('pagechange', …). - Giới hạn 2.000 element mỗi extension;
pos-tabletối đa 500 dòng mỗi lần render (dùng phân trang). - Thuộc tính dạng mảng/đối tượng (
options,columns,rows,pagination) truyền trực tiếp bằng JSX, không cần JSON.stringify.
<pos-stack gap="small">
<pos-text-field label="Mã phiếu" value={code} onChange={(e) => setCode(e.detail)} />
<pos-button variant="primary" loading={saving} onClick={save}>Lưu</pos-button>
</pos-stack>Không dùng framework thì dựng bằng DOM API của runtime (xem Môi trường worker):
let code = '';
const field = document.createElement('pos-text-field');
field.label = 'Mã phiếu';
field.addEventListener('change', (e) => { code = e.detail; });
const button = document.createElement('pos-button');
button.variant = 'primary';
button.textContent = 'Lưu';
button.addEventListener('click', () => save(code));
const stack = document.createElement('pos-stack');
stack.gap = 'small';
stack.append(field, button);
document.body.append(stack);Bố cục
pos-stack
Xếp các phần tử con theo hàng dọc hoặc ngang.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
direction |
'vertical' | 'horizontal' |
'vertical' |
Hướng xếp |
gap |
'none' | 'small' | 'medium' | 'large' |
'medium' |
Khoảng cách giữa phần tử |
align |
'start' | 'center' | 'end' | 'stretch' |
'stretch' |
Căn theo trục phụ |
justify |
'start' | 'center' | 'end' | 'space-between' |
'start' |
Căn theo trục chính |
wrap |
boolean |
false |
Cho xuống dòng (khi horizontal) |
pos-grid
Lưới cột đều nhau; trên màn hình hẹp tự chuyển về một cột.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
columns |
1–6 |
2 |
Số cột |
gap |
'none' | 'small' | 'medium' | 'large' |
'medium' |
Khoảng cách |
pos-section
Khung có tiêu đề, dùng làm vỏ ngoài của block.
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
heading |
string |
Tiêu đề |
description |
string |
Mô tả ngắn dưới tiêu đề |
Slot actions: nút hiển thị ở góc phải tiêu đề.
<pos-section heading="Công nợ">
<pos-button slot="actions" variant="tertiary" onClick={reload}>Tải lại</pos-button>
<pos-text>...</pos-text>
</pos-section>pos-divider
Đường kẻ ngang. Không có thuộc tính.
Hiển thị
pos-text
| Thuộc tính | Kiểu | Mặc định |
|---|---|---|
tone |
'default' | 'subdued' | 'success' | 'warning' | 'critical' |
'default' |
size |
'small' | 'medium' | 'large' |
'medium' |
weight |
'regular' | 'semibold' | 'bold' |
'regular' |
pos-heading
| Thuộc tính | Kiểu | Mặc định |
|---|---|---|
level |
1–4 |
3 |
pos-badge
Nhãn trạng thái ngắn.
| Thuộc tính | Kiểu | Mặc định |
|---|---|---|
tone |
'default' | 'info' | 'success' | 'warning' | 'critical' |
'default' |
pos-money
Hiển thị số tiền theo định dạng và đơn vị tiền tệ của shop.
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
value |
number |
Số tiền |
currency |
string |
Mã tiền tệ ISO 4217; mặc định là tiền tệ của shop |
tone |
như pos-text |
Màu chữ |
pos-link
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
href |
string |
URL https://, luôn mở tab mới |
Sự kiện onClick: dùng khi link thực hiện thao tác trong app thay vì mở URL (không đặt href).
pos-image
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
src |
string |
URL https:// thuộc domain trong network.allowed_origins, hoặc public/<tên file> trỏ tới file trong thư mục public/ của dự án |
|
alt |
string |
Văn bản thay thế | |
width, height |
number |
Kích thước (px) | |
fit |
'cover' | 'contain' |
'cover' |
Cách co ảnh |
POS tải ảnh qua máy chủ trung gian của sandbox: chỉ nhận định dạng ảnh, tối đa 5 MB. Ảnh từ domain chưa khai không hiển thị (chế độ phát triển có cảnh báo).
Nhập liệu
Thuộc tính chung của mọi ô nhập:
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
label |
string |
Nhãn |
value |
tuỳ component | Giá trị hiện tại (component có kiểm soát) |
disabled |
boolean |
Khoá ô |
required |
boolean |
Hiện dấu bắt buộc (không tự kiểm tra) |
error |
string |
Thông báo lỗi dưới ô |
help |
string |
Gợi ý dưới ô |
Sự kiện chung: onChange (event.detail là giá trị mới), onBlur.
POS hiện ngay chữ người dùng gõ rồi mới gửi onChange cho app, nên ô không giật dù app đang bận. Nếu app đặt value khác giá trị người dùng vừa nhập (ví dụ chuẩn hoá chữ hoa hoặc xóa ô), POS hiển thị giá trị của app. Nên đặt value ngay trong onChange; đổi value sau một lệnh await có thể ghi đè chữ người dùng đang gõ.
pos-text-field
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
value |
string |
|
placeholder |
string |
|
multiline |
boolean | number |
Ô nhiều dòng; số là số dòng hiển thị |
maxLength |
number |
Giới hạn ký tự |
pos-number-field
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
value |
number | null |
|
min, max, step |
number |
|
suffix |
string |
Đơn vị hiển thị sau số, ví dụ "%" |
pos-select
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
options |
{ label: string; value: string; disabled?: boolean }[] |
Danh sách lựa chọn |
value |
string | string[] |
Mảng khi multiple |
multiple |
boolean |
Chọn nhiều |
placeholder |
string |
|
searchable |
boolean |
Cho gõ tìm trong danh sách |
pos-checkbox
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
checked |
boolean |
|
label |
string |
onChange: event.detail là boolean.
pos-date-picker
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
value |
string | null |
Ngày dạng YYYY-MM-DD |
min, max |
string |
Giới hạn ngày chọn, dạng YYYY-MM-DD |
Phản hồi
pos-banner
Thông báo nổi bật trong extension.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
tone |
'info' | 'success' | 'warning' | 'critical' |
'info' |
|
heading |
string |
Tiêu đề | |
dismissible |
boolean |
false |
Hiện nút đóng |
Sự kiện onDismiss.
pos-spinner
| Thuộc tính | Kiểu | Mặc định |
|---|---|---|
size |
'small' | 'medium' | 'large' |
'medium' |
Dữ liệu và thao tác
pos-button
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
variant |
'primary' | 'secondary' | 'tertiary' | 'danger' |
'secondary' |
Kiểu nút |
size |
'small' | 'medium' |
'medium' |
|
loading |
boolean |
false |
Hiện trạng thái đang xử lý, chặn bấm |
disabled |
boolean |
false |
Sự kiện onClick.
pos-table
Bảng dữ liệu. Ô chỉ chứa giá trị (chuỗi, số); POS định dạng theo format của cột.
| Thuộc tính | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
columns |
Column[] |
Định nghĩa cột | |
rows |
object[] |
[] |
Dữ liệu, tối đa 500 dòng |
rowKey |
string |
'id' |
Trường dùng làm khoá dòng |
loading |
boolean |
false |
|
pagination |
{ page: number; pageSize: number; total: number } | null |
null |
Phân trang phía app |
emptyText |
string |
Nội dung khi không có dòng |
type Column = {
key: string; // tên trường trong row
title: string;
align?: 'left' | 'center' | 'right';
width?: number; // px
format?: 'text' | 'number' | 'money' | 'date' | 'datetime';
};Sự kiện:
onPageChange:event.detaillà{ page, pageSize }.onRowClick:event.detaillà row được bấm.
<pos-table
columns={[
{ key: 'system_id', title: 'Mã đơn' },
{ key: 'total_price', title: 'Tổng tiền', format: 'money', align: 'right' },
]}
rows={orders}
rowKey="system_id"
pagination={{ page, pageSize: 20, total }}
onPageChange={(e) => setPage(e.detail.page)}
/>pos-modal
Hộp thoại mở từ bên trong extension (ví dụ từ một nút trong block).
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
open |
boolean |
Trạng thái mở |
heading |
string |
Tiêu đề |
Slot footer cho các nút cuối hộp thoại. Sự kiện onClose khi người dùng bấm đóng hoặc ra ngoài.
<pos-modal open={open} heading="Xác nhận" onClose={() => setOpen(false)}>
<pos-text>Đẩy đơn sang ERP?</pos-text>
<pos-button slot="footer" onClick={() => setOpen(false)}>Huỷ</pos-button>
<pos-button slot="footer" variant="primary" onClick={push}>Đẩy</pos-button>
</pos-modal>Action presentation: "modal" đã có sẵn modal của POS; không cần bọc thêm pos-modal.
Dự kiến bổ sung
pos-page, pos-tabs, pos-filter-bar, pos-chart (POS vẽ biểu đồ, app chỉ truyền dữ liệu), phục vụ trang Worker UI full màn hình.
