@fayz-ai/saas 0.5.0 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/app/AdminShell.d.ts +14 -1
  2. package/dist/app/AdminShell.d.ts.map +1 -1
  3. package/dist/app/LoginPage.d.ts +2 -16
  4. package/dist/app/LoginPage.d.ts.map +1 -1
  5. package/dist/app/admin-app.d.ts +21 -0
  6. package/dist/app/admin-app.d.ts.map +1 -0
  7. package/dist/app/config.d.ts +40 -3
  8. package/dist/app/config.d.ts.map +1 -1
  9. package/dist/app/scaffold.d.ts +1 -2
  10. package/dist/app/scaffold.d.ts.map +1 -1
  11. package/dist/{chunk-DJW67TCY.js → chunk-DCRT47W4.js} +155 -28
  12. package/dist/chunk-DCRT47W4.js.map +1 -0
  13. package/dist/{chunk-QGMDVLLW.cjs → chunk-SRIUD35R.cjs} +155 -28
  14. package/dist/chunk-SRIUD35R.cjs.map +1 -0
  15. package/dist/crud/CrudDetailPage.d.ts.map +1 -1
  16. package/dist/index.cjs +1127 -5792
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +4 -3
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +839 -5509
  21. package/dist/index.js.map +1 -1
  22. package/dist/org/adapters/supabase.d.ts +18 -0
  23. package/dist/org/adapters/supabase.d.ts.map +1 -1
  24. package/dist/org/context.d.ts.map +1 -1
  25. package/dist/org/index.cjs +11 -10
  26. package/dist/org/index.js +2 -1
  27. package/dist/shell/components/crud/archetypes/ClientOrdersTab.d.ts +4 -2
  28. package/dist/shell/components/crud/archetypes/ClientOrdersTab.d.ts.map +1 -1
  29. package/dist/shell/components/layout/BottomNav.d.ts.map +1 -1
  30. package/dist/shell/components/layout/CommandPalette.d.ts.map +1 -1
  31. package/dist/shell/components/organization/InviteMemberDialog.d.ts.map +1 -1
  32. package/dist/shell/components/organization/PermissionMatrixEditor.d.ts +6 -1
  33. package/dist/shell/components/organization/PermissionMatrixEditor.d.ts.map +1 -1
  34. package/dist/shell/components/organization/PermissionProfilesTab.d.ts.map +1 -1
  35. package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
  36. package/dist/shell/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
  37. package/dist/shell/components/settings/ConnectedUserProfile.d.ts +2 -1
  38. package/dist/shell/components/settings/ConnectedUserProfile.d.ts.map +1 -1
  39. package/dist/shell/components/settings/SettingsPage.d.ts +8 -1
  40. package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
  41. package/dist/shell/config/theme/tokens.d.ts +12 -1
  42. package/dist/shell/config/theme/tokens.d.ts.map +1 -1
  43. package/dist/shell/config/theme/utils.d.ts +8 -0
  44. package/dist/shell/config/theme/utils.d.ts.map +1 -1
  45. package/dist/shell/lib/i18n.d.ts.map +1 -1
  46. package/dist/shell/types/client-orders.d.ts +4 -0
  47. package/dist/shell/types/client-orders.d.ts.map +1 -1
  48. package/dist/shell/types/crud.d.ts +4 -0
  49. package/dist/shell/types/crud.d.ts.map +1 -1
  50. package/package.json +7 -9
  51. package/src/app/AdminShell.tsx +103 -35
  52. package/src/app/LoginPage.tsx +2 -264
  53. package/src/app/{createFayzApp.tsx → admin-app.tsx} +98 -40
  54. package/src/app/config.ts +44 -2
  55. package/src/app/scaffold.tsx +13 -26
  56. package/src/crud/CrudDetailPage.tsx +43 -9
  57. package/src/crud/CrudPage.tsx +1 -1
  58. package/src/index.ts +12 -5
  59. package/src/org/adapters/supabase.ts +176 -28
  60. package/src/org/context.tsx +33 -2
  61. package/src/shell/components/crud/archetypes/ClientOrdersTab.tsx +12 -4
  62. package/src/shell/components/layout/BottomNav.tsx +39 -4
  63. package/src/shell/components/layout/CommandPalette.tsx +14 -10
  64. package/src/shell/components/organization/InviteMemberDialog.tsx +14 -3
  65. package/src/shell/components/organization/PermissionMatrixEditor.tsx +48 -10
  66. package/src/shell/components/organization/PermissionProfilesTab.tsx +65 -23
  67. package/src/shell/components/organization/TeamTab.tsx +36 -9
  68. package/src/shell/components/plugins/PluginSettingsPanel.tsx +5 -3
  69. package/src/shell/components/settings/ConnectedUserProfile.tsx +68 -25
  70. package/src/shell/components/settings/SettingsPage.tsx +12 -3
  71. package/src/shell/config/theme/tokens.ts +13 -1
  72. package/src/shell/config/theme/utils.ts +76 -15
  73. package/src/shell/lib/i18n.ts +4 -0
  74. package/src/shell/types/client-orders.ts +5 -0
  75. package/src/shell/types/crud.ts +4 -0
  76. package/dist/app/createFayzApp.d.ts +0 -11
  77. package/dist/app/createFayzApp.d.ts.map +0 -1
  78. package/dist/chunk-DJW67TCY.js.map +0 -1
  79. package/dist/chunk-QGMDVLLW.cjs.map +0 -1
  80. package/dist/shell/components/organization/ImpersonationBanner.d.ts +0 -3
  81. package/dist/shell/components/organization/ImpersonationBanner.d.ts.map +0 -1
  82. package/dist/shell/components/settings/ConnectedHolidaysSettings.d.ts +0 -3
  83. package/dist/shell/components/settings/ConnectedHolidaysSettings.d.ts.map +0 -1
  84. package/dist/shell/components/settings/ConnectedLocationsOverview.d.ts +0 -3
  85. package/dist/shell/components/settings/ConnectedLocationsOverview.d.ts.map +0 -1
  86. package/dist/shell/createSaasApp.d.ts +0 -145
  87. package/dist/shell/createSaasApp.d.ts.map +0 -1
  88. package/src/shell/components/organization/ImpersonationBanner.tsx +0 -29
  89. package/src/shell/components/settings/ConnectedHolidaysSettings.tsx +0 -99
  90. package/src/shell/components/settings/ConnectedLocationsOverview.tsx +0 -57
  91. package/src/shell/createSaasApp.tsx +0 -943
