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

@suphark/ui

v0.83.0

Published

Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง

Downloads

10,347

Readme

@suphark/ui

Design system กลางของ Suphark apps (Pi Auth, DueBell, Pi PDF, supplychain, แอปใหม่) — shadcn/ui สไตล์ base-nova (Base UI) + Tailwind v4 + lucide + ฟอนต์ Noto Sans Thai Looped + component ที่เราประกอบเองบนนั้น

แพ็กเกจส่ง source (.ts/.tsx) ไม่ build — แอป Next.js transpile เองผ่าน transpilePackages

ดูตัวอย่างสด: https://ui.suphark.com — /components (ตัวอย่าง + โค้ดทุก component), /design-system (กติกา), /shell (AppShell), /brand

ติดตั้ง

pnpm add @suphark/ui @base-ui/react lucide-react react-hook-form

peer ที่ต้องมีอยู่แล้ว: react/react-dom ≥19, tailwindcss ^4, next ≥15 (เฉพาะ @suphark/ui/next), @tanstack/react-table (เฉพาะ @suphark/ui/data-table), recharts (เฉพาะ components/ui/chart)

1. next.config.ts

const nextConfig: NextConfig = {
  transpilePackages: ["@suphark/ui"],
};

2. globals.css

@import "tailwindcss";
@import "@suphark/ui/styles.css"; /* token, ฟอนต์, base, dark variant */
@source "../../node_modules/@suphark/ui/src"; /* ให้ Tailwind สแกน class ในแพ็กเกจ (ปรับ ../ ตามตำแหน่งไฟล์) */

ต้องการฟอนต์อื่น: override ใน @theme { --font-sans: ... } ของแอปหลัง import

3. root layout

import { ThemeProvider, Toaster } from "@suphark/ui";

<html lang="th" suppressHydrationWarning>
  <body>
    <ThemeProvider>
      {children}
      <Toaster richColors />
    </ThemeProvider>
  </body>
</html>

4. tsconfig ของแอป

แอปจะ type-check source ของแพ็กเกจด้วย — แพ็กเกจผ่าน strict + noUncheckedIndexedAccess + verbatimModuleSyntax อยู่แล้ว

หลังอัปเดตเวอร์ชันแพ็กเกจ

ถ้า Turbopack ค้าง Package path ./styles.css is exported … but no valid target file (หรือ resolve entry อื่นไม่ได้) หลัง pnpm install เวอร์ชันใหม่ — เป็น cache ของ dev server ไม่ใช่ปัญหาแพ็กเกจ: หยุด dev server แล้วลบ .next ของแอป

การ import

// ทุกอย่างที่ไม่พึ่ง Next — primitives, shared, theme, hooks, cn
import { Button, Card, FormFieldInput, StatusBadge, ThemeToggle, ConfirmDialog, useConfirm, cn } from "@suphark/ui";
// file explorer / picker / rule: TreeView, Dropzone, UploadList, FileTypeIcon, ColorPicker, IconPicker, CalendarMonthGrid, RuleBuilder ก็อยู่ barrel

// ของที่ใช้ next/navigation หรือ next/link + โครงแอป
import { AppShell, UserMenu, filterNav, FormDialog, RowActions, LinkButton, RefreshButton, PageShell } from "@suphark/ui/next";

// ตารางข้อมูล (ต้องติดตั้ง @tanstack/react-table เอง)
import { DataTable, DataTableColumnHeader, useDataTable } from "@suphark/ui/data-table";

// PWA (PullToRefresh ใช้ next/navigation)
import { PwaServiceWorkerRegistration, PwaInstallCard, PullToRefresh } from "@suphark/ui/pwa";

// บอร์ด Kanban บน dnd-kit (แยก entry เหมือน data-table)
import { createKanbanCollisionDetection } from "@suphark/ui/kanban";

