@nozle-js/react 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/README.md CHANGED
@@ -25,11 +25,9 @@ function App() {
25
25
  return (
26
26
  <BillingProvider
27
27
  apiKey="pk_live_..."
28
- customerId="cust_123"
29
- baseUrl="https://your-api.example.com"
30
- wsUrl="wss://your-ws.example.com/connection/websocket"
31
- stripeKey="pk_test_..."
32
- features={["tokens_used", "code_completions"]}
28
+ baseUrl="https://api.nozle.app" // Default: https://api.nozle.app
29
+ workspaceId="ws_123" // Optional: workspace scoping
30
+ centrifugoUrl="wss://ws.nozle.app/connection/websocket" // Optional: real-time updates
33
31
  >
34
32
  <YourApp />
35
33
  </BillingProvider>
@@ -37,6 +35,8 @@ function App() {
37
35
  }
38
36
  ```
39
37
 
38
+ > **Note:** LLM wrappers (`wrapOpenAI`, `wrapAnthropic`) and server-side methods (`checkAndDeduct`, `customers.upsert`) are only available in `@nozle-js/node`. The React SDK is for client-side billing UI only.
39
+
40
40
  ## Hooks
41
41
 
42
42
  ### `useCan(feature)`
package/dist/index.cjs CHANGED
@@ -303,6 +303,335 @@ function BillingPortalProvider({
303
303
  }) {
304
304
  return /* @__PURE__ */ jsxRuntime.jsx(BillingPortalContext.Provider, { value: { customerId, apiKey }, children });
305
305
  }
306
+ var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
307
+ function fmtPrice(cents, cur) {
308
+ const s = SYMBOLS[cur] ?? cur + " ";
309
+ const val = cents % 100 === 0 ? String(cents / 100) : (cents / 100).toFixed(2);
310
+ return `${s}${val}`;
311
+ }
312
+ function isEnterprise(p) {
313
+ return /enterprise|custom/i.test(p.name + p.code);
314
+ }
315
+ function v(name, fb) {
316
+ return `var(${name}, ${fb})`;
317
+ }
318
+ var C = {
319
+ bg: v("--nozle-pricing-bg", v("--nozle-background", "#ffffff")),
320
+ cardBg: v("--nozle-pricing-card-bg", v("--nozle-card", "#ffffff")),
321
+ hl: v("--nozle-pricing-highlight", v("--nozle-primary", "#6366f1")),
322
+ border: v("--nozle-pricing-border", v("--nozle-border", "#e5e7eb")),
323
+ radius: v("--nozle-pricing-radius", v("--nozle-radius", "12px")),
324
+ text: v("--nozle-foreground", "#111827"),
325
+ muted: v("--nozle-muted-foreground", "#6b7280"),
326
+ mutedBg: v("--nozle-muted", "#f3f4f6"),
327
+ hlFg: v("--nozle-primary-foreground", "#ffffff")
328
+ };
329
+ function Check() {
330
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) });
331
+ }
332
+ function Toggle({ isAnnual, onChange }) {
333
+ const pill = {
334
+ display: "inline-flex",
335
+ borderRadius: "9999px",
336
+ padding: "4px",
337
+ background: C.mutedBg,
338
+ gap: "2px"
339
+ };
340
+ const tab = (active) => ({
341
+ padding: "8px 20px",
342
+ borderRadius: "9999px",
343
+ border: "none",
344
+ background: active ? C.cardBg : "transparent",
345
+ color: active ? C.text : C.muted,
346
+ fontWeight: active ? 600 : 400,
347
+ fontSize: "0.875rem",
348
+ cursor: "pointer",
349
+ boxShadow: active ? "0 1px 3px rgba(0,0,0,0.08)" : "none",
350
+ transition: "all 0.15s ease"
351
+ });
352
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: pill, children: [
353
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => onChange(false), style: tab(!isAnnual), "aria-pressed": !isAnnual, children: "Monthly" }),
354
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: () => onChange(true), style: tab(isAnnual), "aria-pressed": isAnnual, children: [
355
+ "Annual",
356
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
357
+ marginLeft: "6px",
358
+ fontSize: "0.7rem",
359
+ fontWeight: 600,
360
+ color: isAnnual ? C.hl : C.muted
361
+ }, children: "Save 20%" })
362
+ ] })
363
+ ] }) });
364
+ }
365
+ function Card({
366
+ plan,
367
+ features,
368
+ isCurrent,
369
+ isHighlighted,
370
+ isEnt,
371
+ enterpriseEmail,
372
+ onSelect
373
+ }) {
374
+ const [hover, setHover] = React.useState(false);
375
+ const shadow = isHighlighted ? "0 8px 28px rgba(99,102,241,0.12), 0 2px 8px rgba(99,102,241,0.06)" : hover ? "0 8px 24px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.03)" : "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03)";
376
+ const interval = plan.interval === "yearly" ? "/year" : "/month";
377
+ const handleClick = () => {
378
+ if (isCurrent) return;
379
+ if (isEnt && enterpriseEmail) {
380
+ window.location.href = `mailto:${enterpriseEmail}?subject=Enterprise Plan Inquiry`;
381
+ return;
382
+ }
383
+ onSelect?.();
384
+ };
385
+ return /* @__PURE__ */ jsxRuntime.jsxs(
386
+ "div",
387
+ {
388
+ onMouseEnter: () => setHover(true),
389
+ onMouseLeave: () => setHover(false),
390
+ style: {
391
+ position: "relative",
392
+ display: "flex",
393
+ flexDirection: "column",
394
+ background: C.cardBg,
395
+ border: `1px solid ${isHighlighted ? C.hl : C.border}`,
396
+ borderTopWidth: isHighlighted ? "3px" : "1px",
397
+ borderRadius: C.radius,
398
+ padding: "2rem",
399
+ transition: "box-shadow 0.2s ease, transform 0.2s ease",
400
+ boxShadow: shadow,
401
+ transform: hover && !isCurrent ? "translateY(-2px)" : "none"
402
+ },
403
+ children: [
404
+ isHighlighted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
405
+ position: "absolute",
406
+ top: "-1px",
407
+ left: "50%",
408
+ transform: "translate(-50%, -50%)",
409
+ background: C.hl,
410
+ color: C.hlFg,
411
+ fontSize: "0.7rem",
412
+ fontWeight: 700,
413
+ padding: "4px 12px",
414
+ borderRadius: "9999px",
415
+ letterSpacing: "0.025em",
416
+ textTransform: "uppercase",
417
+ whiteSpace: "nowrap"
418
+ }, children: isCurrent ? "Current Plan" : "Most Popular" }),
419
+ isCurrent && !isHighlighted && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
420
+ position: "absolute",
421
+ top: "0",
422
+ left: "50%",
423
+ transform: "translate(-50%, -50%)",
424
+ background: C.mutedBg,
425
+ color: C.muted,
426
+ fontSize: "0.7rem",
427
+ fontWeight: 700,
428
+ padding: "4px 12px",
429
+ borderRadius: "9999px",
430
+ letterSpacing: "0.025em",
431
+ textTransform: "uppercase",
432
+ whiteSpace: "nowrap"
433
+ }, children: "Current Plan" }),
434
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "1.25rem", marginTop: isHighlighted || isCurrent ? "0.5rem" : 0 }, children: [
435
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { style: {
436
+ fontSize: "1.25rem",
437
+ fontWeight: 700,
438
+ color: C.text,
439
+ margin: 0,
440
+ lineHeight: 1.3
441
+ }, children: plan.name }),
442
+ plan.description && /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
443
+ fontSize: "0.875rem",
444
+ color: C.muted,
445
+ margin: "6px 0 0",
446
+ lineHeight: 1.5
447
+ }, children: plan.description })
448
+ ] }),
449
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginBottom: "1.5rem" }, children: isEnt ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "2.25rem", fontWeight: 700, color: C.text, lineHeight: 1 }, children: "Custom" }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
450
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "2.75rem", fontWeight: 700, color: C.text, lineHeight: 1, letterSpacing: "-0.025em" }, children: fmtPrice(plan.amount_cents, plan.amount_currency) }),
451
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "0.9375rem", color: C.muted, marginLeft: "4px" }, children: interval })
452
+ ] }) }),
453
+ features && features.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { style: {
454
+ listStyle: "none",
455
+ padding: 0,
456
+ margin: "0 0 2rem",
457
+ display: "flex",
458
+ flexDirection: "column",
459
+ gap: "0.75rem",
460
+ flex: 1
461
+ }, children: features.map((feat) => /* @__PURE__ */ jsxRuntime.jsxs("li", { style: {
462
+ display: "flex",
463
+ alignItems: "flex-start",
464
+ gap: "10px",
465
+ fontSize: "0.875rem",
466
+ color: C.text,
467
+ lineHeight: 1.5
468
+ }, children: [
469
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: C.hl, marginTop: "2px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Check, {}) }),
470
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: feat })
471
+ ] }, feat)) }),
472
+ (!features || features.length === 0) && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 } }),
473
+ onSelect && /* @__PURE__ */ jsxRuntime.jsx(
474
+ CTAButton,
475
+ {
476
+ onClick: handleClick,
477
+ disabled: isCurrent,
478
+ variant: isCurrent ? "current" : isEnt ? "outline" : isHighlighted ? "primary" : "secondary",
479
+ children: isCurrent ? "Current Plan" : isEnt ? "Contact Sales" : `Get ${plan.name}`
480
+ }
481
+ )
482
+ ]
483
+ }
484
+ );
485
+ }
486
+ function CTAButton({
487
+ children,
488
+ onClick,
489
+ disabled,
490
+ variant
491
+ }) {
492
+ const [hover, setHover] = React.useState(false);
493
+ const base = {
494
+ width: "100%",
495
+ padding: "12px 24px",
496
+ borderRadius: "10px",
497
+ fontSize: "0.9375rem",
498
+ fontWeight: 600,
499
+ cursor: disabled ? "default" : "pointer",
500
+ border: "none",
501
+ transition: "all 0.15s ease",
502
+ lineHeight: 1.4
503
+ };
504
+ const variants = {
505
+ primary: {
506
+ background: hover && !disabled ? "#4f46e5" : C.hl,
507
+ color: C.hlFg
508
+ },
509
+ secondary: {
510
+ background: hover && !disabled ? "#f0f0f3" : C.mutedBg,
511
+ color: C.text,
512
+ border: `1px solid ${C.border}`
513
+ },
514
+ outline: {
515
+ background: hover && !disabled ? "rgba(99,102,241,0.04)" : "transparent",
516
+ color: C.hl,
517
+ border: `1.5px solid ${C.hl}`
518
+ },
519
+ current: {
520
+ background: C.mutedBg,
521
+ color: C.muted
522
+ }
523
+ };
524
+ return /* @__PURE__ */ jsxRuntime.jsx(
525
+ "button",
526
+ {
527
+ onClick: disabled ? void 0 : onClick,
528
+ disabled,
529
+ onMouseEnter: () => setHover(true),
530
+ onMouseLeave: () => setHover(false),
531
+ style: { ...base, ...variants[variant] },
532
+ children
533
+ }
534
+ );
535
+ }
536
+ function Skeleton() {
537
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
538
+ display: "grid",
539
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
540
+ gap: "1.5rem"
541
+ }, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
542
+ background: C.cardBg,
543
+ border: `1px solid ${C.border}`,
544
+ borderRadius: C.radius,
545
+ padding: "2rem",
546
+ height: "380px"
547
+ }, children: [
548
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 20, width: "40%", borderRadius: 6, background: "#f0f0f0", marginBottom: 12 } }),
549
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 40, width: "50%", borderRadius: 6, background: "#f0f0f0", marginBottom: 24 } }),
550
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "80%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
551
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "70%", borderRadius: 6, background: "#f0f0f0", marginBottom: 10 } }),
552
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 14, width: "60%", borderRadius: 6, background: "#f0f0f0" } })
553
+ ] }, i)) });
554
+ }
555
+ function PricingTable({
556
+ customerId,
557
+ currentPlanCode,
558
+ plans: propPlans,
559
+ features,
560
+ onSelect,
561
+ showToggle = true,
562
+ highlightPlan,
563
+ className,
564
+ enterpriseEmail
565
+ }) {
566
+ const client = useNozleClient();
567
+ const [plans, setPlans] = React.useState(propPlans ?? []);
568
+ const [currentPlan, setCurrentPlan] = React.useState(currentPlanCode ?? null);
569
+ const [isAnnual, setIsAnnual] = React.useState(false);
570
+ const [loading, setLoading] = React.useState(!propPlans);
571
+ React.useEffect(() => {
572
+ if (propPlans) {
573
+ setPlans(propPlans);
574
+ return;
575
+ }
576
+ let cancelled = false;
577
+ client.fetch("/api/v1/plans").then(async (res) => {
578
+ if (!cancelled && res.ok) {
579
+ const data = await res.json();
580
+ setPlans(data.plans || []);
581
+ }
582
+ }).finally(() => {
583
+ if (!cancelled) setLoading(false);
584
+ });
585
+ return () => {
586
+ cancelled = true;
587
+ };
588
+ }, [client, propPlans]);
589
+ React.useEffect(() => {
590
+ if (currentPlanCode !== void 0) {
591
+ setCurrentPlan(currentPlanCode);
592
+ return;
593
+ }
594
+ if (!customerId) return;
595
+ let cancelled = false;
596
+ client.fetch(`/api/v1/billing/status?customer_id=${encodeURIComponent(customerId)}`).then(async (res) => {
597
+ if (!cancelled && res.ok) {
598
+ const data = await res.json();
599
+ setCurrentPlan(data.plan ?? data.planName ?? null);
600
+ }
601
+ }).catch(() => {
602
+ });
603
+ return () => {
604
+ cancelled = true;
605
+ };
606
+ }, [client, customerId, currentPlanCode]);
607
+ const hasAnnual = plans.some((p) => p.interval === "yearly");
608
+ const filtered = showToggle && hasAnnual ? plans.filter((p) => p.interval === (isAnnual ? "yearly" : "monthly") || isEnterprise(p)) : plans;
609
+ if (loading) return /* @__PURE__ */ jsxRuntime.jsx(Skeleton, {});
610
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: {
611
+ fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
612
+ color: C.text,
613
+ background: C.bg
614
+ }, children: [
615
+ showToggle && hasAnnual && /* @__PURE__ */ jsxRuntime.jsx(Toggle, { isAnnual, onChange: setIsAnnual }),
616
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
617
+ display: "grid",
618
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
619
+ gap: "1.5rem"
620
+ }, children: filtered.map((plan, i) => /* @__PURE__ */ jsxRuntime.jsx(
621
+ Card,
622
+ {
623
+ plan,
624
+ features: features?.[i],
625
+ isCurrent: currentPlan === plan.code,
626
+ isHighlighted: highlightPlan === plan.code,
627
+ isEnt: isEnterprise(plan),
628
+ enterpriseEmail,
629
+ onSelect: onSelect ? () => onSelect(plan) : void 0
630
+ },
631
+ plan.code
632
+ )) })
633
+ ] });
634
+ }
306
635
  function PlanCard({
307
636
  id,
308
637
  name,
@@ -447,91 +776,6 @@ function PlanCard({
447
776
  }
448
777
  );
449
778
  }
450
- function PricingTable({
451
- plans,
452
- currentPlanId,
453
- onSelect
454
- }) {
455
- const [isAnnual, setIsAnnual] = React.useState(false);
456
- return /* @__PURE__ */ jsxRuntime.jsxs(
457
- "div",
458
- {
459
- style: {
460
- background: "var(--nozle-background, var(--background))",
461
- color: "var(--nozle-foreground, var(--foreground))"
462
- },
463
- children: [
464
- /* @__PURE__ */ jsxRuntime.jsxs(
465
- "div",
466
- {
467
- style: {
468
- display: "flex",
469
- gap: "0.5rem",
470
- justifyContent: "center",
471
- marginBottom: "1.5rem"
472
- },
473
- children: [
474
- /* @__PURE__ */ jsxRuntime.jsx(
475
- "button",
476
- {
477
- onClick: () => setIsAnnual(false),
478
- style: {
479
- padding: "0.5rem 1rem",
480
- borderRadius: "var(--nozle-radius, 0.5rem)",
481
- border: "1px solid var(--nozle-border, var(--border))",
482
- background: !isAnnual ? "var(--nozle-primary, var(--primary))" : "transparent",
483
- color: !isAnnual ? "var(--nozle-primary-foreground, var(--primary-foreground))" : "var(--nozle-foreground, var(--foreground))",
484
- cursor: "pointer",
485
- fontWeight: !isAnnual ? 600 : 400
486
- },
487
- "aria-pressed": !isAnnual,
488
- children: "Monthly"
489
- }
490
- ),
491
- /* @__PURE__ */ jsxRuntime.jsx(
492
- "button",
493
- {
494
- onClick: () => setIsAnnual(true),
495
- style: {
496
- padding: "0.5rem 1rem",
497
- borderRadius: "var(--nozle-radius, 0.5rem)",
498
- border: "1px solid var(--nozle-border, var(--border))",
499
- background: isAnnual ? "var(--nozle-primary, var(--primary))" : "transparent",
500
- color: isAnnual ? "var(--nozle-primary-foreground, var(--primary-foreground))" : "var(--nozle-foreground, var(--foreground))",
501
- cursor: "pointer",
502
- fontWeight: isAnnual ? 600 : 400
503
- },
504
- "aria-pressed": isAnnual,
505
- children: "Annual"
506
- }
507
- )
508
- ]
509
- }
510
- ),
511
- /* @__PURE__ */ jsxRuntime.jsx(
512
- "div",
513
- {
514
- style: {
515
- display: "grid",
516
- gridTemplateColumns: `repeat(${Math.min(plans.length, 3)}, 1fr)`,
517
- gap: "1rem"
518
- },
519
- children: plans.map((plan) => /* @__PURE__ */ jsxRuntime.jsx(
520
- PlanCard,
521
- {
522
- ...plan,
523
- isAnnual,
524
- isCurrent: plan.id === currentPlanId,
525
- onSelect: onSelect ? () => onSelect(plan.id) : void 0
526
- },
527
- plan.id
528
- ))
529
- }
530
- )
531
- ]
532
- }
533
- );
534
- }
535
779
  function PlanComparison({
536
780
  plans,
537
781
  features