@suphark/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/components.json +21 -0
  5. package/package.json +108 -0
  6. package/scripts/audit-base-ui-props.mjs +179 -0
  7. package/scripts/audit-tailwind-typography.mjs +100 -0
  8. package/src/components/design-system/design-system-showcase.tsx +138 -0
  9. package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
  10. package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
  11. package/src/components/design-system/sections/showcase-forms.tsx +373 -0
  12. package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
  13. package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
  14. package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
  15. package/src/components/design-system/sections/showcase-overview.tsx +368 -0
  16. package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
  17. package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
  18. package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
  19. package/src/components/shared/button/add-new-button.tsx +19 -0
  20. package/src/components/shared/button/base/action-button.tsx +134 -0
  21. package/src/components/shared/button/base/link-button.tsx +130 -0
  22. package/src/components/shared/button/cancel-button.tsx +27 -0
  23. package/src/components/shared/button/confirm-button.tsx +26 -0
  24. package/src/components/shared/button/delete-button.tsx +36 -0
  25. package/src/components/shared/button/floating-save-button.tsx +89 -0
  26. package/src/components/shared/button/index.ts +13 -0
  27. package/src/components/shared/button/print-button.tsx +67 -0
  28. package/src/components/shared/button/refresh-button.tsx +63 -0
  29. package/src/components/shared/button/save-button.tsx +29 -0
  30. package/src/components/shared/button/submit-button.tsx +58 -0
  31. package/src/components/shared/client-redirect.tsx +11 -0
  32. package/src/components/shared/combobox.tsx +116 -0
  33. package/src/components/shared/data-list.tsx +65 -0
  34. package/src/components/shared/date/date-range-picker.tsx +106 -0
  35. package/src/components/shared/description-list.tsx +27 -0
  36. package/src/components/shared/empty-row.tsx +13 -0
  37. package/src/components/shared/empty-state.tsx +31 -0
  38. package/src/components/shared/env-snippet.tsx +62 -0
  39. package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
  40. package/src/components/shared/form/form-field-boolean.tsx +93 -0
  41. package/src/components/shared/form/form-field-color-select.tsx +86 -0
  42. package/src/components/shared/form/form-field-combobox.tsx +167 -0
  43. package/src/components/shared/form/form-field-date.tsx +55 -0
  44. package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
  45. package/src/components/shared/form/form-field-input.tsx +372 -0
  46. package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
  47. package/src/components/shared/form/form-field-radio-group.tsx +56 -0
  48. package/src/components/shared/form/form-field-select.tsx +312 -0
  49. package/src/components/shared/form/simple-select.tsx +152 -0
  50. package/src/components/shared/form-dialog.tsx +109 -0
  51. package/src/components/shared/form-message.tsx +30 -0
  52. package/src/components/shared/initials.tsx +33 -0
  53. package/src/components/shared/layout/error-state.tsx +160 -0
  54. package/src/components/shared/layout/page-shell.tsx +57 -0
  55. package/src/components/shared/link/transition-link.tsx +72 -0
  56. package/src/components/shared/multi-select-combobox.tsx +392 -0
  57. package/src/components/shared/name-id-fields.tsx +133 -0
  58. package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
  59. package/src/components/shared/notice.tsx +35 -0
  60. package/src/components/shared/number/formatted-number.tsx +133 -0
  61. package/src/components/shared/page-header.tsx +25 -0
  62. package/src/components/shared/page-skeleton.tsx +18 -0
  63. package/src/components/shared/row-actions.tsx +93 -0
  64. package/src/components/shared/row-menu.tsx +55 -0
  65. package/src/components/shared/secret-reveal.tsx +53 -0
  66. package/src/components/shared/section-card.tsx +44 -0
  67. package/src/components/shared/stat.tsx +26 -0
  68. package/src/components/shared/status-badge.tsx +47 -0
  69. package/src/components/shared/text/primary-secondary-text.tsx +186 -0
  70. package/src/components/shared/theme/index.ts +4 -0
  71. package/src/components/shared/theme/theme-menu-items.tsx +43 -0
  72. package/src/components/shared/theme/theme-provider.tsx +39 -0
  73. package/src/components/shared/theme/theme-switch.tsx +36 -0
  74. package/src/components/shared/theme/theme-toggle.tsx +76 -0
  75. package/src/components/shared/time/client-date-formatter.tsx +188 -0
  76. package/src/components/shared/ui-text.tsx +47 -0
  77. package/src/components/ui/__tests__/combobox.test.tsx +40 -0
  78. package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
  79. package/src/components/ui/__tests__/slider.test.tsx +18 -0
  80. package/src/components/ui/__tests__/tabs.test.tsx +33 -0
  81. package/src/components/ui/accordion.tsx +69 -0
  82. package/src/components/ui/alert-dialog.tsx +161 -0
  83. package/src/components/ui/alert.tsx +68 -0
  84. package/src/components/ui/aspect-ratio.tsx +22 -0
  85. package/src/components/ui/attachment.tsx +194 -0
  86. package/src/components/ui/avatar.tsx +92 -0
  87. package/src/components/ui/badge.tsx +48 -0
  88. package/src/components/ui/breadcrumb.tsx +102 -0
  89. package/src/components/ui/button-group.tsx +77 -0
  90. package/src/components/ui/button.tsx +57 -0
  91. package/src/components/ui/calendar.tsx +184 -0
  92. package/src/components/ui/card.tsx +87 -0
  93. package/src/components/ui/carousel.tsx +230 -0
  94. package/src/components/ui/chart.tsx +338 -0
  95. package/src/components/ui/checkbox.tsx +27 -0
  96. package/src/components/ui/collapsible.tsx +17 -0
  97. package/src/components/ui/combobox.tsx +272 -0
  98. package/src/components/ui/command.tsx +180 -0
  99. package/src/components/ui/context-menu.tsx +244 -0
  100. package/src/components/ui/date-picker.tsx +308 -0
  101. package/src/components/ui/dialog.tsx +137 -0
  102. package/src/components/ui/drawer.tsx +208 -0
  103. package/src/components/ui/dropdown-menu.tsx +257 -0
  104. package/src/components/ui/empty.tsx +93 -0
  105. package/src/components/ui/field.tsx +223 -0
  106. package/src/components/ui/form.tsx +161 -0
  107. package/src/components/ui/hover-card.tsx +46 -0
  108. package/src/components/ui/input-group.tsx +145 -0
  109. package/src/components/ui/input-otp.tsx +85 -0
  110. package/src/components/ui/input.tsx +19 -0
  111. package/src/components/ui/item.tsx +187 -0
  112. package/src/components/ui/kbd.tsx +26 -0
  113. package/src/components/ui/label.tsx +19 -0
  114. package/src/components/ui/menubar.tsx +266 -0
  115. package/src/components/ui/navigation-menu.tsx +160 -0
  116. package/src/components/ui/pagination.tsx +117 -0
  117. package/src/components/ui/popover.tsx +76 -0
  118. package/src/components/ui/progress.tsx +66 -0
  119. package/src/components/ui/radio-group.tsx +38 -0
  120. package/src/components/ui/relative-date.tsx +40 -0
  121. package/src/components/ui/resizable.tsx +41 -0
  122. package/src/components/ui/scroll-area.tsx +67 -0
  123. package/src/components/ui/searchable-select.tsx +124 -0
  124. package/src/components/ui/select.tsx +189 -0
  125. package/src/components/ui/separator.tsx +21 -0
  126. package/src/components/ui/sheet.tsx +124 -0
  127. package/src/components/ui/sidebar.tsx +691 -0
  128. package/src/components/ui/skeleton.tsx +13 -0
  129. package/src/components/ui/slider.tsx +55 -0
  130. package/src/components/ui/sonner.tsx +45 -0
  131. package/src/components/ui/spinner.tsx +16 -0
  132. package/src/components/ui/switch.tsx +32 -0
  133. package/src/components/ui/table.tsx +95 -0
  134. package/src/components/ui/tabs.tsx +79 -0
  135. package/src/components/ui/textarea.tsx +17 -0
  136. package/src/components/ui/toggle-group.tsx +86 -0
  137. package/src/components/ui/toggle.tsx +44 -0
  138. package/src/components/ui/tooltip.tsx +54 -0
  139. package/src/hooks/use-copy-to-clipboard.ts +27 -0
  140. package/src/hooks/use-mobile.ts +21 -0
  141. package/src/index.ts +156 -0
  142. package/src/lib/formatters.ts +44 -0
  143. package/src/lib/permission.ts +75 -0
  144. package/src/lib/status-colors.ts +52 -0
  145. package/src/lib/utils.ts +6 -0
  146. package/src/next.ts +15 -0
  147. package/src/styles.css +214 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
