move upgrade modal to ui package and add trial status and cancel trial ui

This commit is contained in:
Gregor Vostrak
2026-10-13 14:58:12 +02:00
parent 11dd4e174e
commit 9e6013eaf1
33 changed files with 1020 additions and 262 deletions

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { LockClosedIcon } from '@heroicons/vue/20/solid';
import UpgradeModal from './UpgradeModal.vue';
import type { UpgradeFeature } from './UpgradeModal/useUpgradeCta';
import { ref } from 'vue';
defineProps<{
// The Professional feature behind the badge, picks the variant of the upgrade modal
feature: UpgradeFeature;
}>();
const showUpgradeModal = ref(false);
</script>
<template>
<UpgradeModal v-model:show="showUpgradeModal" :feature="feature" />
<button
data-testid="upgrade_badge"
class="inline-flex bg-secondary hover:bg-tertiary px-2 py-1 rounded border border-border-secondary hover:border-border-tertiary items-center space-x-1"
@click.prevent="showUpgradeModal = true">
<LockClosedIcon class="w-3 text-text-tertiary"></LockClosedIcon>
<span class="text-xs text-text-secondary font-semibold"> Upgrade </span>
</button>
</template>
<style scoped></style>

View File

@@ -0,0 +1,139 @@
<script setup lang="ts">
import { Dialog, DialogContent, DialogTitle, DialogDescription } from './dialog';
import { LockClosedIcon } from '@heroicons/vue/16/solid';
import { computed, type Component } from 'vue';
import MockupBillableRates from './UpgradeModal/MockupBillableRates.vue';
import MockupEstimates from './UpgradeModal/MockupEstimates.vue';
import MockupGoals from './UpgradeModal/MockupGoals.vue';
import MockupInvoicing from './UpgradeModal/MockupInvoicing.vue';
import MockupPdfReports from './UpgradeModal/MockupPdfReports.vue';
import MockupShareableReports from './UpgradeModal/MockupShareableReports.vue';
import UpgradeCtaFooter from './UpgradeModal/UpgradeCtaFooter.vue';
import { featureIcons } from './UpgradeModal/featureIcons';
import {
professionalFeatures,
type ProfessionalFeatureKey,
type UpgradeFeature,
} from './UpgradeModal/useUpgradeCta';
const show = defineModel<boolean>('show', { default: false });
const props = defineProps<{
// The Professional feature the user tried to use, picks the mock-up and the copy
feature: UpgradeFeature;
}>();
const contents: Record<
UpgradeFeature,
{
mockup: Component;
pill: string;
headline: string;
body: string;
// Feature of the pricing page that is listed first, if the feature is listed there
featureKey?: ProfessionalFeatureKey;
}
> = {
'billable-rates': {
mockup: MockupBillableRates,
pill: 'Unlock billable rates',
headline: 'Turn tracked hours into revenue',
body: 'Upgrade to solidtime Professional to track billable hours',
featureKey: 'rates',
},
estimates: {
mockup: MockupEstimates,
pill: 'Unlock time estimates',
headline: 'Keep every project on budget',
body: 'Upgrade to solidtime Professional to set time estimates',
featureKey: 'estimates',
},
goals: {
mockup: MockupGoals,
pill: 'Unlock unlimited goals',
headline: 'Set a goal for everything that matters',
body: 'Upgrade to solidtime Professional to track unlimited goals',
},
invoicing: {
mockup: MockupInvoicing,
pill: 'Unlock invoicing',
headline: 'Get paid for every tracked hour',
body: 'Upgrade to solidtime Professional to create invoices',
featureKey: 'invoicing',
},
'pdf-reports': {
mockup: MockupPdfReports,
pill: 'Unlock PDF reports',
headline: 'Send reports your clients will read',
body: 'Upgrade to solidtime Professional to export PDF reports',
featureKey: 'pdf',
},
'shareable-reports': {
mockup: MockupShareableReports,
pill: 'Unlock shareable reports',
headline: 'Share live reports with one link',
body: 'Upgrade to solidtime Professional to share reports',
featureKey: 'shared',
},
};
const content = computed(() => contents[props.feature]);
// The feature the user tried to use comes first
const orderedFeatures = computed(() => [
...professionalFeatures.filter((feature) => feature.key === content.value.featureKey),
...professionalFeatures.filter((feature) => feature.key !== content.value.featureKey),
]);
</script>
<template>
<Dialog v-model:open="show">
<DialogContent class="sm:max-w-xl p-0 overflow-hidden">
<div class="relative">
<component :is="content.mockup" />
<div
class="absolute bottom-0 left-1/2 z-10 -translate-x-1/2 translate-y-1/2 whitespace-nowrap">
<div
class="flex items-center gap-1.5 rounded-full border border-gray-200 bg-white px-3 py-1 dark:border-border-secondary dark:bg-default-background text-xs font-medium text-text-primary shadow-sm">
<LockClosedIcon class="h-3.5 w-3.5 text-accent-500 dark:text-accent-300" />
{{ content.pill }}
</div>
</div>
</div>
<div class="px-8 pt-9 text-center">
<DialogTitle class="text-xl font-semibold tracking-tight text-text-primary">
{{ content.headline }}
</DialogTitle>
<DialogDescription class="mt-1.5 text-sm text-text-secondary">
{{ content.body }}
</DialogDescription>
</div>
<div class="grid grid-cols-2 gap-x-6 gap-y-4 px-8 pt-9">
<div v-for="item in orderedFeatures" :key="item.key" class="flex gap-3">
<div
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border-tertiary bg-card-background">
<component
:is="featureIcons[item.key]"
class="h-3.5 w-3.5 text-text-secondary" />
</div>
<div class="min-w-0">
<p class="text-sm font-medium text-text-primary">{{ item.title }}</p>
<p class="mt-0.5 text-xs leading-relaxed text-text-tertiary">
{{ item.description }}
</p>
</div>
</div>
</div>
<div class="px-8 pt-9 pb-6">
<UpgradeCtaFooter
align="stack"
label="Upgrade to Professional"
class="mx-auto max-w-xs"
@close="show = false" />
</div>
</DialogContent>
</Dialog>
</template>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import { LockClosedIcon } from '@heroicons/vue/16/solid';
import MockupFrame from './MockupFrame.vue';
const previewRows = [
{ name: 'Acme Corp', duration: '42h 30min', cost: '€ 5,100.00' },
{ name: 'Globex', duration: '27h 15min', cost: '€ 3,270.00' },
{ name: 'Initech', duration: '12h 45min', cost: '€ 1,530.00' },
];
</script>
<template>
<MockupFrame :glow-in-light-mode="false">
<div class="px-6 pt-4">
<div
class="grid grid-cols-[1fr_100px_100px] border-b border-gray-300/70 pb-1.5 text-[11px] text-gray-400 dark:border-white/10 dark:text-white/40">
<span>Client</span>
<span class="text-right">Duration</span>
<span class="flex items-center justify-end gap-1">
<LockClosedIcon class="h-3 w-3" />
Cost
</span>
</div>
<div
v-for="row in previewRows"
:key="row.name"
class="grid grid-cols-[1fr_100px_100px] border-b border-gray-300/50 py-1.5 text-sm last:border-b-0 dark:border-white/5">
<span class="text-gray-800 dark:text-white/90">{{ row.name }}</span>
<span class="text-right tabular-nums text-gray-500 dark:text-white/50">{{
row.duration
}}</span>
<span
class="select-none text-right font-medium text-accent-600 blur-[5px] dark:text-accent-300"
>{{ row.cost }}</span
>
</div>
</div>
</MockupFrame>
</template>