// ชุดไอคอนมาตรฐาน 300+ ตัว ค้นไทย/อังกฤษได้ (แยก entry เพราะ import ไอคอนทั้งชุด) — เก็บแค่ชื่อไอคอนในฐานข้อมูล
import { IconSelect, NamedIcon, ICON_CATALOG } from "@suphark/ui/icons";
// NamedIcon / IconSelect รับชื่อที่เก็บแบบเดิมได้ด้วย ("Home", "CreditCard", "alert-triangle") — ไม่ต้องแปลงข้อมูลใน DB
// ตรวจค่าฝั่ง server (server action / zod): icon: z.string().refine(isCatalogIconName) — 0.83.0
// ชุดสีมาตรฐาน 20 สี (สลับ dark ให้เอง) อยู่ใน barrel: ColorSelect, LabelBadge, paletteClasses, PALETTE_COLORS

// import ตรงรายไฟล์ก็ได้ (เช่น chart ที่ต้องมี recharts)
import { ChartContainer } from "@suphark/ui/components/ui/chart";

โครงสร้าง

| โฟลเดอร์ | คืออะไร | กติกา | |---|---|---| | src/components/ui/* | shadcn/ui ต้นฉบับ 59 ตัว (+ __tests__) | ห้ามแก้มือ — เพิ่ม/อัปเดตด้วย pnpm dlx shadcn@latest add <name> ใน packages/ui (components.json ตั้ง alias ไว้แล้ว) | | src/components/shared/* | ของที่เราประกอบเอง: ปุ่มมาตรฐาน (button/), form-field บน react-hook-form (form/), layout, theme, combobox, ชุดจาก Pi Auth (FormDialog, NameIdFields, StatusBadge, RowMenu, …), picker (IconPicker/ColorPicker/CalendarMonthGrid), file explorer (TreeView/Dropzone/UploadList/FileTypeIcon) | แก้ได้ แต่คง props เดิมให้มากที่สุด (minor = เพิ่ม, major = เปลี่ยน) | | src/components/data-table/* | DataTable บน tanstack (toolbar, faceted filter, pagination, view options, row actions, ลากเรียงแถว) | entry @suphark/ui/data-table | | src/components/pwa/* | PWA: ลงทะเบียน/อัปเดต service worker, การ์ดชวนติดตั้ง, ดึงลงเพื่อรีเฟรช | entry @suphark/ui/pwa | | src/components/app-shell/* | AppShell (sidebar + header + เนื้อหา), AppNav, AppBrand, UserMenu, HeaderBreadcrumbs, NavLink, filterNav/searchNav | export ผ่าน @suphark/ui/next (ใช้ next/link, next/navigation) | | src/components/design-system/* | หน้า showcase (render ใน apps/showcase) | ไม่อยู่ใน barrel | | src/lib/*, src/hooks/* | cn, formatter วันที่ไทย + formatBytes, สีสถานะ, permission context, useIsMobile, useCopyToClipboard, useMounted, useTicker | | | src/styles.css | token (slate + success/warning/info + พื้นอ่อนตามโทน *-subtle/*-border + sidebar/chart), สเกลตัวอักษรตามบทบาท (text-body …), radius, ฟอนต์, scrollbar, dark variant | | | scripts/*.mjs | audit บังคับ: Base UI props (default*/onSelect), typography scale | แอปปลายทางรันได้จาก node_modules/@suphark/ui/scripts |

โครงแอป (AppShell)

// app/(app)/layout.tsx — server component ได้ (icon เป็น element)
import { ThemeMenuItems } from "@suphark/ui";
import { AppShell, type AppNavGroup, filterNav, UserMenu } from "@suphark/ui/next";
import { cookies } from "next/headers";

const NAV: AppNavGroup[] = [
  { title: "งาน", items: [
    { title: "แดชบอร์ด", href: "/", icon: <LayoutDashboard />, exact: true },
    { title: "จัดซื้อ", icon: <ShoppingCart />, items: [{ title: "แพ็กเกจ", href: "/packages", permission: "package:read" }] },
  ]},
];

