npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@finhay-pro/leaf-design-system

v0.1.8

Published

React component library của Finhay Leaf Design System

Readme

@finhay-pro/leaf-design-system

Design system của Finhay: 2 bộ token — Color và Typography (CSS variables trong @finhay/leaf-tokens), class typography / màu dùng sẵn và component (React + HTML tĩnh dùng chung markup). Màu và chữ luôn đọc từ token; spacing / radius / độ dày viền không phải token của Leaf — component tự khai báo trong CSS của nó, app dùng Tailwind hoặc CSS của page.

Leaf không có class layout / spacing / responsive. Phần đó thuộc về app, theo 2 cách dùng:

| Loại project | Leaf cung cấp | Layout · spacing · responsive · hover | | ------------------- | ------------------------------------------ | ------------------------------------- | | Next.js (có bundle) | Token + class typography / màu + component | Tailwind CSS v4 | | Landing page tĩnh | Bản CDN (1 <link> + 1 <script>) | CSS riêng của page |

Xem Dùng với Next.js + Tailwind và HTML tĩnh (CDN).

Cài đặt

pnpm add @finhay-pro/leaf-design-system

Import CSS (bắt buộc, làm một lần ở entry của app)

Class chỉ đọc var(--…), không tự khai báo giá trị — nên luôn import tokens.css trước, rồi chọn phần class cần dùng.

| Import | Nội dung | | -------------------------------------------------- | ------------------------------------------------------------------------------- | | @finhay-pro/leaf-design-system/tokens.css | Chỉ token (primitive + semantic màu + typography), không class. | | @finhay-pro/leaf-design-system/typography.css | Chỉ class typography. | | @finhay-pro/leaf-design-system/color.css | Chỉ class color. | | @finhay-pro/leaf-design-system/button.css | Style của component Button. | | @finhay-pro/leaf-design-system/tabs.css | Style của component Tabs. | | @finhay-pro/leaf-design-system/dialog.css | Style của component Dialog. | | @finhay-pro/leaf-design-system/logo.css | Style của component Logo. | | @finhay-pro/leaf-design-system/logo-assets.css | SVG data-URI cho Logo HTML tĩnh — không nằm trong classes.css / styles.css. | | @finhay-pro/leaf-design-system/skeleton.css | Style của component Skeleton. | | @finhay-pro/leaf-design-system/spinner.css | Style của component Spinner. | | @finhay-pro/leaf-design-system/toggle.css | Style của component Toggle. | | @finhay-pro/leaf-design-system/slider.css | Style của component Slider. | | @finhay-pro/leaf-design-system/tooltip.css | Style của component Tooltip. | | @finhay-pro/leaf-design-system/radio.css | Style của component Radio / RadioGroup. | | @finhay-pro/leaf-design-system/checkbox.css | Style của component Checkbox. | | @finhay-pro/leaf-design-system/input.css | Style của component Input (field / inline, dùng chung cho InputDate). | | @finhay-pro/leaf-design-system/input-otp.css | Style của component InputOtp. | | @finhay-pro/leaf-design-system/input-stepper.css | Style của component InputStepper. | | @finhay-pro/leaf-design-system/calendar.css | Style của component Calendar (cần kèm button.css cho footer). | | @finhay-pro/leaf-design-system/input-date.css | Popover của InputDate (cần kèm input.css + calendar.css + button.css). | | @finhay-pro/leaf-design-system/toast.css | Style của Toaster / ToastContent. | | @finhay-pro/leaf-design-system/dropdown.css | Style của component Dropdown. | | @finhay-pro/leaf-design-system/classes.css | Tất cả class + component, không token. | | @finhay-pro/leaf-design-system/styles.css | Token + tất cả class + component. Một dòng là đủ. |

Import bằng CSS:

/* 1. Token — luôn đứng đầu */
@import "@finhay-pro/leaf-design-system/tokens.css";

/* 2. Class typography + màu */
@import "@finhay-pro/leaf-design-system/typography.css";
@import "@finhay-pro/leaf-design-system/color.css";

/* 3. Component nào dùng thì import cái đó */
@import "@finhay-pro/leaf-design-system/button.css";
@import "@finhay-pro/leaf-design-system/tabs.css";
@import "@finhay-pro/leaf-design-system/dialog.css";
@import "@finhay-pro/leaf-design-system/logo.css";
@import "@finhay-pro/leaf-design-system/skeleton.css";
@import "@finhay-pro/leaf-design-system/spinner.css";
@import "@finhay-pro/leaf-design-system/toggle.css";
@import "@finhay-pro/leaf-design-system/slider.css";
@import "@finhay-pro/leaf-design-system/tooltip.css";
@import "@finhay-pro/leaf-design-system/radio.css";
@import "@finhay-pro/leaf-design-system/checkbox.css";
@import "@finhay-pro/leaf-design-system/input.css";
@import "@finhay-pro/leaf-design-system/input-otp.css";
@import "@finhay-pro/leaf-design-system/input-stepper.css";
@import "@finhay-pro/leaf-design-system/calendar.css";
@import "@finhay-pro/leaf-design-system/input-date.css";
@import "@finhay-pro/leaf-design-system/toast.css";
@import "@finhay-pro/leaf-design-system/dropdown.css";

Muốn gọn thì một dòng @import "@finhay-pro/leaf-design-system/styles.css"; thay cho cả khối trên.

CSS của component tự đủ về layout (display, flex, gap, padding, radius, viền…): chỉ cần tokens.css + typography.css + CSS của component là component hiển thị đúng. Riêng Calendar gắn thêm class màu (bg-secondary, border-tertiary…) nên cần cả color.css. App đang giữ bộ màu riêng có thể bỏ color.css (trừ khi dùng Calendar).

