@nebulr-group/bridge-svelte 0.4.0-beta.3 → 0.4.0-beta.5

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.
@@ -10,6 +10,11 @@
10
10
  onComplete?: () => void;
11
11
  onError?: (error: Error) => void;
12
12
  loginHref?: string;
13
+ /**
14
+ * Override the built-in heading ("Set new password" / "Reset your password").
15
+ * Pass `null`/`''` to render no heading and use your own page title.
16
+ */
17
+ heading?: string | null;
13
18
  }
14
19
 
15
20
  let {
@@ -17,6 +22,7 @@
17
22
  onComplete,
18
23
  onError,
19
24
  loginHref = '/login',
25
+ heading = undefined,
20
26
  class: className,
21
27
  style,
22
28
  ...rest
@@ -33,6 +39,14 @@
33
39
 
34
40
  const isSetMode = $derived(!!token);
35
41
 
42
+ // In a success state ("Password set" / the email-sent alert) the form heading
43
+ // is redundant, so suppress it. Otherwise use the override (if provided) or
44
+ // the built-in, state-appropriate heading.
45
+ const builtInHeading = $derived(isSetMode ? 'Set new password' : 'Reset your password');
46
+ const wrapperHeading = $derived(
47
+ passwordReset || emailSent ? null : heading !== undefined ? heading : builtInHeading,
48
+ );
49
+
36
50
  async function handleSendLink() {
37
51
  if (loading) return;
38
52
  error = null;
@@ -77,7 +91,7 @@
77
91
  </script>
78
92
 
79
93
  <AuthFormWrapper
80
- heading={isSetMode ? 'Set new password' : 'Reset your password'}
94
+ heading={wrapperHeading}
81
95
  class={className}
82
96
  {style}
83
97
  {...rest}
@@ -4,6 +4,11 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
4
4
  onComplete?: () => void;
5
5
  onError?: (error: Error) => void;
6
6
  loginHref?: string;
7
+ /**
8
+ * Override the built-in heading ("Set new password" / "Reset your password").
9
+ * Pass `null`/`''` to render no heading and use your own page title.
10
+ */
11
+ heading?: string | null;
7
12
  }
8
13
  declare const ForgotPassword: import("svelte").Component<Props, {}, "">;
9
14
  type ForgotPassword = ReturnType<typeof ForgotPassword>;
@@ -219,7 +219,7 @@
219
219
 
220
220
  <!-- Inline forgot password -->
221
221
  {:else if step === 'forgot-password'}
222
- <AuthFormWrapper heading="Reset your password">
222
+ <AuthFormWrapper heading={fpEmailSent ? null : 'Reset your password'}>
223
223
  {#if error}
224
224
  <Alert variant="error">{error}</Alert>
225
225
  {/if}
@@ -257,7 +257,7 @@
257
257
 
258
258
  <!-- Inline magic link -->
259
259
  {:else if step === 'magic-link'}
260
- <AuthFormWrapper heading="Sign in with email link">
260
+ <AuthFormWrapper heading={mlSent ? null : 'Sign in with email link'}>
261
261
  {#if error}
262
262
  <Alert variant="error">{error}</Alert>
263
263
  {/if}
@@ -9,12 +9,15 @@
9
9
  onSent?: () => void;
10
10
  onError?: (error: Error) => void;
11
11
  loginHref?: string;
12
+ /** Heading text. Pass `null`/`''` to render no heading and use your own page title. */
13
+ heading?: string | null;
12
14
  }
13
15
 
14
16
  let {
15
17
  onSent,
16
18
  onError,
17
19
  loginHref = '/login',
20
+ heading = 'Sign in with email link',
18
21
  class: className,
19
22
  style,
20
23
  ...rest
@@ -51,7 +54,7 @@
51
54
  }
52
55
  </script>
53
56
 
54
- <AuthFormWrapper heading="Sign in with email link" class={className} {style} {...rest}>
57
+ <AuthFormWrapper heading={sent ? null : heading} class={className} {style} {...rest}>
55
58
  {#if error}
56
59
  <Alert variant="error">{error}</Alert>
57
60
  {/if}
@@ -3,6 +3,8 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
3
3
  onSent?: () => void;
4
4
  onError?: (error: Error) => void;
5
5
  loginHref?: string;
6
+ /** Heading text. Pass `null`/`''` to render no heading and use your own page title. */
7
+ heading?: string | null;
6
8
  }
7
9
  declare const MagicLink: import("svelte").Component<Props, {}, "">;
8
10
  type MagicLink = ReturnType<typeof MagicLink>;
@@ -13,7 +13,8 @@
13
13
  showLoginLink?: boolean;
14
14
  /** Override login page URL. Default: derived from BridgeConfig loginRoute. */
15
15
  loginHref?: string | undefined;
16
- heading?: string;
16
+ /** Heading text. Pass `null`/`''` to render no heading and use your own page title. */
17
+ heading?: string | null;
17
18
  footer?: Snippet;
18
19
  }
19
20
 
@@ -55,7 +56,9 @@
55
56
  }