export default async function Layout({ children }) {
  const defaultOpen = (await cookies()).get("sidebar_state")?.value !== "false";
  return (
    <AppShell
      brand={{ name: "Pi Auth", subtitle: tenant.name, version: "v1.2.0", versionHref: "/change-log" }}
      nav={filterNav(NAV, can)}
      searchable
      defaultOpen={defaultOpen}
      brandColor={tenant.primaryColor}
      header={{ breadcrumbs: { skip: ["console"] }, actions: <NotificationBell /> }}
      sidebarFooter={<UserMenu user={{ name, email, image }} onSignOut={signOut}><ThemeMenuItems /></UserMenu>}
    >
      {children}
    </AppShell>
  );
}
  • layout="top" = แบบ Clerk dashboard: crumbs={[<HeaderCrumb …/>, …]} (switcher องค์กร/แอป/สภาพแวดล้อม — ห่อ DropdownMenu เอง), nav ชั้นแรกเป็นแท็บบน ชั้นสองเป็น sub-nav ซ้าย, header.actions ขวา — ตัวอย่าง apps/showcase/src/app/shell-top
  • header={false} ไม่มี header · collapsible="offcanvas" ซ่อนทั้งแผง (supplychain) · contentMaxWidth="max-w-5xl" จำกัดความกว้าง (Pi Auth)
  • pin={{ pinnedHrefs, onTogglePin }} เปิดปุ่มปักหมุดเมนู (แอปเก็บ state เอง) · header.center วาง pinned menus
  • layout top: HeaderCrumbs บนจอแคบเหลือ crumb สุดท้าย + ปุ่ม … เปิด popover ให้สลับองค์กร/แอปได้ — header.actions ควรซ่อนของไม่จำเป็นบนมือถือ (hidden sm:inline-flex) ไม่งั้นแย่งที่ crumb
  • sidebarHeader ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน — ใส่ sidebarHeaderCollapsed ถ้าต้องการไอคอนแทน (เช่นปุ่ม ⌘K)
  • ของที่ต้องเลื่อนไปกับเมนู (ต้นไม้โฟลเดอร์ยาว, มิเตอร์พื้นที่) ใส่ sidebarContentBefore / sidebarContentAfter (อยู่ใน SidebarContent) — sidebarHeader ตรึงอยู่บนไม่เลื่อน
  • header.breadcrumbs.labels รับฟังก์ชัน (segment, href) => ReactNode ได้ (ใส่ t() ของ next-intl) — แต่ฟังก์ชันส่งจาก server layout ไม่ได้ (React error "Functions cannot be passed directly to Client Components") ให้ทำ client component ที่เรียก <HeaderBreadcrumbs labels={…} /> แล้วส่งเป็น breadcrumbs: <MyBreadcrumbs /> (ดู apps/showcase/src/app/shell/breadcrumbs.tsx)
  • <UserMenu> children คือรายการเมนู: ThemeMenuItems มี DropdownMenuGroup + หัว "ธีม" ในตัวแล้ว (ปรับข้อความด้วย label/labels) ไม่ต้องห่อเพิ่ม; รายการอื่นใส่ DropdownMenuItem/DropdownMenuGroup เอง
  • ตัวอย่างรันได้ที่ apps/showcase/src/app/shell

Override contract — แอปปรับได้แค่นี้

| ทำได้ | ทำไม่ได้ | |---|---| | --brand / --brand-foreground (inline บน wrapper หรือ data-brand) | แก้ semantic token (--primary, --background, --muted, --destructive …) ตรง ๆ | | --font-sans / --font-mono ใน @theme ของแอป | แก้ --sidebar-*, --chart-* | | --radius (ค่าเดียว radius ทั้งหมดคำนวณจากตัวนี้) | เปลี่ยนวิธีสลับธีม (ต้องใช้ ThemeProvider ของแพ็กเกจ) | | className เพื่อจัด layout | className เปลี่ยนสี/ขนาดตัวอักษรของ primitive |

กติกาฉบับเต็มสำหรับ agent/CLAUDE.md ของแอป: docs/agent-rules.md