@@ -2,6 +2,8 @@ import * as React from 'react'
2
2
  import {
3
3
  AppShell,
4
4
  type NavigationItem,
5
+ type BottomNavItem,
6
+ type MobileHeaderVariant,
5
7
  ModuleLayoutProvider,
6
8
  PageTransition,
7
9
  type ModuleNavVariant,
@@ -14,13 +16,15 @@ import {
14
16
  Avatar,
15
17
  AvatarImage,
16
18
  AvatarFallback,
19
+ useLayoutStore,
17
20
  } from '@fayz-ai/ui'
21
+ import { CommandPalette, type CommandItem } from '../shell/components/layout/CommandPalette'
18
22
  import { useOrganizationStore } from '../org/store'
19
23
  import { useAuth } from '@fayz-ai/auth'
24
+ import { AuthGate } from '@fayz-ai/plugin-auth'
20
25
  import { usePluginRuntime, resolvePluginComponent, useTranslation } from '@fayz-ai/core'
21
26
  import type { PermissionAction, PluginNavigationEntry, PluginRouteDefinition, PluginSettingsTab } from '@fayz-ai/core'
22
27
  import { useAdminPath, navigateTo, matchRoute, routeScore } from './routing'
23
- import { LoginPage } from './LoginPage'
24
28
  import type { CustomPage } from './config'
25
29
  import type { AuthProvider } from '@fayz-ai/core'
26
30
  import { setEntityRouteMap } from '../lib/entity-routes'
@@ -55,12 +59,25 @@ export interface AdminShellProps {
55
59
  oauthProviders?: Exclude<AuthProvider, 'email'>[]
56
60
  showSettings?: boolean
57
61
  showOrgSettings?: boolean
62
+ /** Show the workspace/org switcher at the sidebar top. Default: true.
63
+ * Single-org apps (config.org.multiOrg === false) get false from the
64
+ * scaffold so B2C users never see an organization concept. */
65
+ showOrgSwitcher?: boolean
66
+ /** Show the branding ("Identidade Visual") + company "Geral" settings tabs.
67
+ * Default: true. B2C apps pass false to hide org-identity settings. */
68
+ showBranding?: boolean
58
69
  /** Wrap the main content in an inset "framed" card (default: true). The
59
70
  * sidebar is always flush/full-height. */
60
71
  contentFrame?: boolean
61
72
  /** How module-internal navigation renders. Defaults to 'tabs' for the
62
73
  * 'sidebar' layout and 'rail' for 'topbar'. */
63
74
  moduleNav?: ModuleNavVariant
75
+ /** Mobile bottom tab bar, passed straight through to AppShell. */
76
+ bottomNav?: BottomNavItem[]
77
+ /** Fired when a bottom-nav `action` item (e.g. the center "+") is tapped. */
78
+ onBottomNavAction?: (id: string) => void
79
+ /** Mobile header treatment (<md), passed straight through to AppShell. */
80
+ mobileHeader?: MobileHeaderVariant
64
81
  }
65
82
 
66
83
  // ---------------------------------------------------------------------------
@@ -196,10 +213,25 @@ function sectionOrder(section: NavigationItem['section']): number {
196
213
  return section === 'main' ? 0 : section === 'secondary' ? 1 : 2
197
214
  }
198
215
 
199
- function compareNavigation(a: OrderedNavigationItem, b: OrderedNavigationItem): number {
216
+ function compareNavigation(a: NavigationItem & { position?: number }, b: NavigationItem & { position?: number }): number {
200
217
  const sectionDelta = sectionOrder(a.section) - sectionOrder(b.section)
201
218
  if (sectionDelta !== 0) return sectionDelta
202
- return a.position - b.position
219
+ return (a.position ?? 0) - (b.position ?? 0)
220
+ }
221
+
222
+ function findModuleParent(
223
+ item: OrderedNavigationItem,
224
+ items: OrderedNavigationItem[],
225
+ ): OrderedNavigationItem | null {
226
+ const candidates = items
227
+ .filter((candidate) =>
228
+ candidate.section === item.section &&
229
+ candidate.route !== item.route &&
230
+ item.route.startsWith(`${candidate.route}/`)
231
+ )
232
+ .sort((a, b) => b.route.length - a.route.length)
233
+
234
+ return candidates[0] ?? null
203
235
  }
204
236
 
205
237
  function pageToNavigationItem(page: CustomPage, fallbackPosition: number): OrderedNavigationItem | null {
@@ -214,7 +246,8 @@ function pageToNavigationItem(page: CustomPage, fallbackPosition: number): Order
214
246
  permission: page.permission,
215
247
  position: page.position ?? fallbackPosition,
216
248
  children: page.children
217
- ?.map((child, index) => pageToNavigationItem(child, index))
249
+ ?.filter((child) => child.nav !== false)
250
+ .map((child, index) => pageToNavigationItem(child, index))
218
251
  .filter((item): item is OrderedNavigationItem => Boolean(item)),
219
252
  }
220
253
  }
@@ -275,7 +308,7 @@ function buildSettingsTabs(
275
308
  return settingsTabs
276
309
  }
277
310
 
278
- function AdminShellInner({ appName, layout = 'sidebar', logo, pages = [], showSettings = true, showOrgSettings = false, contentFrame = true, moduleNav }: AdminShellProps) {
311
+ function AdminShellInner({ appName, layout = 'sidebar', logo, pages = [], showSettings = true, showOrgSettings = false, showBranding = true, showOrgSwitcher = true, contentFrame = true, moduleNav, bottomNav, onBottomNavAction, mobileHeader }: AdminShellProps) {
279
312
  const runtime = usePluginRuntime()
280
313
  const t = useTranslation()
281
314
  const can = usePermissionOptional()
@@ -293,12 +326,49 @@ function AdminShellInner({ appName, layout = 'sidebar', logo, pages = [], showSe
293
326
  position: entry.position,
294
327
  }))
295
328
  for (const [index, page] of pages.entries()) {
329
+ // `nav: false` routes the page but keeps it out of the sidebar/topbar nav
330
+ // (mobile-only pages reached via bottomNav/avatar). Routes are still built
331
+ // from `pages` in collectPageRoutes below, so the page remains navigable.
332
+ if (page.nav === false) continue
296
333
  const item = pageToNavigationItem(page, index + 1000)
297
- if (item) items.push(item)
334
+ if (!item) continue
335
+
336
+ const parent = findModuleParent(item, items)
337
+ if (parent) {
338
+ const children = parent.children ?? [{
339
+ ...parent,
340
+ id: `${parent.id}:index`,
341
+ children: undefined,
342
+ position: -1,
343
+ }]
344
+ if (!children.some((child) => child.route === item.route)) {
345
+ parent.children = [...children, item].sort(compareNavigation)
346
+ }
347
+ continue
348
+ }
349
+
350
+ items.push(item)
298
351
  }
299
352
  return items.sort(compareNavigation)
300
353
  }, [runtime.navigation, pages])
