diff --git a/src/app/globals.css b/src/app/globals.css
index 314d9e2..5a11a1c 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -175,13 +175,36 @@
@apply text-sm text-muted-foreground;
}
- [data-slot="table-cell"]:has(> [data-slot="button"]),
- [data-slot="table-cell"]:has(> a) {
+ [data-slot="table-head"],
+ [data-slot="table-cell"] {
text-align: center;
}
- [data-slot="table-cell"] > .flex:has([data-slot="button"]),
- [data-slot="table-cell"] > .flex:has(a) {
- justify-content: center;
+ [data-slot="table-cell"] > .flex,
+ [data-slot="table-head"] > .flex {
+ justify-content: center !important;
+ }
+
+ [data-slot="table-cell"] > .flex.flex-col {
+ align-items: center !important;
+ }
+
+ [data-slot="table-cell"] > .flex.flex-wrap {
+ justify-content: center !important;
+ }
+
+ [data-slot="table-cell"] [data-slot="badge"],
+ [data-slot="table-cell"] [data-slot="switch"],
+ [data-slot="table-cell"] [role="checkbox"],
+ [data-slot="table-cell"] [data-slot="button"],
+ [data-slot="table-cell"] > a {
+ margin-inline: auto;
+ }
+
+ [data-slot="table-cell"]:has(> [data-slot="button"]),
+ [data-slot="table-cell"]:has(> a),
+ [data-slot="table-cell"]:has(> [role="checkbox"]),
+ [data-slot="table-cell"]:has(> [data-slot="switch"]) {
+ text-align: center;
}
}
diff --git a/src/components/admin/admin-table-export-button.tsx b/src/components/admin/admin-table-export-button.tsx
index 27d383e..881d8ee 100644
--- a/src/components/admin/admin-table-export-button.tsx
+++ b/src/components/admin/admin-table-export-button.tsx
@@ -15,7 +15,10 @@ const OMIT_HEADER_TOKENS = [
"download",
] as const;
-function shouldOmitColumn(headerText: string): boolean {
+function shouldOmitColumn(cell: Element, headerText: string): boolean {
+ if (cell.hasAttribute("data-export-ignore")) {
+ return true;
+ }
const normalized = headerText.trim().toLowerCase();
if (normalized === "") {
return true;
@@ -33,9 +36,10 @@ function stripOmittedColumns(table: HTMLTableElement): HTMLTableElement {
const omitIndexes = Array.from(headerRow.children)
.map((cell, index) => ({
index,
+ cell,
text: cell.textContent ?? "",
}))
- .filter((item) => shouldOmitColumn(item.text))
+ .filter((item) => shouldOmitColumn(item.cell, item.text))
.map((item) => item.index)
.sort((a, b) => b - a);
diff --git a/src/components/admin/confirmable-switch.tsx b/src/components/admin/confirmable-switch.tsx
new file mode 100644
index 0000000..e2dcc12
--- /dev/null
+++ b/src/components/admin/confirmable-switch.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { Switch } from "@/components/ui/switch";
+
+export type ConfirmableSwitchProps = {
+ checked: boolean;
+ disabled?: boolean;
+ confirmBusy?: boolean;
+ "aria-label": string;
+ onCheckedChange: (checked: boolean) => void;
+};
+
+/**
+ * Controlled switch for flows that confirm before applying state.
+ * Disables interaction while a confirm dialog is in progress.
+ */
+export function ConfirmableSwitch({
+ checked,
+ disabled,
+ confirmBusy,
+ "aria-label": ariaLabel,
+ onCheckedChange,
+}: ConfirmableSwitchProps) {
+ return (
+
+ );
+}
diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx
index cf34b3b..241c713 100644
--- a/src/components/ui/table.tsx
+++ b/src/components/ui/table.tsx
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
) {
|
-
-
-
+
+ {role.status === 1 ? t("status.enabled") : t("status.disabled")}
+
{role.user_count}
{role.permission_slugs.length}
diff --git a/src/modules/admin-users/admin-users-console.tsx b/src/modules/admin-users/admin-users-console.tsx
index eaf0848..294930f 100644
--- a/src/modules/admin-users/admin-users-console.tsx
+++ b/src/modules/admin-users/admin-users-console.tsx
@@ -16,7 +16,9 @@ import {
} from "@/api/admin-users";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
+import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Badge } from "@/components/ui/badge";
+import { resolveAdminUserStatusTone } from "@/lib/admin-status-tone";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -391,13 +393,9 @@ export function AdminUsersConsole(): React.ReactElement {
{row.nickname ?? ""}
-
-
-
+
+ {row.status === 0 ? t("status.enabled") : t("status.disabled")}
+
diff --git a/src/modules/config/config-chip-group.tsx b/src/modules/config/config-chip-group.tsx
index 9373722..173ca4a 100644
--- a/src/modules/config/config-chip-group.tsx
+++ b/src/modules/config/config-chip-group.tsx
@@ -25,16 +25,17 @@ type ConfigChipProps = {
onClick: () => void;
children: ReactNode;
disabled?: boolean;
+ className?: string;
};
-export function ConfigChip({ active, onClick, children, disabled }: ConfigChipProps) {
+export function ConfigChip({ active, onClick, children, disabled, className }: ConfigChipProps) {
return (
|