สีแบรนด์ต่อ tenant/แอป (--brand)

ตั้ง --brand / --brand-foreground inline บน wrapper (หรือใส่ data-brand) แล้ว primary/ring/sidebar-primary ข้างในตามไปด้วย — ค่าเริ่มต้นไม่ตั้ง --brand (primary = slate) ดูตัวอย่างที่ apps/showcase/src/app/brand

<div style={{ "--brand": tenant.color, "--brand-foreground": "oklch(0.985 0 0)" } as React.CSSProperties}>…</div>

ปุ่มไอคอน — tooltip มาให้เอง (มาตรฐานทุกโครงการ)

ไอคอนที่ไม่มีข้อความกำกับ ต้องบอกได้ว่าคืออะไรเมื่อวางเมาส์ — Button ของแพ็กเกจ ห่อ Tooltip ให้เองเมื่อมี aria-label และ children ไม่มีข้อความ (ไอคอนหรือ <span className="sr-only"> ไม่นับเป็นข้อความ) ทุก call site จึงได้ทันทีโดยไม่ต้องแก้อะไร รวมถึงปุ่มที่เป็น trigger ของ dialog/menu เพราะปุ่มอยู่ชั้นในสุดของ render=

<Button variant="ghost" size="icon-sm" aria-label="ดู client secret"><KeySquareIcon /></Button>
// → hover/focus แล้วขึ้น "ดู client secret" โดยไม่ต้องห่อ SimpleTooltip

<FormDialog trigger={<Button size="icon-sm" aria-label="แก้ไข"><PencilIcon /></Button>} … />
// → ได้ tooltip ด้วย (ไม่ต้องมี prop ใหม่ที่ FormDialog/RowActions)

งานค้างต้องหมุน ห้ามเงียบ (0.36.0): <Button loading={saving}><SaveIcon /> บันทึก</Button> — ไอคอนกลายเป็น spinner, กดซ้ำไม่ได้, aria-busy · type="submit" ใน <form action={…}> หมุนเองจาก useFormStatus · FormDialog/ActionForm ทำให้ทั้งปุ่มหมุนและตาข่ายฟอร์ม invalid · ฟอร์มเขียนเอง:

const invalid = useFormInvalidNotice();
<form action={save} {...invalid.formProps}>
  …
  {invalid.notice /* "กรุณากรอกหรือแก้ไข: ชื่อ, รหัสสาขา" — รวมช่องที่ซ่อน/โฟกัสไม่ได้ */}
  <Button type="submit">บันทึก</Button>
</form>

ไม่อยากใส่ทีละฟอร์ม: <FormInvalidNet /> (0.37.0) แจ้งเป็น toast ให้ทุก <form> ที่ไม่มีตาข่ายของตัวเอง — AppShell ใส่ให้แล้ว หน้านอก AppShell วางใน root layout ใต้ Toaster

