@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/src/index.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @suphark/ui — design system กลางของ Suphark apps
|
|
3
|
+
*
|
|
4
|
+
* - `components/ui/*` shadcn/ui (base-nova / Base UI) ต้นฉบับ — สร้าง/อัปเดตด้วย `pnpm dlx shadcn@latest add` เท่านั้น ห้ามแก้มือ
|
|
5
|
+
* - `components/shared/*` component ที่เราประกอบเอง (ปุ่มมาตรฐาน, form-field บน react-hook-form, layout, theme, …)
|
|
6
|
+
* - `lib/*`, `hooks/*` utility ที่ component ข้างบนใช้ร่วมกัน
|
|
7
|
+
*
|
|
8
|
+
* barrel นี้ **ไม่พึ่ง next** — component ที่ใช้ next/navigation หรือ next/link อยู่ที่ `@suphark/ui/next`
|
|
9
|
+
* `chart` (recharts) ไม่อยู่ใน barrel: import ตรง `@suphark/ui/components/ui/chart` เมื่อแอปติดตั้ง recharts เอง
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export * from "./components/shared/button";
|
|
13
|
+
export { ClientRedirect } from "./components/shared/client-redirect";
|
|
14
|
+
export { Combobox } from "./components/shared/combobox";
|
|
15
|
+
export { DataList, DataRow } from "./components/shared/data-list";
|
|
16
|
+
export { DateRangePicker } from "./components/shared/date/date-range-picker";
|
|
17
|
+
export { DescriptionList } from "./components/shared/description-list";
|
|
18
|
+
export { EmptyRow } from "./components/shared/empty-row";
|
|
19
|
+
export { EmptyState } from "./components/shared/empty-state";
|
|
20
|
+
export { EnvSnippet, randomSecret } from "./components/shared/env-snippet";
|
|
21
|
+
export { FormFieldBoolean } from "./components/shared/form/form-field-boolean";
|
|
22
|
+
export { FormFieldColorSelect } from "./components/shared/form/form-field-color-select";
|
|
23
|
+
export { FormFieldCombobox } from "./components/shared/form/form-field-combobox";
|
|
24
|
+
export { FormFieldDate } from "./components/shared/form/form-field-date";
|
|
25
|
+
export { FormFieldFormattedNumber } from "./components/shared/form/form-field-formatted-number";
|
|
26
|
+
export {
|
|
27
|
+
type AutocompleteOption,
|
|
28
|
+
FormFieldInput,
|
|
29
|
+
type FormFieldInputProps,
|
|
30
|
+
} from "./components/shared/form/form-field-input";
|
|
31
|
+
export { FormFieldRadioCards } from "./components/shared/form/form-field-radio-cards";
|
|
32
|
+
export { FormFieldRadioGroup } from "./components/shared/form/form-field-radio-group";
|
|
33
|
+
export { FormFieldSelect, type SelectOption } from "./components/shared/form/form-field-select";
|
|
34
|
+
export {
|
|
35
|
+
SimpleSelect,
|
|
36
|
+
type SimpleSelectGroup,
|
|
37
|
+
type SimpleSelectOption,
|
|
38
|
+
type SimpleSelectOptions,
|
|
39
|
+
type SimpleSelectProps,
|
|
40
|
+
} from "./components/shared/form/simple-select";
|
|
41
|
+
export { FormMessage, type FormState } from "./components/shared/form-message";
|
|
42
|
+
export { Initials, initialsOf } from "./components/shared/initials";
|
|
43
|
+
export { ErrorState, type RouteError } from "./components/shared/layout/error-state";
|
|
44
|
+
export {
|
|
45
|
+
MultiSelectCombobox,
|
|
46
|
+
type MultiSelectComboboxProps,
|
|
47
|
+
type MultiSelectOption,
|
|
48
|
+
} from "./components/shared/multi-select-combobox";
|
|
49
|
+
export {
|
|
50
|
+
type IdentifierStyle,
|
|
51
|
+
NameIdFields,
|
|
52
|
+
toIdentifier,
|
|
53
|
+
} from "./components/shared/name-id-fields";
|
|
54
|
+
export { Notice, type NoticeTone } from "./components/shared/notice";
|
|
55
|
+
export { type DecimalLike, FormattedNumber } from "./components/shared/number/formatted-number";
|
|
56
|
+
export { PageHeader } from "./components/shared/page-header";
|
|
57
|
+
export { PageSkeleton } from "./components/shared/page-skeleton";
|
|
58
|
+
export { SecretReveal } from "./components/shared/secret-reveal";
|
|
59
|
+
export { SectionCard } from "./components/shared/section-card";
|
|
60
|
+
export { Stat } from "./components/shared/stat";
|
|
61
|
+
export { StatusBadge, statusTone, type Tone } from "./components/shared/status-badge";
|
|
62
|
+
export {
|
|
63
|
+
PrimarySecondaryText,
|
|
64
|
+
type PrimarySecondaryTextProps,
|
|
65
|
+
} from "./components/shared/text/primary-secondary-text";
|
|
66
|
+
export * from "./components/shared/theme";
|
|
67
|
+
export { ClientDateFormatter } from "./components/shared/time/client-date-formatter";
|
|
68
|
+
export {
|
|
69
|
+
UI_TEXT_EN,
|
|
70
|
+
UI_TEXT_TH,
|
|
71
|
+
type UiText,
|
|
72
|
+
UiTextProvider,
|
|
73
|
+
useUiText,
|
|
74
|
+
} from "./components/shared/ui-text";
|
|
75
|
+
// ---------- shadcn/ui primitives ----------
|
|
76
|
+
export * from "./components/ui/accordion";
|
|
77
|
+
export * from "./components/ui/alert";
|
|
78
|
+
export * from "./components/ui/alert-dialog";
|
|
79
|
+
export * from "./components/ui/aspect-ratio";
|
|
80
|
+
export * from "./components/ui/attachment";
|
|
81
|
+
export * from "./components/ui/avatar";
|
|
82
|
+
export * from "./components/ui/badge";
|
|
83
|
+
export * from "./components/ui/breadcrumb";
|
|
84
|
+
export * from "./components/ui/button";
|
|
85
|
+
export * from "./components/ui/button-group";
|
|
86
|
+
export * from "./components/ui/calendar";
|
|
87
|
+
export * from "./components/ui/card";
|
|
88
|
+
export * from "./components/ui/carousel";
|
|
89
|
+
export * from "./components/ui/checkbox";
|
|
90
|
+
export * from "./components/ui/collapsible";
|
|
91
|
+
export * from "./components/ui/combobox";
|
|
92
|
+
export * from "./components/ui/command";
|
|
93
|
+
export * from "./components/ui/context-menu";
|
|
94
|
+
export * from "./components/ui/date-picker";
|
|
95
|
+
export * from "./components/ui/dialog";
|
|
96
|
+
export * from "./components/ui/drawer";
|
|
97
|
+
export * from "./components/ui/dropdown-menu";
|
|
98
|
+
export * from "./components/ui/empty";
|
|
99
|
+
export * from "./components/ui/field";
|
|
100
|
+
export * from "./components/ui/form";
|
|
101
|
+
export * from "./components/ui/hover-card";
|
|
102
|
+
export * from "./components/ui/input";
|
|
103
|
+
export * from "./components/ui/input-group";
|
|
104
|
+
export * from "./components/ui/input-otp";
|
|
105
|
+
export * from "./components/ui/item";
|
|
106
|
+
export * from "./components/ui/kbd";
|
|
107
|
+
export * from "./components/ui/label";
|
|
108
|
+
export * from "./components/ui/menubar";
|
|
109
|
+
export * from "./components/ui/navigation-menu";
|
|
110
|
+
export * from "./components/ui/pagination";
|
|
111
|
+
export * from "./components/ui/popover";
|
|
112
|
+
export * from "./components/ui/progress";
|
|
113
|
+
export * from "./components/ui/radio-group";
|
|
114
|
+
export * from "./components/ui/relative-date";
|
|
115
|
+
export * from "./components/ui/resizable";
|
|
116
|
+
export * from "./components/ui/scroll-area";
|
|
117
|
+
export * from "./components/ui/searchable-select";
|
|
118
|
+
export * from "./components/ui/select";
|
|
119
|
+
export * from "./components/ui/separator";
|
|
120
|
+
export * from "./components/ui/sheet";
|
|
121
|
+
export * from "./components/ui/sidebar";
|
|
122
|
+
export * from "./components/ui/skeleton";
|
|
123
|
+
export * from "./components/ui/slider";
|
|
124
|
+
export * from "./components/ui/sonner";
|
|
125
|
+
export * from "./components/ui/spinner";
|
|
126
|
+
export * from "./components/ui/switch";
|
|
127
|
+
export * from "./components/ui/table";
|
|
128
|
+
export * from "./components/ui/tabs";
|
|
129
|
+
export * from "./components/ui/textarea";
|
|
130
|
+
export * from "./components/ui/toggle";
|
|
131
|
+
export * from "./components/ui/toggle-group";
|
|
132
|
+
export * from "./components/ui/tooltip";
|
|
133
|
+
|
|
134
|
+
// ---------- lib / hooks ----------
|
|
135
|
+
export { useCopyToClipboard } from "./hooks/use-copy-to-clipboard";
|
|
136
|
+
export { useIsMobile } from "./hooks/use-mobile";
|
|
137
|
+
export { formatRelativeThaiDate, formatThaiFullDate } from "./lib/formatters";
|
|
138
|
+
export {
|
|
139
|
+
type AtomicPermissionRequirement,
|
|
140
|
+
type CheckPermissionOptions,
|
|
141
|
+
type CheckPermissionRequirement,
|
|
142
|
+
type PermissionChecker,
|
|
143
|
+
type PermissionRequirementObject,
|
|
144
|
+
UiPermissionProvider,
|
|
145
|
+
usePermission,
|
|
146
|
+
usePermissionChecker,
|
|
147
|
+
} from "./lib/permission";
|
|
148
|
+
export {
|
|
149
|
+
type ColorName,
|
|
150
|
+
type ColorTheme,
|
|
151
|
+
getStatusColorClass,
|
|
152
|
+
getStatusColorTheme,
|
|
153
|
+
statusColorThemes,
|
|
154
|
+
TAILWIND_COLORS,
|
|
155
|
+
} from "./lib/status-colors";
|
|
156
|
+
export { cn } from "./lib/utils";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { differenceInDays, formatDistanceToNow } from "date-fns";
|
|
2
|
+
import { th } from "date-fns/locale";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ฟอร์แมตวันที่แบบไทยที่ primitives ใช้ร่วมกัน (DatePicker, RelativeDate)
|
|
6
|
+
* ยกมาจาก supplychain `lib/utils/formatters.ts` เฉพาะส่วนที่ไม่ผูก domain
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** วันที่แบบสัมพัทธ์ภาษาไทย เช่น "วันนี้", "เมื่อวาน", "3 วันที่แล้ว", "2 เดือนที่แล้ว" */
|
|
10
|
+
export function formatRelativeThaiDate(date: Date | string | number | null | undefined): string {
|
|
11
|
+
if (!date) return "ไม่ได้ระบุ";
|
|
12
|
+
const d = new Date(date);
|
|
13
|
+
if (Number.isNaN(d.getTime())) return "ไม่ได้ระบุ";
|
|
14
|
+
|
|
15
|
+
const diffDays = Math.abs(differenceInDays(new Date(), d));
|
|
16
|
+
if (diffDays === 0) return "วันนี้";
|
|
17
|
+
if (diffDays === 1) return "เมื่อวาน";
|
|
18
|
+
|
|
19
|
+
return formatDistanceToNow(d, { addSuffix: true, locale: th });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const THAI_MONTH_SHORT = [
|
|
23
|
+
"ม.ค.",
|
|
24
|
+
"ก.พ.",
|
|
25
|
+
"มี.ค.",
|
|
26
|
+
"เม.ย.",
|
|
27
|
+
"พ.ค.",
|
|
28
|
+
"มิ.ย.",
|
|
29
|
+
"ก.ค.",
|
|
30
|
+
"ส.ค.",
|
|
31
|
+
"ก.ย.",
|
|
32
|
+
"ต.ค.",
|
|
33
|
+
"พ.ย.",
|
|
34
|
+
"ธ.ค.",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** วันที่เต็มแบบไทย ปี พ.ศ. เช่น "23 ก.ค. 2569" */
|
|
38
|
+
export function formatThaiFullDate(date: Date | string | number | null | undefined): string {
|
|
39
|
+
if (!date) return "ไม่ได้ระบุ";
|
|
40
|
+
const d = new Date(date);
|
|
41
|
+
if (Number.isNaN(d.getTime())) return "ไม่ได้ระบุ";
|
|
42
|
+
|
|
43
|
+
return `${d.getDate()} ${THAI_MONTH_SHORT[d.getMonth()]} ${d.getFullYear() + 543}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, createElement, type ReactNode, useCallback, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ชั้นบางสำหรับ "ซ่อนปุ่มตามสิทธิ์" ที่ ActionButton/LinkButton ใช้
|
|
7
|
+
*
|
|
8
|
+
* แพ็กเกจนี้ไม่รู้จักระบบสิทธิ์ของแอปไหน — แอปเป็นคนส่งฟังก์ชัน `check` เข้ามาทาง
|
|
9
|
+
* `UiPermissionProvider` (supplychain ส่ง checkPermission ของตัวเอง, Pi Auth ส่งจาก session)
|
|
10
|
+
* ถ้าไม่มี provider → อนุญาตทุกอย่าง (เหมาะกับแอปที่ไม่มีระบบสิทธิ์และ showcase)
|
|
11
|
+
*
|
|
12
|
+
* รูปแบบ requirement คงเดิมตาม supplychain: "resource:action", { resource, action }, หรือ OR-list
|
|
13
|
+
*/
|
|
14
|
+
export type PermissionRequirementObject = {
|
|
15
|
+
action: string;
|
|
16
|
+
resource: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type AtomicPermissionRequirement = PermissionRequirementObject | string | null | undefined;
|
|
20
|
+
|
|
21
|
+
/** requirement เดียว หรือ OR-list — ตรงตัวใดตัวหนึ่งก็ผ่าน */
|
|
22
|
+
export type CheckPermissionRequirement =
|
|
23
|
+
| AtomicPermissionRequirement
|
|
24
|
+
| readonly AtomicPermissionRequirement[];
|
|
25
|
+
|
|
26
|
+
export type CheckPermissionOptions = {
|
|
27
|
+
organizationId?: string;
|
|
28
|
+
orgTag?: string | string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type PermissionChecker = (
|
|
32
|
+
requirement: CheckPermissionRequirement,
|
|
33
|
+
options?: CheckPermissionOptions,
|
|
34
|
+
) => boolean;
|
|
35
|
+
|
|
36
|
+
const UiPermissionContext = createContext<PermissionChecker | null>(null);
|
|
37
|
+
|
|
38
|
+
export function UiPermissionProvider({
|
|
39
|
+
check,
|
|
40
|
+
children,
|
|
41
|
+
}: {
|
|
42
|
+
check: PermissionChecker;
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
}) {
|
|
45
|
+
return createElement(UiPermissionContext.Provider, { value: check }, children);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** requirement ที่ถือว่า "ไม่ต้องมีสิทธิ์" */
|
|
49
|
+
function isPublic(requirement: CheckPermissionRequirement): boolean {
|
|
50
|
+
return !requirement || requirement === "public" || requirement === "any";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** true เมื่อผู้ใช้ปัจจุบันผ่าน requirement (หรือไม่มี provider) */
|
|
54
|
+
export function usePermission(
|
|
55
|
+
requirement: CheckPermissionRequirement,
|
|
56
|
+
options?: CheckPermissionOptions,
|
|
57
|
+
): boolean {
|
|
58
|
+
const check = useContext(UiPermissionContext);
|
|
59
|
+
if (isPublic(requirement)) return true;
|
|
60
|
+
if (!check) return true;
|
|
61
|
+
return check(requirement, options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** ตัว check ดิบ สำหรับ component ที่ต้องตรวจหลาย requirement */
|
|
65
|
+
export function usePermissionChecker(): PermissionChecker {
|
|
66
|
+
const check = useContext(UiPermissionContext);
|
|
67
|
+
return useCallback<PermissionChecker>(
|
|
68
|
+
(requirement, options) => {
|
|
69
|
+
if (isPublic(requirement)) return true;
|
|
70
|
+
if (!check) return true;
|
|
71
|
+
return check(requirement, options);
|
|
72
|
+
},
|
|
73
|
+
[check],
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type ColorName = "gray" | "red" | "yellow" | "green" | "blue" | "indigo" | "purple" | "pink";
|
|
2
|
+
|
|
3
|
+
export type ColorTheme = {
|
|
4
|
+
background: string;
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const statusColorThemes: Record<ColorName, ColorTheme> = {
|
|
9
|
+
gray: { background: "bg-gray-200", text: "text-gray-800" },
|
|
10
|
+
red: { background: "bg-red-200", text: "text-red-800" },
|
|
11
|
+
yellow: { background: "bg-yellow-200", text: "text-yellow-800" },
|
|
12
|
+
green: { background: "bg-green-200", text: "text-green-800" },
|
|
13
|
+
blue: { background: "bg-blue-200", text: "text-blue-800" },
|
|
14
|
+
indigo: { background: "bg-indigo-200", text: "text-indigo-800" },
|
|
15
|
+
purple: { background: "bg-purple-200", text: "text-purple-800" },
|
|
16
|
+
pink: { background: "bg-pink-200", text: "text-pink-800" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const TAILWIND_COLORS: { name: ColorName; label: string }[] = [
|
|
20
|
+
{ name: "gray", label: "Gray" },
|
|
21
|
+
{ name: "red", label: "Red" },
|
|
22
|
+
{ name: "yellow", label: "Yellow" },
|
|
23
|
+
{ name: "green", label: "Green" },
|
|
24
|
+
{ name: "blue", label: "Blue" },
|
|
25
|
+
{ name: "indigo", label: "Indigo" },
|
|
26
|
+
{ name: "purple", label: "Purple" },
|
|
27
|
+
{ name: "pink", label: "Pink" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// --- [ฟังก์ชันใหม่ที่แนะนำ] ---
|
|
31
|
+
/**
|
|
32
|
+
* ดึง Object theme สีทั้งหมด (background และ text) จากชื่อสี
|
|
33
|
+
* @param color - ชื่อสีจาก ColorName
|
|
34
|
+
* @returns Object ColorTheme ที่ประกอบด้วย background และ text class
|
|
35
|
+
*/
|
|
36
|
+
export function getStatusColorTheme(color?: string | null): ColorTheme {
|
|
37
|
+
if (color && statusColorThemes[color as ColorName]) {
|
|
38
|
+
return statusColorThemes[color as ColorName];
|
|
39
|
+
}
|
|
40
|
+
return statusColorThemes.gray; // คืนค่า theme สีเทาเป็น default
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- [ฟังก์ชันเดิมที่ปรับปรุงแล้ว] ---
|
|
44
|
+
/**
|
|
45
|
+
* ดึง Tailwind classes ทั้ง background และ text มารวมกันเป็น string เดียว
|
|
46
|
+
* @param color - ชื่อสีจาก ColorName
|
|
47
|
+
* @returns String ของ Tailwind classes
|
|
48
|
+
*/
|
|
49
|
+
export function getStatusColorClass(color?: string | null): string {
|
|
50
|
+
const theme = getStatusColorTheme(color);
|
|
51
|
+
return `${theme.background} ${theme.text}`;
|
|
52
|
+
}
|
package/src/lib/utils.ts
ADDED
package/src/next.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @suphark/ui/next — component ที่ต้องรันใน Next.js (ใช้ next/navigation หรือ next/link)
|
|
3
|
+
* แยกจาก barrel หลักเพื่อให้ `@suphark/ui` ใช้ได้ในสภาพแวดล้อมที่ไม่มี next (vitest, Vite, Storybook)
|
|
4
|
+
*/
|
|
5
|
+
export { LinkButton, type LinkButtonProps } from "./components/shared/button/base/link-button";
|
|
6
|
+
export { RefreshButton } from "./components/shared/button/refresh-button";
|
|
7
|
+
export { FormDialog } from "./components/shared/form-dialog";
|
|
8
|
+
export { PageShell } from "./components/shared/layout/page-shell";
|
|
9
|
+
export { TransitionLink } from "./components/shared/link/transition-link";
|
|
10
|
+
export {
|
|
11
|
+
PageBreadcrumb,
|
|
12
|
+
type PageBreadcrumbItem,
|
|
13
|
+
} from "./components/shared/navigation/page-breadcrumb";
|
|
14
|
+
export { type RowAction, RowActions } from "./components/shared/row-actions";
|
|
15
|
+
export { RowMenu, type RowMenuDialog, type RowMenuItem } from "./components/shared/row-menu";
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @suphark/ui/styles.css — token + ฟอนต์ + base ของ design system (Tailwind v4, shadcn base-nova, baseColor slate)
|
|
3
|
+
*
|
|
4
|
+
* แอปเป็นคน @import "tailwindcss" เอง แล้ว import ไฟล์นี้ต่อ พร้อม @source ให้ Tailwind สแกน class ในแพ็กเกจ:
|
|
5
|
+
*
|
|
6
|
+
* @import "tailwindcss";
|
|
7
|
+
* @import "@suphark/ui/styles.css";
|
|
8
|
+
* @source "../../node_modules/@suphark/ui/src";
|
|
9
|
+
*
|
|
10
|
+
* ฟอนต์: Noto Sans Thai Looped (@fontsource) โหลดที่นี่ — แอปที่ต้องการฟอนต์อื่น override --font-sans ใน @theme ของตัวเอง
|
|
11
|
+
* ส่วน print/A4 และสไตล์เฉพาะ feature ของ supplychain ไม่ได้ยกมา (อยู่ที่แอปนั้น)
|
|
12
|
+
*/
|
|
13
|
+
@import "tw-animate-css";
|
|
14
|
+
@import "@fontsource/noto-sans-thai-looped/400.css";
|
|
15
|
+
@import "@fontsource/noto-sans-thai-looped/500.css";
|
|
16
|
+
@import "@fontsource/noto-sans-thai-looped/700.css";
|
|
17
|
+
|
|
18
|
+
@custom-variant dark (&:is(.dark *));
|
|
19
|
+
|
|
20
|
+
@theme inline {
|
|
21
|
+
--color-background: var(--background);
|
|
22
|
+
--color-foreground: var(--foreground);
|
|
23
|
+
--font-sans: "Noto Sans Thai Looped", Tahoma, sans-serif;
|
|
24
|
+
--font-mono:
|
|
25
|
+
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
|
26
|
+
monospace;
|
|
27
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
28
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
29
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
30
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
31
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
32
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
33
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
34
|
+
--color-sidebar: var(--sidebar);
|
|
35
|
+
--color-chart-5: var(--chart-5);
|
|
36
|
+
--color-chart-4: var(--chart-4);
|
|
37
|
+
--color-chart-3: var(--chart-3);
|
|
38
|
+
--color-chart-2: var(--chart-2);
|
|
39
|
+
--color-chart-1: var(--chart-1);
|
|
40
|
+
--color-ring: var(--ring);
|
|
41
|
+
--color-input: var(--input);
|
|
42
|
+
--color-border: var(--border);
|
|
43
|
+
--color-destructive: var(--destructive);
|
|
44
|
+
--color-success: var(--success);
|
|
45
|
+
--color-success-foreground: var(--success-foreground);
|
|
46
|
+
--color-warning: var(--warning);
|
|
47
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
48
|
+
--color-warning-muted-foreground: var(--warning-muted-foreground);
|
|
49
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
50
|
+
--color-accent: var(--accent);
|
|
51
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
52
|
+
--color-muted: var(--muted);
|
|
53
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
54
|
+
--color-secondary: var(--secondary);
|
|
55
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
56
|
+
--color-primary: var(--primary);
|
|
57
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
58
|
+
--color-popover: var(--popover);
|
|
59
|
+
--color-card-foreground: var(--card-foreground);
|
|
60
|
+
--color-card: var(--card);
|
|
61
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
62
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
63
|
+
--radius-lg: var(--radius);
|
|
64
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:root {
|
|
68
|
+
--radius: 0.625rem;
|
|
69
|
+
--background: oklch(1 0 0);
|
|
70
|
+
--foreground: oklch(0.129 0.042 264.695);
|
|
71
|
+
--card: oklch(1 0 0);
|
|
72
|
+
--card-foreground: oklch(0.129 0.042 264.695);
|
|
73
|
+
--popover: oklch(1 0 0);
|
|
74
|
+
--popover-foreground: oklch(0.129 0.042 264.695);
|
|
75
|
+
--primary: oklch(0.208 0.042 265.755);
|
|
76
|
+
--primary-foreground: oklch(0.984 0.003 247.858);
|
|
77
|
+
--secondary: oklch(0.968 0.007 247.896);
|
|
78
|
+
--secondary-foreground: oklch(0.208 0.042 265.755);
|
|
79
|
+
--muted: oklch(0.968 0.007 247.896);
|
|
80
|
+
--muted-foreground: oklch(0.554 0.046 257.417);
|
|
81
|
+
--accent: oklch(0.968 0.007 247.896);
|
|
82
|
+
--accent-foreground: oklch(0.208 0.042 265.755);
|
|
83
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
84
|
+
--success: oklch(0.527 0.154 150.069);
|
|
85
|
+
--success-foreground: oklch(0.985 0.002 247.839);
|
|
86
|
+
--warning: oklch(0.852 0.199 91.936);
|
|
87
|
+
--warning-foreground: oklch(0.286 0.066 53.813);
|
|
88
|
+
--warning-muted-foreground: oklch(0.421 0.095 57.708);
|
|
89
|
+
--border: oklch(0.929 0.013 255.508);
|
|
90
|
+
--input: oklch(0.929 0.013 255.508);
|
|
91
|
+
--ring: oklch(0.704 0.04 256.788);
|
|
92
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
93
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
94
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
95
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
96
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
97
|
+
--sidebar: oklch(0.984 0.003 247.858);
|
|
98
|
+
--sidebar-foreground: oklch(0.129 0.042 264.695);
|
|
99
|
+
--sidebar-primary: oklch(0.208 0.042 265.755);
|
|
100
|
+
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
|
101
|
+
--sidebar-accent: oklch(0.968 0.007 247.896);
|
|
102
|
+
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
|
|
103
|
+
--sidebar-border: oklch(0.929 0.013 255.508);
|
|
104
|
+
--sidebar-ring: oklch(0.704 0.04 256.788);
|
|
105
|
+
--scrollbar-thumb: rgba(0, 0, 0, 0.1);
|
|
106
|
+
--scrollbar-thumb-hover: rgba(0, 0, 0, 0.2);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.dark {
|
|
110
|
+
--background: oklch(0.129 0.042 264.695);
|
|
111
|
+
--foreground: oklch(0.984 0.003 247.858);
|
|
112
|
+
--card: oklch(0.208 0.042 265.755);
|
|
113
|
+
--card-foreground: oklch(0.984 0.003 247.858);
|
|
114
|
+
--popover: oklch(0.208 0.042 265.755);
|
|
115
|
+
--popover-foreground: oklch(0.984 0.003 247.858);
|
|
116
|
+
--primary: oklch(0.929 0.013 255.508);
|
|
117
|
+
--primary-foreground: oklch(0.208 0.042 265.755);
|
|
118
|
+
--secondary: oklch(0.279 0.041 260.031);
|
|
119
|
+
--secondary-foreground: oklch(0.984 0.003 247.858);
|
|
120
|
+
--muted: oklch(0.279 0.041 260.031);
|
|
121
|
+
--muted-foreground: oklch(0.704 0.04 256.788);
|
|
122
|
+
--accent: oklch(0.279 0.041 260.031);
|
|
123
|
+
--accent-foreground: oklch(0.984 0.003 247.858);
|
|
124
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
125
|
+
--success: oklch(0.627 0.194 149.214);
|
|
126
|
+
--success-foreground: oklch(0.145 0.028 147.727);
|
|
127
|
+
--warning: oklch(0.852 0.199 91.936);
|
|
128
|
+
--warning-foreground: oklch(0.286 0.066 53.813);
|
|
129
|
+
--warning-muted-foreground: oklch(0.905 0.182 98.111);
|
|
130
|
+
--border: oklch(1 0 0 / 10%);
|
|
131
|
+
--input: oklch(1 0 0 / 15%);
|
|
132
|
+
--ring: oklch(0.551 0.027 264.364);
|
|
133
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
134
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
135
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
136
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
137
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
138
|
+
--sidebar: oklch(0.208 0.042 265.755);
|
|
139
|
+
--sidebar-foreground: oklch(0.984 0.003 247.858);
|
|
140
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
141
|
+
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
|
142
|
+
--sidebar-accent: oklch(0.279 0.041 260.031);
|
|
143
|
+
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
|
|
144
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
145
|
+
--sidebar-ring: oklch(0.551 0.027 264.364);
|
|
146
|
+
--scrollbar-thumb: rgba(255, 255, 255, 0.1);
|
|
147
|
+
--scrollbar-thumb-hover: rgba(255, 255, 255, 0.2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
* สีแบรนด์ต่อ tenant/แอป (จาก Pi Auth) — ตั้ง --brand / --brand-foreground inline บน wrapper
|
|
152
|
+
* (<div style="--brand: oklch(...); --brand-foreground: ...">) แล้ว primary / ring / sidebar-primary ในนั้นตามไปด้วย
|
|
153
|
+
* ต้องเป็น rule บน wrapper ไม่ใช่ --primary: var(--brand) ที่ :root — เพราะ custom property ถูกคำนวณที่จุดประกาศ
|
|
154
|
+
* แล้วลูกสืบทอด "ค่าที่คำนวณแล้ว" การตั้ง --brand ใหม่ที่ลูกจึงไม่เปลี่ยน --primary (บั๊ก @pi/ui 4cd39d8)
|
|
155
|
+
* ค่าเริ่มต้นของแพ็กเกจไม่ตั้ง --brand → primary เป็น slate ตามต้นทาง supplychain
|
|
156
|
+
*/
|
|
157
|
+
[style*="--brand"],
|
|
158
|
+
[data-brand] {
|
|
159
|
+
--primary: var(--brand);
|
|
160
|
+
--primary-foreground: var(--brand-foreground);
|
|
161
|
+
--ring: var(--brand);
|
|
162
|
+
--sidebar-primary: var(--brand);
|
|
163
|
+
--sidebar-primary-foreground: var(--brand-foreground);
|
|
164
|
+
--sidebar-ring: var(--brand);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@layer base {
|
|
168
|
+
* {
|
|
169
|
+
@apply border-border outline-ring/50;
|
|
170
|
+
}
|
|
171
|
+
body {
|
|
172
|
+
@apply bg-background font-sans text-foreground;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@utility no-scrollbar {
|
|
177
|
+
-ms-overflow-style: none; /* IE and Edge */
|
|
178
|
+
scrollbar-width: none; /* Firefox */
|
|
179
|
+
&::-webkit-scrollbar {
|
|
180
|
+
display: none; /* Safari and Chrome */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@utility scrollbar-thin {
|
|
185
|
+
scrollbar-width: thin;
|
|
186
|
+
scrollbar-color: var(--scrollbar-thumb) transparent;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Global scrollbar styling */
|
|
190
|
+
::-webkit-scrollbar {
|
|
191
|
+
width: 6px;
|
|
192
|
+
height: 6px;
|
|
193
|
+
}
|
|
194
|
+
::-webkit-scrollbar-track {
|
|
195
|
+
background: transparent;
|
|
196
|
+
}
|
|
197
|
+
::-webkit-scrollbar-thumb {
|
|
198
|
+
background-color: var(--scrollbar-thumb);
|
|
199
|
+
border-radius: 9999px;
|
|
200
|
+
}
|
|
201
|
+
::-webkit-scrollbar-thumb:hover {
|
|
202
|
+
background-color: var(--scrollbar-thumb-hover);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
* {
|
|
206
|
+
scrollbar-width: thin;
|
|
207
|
+
scrollbar-color: var(--scrollbar-thumb) transparent;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
html,
|
|
211
|
+
body,
|
|
212
|
+
.font-sans {
|
|
213
|
+
font-family: var(--font-sans);
|
|
214
|
+
}
|