@fayz-ai/saas 0.6.0 → 0.7.0

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 (96) hide show
  1. package/README.md +1 -1
  2. package/dist/app/AdminShell.d.ts +14 -1
  3. package/dist/app/AdminShell.d.ts.map +1 -1
  4. package/dist/app/LoginPage.d.ts +2 -16
  5. package/dist/app/LoginPage.d.ts.map +1 -1
  6. package/dist/app/admin-app.d.ts +21 -0
  7. package/dist/app/admin-app.d.ts.map +1 -0
  8. package/dist/app/config.d.ts +40 -3
  9. package/dist/app/config.d.ts.map +1 -1
  10. package/dist/app/scaffold.d.ts +1 -2
  11. package/dist/app/scaffold.d.ts.map +1 -1
  12. package/dist/{chunk-DJW67TCY.js → chunk-DCRT47W4.js} +155 -28
  13. package/dist/chunk-DCRT47W4.js.map +1 -0
  14. package/dist/{chunk-QGMDVLLW.cjs → chunk-SRIUD35R.cjs} +155 -28
  15. package/dist/chunk-SRIUD35R.cjs.map +1 -0
  16. package/dist/crud/CrudDetailPage.d.ts.map +1 -1
  17. package/dist/hooks/useModuleNavigation.d.ts.map +1 -1
  18. package/dist/index.cjs +1131 -5792
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +4 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +844 -5510
  23. package/dist/index.js.map +1 -1
  24. package/dist/org/adapters/supabase.d.ts +18 -0
  25. package/dist/org/adapters/supabase.d.ts.map +1 -1
  26. package/dist/org/context.d.ts.map +1 -1
  27. package/dist/org/index.cjs +11 -10
  28. package/dist/org/index.js +2 -1
  29. package/dist/shell/components/crud/archetypes/ClientOrdersTab.d.ts +4 -2
  30. package/dist/shell/components/crud/archetypes/ClientOrdersTab.d.ts.map +1 -1
  31. package/dist/shell/components/layout/BottomNav.d.ts.map +1 -1
  32. package/dist/shell/components/layout/CommandPalette.d.ts.map +1 -1
  33. package/dist/shell/components/organization/InviteMemberDialog.d.ts.map +1 -1
  34. package/dist/shell/components/organization/PermissionMatrixEditor.d.ts +6 -1
  35. package/dist/shell/components/organization/PermissionMatrixEditor.d.ts.map +1 -1
  36. package/dist/shell/components/organization/PermissionProfilesTab.d.ts.map +1 -1
  37. package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
  38. package/dist/shell/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
  39. package/dist/shell/components/settings/ConnectedUserProfile.d.ts +2 -1
  40. package/dist/shell/components/settings/ConnectedUserProfile.d.ts.map +1 -1
  41. package/dist/shell/components/settings/SettingsPage.d.ts +8 -1
  42. package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
  43. package/dist/shell/config/theme/tokens.d.ts +12 -1
  44. package/dist/shell/config/theme/tokens.d.ts.map +1 -1
  45. package/dist/shell/config/theme/utils.d.ts +8 -0
  46. package/dist/shell/config/theme/utils.d.ts.map +1 -1
  47. package/dist/shell/hooks/useModuleNavigation.d.ts.map +1 -1
  48. package/dist/shell/lib/i18n.d.ts.map +1 -1
  49. package/dist/shell/types/client-orders.d.ts +4 -0
  50. package/dist/shell/types/client-orders.d.ts.map +1 -1
  51. package/dist/shell/types/crud.d.ts +4 -0
  52. package/dist/shell/types/crud.d.ts.map +1 -1
  53. package/package.json +6 -5
  54. package/src/app/AdminShell.tsx +103 -35
  55. package/src/app/LoginPage.tsx +2 -264
  56. package/src/app/{createFayzApp.tsx → admin-app.tsx} +98 -40
  57. package/src/app/config.ts +44 -2
  58. package/src/app/scaffold.tsx +13 -26
  59. package/src/crud/CrudDetailPage.tsx +43 -9
  60. package/src/crud/CrudPage.tsx +1 -1
  61. package/src/hooks/useModuleNavigation.ts +7 -1
  62. package/src/index.ts +12 -5
  63. package/src/org/adapters/supabase.ts +176 -28
  64. package/src/org/context.tsx +33 -2
  65. package/src/shell/components/crud/archetypes/ClientOrdersTab.tsx +12 -4
  66. package/src/shell/components/layout/BottomNav.tsx +39 -4
  67. package/src/shell/components/layout/CommandPalette.tsx +14 -10
  68. package/src/shell/components/organization/InviteMemberDialog.tsx +14 -3
  69. package/src/shell/components/organization/PermissionMatrixEditor.tsx +48 -10
  70. package/src/shell/components/organization/PermissionProfilesTab.tsx +65 -23
  71. package/src/shell/components/organization/TeamTab.tsx +36 -9
  72. package/src/shell/components/plugins/PluginSettingsPanel.tsx +5 -3
  73. package/src/shell/components/settings/ConnectedUserProfile.tsx +68 -25
  74. package/src/shell/components/settings/SettingsPage.tsx +12 -3
  75. package/src/shell/config/theme/tokens.ts +13 -1
  76. package/src/shell/config/theme/utils.ts +76 -15
  77. package/src/shell/hooks/useModuleNavigation.ts +7 -1
  78. package/src/shell/lib/i18n.ts +4 -0
  79. package/src/shell/types/client-orders.ts +5 -0
  80. package/src/shell/types/crud.ts +4 -0
  81. package/dist/app/createFayzApp.d.ts +0 -11
  82. package/dist/app/createFayzApp.d.ts.map +0 -1
  83. package/dist/chunk-DJW67TCY.js.map +0 -1
  84. package/dist/chunk-QGMDVLLW.cjs.map +0 -1
  85. package/dist/shell/components/organization/ImpersonationBanner.d.ts +0 -3
  86. package/dist/shell/components/organization/ImpersonationBanner.d.ts.map +0 -1
  87. package/dist/shell/components/settings/ConnectedHolidaysSettings.d.ts +0 -3
  88. package/dist/shell/components/settings/ConnectedHolidaysSettings.d.ts.map +0 -1
  89. package/dist/shell/components/settings/ConnectedLocationsOverview.d.ts +0 -3
  90. package/dist/shell/components/settings/ConnectedLocationsOverview.d.ts.map +0 -1
  91. package/dist/shell/createSaasApp.d.ts +0 -145
  92. package/dist/shell/createSaasApp.d.ts.map +0 -1
  93. package/src/shell/components/organization/ImpersonationBanner.tsx +0 -29
  94. package/src/shell/components/settings/ConnectedHolidaysSettings.tsx +0 -99
  95. package/src/shell/components/settings/ConnectedLocationsOverview.tsx +0 -57
  96. package/src/shell/createSaasApp.tsx +0 -943
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react'
2
- import type { AuthAdapter, OrgAdapter } from '@fayz-ai/core'
2
+ import type { OrgAdapter, AuthAdapter } from '@fayz-ai/core'
3
3
  import {
4
4
  PluginRuntimeProvider,
5
5
  resolvePluginRuntime,
@@ -8,7 +8,13 @@ import {
8
8
  mergeTranslations,
9
9
  setCurrentLocale,
10
10
  } from '@fayz-ai/core'
11
- import { AuthProvider, createSupabaseAuthAdapter, createMockAuthAdapter } from '@fayz-ai/auth'
11
+ import {
12
+ AuthProvider,
13
+ createAuthRuntime,
14
+ type AuthLayout,
15
+ type AuthPluginOptions,
16
+ type ResolvedAuthPlugin,
17
+ } from '@fayz-ai/plugin-auth'
12
18
  import { NavTransitionProvider } from '@fayz-ai/ui'
13
19
  import { createFayzSupabaseClient, getFayzSupabaseClientOptional } from '../supabase/client'
14
20
  import { OrgProvider } from '../org/context'
@@ -22,8 +28,7 @@ import { useBillingStore } from '../billing/store'
22
28
  import type { Plan } from '@fayz-ai/core'
23
29
  import type { PlanConfig } from '../shell/types/billing'
24
30
  import { useThemeStore } from '../shell/stores/theme.store'
25
- import { resolveTheme, type CreateThemeOptions } from '../shell/config/theme/utils'
26
- import type { SaasTheme } from '../shell/config/theme/tokens'
31
+ import { resolveTheme, isSaasTheme } from '../shell/config/theme/utils'
27
32
  import { AdminShell } from './AdminShell'
28
33
  import { useAdminPath } from './routing'
29
34
  import type { FayzAppConfig } from './config'
@@ -32,32 +37,87 @@ import type { FayzAppConfig } from './config'
32
37
  // Adapter resolution
33
38
  // ---------------------------------------------------------------------------
34
39
 
35
- function resolveAuthAdapter(config: FayzAppConfig): AuthAdapter {
40
+ function isResolvedAuthPlugin(value: FayzAppConfig['auth']): value is ResolvedAuthPlugin {
41
+ return Boolean(value && 'kind' in value && value.kind === 'auth')
42
+ }
43
+
44
+ function resolveAuthRuntime(config: FayzAppConfig): ResolvedAuthPlugin {
36
45
  const auth = config.auth
37
- if (auth?.adapter && typeof auth.adapter === 'object') return auth.adapter as AuthAdapter
38
-
39
- const strategy = auth?.adapter ?? (config.supabaseUrl ? 'supabase' : 'mock')
40
- if (strategy === 'supabase') {
41
- if (!config.supabaseUrl || !config.supabaseAnonKey) {
42
- throw new Error(
43
- "[@fayz-ai/saas] auth.adapter='supabase' requires supabaseUrl and supabaseAnonKey in FayzAppConfig.",
44
- )
46
+ if (isResolvedAuthPlugin(auth)) return auth
47
+
48
+ const provider = auth?.provider ?? auth?.adapter ?? (config.supabaseUrl ? 'supabase' : 'mock')
49
+ const oauth = auth?.oauth ?? {
50
+ enabled: auth?.showOAuth ?? false,
51
+ providers: auth?.oauthProviders ?? ['google'],
52
+ }
53
+
54
+ return createAuthRuntime({
55
+ provider,
56
+ requireAuth: auth?.requireAuth ?? true,
57
+ layout: auth?.layout ?? auth?.loginLayout,
58
+ logo: auth?.logo ?? auth?.loginLogo,
59
+ tagline: auth?.tagline ?? auth?.loginTagline,
60
+ description: auth?.description ?? auth?.loginDescription,
61
+ oauth,
62
+ routes: auth?.routes,
63
+ supabase: {
64
+ url: auth?.supabase?.url ?? config.supabaseUrl,
65
+ anonKey: auth?.supabase?.anonKey ?? config.supabaseAnonKey,
66
+ client: auth?.supabase?.client ?? getFayzSupabaseClientOptional() ?? undefined,
67
+ callbackUrl: auth?.supabase?.callbackUrl,
68
+ resetPasswordUrl: auth?.supabase?.resetPasswordUrl,
69
+ },
70
+ } satisfies AuthPluginOptions)
71
+ }
72
+
73
+ export function getAuthShellProps(auth: FayzAppConfig['auth']): {
74
+ requireAuth?: boolean
75
+ loginTagline?: string
76
+ loginDescription?: string
77
+ loginLogo?: React.ReactNode
78
+ loginLayout?: AuthLayout
79
+ showOAuth?: boolean
80
+ oauthProviders?: Exclude<import('@fayz-ai/core').AuthProvider, 'email'>[]
81
+ } {
82
+ if (!auth) return {}
83
+ if (isResolvedAuthPlugin(auth)) {
84
+ return {
85
+ requireAuth: auth.requireAuth,
86
+ loginTagline: auth.tagline,
87
+ loginDescription: auth.description,
88
+ loginLogo: auth.logo,
89
+ loginLayout: auth.layout,
90
+ showOAuth: auth.oauth.enabled,
91
+ oauthProviders: auth.oauth.providers,
45
92
  }
46
- return createSupabaseAuthAdapter({
47
- supabaseUrl: config.supabaseUrl,
48
- supabaseAnonKey: config.supabaseAnonKey,
49
- supabaseClient: getFayzSupabaseClientOptional() ?? undefined,
50
- })
51
93
  }
52
- return createMockAuthAdapter()
94
+ return {
95
+ requireAuth: auth.requireAuth,
96
+ loginTagline: auth.tagline ?? auth.loginTagline,
97
+ loginDescription: auth.description ?? auth.loginDescription,
98
+ loginLogo: auth.logo ?? auth.loginLogo,
99
+ loginLayout: auth.layout ?? auth.loginLayout,
100
+ showOAuth: auth.oauth?.enabled ?? auth.showOAuth,
101
+ oauthProviders: auth.oauth?.providers ?? auth.oauthProviders,
102
+ }
53
103
  }
54
104
 
55
- function resolveOrgAdapter(config: FayzAppConfig): OrgAdapter {
105
+ function resolveOrgAdapter(config: FayzAppConfig, authAdapter?: AuthAdapter): OrgAdapter {
56
106
  const org = config.org
57
107
  if (org?.adapter && typeof org.adapter === 'object') return org.adapter as OrgAdapter
58
108
 
59
109
  const strategy = org?.adapter ?? (config.supabaseUrl ? 'supabase' : 'mock')
60
- if (strategy === 'supabase') return createSupabaseOrgAdapter()
110
+ // The app declares its own business roles via permissions.defaultProfiles; pass
111
+ // their identity (id/name/description) to both adapters so the role SET is
112
+ // app-owned, not baked into the SDK.
113
+ const declaredRoles = config.permissions?.defaultProfiles?.map((p) => ({
114
+ id: p.id,
115
+ name: p.name,
116
+ description: p.description,
117
+ }))
118
+ // Inject the resolved auth adapter so the org adapter can DELIVER invites (send
119
+ // the e-mail + create the auth user) — org owns the audit row + role, auth owns delivery.
120
+ if (strategy === 'supabase') return createSupabaseOrgAdapter({ roles: declaredRoles, authAdapter })
61
121
  return createMockOrgAdapter(config.permissions?.defaultProfiles)
62
122
  }
63
123
 
@@ -116,12 +176,9 @@ function ThemeInitializer({ config }: { config: FayzAppConfig }) {
116
176
 
117
177
  React.useEffect(() => {
118
178
  if (config.theme) {
119
- const isSaasTheme = 'brand' in config.theme && (
120
- 'radius' in config.theme || 'sidebar' in config.theme || 'font' in config.theme
121
- )
122
- const resolved = isSaasTheme
123
- ? resolveTheme(config.theme as SaasTheme)
124
- : config.theme as CreateThemeOptions
179
+ const resolved = isSaasTheme(config.theme)
180
+ ? resolveTheme(config.theme)
181
+ : config.theme
125
182
  setOverrides(resolved)
126
183
  }
127
184
  if (config.defaultThemeMode && typeof window !== 'undefined' && !localStorage.getItem('saas-core:theme-mode')) {
@@ -134,7 +191,7 @@ function ThemeInitializer({ config }: { config: FayzAppConfig }) {
134
191
  }
135
192
 
136
193
  // ---------------------------------------------------------------------------
137
- // AdminProviders — the provider stack shared by the createFayzApp/createSaasApp
194
+ // AdminProviders — the provider stack shared by the admin factory/createSaasApp
138
195
  // sugar path AND the manifest-driven AdminScaffold. Resolves adapters + i18n
139
196
  // once (stable refs) and recomputes the plugin runtime reactively as the path
140
197
  // changes, so route-scoped widget visibility stays correct.
@@ -152,16 +209,20 @@ export function AdminProviders({ config, children }: { config: FayzAppConfig; ch
152
209
  createFayzSupabaseClient(config.supabaseUrl, config.supabaseAnonKey)
153
210
  }
154
211
  const i18n = buildI18nConfig(config)
155
- setCurrentLocale(i18n.defaultLocale)
212
+ const authRuntime = resolveAuthRuntime(config)
156
213
  return {
157
- authAdapter: resolveAuthAdapter(config),
158
- orgAdapter: resolveOrgAdapter(config),
214
+ authRuntime,
215
+ orgAdapter: resolveOrgAdapter(config, authRuntime.adapter),
159
216
  i18n,
160
217
  plugins: config.plugins ?? [],
161
218
  }
162
219
  // eslint-disable-next-line react-hooks/exhaustive-deps
163
220
  }, [config])
164
221
 
222
+ React.useEffect(() => {
223
+ setCurrentLocale(resolved.i18n.defaultLocale)
224
+ }, [resolved.i18n.defaultLocale])
225
+
165
226
  // Recompute the plugin runtime when the hash path or tenant changes
166
227
  // (route-scoped visibility + tenant-scoped plugin data providers).
167
228
  const path = useAdminPath()
@@ -188,7 +249,7 @@ export function AdminProviders({ config, children }: { config: FayzAppConfig; ch
188
249
  )
189
250
 
190
251
  return (
191
- <AuthProvider adapter={resolved.authAdapter}>
252
+ <AuthProvider adapter={resolved.authRuntime.adapter}>
192
253
  <OrgProvider adapter={resolved.orgAdapter} autoCreate={config.org?.autoCreate ?? true}>
193
254
  <PluginRuntimeProvider value={runtime}>
194
255
  <PermissionsProvider config={config.permissions}>
@@ -224,6 +285,7 @@ AdminProviders.displayName = 'AdminProviders'
224
285
  // ---------------------------------------------------------------------------
225
286
 
226
287
  export function createFayzApp(config: FayzAppConfig): React.ComponentType {
288
+ const authShellProps = getAuthShellProps(config.auth)
227
289
  function FayzApp({ children }: { children?: React.ReactNode }) {
228
290
  return (
229
291
  <AdminProviders config={config}>
@@ -234,16 +296,12 @@ export function createFayzApp(config: FayzAppConfig): React.ComponentType {
234
296
  layout={config.layout}
235
297
  contentFrame={config.contentFrame}
236
298
  moduleNav={config.moduleNav}
299
+ bottomNav={config.bottomNav}
237
300
  pages={config.pages}
238
- requireAuth={config.auth?.requireAuth}
239
- loginTagline={config.auth?.loginTagline}
240
- loginDescription={config.auth?.loginDescription}
241
- loginLogo={config.auth?.loginLogo}
242
- loginLayout={config.auth?.loginLayout}
243
- showOAuth={config.auth?.showOAuth}
244
- oauthProviders={config.auth?.oauthProviders}
301
+ {...authShellProps}
245
302
  showSettings
246
- showOrgSettings={Boolean(config.org)}
303
+ showOrgSettings={config.orgSettings ?? Boolean(config.org)}
304
+ showBranding={config.branding ?? true}
247
305
  />
248
306
  )}
249
307
  </AdminProviders>
package/src/app/config.ts CHANGED
@@ -8,6 +8,8 @@ import type {
8
8
  LocaleConfig,
9
9
  PermissionsConfig,
10
10
  } from '@fayz-ai/core'
11
+ import type { AuthPluginOptions, ResolvedAuthPlugin } from '@fayz-ai/plugin-auth'
12
+ import type { BottomNavItem, MobileHeaderVariant } from '@fayz-ai/ui'
11
13
  import type { SaasTheme } from '../shell/config/theme/tokens'
12
14
  import type { CreateThemeOptions } from '../shell/config/theme/utils'
13
15
  import type { PlanConfig } from '../shell/types/billing'
@@ -30,6 +32,11 @@ export interface CustomPage {
30
32
  position?: number
31
33
  badge?: string | number
32
34
  permission?: { feature: string; action: 'read' | 'create' | 'edit' | 'delete' }
35
+ /** Show this page in the sidebar/topbar navigation. Default: true. Set `false`
36
+ * for mobile-only pages reachable via bottomNav/avatar (e.g. a "Mais" overflow
37
+ * hub or a "Perfil" avatar target): the route still works, but the entry is
38
+ * hidden from desktop navigation where those mobile constructs are meaningless. */
39
+ nav?: boolean
33
40
  children?: CustomPage[]
34
41
  }
35
42
 
@@ -39,9 +46,19 @@ export interface CustomPage {
39
46
 
40
47
  export interface AuthConfig {
41
48
  /** 'supabase' | 'mock' or bring your own AuthAdapter */
42
- adapter?: 'supabase' | 'mock' | AuthAdapter
49
+ adapter?: AuthPluginOptions['adapter']
50
+ /** New plugin-auth provider selector. Existing apps may keep using adapter. */
51
+ provider?: AuthPluginOptions['provider']
43
52
  /** Redirect to login when no session is found (default: true) */
44
53
  requireAuth?: boolean
54
+ /** New plugin-auth route and provider options. */
55
+ routes?: AuthPluginOptions['routes']
56
+ supabase?: AuthPluginOptions['supabase']
57
+ oauth?: AuthPluginOptions['oauth']
58
+ layout?: AuthPluginOptions['layout']
59
+ logo?: ReactNode
60
+ tagline?: string
61
+ description?: string
45
62
  loginLogo?: ReactNode
46
63
  loginLayout?: 'split' | 'centered'
47
64
  loginTagline?: string
@@ -50,6 +67,8 @@ export interface AuthConfig {
50
67
  oauthProviders?: Exclude<AuthProvider, 'email'>[]
51
68
  }
52
69
 
70
+ export type AppAuthConfig = AuthConfig | ResolvedAuthPlugin
71
+
53
72
  export interface OrgConfig {
54
73
  /** 'supabase' | 'mock' or bring your own OrgAdapter */
55
74
  adapter?: 'supabase' | 'mock' | OrgAdapter
@@ -90,7 +109,7 @@ export interface FayzAppConfig {
90
109
  // -------------------------------------------------------------------------
91
110
  // Auth
92
111
  // -------------------------------------------------------------------------
93
- auth?: AuthConfig
112
+ auth?: AppAuthConfig
94
113
 
95
114
  // -------------------------------------------------------------------------
96
115
  // Organization / multi-tenancy
@@ -114,6 +133,17 @@ export interface FayzAppConfig {
114
133
  defaultThemeMode?: ThemeMode
115
134
  /** Shell layout variant (default: 'sidebar') */
116
135
  layout?: 'sidebar' | 'topbar' | 'minimal'
136
+ /** Mobile bottom tab bar. Rendered by AppShell on small screens (md:hidden)
137
+ * for every layout variant. Route items map a lucide icon name to a route; an
138
+ * `{ kind: 'action', id, icon }` item renders as a raised center button
139
+ * (Mobills style) that fires `onBottomNavAction(id)` instead of navigating. */
140
+ bottomNav?: BottomNavItem[]
141
+ /** Fired when a bottom-nav `action` item (e.g. the center "+") is tapped. */
142
+ onBottomNavAction?: (id: string) => void
143
+ /** Mobile header treatment (<md), default 'minimal'. 'transparent' renders no
144
+ * header bar (edge-to-edge content) with a floating profile avatar top-right;
145
+ * 'hidden' renders neither. Desktop keeps its sidebar + user menu. */
146
+ mobileHeader?: MobileHeaderVariant
117
147
  /** Wrap the main content in an inset "framed" card (default: true). The
118
148
  * sidebar itself is always flush/full-height. */
119
149
  contentFrame?: boolean
@@ -129,6 +159,18 @@ export interface FayzAppConfig {
129
159
  // -------------------------------------------------------------------------
130
160
  locale?: LocaleConfig
131
161
 
162
+ // -------------------------------------------------------------------------
163
+ // Settings surface
164
+ // -------------------------------------------------------------------------
165
+ /** Show org-level settings tabs (Equipe/Permissões/Localizações/Regras).
166
+ * Defaults to `Boolean(org)` for back-compat. B2C/personal apps that still
167
+ * need an org for the workspace can set `false` to hide these ERP tabs. */
168
+ orgSettings?: boolean
169
+ /** Show the branding ("Identidade Visual") + company "Geral" settings tabs.
170
+ * Default: `true`. B2C apps set `false` to drop org-identity settings, leaving
171
+ * Perfil + Segurança + plugin settings only. */
172
+ branding?: boolean
173
+
132
174
  // -------------------------------------------------------------------------
133
175
  // Permissions
134
176
  // -------------------------------------------------------------------------
@@ -12,10 +12,9 @@ import type {
12
12
  PermissionsConfig,
13
13
  BillingConfig as ManifestBillingConfig,
14
14
  } from '@fayz-ai/core'
15
- import { AdminProviders } from './createFayzApp'
15
+ import { AdminProviders, getAuthShellProps } from './admin-app'
16
16
  import { AdminShell } from './AdminShell'
17
17
  import type { FayzAppConfig, CustomPage } from './config'
18
- import { createSaasApp, type SaasAppConfig } from '../shell/createSaasApp'
19
18
 
20
19
  // ---------------------------------------------------------------------------
21
20
  // Admin scaffold — renders an admin app from a pure-data AppManifest. Mirrors
@@ -32,13 +31,9 @@ function slug(name: string): string {
32
31
  }
33
32
 
34
33
  const liveConfigOption = '__fayzLiveConfigRef'
35
- const liveSaasConfigs = new Map<string, FayzAppConfig | SaasAppConfig>()
34
+ const liveSaasConfigs = new Map<string, FayzAppConfig>()
36
35
 
37
- function hasLegacyShellConfig(config: FayzAppConfig | SaasAppConfig): config is SaasAppConfig {
38
- return 'organization' in config || 'settingsTabs' in config || 'bottomNav' in config || 'pluginRuntime' in config
39
- }
40
-
41
- function getAuthRequireAuth(config: FayzAppConfig | SaasAppConfig): boolean {
36
+ function getAuthRequireAuth(config: FayzAppConfig): boolean {
42
37
  return config.auth?.requireAuth ?? true
43
38
  }
44
39
 
@@ -50,7 +45,7 @@ function getLiveConfigRef(id: string): string {
50
45
  * migration, config-authored apps keep a live config ref so renderApp can
51
46
  * preserve code-backed plugins and custom pages while the JSON manifest path
52
47
  * matures. Serialized manifests still resolve through plugin/page registries. */
53
- export function defineSaas(config: FayzAppConfig | SaasAppConfig): AppManifest {
48
+ export function defineSaas(config: FayzAppConfig): AppManifest {
54
49
  const id = slug(config.name)
55
50
  const configRef = getLiveConfigRef(id)
56
51
  liveSaasConfigs.set(configRef, config)
@@ -121,7 +116,7 @@ function manifestToFayzConfig(manifest: AppManifest, surfaceName: string): FayzA
121
116
  const options = (manifest.surfaces[surfaceName]?.options ?? {}) as Record<string, unknown>
122
117
  const liveConfigRef = options[liveConfigOption]
123
118
  const liveConfig = typeof liveConfigRef === 'string' ? liveSaasConfigs.get(liveConfigRef) : undefined
124
- if (liveConfig && !hasLegacyShellConfig(liveConfig)) return liveConfig
119
+ if (liveConfig) return liveConfig
125
120
 
126
121
  return {
127
122
  name: manifest.name,
@@ -138,15 +133,8 @@ function manifestToFayzConfig(manifest: AppManifest, surfaceName: string): FayzA
138
133
  }
139
134
 
140
135
  export function AdminScaffold({ manifest, surface }: { manifest: AppManifest; surface: string }) {
141
- const liveConfigRef = manifest.surfaces[surface]?.options?.[liveConfigOption]
142
- const liveConfig = typeof liveConfigRef === 'string' ? liveSaasConfigs.get(liveConfigRef) : undefined
143
- const LegacyApp = React.useMemo(() => {
144
- if (!liveConfig || !hasLegacyShellConfig(liveConfig)) return null
145
- return createSaasApp(liveConfig)
146
- }, [liveConfig])
147
136
  const config = React.useMemo(() => manifestToFayzConfig(manifest, surface), [manifest, surface])
148
-
149
- if (LegacyApp) return <LegacyApp />
137
+ const authShellProps = React.useMemo(() => getAuthShellProps(config.auth), [config.auth])
150
138
 
151
139
  return (
152
140
  <AdminProviders config={config}>
@@ -154,16 +142,15 @@ export function AdminScaffold({ manifest, surface }: { manifest: AppManifest; su
154
142
  appName={config.name}
155
143
  logo={config.logo}
156
144
  layout={config.layout}
145
+ bottomNav={config.bottomNav}
146
+ onBottomNavAction={config.onBottomNavAction}
147
+ mobileHeader={config.mobileHeader}
157
148
  pages={config.pages}
158
- requireAuth={config.auth?.requireAuth}
159
- loginTagline={config.auth?.loginTagline}
160
- loginDescription={config.auth?.loginDescription}
161
- loginLogo={config.auth?.loginLogo}
162
- loginLayout={config.auth?.loginLayout}
163
- showOAuth={config.auth?.showOAuth}
164
- oauthProviders={config.auth?.oauthProviders}
149
+ {...authShellProps}
165
150
  showSettings
166
- showOrgSettings={Boolean(config.org)}
151
+ showOrgSettings={config.orgSettings ?? Boolean(config.org)}
152
+ showOrgSwitcher={config.org?.multiOrg !== false}
153
+ showBranding={config.branding ?? true}
167
154
  />
168
155
  </AdminProviders>
169
156
  )
@@ -1,5 +1,5 @@
1
- import React, { useCallback } from 'react'
2
- import { Edit, Trash2, Eye, Shield, CalendarDays, FileText, Clock, Package, Users, DollarSign, MapPin, BarChart3, Briefcase, ShoppingBag } from 'lucide-react'
1
+ import React, { useCallback, useEffect, useState } from 'react'
2
+ import { Edit, Trash2, Eye, Shield, CalendarDays, FileText, Clock, Package, Users, DollarSign, MapPin, BarChart3, Briefcase, ShoppingBag, FolderOpen, MessageSquare, Route } from 'lucide-react'
3
3
  import type { LucideIcon } from 'lucide-react'
4
4
  import { Card, CardContent } from '@fayz-ai/ui'
5
5
  import { Button } from '@fayz-ai/ui'
@@ -223,8 +223,35 @@ function getArchetypeTabs(layout?: FormLayout, archetypeKind?: string): DetailTa
223
223
  return tabs
224
224
  }
225
225
 
226
+ function mergeDetailTabs(archetypeTabs: DetailTab[], entityTabs: DetailTab[]): DetailTab[] {
227
+ const merged = new Map<string, DetailTab>()
228
+
229
+ for (const tab of entityTabs) merged.set(tab.id, tab)
230
+ for (const tab of archetypeTabs) {
231
+ if (!merged.has(tab.id)) merged.set(tab.id, tab)
232
+ }
233
+
234
+ return Array.from(merged.values())
235
+ }
236
+
237
+ function normalizeTabId(value?: string): string | undefined {
238
+ return value?.split('?')[0]
239
+ }
240
+
241
+ function resolveActiveTab(initialTab: string | undefined, tabs: DetailTab[]): string {
242
+ const normalized = normalizeTabId(initialTab)
243
+ if (!normalized) return 'overview'
244
+ if (normalized === 'overview') return 'overview'
245
+
246
+ const direct = tabs.find((tab) => tab.id === normalized)
247
+ if (direct) return direct.id
248
+
249
+ const alias = tabs.find((tab) => (tab as DetailTab & { aliases?: string[] }).aliases?.includes(normalized))
250
+ return alias?.id ?? 'overview'
251
+ }
252
+
226
253
  const TAB_ICONS: Record<string, LucideIcon> = {
227
- Shield, CalendarDays, FileText, Clock, Package, Users, DollarSign, MapPin, BarChart3, Briefcase, ShoppingBag,
254
+ Shield, CalendarDays, FileText, Clock, Package, Users, DollarSign, MapPin, BarChart3, Briefcase, ShoppingBag, FolderOpen, MessageSquare, Route,
228
255
  }
229
256
 
230
257
  export function CrudDetailPage({
@@ -246,21 +273,28 @@ export function CrudDetailPage({
246
273
  const imageValue = entityDef.imageField ? item[entityDef.imageField] : null
247
274
  const initial = typeof displayValue === 'string' ? displayValue.charAt(0).toUpperCase() : '?'
248
275
 
249
- // Merge archetype tabs + custom entity tabs
276
+ // Merge archetype tabs + custom entity tabs. Entity-specific tabs win by id
277
+ // so vertical apps can replace generic person/product tabs without duplicates.
250
278
  const runtime = usePluginRuntimeOptional()
251
279
  const archetypeTabs = getArchetypeTabs(entityDef.layout, entityDef.data?.archetypeKind)
252
- const customTabs = [...archetypeTabs, ...(entityDef.detailTabs ?? [])]
280
+ const customTabs = mergeDetailTabs(archetypeTabs, entityDef.detailTabs ?? [])
253
281
  // Drop tabs that require a plugin-contributed widget when none is registered
254
282
  // (e.g. the financial statement tab only appears when the financial plugin is active).
255
283
  .filter((tab) => !tab.requiresWidgetZone || (runtime ? getWidgetsForZone(runtime, tab.requiresWidgetZone).length > 0 : false))
284
+ .filter((tab) => !(tab as DetailTab & { hidden?: boolean }).hidden)
256
285
  const entityId = entityDef.name.toLowerCase().replace(/\s+/g, '-')
257
286
  const widgetZone = `${entityId}.detail.tabs`
258
287
 
259
- // Resolve initial tab — validate it exists, fallback to "overview"
260
- const validTabs = ['overview', ...customTabs.map((t) => t.id)]
261
- const activeTab = initialTab && validTabs.includes(initialTab) ? initialTab : 'overview'
288
+ // Resolve initial tab — allow compatibility aliases, fallback to "overview".
289
+ const activeTab = resolveActiveTab(initialTab, customTabs)
290
+ const [selectedTab, setSelectedTab] = useState(activeTab)
291
+
292
+ useEffect(() => {
293
+ setSelectedTab(activeTab)
294
+ }, [activeTab])
262
295
 
263
296
  const handleTabChange = useCallback((value: string) => {
297
+ setSelectedTab(value)
264
298
  if (embedded) return
265
299
  const detailPath = `${basePath}/${item.id}`
266
300
  const newHash = value === 'overview' ? detailPath : `${detailPath}/${value}`
@@ -326,7 +360,7 @@ export function CrudDetailPage({
326
360
  {!embedded && <Separator />}
327
361
 
328
362
  {/* Tabs */}
329
- <Tabs defaultValue={activeTab} onValueChange={handleTabChange}>
363
+ <Tabs value={selectedTab} onValueChange={handleTabChange}>
330
364
  <TabsList className={`max-w-full justify-start overflow-x-auto scrollbar-none${embedded ? ' h-8' : ''}`}>
331
365
  <TabsTrigger value="overview" className={`gap-1.5 ${embedded ? 'text-xs px-2 py-1' : ''}`}>
332
366
  <Eye className={embedded ? 'h-3 w-3' : 'h-3.5 w-3.5'} />
@@ -349,7 +349,7 @@ export function CrudPage<T extends { id: string }>({ entityDef: rawEntityDef, us
349
349
  // Parse /uuid or /uuid/tab-name
350
350
  const subParts = sub.slice(1).split('/')
351
351
  const id = subParts[0]
352
- const initialTab = subParts[1] || undefined
352
+ const initialTab = subParts[1]?.split('?')[0] || undefined
353
353
  const item = store.getById(id)
354
354
  viewKey = `detail-${id}`
355
355
 
@@ -44,13 +44,19 @@ export function useModuleNavigation(hashBase: string, depthMap?: DepthMap, homeV
44
44
  // '/inventory/dashboard' → treat as home view
45
45
  if (segments.length === 1 && segments[0] === homeView) return homeView
46
46
  // Detect action + ID patterns: '/products/edit/uuid' or '/stock/detail/uuid'
47
- const ACTION_KEYWORDS = ['edit', 'detail', 'view']
47
+ const ACTION_KEYWORDS = ['edit', 'detail', 'view', 'post']
48
48
  if (segments.length >= 3) {
49
49
  const maybeAction = segments[segments.length - 2]
50
50
  if (ACTION_KEYWORDS.includes(maybeAction)) {
51
51
  return segments.slice(0, -2).join('-') + '-' + maybeAction + ':' + segments[segments.length - 1]
52
52
  }
53
53
  }
54
+ // Generic fallback: a trailing UUID is always an ID for the view formed
55
+ // by the preceding segments ('/content/post/<uuid>' → 'content-post:<uuid>')
56
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
57
+ if (segments.length >= 2 && UUID_RE.test(segments[segments.length - 1])) {
58
+ return segments.slice(0, -1).join('-') + ':' + segments[segments.length - 1]
59
+ }
54
60
  return segments.join('-')
55
61
  }
56
62
  return homeView
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // App factory (main entry point)
3
3
  // ---------------------------------------------------------------------------
4
- export { createFayzApp, AdminProviders } from './app/createFayzApp'
4
+ export { createFayzApp, AdminProviders } from './app/admin-app'
5
5
  export type { FayzAppConfig, AuthConfig, OrgConfig, ChatConfig, CustomPage, PageSection } from './app/config'
6
6
 
7
7
  // Native admin scaffold — manifest-first entry. Importing this registers the
@@ -11,6 +11,11 @@ export { AdminShell } from './app/AdminShell'
11
11
  export { LoginPage } from './app/LoginPage'
12
12
  export { navigateTo as adminNavigateTo, useAdminPath } from './app/routing'
13
13
 
14
+ // Auth hook re-exported through the saas front-door so app screens (e.g. a
15
+ // Profile page) can read the current user + signOut without depending on
16
+ // @fayz-ai/auth directly.
17
+ export { useAuth } from '@fayz-ai/auth'
18
+
14
19
  // ---------------------------------------------------------------------------
15
20
  // CRUD engine — NATIVE code is now in ./crud (de-bridged from saas-core: the
16
21
  // full list/form/detail engine, archetype layouts, providers and store).
@@ -34,10 +39,6 @@ export { PermissionGate } from './permissions/PermissionGate'
34
39
  export { WidgetSlot } from './plugins/WidgetSlot'
35
40
  export { useFieldRules } from './hooks/useFieldRules'
36
41
 
37
- // Native admin shell — createSaasApp orchestrator (de-bridged from saas-core
38
- // into ./shell). The default createCrudPage is the native one.
39
- export { createSaasApp } from './shell/createSaasApp'
40
- export type { SaasAppConfig, PageConfig } from './shell/createSaasApp'
41
42
  export { createCrudPage } from './crud/createCrudPage'
42
43
  // Client-orders archetype (used by beauty-saas client detail tab)
43
44
  export { ClientOrdersTab } from './shell/components/crud/archetypes/ClientOrdersTab'
@@ -173,6 +174,12 @@ export { resolveEntityHref } from './lib/entity-routes'
173
174
  export { usePluginPrefs } from './hooks/usePluginPrefs'
174
175
  export { setScheduleBlockConfig, getScheduleBlockConfig, subscribeScheduleBlockConfig } from './lib/schedule-config'
175
176
 
177
+ // Assistant chat store — lets apps drive the shell's chat panel imperatively
178
+ // (e.g. an "Ask the assistant" CTA calling useChatStore().setOpen(true)). The
179
+ // store is a shared zustand singleton, so app + shell read the same instance.
180
+ export { useChatStore } from './shell/stores/chat.store'
181
+ export type { ChatMessage } from './shell/stores/chat.store'
182
+
176
183
  // Shell plugin-settings panel + dedup util (used by vertical plugins)
177
184
  export { PluginSettingsPanel } from './shell/components/plugins/PluginSettingsPanel'
178
185
  export { ConnectorsHub } from './shell/components/plugins/ConnectorsHub'