56
57
  </script>
57
58
 
58
- <AuthFormWrapper {heading} class={className} {style} {...rest}>
59
+ <!-- In the success state, the "Check your email" heading below is the title,
60
+ so suppress the form heading to avoid two stacked headings. -->
61
+ <AuthFormWrapper heading={success ? null : heading} class={className} {style} {...rest}>
59
62
  {#if success}
60
63
  <h2 class="bridge-success-heading">Check your email</h2>
61
64
  <p class="bridge-step-desc">We sent a verification link to <strong>{email}</strong>. Check your inbox to activate your account.</p>
@@ -6,7 +6,8 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
6
6
  showLoginLink?: boolean;
7
7
  /** Override login page URL. Default: derived from BridgeConfig loginRoute. */
8
8
  loginHref?: string | undefined;
9
- heading?: string;
9
+ /** Heading text. Pass `null`/`''` to render no heading and use your own page title. */
10
+ heading?: string | null;
10
11
  footer?: Snippet;
11
12
  }
12
13
  declare const SignupForm: import("svelte").Component<Props, {}, "">;
@@ -3,7 +3,8 @@
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
 
5
5
  interface Props extends HTMLAttributes<HTMLDivElement> {
6
- heading?: string;
6
+ /** Heading text. Pass `null` or `''` to render no heading. */
7
+ heading?: string | null;
7
8
  headingSnippet?: Snippet;
8
9
  children?: Snippet;
9
10
  }
@@ -1,7 +1,8 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  interface Props extends HTMLAttributes<HTMLDivElement> {
4
- heading?: string;
4
+ /** Heading text. Pass `null` or `''` to render no heading. */
5
+ heading?: string | null;
5
6
  headingSnippet?: Snippet;
6
7
  children?: Snippet;
7
8
  }
@@ -8,11 +8,20 @@
8
8
  import Alert from '../sdk-auth/shared/Alert.svelte';
9
9
  import Spinner from '../sdk-auth/shared/Spinner.svelte';
10
10
 
11
+ type BillingInterval = PriceOfferSdk['recurrenceInterval'];
12
+
11
13
  interface Props extends HTMLAttributes<HTMLDivElement> {
12
14
  successRedirect?: string;
13
15
  cancelRedirect?: string;
16
+ /**
17
+ * Which billing interval tab is selected by default (`'month'` | `'year'` |
18
+ * `'week'` | `'day'`). Falls back to the first available interval when the
19
+ * requested one isn't offered by any plan. Default: `'month'`.
20
+ */
21
+ defaultInterval?: BillingInterval;
14
22
  onSelect?: (detail: { plan: Plan; price: PriceOfferSdk }) => void;
15
- planCard?: Snippet<[{ plan: Plan; prices: PriceOfferSdk[]; isCurrent: boolean; onPick: (price: PriceOfferSdk) => void }]>;
23
+ /** Custom card renderer. `prices` is the plan's full price list; `interval` is the active tab. */
24
+ planCard?: Snippet<[{ plan: Plan; prices: PriceOfferSdk[]; isCurrent: boolean; interval: BillingInterval; onPick: (price: PriceOfferSdk) => void }]>;
16
25
  emptyState?: Snippet;
17
26
  loadingState?: Snippet;
18
27
  }
