npm package: @lucasvu/scope-ui
Description: Bộ UI package dùng chung cho các app/micro-frontend trong workspace.
Latest version: 0.1.7

# @siraya/scope-ui

Bộ UI package dùng chung cho các app/micro-frontend trong workspace.

Public surface chính:

- `@siraya/scope-ui`: React component surface
- `@siraya/scope-ui/core`: theme/runtime/contracts và AI metadata
- `@siraya/scope-ui/styles.css`: base stylesheet
- `@siraya/scope-ui/themes/*`: bundled theme layers
- `scope-ui-init`: CLI bootstrap `AGENTS.md`, theme override và layout preset

## Cách dùng nhanh

1. Import stylesheet ở app entry:

```ts
import '@siraya/scope-ui/styles.css'
import '@siraya/scope-ui/themes/theme-ui-new-main-fe.css'
```

2. Nếu theme layer dùng `data-client-theme`, khởi tạo ở app entry:

```ts
import {
  applyDocumentClientTheme,
  initializeDocumentColorMode,
} from '@siraya/scope-ui/core'

initializeDocumentColorMode()
applyDocumentClientTheme('neo-slate')
```

3. Dùng component từ root package:

```tsx
import { Button, Card, CardHeader, CardTitle, CardContent, DataTable, Field, Input } from '@siraya/scope-ui'

export function Example() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Form đăng ký</CardTitle>
      </CardHeader>
      <CardContent className="ui-grid ui-grid--two">
        <Field label="Email" required>
          <Input type="email" placeholder="you@example.com" />
        </Field>
        <Field label="Gói sử dụng">
          <DataTable
            data={[{ name: 'Starter', price: '$19' }]}
            rowKey="name"
            columns={[
              { key: 'name', title: 'Tên', sortable: true },
              { key: 'price', title: 'Giá' },
            ]}
          />
        </Field>
      </CardContent>
      <Button block>Tiếp tục</Button>
    </Card>
  )
}
```

## Thành phần có sẵn

- Nút: `Button` (variant `default | secondary | ghost | outline | destructive | link | confirm | create`, size `sm | md | lg | icon`, hỗ trợ `block`)
- Badge: `Badge` (solid, outline, success, warning)
- Thẻ: `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `CardAction`
- Form: `Form`, `Field`, `Item`, `Label`, `Control`, `Description`, `Message`, `Input`, `Textarea`, `Select`, `SearchableSelect`, `Combobox`, `AsyncCombobox`, `MultiSelect`
- Numeric: `NumericInput` (hỗ trợ integer/decimal, parse an toàn, format/clamp khi blur, dùng cho price/amount)
- Tabs: `Tabs` với mảng `items` `{ value, label, content, badge? }`
- Thông báo: `Alert` (tone info/success/warning/danger)
- Số liệu: `Stat` (label, value, delta, trend up/down/flat)
- Bảng: `DataTable<T>` với `columns` và `render` tuỳ biến
- Tooltip: `Tooltip`, `OverflowTooltip`, `LineClampTooltip` (hiển thị tooltip khi bị cắt theo dòng)
- Lưới tiện dụng: class `ui-grid`, `ui-grid--two`, `ui-grid--three` để xếp block nhanh
- Legacy compatibility: `MainFe.*` và các type alias `MainFe*Props`

## NumericInput

`NumericInput` dùng cho các ô số cần giữ trải nghiệm nhập liệu mượt (không nhảy caret, chấp nhận trạng thái tạm như `.` hoặc `12.`), đồng thời parse/format có kiểm soát.

### Public API

```ts
type NumericInputProps = {
  value?: string | number;
  defaultValue?: string | number;
  onValueChange?: (raw: string) => void;
  onNumberChange?: (num: number | null) => void;
  mode?: 'integer' | 'decimal'; // default: 'decimal'
  decimalScale?: number;
  maxDecimalScale?: number;
  min?: number;
  max?: number;
  allowNegative?: boolean; // default: false
  decimalSeparator?: '.' | ',' | 'auto'; // default: 'auto'
  formatOnBlur?: 'none' | 'trim' | 'fixed'; // default: 'trim'
  clampOnBlur?: boolean; // default: false
  disabled?: boolean;
  readOnly?: boolean;
  name?: string;
  onBlur?: (e) => void;
  onFocus?: (e) => void;
  // + các input props khác: className, placeholder, ...
};
```

### Rule chính

- Luôn giữ `rawValue` theo đúng ký tự user nhập.
- `onValueChange` luôn bắn khi user gõ.
- `onNumberChange` trả `number` khi parse được, ngược lại trả `null`.
- Không tự chèn dấu phân tách hàng nghìn trong lúc gõ.
- Có hỗ trợ paste chuỗi như `1,234.50` hoặc `1.234,50` (`decimalSeparator="auto"`).
- Với `maxDecimalScale`, input sẽ chặn vượt số chữ số phần thập phân.

### Ví dụ dùng

```tsx
import { NumericInput } from '@siraya/scope-ui';

