@petrarca/sonnet-shell 0.5.0 → 0.6.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.
@@ -1,43 +1,50 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
3
  /**
4
- * TenantSelection -- shown after authentication when the user has
5
- * multiple tenants and no default is set. The user must pick a tenant
6
- * before the app loads.
4
+ * OrgSelection -- shown after authentication when the user belongs to
5
+ * multiple organizations and no default is set. The user selects which
6
+ * organization context to act in before the app loads.
7
+ *
8
+ * "Organization" is used generically: in Sestina this is an org-unit; in
9
+ * another app it could be a tenant, workspace, team, or company. The
10
+ * consumer maps their domain type to OrgOption.
7
11
  *
8
12
  * Fully props-driven with no app-specific imports.
9
13
  * Target package: @petrarca/sonnet-shell
10
14
  */
11
- /** Minimal tenant descriptor. Consumers map their domain type to this shape. */
12
- interface TenantOption {
15
+ /** Minimal organization descriptor. Consumers map their domain type to this shape. */
16
+ interface OrgOption {
13
17
  id: string;
14
18
  name: string;
15
19
  }
16
- interface TenantSelectionProps {
17
- tenants: TenantOption[];
20
+ interface OrgSelectionProps {
21
+ orgs: OrgOption[];
18
22
  isLoading: boolean;
19
23
  error: string | null;
20
24
  onSelect: (id: string) => void;
21
25
  }
22
- declare function TenantSelection({ tenants, isLoading, error, onSelect, }: TenantSelectionProps): React.ReactElement;
26
+ declare function OrgSelection({ orgs, isLoading, error, onSelect, }: OrgSelectionProps): React.ReactElement;
23
27
 
24
28
  /**
25
29
  * Login -- multi-step authentication flow.
26
30
  *
27
- * Step 1: Credentials (local auth form or OIDC redirect)
28
- * Step 2: Tenant selection (if user has multiple tenants and no default)
31
+ * Steps:
32
+ * identifier -- email/username entry (when onDiscover is provided)
33
+ * auth -- resolved providers: password form first, SSO buttons below
34
+ * credentials -- legacy single-screen (no onDiscover)
35
+ * org-selection -- organization picker after successful auth
36
+ *
37
+ * When onDiscover is NOT provided, falls back to the legacy single-screen
38
+ * behaviour. Backward compatible.
29
39
  *
30
- * Steps are shown conditionally. If the user has a default tenant,
31
- * step 2 is skipped and the app loads immediately.
40
+ * All async operations are injected via props. The component knows nothing
41
+ * about OIDC, PKCE, Sestina, or discover endpoints.
32
42
  *
33
- * ProtectedRoute passes a `step` hint in location.state so this
34
- * component always opens on the correct step when redirected.
43
+ * Design: centered card on gray-100, white card with elevated shadow,
44
+ * consistent with Auth0 Universal Login / WorkOS AuthKit visual language.
35
45
  *
36
- * All auth state and operations are injected via props -- no direct
37
- * store or context imports.
38
46
  * Target package: @petrarca/sonnet-shell
39
47
  */
40
- /** Auth feature flags and provider configuration. Consumer-supplied. */
41
48
  interface AuthConfig {
42
49
  enabled: boolean;
43
50
  features: {
@@ -55,30 +62,35 @@ interface AuthConfig {
55
62
  direct_redirect: string | null;
56
63
  };
57
64
  }
65
+ interface LoginProvider {
66
+ type: "local" | "external";
67
+ name: string;
68
+ label: string;
69
+ }
58
70
  interface LoginProps {
59
- /** Auth feature flags and OAuth provider config. */
60
71
  config: AuthConfig | null;
61
- /** Whether the user is currently authenticated (token present). */
62
72
  isAuthenticated: boolean;
63
- /** Active tenant id, null when not yet selected. */
64
73
  activeTenantId: string | null;
65
- /** Whether an async operation is in flight. */
66
74
  isLoading: boolean;
67
- /** Auth error message to display, null when none. */
68
75
  error: string | null;
69
- /** The authenticated user's available tenants. */
70
- tenants: TenantOption[] | null;
71
- /** Perform credential login. Throws on failure. */
76
+ orgs: OrgOption[] | null;
72
77
  onLogin: (credentials: {
73
78
  identifier: string;
74
79
  password: string;
75
80
  }) => Promise<void>;
76
- /** Fetch the current user after login to resolve tenant state. */
77
81
  onFetchCurrentUser: () => Promise<void>;
78
- /** Switch to the given tenant. Throws on failure. */
79
- onSwitchTenant: (id: string) => Promise<void>;
80
- /** Clear the current auth error. */
82
+ onSelectOrg: (id: string) => Promise<void>;
81
83
  onClearError: () => void;
84
+ /** Application name shown above the card as a brand label. */
85
+ appName?: string;
86
+ /** Logo URL shown above the card. If provided, appName is used as alt text. */
87
+ logoUrl?: string;
88
+ /** Enables identifier-first flow. App calls discover endpoint and returns providers. */
89
+ onDiscover?: (identifier: string) => Promise<LoginProvider[]>;
90
+ /** Called when user clicks an external provider button. App handles PKCE + redirect. */
91
+ onExternalLogin?: (provider: LoginProvider) => Promise<void> | void;
92
+ /** Whether discovery is in flight. */
93
+ isDiscovering?: boolean;
82
94
  }
83
95
  declare function Login(props: LoginProps): React.ReactElement | null;
84
96
 
@@ -114,4 +126,4 @@ interface ProtectedRouteProps {
114
126
  }
115
127
  declare function ProtectedRoute({ children, authEnabled, authConfigLoading, isAuthenticated, accessToken, activeTenantId, initializeAuth, }: ProtectedRouteProps): React.ReactElement;
116
128
 
117
- export { type AuthConfig, Login, type LoginProps, ProtectedRoute, type ProtectedRouteProps, type TenantOption, TenantSelection };
129
+ export { type AuthConfig, Login, type LoginProps, type LoginProvider, type OrgOption, OrgSelection, ProtectedRoute, type ProtectedRouteProps };
@@ -1,51 +1,50 @@
1
1
  // src/auth/Login.tsx
2
2
  import { useState as useState2, useEffect, useCallback as useCallback2 } from "react";
3
3
  import { useNavigate, useLocation } from "react-router-dom";
4
- import { Button as Button2 } from "@petrarca/sonnet-ui";
5
4
  import { Input } from "@petrarca/sonnet-ui";
6
5
  import { Label } from "@petrarca/sonnet-ui";
7
- import { Alert as Alert2 } from "@petrarca/sonnet-ui";
6
+ import { Spinner } from "@petrarca/sonnet-ui";
8
7
 
9
- // src/auth/TenantSelection.tsx
8
+ // src/auth/OrgSelection.tsx
10
9
  import { useState, useCallback } from "react";
11
10
  import { Check } from "lucide-react";
12
11
  import { Button } from "@petrarca/sonnet-ui";
13
12
  import { Alert } from "@petrarca/sonnet-ui";
14
13
  import { jsx, jsxs } from "react/jsx-runtime";
15
- function TenantSelection({
16
- tenants,
14
+ function OrgSelection({
15
+ orgs,
17
16
  isLoading,
18
17
  error,
19
18
  onSelect
20
19
  }) {
21
20
  const [selectedId, setSelectedId] = useState(null);
22
21
  const handleContinue = useCallback(() => {
23
- if (selectedId) {
24
- onSelect(selectedId);
25
- }
22
+ if (selectedId) onSelect(selectedId);
26
23
  }, [selectedId, onSelect]);
27
- return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center min-h-screen bg-slate-100", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-md bg-white rounded-lg shadow-lg p-8", children: [
28
- /* @__PURE__ */ jsx("h1", { className: "text-center mb-2", children: "Select Tenant" }),
29
- /* @__PURE__ */ jsx("p", { className: "text-center text-sm text-muted-foreground mb-6", children: "Choose the tenant you want to work in." }),
30
- error && /* @__PURE__ */ jsx(Alert, { className: "mb-4 border-red-200 bg-red-50 text-red-700", children: /* @__PURE__ */ jsx("p", { className: "text-sm", children: error }) }),
31
- /* @__PURE__ */ jsx("div", { className: "space-y-2 mb-6 max-h-64 overflow-y-auto", children: tenants.map((tenant) => /* @__PURE__ */ jsxs(
24
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center min-h-screen bg-slate-50 px-4", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm bg-white rounded-xl shadow-sm border border-slate-200 p-8", children: [
25
+ /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
26
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold text-slate-900 text-center", children: "Select organization" }),
27
+ /* @__PURE__ */ jsx("p", { className: "text-center text-sm text-slate-500 mt-1", children: "Choose the organization you want to work in." })
28
+ ] }),
29
+ error ? /* @__PURE__ */ jsx(Alert, { className: "mb-4 border-red-200 bg-red-50 text-red-700 text-sm py-2.5 px-3", children: error }) : null,
30
+ /* @__PURE__ */ jsx("div", { className: "space-y-2 mb-6 max-h-64 overflow-y-auto", children: orgs.map((org) => /* @__PURE__ */ jsxs(
32
31
  "button",
33
32
  {
34
- onClick: () => setSelectedId(tenant.id),
35
- className: `w-full flex items-center justify-between px-4 py-3 rounded-md border text-left text-sm transition-colors ${selectedId === tenant.id ? "border-blue-500 bg-blue-50 text-blue-900" : "border-slate-200 hover:bg-slate-50"}`,
33
+ onClick: () => setSelectedId(org.id),
34
+ 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 ${selectedId === org.id ? "border-blue-500 bg-blue-50 text-blue-900" : "border-slate-200 hover:bg-slate-50 text-slate-700"}`,
36
35
  children: [
37
- /* @__PURE__ */ jsx("span", { children: tenant.name }),
38
- selectedId === tenant.id && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-blue-500" })
36
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: org.name }),
37
+ selectedId === org.id ? /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-blue-500 shrink-0" }) : null
39
38
  ]
40
39
  },
41
- tenant.id
40
+ org.id
42
41
  )) }),
43
42
  /* @__PURE__ */ jsx(
44
43
  Button,
45
44
  {
46
45
  onClick: handleContinue,
47
46
  disabled: !selectedId || isLoading,
48
- className: "w-full",
47
+ className: "w-full h-10",
49
48
  children: isLoading ? "Loading..." : "Continue"
50
49
  }
51
50
  )
@@ -60,43 +59,65 @@ function useLoginFlow(props) {
60
59
  config,
61
60
  isAuthenticated,
62
61
  activeTenantId,
63
- tenants,
62
+ orgs,
64
63
  onLogin,
65
64
  onFetchCurrentUser,
66
- onSwitchTenant,
67
- onClearError
65
+ onSelectOrg,
66
+ onClearError,
67
+ onDiscover,
68
+ onExternalLogin
68
69
  } = props;
69
70
  const navigate = useNavigate();
70
71
  const location = useLocation();
71
72
  const locationState = location.state;
73
+ const identifierFirst = Boolean(onDiscover);
74
+ const initial = locationState?.step ?? (identifierFirst ? "identifier" : "credentials");
72
75
  const [redirectTo] = useState2(() => locationState?.from?.pathname ?? "/");
73
- const [step, setStep] = useState2(
74
- () => locationState?.step ?? "credentials"
75
- );
76
+ const [step, setStep] = useState2(initial);
76
77
  const [identifier, setIdentifier] = useState2("");
77
78
  const [password, setPassword] = useState2("");
78
- const [tenantError, setTenantError] = useState2(null);
79
+ const [providers, setProviders] = useState2([]);
80
+ const [discoverError, setDiscoverError] = useState2(null);
81
+ const [isRedirecting, setIsRedirecting] = useState2(false);
82
+ const [orgError, setOrgError] = useState2(null);
79
83
  useEffect(() => {
80
84
  if (isAuthenticated && activeTenantId)
81
85
  navigate(redirectTo, { replace: true });
82
86
  }, [isAuthenticated, activeTenantId, navigate, redirectTo]);
83
87
  useEffect(() => {
84
- if (!isAuthenticated || !tenants || activeTenantId) return;
85
- if (tenants.length === 1) {
86
- onSwitchTenant(tenants[0].id).catch((err) => {
87
- setTenantError(
88
- normalizeError(err).message || "Failed to select tenant"
88
+ if (!isAuthenticated || !orgs || activeTenantId) return;
89
+ if (orgs.length === 1) {
90
+ onSelectOrg(orgs[0].id).catch((err) => {
91
+ setOrgError(
92
+ normalizeError(err).message || "Failed to select organization"
89
93
  );
90
- setStep("tenant-selection");
94
+ setStep("org-selection");
91
95
  });
92
- } else if (tenants.length > 1) {
93
- setStep("tenant-selection");
96
+ } else if (orgs.length > 1) {
97
+ setStep("org-selection");
94
98
  }
95
- }, [isAuthenticated, tenants, activeTenantId, onSwitchTenant]);
99
+ }, [isAuthenticated, orgs, activeTenantId, onSelectOrg]);
96
100
  useEffect(() => {
97
101
  if (config?.ui.direct_redirect)
98
102
  window.location.href = config.ui.direct_redirect;
99
103
  }, [config]);
104
+ const handleIdentifierSubmit = useCallback2(
105
+ async (e) => {
106
+ e.preventDefault();
107
+ if (!onDiscover || !identifier.trim()) return;
108
+ setDiscoverError(null);
109
+ try {
110
+ const resolved = await onDiscover(identifier.trim());
111
+ setProviders(resolved);
112
+ setStep("auth");
113
+ } catch (err) {
114
+ setDiscoverError(
115
+ normalizeError(err).message || "Failed to look up providers"
116
+ );
117
+ }
118
+ },
119
+ [identifier, onDiscover]
120
+ );
100
121
  const handleCredentialSubmit = useCallback2(
101
122
  async (e) => {
102
123
  e.preventDefault();
@@ -109,94 +130,164 @@ function useLoginFlow(props) {
109
130
  },
110
131
  [identifier, password, onLogin, onFetchCurrentUser, onClearError]
111
132
  );
112
- const handleTenantSelect = useCallback2(
133
+ const handleExternalLogin = useCallback2(
134
+ async (provider) => {
135
+ if (!onExternalLogin) return;
136
+ setIsRedirecting(true);
137
+ try {
138
+ await onExternalLogin(provider);
139
+ } catch {
140
+ setIsRedirecting(false);
141
+ }
142
+ },
143
+ [onExternalLogin]
144
+ );
145
+ const handleOrgSelect = useCallback2(
113
146
  async (id) => {
114
- setTenantError(null);
147
+ setOrgError(null);
115
148
  try {
116
- await onSwitchTenant(id);
149
+ await onSelectOrg(id);
117
150
  } catch (err) {
118
- setTenantError(
119
- normalizeError(err).message || "Failed to select tenant"
151
+ setOrgError(
152
+ normalizeError(err).message || "Failed to select organization"
120
153
  );
121
154
  }
122
155
  },
123
- [onSwitchTenant]
156
+ [onSelectOrg]
124
157
  );
158
+ const goBack = useCallback2(() => {
159
+ setPassword("");
160
+ setDiscoverError(null);
161
+ onClearError();
162
+ setStep(identifierFirst ? "identifier" : "credentials");
163
+ }, [identifierFirst, onClearError]);
125
164
  return {
126
165
  step,
127
166
  identifier,
128
167
  setIdentifier,
129
168
  password,
130
169
  setPassword,
131
- tenantError,
170
+ providers,
171
+ discoverError,
172
+ isRedirecting,
173
+ orgError,
174
+ handleIdentifierSubmit,
132
175
  handleCredentialSubmit,
133
- handleTenantSelect
176
+ handleExternalLogin,
177
+ handleOrgSelect,
178
+ goBack
134
179
  };
135
180
  }
136
181
  function Login(props) {
137
- const { config, isLoading, error, tenants } = props;
182
+ const { config, isLoading, isDiscovering, error, orgs, appName, logoUrl } = props;
138
183
  const flow = useLoginFlow(props);
139
- if (config?.ui.direct_redirect) {
184
+ if (config?.ui.direct_redirect || flow.isRedirecting)
140
185
  return /* @__PURE__ */ jsx2(RedirectingMessage, {});
141
- }
142
- if (flow.step === "tenant-selection" && tenants) {
186
+ if (flow.step === "org-selection" && orgs) {
143
187
  return /* @__PURE__ */ jsx2(
144
- TenantSelection,
188
+ OrgSelection,
145
189
  {
146
- tenants,
190
+ orgs,
147
191
  isLoading,
148
- error: flow.tenantError,
149
- onSelect: flow.handleTenantSelect
192
+ error: flow.orgError,
193
+ onSelect: flow.handleOrgSelect
150
194
  }
151
195
  );
152
196
  }
153
- return /* @__PURE__ */ jsx2(
154
- CredentialPage,
155
- {
156
- config,
157
- error,
158
- isLoading,
159
- flow
160
- }
161
- );
162
- }
163
- function RedirectingMessage() {
164
- return /* @__PURE__ */ jsx2("div", { className: "flex items-center justify-center min-h-screen", children: /* @__PURE__ */ jsx2("p", { className: "text-muted-foreground", children: "Redirecting to authentication..." }) });
165
- }
166
- function CredentialPage({
167
- config,
168
- error,
169
- isLoading,
170
- flow
171
- }) {
172
- return /* @__PURE__ */ jsx2("div", { className: "flex items-center justify-center min-h-screen bg-slate-100", children: /* @__PURE__ */ jsxs2("div", { className: "w-full max-w-md bg-white rounded-lg shadow-lg p-8", children: [
173
- /* @__PURE__ */ jsx2("h1", { className: "text-center mb-6", children: "Sign In" }),
174
- error && /* @__PURE__ */ jsx2(Alert2, { className: "mb-4 border-red-200 bg-red-50 text-red-700", children: /* @__PURE__ */ jsx2("p", { className: "text-sm", children: error }) }),
175
- config?.features.local_auth && /* @__PURE__ */ jsx2(
176
- CredentialForm,
197
+ return /* @__PURE__ */ jsx2(PageShell, { children: /* @__PURE__ */ jsxs2(Card, { children: [
198
+ flow.step === "identifier" && /* @__PURE__ */ jsx2(
199
+ IdentifierStep,
177
200
  {
201
+ appName,
202
+ logoUrl,
178
203
  identifier: flow.identifier,
204
+ error: flow.discoverError,
205
+ isLoading: isDiscovering ?? false,
206
+ onIdentifierChange: flow.setIdentifier,
207
+ onSubmit: flow.handleIdentifierSubmit
208
+ }
209
+ ),
210
+ flow.step === "auth" && /* @__PURE__ */ jsx2(
211
+ AuthStep,
212
+ {
213
+ appName,
214
+ logoUrl,
215
+ identifier: flow.identifier,
216
+ providers: flow.providers,
179
217
  password: flow.password,
218
+ error,
180
219
  isLoading,
181
- onIdentifierChange: flow.setIdentifier,
182
220
  onPasswordChange: flow.setPassword,
183
- onSubmit: flow.handleCredentialSubmit
221
+ onCredentialSubmit: flow.handleCredentialSubmit,
222
+ onExternalLogin: flow.handleExternalLogin,
223
+ onBack: flow.goBack
184
224
  }
185
225
  ),
186
- config?.features.oauth && config.oauth?.providers && /* @__PURE__ */ jsx2(OAuthProviders, { providers: config.oauth.providers })
226
+ flow.step === "credentials" && /* @__PURE__ */ jsx2(
227
+ CredentialsStep,
228
+ {
229
+ appName,
230
+ logoUrl,
231
+ config,
232
+ identifier: flow.identifier,
233
+ password: flow.password,
234
+ error,
235
+ isLoading,
236
+ onIdentifierChange: flow.setIdentifier,
237
+ onPasswordChange: flow.setPassword,
238
+ onSubmit: flow.handleCredentialSubmit,
239
+ onExternalLogin: flow.handleExternalLogin
240
+ }
241
+ )
187
242
  ] }) });
188
243
  }
189
- function CredentialForm({
244
+ function PageShell({ children }) {
245
+ return /* @__PURE__ */ jsx2("div", { className: "flex flex-col items-center justify-center min-h-screen bg-gray-100 px-4", children });
246
+ }
247
+ function Card({ children }) {
248
+ return /* @__PURE__ */ jsx2(
249
+ "div",
250
+ {
251
+ className: "w-full max-w-sm bg-white rounded-xl border border-gray-200 p-8",
252
+ style: {
253
+ boxShadow: "0 24px 48px rgba(15,23,42,0.10), 0 2px 4px rgba(15,23,42,0.06)"
254
+ },
255
+ children
256
+ }
257
+ );
258
+ }
259
+ function BrandHeader({
260
+ appName,
261
+ logoUrl
262
+ }) {
263
+ if (!appName && !logoUrl) return null;
264
+ return /* @__PURE__ */ jsx2("div", { className: "flex flex-col items-center mb-6", children: logoUrl ? /* @__PURE__ */ jsx2("img", { src: logoUrl, alt: appName ?? "Logo", className: "h-10 w-auto" }) : /* @__PURE__ */ jsx2("span", { className: "text-lg font-semibold text-slate-500 tracking-wide", children: appName }) });
265
+ }
266
+ function IdentifierStep({
267
+ appName,
268
+ logoUrl,
190
269
  identifier,
191
- password,
270
+ error,
192
271
  isLoading,
193
272
  onIdentifierChange,
194
- onPasswordChange,
195
273
  onSubmit
196
274
  }) {
197
- return /* @__PURE__ */ jsxs2("form", { onSubmit, className: "space-y-4", children: [
198
- /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
199
- /* @__PURE__ */ jsx2(Label, { htmlFor: "identifier", children: "Email or Username" }),
275
+ return /* @__PURE__ */ jsxs2("form", { onSubmit, className: "space-y-6", children: [
276
+ /* @__PURE__ */ jsx2(BrandHeader, { appName, logoUrl }),
277
+ /* @__PURE__ */ jsxs2("div", { className: "text-center", children: [
278
+ /* @__PURE__ */ jsx2("h1", { className: "text-2xl font-semibold text-slate-900 leading-tight", children: "Welcome" }),
279
+ /* @__PURE__ */ jsx2("p", { className: "mt-2 text-sm text-gray-500", children: "Enter your email or username to continue." })
280
+ ] }),
281
+ error ? /* @__PURE__ */ jsx2(ErrorAlert, { message: error }) : null,
282
+ /* @__PURE__ */ jsxs2("div", { children: [
283
+ /* @__PURE__ */ jsx2(
284
+ Label,
285
+ {
286
+ htmlFor: "identifier",
287
+ className: "block text-sm font-medium text-gray-700 mb-1.5",
288
+ children: "Email or username"
289
+ }
290
+ ),
200
291
  /* @__PURE__ */ jsx2(
201
292
  Input,
202
293
  {
@@ -205,54 +296,256 @@ function CredentialForm({
205
296
  value: identifier,
206
297
  onChange: (e) => onIdentifierChange(e.target.value),
207
298
  required: true,
208
- placeholder: "Enter your email or username"
299
+ autoFocus: true,
300
+ autoComplete: "email",
301
+ placeholder: "you@company.com",
302
+ className: "h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500"
209
303
  }
210
304
  )
211
305
  ] }),
212
- /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
213
- /* @__PURE__ */ jsx2(Label, { htmlFor: "password", children: "Password" }),
306
+ /* @__PURE__ */ jsx2(
307
+ PrimaryButton,
308
+ {
309
+ type: "submit",
310
+ disabled: isLoading || !identifier.trim(),
311
+ loading: isLoading,
312
+ loadingText: "Looking up...",
313
+ children: "Continue"
314
+ }
315
+ )
316
+ ] });
317
+ }
318
+ function AuthStep({
319
+ appName,
320
+ logoUrl,
321
+ identifier,
322
+ providers,
323
+ password,
324
+ error,
325
+ isLoading,
326
+ onPasswordChange,
327
+ onCredentialSubmit,
328
+ onExternalLogin,
329
+ onBack
330
+ }) {
331
+ const external = providers.filter((p) => p.type === "external");
332
+ const hasLocal = providers.some((p) => p.type === "local");
333
+ const hasBoth = external.length > 0 && hasLocal;
334
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-6", children: [
335
+ /* @__PURE__ */ jsx2(BrandHeader, { appName, logoUrl }),
336
+ /* @__PURE__ */ jsxs2("div", { children: [
337
+ /* @__PURE__ */ jsxs2(
338
+ "button",
339
+ {
340
+ type: "button",
341
+ onClick: onBack,
342
+ className: "flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-700 transition-colors mb-4",
343
+ children: [
344
+ /* @__PURE__ */ jsx2("span", { "aria-hidden": true, children: "\u2190" }),
345
+ " Back"
346
+ ]
347
+ }
348
+ ),
349
+ /* @__PURE__ */ jsx2("h1", { className: "text-2xl font-semibold text-slate-900 leading-tight", children: "Welcome back" }),
350
+ /* @__PURE__ */ jsx2("p", { className: "mt-1 text-sm text-gray-500", children: identifier })
351
+ ] }),
352
+ error ? /* @__PURE__ */ jsx2(ErrorAlert, { message: error }) : null,
353
+ hasLocal ? /* @__PURE__ */ jsxs2("form", { onSubmit: onCredentialSubmit, className: "space-y-4", children: [
354
+ /* @__PURE__ */ jsxs2("div", { children: [
355
+ /* @__PURE__ */ jsx2(
356
+ Label,
357
+ {
358
+ htmlFor: "password",
359
+ className: "block text-sm font-medium text-gray-700 mb-1.5",
360
+ children: "Password"
361
+ }
362
+ ),
363
+ /* @__PURE__ */ jsx2(
364
+ Input,
365
+ {
366
+ id: "password",
367
+ type: "password",
368
+ value: password,
369
+ onChange: (e) => onPasswordChange(e.target.value),
370
+ required: true,
371
+ autoFocus: true,
372
+ autoComplete: "current-password",
373
+ placeholder: "Enter your password",
374
+ className: "h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500"
375
+ }
376
+ )
377
+ ] }),
214
378
  /* @__PURE__ */ jsx2(
215
- Input,
379
+ PrimaryButton,
216
380
  {
217
- id: "password",
218
- type: "password",
219
- value: password,
220
- onChange: (e) => onPasswordChange(e.target.value),
221
- required: true,
222
- placeholder: "Enter your password"
381
+ type: "submit",
382
+ disabled: isLoading,
383
+ loading: isLoading,
384
+ loadingText: "Signing in...",
385
+ children: "Sign in"
223
386
  }
224
387
  )
225
- ] }),
226
- /* @__PURE__ */ jsx2(Button2, { type: "submit", disabled: isLoading, className: "w-full", children: isLoading ? "Signing in..." : "Sign In" })
388
+ ] }) : null,
389
+ hasBoth ? /* @__PURE__ */ jsx2(Separator, {}) : null,
390
+ external.length > 0 ? /* @__PURE__ */ jsx2("div", { className: "space-y-2", children: external.map((p) => /* @__PURE__ */ jsx2(
391
+ SsoButton,
392
+ {
393
+ label: p.label,
394
+ onClick: () => onExternalLogin(p)
395
+ },
396
+ p.name
397
+ )) }) : null,
398
+ providers.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "text-sm text-gray-500 text-center py-2", children: "No login methods available." }) : null
227
399
  ] });
228
400
  }
229
- function OAuthProviders({ providers }) {
230
- return /* @__PURE__ */ jsxs2("div", { className: "mt-6", children: [
231
- /* @__PURE__ */ jsxs2("div", { className: "relative mb-4", children: [
232
- /* @__PURE__ */ jsx2("div", { className: "absolute inset-0 flex items-center", children: /* @__PURE__ */ jsx2("div", { className: "w-full border-t border-slate-300" }) }),
233
- /* @__PURE__ */ jsx2("div", { className: "relative flex justify-center text-sm", children: /* @__PURE__ */ jsx2("span", { className: "px-2 bg-white text-muted-foreground", children: "Or continue with" }) })
234
- ] }),
235
- /* @__PURE__ */ jsx2("div", { className: "space-y-2", children: Object.entries(providers).map(
236
- ([key, provider]) => provider.enabled ? /* @__PURE__ */ jsxs2(
237
- "a",
401
+ function resolveOAuthProviders(config) {
402
+ return config?.features.oauth && config.oauth?.providers ? Object.entries(config.oauth.providers).filter(([, p]) => p.enabled) : [];
403
+ }
404
+ function CredentialsStep({
405
+ appName,
406
+ logoUrl,
407
+ config,
408
+ identifier,
409
+ password,
410
+ error,
411
+ isLoading,
412
+ onIdentifierChange,
413
+ onPasswordChange,
414
+ onSubmit,
415
+ onExternalLogin
416
+ }) {
417
+ const oauthProviders = resolveOAuthProviders(config);
418
+ const hasBoth = config?.features.local_auth && oauthProviders.length > 0;
419
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-6", children: [
420
+ /* @__PURE__ */ jsx2(BrandHeader, { appName, logoUrl }),
421
+ /* @__PURE__ */ jsx2("h1", { className: "text-2xl font-semibold text-slate-900 leading-tight text-center", children: "Welcome" }),
422
+ error ? /* @__PURE__ */ jsx2(ErrorAlert, { message: error }) : null,
423
+ config?.features.local_auth ? /* @__PURE__ */ jsxs2("form", { onSubmit, className: "space-y-4", children: [
424
+ /* @__PURE__ */ jsxs2("div", { children: [
425
+ /* @__PURE__ */ jsx2(
426
+ Label,
427
+ {
428
+ htmlFor: "identifier",
429
+ className: "block text-sm font-medium text-gray-700 mb-1.5",
430
+ children: "Email or username"
431
+ }
432
+ ),
433
+ /* @__PURE__ */ jsx2(
434
+ Input,
435
+ {
436
+ id: "identifier",
437
+ type: "text",
438
+ value: identifier,
439
+ onChange: (e) => onIdentifierChange(e.target.value),
440
+ required: true,
441
+ autoFocus: true,
442
+ autoComplete: "email",
443
+ placeholder: "you@company.com",
444
+ className: "h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500"
445
+ }
446
+ )
447
+ ] }),
448
+ /* @__PURE__ */ jsxs2("div", { children: [
449
+ /* @__PURE__ */ jsx2(
450
+ Label,
451
+ {
452
+ htmlFor: "password",
453
+ className: "block text-sm font-medium text-gray-700 mb-1.5",
454
+ children: "Password"
455
+ }
456
+ ),
457
+ /* @__PURE__ */ jsx2(
458
+ Input,
459
+ {
460
+ id: "password",
461
+ type: "password",
462
+ value: password,
463
+ onChange: (e) => onPasswordChange(e.target.value),
464
+ required: true,
465
+ autoComplete: "current-password",
466
+ placeholder: "Enter your password",
467
+ className: "h-10 rounded-lg border-gray-300 text-sm placeholder:text-gray-400 focus:ring-indigo-500 focus:border-indigo-500"
468
+ }
469
+ )
470
+ ] }),
471
+ /* @__PURE__ */ jsx2(
472
+ PrimaryButton,
238
473
  {
239
- href: provider.login_url,
240
- className: "w-full flex items-center justify-center py-2 px-4 border border-slate-300 rounded-md hover:bg-slate-50",
241
- children: [
242
- "Continue with ",
243
- provider.name
244
- ]
245
- },
246
- key
247
- ) : null
248
- ) })
474
+ type: "submit",
475
+ disabled: isLoading,
476
+ loading: isLoading,
477
+ loadingText: "Signing in...",
478
+ children: "Sign in"
479
+ }
480
+ )
481
+ ] }) : null,
482
+ hasBoth ? /* @__PURE__ */ jsx2(Separator, {}) : null,
483
+ oauthProviders.length > 0 ? /* @__PURE__ */ jsx2("div", { className: "space-y-2", children: oauthProviders.map(([key, p]) => /* @__PURE__ */ jsx2(
484
+ SsoButton,
485
+ {
486
+ label: `Continue with ${p.name}`,
487
+ onClick: () => onExternalLogin({ type: "external", name: key, label: p.name })
488
+ },
489
+ key
490
+ )) }) : null
249
491
  ] });
250
492
  }
493
+ function RedirectingMessage() {
494
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center min-h-screen bg-gray-100 gap-3 text-gray-500", children: [
495
+ /* @__PURE__ */ jsx2(Spinner, { size: "lg" }),
496
+ /* @__PURE__ */ jsx2("p", { className: "text-sm", children: "Redirecting..." })
497
+ ] });
498
+ }
499
+ function ErrorAlert({ message }) {
500
+ return /* @__PURE__ */ jsx2("div", { className: "rounded-lg border border-red-200 bg-red-50 px-3 py-2.5 text-sm text-red-700", children: message });
501
+ }
502
+ function Separator() {
503
+ return /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
504
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0 flex items-center", children: /* @__PURE__ */ jsx2("div", { className: "w-full border-t border-gray-200" }) }),
505
+ /* @__PURE__ */ jsx2("div", { className: "relative flex justify-center text-xs", children: /* @__PURE__ */ jsx2("span", { className: "bg-white px-2 text-gray-400", children: "or continue with" }) })
506
+ ] });
507
+ }
508
+ function PrimaryButton({
509
+ type = "button",
510
+ disabled,
511
+ loading,
512
+ loadingText,
513
+ children,
514
+ onClick
515
+ }) {
516
+ return /* @__PURE__ */ jsxs2(
517
+ "button",
518
+ {
519
+ type,
520
+ disabled,
521
+ onClick,
522
+ 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",
523
+ style: {
524
+ boxShadow: disabled ? void 0 : "0 4px 14px rgba(79,70,229,0.30)"
525
+ },
526
+ children: [
527
+ loading ? /* @__PURE__ */ jsx2(Spinner, { size: "sm", className: "mr-2" }) : null,
528
+ loading && loadingText ? loadingText : children
529
+ ]
530
+ }
531
+ );
532
+ }
533
+ function SsoButton({ label, onClick }) {
534
+ return /* @__PURE__ */ jsx2(
535
+ "button",
536
+ {
537
+ type: "button",
538
+ onClick,
539
+ 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",
540
+ children: label
541
+ }
542
+ );
543
+ }
251
544
 
252
545
  // src/auth/ProtectedRoute.tsx
253
546
  import { Navigate, useLocation as useLocation2 } from "react-router-dom";
254
547
  import { useEffect as useEffect2, useRef, useState as useState3 } from "react";
255
- import { Spinner } from "@petrarca/sonnet-ui";
548
+ import { Spinner as Spinner2 } from "@petrarca/sonnet-ui";
256
549
  import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
257
550
  function ProtectedRoute({
258
551
  children,
@@ -270,7 +563,7 @@ function ProtectedRoute({
270
563
  initRef.current().finally(() => setInitializing(false));
271
564
  }, []);
272
565
  if (authConfigLoading || initializing) {
273
- return /* @__PURE__ */ jsx3("div", { className: "flex items-center justify-center min-h-screen", children: /* @__PURE__ */ jsx3(Spinner, { size: "lg" }) });
566
+ return /* @__PURE__ */ jsx3("div", { className: "flex items-center justify-center min-h-screen", children: /* @__PURE__ */ jsx3(Spinner2, { size: "lg" }) });
274
567
  }
275
568
  if (!authEnabled) {
276
569
  return /* @__PURE__ */ jsx3(Fragment, { children });
@@ -299,7 +592,7 @@ function ProtectedRoute({
299
592
  }
300
593
  export {
301
594
  Login,
302
- ProtectedRoute,
303
- TenantSelection
595
+ OrgSelection,
596
+ ProtectedRoute
304
597
  };
305
598
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/Login.tsx","../../src/auth/TenantSelection.tsx","../../src/auth/ProtectedRoute.tsx"],"sourcesContent":["import { useState, useEffect, useCallback } from \"react\";\nimport { useNavigate, useLocation } from \"react-router-dom\";\nimport { Button } from \"@petrarca/sonnet-ui\";\nimport { Input } from \"@petrarca/sonnet-ui\";\nimport { Label } from \"@petrarca/sonnet-ui\";\nimport { Alert } from \"@petrarca/sonnet-ui\";\nimport { TenantSelection, type TenantOption } from \"./TenantSelection\";\nimport { normalizeError } from \"@petrarca/sonnet-core\";\n\n/**\n * Login -- multi-step authentication flow.\n *\n * Step 1: Credentials (local auth form or OIDC redirect)\n * Step 2: Tenant selection (if user has multiple tenants and no default)\n *\n * Steps are shown conditionally. If the user has a default tenant,\n * step 2 is skipped and the app loads immediately.\n *\n * ProtectedRoute passes a `step` hint in location.state so this\n * component always opens on the correct step when redirected.\n *\n * All auth state and operations are injected via props -- no direct\n * store or context imports.\n * Target package: @petrarca/sonnet-shell\n */\n\n/** Auth feature flags and provider configuration. Consumer-supplied. */\nexport interface AuthConfig {\n enabled: boolean;\n features: {\n local_auth: boolean;\n oauth: boolean;\n };\n oauth?: {\n providers: Record<\n string,\n {\n enabled: boolean;\n name: string;\n login_url: string;\n }\n >;\n } | null;\n ui: {\n direct_redirect: string | null;\n };\n}\n\ntype LoginStep = \"credentials\" | \"tenant-selection\";\n\ninterface LocationState {\n from?: { pathname: string };\n step?: LoginStep;\n}\n\nexport interface LoginProps {\n /** Auth feature flags and OAuth provider config. */\n config: AuthConfig | null;\n /** Whether the user is currently authenticated (token present). */\n isAuthenticated: boolean;\n /** Active tenant id, null when not yet selected. */\n activeTenantId: string | null;\n /** Whether an async operation is in flight. */\n isLoading: boolean;\n /** Auth error message to display, null when none. */\n error: string | null;\n /** The authenticated user's available tenants. */\n tenants: TenantOption[] | null;\n /** Perform credential login. Throws on failure. */\n onLogin: (credentials: {\n identifier: string;\n password: string;\n }) => Promise<void>;\n /** Fetch the current user after login to resolve tenant state. */\n onFetchCurrentUser: () => Promise<void>;\n /** Switch to the given tenant. Throws on failure. */\n onSwitchTenant: (id: string) => Promise<void>;\n /** Clear the current auth error. */\n onClearError: () => void;\n}\n\n// Custom hook: manages the login flow state (steps, redirects, tenant resolution).\nfunction useLoginFlow(props: LoginProps) {\n const {\n config,\n isAuthenticated,\n activeTenantId,\n tenants,\n onLogin,\n onFetchCurrentUser,\n onSwitchTenant,\n onClearError,\n } = props;\n const navigate = useNavigate();\n const location = useLocation();\n const locationState = location.state as LocationState | null;\n\n const [redirectTo] = useState(() => locationState?.from?.pathname ?? \"/\");\n const [step, setStep] = useState<LoginStep>(\n () => locationState?.step ?? \"credentials\",\n );\n const [identifier, setIdentifier] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [tenantError, setTenantError] = 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 || !tenants || activeTenantId) return;\n if (tenants.length === 1) {\n onSwitchTenant(tenants[0].id).catch((err) => {\n setTenantError(\n normalizeError(err).message || \"Failed to select tenant\",\n );\n setStep(\"tenant-selection\");\n });\n } else if (tenants.length > 1) {\n setStep(\"tenant-selection\");\n }\n }, [isAuthenticated, tenants, activeTenantId, onSwitchTenant]);\n\n useEffect(() => {\n if (config?.ui.direct_redirect)\n window.location.href = config.ui.direct_redirect;\n }, [config]);\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 prop */\n }\n },\n [identifier, password, onLogin, onFetchCurrentUser, onClearError],\n );\n\n const handleTenantSelect = useCallback(\n async (id: string) => {\n setTenantError(null);\n try {\n await onSwitchTenant(id);\n } catch (err) {\n setTenantError(\n normalizeError(err).message || \"Failed to select tenant\",\n );\n }\n },\n [onSwitchTenant],\n );\n\n return {\n step,\n identifier,\n setIdentifier,\n password,\n setPassword,\n tenantError,\n handleCredentialSubmit,\n handleTenantSelect,\n };\n}\n\nexport function Login(props: LoginProps): React.ReactElement | null {\n const { config, isLoading, error, tenants } = props;\n const flow = useLoginFlow(props);\n\n if (config?.ui.direct_redirect) {\n return <RedirectingMessage />;\n }\n\n if (flow.step === \"tenant-selection\" && tenants) {\n return (\n <TenantSelection\n tenants={tenants}\n isLoading={isLoading}\n error={flow.tenantError}\n onSelect={flow.handleTenantSelect}\n />\n );\n }\n\n return (\n <CredentialPage\n config={config}\n error={error}\n isLoading={isLoading}\n flow={flow}\n />\n );\n}\n\nfunction RedirectingMessage() {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <p className=\"text-muted-foreground\">Redirecting to authentication...</p>\n </div>\n );\n}\n\ninterface CredentialPageProps {\n config: AuthConfig | null;\n error: string | null;\n isLoading: boolean;\n flow: ReturnType<typeof useLoginFlow>;\n}\n\nfunction CredentialPage({\n config,\n error,\n isLoading,\n flow,\n}: CredentialPageProps) {\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-slate-100\">\n <div className=\"w-full max-w-md bg-white rounded-lg shadow-lg p-8\">\n <h1 className=\"text-center mb-6\">Sign In</h1>\n\n {error && (\n <Alert className=\"mb-4 border-red-200 bg-red-50 text-red-700\">\n <p className=\"text-sm\">{error}</p>\n </Alert>\n )}\n\n {config?.features.local_auth && (\n <CredentialForm\n identifier={flow.identifier}\n password={flow.password}\n isLoading={isLoading}\n onIdentifierChange={flow.setIdentifier}\n onPasswordChange={flow.setPassword}\n onSubmit={flow.handleCredentialSubmit}\n />\n )}\n\n {config?.features.oauth && config.oauth?.providers && (\n <OAuthProviders providers={config.oauth.providers} />\n )}\n </div>\n </div>\n );\n}\n\n// -- Sub-components extracted to reduce Login complexity --\n\ninterface CredentialFormProps {\n identifier: string;\n password: string;\n isLoading: boolean;\n onIdentifierChange: (value: string) => void;\n onPasswordChange: (value: string) => void;\n onSubmit: (e: React.FormEvent) => void;\n}\n\nfunction CredentialForm({\n identifier,\n password,\n isLoading,\n onIdentifierChange,\n onPasswordChange,\n onSubmit,\n}: CredentialFormProps) {\n return (\n <form onSubmit={onSubmit} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"identifier\">Email or Username</Label>\n <Input\n id=\"identifier\"\n type=\"text\"\n value={identifier}\n onChange={(e) => onIdentifierChange(e.target.value)}\n required\n placeholder=\"Enter your email or username\"\n />\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n value={password}\n onChange={(e) => onPasswordChange(e.target.value)}\n required\n placeholder=\"Enter your password\"\n />\n </div>\n\n <Button type=\"submit\" disabled={isLoading} className=\"w-full\">\n {isLoading ? \"Signing in...\" : \"Sign In\"}\n </Button>\n </form>\n );\n}\n\ninterface OAuthProvidersProps {\n providers: Record<\n string,\n { enabled: boolean; name: string; login_url: string }\n >;\n}\n\nfunction OAuthProviders({ providers }: OAuthProvidersProps) {\n return (\n <div className=\"mt-6\">\n <div className=\"relative mb-4\">\n <div className=\"absolute inset-0 flex items-center\">\n <div className=\"w-full border-t border-slate-300\"></div>\n </div>\n <div className=\"relative flex justify-center text-sm\">\n <span className=\"px-2 bg-white text-muted-foreground\">\n Or continue with\n </span>\n </div>\n </div>\n\n <div className=\"space-y-2\">\n {Object.entries(providers).map(([key, provider]) =>\n provider.enabled ? (\n <a\n key={key}\n href={provider.login_url}\n className=\"w-full flex items-center justify-center py-2 px-4 border border-slate-300 rounded-md hover:bg-slate-50\"\n >\n Continue with {provider.name}\n </a>\n ) : null,\n )}\n </div>\n </div>\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 * TenantSelection -- shown after authentication when the user has\n * multiple tenants and no default is set. The user must pick a tenant\n * before the app loads.\n *\n * Fully props-driven with no app-specific imports.\n * Target package: @petrarca/sonnet-shell\n */\n\n/** Minimal tenant descriptor. Consumers map their domain type to this shape. */\nexport interface TenantOption {\n id: string;\n name: string;\n}\n\ninterface TenantSelectionProps {\n tenants: TenantOption[];\n isLoading: boolean;\n error: string | null;\n onSelect: (id: string) => void;\n}\n\nexport function TenantSelection({\n tenants,\n isLoading,\n error,\n onSelect,\n}: TenantSelectionProps): React.ReactElement {\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const handleContinue = useCallback(() => {\n if (selectedId) {\n onSelect(selectedId);\n }\n }, [selectedId, onSelect]);\n\n return (\n <div className=\"flex items-center justify-center min-h-screen bg-slate-100\">\n <div className=\"w-full max-w-md bg-white rounded-lg shadow-lg p-8\">\n <h1 className=\"text-center mb-2\">Select Tenant</h1>\n <p className=\"text-center text-sm text-muted-foreground mb-6\">\n Choose the tenant you want to work in.\n </p>\n\n {error && (\n <Alert className=\"mb-4 border-red-200 bg-red-50 text-red-700\">\n <p className=\"text-sm\">{error}</p>\n </Alert>\n )}\n\n <div className=\"space-y-2 mb-6 max-h-64 overflow-y-auto\">\n {tenants.map((tenant) => (\n <button\n key={tenant.id}\n onClick={() => setSelectedId(tenant.id)}\n className={`w-full flex items-center justify-between px-4 py-3 rounded-md border text-left text-sm transition-colors ${\n selectedId === tenant.id\n ? \"border-blue-500 bg-blue-50 text-blue-900\"\n : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n <span>{tenant.name}</span>\n {selectedId === tenant.id && (\n <Check className=\"h-4 w-4 text-blue-500\" />\n )}\n </button>\n ))}\n </div>\n\n <Button\n onClick={handleContinue}\n disabled={!selectedId || isLoading}\n className=\"w-full\"\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","Button","Alert","jsx","jsxs","useState","useCallback","Alert","Button","useLocation","useEffect","useState","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 { 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@petrarca/sonnet-shell",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Application shell, layout, navigation, auth flow, and imperative API for the Petrarca Sonnet component library",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -25,8 +25,8 @@
25
25
  "dependencies": {
26
26
  "lucide-react": "^0.553.0",
27
27
  "sonner": "^2.0.7",
28
- "@petrarca/sonnet-core": "0.5.0",
29
- "@petrarca/sonnet-ui": "0.5.0"
28
+ "@petrarca/sonnet-core": "0.6.0",
29
+ "@petrarca/sonnet-ui": "0.6.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": ">=19",