Mantle Vue Component Library Setup for Laravel + Vue Project
Mantle is a modern, reusable Vue component library designed. It provides a collection of pre-built, customizable Vue components that integrate seamlessly with your ecosystem, including Tailwind CSS support.
Features
- Vue Compatible: Built with the latest Vue components
- Tailwind CSS Ready: All components come with Tailwind CSS styling
- Laravel Integration: Custom Artisan command for easy installation
- Flexible Storage: Choose between
resources/jsornode_modules - Selective Installation: Pull all components, specific folders, or selected components
- Vite Optimized: Automatic alias configuration for optimal performance
- Developer Friendly: Comprehensive documentation and examples
Requirements
Before installing Mantle, ensure your project meets these requirements:
- Laravel: 9.x or higher
- Node.js: 16.x or higher
- npm/yarn: Latest stable version
- Git: For cloning the component repository
- Vue: Already configured in your Laravel project
- Tailwind CSS: Recommended for styling (optional but highly recommended)
Installation Guide
Follow these steps to integrate Mantle components into your project:
Step 1: Create a New Laravel Project
composer create-project laravel/laravel my-project
### Step 2 : After Creating New Laravel Project
```bash
cd my-project
Step 3 : Install NPM package requirements Dependencies
npm install vue @amcharts/amcharts5 @codemirror/lang-json @codemirror/theme-one-dark @tailwindcss/forms @vue-flow/core @vueform/slider @vueform/toggle @vuepic/vue-datepicker @vueup/vue-quill @vueuse/core axios codemirror js-beautify lodash mitt vue-tabler-icons vuedraggable vue-codemirror moment detect-browser @vitejs/plugin-vue
Step 4 : Install Dev dependencies
npm install -D @tailwindcss/vite autoprefixer axios concurrently postcss laravel-vite-plugin tailwind-scrollbar tailwindcss vite
npm install -D @tailwindcss/postcss
Step 5 : create the custom Artisan command that will handle component installation:
php artisan make:command PullComponentsThis creates app/Console/Commands/PullComponents.php.
Then pick this below whole code and replace or put it in app/Console/Commands/PullComponents.php. to fetch the components.
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class PullComponents extends Command
{
protected $signature = 'components:pull';
protected $description = 'Pull Vue components or folders dynamically from GitHub';
public function handle()
{
$jsonPath = base_path('components.json');
if (! File::exists($jsonPath)) {
$this->error('components.json not found.');
return;
}
$config = json_decode(File::get($jsonPath), true);
$repo = $config['repo'] ?? null;
$branch = $config['tag'] ?? $config['branch'] ?? 'mantle-ui-library-vue';
if (! $repo || ! is_array($config)) {
$this->error('Invalid or missing repo configuration.');
return;
}
$location = $this->choice('Where do you want to store the components?', ['resources', 'node_modules'], 0);
$resourcePath = base_path('resources/js/components/mntl');
$nodeModulesPath = base_path('node_modules/mntl');
foreach ([$resourcePath, $nodeModulesPath] as $path) {
if (File::exists($path)) {
File::deleteDirectory($path);
$this->info("๐งน Cleaned previous components from: $path");
}
}
$validatorsTargetPath = base_path('resources/js/plugins/validators.js');
if (File::exists($validatorsTargetPath)) {
File::delete($validatorsTargetPath);
$this->info('Cleaned previous validators.js');
}
$mntlStylesTargetPath = base_path('resources/js/mntl.styles.js');
if (File::exists($mntlStylesTargetPath)) {
File::delete($mntlStylesTargetPath);
$this->info('Cleaned previous mntl.styles.js');
}
$fetchType = $this->choice(
'What do you want to fetch?',
['All Components', 'Folders', 'Selected Components'],
2
);
$baseTargetPath = $location === 'resources'
? base_path('resources/js/components/mntl')
: base_path('node_modules/mntl');
$tmpDir = base_path('components-tmp');
File::ensureDirectoryExists($baseTargetPath);
$tempClone = $tmpDir.DIRECTORY_SEPARATOR.md5($repo.$branch);
if (! File::exists($tempClone)) {
$this->info("Cloning $repo (branch: $branch)...");
exec("git clone --depth 1 --branch {$branch} {$repo} {$tempClone} && rd /s /q {$tempClone}\\.git");
File::deleteDirectory($tempClone.DIRECTORY_SEPARATOR.'.git');
$unwantedFiles = ['.gitignore', '.gitattributes', '.editorconfig'];
foreach ($unwantedFiles as $file) {
$fullPath = $tempClone.DIRECTORY_SEPARATOR.$file;
if (File::exists($fullPath)) {
File::delete($fullPath);
}
}
}
$sourceRoot = $tempClone.'/src/mntl';
if (! File::exists($sourceRoot)) {
$this->error('src/mntl directory not found in repo.');
return;
}
if ($fetchType === 'All Components') {
$this->copyAllFiles($sourceRoot, $baseTargetPath);
}
if ($fetchType === 'Folders') {
$folders = $config['folders'] ?? [];
foreach ($folders as $folder) {
$src = $sourceRoot.'/'.$folder;
$dest = $baseTargetPath.'/'.$folder;
$this->copyAllFiles($src, $dest);
}
}
if ($fetchType === 'Selected Components') {
$componentFiles = collect(File::allFiles($sourceRoot))
->filter(fn ($file) => in_array($file->getExtension(), ['vue', 'js', 'ts', 'css']))
->mapWithKeys(function ($file) {
return [$file->getFilenameWithoutExtension() => $file->getPathname()];
});
foreach ($config['components'] ?? [] as $componentName) {
if (! isset($componentFiles[$componentName])) {
$this->warn("โ ๏ธ Component '$componentName' not found. Skipping.");
continue;
}
$sourcePath = $componentFiles[$componentName];
$relativePath = ltrim(str_replace($sourceRoot, '', $sourcePath), DIRECTORY_SEPARATOR);
$targetPath = $baseTargetPath.DIRECTORY_SEPARATOR.$relativePath;
if (File::exists($targetPath)) {
$this->info("Skipped $componentName (already exists)");
continue;
}
File::ensureDirectoryExists(dirname($targetPath));
File::copy($sourcePath, $targetPath);
$this->info("Copied $componentName โ ".str_replace(base_path().DIRECTORY_SEPARATOR, '', $targetPath));
}
}
$mntlFile = collect(File::allFiles($sourceRoot))
->filter(fn ($file) => $file->getExtension() === 'js' && $file->getFilename() === 'mntl.js')
->mapWithKeys(function ($file) {
return [$file->getFilenameWithoutExtension() => $file->getPathname()];
});
if (isset($mntlFile['mntl'])) {
File::copy($mntlFile['mntl'], $baseTargetPath.DIRECTORY_SEPARATOR.'mntl.js');
$this->info('Copied mntl.js');
} else {
$this->error('mntl.js not found in repo');
}
$this->copyValidatorsJS($tempClone);
$this->copymntlStylesJS($tempClone, $baseTargetPath);
File::deleteDirectory($tempClone);
File::deleteDirectory($tmpDir);
$this->updateViteAlias($location);
}
private function copyAllFiles($src, $dest)
{
if (! File::exists($src)) {
$this->warn("โ ๏ธ Directory not found: $src");
return;
}
$files = File::allFiles($src);
foreach ($files as $file) {
if (! in_array($file->getExtension(), ['vue', 'js', 'ts', 'css'])) {
continue;
}
$relativePath = ltrim(str_replace($src, '', $file->getPathname()), DIRECTORY_SEPARATOR);
$targetPath = rtrim($dest, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$relativePath;
if (File::exists($targetPath)) {
$this->info("Skipped {$file->getFilename()} (already exists)");
continue;
}
File::ensureDirectoryExists(dirname($targetPath));
File::copy($file->getPathname(), $targetPath);
$this->info('Copied '.$file->getFilename());
}
}
private function copyValidatorsJS($tempClone)
{
$validatorsPathInRepo = $tempClone.'/src/mntl/plugins/validators.js';
$validatorsTargetPath = base_path('resources/js/plugins/validators.js');
if (! File::exists($validatorsPathInRepo)) {
$this->warn('validators.js file not found at expected path in repo.');
return;
}
File::ensureDirectoryExists(dirname($validatorsTargetPath));
File::copy($validatorsPathInRepo, $validatorsTargetPath);
$this->info('Copied validators.js to resources/js/plugins/validators.js');
}
private function copymntlStylesJS($tempClone, $baseTargetPath)
{
$mntlStylesPathInRepo = $tempClone.'/src/mntl/mntl.styles.js';
$targetPath = $baseTargetPath.DIRECTORY_SEPARATOR.'mntl.styles.js';
if (! File::exists($mntlStylesPathInRepo)) {
$this->warn('mntl.styles.js not found in repo.');
return;
}
File::ensureDirectoryExists(dirname($targetPath));
File::copy($mntlStylesPathInRepo, $targetPath);
$this->info('Copied mntl.styles.js โ '.$targetPath);
}
private function updateViteAlias($location)
{
$vitePath = base_path('vite.config.js');
if (! File::exists($vitePath)) {
$this->warn('vite.config.js not found. Skipping alias setup.');
return;
}
$content = File::get($vitePath);
if (str_contains($content, '@mntl')) {
$this->info('@mntl alias already exists.');
return;
}
if (! str_contains($content, "from 'path'")) {
$content = "import path from 'path';\n".$content;
}
if (! str_contains($content, "from 'fs'")) {
$content = "import fs from 'fs';\n".$content;
}
if (preg_match('/export\s+default\s+defineConfig\s*\(\s*{/', $content)) {
$content = preg_replace(
'/export\s+default\s+defineConfig\s*\(\s*{/',
"export default defineConfig(() => {\n\n const mntlPath = fs.existsSync('resources/js/components/mntl')\n ? 'resources/js/components/mntl'\n : 'node_modules/mntl';\n\n return {",
$content,
1
);
$content = preg_replace(
'/}\s*\);\s*$/',
" };\n});",
$content
);
}
if (preg_match('/return\s*{/', $content)) {
$content = preg_replace(
'/return\s*{/',
"return {\n resolve: {\n alias: {\n '@mntl': path.resolve(__dirname, mntlPath),\n }\n },",
$content,
1
);
File::put($vitePath, $content);
$this->info('Dynamic @mntl alias added successfully (auto-detect mode)');
return;
}
$this->warn('Could not inject alias automatically. Unknown Vite structure.');
}
}
Step 2: Register the Command (Laravel < 11)
For Laravel versions prior to 11, you need to manually register the command in app/Console/Kernel.php:
// app/Console/Kernel.php
protected $commands = [
// ... other commands
\App\Console\Commands\PullComponents::class,
];
Note: In Laravel 11+, commands are auto-discovered from the app/Console/Commands directory, so manual registration is not required.
Step 3: Create components.json Configuration
Create a components.json file in your project root with the following structure:
{
"repo": "https://github.com/etlok/mantle-dashboard-v1",
"branch": "mantle-ui-library-vue",
"components": [
"ColorInput"
],
"folders": [
"Forms",
"Layouts"
]
}Configuration Options:
repo: GitHub repository URL containing the Mantle componentsbranch: Branch to pull from (e.g., "main", "develop") - mutually exclusive withtagtag: Specific release tag (e.g., "v1.0.0") - mutually exclusive withbranchcomponents: Array of specific component names to pull (when using "Selected Components" option)folders: Array of folder names to pull (when using "Folders" option)
Step 4: Run the Installation Command
Execute the Mantle component pull command:
php artisan components:pullThe command will prompt you with several options:
Storage Location: Choose where to store components
resources: Store inresources/js/components/mntl(recommended for customization)node_modules: Store innode_modules/mntl(recommended for library usage)
Fetch Type: Choose what to pull
All Components: Pull all available Vue componentsFolders: Pull components from specified folders incomponents.jsonor you can new component also based on your requirememnt incomponents.jsonSelected Components: Pull only the components listed incomponents.jsonor you can new component also based on your requirememnt incomponents.json
Step 5: Automatic Vite Configuration
The command automatically updates your vite.config.js to include the @mntl alias:
//Copy and Paste Below Code in vite.config.js
// vite.config.js (after running the command)
import fs from 'fs';
import path from 'path';
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import tailwindcss from '@tailwindcss/vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig(() => {
const mntlPath = fs.existsSync('resources/js/components/mntl')
? 'resources/js/components/mntl'
: 'node_modules/mntl';
return {
resolve: {
alias: {
'@mntl': path.resolve(__dirname, mntlPath),
}
},
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
vue(),
tailwindcss(),
],
server: {
watch: {
ignored: ['**/storage/framework/views/**'],
},
},
};
});
create AppDataHandler Component
Location: resources/js/components/Custom/AppDataHandler.vue
AppDataHandler acts as a centralized store for the Vue frontend โ similar to Vuex/Pinia but implemented as a single renderless component. It is mounted once at the root layout and accessed globally via this.agent.store
copy and paste below content in AppDataHandler.vue file
<script>
import {Mntl, MntlModel, MntlUtils} from "../mntl/mntl.js";
import {useIdle} from "@vueuse/core";
export default {
name: "AppDataHandler",
data(){
return {
views: {
},
loading:{
user: false
},
auth: {
url: "",
entity: 'company',
entity_id: null,
project: 'webauth',
token: ''
},
refData:{},
idleTimer: null,
reconnectionTimer: null,
promises: {
user: null,
connected: null,
status: null
},
data: {
user: null,
permissions: [],
role_configs:null,
},
}
},
mounted() {
this.init();
},
methods: {
goTo(link){
window.location = link;
},
hasAccess(scopes){
let hasAccess = true;
if(this.data.permissions.includes('dashboard:admin')) return true;
scopes.forEach((scope)=>{
let scopeParts = scope.split('|');
if(scopeParts.length === 1) {
if(!this.data.permissions.includes(scope)) {
hasAccess = false;
}
} else {
let any = false;
scopeParts.forEach((part)=>{
if(this.data.permissions.includes(part)) {
any = true;
}
});
if(!any) {
hasAccess = false;
}
}
});
return hasAccess;
},
repo(k){
return MntlUtils.getValue(k,this.data);
},
init(){
let self = this;
this.loadUser();
},
loadUser(){
if(Laravel.user.authenticated) {
let params ;
if(this.loading.user) return;
this.loading.user = true;
this.data.user = null;
this.promises.user = new Promise((resolve, reject)=>{
let url = '/api/me'
axios(url, {
method:'get',
headers: {
'Content-Type': 'application/json'
},
params: params,
responseType: 'json',
})
.then((response) => {
this.data.user = response.data.user;
this.loading.user = false;
resolve();
})
.catch((error) => {
this.agent.handler.handleError(error);
this.loading.user = false;
reject();
});
})
}
},
getMe(){
return new Promise((resolve, reject)=>{
let url = '/api/me'
axios(url, {
method:'get',
headers: {
'Content-Type': 'application/json'
},
responseType: 'json',
})
.then((response) => {
this.data.user = response.data.user;
resolve();
})
.catch((error) => {
this.agent.handler.handleError(error);
this.loading.user = false;
reject();
});
});
},
logout(){
let url = '/api/user/logout'
axios(url, {
method:'post',
data: {},
headers: {
'Content-Type': 'application/json'
},
responseType: 'json',
})
.then((response) => {
window.location = '/';
})
.catch((error) => {
this.agent.handler.handleError(error);
});
},
getUser(){
if(this.data.user === null) {
return this.promises.user;
} else {
return new Promise((resolve)=>{
resolve();
})
}
},
buildView(view, data){
if(this.views.hasOwnProperty(view)) {
} else {
return {
id: MntlUtils.guid(),
component: view,
data: data
}
}
},
connect(){
return new Promise((resolve, reject)=> {
if(this.socket().status.connected) {
resolve();
return;
}
this.socket().setConfiguration(this.auth);
try {
this.socket().connect(this.auth.url+this.auth.project+'/' + this.auth.entity + '/' + this.auth.entity_id);
this.promises.connected = resolve;
} catch (e) {
reject();
}
});
},
socket(){
return this.$refs.websocketConnection;
},
websocketError(){
},
handleToastMessage(message){
return new Promise((resolve, reject)=>{
let params = {
title: message.title,
html: message.text,
icon: message.hasOwnProperty('icon') ? message.icon : 'warning',
toast: message.toast === true, // โ
use actual value
target: 'body',
position: message.hasOwnProperty('position')
? message.position
: (message.toast === true ? 'top-end' : 'center') // default fallback
};
if (message.hasOwnProperty('attributes')) {
params = {
...params,
...message.attributes
};
}
this.$swal(params).then(()=>{
resolve();
});
});
},
handleToastError(error) {
return new Promise((resolve, reject)=>{
if(error.hasOwnProperty('response')) {
var message = {};
if(error.response.data.hasOwnProperty('errorMessage')) {
message = error.response.data.errorMessage;
} else {
message = {
title: 'Something Went Wrong!',
text: 'An error occurred while trying to perform this operation.'
}
}
this.$swal({
title: message.title,
html: message.text,
icon: 'error',
toast: true,
target: 'body',
position: 'bottom-end',
timer: 5000, // auto close in 3 seconds
timerProgressBar: true, // shows progress bar
showConfirmButton: false // hides OK button
}).then(()=>{
resolve();
});
} else {
this.$swal({
title: 'Something Went Wrong',
html: 'We are apologize for this inconvenience but we are on it.',
icon: 'error',
toast: false,
target: 'body',
position:'center'
}).then(()=>{
resolve();
});
}
});
},
}
}
</script>
<template>
<div>
</div>
</template>
<style scoped>
</style>Create App.vue in resources/js/App.vue and paste below code.
<template>
<Test />
</template>
<script>
import Test from './components/Custom/Test.vue';
export default {
components: {
Test
}
}
</script>
Create Test.vue in resources/js/components/Custom/Test.vue and Copy the code present in Test.html.
Update resources/js/app.js by pasting below code.
import './bootstrap';
import '../css/app.css';
import { createApp } from 'vue/dist/vue.esm-bundler';
import { detect } from 'detect-browser';
import mitt from 'mitt';
import { Mntl, MntlUtils } from './components/mntl/mntl.js';
import { MntlStyles } from './components/mntl/mntl.styles.js';
import App from './App.vue';
window._events = mitt();
const app = createApp(App);
// Register local Mantle components
const localMantle = import.meta.glob('./components/mntl/**/*.vue', {
eager: true
});
for (const [path, definition] of Object.entries(localMantle)) {
const name = path.split('/').pop().replace('.vue', '');
const kebabCase = name
.replace(/([a-z])([A-Z])/g, '$1-$2')
.toLowerCase();
app.component(kebabCase, definition.default);
}
// Register Mantle components from alias (optional)
try {
const nodeMantle = import.meta.glob('@mntl/**/*.vue', {
eager: true
});
for (const [path, definition] of Object.entries(nodeMantle)) {
const name = path.split('/').pop().replace('.vue', '');
const kebabCase = name
.replace(/([a-z])([A-Z])/g, '$1-$2')
.toLowerCase();
if (!app._context.components[kebabCase]) {
app.component(kebabCase, definition.default);
}
}
} catch (error) {
console.warn('No @mntl alias components found');
}
// Mantle configuration
Mntl.config = {
modal: {
settings: {
show: {
backdrop: true
},
allow: {
dismiss: true
},
styles: {
backdrop: {
default:
'z-50 fixed transition-all delay-100 duration-300 inset-0 bg-black/[0.41] backdrop-blur-[2px]'
}
}
}
},
styles: MntlStyles.config,
app: {
settings: {}
}
};
// Global agent object
app.config.globalProperties.agent = {
mobile: false,
browser: detect(),
screen: {
width: window.innerWidth,
height: window.innerHeight,
orientation:
window.innerWidth > window.innerHeight
? 'landscape'
: 'portrait'
},
handler: null
};
// Optional globals
app.config.globalProperties.Mntl = Mntl;
app.config.globalProperties.MntlUtils = MntlUtils;
// Mount app
app.mount('#app');
What happens in app.js**:
- Local Components: Uses
import.meta.globto dynamically import all.vuefiles from./components/mntl/** - Alias Components: Uses
@mntlalias to import from eitherresources/js/components/mntlornode_modules/mntl - Naming Convention: Converts PascalCase component names to kebab-case (e.g.,
ColorInputโcolor-input) - Deduplication: Prevents duplicate registration by checking if component already exists
Step 7:Update resources/css/app.css Copy and Paste Bewlow code in resources/css/app.css.
@import "tailwindcss";
@source "../**/*.{js,vue,blade.php}";
@source "../js/components/mntl/**/*.{js,vue}";
Create Blade View inside resources/views/test.blade.php and copy the below code
<!DOCTYPE html> <html> <head> <title>Mantle UI Demo</title> @vite(['resources/js/app.js']) </head> <body> <div id="app"></div> </body> </html>
Create postcss.config.js in root directory and paste below code
import tailwindcss from '@tailwindcss/postcss'
import autoprefixer from 'autoprefixer'
export default {
plugins: [
tailwindcss(),
autoprefixer(),
],
}
Create tailwind.config.js in root directory and paste below code
import forms from '@tailwindcss/forms';
export default {
content: [
'./resources/**/*.{blade.php,js,vue}',
'./resources/js/**/*.{js,vue}',
'./resources/views/**/*.blade.php',
'./resources/js/components/mntl/**/*.{js,vue}',
],
theme: {
extend: {
fontFamily: {
'instrument-sans': ["'Instrument Sans'", 'ui-sans-serif', 'system-ui', 'sans-serif'],
},
},
},
plugins: [forms],
};
Update routes/web.php:
Route::get('/test', function () { return view('test'); });
Step 8: Build Assets
Compile your assets with Vite:
npm run build
# or for development
npm run devFolder Structure After Installation
After running the command, your project structure will look like this:
your-laravel-project/
โโโ app/
โ โโโ Console/
โ โโโ Commands/
โ โโโ PullComponents.php
โโโ components.json
โโโ resources/
โ โโโ js/
โ โโโ app.js (updated)
โ โโโ components/
โ โ โโโ mntl/ (if chosen 'resources' location)
โ โ โโโ ColorInput.vue
โ โ โโโ DataTable.vue
โ โ โโโ mntl.js
โ โ โโโ mntl.styles.js
โ โ โโโ ...
โ โโโ plugins/
โ โโโ validators.js
โโโ node_modules/
โ โโโ mntl/ (if chosen 'node_modules' location)
โ โโโ ColorInput.vue
โ โโโ DataTable.vue
โ โโโ mntl.js
โ โโโ mntl.styles.js
โ โโโ ...
โโโ vite.config.js (updated with @mntl alias)
โโโ ...
Command Options Explained
Storage Location Options
resources:
- Stores components in
resources/js/components/mntl - Allows customization of component styles and behavior
- Components are part of your source code
- Recommended for projects that need to modify components
- Stores components in
node_modules:
- Stores components in
node_modules/mntl - Treats components as external dependency
- Easier updates via npm/yarn
- Recommended for projects using components as-is
- Stores components in
Fetch Type Options
All Components:
- Downloads all available Vue components from the repository
- Includes every
.vuefile in thesrc/mntldirectory - Largest download size but most comprehensive
Folders:
- Downloads components from specified folders in
components.json - Useful for organizing components by category (Forms, Layouts, etc.)
- More selective than "All Components"
- Downloads components from specified folders in
Selected Components:
- Downloads only the components listed in
components.json - Most selective option, smallest download size
- Ideal for minimal installations
- Downloads only the components listed in
Additional Files Copied
The command also copies essential supporting files:
- mntl.js: Main JavaScript utilities and configurations
- mntl.styles.js: Global styles and CSS variables
- validators.js: Form validation utilities (copied to
resources/js/plugins/)
Notes and Best Practices
Version Management
- Use
branchfor development versions - Use
tagfor stable releases - Keep
components.jsonin version control
Performance Considerations
- Use "Selected Components" for production to reduce bundle size
- Enable Vite's code splitting for better performance
- Consider lazy loading for large component libraries
Customization
- When using
resourceslocation, you can modify components directly - For
node_moduleslocation, fork the repository for customizations - Always test customizations thoroughly
Updates
- Re-run
php artisan components:pullto update components - The command cleans old files before pulling new ones
- Backup custom modifications before updating
Troubleshooting
- Ensure Git is installed and accessible
- Check repository permissions if cloning fails
- Verify
components.jsonsyntax - Clear Vite cache:
npm run buildafter configuration changes
Contributing
To contribute to the Mantle component library:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
License
Mantle is open-sourced software licensed under the MIT license.
Support
For support and questions:
- GitHub Issues: Report bugs and request features
- Documentation: Check this README for detailed usage
- Community: Join our Discord server for discussions
MNTL Vue Component Library
This repository contains a small reusable Vue widget pattern for MNTL components. The below example files used as the test harness are:
src\mntl\Layouts\pages\TestDataResources.vueโ example dashboard page used for testing and demoing the widget flow.
The example dashboard page is not the final product; it is a test page that helps you verify the three supported data source scenarios and see how the widget renders.
What ChartWidget expects
ChartWidget receives props:
definitionโ widget configuration includingtitle,labelField,valueField, anddataSource.settingsโ optional settings object, typically used for astoresource.dataโ optional raw rows if you want to bypass data source loading.
Supported dataSource types
The definition.dataSource object controls how ChartWidget loads data:
api- fetches data with a URL
- uses
resources/js/components/crux/mntl.jsloadDataSource() - expected config:
type: 'api',url,params
store- Make a function in appDataHandler.vue and pass that function with parameter .
- expected config:
type: 'store',function,params
parent- calls a method on the parent Vue component
- expected config:
type: 'parent',function,params
Example ChartWidget test definitions
widgetDefinition: {
widgets: [
{
id: 'api-chart',
definition: {
title: 'API source',
dataSource: {
type: 'api',
url: 'https://jsonplaceholder.typicode.com/users',
params: {}
},
labelField: 'name',
valueField: 'id'
}
},
{
id: 'store-chart',
definition: {
title: 'Store source',
dataSource: {
type: 'store',
function: 'getStoreData',
params: {}
},
labelField: 'category',
valueField: 'count'
},
settings: {
store: defaultStore
}
},
{
id: 'parent-chart',
definition: {
title: 'Parent source',
dataSource: {
type: 'parent',
function: 'getParentData',
params: {}
},
labelField: 'label',
valueField: 'value'
}
}
]
}How the data flows
TestDataResources.vuedefineswidgetDefinition.widgetsand passes each widget intoChartWidget.ChartWidget.vueusesgetDataSourceDefinition()andload()to resolve thedataSourcefromdefinition.ChartWidgetcallsresources/js/components/crux/mntl.js:normalizeDataSourceDefinition()to readdataSourcefromdefinitionloadDataSource()to execute the sourcenormalizeDataSourceRows()to turn any result into an array of rows
ChartWidgetmaps rows throughlabelFieldandvalueFieldand renders the chart rows.
What end users should do
If you are building a page with this widget library:
- Import
ChartWidgetinto your page/component. - Create
widgetDefinition.widgetswith adefinition.dataSourceobject. - For store-based widgets, pass
settings.storecontaining the method. - For parent-based widgets, ensure the parent component has the named method.
- Render
<chart-widget>and pass:definition,:settings, and optional:data.
Example page usage
<template>
<div>
<chart-widget
:id="widget.id"
:definition="widget.definition"
:settings="widget.settings"
/>
</div>
</template>Testing the example UI
- Run the frontend:
npm run dev- Open the page that renders
TestDataResources.vue. - Verify the three example widgets render:
- API widget
- store widget
- parent widget
- Check the browser console for these logs:
ChartWidget loading sourceChartWidget loaded rows
If you want to use the widget library in a real page, copy the pattern from TestDataResources.vue and adapt the dataSource object to your application data.
MNTL UI Library for Seperate Vue Project Setup
MNTL is a reusable Vue component library published as:
@etlok-systems/mantle-ui-library-vueThe library provides:
- Reusable Vue UI Components
- Form Components
- Layout Components
- Dashboard Components
- Chat Agent Components
- Widget Components
- Custom Components
- Validation Helpers
- Utility Functions
- MNTL Runtime Helpers
- Default Style Configuration
- Vue Plugin Support
Requirements
Before installing the package, ensure your application uses:
- Vue 3
- Vite (recommended)
- Node.js 18+ (recommended)
Installation
Creating a New Vue Project
If you don't already have a Vue application:
Create a new Vite project
npm create vite@latest my-app -- --template vue
Navigate into the project
cd my-app
npm install
npm install vue
Install MNTL
npm install @etlok-systems/mantle-ui-library-vue
npm view @etlok-systems/mantle-ui-library-vue
Install dependencies
npm install -D tailwindcss postcss autoprefixer @tailwindcss/postcss @tailwindcss/vite
Global Installation (Recommended)
Register the entire component library globally.
main.js
import { createApp } from 'vue'
import App from './App.vue'
import MNTL from '@etlok-systems/mantle-ui-library-vue'
import '@etlok-systems/mantle-ui-library-vue/style.css'
const app = createApp(App)
app.use(MNTL)
app.mount('#app')
Paste Below code in style.css
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--mntl-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--mntl-color-text: #0f172a;
--mntl-color-muted: #64748b;
--mntl-color-border: #e2e8f0;
--mntl-color-surface: #ffffff;
}
.mntl-root,
.mntl-root * {
box-sizing: border-box;
}
.mntl-root {
color: var(--mntl-color-text);
font-family: var(--mntl-font-family);
}
Create postcss.config.js in root directory and paste below code
import tailwindcss from '@tailwindcss/postcss'
import autoprefixer from 'autoprefixer'
export default {
plugins: [
tailwindcss(),
autoprefixer(),
],
}
Create tailwind.config.js in root directory and paste below code
import forms from '@tailwindcss/forms';
export default {
content: ['./index.html', './src/**/*.{js,ts,vue}'],
theme: {
extend: {},
},
plugins: [forms],
};Start development server
npm run devApplication will run on:
http://localhost:5173
After registration, all MNTL components are available globally.
Example:
<ActionButton />or
<action-button />Individual Component Import
You may import only the components you need.
<script setup>
import {
ActionButton,
DateCell
} from '@etlok-systems/mantle-ui-library-vue'
</script>Example Usage
DateCell
<script setup>
import { DateCell } from '@etlok-systems/mantle-ui-library-vue'
const definition = {
schema: {
text: 'created_at'
},
format: 'DD MMM YYYY'
}
const data = {
value(key) {
return {
created_at: '2026-06-22T10:00:00Z'
}[key]
}
}
</script>
<template>
<DateCell
:definition="definition"
:data="data"
/>
</template>Available Exports
The package exports components, utilities, styles and helpers.
import {
Mntl,
MntlUtils,
MntlFormControl,
MntlQuery,
MntlModel,
MntlCollection,
MntlChildQuery
} from './mntl/mntl.js';
import {
ActionButton,
ActionsCell,
AddNewModal,
AlertElement,
AppLayout,
AutoCompleteElement,
AutoCompleteMultiPickerOverlay,
AutoCompletePickerOverlay,
BadgeElement,
BaseListRow,
BaseRow,
BaseTableEmptyRecordRow,
BaseTableRow,
BulkUploaderOverlay,
ChartWidget,
ChatAgent,
CollectionLayout,
ColorInput,
ColumnSelectDropdown,
ConnectionCard,
CreateModal,
CustomLayout,
DashboardLayout,
DashboardNav,
DataHandler,
DateCell,
DatePickerElement,
DatePickerOverlay,
DetailModal,
DropdownActionsCell,
DynamicIcon,
FileUploadElement,
GMapsPlacesInputElement,
HelpTextAddOn,
HiddenElement,
ImageUploadElement,
InputElement,
JsonArrayElement,
JsonElement,
LayoutHeader,
ListLayout,
ListView,
LookupCell,
MessageHandler,
MultiAutoCompleteElement,
MultiListElement,
MultiPickerElement,
MultiPickerOverlay,
MultipleFileUploadElement,
MultiSelectElement,
MultiSelectPickerElement,
MultiValuesCell,
NavLink,
NavSection,
Paginated,
PivotCell,
PivotRow,
Reader,
ResponsiveAppContainer,
ResponsiveDashboardContainer,
RunLogPanel,
RunStepCard,
RunStepDetailList,
RunStepDetailRow,
RunSummaryBanner,
RunSummaryStats,
SelectElement,
StandardCell,
StandardForm,
StandardModal,
StandardMultiSelectFilter,
StripeElement,
TableLayout,
TableLayoutOld,
TagArrayCell,
TagInputElement,
TestDataResources,
TextElement,
TextToggleInputElement,
TimePickerElement,
Toast,
ToggleElement,
TopNav,
Uploader,
WidgetLayout,
WorkflowCanvas
} from '@etlok-systems/mantle-ui-library-vue'Updating to a New Version
Install the latest published version:
npm install @etlok-systems/mantle-ui-library-vue@latestUpdate an existing installation:
npm update @etlok-systems/mantle-ui-library-vueTroubleshooting
Styles Not Applied
Ensure:
import '@etlok-systems/mantle-ui-library-vue/style.css'is included in your application's entry file.
If using Tailwind CSS, verify that the package path is included in the Tailwind content configuration.
Component Not Found
Ensure the component is either:
- Registered globally using:
app.use(MNTL)or
- Imported directly:
import { ActionButton } from '@etlok-systems/mantle-ui-library-vue'Clean Installation
If package updates are not reflected:
npm uninstall @etlok-systems/mantle-ui-library-vue
npm cache clean --force
npm install @etlok-systems/mantle-ui-library-vue@latestSupport
For issues, feature requests, or contribution guidelines, please contact the ETLOK Systems development team.
Happy coding with Mantle!