├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── nova.php ├── dist ├── css │ └── card.css ├── js │ ├── tool.js │ └── tool.js.LICENSE.txt └── mix-manifest.json ├── nova.mix.js ├── package.json ├── resources ├── css │ └── card.css └── js │ ├── components │ ├── CollapseButton.vue │ ├── Menu.vue │ ├── MenuGroup.vue │ ├── MenuItem.vue │ ├── MenuSection.vue │ ├── Noop.vue │ ├── SectionHeader.vue │ └── SvgIcon.vue │ ├── lib │ └── ClickOutsideDirective.js │ ├── mixins │ └── MenuMixin.js │ ├── native │ └── UserMenu.vue │ └── tool.js ├── screenshots ├── dark.png └── light.png ├── src └── CollapsibleResourceManagerServiceProvider.php ├── tailwind.config.js ├── webpack.mix.js └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: milewski 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /node_modules 4 | package-lock.json 5 | composer.phar 6 | composer.lock 7 | phpunit.xml 8 | .phpunit.result.cache 9 | .DS_Store 10 | Thumbs.db 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Digital Creative 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Collapsible Resource Manager 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/digital-creative/collapsible-resource-manager)](https://packagist.org/packages/digital-creative/collapsible-resource-manager) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/digital-creative/collapsible-resource-manager)](https://packagist.org/packages/digital-creative/collapsible-resource-manager) 5 | [![License](https://img.shields.io/packagist/l/digital-creative/collapsible-resource-manager)](https://github.com/dcasia/collapsible-resource-manager/blob/main/LICENSE) 6 | 7 | 8 | 9 | Laravel Nova Collapsible Resource Manager in action 10 | 11 | 12 | Provides an easy way to order and group your resources on the sidebar. 13 | 14 | # Installation 15 | 16 | You can install the package via composer: 17 | 18 | ``` 19 | composer require digital-creative/collapsible-resource-manager 20 | ``` 21 | 22 | # Usage 23 | 24 | This package can serve as a seamless replacement for Nova's default sidebar menu. 25 | The only modification it makes to the original Nova menu is the addition of the `->icon()` method to the MenuItem class, which enables you to easily incorporate icons into each menu item. 26 | 27 | For all available options you can check nova documentation [here](https://nova.laravel.com/docs/customization/menus.html#menu-sections). 28 | 29 | ```php 30 | class NovaServiceProvider extends NovaApplicationServiceProvider { 31 | 32 | public function boot(): void 33 | { 34 | //... 35 | Nova::mainMenu(function (Request $request): array { 36 | return [ 37 | MenuSection::make('Content', [ 38 | MenuGroup::make('User Base', [ 39 | MenuItem::resource(User::class)->icon('...'), 40 | MenuItem::resource(Article::class)->icon('annotation'), 41 | MenuItem::resource(Comment::class)->icon('chat-alt'), 42 | ]), 43 | ])->icon('lightning-bolt')->collapsable(), 44 | ]; 45 | }); 46 | //... 47 | } 48 | 49 | } 50 | ``` 51 | 52 | ## Configuration 53 | 54 | You can also enable/disable the main header menu handling. For example, if you don't want the user menu, theme switcher, 55 | and notification icon to be moved to the bottom left side, you can manually disable it by adding these lines to your Nova config file: 56 | 57 | ```php 58 | // config/nova.php 59 | 60 | 'vendors' => [ 61 | 'collapsible_resource_manager' => [ 62 | 'move_user_menu' => false, 63 | 'move_theme_switcher' => false, 64 | 'move_notification_center' => false, 65 | 'section_title' => true, 66 | 'collapse_on_select' => true, 67 | 'collapse_on_refresh' => false, 68 | ] 69 | ] 70 | ``` 71 | 72 | ## ⭐️ Show Your Support 73 | 74 | Please give a ⭐️ if this project helped you! 75 | 76 | ### Other Packages You Might Like 77 | 78 | - [Nova Dashboard](https://github.com/dcasia/nova-dashboard) - The missing dashboard for Laravel Nova! 79 | - [Nova Welcome Card](https://github.com/dcasia/nova-welcome-card) - A configurable version of the `Help card` that comes with Nova. 80 | - [Icon Action Toolbar](https://github.com/dcasia/icon-action-toolbar) - Replaces the default boring action menu with an inline row of icon-based actions. 81 | - [Expandable Table Row](https://github.com/dcasia/expandable-table-row) - Provides an easy way to append extra data to each row of your resource tables. 82 | - [Collapsible Resource Manager](https://github.com/dcasia/collapsible-resource-manager) - Provides an easy way to order and group your resources on the sidebar. 83 | - [Resource Navigation Tab](https://github.com/dcasia/resource-navigation-tab) - Organize your resource fields into tabs. 84 | - [Resource Navigation Link](https://github.com/dcasia/resource-navigation-link) - Create links to internal or external resources. 85 | - [Nova Mega Filter](https://github.com/dcasia/nova-mega-filter) - Display all your filters in a card instead of a tiny dropdown! 86 | - [Nova Pill Filter](https://github.com/dcasia/nova-pill-filter) - A Laravel Nova filter that renders into clickable pills. 87 | - [Nova Slider Filter](https://github.com/dcasia/nova-slider-filter) - A Laravel Nova filter for picking range between a min/max value. 88 | - [Nova Range Input Filter](https://github.com/dcasia/nova-range-input-filter) - A Laravel Nova range input filter. 89 | - [Nova FilePond](https://github.com/dcasia/nova-filepond) - A Nova field for uploading File, Image and Video using Filepond. 90 | - [Custom Relationship Field](https://github.com/dcasia/custom-relationship-field) - Emulate HasMany relationship without having a real relationship set between resources. 91 | - [Column Toggler](https://github.com/dcasia/column-toggler) - A Laravel Nova package that allows you to hide/show columns in the index view. 92 | - [Batch Edit Toolbar](https://github.com/dcasia/batch-edit-toolbar) - Allows you to update a single column of a resource all at once directly from the index page. 93 | 94 | ## License 95 | 96 | The MIT License (MIT). Please see [License File](https://raw.githubusercontent.com/dcasia/collapsible-resource-manager/master/LICENSE) for more information. 97 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "digital-creative/collapsible-resource-manager", 3 | "description": "A custom sidebar menu with collapsible groups", 4 | "keywords": [ 5 | "laravel", 6 | "nova", 7 | "sidebar", 8 | "collapse", 9 | "collapsible" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Rafael Milewski" 14 | } 15 | ], 16 | "license": "MIT", 17 | "require": { 18 | "php": ">=8.1", 19 | "laravel/nova": "^5.0" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "DigitalCreative\\CollapsibleResourceManager\\": "src/" 24 | } 25 | }, 26 | "extra": { 27 | "laravel": { 28 | "providers": [ 29 | "DigitalCreative\\CollapsibleResourceManager\\CollapsibleResourceManagerServiceProvider" 30 | ] 31 | } 32 | }, 33 | "config": { 34 | "sort-packages": true 35 | }, 36 | "minimum-stability": "dev", 37 | "prefer-stable": true 38 | } 39 | -------------------------------------------------------------------------------- /config/nova.php: -------------------------------------------------------------------------------- 1 | true, 7 | 'move_theme_switcher' => true, 8 | 'move_notification_center' => false, 9 | 'section_title' => true, 10 | 'collapse_on_select' => false, 11 | 'collapse_on_refresh' => false, 12 | ]; 13 | -------------------------------------------------------------------------------- /dist/css/card.css: -------------------------------------------------------------------------------- 1 | .container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}div[id^=collapsible-resource-manager] :is(.pointer-events-none){pointer-events:none}div[id^=collapsible-resource-manager] :is(.fixed){position:fixed}div[id^=collapsible-resource-manager] :is(.absolute){position:absolute}div[id^=collapsible-resource-manager] :is(.relative){position:relative}div[id^=collapsible-resource-manager] :is(.sticky){position:sticky}div[id^=collapsible-resource-manager] :is(.-top-10){top:-2.5rem}div[id^=collapsible-resource-manager] :is(.bottom-0){bottom:0}div[id^=collapsible-resource-manager] :is(.my-1){margin-bottom:.25rem;margin-top:.25rem}div[id^=collapsible-resource-manager] :is(.mb-1){margin-bottom:.25rem}div[id^=collapsible-resource-manager] :is(.ml-auto){margin-left:auto}div[id^=collapsible-resource-manager] :is(.mr-1){margin-right:.25rem}div[id^=collapsible-resource-manager] :is(.mr-2){margin-right:.5rem}div[id^=collapsible-resource-manager] :is(.mr-3){margin-right:.75rem}div[id^=collapsible-resource-manager] :is(.mt-2){margin-top:.5rem}div[id^=collapsible-resource-manager] :is(.mt-4){margin-top:1rem}div[id^=collapsible-resource-manager] :is(.flex){display:flex}div[id^=collapsible-resource-manager] :is(.hidden){display:none}div[id^=collapsible-resource-manager] :is(.h-10){height:2.5rem}div[id^=collapsible-resource-manager] :is(.h-8){height:2rem}div[id^=collapsible-resource-manager] :is(.h-\[14px\]){height:14px}div[id^=collapsible-resource-manager] :is(.h-\[40px\]){height:40px}div[id^=collapsible-resource-manager] :is(.h-full){height:100%}div[id^=collapsible-resource-manager] :is(.min-h-\[calc\(100vh-50px\)\]){min-height:calc(100vh - 50px)}div[id^=collapsible-resource-manager] :is(.min-h-\[calc\(100vh-56px\)\]){min-height:calc(100vh - 56px)}div[id^=collapsible-resource-manager] :is(.w-8){width:2rem}div[id^=collapsible-resource-manager] :is(.w-\[0px\]){width:0}div[id^=collapsible-resource-manager] :is(.w-\[320px\]){width:320px}div[id^=collapsible-resource-manager] :is(.w-\[40px\]){width:40px}div[id^=collapsible-resource-manager] :is(.w-full){width:100%}div[id^=collapsible-resource-manager] :is(.min-w-\[24px\]){min-width:24px}div[id^=collapsible-resource-manager] :is(.max-w-\[14px\]){max-width:14px}div[id^=collapsible-resource-manager] :is(.max-w-xxs){max-width:15rem}div[id^=collapsible-resource-manager] :is(.flex-1){flex:1 1 0%}div[id^=collapsible-resource-manager] :is(.rotate-90){--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}div[id^=collapsible-resource-manager] :is(.transform){transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}div[id^=collapsible-resource-manager] :is(.cursor-pointer){cursor:pointer}div[id^=collapsible-resource-manager] :is(.flex-col){flex-direction:column}div[id^=collapsible-resource-manager] :is(.items-center){align-items:center}div[id^=collapsible-resource-manager] :is(.justify-center){justify-content:center}div[id^=collapsible-resource-manager] :is(.justify-between){justify-content:space-between}div[id^=collapsible-resource-manager] :is(.space-x-2>:not([hidden])~:not([hidden])){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}div[id^=collapsible-resource-manager] :is(.space-y-1>:not([hidden])~:not([hidden])){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}div[id^=collapsible-resource-manager] :is(.space-y-2>:not([hidden])~:not([hidden])){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}div[id^=collapsible-resource-manager] :is(.overflow-hidden){overflow:hidden}div[id^=collapsible-resource-manager] :is(.overflow-x-hidden){overflow-x:hidden}div[id^=collapsible-resource-manager] :is(.overflow-y-hidden){overflow-y:hidden}div[id^=collapsible-resource-manager] :is(.whitespace-nowrap){white-space:nowrap}div[id^=collapsible-resource-manager] :is(.rounded){border-radius:.25rem}div[id^=collapsible-resource-manager] :is(.rounded-full){border-radius:9999px}div[id^=collapsible-resource-manager] :is(.border-b){border-bottom-width:1px}div[id^=collapsible-resource-manager] :is(.border-l){border-left-width:1px}div[id^=collapsible-resource-manager] :is(.border-r){border-right-width:1px}div[id^=collapsible-resource-manager] :is(.border-gray-200){border-color:rgba(var(--colors-gray-200))}div[id^=collapsible-resource-manager] :is(.border-gray-700){border-color:rgba(var(--colors-gray-700))}div[id^=collapsible-resource-manager] :is(.border-transparent){border-color:transparent}div[id^=collapsible-resource-manager] :is(.bg-\[rgba\(var\(--colors-gray-50\)\)\]){background-color:rgba(var(--colors-gray-50))}div[id^=collapsible-resource-manager] :is(.bg-gray-100){background-color:rgba(var(--colors-gray-100))}div[id^=collapsible-resource-manager] :is(.bg-gray-200){background-color:rgba(var(--colors-gray-200))}div[id^=collapsible-resource-manager] :is(.bg-gray-900){background-color:rgba(var(--colors-gray-900))}div[id^=collapsible-resource-manager] :is(.bg-white){--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}div[id^=collapsible-resource-manager] :is(.bg-gradient-to-t){background-image:linear-gradient(to top,var(--tw-gradient-stops))}div[id^=collapsible-resource-manager] :is(.from-white){--tw-gradient-from:#fff var(--tw-gradient-from-position);--tw-gradient-to:hsla(0,0%,100%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}div[id^=collapsible-resource-manager] :is(.to-transparent){--tw-gradient-to:transparent var(--tw-gradient-to-position)}div[id^=collapsible-resource-manager] :is(.p-2){padding:.5rem}div[id^=collapsible-resource-manager] :is(.px-1){padding-left:.25rem;padding-right:.25rem}div[id^=collapsible-resource-manager] :is(.px-2){padding-left:.5rem;padding-right:.5rem}div[id^=collapsible-resource-manager] :is(.px-3){padding-left:.75rem;padding-right:.75rem}div[id^=collapsible-resource-manager] :is(.px-4){padding-left:1rem;padding-right:1rem}div[id^=collapsible-resource-manager] :is(.py-1){padding-bottom:.25rem;padding-top:.25rem}div[id^=collapsible-resource-manager] :is(.py-2){padding-bottom:.5rem;padding-top:.5rem}div[id^=collapsible-resource-manager] :is(.pb-2){padding-bottom:.5rem}div[id^=collapsible-resource-manager] :is(.pb-4){padding-bottom:1rem}div[id^=collapsible-resource-manager] :is(.pt-4){padding-top:1rem}div[id^=collapsible-resource-manager] :is(.text-lg){font-size:1.125rem;line-height:1.75rem}div[id^=collapsible-resource-manager] :is(.font-bold){font-weight:700}div[id^=collapsible-resource-manager] :is(.text-gray-200){color:rgba(var(--colors-gray-200))}div[id^=collapsible-resource-manager] :is(.text-gray-500){color:rgba(var(--colors-gray-500))}div[id^=collapsible-resource-manager] :is(.text-primary-500){color:rgba(var(--colors-primary-500))}div[id^=collapsible-resource-manager] :is(.ring-gray-600){--tw-ring-color:rgba(var(--colors-gray-600))}div[id^=collapsible-resource-manager] :is(.filter){filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}div[id^=collapsible-resource-manager] :is(.transition-transform){transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}div[id^=collapsible-resource-manager] :is(.transition-width){transition-duration:.15s;transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1)}div[id^=collapsible-resource-manager] :is(.duration-300){transition-duration:.3s}div[id^=collapsible-resource-manager] :is(.hover\:bg-gray-200:hover){background-color:rgba(var(--colors-gray-200))}div[id^=collapsible-resource-manager] :is(.hover\:text-gray-400:hover){color:rgba(var(--colors-gray-400))}div[id^=collapsible-resource-manager] :is(.hover\:text-primary-500:hover){color:rgba(var(--colors-primary-500))}div[id^=collapsible-resource-manager] :is(.focus\:outline-none:focus){outline:2px solid transparent;outline-offset:2px}div[id^=collapsible-resource-manager] :is(.focus\:ring:focus){--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}div[id^=collapsible-resource-manager] :is(.focus\:ring-primary-200:focus){--tw-ring-color:rgba(var(--colors-primary-200))}div[id^=collapsible-resource-manager] :is(.active\:outline-none:active){outline:2px solid transparent;outline-offset:2px}div[id^=collapsible-resource-manager] :is(.active\:ring:active){--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}div[id^=collapsible-resource-manager] :is(.group:hover .group-hover\:block){display:block}div[id^=collapsible-resource-manager] :is(.dark\:border-r:is(.dark *)){border-right-width:1px}div[id^=collapsible-resource-manager] :is(.dark\:border-gray-700:is(.dark *)){border-color:rgba(var(--colors-gray-700))}div[id^=collapsible-resource-manager] :is(.dark\:bg-\[rgba\(var\(--colors-gray-900\)\2c \.65\)\]:is(.dark *)){background-color:rgba(var(--colors-gray-900),.65)}div[id^=collapsible-resource-manager] :is(.dark\:bg-gray-800:is(.dark *)){background-color:rgba(var(--colors-gray-800))}div[id^=collapsible-resource-manager] :is(.dark\:bg-gray-900:is(.dark *)){background-color:rgba(var(--colors-gray-900))}div[id^=collapsible-resource-manager] :is(.dark\:from-gray-800:is(.dark *)){--tw-gradient-from:rgba(var(--colors-gray-800)) var(--tw-gradient-from-position);--tw-gradient-to:rgba(var(--colors-gray-800),0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}div[id^=collapsible-resource-manager] :is(.dark\:text-gray-400:is(.dark *)){color:rgba(var(--colors-gray-400))}div[id^=collapsible-resource-manager] :is(.dark\:hover\:bg-gray-700:hover:is(.dark *)){background-color:rgba(var(--colors-gray-700))}div[id^=collapsible-resource-manager] :is(.dark\:hover\:bg-gray-800:hover:is(.dark *)){background-color:rgba(var(--colors-gray-800))}div[id^=collapsible-resource-manager] :is(.dark\:hover\:text-gray-200:hover:is(.dark *)){color:rgba(var(--colors-gray-200))}div[id^=collapsible-resource-manager] :is(.dark\:focus\:ring-gray-600:focus:is(.dark *)){--tw-ring-color:rgba(var(--colors-gray-600))}@media (min-width:768px){div[id^=collapsible-resource-manager] :is(.md\:inline){display:inline}}@media (min-width:1024px){div[id^=collapsible-resource-manager] :is(.lg\:flex){display:flex}div[id^=collapsible-resource-manager] :is(.lg\:dark\:bg-\[rgba\(var\(--colors-gray-500\)\2c \.05\)\]:is(.dark *)){background-color:rgba(var(--colors-gray-500),.05)}} 2 | -------------------------------------------------------------------------------- /dist/js/tool.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress 2 | * @license MIT */ 3 | 4 | /*! 5 | * The buffer module from node.js, for the browser. 6 | * 7 | * @author Feross Aboukhadijeh 8 | * @license MIT 9 | */ 10 | 11 | /*! 12 | * vuex v4.1.0 13 | * (c) 2022 Evan You 14 | * @license MIT 15 | */ 16 | 17 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 18 | 19 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 20 | -------------------------------------------------------------------------------- /dist/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/tool.js": "/js/tool.js", 3 | "/css/card.css": "/css/card.css" 4 | } 5 | -------------------------------------------------------------------------------- /nova.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix') 2 | const webpack = require('webpack') 3 | const path = require('path') 4 | 5 | class NovaExtension { 6 | name() { 7 | return 'nova-extension' 8 | } 9 | 10 | register(name) { 11 | this.name = name 12 | } 13 | 14 | webpackConfig(webpackConfig) { 15 | webpackConfig.externals = { 16 | vue: 'Vue', 17 | } 18 | 19 | webpackConfig.resolve.alias = { 20 | ...(webpackConfig.resolve.alias || {}), 21 | 'laravel-nova': path.join(__dirname, '../../vendor/laravel/nova/resources/js/mixins/packages.js'), 22 | 'laravel-nova-ui': path.join(__dirname, '../../vendor/laravel/nova/node_modules/laravel-nova-ui'), 23 | '@': path.resolve(__dirname, '../../vendor/laravel/nova/resources/js/'), 24 | } 25 | 26 | webpackConfig.output = { 27 | uniqueName: this.name, 28 | } 29 | } 30 | } 31 | 32 | mix.extend('nova', new NovaExtension()) 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "watch": "mix watch", 5 | "production": "mix --production", 6 | "nova:install": "npm --prefix='../../vendor/laravel/nova' ci" 7 | }, 8 | "devDependencies": { 9 | "@vue/compiler-sfc": "^3.4.27", 10 | "laravel-mix": "^6.0.41", 11 | "mix-tailwindcss": "^1.3.0", 12 | "sass": "^1.77.2", 13 | "sass-loader": "^14.2.1", 14 | "scrollbooster": "^3.0.2", 15 | "tailwindcss": "^3.4.3", 16 | "vue-collapsed": "^1.3.3", 17 | "vue-loader": "^17.4.2", 18 | "vuex": "^4.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/css/card.css: -------------------------------------------------------------------------------- 1 | @tailwind components; 2 | @tailwind utilities; 3 | -------------------------------------------------------------------------------- /resources/js/components/CollapseButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /resources/js/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 347 | 348 | 421 | -------------------------------------------------------------------------------- /resources/js/components/MenuGroup.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | -------------------------------------------------------------------------------- /resources/js/components/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 62 | -------------------------------------------------------------------------------- /resources/js/components/MenuSection.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /resources/js/components/Noop.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/js/components/SectionHeader.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 42 | -------------------------------------------------------------------------------- /resources/js/components/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /resources/js/lib/ClickOutsideDirective.js: -------------------------------------------------------------------------------- 1 | export default { 2 | mounted: function (element, binding) { 3 | 4 | element.clickOutsideEvent = function (event) { 5 | 6 | if (!(element === event.target || element.contains(event.target))) { 7 | binding.value(event, element) 8 | } 9 | 10 | } 11 | 12 | document.addEventListener('click', element.clickOutsideEvent) 13 | 14 | }, 15 | unmounted: element => { 16 | document.removeEventListener('click', element.clickOutsideEvent) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/mixins/MenuMixin.js: -------------------------------------------------------------------------------- 1 | import { mapState, mapMutations } from 'vuex' 2 | 3 | export default { 4 | computed: { 5 | ...mapState([ 'mainMenuShown' ]), 6 | config() { 7 | return Nova.config('collapsible_resource_manager') 8 | }, 9 | }, 10 | methods: { 11 | ...mapMutations([ 'toggleMainMenu' ]), 12 | restoreFromLocalStorage() { 13 | const storage = JSON.parse(sessionStorage.getItem('nova.collapsibleResourceManager')) || {} 14 | 15 | this.currentActiveMenu = storage.currentActiveMenu 16 | this.currentActiveSection = storage.currentActiveSection 17 | 18 | if (this.isDesktop) { 19 | this.$store.state.mainMenuShown = storage.mainMenuShown 20 | } 21 | }, 22 | saveToLocalStorage() { 23 | const data = { 24 | currentActiveMenu: this.currentActiveMenu, 25 | currentActiveSection: this.currentActiveSection, 26 | mainMenuShown: this.$store.state.mainMenuShown, 27 | } 28 | 29 | sessionStorage.setItem('nova.collapsibleResourceManager', JSON.stringify(data)) 30 | }, 31 | collapseMenu() { 32 | this.$store.state.mainMenuShown = false 33 | }, 34 | openMenu() { 35 | this.$store.state.mainMenuShown = true 36 | }, 37 | onClickOutside() { 38 | if (this.config.auto_collapse_desktop_menu && this.currentActiveMenu && this.isDesktop) { 39 | this.collapseMenu() 40 | } 41 | }, 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /resources/js/native/UserMenu.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 98 | -------------------------------------------------------------------------------- /resources/js/tool.js: -------------------------------------------------------------------------------- 1 | import Menu from './components/Menu.vue' 2 | import Noop from './components/Noop.vue' 3 | import { createVNode, render, nextTick } from 'vue' 4 | 5 | function getNovaVersion() { 6 | const [ version ] = Nova.config('version').replaceAll('.', '').trim().match(/^\d+/) 7 | return parseFloat(version.length < 5 ? version.padEnd(5, 0) : version) 8 | } 9 | 10 | const version = getNovaVersion() 11 | const config = Nova.config('collapsible_resource_manager') 12 | 13 | const settings = { 14 | UserMenu: config.move_user_menu, 15 | NotificationCenter: config.move_notification_center, 16 | ThemeDropdown: config.move_theme_switcher, 17 | } 18 | 19 | Nova.booting(app => { 20 | 21 | const components = { 22 | NotificationCenter: null, 23 | UserMenu: null, 24 | ThemeDropdown: null, 25 | MainMenu: null, 26 | } 27 | 28 | const componentFn = app.component 29 | 30 | app.component = function (name, component) { 31 | 32 | /** 33 | * Here we grab these components to render later into a different place 34 | */ 35 | if ([ 'NotificationCenter', 'UserMenu', 'ThemeDropdown', 'MainMenu' ].includes(name)) { 36 | 37 | for (const key in settings) { 38 | 39 | if (key === name && settings[ key ] === false) { 40 | return componentFn.call(this, name, component) 41 | } 42 | 43 | } 44 | 45 | components[ name ] = component 46 | 47 | return componentFn.call(this, name, Noop) 48 | 49 | } 50 | 51 | return componentFn.call(this, name, component) 52 | 53 | } 54 | 55 | app.mixin({ 56 | data() { 57 | return { 58 | toDestroy: [], 59 | } 60 | }, 61 | async mounted() { 62 | 63 | if (this._.type?.__file?.endsWith('GlobalSearch.vue')) { 64 | 65 | if (settings.UserMenu && settings.NotificationCenter && settings.ThemeDropdown) { 66 | this._.vnode.el.classList.add('handle-global-search-component') 67 | } 68 | } 69 | 70 | /* 71 | if (this._.type?.__file?.endsWith('AppLogo.vue')) { 72 | const element = this._.vnode.el.parentElement.parentElement; 73 | const container = document.createElement('div') 74 | container.className = 'hidden md:inline' 75 | container.id = `collapsible-resource-manager-collapse-button` 76 | 77 | element.insertAdjacentElement('afterend', container) 78 | this.toDestroy.push(container) 79 | } 80 | */ 81 | 82 | if (this._.type?.name === 'Noop') { 83 | 84 | const screen = this._.attrs[ 'data-screen' ] 85 | const mobile = this._.attrs[ 'mobile' ] 86 | 87 | if (mobile) { 88 | this._.vnode.el?.parentElement?.classList?.add('hidden') 89 | } 90 | 91 | if (screen === undefined) { 92 | return 93 | } 94 | 95 | let count = 10 96 | 97 | /** 98 | * Make sure we are running this code in the last tick 99 | */ 100 | while (count--) { 101 | await nextTick() 102 | } 103 | 104 | const container = document.createElement('div') 105 | container.id = `collapsible-resource-manager-${ screen }` 106 | 107 | let element = null 108 | 109 | if (screen === 'desktop') { 110 | element = document.querySelector('#nova div[dusk="content"] > div:first-child') 111 | } 112 | 113 | if (screen === 'responsive') { 114 | 115 | if (version <= 42713) { 116 | element = this._.vnode.el 117 | } else { 118 | element = this._.vnode.el.parentElement.parentElement 119 | } 120 | 121 | } 122 | 123 | if (element) { 124 | 125 | element.replaceWith(container) 126 | 127 | const vnode = createVNode(Menu, { screen, ...components }) 128 | vnode.appContext = app._context 129 | 130 | render(vnode, container) 131 | this.toDestroy.push(container) 132 | 133 | } 134 | 135 | } 136 | 137 | }, 138 | unmounted() { 139 | 140 | for (const element of this.toDestroy) { 141 | render(null, element) 142 | } 143 | 144 | }, 145 | }) 146 | 147 | }) 148 | -------------------------------------------------------------------------------- /screenshots/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcasia/collapsible-resource-manager/a1ad74e4b4c2ace3ab51f652ee42f0d8e22b86a6/screenshots/dark.png -------------------------------------------------------------------------------- /screenshots/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcasia/collapsible-resource-manager/a1ad74e4b4c2ace3ab51f652ee42f0d8e22b86a6/screenshots/light.png -------------------------------------------------------------------------------- /src/CollapsibleResourceManagerServiceProvider.php: -------------------------------------------------------------------------------- 1 | data(array_merge($this->data ?? [], [ 'icon' => $value ])); 18 | }); 19 | 20 | Nova::serving(function (ServingNova $event): void { 21 | 22 | Nova::provideToScript([ 23 | 'collapsible_resource_manager' => config('nova.vendors.collapsible_resource_manager'), 24 | ]); 25 | 26 | Nova::script('collapsible-resource-manager', __DIR__ . '/../dist/js/tool.js'); 27 | Nova::style('collapsible-resource-manager', __DIR__ . '/../dist/css/card.css'); 28 | 29 | }); 30 | } 31 | 32 | public function register(): void 33 | { 34 | $this->mergeConfigFrom( 35 | __DIR__ . '/../config/nova.php', 'nova.vendors.collapsible_resource_manager', 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { theme, ...theRest } = require('../../vendor/laravel/nova/tailwind.config') 2 | 3 | module.exports = { 4 | ...theRest, 5 | theme: { 6 | ...theme, 7 | extend: { 8 | ...theme.extend, 9 | transitionProperty: { 10 | width: 'width', 11 | }, 12 | }, 13 | }, 14 | important: 'div[id^="collapsible-resource-manager"]', 15 | } 16 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix') 2 | 3 | require('./nova.mix') 4 | require('mix-tailwindcss') 5 | 6 | mix 7 | .setPublicPath('dist') 8 | .js('resources/js/tool.js', 'js') 9 | .vue({ version: 3 }) 10 | .postCss('resources/css/card.css', 'css') 11 | .tailwind() 12 | .nova('digital-creative/collapsible-resource-manager') 13 | --------------------------------------------------------------------------------