| ต้องการ | เขียน | | --- | --- | | ข้อความอื่นที่ไม่ใช่ aria-label (หรือปุ่มที่มีข้อความแล้วแต่อยากอธิบายเพิ่ม) | tooltip="ตรวจสอบอีกครั้ง" | | ปิด (เช่นปุ่มในเมนูที่มีข้อความอยู่แล้ว) | tooltip={false} | | เปลี่ยนด้าน / หน่วงเวลา | tooltipSide="bottom", tooltipDelay={0} |

  • ไม่ต้องมี TooltipProvider — Base UI ไม่บังคับ; ใส่ที่ root layout ได้ถ้าอยากคุม delay ร่วมกันทุก tooltip (ค่าเริ่มต้นของ Base UI = 600ms, TooltipProvider ของแพ็กเกจตั้ง 0)
  • ปุ่มที่ถูกห่อ SimpleTooltip อยู่แล้ว ไม่ขึ้นสองชั้น (SimpleTooltip ปิดตัวอัตโนมัติให้ด้วย ButtonTooltipSuppress) — ถ้าแอปห่อ tooltip ด้วยกลไกอื่นเอง ครอบ <ButtonTooltipSuppress> หรือใส่ tooltip={false}
  • จอสัมผัสไม่มี hover — aria-label ยังทำหน้าที่เดิมให้ screen reader; ข้อความที่ "ต้องอ่านได้ทุกคน" อย่าเก็บไว้ใน tooltip เท่านั้น
  • ไม่มี aria-label แต่ใช้รูปแบบ shadcn <span className="sr-only">ข้อความ</span> ข้างไอคอน ก็ใช้ข้อความนั้นเป็น tooltip ให้เหมือนกัน
  • ปุ่มที่ disabled ไม่ขึ้น tooltip (ปุ่มที่ปิดอยู่ไม่รับ pointer ตามสไตล์ของ shadcn) — ถ้าต้องอธิบายว่าทำไมกดไม่ได้ ให้ห่อ SimpleTooltip ที่ <span> ครอบปุ่มแทน
  • มี title="…" อยู่เดิมให้เอาออก ไม่งั้นจะได้ tooltip ซ้อนกับของเบราว์เซอร์
  • <button> ดิบ (ไม่ผ่าน Button) ไม่ได้ของนี้ — อีกเหตุผลที่ห้ามเขียนปุ่มเอง · และต้อง import Button จาก @suphark/ui เท่านั้น: @suphark/ui/components/ui/button คือไฟล์ shadcn ดิบที่ไม่มี tooltip
  • ตรวจว่าปุ่มมี tooltip ไหม อย่าดูจาก data-slot — ปุ่มที่เป็น render=/trigger ของ Dialog/Popover/DropdownMenu จะได้ data-slot ของ trigger ชั้นนอก (dialog-trigger ฯลฯ) ทั้งที่มี tooltip อยู่ ให้เอาเมาส์ไปวางจริง หรือเช็ก [data-slot=tooltip-content] หลัง hover
  • ปุ่มไอคอนของแพ็กเกจเอง (RowActions, ActionMenu, SimpleDialog ปุ่มปิด, DataTable แบ่งหน้า, ThemeToggle, Notice, UploadList, TimePicker, MonthStepper, SecretReveal, CommandPalette …) ได้ tooltip ไปพร้อมกันตั้งแต่ 0.27.0

ยืนยันก่อนทำ — ห้ามใช้ window.confirm

webview/automation บางตัวปิด window.confirm ทิ้ง (กดแล้วไม่เกิดอะไร) และไม่เข้า design system — ใช้ 3 ตัวนี้แทน

| สถานการณ์ | ใช้ | | --- | --- | | รายการในเมนู … ท้ายแถว | RowActions / RowMenu prop confirm (string หรือ { title, description, confirmLabel }) | | ปุ่มเดี่ยวเรียก server action | ConfirmActionButton (@suphark/ui/next) — confirm + action + data | | โค้ด client อื่น ๆ (fetch/mutation เอง) | const { confirm, confirmDialog } = useConfirm(); if (!(await confirm({ description, destructive: true }))) return; แล้ว render {confirmDialog} | | คุม state เอง | ConfirmDialog controlled |

ข้อความค่าเริ่มต้น (หัว/ปุ่ม) มาจาก UiText.confirmTitle / UiText.confirm

ต้องการ alert / prompt / choose (หลายปุ่ม) / checklist ด้วย หรือไม่อยาก render confirmDialog ทุกที่: ครอบ <DialogsProvider> ที่ root แล้ว const dialogs = useDialogs() — await dialogs.prompt({ description: "ชื่อใหม่", defaultValue }) คืน string หรือ null; หัว/ปุ่มค่าเริ่มต้นจาก UiText.ok/alertTitle/promptTitle/chooseTitle/checklistTitle

ลากจัดลำดับ (@suphark/ui/sortable)