301
354
 
355
+ // Command palette (⌘K + the topbar Search button). The palette owns the ⌘K
356
+ // key listener, so it must be MOUNTED for the shortcut to work — the topbar
357
+ // Search button drives the same shared layout store.
358
+ const commandPaletteOpen = useLayoutStore((s) => s.commandPaletteOpen)
359
+ const setCommandPaletteOpen = useLayoutStore((s) => s.setCommandPaletteOpen)
360
+ const commandItems = React.useMemo<CommandItem[]>(() => {
361
+ const navGroup = tr('layout.commandPalette.navigation', 'Navigation')
362
+ const items: CommandItem[] = []
363
+ for (const nav of navigation) {
364
+ items.push({ id: nav.id, label: nav.label, icon: nav.icon, group: navGroup, action: () => navigateTo(nav.route) })
365
+ for (const child of nav.children ?? []) {
366
+ items.push({ id: child.id, label: `${nav.label} · ${child.label}`, icon: child.icon, group: navGroup, action: () => navigateTo(child.route) })
367
+ }
368
+ }
369
+ return items
370
+ }, [navigation])
371
+
302
372
  // Route table: plugin routes + custom pages, most-specific-first.
303
373
  const routes = React.useMemo<RouteEntry[]>(() => {
304
374
  const list: RouteEntry[] = []
@@ -362,7 +432,7 @@ function AdminShellInner({ appName, layout = 'sidebar', logo, pages = [], showSe
362
432
 
363
433
  const activeContent = ActiveComponent ? (
364
434
  match.route.path === '/settings' || match.route.path === '/settings/*'
365
- ? <SettingsPage extraTabs={settingsTabs} />
435
+ ? <SettingsPage extraTabs={settingsTabs} showCompany={showBranding} showBranding={showBranding} />
366
436
  : <ActiveComponent {...activeParams} />
367
437
  ) : null
368
438
 
@@ -376,10 +446,14 @@ function AdminShellInner({ appName, layout = 'sidebar', logo, pages = [], showSe
376
446
  user={shellUser}
377
447
  pageTitle={activePageTitle}
378
448
  currentPath={path}
449
+ bottomNav={bottomNav}
450
+ onBottomNavAction={onBottomNavAction}
451
+ mobileHeader={mobileHeader}
379
452
  onNavigate={(route) => navigateTo(route)}
380
453
  onSignOut={() => { void signOut() }}
454
+ onProfile={() => navigateTo('/perfil')}
381
455
  onSettings={() => navigateTo('/settings')}
382
- sidebarTopContent={<WorkspaceSwitcher />}
456
+ sidebarTopContent={showOrgSwitcher ? <WorkspaceSwitcher /> : undefined}
383
457
  userMenuSlot={
384
458
  <AdminUserMenu
385
459
  user={shellUser}
@@ -395,7 +469,7 @@ function AdminShellInner({ appName, layout = 'sidebar', logo, pages = [], showSe
395
469
  >
396
470
  {match?.route.fullBleed
397
471
  ? <div className="h-full min-h-0 overflow-hidden">{activeContent}</div>
398
- : <div className="space-y-6 p-6">{activeContent}</div>}
472
+ : <div className="space-y-4 p-3 md:space-y-6 md:p-6">{activeContent}</div>}
399
473
  </PageTransition>
400
474
  ) : (
401
475
  <div className="flex h-full flex-col items-center justify-center p-12 text-center">
@@ -413,37 +487,31 @@ function AdminShellInner({ appName, layout = 'sidebar', logo, pages = [], showSe
413
487
  </div>
414
488
  )}
415
489
  </AppShell>
490
+ <CommandPalette
491
+ commands={commandItems}
492
+ open={commandPaletteOpen}
493
+ onOpenChange={setCommandPaletteOpen}
494
+ />
416
495
  </ModuleLayoutProvider>
417
496
  )
418
497
  }
419
498
 
420
499
  /** The shell + an auth gate in front of it. */
421
500
  export function AdminShell(props: AdminShellProps) {
422
- const { isAuthenticated, isLoading } = useAuth()
423
- const requireAuth = props.requireAuth ?? true
424
-
425
- if (requireAuth && !isAuthenticated) {
426
- if (isLoading) {
427
- return (
428
- <div className="flex min-h-screen items-center justify-center bg-background">
429
- <span className="text-sm text-muted-foreground">Loading…</span>
430
- </div>
431
- )
432
- }
433
- return (
434
- <LoginPage
435
- appName={props.appName}
436
- logo={props.logo}
437
- loginLogo={props.loginLogo}
438
- layout={props.loginLayout}
439
- tagline={props.loginTagline}
440
- description={props.loginDescription}
441
- showOAuth={props.showOAuth}
442
- oauthProviders={props.oauthProviders}
443
- />
444
- )
445
- }
446
-
447
- return <AdminShellInner {...props} />
501
+ return (
502
+ <AuthGate
503
+ requireAuth={props.requireAuth ?? true}
504
+ appName={props.appName}
505
+ logo={props.logo}
506
+ loginLogo={props.loginLogo}
507
+ layout={props.loginLayout}
508
+ tagline={props.loginTagline}
509
+ description={props.loginDescription}
510
+ showOAuth={props.showOAuth}
511
+ oauthProviders={props.oauthProviders}
512
+ >
513
+ <AdminShellInner {...props} />
514
+ </AuthGate>
515
+ )
448
516
  }
449
517
  AdminShell.displayName = 'AdminShell'
@@ -1,264 +1,2 @@
1
- import * as React from 'react'
2
- import { Button, Input } from '@fayz-ai/ui'
3
- import { useAuth } from '@fayz-ai/auth'
4
- import { useTranslation } from '@fayz-ai/core'
5
- import type { AuthProvider } from '@fayz-ai/core'
6
-
7
- export interface LoginPageProps {
8
- appName?: string
9
- logo?: React.ReactNode
10
- loginLogo?: React.ReactNode
11
- layout?: 'split' | 'centered'
12
- tagline?: string
13
- description?: string
14
- showOAuth?: boolean
15
- oauthProviders?: Exclude<AuthProvider, 'email'>[]
16
- }
17
-
18
- type View = 'login' | 'signup' | 'recovery'
19
-
20
- function providerLabel(provider: string): string {
21
- return provider.charAt(0).toUpperCase() + provider.slice(1)
22
- }
23
-
24
- export function LoginPage({
25
- appName = 'Fayz',
26
- logo,
27
- loginLogo,
28
- layout = 'split',
29
- tagline,
30
- description,
31
- showOAuth = false,
32
- oauthProviders = ['google'],
33
- }: LoginPageProps) {
34
- const t = useTranslation()
35
- const tr = (key: string, fallback: string) => {
36
- const v = t(key)
37
- return !v || v === key || v.includes('{{') ? fallback : v
38
- }
39
- const { signIn, signUp, signInWithOAuth, resetPassword, isLoading, error } = useAuth()
40
- const [view, setView] = React.useState<View>('login')
41
- const [email, setEmail] = React.useState('')
42
- const [password, setPassword] = React.useState('')
43
- const [fullName, setFullName] = React.useState('')
44
- const [notice, setNotice] = React.useState<string | null>(null)
45
- const [submitting, setSubmitting] = React.useState(false)
46
-
47
- const busy = submitting || isLoading
48
- const brandLogo = loginLogo ?? logo
49
- const heading = view === 'login'
50
- ? tr('auth.login.title', 'Welcome back')
51
- : view === 'signup'
52
- ? tr('auth.signup.title', 'Create account')
53
- : tr('auth.recovery.title', 'Reset password')
54
- const subtitle = view === 'login'
55
- ? tr('auth.login.enterCredentials', 'Enter your credentials to continue')
56
- : view === 'signup'
57
- ? tr('auth.signup.getStarted', 'Create your account to get started')
58
- : tr('auth.recovery.resetSubtitle', 'Enter your email and we will send reset instructions')
59
-
60
- async function submitLogin(e: React.FormEvent) {
61
- e.preventDefault()
62
- setNotice(null)
63
- setSubmitting(true)
64
- try {
65
- await signIn(email, password)
66
- } catch {
67
- // error is surfaced by useAuth().error
68
- } finally {
69
- setSubmitting(false)
70
- }
71
- }
72
-
73
- async function submitSignup(e: React.FormEvent) {
74
- e.preventDefault()
75
- setNotice(null)
76
- setSubmitting(true)
77
- try {
78
- await signUp(email, password, fullName)
79
- } catch {
80
- // error is surfaced by useAuth().error
81
- } finally {
82
- setSubmitting(false)
83
- }
84
- }
85
-
86
- async function submitRecovery(e: React.FormEvent) {
87
- e.preventDefault()
88
- setSubmitting(true)
89
- try {
90
- await resetPassword(email)
91
- setNotice(tr('auth.recovery.sent', 'Password reset instructions sent.'))
92
- } catch {
93
- // error is surfaced by useAuth().error
94
- } finally {
95
- setSubmitting(false)
96
- }
97
- }
98
-
99
- async function submitOAuth(provider: AuthProvider) {
100
- setSubmitting(true)
101
- try {
102
- await signInWithOAuth(provider)
103
- } finally {
104
- setSubmitting(false)
105
- }
106
- }
107
-
108
- const LoginForm = (
109
- <form onSubmit={view === 'signup' ? submitSignup : view === 'recovery' ? submitRecovery : submitLogin} className="space-y-4">
110
- {view === 'signup' && (
111
- <div className="space-y-1.5">
112
- <label className="text-sm font-medium text-foreground" htmlFor="login-name">
113
- {tr('auth.fullName', 'Full name')}
114
- </label>
115
- <Input id="login-name" value={fullName} onChange={(e) => setFullName(e.target.value)} required disabled={busy} />
116
- </div>
117
- )}
118
- <div className="space-y-1.5">
119
- <label className="text-sm font-medium text-foreground" htmlFor="login-email">
120
- {tr('auth.email', 'Email')}
121
- </label>
122
- <Input
123
- id="login-email"
124
- type="email"
125
- autoComplete="username"
126
- value={email}
127
- onChange={(e) => setEmail(e.target.value)}
128
- required
129
- disabled={busy}
130
- />
131
- </div>
132
- {view !== 'recovery' && (
133
- <div className="space-y-1.5">
134
- <div className="flex items-center justify-between gap-3">
135
- <label className="text-sm font-medium text-foreground" htmlFor="login-password">
136
- {tr('auth.password', 'Password')}
137
- </label>
138
- {view === 'login' && (
139
- <button type="button" className="text-xs font-medium text-primary hover:underline" onClick={() => setView('recovery')}>
140
- {tr('auth.forgotPassword', 'Forgot password?')}
141
- </button>
142
- )}
143
- </div>
144
- <Input
145
- id="login-password"
146
- type="password"
147
- autoComplete={view === 'signup' ? 'new-password' : 'current-password'}
148
- value={password}
149
- onChange={(e) => setPassword(e.target.value)}
150
- required
151
- disabled={busy}
152
- />
153
- </div>
154
- )}
155
- {error && <p className="text-sm text-destructive">{String(error.message ?? error)}</p>}
156
- {notice && <p className="text-sm text-success">{notice}</p>}
157
- <Button type="submit" className="w-full" disabled={busy}>
158
- {busy
159
- ? tr('common.loading', 'Loading...')
160
- : view === 'signup'
161
- ? tr('auth.signUp', 'Sign up')
162
- : view === 'recovery'
163
- ? tr('auth.recovery.submit', 'Send reset link')
164
- : tr('auth.signIn', 'Sign in')}
165
- </Button>
166
- {showOAuth && view === 'login' && oauthProviders.length > 0 && (
167
- <div className="space-y-2">
168
- {oauthProviders.map((provider) => (
169
- <Button
170
- key={provider}
171
- type="button"
172
- variant="outline"
173
- className="w-full"
174
- disabled={busy}
175
- onClick={() => { void submitOAuth(provider) }}
176
- >
177
- {tr(`auth.oauth.${provider}`, `Continue with ${providerLabel(provider)}`)}
178
- </Button>
179
- ))}
180
- </div>
181
- )}
182
- </form>
183
- )
184
-
185
- const viewSwitch = view === 'login' ? (
186
- <p className="text-center text-sm text-muted-foreground">
187
- {tr('auth.login.noAccount', 'Do not have an account?')}{' '}
188
- <button type="button" onClick={() => setView('signup')} className="font-medium text-primary hover:underline">
189
- {tr('auth.signUp', 'Sign up')}
190
- </button>
191
- </p>
192
- ) : (
193
- <p className="text-center text-sm text-muted-foreground">
194
- {view === 'signup' ? tr('auth.signup.hasAccount', 'Already have an account?') : tr('auth.recovery.remembered', 'Remembered your password?')}{' '}
195
- <button type="button" onClick={() => setView('login')} className="font-medium text-primary hover:underline">
196
- {tr('auth.signIn', 'Sign in')}
197
- </button>
198
- </p>
199
- )
200
-
201
- if (layout === 'centered') {
202
- return (
203
- <div className="flex min-h-screen items-center justify-center bg-sidebar p-4">
204
- <div className="relative w-full max-w-md rounded-2xl border border-border/50 bg-card p-8 shadow-2xl sm:p-10">
205
- <div className="mb-8 text-center">
206
- {brandLogo && <div className="mb-4 flex justify-center">{brandLogo}</div>}
207
- <h1 className="text-2xl font-bold text-foreground">{heading}</h1>
208
- <p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
209
- </div>
210
- <div className="space-y-6">
211
- {LoginForm}
212
- {viewSwitch}
213
- </div>
214
- </div>
215
- </div>
216
- )
217
- }
218
-
219
- return (
220
- <div className="flex min-h-screen bg-sidebar">
221
- <div className="relative hidden w-1/2 overflow-hidden lg:block">
222
- <div className="absolute inset-0 bg-gradient-to-br from-sidebar-accent/50 via-transparent to-sidebar/80" />
223
- <div className="absolute -left-20 -top-20 h-80 w-80 rounded-full bg-primary/10" />
224
- <div className="absolute -bottom-10 -right-10 h-60 w-60 rounded-full bg-primary/5" />
225
- <div className="absolute right-1/4 top-1/3 h-40 w-40 rounded-full bg-primary/5" />
226
- <div className="relative flex h-full flex-col justify-end p-12">
227
- <div className="space-y-4">
228
- <p className="text-sm font-medium text-sidebar-muted">{appName}</p>
229
- <h2 className="text-3xl font-bold text-sidebar-foreground">
230
- {tagline ?? tr('auth.login.welcomeTo', `Welcome to ${appName}`)}
231
- </h2>
232
- {description && <p className="max-w-md text-sm leading-relaxed text-sidebar-muted">{description}</p>}
233
- <div className="mt-8 flex items-center gap-3 rounded-xl bg-sidebar-accent/40 p-4">
234
- <div className="flex -space-x-2">
235
- {['bg-primary', 'bg-success', 'bg-warning'].map((bg, i) => (
236
- <div key={i} className={`${bg} h-8 w-8 rounded-full border-2 border-sidebar`} />
237
- ))}
238
- </div>
239
- <p className="text-xs text-sidebar-foreground/80">
240
- {tr('auth.login.socialProof', `${appName} keeps your operation moving.`)}
241
- </p>
242
- </div>
243
- </div>
244
- </div>
245
- </div>
246
- <div className="flex flex-1 flex-col lg:p-3 lg:pl-3">
247
- <div className="flex flex-1 flex-col justify-center bg-background px-6 py-12 lg:rounded-[1.25rem] lg:px-16">
248
- <div className="mx-auto w-full max-w-sm">
249
- {brandLogo && <div className="mb-8">{brandLogo}</div>}
250
- <div className="mb-8">
251
- <h1 className="text-2xl font-bold text-foreground">{heading}</h1>
252
- <p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
253
- </div>
254
- <div className="space-y-6">
255
- {LoginForm}
256
- {viewSwitch}
257
- </div>
258
- </div>
259
- </div>
260
- </div>
261
- </div>
262
- )
263
- }
264
- LoginPage.displayName = 'AdminLoginPage'
1
+ export { LoginPage } from '@fayz-ai/plugin-auth'
2
+ export type { LoginPageProps } from '@fayz-ai/plugin-auth'