@elvix.is/sdk 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.d.ts CHANGED
@@ -20,11 +20,59 @@ declare function ElvixCard({ title, footer, className, children, }: {
20
20
  children: ReactNode;
21
21
  }): react.JSX.Element;
22
22
 
23
+ /**
24
+ * Editable sign-in copy.
25
+ *
26
+ * Every user-facing string in the sign-in surface is overridable. Precedence,
27
+ * lowest to highest:
28
+ *
29
+ * built-in English defaults < Console-configured (bootstrap `strings`) < `copy` prop
30
+ *
31
+ * So an integrating developer edits copy in the elvix Console (no redeploy) and
32
+ * can still override per-embed in code. `title` and `submitButton` are left out
33
+ * of the defaults because their built-in value depends on the Application's
34
+ * sign-in verb ("Sign in" vs "Log in"); the component fills them from the verb
35
+ * when neither Console nor prop sets them.
36
+ *
37
+ * Strings may contain `{app}` / `{email}` tokens; `fillCopy` interpolates them.
38
+ */
39
+ type ElvixCopy = {
40
+ /** Heading. Token: {app}. Built-in: "Sign in to {app}" / "Log in to {app}". */
41
+ title?: string;
42
+ /** Subtitle under the heading. */
43
+ subtitle?: string;
44
+ /** Google factor button. */
45
+ googleButton?: string;
46
+ /** Email field placeholder. */
47
+ emailPlaceholder?: string;
48
+ /** Email submit button (identify step). */
49
+ sendCodeButton?: string;
50
+ /** Email submit button while the request is in flight. */
51
+ sendingLabel?: string;
52
+ /** Code step subtitle. Token: {email}. */
53
+ codeSentSubtitle?: string;
54
+ /** OTP field placeholder. */
55
+ codePlaceholder?: string;
56
+ /** OTP submit button. Built-in: the sign-in verb. */
57
+ submitButton?: string;
58
+ /** OTP submit button while verifying. */
59
+ verifyingLabel?: string;
60
+ /** Terminal "done" pane text. */
61
+ signedInText?: string;
62
+ /** Validation: empty email. */
63
+ errorEnterEmail?: string;
64
+ /** Validation: code not 6 digits. */
65
+ errorEnterCode?: string;
66
+ };
67
+ /** Built-in English defaults for the verb-independent strings. */
68
+ declare const DEFAULT_COPY: ElvixCopy;
69
+
23
70
  /**
24
71
  * Public types for the React surface. Mirrors the elvix.is bootstrap
25
72
  * envelope so customers can type their host code without importing
26
73
  * private elvix internals.
27
74
  */