ลากเรียงกลุ่ม เรียงรายการในกลุ่ม และย้ายรายการข้ามกลุ่ม ในตาราง/รายการเดียวกัน — แพ็กเกจดูแล sensors, collision, การย้ายระหว่างลาก และ คืนลำดับเดิมให้เองเมื่อบันทึกไม่สำเร็จ ส่วนการเรนเดอร์แถวยังเป็นของแอป

const [order, setOrder] = useState(() =>
  sortableOrderOf(groups, items, { groupId: (g) => g.id, itemId: (i) => i.id, itemGroupId: (i) => i.groupId }),
);

<SortableProvider order={order} onOrderChange={setOrder} onCommit={(next) => saveOrder(sortableOrderToPayload(next))}>
  <SortableGroup itemIds={order.items[groupId] ?? []}>
    {/* แต่ละแถว: const row = useSortableRow({ type: "item", id }); */}
    <TableRow ref={row.ref} style={row.style}>
      <TableCell><DragHandle label={`ลาก ${item.name}`} {...row.handleProps} /></TableCell>
      …
    </TableRow>
  </SortableGroup>
</SortableProvider>
  • กลุ่มที่ไม่มีหัวกลุ่ม (เช่น "ยังไม่จัดกลุ่ม") ใช้ useSortableDropZone(groupId) แปะกับแถว placeholder ของตัวเอง และดูว่าควรแสดงเมื่อไหร่จาก useSortableState().dragging — วางแถวนี้ไว้ใต้แถวที่ลากได้ทั้งหมด (ท้ายตาราง) ถ้ามันโผล่ขึ้นเหนือแถวที่กำลังลาก layout จะเลื่อนตอนเริ่มลาก แล้วการลากด้วยคีย์บอร์ดจะไม่ขยับ
  • ตั้ง id ของ SortableProvider ได้ (ไม่ตั้ง = useId()) — ต้องคงที่ระหว่าง SSR กับ client ไม่งั้น aria-describedby ของ dnd-kit จะ hydration mismatch · ตรวจได้จากที่จับ: aria-describedby จะเป็น id ที่ส่งเข้าไปตรง ๆ (ไม่มี prefix) และต้องชี้ element ที่มีข้อความวิธีลาก — element นี้สร้างหลัง hydrate จึงต้องรอหน้า mount ก่อนค่อย query
  • onCommit ล้มเหลว (throw หรือ { ok: false, error }) = คืนลำดับเดิม + toast; ถ้าอยากจัดการ error เองใส่ onCommitError
  • การ์ดลอยตามเมาส์: renderOverlay={(drag) => drag.kind === "item" ? <ItemCard id={drag.id} /> : <GroupCard id={drag.id} />} (วาดใน portal จึงเป็น div ได้ ไม่ต้องเป็น <tr>) — ไม่ใส่ = แถวขยับเองแบบเดิม · โฟลเดอร์ที่ย่ออยู่: useSortableDropZone(id, { onDwell: collapsed ? open : undefined }) ลากค้าง 600ms แล้วเปิดให้ · วางบนหัวกลุ่ม = ขึ้นบนสุดของกลุ่ม, วางบนโซนของ UNGROUPED = ต่อท้าย
  • เลือกหลายรายการแล้วลากพร้อมกัน: SortableProvider multiSelect แล้วกระจาย row.selectProps ลงแถว (ทำสีด้วย data-[selected]:bg-primary/10) — ⌘/Ctrl+คลิก, Shift+คลิก, ที่จับกด x / ⌘/Ctrl+A, Esc ล้าง · แถบนับใช้ SelectionBar + useSortableState().selected/clearSelection · renderOverlay ได้ drag.ids ไว้ทำการ์ดซ้อน
  • จัดชั้นบนสุดอิสระ (กลุ่มกับรายการไม่มีกลุ่มสลับกันได้): SortableProvider freeLayout + เรนเดอร์ตาม rootEntries(order) (ครอบทั้งหมดด้วย <SortableGroup itemIds={order.items[UNGROUPED]}> แล้วแต่ละกลุ่มมี SortableGroup ของตัวเองข้างใน) + บันทึก payload.root และส่งกลับ sortableOrderOf(..., { root }) ตอนโหลด · หัวกลุ่มครึ่งบน = ก่อนกลุ่ม, ครึ่งล่าง = เข้ากลุ่ม
  • DatePicker: ช่องพิมพ์ได้โดยตรง — "15/3", "15/3/69", "t+3", "t+1m", ช่วง "1/7-5/7" / "t-7..t" (mode="range") เห็นผลที่อ่านออกใต้ช่อง กด Enter ยืนยัน · ↓ เปิดปฏิทิน/เข้าปฏิทิน (ลูกศร+Enter เลือกวัน) · Esc ยกเลิก · ในฟอร์ม name/defaultValue/required ส่ง YYYY-MM-DD · ห่อใน LabeledField ได้ (label โฟกัสที่ช่องพิมพ์)
  • ห่อ component ที่ props เป็น union (เช่น DatePicker ที่มี mode="range"): ใช้ DistributiveOmit/DistributivePick แทน Omit/Pick ไม่งั้น union ถูกยุบจนแยกตาม mode ไม่ได้
  • sortableOrderToPayload(order) คืน { groups, items } — groups คือลำดับของกลุ่ม, items คือรายการในแต่ละกลุ่มเรียงตามนั้น (ของที่ยังไม่มีกลุ่มมาก่อนด้วย groupId: null)
  • SortableProvider disabled (เช่นระหว่างกรอง ซึ่งลำดับที่เห็นไม่ใช่ลำดับจริง) ทำให้ DragHandle จางลงและกดไม่ได้ — ไม่อยากให้เห็นเลยใส่ hideWhenDisabled
  • ข้อความ screen reader: ใส่ announcements/screenReaderInstructions เป็น prop (callback ได้ { id, kind, groupId, index, total }) หรือปล่อยให้ใช้แม่แบบจาก UiText.sortable

