@suphark/ui 0.15.0 → 0.16.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,13 @@
|
|
|
2
2
|
|
|
3
3
|
รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
|
|
4
4
|
|
|
5
|
+
## [0.16.0] — 2026-09-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`SearchInput`** (barrel) — ช่องค้นหามาตรฐานที่ Pi Auth (~15 จุด) / Pi PDF (5) / DueBell (8) เขียนซ้ำ: ไอคอนแว่นขยายซ้าย, ปุ่มล้าง (X) ขวาเมื่อมีข้อความ (กดแล้ว focus กลับที่ช่อง), **Escape ล้างค่า** (`clearOnEscape`), `type="search"` แต่ซ่อนปุ่ม X ของ WebKit; controlled ล้วน `value` + `onValueChange(next)` (แอปคุม state/debounce เอง), `onClear`, `size` xs|sm|default (h-7/h-8/h-9), `label`/`clearLabel`/`placeholder` (ค่าเริ่มต้นจาก **`UiText.search`** `{ placeholder, label, clear }` — key ใหม่), `groupClassName` (กรอบ) แยกจาก `className` (ตัว `<input>`), ส่ง `ref` ถึง input ได้; props อื่นของ `<input>` ส่งผ่านหมด
|
|
10
|
+
- showcase: หน้า `/components/search-input`; **เมนูซ้ายของ `/components` มีช่องค้นหาแล้ว** — กรองตามชื่อ/slug/ชื่อ export/คำอธิบาย, บอกจำนวนที่พบ (`aria-live`), กด `/` ที่ไหนก็ได้เพื่อโฟกัสช่องค้นหา (ไม่ดักเมื่อกำลังพิมพ์ในช่องอื่น)
|
|
11
|
+
|
|
5
12
|
## [0.15.0] — 2026-09-30
|
|
6
13
|
|
|
7
14
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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 เอง",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useUiText } from "@suphark/ui/components/shared/ui-text";
|
|
4
|
+
import {
|
|
5
|
+
InputGroup,
|
|
6
|
+
InputGroupAddon,
|
|
7
|
+
InputGroupButton,
|
|
8
|
+
InputGroupInput,
|
|
9
|
+
} from "@suphark/ui/components/ui/input-group";
|
|
10
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
11
|
+
import { Search, X } from "lucide-react";
|
|
12
|
+
import { type ComponentProps, useRef } from "react";
|
|
13
|
+
|
|
14
|
+
export interface SearchInputProps
|
|
15
|
+
extends Omit<ComponentProps<"input">, "size" | "value" | "onChange" | "type"> {
|
|
16
|
+
value: string;
|
|
17
|
+
/** รับค่าใหม่ตรง ๆ (ไม่ใช่ event) — ปุ่มล้างก็เรียกด้วย `""` */
|
|
18
|
+
onValueChange: (value: string) => void;
|
|
19
|
+
/** เรียกเพิ่มหลังกดปุ่มล้าง/Escape (เช่นรีเซ็ตหน้า pagination) */
|
|
20
|
+
onClear?: () => void;
|
|
21
|
+
/** xs = h-7, sm = h-8 (ค่าเริ่มต้น), default = h-9 */
|
|
22
|
+
size?: "xs" | "sm" | "default";
|
|
23
|
+
/** aria-label ของช่อง (ค่าเริ่มต้น `UiText.search.label`) */
|
|
24
|
+
label?: string;
|
|
25
|
+
/** aria-label ของปุ่มล้าง (ค่าเริ่มต้น `UiText.search.clear`) */
|
|
26
|
+
clearLabel?: string;
|
|
27
|
+
/** Escape ในช่องล้างค่า (ค่าเริ่มต้น true) */
|
|
28
|
+
clearOnEscape?: boolean;
|
|
29
|
+
/** class ของกรอบ (InputGroup) — `className` ไปที่ `<input>` */
|
|
30
|
+
groupClassName?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ช่องค้นหามาตรฐาน: ไอคอนแว่นขยายซ้าย + ปุ่มล้าง (X) ขวาเมื่อมีข้อความ + Escape ล้างค่า
|
|
35
|
+
* เป็น controlled ล้วน (แอปคุม state/debounce เอง) — ซ้ำอยู่ใน Pi Auth/Pi PDF/DueBell จึงยกมาไว้ที่นี่
|
|
36
|
+
*
|
|
37
|
+
* const [q, setQ] = useState("");
|
|
38
|
+
* <SearchInput value={q} onValueChange={setQ} placeholder="ค้นหาสมาชิก…" />
|
|
39
|
+
*/
|
|
40
|
+
export function SearchInput({
|
|
41
|
+
value,
|
|
42
|
+
onValueChange,
|
|
43
|
+
onClear,
|
|
44
|
+
size = "sm",
|
|
45
|
+
label,
|
|
46
|
+
clearLabel,
|
|
47
|
+
clearOnEscape = true,
|
|
48
|
+
placeholder,
|
|
49
|
+
className,
|
|
50
|
+
groupClassName,
|
|
51
|
+
disabled,
|
|
52
|
+
onKeyDown,
|
|
53
|
+
ref,
|
|
54
|
+
...props
|
|
55
|
+
}: SearchInputProps) {
|
|
56
|
+
const text = useUiText();
|
|
57
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
58
|
+
|
|
59
|
+
function clear() {
|
|
60
|
+
onValueChange("");
|
|
61
|
+
onClear?.();
|
|
62
|
+
inputRef.current?.focus();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<InputGroup
|
|
67
|
+
data-slot="search-input"
|
|
68
|
+
className={cn(size === "xs" ? "h-7" : size === "default" ? "h-9" : "h-8", groupClassName)}
|
|
69
|
+
>
|
|
70
|
+
<InputGroupAddon>
|
|
71
|
+
<Search aria-hidden="true" className={size === "xs" ? "size-3.5" : undefined} />
|
|
72
|
+
</InputGroupAddon>
|
|
73
|
+
<InputGroupInput
|
|
74
|
+
{...props}
|
|
75
|
+
ref={(node) => {
|
|
76
|
+
inputRef.current = node;
|
|
77
|
+
if (typeof ref === "function") ref(node);
|
|
78
|
+
else if (ref) ref.current = node;
|
|
79
|
+
}}
|
|
80
|
+
type="search"
|
|
81
|
+
value={value}
|
|
82
|
+
disabled={disabled}
|
|
83
|
+
placeholder={placeholder ?? text.search?.placeholder ?? "ค้นหา…"}
|
|
84
|
+
aria-label={label ?? text.search?.label ?? "ค้นหา"}
|
|
85
|
+
autoComplete="off"
|
|
86
|
+
spellCheck={false}
|
|
87
|
+
onChange={(e) => onValueChange(e.target.value)}
|
|
88
|
+
onKeyDown={(e) => {
|
|
89
|
+
onKeyDown?.(e);
|
|
90
|
+
if (clearOnEscape && e.key === "Escape" && value && !e.defaultPrevented) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
clear();
|
|
93
|
+
}
|
|
94
|
+
}}
|
|
95
|
+
className={cn(
|
|
96
|
+
// ซ่อนปุ่ม X ของ type=search ใน WebKit — เรามีปุ่มล้างของเราเอง
|
|
97
|
+
"[&::-webkit-search-cancel-button]:appearance-none",
|
|
98
|
+
size === "xs" && "text-xs",
|
|
99
|
+
className,
|
|
100
|
+
)}
|
|
101
|
+
/>
|
|
102
|
+
{value && !disabled && (
|
|
103
|
+
<InputGroupAddon align="inline-end">
|
|
104
|
+
<InputGroupButton
|
|
105
|
+
size={size === "xs" ? "icon-xs" : "icon-sm"}
|
|
106
|
+
onClick={clear}
|
|
107
|
+
aria-label={clearLabel ?? text.search?.clear ?? "ล้างการค้นหา"}
|
|
108
|
+
>
|
|
109
|
+
<X aria-hidden="true" />
|
|
110
|
+
</InputGroupButton>
|
|
111
|
+
</InputGroupAddon>
|
|
112
|
+
)}
|
|
113
|
+
</InputGroup>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -49,6 +49,8 @@ export interface UiText {
|
|
|
49
49
|
theme?: Partial<{ label: string; light: string; dark: string; system: string }>;
|
|
50
50
|
/** SectionNav: aria-label ของแถบแท็บย่อย (optional — "แท็บย่อย") */
|
|
51
51
|
sectionNav?: Partial<{ tabs: string }>;
|
|
52
|
+
/** SearchInput: placeholder / aria-label ช่อง / aria-label ปุ่มล้าง (optional) */
|
|
53
|
+
search?: Partial<{ placeholder: string; label: string; clear: string }>;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export const UI_TEXT_TH: UiText = {
|
|
@@ -82,6 +84,7 @@ export const UI_TEXT_TH: UiText = {
|
|
|
82
84
|
signOut: "ออกจากระบบ",
|
|
83
85
|
theme: { label: "ธีม", light: "สว่าง", dark: "มืด", system: "ตามระบบ" },
|
|
84
86
|
sectionNav: { tabs: "แท็บย่อย" },
|
|
87
|
+
search: { placeholder: "ค้นหา…", label: "ค้นหา", clear: "ล้างการค้นหา" },
|
|
85
88
|
};
|
|
86
89
|
|
|
87
90
|
export const UI_TEXT_EN: UiText = {
|
|
@@ -115,6 +118,7 @@ export const UI_TEXT_EN: UiText = {
|
|
|
115
118
|
signOut: "Sign out",
|
|
116
119
|
theme: { label: "Theme", light: "Light", dark: "Dark", system: "System" },
|
|
117
120
|
sectionNav: { tabs: "Section tabs" },
|
|
121
|
+
search: { placeholder: "Search…", label: "Search", clear: "Clear search" },
|
|
118
122
|
};
|
|
119
123
|
|
|
120
124
|
const UiTextContext = createContext<UiText>(UI_TEXT_TH);
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,7 @@ export { FormFieldRadioCards } from "./components/shared/form/form-field-radio-c
|
|
|
76
76
|
export { FormFieldRadioGroup } from "./components/shared/form/form-field-radio-group";
|
|
77
77
|
export { FormFieldSelect, type SelectOption } from "./components/shared/form/form-field-select";
|
|
78
78
|
export { CheckboxField, LabeledField } from "./components/shared/form/labeled-field";
|
|
79
|
+
export { SearchInput, type SearchInputProps } from "./components/shared/form/search-input";
|
|
79
80
|
export {
|
|
80
81
|
SimpleSelect,
|
|
81
82
|
type SimpleSelectGroup,
|