// 1) Integer qty
<NumericInput
  mode="integer"
  min={0}
  value={qty}
  onValueChange={setQty}
/>;

// 2) Money
<NumericInput
  mode="decimal"
  decimalScale={2}
  maxDecimalScale={2}
  formatOnBlur="fixed"
  value={price}
  onValueChange={setPrice}
/>;

// 3) Percent
<NumericInput
  mode="decimal"
  decimalScale={2}
  maxDecimalScale={2}
  min={0}
  max={100}
  clampOnBlur
  value={percent}
  onValueChange={setPercent}
/>;
```

## DataTable

```tsx
import type { DataTableColumn, DataTableSortState } from '@siraya/scope-ui'
```

### Tối thiểu

- `data`: mảng record
- `columns`: cấu hình cột
- `rowKey`: key của record (string/number) hoặc hàm `(record) => key`

### Props chính

```tsx
type DataTableProps<T> = {
  columns: DataTableColumn<T>[];
  data: T[];
  rowKey: keyof T | ((record: T) => string | number);
  loading?: boolean;
  emptyText?: ReactNode;
  pagination?: {
    page: number;
    pageSize: number;
    total: number;
    onChange: (page: number) => void;
    pageSizeOptions?: number[];
    onPageSizeChange?: (pageSize: number) => void;
  };
  sort?: DataTableSortState | null;
  onSortChange?: (sort: DataTableSortState | null) => void;
  sortMode?: 'client' | 'server';
  onRowClick?: (record: T) => void;
  rowSelection?: {
    selectedRowKeys: Array<string | number>;
    onChange: (keys: Array<string | number>) => void;
  };
  renderActions?: (record: T) => ReactNode;
  className?: string;
};
```

### Cấu hình cột

```tsx
type DataTableColumn<T> = {
  key: string;                 // khóa duy nhất của cột
  title: ReactNode;            // tiêu đề hiển thị ở header
  dataIndex?: keyof T;         // map dữ liệu (mặc định lấy theo key)
  width?: number | string;     // độ rộng ưu tiên (vd: 180, '180px', '20%')
  render?: (value, record, index) => ReactNode; // custom cell
  sortable?: boolean;          // bật/tắt sort cho cột (ưu tiên cao nhất)
  sorter?: (a, b) => number;   // so sánh asc/desc
  sortValue?: (record) => string | number | Date | null;
};
```

### Độ rộng cột (width)

- Nếu cần giữ độ rộng cố định cho một cột, đặt `width` trong cấu hình cột.
- Cột có `width` sẽ ưu tiên theo giá trị này; các cột còn lại vẫn tự đo theo nội dung.

```tsx
const columns: DataTableColumn<User>[] = [
  { key: 'name', title: 'Name', dataIndex: 'name', width: 180 },
  { key: 'email', title: 'Email', dataIndex: 'email' },
];
```

### Bật/tắt nhanh

- Sort: set `sortable: true` (hoặc `sorter`/`sortValue`); muốn tắt hẳn thì `sortable: false`.
- Checkbox: truyền `rowSelection` để hiện; bỏ `rowSelection` để ẩn.
- Actions column: truyền `renderActions` để hiện; bỏ `renderActions` để ẩn.
- Pagination: truyền `pagination` để hiện; bỏ `pagination` để ẩn.
- Page size dropdown: truyền `pagination.onPageSizeChange` (kèm `pageSizeOptions` nếu cần).

### Sort (client)

Chỉ cột có `sortable`, `sorter`, hoặc `sortValue` mới hiện icon sort. Nếu muốn tắt hẳn, đặt `sortable: false`. Thứ tự click: `asc` -> `desc` -> bỏ sort.

```tsx
const columns: DataTableColumn<User>[] = [
  { key: 'name', title: 'Name', dataIndex: 'name', sortable: true },
  {
    key: 'createdAt',
    title: 'Created',
    dataIndex: 'createdAt',
    sortValue: (row) => new Date(row.createdAt),
  },
  {
    key: 'status',
    title: 'Status',
    sorter: (a, b) => a.status.localeCompare(b.status),
  },
];
```

### Sort (server)

Khi gọi API để sort/paginate, dùng `sortMode="server"` để DataTable không tự sort dữ liệu. `onSortChange` trả về `{ key, direction } | null`, bạn tự gọi API và set data mới.

```tsx
const [sort, setSort] = useState<DataTableSortState | null>(null);

