@petrarca/sonnet-shell 0.4.2 → 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,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