npm.io
1.0.2 โ€ข Published 11h ago

@etlok-systems/mantle-ui-library-vue

Licence
MIT
Version
1.0.2
Deps
19
Size
5.2 MB
Vulns
0
Weekly
0

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/js or node_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 PullComponents

This 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 components
  • branch: Branch to pull from (e.g., "main", "develop") - mutually exclusive with tag
  • tag: Specific release tag (e.g., "v1.0.0") - mutually exclusive with branch
  • components: 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:pull

The command will prompt you with several options:

  1. Storage Location: Choose where to store components

    • resources: Store in resources/js/components/mntl (recommended for customization)
    • node_modules: Store in node_modules/mntl (recommended for library usage)
  2. Fetch Type: Choose what to pull

    • All Components: Pull all available Vue components
    • Folders: Pull components from specified folders in components.json or you can new component also based on your requirememnt in components.json
    • Selected Components: Pull only the components listed in components.json or you can new component also based on your requirememnt in components.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.glob to dynamically import all .vue files from ./components/mntl/**
  • Alias Components: Uses @mntl alias to import from either resources/js/components/mntl or node_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 dev

Folder 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
  1. 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
  2. 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
Fetch Type Options
  1. All Components:

    • Downloads all available Vue components from the repository
    • Includes every .vue file in the src/mntl directory
    • Largest download size but most comprehensive
  2. Folders:

    • Downloads components from specified folders in components.json
    • Useful for organizing components by category (Forms, Layouts, etc.)
    • More selective than "All Components"
  3. Selected Components:

    • Downloads only the components listed in components.json
    • Most selective option, smallest download size
    • Ideal for minimal installations

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 branch for development versions
  • Use tag for stable releases
  • Keep components.json in 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 resources location, you can modify components directly
  • For node_modules location, fork the repository for customizations
  • Always test customizations thoroughly
Updates
  • Re-run php artisan components:pull to 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.json syntax
  • Clear Vite cache: npm run build after configuration changes

Contributing

To contribute to the Mantle component library:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. 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 including title, labelField, valueField, and dataSource.
  • settings โ€” optional settings object, typically used for a store source.
  • 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.js loadDataSource()
    • 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
  1. TestDataResources.vue defines widgetDefinition.widgets and passes each widget into ChartWidget.
  2. ChartWidget.vue uses getDataSourceDefinition() and load() to resolve the dataSource from definition.
  3. ChartWidget calls resources/js/components/crux/mntl.js:
    • normalizeDataSourceDefinition() to read dataSource from definition
    • loadDataSource() to execute the source
    • normalizeDataSourceRows() to turn any result into an array of rows
  4. ChartWidget maps rows through labelField and valueField and renders the chart rows.
What end users should do

If you are building a page with this widget library:

  1. Import ChartWidget into your page/component.
  2. Create widgetDefinition.widgets with a definition.dataSource object.
  3. For store-based widgets, pass settings.store containing the method.
  4. For parent-based widgets, ensure the parent component has the named method.
  5. 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
  1. Run the frontend:
npm run dev
  1. Open the page that renders TestDataResources.vue.
  2. Verify the three example widgets render:
    • API widget
    • store widget
    • parent widget
  3. Check the browser console for these logs:
    • ChartWidget loading source
    • ChartWidget 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-vue

The 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

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 dev

Application 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@latest

Update an existing installation:

npm update @etlok-systems/mantle-ui-library-vue

Troubleshooting

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@latest

Support

For issues, feature requests, or contribution guidelines, please contact the ETLOK Systems development team.

Happy coding with Mantle!

Keywords