PWA (@suphark/ui/pwa)

// layout ("use client" หรือ component ลูก) — ครั้งเดียว
<PwaServiceWorkerRegistration url="/sw.js" />
// ที่ไหนก็ได้
<PwaInstallCard appName="Pi Done" text={PWA_INSTALL_CARD_TEXT_EN} />
<PullToRefresh /> // + data-pull-to-refresh-root บน element ที่เลื่อนได้

PwaServiceWorkerRegistration เช็คอัปเดตตอนโหลด / กลับมาที่แท็บ / ทุก 15 นาที และ reload หน้าครั้งเดียวเมื่อ worker ใหม่คุมหน้า (เลื่อนขณะผู้ใช้กำลังพิมพ์, ไม่ reload ตอนติดตั้งครั้งแรก) — ฝั่ง worker ของแอปต้อง: ฝัง deployment id ใน script (เช่น process.env.VERCEL_DEPLOYMENT_ID) ให้ byte เปลี่ยนทุก deploy, self.skipWaiting() ใน install โดยไม่ขึ้นกับผล precache, self.clients.claim() ใน activate, และเสิร์ฟ script ด้วย Cache-Control: no-cache

Kanban (dnd-kit)

createKanbanCollisionDetection({ columnItems, resolveColumnId }) (จาก @suphark/ui/kanban) ใช้เป็น collisionDetection ของ DndContext เมื่อมีหลายคอลัมน์ (SortableContext ต่อคอลัมน์) — ตัวอย่างบอร์ดเต็มที่ ui.suphark.com/components/kanban-board; wrapper ของการ์ดห้าม overflow-hidden (dnd-kit ใส่ transform ที่ node ที่ setNodeRef ถ้าถูกครอบด้วย overflow-hidden การ์ดที่ขยับหลบจะถูก clip)

ตรวจกติกาในแอป — npx suphark-ui-audit

