@petrarca/sonnet-shell 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,54 +1,66 @@
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 { Alert as Alert2 } from "@petrarca/sonnet-ui";
4
5
  import { Button as Button2 } from "@petrarca/sonnet-ui";
5
6
  import { Input } from "@petrarca/sonnet-ui";
6
7
  import { Label } from "@petrarca/sonnet-ui";
7
- import { Alert as Alert2 } from "@petrarca/sonnet-ui";
8
+ import { Spinner } from "@petrarca/sonnet-ui";
8
9
 
9
- // src/auth/TenantSelection.tsx
10
+ // src/auth/OrgSelection.tsx
10
11
  import { useState, useCallback } from "react";
11
12
  import { Check } from "lucide-react";
12
13
  import { Button } from "@petrarca/sonnet-ui";
13
14
  import { Alert } from "@petrarca/sonnet-ui";
14
- import { jsx, jsxs } from "react/jsx-runtime";
15
- function TenantSelection({
16
- tenants,
15
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
16
+ function OrgSelection({
17
+ orgs,
17
18
  isLoading,
18
19
  error,
19
- onSelect
20
+ onSelect,
21
+ emptyAction
20
22
  }) {
21
23
  const [selectedId, setSelectedId] = useState(null);
24
+ const isEmpty = orgs.length === 0;
22
25
  const handleContinue = useCallback(() => {
23
- if (selectedId) {
24
- onSelect(selectedId);
25
- }
26
+ if (selectedId) onSelect(selectedId);
26
27
  }, [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(
32
- "button",
33
- {
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"}`,
36
- children: [
37
- /* @__PURE__ */ jsx("span", { children: tenant.name }),
38
- selectedId === tenant.id && /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-blue-500" })
39
- ]
40
- },
41
- tenant.id
42
- )) }),
43
- /* @__PURE__ */ jsx(
44
- Button,
28
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center min-h-screen bg-muted px-4", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm bg-card rounded-lg shadow-sm border border-border p-8", children: [
29
+ /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
30
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold text-foreground text-center", children: "Select organization" }),
31
+ /* @__PURE__ */ jsx("p", { className: "text-center text-sm text-muted-foreground mt-1", children: isEmpty ? "No organizations assigned to your account." : "Choose the organization you want to work in." })
32
+ ] }),
33
+ error ? /* @__PURE__ */ jsx(
34
+ Alert,
45
35
  {
46
- onClick: handleContinue,
47
- disabled: !selectedId || isLoading,
48
- className: "w-full",
49
- children: isLoading ? "Loading..." : "Continue"
36
+ variant: "destructive",
37
+ className: "mb-4 bg-destructive/10 text-sm py-2.5 px-3",
38
+ children: error
50
39
  }
51
- )
40
+ ) : null,
41
+ isEmpty ? emptyAction ? /* @__PURE__ */ jsx("div", { className: "mt-2", children: emptyAction }) : null : /* @__PURE__ */ jsxs(Fragment, { children: [
42
+ /* @__PURE__ */ jsx("div", { className: "space-y-2 mb-6 max-h-64 overflow-y-auto", children: orgs.map((org) => /* @__PURE__ */ jsxs(
43
+ "button",
44
+ {
45
+ onClick: () => setSelectedId(org.id),
46
+ 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 ${selectedId === org.id ? "border-primary bg-state-selected text-foreground" : "border-border hover:bg-state-hover text-foreground"}`,
47
+ children: [
48
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: org.name }),
49
+ selectedId === org.id ? /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-primary shrink-0" }) : null
50
+ ]
51
+ },
52
+ org.id
53
+ )) }),
54
+ /* @__PURE__ */ jsx(
55
+ Button,
56
+ {
57
+ onClick: handleContinue,
58
+ disabled: !selectedId || isLoading,
59
+ className: "w-full h-10",
60
+ children: isLoading ? "Loading..." : "Continue"
61
+ }
62
+ )
63
+ ] })
52
64
  ] }) });
53
65
  }
54
66
 
@@ -60,43 +72,80 @@ function useLoginFlow(props) {
60
72
  config,
61
73
  isAuthenticated,
62
74
  activeTenantId,
63
- tenants,
75
+ orgs,
64
76
  onLogin,
65
77
  onFetchCurrentUser,
66
- onSwitchTenant,
67
- onClearError
78
+ onSelectOrg,
79
+ onClearError,
80
+ onDiscover,
81
+ onExternalLogin,
82
+ requiresOrgSelection = true
68
83
  } = props;
69
84
  const navigate = useNavigate();
70
85
  const location = useLocation();
71
86
  const locationState = location.state;
87
+ const identifierFirst = Boolean(onDiscover);
88
+ const requestedStep = requiresOrgSelection && locationState?.step ? locationState.step : void 0;
89
+ const initial = requestedStep ?? (identifierFirst ? "identifier" : "credentials");
72
90
  const [redirectTo] = useState2(() => locationState?.from?.pathname ?? "/");
73
- const [step, setStep] = useState2(
74
- () => locationState?.step ?? "credentials"
75
- );
91
+ const [step, setStep] = useState2(initial);
76
92
  const [identifier, setIdentifier] = useState2("");
77
93
  const [password, setPassword] = useState2("");
78
- const [tenantError, setTenantError] = useState2(null);
94
+ const [providers, setProviders] = useState2([]);
95
+ const [discoverError, setDiscoverError] = useState2(null);
96
+ const [isRedirecting, setIsRedirecting] = useState2(false);
97
+ const [orgError, setOrgError] = useState2(null);
79
98
  useEffect(() => {
80
- if (isAuthenticated && activeTenantId)
99
+ if (isAuthenticated && (activeTenantId || !requiresOrgSelection))
81
100
  navigate(redirectTo, { replace: true });
82
- }, [isAuthenticated, activeTenantId, navigate, redirectTo]);
101
+ }, [
102
+ isAuthenticated,
103
+ activeTenantId,
104
+ requiresOrgSelection,
105
+ navigate,
106
+ redirectTo
107
+ ]);
83
108
  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"
109
+ if (!requiresOrgSelection) return;
110
+ if (!isAuthenticated || !orgs || activeTenantId) return;
111
+ if (orgs.length === 1) {
112
+ onSelectOrg(orgs[0].id).catch((err) => {
113
+ setOrgError(
114
+ normalizeError(err).message || "Failed to select organization"
89
115
  );
90
- setStep("tenant-selection");
116
+ setStep("org-selection");
91
117
  });
92
- } else if (tenants.length > 1) {
93
- setStep("tenant-selection");
118
+ } else if (orgs.length > 1) {
119
+ setStep("org-selection");
94
120
  }
95
- }, [isAuthenticated, tenants, activeTenantId, onSwitchTenant]);
121
+ }, [
122
+ requiresOrgSelection,
123
+ isAuthenticated,
124
+ orgs,
125
+ activeTenantId,
126
+ onSelectOrg
127
+ ]);
96
128
  useEffect(() => {
97
129
  if (config?.ui.direct_redirect)
98
130
  window.location.href = config.ui.direct_redirect;
99
131
  }, [config]);
132
+ const handleIdentifierSubmit = useCallback2(
133
+ async (e) => {
134
+ e.preventDefault();
135
+ if (!onDiscover || !identifier.trim()) return;
136
+ setDiscoverError(null);
137
+ try {
138
+ const resolved = await onDiscover(identifier.trim());
139
+ setProviders(resolved);
140
+ setStep("auth");
141
+ } catch (err) {
142
+ setDiscoverError(
143
+ normalizeError(err).message || "Failed to look up providers"
144
+ );
145
+ }
146
+ },
147
+ [identifier, onDiscover]
148
+ );
100
149
  const handleCredentialSubmit = useCallback2(
101
150
  async (e) => {
102
151
  e.preventDefault();
@@ -109,94 +158,176 @@ function useLoginFlow(props) {
109
158
  },
110
159
  [identifier, password, onLogin, onFetchCurrentUser, onClearError]
111
160
  );
112
- const handleTenantSelect = useCallback2(
161
+ const handleExternalLogin = useCallback2(
162
+ async (provider) => {
163
+ if (!onExternalLogin) return;
164
+ setIsRedirecting(true);
165
+ try {
166
+ await onExternalLogin(provider);
167
+ } catch {
168
+ setIsRedirecting(false);
169
+ }
170
+ },
171
+ [onExternalLogin]
172
+ );
173
+ const handleOrgSelect = useCallback2(
113
174
  async (id) => {
114
- setTenantError(null);
175
+ setOrgError(null);
115
176
  try {
116
- await onSwitchTenant(id);
177
+ await onSelectOrg(id);
117
178
  } catch (err) {
118
- setTenantError(
119
- normalizeError(err).message || "Failed to select tenant"
179
+ setOrgError(
180
+ normalizeError(err).message || "Failed to select organization"
120
181
  );
121
182
  }
122
183
  },
123
- [onSwitchTenant]
184
+ [onSelectOrg]
124
185
  );
186
+ const goBack = useCallback2(() => {
187
+ setPassword("");
188
+ setDiscoverError(null);
189
+ onClearError();
190
+ setStep(identifierFirst ? "identifier" : "credentials");
191
+ }, [identifierFirst, onClearError]);
125
192
  return {
126
193
  step,
127
194
  identifier,
128
195
  setIdentifier,
129
196
  password,
130
197
  setPassword,
131
- tenantError,
198
+ providers,
199
+ discoverError,
200
+ isRedirecting,
201
+ orgError,
202
+ handleIdentifierSubmit,
132
203
  handleCredentialSubmit,
133
- handleTenantSelect
204
+ handleExternalLogin,
205
+ handleOrgSelect,
206
+ goBack
134
207
  };
135
208
  }
209
+ function resolveScreen(args) {
210
+ if (args.directRedirect || args.isRedirecting) return "redirecting";
211
+ const canShowOrgPicker = args.requiresOrgSelection && args.step === "org-selection" && args.orgsLoaded;
212
+ if (args.isAuthenticated && !args.activeTenantId && !canShowOrgPicker)
213
+ return "redirecting";
214
+ return canShowOrgPicker ? "org-selection" : "form";
215
+ }
136
216
  function Login(props) {
137
- const { config, isLoading, error, tenants } = props;
217
+ const {
218
+ config,
219
+ isAuthenticated,
220
+ activeTenantId,
221
+ isLoading,
222
+ isDiscovering,
223
+ error,
224
+ orgs,
225
+ appName,
226
+ logoUrl,
227
+ orgEmptyAction,
228
+ requiresOrgSelection = true
229
+ } = props;
138
230
  const flow = useLoginFlow(props);
139
- if (config?.ui.direct_redirect) {
140
- return /* @__PURE__ */ jsx2(RedirectingMessage, {});
141
- }
142
- if (flow.step === "tenant-selection" && tenants) {
231
+ const screen = resolveScreen({
232
+ isRedirecting: flow.isRedirecting,
233
+ directRedirect: Boolean(config?.ui.direct_redirect),
234
+ isAuthenticated,
235
+ activeTenantId,
236
+ requiresOrgSelection,
237
+ step: flow.step,
238
+ orgsLoaded: orgs !== null
239
+ });
240
+ if (screen === "redirecting") return /* @__PURE__ */ jsx2(RedirectingMessage, {});
241
+ if (screen === "org-selection") {
143
242
  return /* @__PURE__ */ jsx2(
144
- TenantSelection,
243
+ OrgSelection,
145
244
  {
146
- tenants,
245
+ orgs: orgs ?? [],
147
246
  isLoading,
148
- error: flow.tenantError,
149
- onSelect: flow.handleTenantSelect
247
+ error: flow.orgError,
248
+ onSelect: flow.handleOrgSelect,
249
+ emptyAction: orgEmptyAction
150
250
  }
151
251
  );
152
252
  }
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,
253
+ return /* @__PURE__ */ jsx2(PageShell, { children: /* @__PURE__ */ jsxs2(Card, { children: [
254
+ flow.step === "identifier" && /* @__PURE__ */ jsx2(
255
+ IdentifierStep,
177
256
  {
257
+ appName,
258
+ logoUrl,
178
259
  identifier: flow.identifier,
260
+ error: flow.discoverError,
261
+ isLoading: isDiscovering ?? false,
262
+ onIdentifierChange: flow.setIdentifier,
263
+ onSubmit: flow.handleIdentifierSubmit
264
+ }
265
+ ),
266
+ flow.step === "auth" && /* @__PURE__ */ jsx2(
267
+ AuthStep,
268
+ {
269
+ appName,
270
+ logoUrl,
271
+ identifier: flow.identifier,
272
+ providers: flow.providers,
179
273
  password: flow.password,
274
+ error,
180
275
  isLoading,
181
- onIdentifierChange: flow.setIdentifier,
182
276
  onPasswordChange: flow.setPassword,
183
- onSubmit: flow.handleCredentialSubmit
277
+ onCredentialSubmit: flow.handleCredentialSubmit,
278
+ onExternalLogin: flow.handleExternalLogin,
279
+ onBack: flow.goBack
184
280
  }
185
281
  ),
186
- config?.features.oauth && config.oauth?.providers && /* @__PURE__ */ jsx2(OAuthProviders, { providers: config.oauth.providers })
282
+ flow.step === "credentials" && /* @__PURE__ */ jsx2(
283
+ CredentialsStep,
284
+ {
285
+ appName,
286
+ logoUrl,
287
+ config,
288
+ identifier: flow.identifier,
289
+ password: flow.password,
290
+ error,
291
+ isLoading,
292
+ onIdentifierChange: flow.setIdentifier,
293
+ onPasswordChange: flow.setPassword,
294
+ onSubmit: flow.handleCredentialSubmit,
295
+ onExternalLogin: flow.handleExternalLogin
296
+ }
297
+ )
187
298
  ] }) });
188
299
  }
189
- function CredentialForm({
300
+ function PageShell({ children }) {
301
+ return /* @__PURE__ */ jsx2("div", { className: "flex flex-col items-center justify-center min-h-screen bg-muted px-4", children });
302
+ }
303
+ function Card({ children }) {
304
+ return /* @__PURE__ */ jsx2("div", { className: "w-full max-w-sm bg-card rounded-lg border border-border shadow-sm p-8", children });
305
+ }
306
+ function BrandHeader({
307
+ appName,
308
+ logoUrl
309
+ }) {
310
+ if (!appName && !logoUrl) return null;
311
+ 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-muted-foreground tracking-wide", children: appName }) });
312
+ }
313
+ function IdentifierStep({
314
+ appName,
315
+ logoUrl,
190
316
  identifier,
191
- password,
317
+ error,
192
318
  isLoading,
193
319
  onIdentifierChange,
194
- onPasswordChange,
195
320
  onSubmit
196
321
  }) {
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" }),
322
+ return /* @__PURE__ */ jsxs2("form", { onSubmit, className: "space-y-6", children: [
323
+ /* @__PURE__ */ jsx2(BrandHeader, { appName, logoUrl }),
324
+ /* @__PURE__ */ jsxs2("div", { className: "text-center", children: [
325
+ /* @__PURE__ */ jsx2("h1", { className: "text-2xl font-semibold text-foreground leading-tight", children: "Welcome" }),
326
+ /* @__PURE__ */ jsx2("p", { className: "mt-2 text-sm text-muted-foreground", children: "Enter your email or username to continue." })
327
+ ] }),
328
+ error ? /* @__PURE__ */ jsx2(ErrorAlert, { message: error }) : null,
329
+ /* @__PURE__ */ jsxs2("div", { children: [
330
+ /* @__PURE__ */ jsx2(Label, { htmlFor: "identifier", className: "mb-1.5 block", children: "Email or username" }),
200
331
  /* @__PURE__ */ jsx2(
201
332
  Input,
202
333
  {
@@ -205,55 +336,239 @@ function CredentialForm({
205
336
  value: identifier,
206
337
  onChange: (e) => onIdentifierChange(e.target.value),
207
338
  required: true,
208
- placeholder: "Enter your email or username"
339
+ autoFocus: true,
340
+ autoComplete: "email",
341
+ placeholder: "you@company.com"
209
342
  }
210
343
  )
211
344
  ] }),
212
- /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
213
- /* @__PURE__ */ jsx2(Label, { htmlFor: "password", children: "Password" }),
345
+ /* @__PURE__ */ jsx2(
346
+ PrimaryButton,
347
+ {
348
+ type: "submit",
349
+ disabled: isLoading || !identifier.trim(),
350
+ loading: isLoading,
351
+ loadingText: "Looking up...",
352
+ children: "Continue"
353
+ }
354
+ )
355
+ ] });
356
+ }
357
+ function AuthStep({
358
+ appName,
359
+ logoUrl,
360
+ identifier,
361
+ providers,
362
+ password,
363
+ error,
364
+ isLoading,
365
+ onPasswordChange,
366
+ onCredentialSubmit,
367
+ onExternalLogin,
368
+ onBack
369
+ }) {
370
+ const external = providers.filter((p) => p.type === "external");
371
+ const hasLocal = providers.some((p) => p.type === "local");
372
+ const hasBoth = external.length > 0 && hasLocal;
373
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-6", children: [
374
+ /* @__PURE__ */ jsx2(BrandHeader, { appName, logoUrl }),
375
+ /* @__PURE__ */ jsxs2("div", { children: [
376
+ /* @__PURE__ */ jsxs2(
377
+ "button",
378
+ {
379
+ type: "button",
380
+ onClick: onBack,
381
+ className: "flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors mb-4",
382
+ children: [
383
+ /* @__PURE__ */ jsx2("span", { "aria-hidden": true, children: "\u2190" }),
384
+ " Back"
385
+ ]
386
+ }
387
+ ),
388
+ /* @__PURE__ */ jsx2("h1", { className: "text-2xl font-semibold text-foreground leading-tight", children: "Welcome back" }),
389
+ /* @__PURE__ */ jsx2("p", { className: "mt-1 text-sm text-muted-foreground", children: identifier })
390
+ ] }),
391
+ error ? /* @__PURE__ */ jsx2(ErrorAlert, { message: error }) : null,
392
+ hasLocal ? /* @__PURE__ */ jsxs2("form", { onSubmit: onCredentialSubmit, className: "space-y-4", children: [
393
+ /* @__PURE__ */ jsxs2("div", { children: [
394
+ /* @__PURE__ */ jsx2(Label, { htmlFor: "password", className: "mb-1.5 block", children: "Password" }),
395
+ /* @__PURE__ */ jsx2(
396
+ Input,
397
+ {
398
+ id: "password",
399
+ type: "password",
400
+ value: password,
401
+ onChange: (e) => onPasswordChange(e.target.value),
402
+ required: true,
403
+ autoFocus: true,
404
+ autoComplete: "current-password",
405
+ placeholder: "Enter your password"
406
+ }
407
+ )
408
+ ] }),
214
409
  /* @__PURE__ */ jsx2(
215
- Input,
410
+ PrimaryButton,
216
411
  {
217
- id: "password",
218
- type: "password",
219
- value: password,
220
- onChange: (e) => onPasswordChange(e.target.value),
221
- required: true,
222
- placeholder: "Enter your password"
412
+ type: "submit",
413
+ disabled: isLoading,
414
+ loading: isLoading,
415
+ loadingText: "Signing in...",
416
+ children: "Sign in"
223
417
  }
224
418
  )
225
- ] }),
226
- /* @__PURE__ */ jsx2(Button2, { type: "submit", disabled: isLoading, className: "w-full", children: isLoading ? "Signing in..." : "Sign In" })
419
+ ] }) : null,
420
+ hasBoth ? /* @__PURE__ */ jsx2(Separator, {}) : null,
421
+ external.length > 0 ? /* @__PURE__ */ jsx2("div", { className: "space-y-2", children: external.map((p) => /* @__PURE__ */ jsx2(
422
+ SsoButton,
423
+ {
424
+ label: p.label,
425
+ onClick: () => onExternalLogin(p)
426
+ },
427
+ p.name
428
+ )) }) : null,
429
+ providers.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "text-sm text-muted-foreground text-center py-2", children: "No login methods available." }) : null
227
430
  ] });
228
431
  }
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",
432
+ function resolveOAuthProviders(config) {
433
+ return config?.features.oauth && config.oauth?.providers ? Object.entries(config.oauth.providers).filter(([, p]) => p.enabled) : [];
434
+ }
435
+ function CredentialsStep({
436
+ appName,
437
+ logoUrl,
438
+ config,
439
+ identifier,
440
+ password,
441
+ error,
442
+ isLoading,
443
+ onIdentifierChange,
444
+ onPasswordChange,
445
+ onSubmit,
446
+ onExternalLogin
447
+ }) {
448
+ const oauthProviders = resolveOAuthProviders(config);
449
+ const hasBoth = config?.features.local_auth && oauthProviders.length > 0;
450
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-6", children: [
451
+ /* @__PURE__ */ jsx2(BrandHeader, { appName, logoUrl }),
452
+ /* @__PURE__ */ jsx2("h1", { className: "text-2xl font-semibold text-foreground leading-tight text-center", children: "Welcome" }),
453
+ error ? /* @__PURE__ */ jsx2(ErrorAlert, { message: error }) : null,
454
+ config?.features.local_auth ? /* @__PURE__ */ jsxs2("form", { onSubmit, className: "space-y-4", children: [
455
+ /* @__PURE__ */ jsxs2("div", { children: [
456
+ /* @__PURE__ */ jsx2(Label, { htmlFor: "identifier", className: "mb-1.5 block", children: "Email or username" }),
457
+ /* @__PURE__ */ jsx2(
458
+ Input,
459
+ {
460
+ id: "identifier",
461
+ type: "text",
462
+ value: identifier,
463
+ onChange: (e) => onIdentifierChange(e.target.value),
464
+ required: true,
465
+ autoFocus: true,
466
+ autoComplete: "email",
467
+ placeholder: "you@company.com"
468
+ }
469
+ )
470
+ ] }),
471
+ /* @__PURE__ */ jsxs2("div", { children: [
472
+ /* @__PURE__ */ jsx2(Label, { htmlFor: "password", className: "mb-1.5 block", children: "Password" }),
473
+ /* @__PURE__ */ jsx2(
474
+ Input,
475
+ {
476
+ id: "password",
477
+ type: "password",
478
+ value: password,
479
+ onChange: (e) => onPasswordChange(e.target.value),
480
+ required: true,
481
+ autoComplete: "current-password",
482
+ placeholder: "Enter your password"
483
+ }
484
+ )
485
+ ] }),
486
+ /* @__PURE__ */ jsx2(
487
+ PrimaryButton,
238
488
  {
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
- ) })
489
+ type: "submit",
490
+ disabled: isLoading,
491
+ loading: isLoading,
492
+ loadingText: "Signing in...",
493
+ children: "Sign in"
494
+ }
495
+ )
496
+ ] }) : null,
497
+ hasBoth ? /* @__PURE__ */ jsx2(Separator, {}) : null,
498
+ oauthProviders.length > 0 ? /* @__PURE__ */ jsx2("div", { className: "space-y-2", children: oauthProviders.map(([key, p]) => /* @__PURE__ */ jsx2(
499
+ SsoButton,
500
+ {
501
+ label: `Continue with ${p.name}`,
502
+ onClick: () => onExternalLogin({ type: "external", name: key, label: p.name })
503
+ },
504
+ key
505
+ )) }) : null
506
+ ] });
507
+ }
508
+ function RedirectingMessage() {
509
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center min-h-screen bg-muted gap-3 text-muted-foreground", children: [
510
+ /* @__PURE__ */ jsx2(Spinner, { size: "lg" }),
511
+ /* @__PURE__ */ jsx2("p", { className: "text-sm", children: "Redirecting..." })
512
+ ] });
513
+ }
514
+ function ErrorAlert({ message }) {
515
+ return /* @__PURE__ */ jsx2(
516
+ Alert2,
517
+ {
518
+ variant: "destructive",
519
+ className: "bg-destructive/10 px-3 py-2.5 text-sm",
520
+ children: message
521
+ }
522
+ );
523
+ }
524
+ function Separator() {
525
+ return /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
526
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0 flex items-center", children: /* @__PURE__ */ jsx2("div", { className: "w-full border-t border-border" }) }),
527
+ /* @__PURE__ */ jsx2("div", { className: "relative flex justify-center text-xs", children: /* @__PURE__ */ jsx2("span", { className: "bg-card px-2 text-muted-foreground", children: "or continue with" }) })
249
528
  ] });
250
529
  }
530
+ function PrimaryButton({
531
+ type = "button",
532
+ disabled,
533
+ loading,
534
+ loadingText,
535
+ children,
536
+ onClick
537
+ }) {
538
+ return /* @__PURE__ */ jsxs2(
539
+ Button2,
540
+ {
541
+ type,
542
+ size: "lg",
543
+ disabled,
544
+ onClick,
545
+ className: "w-full",
546
+ children: [
547
+ loading ? /* @__PURE__ */ jsx2(Spinner, { size: "sm" }) : null,
548
+ loading && loadingText ? loadingText : children
549
+ ]
550
+ }
551
+ );
552
+ }
553
+ function SsoButton({ label, onClick }) {
554
+ return /* @__PURE__ */ jsx2(
555
+ Button2,
556
+ {
557
+ type: "button",
558
+ variant: "outline",
559
+ size: "lg",
560
+ onClick,
561
+ className: "w-full",
562
+ children: label
563
+ }
564
+ );
565
+ }
251
566
 
252
567
  // src/auth/ProtectedRoute.tsx
253
568
  import { Navigate, useLocation as useLocation2 } from "react-router-dom";
254
569
  import { useEffect as useEffect2, useRef, useState as useState3 } from "react";
255
- import { Spinner } from "@petrarca/sonnet-ui";
256
- import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
570
+ import { Spinner as Spinner2 } from "@petrarca/sonnet-ui";
571
+ import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
257
572
  function ProtectedRoute({
258
573
  children,
259
574
  authEnabled,
@@ -261,7 +576,8 @@ function ProtectedRoute({
261
576
  isAuthenticated,
262
577
  accessToken,
263
578
  activeTenantId,
264
- initializeAuth
579
+ initializeAuth,
580
+ requiresOrgSelection = true
265
581
  }) {
266
582
  const location = useLocation2();
267
583
  const [initializing, setInitializing] = useState3(true);
@@ -270,10 +586,10 @@ function ProtectedRoute({
270
586
  initRef.current().finally(() => setInitializing(false));
271
587
  }, []);
272
588
  if (authConfigLoading || initializing) {
273
- return /* @__PURE__ */ jsx3("div", { className: "flex items-center justify-center min-h-screen", children: /* @__PURE__ */ jsx3(Spinner, { size: "lg" }) });
589
+ return /* @__PURE__ */ jsx3("div", { className: "flex items-center justify-center min-h-screen", children: /* @__PURE__ */ jsx3(Spinner2, { size: "lg" }) });
274
590
  }
275
591
  if (!authEnabled) {
276
- return /* @__PURE__ */ jsx3(Fragment, { children });
592
+ return /* @__PURE__ */ jsx3(Fragment2, { children });
277
593
  }
278
594
  if (!isAuthenticated || !accessToken) {
279
595
  return /* @__PURE__ */ jsx3(
@@ -285,21 +601,21 @@ function ProtectedRoute({
285
601
  }
286
602
  );
287
603
  }
288
- if (!activeTenantId) {
604
+ if (requiresOrgSelection && !activeTenantId) {
289
605
  return /* @__PURE__ */ jsx3(
290
606
  Navigate,
291
607
  {
292
608
  to: "/login",
293
- state: { from: location, step: "tenant-selection" },
609
+ state: { from: location, step: "org-selection" },
294
610
  replace: true
295
611
  }
296
612
  );
297
613
  }
298
- return /* @__PURE__ */ jsx3(Fragment, { children });
614
+ return /* @__PURE__ */ jsx3(Fragment2, { children });
299
615
  }
300
616
  export {
301
617
  Login,
302
- ProtectedRoute,
303
- TenantSelection
618
+ OrgSelection,
619
+ ProtectedRoute
304
620
  };
305
621
  //# sourceMappingURL=index.js.map