Dùng với Next.js + Tailwind

Leaf lo token, typography, màu và component; Tailwind CSS v4 lo layout, spacing, radius, độ dày viền, responsive và state (hover:, md:…). Cấu hình mẫu trong globals.css (site docs apps/docs dùng đúng cấu hình này):

@layer theme, base, components, utilities;

/* Leaf trước Tailwind utilities → class Tailwind override được class màu Leaf khi cần */
@import "@finhay-pro/leaf-design-system/styles.css";
@import "tailwindcss";

/* Màu và cỡ chữ chỉ lấy từ Leaf — tắt palette và thang font-size của Tailwind */
@theme {
    --color-*: initial;
    --text-*: initial;
}
import { Button } from "@finhay-pro/leaf-design-system";

<section className="bg-secondary border-quaternary flex flex-col gap-3 rounded-xl border p-4 md:p-6">
    <h3 className="heading-3 text-primary">Tích luỹ</h3>
    <p className="body-4 text-secondary hover:text-(color:--text-highlight)">Lợi nhuận 8.5%/năm</p>
    <Button className="w-full md:w-auto">Đầu tư</Button>
</section>;
  • Class typography / màu Leaf giữ nguyên tên (body-3, text-secondary, bg-secondary, border-quaternary…). Giá trị lẻ dùng token qua cú pháp biến của Tailwind: bg-(--base-tertiary), text-(color:--text-highlight).
  • Spacing / radius / độ dày viền dùng thang của Tailwind (base 4px). Thang px theo Figma:

| Figma | px | Tailwind | | ----------------- | -------------------------------------------------------- | ------------------------------------------------------------------------ | | Spacing 2XS…7XL | 2 · 4 · 8 · 12 · 16 · 24 · 36 · 48 · 60 · 84 · 120 · 240 | 0.5 1 2 3 4 6 9 12 15 21 30 60 (p-4, gap-3…) | | Radius S/M/L/Full | 4 · 8 · 12 · full | rounded-sm / rounded-lg / rounded-xl / rounded-full | | Border S/M/L/XL | 0.5 · 1 · 1.5 · 2 | border-[0.5px] / border / border-[1.5px] / border-2 |

  • Override CSS của component bằng className Tailwind: utility (@layer utilities) luôn thắng CSS component (@layer components), không cần !important.

Typography class

Tên class trùng đúng tên token trong Figma:

| Nhóm | Class | | ------- | ------------------------------------------------------------- | | Display | display-1, display-2, display-3 | | Heading | heading-1, heading-2, heading-3 | | Body | body-1 / body-1-highlight … body-5 / body-5-highlight |

Body có hai phiên bản: tên trần là văn bản thường, hậu tố -highlight dùng khi cần nhấn mạnh.

| Class | font-size | line-height | font-weight | | ----------------------------- | --------- | ----------- | ----------- | | display-1 | 122px | 134px | 700 | | display-2 | 76px | 88px | 600 | | display-3 | 60px | 72px | 600 | | heading-1 | 36px | 48px | 600 | | heading-2 | 28px | 36px | 600 | | heading-3 | 24px | 32px | 600 | | body-1 / body-1-highlight | 20px | 30px | 400 / 600 | | body-2 / body-2-highlight | 18px | 28px | 400 / 600 | | body-3 / body-3-highlight | 16px | 24px | 400 / 600 | | body-4 / body-4-highlight | 14px | 20px | 400 / 600 | | body-5 / body-5-highlight | 12px | 16px | 400 / 600 |

Cách dùng

Chọn thẻ theo ngữ nghĩa nội dung, chọn class theo hình thức mong muốn — hai việc độc lập nhau, nên một h1 hoàn toàn có thể hiển thị bằng style display-3.

<h1 class="heading-1">Danh mục đầu tư</h1>
<p class="body-3">Cập nhật lần cuối 5 phút trước.</p>
<label class="body-4-highlight" for="amount">Số tiền đầu tư</label>

<!-- Vẫn là h1 để screen reader và SEO hiểu đúng cấp bậc, nhưng to như display -->
<h1 class="display-3">Finhay</h1>

Trong React / Next.js, class đi thẳng vào className của bất kỳ element hay component nào:

<h2 className="heading-2">Tài sản của bạn</h2>

// Class typography lo cỡ chữ, color lo màu; khoảng cách là của app (Tailwind `mt-2`)
<p className="body-3 text-secondary mt-2">Lợi nhuận kỳ vọng 8.5%/năm</p>

// Gắn vào component của thư viện khác cũng chỉ là một chuỗi class
<Link href="/tai-khoan" className="body-3-highlight">
    Tài khoản
</Link>

Color class

Tiền tố nói thuộc tính, phần sau nói vai trò của token: text-* cho màu chữ, bg-* cho màu nền, border-* cho màu viền. Ba bộ dùng chung một hậu tố nên nhớ một bộ là suy ra được cả ba — text-red / bg-red / border-red, text-primary / bg-primary / border-primary. Nhóm system bỏ chữ system trong tên class (token vẫn là --system-red), còn border-* lấy màu từ thang base vì viền luôn là màu bề mặt. Class chỉ trỏ vào token nên tự đổi theo theme, không cần viết thêm gì cho light mode.

| Nhóm | Class | | ---------------- | ------------------------------------------------------------------------------------------------------ | | Text color | text-primary, text-secondary, text-tertiary, text-quaternary, text-highlight | | Background | bg-primary … bg-quinary, bg-highlight | | Border color | border-primary … border-quinary (màu của thang base), border-highlight | | System (đủ 3 bộ) | text-*, bg-*, border-* với red, green, yellow, orange, blue, purple (+ -secondary) |

