1
0
forked from baron/baron-sso

Merge branch 'dev' into feature/tenant-user-list-ui-improvement

This commit is contained in:
2026-05-14 11:23:57 +09:00
37 changed files with 1080 additions and 765 deletions

View File

@@ -1 +0,0 @@

View File

@@ -0,0 +1,169 @@
import type { ReactNode, ThHTMLAttributes } from "react";
import type { SortConfig } from "../../utils";
import { commonTableHeadClass } from "../../../ui/table";
export const sortableTableHeadBaseClassName =
commonTableHeadClass;
export const sortableTableHeaderClassName =
"sticky top-0 z-10 bg-secondary shadow-sm";
function SortAscendingIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m12 5-5 5" />
<path d="m12 5 5 5" />
<path d="M12 19V5" />
</svg>
);
}
function SortDescendingIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m12 19-5-5" />
<path d="m12 19 5-5" />
<path d="M12 5v14" />
</svg>
);
}
function SortIdleIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="ml-1 h-3.5 w-3.5 opacity-50"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m7 15 5 5 5-5" />
<path d="m7 9 5-5 5 5" />
</svg>
);
}
type SortableTableHeadAlign = "left" | "center" | "right";
function alignClassName(align: SortableTableHeadAlign) {
switch (align) {
case "center":
return "text-center";
case "right":
return "text-right";
default:
return "text-left";
}
}
function buttonAlignClassName(align: SortableTableHeadAlign) {
switch (align) {
case "center":
return "justify-center";
case "right":
return "justify-end";
default:
return "justify-start";
}
}
function sortAriaValue(
isActive: boolean,
direction: "asc" | "desc" | null,
): ThHTMLAttributes<HTMLTableCellElement>["aria-sort"] {
if (!isActive || direction === null) {
return "none";
}
return direction === "asc" ? "ascending" : "descending";
}
type SortableTableHeadProps<Key extends string> = Omit<
ThHTMLAttributes<HTMLTableCellElement>,
"children"
> & {
align?: SortableTableHeadAlign;
contentClassName?: string;
disabled?: boolean;
label: ReactNode;
onSort: (key: Key) => void;
sortConfig: SortConfig<Key> | null;
sortKey: Key;
};
export function SortableTableHead<Key extends string>({
align = "left",
className = "",
contentClassName = "",
disabled = false,
label,
onSort,
sortConfig,
sortKey,
...props
}: SortableTableHeadProps<Key>) {
const isActive = sortConfig?.key === sortKey;
const direction = isActive ? sortConfig?.direction ?? null : null;
return (
<th
aria-sort={sortAriaValue(isActive, direction)}
className={[
sortableTableHeadBaseClassName,
alignClassName(align),
disabled ? "" : "transition-colors hover:bg-muted/50",
className,
]
.filter(Boolean)
.join(" ")}
{...props}
>
<button
type="button"
onClick={() => onSort(sortKey)}
disabled={disabled}
className={[
"flex w-full items-center font-inherit",
buttonAlignClassName(align),
disabled ? "cursor-default opacity-70" : "cursor-pointer",
contentClassName,
]
.filter(Boolean)
.join(" ")}
>
<span>{label}</span>
{direction === "asc" ? (
<span className="ml-1 inline-flex">
<SortAscendingIcon />
</span>
) : direction === "desc" ? (
<span className="ml-1 inline-flex">
<SortDescendingIcon />
</span>
) : (
<SortIdleIcon />
)}
</button>
</th>
);
}

View File

@@ -0,0 +1 @@
export * from "./SortableTableHead";

View File

@@ -15,6 +15,7 @@ apply = "Apply"
actions = "Actions"
add = "Add"
all = "All"
apply = "Apply"
admin_only = "Admin Only"
apply = "Apply"
approve = "Approve"

View File

@@ -15,6 +15,7 @@ apply = "적용"
actions = "액션"
add = "추가"
all = "전체"
apply = "적용"
admin_only = "관리자 전용"
apply = "적용"
approve = "승인"

View File

@@ -15,6 +15,7 @@ apply = "Apply"
actions = ""
add = ""
all = ""
apply = ""
admin_only = ""
apply = ""
approve = ""

15
common/ui/table.ts Normal file
View File

@@ -0,0 +1,15 @@
export const commonTableWrapperClass = "relative w-full";
export const commonTableClass = "w-full caption-bottom text-sm";
export const commonTableHeaderClass = "[&_tr]:border-b";
export const commonTableBodyClass = "[&_tr:last-child]:border-0";
export const commonTableFooterClass = "bg-muted/50 font-medium text-foreground";
export const commonTableRowClass =
"border-b transition-colors hover:bg-muted/30 data-[state=selected]:bg-muted";
export const commonTableHeadClass =
"h-12 px-6 text-left text-xs font-sans font-bold uppercase tracking-[0.08em] text-foreground align-middle";
export const commonTableCellClass = "p-6 align-middle text-sm";
export const commonTableCaptionClass = "mt-4 text-sm text-muted-foreground";
export const commonTableShellClass =
"flex-1 rounded-md border overflow-hidden flex flex-col";
export const commonTableViewportClass =
"flex-1 overflow-auto relative custom-scrollbar";