@r2digisolutions/components 0.19.8 → 0.19.9
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/components/molecules/Form/Form.svelte +1 -0
- package/dist/components/organisms/Dialog/Dialog.svelte +7 -2
- package/dist/components/organisms/PricingTable/PricingTable.stories.d.ts +1 -0
- package/dist/components/organisms/PricingTable/PricingTable.stories.js +12 -1
- package/dist/components/organisms/PricingTable/PricingTable.svelte +214 -136
- package/dist/components/organisms/PricingTable/PricingTable.svelte.d.ts +10 -2
- package/dist/components/organisms/PricingTable/PricingTableStory.svelte +9 -2
- package/package.json +1 -1
|
@@ -279,8 +279,13 @@
|
|
|
279
279
|
{#if children}
|
|
280
280
|
<div
|
|
281
281
|
class={[
|
|
282
|
-
'px-5 py-2 text-sm text-primary',
|
|
283
|
-
scrollable &&
|
|
282
|
+
'min-h-0 flex-1 px-5 py-2 text-sm text-primary',
|
|
283
|
+
scrollable &&
|
|
284
|
+
(size === 'full'
|
|
285
|
+
? 'max-h-[calc(100vh-11rem)] overflow-y-auto'
|
|
286
|
+
: size === 'xl'
|
|
287
|
+
? 'max-h-[min(70vh,36rem)] overflow-y-auto'
|
|
288
|
+
: 'max-h-[min(60vh,28rem)] overflow-y-auto')
|
|
284
289
|
]}
|
|
285
290
|
>
|
|
286
291
|
{@render children()}
|
|
@@ -62,6 +62,7 @@ export declare const Bento: Story;
|
|
|
62
62
|
export declare const Compact: Story;
|
|
63
63
|
export declare const Split: Story;
|
|
64
64
|
export declare const Table: Story;
|
|
65
|
+
export declare const ListCompare: Story;
|
|
65
66
|
export declare const CardsPlusComparison: Story;
|
|
66
67
|
export declare const TwoPlans: Story;
|
|
67
68
|
export declare const FourPlans: Story;
|
|
@@ -9,7 +9,17 @@ const meta = {
|
|
|
9
9
|
argTypes: {
|
|
10
10
|
layout: {
|
|
11
11
|
control: 'select',
|
|
12
|
-
options: [
|
|
12
|
+
options: [
|
|
13
|
+
'grid',
|
|
14
|
+
'horizontal',
|
|
15
|
+
'vertical',
|
|
16
|
+
'bento',
|
|
17
|
+
'compact',
|
|
18
|
+
'split',
|
|
19
|
+
'table',
|
|
20
|
+
'list',
|
|
21
|
+
'compare'
|
|
22
|
+
]
|
|
13
23
|
},
|
|
14
24
|
columns: { control: 'select', options: [2, 3, 4] },
|
|
15
25
|
planSet: {
|
|
@@ -39,6 +49,7 @@ export const Bento = { args: { layout: 'bento', planSet: 'bento' } };
|
|
|
39
49
|
export const Compact = { args: { layout: 'compact', planSet: 'four', columns: 4 } };
|
|
40
50
|
export const Split = { args: { layout: 'split' } };
|
|
41
51
|
export const Table = { args: { layout: 'table', planSet: 'comparison' } };
|
|
52
|
+
export const ListCompare = { args: { layout: 'list', planSet: 'comparison' } };
|
|
42
53
|
export const CardsPlusComparison = {
|
|
43
54
|
args: { layout: 'grid', showComparison: true, planSet: 'comparison' }
|
|
44
55
|
};
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
| 'bento'
|
|
7
7
|
| 'compact'
|
|
8
8
|
| 'split'
|
|
9
|
-
| 'table'
|
|
9
|
+
| 'table'
|
|
10
|
+
| 'list'
|
|
11
|
+
| 'compare';
|
|
10
12
|
|
|
11
13
|
export type PricingFeature =
|
|
12
14
|
| string
|
|
@@ -22,8 +24,12 @@
|
|
|
22
24
|
price: string;
|
|
23
25
|
/** Yearly price when billing toggle is yearly */
|
|
24
26
|
priceYearly?: string;
|
|
27
|
+
/** Struck-through list price (optional) */
|
|
28
|
+
compareAtPrice?: string;
|
|
25
29
|
period?: string;
|
|
26
30
|
periodYearly?: string;
|
|
31
|
+
/** Small hint under the price (e.g. savings) */
|
|
32
|
+
priceNote?: string;
|
|
27
33
|
description?: string;
|
|
28
34
|
features: PricingFeature[];
|
|
29
35
|
cta?: string;
|
|
@@ -45,6 +51,8 @@
|
|
|
45
51
|
|
|
46
52
|
<script lang="ts">
|
|
47
53
|
import type { Snippet } from 'svelte';
|
|
54
|
+
import Check from '@lucide/svelte/icons/check';
|
|
55
|
+
import X from '@lucide/svelte/icons/x';
|
|
48
56
|
import Button from '../../atoms/Button/Button.svelte';
|
|
49
57
|
import Badge from '../../atoms/Badge/Badge.svelte';
|
|
50
58
|
import SegmentedControl from '../../molecules/SegmentedControl/SegmentedControl.svelte';
|
|
@@ -61,7 +69,8 @@
|
|
|
61
69
|
/** Show monthly/yearly toggle when plans have priceYearly */
|
|
62
70
|
showBillingToggle?: boolean;
|
|
63
71
|
billingPeriod?: 'monthly' | 'yearly';
|
|
64
|
-
|
|
72
|
+
billingLabels?: { monthly?: string; yearly?: string };
|
|
73
|
+
/** Explicit feature matrix for compare/list/table (auto-built when omitted) */
|
|
65
74
|
comparisonRows?: PricingComparisonRow[];
|
|
66
75
|
/** Append comparison matrix below card layouts */
|
|
67
76
|
showComparison?: boolean;
|
|
@@ -81,6 +90,7 @@
|
|
|
81
90
|
maxFeatures,
|
|
82
91
|
showBillingToggle = false,
|
|
83
92
|
billingPeriod = $bindable<'monthly' | 'yearly'>('monthly'),
|
|
93
|
+
billingLabels,
|
|
84
94
|
comparisonRows,
|
|
85
95
|
showComparison = false,
|
|
86
96
|
class: className = '',
|
|
@@ -89,6 +99,10 @@
|
|
|
89
99
|
onbillingperiodchange
|
|
90
100
|
}: PricingTableProps = $props();
|
|
91
101
|
|
|
102
|
+
const isCompareLayout = $derived(
|
|
103
|
+
layout === 'table' || layout === 'list' || layout === 'compare'
|
|
104
|
+
);
|
|
105
|
+
|
|
92
106
|
const resolvedColumns = $derived.by(() => {
|
|
93
107
|
if (columns) return columns;
|
|
94
108
|
const n = plans.length;
|
|
@@ -138,12 +152,16 @@
|
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
function displayPeriod(plan: PricingPlan) {
|
|
141
|
-
if (billingPeriod === 'yearly') {
|
|
155
|
+
if (billingPeriod === 'yearly' && plan.priceYearly) {
|
|
142
156
|
return normalizePeriod(plan.periodYearly ?? 'yr');
|
|
143
157
|
}
|
|
144
158
|
return normalizePeriod(plan.period);
|
|
145
159
|
}
|
|
146
160
|
|
|
161
|
+
const canToggleBilling = $derived(
|
|
162
|
+
showBillingToggle && plans.some((p) => Boolean(p.priceYearly))
|
|
163
|
+
);
|
|
164
|
+
|
|
147
165
|
function featureList(plan: PricingPlan) {
|
|
148
166
|
const list = plan.features.map((f) =>
|
|
149
167
|
typeof f === 'string' ? { label: f, included: true as boolean | undefined } : f
|
|
@@ -189,7 +207,7 @@
|
|
|
189
207
|
}));
|
|
190
208
|
});
|
|
191
209
|
|
|
192
|
-
const showMatrix = $derived(
|
|
210
|
+
const showMatrix = $derived(isCompareLayout || showComparison);
|
|
193
211
|
|
|
194
212
|
function select(id: string, disabled?: boolean) {
|
|
195
213
|
if (disabled) return;
|
|
@@ -221,8 +239,52 @@
|
|
|
221
239
|
billingPeriod = id as 'monthly' | 'yearly';
|
|
222
240
|
onbillingperiodchange?.(billingPeriod);
|
|
223
241
|
}
|
|
242
|
+
|
|
243
|
+
function planColClass(plan: PricingPlan) {
|
|
244
|
+
const selected = selectedId === plan.id;
|
|
245
|
+
return [
|
|
246
|
+
plan.featured && 'bg-brand-50/50 dark:bg-brand-950/20',
|
|
247
|
+
selected && 'bg-brand-50/80 ring-inset ring-2 ring-brand-500/35 dark:bg-brand-950/30'
|
|
248
|
+
];
|
|
249
|
+
}
|
|
224
250
|
</script>
|
|
225
251
|
|
|
252
|
+
{#snippet priceBlock(plan: PricingPlan, opts?: { large?: boolean; dense?: boolean; align?: 'left' | 'center' | 'right' })}
|
|
253
|
+
{@const large = opts?.large ?? false}
|
|
254
|
+
{@const dense = opts?.dense ?? false}
|
|
255
|
+
{@const align = opts?.align ?? 'left'}
|
|
256
|
+
<div
|
|
257
|
+
class={[
|
|
258
|
+
align === 'center' && 'text-center',
|
|
259
|
+
align === 'right' && 'text-right'
|
|
260
|
+
]}
|
|
261
|
+
>
|
|
262
|
+
{#if plan.compareAtPrice}
|
|
263
|
+
<p class={['text-muted line-through', dense ? 'text-[11px]' : 'text-xs']}>
|
|
264
|
+
{plan.compareAtPrice}
|
|
265
|
+
</p>
|
|
266
|
+
{/if}
|
|
267
|
+
<p>
|
|
268
|
+
<span
|
|
269
|
+
class={[
|
|
270
|
+
'font-semibold text-primary',
|
|
271
|
+
large ? 'text-4xl' : dense ? 'text-2xl' : 'text-3xl'
|
|
272
|
+
]}
|
|
273
|
+
>
|
|
274
|
+
{displayPrice(plan)}
|
|
275
|
+
</span>
|
|
276
|
+
{#if displayPeriod(plan)}
|
|
277
|
+
<span class="text-sm text-muted">{displayPeriod(plan)}</span>
|
|
278
|
+
{/if}
|
|
279
|
+
</p>
|
|
280
|
+
{#if plan.priceNote}
|
|
281
|
+
<p class={['mt-1 font-medium text-brand-600 dark:text-brand-400', dense ? 'text-[11px]' : 'text-xs']}>
|
|
282
|
+
{plan.priceNote}
|
|
283
|
+
</p>
|
|
284
|
+
{/if}
|
|
285
|
+
</div>
|
|
286
|
+
{/snippet}
|
|
287
|
+
|
|
226
288
|
{#snippet featureRows(plan: PricingPlan, dense = false)}
|
|
227
289
|
{@const { visible, more } = featureList(plan)}
|
|
228
290
|
{#if showFeatures && visible.length}
|
|
@@ -288,15 +350,8 @@
|
|
|
288
350
|
</div>
|
|
289
351
|
{/if}
|
|
290
352
|
</div>
|
|
291
|
-
<div class="flex shrink-0 flex-col items-stretch gap-3 sm:w-
|
|
292
|
-
|
|
293
|
-
<span class={['font-semibold text-primary', large ? 'text-4xl' : 'text-2xl']}>
|
|
294
|
-
{displayPrice(plan)}
|
|
295
|
-
</span>
|
|
296
|
-
{#if displayPeriod(plan)}
|
|
297
|
-
<span class="text-sm text-muted">{displayPeriod(plan)}</span>
|
|
298
|
-
{/if}
|
|
299
|
-
</p>
|
|
353
|
+
<div class="flex shrink-0 flex-col items-stretch gap-3 sm:w-44 sm:items-stretch">
|
|
354
|
+
{@render priceBlock(plan, { large, dense: true, align: 'right' })}
|
|
300
355
|
<Button
|
|
301
356
|
variant={plan.featured || selectedId === plan.id ? 'primary' : 'secondary'}
|
|
302
357
|
size="sm"
|
|
@@ -328,19 +383,9 @@
|
|
|
328
383
|
{/if}
|
|
329
384
|
</div>
|
|
330
385
|
|
|
331
|
-
<
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
'font-semibold text-primary',
|
|
335
|
-
large ? 'text-4xl' : dense ? 'text-2xl' : 'text-3xl'
|
|
336
|
-
]}
|
|
337
|
-
>
|
|
338
|
-
{displayPrice(plan)}
|
|
339
|
-
</span>
|
|
340
|
-
{#if displayPeriod(plan)}
|
|
341
|
-
<span class="text-sm text-muted">{displayPeriod(plan)}</span>
|
|
342
|
-
{/if}
|
|
343
|
-
</p>
|
|
386
|
+
<div class={dense ? 'mb-3' : 'mb-4'}>
|
|
387
|
+
{@render priceBlock(plan, { large, dense })}
|
|
388
|
+
</div>
|
|
344
389
|
|
|
345
390
|
<div class="mb-auto flex-1">
|
|
346
391
|
{@render featureRows(plan, dense)}
|
|
@@ -364,13 +409,16 @@
|
|
|
364
409
|
{#snippet matrixCell(value: boolean | string)}
|
|
365
410
|
{#if typeof value === 'boolean'}
|
|
366
411
|
{#if value}
|
|
367
|
-
<span
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
412
|
+
<span
|
|
413
|
+
class="inline-flex text-brand-600 dark:text-brand-400"
|
|
414
|
+
aria-label="Included"
|
|
415
|
+
>
|
|
416
|
+
<Check class="mx-auto h-4 w-4" strokeWidth={2.75} aria-hidden="true" />
|
|
371
417
|
</span>
|
|
372
418
|
{:else}
|
|
373
|
-
<span class="text-muted" aria-label="Not included"
|
|
419
|
+
<span class="inline-flex text-muted" aria-label="Not included">
|
|
420
|
+
<X class="mx-auto h-4 w-4" strokeWidth={2.25} aria-hidden="true" />
|
|
421
|
+
</span>
|
|
374
422
|
{/if}
|
|
375
423
|
{:else}
|
|
376
424
|
<span class="text-sm font-medium text-primary">{value}</span>
|
|
@@ -378,124 +426,150 @@
|
|
|
378
426
|
{/snippet}
|
|
379
427
|
|
|
380
428
|
{#snippet comparisonTable(withPricingHeader: boolean)}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
class="grid min-w-160 divide-x divide-border border-b border-border"
|
|
390
|
-
style:grid-template-columns={matrixCols}
|
|
391
|
-
>
|
|
392
|
-
<div class="bg-surface-overlay/30" aria-hidden="true"></div>
|
|
393
|
-
{#each plans as plan (plan.id)}
|
|
394
|
-
{@const badgeText = plan.badge ?? (plan.featured ? featuredBadgeLabel : undefined)}
|
|
395
|
-
<div
|
|
396
|
-
class={[
|
|
397
|
-
'flex flex-col items-center px-4 py-6 text-center',
|
|
398
|
-
plan.featured && 'bg-brand-50/70 dark:bg-brand-950/25'
|
|
399
|
-
]}
|
|
400
|
-
>
|
|
401
|
-
<div class="flex min-h-6 flex-wrap items-center justify-center gap-1.5">
|
|
402
|
-
<span class="text-sm font-semibold text-primary">{plan.name}</span>
|
|
403
|
-
{#if badgeText}
|
|
404
|
-
<Badge size="sm" variant="primary" class="whitespace-nowrap">{badgeText}</Badge>
|
|
405
|
-
{/if}
|
|
406
|
-
</div>
|
|
407
|
-
<p class="mt-1 min-h-8 max-w-44 text-xs leading-snug text-secondary">
|
|
408
|
-
{plan.description ?? '\u00a0'}
|
|
409
|
-
</p>
|
|
410
|
-
<p class="mt-3">
|
|
411
|
-
<span class="text-3xl font-semibold tracking-tight text-primary">
|
|
412
|
-
{displayPrice(plan)}
|
|
413
|
-
</span>
|
|
414
|
-
{#if displayPeriod(plan)}
|
|
415
|
-
<span class="text-xs text-muted">{displayPeriod(plan)}</span>
|
|
416
|
-
{/if}
|
|
417
|
-
</p>
|
|
418
|
-
<Button
|
|
419
|
-
variant={plan.featured || selectedId === plan.id ? 'primary' : 'secondary'}
|
|
420
|
-
size="sm"
|
|
421
|
-
class="mt-4 w-full max-w-40"
|
|
422
|
-
disabled={plan.disabled}
|
|
423
|
-
onclick={() => select(plan.id, plan.disabled)}
|
|
429
|
+
<div class="w-full overflow-x-auto rounded-xl border border-border bg-surface-elevated">
|
|
430
|
+
<table class="w-full min-w-160 border-collapse text-sm" aria-label="Plan feature comparison">
|
|
431
|
+
<thead class="sticky top-0 z-30">
|
|
432
|
+
{#if withPricingHeader}
|
|
433
|
+
<tr class="border-b border-border bg-surface-elevated shadow-sm">
|
|
434
|
+
<th
|
|
435
|
+
scope="col"
|
|
436
|
+
class="sticky left-0 z-40 bg-surface-elevated px-4 py-4 text-left align-bottom"
|
|
424
437
|
>
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
438
|
+
<span class="text-muted text-[11px] font-semibold tracking-wide uppercase">
|
|
439
|
+
Incluye
|
|
440
|
+
</span>
|
|
441
|
+
</th>
|
|
442
|
+
{#each plans as plan (plan.id)}
|
|
443
|
+
{@const badgeText =
|
|
444
|
+
plan.badge ?? (plan.featured ? featuredBadgeLabel : undefined)}
|
|
445
|
+
{@const selected = selectedId === plan.id}
|
|
446
|
+
<th
|
|
447
|
+
scope="col"
|
|
448
|
+
class={[
|
|
449
|
+
'min-w-40 px-3 py-4 text-center align-top font-normal',
|
|
450
|
+
planColClass(plan)
|
|
451
|
+
]}
|
|
452
|
+
>
|
|
453
|
+
<button
|
|
454
|
+
type="button"
|
|
455
|
+
class={[
|
|
456
|
+
'flex w-full flex-col items-center gap-2 rounded-xl px-2 py-2 text-center transition-colors',
|
|
457
|
+
selected && 'bg-brand-500/10',
|
|
458
|
+
!plan.disabled && 'hover:bg-surface-overlay/50',
|
|
459
|
+
plan.disabled && 'cursor-not-allowed opacity-50'
|
|
460
|
+
]}
|
|
461
|
+
disabled={plan.disabled}
|
|
462
|
+
aria-pressed={selected}
|
|
463
|
+
onclick={() => select(plan.id, plan.disabled)}
|
|
464
|
+
>
|
|
465
|
+
<div class="flex min-h-6 flex-wrap items-center justify-center gap-1.5">
|
|
466
|
+
<span class="text-sm font-semibold text-primary">{plan.name}</span>
|
|
467
|
+
{#if badgeText}
|
|
468
|
+
<Badge size="sm" variant="primary" class="whitespace-nowrap">
|
|
469
|
+
{badgeText}
|
|
470
|
+
</Badge>
|
|
471
|
+
{/if}
|
|
472
|
+
</div>
|
|
473
|
+
{#if plan.description}
|
|
474
|
+
<p class="line-clamp-2 max-w-44 text-xs leading-snug text-secondary">
|
|
475
|
+
{plan.description}
|
|
476
|
+
</p>
|
|
477
|
+
{/if}
|
|
478
|
+
{@render priceBlock(plan, { dense: true, align: 'center' })}
|
|
479
|
+
<span
|
|
480
|
+
class={[
|
|
481
|
+
'inline-flex h-8 w-full max-w-40 items-center justify-center rounded-lg px-3 text-xs font-medium',
|
|
482
|
+
selected || plan.featured
|
|
483
|
+
? 'bg-brand-600 text-white'
|
|
484
|
+
: 'bg-surface-overlay text-primary ring-1 ring-border'
|
|
485
|
+
]}
|
|
486
|
+
>
|
|
487
|
+
{plan.cta ?? 'Select'}
|
|
488
|
+
</span>
|
|
489
|
+
</button>
|
|
490
|
+
</th>
|
|
491
|
+
{/each}
|
|
492
|
+
</tr>
|
|
493
|
+
{:else}
|
|
494
|
+
<tr class="border-b border-border bg-surface/80">
|
|
495
|
+
<th
|
|
496
|
+
scope="col"
|
|
497
|
+
class="sticky left-0 z-40 bg-surface px-4 py-2.5 text-left"
|
|
498
|
+
></th>
|
|
499
|
+
{#each plans as plan (plan.id)}
|
|
500
|
+
<th
|
|
501
|
+
scope="col"
|
|
502
|
+
class={[
|
|
503
|
+
'px-3 py-2.5 text-center text-xs font-semibold text-secondary',
|
|
504
|
+
planColClass(plan)
|
|
505
|
+
]}
|
|
506
|
+
>
|
|
507
|
+
<button
|
|
508
|
+
type="button"
|
|
509
|
+
class="w-full rounded-md px-1 py-0.5 hover:bg-surface-overlay/60"
|
|
510
|
+
disabled={plan.disabled}
|
|
511
|
+
aria-pressed={selectedId === plan.id}
|
|
512
|
+
onclick={() => select(plan.id, plan.disabled)}
|
|
513
|
+
>
|
|
514
|
+
{plan.name}
|
|
515
|
+
</button>
|
|
516
|
+
</th>
|
|
517
|
+
{/each}
|
|
518
|
+
</tr>
|
|
519
|
+
{/if}
|
|
520
|
+
</thead>
|
|
521
|
+
<tbody>
|
|
522
|
+
{#each resolvedComparisonRows as row, ri (row.id)}
|
|
523
|
+
<tr
|
|
447
524
|
class={[
|
|
448
|
-
'
|
|
449
|
-
|
|
525
|
+
'border-b border-border last:border-b-0',
|
|
526
|
+
ri % 2 === 1 && 'bg-surface-overlay/25'
|
|
450
527
|
]}
|
|
451
|
-
role="columnheader"
|
|
452
528
|
>
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
{/each}
|
|
456
|
-
</div>
|
|
457
|
-
|
|
458
|
-
{#each resolvedComparisonRows as row, ri (row.id)}
|
|
459
|
-
<div
|
|
460
|
-
class={[
|
|
461
|
-
'grid divide-x divide-border border-b border-border last:border-b-0',
|
|
462
|
-
ri % 2 === 1 && 'bg-surface-overlay/20'
|
|
463
|
-
]}
|
|
464
|
-
style:grid-template-columns={matrixCols}
|
|
465
|
-
role="row"
|
|
466
|
-
>
|
|
467
|
-
<div
|
|
468
|
-
class="flex items-center px-4 py-3.5 text-sm font-medium text-primary"
|
|
469
|
-
role="rowheader"
|
|
470
|
-
>
|
|
471
|
-
{row.label}
|
|
472
|
-
</div>
|
|
473
|
-
{#each row.values as value, vi}
|
|
474
|
-
{@const plan = plans[vi]}
|
|
475
|
-
<div
|
|
529
|
+
<th
|
|
530
|
+
scope="row"
|
|
476
531
|
class={[
|
|
477
|
-
'
|
|
478
|
-
|
|
532
|
+
'sticky left-0 z-10 border-r border-border px-4 py-3 text-left text-sm font-medium text-primary',
|
|
533
|
+
ri % 2 === 1 ? 'bg-surface-overlay/40' : 'bg-surface-elevated'
|
|
479
534
|
]}
|
|
480
|
-
role="cell"
|
|
481
535
|
>
|
|
482
|
-
{
|
|
483
|
-
</
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
536
|
+
{row.label}
|
|
537
|
+
</th>
|
|
538
|
+
{#each row.values as value, vi (`${row.id}-${plans[vi]?.id ?? vi}`)}
|
|
539
|
+
{@const plan = plans[vi]}
|
|
540
|
+
<td
|
|
541
|
+
class={[
|
|
542
|
+
'px-3 py-3 text-center',
|
|
543
|
+
plan && planColClass(plan)
|
|
544
|
+
]}
|
|
545
|
+
>
|
|
546
|
+
{@render matrixCell(value)}
|
|
547
|
+
</td>
|
|
548
|
+
{/each}
|
|
549
|
+
</tr>
|
|
550
|
+
{:else}
|
|
551
|
+
<tr>
|
|
552
|
+
<td
|
|
553
|
+
class="text-muted px-4 py-8 text-center text-sm"
|
|
554
|
+
colspan={Math.max(plans.length, 1) + 1}
|
|
555
|
+
>
|
|
556
|
+
Sin características para comparar.
|
|
557
|
+
</td>
|
|
558
|
+
</tr>
|
|
559
|
+
{/each}
|
|
560
|
+
</tbody>
|
|
561
|
+
</table>
|
|
488
562
|
</div>
|
|
489
563
|
{/snippet}
|
|
490
564
|
|
|
491
565
|
<div class={['w-full space-y-4', className]}>
|
|
492
|
-
{#if
|
|
566
|
+
{#if canToggleBilling}
|
|
493
567
|
<div class="flex justify-center">
|
|
494
568
|
<SegmentedControl
|
|
495
569
|
size="sm"
|
|
496
570
|
items={[
|
|
497
|
-
{ id: 'monthly', label: 'Monthly' },
|
|
498
|
-
{ id: 'yearly', label: 'Yearly' }
|
|
571
|
+
{ id: 'monthly', label: billingLabels?.monthly ?? 'Monthly' },
|
|
572
|
+
{ id: 'yearly', label: billingLabels?.yearly ?? 'Yearly' }
|
|
499
573
|
]}
|
|
500
574
|
bind:value={billingPeriod}
|
|
501
575
|
onchange={onBillingChange}
|
|
@@ -503,7 +577,11 @@
|
|
|
503
577
|
</div>
|
|
504
578
|
{/if}
|
|
505
579
|
|
|
506
|
-
{#if
|
|
580
|
+
{#if plans.length === 0}
|
|
581
|
+
<p class="text-muted rounded-xl border border-border bg-surface-elevated px-4 py-8 text-center text-sm">
|
|
582
|
+
No hay tarifas disponibles para este periodo.
|
|
583
|
+
</p>
|
|
584
|
+
{:else if isCompareLayout}
|
|
507
585
|
{@render comparisonTable(true)}
|
|
508
586
|
{:else if layout === 'split' && featuredPlan}
|
|
509
587
|
<div class={containerClass} role="list">
|
|
@@ -553,7 +631,7 @@
|
|
|
553
631
|
</div>
|
|
554
632
|
{/if}
|
|
555
633
|
|
|
556
|
-
{#if showMatrix &&
|
|
634
|
+
{#if showMatrix && !isCompareLayout}
|
|
557
635
|
<div class="pt-2">
|
|
558
636
|
{@render comparisonTable(false)}
|
|
559
637
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type PricingLayout = 'grid' | 'horizontal' | 'vertical' | 'bento' | 'compact' | 'split' | 'table';
|
|
1
|
+
export type PricingLayout = 'grid' | 'horizontal' | 'vertical' | 'bento' | 'compact' | 'split' | 'table' | 'list' | 'compare';
|
|
2
2
|
export type PricingFeature = string | {
|
|
3
3
|
label: string;
|
|
4
4
|
included?: boolean;
|
|
@@ -10,8 +10,12 @@ export interface PricingPlan {
|
|
|
10
10
|
price: string;
|
|
11
11
|
/** Yearly price when billing toggle is yearly */
|
|
12
12
|
priceYearly?: string;
|
|
13
|
+
/** Struck-through list price (optional) */
|
|
14
|
+
compareAtPrice?: string;
|
|
13
15
|
period?: string;
|
|
14
16
|
periodYearly?: string;
|
|
17
|
+
/** Small hint under the price (e.g. savings) */
|
|
18
|
+
priceNote?: string;
|
|
15
19
|
description?: string;
|
|
16
20
|
features: PricingFeature[];
|
|
17
21
|
cta?: string;
|
|
@@ -41,7 +45,11 @@ interface PricingTableProps {
|
|
|
41
45
|
/** Show monthly/yearly toggle when plans have priceYearly */
|
|
42
46
|
showBillingToggle?: boolean;
|
|
43
47
|
billingPeriod?: 'monthly' | 'yearly';
|
|
44
|
-
|
|
48
|
+
billingLabels?: {
|
|
49
|
+
monthly?: string;
|
|
50
|
+
yearly?: string;
|
|
51
|
+
};
|
|
52
|
+
/** Explicit feature matrix for compare/list/table (auto-built when omitted) */
|
|
45
53
|
comparisonRows?: PricingComparisonRow[];
|
|
46
54
|
/** Append comparison matrix below card layouts */
|
|
47
55
|
showComparison?: boolean;
|
|
@@ -190,7 +190,9 @@
|
|
|
190
190
|
const widthClass = $derived.by(() => {
|
|
191
191
|
const layout = props.layout ?? 'grid';
|
|
192
192
|
if (layout === 'vertical') return 'w-112 max-w-full';
|
|
193
|
-
if (layout === 'table' || props.showComparison)
|
|
193
|
+
if (layout === 'table' || layout === 'list' || layout === 'compare' || props.showComparison) {
|
|
194
|
+
return 'w-208 max-w-full';
|
|
195
|
+
}
|
|
194
196
|
if (layout === 'horizontal') return 'w-192 max-w-full';
|
|
195
197
|
if (layout === 'bento' || layout === 'split') return 'w-208 max-w-full';
|
|
196
198
|
if ((props.columns ?? 3) >= 4 || planSet === 'four') return 'w-224 max-w-full';
|
|
@@ -209,7 +211,12 @@
|
|
|
209
211
|
showFeatures={props.showFeatures ?? true}
|
|
210
212
|
showComparison={props.showComparison ?? false}
|
|
211
213
|
maxFeatures={props.maxFeatures}
|
|
212
|
-
comparisonRows={planSet === 'comparison' ||
|
|
214
|
+
comparisonRows={planSet === 'comparison' ||
|
|
215
|
+
props.layout === 'table' ||
|
|
216
|
+
props.layout === 'list' ||
|
|
217
|
+
props.layout === 'compare'
|
|
218
|
+
? comparisonRows
|
|
219
|
+
: undefined}
|
|
213
220
|
onselect={(id) => (selectedId = id)}
|
|
214
221
|
>
|
|
215
222
|
{#snippet footer()}
|