<!-- Viền: class màu chỉ set border-color — độ dày / padding do app (Tailwind hoặc CSS của page) -->
<div class="bg-secondary border-quaternary card">
    <h3 class="heading-3 text-primary">Tích luỹ</h3>
    <p class="body-4 text-secondary">Lợi nhuận kỳ vọng 8.5%/năm</p>
    <p class="body-4 text-red">Số dư không đủ</p>
</div>

<!-- Dark là mặc định; light chỉ cần một attribute, class giữ nguyên -->
<html data-theme="light"></html>

Cần biết

  • Class typography chỉ set đúng 3 thuộc tính (font-size, line-height, font-weight); class color chỉ set đúng 1 (color / background-color / border-color). Font family, letter spacing… nằm ngoài phạm vi — app tự setup ở tầng global (body { font-family: var(--font-sans) }, CSS reset…).
  • Leaf không có class layout / spacing / radius / border width / responsive. Next.js dùng Tailwind, landing page dùng CSS của page. Leaf cũng không có token spacing / radius / độ dày viền — chỉ có token màu và typography.
  • Typography và CSS component nằm trong @layer components; class màu nằm trong @layer utilities (đè được CSS component, vd. leaf-spinner text-highlight). Utility của app (Tailwind) và CSS không layer (CSS / SCSS module của app) đều override được, không cần !important.
  • Mỗi file CSS mở đầu bằng @layer theme, base, components, utilities; — trùng thứ tự layer của Tailwind v4, nên import file theo thứ tự nào thì class màu vẫn đứng trên CSS component. Component không trông vào CSS reset: tự reset button, tự xử lý [hidden].
  • Class không có biến thể hover: / focus: / opacity — nó là CSS thuần. Cần state thì dùng Tailwind (hover:text-(color:--text-highlight)) hoặc CSS của page với cùng token.
  • Chỉ semantic token mới có class màu. Primitive (--neutral-500, --brand-300…) là nguồn nội bộ, dùng thẳng trong UI là mất khả năng đổi theme. Spacing / border-width / radius là thang kích thước cố định, không đổi theo theme.
  • Tên class không có prefix. Đây là những tên khá phổ thông, nên app dùng Leaf cần cân nhắc kỹ nếu load kèm CSS framework khác (Bootstrap cũng định nghĩa .display-1–.display-6). Với Tailwind v4: tắt palette (--color-*: initial) để text-* / bg-* / border-* chỉ còn class màu của Leaf.
  • Typography token hiện chỉ có bộ Desktop. Chưa có responsive type set, nên các style Display cần app tự xử lý ở breakpoint nhỏ.

Phát triển

pnpm --filter @finhay-pro/leaf-design-system build      # tsc → dist/ + gom CSS + bản CDN
pnpm --filter @finhay-pro/leaf-design-system dev        # gom CSS + tsc --watch
pnpm --filter @finhay-pro/leaf-design-system typecheck

CSS trong dist được sinh bởi scripts/build-css.mjs (mỗi src/**/*.css ra một file riêng, nội tuyến token vào tokens.css / styles.css). Sau đó scripts/build-cdn.mjs sinh bản CDN dist/cdn/leaf-<version>.min.{css,js}. Không sửa file trong dist.

build ghi đè dist tại chỗ, không xoá dist trước (chỉ prepublishOnly mới clean): turbo run dev luôn build package trước docs, và nếu dist biến mất dù chỉ một lúc thì Turbopack cache luôn lỗi resolve @finhay-pro/leaf-design-system/…css. Gặp lỗi "no valid target file was found" khi chạy docs: xoá apps/docs/.next/dev rồi chạy lại pnpm dev.

Mọi hình thức nằm trong CSS và chỉ đọc attribute (data-*, aria-*, :disabled), kể cả typography của label. Component React chỉ gắn attribute, không gắn class theo prop — nhờ vậy HTML tĩnh viết cùng markup là ra cùng kết quả. Attribute thiếu → CSS lấy mặc định giống prop React.

CSS component tự đủ: layout (display / flex / grid / gap / padding / radius / viền…) nằm trong <tên>.css của component; markup chỉ gắn class leaf-* (+ class typography / màu khi cần). Phần tử nào CSS set display thì kèm rule [hidden] { display: none }.

Thêm bộ class mới (typography / màu): tạo src/styles/<tên>.css — script tự sinh dist/<tên>.css và gom vào classes.css + styles.css; chỉ cần thêm một dòng "./<tên>.css" vào exports của package.json. Thêm component React (khi thật sự cần state/behavior, không phải để bọc style): tạo src/components/<Tên>/ (kèm <tên>.css nếu cần) rồi re-export trong src/index.ts. CSS của component cũng ra dist/<tên>.css và được gom vào classes.css / styles.css — nhớ thêm dòng "./<tên>.css" vào exports. Tên file CSS phải duy nhất trong cả src/ (script báo lỗi nếu trùng). Component có hành vi (Tabs · Toggle · Dialog · Slider · Tooltip · InputOtp · InputStepper · Toast · Dropdown) cần thêm bản vanilla cho HTML tĩnh trong src/vanilla/ và gắn vào window.Leaf qua src/vanilla/index.ts. src/vanilla chỉ vào bản CDN, không vào bản npm.

Components