75
+
28
76
  type ElvixBrand = {
29
77
  light: {
30
78
  primary: string;
@@ -60,6 +108,12 @@ type ElvixBootstrapEnvelope = {
60
108
  };
61
109
  signInVerb: "signin" | "login";
62
110
  signinGate: "public" | "private_beta" | "closed";
111
+ /**
112
+ * Console-configured sign-in copy overrides. Any subset of the strings the
113
+ * sign-in surface renders; missing keys fall back to the built-in English
114
+ * defaults. A `copy` prop on the component overrides these in turn.
115
+ */
116
+ strings?: Partial<ElvixCopy>;
63
117
  };
64
118
  type ElvixSignInResultOk = {
65
119
  ok: true;
@@ -119,13 +173,63 @@ declare function ElvixProvider({ clientId, theme, brand, baseUrl, children, clas
119
173
  * exposes. Hosts navigate from the callback; this component never
120
174
  * calls `router.push` itself.
121
175
  */
122
- declare function ElvixSignIn({ onResult, redirectAfterSignIn, className, }: {
176
+ declare function ElvixSignIn({ onResult, redirectAfterSignIn, copy: copyProp, className, }: {
123
177
  onResult?: (r: ElvixSignInResult) => void;
124
178
  /** Default redirect target on success when the server doesn't echo one. */
125
179
  redirectAfterSignIn?: string;
180
+ /**
181
+ * Thin per-embed copy override. The primary way to edit copy is the elvix
182
+ * Console (served live in the bootstrap `strings`); this prop just lets a
183
+ * single embed tweak a string or two without a Console change.
184
+ */
185
+ copy?: Partial<ElvixCopy>;
126
186
  className?: string;
127
187
  }): react.JSX.Element;
128
188
 
189
+ type ElvixSignInButtonSize = "sm" | "md" | "lg";
190
+ type ElvixSignInButtonTheme = "light" | "dark" | "auto";
191
+ type ElvixSignInButtonVariant = "filled" | "filled-black" | "white" | "outline" | "ghost";
192
+ type ElvixSignInButtonShape = "rectangle" | "pill" | "square" | "circle";
193
+ type ElvixSignInButtonType = "standard" | "icon";
194
+ type ElvixSignInButtonMode = "redirect" | "callback" | "embed";
195
+ type ElvixSignInPreset = "sign-in-with-elvix" | "continue-with-elvix" | "sign-up-with-elvix" | "sign-in" | "log-in" | "continue";
196
+ type ElvixSignInButtonProps = {
197
+ clientId?: string;
198
+ /** elvix origin for redirect mode. Defaults to https://elvix.is. */
199
+ baseUrl?: string;
200
+ returnUrl?: string;
201
+ type?: ElvixSignInButtonType;
202
+ variant?: ElvixSignInButtonVariant;
203
+ shape?: ElvixSignInButtonShape;
204
+ size?: ElvixSignInButtonSize;
205
+ theme?: ElvixSignInButtonTheme;
206
+ preset?: ElvixSignInPreset;
207
+ label?: string;
208
+ className?: string;
209
+ href?: string;
210
+ mode?: ElvixSignInButtonMode;
211
+ onClick?: () => void;
212
+ /** Terminal outcome of mode="embed": success (with token) or error. */
213
+ onResult?: (result: ElvixSignInResult) => void;
214
+ };
215
+ declare function ElvixSignInButton({ clientId, baseUrl, returnUrl, type, variant, shape, size, theme, preset, label, className, href, mode, onClick, onResult, }: ElvixSignInButtonProps): react.JSX.Element;
216
+
217
+ type ElvixSecuredBadgeVariant = "white" | "dark" | "outline";
218
+ type ElvixSecuredBadgeSize = "sm" | "md" | "lg";
219
+ type ElvixSecuredBadgeTheme = "light" | "dark";
220
+ type ElvixSecuredBadgeProps = {
221
+ variant?: ElvixSecuredBadgeVariant;
222
+ size?: ElvixSecuredBadgeSize;
223
+ /** Which side of the colour wheel the host page is on (for the outline variant). */
224
+ theme?: ElvixSecuredBadgeTheme;
225
+ /** Active-protection dot colour. Defaults to brand lavender. */
226
+ accentColor?: string;
227
+ /** Where the badge links. Defaults to elvix. */
228
+ href?: string;
229
+ className?: string;
230
+ };
231
+ declare function ElvixSecuredBadge({ variant, size, theme, accentColor, href, className, }: ElvixSecuredBadgeProps): react.JSX.Element;
232
+
129
233
  /**
130
234
  * Session token store for cross-origin SDK use.
131
235
  *
@@ -280,4 +384,4 @@ declare function ElvixLegalEntities({ onResult, }: {
280
384
  onResult?: (r: ElvixActionResult) => void;
281
385
  }): react.JSX.Element;
282
386
 
283
- export { ElvixActionResult, ElvixAddressBook, ElvixAvatar, ElvixBanner, type ElvixBootstrapEnvelope, type ElvixBrand, ElvixCard, ElvixDeactivate, ElvixExport, ElvixIdentityForm, ElvixLanguages, ElvixLeave, ElvixLegalEntities, ElvixLifecycleWatcher, ElvixProvider, ElvixRegion, ElvixSessions, ElvixSignIn, type ElvixSignInMethod, type ElvixSignInResult, type ElvixSignInResultErr, type ElvixSignInResultOk, type ElvixTheme, ElvixUsername, type UseUserListResult, getElvixToken, setElvixToken, useElvixApp, useElvixContext, useUserMemberships, useUserRoles, useUserScopes };
387
+ export { DEFAULT_COPY, ElvixActionResult, ElvixAddressBook, ElvixAvatar, ElvixBanner, type ElvixBootstrapEnvelope, type ElvixBrand, ElvixCard, type ElvixCopy, ElvixDeactivate, ElvixExport, ElvixIdentityForm, ElvixLanguages, ElvixLeave, ElvixLegalEntities, ElvixLifecycleWatcher, ElvixProvider, ElvixRegion, ElvixSecuredBadge, ElvixSessions, ElvixSignIn, ElvixSignInButton, type ElvixSignInMethod, type ElvixSignInResult, type ElvixSignInResultErr, type ElvixSignInResultOk, type ElvixTheme, ElvixUsername, type UseUserListResult, getElvixToken, setElvixToken, useElvixApp, useElvixContext, useUserMemberships, useUserRoles, useUserScopes };
package/dist/react.js CHANGED
@@ -167,6 +167,42 @@ function withAlpha(hex, a) {
167
167
  // src/react/elvix-sign-in.tsx
168
168
  import { useState as useState2 } from "react";
169
169
 
170
+ // src/react/copy.ts
171
+ var DEFAULT_COPY = {
172
+ subtitle: "Pick how you want to continue.",
173
+ googleButton: "Continue with Google",
174
+ emailPlaceholder: "you@example.com",
175
+ sendCodeButton: "Send code",
176
+ sendingLabel: "Sending\u2026",
177
+ codeSentSubtitle: "We sent a 6-digit code to {email}.",
178
+ codePlaceholder: "123456",
179
+ verifyingLabel: "Verifying\u2026",
180
+ signedInText: "Signed in.",
181
+ errorEnterEmail: "Enter an email.",
182
+ errorEnterCode: "Enter the 6-digit code."
183
+ };
184
+ function resolveCopy(bootstrap, prop) {
185
+ return {
186
+ ...DEFAULT_COPY,
187
+ ...stripUndefined(bootstrap),
188
+ ...stripUndefined(prop)
189
+ };
190
+ }
191
+ function fillCopy(template, tokens) {
192
+ return template.replace(
193
+ /\{(\w+)\}/g,
194
+ (whole, key) => key in tokens ? tokens[key] : whole
195
+ );
196
+ }
197
+ function stripUndefined(o) {
198
+ if (!o) return {};
199
+ const out = {};
200
+ for (const [k, v] of Object.entries(o)) {
201
+ if (v !== void 0) out[k] = v;
202
+ }
203
+ return out;
204
+ }
205
+
170
206
  // src/react/session.ts
171
207
  var STORAGE_KEY = "elvix.session.token";
172
208
  var memToken = null;
@@ -193,15 +229,26 @@ function authInit() {
193
229
  const token = getElvixToken();
194
230
  return token ? { headers: { authorization: `Bearer ${token}` }, credentials: "omit" } : { headers: {}, credentials: "include" };
195
231
  }
232
+ function isSameOrigin(baseUrl) {
233
+ if (!baseUrl) return true;
234
+ if (typeof window === "undefined") return true;
235
+ try {
236
+ return new URL(baseUrl, window.location.href).origin === window.location.origin;
237
+ } catch {
238
+ return true;
239
+ }
240
+ }
196
241
 
197
242
  // src/react/elvix-sign-in.tsx
198
243
  function ElvixSignIn({
199
244
  onResult,
200
245
  redirectAfterSignIn,
246
+ copy: copyProp,
201
247
  className = ""
202
248
  }) {
203
249
  const ctx = useElvixContext();
204
250
  const app = useElvixApp();
251
+ const copy = resolveCopy(app?.strings, copyProp);
205
252
  const [step, setStep] = useState2("identify");
206
253
  const [email, setEmail] = useState2("");
207
254
  const [code, setCode] = useState2("");
@@ -209,6 +256,9 @@ function ElvixSignIn({
209
256
  const [busy, setBusy] = useState2(false);
210
257
  const [error, setError] = useState2(null);
211
258
  const verb = app?.signInVerb === "login" ? "Log in" : "Sign in";
259
+ const defaultTitle = app?.appName ? `${verb} to ${app.appName}` : verb;
260
+ const title = copy.title ? fillCopy(copy.title, { app: app?.appName ?? "" }) : defaultTitle;
261
+ const submitLabel = copy.submitButton ?? verb;
212
262
  function fail(error2, message) {
213
263
  setError(message ?? error2);
214
264
  onResult?.({ ok: false, error: error2, message });
@@ -221,14 +271,14 @@ function ElvixSignIn({
221
271
  }
222
272
  async function startOtp(e) {
223
273
  e.preventDefault();
224
- if (!email.trim()) return fail("invalid_input", "Enter an email.");
274
+ if (!email.trim()) return fail("invalid_input", copy.errorEnterEmail);
225
275
  setBusy(true);
226
276
  setError(null);
227
277
  try {
228
278
  const res = await fetch(`${ctx.baseUrl}/api/auth/otp/start`, {
229
279
  method: "POST",
230
280
  headers: { "content-type": "application/json" },
231
- credentials: "include",
281
+ credentials: isSameOrigin(ctx.baseUrl) ? "include" : "omit",
232
282
  body: JSON.stringify({
233
283
  email: email.trim(),
234
284
  intent: "app",
@@ -250,14 +300,14 @@ function ElvixSignIn({
250
300
  async function verifyOtp(e) {
251
301
  e.preventDefault();
252
302
  if (!challengeId) return;
253
- if (code.trim().length !== 6) return fail("invalid_input", "Enter the 6-digit code.");
303
+ if (code.trim().length !== 6) return fail("invalid_input", copy.errorEnterCode);
254
304
  setBusy(true);
255
305
  setError(null);
256
306
  try {
257
307
  const res = await fetch(`${ctx.baseUrl}/api/auth/otp/verify`, {
258
308
  method: "POST",
259
309
  headers: { "content-type": "application/json" },
260
- credentials: "include",
310
+ credentials: isSameOrigin(ctx.baseUrl) ? "include" : "omit",
261
311
  body: JSON.stringify({ challengeId, code: code.trim() })
262
312
  });
263
313
  const body = await res.json();
@@ -280,9 +330,9 @@ function ElvixSignIn({
280
330
  }
281
331
  const card = `elvix-card ${className}`.trim();
282
332
  if (step === "done") {
283
- return /* @__PURE__ */ React.createElement("div", { className: card, "data-elvix-pane": "done" }, /* @__PURE__ */ React.createElement("p", null, "Signed in."));
333
+ return /* @__PURE__ */ React.createElement("div", { className: card, "data-elvix-pane": "done" }, /* @__PURE__ */ React.createElement("p", null, copy.signedInText));
284
334
  }
285
- return /* @__PURE__ */ React.createElement("div", { className: card, "data-elvix-pane": step }, /* @__PURE__ */ React.createElement("h2", { className: "elvix-h" }, app?.appName ? `${verb} to ${app.appName}` : verb), step === "identify" && /* @__PURE__ */ React.createElement(React.Fragment, null, app?.methods.google && /* @__PURE__ */ React.createElement(
335
+ return /* @__PURE__ */ React.createElement("div", { className: card, "data-elvix-pane": step }, /* @__PURE__ */ React.createElement("h2", { className: "elvix-h" }, title), copy.subtitle && /* @__PURE__ */ React.createElement("p", { className: "elvix-muted elvix-subtitle" }, copy.subtitle), step === "identify" && /* @__PURE__ */ React.createElement(React.Fragment, null, app?.methods.google && /* @__PURE__ */ React.createElement(
286
336
  "button",
287
337
  {
288
338
  type: "button",
@@ -291,19 +341,19 @@ function ElvixSignIn({
291
341
  className: "elvix-btn elvix-btn-google",
292
342
  "data-elvix-method": "google"
293
343
  },
294
- "Continue with Google"
344
+ copy.googleButton
295
345
  ), app?.methods.emailOtp && /* @__PURE__ */ React.createElement("form", { onSubmit: startOtp, "data-elvix-method": "email_otp", className: "elvix-otp-form" }, /* @__PURE__ */ React.createElement(
296
346
  "input",
297
347
  {
298
348
  type: "email",
299
349
  value: email,
300
350
  onChange: (ev) => setEmail(ev.target.value),
301
- placeholder: "you@example.com",
351
+ placeholder: copy.emailPlaceholder,
302
352
  required: true,
303
353
  disabled: busy,
304
354
  className: "elvix-input"
305
355
  }
306
- ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Sending\u2026" : "Send code"))), step === "code" && /* @__PURE__ */ React.createElement("form", { onSubmit: verifyOtp, className: "elvix-otp-form" }, /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "We sent a 6-digit code to ", /* @__PURE__ */ React.createElement("strong", null, email), "."), /* @__PURE__ */ React.createElement(
356
+ ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? copy.sendingLabel : copy.sendCodeButton))), step === "code" && /* @__PURE__ */ React.createElement("form", { onSubmit: verifyOtp, className: "elvix-otp-form" }, /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, fillCopy(copy.codeSentSubtitle ?? "", { email })), /* @__PURE__ */ React.createElement(
307
357
  "input",
308
358
  {
309
359
  type: "text",
@@ -312,22 +362,216 @@ function ElvixSignIn({
312
362
  maxLength: 6,
313
363
  value: code,
314
364
  onChange: (ev) => setCode(ev.target.value.replace(/\D/g, "")),
315
- placeholder: "123456",
365
+ placeholder: copy.codePlaceholder,
316
366
  required: true,
317
367
  disabled: busy,
318
368
  className: "elvix-input"
319
369
  }
320
- ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Verifying\u2026" : verb)), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error));
370
+ ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? copy.verifyingLabel : submitLabel)), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error));
371
+ }
372
+
373
+ // src/react/elvix-sign-in-button.tsx
374
+ import { useState as useState3 } from "react";
375
+
376
+ // src/react/elvix-shield.tsx
377
+ function ElvixShield({
378
+ size,
379
+ fill,
380
+ accent
381
+ }) {
382
+ return /* @__PURE__ */ React.createElement("svg", { width: size, height: size, viewBox: "2 2 20 20", "aria-hidden": true, style: { display: "block" } }, /* @__PURE__ */ React.createElement(
383
+ "path",
384
+ {
385
+ fill,
386
+ fillRule: "evenodd",
387
+ d: "M 6 2.5 C 4.34 2.5 3 3.84 3 5.5 L 3 12.5 C 3 17.5 7 20.7 12 22 C 17 20.7 21 17.5 21 12.5 L 21 5.5 C 21 3.84 19.66 2.5 18 2.5 L 6 2.5 Z M 12 8.4 C 9.79 8.4 8 10.19 8 12.4 C 8 14.61 9.79 16.4 12 16.4 C 13.21 16.4 14.3 15.86 15.04 15 L 13.6 13.77 C 13.21 14.23 12.64 14.5 12 14.5 C 11.04 14.5 10.21 13.86 9.91 13 L 15.95 13 C 15.98 12.8 16 12.6 16 12.4 C 16 10.19 14.21 8.4 12 8.4 Z M 9.91 11.8 L 14.09 11.8 C 13.79 10.94 12.96 10.3 12 10.3 C 11.04 10.3 10.21 10.94 9.91 11.8 Z"
388
+ }
389
+ ), /* @__PURE__ */ React.createElement("circle", { cx: "19.5", cy: "4.5", r: "2.4", fill: accent }));
390
+ }
391
+
392
+ // src/react/elvix-sign-in-button.tsx
393
+ var ELVIX_URL = "https://elvix.is";
394
+ var PRESET_LABEL = {
395
+ "sign-in-with-elvix": "Sign in with elvix",
396
+ "continue-with-elvix": "Continue with elvix",
397
+ "sign-up-with-elvix": "Sign up with elvix",
398
+ "sign-in": "Sign in",
399
+ "log-in": "Log in",
400
+ continue: "Continue"
401
+ };
402
+ var SIZE_STANDARD = {
403
+ sm: { height: 36, padX: 12, font: 14, gap: 8 },
404
+ md: { height: 40, padX: 12, font: 14, gap: 10 },
405
+ lg: { height: 48, padX: 16, font: 15, gap: 12 }
406
+ };
407
+ var SIZE_ICON = { sm: 36, md: 40, lg: 48 };
408
+ var ICON_SIZE = { sm: 18, md: 20, lg: 22 };
409
+ var RADIUS = { rectangle: 10, pill: 9999, square: 10, circle: 9999 };
410
+ function variantTone(variant, theme) {
411
+ const dark = theme === "dark" || theme === "auto";
412
+ switch (variant) {
413
+ case "filled":
414
+ return { bg: "#6c5ce7", color: "#fff", border: "1px solid rgba(0,0,0,0.1)", shadow: "0 4px 16px -4px rgba(108,92,231,0.45)" };
415
+ case "filled-black":
416
+ return { bg: "#0a0a0b", color: "#fff", border: `1px solid ${dark ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.1)"}` };
417
+ case "white":
418
+ return { bg: "#fff", color: "#0a0a0b", border: `1px solid ${dark ? "transparent" : "#e4e4e7"}` };
419
+ case "outline":
420
+ return dark ? { bg: "transparent", color: "#fff", border: "1px solid rgba(142,125,255,0.4)" } : { bg: "#fff", color: "#0a0a0b", border: "1px solid rgba(0,0,0,0.15)" };
421
+ case "ghost":
422
+ return { bg: "transparent", color: dark ? "#fff" : "#0a0a0b", border: "1px solid transparent" };
423
+ }
424
+ }
425
+ function shieldColor(variant, theme) {
426
+ if (variant === "filled" || variant === "filled-black") return "#ffffff";
427
+ if (variant === "white") return "#0a0a0b";
428
+ return theme === "light" ? "#0a0a0b" : "#ffffff";
429
+ }
430
+ function ElvixSignInButton({
431
+ clientId,
432
+ baseUrl = ELVIX_URL,
433
+ returnUrl,
434
+ type = "standard",
435
+ variant = "filled",
436
+ shape = "rectangle",
437
+ size = "md",
438
+ theme = "dark",
439
+ preset = "sign-in-with-elvix",
440
+ label,
441
+ className,
442
+ href,
443
+ mode = "redirect",
444
+ onClick,
445
+ onResult
446
+ }) {
447
+ const [embedOpen, setEmbedOpen] = useState3(false);
448
+ const isIcon = type === "icon";
449
+ const resolvedLabel = label ?? PRESET_LABEL[preset];
450
+ const tone = variantTone(variant, theme);
451
+ const std = SIZE_STANDARD[size];
452
+ const effectiveShape = isIcon ? shape === "pill" || shape === "circle" ? "circle" : "square" : shape;
453
+ const style = {
454
+ display: "inline-flex",
455
+ alignItems: "center",
456
+ justifyContent: "center",
457
+ fontWeight: 500,
458
+ fontSize: std.font,
459
+ cursor: "pointer",
460
+ userSelect: "none",
461
+ textDecoration: "none",
462
+ borderRadius: RADIUS[effectiveShape],
463
+ background: tone.bg,
464
+ color: tone.color,
465
+ border: tone.border,
466
+ boxShadow: tone.shadow,
467
+ transition: "background 0.15s, border-color 0.15s",
468
+ ...isIcon ? { height: SIZE_ICON[size], width: SIZE_ICON[size] } : { height: std.height, paddingLeft: std.padX, paddingRight: std.padX, gap: std.gap }
469
+ };
470
+ const content = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ElvixShield, { size: ICON_SIZE[size], fill: shieldColor(variant, theme), accent: "#8e7dff" }), isIcon ? null : /* @__PURE__ */ React.createElement("span", null, resolvedLabel));
471
+ if (mode === "callback") {
472
+ return /* @__PURE__ */ React.createElement("button", { type: "button", onClick, className, style, "aria-label": isIcon ? resolvedLabel : void 0 }, content);
473
+ }
474
+ if (mode === "embed") {
475
+ return /* @__PURE__ */ React.createElement("div", { "data-elvix-signin-button-embed": "" }, !embedOpen && /* @__PURE__ */ React.createElement(
476
+ "button",
477
+ {
478
+ type: "button",
479
+ onClick: () => setEmbedOpen(true),
480
+ className,
481
+ style,
482
+ "aria-label": isIcon ? resolvedLabel : void 0
483
+ },
484
+ content
485
+ ), embedOpen && /* @__PURE__ */ React.createElement(
486
+ ElvixSignIn,
487
+ {
488
+ onResult: (r) => {
489
+ onResult?.(r);
490
+ }
491
+ }
492
+ ));
493
+ }
494
+ const destination = (() => {
495
+ if (href) return href;
496
+ const base = clientId ? `${baseUrl}/sign-in/${clientId}` : `${baseUrl}/sign-in`;
497
+ if (!returnUrl) return base;
498
+ const sep = base.includes("?") ? "&" : "?";
499
+ return `${base}${sep}return=${encodeURIComponent(returnUrl)}`;
500
+ })();
501
+ return /* @__PURE__ */ React.createElement("a", { href: destination, className, style, "aria-label": isIcon ? resolvedLabel : void 0 }, content);
502
+ }
503
+
504
+ // src/react/elvix-secured-badge.tsx
505
+ var ELVIX_URL2 = "https://elvix.is";
506
+ var SIZE = {
507
+ sm: { height: 28, padX: 10, font: 11.5, icon: 14, gap: 6 },
508
+ md: { height: 32, padX: 12, font: 12.5, icon: 16, gap: 7 },
509
+ lg: { height: 36, padX: 14, font: 13, icon: 18, gap: 8 }
510
+ };
511
+ var TONE = {
512
+ white: {
513
+ light: { bg: "#ffffff", border: "#e4e4e7", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" },
514
+ dark: { bg: "#ffffff", border: "transparent", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" }
515
+ },
516
+ dark: {
517
+ light: { bg: "#0a0a0b", border: "rgba(0,0,0,0.1)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" },
518
+ dark: { bg: "#0a0a0b", border: "rgba(255,255,255,0.1)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" }
519
+ },
520
+ outline: {
521
+ light: { bg: "transparent", border: "rgba(0,0,0,0.15)", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" },
522
+ dark: { bg: "transparent", border: "rgba(142,125,255,0.4)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" }
523
+ }
524
+ };
525
+ function ElvixSecuredBadge({
526
+ variant = "white",
527
+ size = "md",
528
+ theme = "dark",
529
+ accentColor = "#8e7dff",
530
+ href = ELVIX_URL2,
531
+ className = ""
532
+ }) {
533
+ const s = SIZE[size];
534
+ const t = TONE[variant][theme];
535
+ const style = {
536
+ display: "inline-flex",
537
+ alignItems: "center",
538
+ gap: s.gap,
539
+ height: s.height,
540
+ paddingLeft: s.padX,
541
+ paddingRight: s.padX,
542
+ fontSize: s.font,
543
+ fontWeight: 500,
544
+ borderRadius: 9999,
545
+ background: t.bg,
546
+ border: `1px solid ${t.border}`,
547
+ color: t.brand,
548
+ textDecoration: "none",
549
+ userSelect: "none",
550
+ lineHeight: 1
551
+ };
552
+ return /* @__PURE__ */ React.createElement(
553
+ "a",
554
+ {
555
+ href,
556
+ target: "_blank",
557
+ rel: "noopener noreferrer",
558
+ className,
559
+ style,
560
+ "data-elvix-secured-badge": ""
561
+ },
562
+ /* @__PURE__ */ React.createElement(ElvixShield, { size: s.icon, fill: t.shield, accent: accentColor }),
563
+ /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "baseline", gap: 4 } }, /* @__PURE__ */ React.createElement("span", { style: { color: t.lead } }, "Secured by"), /* @__PURE__ */ React.createElement("span", { style: { color: t.brand, fontWeight: 600 } }, "elvix"))
564
+ );
321
565
  }
322
566
 
323
567
  // src/react/hooks.ts
324
- import { useCallback, useEffect as useEffect2, useState as useState3 } from "react";
568
+ import { useCallback, useEffect as useEffect2, useState as useState4 } from "react";
325
569
  var POLL_MS = 7e3;
326
570
  function useUserList(kind, opts) {
327
571
  const { applicationId, baseUrl = "", pollMs = POLL_MS } = opts;
328
- const [slugs, setSlugs] = useState3([]);
329
- const [loading, setLoading] = useState3(true);
330
- const [error, setError] = useState3(null);
572
+ const [slugs, setSlugs] = useState4([]);
573
+ const [loading, setLoading] = useState4(true);
574
+ const [error, setError] = useState4(null);
331
575
  const refresh = useCallback(async () => {
332
576
  setError(null);
333
577
  try {
@@ -394,7 +638,7 @@ function ElvixLifecycleWatcher({
394
638
  }
395
639
 
396
640
  // src/react/elvix-username.tsx
397
- import { useState as useState4 } from "react";
641
+ import { useState as useState5 } from "react";
398
642
 
399
643
  // src/react/lib.ts
400
644
  async function appPost(opts, path, body) {
@@ -456,10 +700,10 @@ function ElvixUsername({
456
700
  onResult
457
701
  }) {
458
702
  const ctx = useElvixContext();
459
- const [value, setValue] = useState4("");
460
- const [busy, setBusy] = useState4(false);
461
- const [error, setError] = useState4(null);
462
- const [done, setDone] = useState4(null);
703
+ const [value, setValue] = useState5("");
704
+ const [busy, setBusy] = useState5(false);
705
+ const [error, setError] = useState5(null);
706
+ const [done, setDone] = useState5(null);
463
707
  async function submit(e) {
464
708
  e.preventDefault();
465
709
  if (!ctx.app) return;
@@ -497,14 +741,14 @@ function ElvixUsername({
497
741
  }
498
742
 
499
743
  // src/react/elvix-avatar.tsx
500
- import { useState as useState5 } from "react";
744
+ import { useState as useState6 } from "react";
501
745
  function ElvixAvatar({
502
746
  onResult
503
747
  }) {
504
748
  const ctx = useElvixContext();
505
- const [busy, setBusy] = useState5(false);
506
- const [error, setError] = useState5(null);
507
- const [preview, setPreview] = useState5(null);
749
+ const [busy, setBusy] = useState6(false);
750
+ const [error, setError] = useState6(null);
751
+ const [preview, setPreview] = useState6(null);
508
752
  async function onFile(e) {
509
753
  const file = e.target.files?.[0];
510
754
  if (!file || !ctx.app) return;
@@ -538,14 +782,14 @@ function ElvixAvatar({
538
782
  }
539
783
 
540
784
  // src/react/elvix-banner.tsx
541
- import { useState as useState6 } from "react";
785
+ import { useState as useState7 } from "react";
542
786
  function ElvixBanner({
543
787
  onResult
544
788
  }) {
545
789
  const ctx = useElvixContext();
546
- const [busy, setBusy] = useState6(false);
547
- const [error, setError] = useState6(null);
548
- const [preview, setPreview] = useState6(null);
790
+ const [busy, setBusy] = useState7(false);
791
+ const [error, setError] = useState7(null);
792
+ const [preview, setPreview] = useState7(null);
549
793
  async function onFile(e) {
550
794
  const file = e.target.files?.[0];
551
795
  if (!file || !ctx.app) return;
@@ -579,18 +823,18 @@ function ElvixBanner({
579
823
  }
580
824
 
581
825
  // src/react/elvix-identity-form.tsx
582
- import { useState as useState7 } from "react";
826
+ import { useState as useState8 } from "react";
583
827
  function ElvixIdentityForm({
584
828
  initialName = "",
585
829
  initialBio = "",
586
830
  onResult
587
831
  }) {
588
832
  const ctx = useElvixContext();
589
- const [name, setName] = useState7(initialName);
590
- const [bio, setBio] = useState7(initialBio);
591
- const [busy, setBusy] = useState7(false);
592
- const [error, setError] = useState7(null);
593
- const [saved, setSaved] = useState7(false);
833
+ const [name, setName] = useState8(initialName);
834
+ const [bio, setBio] = useState8(initialBio);
835
+ const [busy, setBusy] = useState8(false);
836
+ const [error, setError] = useState8(null);
837
+ const [saved, setSaved] = useState8(false);
594
838
  async function submit(e) {
595
839
  e.preventDefault();
596
840
  if (!ctx.app) return;
@@ -610,18 +854,18 @@ function ElvixIdentityForm({
610
854
  }
611
855
 
612
856
  // src/react/elvix-region.tsx
613
- import { useState as useState8 } from "react";
857
+ import { useState as useState9 } from "react";
614
858
  function ElvixRegion({
615
859
  initialCountry = "",
616
860
  initialTimezone = "",
617
861
  onResult
618
862
  }) {
619
863
  const ctx = useElvixContext();
620
- const [country, setCountry] = useState8(initialCountry);
621
- const [timezone, setTimezone] = useState8(initialTimezone);
622
- const [busy, setBusy] = useState8(false);
623
- const [error, setError] = useState8(null);
624
- const [saved, setSaved] = useState8(false);
864
+ const [country, setCountry] = useState9(initialCountry);
865
+ const [timezone, setTimezone] = useState9(initialTimezone);
866
+ const [busy, setBusy] = useState9(false);
867
+ const [error, setError] = useState9(null);
868
+ const [saved, setSaved] = useState9(false);
625
869
  async function submit(e) {
626
870
  e.preventDefault();
627
871
  if (!ctx.app) return;
@@ -641,16 +885,16 @@ function ElvixRegion({
641
885
  }
642
886
 
643
887
  // src/react/elvix-languages.tsx
644
- import { useState as useState9 } from "react";
888
+ import { useState as useState10 } from "react";
645
889
  function ElvixLanguages({
646
890
  initial = [],
647
891
  onResult
648
892
  }) {
649
893
  const ctx = useElvixContext();
650
- const [raw, setRaw] = useState9(initial.join(", "));
651
- const [busy, setBusy] = useState9(false);
652
- const [error, setError] = useState9(null);
653
- const [saved, setSaved] = useState9(false);
894
+ const [raw, setRaw] = useState10(initial.join(", "));
895
+ const [busy, setBusy] = useState10(false);
896
+ const [error, setError] = useState10(null);
897
+ const [saved, setSaved] = useState10(false);
654
898
  async function submit(e) {
655
899
  e.preventDefault();
656
900
  if (!ctx.app) return;
@@ -671,18 +915,18 @@ function ElvixLanguages({
671
915
  }
672
916
 
673
917
  // src/react/elvix-sessions.tsx
674
- import { useEffect as useEffect4, useState as useState10 } from "react";
918
+ import { useEffect as useEffect4, useState as useState11 } from "react";
675
919
  function ElvixSessions({
676
920
  onResult
677
921
  }) {
678
922
  const ctx = useElvixContext();
679
- const [rows, setRows] = useState10(null);
680
- const [error, setError] = useState10(null);
681
- const [busy, setBusy] = useState10(false);
923
+ const [rows, setRows] = useState11(null);
924
+ const [error, setError] = useState11(null);
925
+ const [busy, setBusy] = useState11(false);
682
926
  useEffect4(() => {
683
927
  if (!ctx.app) return;
684
928
  fetch(`${ctx.baseUrl}/api/account/apps/${ctx.app.applicationId}/sessions`, {
685
- credentials: "include"
929
+ ...authInit()
686
930
  }).then((r) => r.json()).then((j) => {
687
931
  if (j.success && j.data) setRows(j.data.sessions);
688
932
  else setError("load_failed");
@@ -703,14 +947,14 @@ function ElvixSessions({
703
947
  }
704
948
 
705
949
  // src/react/elvix-export.tsx
706
- import { useState as useState11 } from "react";
950
+ import { useState as useState12 } from "react";
707
951
  function ElvixExport({
708
952
  onResult
709
953
  }) {
710
954
  const ctx = useElvixContext();
711
- const [busy, setBusy] = useState11(false);
712
- const [done, setDone] = useState11(false);
713
- const [error, setError] = useState11(null);
955
+ const [busy, setBusy] = useState12(false);
956
+ const [done, setDone] = useState12(false);
957
+ const [error, setError] = useState12(null);
714
958
  async function start() {
715
959
  if (!ctx.app) return;
716
960
  setBusy(true);
@@ -729,16 +973,16 @@ function ElvixExport({
729
973
  }
730
974
 
731
975
  // src/react/elvix-deactivate.tsx
732
- import { useState as useState12 } from "react";
976
+ import { useState as useState13 } from "react";
733
977
  function ElvixDeactivate({
734
978
  onResult
735
979
  }) {
736
980
  const ctx = useElvixContext();
737
- const [pane, setPane] = useState12("warn");
738
- const [challengeId, setChallengeId] = useState12(null);
739
- const [code, setCode] = useState12("");
740
- const [busy, setBusy] = useState12(false);
741
- const [error, setError] = useState12(null);
981
+ const [pane, setPane] = useState13("warn");
982
+ const [challengeId, setChallengeId] = useState13(null);
983
+ const [code, setCode] = useState13("");
984
+ const [busy, setBusy] = useState13(false);
985
+ const [error, setError] = useState13(null);
742
986
  async function startChallenge() {
743
987
  if (!ctx.app) return;
744
988
  setBusy(true);
@@ -795,16 +1039,16 @@ function ElvixDeactivate({
795
1039
  }
796
1040
 
797
1041
  // src/react/elvix-leave.tsx
798
- import { useState as useState13 } from "react";
1042
+ import { useState as useState14 } from "react";
799
1043
  function ElvixLeave({
800
1044
  onResult
801
1045
  }) {
802
1046
  const ctx = useElvixContext();
803
- const [pane, setPane] = useState13("warn");
804
- const [challengeId, setChallengeId] = useState13(null);
805
- const [code, setCode] = useState13("");
806
- const [busy, setBusy] = useState13(false);
807
- const [error, setError] = useState13(null);
1047
+ const [pane, setPane] = useState14("warn");
1048
+ const [challengeId, setChallengeId] = useState14(null);
1049
+ const [code, setCode] = useState14("");
1050
+ const [busy, setBusy] = useState14(false);
1051
+ const [error, setError] = useState14(null);
808
1052
  async function startChallenge() {
809
1053
  if (!ctx.app) return;
810
1054
  setBusy(true);
@@ -861,16 +1105,16 @@ function ElvixLeave({
861
1105
  }
862
1106
 
863
1107
  // src/react/elvix-address-book.tsx
864
- import { useEffect as useEffect5, useState as useState14 } from "react";
1108
+ import { useEffect as useEffect5, useState as useState15 } from "react";
865
1109
  function ElvixAddressBook({
866
1110
  onResult
867
1111
  }) {
868
1112
  const ctx = useElvixContext();
869
- const [rows, setRows] = useState14(null);
870
- const [error, setError] = useState14(null);
871
- const [busy, setBusy] = useState14(false);
872
- const [adding, setAdding] = useState14(false);
873
- const [form, setForm] = useState14({
1113
+ const [rows, setRows] = useState15(null);
1114
+ const [error, setError] = useState15(null);
1115
+ const [busy, setBusy] = useState15(false);
1116
+ const [adding, setAdding] = useState15(false);
1117
+ const [form, setForm] = useState15({
874
1118
  label: "Home",
875
1119
  line1: "",
876
1120
  postalCode: "",
@@ -880,7 +1124,7 @@ function ElvixAddressBook({
880
1124
  function reload() {
881
1125
  if (!ctx.app) return;
882
1126
  fetch(`${ctx.baseUrl}/api/account/apps/${ctx.app.applicationId}/addresses`, {
883
- credentials: "include"
1127
+ ...authInit()
884
1128
  }).then((r) => r.json()).then((j) => {
885
1129
  if (j.success && j.data) setRows(j.data.addresses);
886
1130
  else setError("load_failed");
@@ -923,16 +1167,16 @@ function ElvixAddressBook({
923
1167
  }
924
1168
 
925
1169
  // src/react/elvix-legal-entities.tsx
926
- import { useEffect as useEffect6, useState as useState15 } from "react";
1170
+ import { useEffect as useEffect6, useState as useState16 } from "react";
927
1171
  function ElvixLegalEntities({
928
1172
  onResult
929
1173
  }) {
930
1174
  const ctx = useElvixContext();
931
- const [rows, setRows] = useState15(null);
932
- const [error, setError] = useState15(null);
933
- const [busy, setBusy] = useState15(false);
934
- const [adding, setAdding] = useState15(false);
935
- const [form, setForm] = useState15({
1175
+ const [rows, setRows] = useState16(null);
1176
+ const [error, setError] = useState16(null);
1177
+ const [busy, setBusy] = useState16(false);
1178
+ const [adding, setAdding] = useState16(false);
1179
+ const [form, setForm] = useState16({
936
1180
  legalName: "",
937
1181
  taxId: "",
938
1182
  country: ""
@@ -940,7 +1184,7 @@ function ElvixLegalEntities({
940
1184
  function reload() {
941
1185
  if (!ctx.app) return;
942
1186
  fetch(`${ctx.baseUrl}/api/account/apps/${ctx.app.applicationId}/legal-entities`, {
943
- credentials: "include"
1187
+ ...authInit()
944
1188
  }).then((r) => r.json()).then((j) => {
945
1189
  if (j.success && j.data) setRows(j.data.entities);
946
1190
  else setError("load_failed");
@@ -982,6 +1226,7 @@ function ElvixLegalEntities({
982
1226
  return /* @__PURE__ */ React.createElement(ElvixCard, { title: "Legal entities" }, error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error), !rows && !error && /* @__PURE__ */ React.createElement("p", null, "Loading\u2026"), rows && rows.length === 0 && /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "No legal entities yet."), rows?.map((e) => /* @__PURE__ */ React.createElement("div", { key: e.id, style: { padding: "8px 0", borderBottom: "1px solid rgba(0,0,0,0.06)", display: "flex", justifyContent: "space-between", gap: 12 } }, /* @__PURE__ */ React.createElement("div", { style: { fontSize: 13 } }, /* @__PURE__ */ React.createElement("div", { style: { fontWeight: 500 } }, e.legalName), /* @__PURE__ */ React.createElement("div", { style: { color: "rgba(0,0,0,0.55)" } }, e.taxId, " \xB7 ", e.country)), /* @__PURE__ */ React.createElement("button", { type: "button", disabled: busy, onClick: () => remove(e.id), className: "elvix-btn elvix-btn-ghost" }, "Remove"))), !adding && /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => setAdding(true), className: "elvix-btn elvix-btn-primary", style: { marginTop: 12 } }, "Add entity"), adding && /* @__PURE__ */ React.createElement("form", { onSubmit: add, className: "elvix-form", style: { marginTop: 12 } }, /* @__PURE__ */ React.createElement("input", { value: form.legalName, onChange: (e) => setForm({ ...form, legalName: e.target.value }), placeholder: "Legal name", required: true, className: "elvix-input" }), /* @__PURE__ */ React.createElement("input", { value: form.taxId, onChange: (e) => setForm({ ...form, taxId: e.target.value }), placeholder: "Tax / VAT ID", required: true, className: "elvix-input" }), /* @__PURE__ */ React.createElement("input", { value: form.country, onChange: (e) => setForm({ ...form, country: e.target.value.toUpperCase() }), placeholder: "Country (ISO-2)", maxLength: 2, required: true, className: "elvix-input" }), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Saving\u2026" : "Save")));
983
1227
  }
984
1228
  export {
1229
+ DEFAULT_COPY,
985
1230
  ElvixAddressBook,
986
1231
  ElvixAvatar,
987
1232
  ElvixBanner,
@@ -995,8 +1240,10 @@ export {
995
1240
  ElvixLifecycleWatcher,
996
1241
  ElvixProvider,
997
1242
  ElvixRegion,
1243
+ ElvixSecuredBadge,
998
1244
  ElvixSessions,
999
1245
  ElvixSignIn,
1246
+ ElvixSignInButton,
1000
1247
  ElvixUsername,
1001
1248
  getElvixToken,
1002
1249
  setElvixToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvix.is/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Official elvix SDK. Drop-in React components, server helpers, and an MCP server so AI coding agents integrate elvix on the first try.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://elvix.is",