@elvix.is/sdk 0.5.1 → 0.5.3

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.
Files changed (3) hide show
  1. package/dist/react.d.ts +132 -15
  2. package/dist/react.js +322 -78
  3. package/package.json +1 -1
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;
@@ -36,6 +84,12 @@ type ElvixBrand = {
36
84
  };
37
85
  };
38
86
  type ElvixSignInMethod = "google" | "email_otp" | "passkey" | "username";
87
+ /**
88
+ * The public render envelope `GET /api/v1/bootstrap/<clientId>` returns. Flat
89
+ * to match the wire shape exactly (the provider builds the {light,dark} brand
90
+ * chord from the colour fields). Any field here is already public — it's what
91
+ * the sign-in surface shows.
92
+ */
39
93
  type ElvixBootstrapEnvelope = {
40
94
  applicationId: string;
41
95
  clientId: string;
@@ -45,21 +99,34 @@ type ElvixBootstrapEnvelope = {
45
99
  logoUrlDark: string | null;
46
100
  iconUrl: string | null;
47
101
  iconUrlDark: string | null;
48
- brand: ElvixBrand;
49
- methods: {
50
- google: boolean;
51
- emailOtp: boolean;
52
- passkey: boolean;
53
- username: boolean;
54
- };
55
- legal: {
56
- privacyPolicyUrl: string;
57
- termsOfServiceUrl: string;
58
- supportEmail: string;
59
- supportUrl: string | null;
60
- };
102
+ websiteUrl: string | null;
103
+ privacyPolicyUrl: string;
104
+ termsOfServiceUrl: string;
105
+ supportUrl: string | null;
106
+ brandColor: string;
107
+ brandColorDark: string | null;
108
+ onBrandColor: string;
109
+ onBrandColorDark: string | null;
110
+ brandPreset: string;
111
+ methodGoogle: boolean;
112
+ methodEmailOtp: boolean;
113
+ methodPasskey: boolean;
114
+ methodUsername: boolean;
115
+ layout: string;
116
+ socialLayout: string;
117
+ presentation: string;
118
+ theme: "light" | "dark" | "system";
119
+ showHeader: boolean;
120
+ transparentBg: boolean;
61
121
  signInVerb: "signin" | "login";
62
122
  signinGate: "public" | "private_beta" | "closed";
123
+ archivedAt: string | null;
124
+ /**
125
+ * Console-configured sign-in copy overrides. Any subset of the strings the
126
+ * sign-in surface renders; missing keys fall back to the built-in English
127
+ * defaults. A `copy` prop on the component overrides these in turn.
128
+ */
129
+ strings?: Partial<ElvixCopy>;
63
130
  };
64
131
  type ElvixSignInResultOk = {
65
132
  ok: true;
@@ -119,13 +186,63 @@ declare function ElvixProvider({ clientId, theme, brand, baseUrl, children, clas
119
186
  * exposes. Hosts navigate from the callback; this component never
120
187
  * calls `router.push` itself.
121
188
  */
122
- declare function ElvixSignIn({ onResult, redirectAfterSignIn, className, }: {
189
+ declare function ElvixSignIn({ onResult, redirectAfterSignIn, copy: copyProp, className, }: {
123
190
  onResult?: (r: ElvixSignInResult) => void;
124
191
  /** Default redirect target on success when the server doesn't echo one. */
125
192
  redirectAfterSignIn?: string;
193
+ /**
194
+ * Thin per-embed copy override. The primary way to edit copy is the elvix
195
+ * Console (served live in the bootstrap `strings`); this prop just lets a
196
+ * single embed tweak a string or two without a Console change.
197
+ */
198
+ copy?: Partial<ElvixCopy>;
126
199
  className?: string;
127
200
  }): react.JSX.Element;
128
201
 
202
+ type ElvixSignInButtonSize = "sm" | "md" | "lg";
203
+ type ElvixSignInButtonTheme = "light" | "dark" | "auto";
204
+ type ElvixSignInButtonVariant = "filled" | "filled-black" | "white" | "outline" | "ghost";
205
+ type ElvixSignInButtonShape = "rectangle" | "pill" | "square" | "circle";
206
+ type ElvixSignInButtonType = "standard" | "icon";
207
+ type ElvixSignInButtonMode = "redirect" | "callback" | "embed";
208
+ type ElvixSignInPreset = "sign-in-with-elvix" | "continue-with-elvix" | "sign-up-with-elvix" | "sign-in" | "log-in" | "continue";
209
+ type ElvixSignInButtonProps = {
210
+ clientId?: string;
211
+ /** elvix origin for redirect mode. Defaults to https://elvix.is. */
212
+ baseUrl?: string;
213
+ returnUrl?: string;
214
+ type?: ElvixSignInButtonType;
215
+ variant?: ElvixSignInButtonVariant;
216
+ shape?: ElvixSignInButtonShape;
217
+ size?: ElvixSignInButtonSize;
218
+ theme?: ElvixSignInButtonTheme;
219
+ preset?: ElvixSignInPreset;
220
+ label?: string;
221
+ className?: string;
222
+ href?: string;
223
+ mode?: ElvixSignInButtonMode;
224
+ onClick?: () => void;
225
+ /** Terminal outcome of mode="embed": success (with token) or error. */
226
+ onResult?: (result: ElvixSignInResult) => void;
227
+ };
228
+ declare function ElvixSignInButton({ clientId, baseUrl, returnUrl, type, variant, shape, size, theme, preset, label, className, href, mode, onClick, onResult, }: ElvixSignInButtonProps): react.JSX.Element;
229
+
230
+ type ElvixSecuredBadgeVariant = "white" | "dark" | "outline";
231
+ type ElvixSecuredBadgeSize = "sm" | "md" | "lg";
232
+ type ElvixSecuredBadgeTheme = "light" | "dark";
233
+ type ElvixSecuredBadgeProps = {
234
+ variant?: ElvixSecuredBadgeVariant;
235
+ size?: ElvixSecuredBadgeSize;
236
+ /** Which side of the colour wheel the host page is on (for the outline variant). */
237
+ theme?: ElvixSecuredBadgeTheme;
238
+ /** Active-protection dot colour. Defaults to brand lavender. */
239
+ accentColor?: string;
240
+ /** Where the badge links. Defaults to elvix. */
241
+ href?: string;
242
+ className?: string;
243
+ };
244
+ declare function ElvixSecuredBadge({ variant, size, theme, accentColor, href, className, }: ElvixSecuredBadgeProps): react.JSX.Element;
245
+
129
246
  /**
130
247
  * Session token store for cross-origin SDK use.
131
248
  *
@@ -280,4 +397,4 @@ declare function ElvixLegalEntities({ onResult, }: {
280
397
  onResult?: (r: ElvixActionResult) => void;
281
398
  }): react.JSX.Element;
282
399
 
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 };
400
+ 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
@@ -151,8 +151,14 @@ function ElvixProvider({
151
151
  ));
152
152
  }
153
153
  function appBrand(app) {
154
- if (!app?.brand) return null;
155
- return app.brand;
154
+ if (!app?.brandColor) return null;
155
+ return {
156
+ light: { primary: app.brandColor, on: app.onBrandColor },
157
+ dark: {
158
+ primary: app.brandColorDark ?? app.brandColor,
159
+ on: app.onBrandColorDark ?? app.onBrandColor
160
+ }
161
+ };
156
162
  }
157
163
  function withAlpha(hex, a) {
158
164
  const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
@@ -167,6 +173,42 @@ function withAlpha(hex, a) {
167
173
  // src/react/elvix-sign-in.tsx
168
174
  import { useState as useState2 } from "react";
169
175
 
176
+ // src/react/copy.ts
177
+ var DEFAULT_COPY = {
178
+ subtitle: "Pick how you want to continue.",
179
+ googleButton: "Continue with Google",
180
+ emailPlaceholder: "you@example.com",
181
+ sendCodeButton: "Send code",
182
+ sendingLabel: "Sending\u2026",
183
+ codeSentSubtitle: "We sent a 6-digit code to {email}.",
184
+ codePlaceholder: "123456",
185
+ verifyingLabel: "Verifying\u2026",
186
+ signedInText: "Signed in.",
187
+ errorEnterEmail: "Enter an email.",
188
+ errorEnterCode: "Enter the 6-digit code."
189
+ };
190
+ function resolveCopy(bootstrap, prop) {
191
+ return {
192
+ ...DEFAULT_COPY,
193
+ ...stripUndefined(bootstrap),
194
+ ...stripUndefined(prop)
195
+ };
196
+ }
197
+ function fillCopy(template, tokens) {
198
+ return template.replace(
199
+ /\{(\w+)\}/g,
200
+ (whole, key) => key in tokens ? tokens[key] : whole
201
+ );
202
+ }
203
+ function stripUndefined(o) {
204
+ if (!o) return {};
205
+ const out = {};
206
+ for (const [k, v] of Object.entries(o)) {
207
+ if (v !== void 0) out[k] = v;
208
+ }
209
+ return out;
210
+ }
211
+
170
212
  // src/react/session.ts
171
213
  var STORAGE_KEY = "elvix.session.token";
172
214
  var memToken = null;
@@ -207,10 +249,12 @@ function isSameOrigin(baseUrl) {
207
249
  function ElvixSignIn({
208
250
  onResult,
209
251
  redirectAfterSignIn,
252
+ copy: copyProp,
210
253
  className = ""
211
254
  }) {
212
255
  const ctx = useElvixContext();
213
256
  const app = useElvixApp();
257
+ const copy = resolveCopy(app?.strings, copyProp);
214
258
  const [step, setStep] = useState2("identify");
215
259
  const [email, setEmail] = useState2("");
216
260
  const [code, setCode] = useState2("");
@@ -218,6 +262,9 @@ function ElvixSignIn({
218
262
  const [busy, setBusy] = useState2(false);
219
263
  const [error, setError] = useState2(null);
220
264
  const verb = app?.signInVerb === "login" ? "Log in" : "Sign in";
265
+ const defaultTitle = app?.appName ? `${verb} to ${app.appName}` : verb;
266
+ const title = copy.title ? fillCopy(copy.title, { app: app?.appName ?? "" }) : defaultTitle;
267
+ const submitLabel = copy.submitButton ?? verb;
221
268
  function fail(error2, message) {
222
269
  setError(message ?? error2);
223
270
  onResult?.({ ok: false, error: error2, message });
@@ -230,7 +277,7 @@ function ElvixSignIn({
230
277
  }
231
278
  async function startOtp(e) {
232
279
  e.preventDefault();
233
- if (!email.trim()) return fail("invalid_input", "Enter an email.");
280
+ if (!email.trim()) return fail("invalid_input", copy.errorEnterEmail);
234
281
  setBusy(true);
235
282
  setError(null);
236
283
  try {
@@ -259,7 +306,7 @@ function ElvixSignIn({
259
306
  async function verifyOtp(e) {
260
307
  e.preventDefault();
261
308
  if (!challengeId) return;
262
- if (code.trim().length !== 6) return fail("invalid_input", "Enter the 6-digit code.");
309
+ if (code.trim().length !== 6) return fail("invalid_input", copy.errorEnterCode);
263
310
  setBusy(true);
264
311
  setError(null);
265
312
  try {
@@ -289,9 +336,9 @@ function ElvixSignIn({
289
336
  }
290
337
  const card = `elvix-card ${className}`.trim();
291
338
  if (step === "done") {
292
- return /* @__PURE__ */ React.createElement("div", { className: card, "data-elvix-pane": "done" }, /* @__PURE__ */ React.createElement("p", null, "Signed in."));
339
+ return /* @__PURE__ */ React.createElement("div", { className: card, "data-elvix-pane": "done" }, /* @__PURE__ */ React.createElement("p", null, copy.signedInText));
293
340
  }
294
- 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(
341
+ 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?.methodGoogle && /* @__PURE__ */ React.createElement(
295
342
  "button",
296
343
  {
297
344
  type: "button",
@@ -300,19 +347,19 @@ function ElvixSignIn({
300
347
  className: "elvix-btn elvix-btn-google",
301
348
  "data-elvix-method": "google"
302
349
  },
303
- "Continue with Google"
304
- ), app?.methods.emailOtp && /* @__PURE__ */ React.createElement("form", { onSubmit: startOtp, "data-elvix-method": "email_otp", className: "elvix-otp-form" }, /* @__PURE__ */ React.createElement(
350
+ copy.googleButton
351
+ ), app?.methodEmailOtp && /* @__PURE__ */ React.createElement("form", { onSubmit: startOtp, "data-elvix-method": "email_otp", className: "elvix-otp-form" }, /* @__PURE__ */ React.createElement(
305
352
  "input",
306
353
  {
307
354
  type: "email",
308
355
  value: email,
309
356
  onChange: (ev) => setEmail(ev.target.value),
310
- placeholder: "you@example.com",
357
+ placeholder: copy.emailPlaceholder,
311
358
  required: true,
312
359
  disabled: busy,
313
360
  className: "elvix-input"
314
361
  }
315
- ), /* @__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(
362
+ ), /* @__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(
316
363
  "input",
317
364
  {
318
365
  type: "text",
@@ -321,22 +368,216 @@ function ElvixSignIn({
321
368
  maxLength: 6,
322
369
  value: code,
323
370
  onChange: (ev) => setCode(ev.target.value.replace(/\D/g, "")),
324
- placeholder: "123456",
371
+ placeholder: copy.codePlaceholder,
325
372
  required: true,
326
373
  disabled: busy,
327
374
  className: "elvix-input"
328
375
  }
329
- ), /* @__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));
376
+ ), /* @__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));
377
+ }
378
+
379
+ // src/react/elvix-sign-in-button.tsx
380
+ import { useState as useState3 } from "react";
381
+
382
+ // src/react/elvix-shield.tsx
383
+ function ElvixShield({
384
+ size,
385
+ fill,
386
+ accent
387
+ }) {
388
+ return /* @__PURE__ */ React.createElement("svg", { width: size, height: size, viewBox: "2 2 20 20", "aria-hidden": true, style: { display: "block" } }, /* @__PURE__ */ React.createElement(
389
+ "path",
390
+ {
391
+ fill,
392
+ fillRule: "evenodd",
393
+ 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"
394
+ }
395
+ ), /* @__PURE__ */ React.createElement("circle", { cx: "19.5", cy: "4.5", r: "2.4", fill: accent }));
396
+ }
397
+
398
+ // src/react/elvix-sign-in-button.tsx
399
+ var ELVIX_URL = "https://elvix.is";
400
+ var PRESET_LABEL = {
401
+ "sign-in-with-elvix": "Sign in with elvix",
402
+ "continue-with-elvix": "Continue with elvix",
403
+ "sign-up-with-elvix": "Sign up with elvix",
404
+ "sign-in": "Sign in",
405
+ "log-in": "Log in",
406
+ continue: "Continue"
407
+ };
408
+ var SIZE_STANDARD = {
409
+ sm: { height: 36, padX: 12, font: 14, gap: 8 },
410
+ md: { height: 40, padX: 12, font: 14, gap: 10 },
411
+ lg: { height: 48, padX: 16, font: 15, gap: 12 }
412
+ };
413
+ var SIZE_ICON = { sm: 36, md: 40, lg: 48 };
414
+ var ICON_SIZE = { sm: 18, md: 20, lg: 22 };
415
+ var RADIUS = { rectangle: 10, pill: 9999, square: 10, circle: 9999 };
416
+ function variantTone(variant, theme) {
417
+ const dark = theme === "dark" || theme === "auto";
418
+ switch (variant) {
419
+ case "filled":
420
+ 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)" };
421
+ case "filled-black":
422
+ return { bg: "#0a0a0b", color: "#fff", border: `1px solid ${dark ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.1)"}` };
423
+ case "white":
424
+ return { bg: "#fff", color: "#0a0a0b", border: `1px solid ${dark ? "transparent" : "#e4e4e7"}` };
425
+ case "outline":
426
+ 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)" };
427
+ case "ghost":
428
+ return { bg: "transparent", color: dark ? "#fff" : "#0a0a0b", border: "1px solid transparent" };
429
+ }
430
+ }
431
+ function shieldColor(variant, theme) {
432
+ if (variant === "filled" || variant === "filled-black") return "#ffffff";
433
+ if (variant === "white") return "#0a0a0b";
434
+ return theme === "light" ? "#0a0a0b" : "#ffffff";
435
+ }
436
+ function ElvixSignInButton({
437
+ clientId,
438
+ baseUrl = ELVIX_URL,
439
+ returnUrl,
440
+ type = "standard",
441
+ variant = "filled",
442
+ shape = "rectangle",
443
+ size = "md",
444
+ theme = "dark",
445
+ preset = "sign-in-with-elvix",
446
+ label,
447
+ className,
448
+ href,
449
+ mode = "redirect",
450
+ onClick,
451
+ onResult
452
+ }) {
453
+ const [embedOpen, setEmbedOpen] = useState3(false);
454
+ const isIcon = type === "icon";
455
+ const resolvedLabel = label ?? PRESET_LABEL[preset];
456
+ const tone = variantTone(variant, theme);
457
+ const std = SIZE_STANDARD[size];
458
+ const effectiveShape = isIcon ? shape === "pill" || shape === "circle" ? "circle" : "square" : shape;
459
+ const style = {
460
+ display: "inline-flex",
461
+ alignItems: "center",
462
+ justifyContent: "center",
463
+ fontWeight: 500,
464
+ fontSize: std.font,
465
+ cursor: "pointer",
466
+ userSelect: "none",
467
+ textDecoration: "none",
468
+ borderRadius: RADIUS[effectiveShape],
469
+ background: tone.bg,
470
+ color: tone.color,
471
+ border: tone.border,
472
+ boxShadow: tone.shadow,
473
+ transition: "background 0.15s, border-color 0.15s",
474
+ ...isIcon ? { height: SIZE_ICON[size], width: SIZE_ICON[size] } : { height: std.height, paddingLeft: std.padX, paddingRight: std.padX, gap: std.gap }
475
+ };
476
+ 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));
477
+ if (mode === "callback") {
478
+ return /* @__PURE__ */ React.createElement("button", { type: "button", onClick, className, style, "aria-label": isIcon ? resolvedLabel : void 0 }, content);
479
+ }
480
+ if (mode === "embed") {
481
+ return /* @__PURE__ */ React.createElement("div", { "data-elvix-signin-button-embed": "" }, !embedOpen && /* @__PURE__ */ React.createElement(
482
+ "button",
483
+ {
484
+ type: "button",
485
+ onClick: () => setEmbedOpen(true),
486
+ className,
487
+ style,
488
+ "aria-label": isIcon ? resolvedLabel : void 0
489
+ },
490
+ content
491
+ ), embedOpen && /* @__PURE__ */ React.createElement(
492
+ ElvixSignIn,
493
+ {
494
+ onResult: (r) => {
495
+ onResult?.(r);
496
+ }
497
+ }
498
+ ));
499
+ }
500
+ const destination = (() => {
501
+ if (href) return href;
502
+ const base = clientId ? `${baseUrl}/sign-in/${clientId}` : `${baseUrl}/sign-in`;
503
+ if (!returnUrl) return base;
504
+ const sep = base.includes("?") ? "&" : "?";
505
+ return `${base}${sep}return=${encodeURIComponent(returnUrl)}`;
506
+ })();
507
+ return /* @__PURE__ */ React.createElement("a", { href: destination, className, style, "aria-label": isIcon ? resolvedLabel : void 0 }, content);
508
+ }
509
+
510
+ // src/react/elvix-secured-badge.tsx
511
+ var ELVIX_URL2 = "https://elvix.is";
512
+ var SIZE = {
513
+ sm: { height: 28, padX: 10, font: 11.5, icon: 14, gap: 6 },
514
+ md: { height: 32, padX: 12, font: 12.5, icon: 16, gap: 7 },
515
+ lg: { height: 36, padX: 14, font: 13, icon: 18, gap: 8 }
516
+ };
517
+ var TONE = {
518
+ white: {
519
+ light: { bg: "#ffffff", border: "#e4e4e7", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" },
520
+ dark: { bg: "#ffffff", border: "transparent", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" }
521
+ },
522
+ dark: {
523
+ light: { bg: "#0a0a0b", border: "rgba(0,0,0,0.1)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" },
524
+ dark: { bg: "#0a0a0b", border: "rgba(255,255,255,0.1)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" }
525
+ },
526
+ outline: {
527
+ light: { bg: "transparent", border: "rgba(0,0,0,0.15)", lead: "#71717a", brand: "#0a0a0b", shield: "#0a0a0b" },
528
+ dark: { bg: "transparent", border: "rgba(142,125,255,0.4)", lead: "#d4d4d8", brand: "#ffffff", shield: "#ffffff" }
529
+ }
530
+ };
531
+ function ElvixSecuredBadge({
532
+ variant = "white",
533
+ size = "md",
534
+ theme = "dark",
535
+ accentColor = "#8e7dff",
536
+ href = ELVIX_URL2,
537
+ className = ""
538
+ }) {
539
+ const s = SIZE[size];
540
+ const t = TONE[variant][theme];
541
+ const style = {
542
+ display: "inline-flex",
543
+ alignItems: "center",
544
+ gap: s.gap,
545
+ height: s.height,
546
+ paddingLeft: s.padX,
547
+ paddingRight: s.padX,
548
+ fontSize: s.font,
549
+ fontWeight: 500,
550
+ borderRadius: 9999,
551
+ background: t.bg,
552
+ border: `1px solid ${t.border}`,
553
+ color: t.brand,
554
+ textDecoration: "none",
555
+ userSelect: "none",
556
+ lineHeight: 1
557
+ };
558
+ return /* @__PURE__ */ React.createElement(
559
+ "a",
560
+ {
561
+ href,
562
+ target: "_blank",
563
+ rel: "noopener noreferrer",
564
+ className,
565
+ style,
566
+ "data-elvix-secured-badge": ""
567
+ },
568
+ /* @__PURE__ */ React.createElement(ElvixShield, { size: s.icon, fill: t.shield, accent: accentColor }),
569
+ /* @__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"))
570
+ );
330
571
  }
331
572
 
332
573
  // src/react/hooks.ts
333
- import { useCallback, useEffect as useEffect2, useState as useState3 } from "react";
574
+ import { useCallback, useEffect as useEffect2, useState as useState4 } from "react";
334
575
  var POLL_MS = 7e3;
335
576
  function useUserList(kind, opts) {
336
577
  const { applicationId, baseUrl = "", pollMs = POLL_MS } = opts;
337
- const [slugs, setSlugs] = useState3([]);
338
- const [loading, setLoading] = useState3(true);
339
- const [error, setError] = useState3(null);
578
+ const [slugs, setSlugs] = useState4([]);
579
+ const [loading, setLoading] = useState4(true);
580
+ const [error, setError] = useState4(null);
340
581
  const refresh = useCallback(async () => {
341
582
  setError(null);
342
583
  try {
@@ -403,7 +644,7 @@ function ElvixLifecycleWatcher({
403
644
  }
404
645
 
405
646
  // src/react/elvix-username.tsx
406
- import { useState as useState4 } from "react";
647
+ import { useState as useState5 } from "react";
407
648
 
408
649
  // src/react/lib.ts
409
650
  async function appPost(opts, path, body) {
@@ -465,10 +706,10 @@ function ElvixUsername({
465
706
  onResult
466
707
  }) {
467
708
  const ctx = useElvixContext();
468
- const [value, setValue] = useState4("");
469
- const [busy, setBusy] = useState4(false);
470
- const [error, setError] = useState4(null);
471
- const [done, setDone] = useState4(null);
709
+ const [value, setValue] = useState5("");
710
+ const [busy, setBusy] = useState5(false);
711
+ const [error, setError] = useState5(null);
712
+ const [done, setDone] = useState5(null);
472
713
  async function submit(e) {
473
714
  e.preventDefault();
474
715
  if (!ctx.app) return;
@@ -506,14 +747,14 @@ function ElvixUsername({
506
747
  }
507
748
 
508
749
  // src/react/elvix-avatar.tsx
509
- import { useState as useState5 } from "react";
750
+ import { useState as useState6 } from "react";
510
751
  function ElvixAvatar({
511
752
  onResult
512
753
  }) {
513
754
  const ctx = useElvixContext();
514
- const [busy, setBusy] = useState5(false);
515
- const [error, setError] = useState5(null);
516
- const [preview, setPreview] = useState5(null);
755
+ const [busy, setBusy] = useState6(false);
756
+ const [error, setError] = useState6(null);
757
+ const [preview, setPreview] = useState6(null);
517
758
  async function onFile(e) {
518
759
  const file = e.target.files?.[0];
519
760
  if (!file || !ctx.app) return;
@@ -547,14 +788,14 @@ function ElvixAvatar({
547
788
  }
548
789
 
549
790
  // src/react/elvix-banner.tsx
550
- import { useState as useState6 } from "react";
791
+ import { useState as useState7 } from "react";
551
792
  function ElvixBanner({
552
793
  onResult
553
794
  }) {
554
795
  const ctx = useElvixContext();
555
- const [busy, setBusy] = useState6(false);
556
- const [error, setError] = useState6(null);
557
- const [preview, setPreview] = useState6(null);
796
+ const [busy, setBusy] = useState7(false);
797
+ const [error, setError] = useState7(null);
798
+ const [preview, setPreview] = useState7(null);
558
799
  async function onFile(e) {
559
800
  const file = e.target.files?.[0];
560
801
  if (!file || !ctx.app) return;
@@ -588,18 +829,18 @@ function ElvixBanner({
588
829
  }
589
830
 
590
831
  // src/react/elvix-identity-form.tsx
591
- import { useState as useState7 } from "react";
832
+ import { useState as useState8 } from "react";
592
833
  function ElvixIdentityForm({
593
834
  initialName = "",
594
835
  initialBio = "",
595
836
  onResult
596
837
  }) {
597
838
  const ctx = useElvixContext();
598
- const [name, setName] = useState7(initialName);
599
- const [bio, setBio] = useState7(initialBio);
600
- const [busy, setBusy] = useState7(false);
601
- const [error, setError] = useState7(null);
602
- const [saved, setSaved] = useState7(false);
839
+ const [name, setName] = useState8(initialName);
840
+ const [bio, setBio] = useState8(initialBio);
841
+ const [busy, setBusy] = useState8(false);
842
+ const [error, setError] = useState8(null);
843
+ const [saved, setSaved] = useState8(false);
603
844
  async function submit(e) {
604
845
  e.preventDefault();
605
846
  if (!ctx.app) return;
@@ -619,18 +860,18 @@ function ElvixIdentityForm({
619
860
  }
620
861
 
621
862
  // src/react/elvix-region.tsx
622
- import { useState as useState8 } from "react";
863
+ import { useState as useState9 } from "react";
623
864
  function ElvixRegion({
624
865
  initialCountry = "",
625
866
  initialTimezone = "",
626
867
  onResult
627
868
  }) {
628
869
  const ctx = useElvixContext();
629
- const [country, setCountry] = useState8(initialCountry);
630
- const [timezone, setTimezone] = useState8(initialTimezone);
631
- const [busy, setBusy] = useState8(false);
632
- const [error, setError] = useState8(null);
633
- const [saved, setSaved] = useState8(false);
870
+ const [country, setCountry] = useState9(initialCountry);
871
+ const [timezone, setTimezone] = useState9(initialTimezone);
872
+ const [busy, setBusy] = useState9(false);
873
+ const [error, setError] = useState9(null);
874
+ const [saved, setSaved] = useState9(false);
634
875
  async function submit(e) {
635
876
  e.preventDefault();
636
877
  if (!ctx.app) return;
@@ -650,16 +891,16 @@ function ElvixRegion({
650
891
  }
651
892
 
652
893
  // src/react/elvix-languages.tsx
653
- import { useState as useState9 } from "react";
894
+ import { useState as useState10 } from "react";
654
895
  function ElvixLanguages({
655
896
  initial = [],
656
897
  onResult
657
898
  }) {
658
899
  const ctx = useElvixContext();
659
- const [raw, setRaw] = useState9(initial.join(", "));
660
- const [busy, setBusy] = useState9(false);
661
- const [error, setError] = useState9(null);
662
- const [saved, setSaved] = useState9(false);
900
+ const [raw, setRaw] = useState10(initial.join(", "));
901
+ const [busy, setBusy] = useState10(false);
902
+ const [error, setError] = useState10(null);
903
+ const [saved, setSaved] = useState10(false);
663
904
  async function submit(e) {
664
905
  e.preventDefault();
665
906
  if (!ctx.app) return;
@@ -680,14 +921,14 @@ function ElvixLanguages({
680
921
  }
681
922
 
682
923
  // src/react/elvix-sessions.tsx
683
- import { useEffect as useEffect4, useState as useState10 } from "react";
924
+ import { useEffect as useEffect4, useState as useState11 } from "react";
684
925
  function ElvixSessions({
685
926
  onResult
686
927
  }) {
687
928
  const ctx = useElvixContext();
688
- const [rows, setRows] = useState10(null);
689
- const [error, setError] = useState10(null);
690
- const [busy, setBusy] = useState10(false);
929
+ const [rows, setRows] = useState11(null);
930
+ const [error, setError] = useState11(null);
931
+ const [busy, setBusy] = useState11(false);
691
932
  useEffect4(() => {
692
933
  if (!ctx.app) return;
693
934
  fetch(`${ctx.baseUrl}/api/account/apps/${ctx.app.applicationId}/sessions`, {
@@ -712,14 +953,14 @@ function ElvixSessions({
712
953
  }
713
954
 
714
955
  // src/react/elvix-export.tsx
715
- import { useState as useState11 } from "react";
956
+ import { useState as useState12 } from "react";
716
957
  function ElvixExport({
717
958
  onResult
718
959
  }) {
719
960
  const ctx = useElvixContext();
720
- const [busy, setBusy] = useState11(false);
721
- const [done, setDone] = useState11(false);
722
- const [error, setError] = useState11(null);
961
+ const [busy, setBusy] = useState12(false);
962
+ const [done, setDone] = useState12(false);
963
+ const [error, setError] = useState12(null);
723
964
  async function start() {
724
965
  if (!ctx.app) return;
725
966
  setBusy(true);
@@ -738,16 +979,16 @@ function ElvixExport({
738
979
  }
739
980
 
740
981
  // src/react/elvix-deactivate.tsx
741
- import { useState as useState12 } from "react";
982
+ import { useState as useState13 } from "react";
742
983
  function ElvixDeactivate({
743
984
  onResult
744
985
  }) {
745
986
  const ctx = useElvixContext();
746
- const [pane, setPane] = useState12("warn");
747
- const [challengeId, setChallengeId] = useState12(null);
748
- const [code, setCode] = useState12("");
749
- const [busy, setBusy] = useState12(false);
750
- const [error, setError] = useState12(null);
987
+ const [pane, setPane] = useState13("warn");
988
+ const [challengeId, setChallengeId] = useState13(null);
989
+ const [code, setCode] = useState13("");
990
+ const [busy, setBusy] = useState13(false);
991
+ const [error, setError] = useState13(null);
751
992
  async function startChallenge() {
752
993
  if (!ctx.app) return;
753
994
  setBusy(true);
@@ -804,16 +1045,16 @@ function ElvixDeactivate({
804
1045
  }
805
1046
 
806
1047
  // src/react/elvix-leave.tsx
807
- import { useState as useState13 } from "react";
1048
+ import { useState as useState14 } from "react";
808
1049
  function ElvixLeave({
809
1050
  onResult
810
1051
  }) {
811
1052
  const ctx = useElvixContext();
812
- const [pane, setPane] = useState13("warn");
813
- const [challengeId, setChallengeId] = useState13(null);
814
- const [code, setCode] = useState13("");
815
- const [busy, setBusy] = useState13(false);
816
- const [error, setError] = useState13(null);
1053
+ const [pane, setPane] = useState14("warn");
1054
+ const [challengeId, setChallengeId] = useState14(null);
1055
+ const [code, setCode] = useState14("");
1056
+ const [busy, setBusy] = useState14(false);
1057
+ const [error, setError] = useState14(null);
817
1058
  async function startChallenge() {
818
1059
  if (!ctx.app) return;
819
1060
  setBusy(true);
@@ -870,16 +1111,16 @@ function ElvixLeave({
870
1111
  }
871
1112
 
872
1113
  // src/react/elvix-address-book.tsx
873
- import { useEffect as useEffect5, useState as useState14 } from "react";
1114
+ import { useEffect as useEffect5, useState as useState15 } from "react";
874
1115
  function ElvixAddressBook({
875
1116
  onResult
876
1117
  }) {
877
1118
  const ctx = useElvixContext();
878
- const [rows, setRows] = useState14(null);
879
- const [error, setError] = useState14(null);
880
- const [busy, setBusy] = useState14(false);
881
- const [adding, setAdding] = useState14(false);
882
- const [form, setForm] = useState14({
1119
+ const [rows, setRows] = useState15(null);
1120
+ const [error, setError] = useState15(null);
1121
+ const [busy, setBusy] = useState15(false);
1122
+ const [adding, setAdding] = useState15(false);
1123
+ const [form, setForm] = useState15({
883
1124
  label: "Home",
884
1125
  line1: "",
885
1126
  postalCode: "",
@@ -932,16 +1173,16 @@ function ElvixAddressBook({
932
1173
  }
933
1174
 
934
1175
  // src/react/elvix-legal-entities.tsx
935
- import { useEffect as useEffect6, useState as useState15 } from "react";
1176
+ import { useEffect as useEffect6, useState as useState16 } from "react";
936
1177
  function ElvixLegalEntities({
937
1178
  onResult
938
1179
  }) {
939
1180
  const ctx = useElvixContext();
940
- const [rows, setRows] = useState15(null);
941
- const [error, setError] = useState15(null);
942
- const [busy, setBusy] = useState15(false);
943
- const [adding, setAdding] = useState15(false);
944
- const [form, setForm] = useState15({
1181
+ const [rows, setRows] = useState16(null);
1182
+ const [error, setError] = useState16(null);
1183
+ const [busy, setBusy] = useState16(false);
1184
+ const [adding, setAdding] = useState16(false);
1185
+ const [form, setForm] = useState16({
945
1186
  legalName: "",
946
1187
  taxId: "",
947
1188
  country: ""
@@ -991,6 +1232,7 @@ function ElvixLegalEntities({
991
1232
  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")));
992
1233
  }
993
1234
  export {
1235
+ DEFAULT_COPY,
994
1236
  ElvixAddressBook,
995
1237
  ElvixAvatar,
996
1238
  ElvixBanner,
@@ -1004,8 +1246,10 @@ export {
1004
1246
  ElvixLifecycleWatcher,
1005
1247
  ElvixProvider,
1006
1248
  ElvixRegion,
1249
+ ElvixSecuredBadge,
1007
1250
  ElvixSessions,
1008
1251
  ElvixSignIn,
1252
+ ElvixSignInButton,
1009
1253
  ElvixUsername,
1010
1254
  getElvixToken,
1011
1255
  setElvixToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvix.is/sdk",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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",