Skip to content

Commit d6dc8c1

Browse files
committed
objectui
1 parent 7c6cdd4 commit d6dc8c1

25 files changed

Lines changed: 1620 additions & 843 deletions

apps/account/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"preview": "vite preview"
1818
},
1919
"dependencies": {
20-
"@object-ui/i18n": "^4.5.0",
20+
"@object-ui/i18n": "^4.6.0",
2121
"@objectstack/client": "workspace:*",
2222
"@objectstack/client-react": "workspace:*",
2323
"@objectstack/spec": "workspace:*",

apps/account/src/components/account-sidebar.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import { Link, useLocation } from '@tanstack/react-router';
2828
import {
2929
Building2,
30+
Home,
3031
Inbox,
3132
KeyRound,
3233
Link2,
@@ -57,6 +58,7 @@ import { useOrganizations } from '@/hooks/useSession';
5758

5859
interface NavItem {
5960
to:
61+
| '/account'
6062
| '/account/profile'
6163
| '/account/security'
6264
| '/account/sessions'
@@ -67,9 +69,11 @@ interface NavItem {
6769
| '/organizations';
6870
label: string;
6971
icon: React.ComponentType<{ className?: string }>;
72+
exact?: boolean;
7073
}
7174

7275
const ACCOUNT_ITEMS: NavItem[] = [
76+
{ to: '/account', label: 'home', icon: Home, exact: true },
7377
{ to: '/account/profile', label: 'profile', icon: User },
7478
{ to: '/account/security', label: 'security', icon: Shield },
7579
{ to: '/account/sessions', label: 'sessions', icon: Monitor },
@@ -78,14 +82,30 @@ const ACCOUNT_ITEMS: NavItem[] = [
7882
{ to: '/account/linked-accounts', label: 'linkedAccounts', icon: Link2 },
7983
];
8084

85+
/**
86+
* Shared classes added to every SidebarMenuButton in the Account portal:
87+
* - left accent bar that fades in on active
88+
* - icon scale-up on hover (and gradient tint on active)
89+
* - smoother transition
90+
*/
91+
const NAV_BUTTON_CLASSES = [
92+
'group/nav relative transition-all duration-150',
93+
'before:absolute before:left-0 before:top-1/2 before:h-5 before:w-[3px] before:-translate-y-1/2 before:rounded-r-full before:bg-brand-gradient before:opacity-0 before:transition-opacity',
94+
'data-[active=true]:before:opacity-100',
95+
'data-[active=true]:bg-sidebar-accent/80 data-[active=true]:text-sidebar-accent-foreground',
96+
'[&>svg]:transition-transform [&>svg]:duration-150 hover:[&>svg]:scale-110',
97+
'data-[active=true]:[&>svg]:text-primary',
98+
].join(' ');
99+
81100
export function AccountSidebar() {
82101
const { t } = useObjectTranslation();
83102
const { pathname } = useLocation();
84103
const { organizations } = useOrganizations();
85104

86-
// /account on its own redirects to /account/profile, so treat the bare
87-
// path as the profile page for active-state purposes.
88-
const normalised = pathname === '/account' ? '/account/profile' : pathname;
105+
// /account is the home dashboard now; sub-routes match by prefix and the
106+
// `exact` flag is honoured for the Home item so it doesn't light up on
107+
// every sub-route.
108+
const normalised = pathname;
89109

90110
// Detect /organizations/<orgId>/* — used to surface the org-scoped sub-items.
91111
const orgMatch = pathname.match(/^\/organizations\/([^/]+)(?:\/.*)?$/);
@@ -101,10 +121,12 @@ export function AccountSidebar() {
101121
<SidebarMenu>
102122
{ACCOUNT_ITEMS.map((item) => {
103123
const Icon = item.icon;
104-
const isActive = normalised === item.to || normalised.startsWith(`${item.to}/`);
124+
const isActive = item.exact
125+
? normalised === item.to || normalised === `${item.to}/`
126+
: normalised === item.to || normalised.startsWith(`${item.to}/`);
105127
return (
106128
<SidebarMenuItem key={item.to}>
107-
<SidebarMenuButton asChild isActive={isActive} tooltip={t(`sidebar.items.${item.label}`)}>
129+
<SidebarMenuButton asChild isActive={isActive} tooltip={t(`sidebar.items.${item.label}`)} className={NAV_BUTTON_CLASSES}>
108130
<Link to={item.to}>
109131
<Icon className="size-4" />
110132
<span>{t(`sidebar.items.${item.label}`)}</span>
@@ -128,6 +150,7 @@ export function AccountSidebar() {
128150
asChild
129151
isActive={pathname === '/organizations'}
130152
tooltip={t('sidebar.items.overview')}
153+
className={NAV_BUTTON_CLASSES}
131154
>
132155
<Link to="/organizations">
133156
<Building2 className="size-4" />
@@ -142,6 +165,7 @@ export function AccountSidebar() {
142165
asChild
143166
isActive={pathname === `/organizations/${activeOrgId}/general`}
144167
tooltip={t('sidebar.items.general')}
168+
className={NAV_BUTTON_CLASSES}
145169
>
146170
<Link to="/organizations/$orgId/general" params={{ orgId: activeOrgId }}>
147171
<Settings className="size-4" />
@@ -154,6 +178,7 @@ export function AccountSidebar() {
154178
asChild
155179
isActive={pathname === `/organizations/${activeOrgId}/members`}
156180
tooltip={t('sidebar.items.members')}
181+
className={NAV_BUTTON_CLASSES}
157182
>
158183
<Link to="/organizations/$orgId/members" params={{ orgId: activeOrgId }}>
159184
<Users className="size-4" />
@@ -166,6 +191,7 @@ export function AccountSidebar() {
166191
asChild
167192
isActive={pathname === `/organizations/${activeOrgId}/teams`}
168193
tooltip={t('sidebar.items.teams')}
194+
className={NAV_BUTTON_CLASSES}
169195
>
170196
<Link to="/organizations/$orgId/teams" params={{ orgId: activeOrgId }}>
171197
<Users2 className="size-4" />
@@ -190,6 +216,7 @@ export function AccountSidebar() {
190216
asChild
191217
isActive={pathname.startsWith('/account/oauth-applications')}
192218
tooltip={t('sidebar.items.oauthApps')}
219+
className={NAV_BUTTON_CLASSES}
193220
>
194221
<Link to="/account/oauth-applications">
195222
<KeyRound className="size-4" />
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* AuthShell — shared split-screen layout for every unauthenticated page
5+
* (login, register, forgot-password, reset-password, verify-email, …).
6+
*
7+
* ┌─────────────────────────────┬─────────────────────────────┐
8+
* │ │ │
9+
* │ Form column │ Brand panel │
10+
* │ (white, centred card) │ (gradient mesh, glow) │
11+
* │ │ │
12+
* └─────────────────────────────┴─────────────────────────────┘
13+
*
14+
* On < lg the brand panel is hidden and a compact brand tile sits above the
15+
* form so the page stays cohesive on mobile.
16+
*/
17+
18+
import * as React from 'react';
19+
import { useObjectTranslation } from '@object-ui/i18n';
20+
import { GalleryVerticalEnd } from 'lucide-react';
21+
import { cn } from '@/lib/utils';
22+
23+
export interface AuthShellProps {
24+
/** Form / interactive content rendered in the left column. */
25+
children: React.ReactNode;
26+
/** Override the right-panel headline. */
27+
headline?: React.ReactNode;
28+
/** Override the right-panel sub-line. */
29+
subline?: React.ReactNode;
30+
/** Optional max-width on the form container (default `sm`). */
31+
formWidth?: 'sm' | 'md';
32+
}
33+
34+
export function AuthShell({
35+
children,
36+
headline,
37+
subline,
38+
formWidth = 'sm',
39+
}: AuthShellProps) {
40+
const { t } = useObjectTranslation();
41+
const widthCls = formWidth === 'md' ? 'max-w-md' : 'max-w-sm';
42+
return (
43+
<div className="relative grid min-h-svh w-full lg:grid-cols-2">
44+
{/* Left: form column */}
45+
<div className="flex flex-col items-center justify-center gap-6 bg-background p-6 md:p-10">
46+
<div className={cn('flex w-full flex-col gap-6', widthCls)}>
47+
<a
48+
href="#"
49+
className="flex items-center gap-2 self-center font-semibold tracking-tight"
50+
>
51+
<div className="flex size-7 items-center justify-center rounded-md bg-brand-gradient text-primary-foreground shadow-sm shadow-primary/30">
52+
<GalleryVerticalEnd className="size-4" />
53+
</div>
54+
<span>ObjectStack</span>
55+
</a>
56+
{children}
57+
</div>
58+
</div>
59+
60+
{/* Right: brand panel (≥ lg) */}
61+
<aside
62+
aria-hidden
63+
className="relative hidden overflow-hidden bg-brand-mesh text-white lg:flex lg:flex-col lg:justify-between lg:p-12"
64+
>
65+
<div
66+
className="pointer-events-none absolute inset-0 opacity-[0.12]"
67+
style={{
68+
backgroundImage:
69+
'linear-gradient(rgba(255,255,255,0.6) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.6) 1px, transparent 1px)',
70+
backgroundSize: '48px 48px',
71+
maskImage:
72+
'radial-gradient(ellipse at center, black 40%, transparent 75%)',
73+
}}
74+
/>
75+
<div className="pointer-events-none absolute -left-24 top-1/3 size-[28rem] rounded-full bg-white/15 blur-3xl" />
76+
<div className="pointer-events-none absolute -right-20 -bottom-20 size-[24rem] rounded-full bg-white/10 blur-3xl" />
77+
78+
<div className="relative flex items-center gap-2 text-sm font-semibold tracking-tight">
79+
<div className="flex size-7 items-center justify-center rounded-md bg-white/15 ring-1 ring-white/30 backdrop-blur">
80+
<GalleryVerticalEnd className="size-4" />
81+
</div>
82+
ObjectStack
83+
</div>
84+
85+
<div className="relative max-w-md space-y-5">
86+
<h2 className="text-balance text-3xl font-semibold leading-tight tracking-tight md:text-4xl">
87+
{headline ??
88+
t('auth.login.brandHeadline', {
89+
defaultValue: 'The post-SaaS operating system for your team.',
90+
})}
91+
</h2>
92+
<p className="text-balance text-base/relaxed text-white/80">
93+
{subline ??
94+
t('auth.login.brandSubline', {
95+
defaultValue:
96+
'One identity, every workspace. Sign in to manage your account, organizations and connected apps.',
97+
})}
98+
</p>
99+
</div>
100+
101+
<div className="relative text-xs text-white/60">
102+
© {new Date().getFullYear()} ObjectStack
103+
</div>
104+
</aside>
105+
</div>
106+
);
107+
}

apps/account/src/components/page-header.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
import * as React from 'react';
1818
import { cn } from '@/lib/utils';
1919

20-
export interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
20+
export interface PageHeaderProps
21+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
2122
title: React.ReactNode;
2223
description?: React.ReactNode;
2324
icon?: React.ComponentType<{ className?: string }>;

apps/account/src/components/top-bar.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import { Link, useLocation, useParams } from '@tanstack/react-router';
1212
import { useMemo } from 'react';
13-
import { UserCircle2 } from 'lucide-react';
13+
import { ChevronRight, UserCircle2 } from 'lucide-react';
1414
import { useObjectTranslation } from '@object-ui/i18n';
1515
import {
1616
Breadcrumb,
@@ -30,7 +30,7 @@ function AccountBrand({ label }: { label: string }) {
3030
return (
3131
<Link
3232
to="/account"
33-
className="flex h-7 w-7 items-center justify-center rounded-md bg-primary text-primary-foreground hover:opacity-90"
33+
className="flex h-8 w-8 items-center justify-center rounded-md bg-brand-gradient text-primary-foreground shadow-sm shadow-primary/30 ring-1 ring-white/15 transition-transform hover:scale-105"
3434
aria-label={label}
3535
>
3636
<UserCircle2 className="h-4 w-4" />
@@ -39,7 +39,12 @@ function AccountBrand({ label }: { label: string }) {
3939
}
4040

4141
function SlashDivider() {
42-
return <span aria-hidden className="select-none text-muted-foreground/50">/</span>;
42+
return (
43+
<ChevronRight
44+
aria-hidden
45+
className="size-3.5 shrink-0 text-muted-foreground/50"
46+
/>
47+
);
4348
}
4449

4550
export function TopBar() {
@@ -80,19 +85,20 @@ export function TopBar() {
8085
}, [location.pathname, params.orgId, t]);
8186

8287
return (
83-
<header className="flex h-12 shrink-0 items-center justify-between gap-2 border-b px-2 sm:px-4">
88+
<header className="sticky top-0 z-30 flex h-14 shrink-0 items-center justify-between gap-2 border-b bg-background/80 px-2 backdrop-blur supports-[backdrop-filter]:bg-background/70 sm:px-4">
8489
{/* Left: brand + mobile trigger + breadcrumb */}
85-
<div className="flex min-w-0 items-center gap-1 sm:gap-2">
90+
<div className="flex min-w-0 items-center gap-1.5 sm:gap-2.5">
8691
<div className="sm:hidden">
8792
<SidebarTrigger className="h-9 w-9" />
8893
</div>
8994
<AccountBrand label={t('topBar.accountHome')} />
90-
<SlashDivider />
91-
<span className="hidden text-sm font-medium sm:inline">{t('topBar.brand')}</span>
92-
<div className="hidden items-center gap-1 sm:flex">
93-
<Separator orientation="vertical" className="mx-2 h-4" />
95+
<span className="hidden text-sm font-semibold tracking-tight sm:inline">
96+
{t('topBar.brand')}
97+
</span>
98+
<div className="hidden items-center gap-1.5 sm:flex">
99+
<Separator orientation="vertical" className="mx-1 h-4" />
94100
<OrganizationSwitcher />
95-
<Separator orientation="vertical" className="mx-2 h-4" />
101+
<SlashDivider />
96102
<Breadcrumb>
97103
<BreadcrumbList>
98104
{breadcrumbs.map((item, index) => (

apps/account/src/i18n/locales/en.json

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"developer": "Developer"
5454
},
5555
"items": {
56+
"home": "Home",
5657
"profile": "Profile",
5758
"security": "Security",
5859
"sessions": "Sessions",
@@ -80,6 +81,47 @@
8081
"organizations": "Organizations",
8182
"signOut": "Sign out"
8283
},
84+
"home": {
85+
"welcome": "Welcome back",
86+
"verified": "Verified",
87+
"quickAccess": "Quick access",
88+
"stats": {
89+
"score": "Security score",
90+
"orgs": "Organizations",
91+
"twoFactor": "Two-factor auth",
92+
"enabled": "Enabled",
93+
"disabled": "Disabled",
94+
"manage": "Manage",
95+
"review": "Review",
96+
"enable": "Enable now"
97+
},
98+
"tiles": {
99+
"profile": {
100+
"title": "Profile",
101+
"description": "Display name, avatar and contact info."
102+
},
103+
"security": {
104+
"title": "Security",
105+
"description": "Change your password and review activity."
106+
},
107+
"twoFactor": {
108+
"title": "Two-factor auth",
109+
"description": "Add an authenticator app for stronger sign-in."
110+
},
111+
"sessions": {
112+
"title": "Active sessions",
113+
"description": "See and revoke devices that are signed in."
114+
},
115+
"linked": {
116+
"title": "Linked accounts",
117+
"description": "Manage your social and SSO providers."
118+
},
119+
"oauth": {
120+
"title": "OAuth applications",
121+
"description": "Apps you have authorised to access your account."
122+
}
123+
}
124+
},
83125
"profile": {
84126
"title": "Profile",
85127
"description": "Update your display name.",
@@ -205,7 +247,9 @@
205247
"noAccount": "Don't have an account?",
206248
"signUp": "Sign up",
207249
"welcomeToast": "Welcome back",
208-
"failed": "Sign in failed"
250+
"failed": "Sign in failed",
251+
"brandHeadline": "The post-SaaS operating system for your team.",
252+
"brandSubline": "One identity, every workspace. Sign in to manage your account, organizations and connected apps."
209253
},
210254
"register": {
211255
"title": "Create an account",

0 commit comments

Comments
 (0)