@petrarca/sonnet-shell 0.6.0 → 0.8.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.
- package/README.md +56 -211
- package/dist/auth/index.d.ts +54 -12
- package/dist/auth/index.js +136 -113
- package/dist/auth/index.js.map +1 -1
- package/dist/index.d.ts +27 -5
- package/dist/index.js +132 -51
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/Login.tsx","../../src/auth/OrgSelection.tsx","../../src/auth/ProtectedRoute.tsx"],"sourcesContent":["import { useState, useEffect, useCallback } from \"react\";\nimport { useNavigate, useLocation } from \"react-router-dom\";\nimport { Input } from \"@petrarca/sonnet-ui\";\nimport { Label } from \"@petrarca/sonnet-ui\";\nimport { Spinner } from \"@petrarca/sonnet-ui\";\nimport { OrgSelection, type OrgOption } from \"./OrgSelection\";\nimport { normalizeError } from \"@petrarca/sonnet-core\";\n\n/**\n * Login -- multi-step authentication flow.\n *\n * Steps:\n * identifier -- email/username entry (when onDiscover is provided)\n * auth -- resolved providers: password form first, SSO buttons below\n * credentials -- legacy single-screen (no onDiscover)\n * org-selection -- organization picker after successful auth\n *\n * When onDiscover is NOT provided, falls back to the legacy single-screen\n * behaviour. Backward compatible.\n *\n * All async operations are injected via props. The component knows nothing\n * about OIDC, PKCE, Sestina, or discover endpoints.\n *\n * Design: centered card on gray-100, white card with elevated shadow,\n * consistent with Auth0 Universal Login / WorkOS AuthKit visual language.\n *\n * Target package: @petrarca/sonnet-shell\n */\n\nexport interface AuthConfig {\n enabled: boolean;\n features: { local_auth: boolean; oauth: boolean };\n oauth?: {\n providers: Record<\n string,\n { enabled: boolean; name: string; login_url: string }\n >;\n } | null;\n ui: { direct_redirect: string | null };\n}\n\nexport interface LoginProvider {\n type: \"local\" | \"external\";\n name: string;\n label: string;\n}\n\ntype LoginStep = \"identifier\" | \"auth\" | \"credentials\" | \"org-selection\";\n\ninterface LocationState {\n from?: { pathname: string };\n step?: LoginStep;\n}\n\nexport interface LoginProps {\n config: AuthConfig | null;\n isAuthenticated: boolean;\n activeTenantId: string | null;\n isLoading: boolean;\n error: string | null;\n orgs: OrgOption[] | null;\n onLogin: (credentials: {\n identifier: string;\n password: string;\n }) => Promise<void>;\n onFetchCurrentUser: () => Promise<void>;\n onSelectOrg: (id: string) => Promise<void>;\n onClearError: () => void;\n /** Application name shown above the card as a brand label. */\n appName?: string;\n /** Logo URL shown above the card. If provided, appName is used as alt text. */\n logoUrl?: string;\n /** Enables identifier-first flow. App calls discover endpoint and returns providers. */\n onDiscover?: (identifier: string) => Promise<LoginProvider[]>;\n /** Called when user clicks an external provider button. App handles PKCE + redirect. */\n onExternalLogin?: (provider: LoginProvider) => Promise<void> | void;\n /** Whether discovery is in flight. */\n isDiscovering?: boolean;\n}\n\n// -- Flow state hook --\n\nfunction useLoginFlow(props: LoginProps) {\n const {\n config,\n isAuthenticated,\n activeTenantId,\n orgs,\n onLogin,\n onFetchCurrentUser,\n onSelectOrg,\n onClearError,\n onDiscover,\n onExternalLogin,\n } = props;\n\n const navigate = useNavigate();\n const location = useLocation();\n const locationState = location.state as LocationState | null;\n const identifierFirst = Boolean(onDiscover);\n const initial: LoginStep =\n locationState?.step ?? (identifierFirst ? \"identifier\" : \"credentials\");\n\n const [redirectTo] = useState(() => locationState?.from?.pathname ?? \"/\");\n const [step, setStep] = useState<LoginStep>(initial);\n const [identifier, setIdentifier] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [providers, setProviders] = useState<LoginProvider[]>([]);\n const [discoverError, setDiscoverError] = useState<string | null>(null);\n const [isRedirecting, setIsRedirecting] = useState(false);\n const [orgError, setOrgError] = useState<string | null>(null);\n\n useEffect(() => {\n if (isAuthenticated && activeTenantId)\n navigate(redirectTo, { replace: true });\n }, [isAuthenticated, activeTenantId, navigate, redirectTo]);\n\n useEffect(() => {\n if (!isAuthenticated || !orgs || activeTenantId) return;\n if (orgs.length === 1) {\n onSelectOrg(orgs[0].id).catch((err) => {\n setOrgError(\n normalizeError(err).message || \"Failed to select organization\",\n );\n setStep(\"org-selection\");\n });\n } else if (orgs.length > 1) {\n setStep(\"org-selection\");\n }\n }, [isAuthenticated, orgs, activeTenantId, onSelectOrg]);\n\n useEffect(() => {\n if (config?.ui.direct_redirect)\n window.location.href = config.ui.direct_redirect;\n }, [config]);\n\n const handleIdentifierSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n if (!onDiscover || !identifier.trim()) return;\n setDiscoverError(null);\n try {\n const resolved = await onDiscover(identifier.trim());\n setProviders(resolved);\n setStep(\"auth\");\n } catch (err) {\n setDiscoverError(\n normalizeError(err).message || \"Failed to look up providers\",\n );\n }\n },\n [identifier, onDiscover],\n );\n\n const handleCredentialSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n onClearError();\n try {\n await onLogin({ identifier, password });\n await onFetchCurrentUser();\n } catch {\n /* error via props */\n }\n },\n [identifier, password, onLogin, onFetchCurrentUser, onClearError],\n );\n\n const handleExternalLogin = useCallback(\n async (provider: LoginProvider) => {\n if (!onExternalLogin) return;\n setIsRedirecting(true);\n try {\n await onExternalLogin(provider);\n } catch {\n setIsRedirecting(false);\n }\n },\n [onExternalLogin],\n );\n\n const handleOrgSelect = useCallback(\n async (id: string) => {\n setOrgError(null);\n try {\n await onSelectOrg(id);\n } catch (err) {\n setOrgError(\n normalizeError(err).message || \"Failed to select organization\",\n );\n }\n },\n [onSelectOrg],\n );\n\n const goBack = useCallback(() => {\n setPassword(\"\");\n setDiscoverError(null);\n onClearError();\n setStep(identifierFirst ? \"identifier\" : \"credentials\");\n }, [identifierFirst, onClearError]);\n\n return {\n step,\n identifier,\n setIdentifier,\n password,\n setPassword,\n providers,\n discoverError,\n isRedirecting,\n orgError,\n handleIdentifierSubmit,\n handleCredentialSubmit,\n handleExternalLogin,\n handleOrgSelect,\n goBack,\n };\n}\n\n// -- Main component --\n\nexport function Login(props: LoginProps): React.ReactElement | null {\n const { config, isLoading, isDiscovering, error, orgs, appName, logoUrl } =\n props;\n const flow = useLoginFlow(props);\n\n if (config?.ui.direct_redirect || flow.isRedirecting)\n return <RedirectingMessage />;\n\n if (flow.step === \"org-selection\" && orgs) {\n return (\n <OrgSelection\n orgs={orgs}\n isLoading={isLoading}\n error={flow.orgError}\n onSelect={flow.handleOrgSelect}\n />\n );\n }\n\n return (\n <PageShell>\n <Card>\n {flow.step === \"identifier\" && (\n <IdentifierStep\n appName={appName}\n logoUrl={logoUrl}\n identifier={flow.identifier}\n error={flow.discoverError}\n isLoading={isDiscovering ?? false}\n onIdentifierChange={flow.setIdentifier}\n onSubmit={flow.handleIdentifierSubmit}\n />\n )}\n\n {flow.step === \"auth\" && (\n <AuthStep\n appName={appName}\n logoUrl={logoUrl}\n identifier={flow.identifier}\n providers={flow.providers}\n password={flow.password}\n error={error}\n isLoading={isLoading}\n onPasswordChange={flow.setPassword}\n onCredentialSubmit={flow.handleCredentialSubmit}\n onExternalLogin={flow.handleExternalLogin}\n onBack={flow.goBack}\n />\n )}\n\n {flow.step === \"credentials\" && (\n <CredentialsStep\n appName={appName}\n logoUrl={logoUrl}\n config={config}\n identifier={flow.identifier}\n password={flow.password}\n error={error}\n isLoading={isLoading}\n onIdentifierChange={flow.setIdentifier}\n onPasswordChange={flow.setPassword}\n onSubmit={flow.handleCredentialSubmit}\n onExternalLogin={flow.handleExternalLogin}\n />\n )}\n </Card>\n </PageShell>\n );\n}\n\n// -- Layout --\n\nfunction PageShell({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen bg-gray-100 px-4\">\n {children}\n </div>\n );\n}\n\nfunction Card({ children }: { children: React.ReactNode }) {\n return (\n <div\n className=\"w-full max-w-sm bg-white rounded-xl border border-gray-200 p-8\"\n style={{\n boxShadow:\n \"0 24px 48px rgba(15,23,42,0.10), 0 2px 4px rgba(15,23,42,0.06)\",\n }}\n >\n {children}\n </div>\n );\n}\n\n// -- Shared brand header (inside the card) --\n\nfunction BrandHeader({\n appName,\n logoUrl,\n}: {\n appName?: string;\n logoUrl?: string;\n}) {\n if (!appName && !logoUrl) return null;\n return (\n <div className=\"flex flex-col items-center mb-6\">\n {logoUrl ? (\n <img src={logoUrl} alt={appName ?? \"Logo\"} className=\"h-10 w-auto\" />\n ) : (\n <span className=\"text-lg font-semibold text-slate-500 tracking-wide\">\n {appName}\n </span>\n )}\n </div>\n );\n}\n\n// -- Identifier step --\n\ninterface IdentifierStepProps {\n appName?: string;\n logoUrl?: string;\n identifier: string;\n error: string | null;\n isLoading: boolean;\n onIdentifierChange: (value: string) => void;\n onSubmit: (e: React.FormEvent) => void;\n}\n\nfunction IdentifierStep({\n appName,\n logoUrl,\n identifier,\n error,\n isLoading,\n onIdentifierChange,\n onSubmit,\n}: IdentifierStepProps) {\n return (\n <form onSubmit={onSubmit} className=\"space-y-6\">\n <BrandHeader appName={appName} logoUrl={logoUrl} />\n <div className=\"text-center\">\n <h1 className=\"text-2xl font-semibold text-slate-900 leading-tight\">\n Welcome\n </h1>\n <p className=\"mt-2 text-sm text-gray-500\">\n Enter your email or username to continue.\n </p>\n </div>\n\n {error ? <ErrorAlert message={error} /> : null}\n\n <div>\n <Label\n htmlFor=\"identifier\"\n className=\"block text-sm font-medium text-gray-700 mb-1.5\"\n >\n Email or username\n </Label>\n <Input\n id=\"identifier\"\n type=\"text\"\n value={identifier}\n onChange={(e) => onIdentifierChange(e.target.value)}\n required\n autoFocus\n autoComplete=\"email\"\n placeholder=\"you@company.com\"\n className=\"h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500\"\n />\n </div>\n\n <PrimaryButton\n type=\"submit\"\n disabled={isLoading || !identifier.trim()}\n loading={isLoading}\n loadingText=\"Looking up...\"\n >\n Continue\n </PrimaryButton>\n </form>\n );\n}\n\n// -- Auth step (identifier-first resolved providers) --\n\ninterface AuthStepProps {\n appName?: string;\n logoUrl?: string;\n identifier: string;\n providers: LoginProvider[];\n password: string;\n error: string | null;\n isLoading: boolean;\n onPasswordChange: (value: string) => void;\n onCredentialSubmit: (e: React.FormEvent) => void;\n onExternalLogin: (provider: LoginProvider) => void;\n onBack: () => void;\n}\n\nfunction AuthStep({\n appName,\n logoUrl,\n identifier,\n providers,\n password,\n error,\n isLoading,\n onPasswordChange,\n onCredentialSubmit,\n onExternalLogin,\n onBack,\n}: AuthStepProps) {\n const external = providers.filter((p) => p.type === \"external\");\n const hasLocal = providers.some((p) => p.type === \"local\");\n const hasBoth = external.length > 0 && hasLocal;\n\n return (\n <div className=\"space-y-6\">\n <BrandHeader appName={appName} logoUrl={logoUrl} />\n <div>\n <button\n type=\"button\"\n onClick={onBack}\n className=\"flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 transition-colors mb-4\"\n >\n <span aria-hidden>←</span> Back\n </button>\n <h1 className=\"text-2xl font-semibold text-slate-900 leading-tight\">\n Welcome back\n </h1>\n <p className=\"mt-1 text-sm text-gray-500\">{identifier}</p>\n </div>\n\n {error ? <ErrorAlert message={error} /> : null}\n\n {hasLocal ? (\n <form onSubmit={onCredentialSubmit} className=\"space-y-4\">\n <div>\n <Label\n htmlFor=\"password\"\n className=\"block text-sm font-medium text-gray-700 mb-1.5\"\n >\n Password\n </Label>\n <Input\n id=\"password\"\n type=\"password\"\n value={password}\n onChange={(e) => onPasswordChange(e.target.value)}\n required\n autoFocus\n autoComplete=\"current-password\"\n placeholder=\"Enter your password\"\n className=\"h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500\"\n />\n </div>\n <PrimaryButton\n type=\"submit\"\n disabled={isLoading}\n loading={isLoading}\n loadingText=\"Signing in...\"\n >\n Sign in\n </PrimaryButton>\n </form>\n ) : null}\n\n {hasBoth ? <Separator /> : null}\n\n {external.length > 0 ? (\n <div className=\"space-y-2\">\n {external.map((p) => (\n <SsoButton\n key={p.name}\n label={p.label}\n onClick={() => onExternalLogin(p)}\n />\n ))}\n </div>\n ) : null}\n\n {providers.length === 0 ? (\n <p className=\"text-sm text-gray-500 text-center py-2\">\n No login methods available.\n </p>\n ) : null}\n </div>\n );\n}\n\n// -- Legacy credentials step --\n\ninterface CredentialsStepProps {\n appName?: string;\n logoUrl?: string;\n config: AuthConfig | null;\n identifier: string;\n password: string;\n error: string | null;\n isLoading: boolean;\n onIdentifierChange: (value: string) => void;\n onPasswordChange: (value: string) => void;\n onSubmit: (e: React.FormEvent) => void;\n onExternalLogin: (provider: LoginProvider) => void;\n}\n\nfunction resolveOAuthProviders(config: AuthConfig | null) {\n return config?.features.oauth && config.oauth?.providers\n ? Object.entries(config.oauth.providers).filter(([, p]) => p.enabled)\n : [];\n}\n\nfunction CredentialsStep({\n appName,\n logoUrl,\n config,\n identifier,\n password,\n error,\n isLoading,\n onIdentifierChange,\n onPasswordChange,\n onSubmit,\n onExternalLogin,\n}: CredentialsStepProps) {\n const oauthProviders = resolveOAuthProviders(config);\n const hasBoth = config?.features.local_auth && oauthProviders.length > 0;\n\n return (\n <div className=\"space-y-6\">\n <BrandHeader appName={appName} logoUrl={logoUrl} />\n <h1 className=\"text-2xl font-semibold text-slate-900 leading-tight text-center\">\n Welcome\n </h1>\n\n {error ? <ErrorAlert message={error} /> : null}\n\n {config?.features.local_auth ? (\n <form onSubmit={onSubmit} className=\"space-y-4\">\n <div>\n <Label\n htmlFor=\"identifier\"\n className=\"block text-sm font-medium text-gray-700 mb-1.5\"\n >\n Email or username\n </Label>\n <Input\n id=\"identifier\"\n type=\"text\"\n value={identifier}\n onChange={(e) => onIdentifierChange(e.target.value)}\n required\n autoFocus\n autoComplete=\"email\"\n placeholder=\"you@company.com\"\n className=\"h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500\"\n />\n </div>\n <div>\n <Label\n htmlFor=\"password\"\n className=\"block text-sm font-medium text-gray-700 mb-1.5\"\n >\n Password\n </Label>\n <Input\n id=\"password\"\n type=\"password\"\n value={password}\n onChange={(e) => onPasswordChange(e.target.value)}\n required\n autoComplete=\"current-password\"\n placeholder=\"Enter your password\"\n className=\"h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500\"\n />\n </div>\n <PrimaryButton\n type=\"submit\"\n disabled={isLoading}\n loading={isLoading}\n loadingText=\"Signing in...\"\n >\n Sign in\n </PrimaryButton>\n </form>\n ) : null}\n\n {hasBoth ? <Separator /> : null}\n\n {oauthProviders.length > 0 ? (\n <div className=\"space-y-2\">\n {oauthProviders.map(([key, p]) => (\n <SsoButton\n key={key}\n label={`Continue with ${p.name}`}\n onClick={() =>\n onExternalLogin({ type: \"external\", name: key, label: p.name })\n }\n />\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n\n// -- Shared primitives --\n\nfunction RedirectingMessage() {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen bg-gray-100 gap-3 text-gray-500\">\n <Spinner size=\"lg\" />\n <p className=\"text-sm\">Redirecting...</p>\n </div>\n );\n}\n\nfunction ErrorAlert({ message }: { message: string }) {\n return (\n <div className=\"rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700\">\n {message}\n </div>\n );\n}\n\nfunction Separator() {\n return (\n <div className=\"relative\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-gray-200\" />\n </div>\n <div className=\"relative flex justify-center text-xs\">\n <span className=\"bg-white px-2 text-gray-400\">or continue with</span>\n </div>\n </div>\n );\n}\n\ninterface PrimaryButtonProps {\n type?: \"submit\" | \"button\";\n disabled?: boolean;\n loading?: boolean;\n loadingText?: string;\n children: React.ReactNode;\n onClick?: () => void;\n}\n\nfunction PrimaryButton({\n type = \"button\",\n disabled,\n loading,\n loadingText,\n children,\n onClick,\n}: PrimaryButtonProps) {\n return (\n <button\n type={type}\n disabled={disabled}\n onClick={onClick}\n className=\"w-full inline-flex items-center justify-center rounded-full bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-md transition duration-150 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed\"\n style={{\n boxShadow: disabled ? undefined : \"0 4px 14px rgba(79,70,229,0.30)\",\n }}\n >\n {loading ? <Spinner size=\"sm\" className=\"mr-2\" /> : null}\n {loading && loadingText ? loadingText : children}\n </button>\n );\n}\n\nfunction SsoButton({ label, onClick }: { label: string; onClick: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className=\"w-full flex items-center justify-center gap-2.5 h-10 px-4 rounded-lg border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-400 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2\"\n >\n {label}\n </button>\n );\n}\n","import { useState, useCallback } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { Button } from \"@petrarca/sonnet-ui\";\nimport { Alert } from \"@petrarca/sonnet-ui\";\n\n/**\n * OrgSelection -- shown after authentication when the user belongs to\n * multiple organizations and no default is set. The user selects which\n * organization context to act in before the app loads.\n *\n * \"Organization\" is used generically: in Sestina this is an org-unit; in\n * another app it could be a tenant, workspace, team, or company. The\n * consumer maps their domain type to OrgOption.\n *\n * Fully props-driven with no app-specific imports.\n * Target package: @petrarca/sonnet-shell\n */\n\n/** Minimal organization descriptor. Consumers map their domain type to this shape. */\nexport interface OrgOption {\n id: string;\n name: string;\n}\n\ninterface OrgSelectionProps {\n orgs: OrgOption[];\n isLoading: boolean;\n error: string | null;\n onSelect: (id: string) => void;\n}\n\nexport function OrgSelection({\n orgs,\n isLoading,\n error,\n onSelect,\n}: OrgSelectionProps): React.ReactElement {\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const handleContinue = useCallback(() => {\n if (selectedId) onSelect(selectedId);\n }, [selectedId, onSelect]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-slate-50 px-4\">\n <div className=\"w-full max-w-sm bg-white rounded-xl shadow-sm border border-slate-200 p-8\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-semibold text-slate-900 text-center\">\n Select organization\n </h1>\n <p className=\"text-center text-sm text-slate-500 mt-1\">\n Choose the organization you want to work in.\n </p>\n </div>\n\n {error ? (\n <Alert className=\"mb-4 border-red-200 bg-red-50 text-red-700 text-sm py-2.5 px-3\">\n {error}\n </Alert>\n ) : null}\n\n <div className=\"space-y-2 mb-6 max-h-64 overflow-y-auto\">\n {orgs.map((org) => (\n <button\n key={org.id}\n onClick={() => setSelectedId(org.id)}\n className={`w-full flex items-center justify-between px-4 py-3 rounded-lg border text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-slate-400 ${\n selectedId === org.id\n ? \"border-blue-500 bg-blue-50 text-blue-900\"\n : \"border-slate-200 hover:bg-slate-50 text-slate-700\"\n }`}\n >\n <span className=\"font-medium\">{org.name}</span>\n {selectedId === org.id ? (\n <Check className=\"h-4 w-4 text-blue-500 shrink-0\" />\n ) : null}\n </button>\n ))}\n </div>\n\n <Button\n onClick={handleContinue}\n disabled={!selectedId || isLoading}\n className=\"w-full h-10\"\n >\n {isLoading ? \"Loading...\" : \"Continue\"}\n </Button>\n </div>\n </div>\n );\n}\n","import { Navigate, useLocation } from \"react-router-dom\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { Spinner } from \"@petrarca/sonnet-ui\";\n\n/**\n * ProtectedRoute -- guards the app shell.\n *\n * On first mount, calls the injected `initializeAuth` callback so\n * the consumer can validate or restore a persisted session. While\n * initialization is in progress a loading spinner is shown.\n *\n * Once initialization is complete, guards based on:\n * - Auth disabled (`authEnabled` false): allow all access\n * - Not authenticated: redirect to /login with step=credentials\n * - Authenticated but no tenant: redirect to /login with step=tenant-selection\n *\n * All auth state is injected via props -- no direct store or context imports.\n * Target package: @petrarca/sonnet-shell\n */\n\nexport interface ProtectedRouteProps {\n children: ReactNode;\n /** Whether auth is globally enabled. When false, all access is allowed. */\n authEnabled: boolean;\n /** Whether the auth config is still loading. */\n authConfigLoading: boolean;\n /** Whether the user holds a valid session. */\n isAuthenticated: boolean;\n /** The current access token (null when not authenticated). */\n accessToken: string | null;\n /** The active tenant id (null when not yet selected). */\n activeTenantId: string | null;\n /** Called once on mount to restore and validate a persisted session. */\n initializeAuth: () => Promise<void>;\n}\n\nexport function ProtectedRoute({\n children,\n authEnabled,\n authConfigLoading,\n isAuthenticated,\n accessToken,\n activeTenantId,\n initializeAuth,\n}: ProtectedRouteProps): React.ReactElement {\n const location = useLocation();\n const [initializing, setInitializing] = useState(true);\n const initRef = useRef(initializeAuth);\n\n useEffect(() => {\n initRef.current().finally(() => setInitializing(false));\n }, []);\n\n if (authConfigLoading || initializing) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n // Auth disabled: allow all access\n if (!authEnabled) {\n return <>{children}</>;\n }\n\n // Not authenticated: go to credentials step\n if (!isAuthenticated || !accessToken) {\n return (\n <Navigate\n to=\"/login\"\n state={{ from: location, step: \"credentials\" }}\n replace\n />\n );\n }\n\n // Authenticated but no tenant: go to tenant selection step\n if (!activeTenantId) {\n return (\n <Navigate\n to=\"/login\"\n state={{ from: location, step: \"tenant-selection\" }}\n replace\n />\n );\n }\n\n return <>{children}</>;\n}\n"],"mappings":"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","names":["useState","useCallback","jsx","jsxs","useState","useCallback","useLocation","useEffect","useState","Spinner","jsx"]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/Login.tsx","../../src/auth/OrgSelection.tsx","../../src/auth/ProtectedRoute.tsx"],"sourcesContent":["import { useState, useEffect, useCallback } from \"react\";\nimport { useNavigate, useLocation } from \"react-router-dom\";\nimport { Alert } from \"@petrarca/sonnet-ui\";\nimport { Button } from \"@petrarca/sonnet-ui\";\nimport { Input } from \"@petrarca/sonnet-ui\";\nimport { Label } from \"@petrarca/sonnet-ui\";\nimport { Spinner } from \"@petrarca/sonnet-ui\";\nimport { OrgSelection, type OrgOption } from \"./OrgSelection\";\nimport { normalizeError } from \"@petrarca/sonnet-core\";\n\n/**\n * Login -- multi-step authentication flow.\n *\n * Steps:\n * identifier -- email/username entry (when onDiscover is provided)\n * auth -- resolved providers: password form first, SSO buttons below\n * credentials -- single-screen login (no onDiscover)\n * org-selection -- organization picker after successful auth\n *\n * When onDiscover is NOT provided, the identifier and password sit on one\n * screen instead of the identifier-first two-step flow.\n *\n * All async operations are injected via props. The component knows nothing\n * about OIDC, PKCE, or discover endpoints.\n *\n * Design: a centred card on a quiet page, in the spirit of Auth0 Universal\n * Login / WorkOS AuthKit. Every colour comes from a design token, so the screen\n * follows the consumer's theme -- the distinct composition is deliberate, the\n * palette is not a private one.\n *\n * Target package: @petrarca/sonnet-shell\n */\n\nexport interface AuthConfig {\n enabled: boolean;\n features: { local_auth: boolean; oauth: boolean };\n oauth?: {\n providers: Record<\n string,\n { enabled: boolean; name: string; login_url: string }\n >;\n } | null;\n ui: { direct_redirect: string | null };\n}\n\nexport interface LoginProvider {\n type: \"local\" | \"external\";\n name: string;\n label: string;\n /**\n * Opaque per-provider data the app's discover step attaches and its\n * `onExternalLogin` reads back -- e.g. an OIDC authorize URL and target id.\n * The shell never inspects it; it only carries it through. This keeps Login\n * domain-agnostic (it \"knows nothing about OIDC, PKCE, or discover\n * endpoints\") while sparing the consumer an `as any` cast on the provider.\n */\n metadata?: Record<string, unknown>;\n}\n\ntype LoginStep = \"identifier\" | \"auth\" | \"credentials\" | \"org-selection\";\n\ninterface LocationState {\n from?: { pathname: string };\n step?: LoginStep;\n}\n\nexport interface LoginProps {\n config: AuthConfig | null;\n isAuthenticated: boolean;\n activeTenantId: string | null;\n isLoading: boolean;\n error: string | null;\n orgs: OrgOption[] | null;\n onLogin: (credentials: {\n identifier: string;\n password: string;\n }) => Promise<void>;\n onFetchCurrentUser: () => Promise<void>;\n onSelectOrg: (id: string) => Promise<void>;\n onClearError: () => void;\n /** Application name shown above the card as a brand label. */\n appName?: string;\n /** Logo URL shown above the card. If provided, appName is used as alt text. */\n logoUrl?: string;\n /** Enables identifier-first flow. App calls discover endpoint and returns providers. */\n onDiscover?: (identifier: string) => Promise<LoginProvider[]>;\n /** Called when user clicks an external provider button. App handles PKCE + redirect. */\n onExternalLogin?: (provider: LoginProvider) => Promise<void> | void;\n /** Whether discovery is in flight. */\n isDiscovering?: boolean;\n /**\n * Rendered on the organization step when the user has no organizations to\n * select. The library cannot know why the list is empty, so the escape --\n * sign out, retry, contact support -- is the consumer's to supply.\n * See `OrgSelection`'s `emptyAction`.\n */\n orgEmptyAction?: React.ReactNode;\n /**\n * Whether the user must select an organization after authenticating. Default\n * true. Set false for apps with no organization/tenant concept: the org step\n * is never entered, a stale org-selection step from location state is\n * ignored, and an authenticated user proceeds into the app even when\n * `activeTenantId` is null. Pair with `ProtectedRoute`'s matching prop.\n */\n requiresOrgSelection?: boolean;\n}\n\n// -- Flow state hook --\n\nfunction useLoginFlow(props: LoginProps) {\n const {\n config,\n isAuthenticated,\n activeTenantId,\n orgs,\n onLogin,\n onFetchCurrentUser,\n onSelectOrg,\n onClearError,\n onDiscover,\n onExternalLogin,\n requiresOrgSelection = true,\n } = props;\n\n const navigate = useNavigate();\n const location = useLocation();\n const locationState = location.state as LocationState | null;\n const identifierFirst = Boolean(onDiscover);\n // A stale org-selection step from location state must not strand a no-org\n // app on a screen it never wants.\n const requestedStep =\n requiresOrgSelection && locationState?.step\n ? locationState.step\n : undefined;\n const initial: LoginStep =\n requestedStep ?? (identifierFirst ? \"identifier\" : \"credentials\");\n\n const [redirectTo] = useState(() => locationState?.from?.pathname ?? \"/\");\n const [step, setStep] = useState<LoginStep>(initial);\n const [identifier, setIdentifier] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [providers, setProviders] = useState<LoginProvider[]>([]);\n const [discoverError, setDiscoverError] = useState<string | null>(null);\n const [isRedirecting, setIsRedirecting] = useState(false);\n const [orgError, setOrgError] = useState<string | null>(null);\n\n // Enter the app once authenticated. With org selection required that means a\n // tenant is set; without it, being authenticated is enough (activeTenantId\n // may legitimately stay null for an app with no org concept).\n useEffect(() => {\n if (isAuthenticated && (activeTenantId || !requiresOrgSelection))\n navigate(redirectTo, { replace: true });\n }, [\n isAuthenticated,\n activeTenantId,\n requiresOrgSelection,\n navigate,\n redirectTo,\n ]);\n\n useEffect(() => {\n if (!requiresOrgSelection) return;\n if (!isAuthenticated || !orgs || activeTenantId) return;\n if (orgs.length === 1) {\n onSelectOrg(orgs[0].id).catch((err) => {\n setOrgError(\n normalizeError(err).message || \"Failed to select organization\",\n );\n setStep(\"org-selection\");\n });\n } else if (orgs.length > 1) {\n setStep(\"org-selection\");\n }\n }, [\n requiresOrgSelection,\n isAuthenticated,\n orgs,\n activeTenantId,\n onSelectOrg,\n ]);\n\n useEffect(() => {\n if (config?.ui.direct_redirect)\n window.location.href = config.ui.direct_redirect;\n }, [config]);\n\n const handleIdentifierSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n if (!onDiscover || !identifier.trim()) return;\n setDiscoverError(null);\n try {\n const resolved = await onDiscover(identifier.trim());\n setProviders(resolved);\n setStep(\"auth\");\n } catch (err) {\n setDiscoverError(\n normalizeError(err).message || \"Failed to look up providers\",\n );\n }\n },\n [identifier, onDiscover],\n );\n\n const handleCredentialSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n onClearError();\n try {\n await onLogin({ identifier, password });\n await onFetchCurrentUser();\n } catch {\n /* error via props */\n }\n },\n [identifier, password, onLogin, onFetchCurrentUser, onClearError],\n );\n\n const handleExternalLogin = useCallback(\n async (provider: LoginProvider) => {\n if (!onExternalLogin) return;\n setIsRedirecting(true);\n try {\n await onExternalLogin(provider);\n } catch {\n setIsRedirecting(false);\n }\n },\n [onExternalLogin],\n );\n\n const handleOrgSelect = useCallback(\n async (id: string) => {\n setOrgError(null);\n try {\n await onSelectOrg(id);\n } catch (err) {\n setOrgError(\n normalizeError(err).message || \"Failed to select organization\",\n );\n }\n },\n [onSelectOrg],\n );\n\n const goBack = useCallback(() => {\n setPassword(\"\");\n setDiscoverError(null);\n onClearError();\n setStep(identifierFirst ? \"identifier\" : \"credentials\");\n }, [identifierFirst, onClearError]);\n\n return {\n step,\n identifier,\n setIdentifier,\n password,\n setPassword,\n providers,\n discoverError,\n isRedirecting,\n orgError,\n handleIdentifierSubmit,\n handleCredentialSubmit,\n handleExternalLogin,\n handleOrgSelect,\n goBack,\n };\n}\n\n// -- Main component --\n\ntype Screen = \"redirecting\" | \"org-selection\" | \"form\";\n\n/**\n * Decide which screen to show, kept pure and separate so the branch logic is\n * testable and the component stays under the complexity limit.\n *\n * `org-selection` renders only once orgs have loaded (`orgs !== null`); an empty\n * array is a loaded, legitimately-empty list. An authenticated user with no\n * tenant sees the spinner rather than an empty card in every other case --\n * org selection not required, a non-org-selection step, or orgs still loading.\n */\nfunction resolveScreen(args: {\n isRedirecting: boolean;\n directRedirect: boolean;\n isAuthenticated: boolean;\n activeTenantId: string | null;\n requiresOrgSelection: boolean;\n step: LoginStep;\n orgsLoaded: boolean;\n}): Screen {\n if (args.directRedirect || args.isRedirecting) return \"redirecting\";\n\n const canShowOrgPicker =\n args.requiresOrgSelection &&\n args.step === \"org-selection\" &&\n args.orgsLoaded;\n\n if (args.isAuthenticated && !args.activeTenantId && !canShowOrgPicker)\n return \"redirecting\";\n\n return canShowOrgPicker ? \"org-selection\" : \"form\";\n}\n\nexport function Login(props: LoginProps): React.ReactElement | null {\n const {\n config,\n isAuthenticated,\n activeTenantId,\n isLoading,\n isDiscovering,\n error,\n orgs,\n appName,\n logoUrl,\n orgEmptyAction,\n requiresOrgSelection = true,\n } = props;\n const flow = useLoginFlow(props);\n\n const screen = resolveScreen({\n isRedirecting: flow.isRedirecting,\n directRedirect: Boolean(config?.ui.direct_redirect),\n isAuthenticated,\n activeTenantId,\n requiresOrgSelection,\n step: flow.step,\n orgsLoaded: orgs !== null,\n });\n\n if (screen === \"redirecting\") return <RedirectingMessage />;\n\n if (screen === \"org-selection\") {\n return (\n <OrgSelection\n orgs={orgs ?? []}\n isLoading={isLoading}\n error={flow.orgError}\n onSelect={flow.handleOrgSelect}\n emptyAction={orgEmptyAction}\n />\n );\n }\n\n return (\n <PageShell>\n <Card>\n {flow.step === \"identifier\" && (\n <IdentifierStep\n appName={appName}\n logoUrl={logoUrl}\n identifier={flow.identifier}\n error={flow.discoverError}\n isLoading={isDiscovering ?? false}\n onIdentifierChange={flow.setIdentifier}\n onSubmit={flow.handleIdentifierSubmit}\n />\n )}\n\n {flow.step === \"auth\" && (\n <AuthStep\n appName={appName}\n logoUrl={logoUrl}\n identifier={flow.identifier}\n providers={flow.providers}\n password={flow.password}\n error={error}\n isLoading={isLoading}\n onPasswordChange={flow.setPassword}\n onCredentialSubmit={flow.handleCredentialSubmit}\n onExternalLogin={flow.handleExternalLogin}\n onBack={flow.goBack}\n />\n )}\n\n {flow.step === \"credentials\" && (\n <CredentialsStep\n appName={appName}\n logoUrl={logoUrl}\n config={config}\n identifier={flow.identifier}\n password={flow.password}\n error={error}\n isLoading={isLoading}\n onIdentifierChange={flow.setIdentifier}\n onPasswordChange={flow.setPassword}\n onSubmit={flow.handleCredentialSubmit}\n onExternalLogin={flow.handleExternalLogin}\n />\n )}\n </Card>\n </PageShell>\n );\n}\n\n// -- Layout --\n\nfunction PageShell({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen bg-muted px-4\">\n {children}\n </div>\n );\n}\n\nfunction Card({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"w-full max-w-sm bg-card rounded-lg border border-border shadow-sm p-8\">\n {children}\n </div>\n );\n}\n\n// -- Shared brand header (inside the card) --\n\nfunction BrandHeader({\n appName,\n logoUrl,\n}: {\n appName?: string;\n logoUrl?: string;\n}) {\n if (!appName && !logoUrl) return null;\n return (\n <div className=\"flex flex-col items-center mb-6\">\n {logoUrl ? (\n <img src={logoUrl} alt={appName ?? \"Logo\"} className=\"h-10 w-auto\" />\n ) : (\n <span className=\"text-lg font-semibold text-muted-foreground tracking-wide\">\n {appName}\n </span>\n )}\n </div>\n );\n}\n\n// -- Identifier step --\n\ninterface IdentifierStepProps {\n appName?: string;\n logoUrl?: string;\n identifier: string;\n error: string | null;\n isLoading: boolean;\n onIdentifierChange: (value: string) => void;\n onSubmit: (e: React.FormEvent) => void;\n}\n\nfunction IdentifierStep({\n appName,\n logoUrl,\n identifier,\n error,\n isLoading,\n onIdentifierChange,\n onSubmit,\n}: IdentifierStepProps) {\n return (\n <form onSubmit={onSubmit} className=\"space-y-6\">\n <BrandHeader appName={appName} logoUrl={logoUrl} />\n <div className=\"text-center\">\n <h1 className=\"text-2xl font-semibold text-foreground leading-tight\">\n Welcome\n </h1>\n <p className=\"mt-2 text-sm text-muted-foreground\">\n Enter your email or username to continue.\n </p>\n </div>\n\n {error ? <ErrorAlert message={error} /> : null}\n\n <div>\n <Label htmlFor=\"identifier\" className=\"mb-1.5 block\">\n Email or username\n </Label>\n <Input\n id=\"identifier\"\n type=\"text\"\n value={identifier}\n onChange={(e) => onIdentifierChange(e.target.value)}\n required\n autoFocus\n autoComplete=\"email\"\n placeholder=\"you@company.com\"\n />\n </div>\n\n <PrimaryButton\n type=\"submit\"\n disabled={isLoading || !identifier.trim()}\n loading={isLoading}\n loadingText=\"Looking up...\"\n >\n Continue\n </PrimaryButton>\n </form>\n );\n}\n\n// -- Auth step (identifier-first resolved providers) --\n\ninterface AuthStepProps {\n appName?: string;\n logoUrl?: string;\n identifier: string;\n providers: LoginProvider[];\n password: string;\n error: string | null;\n isLoading: boolean;\n onPasswordChange: (value: string) => void;\n onCredentialSubmit: (e: React.FormEvent) => void;\n onExternalLogin: (provider: LoginProvider) => void;\n onBack: () => void;\n}\n\nfunction AuthStep({\n appName,\n logoUrl,\n identifier,\n providers,\n password,\n error,\n isLoading,\n onPasswordChange,\n onCredentialSubmit,\n onExternalLogin,\n onBack,\n}: AuthStepProps) {\n const external = providers.filter((p) => p.type === \"external\");\n const hasLocal = providers.some((p) => p.type === \"local\");\n const hasBoth = external.length > 0 && hasLocal;\n\n return (\n <div className=\"space-y-6\">\n <BrandHeader appName={appName} logoUrl={logoUrl} />\n <div>\n <button\n type=\"button\"\n onClick={onBack}\n className=\"flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors mb-4\"\n >\n <span aria-hidden>←</span> Back\n </button>\n <h1 className=\"text-2xl font-semibold text-foreground leading-tight\">\n Welcome back\n </h1>\n <p className=\"mt-1 text-sm text-muted-foreground\">{identifier}</p>\n </div>\n\n {error ? <ErrorAlert message={error} /> : null}\n\n {hasLocal ? (\n <form onSubmit={onCredentialSubmit} className=\"space-y-4\">\n <div>\n <Label htmlFor=\"password\" className=\"mb-1.5 block\">\n Password\n </Label>\n <Input\n id=\"password\"\n type=\"password\"\n value={password}\n onChange={(e) => onPasswordChange(e.target.value)}\n required\n autoFocus\n autoComplete=\"current-password\"\n placeholder=\"Enter your password\"\n />\n </div>\n <PrimaryButton\n type=\"submit\"\n disabled={isLoading}\n loading={isLoading}\n loadingText=\"Signing in...\"\n >\n Sign in\n </PrimaryButton>\n </form>\n ) : null}\n\n {hasBoth ? <Separator /> : null}\n\n {external.length > 0 ? (\n <div className=\"space-y-2\">\n {external.map((p) => (\n <SsoButton\n key={p.name}\n label={p.label}\n onClick={() => onExternalLogin(p)}\n />\n ))}\n </div>\n ) : null}\n\n {providers.length === 0 ? (\n <p className=\"text-sm text-muted-foreground text-center py-2\">\n No login methods available.\n </p>\n ) : null}\n </div>\n );\n}\n\n// -- Single-screen credentials step --\n\ninterface CredentialsStepProps {\n appName?: string;\n logoUrl?: string;\n config: AuthConfig | null;\n identifier: string;\n password: string;\n error: string | null;\n isLoading: boolean;\n onIdentifierChange: (value: string) => void;\n onPasswordChange: (value: string) => void;\n onSubmit: (e: React.FormEvent) => void;\n onExternalLogin: (provider: LoginProvider) => void;\n}\n\nfunction resolveOAuthProviders(config: AuthConfig | null) {\n return config?.features.oauth && config.oauth?.providers\n ? Object.entries(config.oauth.providers).filter(([, p]) => p.enabled)\n : [];\n}\n\nfunction CredentialsStep({\n appName,\n logoUrl,\n config,\n identifier,\n password,\n error,\n isLoading,\n onIdentifierChange,\n onPasswordChange,\n onSubmit,\n onExternalLogin,\n}: CredentialsStepProps) {\n const oauthProviders = resolveOAuthProviders(config);\n const hasBoth = config?.features.local_auth && oauthProviders.length > 0;\n\n return (\n <div className=\"space-y-6\">\n <BrandHeader appName={appName} logoUrl={logoUrl} />\n <h1 className=\"text-2xl font-semibold text-foreground leading-tight text-center\">\n Welcome\n </h1>\n\n {error ? <ErrorAlert message={error} /> : null}\n\n {config?.features.local_auth ? (\n <form onSubmit={onSubmit} className=\"space-y-4\">\n <div>\n <Label htmlFor=\"identifier\" className=\"mb-1.5 block\">\n Email or username\n </Label>\n <Input\n id=\"identifier\"\n type=\"text\"\n value={identifier}\n onChange={(e) => onIdentifierChange(e.target.value)}\n required\n autoFocus\n autoComplete=\"email\"\n placeholder=\"you@company.com\"\n />\n </div>\n <div>\n <Label htmlFor=\"password\" className=\"mb-1.5 block\">\n Password\n </Label>\n <Input\n id=\"password\"\n type=\"password\"\n value={password}\n onChange={(e) => onPasswordChange(e.target.value)}\n required\n autoComplete=\"current-password\"\n placeholder=\"Enter your password\"\n />\n </div>\n <PrimaryButton\n type=\"submit\"\n disabled={isLoading}\n loading={isLoading}\n loadingText=\"Signing in...\"\n >\n Sign in\n </PrimaryButton>\n </form>\n ) : null}\n\n {hasBoth ? <Separator /> : null}\n\n {oauthProviders.length > 0 ? (\n <div className=\"space-y-2\">\n {oauthProviders.map(([key, p]) => (\n <SsoButton\n key={key}\n label={`Continue with ${p.name}`}\n onClick={() =>\n onExternalLogin({ type: \"external\", name: key, label: p.name })\n }\n />\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n\n// -- Shared primitives --\n\nfunction RedirectingMessage() {\n return (\n <div className=\"flex flex-col items-center justify-center min-h-screen bg-muted gap-3 text-muted-foreground\">\n <Spinner size=\"lg\" />\n <p className=\"text-sm\">Redirecting...</p>\n </div>\n );\n}\n\nfunction ErrorAlert({ message }: { message: string }) {\n return (\n <Alert\n variant=\"destructive\"\n className=\"bg-destructive/10 px-3 py-2.5 text-sm\"\n >\n {message}\n </Alert>\n );\n}\n\nfunction Separator() {\n return (\n <div className=\"relative\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-border\" />\n </div>\n <div className=\"relative flex justify-center text-xs\">\n <span className=\"bg-card px-2 text-muted-foreground\">\n or continue with\n </span>\n </div>\n </div>\n );\n}\n\ninterface PrimaryButtonProps {\n type?: \"submit\" | \"button\";\n disabled?: boolean;\n loading?: boolean;\n loadingText?: string;\n children: React.ReactNode;\n onClick?: () => void;\n}\n\nfunction PrimaryButton({\n type = \"button\",\n disabled,\n loading,\n loadingText,\n children,\n onClick,\n}: PrimaryButtonProps) {\n return (\n <Button\n type={type}\n size=\"lg\"\n disabled={disabled}\n onClick={onClick}\n className=\"w-full\"\n >\n {/* Button supplies gap-2, so the spinner needs no margin of its own. */}\n {loading ? <Spinner size=\"sm\" /> : null}\n {loading && loadingText ? loadingText : children}\n </Button>\n );\n}\n\nfunction SsoButton({ label, onClick }: { label: string; onClick: () => void }) {\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"lg\"\n onClick={onClick}\n className=\"w-full\"\n >\n {label}\n </Button>\n );\n}\n","import { useState, useCallback } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { Button } from \"@petrarca/sonnet-ui\";\nimport { Alert } from \"@petrarca/sonnet-ui\";\n\n/**\n * OrgSelection -- shown after authentication when the user belongs to\n * multiple organizations and no default is set. The user selects which\n * organization context to act in before the app loads.\n *\n * \"Organization\" is used generically: an app might map it to an org-unit,\n * tenant, workspace, team, or company. The consumer maps its domain type to\n * OrgOption.\n *\n * Fully props-driven with no app-specific imports.\n * Target package: @petrarca/sonnet-shell\n */\n\n/** Minimal organization descriptor. Consumers map their domain type to this shape. */\nexport interface OrgOption {\n id: string;\n name: string;\n}\n\ninterface OrgSelectionProps {\n orgs: OrgOption[];\n isLoading: boolean;\n error: string | null;\n onSelect: (id: string) => void;\n /**\n * Rendered in place of the picker when `orgs` is empty. The library states\n * the fact (\"No organizations assigned\") but cannot know why the list is\n * empty -- pending provisioning, filtered memberships, or none at all -- so\n * the escape (sign out, retry, contact support) is the consumer's to supply.\n * Omitted, the message stands alone.\n */\n emptyAction?: React.ReactNode;\n}\n\nexport function OrgSelection({\n orgs,\n isLoading,\n error,\n onSelect,\n emptyAction,\n}: OrgSelectionProps): React.ReactElement {\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const isEmpty = orgs.length === 0;\n\n const handleContinue = useCallback(() => {\n if (selectedId) onSelect(selectedId);\n }, [selectedId, onSelect]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-muted px-4\">\n <div className=\"w-full max-w-sm bg-card rounded-lg shadow-sm border border-border p-8\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-semibold text-foreground text-center\">\n Select organization\n </h1>\n <p className=\"text-center text-sm text-muted-foreground mt-1\">\n {isEmpty\n ? \"No organizations assigned to your account.\"\n : \"Choose the organization you want to work in.\"}\n </p>\n </div>\n\n {error ? (\n <Alert\n variant=\"destructive\"\n className=\"mb-4 bg-destructive/10 text-sm py-2.5 px-3\"\n >\n {error}\n </Alert>\n ) : null}\n\n {isEmpty ? (\n emptyAction ? (\n <div className=\"mt-2\">{emptyAction}</div>\n ) : null\n ) : (\n <>\n <div className=\"space-y-2 mb-6 max-h-64 overflow-y-auto\">\n {orgs.map((org) => (\n <button\n key={org.id}\n onClick={() => setSelectedId(org.id)}\n className={`w-full flex items-center justify-between px-4 py-3 rounded-md border text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${\n selectedId === org.id\n ? \"border-primary bg-state-selected text-foreground\"\n : \"border-border hover:bg-state-hover text-foreground\"\n }`}\n >\n <span className=\"font-medium\">{org.name}</span>\n {selectedId === org.id ? (\n <Check className=\"h-4 w-4 text-primary shrink-0\" />\n ) : null}\n </button>\n ))}\n </div>\n\n <Button\n onClick={handleContinue}\n disabled={!selectedId || isLoading}\n className=\"w-full h-10\"\n >\n {isLoading ? \"Loading...\" : \"Continue\"}\n </Button>\n </>\n )}\n </div>\n </div>\n );\n}\n","import { Navigate, useLocation } from \"react-router-dom\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\nimport { Spinner } from \"@petrarca/sonnet-ui\";\n\n/**\n * ProtectedRoute -- guards the app shell.\n *\n * On first mount, calls the injected `initializeAuth` callback so\n * the consumer can validate or restore a persisted session. While\n * initialization is in progress a loading spinner is shown.\n *\n * Once initialization is complete, guards based on:\n * - Auth disabled (`authEnabled` false): allow all access\n * - Not authenticated: redirect to /login with step=credentials\n * - Authenticated but no tenant: redirect to /login with step=org-selection\n *\n * Apps with no organization/tenant concept set `requiresOrgSelection={false}`;\n * an authenticated user is then let through and `activeTenantId` is ignored.\n *\n * All auth state is injected via props -- no direct store or context imports.\n * Target package: @petrarca/sonnet-shell\n */\n\nexport interface ProtectedRouteProps {\n children: ReactNode;\n /** Whether auth is globally enabled. When false, all access is allowed. */\n authEnabled: boolean;\n /** Whether the auth config is still loading. */\n authConfigLoading: boolean;\n /** Whether the user holds a valid session. */\n isAuthenticated: boolean;\n /** The current access token (null when not authenticated). */\n accessToken: string | null;\n /** The active tenant id (null when not yet selected). */\n activeTenantId: string | null;\n /** Called once on mount to restore and validate a persisted session. */\n initializeAuth: () => Promise<void>;\n /**\n * Whether an organization must be selected before entering the app. Default\n * true. Set false for apps with no organization/tenant concept: an\n * authenticated user is admitted and `activeTenantId` is not consulted.\n */\n requiresOrgSelection?: boolean;\n}\n\nexport function ProtectedRoute({\n children,\n authEnabled,\n authConfigLoading,\n isAuthenticated,\n accessToken,\n activeTenantId,\n initializeAuth,\n requiresOrgSelection = true,\n}: ProtectedRouteProps): React.ReactElement {\n const location = useLocation();\n const [initializing, setInitializing] = useState(true);\n const initRef = useRef(initializeAuth);\n\n useEffect(() => {\n initRef.current().finally(() => setInitializing(false));\n }, []);\n\n if (authConfigLoading || initializing) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n // Auth disabled: allow all access\n if (!authEnabled) {\n return <>{children}</>;\n }\n\n // Not authenticated: go to credentials step\n if (!isAuthenticated || !accessToken) {\n return (\n <Navigate\n to=\"/login\"\n state={{ from: location, step: \"credentials\" }}\n replace\n />\n );\n }\n\n // Authenticated but no tenant: go to organization selection. Skipped when the\n // app has no organization concept.\n if (requiresOrgSelection && !activeTenantId) {\n return (\n <Navigate\n to=\"/login\"\n state={{ from: location, step: \"org-selection\" }}\n replace\n />\n );\n }\n\n return <>{children}</>;\n}\n"],"mappings":"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","names":["useState","useCallback","Alert","Button","jsx","jsxs","useState","useCallback","Alert","Button","useLocation","useEffect","useState","Spinner","Fragment","jsx"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -232,7 +232,7 @@ interface ShellModule {
|
|
|
232
232
|
* AND. Absent ⇒ the module is always available.
|
|
233
233
|
*
|
|
234
234
|
* The shell does not interpret these — the app's `resolveCapability` evaluates
|
|
235
|
-
* each one. See docs/design/module-capabilities.md.
|
|
235
|
+
* each one. See docs/design/module-capabilities.md in this package.
|
|
236
236
|
*/
|
|
237
237
|
requires?: string[];
|
|
238
238
|
}
|
|
@@ -248,7 +248,12 @@ interface CapabilityResult {
|
|
|
248
248
|
}
|
|
249
249
|
/** App-provided function that evaluates a single capability string. */
|
|
250
250
|
type CapabilityResolver = (capability: string) => CapabilityResult;
|
|
251
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* A shell surface that gates modules. `rail` also governs the sidebar layout --
|
|
253
|
+
* both are renderings of the same primary navigation and share one policy.
|
|
254
|
+
* `route` is planned but not yet wired: nothing passes it to `evaluateModule`
|
|
255
|
+
* today. See `DEFAULT_CAPABILITY_POLICY`.
|
|
256
|
+
*/
|
|
252
257
|
type Surface = "rail" | "command" | "route";
|
|
253
258
|
/** How an unavailable module renders on a surface. */
|
|
254
259
|
type Effect = "visible" | "disabled" | "hidden" | "redirect";
|
|
@@ -316,7 +321,7 @@ interface ShellConfig {
|
|
|
316
321
|
/**
|
|
317
322
|
* Evaluates a module capability (from `ShellModule.requires`) for gating the
|
|
318
323
|
* icon rail / command menu / routes. Absent ⇒ no gating (all modules
|
|
319
|
-
* available). See docs/design/module-capabilities.md.
|
|
324
|
+
* available). See docs/design/module-capabilities.md in this package.
|
|
320
325
|
*/
|
|
321
326
|
resolveCapability?: CapabilityResolver;
|
|
322
327
|
/**
|
|
@@ -501,8 +506,16 @@ interface SidebarItemProps {
|
|
|
501
506
|
expanded?: boolean;
|
|
502
507
|
onToggle?: () => void;
|
|
503
508
|
onClick?: () => void;
|
|
509
|
+
/**
|
|
510
|
+
* Render greyed and non-navigable (e.g. a capability requirement is unmet).
|
|
511
|
+
* Uses aria-disabled, not native disable, so the row stays focusable and its
|
|
512
|
+
* title explaining why remains discoverable -- matching RailIcon.
|
|
513
|
+
*/
|
|
514
|
+
disabled?: boolean;
|
|
515
|
+
/** Tooltip / title shown when disabled (e.g. why it is unavailable). */
|
|
516
|
+
title?: string;
|
|
504
517
|
}
|
|
505
|
-
declare function SidebarItem({ icon: Icon, label, path, badge, indent, expandable, expanded, onToggle, onClick, }: SidebarItemProps): React__default.ReactElement;
|
|
518
|
+
declare function SidebarItem({ icon: Icon, label, path, badge, indent, expandable, expanded, onToggle, onClick, disabled, title, }: SidebarItemProps): React__default.ReactElement;
|
|
506
519
|
|
|
507
520
|
/**
|
|
508
521
|
* ShellRail — icon-rail + sub-nav-panel layout, auto-wired from the
|
|
@@ -583,7 +596,16 @@ interface UserMenuProps {
|
|
|
583
596
|
}
|
|
584
597
|
declare function UserMenu({ user, onSignOut }: UserMenuProps): react_jsx_runtime.JSX.Element;
|
|
585
598
|
|
|
586
|
-
/**
|
|
599
|
+
/**
|
|
600
|
+
* Default (kind, surface) → effect matrix. App overrides are merged over this.
|
|
601
|
+
*
|
|
602
|
+
* The `route` column is planned, not yet wired: no caller passes the `"route"`
|
|
603
|
+
* surface to `evaluateModule` (only `"rail"` and `"command"` are evaluated
|
|
604
|
+
* today), so its `redirect` effects do not fire. The column is kept because
|
|
605
|
+
* route gating is a designed capability -- a gated module's routes still render
|
|
606
|
+
* if reached directly -- and removing it would be a type break for the
|
|
607
|
+
* capability we intend to build. See docs/design/module-capabilities.md.
|
|
608
|
+
*/
|
|
587
609
|
declare const DEFAULT_CAPABILITY_POLICY: Record<UnmetKind, Record<Surface, Effect>>;
|
|
588
610
|
/** Merge a sparse app override over the default matrix. */
|
|
589
611
|
declare function resolveCapabilityPolicy(override?: CapabilityPolicy): Record<UnmetKind, Record<Surface, Effect>>;
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
useRef as useRef2,
|
|
7
7
|
useMemo as useMemo5
|
|
8
8
|
} from "react";
|
|
9
|
-
import { useResizablePanel as useResizablePanel2 } from "@petrarca/sonnet-core/hooks";
|
|
9
|
+
import { useResizablePanel as useResizablePanel2, useMediaQuery } from "@petrarca/sonnet-core/hooks";
|
|
10
10
|
import { Outlet, useLocation as useLocation2, useNavigate as useNavigate2 } from "react-router-dom";
|
|
11
11
|
import { TooltipProvider } from "@petrarca/sonnet-ui";
|
|
12
12
|
import { ScrollArea as ScrollArea2 } from "@petrarca/sonnet-ui";
|
|
@@ -448,9 +448,10 @@ function GroupHeading({
|
|
|
448
448
|
type: "button",
|
|
449
449
|
onClick: canCollapse ? onToggle : void 0,
|
|
450
450
|
className: cn2(
|
|
451
|
-
"flex w-full items-center gap-1 px-2 py-1.5",
|
|
451
|
+
"flex w-full items-center gap-1 rounded px-2 py-1.5",
|
|
452
452
|
"text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/70",
|
|
453
|
-
canCollapse ? "hover:text-foreground cursor-pointer" : "cursor-default"
|
|
453
|
+
canCollapse ? "hover:text-foreground cursor-pointer" : "cursor-default",
|
|
454
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
454
455
|
),
|
|
455
456
|
children: [
|
|
456
457
|
canCollapse && /* @__PURE__ */ jsx5(
|
|
@@ -511,8 +512,9 @@ function ItemTrailing({
|
|
|
511
512
|
"button",
|
|
512
513
|
{
|
|
513
514
|
type: "button",
|
|
515
|
+
"aria-label": expanded ? "Collapse" : "Expand",
|
|
514
516
|
onClick: handleClick,
|
|
515
|
-
className: "p-0.5 text-muted-foreground hover:text-foreground",
|
|
517
|
+
className: "rounded p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
516
518
|
children: /* @__PURE__ */ jsx6(
|
|
517
519
|
ChevronRight,
|
|
518
520
|
{
|
|
@@ -527,6 +529,12 @@ function ItemTrailing({
|
|
|
527
529
|
}
|
|
528
530
|
return null;
|
|
529
531
|
}
|
|
532
|
+
function itemStateClass(disabled, isActive) {
|
|
533
|
+
if (disabled)
|
|
534
|
+
return "text-muted-foreground/40 cursor-default hover:bg-transparent";
|
|
535
|
+
if (isActive) return "bg-accent text-accent-foreground";
|
|
536
|
+
return "text-muted-foreground hover:text-foreground hover:bg-accent/50";
|
|
537
|
+
}
|
|
530
538
|
function SidebarItem({
|
|
531
539
|
icon: Icon,
|
|
532
540
|
label,
|
|
@@ -536,20 +544,28 @@ function SidebarItem({
|
|
|
536
544
|
expandable = false,
|
|
537
545
|
expanded = false,
|
|
538
546
|
onToggle,
|
|
539
|
-
onClick
|
|
547
|
+
onClick,
|
|
548
|
+
disabled = false,
|
|
549
|
+
title
|
|
540
550
|
}) {
|
|
541
551
|
const { pathname } = useLocation();
|
|
542
|
-
const isActive = pathname === path;
|
|
552
|
+
const isActive = !disabled && pathname === path;
|
|
543
553
|
const style = indent > 0 ? { paddingLeft: `${indent * 16 + 8}px` } : void 0;
|
|
554
|
+
const disabledProps = disabled ? {
|
|
555
|
+
"aria-disabled": true,
|
|
556
|
+
title,
|
|
557
|
+
onClick: (e) => e.preventDefault()
|
|
558
|
+
} : { onClick };
|
|
544
559
|
return /* @__PURE__ */ jsx6("li", { children: /* @__PURE__ */ jsxs4(
|
|
545
560
|
Link,
|
|
546
561
|
{
|
|
547
562
|
to: path,
|
|
548
|
-
|
|
563
|
+
...disabledProps,
|
|
549
564
|
style,
|
|
550
565
|
className: cn3(
|
|
551
566
|
"flex items-center gap-2 rounded px-2 py-1.5 text-[13px] font-medium transition-colors select-none",
|
|
552
|
-
|
|
567
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
568
|
+
itemStateClass(disabled, isActive)
|
|
553
569
|
),
|
|
554
570
|
children: [
|
|
555
571
|
Icon && /* @__PURE__ */ jsx6(Icon, { className: "h-4 w-4 shrink-0" }),
|
|
@@ -597,7 +613,8 @@ function SubNavPanel({
|
|
|
597
613
|
"button",
|
|
598
614
|
{
|
|
599
615
|
type: "button",
|
|
600
|
-
|
|
616
|
+
"aria-label": "Expand navigation",
|
|
617
|
+
className: "rounded text-muted-foreground/50 hover:text-muted-foreground transition-colors p-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
601
618
|
onClick: onToggleCollapse,
|
|
602
619
|
children: /* @__PURE__ */ jsx7(ChevronsRight, { className: "h-4 w-4" })
|
|
603
620
|
}
|
|
@@ -613,7 +630,8 @@ function SubNavPanel({
|
|
|
613
630
|
"button",
|
|
614
631
|
{
|
|
615
632
|
type: "button",
|
|
616
|
-
|
|
633
|
+
"aria-label": "Collapse navigation",
|
|
634
|
+
className: "rounded text-muted-foreground/40 hover:text-muted-foreground transition-colors p-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
617
635
|
onClick: onToggleCollapse,
|
|
618
636
|
children: /* @__PURE__ */ jsx7(ChevronsLeft, { className: "h-3.5 w-3.5" })
|
|
619
637
|
}
|
|
@@ -757,7 +775,7 @@ function ShellRail() {
|
|
|
757
775
|
active: isActive(service.id),
|
|
758
776
|
onClick: () => onServiceSelect(service.id),
|
|
759
777
|
disabled,
|
|
760
|
-
tooltip: disabled && reason ? `${service.label}
|
|
778
|
+
tooltip: disabled && reason ? `${service.label} -- ${reason}` : void 0
|
|
761
779
|
},
|
|
762
780
|
service.id
|
|
763
781
|
);
|
|
@@ -1011,7 +1029,7 @@ var ModuleEffectBoundary = class extends React3.Component {
|
|
|
1011
1029
|
}
|
|
1012
1030
|
componentDidCatch(error) {
|
|
1013
1031
|
warnLog4(
|
|
1014
|
-
"[ModuleEffects] module '{}' useEffects threw
|
|
1032
|
+
"[ModuleEffects] module '{}' useEffects threw -- effects disabled for this module: {}",
|
|
1015
1033
|
this.props.moduleId,
|
|
1016
1034
|
error
|
|
1017
1035
|
);
|
|
@@ -1068,22 +1086,35 @@ function selectNavModule(service, navigate, setSubNavCollapsed, closeSidePane) {
|
|
|
1068
1086
|
navigate(target);
|
|
1069
1087
|
}
|
|
1070
1088
|
function useShellApiInit(deps) {
|
|
1071
|
-
const
|
|
1089
|
+
const latest = useRef2(deps);
|
|
1090
|
+
useEffect3(() => {
|
|
1091
|
+
latest.current = deps;
|
|
1092
|
+
});
|
|
1072
1093
|
useEffect3(() => {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1094
|
+
initNavigation(
|
|
1095
|
+
(path) => latest.current.navigate(path),
|
|
1096
|
+
() => latest.current.openCommandMenu()
|
|
1097
|
+
);
|
|
1098
|
+
initFeatureNav((featureId) => latest.current.featureLookup(featureId));
|
|
1099
|
+
initPanel(
|
|
1100
|
+
(opts) => latest.current.handlePanelOpen(opts),
|
|
1101
|
+
() => latest.current.handlePanelClose()
|
|
1102
|
+
);
|
|
1103
|
+
initFullscreen(
|
|
1104
|
+
(opts) => latest.current.handleFullscreenEnter(opts),
|
|
1105
|
+
() => latest.current.handleFullscreenExit()
|
|
1106
|
+
);
|
|
1078
1107
|
initSidePane(
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1108
|
+
(opts) => latest.current.handleSidePaneOpen(opts),
|
|
1109
|
+
() => latest.current.handleSidePaneClose(),
|
|
1110
|
+
() => latest.current.handleSidePaneIsOpen(),
|
|
1111
|
+
() => latest.current.handleSidePaneActiveModuleId(),
|
|
1112
|
+
(fullWidth) => latest.current.handleSidePaneSetFullWidth(fullWidth)
|
|
1084
1113
|
);
|
|
1085
1114
|
initDialog(
|
|
1086
|
-
(opts) => new Promise(
|
|
1115
|
+
(opts) => new Promise(
|
|
1116
|
+
(resolve) => latest.current.setDialogState({ opts, resolve })
|
|
1117
|
+
)
|
|
1087
1118
|
);
|
|
1088
1119
|
}, []);
|
|
1089
1120
|
}
|
|
@@ -1140,6 +1171,11 @@ function useSidePaneState2() {
|
|
|
1140
1171
|
const handleSetFullWidth = useCallback2((fullWidth) => {
|
|
1141
1172
|
setSidePaneState((prev) => prev ? { ...prev, fullWidth } : prev);
|
|
1142
1173
|
}, []);
|
|
1174
|
+
const handleToggle = useCallback2((opts) => {
|
|
1175
|
+
setSidePaneState(
|
|
1176
|
+
(prev) => prev?.moduleId === opts.moduleId ? null : opts
|
|
1177
|
+
);
|
|
1178
|
+
}, []);
|
|
1143
1179
|
return {
|
|
1144
1180
|
sidePaneState,
|
|
1145
1181
|
setSidePaneState,
|
|
@@ -1147,7 +1183,8 @@ function useSidePaneState2() {
|
|
|
1147
1183
|
handleClose,
|
|
1148
1184
|
handleIsOpen,
|
|
1149
1185
|
handleActiveModuleId,
|
|
1150
|
-
handleSetFullWidth
|
|
1186
|
+
handleSetFullWidth,
|
|
1187
|
+
handleToggle
|
|
1151
1188
|
};
|
|
1152
1189
|
}
|
|
1153
1190
|
var DEFAULT_RESIZE_MIN = 240;
|
|
@@ -1224,6 +1261,7 @@ function AppShell({ registry, sidebar }) {
|
|
|
1224
1261
|
const { modules } = registry;
|
|
1225
1262
|
const { activeService, setSelectedServiceId } = useActiveService(modules);
|
|
1226
1263
|
const [subNavCollapsed, setSubNavCollapsed] = useState2(false);
|
|
1264
|
+
const isBelowTablet = useMediaQuery("(max-width: 1023px)");
|
|
1227
1265
|
const [commandMenuOpen, setCommandMenuOpen] = useState2(false);
|
|
1228
1266
|
const openCommandMenu = useCallback2(() => setCommandMenuOpen(true), []);
|
|
1229
1267
|
const [fullscreenState, setFullscreenState] = useState2(null);
|
|
@@ -1290,21 +1328,21 @@ function AppShell({ registry, sidebar }) {
|
|
|
1290
1328
|
pane: sidePane2.sidePaneState,
|
|
1291
1329
|
open: sidePane2.handleOpen,
|
|
1292
1330
|
close: sidePane2.handleClose,
|
|
1293
|
-
toggle:
|
|
1294
|
-
if (sidePane2.sidePaneState?.moduleId === opts.moduleId) {
|
|
1295
|
-
sidePane2.setSidePaneState(null);
|
|
1296
|
-
} else {
|
|
1297
|
-
sidePane2.setSidePaneState(opts);
|
|
1298
|
-
}
|
|
1299
|
-
},
|
|
1331
|
+
toggle: sidePane2.handleToggle,
|
|
1300
1332
|
setFullWidth: sidePane2.handleSetFullWidth
|
|
1301
1333
|
}),
|
|
1302
|
-
[
|
|
1334
|
+
[
|
|
1335
|
+
sidePane2.sidePaneState,
|
|
1336
|
+
sidePane2.handleOpen,
|
|
1337
|
+
sidePane2.handleClose,
|
|
1338
|
+
sidePane2.handleToggle,
|
|
1339
|
+
sidePane2.handleSetFullWidth
|
|
1340
|
+
]
|
|
1303
1341
|
);
|
|
1304
1342
|
const navigationValue = {
|
|
1305
1343
|
activeService,
|
|
1306
1344
|
sidePaneModuleId: sidePane2.sidePaneState?.moduleId ?? null,
|
|
1307
|
-
subNavCollapsed,
|
|
1345
|
+
subNavCollapsed: subNavCollapsed || isBelowTablet,
|
|
1308
1346
|
onToggleSubNav: () => setSubNavCollapsed((c) => !c),
|
|
1309
1347
|
onServiceSelect: handleServiceSelect
|
|
1310
1348
|
};
|
|
@@ -1519,34 +1557,66 @@ function Sidebar({
|
|
|
1519
1557
|
}
|
|
1520
1558
|
|
|
1521
1559
|
// src/ShellSidebar.tsx
|
|
1560
|
+
import React5, { useMemo as useMemo6 } from "react";
|
|
1522
1561
|
import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1523
1562
|
function ShellSidebar({
|
|
1524
1563
|
bottomGroupLabel = "Tools",
|
|
1525
1564
|
...sidebarProps
|
|
1526
1565
|
}) {
|
|
1527
1566
|
const { mainModules, bottomModules } = useShellModules();
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1567
|
+
const { resolveCapability, capabilityPolicy } = useShellConfig();
|
|
1568
|
+
const policy = useMemo6(
|
|
1569
|
+
() => resolveCapabilityPolicy(capabilityPolicy),
|
|
1570
|
+
[capabilityPolicy]
|
|
1530
1571
|
);
|
|
1572
|
+
const effectOf = React5.useCallback(
|
|
1573
|
+
(moduleId) => {
|
|
1574
|
+
const module = [...mainModules, ...bottomModules].find(
|
|
1575
|
+
(m) => m.id === moduleId
|
|
1576
|
+
);
|
|
1577
|
+
if (!module) return { effect: "visible" };
|
|
1578
|
+
return evaluateModule(module, "rail", resolveCapability, policy);
|
|
1579
|
+
},
|
|
1580
|
+
[mainModules, bottomModules, resolveCapability, policy]
|
|
1581
|
+
);
|
|
1582
|
+
const topNavItems = mainModules.flatMap((mod) => {
|
|
1583
|
+
const { effect, reason } = effectOf(mod.id);
|
|
1584
|
+
if (effect === "hidden" || effect === "redirect") return [];
|
|
1585
|
+
const disabled = effect === "disabled";
|
|
1586
|
+
return (mod.topNav ?? []).map((link) => ({
|
|
1587
|
+
link,
|
|
1588
|
+
icon: mod.icon,
|
|
1589
|
+
disabled,
|
|
1590
|
+
reason
|
|
1591
|
+
}));
|
|
1592
|
+
});
|
|
1531
1593
|
return /* @__PURE__ */ jsxs13(Sidebar, { ...sidebarProps, children: [
|
|
1532
|
-
topNavItems.length > 0 && /* @__PURE__ */ jsx18(SidebarGroup, { separator: true, children: topNavItems.map(({ link, icon }) => /* @__PURE__ */ jsx18(
|
|
1594
|
+
topNavItems.length > 0 && /* @__PURE__ */ jsx18(SidebarGroup, { separator: true, children: topNavItems.map(({ link, icon, disabled, reason }) => /* @__PURE__ */ jsx18(
|
|
1533
1595
|
SidebarItem,
|
|
1534
1596
|
{
|
|
1535
1597
|
icon,
|
|
1536
1598
|
label: link.label,
|
|
1537
1599
|
path: link.path,
|
|
1538
|
-
badge: link.badge
|
|
1600
|
+
badge: link.badge,
|
|
1601
|
+
disabled,
|
|
1602
|
+
title: disabled ? reason : void 0
|
|
1539
1603
|
},
|
|
1540
1604
|
link.path
|
|
1541
1605
|
)) }),
|
|
1542
1606
|
mainModules.map((mod) => {
|
|
1607
|
+
const { effect, reason } = effectOf(mod.id);
|
|
1608
|
+
if (effect === "hidden" || effect === "redirect") return null;
|
|
1609
|
+
const disabled = effect === "disabled";
|
|
1610
|
+
const title = disabled ? reason : void 0;
|
|
1543
1611
|
if (mod.navigation.length === 0) {
|
|
1544
1612
|
return /* @__PURE__ */ jsx18(
|
|
1545
1613
|
SidebarItem,
|
|
1546
1614
|
{
|
|
1547
1615
|
icon: mod.icon,
|
|
1548
1616
|
label: mod.label,
|
|
1549
|
-
path: mod.basePath
|
|
1617
|
+
path: mod.basePath,
|
|
1618
|
+
disabled,
|
|
1619
|
+
title
|
|
1550
1620
|
},
|
|
1551
1621
|
mod.id
|
|
1552
1622
|
);
|
|
@@ -1565,7 +1635,9 @@ function ShellSidebar({
|
|
|
1565
1635
|
icon: mod.icon,
|
|
1566
1636
|
label: link.label,
|
|
1567
1637
|
path: link.path,
|
|
1568
|
-
badge: link.badge
|
|
1638
|
+
badge: link.badge,
|
|
1639
|
+
disabled,
|
|
1640
|
+
title: disabled ? reason : void 0
|
|
1569
1641
|
},
|
|
1570
1642
|
link.path
|
|
1571
1643
|
))
|
|
@@ -1578,21 +1650,30 @@ function ShellSidebar({
|
|
|
1578
1650
|
icon: mod.icon,
|
|
1579
1651
|
label: link.label,
|
|
1580
1652
|
path: link.path,
|
|
1581
|
-
badge: link.badge
|
|
1653
|
+
badge: link.badge,
|
|
1654
|
+
disabled,
|
|
1655
|
+
title: disabled ? reason : void 0
|
|
1582
1656
|
},
|
|
1583
1657
|
link.path
|
|
1584
1658
|
))
|
|
1585
1659
|
]) }, mod.id);
|
|
1586
1660
|
}),
|
|
1587
|
-
bottomModules.length > 0 && /* @__PURE__ */ jsx18(SidebarGroup, { heading: bottomGroupLabel, collapsible: true, children: bottomModules.map((mod) =>
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1661
|
+
bottomModules.length > 0 && /* @__PURE__ */ jsx18(SidebarGroup, { heading: bottomGroupLabel, collapsible: true, children: bottomModules.map((mod) => {
|
|
1662
|
+
const { effect, reason } = effectOf(mod.id);
|
|
1663
|
+
if (effect === "hidden" || effect === "redirect") return null;
|
|
1664
|
+
const disabled = effect === "disabled";
|
|
1665
|
+
return /* @__PURE__ */ jsx18(
|
|
1666
|
+
SidebarItem,
|
|
1667
|
+
{
|
|
1668
|
+
icon: mod.icon,
|
|
1669
|
+
label: mod.label,
|
|
1670
|
+
path: mod.basePath,
|
|
1671
|
+
disabled,
|
|
1672
|
+
title: disabled ? reason : void 0
|
|
1673
|
+
},
|
|
1674
|
+
mod.id
|
|
1675
|
+
);
|
|
1676
|
+
}) })
|
|
1596
1677
|
] });
|
|
1597
1678
|
}
|
|
1598
1679
|
|
|
@@ -1604,7 +1685,7 @@ function ShellVersion({
|
|
|
1604
1685
|
}) {
|
|
1605
1686
|
return /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
|
|
1606
1687
|
/* @__PURE__ */ jsx19("span", { children: name }),
|
|
1607
|
-
version && /* @__PURE__ */ jsxs14("span", { className: "text-
|
|
1688
|
+
version && /* @__PURE__ */ jsxs14("span", { className: "text-tertiary-foreground", children: [
|
|
1608
1689
|
"v",
|
|
1609
1690
|
version
|
|
1610
1691
|
] })
|