@@ -20,6 +29,7 @@
20
29
  let {
21
30
  successRedirect = '/subscription',
22
31
  cancelRedirect = '/subscription',
32
+ defaultInterval = 'month',
23
33
  onSelect,
24
34
  planCard,
25
35
  emptyState,
@@ -37,6 +47,50 @@
37
47
  const loading = $derived($subscriptionStore.loading);
38
48
  const storeError = $derived($subscriptionStore.error);
39
49
 
50
+ const INTERVAL_LABELS: Record<BillingInterval, string> = {
51
+ day: 'Daily',
52
+ week: 'Weekly',
53
+ month: 'Monthly',
54
+ year: 'Yearly',
55
+ };
56
+
57
+ // Distinct billing intervals offered by any *paid* price across all plans,
58
+ // in a stable display order. (Free, amount-0 prices are interval-agnostic
59
+ // and always shown, so they don't contribute a tab.)
60
+ const availableIntervals = $derived.by<BillingInterval[]>(() => {
61
+ const order: BillingInterval[] = ['day', 'week', 'month', 'year'];
62
+ return order.filter((i) =>
63
+ (plans ?? []).some((plan) =>
64
+ plan.prices.some((p) => p.amount > 0 && p.recurrenceInterval === i),
65
+ ),
66
+ );
67
+ });
68
+
69
+ // Only render the toggle when there's a genuine choice (≥2 intervals).
70
+ const showIntervalTabs = $derived(availableIntervals.length >= 2);
71
+
72
+ // The user's explicit tab choice (null until they pick one). The effective
73
+ // selection is derived — the override when still valid, else the requested
74
+ // `defaultInterval`, else the first available. Kept as $derived (rather than
75
+ // an $effect that writes state) so it reconciles automatically as plans load.
76
+ let intervalOverride = $state<BillingInterval | null>(null);
77
+ const selectedInterval = $derived.by<BillingInterval>(() => {
78
+ if (intervalOverride && availableIntervals.includes(intervalOverride)) {
79
+ return intervalOverride;
80
+ }
81
+ return availableIntervals.includes(defaultInterval)
82
+ ? defaultInterval
83
+ : (availableIntervals[0] ?? defaultInterval);
84
+ });
85
+
86
+ // Prices to show for a plan under the active tab: the matching interval, plus
87
+ // any free (amount-0) prices which apply regardless of interval.
88
+ function pricesForInterval(plan: Plan): PriceOfferSdk[] {
89
+ return plan.prices.filter(
90
+ (p) => p.amount === 0 || p.recurrenceInterval === selectedInterval,
91
+ );
92
+ }
93
+
40
94
  type UiState = 'idle' | 'payment-failed' | 'setup-payments' | 'select-plan' | 'active' | 'trial';
41
95
 
42
96
  const uiState = $derived<UiState>((() => {
@@ -151,13 +205,35 @@
151
205
  <p class="bridge-plan-empty">No plans available.</p>
152
206
  {/if}
153
207
  {:else if plans}
208
+ {#if showIntervalTabs}
209
+ <div
210
+ class="bridge-plan-interval-tabs"
211
+ data-bridge-plan-interval-tabs
212
+ role="group"
213
+ aria-label="Billing interval"
214
+ >
215
+ {#each availableIntervals as interval (interval)}
216
+ <button
217
+ type="button"
218
+ class="bridge-plan-interval-tab"
219
+ data-active={interval === selectedInterval}
220
+ aria-pressed={interval === selectedInterval}
221
+ onclick={() => (intervalOverride = interval)}
222
+ >
223
+ {INTERVAL_LABELS[interval]}
224
+ </button>
225
+ {/each}
226
+ </div>
227
+ {/if}
228
+
154
229
  <div class="bridge-plan-cards" data-bridge-plan-cards>
155
230
  {#each plans as plan (plan.key)}
156
231
  {@const isCurrent = plan.key === currentPlanKey}
157
232
  {@const onPick = (price: PriceOfferSdk) => handlePick(plan, price)}
233
+ {@const visiblePrices = pricesForInterval(plan)}
158
234
 
159
235
  {#if planCard}
160
- {@render planCard({ plan, prices: plan.prices, isCurrent, onPick })}
236
+ {@render planCard({ plan, prices: plan.prices, isCurrent, interval: selectedInterval, onPick })}
161
237
  {:else}
162
238
  <div
163
239
  data-bridge-plan-card
@@ -177,7 +253,7 @@
177
253
  {/if}
178
254
 
179
255
  <div class="bridge-plan-prices">
180
- {#each plan.prices as price (price.recurrenceInterval + price.currency)}
256
+ {#each visiblePrices as price (price.recurrenceInterval + price.currency)}
181
257
  <button
182
258
  class="bridge-btn-primary bridge-plan-select-btn"
183
259
  disabled={isCurrent || picking}
@@ -201,6 +277,10 @@
201
277
  >
202
278
  {isCurrent ? 'Current plan' : 'Select plan'}
203
279
  </button>
280
+ {:else if visiblePrices.length === 0}
281
+ <p class="bridge-plan-unavailable" data-bridge-plan-unavailable>
282
+ Not available {INTERVAL_LABELS[selectedInterval].toLowerCase()}
283
+ </p>
204
284
  {/if}
205
285
  </div>
206
286
  </div>
@@ -1,17 +1,26 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import type { Plan, PriceOfferSdk } from '@nebulr-group/bridge-auth-core';
4
+ type BillingInterval = PriceOfferSdk['recurrenceInterval'];
4
5
  interface Props extends HTMLAttributes<HTMLDivElement> {
5
6
  successRedirect?: string;
6
7
  cancelRedirect?: string;
8
+ /**
9
+ * Which billing interval tab is selected by default (`'month'` | `'year'` |
10
+ * `'week'` | `'day'`). Falls back to the first available interval when the
11
+ * requested one isn't offered by any plan. Default: `'month'`.
12
+ */
13
+ defaultInterval?: BillingInterval;
7
14
  onSelect?: (detail: {
8
15
  plan: Plan;
9
16
  price: PriceOfferSdk;
10
17
  }) => void;
18
+ /** Custom card renderer. `prices` is the plan's full price list; `interval` is the active tab. */
11
19
  planCard?: Snippet<[{
12
20
  plan: Plan;
13
21
  prices: PriceOfferSdk[];
14
22
  isCurrent: boolean;
23
+ interval: BillingInterval;
15
24
  onPick: (price: PriceOfferSdk) => void;
16
25
  }]>;
17
26
  emptyState?: Snippet;
@@ -104,6 +104,10 @@ export function startBridgeRuntime(options = {}) {
104
104
  getAuthToken: () => _currentAuthToken,
105
105
  });
106
106
  let _connectedOnce = false;
107
+ // Set just before we call _realtime.reauthorize() so the resulting
108
+ // reconnect's setOnOpen handler knows the token is already fresh and skips
109
+ // its proactive refresh — see the loop note in setOnOpen below.
110
+ let _reauthInFlight = false;
107
111
  _realtime.setOnOpen(() => {
108
112
  _setRealtimeStatus('open');
109
113
  // On reconnect (not initial connect), proactively refresh tokens.
@@ -111,7 +115,18 @@ export function startBridgeRuntime(options = {}) {
111
115
  // client missed the user.state_changed broadcast. Refreshing here
112
116
  // syncs tokens before the first post-reconnect request can fail with
113
117
  // TOKEN_VERSION_STALE.
114
- if (_connectedOnce) {
118
+ //
119
+ // EXCEPT when this reconnect was caused by our OWN reauthorize() below
120
+ // (a token-only refresh). In that case the token is already current, so
121
+ // refreshing again would mint yet another JWT, which the tokenStore
122
+ // subscription would see as a change and reauthorize() again → reconnect
123
+ // → setOnOpen → refresh → … an unbounded loop that hammers /auth/token
124
+ // (observed ~32 cycles/sec, jamming the page's main thread and stalling
125
+ // every downstream wait). Only genuine, externally-triggered reconnects
126
+ // (network blips, server restarts) should trigger the catch-up refresh.
127
+ const causedByReauthorize = _reauthInFlight;
128
+ _reauthInFlight = false;
129
+ if (_connectedOnce && !causedByReauthorize) {
115
130
  getBridgeAuth().refreshTokens().catch(() => { });
116
131
  }
117
132
  _connectedOnce = true;
@@ -224,6 +239,10 @@ export function startBridgeRuntime(options = {}) {
224
239
  // Centrifugo's connection-token TTL drops it. Force a reauthorize so the
225
240
  // server re-validates against the new token immediately.
226
241
  if (prevAuthToken && _currentAuthToken && prevAuthToken !== _currentAuthToken) {
242
+ // Flag this as a self-induced reconnect so setOnOpen does NOT fire its
243
+ // proactive refresh (which would mint a new token → land back here →
244
+ // loop forever). The token we're reauthorizing with is already current.
245
+ _reauthInFlight = true;
227
246
  void _realtime.reauthorize();
228
247
  }
229
248
  });
@@ -36,7 +36,11 @@ export interface BridgeTenantSurface {
36
36
  id: Readable<string | null>;
37
37
  /** Workspace display name. Populated by session.snapshot. */
38
38
  name: Readable<string | null>;
39
- /** Canonical subscription (plan + status + endsAt). Populated by session.snapshot. */
39
+ /**
40
+ * Canonical subscription (plan + status + endsAt). Live via `session.snapshot`
41
+ * when the realtime channel is active; otherwise transparently falls back to
42
+ * the REST subscription endpoint (fetched lazily on first read).
43
+ */
40
44
  subscription: Readable<SubscriptionSnapshot | null>;
41
45
  /**
42
46
  * Entitlements scope. `snapshot` is the full `{ key: boolean }` map;
@@ -52,7 +56,11 @@ export interface BridgeTenantSurface {
52
56
  export interface BridgeSurface {
53
57
  app: BridgeAppSurface;
54
58
  tenant: BridgeTenantSurface;
55
- /** Authenticated user (id/email/role/tenantId). Populated by session.snapshot. */
59
+ /**
60
+ * Authenticated user (id/email/role/tenantId). Live via `session.snapshot`
61
+ * when the realtime channel is active; otherwise transparently seeded from the
62
+ * access-token claims (the only client-side source of `role`).
63
+ */
56
64
  user: Readable<UserSnapshot | null>;
57
65
  /**
58
66
  * Phase 5 (TBP-328) — single attribute write surface. `set/bind/bindMany`
@@ -16,11 +16,11 @@
16
16
  * only exposes the singleton aggregate `bridge`; consumers can import it
17
17
  * directly until the context hook ships.
18
18
  */
19
- import { derived } from 'svelte/store';
19
+ import { derived, get } from 'svelte/store';
20
20
  import { appBrandingStore, tenantEntitlementsStore, tenantIdStore, tenantNameStore, tenantSubscriptionStore, userSnapshotStore, } from './snapshot-stores.js';
21
21
  import { LazySlice } from './lazy-slice.js';
22
22
  import { DevAttributeProvider } from '@nebulr-group/bridge-auth-core';
23
- import { getBridgeAuth } from './bridge-instance.js';
23
+ import { getBridgeAuth, tokenStore, subscriptionStore, loadSubscription } from './bridge-instance.js';
24
24
  import { bridgeEvents } from './events.js';
25
25
  let _lastEntitlements = null;
26
26
  // Keep `_lastEntitlements` in sync so `can()` can answer synchronously.
@@ -54,6 +54,89 @@ const _plansSlice = new LazySlice({
54
54
  // decision #20: providers < setContext < per-call, and dev's provider is
55
55
  // effectively the per-call equivalent for set/bind/bindMany).
56
56
  const _devAttributes = new DevAttributeProvider();
57
+ // ── Layered sources behind `bridge.user` / `bridge.tenant.subscription` ──────
58
+ //
59
+ // Design contract (locked): the developer always reads the SAME store; whether
60
+ // the value comes from the live realtime channel or a static fallback is
61
+ // invisible to them.
62
+ // A. Default — the `session.snapshot` channel populates and live-updates the
63
+ // snapshot stores; they win whenever present.
64
+ // B. Fallback (channel not active) — `bridge.user` is seeded from the access
65
+ // token claims (instant, full-fidelity for role); `bridge.tenant.subscription`
66
+ // falls back to the REST subscription endpoint (full plan name/status,
67
+ // fetched lazily). These are NOT live-updated — by design, since the app
68
+ // opted out of the live channel.
69
+ /**
70
+ * JWT-derived user, recomputed whenever the token set changes (login, refresh,
71
+ * workspace switch, logout). Reuses auth-core's `getCurrentUser()` — the single
72
+ * home for access-token claim parsing. `null` when unauthenticated.
73
+ */
74
+ const _jwtUser = derived(tokenStore, ($t) => {
75
+ if (!$t?.accessToken)
76
+ return null;
77
+ let claims = null;
78
+ try {
79
+ claims = getBridgeAuth().getCurrentUser();
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ if (!claims)
85
+ return null;
86
+ return {
87
+ id: claims.id,
88
+ email: claims.email,
89
+ // UserSnapshot requires these; the live snapshot supplies the authoritative
90
+ // values, the JWT fallback fills them when claims carry them.
91
+ role: claims.role ?? '',
92
+ tenantId: claims.tenantId ?? '',
93
+ };
94
+ });
95
+ /** `bridge.user`: live snapshot wins; JWT seed is the static fallback. */
96
+ const _userSurface = derived([userSnapshotStore, _jwtUser], ([snap, jwt]) => snap ?? jwt);
97
+ /** Map the REST `SubscriptionStatus` to the canonical `SubscriptionSnapshot` shape. */
98
+ function mapRestSubscription(status) {
99
+ if (!status)
100
+ return null;
101
+ const plan = status.plan;
102
+ let slug = '';
103
+ let name = '';
104
+ if (typeof plan === 'string') {
105
+ slug = plan;
106
+ name = plan;
107
+ }
108
+ else if (plan) {
109
+ slug = plan.key;
110
+ name = plan.name;
111
+ }
112
+ // REST exposes lifecycle as booleans; derive a best-effort status string to
113
+ // parallel the snapshot's `status` field.
114
+ const statusStr = status.paymentFailed
115
+ ? 'past_due'
116
+ : status.trial
117
+ ? 'trialing'
118
+ : status.paymentsEnabled || status.plan
119
+ ? 'active'
120
+ : 'incomplete';
121
+ return { plan: { slug, name }, status: statusStr };
122
+ }
123
+ /**
124
+ * `bridge.tenant.subscription`: live snapshot wins; otherwise the REST endpoint
125
+ * is fetched lazily on first subscribe (only when no snapshot and not already
126
+ * loading/loaded). In live apps the snapshot is present, so REST is never hit.
127
+ */
128
+ const _subscriptionMerged = derived([tenantSubscriptionStore, subscriptionStore], ([snap, rest]) => snap ?? mapRestSubscription(rest.status));
129
+ const _subscriptionSurface = {
130
+ subscribe(run, invalidate) {
131
+ const noSnapshot = get(tenantSubscriptionStore) == null;
132
+ const rest = get(subscriptionStore);
133
+ if (noSnapshot && rest.status == null && !rest.loading) {
134
+ // Lazy fallback fetch — throws if unauthenticated; swallow (stays null).
135
+ loadSubscription().catch(() => { });
136
+ }
137
+ return _subscriptionMerged.subscribe(run, invalidate);
138
+ },
139
+ };
57
140
  export const bridge = {
58
141
  app: {
59
142
  branding: appBrandingStore,
@@ -62,13 +145,13 @@ export const bridge = {
62
145
  tenant: {
63
146
  id: tenantIdStore,
64
147
  name: tenantNameStore,
65
- subscription: tenantSubscriptionStore,
148
+ subscription: _subscriptionSurface,
66
149
  entitlements: {
67
150
  snapshot: tenantEntitlementsStore,
68
151
  can: entitlementsCan,
69
152
  },
70
153
  },
71
- user: userSnapshotStore,
154
+ user: _userSurface,
72
155
  attributes: _devAttributes,
73
156
  events: bridgeEvents,
74
157
  };
package/dist/styles.css CHANGED
@@ -935,6 +935,39 @@
935
935
  margin-top: 0.75rem;
936
936
  }
937
937
 
938
+ .bridge-plan-interval-tabs {
939
+ display: inline-flex;
940
+ gap: 0.25rem;
941
+ margin-bottom: 1.25rem;
942
+ padding: 0.25rem;
943
+ border: 1px solid var(--bridge-border, #e2e8f0);
944
+ border-radius: var(--bridge-border-radius, 0.5rem);
945
+ background: var(--bridge-muted-bg, var(--bridge-bg-muted, #f8fafc));
946
+ }
947
+
948
+ .bridge-plan-interval-tab {
949
+ appearance: none;
950
+ border: none;
951
+ background: transparent;
952
+ cursor: pointer;
953
+ font: inherit;
954
+ font-size: 0.8125rem;
955
+ padding: 0.375rem 0.875rem;
956
+ border-radius: calc(var(--bridge-border-radius, 0.5rem) - 0.2rem);
957
+ color: var(--bridge-muted, #64748b);
958
+ }
959
+
960
+ .bridge-plan-interval-tab[data-active="true"] {
961
+ background: var(--bridge-primary, #2563eb);
962
+ color: var(--bridge-primary-fg, var(--bridge-primary-foreground, #fff));
963
+ }
964
+
965
+ .bridge-plan-unavailable {
966
+ margin: 0;
967
+ font-size: 0.8125rem;
968
+ color: var(--bridge-muted, #64748b);
969
+ }
970
+
938
971
  .bridge-plan-cards {
939
972
  display: grid;
940
973
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebulr-group/bridge-svelte",
3
- "version": "0.4.0-beta.3",
3
+ "version": "0.4.0-beta.5",
4
4
  "description": "Bridge Svelte library, This library helps you to add bridge authentication and feature flags, and payments to your svelte application.",
5
5
  "author": "Iman Pouya",
6
6
  "license": "MIT",
@@ -67,7 +67,7 @@
67
67
  }
68
68
  },
69
69
  "dependencies": {
70
- "@nebulr-group/bridge-auth-core": "0.4.0-beta.2"
70
+ "@nebulr-group/bridge-auth-core": "0.4.0-beta.7"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@sveltejs/kit": "^2.16.0",