แพ็กเกจมี bin ให้รันในแอปปลายทาง (ตรวจ src ค่าเริ่มต้น; ข้าม components/ui/**, tests, generated):

npx suphark-ui-audit            # หรือใส่ใน package.json: "audit:ui": "suphark-ui-audit src"
npx suphark-ui-audit --strict   # warning นับเป็น error (ใช้ใน CI เมื่อล้างหมดแล้ว)

| กฎ | ระดับ | เจอ → ใช้แทน | | --- | --- | --- | | window-dialog | error | window.confirm/alert/prompt( → ConfirmDialog / useConfirm() / useDialogs() | | radix-state | error | data-[state=open\|checked\|active…] → data-open / data-checked / data-active / data-popup-open (Base UI ไม่มี data-state) | | arbitrary-text | error | text-[Npx] → text-xs … text-3xl | | base-ui-props | error | default* ที่ค่าเปลี่ยนได้ / onSelect บนเมนู (ดู scripts/audit-base-ui-props.mjs) | | raw-palette | warning | bg-red-500, text-slate-600 … → bg-destructive, text-muted-foreground, bg-success, bg-warning, bg-info, text-primary; พื้นอ่อน bg-sky-50 text-sky-800 → bg-info-subtle text-info-subtle-foreground | | raw-palette (คู่ป้ายสี) | warning | bg-teal-100 text-teal-800 บรรทัดเดียวกัน = ป้ายสีที่ทำเอง ไม่มี dark → LabelBadge / paletteClasses(color) (0.81.0) | | space-utility | warning | space-x-* / space-y-* → flex/grid + gap-* (0.81.0) | | z-arbitrary | warning | z-[…] → overlay จัดลำดับเอง; ซ้อนภายใน component ใช้ z-1 / z-10 (0.81.0) | | important | warning (เฉพาะ --important) | utility ลงท้าย ! ในโค้ดแพ็กเกจ |

ยกเว้นรายบรรทัด // ui-audit-ok: <เหตุผล> (บรรทัดเดียวกันหรือบรรทัดก่อน) — ยกเว้นทั้งไฟล์ /* ui-audit-ok-file: <เหตุผล> */

สิทธิ์ (ActionButton/LinkButton permission prop)

แพ็กเกจไม่รู้จักระบบสิทธิ์ของแอป — ครอบด้วย UiPermissionProvider แล้วส่งฟังก์ชัน check(requirement, options) ของแอปเข้ามา ถ้าไม่ครอบ = อนุญาตทุกอย่าง

<UiPermissionProvider check={(req, opts) => checkPermission(user, req, opts)}>…</UiPermissionProvider>

พัฒนา

pnpm install
pnpm dev                    # apps/showcase → http://localhost:3200/design-system
pnpm verify                 # typecheck + biome + lint:base-ui + check:typography + check:changelog + check:barrel + audit + vitest
pnpm --filter showcase build && pnpm smoke:showcase   # เปิดทุกหน้า showcase ใน Chrome ตรวจ error ฝั่ง client
pnpm --filter @suphark/ui test

เพิ่ม shadcn component:

cd packages/ui && pnpm dlx shadcn@latest add <name>

แล้วเพิ่ม export * from "./components/ui/<name>" ใน src/index.ts แล้วรัน pnpm fix:barrel ทันที (แปลงเป็นรายชื่อ export) — ห้ามมี export * ค้างใน src: server component ที่ import barrel แบบ export * ทำให้ทุกหน้าโหลด client component ทั้งแพ็กเกจ (pnpm check:barrel ตรวจใน verify, CI และก่อน publish); CLI เขียน import { cn } from "cn" ให้เปลี่ยนเป็น @suphark/ui/lib/utils

Versioning

semver — patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props ดู CHANGELOG.md

Publish

pnpm verify                       # จาก root ก่อนทุกครั้ง (หลัง commit/push แล้ว)
cd packages/ui
npm publish --access public       # Windows: npm.cmd — prepublishOnly รัน check:barrel + typecheck ให้อีกรอบ

(ผู้ใช้ npm login เอง; ไม่มีขั้น build เพราะส่ง source)