@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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/components.json +21 -0
- package/package.json +108 -0
- package/scripts/audit-base-ui-props.mjs +179 -0
- package/scripts/audit-tailwind-typography.mjs +100 -0
- package/src/components/design-system/design-system-showcase.tsx +138 -0
- package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
- package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
- package/src/components/design-system/sections/showcase-forms.tsx +373 -0
- package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
- package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
- package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
- package/src/components/design-system/sections/showcase-overview.tsx +368 -0
- package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
- package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
- package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
- package/src/components/shared/button/add-new-button.tsx +19 -0
- package/src/components/shared/button/base/action-button.tsx +134 -0
- package/src/components/shared/button/base/link-button.tsx +130 -0
- package/src/components/shared/button/cancel-button.tsx +27 -0
- package/src/components/shared/button/confirm-button.tsx +26 -0
- package/src/components/shared/button/delete-button.tsx +36 -0
- package/src/components/shared/button/floating-save-button.tsx +89 -0
- package/src/components/shared/button/index.ts +13 -0
- package/src/components/shared/button/print-button.tsx +67 -0
- package/src/components/shared/button/refresh-button.tsx +63 -0
- package/src/components/shared/button/save-button.tsx +29 -0
- package/src/components/shared/button/submit-button.tsx +58 -0
- package/src/components/shared/client-redirect.tsx +11 -0
- package/src/components/shared/combobox.tsx +116 -0
- package/src/components/shared/data-list.tsx +65 -0
- package/src/components/shared/date/date-range-picker.tsx +106 -0
- package/src/components/shared/description-list.tsx +27 -0
- package/src/components/shared/empty-row.tsx +13 -0
- package/src/components/shared/empty-state.tsx +31 -0
- package/src/components/shared/env-snippet.tsx +62 -0
- package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
- package/src/components/shared/form/form-field-boolean.tsx +93 -0
- package/src/components/shared/form/form-field-color-select.tsx +86 -0
- package/src/components/shared/form/form-field-combobox.tsx +167 -0
- package/src/components/shared/form/form-field-date.tsx +55 -0
- package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
- package/src/components/shared/form/form-field-input.tsx +372 -0
- package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
- package/src/components/shared/form/form-field-radio-group.tsx +56 -0
- package/src/components/shared/form/form-field-select.tsx +312 -0
- package/src/components/shared/form/simple-select.tsx +152 -0
- package/src/components/shared/form-dialog.tsx +109 -0
- package/src/components/shared/form-message.tsx +30 -0
- package/src/components/shared/initials.tsx +33 -0
- package/src/components/shared/layout/error-state.tsx +160 -0
- package/src/components/shared/layout/page-shell.tsx +57 -0
- package/src/components/shared/link/transition-link.tsx +72 -0
- package/src/components/shared/multi-select-combobox.tsx +392 -0
- package/src/components/shared/name-id-fields.tsx +133 -0
- package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
- package/src/components/shared/notice.tsx +35 -0
- package/src/components/shared/number/formatted-number.tsx +133 -0
- package/src/components/shared/page-header.tsx +25 -0
- package/src/components/shared/page-skeleton.tsx +18 -0
- package/src/components/shared/row-actions.tsx +93 -0
- package/src/components/shared/row-menu.tsx +55 -0
- package/src/components/shared/secret-reveal.tsx +53 -0
- package/src/components/shared/section-card.tsx +44 -0
- package/src/components/shared/stat.tsx +26 -0
- package/src/components/shared/status-badge.tsx +47 -0
- package/src/components/shared/text/primary-secondary-text.tsx +186 -0
- package/src/components/shared/theme/index.ts +4 -0
- package/src/components/shared/theme/theme-menu-items.tsx +43 -0
- package/src/components/shared/theme/theme-provider.tsx +39 -0
- package/src/components/shared/theme/theme-switch.tsx +36 -0
- package/src/components/shared/theme/theme-toggle.tsx +76 -0
- package/src/components/shared/time/client-date-formatter.tsx +188 -0
- package/src/components/shared/ui-text.tsx +47 -0
- package/src/components/ui/__tests__/combobox.test.tsx +40 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
- package/src/components/ui/__tests__/slider.test.tsx +18 -0
- package/src/components/ui/__tests__/tabs.test.tsx +33 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +161 -0
- package/src/components/ui/alert.tsx +68 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +194 -0
- package/src/components/ui/avatar.tsx +92 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +102 -0
- package/src/components/ui/button-group.tsx +77 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +184 -0
- package/src/components/ui/card.tsx +87 -0
- package/src/components/ui/carousel.tsx +230 -0
- package/src/components/ui/chart.tsx +338 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +17 -0
- package/src/components/ui/combobox.tsx +272 -0
- package/src/components/ui/command.tsx +180 -0
- package/src/components/ui/context-menu.tsx +244 -0
- package/src/components/ui/date-picker.tsx +308 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/drawer.tsx +208 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +93 -0
- package/src/components/ui/field.tsx +223 -0
- package/src/components/ui/form.tsx +161 -0
- package/src/components/ui/hover-card.tsx +46 -0
- package/src/components/ui/input-group.tsx +145 -0
- package/src/components/ui/input-otp.tsx +85 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +187 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/menubar.tsx +266 -0
- package/src/components/ui/navigation-menu.tsx +160 -0
- package/src/components/ui/pagination.tsx +117 -0
- package/src/components/ui/popover.tsx +76 -0
- package/src/components/ui/progress.tsx +66 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/relative-date.tsx +40 -0
- package/src/components/ui/resizable.tsx +41 -0
- package/src/components/ui/scroll-area.tsx +67 -0
- package/src/components/ui/searchable-select.tsx +124 -0
- package/src/components/ui/select.tsx +189 -0
- package/src/components/ui/separator.tsx +21 -0
- package/src/components/ui/sheet.tsx +124 -0
- package/src/components/ui/sidebar.tsx +691 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +55 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +95 -0
- package/src/components/ui/tabs.tsx +79 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toggle-group.tsx +86 -0
- package/src/components/ui/toggle.tsx +44 -0
- package/src/components/ui/tooltip.tsx +54 -0
- package/src/hooks/use-copy-to-clipboard.ts +27 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/index.ts +156 -0
- package/src/lib/formatters.ts +44 -0
- package/src/lib/permission.ts +75 -0
- package/src/lib/status-colors.ts +52 -0
- package/src/lib/utils.ts +6 -0
- package/src/next.ts +15 -0
- 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)
|
package/components.json
ADDED
|
@@ -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();
|