<DataTable
  data={rows}
  columns={columns}
  rowKey="id"
  sort={sort}
  sortMode="server"
  onSortChange={setSort}
/>;
```

### Checkbox chọn dòng

Checkbox chỉ hiện khi truyền `rowSelection`. Không truyền thì sẽ ẩn.

```tsx
<DataTable
  data={rows}
  columns={columns}
  rowKey="id"
  rowSelection={{
    selectedRowKeys,
    onChange: setSelectedRowKeys,
  }}
/>;
```

### Pagination + chọn page size

Nếu truyền `onPageSizeChange`, DataTable sẽ hiện dropdown chọn số dòng mỗi trang.

```tsx
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);

<DataTable
  data={rows}
  columns={columns}
  rowKey="id"
  pagination={{
    page,
    pageSize,
    total,
    onChange: setPage,
    pageSizeOptions: [10, 20, 50, 100],
    onPageSizeChange: (size) => {
      setPageSize(size);
      setPage(1);
    },
  }}
/>;
```

### Actions + click row

```tsx
<DataTable
  data={rows}
  columns={columns}
  rowKey="id"
  onRowClick={(row) => console.log(row)}
  renderActions={(row) => <ActionMenu row={row} />}
/>;
```

### Loading + empty

```tsx
<DataTable
  data={rows}
  columns={columns}
  rowKey="id"
  loading={isLoading}
  emptyText="No data"
/>;
```

### Sticky header khi scroll

Nếu muốn header dính khi scroll trang, không đặt `overflow` ở container cha. Nếu muốn scroll trong bảng, đặt `max-height` + `overflow-auto` cho DataTable.

```tsx
<DataTable className="max-h-[60vh] overflow-auto" ... />
```

### Scroll ngang trên màn nhỏ

DataTable tự hỗ trợ scroll ngang khi nội dung vượt chiều rộng (đặc biệt trên màn nhỏ). Không cần cấu hình thêm.

### Lưu ý

- `rowKey` phải ổn định và unique, đặc biệt khi dùng `rowSelection`.
- Nếu truyền `sort` (controlled), bạn cần cập nhật lại state trong `onSortChange`.
- `sortMode="server"` chỉ đổi trạng thái UI, dữ liệu phải tự fetch/sort từ API.
- `pagination.onChange` dùng page bắt đầu từ 1.
- Dropdown page size chỉ hiện khi có `pagination.onPageSizeChange`.

## LineClampTooltip

Dùng khi cần hiển thị tối đa N dòng, nếu text bị cắt sẽ hiện tooltip đầy đủ khi hover.

```tsx
import { LineClampTooltip } from '@siraya/scope-ui'

<LineClampTooltip
  text={record.apiEndpoint}
  lineClamp={3}
  className="w-[320px]"
  side="top"
  wrap
  portal
/>
```

## Tuỳ chỉnh theme

- Dùng `npx scope-ui-init --list-themes` để xem preset được support.
- Dùng `npx scope-ui-init --theme sunset` để generate `AGENTS.md`, `src/styles/ui-theme.css` và `src/layout-presets/workspace-admin-v1.ts` ở repo consumer.
- Nếu cần theme runtime hoặc metadata cho AI/dev tool, import từ `@siraya/scope-ui/core`.
- Các lớp `ui-*` đã được namespaced để tránh va chạm với CSS hiện tại.

## Gợi ý tích hợp

- Import `@siraya/scope-ui/styles.css` ở host để các remote có thể tái sử dụng cùng theme.
- Nếu muốn kết hợp với Tailwind/shadcn gốc, giữ nguyên class `ui-*` và thêm `content` trỏ tới `packages/ui/src/**/*` trong `tailwind.config`.