| Export | Mô tả | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Button | Khung icon · label · icon — bật/tắt từng phần; width / height; onClick trả Promise → tự loading (spinner thay leading icon, khoá click), loading controlled | | Tabs | Tabs Items — items[] (value · label: ReactNode · disabled); phím ← → Home End | | Dialog | Panel + backdrop blur — open (controlled), title / onBack / onClose(reason), dismissible, closeLabel / backLabel, theme, maxWidth / height / maxHeight | | Logo | Wordmark FHSC · Finhay · FHC — brand / mode / side / size | | Skeleton | Placeholder shimmer — width / height / border (radius), nền --base-tertiary | | Spinner | Indicator 8 nan — animation (spin · fade · pulse · breathe), size | | Toggle | Công tắc On/Off — size md/sm, checked, disabled (Figma Active Off); name / value → input ẩn cho form | | Slider | Kéo từng step trong min–max (mặc định 0–100, bước 1) · mốc steps · labels; name → input ẩn | | Tooltip | Hover/focus/Escape — size, position, align, onOpenChange; trigger="click", openDelay / closeDelay, tự lật phía khi thiếu chỗ, theme; kèm TooltipContent | | Radio / RadioGroup | Radio đơn + nhóm — value / defaultValue / onValueChange, name cho form, disabled, orientation | | Checkbox | Checkbox — checked / defaultChecked / onCheckedChange (checked="indeterminate" cho trạng thái lưng chừng), label, disabled; input native nên đi thẳng vào FormData | | Input | Input dynamic — variant field/inline, shape default/pill, slot label · leadingIcon · trailingIcon · action · message, invalid, revealPassword; as="button" làm trigger | | InputOtp | Ô OTP — length, mask, dán cả mã, onComplete; name → input ẩn | | InputStepper | Nhập số với −/+ — tone buy/sell, step / min / max / precision, locale, unit; phím ↑ ↓, chốt khi blur/Enter | | Calendar | Lịch tháng (port từ vnsc) — chọn ngày / giờ (showTime, timeOnly), minDate / maxDate / disablePast / disableFuture / disableDate, luật giờ (minHour / maxHour / minuteStep…), weekStartsOn, labels; dùng độc lập được | | InputDate | Input trigger + popover Calendar — value chốt khi bấm "Chọn" (hoặc confirmOnSelect), formatDisplay, align, tự lật phía; name → input ẩn ISO | | Toaster / toast | Toast success / error / warning — toast.success(title, { description, duration }), duration: -1 không tự đóng, dừng khi hover / focus, toast.dismiss(id); <Toaster position closeLabel theme /> đặt một lần; kèm ToastContent | | Dropdown | Chọn một giá trị — variant list / radio, trigger field / text, appearance filled / outlined, size md / sm, openOn click / hover, align, readOnly, maxHeight; option leading / disabled; phím ↑ ↓ Home End + typeahead | | LanguageDropdown | Chọn ngôn ngữ Tiếng Việt / English kèm cờ (CDN Finhay) — danh sách ở LEAF_LANGUAGES, value kiểu LeafLanguageCode; mặc định size="sm", openOn="hover", align="end" |

/* Ngoài tokens.css + typography.css, mỗi component cần CSS riêng của nó */
@import "@finhay-pro/leaf-design-system/button.css";
@import "@finhay-pro/leaf-design-system/tabs.css";
@import "@finhay-pro/leaf-design-system/dialog.css";
@import "@finhay-pro/leaf-design-system/logo.css";
@import "@finhay-pro/leaf-design-system/skeleton.css";
@import "@finhay-pro/leaf-design-system/spinner.css";
@import "@finhay-pro/leaf-design-system/toggle.css";
@import "@finhay-pro/leaf-design-system/slider.css";
@import "@finhay-pro/leaf-design-system/tooltip.css";
import {
    Button,
    Tabs,
    Dialog,
    Logo,
    Skeleton,
    Spinner,
    Toggle,
    Slider,
    Tooltip,
} from "@finhay-pro/leaf-design-system";

<Button priority="primary" size="md" leadingIcon={<PlusIcon />} trailingIcon={<PlusIcon />}>
    Label
</Button>
{/* Chỉ icon → layout tròn */}
<Button aria-label="Thêm" leadingIcon={<PlusIcon />} />
<Button size="md" leadingIcon={<Spinner size="md" />}>
    Mua AAA
</Button>

<Skeleton height={48} border={12} />

<Tabs
    variant="buy"
    defaultValue="buy"
    items={[
        { value: "buy", label: "Mua 1.2%" },
        { value: "idle", label: "Mua" },
    ]}
/>

HTML tĩnh (CDN)

Cho landing page / trang dựng tay không có bundler: Leaf CDN + CSS riêng của page. 1 <link> + 1 <script> là dùng được toàn bộ class typography, color và mọi component (CSS). Script chỉ cần khi dùng hành vi Tabs · Toggle · Dialog · Slider · Tooltip · InputOtp · InputStepper · Toast · Dropdown (data-leaf-*). Layout, spacing, responsive của page viết trong CSS của page, dùng token Leaf.

<link rel="stylesheet" href="https://<cdn>/leaf-0.1.8.min.css" />
<script src="https://<cdn>/leaf-0.1.8.min.js" defer></script>

<style>
    /* CSS của page — bọc trong @layer để không đè nhầm CSS component (xem "Cần biết") */
    @layer base {
        body {
            margin: 0;
            font-family: var(--font-sans);
            background: var(--base-primary);
        }
    }

    @layer page {
        .hero {
            display: flex;
            flex-direction: column;
            gap: 16px;
            padding: 48px 16px;
        }

        @media (min-width: 768px) {
            .hero {
                flex-direction: row;
            }
        }
    }
</style>

<section class="hero">
    <h1 class="display-3 text-primary">Finhay</h1>
    <button class="leaf-button" data-size="md">Mở tài khoản</button>
</section>

| File | Nội dung | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | leaf-<version>.min.css | styles.css đã minify: token + typography + color + CSS của mọi component. | | leaf-<version>.min.js | Hành vi Tabs · Toggle · Dialog · Slider · Tooltip · InputOtp · InputStepper · Toast · Dropdown → window.Leaf. Trang CSS-only có thể bỏ. |