View File

@@ -0,0 +1,48 @@
<script setup lang="ts">
import { LockClosedIcon } from '@heroicons/vue/16/solid';
import MockupFrame from './MockupFrame.vue';
const previewRows = [
{ name: 'Website Redesign', spent: '32h', estimate: '40h', progress: 80, muted: false },
{ name: 'Mobile App', spent: '11h', estimate: '20h', progress: 55, muted: true },
{ name: 'Support Retainer', spent: '6h', estimate: '20h', progress: 30, muted: false },
];
</script>
<template>
<MockupFrame :glow-in-light-mode="false">
<div class="px-6 pt-4">
<div
class="grid grid-cols-[1fr_170px] border-b border-gray-300/70 pb-1.5 text-[11px] text-gray-400 dark:border-white/10 dark:text-white/40">
<span>Project</span>
<span class="flex items-center gap-1">
<LockClosedIcon class="h-3 w-3" />
Progress
</span>
</div>
<div
v-for="row in previewRows"
:key="row.name"
class="grid grid-cols-[1fr_170px] items-center border-b border-gray-300/50 py-1.5 text-sm last:border-b-0 dark:border-white/5">
<span class="text-gray-800 dark:text-white/90">{{ row.name }}</span>
<div class="flex select-none items-center gap-2.5 blur-[3px]">
<div
class="h-1.5 flex-1 overflow-hidden rounded-full bg-gray-300/60 dark:bg-white/10">
<div
class="h-full rounded-full"
:class="
row.muted
? 'bg-gray-400 dark:bg-white/40'
: 'bg-accent-500 dark:bg-accent-300'
"
:style="{ width: row.progress + '%' }" />
</div>
<span
class="w-16 text-right text-xs tabular-nums text-gray-500 dark:text-white/60"
>{{ row.spent }} / {{ row.estimate }}</span
>
</div>
</div>
</div>
</MockupFrame>
</template>

