@hulkapps/app-manager-vue 2.5.12 → 3.0.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.
@@ -0,0 +1,900 @@
1
+ <script>
2
+ import Swiper, { Navigation, Pagination } from "swiper";
3
+ import "swiper/swiper-bundle.css";
4
+ import VariantButton from "./VariantButton";
5
+ import {calculatePlanPriceWithDiscounts, formatFeature} from "@/helpers";
6
+
7
+ export default {
8
+ data() {
9
+ return {
10
+ isSyncing: false,
11
+ remainingPlansMonthly: {
12
+ before: 0,
13
+ after: 0
14
+ },
15
+ remainingPlansAnnually: {
16
+ before: 0,
17
+ after: 0
18
+ },
19
+ anyMonthlyPlanHasDiscount: false,
20
+ anyAnnuallyPlanHasDiscount: false,
21
+ loadingPlanId: null,
22
+ };
23
+ },
24
+ name: "PlanTable",
25
+ components: {
26
+ VariantButton,
27
+ },
28
+ props: {
29
+ plans: {
30
+ type: Array,
31
+ required: true,
32
+ },
33
+ currentPlan: {
34
+ type: Object,
35
+ required: false,
36
+ },
37
+ selectedInterval: {
38
+ type: String,
39
+ required: false,
40
+ },
41
+ promotionalDiscount: {
42
+ type: Object,
43
+ required: false,
44
+ },
45
+ features: {
46
+ type: Array,
47
+ required: false,
48
+ default: () => []
49
+ }
50
+ },
51
+ methods: {
52
+ formatFeature,
53
+ async handlePlanClick(plan) {
54
+ this.loadingPlanId = plan.id;
55
+ try {
56
+ await this.$emit("plan-clicked", plan);
57
+ } catch (error) {
58
+ console.error('Error handling plan click:', error);
59
+ this.loadingPlanId = null;
60
+ }
61
+ },
62
+ translateMe(message) {
63
+ return this.$translations.hasOwnProperty(message)
64
+ ? this.$translations[message]
65
+ : message;
66
+ },
67
+ hasFeature(plan, feature) {
68
+ return !!plan.features[feature.uuid];
69
+ },
70
+ syncHeights(which = "features") {
71
+ this.$nextTick(() => {
72
+ setTimeout(() => {
73
+ const slides = document.querySelectorAll(".swiper-slide");
74
+ if (which === "features") {
75
+ const featureType = `-${this.selectedInterval}`; // Append '-monthly' or '-annually' based on selectedInterval
76
+ const featureNames = document.querySelectorAll(
77
+ `.plan-feature-name${featureType}`
78
+ );
79
+ slides.forEach((slide) => {
80
+ const features = slide.querySelectorAll(
81
+ `.plan-feature${featureType}`
82
+ );
83
+ featureNames.forEach((featureName, index) => {
84
+ const feature = features[index];
85
+ if (featureName && feature) {
86
+ feature.style.height = `${featureName.offsetHeight}px`;
87
+ }
88
+ });
89
+ });
90
+ } else if (which === "plans") {
91
+
92
+ const planType = `-${this.selectedInterval}`;
93
+ const planNames = document.querySelectorAll(
94
+ `.plan-header-wrapper${planType}`
95
+ );
96
+ const plansAvailableName = document.querySelector(
97
+ `.plans-available${planType}`
98
+ );
99
+ if (!plansAvailableName) return;
100
+ let planNameHeight = 0;
101
+ slides.forEach((slide, index) => {
102
+ const planName = planNames[index];
103
+ if (planName) {
104
+ planNameHeight = Math.max(
105
+ planName.offsetHeight,
106
+ planNameHeight
107
+ );
108
+ }
109
+ });
110
+ // Set the minHeight for the plans available name
111
+ plansAvailableName.style.minHeight = `${planNameHeight}px`;
112
+
113
+ // Set the minHeight for all elements
114
+ planNames.forEach((el) => {
115
+ el.style.minHeight = `${planNameHeight}px`;
116
+ });
117
+ }
118
+ }, 2); // delay 0ms
119
+ });
120
+ },
121
+ syncAllHeights() {
122
+ this.syncHeights("features");
123
+ this.syncHeights("plans");
124
+ },
125
+ syncNavigationWidth() {
126
+ const swiperPlanNavigations = document.querySelectorAll(
127
+ ".swiper-plan-navigation"
128
+ );
129
+ const pricingTable = document.querySelector(".pricing-table");
130
+ if (!pricingTable) return;
131
+ swiperPlanNavigations.forEach((swiperPlanNavigation) => {
132
+ swiperPlanNavigation.style.width = `${
133
+ pricingTable.offsetWidth + 130
134
+ }px`;
135
+ swiperPlanNavigation.style.left = `${pricingTable.offsetLeft - 65}px`;
136
+ });
137
+ },
138
+ syncScroll(source, targets) {
139
+ if (this.isSyncing) return;
140
+ this.isSyncing = true;
141
+ const scrollTop = source.scrollTop;
142
+ targets.forEach((target) => {
143
+ if (target !== source) {
144
+ target.scrollTop = scrollTop;
145
+ }
146
+ });
147
+ this.isSyncing = false;
148
+ },
149
+ setupScrollListeners() {
150
+ const tableLeftElements = document.querySelectorAll("#table-left");
151
+ const plansTableElements = document.querySelectorAll("#plans-table");
152
+ const allElements = [...tableLeftElements, ...plansTableElements];
153
+ allElements.forEach((element) => {
154
+ element.addEventListener("scroll", () => {
155
+ this.syncScroll(element, allElements);
156
+ });
157
+ });
158
+ },
159
+
160
+ updateRemainingPlans(swiper, type = 'monthly') {
161
+ if (!swiper) return;
162
+
163
+ const totalSlides = swiper.slides.length;
164
+ const currentIndex = swiper.activeIndex;
165
+
166
+ let visibleSlides = 1;
167
+ if (typeof swiper.params.slidesPerView === 'number') {
168
+ visibleSlides = swiper.params.slidesPerView;
169
+ } else if (swiper.params.breakpoints) {
170
+ const viewport = window.innerWidth;
171
+ const breakpoints = swiper.params.breakpoints;
172
+ const sorted = Object.keys(breakpoints)
173
+ .map(Number)
174
+ .sort((a, b) => a - b);
175
+
176
+ for (const bp of sorted) {
177
+ if (viewport >= bp && typeof breakpoints[bp].slidesPerView === 'number') {
178
+ visibleSlides = breakpoints[bp].slidesPerView;
179
+ }
180
+ }
181
+ }
182
+
183
+ const after = Math.max(totalSlides - (currentIndex + visibleSlides), 0);
184
+ const before = currentIndex;
185
+
186
+ if (type === 'annually') {
187
+ this.remainingPlansAnnually = { after, before };
188
+ } else {
189
+ this.remainingPlansMonthly = { after, before };
190
+ }
191
+ },
192
+ },
193
+
194
+ computed: {
195
+ monthlyPlans() {
196
+ return this.plans
197
+ .filter(plan => plan.interval === "EVERY_30_DAYS")
198
+ .map(plan => {
199
+ const planDetails = calculatePlanPriceWithDiscounts(plan, this.promotionalDiscount);
200
+ if (planDetails.has_discount && !this.anyMonthlyPlanHasDiscount) {
201
+ this.anyMonthlyPlanHasDiscount = true;
202
+ }
203
+ return planDetails;
204
+ });
205
+ },
206
+ annualPlans() {
207
+ return this.plans
208
+ .filter(plan => plan.interval === "ANNUAL")
209
+ .map(plan => {
210
+ const planDetails = calculatePlanPriceWithDiscounts(plan, this.promotionalDiscount);
211
+ if (planDetails.has_discount && !this.anyAnnuallyPlanHasDiscount) {
212
+ this.anyAnnuallyPlanHasDiscount = true;
213
+ }
214
+ return planDetails;
215
+ });
216
+ },
217
+ monthlyPlansFeatures() {
218
+ if (!this.features.length) return [];
219
+
220
+ // First, find which features are actually used in any monthly plan
221
+ const usedFeatures = new Set();
222
+ this.monthlyPlans.forEach(plan => {
223
+ Object.keys(plan.features).forEach(featureUuid => {
224
+ usedFeatures.add(featureUuid);
225
+ });
226
+ });
227
+
228
+ // Filter features and group them
229
+ let groupedFeatures = {};
230
+ let hasGroupFields = false;
231
+
232
+ this.features
233
+ .filter(feature => usedFeatures.has(feature.uuid) && !feature.hidden_feature) // Filter out hidden features
234
+ .forEach((feature) => {
235
+ if (feature.group && feature.group_order) {
236
+ hasGroupFields = true;
237
+ }
238
+
239
+ const group = hasGroupFields ? (feature.group || 'Default') : 'Default';
240
+ if (!groupedFeatures[group]) {
241
+ groupedFeatures[group] = {
242
+ name: group,
243
+ order: hasGroupFields ? (feature.group_order || 999) : 999,
244
+ features: []
245
+ };
246
+ }
247
+ groupedFeatures[group].features.push(feature);
248
+ });
249
+
250
+ const groups = Object.values(groupedFeatures).sort((a, b) => a.order - b.order);
251
+ if (groups.length === 1 || !hasGroupFields) {
252
+ groups.forEach(group => group.name = '');
253
+ }
254
+
255
+ return groups;
256
+ },
257
+ annualPlansFeatures() {
258
+ if (!this.features.length) return [];
259
+
260
+ // First, find which features are actually used in any annual plan
261
+ const usedFeatures = new Set();
262
+ this.annualPlans.forEach(plan => {
263
+ // Use feature.uuid instead of feature_id to match with plan.features
264
+ Object.keys(plan.features).forEach(featureUuid => {
265
+ usedFeatures.add(featureUuid);
266
+ });
267
+ });
268
+
269
+ // Filter features and group them
270
+ let groupedFeatures = {};
271
+ let hasGroupFields = false;
272
+
273
+ this.features
274
+ .filter(feature => usedFeatures.has(feature.uuid) && !feature.hidden_feature) // Filter out hidden features
275
+ .forEach((feature) => {
276
+ if (feature.group && feature.group_order) {
277
+ hasGroupFields = true;
278
+ }
279
+
280
+ const group = hasGroupFields ? (feature.group || 'Default') : 'Default';
281
+ if (!groupedFeatures[group]) {
282
+ groupedFeatures[group] = {
283
+ name: group,
284
+ order: hasGroupFields ? (feature.group_order || 999) : 999,
285
+ features: []
286
+ };
287
+ }
288
+ groupedFeatures[group].features.push(feature);
289
+ });
290
+
291
+ const groups = Object.values(groupedFeatures).sort((a, b) => a.order - b.order);
292
+ if (groups.length === 1 || !hasGroupFields) {
293
+ groups.forEach(group => group.name = '');
294
+ }
295
+
296
+ return groups;
297
+ },
298
+ },
299
+
300
+ watch: {
301
+ selectedInterval() {
302
+ let monthlyPlanTable = document.querySelector(".monthly-table");
303
+ let annuallyPlanTable = document.querySelector(".annually-table");
304
+ let monthlyPlanTableNavigation =
305
+ document.querySelector(".nav-monthly-table");
306
+ let annuallyPlanTableNavigation = document.querySelector(
307
+ ".nav-annually-table"
308
+ );
309
+ if (this.selectedInterval === "monthly") {
310
+ monthlyPlanTable.style.visibility = "visible";
311
+ monthlyPlanTable.style.height = "auto";
312
+ monthlyPlanTable.style.padding = "16px";
313
+ monthlyPlanTable.style.padding = "16px";
314
+ annuallyPlanTable.style.visibility = "hidden";
315
+ annuallyPlanTable.style.height = "0px";
316
+ annuallyPlanTable.style.padding = "0px";
317
+ monthlyPlanTableNavigation.style.display = "flex";
318
+ annuallyPlanTableNavigation.style.display = "none";
319
+ this.interval = "EVERY_30_DAYS";
320
+ this.syncAllHeights();
321
+ } else if (this.selectedInterval === "annually") {
322
+ monthlyPlanTable.style.visibility = "hidden";
323
+ monthlyPlanTable.style.height = "0px";
324
+ monthlyPlanTable.style.padding = "0px";
325
+ monthlyPlanTable.style.padding = "0px";
326
+ annuallyPlanTable.style.visibility = "visible";
327
+ annuallyPlanTable.style.height = "auto";
328
+ annuallyPlanTable.style.padding = "16px";
329
+ monthlyPlanTableNavigation.style.display = "none";
330
+ annuallyPlanTableNavigation.style.display = "flex";
331
+ this.interval = "ANNUAL";
332
+ this.syncAllHeights();
333
+ }
334
+ },
335
+ },
336
+
337
+ mounted() {
338
+ new Swiper(this.$refs.swiperMonthlyTable, {
339
+ modules: [Navigation, Pagination],
340
+ loop: false,
341
+ slidesPerView: 2,
342
+ speed: 500,
343
+ navigation: {
344
+ nextEl: ".swiper-plan-monthly-next",
345
+ prevEl: ".swiper-plan-monthly-prev",
346
+ },
347
+ breakpoints: {
348
+ 0: {
349
+ slidesPerView: 1,
350
+ },
351
+ 768: {
352
+ slidesPerView: 2,
353
+ },
354
+ 1024: {
355
+ slidesPerView: Math.min(this.monthlyPlans.length, 3),
356
+ },
357
+ },
358
+ on: {
359
+ slideChange: (swiper) => {
360
+ this.syncAllHeights()
361
+ this.updateRemainingPlans(swiper)
362
+ }, // Run syncHeights on each slide change
363
+ afterInit: (swiper) => {
364
+ this.syncAllHeights()
365
+ this.updateRemainingPlans(swiper)
366
+ }, // Sync heights after initial Swiper setup
367
+ },
368
+ });
369
+ new Swiper(this.$refs.swiperAnnuallyTable, {
370
+ modules: [Navigation, Pagination],
371
+ loop: false,
372
+ slidesPerView: 2,
373
+ speed: 500,
374
+ navigation: {
375
+ nextEl: ".swiper-plan-annually-next",
376
+ prevEl: ".swiper-plan-annually-prev",
377
+ },
378
+ breakpoints: {
379
+ 0: {
380
+ slidesPerView: 1,
381
+ },
382
+ 768: {
383
+ slidesPerView: 2,
384
+ },
385
+ 1024: {
386
+ slidesPerView: Math.min(this.annualPlans.length, 3),
387
+ },
388
+ },
389
+ on: {
390
+ slideChange: (swiper) => {
391
+ this.syncAllHeights()
392
+ this.updateRemainingPlans(swiper, 'annually')
393
+ }, // Run syncHeights on each slide change
394
+ afterInit: (swiper) => {
395
+ this.syncAllHeights()
396
+ this.updateRemainingPlans(swiper, 'annually')
397
+ }, // Sync heights after initial Swiper setup
398
+ },
399
+ });
400
+
401
+ this.syncAllHeights(); // Run syncHeights once after mount
402
+ this.syncNavigationWidth(); // Sync navigation width after mount
403
+ this.setupScrollListeners();
404
+ },
405
+ };
406
+ </script>
407
+
408
+ <template>
409
+ <div class="container">
410
+ <div class="swiper-plan-navigation nav-monthly-table">
411
+ <button class="swiper-plan-monthly-prev">
412
+ <span class="plans-remaining" v-if="this.remainingPlansMonthly.before > 0">+{{ this.remainingPlansMonthly.before }} Plans</span>
413
+ <img src="../../assets/NavigationLeft.svg" alt="Nav Left" />
414
+ </button>
415
+ <button class="swiper-plan-monthly-next">
416
+ <span class="plans-remaining" v-if="this.remainingPlansMonthly.after > 0">+{{ this.remainingPlansMonthly.after }} Plans</span>
417
+ <img src="../../assets/NavigationRight.svg" alt="Nav Right" />
418
+ </button>
419
+ </div>
420
+ <div class="swiper-plan-navigation nav-annually-table">
421
+ <button class="swiper-plan-annually-prev">
422
+ <span class="plans-remaining" v-if="this.remainingPlansAnnually.before > 0">+{{ this.remainingPlansAnnually.before }} Plans</span>
423
+ <img src="../../assets/NavigationLeft.svg" alt="Nav Left" />
424
+ </button>
425
+ <button class="swiper-plan-annually-next">
426
+ <span class="plans-remaining" v-if="this.remainingPlansAnnually.after > 0">+{{ this.remainingPlansAnnually.after }} Plans</span>
427
+ <img src="../../assets/NavigationRight.svg" alt="Nav Right" />
428
+ </button>
429
+ </div>
430
+
431
+ <div class="pricing-table monthly-table">
432
+ <div class="pricing-table-inner__left" id="table-left">
433
+ <div class="table-header plans-available plans-available-monthly">
434
+ <h3>
435
+ {{ monthlyPlans.length }} {{ translateMe("Plans available") }}
436
+ </h3>
437
+ </div>
438
+ <template v-for="group in monthlyPlansFeatures">
439
+ <div v-if="group.name" class="feature-group-header plan-feature-name plan-feature-name-monthly">
440
+ {{ group.name }}
441
+ </div>
442
+ <div
443
+ class="plan-feature-name plan-feature-name-monthly"
444
+ v-for="feature in group.features"
445
+ :key="feature.uuid"
446
+ >
447
+ {{ feature.name }}
448
+ </div>
449
+ </template>
450
+ </div>
451
+ <div class="swiper plans monthly" ref="swiperMonthlyTable" id="plans-table">
452
+ <div class="swiper-wrapper">
453
+ <div
454
+ v-for="(plan, index) in monthlyPlans"
455
+ :key="plan.id"
456
+ class="swiper-slide"
457
+ :class="{ 'last-slide': index === monthlyPlans.length - 1 }"
458
+ >
459
+ <div class="plan-header-wrapper plan-header-wrapper-monthly">
460
+ <div
461
+ :class="[
462
+ 'price-wrapper',
463
+ anyMonthlyPlanHasDiscount ? 'has-discount' : ''
464
+ ]"
465
+ >
466
+ <template v-if="plan.strike_price">
467
+ <h5>
468
+ <span class="strike-price">${{ plan.strike_price }}</span>
469
+ <span class="plan-interval" v-if="plan.strike_price !== 0">{{ translateMe("/mo") }}</span>
470
+ </h5>
471
+ </template>
472
+ <div class="main-price">
473
+ <h4 class="plan-name">{{ plan.name }}</h4>
474
+ <h4 v-if="plan.name !== 'free' && plan.name !== 'FREE'">
475
+ <span class="plan-price">${{ plan.price }}</span>
476
+ <span class="plan-interval">{{ translateMe("/mo") }}</span>
477
+ </h4>
478
+ </div>
479
+ </div>
480
+ <VariantButton
481
+ :variant="'secondary'"
482
+ :disabled="currentPlan && currentPlan.id === plan.id"
483
+ :loading="loadingPlanId === plan.id"
484
+ @click="handlePlanClick(plan)"
485
+ class="button"
486
+ >{{
487
+ currentPlan && currentPlan.id === plan.id
488
+ ? translateMe("Selected Plan")
489
+ : (
490
+ !currentPlan
491
+ ? translateMe("Choose Plan")
492
+ : (
493
+ plan.price > currentPlan.price
494
+ ? translateMe("Upgrade")
495
+ : translateMe("Switch to this plan")
496
+ )
497
+ )
498
+ }}</VariantButton>
499
+ </div>
500
+ <template v-for="group in monthlyPlansFeatures">
501
+ <div v-if="group.name" class="feature-group-header plan-feature plan-feature-monthly">
502
+ {{ group.name }}
503
+ </div>
504
+ <div
505
+ class="plan-feature plan-feature-monthly"
506
+ v-for="feature in group.features"
507
+ :key="feature.uuid"
508
+ >
509
+ <div v-if="hasFeature(plan, feature)">
510
+ <div v-if="feature.value_type === 'boolean'">
511
+ <img
512
+ src="../../assets/CheckTrue.svg"
513
+ alt="Feature is included"
514
+ class="plan-table-checkmark"
515
+ v-if="hasFeature(plan, feature)"
516
+ />
517
+ </div>
518
+ <div v-else>
519
+ <span>{{ translateMe(formatFeature(plan.features[feature.uuid])) }}</span>
520
+ </div>
521
+ </div>
522
+ <div v-else>
523
+ <img
524
+ src="../../assets/CheckFalse.svg"
525
+ alt="Feature is not included"
526
+ class="plan-table-checkmark"
527
+ />
528
+ </div>
529
+ </div>
530
+ </template>
531
+ </div>
532
+ </div>
533
+ </div>
534
+ </div>
535
+ <div class="pricing-table annually-table">
536
+ <div class="pricing-table-inner__left" id="table-left">
537
+ <div class="table-header plans-available plans-available-annually">
538
+ <h3>{{ annualPlans.length }} {{ translateMe("Plans available") }}</h3>
539
+ </div>
540
+ <template v-for="group in annualPlansFeatures">
541
+ <div v-if="group.name" class="feature-group-header plan-feature-name plan-feature-name-annually">
542
+ {{ group.name }}
543
+ </div>
544
+ <div
545
+ class="plan-feature-name plan-feature-name-annually"
546
+ v-for="feature in group.features"
547
+ :key="feature.uuid"
548
+ >
549
+ {{ feature.name }}
550
+ </div>
551
+ </template>
552
+ </div>
553
+ <div class="swiper plans annually" ref="swiperAnnuallyTable" id="plans-table">
554
+ <div class="swiper-wrapper">
555
+ <div
556
+ v-for="(plan, index) in annualPlans"
557
+ :key="plan.id"
558
+ class="swiper-slide"
559
+ :class="{ 'last-slide': index === annualPlans.length - 1 }"
560
+ >
561
+ <div class="plan-header-wrapper plan-header-wrapper-annually">
562
+ <div
563
+ :class="[
564
+ 'price-wrapper',
565
+ anyAnnuallyPlanHasDiscount ? 'has-discount' : ''
566
+ ]"
567
+ >
568
+ <template v-if="plan.strike_price">
569
+ <h5>
570
+ <span class="strike-price">${{ plan.strike_price }}</span>
571
+ <span class="plan-interval" v-if="plan.strike_price !== 0">{{ translateMe("/yr") }}</span>
572
+ </h5>
573
+ </template>
574
+ <div class="main-price">
575
+ <h4 class="plan-name">{{ plan.name }}</h4>
576
+ <h4 v-if="plan.name !== 'free' && plan.name !== 'FREE'">
577
+ <span class="plan-price">${{ plan.price }}</span>
578
+ <span class="plan-interval">{{ translateMe("/yr") }}</span>
579
+ </h4>
580
+ </div>
581
+ </div>
582
+ <VariantButton
583
+ :variant="'secondary'"
584
+ :disabled="currentPlan && currentPlan.id === plan.id"
585
+ :loading="loadingPlanId === plan.id"
586
+ @click="handlePlanClick(plan)"
587
+ class="button"
588
+ >{{
589
+ currentPlan && currentPlan.id === plan.id
590
+ ? translateMe("Selected Plan")
591
+ : (
592
+ !currentPlan
593
+ ? translateMe("Choose Plan")
594
+ : (
595
+ plan.price > currentPlan.price
596
+ ? translateMe("Upgrade")
597
+ : translateMe("Switch to this plan")
598
+ )
599
+ )
600
+ }}</VariantButton>
601
+ </div>
602
+ <template v-for="group in annualPlansFeatures">
603
+ <div v-if="group.name" class="feature-group-header plan-feature plan-feature-annually">
604
+ {{ group.name }}
605
+ </div>
606
+ <div
607
+ class="plan-feature plan-feature-annually"
608
+ v-for="feature in group.features"
609
+ :key="feature.uuid"
610
+ >
611
+ <div v-if="hasFeature(plan, feature)">
612
+ <div v-if="feature.value_type === 'boolean'">
613
+ <img
614
+ src="../../assets/CheckTrue.svg"
615
+ alt="Feature is included"
616
+ class="plan-table-checkmark"
617
+ v-if="hasFeature(plan, feature)"
618
+ />
619
+ </div>
620
+ <div v-else>
621
+ <span>{{ translateMe(formatFeature(plan.features[feature.uuid])) }}</span>
622
+ </div>
623
+ </div>
624
+ <div v-else>
625
+ <img
626
+ src="../../assets/CheckFalse.svg"
627
+ alt="Feature is not included"
628
+ class="plan-table-checkmark"
629
+ />
630
+ </div>
631
+ </div>
632
+ </template>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ </div>
637
+ </div>
638
+ </template>
639
+
640
+ <style scoped>
641
+ .container {
642
+ width: 100%;
643
+ }
644
+
645
+ .pricing-table {
646
+ display: grid;
647
+ width: calc(100% + 2px);
648
+ grid-template-columns: repeat(3, 1fr);
649
+ background-color: white;
650
+ border-radius: 16px;
651
+ box-shadow: 0 4px 6px -1px #0000001a;
652
+ border: 1px solid #e5e5e5;
653
+ }
654
+
655
+ .pricing-table.monthly-table {
656
+ padding: 16px;
657
+ }
658
+
659
+ .plans {
660
+ grid-column: span 2;
661
+ max-height: 500px;
662
+ overflow-y: auto;
663
+ }
664
+
665
+ .annually-table {
666
+ visibility: hidden;
667
+ height: 0;
668
+ }
669
+
670
+ .pricing-table-inner__left {
671
+ display: flex;
672
+ flex-direction: column;
673
+ max-height: 500px;
674
+ overflow-y: auto;
675
+ }
676
+
677
+ .swiper {
678
+ width: 100%;
679
+ height: 100%;
680
+ }
681
+
682
+ .table-header {
683
+ display: flex;
684
+ align-items: center;
685
+ background-color: #f1f1f1;
686
+ padding: 16px;
687
+ border-bottom: 1px solid #e3e3e3;
688
+ position: sticky;
689
+ top: 0;
690
+ }
691
+
692
+ .table-header h3 {
693
+ font-size: 13px;
694
+ font-weight: 700;
695
+ line-height: 20px;
696
+ color: #303030;
697
+ }
698
+
699
+ .plan-header-wrapper {
700
+ padding: 16px;
701
+ border-bottom: 1px solid #e3e3e3;
702
+ }
703
+
704
+ .plan-feature {
705
+ display: flex;
706
+ align-items: center;
707
+ justify-content: center;
708
+ border-bottom: 1px solid #e3e3e3;
709
+ border-left: 1px solid #e3e3e3;
710
+ }
711
+
712
+ .last-slide .plan-feature {
713
+ border-right: 1px solid #e3e3e3;
714
+ }
715
+
716
+ .plan-feature-name {
717
+ font-size: 13px;
718
+ font-weight: 450;
719
+ padding: 12px;
720
+ border-bottom: 1px solid #e3e3e3;
721
+ border-left: 1px solid #e3e3e3;
722
+ }
723
+ .plan-table-checkmark {
724
+ width: 20px;
725
+ height: 20px;
726
+ }
727
+ .plan-header-wrapper {
728
+ display: flex;
729
+ flex-direction: column;
730
+ justify-content: center;
731
+ align-items: center;
732
+ gap: 12px;
733
+ background-color: #f1f1f1;
734
+ border-left: 1px solid #f1f1f1;
735
+ border-right: 1px solid #f1f1f1;
736
+ position: sticky;
737
+ top: 0;
738
+ }
739
+
740
+ .plan-header-wrapper .price-wrapper {
741
+ display: flex;
742
+ flex-direction: column;
743
+ justify-content: center;
744
+ align-items: center;
745
+ gap: 4px;
746
+ }
747
+
748
+ .plan-header-wrapper .price-wrapper.has-discount {
749
+ min-height: 44px;
750
+ }
751
+
752
+ .plan-header-wrapper .price-wrapper .main-price {
753
+ display: flex;
754
+ flex-direction: row;
755
+ gap: 8px;
756
+ justify-content: center;
757
+ align-items: center;
758
+ }
759
+
760
+ .plan-header-wrapper .price-wrapper .strike-price {
761
+ text-decoration: line-through;
762
+ }
763
+
764
+ .plan-header-wrapper .price-wrapper .plan-interval {
765
+ color: #999999;
766
+ font-weight: normal;
767
+ }
768
+
769
+ .plan-header-wrapper .price-wrapper h4 {
770
+ display: inline;
771
+ font-size: 16px;
772
+ font-weight: 700;
773
+ color: #303030;
774
+ width: max-content;
775
+ }
776
+
777
+ .plan-header-wrapper .price-wrapper h4 h6 {
778
+ display: inline;
779
+ font-size: 13px;
780
+ font-weight: 400;
781
+ color: #00000080;
782
+ margin-left: -4px;
783
+ line-height: 0;
784
+ }
785
+
786
+ .swiper-plan-navigation {
787
+ position: absolute;
788
+ margin-top: 32px;
789
+ display: flex;
790
+ justify-content: space-between;
791
+ padding: 16px 0px;
792
+ }
793
+
794
+ .nav-annually-table {
795
+ display: none;
796
+ }
797
+
798
+ .swiper-plan-monthly-prev,
799
+ .swiper-plan-monthly-next,
800
+ .swiper-plan-annually-next,
801
+ .swiper-plan-annually-prev {
802
+ display: flex;
803
+ align-items: center;
804
+ justify-content: center;
805
+ width: 36px;
806
+ height: 36px;
807
+ background-color: #1a1a1a;
808
+ border-radius: 8px;
809
+ cursor: pointer;
810
+ }
811
+
812
+ .swiper-plan-monthly-prev:disabled,
813
+ .swiper-plan-monthly-next:disabled,
814
+ .swiper-plan-annually-next:disabled,
815
+ .swiper-plan-annually-prev:disabled {
816
+ visibility: hidden;
817
+ }
818
+
819
+ .choose-button {
820
+ background-color: white !important;
821
+ }
822
+
823
+ .choose-button:hover {
824
+ background-color: #f9f9f9 !important;
825
+ }
826
+
827
+ .choose-button.disabled {
828
+ background-color: rgba(0, 0, 0, 0.15) !important;
829
+ }
830
+
831
+ #table-left {
832
+ scrollbar-width: none;
833
+ -ms-overflow-style: none;
834
+ }
835
+
836
+ #table-left::-webkit-scrollbar {
837
+ display: none;
838
+ }
839
+
840
+ .plans-remaining {
841
+ position: absolute;
842
+ top: -30px;
843
+ background-color: white;
844
+ color: #333;
845
+ font-weight: 500;
846
+ padding: 6px 12px;
847
+ border-radius: 8px;
848
+ font-size: 14px;
849
+ white-space: nowrap;
850
+ box-shadow: 0 4px 8px #00000026;
851
+ }
852
+
853
+ .plans-remaining::after {
854
+ content: '';
855
+ position: absolute;
856
+ top: 100%;
857
+ left: 50%;
858
+ transform: translateX(-50%);
859
+ width: 0;
860
+ height: 0;
861
+ border-left: 8px solid transparent;
862
+ border-right: 8px solid transparent;
863
+ border-top: 8px solid white;
864
+ filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.1));
865
+ }
866
+
867
+ .feature-group-header.plan-feature-name {
868
+ font-size: 0 !important;
869
+ }
870
+
871
+ .feature-group-header {
872
+ font-size: 14px;
873
+ font-weight: 600;
874
+ background-color: #f5f5f5;
875
+ color: #303030;
876
+ max-height: 30px;
877
+ padding: 5px !important;
878
+ border-bottom: 1px solid #e3e3e3;
879
+ border-left: 1px solid #e3e3e3;
880
+ }
881
+
882
+ .feature-group-header.plan-feature {
883
+ text-align: center;
884
+ border-left: 1px solid #e3e3e3;
885
+ }
886
+
887
+ .last-slide .feature-group-header.plan-feature {
888
+ border-right: 1px solid #e3e3e3;
889
+ }
890
+
891
+ @media (max-width: 640px) {
892
+ .swiper-plan-navigation {
893
+ display: none !important;
894
+ }
895
+
896
+ .pricing-table {
897
+ width: calc(100% + -2px);
898
+ }
899
+ }
900
+ </style>