@suphark/ui 0.9.0 → 0.11.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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
4
4
 
5
+ ## [0.11.0] — 2026-09-21
6
+
7
+ ### Added
8
+
9
+ - `AppShell` (layout sidebar): slot **`sidebarContentBefore`** / **`sidebarContentAfter`** วางใน `SidebarContent` ก่อน/หลังเมนู — เลื่อนไปกับเมนู (ต่างจาก `sidebarHeader` ที่ตรึงบน) ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน; สำหรับต้นไม้โฟลเดอร์ + ถังขยะ + มิเตอร์พื้นที่แบบ Google Drive (ขอจาก Pi Files — เดิมต้องยัดใน `sidebarHeader` แล้วจำกัด max-h เอง)
10
+ - showcase: `/shell` ใส่ `TreeView` + `ProgressBar` ใน `sidebarContentAfter` เป็นตัวอย่าง (`app/shell/sidebar-drive.tsx`)
11
+
12
+ ## [0.10.0] — 2026-09-21
13
+
14
+ ### Added
15
+
16
+ - **`SectionNav`** (`@suphark/ui/next`, สเปกจาก Pi Auth — ชิ้นสุดท้ายที่แอปยังประกอบเอง) — หัวหมวด + แถบแท็บย่อยใต้หัวข้อสำหรับ layout ของหมวดที่มีหลายหน้า (directory/access/security/integrations): `title`/`description`/`actions` ส่งต่อ `PageHeader`, `tabs[{ href? | slug?, label, exact?, badge? }]` (href หรือ `${base}/${slug}`), แต่ละแท็บเป็น `NavLink` (`-mb-px border-b-2` ไฮไลต์ `border-primary` ตาม pathname, `aria-current=page`), badge ต่อท้าย label, `navLabel` (ค่าเริ่มต้น `UiText.sectionNav.tabs` — "แท็บย่อย"/"Section tabs"), ไม่มี tabs = ไม่มี nav; ไม่มี state ใช้จาก server component ได้ (เฉพาะแถบ `<nav>` เป็น client component เล็ก ๆ เพื่ออ่าน UiText); types `SectionNavProps`, `SectionNavTab`; `UiText.sectionNav` (optional)
17
+ - showcase: หน้า `section-nav` (แท็บชี้หน้าจริงในหมวดนำทาง — Link prefetch จะไม่ 404) — 97 หน้า; tests 3 กรณี (รวม 126)
18
+
5
19
  ## [0.9.0] — 2026-09-21
6
20
 
7
21
  ### Added