View File

@@ -0,0 +1,23 @@
<script setup lang="ts">
withDefaults(
defineProps<{
// Show the blue glow in light mode too, dark mode always has it
glowInLightMode?: boolean;
}>(),
{ glowInLightMode: true }
);
</script>
<template>
<!-- Note: Shared header of the upgrade modal, all mock-ups have the same height -->
<div
class="relative h-[156px] overflow-hidden border-b border-border-tertiary bg-background dark:border-white/10 dark:bg-[#0b1220]">
<!-- Note: Glow rises from behind the divider, around the pill -->
<div
:class="glowInLightMode ? '' : 'hidden dark:block'"
class="pointer-events-none absolute -bottom-32 left-1/2 h-56 w-[30rem] -translate-x-1/2 rounded-full bg-accent-400/25 blur-3xl dark:bg-accent-500/[0.18]" />
<div class="relative h-full" aria-hidden="true">
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,54 @@
<script setup lang="ts">
import { LockClosedIcon } from '@heroicons/vue/16/solid';
import MockupFrame from './MockupFrame.vue';
// Note: The first goal is the one of the free plan, the others are locked
const previewGoals = [
{
name: 'Billable hours per month',
tracked: '62h',
target: '80h',
progress: 78,
locked: false,
},
{ name: 'Deep work per week', tracked: '14h', target: '20h', progress: 70, locked: true },
{ name: 'Support per week', tracked: '3h', target: '8h', progress: 40, locked: true },
];
</script>
<template>
<MockupFrame :glow-in-light-mode="false">
<div class="px-6 pt-4">
<div
class="grid grid-cols-[1fr_170px] border-b border-gray-300/70 pb-1.5 text-[11px] text-gray-400 dark:border-white/10 dark:text-white/40">
<span>Goal</span>
<span>Progress</span>
</div>
<div
v-for="goal in previewGoals"
:key="goal.name"
class="grid grid-cols-[1fr_170px] items-center border-b border-gray-300/50 py-1.5 text-sm last:border-b-0 dark:border-white/5">
<span class="flex items-center gap-1.5 text-gray-800 dark:text-white/90">
<LockClosedIcon
v-if="goal.locked"
class="h-3 w-3 text-gray-400 dark:text-white/40" />
{{ goal.name }}
</span>
<div
class="flex select-none items-center gap-2.5"
:class="goal.locked ? 'blur-[3px]' : ''">
<div
class="h-1.5 flex-1 overflow-hidden rounded-full bg-gray-300/60 dark:bg-white/10">
<div
class="h-full rounded-full bg-accent-500 dark:bg-accent-300"
:style="{ width: goal.progress + '%' }" />
</div>
<span
class="w-16 text-right text-xs tabular-nums text-gray-500 dark:text-white/60"
>{{ goal.tracked }} / {{ goal.target }}</span
>
</div>
</div>
</div>
</MockupFrame>
</template>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import MockupFrame from './MockupFrame.vue';
const lineItems = [
{ name: 'Website Redesign', hours: '32h', amount: '€ 3,840.00' },
{ name: 'Mobile App', hours: '18h', amount: '€ 2,160.00' },
];
</script>
<template>
<MockupFrame>
<!-- Note: An invoice created from tracked time, it runs off the bottom edge of the header -->
<div
class="absolute left-1/2 top-5 w-60 -translate-x-1/2 rounded-t-md border border-b-0 border-gray-200 bg-white px-5 pt-4 pb-10 shadow-xl shadow-gray-400/25 dark:border-transparent dark:shadow-2xl dark:shadow-black/50">
<div class="flex items-start justify-between">
<div>
<p class="text-[11px] font-semibold text-gray-900">Invoice 2026-014</p>
<p class="text-[9px] text-gray-500">Acme Corp · Due November 14</p>
</div>
<span
class="rounded bg-accent-500 px-1.5 py-0.5 text-[8px] font-bold tracking-wide text-white"
>SENT</span
>
</div>
<div class="mt-3 space-y-1 text-[9px] text-gray-600">
<div v-for="item in lineItems" :key="item.name" class="flex justify-between">
<span>{{ item.name }} · {{ item.hours }}</span>
<span class="tabular-nums">{{ item.amount }}</span>
</div>
</div>
<div
class="mt-2 flex justify-between border-t border-gray-200 pt-1.5 text-[10px] font-semibold text-gray-900">
<span>Total</span>
<span class="tabular-nums">€ 6,000.00</span>
</div>
</div>
</MockupFrame>
</template>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
const bars = [45, 70, 55, 85, 65, 35, 50];
</script>
<template>
<!-- Note: A PDF page that runs off the bottom edge of the header -->
<div
class="rounded-t-md border border-b-0 border-gray-200 bg-white px-5 pt-4 pb-10 shadow-xl shadow-gray-400/25 dark:border-transparent dark:shadow-2xl dark:shadow-black/50">
<div class="flex items-start justify-between">
<div>
<p class="text-[11px] font-semibold text-gray-900">Time report</p>
<p class="text-[9px] text-gray-500">Acme Corp · October 2026</p>
</div>
<span
class="rounded bg-red-500 px-1.5 py-0.5 text-[8px] font-bold tracking-wide text-white"
>PDF</span
>
</div>
<div class="mt-2.5 flex h-7 items-end gap-1">
<div
v-for="(height, index) in bars"
:key="index"
class="flex-1 rounded-sm bg-accent-500/70"
:style="{ height: height + '%' }" />
</div>
<div class="mt-2 space-y-1 text-[9px] text-gray-600">
<div class="flex justify-between">
<span>Website Redesign</span><span class="tabular-nums">32h 15min</span>
</div>
<div class="flex justify-between">
<span>Mobile App</span><span class="tabular-nums">18h 40min</span>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import MockupFrame from './MockupFrame.vue';
import MockupPdfPage from './MockupPdfPage.vue';
</script>
<template>
<MockupFrame>
<!-- Note: The PDF report with two more pages fanned out behind it -->
<div class="absolute left-1/2 top-5 h-full w-56 -translate-x-1/2">
<div
class="absolute inset-x-0 top-3 h-48 origin-bottom -translate-x-10 -rotate-[8deg] rounded-md border border-gray-200 bg-white/80 shadow-lg shadow-gray-400/20 dark:border-transparent dark:bg-white/70 dark:shadow-xl dark:shadow-black/40" />
<div
class="absolute inset-x-0 top-3 h-48 origin-bottom translate-x-10 rotate-[8deg] rounded-md border border-gray-200 bg-white/80 shadow-lg shadow-gray-400/20 dark:border-transparent dark:bg-white/70 dark:shadow-xl dark:shadow-black/40" />
<MockupPdfPage class="absolute inset-x-0 top-0" />
</div>
</MockupFrame>
</template>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { LinkIcon, LockClosedIcon } from '@heroicons/vue/16/solid';
import MockupFrame from './MockupFrame.vue';
const previewRows = [
{ name: 'Website Redesign', duration: '32h 15min' },
{ name: 'Mobile App', duration: '18h 40min' },
];
</script>
<template>
<MockupFrame>
<!-- Note: A browser window showing the shared report, it runs into the bottom edge -->
<div
class="absolute inset-x-10 top-4 -bottom-px rounded-t-lg border border-b-0 border-gray-300/80 bg-white shadow-xl shadow-gray-400/20 dark:border-white/10 dark:bg-white/[0.04] dark:shadow-2xl dark:shadow-black/40">
<div
class="flex items-center gap-2 border-b border-gray-200 px-3 py-2 dark:border-white/10">
<div class="flex gap-1">
<span class="h-2 w-2 rounded-full bg-gray-300 dark:bg-white/15" />
<span class="h-2 w-2 rounded-full bg-gray-300 dark:bg-white/15" />
<span class="h-2 w-2 rounded-full bg-gray-300 dark:bg-white/15" />
</div>
<div
class="flex min-w-0 flex-1 items-center gap-1.5 rounded-md bg-gray-100 px-2 py-1 text-[11px] text-gray-500 dark:bg-black/30 dark:text-white/50">
<LockClosedIcon class="h-3 w-3 shrink-0 text-gray-400 dark:text-white/30" />
<span class="truncate">app.solidtime.io/shared-report</span>
</div>
<span
class="flex shrink-0 items-center gap-1 rounded-md border border-gray-200 bg-gray-50 px-2 py-1 text-[11px] font-medium text-gray-700 dark:border-transparent dark:bg-white/15 dark:text-white/90">
<LinkIcon class="h-3 w-3" />
Copy link
</span>
</div>
<div class="px-4 pt-2 pb-1.5">
<p class="py-1.5 text-xs font-medium text-gray-900 dark:text-white/90">
Acme Corp · October
</p>
<div
v-for="row in previewRows"
:key="row.name"
class="flex justify-between border-b border-gray-100 py-1 text-xs last:border-b-0 dark:border-white/5">
<span class="text-gray-600 dark:text-white/70">{{ row.name }}</span>
<span class="tabular-nums text-gray-400 dark:text-white/50">{{
row.duration
}}</span>
</div>
</div>
</div>
</MockupFrame>
</template>