4
+
5
+ ## [0.1.0] — 2026-09-18
6
+
7
+ รุ่นแรก — ยกจาก `supplychainnextjs-1` (source of truth) + ชุด generic จาก Pi Auth `@pi/ui`
8
+
9
+ ### Added
10
+
11
+ - **shadcn/ui base-nova 59 primitives** (`components/ui/*`) + tests (combobox, date-picker, slider, tabs)
12
+ - **token/ฟอนต์** `styles.css`: slate + success/warning/sidebar/chart, radius, scrollbar, Noto Sans Thai Looped, dark variant
13
+ - **shared/button**: AddNew, Save, Submit, Cancel, Confirm, Delete, FloatingSave, Print, ActionButton (base)
14
+ - **shared/form** (react-hook-form): FormFieldInput/Select/Combobox/Date/Boolean/RadioGroup/RadioCards/ColorSelect/FormattedNumber, SimpleSelect
15
+ - **shared**: Combobox, MultiSelectCombobox, DateRangePicker, FormattedNumber, PrimarySecondaryText, ClientDateFormatter, ErrorState, PageSkeleton, ClientRedirect
16
+ - **จาก Pi Auth**: FormDialog, FormMessage/FormState, NameIdFields/toIdentifier (+test), StatusBadge/statusTone, RowMenu, RowActions, SecretReveal, EnvSnippet/randomSecret, Notice, EmptyState, EmptyRow, DataList/DataRow, Stat, DescriptionList, PageHeader, SectionCard, Initials/initialsOf, UiText (TH/EN provider)
17
+ - **theme** บน next-themes: ThemeProvider, ThemeToggle (เมนู 3 ตัวเลือก), ThemeSwitch (ปุ่มเดี่ยว), ThemeMenuItems (ใส่ใน DropdownMenu), useTheme
18
+ - **lib**: `cn`, `formatThaiFullDate`/`formatRelativeThaiDate`, status-colors, `UiPermissionProvider`/`usePermission` (แอป inject ตัวตรวจสิทธิ์เอง)
19
+ - **hooks**: `useIsMobile`, `useCopyToClipboard`
20
+ - **สีแบรนด์ต่อ tenant**: rule `[style*="--brand"], [data-brand]` ใน `styles.css` ให้ตั้ง `--brand`/`--brand-foreground` บน wrapper แล้ว primary/ring/sidebar-primary ตาม (แก้ตามบั๊ก @pi/ui `4cd39d8` ที่ `--primary: var(--brand)` บน :root ไม่สืบทอดไปยัง wrapper)
21
+ - entry `@suphark/ui/next` สำหรับ component ที่ใช้ next/navigation (LinkButton, RefreshButton, TransitionLink, PageBreadcrumb, PageShell, FormDialog, RowActions, RowMenu)
22
+ - showcase `components/design-system/*` (8 sections) + `docs/design-system.md`
23
+ - audit scripts `scripts/audit-base-ui-props.mjs`, `scripts/audit-tailwind-typography.mjs` (รับ path เป็น argument ให้แอปปลายทางใช้ได้)
24
+
25
+ ### Changed (เทียบต้นทาง)
26
+
27
+ - `NameIdFields` เขียนใหม่บน `ui/field.tsx` (Field/FieldLabel/FieldDescription) แทน `pi/Field`
28
+ - `ActionButton`/`LinkButton` ใช้ `usePermission` จาก `lib/permission` (context) แทน `@/lib/auth`
29
+ - `FormattedNumber` รับ `DecimalLike` (`{ toString(): string }`) แทน `Prisma.Decimal`
30
+ - `FormFieldInput` แยก `CopyableInput` เพื่อไม่เรียก hook ในเงื่อนไข
31
+ - `ui/form.tsx`, `hooks/use-mobile.ts` เพิ่ม `"use client"` (จำเป็นเมื่อ import ผ่าน barrel จาก server component)
32
+ - `ui/date-picker.tsx` แก้ `parts[0] ?? ""` ให้ผ่าน `noUncheckedIndexedAccess` ของแอปปลายทาง
33
+
34
+ ### Not included (รุ่นถัดไป)
35
+
36
+ - data-table (tanstack) + DeleteManyButton, sidebar/app shell, rich-text-editor (lexical), form-field-image-upload, pdf-export/print, rate, view-tracker, icon-picker, resource-property-filter
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Suphark
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @suphark/ui
2
+
3
+ Design system กลางของ Suphark apps (Pi Auth, DueBell, Pi PDF, supplychain, แอปใหม่) — shadcn/ui สไตล์ `base-nova` (Base UI) + Tailwind v4 + lucide + ฟอนต์ Noto Sans Thai Looped + component ที่เราประกอบเองบนนั้น
4
+
5
+ แพ็กเกจส่ง **source (.ts/.tsx) ไม่ build** — แอป Next.js transpile เองผ่าน `transpilePackages`
6
+
7
+ ## ติดตั้ง
8
+
9
+ ```bash
10
+ pnpm add @suphark/ui @base-ui/react lucide-react react-hook-form
11
+ ```
12
+
13
+ peer ที่ต้องมีอยู่แล้ว: `react`/`react-dom` ≥19, `tailwindcss` ^4, `next` ≥15 (เฉพาะ `@suphark/ui/next`), `recharts` (เฉพาะ `components/ui/chart`)
14
+
15
+ ### 1. next.config.ts
16
+
17
+ ```ts
18
+ const nextConfig: NextConfig = {
19
+ transpilePackages: ["@suphark/ui"],
20
+ };
21
+ ```
22
+
23
+ ### 2. globals.css
24
+
25
+ ```css
26
+ @import "tailwindcss";
27
+ @import "@suphark/ui/styles.css"; /* token, ฟอนต์, base, dark variant */
28
+ @source "../../node_modules/@suphark/ui/src"; /* ให้ Tailwind สแกน class ในแพ็กเกจ (ปรับ ../ ตามตำแหน่งไฟล์) */
29
+ ```
30
+
31
+ ต้องการฟอนต์อื่น: override ใน `@theme { --font-sans: ... }` ของแอปหลัง import
32
+
33
+ ### 3. root layout
34
+
35
+ ```tsx
36
+ import { ThemeProvider, Toaster } from "@suphark/ui";
37
+
38
+ <html lang="th" suppressHydrationWarning>
39
+ <body>
40
+ <ThemeProvider>
41
+ {children}
42
+ <Toaster richColors />
43
+ </ThemeProvider>
44
+ </body>
45
+ </html>
46
+ ```
47
+
48
+ ### 4. tsconfig ของแอป
49
+
50
+ แอปจะ type-check source ของแพ็กเกจด้วย — แพ็กเกจผ่าน `strict` + `noUncheckedIndexedAccess` + `verbatimModuleSyntax` อยู่แล้ว
51
+
52
+ ## การ import
53
+
54
+ ```ts
55
+ // ทุกอย่างที่ไม่พึ่ง Next — primitives, shared, theme, hooks, cn
56
+ import { Button, Card, FormFieldInput, StatusBadge, ThemeToggle, cn } from "@suphark/ui";
57
+
58
+ // ของที่ใช้ next/navigation หรือ next/link
59
+ import { FormDialog, RowActions, LinkButton, RefreshButton, PageShell } from "@suphark/ui/next";
60
+
61
+ // import ตรงรายไฟล์ก็ได้ (เช่น chart ที่ต้องมี recharts)
62
+ import { ChartContainer } from "@suphark/ui/components/ui/chart";
63
+ ```
64
+
65
+ ## โครงสร้าง
66
+
67
+ | โฟลเดอร์ | คืออะไร | กติกา |
68
+ |---|---|---|
69
+ | `src/components/ui/*` | shadcn/ui ต้นฉบับ 59 ตัว (+ `__tests__`) | **ห้ามแก้มือ** — เพิ่ม/อัปเดตด้วย `pnpm dlx shadcn@latest add <name>` ใน `packages/ui` (`components.json` ตั้ง alias ไว้แล้ว) |
70
+ | `src/components/shared/*` | ของที่เราประกอบเอง: ปุ่มมาตรฐาน (`button/`), form-field บน react-hook-form (`form/`), layout, theme, combobox, และชุดจาก Pi Auth (FormDialog, NameIdFields, StatusBadge, RowMenu, …) | แก้ได้ แต่คง props เดิมให้มากที่สุด (minor = เพิ่ม, major = เปลี่ยน) |
71
+ | `src/components/design-system/*` | หน้า showcase (render ใน `apps/showcase`) | ไม่อยู่ใน barrel |
72
+ | `src/lib/*`, `src/hooks/*` | `cn`, formatter วันที่ไทย, สีสถานะ, permission context, `useIsMobile`, `useCopyToClipboard` | |
73
+ | `src/styles.css` | token (slate + success/warning/sidebar/chart), radius, ฟอนต์, scrollbar, dark variant | |
74
+ | `scripts/*.mjs` | audit บังคับ: Base UI props (`default*`/`onSelect`), typography scale | แอปปลายทางรันได้จาก `node_modules/@suphark/ui/scripts` |
75
+
76
+ ### สีแบรนด์ต่อ tenant/แอป (`--brand`)
77
+
78
+ ตั้ง `--brand` / `--brand-foreground` inline บน wrapper (หรือใส่ `data-brand`) แล้ว `primary`/`ring`/`sidebar-primary` ข้างในตามไปด้วย — ค่าเริ่มต้นไม่ตั้ง `--brand` (primary = slate) ดูตัวอย่างที่ `apps/showcase/src/app/brand`
79
+
80
+ ```tsx
81
+ <div style={{ "--brand": tenant.color, "--brand-foreground": "oklch(0.985 0 0)" } as React.CSSProperties}>…</div>
82
+ ```
83
+
84
+ ### สิทธิ์ (ActionButton/LinkButton `permission` prop)
85
+
86
+ แพ็กเกจไม่รู้จักระบบสิทธิ์ของแอป — ครอบด้วย `UiPermissionProvider` แล้วส่งฟังก์ชัน `check(requirement, options)` ของแอปเข้ามา ถ้าไม่ครอบ = อนุญาตทุกอย่าง
87
+
88
+ ```tsx
89
+ <UiPermissionProvider check={(req, opts) => checkPermission(user, req, opts)}>…</UiPermissionProvider>
90
+ ```
91
+
92
+ ## พัฒนา
93
+
94
+ ```bash
95
+ pnpm install
96
+ pnpm dev # apps/showcase → http://localhost:3200/design-system
97
+ pnpm verify # typecheck + biome + lint:base-ui + check:typography + vitest
98
+ pnpm --filter @suphark/ui test
99
+ ```
100
+
101
+ เพิ่ม shadcn component:
102
+
103
+ ```bash
104
+ cd packages/ui && pnpm dlx shadcn@latest add <name>
105
+ ```
106
+
107
+ แล้วเพิ่ม `export * from "./components/ui/<name>"` ใน `src/index.ts`
108
+
109
+ ## Versioning
110
+
111
+ semver — patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props ดู `CHANGELOG.md`
112
+
113
+ ## Publish
114
+
115
+ ```bash
116
+ cd packages/ui
117
+ pnpm publish --access public
118
+ ```
119
+
120
+ (ผู้ใช้ `npm login` เอง; ไม่มีขั้น build เพราะส่ง source)
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "base-nova",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/styles.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@suphark/ui/components",
15
+ "utils": "@suphark/ui/lib/utils",
16
+ "ui": "@suphark/ui/components/ui",
17
+ "lib": "@suphark/ui/lib",
18
+ "hooks": "@suphark/ui/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
package/package.json ADDED
@@ -0,0 +1,108 @@
1
+ {
2
+ "name": "@suphark/ui",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/suphark/ui",
11
+ "directory": "packages/ui"
12
+ },
13
+ "keywords": [
14
+ "shadcn",
15
+ "base-ui",
16
+ "tailwind",
17
+ "design-system",
18
+ "react",
19
+ "nextjs",
20
+ "thai"
21
+ ],
22
+ "sideEffects": [
23
+ "*.css"
24
+ ],
25
+ "files": [
26
+ "src",
27
+ "scripts",
28
+ "components.json",
29
+ "README.md",
30
+ "CHANGELOG.md",
31
+ "LICENSE"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "exports": {
37
+ ".": "./src/index.ts",
38
+ "./next": "./src/next.ts",
39
+ "./styles.css": "./src/styles.css",
40
+ "./components/shared/button": "./src/components/shared/button/index.ts",
41
+ "./components/shared/theme": "./src/components/shared/theme/index.ts",
42
+ "./components/*": "./src/components/*.tsx",
43
+ "./lib/*": "./src/lib/*.ts",
44
+ "./hooks/*": "./src/hooks/*.ts"
45
+ },
46
+ "dependencies": {
47
+ "@fontsource/noto-sans-thai-looped": "^5.3.0",
48
+ "class-variance-authority": "^0.7.1",
49
+ "clsx": "^2.1.1",
50
+ "cmdk": "^1.1.1",
51
+ "date-fns": "^4.4.0",
52
+ "embla-carousel-react": "^8.6.0",
53
+ "input-otp": "^1.5.0",
54
+ "next-themes": "^0.4.6",
55
+ "react-day-picker": "^10.0.1",
56
+ "react-number-format": "^5.4.5",
57
+ "react-resizable-panels": "^4.12.3",
58
+ "sonner": "^2.0.8",
59
+ "tailwind-merge": "^3.6.0",
60
+ "tw-animate-css": "^1.4.0"
61
+ },
62
+ "peerDependencies": {
63
+ "@base-ui/react": ">=1.7.0",
64
+ "lucide-react": ">=1.30.0",
65
+ "next": ">=15",
66
+ "react": ">=19",
67
+ "react-dom": ">=19",
68
+ "react-hook-form": ">=7.50.0",
69
+ "recharts": ">=3",
70
+ "tailwindcss": "^4"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "next": {
74
+ "optional": true
75
+ },
76
+ "recharts": {
77
+ "optional": true
78
+ }
79
+ },
80
+ "devDependencies": {
81
+ "@base-ui/react": "^1.8.0",
82
+ "@testing-library/jest-dom": "^6.9.1",
83
+ "@testing-library/react": "^16.3.2",
84
+ "@testing-library/user-event": "^14.6.1",
85
+ "@types/node": "^24.13.4",
86
+ "@types/react": "^19.2.18",
87
+ "@types/react-dom": "^19.2.5",
88
+ "@vitejs/plugin-react": "^6.1.0",
89
+ "jsdom": "^29.1.1",
90
+ "lucide-react": "^1.45.0",
91
+ "next": "^16.3.5",
92
+ "react": "^19.3.0",
93
+ "react-dom": "^19.3.0",
94
+ "react-hook-form": "^7.86.0",
95
+ "recharts": "^3.10.1",
96
+ "tailwindcss": "^4.3.3",
97
+ "typescript": "^6.0.3",
98
+ "vitest": "^4.1.11"
99
+ },
100
+ "scripts": {
101
+ "typecheck": "tsc --noEmit",
102
+ "test": "vitest run",
103
+ "test:watch": "vitest",
104
+ "lint:base-ui": "node scripts/audit-base-ui-props.mjs",
105
+ "check:typography": "node scripts/audit-tailwind-typography.mjs",
106
+ "verify": "pnpm typecheck && pnpm lint:base-ui && pnpm check:typography && pnpm test"
107
+ }
108
+ }
@@ -0,0 +1,179 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * ตรวจการใช้ prop ของคอมโพเนนต์ที่สร้างบน Base UI ให้ถูกกติกา
6
+ *
7
+ * ## ทำไมต้องมี
8
+ *
9
+ * Base UI มี API ไม่เหมือน Radix ซึ่งเป็นไลบรารีที่โค้ดตัวอย่างส่วนใหญ่ในอินเทอร์เน็ตใช้
10
+ * ความต่างสองข้อนี้ทำให้เกิดบั๊กที่ "เงียบ" คือ TypeScript ผ่าน หน้าจอไม่พัง แต่ทำงานไม่ถูก:
11
+ *
12
+ * 1. **`default*` ที่ค่าเปลี่ยนได้** — เป็นค่าตั้งต้นของคอมโพเนนต์แบบ uncontrolled ใช้ได้
13
+ * แค่ตอน mount ครั้งแรก ถ้าส่งค่าที่เปลี่ยนได้เข้าไป Base UI จะเตือนในคอนโซล และที่แย่กว่า
14
+ * คือหน้าจอจะไม่อัปเดตตามค่าใหม่ ต้องใช้ `value` + `onValueChange` แทน
15
+ *
16
+ * 2. **`onSelect` บนรายการเมนู** — เป็น API ของ Radix ไม่มีใน Base UI ใส่ไปแล้วตัวจัดการ
17
+ * จะไม่ถูกเรียกเลย ปุ่มกดแล้วเงียบสนิท ต้องใช้ `onClick`
18
+ *
19
+ * ทั้งสองแบบเคยหลุดขึ้น production มาแล้ว จึงดักด้วยสคริปต์แทนการอาศัยความจำ
20
+ */
21
+
22
+ /**
23
+ * โฟลเดอร์ที่จะตรวจ: ส่งเป็น argument (`node audit-base-ui-props.mjs src/features src/app`) ไม่ส่ง = `src`
24
+ * แอปปลายทางรันสคริปต์นี้จาก node_modules ได้: ตั้ง UI_DIR ชี้ไปที่ shadcn ของแพ็กเกจ
25
+ * UI_DIR=node_modules/@suphark/ui/src/components/ui node node_modules/@suphark/ui/scripts/audit-base-ui-props.mjs src
26
+ */
27
+ const ROOTS = process.argv.slice(2).length > 0 ? process.argv.slice(2) : ["src"];
28
+
29
+ /** เขียนกำกับไว้เมื่อตรวจแล้วว่าเขียนแบบนี้ถูกต้อง พร้อมเหตุผลต่อท้าย */
30
+ const SUPPRESS_MARKER = "base-ui-ok:";
31
+ const UI_DIR = process.env.UI_DIR ?? "src/components/ui";
32
+
33
+ /** คอมโพเนนต์เมนูที่ไม่มี onSelect — ต้องใช้ onClick */
34
+ const MENU_ITEM_COMPONENTS = new Set([
35
+ "DropdownMenuItem",
36
+ "MenubarItem",
37
+ "ContextMenuItem",
38
+ "SelectItem",
39
+ ]);
40
+
41
+ /**
42
+ * คอมโพเนนต์ที่มี "สถานะ" ของตัวเอง (เปิด/ปิด หรือค่าที่เลือก) เท่านั้นที่กติกานี้ใช้
43
+ *
44
+ * ช่องกรอกอย่าง Input/Textarea ก็มี defaultValue เหมือนกัน แต่นั่นคือรูปแบบ uncontrolled
45
+ * ของ React ที่ถูกต้องอยู่แล้ว ไม่ใช่ปัญหา จึงไม่เอามาปนกัน
46
+ */
47
+ const STATEFUL_COMPONENTS = new Set([
48
+ "Accordion",
49
+ "Tabs",
50
+ "Select",
51
+ "Collapsible",
52
+ "Dialog",
53
+ "AlertDialog",
54
+ "Popover",
55
+ "Sheet",
56
+ "Drawer",
57
+ "Menubar",
58
+ "NavigationMenu",
59
+ "ToggleGroup",
60
+ "RadioGroup",
61
+ "Combobox",
62
+ "HoverCard",
63
+ "DropdownMenu",
64
+ "ContextMenu",
65
+ "SidebarProvider",
66
+ ]);
67
+
68
+ /**
69
+ * ค่าที่ถือว่า "คงที่" จึงส่งเป็น default ได้
70
+ *
71
+ * นับรวมค่าคงที่ที่ตั้งชื่อแบบ SCREAMING_CASE และ array/object ของค่าคงที่
72
+ * เพราะค่าพวกนี้ไม่เปลี่ยนระหว่างคอมโพเนนต์มีชีวิตอยู่
73
+ */
74
+ const STATIC_EXPRESSION =
75
+ /^(true|false|undefined|null|-?\d+(\.\d+)?|["'`][^"'`]*["'`]|[A-Z][A-Z0-9_]*(\.[A-Za-z0-9_]+)*|\[[^[\]]*\])$/;
76
+
77
+ function walk(dir) {
78
+ if (!fs.existsSync(dir)) return [];
79
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
80
+ const full = path.join(dir, entry.name);
81
+ if (entry.isDirectory()) return walk(full);
82
+ return entry.isFile() && full.endsWith(".tsx") && !full.includes("__tests__") ? [full] : [];
83
+ });
84
+ }
85
+
86
+ /** ชื่อคอมโพเนนต์ทั้งหมดที่ export จากไฟล์ ui ที่สร้างบน Base UI */
87
+ function collectBaseUiComponents() {
88
+ const names = new Set();
89
+
90
+ for (const file of walk(UI_DIR)) {
91
+ const source = fs.readFileSync(file, "utf8");
92
+ if (!source.includes("@base-ui/react")) continue;
93
+
94
+ const exportBlock = source.match(/export\s*\{([\s\S]*?)\}/g) ?? [];
95
+ for (const block of exportBlock) {
96
+ for (const raw of block.replace(/export\s*\{|\}/g, "").split(",")) {
97
+ const name = raw
98
+ .trim()
99
+ .split(/\s+as\s+/)
100
+ .pop()
101
+ ?.trim();
102
+ if (name && /^[A-Z]/.test(name)) names.add(name);
103
+ }
104
+ }
105
+ for (const match of source.matchAll(/export function ([A-Z]\w*)/g)) names.add(match[1]);
106
+ }
107
+
108
+ return names;
109
+ }
110
+
111
+ /** คู่ของแท็กเปิดกับ prop ที่อยู่ในนั้น — พอสำหรับกติกาสองข้อนี้ โดยไม่ต้อง parse ทั้งไฟล์ */
112
+ function findViolations(file, baseUiComponents) {
113
+ const source = fs.readFileSync(file, "utf8");
114
+ const violations = [];
115
+ const lines = source.split("\n");
116
+
117
+ /** true เมื่อบรรทัดนั้นถูกคอมเมนต์ไว้ หรือมีคำยืนยันว่าตรวจแล้วตั้งใจเขียนแบบนี้ */
118
+ const isSuppressed = (lineNumber) => {
119
+ if ((lines[lineNumber - 1] ?? "").trim().startsWith("//")) return true;
120
+
121
+ // ยอมรับได้เมื่อผู้เขียนยืนยันไว้ เช่น ค่าที่พิสูจน์แล้วว่าไม่เปลี่ยนตัวตน
122
+ // หรือ wrapper ที่ตั้งใจเปิดให้ใช้ได้ทั้งแบบ controlled และ uncontrolled
123
+ return lines
124
+ .slice(Math.max(0, lineNumber - 4), lineNumber + 8)
125
+ .some((row) => row.includes(SUPPRESS_MARKER));
126
+ };
127
+
128
+ for (const match of source.matchAll(/<([A-Z]\w*)([^>]*?)\/?>/gs)) {
129
+ const [, component, attributes] = match;
130
+ const line = source.slice(0, match.index).split("\n").length;
131
+ if (isSuppressed(line)) continue;
132
+
133
+ if (baseUiComponents.has(component) && STATEFUL_COMPONENTS.has(component)) {
134
+ for (const attr of attributes.matchAll(/\bdefault([A-Z]\w*)=\{([^}]*)\}/g)) {
135
+ const expression = attr[2].trim();
136
+ if (STATIC_EXPRESSION.test(expression)) continue;
137
+
138
+ violations.push({
139
+ file,
140
+ line,
141
+ message:
142
+ `<${component} default${attr[1]}={${expression}}> — ค่าตั้งต้นใช้ได้แค่ตอน mount แรก ` +
143
+ "ถ้าค่านี้เปลี่ยนได้ ให้ใช้ value/open + ตัวจัดการการเปลี่ยนแปลงแทน " +
144
+ "(ถ้าค่าคงที่จริง ให้ย้ายไปเก็บใน useMemo หรือค่าคงที่นอกคอมโพเนนต์)",
145
+ });
146
+ }
147
+ }
148
+
149
+ if (MENU_ITEM_COMPONENTS.has(component) && /\bonSelect=/.test(attributes)) {
150
+ violations.push({
151
+ file,
152
+ line,
153
+ message: `<${component} onSelect={...}> — Base UI ไม่มี prop นี้ ตัวจัดการจะไม่ถูกเรียก ให้ใช้ onClick`,
154
+ });
155
+ }
156
+ }
157
+
158
+ return violations;
159
+ }
160
+
161
+ const baseUiComponents = collectBaseUiComponents();
162
+ if (baseUiComponents.size === 0) {
163
+ console.error(`ไม่พบคอมโพเนนต์ที่สร้างบน Base UI — ตรวจ path ของ ${UI_DIR} (ตั้งค่าผ่าน env UI_DIR ได้)`);
164
+ process.exit(1);
165
+ }
166
+
167
+ const violations = ROOTS.flatMap((root) =>
168
+ walk(root).flatMap((file) => findViolations(file, baseUiComponents)),
169
+ );
170
+
171
+ if (violations.length > 0) {
172
+ console.error(`Base UI props audit failed: พบ ${violations.length} จุดที่ต้องแก้\n`);
173
+ for (const violation of violations) {
174
+ console.error(`- ${violation.file}:${violation.line}\n ${violation.message}`);
175
+ }
176
+ process.exit(1);
177
+ }
178
+
179
+ console.log(`Base UI props audit passed: ตรวจ ${baseUiComponents.size} คอมโพเนนต์`);
@@ -0,0 +1,100 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /** โฟลเดอร์ที่จะตรวจ: argument แรก (ค่าเริ่มต้น `src`) — แอปปลายทางรันจาก node_modules ได้ */
5
+ const SRC_DIR = path.resolve(process.cwd(), process.argv[2] ?? "src");
6
+ const ARBITRARY_TEXT_REGEX = /text-\[\d+(?:px|rem|em)\]/g;
7
+
8
+ /**
9
+ * Recursively find all .ts and .tsx files in directory
10
+ */
11
+ function findFiles(dir) {
12
+ let results = [];
13
+ const list = fs.readdirSync(dir, { withFileTypes: true });
14
+
15
+ for (const entry of list) {
16
+ const fullPath = path.join(dir, entry.name);
17
+ if (entry.isDirectory()) {
18
+ if (entry.name !== "generated" && entry.name !== "__tests__") {
19
+ results = results.concat(findFiles(fullPath));
20
+ }
21
+ } else if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
22
+ results.push(fullPath);
23
+ }
24
+ }
25
+
26
+ return results;
27
+ }
28
+
29
+ /**
30
+ * Masks comments, code tags, and pre tags with whitespace to preserve line/column offsets
31
+ * while preventing documentation / examples from triggering false positive audit errors.
32
+ */
33
+ function maskDocAndCommentBlocks(content) {
34
+ return (
35
+ content
36
+ // Mask block comments /* ... */
37
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length))
38
+ // Mask single-line comments // ...
39
+ .replace(/\/\/.*$/gm, (m) => " ".repeat(m.length))
40
+ // Mask <code>...</code> JSX elements (e.g. in design system doc pages)
41
+ .replace(/<code\b[^>]*>[\s\S]*?<\/code>/gi, (m) => " ".repeat(m.length))
42
+ // Mask <pre>...</pre> JSX elements
43
+ .replace(/<pre\b[^>]*>[\s\S]*?<\/pre>/gi, (m) => " ".repeat(m.length))
44
+ );
45
+ }
46
+
47
+ function auditTypography() {
48
+ const files = findFiles(SRC_DIR);
49
+ let totalIssues = 0;
50
+ const issuesByFile = [];
51
+
52
+ for (const file of files) {
53
+ const originalContent = fs.readFileSync(file, "utf-8");
54
+ const maskedContent = maskDocAndCommentBlocks(originalContent);
55
+ const originalLines = originalContent.split("\n");
56
+ const maskedLines = maskedContent.split("\n");
57
+
58
+ maskedLines.forEach((maskedLine, index) => {
59
+ const originalLine = originalLines[index] ?? "";
60
+
61
+ // Allow line-level ignore via directive comment
62
+ if (originalLine.includes("typography-ignore")) {
63
+ return;
64
+ }
65
+
66
+ const matches = maskedLine.match(ARBITRARY_TEXT_REGEX);
67
+ if (matches) {
68
+ matches.forEach((match) => {
69
+ totalIssues++;
70
+ const relativePath = path.relative(process.cwd(), file);
71
+ issuesByFile.push({
72
+ file: relativePath,
73
+ line: index + 1,
74
+ match,
75
+ snippet: originalLine.trim(),
76
+ });
77
+ });
78
+ }
79
+ });
80
+ }
81
+
82
+ console.log("=== Tailwind Typography Standard Audit ===");
83
+ if (totalIssues === 0) {
84
+ console.log("✅ No arbitrary text size classes found!");
85
+ process.exit(0);
86
+ } else {
87
+ console.warn(`⚠️ Found ${totalIssues} arbitrary text size occurrences (e.g. text-[10px]):\n`);
88
+ issuesByFile.slice(0, 30).forEach((issue) => {
89
+ console.warn(` ${issue.file}:${issue.line} -> ${issue.match}`);
90
+ });
91
+ if (issuesByFile.length > 30) {
92
+ console.warn(`\n ... and ${issuesByFile.length - 30} more occurrences.`);
93
+ }
94
+ console.warn(
95
+ "\nRecommendation: Replace text-[...px] with Tailwind standard font scale (e.g., text-xs, text-sm, text-base).\n",
96
+ );
97
+ }
98
+ }
99
+
100
+ auditTypography();