package/README.md CHANGED
@@ -129,6 +129,7 @@ export default async function Layout({ children }) {
129
129
  - `header={false}` ไม่มี header · `collapsible="offcanvas"` ซ่อนทั้งแผง (supplychain) · `contentMaxWidth="max-w-5xl"` จำกัดความกว้าง (Pi Auth)
130
130
  - `pin={{ pinnedHrefs, onTogglePin }}` เปิดปุ่มปักหมุดเมนู (แอปเก็บ state เอง) · `header.center` วาง pinned menus
131
131
  - `sidebarHeader` ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน — ใส่ `sidebarHeaderCollapsed` ถ้าต้องการไอคอนแทน (เช่นปุ่ม ⌘K)
132
+ - ของที่ต้องเลื่อนไปกับเมนู (ต้นไม้โฟลเดอร์ยาว, มิเตอร์พื้นที่) ใส่ `sidebarContentBefore` / `sidebarContentAfter` (อยู่ใน SidebarContent) — `sidebarHeader` ตรึงอยู่บนไม่เลื่อน
132
133
  - `header.breadcrumbs.labels` รับฟังก์ชัน `(segment, href) => ReactNode` ได้ (ใส่ `t()` ของ next-intl) — **แต่ฟังก์ชันส่งจาก server layout ไม่ได้** (React error "Functions cannot be passed directly to Client Components") ให้ทำ client component ที่เรียก `<HeaderBreadcrumbs labels={…} />` แล้วส่งเป็น `breadcrumbs: <MyBreadcrumbs />` (ดู `apps/showcase/src/app/shell/breadcrumbs.tsx`)
133
134
  - `<UserMenu>` children คือรายการเมนู: `ThemeMenuItems` มี `DropdownMenuGroup` + หัว "ธีม" ในตัวแล้ว (ปรับข้อความด้วย `label`/`labels`) ไม่ต้องห่อเพิ่ม; รายการอื่นใส่ `DropdownMenuItem`/`DropdownMenuGroup` เอง
134
135
  - ตัวอย่างรันได้ที่ `apps/showcase/src/app/shell`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suphark/ui",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง",
@@ -65,6 +65,12 @@ export interface AppShellProps {
65
65
  sidebarHeader?: ReactNode;
66
66
  /** สิ่งที่แสดงแทน `sidebarHeader` ตอนยุบเป็นไอคอน (เช่นปุ่มไอคอน ⌘K) — ไม่ส่ง = ไม่แสดงอะไร */
67
67
  sidebarHeaderCollapsed?: ReactNode;
68
+ /**
69
+ * ส่วนเพิ่มใน `SidebarContent` ก่อน/หลังเมนู — เลื่อนไปกับเมนู (ต่างจาก `sidebarHeader` ที่ตรึงอยู่บน)
70
+ * เช่น ต้นไม้โฟลเดอร์ + มิเตอร์พื้นที่แบบ Drive (Pi Files); ซ่อนอัตโนมัติเมื่อยุบเป็นไอคอน
71
+ */
72
+ sidebarContentBefore?: ReactNode;
73
+ sidebarContentAfter?: ReactNode;
68
74
  /** ท้าย sidebar — ปกติคือ `<UserMenu variant="sidebar" />` */
69
75
  sidebarFooter?: ReactNode;
70
76
  /**
@@ -137,6 +143,8 @@ function SidebarShell({
137
143
  pin,
138
144
  sidebarHeader,
139
145
  sidebarHeaderCollapsed,
146
+ sidebarContentBefore,
147
+ sidebarContentAfter,
140
148
  sidebarFooter,
141
149
  header = true,
142
150
  brandColor,
@@ -176,6 +184,8 @@ function SidebarShell({
176
184
  pin={pin}
177
185
  sidebarHeader={sidebarHeader}
178
186
  sidebarHeaderCollapsed={sidebarHeaderCollapsed}
187
+ sidebarContentBefore={sidebarContentBefore}
188
+ sidebarContentAfter={sidebarContentAfter}
179
189
  sidebarFooter={sidebarFooter}
180
190
  collapsible={collapsible}
181
191
  />
@@ -344,6 +354,8 @@ function ShellSidebar({
344
354
  pin,
345
355
  sidebarHeader,
346
356
  sidebarHeaderCollapsed,
357
+ sidebarContentBefore,
358
+ sidebarContentAfter,
347
359
  sidebarFooter,
348
360
  collapsible,
349
361
  }: Pick<
@@ -355,6 +367,8 @@ function ShellSidebar({
355
367
  | "pin"
356
368
  | "sidebarHeader"
357
369
  | "sidebarHeaderCollapsed"
370
+ | "sidebarContentBefore"
371
+ | "sidebarContentAfter"
358
372
  | "sidebarFooter"
359
373
  | "collapsible"
360
374
  >) {
@@ -407,7 +421,13 @@ function ShellSidebar({
407
421
  )}
408
422
  </SidebarHeader>
409
423
  <SidebarContent>
424
+ {sidebarContentBefore && (
425
+ <div className="px-2 group-data-[collapsible=icon]:hidden">{sidebarContentBefore}</div>
426
+ )}
410
427
  <AppNav nav={visibleNav} pin={pin} forceOpen={searching} />
428
+ {sidebarContentAfter && (
429
+ <div className="px-2 group-data-[collapsible=icon]:hidden">{sidebarContentAfter}</div>
430
+ )}
411
431
  </SidebarContent>
412
432
  {sidebarFooter && <SidebarFooter>{sidebarFooter}</SidebarFooter>}
413
433
  {collapsible !== "none" && <SidebarRail />}
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { useUiText } from "@suphark/ui/components/shared/ui-text";
4
+ import type { ReactNode } from "react";
5
+
6
+ /** แถบแท็บของ SectionNav — แยกเป็น client เพื่ออ่าน aria-label ค่าเริ่มต้นจาก UiText (context) */
7
+ export function SectionNavBar({ label, children }: { label?: string; children: ReactNode }) {
8
+ const text = useUiText();
9
+ return (
10
+ <nav
11
+ aria-label={label ?? text.sectionNav?.tabs ?? "แท็บย่อย"}
12
+ className="flex flex-wrap gap-1 border-b text-sm"
13
+ >
14
+ {children}
15
+ </nav>
16
+ );
17
+ }
@@ -0,0 +1,79 @@
1
+ import { NavLink } from "@suphark/ui/components/app-shell/nav-link";
2
+ import { PageHeader } from "@suphark/ui/components/shared/page-header";
3
+ import { cn } from "@suphark/ui/lib/utils";
4
+ import type { ReactNode } from "react";
5
+ import { SectionNavBar } from "./section-nav-bar";
6
+
7
+ export interface SectionNavTab {
8
+ /** ลิงก์เต็ม — หรือส่ง `slug` แล้วให้ `base` ของ SectionNav ประกอบเป็น `${base}/${slug}` */
9
+ href?: string;
10
+ slug?: string;
11
+ label: ReactNode;
12
+ /** active เฉพาะ path ตรงเป๊ะ (ค่าเริ่มต้น prefix match) */
13
+ exact?: boolean;
14
+ /** ต่อท้าย label เช่น จำนวน */
15
+ badge?: ReactNode;
16
+ }
17
+
18
+ export interface SectionNavProps {
19
+ title: ReactNode;
20
+ description?: ReactNode;
21
+ actions?: ReactNode;
22
+ /** prefix ของ href เมื่อแท็บให้ `slug` (เช่น "/directory") */
23
+ base?: string;
24
+ tabs?: ReadonlyArray<SectionNavTab>;
25
+ /** aria-label ของแถบแท็บ (ค่าเริ่มต้น `UiText.sectionNav.tabs` — "แท็บย่อย") */
26
+ navLabel?: string;
27
+ className?: string;
28
+ }
29
+
30
+ function tabHref(tab: SectionNavTab, base?: string): string {
31
+ if (tab.href) return tab.href;
32
+ const b = (base ?? "").replace(/\/+$/, "");
33
+ return `${b}/${(tab.slug ?? "").replace(/^\/+/, "")}`;
34
+ }
35
+
36
+ /**
37
+ * หัวหมวด + แถบแท็บย่อยใต้หัวข้อ สำหรับ layout ของหมวดที่มีหลายหน้า (directory/access/security/integrations …)
38
+ * = `PageHeader` + `NavLink` แต่ละแท็บ (ไฮไลต์ตาม pathname) — ไม่มี state ใช้จาก server component ได้
39
+ *
40
+ * <SectionNav title="ไดเรกทอรี" base="/directory" tabs={[{ slug: "users", label: "ผู้ใช้" }, { slug: "groups", label: "กลุ่ม", badge: 3 }]} />
41
+ */
42
+ export function SectionNav({
43
+ title,
44
+ description,
45
+ actions,
46
+ base,
47
+ tabs,
48
+ navLabel,
49
+ className,
50
+ }: SectionNavProps) {
51
+ return (
52
+ <div data-slot="section-nav" className={cn("flex flex-col gap-3", className)}>
53
+ <PageHeader title={title} description={description} actions={actions} />
54
+ {tabs && tabs.length > 0 && (
55
+ <SectionNavBar label={navLabel}>
56
+ {tabs.map((tab) => {
57
+ const href = tabHref(tab, base);
58
+ return (
59
+ <NavLink
60
+ key={href}
61
+ href={href}
62
+ exact={tab.exact}
63
+ className="-mb-px inline-flex items-center gap-1.5 border-transparent border-b-2 px-3 py-2 text-muted-foreground transition-colors hover:text-foreground"
64
+ activeClassName="border-primary font-medium text-foreground"
65
+ >
66
+ {tab.label}
67
+ {tab.badge != null && (
68
+ <span className="rounded-full bg-muted px-1.5 text-muted-foreground text-xs tabular-nums">
69
+ {tab.badge}
70
+ </span>
71
+ )}
72
+ </NavLink>
73
+ );
74
+ })}
75
+ </SectionNavBar>
76
+ )}
77
+ </div>
78
+ );
79
+ }
@@ -47,6 +47,8 @@ export interface UiText {
47
47
  signOut?: string;
48
48
  /** ThemeMenuItems: หัวกลุ่ม + ตัวเลือก (optional) */
49
49
  theme?: Partial<{ label: string; light: string; dark: string; system: string }>;
50
+ /** SectionNav: aria-label ของแถบแท็บย่อย (optional — "แท็บย่อย") */
51
+ sectionNav?: Partial<{ tabs: string }>;
50
52
  }
51
53
 
52
54
  export const UI_TEXT_TH: UiText = {
@@ -79,6 +81,7 @@ export const UI_TEXT_TH: UiText = {
79
81
  userMenu: "เมนูผู้ใช้",
80
82
  signOut: "ออกจากระบบ",
81
83
  theme: { label: "ธีม", light: "สว่าง", dark: "มืด", system: "ตามระบบ" },
84
+ sectionNav: { tabs: "แท็บย่อย" },
82
85
  };
83
86
 
84
87
  export const UI_TEXT_EN: UiText = {
@@ -111,6 +114,7 @@ export const UI_TEXT_EN: UiText = {
111
114
  userMenu: "User menu",
112
115
  signOut: "Sign out",
113
116
  theme: { label: "Theme", light: "Light", dark: "Dark", system: "System" },
117
+ sectionNav: { tabs: "Section tabs" },
114
118
  };
115
119
 
116
120
  const UiTextContext = createContext<UiText>(UI_TEXT_TH);
package/src/next.ts CHANGED
@@ -15,6 +15,11 @@ export {
15
15
  PageBreadcrumb,
16
16
  type PageBreadcrumbItem,
17
17
  } from "./components/shared/navigation/page-breadcrumb";
18
+ export {
19
+ SectionNav,
20
+ type SectionNavProps,
21
+ type SectionNavTab,
22
+ } from "./components/shared/navigation/section-nav";
18
23
  export { Stepper, type StepperStep, type StepStatus } from "./components/shared/navigation/stepper";
19
24
  export { type RowAction, type RowActionItem, RowActions } from "./components/shared/row-actions";
20
25
  export { RowMenu, type RowMenuDialog, type RowMenuItem } from "./components/shared/row-menu";