View File

@@ -0,0 +1,78 @@
<script setup lang="ts">
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/16/solid';
import { pricingUrl, useUpgradeCta } from './useUpgradeCta';
const props = withDefaults(
defineProps<{
label?: string;
align?: 'between' | 'end' | 'stack';
showCompare?: boolean;
}>(),
{ label: 'Upgrade to Professional', align: 'between', showCompare: true }
);
const emit = defineEmits<{ close: [] }>();
const { canUpgrade, goToBilling } = useUpgradeCta();
</script>
<template>
<div
:class="
props.align === 'stack'
? 'flex flex-col items-center gap-3'
: 'flex flex-wrap items-center gap-2'
">
<a
v-if="props.align !== 'stack' && props.showCompare"
:href="pricingUrl"
target="_blank"
rel="noopener"
class="mr-auto inline-flex items-center gap-1 text-xs text-text-tertiary transition-colors hover:text-text-primary">
Compare plans
<ArrowTopRightOnSquareIcon class="h-3 w-3" />
</a>
<template v-if="canUpgrade">
<button
v-if="props.align !== 'stack'"
type="button"
class="h-9 whitespace-nowrap rounded-md px-3 text-sm font-medium text-text-secondary transition-colors hover:bg-white/5 hover:text-text-primary"
@click="emit('close')">
Not now
</button>
<button
type="button"
data-testid="upgrade_modal_cta"
class="h-9 whitespace-nowrap rounded-md bg-accent-500 px-4 text-sm font-semibold text-white shadow-[0_0_24px_-6px] shadow-accent-500/60 transition-colors hover:bg-accent-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
:class="props.align === 'stack' ? 'w-full' : ''"
@click="goToBilling">
{{ props.label }}
</button>
</template>
<template v-else>
<span class="text-xs text-text-tertiary">Only the organization owner can upgrade.</span>
<button
type="button"
class="h-9 rounded-md border border-border-secondary px-3 text-sm font-medium text-text-primary transition-colors hover:bg-white/5"
@click="emit('close')">
Got it
</button>
</template>
<div v-if="props.align === 'stack'" class="flex items-center gap-4 text-xs">
<button
v-if="canUpgrade"
type="button"
class="text-text-tertiary transition-colors hover:text-text-primary"
@click="emit('close')">
Not now
</button>
<a
:href="pricingUrl"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-1 text-text-tertiary transition-colors hover:text-text-primary">
Compare plans
<ArrowTopRightOnSquareIcon class="h-3 w-3" />
</a>
</div>
</div>
</template>