Link thật (do tool admin CDN trả về) nằm ở trang docs Develop → Web. Trang demo đủ mọi biến thể: examples/static.html (mở bằng file:// sau khi build).

Button

Không cần JS. Thiếu attribute nào thì lấy mặc định giống prop React.

| Attribute | Giá trị | Mặc định | | ---------------------------- | --------------------------------------------- | --------- | | data-priority | primary secondary tertiary ghost | primary | | data-size | lg md sm | lg | | data-status | normal destruction | normal | | data-icon-only | true — chỉ có icon → tròn, cần aria-label | — | | disabled / aria-disabled | native / "true" | — | | data-loading | true — Leaf.button.setLoading / run gắn | — |

<button class="leaf-button">Tiếp tục</button>
<button class="leaf-button" data-priority="secondary" data-size="md">Secondary</button>

Loading (cần leaf.js): Leaf.button.run(el, promise) bật loading tới khi promise settle, Leaf.button.setLoading(el, true | false) tự bật / tắt — spinner thay leading icon, aria-busy, khoá min-width, nút đang loading không nhận click (kể cả handler của trang).

Tabs

Đánh dấu tablist bằng data-leaf-tabs (opt-in). Leaf.tabs · event leaf:tabs-change.

Toggle

data-leaf-toggle trên .leaf-toggle. Leaf.toggle.set / toggle · leaf:toggle-change.

Dialog

data-leaf-dialog trên .leaf-dialog-backdrop. Mở bằng data-leaf-dialog-open="id" hoặc Leaf.dialog.open. Đóng: Escape / backdrop / data-leaf-dialog-close / Leaf.dialog.close(el, reason?) — leaf:dialog-close mang detail.reason (close · backdrop · escape · api). data-leaf-dialog-dismissible="false" tắt đóng bằng backdrop / Escape. Back: data-leaf-dialog-back → leaf:dialog-back (không tự đóng). Khi mở, các con khác của <body> được đặt inert — đặt dialog tĩnh là con trực tiếp của <body>.

Slider

data-leaf-slider trên .leaf-slider + data-min / data-max / data-step / data-value (mặc định 0 / 100 / 1 / min). Leaf.slider.set(el, value) · leaf:slider-input (liên tục khi kéo) · leaf:slider-change (khi chốt). Sync --leaf-slider-fill (fill + thumb) / data-state mốc; thiếu .leaf-slider__thumb thì leaf.js tự tạo. Có input.leaf-slider__input[type=hidden] trong markup thì leaf.js ghi giá trị đã chốt vào đó (form). Toggle tương tự với input.leaf-toggle__input — disabled khi Off, giống checkbox.

Tooltip

Bubble tĩnh: .leaf-tooltip + data-size / data-position / data-align — không cần JS. Hover/focus: .leaf-tooltip-trigger + data-leaf-tooltip + data-content (hoặc data-tooltip); data-leaf-tooltip-trigger="click" để bấm mở/đóng, data-focusable khi trigger là icon thuần (không có phần tử focus được — leaf.js gắn tabindex / role="button"), data-open-delay / data-close-delay (ms), data-flip="false" tắt tự lật phía khi thiếu chỗ. Bubble kế thừa data-theme của khối bọc trigger.

Logo

Span rỗng .leaf-logo + data-brand / data-mode / data-side + --leaf-logo-size. CSS gắn SVG data-URI — không cần JS. Bản CDN đã gộp sẵn data-URI; dùng qua npm thì import thêm logo-assets.css (không nằm trong classes.css / styles.css vì component React không cần).

Radio / Checkbox / Input

Chỉ CSS — markup như tab Code → HTML tĩnh. Input đọc data-variant / data-shape / data-invalid / data-disabled trên .leaf-input.

InputOtp

data-leaf-input-otp trên .leaf-input-otp (các ô .leaf-input-otp__cell). leaf.js lo nhảy ô, dán, Backspace; Leaf.inputOtp.set / get · leaf:input-otp-change · leaf:input-otp-complete. Có input[type=hidden] trong markup thì leaf.js ghi mã vào đó.

InputStepper

data-leaf-input-stepper trên .leaf-input-stepper + data-step / data-precision / data-locale; min / max đọc từ aria-valuemin / aria-valuemax của input. Nút data-action="decrement|increment". Leaf.inputStepper.set / get · leaf:input-stepper-change.

Toast

Leaf.toast.success | error | warning(title, { description, duration, id }) (hoặc Leaf.toast.show({ status, title, … })) trả id; Leaf.toast.dismiss(id?) đóng (không truyền → đóng hết); Leaf.toast.configure({ position, closeLabel, label, theme }). Vùng chứa .leaf-toaster[data-leaf-toaster] có sẵn trong trang thì dùng, không thì leaf.js tự tạo. Toast viết tay: .leaf-toast[data-leaf-toast] + data-status — nút .leaf-toast__close và thanh progress chạy hết đều đóng toast. Event leaf:toast-show / leaf:toast-dismiss.

Dropdown

data-leaf-dropdown trên .leaf-dropdown (markup như tab Code → HTML tĩnh) + data-variant / data-trigger / data-appearance / data-size / data-align; data-open-on="hover" để rê chuột mở. Option .leaf-dropdown__option[data-value], mục chọn aria-selected="true", disabled aria-disabled="true". Leaf.dropdown.open / close / select(el, value) · leaf:dropdown-change. Có input.leaf-dropdown__input thì leaf.js ghi value vào đó. Panel nằm ngay dưới trigger (không portal) — tránh đặt trong khung overflow: hidden / auto thấp hơn panel.

Skeleton / Spinner

Chỉ CSS. Skeleton: --leaf-skeleton-width / height / radius. Spinner: data-animation / data-size + SVG path; overlay = .leaf-spinner-overlay (app tự hidden/mount).

Cần biết

  • Link gắn theo version. Mỗi bản phát hành là một link mới; trang cũ giữ link cũ nên không tự vỡ. Muốn lên bản mới thì đổi cả link CSS lẫn JS cùng một version, đọc Changelog trước.
  • Không kèm font và CSS reset. Tự load Inter (Google Fonts hoặc self-host) và set body { font-family: var(--font-sans) }.
  • CSS của trang phải nằm trong @layer nếu đụng vào element của component. Style component nằm trong @layer components, mà CSS không layer luôn thắng CSS có layer bất kể specificity — reset kiểu * { padding: 0 } hay button { background: none } đặt trần sẽ xoá padding / nền của .leaf-button. Bọc reset trong @layer base { … }, layout của page trong một layer riêng (vd. @layer page, khai báo sau utilities nếu muốn đè cả class màu).
  • Leaf không có class layout / spacing (flex, gap-*, p-*… đã bỏ từ 0.1.8). Markup component chỉ cần class leaf-* (+ typography / màu); markup cũ còn class layout vẫn hiển thị y hệt — các class đó giờ không có tác dụng.
  • Trình duyệt tối thiểu (CSS dùng @layer): Chrome / Edge 99+, Safari 15.4+, Firefox 97+.
  • Dark là mặc định; data-theme="light" trên <html> để chuyển light.
  • z-index của lớp phủ đọc --leaf-z-dropdown (30) / --leaf-z-dialog (50) / --leaf-z-spinner-overlay (100) / --leaf-z-tooltip (110) / --leaf-z-toast (120) — override trên :root để chèn lớp của trang vào giữa.
  • Chi tiết attribute / markup từng component: tab Code → HTML tĩnh trên docs.

Phát hành bản CDN

  1. Bump version trong packages/react/package.json, ghi thay đổi vào Changelog.
  2. pnpm --filter @finhay-pro/leaf-design-system build → dist/cdn/leaf-<version>.min.css + .min.js.
  3. Đổi version trong examples/static.html, mở bằng file:// để kiểm tra nhanh.
  4. Cập nhật leafCdnVersion trong apps/docs/config/shared.ts (cùng version). Docs sync file vào public/cdn/ lúc build và serve trên domain Vercel.
  5. Deploy docs (npx vercel --prod từ root) để link CDN mới lên production. Landing đang dùng version cũ giữ nguyên link cũ (Finhay CDN hoặc bản Vercel trước đó nếu còn cache/host).

Changelog

0.1.8

Breaking

  • Bỏ class layout / spacing / border width / radius khỏi Leaf: xoá spacing.css, border.css, flex.css, grid.css, sizing.css, overflow.css, position.css, text-layout.css (cả export npm lẫn trong classes.css / styles.css / CDN). Next.js dùng Tailwind (xem Dùng với Next.js + Tailwind), landing page dùng CSS của page. Trang dựng layout bằng các class này (flex, gap-m, p-l, radius-l…) phải giữ link CDN 0.1.7 hoặc chuyển sang CSS riêng trước khi lên 0.1.8.
  • Bỏ token --spacing-*, --border-width-*, --radius-* (file spacing.css / border.css của @finhay/leaf-tokens). Leaf chỉ còn 2 bộ token: Color và Typography. CSS component khai báo giá trị px trực tiếp (giá trị không đổi); app đang đọc các biến này phải tự khai báo lại hoặc chuyển sang Tailwind.

Thay đổi

  • CSS component tự đủ trở lại: layout nằm trong <component>.css, markup React / leaf.js chỉ gắn class leaf-* (+ typography / màu). Import riêng tokens.css + typography.css + <component>.css là đủ (Calendar cần thêm color.css). Giao diện không đổi; markup HTML tĩnh cũ (còn class layout) vẫn hiển thị y hệt.
  • Calendar: thêm class BEM cho các khối trước đây chỉ có class layout (__heading, __months-header, __months-grid, __weekdays, __weekday, __time-group, __select-wrap, __footer).
  • Dialog: truncate của title nằm trong dialog.css (.leaf-dialog__start > h2 | p) — markup không cần class min-w-0 truncate.
  • Docs: dùng Leaf + Tailwind v4 (bỏ SCSS module); bỏ trang Spacing, Border, Flex, Grid, Sizing, Overflow, Position, Text.

Mới

  • Toaster / toast / ToastContent — toast success · error · warning (port từ vnsc). Màu từ token có sẵn: nền --system-green|red|yellow-secondary, viền / icon / progress --text-highlight · --system-red · --system-yellow. Toast gắn data-theme="dark" mặc định (nền thang 800 ở cả hai theme). Tự đóng sau duration (mặc định 4000ms, -1 không tự đóng), dừng khi hover / focus. CSS toast.css; leaf.js Leaf.toast.
  • Dropdown — chọn một giá trị, gom các dropdown của vnsc: variant="radio" (dropdown kèm radio ở màn thị trường) và variant="list" (SubAccounts, chọn lệnh) với trigger field filled / outlined, size md / sm, readOnly, openOn="hover". Bàn phím theo WAI-ARIA select-only combobox. CSS dropdown.css; leaf.js data-leaf-dropdown / Leaf.dropdown.
  • LanguageDropdown — Tiếng Việt / English kèm cờ từ CDN Finhay (.leaf-dropdown__flag). Danh sách ngôn ngữ nằm ở LEAF_LANGUAGES; thêm ngôn ngữ mới thì bổ sung trong Leaf.
  • Docs: /components/dropdown, /components/toast.
  • Radio / RadioGroup (Figma 1146:2081) và Checkbox (Figma 891:10191) — input native, đi thẳng vào FormData. CSS: radio.css, checkbox.css.
  • Họ Input (Figma 1146:2159), export chung từ components/Input:
    • Input — field dynamic: bật/tắt từng slot label · icon · action · message; variant field / inline, shape pill, revealPassword, as="button" làm trigger. CSS input.css.
    • InputOtp — nhảy ô, dán cả mã, mask, onComplete. CSS input-otp.css; leaf.js data-leaf-input-otp / Leaf.inputOtp.
    • InputStepper — −/+ tone buy / sell, step / min / max / precision, format theo locale. CSS input-stepper.css; leaf.js data-leaf-input-stepper / Leaf.inputStepper.
    • InputDate — Input + popover Calendar, chốt khi bấm "Chọn". CSS input-date.css (cần kèm input.css + calendar.css + button.css).
  • Calendar — port từ vnsc, chọn ngày / giờ, dùng độc lập được. CSS calendar.css (footer dùng Button → cần button.css).
  • Docs: /components/radio, /components/checkbox, /components/input (đủ 4 dạng, React + HTML tĩnh), /components/calendar.

0.1.7

Breaking

  • Đổi tên class nền base-* → bg-* (bg-primary, bg-red…). Token CSS vẫn là --base-* (và --button-base-* không đổi).
  • Component React/HTML tĩnh dùng class layout Leaf trên markup (flex, w-full…); CSS component bỏ các thuộc tính đã có class tương đương.
  • Hệ quả: dist/<component>.css không còn tự đủ. App import riêng từng component phải import thêm spacing.css (Tabs gap-m, Slider gap-s / gap-xs) và flex.css · sizing.css · overflow.css · position.css · text-layout.css — hoặc dùng classes.css / styles.css. Đừng trông vào class do app tự sinh — class nào app không tự dùng (vd. gap-m, flex-auto) sẽ không có.
  • HTML tĩnh viết theo markup cũ (chỉ có leaf-*) phải thêm class layout như markup mới (tab Code → HTML tĩnh trên docs / examples/static.html).
  • Radius: thang cũ radius-0|xs|s|m|l|xl|2xl (map sang spacing) đổi thành radius-s|m|l|full (4 · 8 · 12px · 9999px). Class radius-0 / radius-xs / radius-xl / radius-2xl bị xoá.

Mới

  • Border width: token --border-width-* (s 0.5 · m 1 · l 1.5 · xl 2px) và class border-s|m|l|xl (+ biến thể cạnh top / right / bottom / left / inline / block) — set width + border-style: solid.

  • Spacing: thêm bậc 2xs (2px) — token --spacing-2xs và class -2xs cho mọi prefix p / m / gap.

  • Layout class: flex.css, grid.css, sizing.css, overflow.css, position.css, text-layout.css — nằm @layer components. Không có responsive / size cố định trong Leaf (app viết trong SCSS module). Docs tách trang: /styles/flex · /styles/grid · /styles/sizing · /styles/overflow · /styles/position · /styles/text (mỗi trang Overview · Usage · Tokens · Code).

  • Display: block · inline-block · inline · hidden (trong flex.css). Spinner / Tooltip vốn đã dùng block trên markup nhưng trước đây Leaf chưa có.

Sửa lỗi

  • Rule [hidden] (component + class layout + line-clamp-*) set display: none vì display giờ đến từ class — thiếu rule này sẽ không ẩn được (vd. backdrop Dialog đã đóng vẫn phủ màn hình).
  • leaf.js: bubble Tooltip và spinner loading của Button do JS tạo thiếu class layout (relative block shrink-0 trên caret, relative z-10 max-w-full text-left trên body, inline-flex … trên spinner) → caret lệch vị trí. Nay khớp markup React; có test parity.
  • Test classes.test.tsx: mọi class (trừ leaf-*) trên markup React / leaf.js phải có trong CSS của Leaf.

0.1.6

  • Dialog: thêm prop height (CSS --leaf-dialog-height) — chiều cao cố định cho content động / nhiều step để panel không giật, body cuộn bên trong; maxHeight vẫn là trần viewport.

Sửa lỗi — hành vi không còn phụ thuộc CSS reset:

  • Button (vanilla): leading icon không ẩn khi Leaf.button.setLoading ở trang không có CSS reset (.leaf-button__icon { display } thắng [hidden]). Thêm [hidden] { display: none } cho root của Button · Tabs · Toggle · Slider · Spinner · Logo · Tooltip trigger.
  • Mọi stylesheet trong dist mở đầu bằng @layer theme, base, components, utilities;: trước đây app import color.css trước CSS component thì text-* không override được component (vd. leaf-spinner text-highlight).
  • .leaf-button / .leaf-tab thêm appearance: none như Toggle / Dialog.
  • Dialog: typography + màu của title (heading-3 · --text-primary) chuyển vào dialog.css; React không gắn class="heading-3 text-primary" nữa. Markup HTML tĩnh cũ vẫn hiển thị y hệt.

0.1.5

Sửa lỗi

  • tokens.css: comment mô tả system color dùng chuỗi */ làm Lightning CSS / Turbopack parse fail (Unexpected token Semicolon). Đổi wording comment, không đổi giá trị token.

0.1.4

  • System color: thêm secondary (--system-*-secondary → primitive */800) và class text|base|border-*-secondary (vd. bg-red-secondary). Giữ nguyên giá trị ở dark và light.

0.1.3

Breaking

  • Loading → Spinner: component, type, class leaf-spinner / leaf-spinner__icon / leaf-spinner-overlay, biến --leaf-spinner-*, file spinner.css, docs /components/spinner. Không giữ alias (CDN gắn version nên landing page cũ không vỡ).
  • onValueChange / onCheckedChange / onOpenChange không gọi lại khi giá trị không đổi (bấm tab đang chọn, phím → khi Slider đã ở max) — khớp bản vanilla vốn chỉ phát leaf:*-change khi thật sự đổi. App nào dựa vào việc bắn lại (vd. bấm tab đang mở để refetch) cần tự xử lý.
  • Slider: label nằm ngoài thứ tự Tab (slider chỉ có một điểm dừng, nhảy mốc bằng PageUp / PageDown); mốc là <span> thay vì <button> — snippet HTML tĩnh đã đổi theo (markup cũ vẫn chạy).
  • Tabs: size chỉ nhận khi variant="subcontent" (TypeScript chặn tổ hợp không có trong Figma).
  • Dialog: maxWidth / height / maxHeight (CSS --leaf-dialog-max-width / --leaf-dialog-height / --leaf-dialog-max-height). Panel luôn width: 100% trong trần rộng; height cố định khi truyền (content động không giật), không thì auto; maxHeight luôn là trần viewport.

Sửa lỗi

  • Dialog: bôi đen text trong panel rồi thả chuột ra nền không còn đóng nhầm (chỉ đóng khi pointerdown cũng bắt đầu trên backdrop) — cả React lẫn leaf.js.
  • Slider: touch-action: none chỉ còn trên track; title / labels cuộn trang được trên mobile.
  • Tabs: tab nhận tabindex="0" đúng ngay trong HTML server (SSR), không đợi effect.
  • Tooltip: không gắn / gỡ listener lại mỗi lần cha render.

Mới

  • Test: Vitest + jsdom (pnpm test). Test parity chạy cùng một bộ assertion trên component React và markup HTML tĩnh (sinh từ renderToStaticMarkup + data-leaf-*) do leaf.js điều khiển.

  • Mọi component nhận ref (React 19, không cần forwardRef).

  • Button: onClick trả Promise → tự loading (spinner thay leading icon, giữ label và màu priority, aria-busy, khoá click, min-width giữ nút không nhảy) tới khi settle; loading controlled. leaf.js: Leaf.button.setLoading / run / isLoading; nút đang loading không nhận click. Cảnh báo dev khi icon-only thiếu aria-label.

  • Dialog: open (controlled — đóng thì giữ DOM + hidden), onClose(reason), dismissible, closeLabel / backLabel, title: ReactNode, theme. leaf.js: data-leaf-dialog-dismissible, leaf:dialog-close có detail.reason, Leaf.dialog.close(el, reason).

  • Tooltip: trigger="click", openDelay / closeDelay, focusable, tự lật phía khi thiếu chỗ (flip), theme. leaf.js: data-leaf-tooltip-trigger, data-focusable, data-open-delay / data-close-delay, data-flip.

  • Slider: root gắn data-min / data-max / data-step / data-value — cùng surface HTML tĩnh.

  • Tabs: label: ReactNode (badge, icon).

  • Toggle / Slider: name (+ value cho Toggle) → <input type="hidden"> đi vào FormData; leaf.js sync input.leaf-toggle__input / input.leaf-slider__input nếu có trong markup.

  • Portal (Dialog, Tooltip, Spinner overlay) kế thừa data-theme của khối bọc chỗ dùng; khi dialog mở, phần còn lại của <body> được inert; z-index qua --leaf-z-dialog / --leaf-z-spinner-overlay / --leaf-z-tooltip (giá trị mặc định không đổi).

  • Button: width/height prop; size MD/SM khớp Figma gap/padding; token --button-disabled-base|content (alias --button-*-disable).

  • Spacing: token --spacing-* (0…7xl, gồm 2xs) + class p-* / m-* / gap-* (Figma 34:1034).

  • Border: token --border-width-* (s…xl) + class border-s|m|l|xl (+ biến thể cạnh); token --radius-* (s|m|l|full) + class radius-*.

  • Dual-support HTML tĩnh: vanilla Leaf.toggle / dialog / slider / tooltip (data-leaf-*); Logo CSS data-URI theo data-brand / data-mode / data-side; docs htmlSection + examples/static.html cover đủ component.

  • Skeleton: placeholder shimmer (width / height / border), nền --base-tertiary.

  • Spinner: indicator 8 nan (loader-line), animation spin · fade · pulse · breathe; màu mặc định trắng (dark) / đen (light) qua --base-quinary, overlay cũng vậy — không dùng highlight.

  • Toggle: công tắc On/Off (Figma 730:12438) — size md/sm, checked, disabled.

  • Slider: bước rời (Figma 730:12646) — title/unit, labels, activeColor.

  • Tooltip: hover/focus (Figma 891:4983) — size/position/align + TooltipContent.

  • Dialog / Logo: React component + CSS (Logo asset generate qua embed-logo-assets.mjs).

  • Thêm bản CDN cho HTML tĩnh: leaf-<version>.min.css + leaf-<version>.min.js (window.Leaf).

  • Button: typography của label chuyển vào button.css, component không còn gắn class body-*-highlight. Thiếu data-priority / data-size / data-status → primary / lg / normal.

  • Tabs: API items[] (không export Tab); label là string đầy đủ, bỏ slot addon; data-variant / data-size chỉ đọc từ tablist; typography của label trong tabs.css.

  • Tooltip: caret khớp SVG Figma (tam giác nhỏ giữa khung hit-area), không còn full-width.

  • Slider: thumb min = --text-primary, Active = --text-highlight; click label nhảy mốc.

  • Token: --icon-on-color (Figma Icon/icon-on-color) cho Toggle thumb On.

  • Token: --font-sans có fallback khi không có --font-inter (trang ngoài Next.js).