diff --git a/databi/src/social/views/team-kpi-view.css b/databi/src/social/views/team-kpi-view.css
index be703f6..448bb69 100644
--- a/databi/src/social/views/team-kpi-view.css
+++ b/databi/src/social/views/team-kpi-view.css
@@ -8,10 +8,57 @@
.sbi-kpi-board-scroll {
max-height: 620px;
- margin-top: 12px;
border-radius: 0 0 var(--sbi-radius) var(--sbi-radius);
}
+.sbi-kpi-summary {
+ display: grid;
+ grid-template-columns: minmax(220px, 1fr) repeat(2, minmax(170px, 220px));
+ margin-top: 12px;
+ border-top: 1px solid var(--sbi-border);
+ border-bottom: 1px solid var(--sbi-border);
+ background: #f7fafd;
+}
+
+.sbi-kpi-summary-person,
+.sbi-kpi-summary-amount {
+ display: grid;
+ gap: 3px;
+ min-width: 0;
+ padding: 14px 18px;
+}
+
+.sbi-kpi-summary-amount {
+ align-content: center;
+ border-left: 1px solid var(--sbi-border);
+ text-align: right;
+}
+
+.sbi-kpi-summary span,
+.sbi-kpi-summary small {
+ color: var(--sbi-text-3);
+ font-size: 11.5px;
+}
+
+.sbi-kpi-summary-person strong {
+ overflow: hidden;
+ font-size: 14px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.sbi-kpi-summary-amount strong {
+ color: var(--sbi-accent);
+ font-size: 22px;
+ font-variant-numeric: tabular-nums;
+}
+
+.sbi-table td.sbi-kpi-empty-cell {
+ height: 120px;
+ color: var(--sbi-text-3);
+ text-align: center;
+}
+
.sbi-kpi-rank {
display: inline-grid;
width: 24px;
@@ -67,3 +114,9 @@
gap: 14px;
padding: 18px;
}
+
+@media (max-width: 1280px) {
+ .sbi-kpi-summary {
+ grid-template-columns: minmax(180px, 1fr) repeat(2, minmax(150px, 190px));
+ }
+}
diff --git a/databi/src/styles/social-v2.css b/databi/src/styles/social-v2.css
index ff19fdc..05219de 100644
--- a/databi/src/styles/social-v2.css
+++ b/databi/src/styles/social-v2.css
@@ -344,6 +344,25 @@
font-size: 12.5px;
}
+.sbi-operator-filter {
+ width: 210px;
+}
+
+.sbi-operator-option {
+ display: grid !important;
+ gap: 2px;
+}
+
+.sbi-operator-option strong {
+ color: var(--sbi-text);
+ font-size: 13px;
+}
+
+.sbi-operator-option small {
+ color: var(--sbi-text-3);
+ font-size: 11.5px;
+}
+
.sbi-range-label {
color: var(--sbi-text-3);
font-size: 12.5px;
diff --git a/external-admin/index.html b/external-admin/index.html
index 61d85dd..8c5c043 100644
--- a/external-admin/index.html
+++ b/external-admin/index.html
@@ -3,8 +3,9 @@
-
+
External Admin
+
diff --git a/external-admin/src/i18n/emotionCache.js b/external-admin/src/i18n/emotionCache.js
index 7f86a56..cb5ada7 100644
--- a/external-admin/src/i18n/emotionCache.js
+++ b/external-admin/src/i18n/emotionCache.js
@@ -1,9 +1,13 @@
import createCache from "@emotion/cache";
import rtlPlugin from "@mui/stylis-plugin-rtl";
-import { prefixer } from "stylis";
+// Emotion compiles styles with its own bundled stylis (4.2); running the standalone
+// stylis 4.4 `prefixer` over those foreign AST nodes crashes in lift() on
+// ::placeholder/:read-only rules because 4.2 elements carry no `siblings` field.
+// The RTL cache therefore only runs the direction-flipping plugin; vendor prefixing
+// is left out, which modern mobile browsers no longer need.
const externalLtrCache = createCache({ key: "external-ltr", prepend: true });
-const externalRtlCache = createCache({ key: "external-rtl", prepend: true, stylisPlugins: [prefixer, rtlPlugin] });
+const externalRtlCache = createCache({ key: "external-rtl", prepend: true, stylisPlugins: [rtlPlugin] });
export function externalEmotionCache(direction) {
return direction === "rtl" ? externalRtlCache : externalLtrCache;
diff --git a/external-admin/src/i18n/emotionCache.test.js b/external-admin/src/i18n/emotionCache.test.js
index 62592e8..e861ab8 100644
--- a/external-admin/src/i18n/emotionCache.test.js
+++ b/external-admin/src/i18n/emotionCache.test.js
@@ -1,5 +1,9 @@
+import { CacheProvider } from "@emotion/react";
+import styled from "@emotion/styled";
import rtlPlugin from "@mui/stylis-plugin-rtl";
-import { compile, middleware, prefixer, serialize, stringify } from "stylis";
+import { render } from "@testing-library/react";
+import { createElement } from "react";
+import { compile, middleware, serialize, stringify } from "stylis";
import { describe, expect, test } from "vitest";
import { externalEmotionCache } from "./emotionCache.js";
@@ -13,7 +17,7 @@ describe("external admin RTL Emotion cache", () => {
test("mirrors physical MUI-style declarations for Arabic", () => {
const css = serialize(
compile(".control{margin-left:8px;padding-right:12px;text-align:left}"),
- middleware([prefixer, rtlPlugin, stringify])
+ middleware([rtlPlugin, stringify])
);
expect(css).toContain("margin-right:8px");
@@ -21,4 +25,15 @@ describe("external admin RTL Emotion cache", () => {
expect(css).toContain("text-align:right");
expect(css).not.toContain("margin-left:8px");
});
+
+ test("inserts ::placeholder rules through the RTL cache without crashing", () => {
+ // Regression: the standalone stylis 4.4 prefixer crashed in lift() when Emotion's
+ // bundled stylis 4.2 compiled a ::placeholder rule, unmounting the whole app in Arabic.
+ const Input = styled.input({ "&::placeholder": { color: "red", textAlign: "left" } });
+
+ expect(() => render(
+ createElement(CacheProvider, { value: externalEmotionCache("rtl") },
+ createElement(Input, { placeholder: "مثال" }))
+ )).not.toThrow();
+ });
});
diff --git a/external-admin/src/i18n/messages.js b/external-admin/src/i18n/messages.js
index 71b14ec..c420b8d 100644
--- a/external-admin/src/i18n/messages.js
+++ b/external-admin/src/i18n/messages.js
@@ -35,9 +35,9 @@ const en = {
"common.save": "Save",
"common.status": "Status",
"common.user": "User",
- "auth.account": "External admin account",
- "auth.changePassword": "Change external admin password",
- "auth.checkingSession": "Checking external admin session",
+ "auth.account": "Account",
+ "auth.changePassword": "Change password",
+ "auth.checkingSession": "Checking your session",
"auth.confirmNewPassword": "Confirm new password",
"auth.currentPassword": "Current password",
"auth.fillAccountPassword": "Enter your account and password",
@@ -48,7 +48,7 @@ const en = {
"auth.loginFailed": "Sign-in failed",
"auth.logout": "Sign out",
"auth.newPassword": "New password",
- "auth.noPermission": "This external admin account does not have access to this feature.",
+ "auth.noPermission": "This account does not have access to this feature.",
"auth.password": "Password",
"auth.passwordChangedFailed": "Failed to change password",
"auth.passwordMismatch": "The new passwords do not match",
@@ -56,12 +56,13 @@ const en = {
"auth.passwordMinError": "The new password must be at least 8 characters",
"auth.passwordWhitespace": "The new password cannot be empty or contain only whitespace",
"auth.recheck": "Check again",
- "auth.returnOverview": "Return to permission overview",
+ "auth.returnOverview": "Back to Home",
"auth.savePassword": "Save password",
"auth.sessionFailed": "Session verification failed",
"auth.showPassword": "Show password",
"nav.expand": "Open navigation",
- "nav.overview": "Permission overview",
+ "nav.more": "More",
+ "nav.overview": "Home",
"nav.users": "User management",
"nav.bans": "Ban management",
"nav.hosts": "Hosts",
@@ -94,8 +95,10 @@ const en = {
"capability.userLevelGrant": "Grant wealth/VIP levels",
"capability.teamView": "My team",
"overview.currentApp": "Current App",
- "overview.account": "External admin account",
+ "overview.account": "Account",
"overview.permissions": "Permissions enabled",
+ "overview.welcome": "Welcome back",
+ "overview.features": "Features",
"overview.enabled": "Enabled",
"overview.notEnabled": "Not enabled",
"users.title": "User management",
@@ -325,13 +328,13 @@ const zhCN = {
"app.shortTitle": "外管后台",
"language.label": "语言",
"common.actions": "操作", "common.active": "正常", "common.all": "全部", "common.allStatuses": "全部状态", "common.banned": "已封禁", "common.cancel": "取消", "common.confirm": "确认", "common.create": "创建", "common.disabled": "禁用", "common.edit": "编辑", "common.enabled": "启用", "common.expired": "过期", "common.loading": "正在加载", "common.noData": "当前无数据", "common.normal": "正常", "common.query": "查询", "common.reason": "原因", "common.reload": "重新加载", "common.retry": "重试", "common.save": "保存", "common.status": "状态", "common.user": "用户",
- "auth.account": "外管账号", "auth.changePassword": "修改外管密码", "auth.checkingSession": "正在校验外管会话", "auth.confirmNewPassword": "确认新密码", "auth.currentPassword": "当前密码", "auth.fillAccountPassword": "请填写账号和密码", "auth.firstLoginChange": "首次登录必须修改密码", "auth.hidePassword": "隐藏密码", "auth.invalidCredentials": "账号或密码错误", "auth.login": "登录", "auth.loginFailed": "登录失败", "auth.logout": "退出登录", "auth.newPassword": "新密码", "auth.noPermission": "当前外管账号没有访问此功能的权限。", "auth.password": "密码", "auth.passwordChangedFailed": "密码修改失败", "auth.passwordMismatch": "两次输入的新密码不一致", "auth.passwordMin": "至少 8 个字符", "auth.passwordMinError": "新密码至少 8 个字符", "auth.passwordWhitespace": "新密码不能为空或全为空白字符", "auth.recheck": "重新校验", "auth.returnOverview": "返回权限总览", "auth.savePassword": "保存密码", "auth.sessionFailed": "会话校验失败", "auth.showPassword": "显示密码",
- "nav.expand": "展开菜单", "nav.overview": "权限总览", "nav.users": "用户管理", "nav.bans": "封禁管理", "nav.hosts": "主播列表", "nav.agencies": "公会列表", "nav.bds": "BD 列表", "nav.bdManagers": "BD Manager 列表", "nav.superAdmins": "Super Admin 列表", "nav.rooms": "房间管理", "nav.grants": "资源与靓号", "nav.banners": "Banner 管理", "nav.team": "我的团队",
+ "auth.account": "账号", "auth.changePassword": "修改密码", "auth.checkingSession": "正在校验会话", "auth.confirmNewPassword": "确认新密码", "auth.currentPassword": "当前密码", "auth.fillAccountPassword": "请填写账号和密码", "auth.firstLoginChange": "首次登录必须修改密码", "auth.hidePassword": "隐藏密码", "auth.invalidCredentials": "账号或密码错误", "auth.login": "登录", "auth.loginFailed": "登录失败", "auth.logout": "退出登录", "auth.newPassword": "新密码", "auth.noPermission": "当前账号没有访问此功能的权限。", "auth.password": "密码", "auth.passwordChangedFailed": "密码修改失败", "auth.passwordMismatch": "两次输入的新密码不一致", "auth.passwordMin": "至少 8 个字符", "auth.passwordMinError": "新密码至少 8 个字符", "auth.passwordWhitespace": "新密码不能为空或全为空白字符", "auth.recheck": "重新校验", "auth.returnOverview": "返回首页", "auth.savePassword": "保存密码", "auth.sessionFailed": "会话校验失败", "auth.showPassword": "显示密码",
+ "nav.expand": "展开菜单", "nav.more": "更多", "nav.overview": "首页", "nav.users": "用户管理", "nav.bans": "封禁管理", "nav.hosts": "主播列表", "nav.agencies": "公会列表", "nav.bds": "BD 列表", "nav.bdManagers": "BD Manager 列表", "nav.superAdmins": "Super Admin 列表", "nav.rooms": "房间管理", "nav.grants": "资源与靓号", "nav.banners": "Banner 管理", "nav.team": "我的团队",
"capability.userList": "用户列表", "capability.userUpdate": "用户信息编辑", "capability.banList": "账号封禁列表", "capability.ban": "执行封禁", "capability.unban": "解除封禁", "capability.hostList": "主播列表", "capability.agencyList": "公会列表", "capability.bdList": "BD 列表", "capability.bdManagerList": "BD Manager 列表", "capability.superAdminList": "Super Admin 列表", "capability.roomList": "房间管理", "capability.roomUpdate": "房间编辑", "capability.privilegeList": "用户特权道具列表", "capability.privilegeGrant": "特权道具发放", "capability.bannerCreate": "Banner 创建", "capability.prettyIdGrant": "靓号下发", "capability.userTitleGrant": "用户称号下发", "capability.roomBackgroundGrant": "房间背景图下发", "capability.userLevelGrant": "财富/VIP 等级下发", "capability.teamView": "我的团队",
- "overview.currentApp": "当前 App", "overview.account": "外管账号", "overview.permissions": "已开通权限", "overview.enabled": "已开通", "overview.notEnabled": "未开通",
+ "overview.currentApp": "当前 App", "overview.account": "登录账号", "overview.permissions": "已开通权限", "overview.welcome": "欢迎回来", "overview.features": "功能入口", "overview.enabled": "已开通", "overview.notEnabled": "未开通",
"users.title": "用户管理", "users.search": "用户 ID / 短 ID / 名称", "users.prettyId": "短 ID(靓号)", "users.countryGender": "国家/性别", "users.level": "等级", "users.levelSummary": "财富 {wealth} / 魅力 {charm} / VIP {vip}", "users.edit": "编辑", "users.grantLevel": "等级", "users.ban": "封禁", "users.updated": "用户信息已更新", "users.banned": "用户已封禁", "users.levelGranted": "用户等级已下发", "users.operationFailed": "用户操作失败", "users.editTitle": "编辑用户", "users.avatar": "头像", "users.name": "用户名称", "users.gender": "性别", "users.countryCode": "国家/地区码", "users.notSet": "未设置", "users.genderMale": "男", "users.genderFemale": "女", "users.genderUnknown": "未知", "users.banTitle": "封禁用户 {user}", "users.banDays": "封禁天数", "users.banForeverHint": "填 0 表示永久封禁", "users.banReason": "封禁原因", "users.levelTitle": "等级下发 {user}", "users.levelType": "等级类型", "users.wealthLevel": "财富等级", "users.charmLevel": "魅力等级", "users.vipLevel": "VIP 等级", "users.validDays": "有效天数", "users.validDaysMin": "有效期至少 1 天", "users.grantReason": "下发原因",
"bans.title": "账号封禁列表", "bans.reason": "封禁原因", "bans.unban": "解除封禁", "bans.unbanned": "用户已解除封禁", "bans.unbanFailed": "解除封禁失败", "bans.confirmTitle": "解除封禁", "bans.confirmContent": "确认解除用户 {user} 的封禁?",
- "organization.title": "组织管理", "organization.agencies": "公会列表", "organization.bds": "BD 列表", "organization.bdManagers": "BD Manager 列表", "organization.hosts": "主播列表", "organization.managers": "管理员列表", "organization.superAdmins": "Super Admin 列表", "organization.team": "我的团队", "organization.agency": "公会", "organization.owner": "负责人", "organization.parentBd": "上级 BD", "organization.member": "成员", "organization.country": "国家/地区", "organization.userReference": "用户 {id}", "organization.agencyReference": "公会 {id}", "organization.teamSearch": "公会 ID / 负责人用户 ID / 上级 BD ID", "organization.noPermission": "当前外管账号没有访问该组织列表的权限。", "organization.bdLeaderCount": "BD Leader:{count}", "organization.bdCount": "BD:{count}", "organization.agencyCount": "公会:{count}", "organization.truncated": "团队规模超过接口上限,当前结果不完整",
+ "organization.title": "组织管理", "organization.agencies": "公会列表", "organization.bds": "BD 列表", "organization.bdManagers": "BD Manager 列表", "organization.hosts": "主播列表", "organization.managers": "管理员列表", "organization.superAdmins": "Super Admin 列表", "organization.team": "我的团队", "organization.agency": "公会", "organization.owner": "负责人", "organization.parentBd": "上级 BD", "organization.member": "成员", "organization.country": "国家/地区", "organization.userReference": "用户 {id}", "organization.agencyReference": "公会 {id}", "organization.teamSearch": "公会 ID / 负责人用户 ID / 上级 BD ID", "organization.noPermission": "当前账号没有访问该组织列表的权限。", "organization.bdLeaderCount": "BD Leader:{count}", "organization.bdCount": "BD:{count}", "organization.agencyCount": "公会:{count}", "organization.truncated": "团队规模超过接口上限,当前结果不完整",
"rooms.title": "房间管理", "rooms.room": "房间", "rooms.owner": "房主", "rooms.visibleRegion": "可见区域", "rooms.search": "房间 ID / 名称 / 房主", "rooms.updated": "房间信息已更新", "rooms.updateFailed": "房间更新失败", "rooms.editTitle": "编辑房间", "rooms.background": "房间背景图", "rooms.name": "房间名称", "rooms.description": "房间介绍", "rooms.visibleRegionId": "可见区域 ID", "rooms.allRegions": "全部",
"grants.title": "资源与靓号", "grants.grantResource": "发放道具", "grants.grantPrettyId": "下发靓号", "grants.availableResources": "可发放资源", "grants.targetUser": "目标用户", "grants.resource": "资源", "grants.quantity": "数量", "grants.resourceGranted": "特权道具已发放", "grants.prettyIdGranted": "靓号已下发", "grants.failed": "发放失败", "grants.resourceTitle": "特权道具发放", "grants.prettyIdTitle": "靓号下发", "grants.target": "用户 ID / 短 ID(靓号)", "grants.currentTarget": "用户 ID / 当前短 ID", "grants.prettyId": "下发靓号", "grants.validDays": "有效天数", "grants.permanentHint": "填 0 表示永久有效", "grants.reason": "发放原因", "grants.confirm": "确认发放",
"banners.title": "Banner 管理", "banners.banner": "Banner", "banners.type": "类型", "banners.platform": "平台", "banners.sort": "排序", "banners.create": "创建 Banner", "banners.created": "Banner 已创建", "banners.createFailed": "Banner 创建失败", "banners.image": "Banner 图片", "banners.roomImage": "房间内小屏图", "banners.description": "描述", "banners.jumpType": "跳转类型", "banners.h5Param": "参数(H5 链接)", "banners.appParam": "参数(APP 公共跳转 JSON)", "banners.displayScope": "展示范围", "banners.scopeHome": "首页", "banners.scopeRoom": "房间", "banners.scopeRecharge": "充值", "banners.scopeMe": "我的", "banners.regionId": "区域 ID", "banners.countryCode": "国家码", "banners.countryCodeHint": "2-3 位字母,留空为全部", "banners.schedule": "投放时间",
@@ -347,10 +350,10 @@ const ar = {
...en,
"app.title": "الإدارة الخارجية", "app.shortTitle": "الإدارة الخارجية", "language.label": "اللغة",
"common.actions": "الإجراءات", "common.active": "نشط", "common.all": "الكل", "common.allStatuses": "كل الحالات", "common.banned": "محظور", "common.cancel": "إلغاء", "common.confirm": "تأكيد", "common.create": "إنشاء", "common.disabled": "معطّل", "common.edit": "تعديل", "common.enabled": "مفعّل", "common.expired": "منتهي", "common.loading": "جارٍ التحميل", "common.noData": "لا توجد بيانات", "common.normal": "طبيعي", "common.query": "بحث", "common.reason": "السبب", "common.reload": "إعادة التحميل", "common.retry": "إعادة المحاولة", "common.save": "حفظ", "common.status": "الحالة", "common.user": "المستخدم",
- "auth.account": "حساب الإدارة الخارجية", "auth.changePassword": "تغيير كلمة مرور الإدارة الخارجية", "auth.checkingSession": "جارٍ التحقق من الجلسة", "auth.confirmNewPassword": "تأكيد كلمة المرور الجديدة", "auth.currentPassword": "كلمة المرور الحالية", "auth.fillAccountPassword": "أدخل الحساب وكلمة المرور", "auth.firstLoginChange": "يجب تغيير كلمة المرور عند تسجيل الدخول لأول مرة", "auth.hidePassword": "إخفاء كلمة المرور", "auth.invalidCredentials": "الحساب أو كلمة المرور غير صحيحة", "auth.login": "تسجيل الدخول", "auth.loginFailed": "فشل تسجيل الدخول", "auth.logout": "تسجيل الخروج", "auth.newPassword": "كلمة المرور الجديدة", "auth.noPermission": "لا يملك هذا الحساب صلاحية الوصول إلى هذه الميزة.", "auth.password": "كلمة المرور", "auth.passwordChangedFailed": "فشل تغيير كلمة المرور", "auth.passwordMismatch": "كلمتا المرور الجديدتان غير متطابقتين", "auth.passwordMin": "8 أحرف على الأقل", "auth.passwordMinError": "يجب أن تتكون كلمة المرور الجديدة من 8 أحرف على الأقل", "auth.passwordWhitespace": "لا يمكن أن تكون كلمة المرور الجديدة فارغة أو مسافات فقط", "auth.recheck": "إعادة التحقق", "auth.returnOverview": "العودة إلى ملخص الصلاحيات", "auth.savePassword": "حفظ كلمة المرور", "auth.sessionFailed": "فشل التحقق من الجلسة", "auth.showPassword": "إظهار كلمة المرور",
- "nav.expand": "فتح القائمة", "nav.overview": "ملخص الصلاحيات", "nav.users": "إدارة المستخدمين", "nav.bans": "إدارة الحظر", "nav.hosts": "المضيفون", "nav.agencies": "الوكالات", "nav.bds": "قائمة BD", "nav.bdManagers": "مديرو BD", "nav.superAdmins": "المشرفون العامون", "nav.rooms": "إدارة الغرف", "nav.grants": "الموارد والمعرّفات المميزة", "nav.banners": "إدارة اللافتات", "nav.team": "فريقي",
+ "auth.account": "الحساب", "auth.changePassword": "تغيير كلمة المرور", "auth.checkingSession": "جارٍ التحقق من الجلسة", "auth.confirmNewPassword": "تأكيد كلمة المرور الجديدة", "auth.currentPassword": "كلمة المرور الحالية", "auth.fillAccountPassword": "أدخل الحساب وكلمة المرور", "auth.firstLoginChange": "يجب تغيير كلمة المرور عند تسجيل الدخول لأول مرة", "auth.hidePassword": "إخفاء كلمة المرور", "auth.invalidCredentials": "الحساب أو كلمة المرور غير صحيحة", "auth.login": "تسجيل الدخول", "auth.loginFailed": "فشل تسجيل الدخول", "auth.logout": "تسجيل الخروج", "auth.newPassword": "كلمة المرور الجديدة", "auth.noPermission": "لا يملك هذا الحساب صلاحية الوصول إلى هذه الميزة.", "auth.password": "كلمة المرور", "auth.passwordChangedFailed": "فشل تغيير كلمة المرور", "auth.passwordMismatch": "كلمتا المرور الجديدتان غير متطابقتين", "auth.passwordMin": "8 أحرف على الأقل", "auth.passwordMinError": "يجب أن تتكون كلمة المرور الجديدة من 8 أحرف على الأقل", "auth.passwordWhitespace": "لا يمكن أن تكون كلمة المرور الجديدة فارغة أو مسافات فقط", "auth.recheck": "إعادة التحقق", "auth.returnOverview": "العودة إلى الرئيسية", "auth.savePassword": "حفظ كلمة المرور", "auth.sessionFailed": "فشل التحقق من الجلسة", "auth.showPassword": "إظهار كلمة المرور",
+ "nav.expand": "فتح القائمة", "nav.more": "المزيد", "nav.overview": "الرئيسية", "nav.users": "إدارة المستخدمين", "nav.bans": "إدارة الحظر", "nav.hosts": "المضيفون", "nav.agencies": "الوكالات", "nav.bds": "قائمة BD", "nav.bdManagers": "مديرو BD", "nav.superAdmins": "المشرفون العامون", "nav.rooms": "إدارة الغرف", "nav.grants": "الموارد والمعرّفات المميزة", "nav.banners": "إدارة اللافتات", "nav.team": "فريقي",
"capability.userList": "قائمة المستخدمين", "capability.userUpdate": "تعديل بيانات المستخدم", "capability.banList": "قائمة الحسابات المحظورة", "capability.ban": "حظر المستخدمين", "capability.unban": "إلغاء حظر المستخدمين", "capability.hostList": "قائمة المضيفين", "capability.agencyList": "قائمة الوكالات", "capability.bdList": "قائمة BD", "capability.bdManagerList": "قائمة مديري BD", "capability.superAdminList": "قائمة المشرفين العامين", "capability.roomList": "إدارة الغرف", "capability.roomUpdate": "تعديل الغرف", "capability.privilegeList": "قائمة عناصر الامتياز", "capability.privilegeGrant": "منح عناصر الامتياز", "capability.bannerCreate": "إنشاء اللافتات", "capability.prettyIdGrant": "منح المعرّفات المميزة", "capability.userTitleGrant": "منح ألقاب المستخدمين", "capability.roomBackgroundGrant": "منح خلفيات الغرف", "capability.userLevelGrant": "منح مستويات الثروة/VIP", "capability.teamView": "فريقي",
- "overview.currentApp": "التطبيق الحالي", "overview.account": "حساب الإدارة الخارجية", "overview.permissions": "الصلاحيات المفعّلة", "overview.enabled": "مفعّلة", "overview.notEnabled": "غير مفعّلة",
+ "overview.currentApp": "التطبيق الحالي", "overview.account": "الحساب", "overview.permissions": "الصلاحيات المفعّلة", "overview.welcome": "مرحبًا بعودتك", "overview.features": "الخدمات", "overview.enabled": "مفعّلة", "overview.notEnabled": "غير مفعّلة",
"users.title": "إدارة المستخدمين", "users.search": "معرّف المستخدم / المعرّف المميز / الاسم", "users.prettyId": "المعرّف المميز", "users.countryGender": "الدولة / الجنس", "users.level": "المستوى", "users.levelSummary": "الثروة {wealth} / الجاذبية {charm} / VIP {vip}", "users.edit": "تعديل", "users.grantLevel": "المستوى", "users.ban": "حظر", "users.updated": "تم تحديث بيانات المستخدم", "users.banned": "تم حظر المستخدم", "users.levelGranted": "تم منح مستوى المستخدم", "users.operationFailed": "فشلت عملية المستخدم", "users.editTitle": "تعديل المستخدم", "users.avatar": "الصورة الشخصية", "users.name": "اسم المستخدم", "users.gender": "الجنس", "users.countryCode": "رمز الدولة/المنطقة", "users.notSet": "غير محدد", "users.genderMale": "ذكر", "users.genderFemale": "أنثى", "users.genderUnknown": "غير معروف", "users.banTitle": "حظر المستخدم {user}", "users.banDays": "مدة الحظر (بالأيام)", "users.banForeverHint": "أدخل 0 للحظر الدائم", "users.banReason": "سبب الحظر", "users.levelTitle": "منح مستوى إلى {user}", "users.levelType": "نوع المستوى", "users.wealthLevel": "مستوى الثروة", "users.charmLevel": "مستوى الجاذبية", "users.vipLevel": "مستوى VIP", "users.validDays": "الصلاحية (بالأيام)", "users.validDaysMin": "يجب ألا تقل الصلاحية عن يوم واحد", "users.grantReason": "سبب المنح",
"bans.title": "قائمة الحسابات المحظورة", "bans.reason": "سبب الحظر", "bans.unban": "إلغاء الحظر", "bans.unbanned": "تم إلغاء حظر المستخدم", "bans.unbanFailed": "فشل إلغاء حظر المستخدم", "bans.confirmTitle": "إلغاء حظر المستخدم", "bans.confirmContent": "هل تريد إلغاء حظر {user}؟",
"organization.title": "المؤسسة", "organization.agencies": "قائمة الوكالات", "organization.bds": "قائمة BD", "organization.bdManagers": "قائمة مديري BD", "organization.hosts": "قائمة المضيفين", "organization.managers": "قائمة المديرين", "organization.superAdmins": "قائمة المشرفين العامين", "organization.team": "فريقي", "organization.agency": "الوكالة", "organization.owner": "المالك", "organization.parentBd": "BD الأعلى", "organization.member": "العضو", "organization.country": "الدولة / المنطقة", "organization.userReference": "المستخدم {id}", "organization.agencyReference": "الوكالة {id}", "organization.teamSearch": "معرّف الوكالة / معرّف المالك / معرّف BD الأعلى", "organization.noPermission": "لا يملك هذا الحساب صلاحية الوصول إلى قائمة المؤسسة هذه.", "organization.bdLeaderCount": "قادة BD: {count}", "organization.bdCount": "BD: {count}", "organization.agencyCount": "الوكالات: {count}", "organization.truncated": "يتجاوز حجم الفريق حد الواجهة. النتائج الحالية غير مكتملة.",
@@ -369,10 +372,10 @@ const tr = {
...en,
"app.title": "Harici Yönetim", "app.shortTitle": "Harici Yönetim", "language.label": "Dil",
"common.actions": "İşlemler", "common.active": "Aktif", "common.all": "Tümü", "common.allStatuses": "Tüm durumlar", "common.banned": "Yasaklı", "common.cancel": "İptal", "common.confirm": "Onayla", "common.create": "Oluştur", "common.disabled": "Devre dışı", "common.edit": "Düzenle", "common.enabled": "Etkin", "common.expired": "Süresi dolmuş", "common.loading": "Yükleniyor", "common.noData": "Veri yok", "common.normal": "Normal", "common.query": "Ara", "common.reason": "Neden", "common.reload": "Yeniden yükle", "common.retry": "Tekrar dene", "common.save": "Kaydet", "common.status": "Durum", "common.user": "Kullanıcı",
- "auth.account": "Harici yönetim hesabı", "auth.changePassword": "Harici yönetim parolasını değiştir", "auth.checkingSession": "Oturum kontrol ediliyor", "auth.confirmNewPassword": "Yeni parolayı doğrula", "auth.currentPassword": "Mevcut parola", "auth.fillAccountPassword": "Hesabınızı ve parolanızı girin", "auth.firstLoginChange": "İlk girişte parolanızı değiştirmeniz gerekir", "auth.hidePassword": "Parolayı gizle", "auth.invalidCredentials": "Hesap veya parola hatalı", "auth.login": "Giriş yap", "auth.loginFailed": "Giriş başarısız", "auth.logout": "Çıkış yap", "auth.newPassword": "Yeni parola", "auth.noPermission": "Bu harici yönetim hesabının bu özelliğe erişimi yok.", "auth.password": "Parola", "auth.passwordChangedFailed": "Parola değiştirilemedi", "auth.passwordMismatch": "Yeni parolalar eşleşmiyor", "auth.passwordMin": "En az 8 karakter", "auth.passwordMinError": "Yeni parola en az 8 karakter olmalıdır", "auth.passwordWhitespace": "Yeni parola boş veya yalnızca boşluk olamaz", "auth.recheck": "Tekrar kontrol et", "auth.returnOverview": "Yetki özetine dön", "auth.savePassword": "Parolayı kaydet", "auth.sessionFailed": "Oturum doğrulanamadı", "auth.showPassword": "Parolayı göster",
- "nav.expand": "Menüyü aç", "nav.overview": "Yetki özeti", "nav.users": "Kullanıcı yönetimi", "nav.bans": "Yasak yönetimi", "nav.hosts": "Yayıncılar", "nav.agencies": "Ajanslar", "nav.bds": "BD listesi", "nav.bdManagers": "BD Yöneticileri", "nav.superAdmins": "Süper Yöneticiler", "nav.rooms": "Oda yönetimi", "nav.grants": "Kaynaklar ve özel ID'ler", "nav.banners": "Banner yönetimi", "nav.team": "Ekibim",
+ "auth.account": "Hesap", "auth.changePassword": "Parolayı değiştir", "auth.checkingSession": "Oturum kontrol ediliyor", "auth.confirmNewPassword": "Yeni parolayı doğrula", "auth.currentPassword": "Mevcut parola", "auth.fillAccountPassword": "Hesabınızı ve parolanızı girin", "auth.firstLoginChange": "İlk girişte parolanızı değiştirmeniz gerekir", "auth.hidePassword": "Parolayı gizle", "auth.invalidCredentials": "Hesap veya parola hatalı", "auth.login": "Giriş yap", "auth.loginFailed": "Giriş başarısız", "auth.logout": "Çıkış yap", "auth.newPassword": "Yeni parola", "auth.noPermission": "Bu hesabın bu özelliğe erişimi yok.", "auth.password": "Parola", "auth.passwordChangedFailed": "Parola değiştirilemedi", "auth.passwordMismatch": "Yeni parolalar eşleşmiyor", "auth.passwordMin": "En az 8 karakter", "auth.passwordMinError": "Yeni parola en az 8 karakter olmalıdır", "auth.passwordWhitespace": "Yeni parola boş veya yalnızca boşluk olamaz", "auth.recheck": "Tekrar kontrol et", "auth.returnOverview": "Ana sayfaya dön", "auth.savePassword": "Parolayı kaydet", "auth.sessionFailed": "Oturum doğrulanamadı", "auth.showPassword": "Parolayı göster",
+ "nav.expand": "Menüyü aç", "nav.more": "Daha fazla", "nav.overview": "Ana sayfa", "nav.users": "Kullanıcı yönetimi", "nav.bans": "Yasak yönetimi", "nav.hosts": "Yayıncılar", "nav.agencies": "Ajanslar", "nav.bds": "BD listesi", "nav.bdManagers": "BD Yöneticileri", "nav.superAdmins": "Süper Yöneticiler", "nav.rooms": "Oda yönetimi", "nav.grants": "Kaynaklar ve özel ID'ler", "nav.banners": "Banner yönetimi", "nav.team": "Ekibim",
"capability.userList": "Kullanıcı listesi", "capability.userUpdate": "Kullanıcı bilgilerini düzenleme", "capability.banList": "Yasaklı hesap listesi", "capability.ban": "Kullanıcı yasaklama", "capability.unban": "Kullanıcı yasağını kaldırma", "capability.hostList": "Yayıncı listesi", "capability.agencyList": "Ajans listesi", "capability.bdList": "BD listesi", "capability.bdManagerList": "BD Yöneticisi listesi", "capability.superAdminList": "Süper Yönetici listesi", "capability.roomList": "Oda yönetimi", "capability.roomUpdate": "Oda düzenleme", "capability.privilegeList": "Ayrıcalık öğesi listesi", "capability.privilegeGrant": "Ayrıcalık öğesi verme", "capability.bannerCreate": "Banner oluşturma", "capability.prettyIdGrant": "Özel ID verme", "capability.userTitleGrant": "Kullanıcı unvanı verme", "capability.roomBackgroundGrant": "Oda arka planı verme", "capability.userLevelGrant": "Servet/VIP seviyesi verme", "capability.teamView": "Ekibim",
- "overview.currentApp": "Geçerli uygulama", "overview.account": "Harici yönetim hesabı", "overview.permissions": "Etkin yetkiler", "overview.enabled": "Etkin", "overview.notEnabled": "Etkin değil",
+ "overview.currentApp": "Geçerli uygulama", "overview.account": "Hesap", "overview.permissions": "Etkin yetkiler", "overview.welcome": "Tekrar hoş geldiniz", "overview.features": "Özellikler", "overview.enabled": "Etkin", "overview.notEnabled": "Etkin değil",
"users.title": "Kullanıcı yönetimi", "users.search": "Kullanıcı ID / özel ID / ad", "users.prettyId": "Özel ID", "users.countryGender": "Ülke / cinsiyet", "users.level": "Seviye", "users.levelSummary": "Servet {wealth} / Cazibe {charm} / VIP {vip}", "users.edit": "Düzenle", "users.grantLevel": "Seviye", "users.ban": "Yasakla", "users.updated": "Kullanıcı bilgileri güncellendi", "users.banned": "Kullanıcı yasaklandı", "users.levelGranted": "Kullanıcı seviyesi verildi", "users.operationFailed": "Kullanıcı işlemi başarısız", "users.editTitle": "Kullanıcıyı düzenle", "users.avatar": "Avatar", "users.name": "Kullanıcı adı", "users.gender": "Cinsiyet", "users.countryCode": "Ülke/bölge kodu", "users.notSet": "Ayarlanmadı", "users.genderMale": "Erkek", "users.genderFemale": "Kadın", "users.genderUnknown": "Bilinmiyor", "users.banTitle": "{user} kullanıcısını yasakla", "users.banDays": "Yasak süresi (gün)", "users.banForeverHint": "Kalıcı yasak için 0 girin", "users.banReason": "Yasak nedeni", "users.levelTitle": "{user} kullanıcısına seviye ver", "users.levelType": "Seviye türü", "users.wealthLevel": "Servet seviyesi", "users.charmLevel": "Cazibe seviyesi", "users.vipLevel": "VIP seviyesi", "users.validDays": "Geçerlilik (gün)", "users.validDaysMin": "Geçerlilik en az 1 gün olmalıdır", "users.grantReason": "Verme nedeni",
"bans.title": "Yasaklı hesap listesi", "bans.reason": "Yasak nedeni", "bans.unban": "Yasağı kaldır", "bans.unbanned": "Kullanıcı yasağı kaldırıldı", "bans.unbanFailed": "Kullanıcı yasağı kaldırılamadı", "bans.confirmTitle": "Kullanıcı yasağını kaldır", "bans.confirmContent": "{user} kullanıcısının yasağı kaldırılsın mı?",
"organization.title": "Organizasyon", "organization.agencies": "Ajans listesi", "organization.bds": "BD listesi", "organization.bdManagers": "BD Yöneticisi listesi", "organization.hosts": "Yayıncı listesi", "organization.managers": "Yönetici listesi", "organization.superAdmins": "Süper Yönetici listesi", "organization.team": "Ekibim", "organization.agency": "Ajans", "organization.owner": "Sahip", "organization.parentBd": "Üst BD", "organization.member": "Üye", "organization.country": "Ülke / bölge", "organization.userReference": "Kullanıcı {id}", "organization.agencyReference": "Ajans {id}", "organization.teamSearch": "Ajans ID / sahip kullanıcı ID / üst BD ID", "organization.noPermission": "Bu hesabın bu organizasyon listesine erişimi yok.", "organization.bdLeaderCount": "BD Liderleri: {count}", "organization.bdCount": "BD: {count}", "organization.agencyCount": "Ajanslar: {count}", "organization.truncated": "Ekip API sınırını aşıyor. Mevcut sonuçlar eksik.",
diff --git a/external-admin/src/layout/ExternalAdminLayout.jsx b/external-admin/src/layout/ExternalAdminLayout.jsx
index aedfaf5..b9ea997 100644
--- a/external-admin/src/layout/ExternalAdminLayout.jsx
+++ b/external-admin/src/layout/ExternalAdminLayout.jsx
@@ -1,10 +1,11 @@
import ApartmentOutlined from "@mui/icons-material/ApartmentOutlined";
import BadgeOutlined from "@mui/icons-material/BadgeOutlined";
-import DashboardOutlined from "@mui/icons-material/DashboardOutlined";
+import GridViewOutlined from "@mui/icons-material/GridViewOutlined";
import GroupsOutlined from "@mui/icons-material/GroupsOutlined";
+import HomeOutlined from "@mui/icons-material/HomeOutlined";
import ImageOutlined from "@mui/icons-material/ImageOutlined";
+import KeyOutlined from "@mui/icons-material/KeyOutlined";
import LogoutOutlined from "@mui/icons-material/LogoutOutlined";
-import MenuOutlined from "@mui/icons-material/MenuOutlined";
import MeetingRoomOutlined from "@mui/icons-material/MeetingRoomOutlined";
import PeopleOutlineOutlined from "@mui/icons-material/PeopleOutlineOutlined";
import PersonOffOutlined from "@mui/icons-material/PersonOffOutlined";
@@ -16,14 +17,12 @@ import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Divider from "@mui/material/Divider";
import Drawer from "@mui/material/Drawer";
-import IconButton from "@mui/material/IconButton";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Stack from "@mui/material/Stack";
import Toolbar from "@mui/material/Toolbar";
-import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
@@ -38,7 +37,7 @@ import { useExternalSessionBrand } from "../shared/useExternalSessionBrand.js";
const drawerWidth = 248;
const navigation = [
- { icon: DashboardOutlined, labelKey: "nav.overview", path: "/overview" },
+ { icon: HomeOutlined, labelKey: "nav.overview", path: "/overview" },
{ capabilities: ["user:list", "user:update", "user:ban", "user-level:grant"], icon: PeopleOutlineOutlined, labelKey: "nav.users", path: "/users" },
{ capabilities: ["user-ban:list", "user:unban"], icon: PersonOffOutlined, labelKey: "nav.bans", path: "/bans" },
{ capabilities: ["host:list"], icon: BadgeOutlined, labelKey: "nav.hosts", path: "/organization/hosts" },
@@ -52,27 +51,43 @@ const navigation = [
{ capabilities: ["team:view"], icon: GroupsOutlined, labelKey: "nav.team", path: "/organization/team" }
];
+const isItemActive = (item, pathname) => pathname === item.path || pathname.startsWith(`${item.path}/`);
+
export function ExternalAdminLayout() {
const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down("md"));
- const [drawerOpen, setDrawerOpen] = useState(false);
+ const [sheetOpen, setSheetOpen] = useState(false);
const { logout, session } = useExternalAuth();
- const { direction, t } = useExternalI18n();
+ const { t } = useExternalI18n();
const location = useLocation();
const navigate = useNavigate();
const visibleNavigation = useMemo(
() => navigation.filter((item) => !item.capabilities || hasAnyExternalCapability(session, item.capabilities)),
[session]
);
- const current = [...visibleNavigation].reverse().find((item) => location.pathname.startsWith(item.path));
+ const current = [...visibleNavigation].reverse().find((item) => isItemActive(item, location.pathname));
+ // The H5 tab bar keeps three primary destinations reachable with a thumb; everything else lives in the "More" sheet.
+ const tabs = visibleNavigation.slice(0, 3);
+ const moreActive = !tabs.some((item) => isItemActive(item, location.pathname));
useExternalSessionBrand(session, t("app.title"));
const handleLogout = async () => {
+ setSheetOpen(false);
await logout();
navigate("/login", { replace: true });
};
- const drawer = (
+ const accountCard = (
+ <>
+
{String(session?.account || "A").slice(0, 1).toUpperCase()}
+
+ {session?.account}
+ {session?.appName || session?.appCode}{session?.displayName ? ` · ${session.displayName}` : ""}
+
+ >
+ );
+
+ const sidebar = (
setDrawerOpen(false)}
>
@@ -102,47 +116,88 @@ export function ExternalAdminLayout() {
);
})}
+
+ {accountCard}
+
+ } variant="outlined">{t("auth.logout")}
+
);
return (
+ {/* Physical margins/anchors are written for LTR; the RTL Emotion cache flips them for Arabic. */}
- {mobile ? setDrawerOpen(true)}> : null}
- {current ? t(current.labelKey) : t("app.title")}
+ {mobile && !current ? t("app.shortTitle") : t(current?.labelKey || "app.shortTitle")}
-
+
- {String(session?.account || "A").slice(0, 1).toUpperCase()}
- {!mobile ? (
-
- {session?.account}
- {session?.appCode}{session?.displayName ? ` · ${session.displayName}` : ""}
-
- ) : null}
-
- }>{mobile ? "" : t("auth.logout")}
-
+ {!mobile ? accountCard : null}
- setDrawerOpen(false)} ModalProps={{ keepMounted: true }} sx={{ display: { md: "none" }, "& .MuiDrawer-paper": { width: drawerWidth } }}>{drawer}
- {drawer}
-
-
+ {sidebar}
+
+
+ {/* Shown below the md breakpoint via index.css; sx display rules would lose to the stylesheet cascade. */}
+
+ {tabs.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+ {t(item.labelKey)}
+
+ );
+ })}
+
+
+ setSheetOpen(false)}
+ >
+
+ {accountCard}
+
+ {visibleNavigation.map((item) => {
+ const Icon = item.icon;
+ return (
+ setSheetOpen(false)}
+ >
+
+ {t(item.labelKey)}
+
+ );
+ })}
+
+
+
+ } onClick={() => { setSheetOpen(false); navigate("/change-password"); }}>{t("auth.changePassword")}
+ } variant="outlined" onClick={handleLogout}>{t("auth.logout")}
+
+
);
}
diff --git a/external-admin/src/layout/ExternalAdminLayout.test.jsx b/external-admin/src/layout/ExternalAdminLayout.test.jsx
index 7419cad..c767ab0 100644
--- a/external-admin/src/layout/ExternalAdminLayout.test.jsx
+++ b/external-admin/src/layout/ExternalAdminLayout.test.jsx
@@ -1,7 +1,9 @@
-import { render, screen } from "@testing-library/react";
+import { CacheProvider } from "@emotion/react";
+import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { beforeEach, describe, expect, test, vi } from "vitest";
+import { externalEmotionCache } from "../i18n/emotionCache.js";
import { ExternalI18nProvider } from "../i18n/ExternalI18nProvider.jsx";
import { EXTERNAL_LOCALE_STORAGE_KEY } from "../i18n/messages.js";
import { ExternalAdminLayout } from "./ExternalAdminLayout.jsx";
@@ -34,17 +36,22 @@ describe("ExternalAdminLayout RTL", () => {
};
});
- test("opens the mobile navigation from the right in Arabic", async () => {
+ test("shows the H5 tab bar and opens the more sheet in Arabic", async () => {
const user = userEvent.setup();
viewport.mobile = true;
renderLayout();
- await user.click(screen.getByRole("button", { name: "فتح القائمة" }));
+ const tabbar = document.querySelector(".external-tabbar");
+ expect(tabbar).toBeInTheDocument();
+ expect(within(tabbar).getByRole("link", { name: "الرئيسية" })).toBeInTheDocument();
- const openTemporaryDrawer = Array.from(document.querySelectorAll(".MuiDrawer-paper")).find((node) => node.closest(".MuiModal-root"));
- expect(openTemporaryDrawer).toBeInTheDocument();
- expect(openTemporaryDrawer.parentElement).not.toHaveStyle({ visibility: "hidden" });
- expect(screen.getAllByRole("link", { name: "ملخص الصلاحيات" }).length).toBeGreaterThan(0);
+ await user.click(within(tabbar).getByRole("button", { name: "المزيد" }));
+
+ const sheet = document.querySelector(".MuiDrawer-paper.external-sheet");
+ expect(sheet).toBeInTheDocument();
+ expect(sheet.closest(".MuiModal-root")).not.toBeNull();
+ expect(within(sheet).getByRole("link", { name: "الرئيسية" })).toBeInTheDocument();
+ expect(within(sheet).getByRole("button", { name: "تسجيل الخروج" })).toBeInTheDocument();
});
test("places the desktop navigation drawer on the right for Arabic", () => {
@@ -53,7 +60,8 @@ describe("ExternalAdminLayout RTL", () => {
expect(document.documentElement).toHaveAttribute("dir", "rtl");
expect(document.documentElement).toHaveAttribute("lang", "ar");
const permanentDrawer = document.querySelectorAll(".MuiDrawer-paper")[0];
- const generatedClass = Array.from(permanentDrawer.classList).find((className) => className.startsWith("css-"));
+ // Emotion prefixes generated classes with the cache key, so the RTL cache yields external-rtl-*.
+ const generatedClass = Array.from(permanentDrawer.classList).find((className) => className.startsWith("external-rtl-") || className.startsWith("css-"));
const drawerRule = Array.from(document.styleSheets)
.flatMap((sheet) => Array.from(sheet.cssRules || []))
.map((rule) => rule.cssText)
@@ -65,7 +73,7 @@ describe("ExternalAdminLayout RTL", () => {
expect(screen.queryByText("إدارة HYApp الخارجية")).not.toBeInTheDocument();
expect(document.title).toBe("Fami");
expect(document.head.querySelector('link[data-external-app-favicon="true"]')).toHaveAttribute("href", "https://media.example.com/apps/fami.png");
- expect(screen.getAllByText("ملخص الصلاحيات").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("الرئيسية").length).toBeGreaterThan(0);
expect(screen.getByRole("combobox", { name: "اللغة" })).toHaveTextContent("العربية");
view.unmount();
@@ -95,15 +103,19 @@ describe("ExternalAdminLayout RTL", () => {
});
function renderLayout() {
+ // The RTL Emotion cache is part of the production setup: physical styles are written
+ // for LTR and flipped by the cache, so the test must run the same pipeline.
return render(
-
-
-
- }>
- overview content } path="/overview" />
-
-
-
-
+