View File

@@ -0,0 +1,19 @@
import type { Component } from 'vue';
import {
AdjustmentsHorizontalIcon,
ClockIcon,
CurrencyEuroIcon,
DocumentArrowDownIcon,
DocumentTextIcon,
LinkIcon,
} from '@heroicons/vue/16/solid';
import type { ProfessionalFeatureKey } from './useUpgradeCta';
export const featureIcons: Record<ProfessionalFeatureKey, Component> = {
rates: CurrencyEuroIcon,
invoicing: DocumentTextIcon,
estimates: ClockIcon,
rounding: AdjustmentsHorizontalIcon,
pdf: DocumentArrowDownIcon,
shared: LinkIcon,
};

View File

@@ -0,0 +1,72 @@
import { computed, inject, type ComputedRef, type InjectionKey } from 'vue';
export interface UpgradeCta {
canUpgrade: ComputedRef<boolean>;
goToBilling: () => void;
}
export const upgradeCtaKey: InjectionKey<UpgradeCta> = Symbol('upgradeCta');
/**
* Shared call to action for the upgrade modal variations: members who can manage billing are
* sent to the billing page, everyone else is asked to contact the organization owner.
*
* The app layout provides who can upgrade and how to get to the billing page. Without a
* provider (e.g. other consumers of this package) nobody can upgrade from the modal.
*/
export function useUpgradeCta(): UpgradeCta {
return inject(upgradeCtaKey, {
canUpgrade: computed(() => false),
goToBilling: () => {},
});
}
// Note: Copy mirrors https://www.solidtime.io/pricing so the gate and the pricing page match.
export const pricingUrl = 'https://www.solidtime.io/pricing';
// Professional features the upgrade modal has a variant for
export type UpgradeFeature =
'billable-rates' | 'estimates' | 'goals' | 'invoicing' | 'pdf-reports' | 'shareable-reports';
export type ProfessionalFeatureKey =
'rates' | 'invoicing' | 'estimates' | 'rounding' | 'pdf' | 'shared';
export type ProfessionalFeature = {
key: ProfessionalFeatureKey;
title: string;
description: string;
};
// Note: Billing and reporting features of the Professional plan on https://www.solidtime.io/pricing
export const professionalFeatures: ProfessionalFeature[] = [
{
key: 'rates',
title: 'Billable rates',
description: 'Set rates per organization, project or member.',
},
{
key: 'invoicing',
title: 'Invoicing',
description: 'Create invoices straight from your tracked time.',
},
{
key: 'estimates',
title: 'Time estimates',
description: 'Budget hours per project and task and see what is left.',
},
{
key: 'rounding',
title: 'Rounding',
description: 'Round time in reports, e.g. to 15 minutes.',
},
{
key: 'pdf',
title: 'PDF reports',
description: 'Export clean reports to send to clients.',
},
{
key: 'shared',
title: 'Shareable reports',
description: 'Share a live report link, no login needed.',
},
];

View File

@@ -21,6 +21,8 @@ export type { CalendarSettings } from './FullCalendar/calendarSettings';
export type { ActivityPeriod } from './FullCalendar/activityTypes';
export { cn } from './utils/cn';
export { useCssVariable } from './utils/useCssVariable';
export { upgradeCtaKey, useUpgradeCta } from './UpgradeModal/useUpgradeCta';
export type { UpgradeCta, UpgradeFeature } from './UpgradeModal/useUpgradeCta';
import Badge from './Badge.vue';
import Button from './Buttons/Button.vue';
@@ -162,6 +164,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs/index';
import TabBar from './TabBar/TabBar.vue';
import TabBarItem from './TabBar/TabBarItem.vue';
import TimezoneMismatchModal from './TimezoneMismatchModal.vue';
import UpgradeBadge from './UpgradeBadge.vue';
import UpgradeModal from './UpgradeModal.vue';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip/index';
export {
@@ -305,4 +309,6 @@ export {
TooltipContent,
TooltipProvider,
TooltipTrigger,
UpgradeBadge,
UpgradeModal,
};