diff --git a/src/app/[locale]/dashboard/page.tsx b/src/app/[locale]/dashboard/page.tsx index 561beef..51cf8c2 100644 --- a/src/app/[locale]/dashboard/page.tsx +++ b/src/app/[locale]/dashboard/page.tsx @@ -344,7 +344,7 @@ export default async function DashboardPage() { {canCreate && ( + {t("createInstance")} diff --git a/src/app/[locale]/login/page.tsx b/src/app/[locale]/login/page.tsx index 2864100..a111f19 100644 --- a/src/app/[locale]/login/page.tsx +++ b/src/app/[locale]/login/page.tsx @@ -1,11 +1,12 @@ "use client"; import { signIn } from "next-auth/react"; -import { useTranslations } from "next-intl"; -import Link from "next/link"; +import { useTranslations, useLocale } from "next-intl"; +import { Link, getPathname } from "@/i18n/navigation"; export default function LoginPage() { const t = useTranslations("login"); + const locale = useLocale(); return (
@@ -39,7 +40,14 @@ export default function LoginPage() {

@@ -270,7 +270,7 @@ export default function RegisterPage() { @@ -278,12 +278,12 @@ export default function RegisterPage() {

{t("hasAccount")}{" "} - {tCommon("login")} - +

)} diff --git a/src/app/[locale]/support/page.tsx b/src/app/[locale]/support/page.tsx index fc7d990..f603f54 100644 --- a/src/app/[locale]/support/page.tsx +++ b/src/app/[locale]/support/page.tsx @@ -48,7 +48,7 @@ export default async function SupportListPage() { {!user.isPlatform && ( {t("newTicket")} diff --git a/src/components/admin/admin-panel.tsx b/src/components/admin/admin-panel.tsx index 823b879..a577580 100644 --- a/src/components/admin/admin-panel.tsx +++ b/src/components/admin/admin-panel.tsx @@ -246,7 +246,7 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { > {t("requests")} {pendingCount > 0 && tab !== "requests" && ( - + {pendingCount} )} @@ -308,7 +308,7 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { onClick={() => setFilter(f)} className={`px-3 py-1 text-xs rounded-full transition-colors ${ filter === f - ? "bg-accent text-white" + ? "bg-accent text-surface-0" : "bg-surface-2 text-text-muted hover:text-text-secondary border border-border" }`} > diff --git a/src/components/admin/billing/custom-invoice-editor.tsx b/src/components/admin/billing/custom-invoice-editor.tsx index d6f47c5..f7888b4 100644 --- a/src/components/admin/billing/custom-invoice-editor.tsx +++ b/src/components/admin/billing/custom-invoice-editor.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useMemo, useCallback } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter } from "@/i18n/navigation"; import { useTranslations } from "next-intl"; import { Card, CardHeader } from "@/components/ui/card"; import type { @@ -525,7 +525,7 @@ export function CustomInvoiceEditor({ draft, orgBilling }: Props) { diff --git a/src/components/admin/billing/invoice-detail-view.tsx b/src/components/admin/billing/invoice-detail-view.tsx index 674dde3..25e4053 100644 --- a/src/components/admin/billing/invoice-detail-view.tsx +++ b/src/components/admin/billing/invoice-detail-view.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, Fragment } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter } from "@/i18n/navigation"; import { useTranslations } from "next-intl"; import { Card, CardHeader } from "@/components/ui/card"; import type { CreditNote, InvoiceDetail, InvoiceStatus } from "@/types"; @@ -247,7 +247,7 @@ export function InvoiceDetailView({ detail, creditNotes = [] }: Props) { @@ -264,7 +264,7 @@ export function InvoiceDetailView({ detail, creditNotes = [] }: Props) { diff --git a/src/components/admin/billing/invoices-table.tsx b/src/components/admin/billing/invoices-table.tsx index 9b3a40b..01cd650 100644 --- a/src/components/admin/billing/invoices-table.tsx +++ b/src/components/admin/billing/invoices-table.tsx @@ -112,7 +112,7 @@ export function InvoicesTable({ initialInvoices }: Props) { + {t("newInvoiceBtn")} diff --git a/src/components/admin/billing/new-invoice-form.tsx b/src/components/admin/billing/new-invoice-form.tsx index f2943e1..296a1b1 100644 --- a/src/components/admin/billing/new-invoice-form.tsx +++ b/src/components/admin/billing/new-invoice-form.tsx @@ -155,7 +155,7 @@ export function NewInvoiceForm({ orgs }: Props) { diff --git a/src/components/admin/billing/pricing-editor.tsx b/src/components/admin/billing/pricing-editor.tsx index 6f6f67e..7e22d8a 100644 --- a/src/components/admin/billing/pricing-editor.tsx +++ b/src/components/admin/billing/pricing-editor.tsx @@ -236,7 +236,7 @@ export function PricingEditor({ @@ -401,7 +401,7 @@ export function PricingEditor({ @@ -473,7 +473,7 @@ function InlinePriceEditor({ } }} disabled={busy} - className="text-xs px-2 py-1 bg-accent text-white rounded" + className="text-xs px-2 py-1 bg-accent text-surface-0 rounded" > {busy ? "…" : "✓"} diff --git a/src/components/admin/cron/cron-controls.tsx b/src/components/admin/cron/cron-controls.tsx index d6b5653..813f1f5 100644 --- a/src/components/admin/cron/cron-controls.tsx +++ b/src/components/admin/cron/cron-controls.tsx @@ -147,7 +147,7 @@ export function CronControls({ initialRecent, initialLastSuccess }: Props) { @@ -165,7 +165,7 @@ export function CronControls({ initialRecent, initialLastSuccess }: Props) { diff --git a/src/components/admin/openclaw-admin-panel.tsx b/src/components/admin/openclaw-admin-panel.tsx index 4cbffe0..2b13f9a 100644 --- a/src/components/admin/openclaw-admin-panel.tsx +++ b/src/components/admin/openclaw-admin-panel.tsx @@ -107,7 +107,7 @@ export function OpenClawAdminPanel({ initialDefaults, tenants }: Props) { @@ -265,7 +265,7 @@ function TenantOverrideRow({ type="button" onClick={() => submit(false)} disabled={saving || !tag.trim()} - className="text-xs px-3 py-1.5 rounded-lg bg-accent text-white hover:bg-accent/90 transition-colors disabled:opacity-50" + className="text-xs px-3 py-1.5 rounded-lg bg-accent text-surface-0 hover:bg-accent/90 transition-colors disabled:opacity-50" > {saving ? tCommon("loading") : t("saveOverride")} diff --git a/src/components/admin/skills/pending-skill-requests.tsx b/src/components/admin/skills/pending-skill-requests.tsx index c7f25a3..fd062a2 100644 --- a/src/components/admin/skills/pending-skill-requests.tsx +++ b/src/components/admin/skills/pending-skill-requests.tsx @@ -146,7 +146,7 @@ export function PendingSkillRequests({ initialRows }: Props) { diff --git a/src/components/billing/pay-invoice-button.tsx b/src/components/billing/pay-invoice-button.tsx index 93b1765..f6c1be3 100644 --- a/src/components/billing/pay-invoice-button.tsx +++ b/src/components/billing/pay-invoice-button.tsx @@ -50,7 +50,7 @@ export function PayInvoiceButton({ invoiceNumber }: Props) { diff --git a/src/components/billing/running-total-widget.tsx b/src/components/billing/running-total-widget.tsx index aab8ff3..1dddcc8 100644 --- a/src/components/billing/running-total-widget.tsx +++ b/src/components/billing/running-total-widget.tsx @@ -86,7 +86,7 @@ export function RunningTotalWidget({ isOwner }: Props) { {noConfig && isOwner && ( {t("configureBillingCta")} diff --git a/src/components/channel-users/channel-users.tsx b/src/components/channel-users/channel-users.tsx index e9b9781..24d874c 100644 --- a/src/components/channel-users/channel-users.tsx +++ b/src/components/channel-users/channel-users.tsx @@ -328,7 +328,7 @@ export function ChannelUsers({ diff --git a/src/components/dashboard/budget-editable-card.tsx b/src/components/dashboard/budget-editable-card.tsx index 7089eae..b85f16a 100644 --- a/src/components/dashboard/budget-editable-card.tsx +++ b/src/components/dashboard/budget-editable-card.tsx @@ -263,7 +263,7 @@ export function BudgetEditableCard({ diff --git a/src/components/layout/nav-shell.tsx b/src/components/layout/nav-shell.tsx index 7822464..3f465a9 100644 --- a/src/components/layout/nav-shell.tsx +++ b/src/components/layout/nav-shell.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState, useEffect } from "react"; import { useTranslations } from "next-intl"; import { signOut, useSession } from "next-auth/react"; import { usePathname } from "@/i18n/navigation"; @@ -13,6 +14,15 @@ function NavBar() { const pathname = usePathname(); const user = (session as any)?.platformUser; + const [mobileOpen, setMobileOpen] = useState(false); + + // Close the mobile menu on any navigation. Without this the panel + // would stay open across route changes (the component doesn't + // unmount — it lives in the layout). + useEffect(() => { + setMobileOpen(false); + }, [pathname]); + // Hide the nav entirely on auth-only routes. These pages have no // session yet — showing "Dashboard" / "Sign Out" is misleading at // best (the buttons would 401 or redirect-loop). Keep this list @@ -21,6 +31,47 @@ function NavBar() { const isAuthRoute = pathname === "/login" || pathname === "/register"; if (isAuthRoute) return null; + // ------------------------------------------------------------------ + // Visibility gates — computed once, shared by the desktop nav and the + // mobile panel so the two can never diverge. + // + // - team: owner+platform only AND not a personal account (Bug 8 — + // personal accounts have no team). Matches `canMutate` / + // `user.isPersonal === false` server-side. + // - settings: anyone who can mutate org-level state (owners + platform). + // `user`-role customers don't see it (canMutate is false). + // - billing / support: any signed-in user (org-scoped server-side). + // - admin: platform only. + // ------------------------------------------------------------------ + const isOwner = + user && Array.isArray(user.roles) && user.roles.includes("owner"); + const showTeam = !!user && !user.isPersonal && (user.isPlatform || isOwner); + const showSettings = !!user && (user.isPlatform || isOwner); + const showBilling = !!user; + const showSupport = !!user; + const showAdmin = !!user?.isPlatform; + + // Active-state helper. Dashboard/Admin previously used exact `===`, + // so sub-routes (/dashboard/new, /admin/billing, …) showed no active + // item. startsWith keeps the parent lit on its children too. + const isActive = (href: string) => + pathname === href || pathname.startsWith(`${href}/`); + + const links = [ + { href: "/dashboard", label: t("dashboard"), show: !!user }, + { href: "/team", label: t("team"), show: showTeam }, + { href: "/settings", label: t("settings"), show: showSettings }, + { href: "/billing", label: t("billing"), show: showBilling }, + { href: "/support", label: t("support"), show: showSupport }, + { href: "/admin", label: t("admin"), show: showAdmin }, + ].filter((l) => l.show); + + const displayName = user + ? user.isPersonal + ? user.name || (user.email ? user.email.split("@")[0] : user.orgName) + : user.orgName + : ""; + return (
@@ -40,98 +91,96 @@ function NavBar() { - {/* Nav links */} + {/* Desktop nav links */}
{/* Right side */}
{user && ( - // For personal accounts the orgName is opaque - // ("personal-3f2a8b1c") or a synthetic legacy - // "Name (Personal)" — neither is what we want in the nav. - // Show the user's display name instead. The detection logic - // and fallback chain live in `lib/personal-org.ts`; keeping - // a thin inline branch here avoids importing a server-only - // helper into a client component. - {user.isPersonal - ? user.name || (user.email ? user.email.split("@")[0] : user.orgName) - : user.orgName} + {displayName} )} + + {/* Mobile menu toggle — only shown below the `sm` breakpoint, + where the desktop nav and logout button are hidden. */} + {user && ( + + )}
+ + {/* Mobile panel */} + {user && mobileOpen && ( + + )} ); } diff --git a/src/components/onboarding/onboarding-flow.tsx b/src/components/onboarding/onboarding-flow.tsx index d3e1c0a..f455cbd 100644 --- a/src/components/onboarding/onboarding-flow.tsx +++ b/src/components/onboarding/onboarding-flow.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRouter } from "next/navigation"; +import { useRouter } from "@/i18n/navigation"; import { OnboardingWizard } from "./wizard"; import type { OrgBilling } from "@/types"; diff --git a/src/components/onboarding/provisioning-status.tsx b/src/components/onboarding/provisioning-status.tsx index 6af854c..6d06273 100644 --- a/src/components/onboarding/provisioning-status.tsx +++ b/src/components/onboarding/provisioning-status.tsx @@ -489,7 +489,7 @@ export function ProvisioningStatus({ requestId, canAct }: Props) {

diff --git a/src/components/onboarding/wizard.tsx b/src/components/onboarding/wizard.tsx index 0ce9cfe..3703733 100644 --- a/src/components/onboarding/wizard.tsx +++ b/src/components/onboarding/wizard.tsx @@ -606,7 +606,7 @@ export function OnboardingWizard({
@@ -994,7 +994,7 @@ export function OnboardingWizard({ @@ -1182,7 +1182,7 @@ export function OnboardingWizard({ @@ -1397,7 +1397,7 @@ export function OnboardingWizard({ diff --git a/src/components/settings/billing-form.tsx b/src/components/settings/billing-form.tsx index 75ca199..004a9ff 100644 --- a/src/components/settings/billing-form.tsx +++ b/src/components/settings/billing-form.tsx @@ -227,7 +227,7 @@ export function BillingSettingsForm({ initial, isPersonal }: Props) { diff --git a/src/components/settings/billing-settings-form.tsx b/src/components/settings/billing-settings-form.tsx index 1c6b709..767cfb0 100644 --- a/src/components/settings/billing-settings-form.tsx +++ b/src/components/settings/billing-settings-form.tsx @@ -268,7 +268,7 @@ export function BillingSettingsForm({ diff --git a/src/components/settings/profile-form.tsx b/src/components/settings/profile-form.tsx index 9aca729..21fecdb 100644 --- a/src/components/settings/profile-form.tsx +++ b/src/components/settings/profile-form.tsx @@ -153,7 +153,7 @@ export function ProfileSettingsForm({ initial, isPersonal, orgName }: Props) { diff --git a/src/components/settings/saved-card-section.tsx b/src/components/settings/saved-card-section.tsx index 2094a3b..d5252f0 100644 --- a/src/components/settings/saved-card-section.tsx +++ b/src/components/settings/saved-card-section.tsx @@ -1,7 +1,8 @@ "use client"; import { useState, useEffect } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; +import { useRouter } from "@/i18n/navigation"; import { useTranslations } from "next-intl"; import { Card, CardHeader } from "@/components/ui/card"; import type { OrgBillingConfig } from "@/types"; @@ -136,7 +137,7 @@ export function SavedCardSection({ diff --git a/src/components/support/ticket-create-form.tsx b/src/components/support/ticket-create-form.tsx index 9806927..ec06b86 100644 --- a/src/components/support/ticket-create-form.tsx +++ b/src/components/support/ticket-create-form.tsx @@ -119,7 +119,7 @@ export function TicketCreateForm() { diff --git a/src/components/support/ticket-thread.tsx b/src/components/support/ticket-thread.tsx index 3f86369..523a17c 100644 --- a/src/components/support/ticket-thread.tsx +++ b/src/components/support/ticket-thread.tsx @@ -186,7 +186,7 @@ export function TicketThread({ diff --git a/src/components/team/invite-form.tsx b/src/components/team/invite-form.tsx index 905d13d..8798c25 100644 --- a/src/components/team/invite-form.tsx +++ b/src/components/team/invite-form.tsx @@ -141,7 +141,7 @@ export function InviteForm() { diff --git a/src/components/team/team-list.tsx b/src/components/team/team-list.tsx index 9097208..8c9a274 100644 --- a/src/components/team/team-list.tsx +++ b/src/components/team/team-list.tsx @@ -179,7 +179,7 @@ export function TeamList({ type="button" onClick={() => saveEdit(m)} disabled={submitting || !m.authorizationId} - className="text-xs px-2.5 py-1 rounded-md bg-accent text-white hover:bg-accent-dim transition-colors disabled:opacity-50" + className="text-xs px-2.5 py-1 rounded-md bg-accent text-surface-0 hover:bg-accent-dim transition-colors disabled:opacity-50" > {t("save")} diff --git a/src/components/tenants/assigned-users-panel.tsx b/src/components/tenants/assigned-users-panel.tsx index f642efa..5ce52ae 100644 --- a/src/components/tenants/assigned-users-panel.tsx +++ b/src/components/tenants/assigned-users-panel.tsx @@ -218,7 +218,7 @@ export function AssignedUsersPanel({ tenantName, canEdit }: Props) { diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..2a18c2b --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import { forwardRef } from "react"; + +/** + * Shared button primitive. + * + * Why this exists + * --------------- + * The accent fill (#00d4aa) is bright; white text on it measures ~1.9:1, + * which fails WCAG even for large/UI text. Dark text (surface-0) on the + * same accent is ~10:1. The codebase had ~40 hand-rolled accent buttons, + * most using `text-white`. This component centralises the correct token + * (`text-surface-0` on accent) so the contrast can't drift again — reach + * for `