├── resources ├── views │ └── components │ │ ├── divider.blade.php │ │ ├── tooltip.blade.php │ │ ├── table-cell.blade.php │ │ ├── tab-panel.blade.php │ │ ├── card-stack.blade.php │ │ ├── table-row.blade.php │ │ ├── button-group.blade.php │ │ ├── circle-loader.blade.php │ │ ├── box.blade.php │ │ ├── navigation-section.blade.php │ │ ├── popover.blade.php │ │ ├── textarea.blade.php │ │ ├── inline-error.blade.php │ │ ├── select.blade.php │ │ ├── sub-navigation-item.blade.php │ │ ├── tabs.blade.php │ │ ├── callout-card.blade.php │ │ ├── thumbnail.blade.php │ │ ├── dropzone.blade.php │ │ ├── empty-state.blade.php │ │ ├── copy-to-clip.blade.php │ │ ├── navigation.blade.php │ │ ├── tag.blade.php │ │ ├── expandable-search.blade.php │ │ ├── search-input.blade.php │ │ ├── input.blade.php │ │ ├── navigation-item.blade.php │ │ ├── avatar.blade.php │ │ ├── card.blade.php │ │ ├── index-table.blade.php │ │ ├── page-header.blade.php │ │ ├── flash-message.blade.php │ │ ├── action-link.blade.php │ │ ├── media-card.blade.php │ │ ├── pagination.blade.php │ │ ├── button.blade.php │ │ ├── top-bar.blade.php │ │ ├── autocomplete.blade.php │ │ ├── badge.blade.php │ │ ├── modal.blade.php │ │ ├── combobox.blade.php │ │ ├── select-auto.blade.php │ │ ├── alert.blade.php │ │ └── banner.blade.php ├── js │ ├── components │ │ ├── _1_alert.js │ │ ├── _1_expandable-search.js │ │ ├── _2_copy-to-clip.js │ │ ├── _1_file-upload.js │ │ ├── _1_flash-message.js │ │ ├── _1_list-filter.js │ │ ├── _1_swipe-content.js │ │ ├── _2_multiple-custom-select.js │ │ ├── _1_tabs.js │ │ ├── _1_modal-window.js │ │ ├── _1_responsive-sidebar.js │ │ ├── _3_select-autocomplete.js │ │ └── _1_tooltip.js │ ├── dashui.js │ └── _util.js └── css │ └── components │ ├── _1_popover.css │ ├── _2_autocomplete.css │ ├── _1_tooltip.css │ ├── _1_list-filter.css │ ├── _3_select-autocomplete.css │ ├── _1_custom-checkbox.css │ ├── _1_expandable-search.css │ ├── _1_radios-checkboxes.css │ ├── _1_responsive-sidebar.css │ └── _1_modal-window.css ├── src ├── Components │ ├── Divider.php │ ├── Popover.php │ ├── Tooltip.php │ ├── CardStack.php │ ├── TableCell.php │ ├── CalloutCard.php │ ├── CopyToClip.php │ ├── EmptyState.php │ ├── Navigation.php │ ├── CircleLoader.php │ ├── Tabs.php │ ├── TabPanel.php │ ├── InlineError.php │ ├── Tag.php │ ├── ButtonGroup.php │ ├── Alert.php │ ├── ExpandableSearch.php │ ├── Badge.php │ ├── Banner.php │ ├── MediaCard.php │ ├── TableRow.php │ ├── Autocomplete.php │ ├── Select.php │ ├── IndexTable.php │ ├── Textarea.php │ ├── Box.php │ ├── NavigationSection.php │ ├── Dropzone.php │ ├── Modal.php │ ├── Card.php │ ├── Thumbnail.php │ ├── FlashMessage.php │ ├── PageHeader.php │ ├── Pagination.php │ ├── SearchInput.php │ ├── SelectAuto.php │ ├── Avatar.php │ ├── Input.php │ ├── SubNavigationItem.php │ ├── ActionLink.php │ ├── TopBar.php │ ├── Combobox.php │ ├── Button.php │ └── NavigationItem.php └── DashUiServiceProvider.php ├── LICENSE.md ├── composer.json └── README.md /resources/views/components/divider.blade.php: -------------------------------------------------------------------------------- 1 |
class(['border-1 border-[rgba(0,0,0,0.08)]']) }}> 2 | -------------------------------------------------------------------------------- /resources/views/components/tooltip.blade.php: -------------------------------------------------------------------------------- 1 | class(['whitespace-nowrap js-tooltip-trigger']) }}>{{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/components/table-cell.blade.php: -------------------------------------------------------------------------------- 1 | class('whitespace-nowrap px-3 py-1.5 text-sm leading-none') }}>{{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/components/tab-panel.blade.php: -------------------------------------------------------------------------------- 1 |
class(['pt-3 lg:pt-5 js-tabs__panel']) }}> 2 | {{ $slot }} 3 |
4 | -------------------------------------------------------------------------------- /resources/views/components/card-stack.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
class(['bg-white']) }}> 3 | {{ $slot }} 4 |
5 |
6 | -------------------------------------------------------------------------------- /resources/views/components/table-row.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/components/button-group.blade.php: -------------------------------------------------------------------------------- 1 |
class([ 2 | 'flex flex-wrap items-center', 3 | 'gap-y-2 gap-x-2' => ($variant == 'basic'), 4 | 'gap-0' => ($variant == 'segmented'), 5 | ]) }}> 6 | {{ $slot }} 7 |
8 | -------------------------------------------------------------------------------- /resources/views/components/circle-loader.blade.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/Components/Divider.php: -------------------------------------------------------------------------------- 1 | class([ 2 | 'overflow-hidden relative z-1', 3 | 'shadow-[0px_0px_0px_1px_rgba(0,0,0,0.08)_inset]' => $border, 4 | 'shadow-[0px_4px_6px_-2px_rgba(26,26,26,0.20)]' => $shadow, 5 | 'rounded-[6px] lg:rounded-[12px]' => $rounded, 6 | ]) }}> 7 | {{ $slot }} 8 | 9 | -------------------------------------------------------------------------------- /src/Components/Tabs.php: -------------------------------------------------------------------------------- 1 | !$sticky, 'sticky bottom-5 mt-auto' => $sticky])> 2 | @if($title) 3 |
  • 4 | {{ $title }} 5 |
  • 6 | @endif 7 | {{ $slot }} 8 | 9 | -------------------------------------------------------------------------------- /src/Components/TabPanel.php: -------------------------------------------------------------------------------- 1 | class(['popover my-1 w-auto max-w-[fit-content] border-box fixed z-5 js-popover js-tab-focus']) }}> 2 |
    3 |
    4 |
    5 | {{ $slot }} 6 |
    7 |
    8 |
    9 | 10 | -------------------------------------------------------------------------------- /src/Components/Box.php: -------------------------------------------------------------------------------- 1 | class([ 2 | 'block w-full box-border rounded-lg text-sm border-0 py-1 mb-1.5 text-neutral-900 ring-1 ring-inset placeholder:text-neutral-500 focus:ring-2 focus:ring-inset focus:ring-primary-700', 3 | 'ring-neutral-500/90' => !$error, 4 | 'bg-red-100/50 ring-red-800' => $error, 5 | ]) }}>{{ $slot }} 6 | @if($error) 7 | 8 | @elseif($helpText) 9 |

    {{ $helpText }}

    10 | @endif 11 | -------------------------------------------------------------------------------- /src/Components/ActionLink.php: -------------------------------------------------------------------------------- 1 | class(['inline-flex items-center justify-between gap-x-1 text-red-800 mt-0.5']) }}> 2 | 3 | {{ $message }} 4 | 5 | -------------------------------------------------------------------------------- /resources/views/components/select.blade.php: -------------------------------------------------------------------------------- 1 | 8 | @if($error) 9 | 10 | @elseif($helpText) 11 |

    {{ $helpText }}

    12 | @endif 13 | 14 | -------------------------------------------------------------------------------- /src/Components/Combobox.php: -------------------------------------------------------------------------------- 1 | 2 |
    $selected, 5 | 'hover:bg-neutral-50/50 active:bg-white' => !$selected, 6 | ])> 7 | 8 | {{ $label }} 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /resources/js/components/_1_alert.js: -------------------------------------------------------------------------------- 1 | // File#: _1_alert 2 | // Usage: codyhouse.co/license 3 | (function() { 4 | var alertClose = document.getElementsByClassName('js-alert__close-btn'); 5 | if( alertClose.length > 0 ) { 6 | for( var i = 0; i < alertClose.length; i++) { 7 | (function(i){initAlertEvent(alertClose[i]);})(i); 8 | } 9 | }; 10 | }()); 11 | 12 | function initAlertEvent(element) { 13 | element.addEventListener('click', function(event){ 14 | event.preventDefault(); 15 | element.closest('.js-alert').classList.remove('alert--is-visible'); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /resources/views/components/tabs.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
      class(['flex flex-wrap gap-1 js-tabs__controls'])}} aria-label="Tabs Interface"> 3 | @foreach($tabs as $tab) 4 |
    • {{ $tab['label'] }}
    • 5 | @endforeach 6 |
    7 |
    8 | {{ $slot }} 9 |
    10 |
    11 | -------------------------------------------------------------------------------- /src/Components/Button.php: -------------------------------------------------------------------------------- 1 | 2 |
    class(['card__inner bg-white flex items-center']) }}> 3 |
    4 | @if(isset($heading)) 5 |
    attributes->class(['mb-2 text-sm font-semibold']) }}> 6 | {{ $heading }} 7 |
    8 | @endif 9 |
    {{ $slot }}
    10 |
    11 | @if(isset($illustration)) 12 |
    attributes->class(['hidden shrink-0 grow-0 max-w-[120px] lg:block lg:ml-6']) }}">{{ $illustration }}
    13 | @endif 14 |
    15 | 16 | -------------------------------------------------------------------------------- /resources/views/components/thumbnail.blade.php: -------------------------------------------------------------------------------- 1 | class([ 2 | 'inline-flex bg-white overflow-hidden relative', 3 | 'w-6 h-6 rounded-xs' => ($size == 'xs'), 4 | 'w-10 h-10 rounded-md' => ($size == 'sm'), 5 | 'w-14 h-14 rounded-lg' => ($size == 'md'), 6 | 'w-20 h-20 rounded-lg' => ($size == 'lg'), 7 | ]) }}> 8 | @if($src) 9 | {{ $alt??'image' }} 10 | @else 11 | ($size == 'xs'), 14 | 'p-2.5' => ($size == 'sm'), 15 | 'p-4' => ($size == 'md'), 16 | 'p-6' => ($size == 'lg'), 17 | ])>{{ $slot }} 18 | @endif 19 | 20 | -------------------------------------------------------------------------------- /resources/views/components/dropzone.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 8 | 9 | class(['file-upload__input absolute w-px h-px']) }} style="clip: rect(1px, 1px, 1px, 1px); clip-path: inset(50%);"> 10 |
    11 | -------------------------------------------------------------------------------- /resources/views/components/empty-state.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    class(['card__inner bg-white flex flex-col justify-center items-center text-sm text-center lg:py-24']) }}> 3 | @if(isset($illustration)) 4 |
    attributes->class(['flex justify-center max-w-xs mb-6']) }}">{{ $illustration }}
    5 | @endif 6 | 7 |
    8 | @if(isset($heading)) 9 |
    attributes->class(['mb-2 text-sm font-bold']) }}>{{ $heading }}
    10 | @endif 11 | 12 |
    {{ $slot }}
    13 |
    14 |
    15 |
    16 | -------------------------------------------------------------------------------- /resources/css/components/_1_popover.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------- 2 | 3 | File#: _1_popover 4 | Title: Popover 5 | Descr: A pop-up box controlled by a trigger element 6 | Usage: codyhouse.co/license 7 | 8 | -------------------------------- */ 9 | :root { 10 | --popover-viewport-gap: 20px; 11 | --popover-transition-duration: 0.2s; 12 | } 13 | 14 | .popover { 15 | -webkit-overflow-scrolling: touch; 16 | visibility: hidden; 17 | opacity: 0; 18 | transition: visibility 0s var(--popover-transition-duration), opacity var(--popover-transition-duration); 19 | } 20 | 21 | .popover--is-visible { 22 | visibility: visible; 23 | opacity: 1; 24 | transition: visibility 0s, opacity var(--popover-transition-duration); 25 | } 26 | -------------------------------------------------------------------------------- /resources/js/components/_1_expandable-search.js: -------------------------------------------------------------------------------- 1 | // File#: _1_expandable-search 2 | // Usage: codyhouse.co/license 3 | (function() { 4 | var expandableSearch = document.getElementsByClassName('js-expandable-search'); 5 | if(expandableSearch.length > 0) { 6 | for( var i = 0; i < expandableSearch.length; i++) { 7 | (function(i){ // if user types in search input, keep the input expanded when focus is lost 8 | expandableSearch[i].getElementsByClassName('js-expandable-search__input')[0].addEventListener('input', function(event){ 9 | event.target.classList.toggle('expandable-search__input--has-content', event.target.value.length > 0); 10 | }); 11 | })(i); 12 | } 13 | } 14 | }()); 15 | -------------------------------------------------------------------------------- /resources/views/components/copy-to-clip.blade.php: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/navigation.blade.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | -------------------------------------------------------------------------------- /resources/views/components/tag.blade.php: -------------------------------------------------------------------------------- 1 | class(['inline-flex items-center gap-x-1 min-h-2 bg-neutral-200 rounded-lg pl-1.5 py-px']) }}> 2 | {{ $name }} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/views/components/expandable-search.blade.php: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /resources/views/components/search-input.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | class([ 3 | 'search-input block w-full appearance-none box-border rounded-lg text-sm border-0 py-1.5 mb-1.5 text-neutral-900 ring-1 ring-inset placeholder:text-neutral-500 focus:ring-2 focus:ring-inset focus:ring-primary-700', 4 | 'ring-neutral-500/90' => !$error, 5 | 'bg-red-100/50 ring-red-800' => $error, 6 | 'pr-7' => !$iconLeft, 7 | 'pl-8' => $iconLeft, 8 | ]) }}> 9 | 16 |
    17 | -------------------------------------------------------------------------------- /resources/views/components/input.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | @if($prefix) 3 |
    4 | {{ $prefix }} 5 |
    6 | @endif 7 | class([ 8 | 'block w-full box-border rounded-lg border-0 py-1 mb-1.5 text-sm text-neutral-900 ring-1 ring-inset placeholder:text-sm placeholder:text-neutral-500 focus:ring-2 focus:ring-inset focus:ring-primary-700', 9 | 'pl-7' => $prefix, 10 | 'pr-12' => $suffix, 11 | 'ring-neutral-500/90' => !$error, 12 | 'bg-red-100/50 ring-red-800' => $error, 13 | ]) }}> 14 | @if($suffix) 15 |
    16 | {{ $suffix }} 17 |
    18 | @endif 19 |
    20 | @if($error) 21 | 22 | @elseif($helpText) 23 |

    {{ $helpText }}

    24 | @endif 25 | 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Combind 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /resources/views/components/navigation-item.blade.php: -------------------------------------------------------------------------------- 1 |
  • 2 |
    $selected, 5 | 'hover:bg-neutral-50/50 active:bg-white font-[550]' => !$selected, 6 | ])> 7 | $childSelected])> 8 | {{ $icon }} 9 | {{ $label }} 10 | @if($badge) 11 | {{ $badge }} 12 | @endif 13 | 14 | @if(isset($action)) 15 |
    {{ $action }}
    16 | @endif 17 |
    18 | @if(isset($subNavigation)) 19 |
      !$open, 'sub-navigation--active' => $childSelected])> 20 | {{ $subNavigation }} 21 |
    22 | @endif 23 |
  • 24 | -------------------------------------------------------------------------------- /resources/views/components/avatar.blade.php: -------------------------------------------------------------------------------- 1 | class([ 2 | 'inline-flex justify-center items-center overflow-hidden relative', 3 | 'w-5 h-5 rounded-[.25rem] text-[7px]' => ($size == 'xs'), 4 | 'w-6 h-6 rounded-[.375rem] text-[9px]' => ($size == 'sm'), 5 | 'w-7 h-7 rounded-[.375rem] text-[11px]' => ($size == 'md'), 6 | 'w-8 h-8 rounded-[0.5rem] text-[13px]' => ($size == 'lg'), 7 | 'w-9 h-9 rounded-[.5rem] text-[16px]' => ($size == 'xl'), 8 | 'bg-[#b5b5b5]' => !$initials, 9 | 'bg-primary-800 text-white' => $initials, 10 | 'p-[1px]' => !$src 11 | ]) }}> 12 | @if($src) 13 | {{ $name }} 14 | @elseif($initials) 15 | {{ strtoupper($initials) }} 16 | @else 17 | 18 | @endif 19 | 20 | -------------------------------------------------------------------------------- /resources/views/components/card.blade.php: -------------------------------------------------------------------------------- 1 |
    !$reset])> 2 |
    class([ 3 | 'card__inner', 4 | 'bg-white' => ($variant == 'basic'), 5 | 'bg-[rgba(247,247,247,1)]' => ($variant == 'subdued') 6 | ]) }}> 7 | @if(isset($heading)) 8 |
    9 |
    attributes->class(['flex justify-between items-center']) }}> 10 |
    11 | {{ $heading }} 12 |
    13 | @if(isset($actions)) 14 |
    {{ $actions }}
    15 | @endif 16 |
    17 | @if(isset($subheading)) 18 |
    attributes->class(['mt-2 text-neutral-600']) }}>{{ $subheading }}
    19 | @endif 20 |
    21 | @endif 22 | 23 | @if($hasDivider) 24 | 25 | @endif 26 | 27 |
    {{ $slot }}
    28 | 29 | @if(isset($footer)) 30 |
    attributes->class(['mt-4 flex gap-2 justify-end']) }}> 31 | {{ $footer }} 32 |
    33 | @endif 34 |
    35 |
    36 | -------------------------------------------------------------------------------- /resources/views/components/index-table.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | @if(isset($filter)) 3 |
    attributes->class('flex items-center justify-between border-b border-neutral-200 bg-white px-3 py-1.5') }}> 4 | {{ $filter }} 5 |
    6 | @endif 7 |
    8 | 9 | 10 | 11 | @foreach($headings as $column) 12 | 18 | @endforeach 19 | 20 | 21 | 22 | {{ $slot }} 23 | 24 |
    !array_key_exists('alignment',$column), 15 | 'text-center' => array_key_exists('alignment',$column) && $column['alignment'] == 'center', 16 | 'text-right' => array_key_exists('alignment',$column) && $column['alignment'] == 'end', 17 | ])>{{ $column['title'] }}
    25 |
    26 | @if(isset($pagination)) 27 |
    attributes->class('border-t border-neutral-200 bg-gray-100') }}>{{ $pagination }}
    28 | @endif 29 |
    30 | -------------------------------------------------------------------------------- /resources/views/components/page-header.blade.php: -------------------------------------------------------------------------------- 1 |
    class(['mb-4 md:flex md:items-baseline md:justify-between']) }}> 2 |
    3 |
    4 |
    5 | @if($backAction) 6 | 7 | 8 | 9 | @endif 10 |

    {{ $title }}

    11 |
    12 | @if(isset($titleMetadata)) 13 |
    attributes->class(['flex flex-wrap items-center gap-x-1']) }}> 14 | {{ $titleMetadata }} 15 |
    16 | @endif 17 |
    18 | @if($subtitle) 19 |

    $backAction])>{{ $subtitle }}

    20 | @endif 21 |
    22 |
    23 | {{ $slot }} 24 |
    25 |
    26 | -------------------------------------------------------------------------------- /resources/views/components/flash-message.blade.php: -------------------------------------------------------------------------------- 1 |
    class([ 2 | 'fixed bottom-6 left-2/4 -translate-x-1/2 z-10 py-2.5 pl-4 pr-2 rounded-lg transition-all duration-200 translate-y-4 origin-bottom-center opacity-0 invisible shadow-[0_0_0_1px_hsl(221_39%_11%/0.05),0_0.3px_0.4px_hsl(221_39%_11%/0.02),0_0.9px_1.5px_hsl(221_39%_11%/0.045),0_3.5px_6px_hsl(221_39%_11%/0.09)] [&.flash-message--is-visible]:opacity-100 [&.flash-message--is-visible]:visible [&.flash-message--is-visible]:translate-y-0 js-flash-message', 3 | 'bg-primary-800 text-white' => !$error, 4 | 'bg-red-600 text-white' => $error, 5 | 'js-auto-flash-message' => $show, 6 | ]) }}> 7 |
    8 |

    {{ $message }}

    9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 |
    19 | -------------------------------------------------------------------------------- /resources/views/components/action-link.blade.php: -------------------------------------------------------------------------------- 1 | @if($as === 'link') 2 | class([ 3 | 'text-left block px-2 py-1.5 rounded-sm lg:rounded-md hover:cursor-pointer', 4 | 'hover:bg-neutral-100 active:bg-neutral-200/70' => !$active && !$destructive, 5 | 'bg-neutral-200/70' => $active, 6 | 'text-red-800 hover:bg-red-100/80 active:bg-red-200/60' => $destructive, 7 | ]) }}> 8 | 9 | @if(isset($icon)) 10 | {{ $icon }} 11 | @endif 12 | {{ $label }} 13 | @if(isset($suffix)) 14 | {{ $suffix }} 15 | @endif 16 | 17 | @if(isset($helpText)) 18 | {{ $helpText }} 19 | @endif 20 | 21 | @else 22 | 41 | @endif 42 | -------------------------------------------------------------------------------- /resources/views/components/media-card.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    class([ 3 | 'bg-white', 4 | 'grid gap-1 grid-cols-1 lg:grid-cols-12 lg:gap-2' => !$portrait, 5 | ]) }}> 6 |
    !$portrait])> 7 |
    attributes->class(['aspect-w-4 aspect-h-3']) }}>{{ $media }}
    8 |
    9 |
    !$portrait])> 10 |
    11 | @if(isset($heading)) 12 |
    13 |
    14 |
    attributes->class(['text-sm font-semibold']) }}> 15 | {{ $heading }} 16 |
    17 | @if(isset($actions)) 18 | 19 | 20 | 21 | 22 | {{ $actions }} 23 | 24 | @endif 25 |
    26 |
    27 | @endif 28 |
    {{ $slot }}
    29 |
    30 |
    31 |
    32 |
    33 | -------------------------------------------------------------------------------- /resources/views/components/pagination.blade.php: -------------------------------------------------------------------------------- 1 |
    class(['px-3 py-2 flex items-center justify-between']) }}> 2 |
    {{ $label }}
    3 | 4 | @if($previous) 5 | 6 | 7 | 8 | @else 9 | 10 | 11 | 12 | @endif 13 | 14 | @if($next) 15 | 16 | 17 | 18 | @else 19 | 20 | 21 | 22 | @endif 23 | 24 |
    25 | -------------------------------------------------------------------------------- /resources/views/components/button.blade.php: -------------------------------------------------------------------------------- 1 | @if($as === 'link') 2 | class([ 4 | 'btn', 5 | 'group' => (!$pressed && !$disabled && ($variant !== 'plain') && ($variant !== 'subtle')), 6 | 'btn--primary' => ($variant === 'primary'), 7 | 'btn--primary-critical' => ($variant === 'primary' && $tone === 'critical'), 8 | 'btn--primary-success' => ($variant === 'primary' && $tone === 'success'), 9 | 'btn--secondary' => ($variant === 'secondary'), 10 | 'btn--subtle' => ($variant === 'subtle'), 11 | 'btn--plain' => ($variant === 'plain'), 12 | 'btn--plain-critical' => ($variant === 'plain' && $tone === 'critical'), 13 | 'btn--pressed' => $pressed, 14 | 'py-3 text-sm' => ($size === 'large'), 15 | 'w-full justify-center' => $fullWidth, 16 | ]) }}> 17 |
    !$disabled])>{{ $slot }}
    18 |
    19 | @else 20 | 37 | @endif 38 | -------------------------------------------------------------------------------- /resources/views/components/top-bar.blade.php: -------------------------------------------------------------------------------- 1 |
    class(['top-bar fixed top-0 left-0 w-full overflow-hidden px-3 shadow-xs h-full bg-primary-900 z-4 lg:px-4']) }}> 2 |
    3 |
    4 | 7 |
    8 | 13 |
    14 | {{ $searchField }} 15 |
    16 |
    17 | 25 | 26 | {{ $userMenu }} 27 | 28 |
    29 |
    30 |
    31 | -------------------------------------------------------------------------------- /resources/views/components/autocomplete.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | class(['js-autocomplete__input']) }} /> 4 | 5 | 10 |
    11 | 12 | 13 |
    14 |
      15 | 16 |
    17 |
    18 | 19 |

    0 results found.

    20 |
    21 | 22 | 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "combindma/dash-ui", 3 | "description": "A streamlined and stylish UI component library for Laravel Blade, crafted with TailwindCSS and AlpineJs for simplicity and elegance.", 4 | "keywords": [ 5 | "Combind", 6 | "laravel", 7 | "dash-ui" 8 | ], 9 | "homepage": "https://github.com/combindma/dash-ui", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Combind", 14 | "email": "webmaster@combind.ma", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.3", 20 | "illuminate/contracts": "^12.0", 21 | "spatie/laravel-package-tools": "^1.14.0" 22 | }, 23 | "require-dev": { 24 | "laravel/pint": "^1.0", 25 | "nunomaduro/collision": "^8.0", 26 | "orchestra/testbench": "^10.0", 27 | "pestphp/pest": "^3.00", 28 | "pestphp/pest-plugin-arch": "^3.0", 29 | "pestphp/pest-plugin-laravel": "^3.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Combindma\\DashUi\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Combindma\\DashUi\\Tests\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "post-autoload-dump": "@composer run prepare", 43 | "clear": "@php vendor/bin/testbench package:purge-dash-ui --ansi", 44 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 45 | "build": [ 46 | "@composer run prepare", 47 | "@php vendor/bin/testbench workbench:build --ansi" 48 | ], 49 | "start": [ 50 | "Composer\\Config::disableProcessTimeout", 51 | "@composer run build", 52 | "@php vendor/bin/testbench serve" 53 | ], 54 | "analyse": "vendor/bin/phpstan analyse", 55 | "test": "vendor/bin/pest", 56 | "test-coverage": "vendor/bin/pest --coverage", 57 | "format": "vendor/bin/pint" 58 | }, 59 | "config": { 60 | "sort-packages": true, 61 | "allow-plugins": { 62 | "pestphp/pest-plugin": true, 63 | "phpstan/extension-installer": true 64 | } 65 | }, 66 | "extra": { 67 | "laravel": { 68 | "providers": [ 69 | "Combindma\\DashUi\\DashUiServiceProvider" 70 | ] 71 | } 72 | }, 73 | "minimum-stability": "dev", 74 | "prefer-stable": true 75 | } 76 | -------------------------------------------------------------------------------- /resources/js/components/_2_copy-to-clip.js: -------------------------------------------------------------------------------- 1 | // File#: _2_copy-to-clip 2 | // Usage: codyhouse.co/license 3 | (function() { 4 | var CopyClipboard = function() { 5 | this.copyTargetClass = 'js-copy-to-clip'; 6 | this.copyStatusClass = 'copy-to-clip--copied'; 7 | this.resetDelay = 2000; // delay for removing the copy-to-clip--copied class 8 | initCopyToClipboard(this); 9 | }; 10 | 11 | function initCopyToClipboard(element) { 12 | document.addEventListener('click', function(event) { 13 | var target = event.target.closest('.'+element.copyTargetClass); 14 | if(!target) return; 15 | copyContentToClipboard(element, target); 16 | }); 17 | }; 18 | 19 | function copyContentToClipboard(element, target) { 20 | // copy to clipboard 21 | navigator.clipboard.writeText(getContentToCopy(target)).then(function() { // content successfully copied 22 | // add success class to target 23 | target.classList.add(element.copyStatusClass); 24 | 25 | setTimeout(function(){ // remove success class from target 26 | target.classList.remove(element.copyStatusClass); 27 | }, element.resetDelay); 28 | 29 | // change tooltip content 30 | var newTitle = target.getAttribute('data-success-title'); 31 | if(newTitle && newTitle != '') target.dispatchEvent(new CustomEvent("newContent", {detail: newTitle})); 32 | 33 | // dispatch success event 34 | target.dispatchEvent(new CustomEvent("contentCopied")); 35 | }, function() { // error while copying the code 36 | // dispatch error event 37 | target.dispatchEvent(new CustomEvent("contentNotCopied")); 38 | }); 39 | }; 40 | 41 | function getContentToCopy(target) { 42 | var content = target.innerText; 43 | var ariaControls = document.getElementById(target.getAttribute('aria-controls')), 44 | defaultText = target.getAttribute('data-clipboard-content'); 45 | if(ariaControls) { 46 | content = ariaControls.innerText; 47 | } else if(defaultText && defaultText != '') { 48 | content = defaultText; 49 | } 50 | return content; 51 | }; 52 | 53 | window.CopyClipboard = CopyClipboard; 54 | 55 | var copyToClipboard = document.getElementsByClassName('js-copy-to-clip'); 56 | if(copyToClipboard.length > 0) { 57 | new CopyClipboard(); 58 | } 59 | }()); 60 | -------------------------------------------------------------------------------- /resources/css/components/_2_autocomplete.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------- 2 | 3 | File#: _2_autocomplete 4 | Title: Autocomplete 5 | Descr: Autocomplete plugin for input elements 6 | Usage: codyhouse.co/license 7 | 8 | -------------------------------- */ 9 | :root { 10 | --autocomplete-dropdown-vertical-gap: 4px; 11 | /* gap between input and results list */ 12 | --autocomplete-dropdown-max-height: 150px; 13 | --autocomplete-dropdown-scrollbar-width: 6px; 14 | /* custom scrollbar width - webkit browsers */ 15 | } 16 | 17 | .autocomplete__loader { 18 | /* loader visible while searching */ 19 | /* CSS variables inherited from the circle-loader component */ 20 | --circle-loader-v1-size: 1em; 21 | --circle-loader-v1-stroke-width: 2px; 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | .autocomplete:not(.autocomplete--searching) .autocomplete__loader { 27 | /* .autocomplete--searching is used to show the loader element - added in JS */ 28 | display: none; 29 | } 30 | 31 | /* results dropdown */ 32 | .autocomplete__results { 33 | position: absolute; 34 | @apply z-5; 35 | width: 100%; 36 | left: 0; 37 | top: calc(100% + var(--autocomplete-dropdown-vertical-gap)); 38 | transform: translateY(4px); 39 | /* slide in animation */ 40 | @apply bg-white; 41 | @apply rounded-lg; 42 | opacity: 0; 43 | visibility: hidden; 44 | transition: opacity 0.3s, visibility 0s 0.3s, transform 0.3s cubic-bezier(0.55, 0.055, 0.675, 0.19); 45 | overflow: hidden; 46 | } 47 | .autocomplete--results-visible .autocomplete__results { 48 | opacity: 1; 49 | visibility: visible; 50 | transition: opacity 0.3s, transform 0.3s cubic-bezier(0.215, 0.61, 0.355, 1); 51 | transform: translateY(0); 52 | } 53 | 54 | .autocomplete__list { 55 | max-height: var(--autocomplete-dropdown-max-height); 56 | overflow: auto; 57 | -webkit-overflow-scrolling: touch; 58 | /* custom scrollbar */ 59 | } 60 | .autocomplete__list::-webkit-scrollbar { 61 | /* scrollbar width */ 62 | width: var(--autocomplete-dropdown-scrollbar-width); 63 | } 64 | .autocomplete__list::-webkit-scrollbar-track { 65 | /* progress bar */ 66 | @apply bg-gray-900/[.08]; 67 | border-radius: 0; 68 | } 69 | .autocomplete__list::-webkit-scrollbar-thumb { 70 | /* handle */ 71 | @apply bg-gray-900/[.12]; 72 | border-radius: 0; 73 | } 74 | .autocomplete__list::-webkit-scrollbar-thumb:hover { 75 | @apply bg-gray-900/20; 76 | } 77 | 78 | /* single result item */ 79 | .autocomplete__item { 80 | cursor: pointer; 81 | transition: 0.2s; 82 | } 83 | .autocomplete__item:hover { 84 | @apply bg-gray-900/[.07]; 85 | } 86 | .autocomplete__item:focus { 87 | outline: none; 88 | @apply bg-primary-700/[.15]; 89 | } 90 | -------------------------------------------------------------------------------- /resources/js/components/_1_file-upload.js: -------------------------------------------------------------------------------- 1 | // File#: _1_file-upload 2 | // Usage: codyhouse.co/license 3 | (function() { 4 | var InputFile = function(element) { 5 | this.element = element; 6 | this.input = this.element.getElementsByClassName('file-upload__input')[0]; 7 | this.label = this.element.getElementsByClassName('file-upload__label')[0]; 8 | this.multipleUpload = this.input.hasAttribute('multiple'); // allow for multiple files selection 9 | 10 | // this is the label text element -> when user selects a file, it will be changed from the default value to the name of the file 11 | this.labelText = this.element.getElementsByClassName('file-upload__text')[0]; 12 | this.initialLabel = this.labelText.textContent; 13 | 14 | initInputFileEvents(this); 15 | }; 16 | 17 | function initInputFileEvents(inputFile) { 18 | // make label focusable 19 | inputFile.label.setAttribute('tabindex', '0'); 20 | inputFile.input.setAttribute('tabindex', '-1'); 21 | 22 | // move focus from input to label -> this is triggered when a file is selected or the file picker modal is closed 23 | inputFile.input.addEventListener('focusin', function(event){ 24 | inputFile.label.focus(); 25 | }); 26 | 27 | // press 'Enter' key on label element -> trigger file selection 28 | inputFile.label.addEventListener('keydown', function(event) { 29 | if( event.keyCode && event.keyCode == 13 || event.key && event.key.toLowerCase() == 'enter') {inputFile.input.click();} 30 | }); 31 | 32 | // file has been selected -> update label text 33 | inputFile.input.addEventListener('change', function(event){ 34 | updateInputLabelText(inputFile); 35 | }); 36 | }; 37 | 38 | function updateInputLabelText(inputFile) { 39 | var label = ''; 40 | if(inputFile.input.files && inputFile.input.files.length < 1) { 41 | label = inputFile.initialLabel; // no selection -> revert to initial label 42 | } else if(inputFile.multipleUpload && inputFile.input.files && inputFile.input.files.length > 1) { 43 | label = inputFile.input.files.length+ ' files'; // multiple selection -> show number of files 44 | } else { 45 | label = inputFile.input.value.split('\\').pop(); // single file selection -> show name of the file 46 | } 47 | inputFile.labelText.textContent = label; 48 | }; 49 | 50 | //initialize the InputFile objects 51 | var inputFiles = document.getElementsByClassName('file-upload'); 52 | if( inputFiles.length > 0 ) { 53 | for( var i = 0; i < inputFiles.length; i++) { 54 | (function(i){new InputFile(inputFiles[i]);})(i); 55 | } 56 | } 57 | }()); 58 | -------------------------------------------------------------------------------- /resources/views/components/badge.blade.php: -------------------------------------------------------------------------------- 1 |
    class([ 2 | 'inline-flex items-center justify-center px-2 rounded-[8px] font-[450] leading-none', 3 | 'bg-neutral-300/40 text-neutral-600' => !$tone, 4 | 'bg-blue-200/90 text-blue-950' => ($tone == 'info'), 5 | 'bg-green-200/90 text-success-800' => ($tone == 'success'), 6 | 'bg-yellow-200/90 text-yellow-900' => ($tone == 'attention'), 7 | 'bg-orange-200/90 text-amber-900' => ($tone == 'warning'), 8 | 'bg-red-200/90 text-red-900' => ($tone == 'critical'), 9 | 'py-1' => !$progress, 10 | 'py-0' => $progress, 11 | ]) }}> 12 | @if($progress == 'incomplete') 13 | 14 | 15 | 16 | @elseif($progress == 'partiallyComplete') 17 | 18 | 19 | 20 | @elseif($progress == 'complete') 21 | 22 | 23 | 24 | @endif 25 | {{ $slot }} 26 |
    27 | -------------------------------------------------------------------------------- /resources/views/components/modal.blade.php: -------------------------------------------------------------------------------- 1 |
    ($size !== 'fullScreen')]) data-modal-prevent-scroll="body"> 2 |
    class([ 3 | 'modal__content w-full max-h-full overflow-auto bg-white lg:rounded-[16px] shadow-lg' => ($size !== 'fullScreen'), 4 | 'lg:max-w-md' => ($size === 'small'), 5 | 'lg:max-w-2xl' => ($size === 'normal'), 6 | 'max-w-7xl' => ($size === 'large'), 7 | 'modal__content bg-white h-full flex flex-col' => ($size === 'fullScreen'), 8 | ]) }}> 9 | @if($title) 10 |
    11 |

    {{ $title }}

    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
    21 | @else 22 |
    23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
    32 | @endif 33 | 34 |
    35 |
    36 | {{ $slot }} 37 |
    38 |
    39 | 40 | @if(isset($actions)) 41 | 42 |
    43 |
    attributes->class(['flex justify-end gap-2 lg:gap-3']) }}> 44 | {{ $actions }} 45 |
    46 |
    47 | @endif 48 |
    49 |
    50 | -------------------------------------------------------------------------------- /resources/views/components/combobox.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 | class(['box-border block w-full rounded-lg border-0 py-1 placeholder:text-sm placeholder:text-neutral-500 text-neutral-900 ring-1 ring-inset ring-neutral-500/90 list-filter__search js-list-filter__search js-multi-select-v2__search focus:ring-primary-700 focus:ring-2 focus:ring-inset']) }} autocomplete="off" type="search"> 6 | 7 | 13 |
    14 | 15 | 34 |
    35 |
    36 | 37 |
    38 |

    0 {{ $selectedText }}

    39 | 40 |
    41 |
    42 |
    43 | -------------------------------------------------------------------------------- /resources/views/components/select-auto.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 6 | 7 | 8 |
    9 | @if($required) 10 | 11 | @else 12 | 13 | @endif 14 | 15 |
    16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 |
    28 |
    29 | 30 | 31 |
    32 |
      33 | 36 | 37 | 41 | 42 | @if(isset($template)) 43 | 46 | @endif 47 | 48 | 49 |
    50 |
    51 | 52 |

    0 results found.

    53 |
    54 | @if($helpText) 55 |

    {{ $helpText }}

    56 | @endif 57 | -------------------------------------------------------------------------------- /resources/css/components/_1_tooltip.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------- 2 | 3 | File#: _1_tooltip 4 | Title: Tooltip 5 | Descr: A popup displaying additional text information 6 | Usage: codyhouse.co/license 7 | 8 | -------------------------------- */ 9 | :root { 10 | --tooltip-triangle-size: 12px; 11 | } 12 | 13 | .tooltip { 14 | /* tooltip element - created using js */ 15 | position: absolute; 16 | @apply z-5; 17 | display: inline-block; 18 | @apply py-1.5 lg:py-2 px-2 lg:px-3; 19 | @apply rounded-sm; 20 | max-width: 200px; 21 | @apply bg-white; 22 | @apply shadow-md; 23 | @apply text-sm lg:text-base; 24 | line-height: 1.4; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | transition: opacity 0.2s, visibility 0.2s; 28 | } 29 | 30 | @supports ((-webkit-clip-path: inset(50%)) or (clip-path: inset(50%))) { 31 | .tooltip::before { 32 | /* tooltip triangle */ 33 | content: ""; 34 | position: absolute; 35 | background-color: inherit; 36 | border: inherit; 37 | width: var(--tooltip-triangle-size); 38 | height: var(--tooltip-triangle-size); 39 | -webkit-clip-path: polygon(0% 0%, 100% 100%, 100% 100%, 0% 100%); clip-path: polygon(0% 0%, 100% 100%, 100% 100%, 0% 100%); 40 | } 41 | } 42 | 43 | .tootip:not(.tooltip--sticky) { 44 | pointer-events: none; 45 | } 46 | 47 | /* size variations */ 48 | .tooltip--sm { 49 | max-width: 150px; 50 | @apply text-xs; 51 | @apply py-1 lg:py-1.5 px-1.5 lg:px-2; 52 | } 53 | 54 | .tooltip--md { 55 | max-width: 300px; 56 | @apply py-2 lg:py-3 px-3 lg:px-5; 57 | } 58 | 59 | .tooltip--lg { 60 | max-width: 350px; 61 | @apply text-base; 62 | @apply py-2 lg:py-3 px-3 lg:px-5; 63 | } 64 | 65 | /* tooltip position */ 66 | .tooltip { 67 | /* variable used in JS to proper place tooltip triangle */ 68 | --tooltip-triangle-translate: 0px; 69 | } 70 | 71 | .tooltip--top::before, .tooltip--bottom::before { 72 | left: calc(50% - var(--tooltip-triangle-size) / 2); 73 | } 74 | 75 | .tooltip--top::before { 76 | bottom: calc(var(--tooltip-triangle-size) * -0.5); 77 | -webkit-transform: translateX(var(--tooltip-triangle-translate)) rotate(-45deg);transform: translateX(var(--tooltip-triangle-translate)) rotate(-45deg); 78 | } 79 | 80 | .tooltip--bottom::before { 81 | top: calc(var(--tooltip-triangle-size) * -0.5); 82 | -webkit-transform: translateX(var(--tooltip-triangle-translate)) rotate(135deg);transform: translateX(var(--tooltip-triangle-translate)) rotate(135deg); 83 | } 84 | 85 | .tooltip--left::before, .tooltip--right::before { 86 | top: calc(50% - var(--tooltip-triangle-size) / 2); 87 | } 88 | 89 | .tooltip--left::before { 90 | right: calc(var(--tooltip-triangle-size) * -0.5); 91 | -webkit-transform: translateX(var(--tooltip-triangle-translate)) rotate(-135deg);transform: translateX(var(--tooltip-triangle-translate)) rotate(-135deg); 92 | } 93 | 94 | .tooltip--right::before { 95 | left: calc(var(--tooltip-triangle-size) * -0.5); 96 | -webkit-transform: translateX(var(--tooltip-triangle-translate)) rotate(45deg);transform: translateX(var(--tooltip-triangle-translate)) rotate(45deg); 97 | } 98 | 99 | .tooltip--is-hidden { 100 | /* class used in JS to hide the tooltip element before its top/left positions are set */ 101 | visibility: hidden; 102 | opacity: 0; 103 | } 104 | -------------------------------------------------------------------------------- /resources/views/components/alert.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    class([ 3 | 'flex justify-between items-baseline p-2 rounded-lg', 4 | 'bg-sky-100 text-sky-900' => ($tone == 'info'), 5 | 'bg-green-100 text-emerald-900' => ($tone == 'success'), 6 | 'bg-orange-200/35 text-orange-900' => ($tone == 'warning'), 7 | 'bg-red-100 text-red-900' => ($tone == 'critical'), 8 | ]) }}> 9 |
    10 | @if($tone == 'success') 11 | 12 | @elseif($tone == 'warning') 13 | 14 | @elseif($tone == 'critical') 15 | 16 | @else 17 | 18 | @endif 19 |
    {{ $slot }}
    20 |
    21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
    30 |
    31 | -------------------------------------------------------------------------------- /resources/js/components/_1_flash-message.js: -------------------------------------------------------------------------------- 1 | // File#: _1_flash-message 2 | // Usage: codyhouse.co/license 3 | (function() { 4 | var FlashMessage = function(element) { 5 | this.element = element; 6 | this.showClass = "flash-message--is-visible"; 7 | this.messageDuration = parseInt(this.element.getAttribute('data-duration')) || 3000; 8 | this.triggers = document.querySelectorAll('[aria-controls="'+this.element.getAttribute('id')+'"]'); 9 | this.temeoutId = null; 10 | this.isVisible = false; 11 | this.initFlashMessage(); 12 | }; 13 | 14 | FlashMessage.prototype.initFlashMessage = function() { 15 | var self = this; 16 | //open modal when clicking on trigger buttons 17 | if ( self.triggers ) { 18 | for(var i = 0; i < self.triggers.length; i++) { 19 | self.triggers[i].addEventListener('click', function(event) { 20 | event.preventDefault(); 21 | self.showFlashMessage(); 22 | }); 23 | } 24 | } 25 | //listen to the event that triggers the opening of a flash message 26 | self.element.addEventListener('showFlashMessage', function(){ 27 | self.showFlashMessage(); 28 | }); 29 | }; 30 | 31 | FlashMessage.prototype.showFlashMessage = function() { 32 | var self = this; 33 | self.element.classList.add(self.showClass); 34 | self.isVisible = true; 35 | //hide other flash messages 36 | self.hideOtherFlashMessages(); 37 | if( self.messageDuration > 0 ) { 38 | //hide the message after an interval (this.messageDuration) 39 | self.temeoutId = setTimeout(function(){ 40 | self.hideFlashMessage(); 41 | }, self.messageDuration); 42 | } 43 | }; 44 | 45 | FlashMessage.prototype.hideFlashMessage = function() { 46 | this.element.classList.remove(this.showClass); 47 | this.isVisible = false; 48 | //reset timeout 49 | clearTimeout(this.temeoutId); 50 | this.temeoutId = null; 51 | }; 52 | 53 | FlashMessage.prototype.hideOtherFlashMessages = function() { 54 | var event = new CustomEvent('flashMessageShown', { detail: this.element }); 55 | window.dispatchEvent(event); 56 | }; 57 | 58 | FlashMessage.prototype.checkFlashMessage = function(message) { 59 | if( !this.isVisible ) return; 60 | if( this.element == message) return; 61 | this.hideFlashMessage(); 62 | }; 63 | 64 | window.FlashMessage = FlashMessage; 65 | 66 | //initialize the FlashMessage objects 67 | var flashMessages = document.getElementsByClassName('js-flash-message'); 68 | 69 | if( flashMessages.length > 0 ) { 70 | var flashMessagesArray = []; 71 | for( var i = 0; i < flashMessages.length; i++) { 72 | (function(i){flashMessagesArray.push(new FlashMessage(flashMessages[i]));})(i); 73 | } 74 | 75 | //listen for a flash message to be shown -> close the others 76 | window.addEventListener('flashMessageShown', function(event){ 77 | flashMessagesArray.forEach(function(element){ 78 | element.checkFlashMessage(event.detail); 79 | }); 80 | }); 81 | } 82 | 83 | //This section override the original behavior 84 | var autoFlashMessages = document.getElementsByClassName('js-auto-flash-message'); 85 | 86 | function showFlashMessage(element) { 87 | var event = new CustomEvent('showFlashMessage'); 88 | element.dispatchEvent(event); 89 | }; 90 | 91 | //show first flash message available in the page 92 | if( autoFlashMessages.length > 0 ) { 93 | showFlashMessage(autoFlashMessages[0]); 94 | } 95 | }()); 96 | -------------------------------------------------------------------------------- /resources/css/components/_1_list-filter.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------- 2 | 3 | File#: _1_list-filter 4 | Title: List Filter 5 | Descr: A list of filterable search items 6 | Usage: codyhouse.co/license 7 | 8 | -------------------------------- */ 9 | :root { 10 | --list-filter-height: 190px; 11 | } 12 | 13 | .list-filter__form { 14 | overflow: hidden; 15 | } 16 | 17 | .list-filter__search { 18 | position: relative; 19 | width: 100%; 20 | z-index: 1; 21 | } 22 | .list-filter__search::-webkit-search-decoration, .list-filter__search::-webkit-search-cancel-button, .list-filter__search::-webkit-search-results-button, .list-filter__search::-webkit-search-results-decoration { 23 | -webkit-appearance: none; 24 | } 25 | .list-filter__search::-ms-clear, .list-filter__search::-ms-reveal { 26 | display: none; 27 | width: 0; 28 | height: 0; 29 | } 30 | .list-filter__search:focus { 31 | outline: none; 32 | } 33 | .list-filter__search:focus + .list-filter__focus-marker { 34 | opacity: 1; 35 | } 36 | .list-filter__search:-moz-placeholder-shown ~ .list-filter__search-cancel-btn { 37 | display: none; 38 | } 39 | .list-filter__search:-ms-input-placeholder ~ .list-filter__search-cancel-btn { 40 | display: none; 41 | } 42 | .list-filter__search:placeholder-shown ~ .list-filter__search-cancel-btn { 43 | display: none; 44 | } 45 | 46 | .list-filter__search-cancel-btn { 47 | /* custom search cancel button */ 48 | display: inline-block; 49 | position: absolute; 50 | z-index: 2; 51 | @apply text-gray-500; 52 | top: 50%; 53 | transform: translateY(-50%); 54 | cursor: pointer; 55 | border-radius: 50%; 56 | } 57 | .list-filter__search-cancel-btn:hover { 58 | opacity: 0.85; 59 | } 60 | 61 | .list-filter__focus-marker { 62 | display: block; 63 | height: 1em; 64 | width: 3px; 65 | @apply bg-primary-700; 66 | position: absolute; 67 | left: 0; 68 | top: calc(50% - 0.5em); 69 | opacity: 0; 70 | pointer-events: none; 71 | transition: opacity 0.2s; 72 | } 73 | 74 | .list-filter__list-wrapper { 75 | position: relative; 76 | height: var(--list-filter-height); 77 | } 78 | 79 | .list-filter__list { 80 | position: absolute; 81 | top: 0; 82 | left: 0; 83 | width: 100%; 84 | height: 100%; 85 | overflow: auto; 86 | } 87 | 88 | .list-filter__item { 89 | display: flex; 90 | align-items: center; 91 | cursor: default; 92 | transition: background-color 0.2s; 93 | } 94 | .list-filter__item:hover { 95 | @apply bg-gray-900/[.07]; 96 | } 97 | 98 | .list-filter__status { 99 | display: block; 100 | flex-shrink: 0; 101 | --size: 8px; 102 | width: var(--size); 103 | height: var(--size); 104 | border-radius: 50%; 105 | } 106 | .list-filter__item--user-active .list-filter__status { 107 | @apply bg-teal-600; 108 | } 109 | .list-filter__item--user-active .list-filter__status::after { 110 | content: "user active"; 111 | position: absolute; 112 | clip: rect(1px, 1px, 1px, 1px); 113 | clip-path: inset(50%); 114 | } 115 | .list-filter__item--user-pending .list-filter__status { 116 | @apply bg-amber-400; 117 | } 118 | .list-filter__item--user-pending .list-filter__status::after { 119 | content: "user pending"; 120 | position: absolute; 121 | clip: rect(1px, 1px, 1px, 1px); 122 | clip-path: inset(50%); 123 | } 124 | 125 | .list-filter__action-btn { 126 | display: flex; 127 | flex-shrink: 0; 128 | @apply bg-white; 129 | @apply border border-gray-900/[.15]; 130 | border-radius: 50%; 131 | width: 24px; 132 | height: 24px; 133 | cursor: pointer; 134 | transition: 0.2s; 135 | } 136 | .list-filter__action-btn .icon { 137 | margin: auto; 138 | } 139 | .list-filter__action-btn:hover { 140 | @apply border-red-600 text-red-600; 141 | } 142 | -------------------------------------------------------------------------------- /src/DashUiServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('dash-ui') 57 | ->hasViewComponents( 58 | 'dashui', 59 | Button::class, 60 | ButtonGroup::class, 61 | Popover::class, 62 | CalloutCard::class, 63 | Box::class, 64 | Card::class, 65 | CardStack::class, 66 | Divider::class, 67 | EmptyState::class, 68 | MediaCard::class, 69 | PageHeader::class, 70 | Modal::class, 71 | Tooltip::class, 72 | Avatar::class, 73 | Thumbnail::class, 74 | Badge::class, 75 | Banner::class, 76 | Alert::class, 77 | InlineError::class, 78 | FlashMessage::class, 79 | Input::class, 80 | Textarea::class, 81 | Select::class, 82 | Combobox::class, 83 | SearchInput::class, 84 | ExpandableSearch::class, 85 | CircleLoader::class, 86 | Dropzone::class, 87 | Tag::class, 88 | IndexTable::class, 89 | TableRow::class, 90 | TableCell::class, 91 | Pagination::class, 92 | ActionLink::class, 93 | Tabs::class, 94 | TabPanel::class, 95 | TopBar::class, 96 | Navigation::class, 97 | NavigationItem::class, 98 | NavigationSection::class, 99 | SubNavigationItem::class, 100 | CopyToClip::class, 101 | Autocomplete::class, 102 | SelectAuto::class, 103 | ) 104 | ->hasViews(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /resources/css/components/_3_select-autocomplete.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------- 2 | 3 | File#: _3_select-autocomplete 4 | Title: Select Autocomplete 5 | Descr: Selection dropdown with autocomplete 6 | Usage: codyhouse.co/license 7 | 8 | -------------------------------- */ 9 | .select-auto.autocomplete { 10 | --autocomplete-dropdown-vertical-gap: 4px; 11 | /* gap between input and results list */ 12 | --autocomplete-dropdown-max-height: 250px; 13 | --autocomplete-dropdown-scrollbar-width: 6px; 14 | /* custom scrollbar - webkit browsers */ 15 | } 16 | 17 | /* input */ 18 | .select-auto__input-wrapper { 19 | --input-btn-icon-size: 12px; 20 | /* btn/icon size */ 21 | --input-btn-icon-margin-right: 0.5rem; 22 | /* btn/icon size */ 23 | --input-btn-text-gap: 0.375rem; 24 | /* gap between button/icon and text */ 25 | position: relative; 26 | } 27 | @media (min-width: 64rem) { 28 | .select-auto__input-wrapper { 29 | --input-btn-icon-margin-right: 0.75rem; 30 | --input-btn-text-gap: 0.5625rem; 31 | } 32 | } 33 | .select-auto__input-wrapper .form-control { 34 | width: 100%; 35 | height: 100%; 36 | padding-right: calc(var(--input-btn-text-gap) + var(--input-btn-icon-size) + var(--input-btn-icon-margin-right)); 37 | } 38 | 39 | .select-auto__input-icon-wrapper .icon, 40 | .select-auto__input-btn .icon { 41 | display: block; 42 | margin: auto; 43 | width: var(--input-btn-icon-size, 16px); 44 | height: var(--input-btn-icon-size, 16px); 45 | } 46 | 47 | .select-auto__input-icon-wrapper { 48 | position: absolute; 49 | right: var(--input-btn-icon-margin-right); 50 | top: 50%; 51 | transform: translateY(-50%); 52 | display: inline-flex; 53 | pointer-events: none; 54 | } 55 | 56 | .select-auto__input-btn { 57 | /* search cancel button */ 58 | display: none; 59 | justify-content: center; 60 | align-items: center; 61 | pointer-events: auto; 62 | cursor: pointer; 63 | @apply text-gray-500; 64 | /* icon color */ 65 | transition: 0.3s; 66 | } 67 | .select-auto__input-btn:hover { 68 | @apply text-gray-700; 69 | } 70 | .select-auto__input-btn:active { 71 | transform: translateY(2px); 72 | } 73 | 74 | .select-auto--selection-done .select-auto__input-icon-wrapper > .icon { 75 | display: none; 76 | } 77 | .select-auto--selection-done .select-auto__input-btn { 78 | display: inline-flex; 79 | } 80 | 81 | /* dropdown */ 82 | .select-auto__results { 83 | @apply text-base; 84 | } 85 | 86 | /* single result item */ 87 | .select-auto__option { 88 | position: relative; 89 | cursor: pointer; 90 | transition: 0.2s; 91 | } 92 | .select-auto__option:hover { 93 | @apply bg-gray-900/5; 94 | } 95 | .select-auto__option:focus { 96 | outline: none; 97 | @apply bg-primary-700/[.12]; 98 | } 99 | .select-auto__option.select-auto__option--selected { 100 | @apply bg-neutral-200/70; 101 | padding-right: calc(1em + 0.75rem); 102 | -webkit-font-smoothing: antialiased; 103 | -moz-osx-font-smoothing: grayscale; 104 | } 105 | @media (min-width: 64rem) { 106 | .select-auto__option.select-auto__option--selected { 107 | padding-right: calc(1em + 1.125rem); 108 | } 109 | } 110 | .select-auto__option.select-auto__option--selected:focus { 111 | @apply bg-primary-800; 112 | } 113 | .select-auto__option.select-auto__option--selected::after { 114 | content: ""; 115 | position: absolute; 116 | @apply right-3 lg:right-5; 117 | top: calc(50% - 0.5em); 118 | height: 1em; 119 | width: 1em; 120 | background-color: currentColor; 121 | -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpolyline stroke-width='2' stroke='%23ffffff' fill='none' stroke-linecap='round' stroke-linejoin='round' points='1,9 5,13 15,3 '/%3E%3C/svg%3E");mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpolyline stroke-width='2' stroke='%23ffffff' fill='none' stroke-linecap='round' stroke-linejoin='round' points='1,9 5,13 15,3 '/%3E%3C/svg%3E"); 122 | } 123 | 124 | .select-auto__group-title, .select-auto__no-results-msg { 125 | outline: none; 126 | } 127 | -------------------------------------------------------------------------------- /resources/css/components/_1_custom-checkbox.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------- 2 | 3 | File#: _1_custom-checkbox 4 | Title: Custom Checkbox 5 | Descr: Replace the native checkbox button with a custom element (e.g., an icon) 6 | Usage: codyhouse.co/license 7 | 8 | -------------------------------- */ 9 | :root { 10 | --custom-checkbox-size: 20px; 11 | --custom-checkbox-radius: 4px; 12 | --custom-checkbox-border-width: 1px; 13 | --custom-checkbox-marker-size: 18px; 14 | } 15 | 16 | .custom-checkbox { 17 | position: relative; 18 | z-index: 1; 19 | display: inline-block; 20 | font-size: var(--custom-checkbox-size); 21 | } 22 | 23 | .custom-checkbox__input { 24 | position: relative; 25 | /* hide native input */ 26 | margin: 0; 27 | padding: 0; 28 | opacity: 0; 29 | height: 1em; 30 | width: 1em; 31 | display: block; 32 | z-index: 1; 33 | } 34 | 35 | .custom-checkbox__control { 36 | position: absolute; 37 | width: 100%; 38 | height: 100%; 39 | top: 0; 40 | left: 0; 41 | z-index: -1; 42 | pointer-events: none; 43 | transition: -webkit-transform 0.2s; 44 | transition: transform 0.2s; 45 | transition: transform 0.2s, -webkit-transform 0.2s; 46 | @apply text-gray-400/[.65]; 47 | /* unchecked color */ 48 | } 49 | .custom-checkbox__control::before, .custom-checkbox__control::after { 50 | content: ""; 51 | position: absolute; 52 | } 53 | .custom-checkbox__control::before { 54 | /* focus circle */ 55 | width: 160%; 56 | height: 160%; 57 | background-color: currentColor; 58 | top: 50%; 59 | left: 50%; 60 | transform: translate(-50%, -50%) scale(0); 61 | opacity: 0; 62 | border-radius: 50%; 63 | will-change: transform; 64 | transition: -webkit-transform 0.2s; 65 | transition: transform 0.2s; 66 | transition: transform 0.2s, -webkit-transform 0.2s; 67 | } 68 | .custom-checkbox__control::after { 69 | /* custom checkbox */ 70 | top: 0; 71 | left: 0; 72 | width: 100%; 73 | height: 100%; 74 | /* custom checkbox style */ 75 | @apply bg-white; 76 | border-radius: var(--custom-checkbox-radius); 77 | box-shadow: inset 0 0 0 var(--custom-checkbox-border-width) currentColor, 0 0.1px 0.3px rgba(0, 0, 0, 0.06),0 1px 2px rgba(0, 0, 0, 0.12); 78 | /* border */ 79 | } 80 | 81 | .custom-checkbox__input:checked ~ .custom-checkbox__control::after, 82 | .custom-checkbox__input:indeterminate ~ .custom-checkbox__control::after { 83 | background-color: currentColor; 84 | background-repeat: no-repeat; 85 | background-position: center; 86 | background-size: var(--custom-checkbox-marker-size); 87 | box-shadow: none; 88 | } 89 | 90 | .custom-checkbox__input:checked ~ .custom-checkbox__control { 91 | @apply text-primary-700; 92 | /* checked color */ 93 | } 94 | .custom-checkbox__input:checked ~ .custom-checkbox__control::after { 95 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpolyline points='2.5 8 6.5 12 13.5 3' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5'/%3E%3C/svg%3E"); 96 | } 97 | 98 | .custom-checkbox__input:indeterminate ~ .custom-checkbox__control::after { 99 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cline x1='2' y1='8' x2='14' y2='8' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'/%3E%3C/svg%3E"); 100 | } 101 | 102 | .custom-checkbox__input:active ~ .custom-checkbox__control { 103 | transform: scale(0.9); 104 | } 105 | 106 | .custom-checkbox__input:checked:active ~ .custom-checkbox__control, 107 | .custom-checkbox__input:indeterminate:active ~ .custom-checkbox__control { 108 | transform: scale(1); 109 | } 110 | 111 | .custom-checkbox__input:focus ~ .custom-checkbox__control::before { 112 | opacity: 0.2; 113 | transform: translate(-50%, -50%) scale(1); 114 | } 115 | 116 | /* --icon */ 117 | .custom-checkbox--icon { 118 | --custom-checkbox-size: 32px; 119 | } 120 | .custom-checkbox--icon .custom-checkbox__control::after { 121 | display: none; 122 | } 123 | .custom-checkbox--icon .icon { 124 | display: block; 125 | color: inherit; 126 | position: relative; 127 | z-index: 1; 128 | } 129 | -------------------------------------------------------------------------------- /resources/css/components/_1_expandable-search.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------- 2 | 3 | File#: _1_expandable-search 4 | Title: Expandable Search 5 | Descr: A search button that expands to reveal a search input element 6 | Usage: codyhouse.co/license 7 | 8 | -------------------------------- */ 9 | :root { 10 | --expandable-search-size-compact: 2.2rem; 11 | /* height and width - compact version / height - expanded version */ 12 | --expandable-search-size-expanded: 18rem; 13 | /* width - expanded version */ 14 | --expandable-search-radius: 6px; 15 | /* border radius */ 16 | --expandable-search-icon-size: 1.2rem; 17 | /* lens icon size */ 18 | --expandable-search-btn-padding: 2px; 19 | /* gap between button and input element */ 20 | } 21 | 22 | .expandable-search__input { 23 | width: var(--expandable-search-size-compact); 24 | height: var(--expandable-search-size-compact); 25 | box-shadow: 0 1px 0 0 #E3E3E3 inset, 1px 0 0 0 #E3E3E3 inset, -1px 0 0 0 #E3E3E3 inset, 0 -1px 0 0 #B5B5B5 inset; 26 | transition: width 0.3s cubic-bezier(0.215, 0.61, 0.355, 1), box-shadow 0.3s, background-color 0.3s; 27 | } 28 | .expandable-search__input--has-content { 29 | @apply ring-neutral-500/90 ring-1 ring-inset; 30 | } 31 | .expandable-search__input::-webkit-input-placeholder { 32 | opacity: 0; 33 | color: transparent; 34 | } 35 | .expandable-search__input::-moz-placeholder { 36 | opacity: 0; 37 | color: transparent; 38 | } 39 | .expandable-search__input:-ms-input-placeholder { 40 | opacity: 0; 41 | color: transparent; 42 | } 43 | .expandable-search__input::-ms-input-placeholder { 44 | opacity: 0; 45 | color: transparent; 46 | } 47 | .expandable-search__input::placeholder { 48 | opacity: 0; 49 | color: transparent; 50 | } 51 | 52 | .expandable-search__input:not(:focus):not(.expandable-search__input--has-content) { 53 | padding: 0; 54 | } 55 | .expandable-search__input:focus, .expandable-search__input.expandable-search__input--has-content { 56 | @apply bg-white pr-2; 57 | width: var(--expandable-search-size-expanded); 58 | padding-top: 0; 59 | padding-bottom: 0; 60 | outline: none; 61 | @apply text-neutral-900; 62 | cursor: auto; 63 | user-select: auto; 64 | } 65 | 66 | .expandable-search__input:focus::-webkit-input-placeholder, .expandable-search__input.expandable-search__input--has-content::-webkit-input-placeholder { 67 | opacity: 1; 68 | @apply text-neutral-500; 69 | } 70 | .expandable-search__input:focus::-moz-placeholder, .expandable-search__input.expandable-search__input--has-content::-moz-placeholder { 71 | opacity: 1; 72 | @apply text-neutral-500; 73 | } 74 | .expandable-search__input:focus:-ms-input-placeholder, .expandable-search__input.expandable-search__input--has-content:-ms-input-placeholder { 75 | opacity: 1; 76 | @apply text-neutral-500; 77 | } 78 | .expandable-search__input:focus::-ms-input-placeholder, .expandable-search__input.expandable-search__input--has-content::-ms-input-placeholder { 79 | opacity: 1; 80 | @apply text-neutral-500; 81 | } 82 | .expandable-search__input:focus::placeholder, .expandable-search__input.expandable-search__input--has-content::placeholder { 83 | opacity: 1; 84 | @apply text-neutral-500; 85 | } 86 | .expandable-search__input:focus + .expandable-search__btn { 87 | pointer-events: auto; 88 | } 89 | .expandable-search__input::-webkit-search-decoration, .expandable-search__input::-webkit-search-cancel-button, .expandable-search__input::-webkit-search-results-button, .expandable-search__input::-webkit-search-results-decoration { 90 | display: none; 91 | } 92 | 93 | .expandable-search__btn { 94 | transition: background-color 0.3s; 95 | } 96 | 97 | .expandable-search__btn { 98 | position: absolute; 99 | display: flex; 100 | top: var(--expandable-search-btn-padding, 2px); 101 | right: var(--expandable-search-btn-padding, 2px); 102 | width: calc(var(--expandable-search-size-compact) - var(--expandable-search-btn-padding, 2px)*2); 103 | height: calc(var(--expandable-search-size-compact) - var(--expandable-search-btn-padding, 2px)*2); 104 | border-radius: var(--expandable-search-radius); 105 | z-index: 1; 106 | pointer-events: none; 107 | transition: background-color 0.3s; 108 | @apply text-neutral-600 bg-white; 109 | } 110 | 111 | .expandable-search__btn:hover { 112 | box-shadow: 0 1px 0 0 #EBEBEB inset, -1px 0 0 0 #EBEBEB inset, 1px 0 0 0 #EBEBEB inset, 0 -1px 0 0 #CCC inset; 113 | @apply bg-neutral-50; 114 | } 115 | .expandable-search__btn:focus { 116 | outline: none; 117 | @apply bg-neutral-50; 118 | } 119 | -------------------------------------------------------------------------------- /resources/css/components/_1_radios-checkboxes.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------- 2 | 3 | File#: _1_radios-checkboxes 4 | Title: Radios and Checkboxes 5 | Descr: Custom radio and checkbox buttons 6 | Usage: codyhouse.co/license 7 | 8 | -------------------------------- */ 9 | :root { 10 | /* radios + checkboxes */ 11 | --checkbox-radio-size: 16px; 12 | --checkbox-radio-gap: 0.375rem; 13 | /* gap between button and label */ 14 | --checkbox-radio-border-width: 0.04125rem; 15 | --checkbox-radio-line-height: 1.4; 16 | /* radios */ 17 | --radio-marker-size: 8px; 18 | /* checkboxes */ 19 | --checkbox-marker-size: 10px; 20 | --checkbox-radius: 4px; 21 | } 22 | @media (min-width: 64rem) { 23 | :root { 24 | --checkbox-radio-gap: 0.5625rem; 25 | } 26 | } 27 | 28 | /* hide native buttons */ 29 | .radio, 30 | .checkbox { 31 | position: absolute; 32 | padding: 0; 33 | margin: 0; 34 | margin-top: calc((1em * var(--checkbox-radio-line-height) - var(--checkbox-radio-size)) / 2); 35 | opacity: 0; 36 | height: var(--checkbox-radio-size); 37 | width: var(--checkbox-radio-size); 38 | pointer-events: none; 39 | } 40 | 41 | /* label */ 42 | .radio + label, 43 | .checkbox + label { 44 | display: inline-block; 45 | line-height: 1; 46 | user-select: none; 47 | cursor: pointer; 48 | padding-left: calc(var(--checkbox-radio-size) + var(--checkbox-radio-gap)); 49 | } 50 | 51 | /* custom inputs - basic style */ 52 | .radio + label::before, 53 | .checkbox + label::before { 54 | content: ""; 55 | box-sizing: border-box; 56 | display: inline-block; 57 | position: relative; 58 | vertical-align: middle; 59 | top: -0.1em; 60 | margin-left: calc(-1 * (var(--checkbox-radio-size) + var(--checkbox-radio-gap))); 61 | flex-shrink: 0; 62 | width: var(--checkbox-radio-size); 63 | height: var(--checkbox-radio-size); 64 | @apply bg-white; 65 | border-width: var(--checkbox-radio-border-width); 66 | @apply border-neutral-500; 67 | border-style: solid; 68 | @apply shadow-xs; 69 | background-repeat: no-repeat; 70 | background-position: center; 71 | margin-right: var(--checkbox-radio-gap); 72 | transition: border 0.2s, -webkit-transform 0.2s; 73 | transition: transform 0.2s, border 0.2s; 74 | transition: transform 0.2s, border 0.2s, -webkit-transform 0.2s; 75 | } 76 | 77 | /* :hover */ 78 | .radio:not(:checked):not(:focus) + label:hover::before, 79 | .checkbox:not(:checked):not(:focus) + label:hover::before { 80 | @apply border-neutral-600; 81 | } 82 | 83 | /* radio only style */ 84 | .radio + label::before { 85 | border-radius: 50%; 86 | } 87 | 88 | /* checkbox only style */ 89 | .checkbox + label::before { 90 | border-radius: var(--checkbox-radius); 91 | } 92 | 93 | /* :checked */ 94 | .radio:checked + label::before, 95 | .checkbox:checked + label::before { 96 | @apply bg-primary-700; 97 | @apply shadow-xs; 98 | @apply border-primary-700; 99 | transition: -webkit-transform 0.2s; 100 | transition: transform 0.2s; 101 | transition: transform 0.2s, -webkit-transform 0.2s; 102 | } 103 | 104 | /* :active */ 105 | .radio:active + label::before, 106 | .checkbox:active + label::before { 107 | -webkit-transform: scale(0.8);transform: scale(0.8); 108 | transition: -webkit-transform 0.2s; 109 | transition: transform 0.2s; 110 | transition: transform 0.2s, -webkit-transform 0.2s; 111 | } 112 | 113 | /* :checked:active */ 114 | .radio:checked:active + label::before, 115 | .checkbox:checked:active + label::before { 116 | -webkit-transform: none;transform: none; 117 | transition: none; 118 | } 119 | 120 | /* radio button icon */ 121 | .radio:checked + label::before { 122 | background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg class='nc-icon-wrapper' fill='%23ffffff'%3E%3Ccircle cx='8' cy='8' r='8' fill='%23ffffff'%3E%3C/circle%3E%3C/g%3E%3C/svg%3E"); 123 | background-size: var(--radio-marker-size); 124 | } 125 | 126 | /* checkbox button icon */ 127 | .checkbox:checked + label::before { 128 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpolyline points='1 6.5 4 9.5 11 2.5' fill='none' stroke='%23FFFFFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'/%3E%3C/svg%3E"); 129 | background-size: var(--checkbox-marker-size); 130 | } 131 | 132 | /* :focus */ 133 | .radio:checked:active + label::before, 134 | .checkbox:checked:active + label::before, 135 | .radio:focus + label::before, 136 | .checkbox:focus + label::before { 137 | @apply border-primary-700; 138 | box-shadow: 0 0 0 3px hsla(245 58% 51% / 0.2); 139 | } 140 | -------------------------------------------------------------------------------- /resources/js/components/_1_list-filter.js: -------------------------------------------------------------------------------- 1 | // File#: _1_list-filter 2 | // Usage: codyhouse.co/license 3 | (function() { 4 | var ListFilter = function(element) { 5 | this.element = element; 6 | this.search = this.element.getElementsByClassName('js-list-filter__search'); 7 | this.searchCancel = this.element.getElementsByClassName('js-list-filter__search-cancel-btn'); 8 | this.list = this.element.getElementsByClassName('js-list-filter__list')[0]; 9 | this.items = this.list.getElementsByClassName('js-list-filter__item'); 10 | this.noResults = this.element.getElementsByClassName('js-list-filter__no-results-msg'); 11 | this.searchTags = []; 12 | this.resultsNr = this.element.getElementsByClassName('js-list-filter__results-nr'); 13 | this.visibleItemsNr = 0; 14 | initListFilter(this); 15 | }; 16 | 17 | function initListFilter(element) { 18 | // get the filterable list of tags 19 | for(var i = 0; i < element.items.length; i++) { 20 | var tags = ''; 21 | var label = element.items[i].getElementsByClassName('js-list-filter__label'); 22 | if(label.length > 0) { 23 | tags = label[0].textContent; 24 | } 25 | var additionalTags = element.items[i].getAttribute('data-filter-tags'); 26 | if(additionalTags) tags = tags + ' ' + additionalTags; 27 | element.searchTags.push(tags); 28 | } 29 | 30 | // filter list based on search input value 31 | filterItems(element, element.search[0].value.trim()); 32 | 33 | // filter list when search input is updated 34 | element.search[0].addEventListener('input', function(){ 35 | filterItems(element, element.search[0].value.trim()); 36 | }); 37 | 38 | // reset search results 39 | if(element.searchCancel.length > 0) { 40 | element.searchCancel[0].addEventListener('click', function() { 41 | element.search[0].value= ""; 42 | element.search[0].dispatchEvent(new Event('input')); 43 | }); 44 | } 45 | 46 | 47 | // remove item from the list when clicking on the remove button 48 | element.list.addEventListener('click', function(event){ 49 | var removeBtn = event.target.closest('.js-list-filter__action-btn--remove'); 50 | if(!removeBtn) return; 51 | event.preventDefault(); 52 | removeItem(element, removeBtn); 53 | }); 54 | }; 55 | 56 | function filterItems(element, value) { 57 | var array = []; 58 | var searchArray = value.toLowerCase().split(' '); 59 | for(var i = 0; i < element.items.length; i++) { 60 | value == '' ? array.push(false) : array.push(filterItem(element, i, searchArray)); 61 | } 62 | updateVisibility(element, array); 63 | }; 64 | 65 | function filterItem(element, index, searchArray) { 66 | // return false if item should be visible 67 | var found = true; 68 | for(var i = 0; i < searchArray.length; i++) { 69 | if(searchArray[i] != '' && searchArray[i] != ' ' && element.searchTags[index].toLowerCase().indexOf(searchArray[i]) < 0) { 70 | found = false; 71 | break; 72 | } 73 | } 74 | return !found; 75 | }; 76 | 77 | function updateVisibility(element, list) { 78 | element.visibleItemsNr = 0; 79 | for(var i = 0; i < list.length; i++) { 80 | // hide/show items 81 | if(!list[i]) element.visibleItemsNr = element.visibleItemsNr + 1; 82 | element.items[i].classList.toggle('hidden', list[i]); 83 | } 84 | // hide/show no results message 85 | if(element.noResults.length > 0) element.noResults[0].classList.toggle('hidden', element.visibleItemsNr > 0); 86 | 87 | updateResultsNr(element); 88 | }; 89 | 90 | function updateResultsNr(element) { 91 | // update number of results 92 | if(element.resultsNr.length > 0) element.resultsNr[0].innerHTML = element.visibleItemsNr; 93 | }; 94 | 95 | function removeItem(element, btn) { 96 | var item = btn.closest('.js-list-filter__item'); 97 | if(!item) return; 98 | var index = Array.prototype.indexOf.call(element.items, item); 99 | // remove item from the DOM 100 | item.remove(); 101 | // update list of search tags 102 | element.searchTags.splice(index, 1); 103 | // update number of results 104 | element.visibleItemsNr = element.visibleItemsNr - 1; 105 | updateResultsNr(element); 106 | } 107 | 108 | //initialize the ListFilter objects 109 | var listFilters = document.getElementsByClassName('js-list-filter'); 110 | if( listFilters.length > 0 ) { 111 | for( var i = 0; i < listFilters.length; i++) { 112 | (function(i){new ListFilter(listFilters[i]);})(i); 113 | } 114 | } 115 | }()); 116 | -------------------------------------------------------------------------------- /resources/css/components/_1_responsive-sidebar.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------- 2 | 3 | File#: _1_responsive-sidebar 4 | Title: Responsive Sidebar 5 | Descr: Responsive sidebar container 6 | Usage: codyhouse.co/license 7 | 8 | -------------------------------- */ 9 | 10 | .sidebar { 11 | position: relative; 12 | } 13 | 14 | .sidebar__header { 15 | @apply px-5 py-3 text-white bg-primary-900 z-2 lg:px-8 lg:py-5; 16 | } 17 | 18 | .sidebar__panel { 19 | background-color: #ebebeb; 20 | height: 100%; 21 | max-height: 100%; 22 | overflow: auto; 23 | display: flex; 24 | flex-direction: column; 25 | align-items: stretch; 26 | } 27 | 28 | .sidebar--static .sidebar__panel { 29 | position: fixed; 30 | top: var(--top-bar-height); 31 | left: 0; 32 | width: 100%; 33 | max-width: 15rem; 34 | max-height: calc(100vh - var(--top-bar-height)); 35 | } 36 | 37 | /* mobile version only (--default) 👇 */ 38 | .sidebar:not(.sidebar--static) { 39 | position: fixed; 40 | top: 0; 41 | left: 0; 42 | @apply z-10; 43 | width: 100%; 44 | height: 100%; 45 | visibility: hidden; 46 | transition: visibility 0s 0.3s; 47 | } 48 | 49 | .sidebar:not(.sidebar--static)::after { 50 | /* overlay layer */ 51 | content: ""; 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | width: 100%; 56 | height: 100%; 57 | @apply bg-neutral-900/75; 58 | transition: background-color 0.3s; 59 | z-index: 1; 60 | } 61 | 62 | .sidebar:not(.sidebar--static) .sidebar__panel { 63 | /* content */ 64 | position: absolute; 65 | top: 0; 66 | left: 0; 67 | z-index: 2; 68 | width: 100%; 69 | max-width: 380px; 70 | height: 100%; 71 | overflow: auto; 72 | -webkit-overflow-scrolling: touch; 73 | -webkit-transform: translateX(-100%); 74 | transform: translateX(-100%); 75 | transition: box-shadow 0.3s, -webkit-transform 0.3s; 76 | transition: box-shadow 0.3s, transform 0.3s; 77 | transition: box-shadow 0.3s, transform 0.3s, -webkit-transform 0.3s; 78 | } 79 | 80 | .sidebar:not(.sidebar--static).sidebar--right-on-mobile .sidebar__panel { 81 | left: auto; 82 | right: 0; 83 | -webkit-transform: translateX(100%); 84 | transform: translateX(100%); 85 | } 86 | 87 | .sidebar:not(.sidebar--static).sidebar--is-visible { 88 | visibility: visible; 89 | transition: none; 90 | } 91 | 92 | .sidebar:not(.sidebar--static).sidebar--is-visible::after { 93 | @apply bg-gray-900/[.85]; 94 | } 95 | 96 | .sidebar:not(.sidebar--static).sidebar--is-visible .sidebar__panel { 97 | transform: translateX(0); 98 | @apply shadow-md; 99 | } 100 | 101 | /* end mobile version */ 102 | .sidebar__header { 103 | display: flex; 104 | align-items: center; 105 | justify-content: space-between; 106 | position: -webkit-sticky; 107 | position: sticky; 108 | top: 0; 109 | } 110 | 111 | .sidebar__close-btn { 112 | --size: 32px; 113 | width: var(--size); 114 | height: var(--size); 115 | display: flex; 116 | border-radius: 50%; 117 | @apply bg-white; 118 | @apply shadow-md; 119 | transition: 0.2s; 120 | flex-shrink: 0; 121 | } 122 | 123 | .sidebar__close-btn .icon { 124 | display: block; 125 | margin: auto; 126 | } 127 | 128 | .sidebar__close-btn:hover { 129 | @apply bg-white; 130 | @apply shadow-lg; 131 | } 132 | 133 | /* desktop version only (--static) 👇 */ 134 | .sidebar--static { 135 | flex-shrink: 0; 136 | flex-grow: 1; 137 | } 138 | 139 | .sidebar--static .sidebar__header { 140 | display: none; 141 | } 142 | 143 | .sidebar--sticky-on-desktop { 144 | position: sticky; 145 | @apply top-3 lg:top-5; 146 | max-height: calc(100vh - 0.75rem); 147 | overflow: auto; 148 | -webkit-overflow-scrolling: touch; 149 | } 150 | 151 | @media (min-width: 64rem) { 152 | .sidebar--sticky-on-desktop { 153 | max-height: calc(100vh - 1.125rem); 154 | } 155 | } 156 | 157 | /* end desktop version */ 158 | .sidebar, .sidebar-loaded\:show { 159 | opacity: 0; 160 | /* hide sidebar - or other elements using the .sidebar-loaded:show class - while it is initialized in JS */ 161 | } 162 | 163 | .sidebar--loaded { 164 | opacity: 1; 165 | } 166 | 167 | /* detect when the sidebar needs to switch from the mobile layout to a static one - used in JS */ 168 | [class*=sidebar--static]::before { 169 | display: none; 170 | } 171 | 172 | .sidebar--static::before { 173 | content: "static"; 174 | } 175 | 176 | .sidebar--static\@xs::before { 177 | content: "mobile"; 178 | } 179 | 180 | @media (min-width: 32rem) { 181 | .sidebar--static\@xs::before { 182 | content: "static"; 183 | } 184 | } 185 | 186 | .sidebar--static\@sm::before { 187 | content: "mobile"; 188 | } 189 | 190 | @media (min-width: 48rem) { 191 | .sidebar--static\@sm::before { 192 | content: "static"; 193 | } 194 | } 195 | 196 | .sidebar--static\@md::before { 197 | content: "mobile"; 198 | } 199 | 200 | @media (min-width: 64rem) { 201 | .sidebar--static\@md::before { 202 | content: "static"; 203 | } 204 | } 205 | 206 | .sidebar--static\@lg::before { 207 | content: "mobile"; 208 | } 209 | 210 | @media (min-width: 80rem) { 211 | .sidebar--static\@lg::before { 212 | content: "static"; 213 | } 214 | } 215 | 216 | .sidebar--static\@xl::before { 217 | content: "mobile"; 218 | } 219 | 220 | @media (min-width: 90rem) { 221 | .sidebar--static\@xl::before { 222 | content: "static"; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /resources/js/components/_1_swipe-content.js: -------------------------------------------------------------------------------- 1 | // File#: _1_swipe-content 2 | (function() { 3 | var SwipeContent = function(element) { 4 | this.element = element; 5 | this.delta = [false, false]; 6 | this.dragging = false; 7 | this.intervalId = false; 8 | initSwipeContent(this); 9 | }; 10 | 11 | function initSwipeContent(content) { 12 | content.element.addEventListener('mousedown', handleEvent.bind(content)); 13 | content.element.addEventListener('touchstart', handleEvent.bind(content), {passive: true}); 14 | }; 15 | 16 | function initDragging(content) { 17 | //add event listeners 18 | content.element.addEventListener('mousemove', handleEvent.bind(content)); 19 | content.element.addEventListener('touchmove', handleEvent.bind(content), {passive: true}); 20 | content.element.addEventListener('mouseup', handleEvent.bind(content)); 21 | content.element.addEventListener('mouseleave', handleEvent.bind(content)); 22 | content.element.addEventListener('touchend', handleEvent.bind(content)); 23 | }; 24 | 25 | function cancelDragging(content) { 26 | //remove event listeners 27 | if(content.intervalId) { 28 | (!window.requestAnimationFrame) ? clearInterval(content.intervalId) : window.cancelAnimationFrame(content.intervalId); 29 | content.intervalId = false; 30 | } 31 | content.element.removeEventListener('mousemove', handleEvent.bind(content)); 32 | content.element.removeEventListener('touchmove', handleEvent.bind(content)); 33 | content.element.removeEventListener('mouseup', handleEvent.bind(content)); 34 | content.element.removeEventListener('mouseleave', handleEvent.bind(content)); 35 | content.element.removeEventListener('touchend', handleEvent.bind(content)); 36 | }; 37 | 38 | function handleEvent(event) { 39 | switch(event.type) { 40 | case 'mousedown': 41 | case 'touchstart': 42 | startDrag(this, event); 43 | break; 44 | case 'mousemove': 45 | case 'touchmove': 46 | drag(this, event); 47 | break; 48 | case 'mouseup': 49 | case 'mouseleave': 50 | case 'touchend': 51 | endDrag(this, event); 52 | break; 53 | } 54 | }; 55 | 56 | function startDrag(content, event) { 57 | content.dragging = true; 58 | // listen to drag movements 59 | initDragging(content); 60 | content.delta = [parseInt(unify(event).clientX), parseInt(unify(event).clientY)]; 61 | // emit drag start event 62 | emitSwipeEvents(content, 'dragStart', content.delta, event.target); 63 | }; 64 | 65 | function endDrag(content, event) { 66 | cancelDragging(content); 67 | // credits: https://css-tricks.com/simple-swipe-with-vanilla-javascript/ 68 | var dx = parseInt(unify(event).clientX), 69 | dy = parseInt(unify(event).clientY); 70 | 71 | // check if there was a left/right swipe 72 | if(content.delta && (content.delta[0] || content.delta[0] === 0)) { 73 | var s = getSign(dx - content.delta[0]); 74 | 75 | if(Math.abs(dx - content.delta[0]) > 30) { 76 | (s < 0) ? emitSwipeEvents(content, 'swipeLeft', [dx, dy]) : emitSwipeEvents(content, 'swipeRight', [dx, dy]); 77 | } 78 | 79 | content.delta[0] = false; 80 | } 81 | // check if there was a top/bottom swipe 82 | if(content.delta && (content.delta[1] || content.delta[1] === 0)) { 83 | var y = getSign(dy - content.delta[1]); 84 | 85 | if(Math.abs(dy - content.delta[1]) > 30) { 86 | (y < 0) ? emitSwipeEvents(content, 'swipeUp', [dx, dy]) : emitSwipeEvents(content, 'swipeDown', [dx, dy]); 87 | } 88 | 89 | content.delta[1] = false; 90 | } 91 | // emit drag end event 92 | emitSwipeEvents(content, 'dragEnd', [dx, dy]); 93 | content.dragging = false; 94 | }; 95 | 96 | function drag(content, event) { 97 | if(!content.dragging) return; 98 | // emit dragging event with coordinates 99 | (!window.requestAnimationFrame) 100 | ? content.intervalId = setTimeout(function(){emitDrag.bind(content, event);}, 250) 101 | : content.intervalId = window.requestAnimationFrame(emitDrag.bind(content, event)); 102 | }; 103 | 104 | function emitDrag(event) { 105 | emitSwipeEvents(this, 'dragging', [parseInt(unify(event).clientX), parseInt(unify(event).clientY)]); 106 | }; 107 | 108 | function unify(event) { 109 | // unify mouse and touch events 110 | return event.changedTouches ? event.changedTouches[0] : event; 111 | }; 112 | 113 | function emitSwipeEvents(content, eventName, detail, el) { 114 | var trigger = false; 115 | if(el) trigger = el; 116 | // emit event with coordinates 117 | var event = new CustomEvent(eventName, {detail: {x: detail[0], y: detail[1], origin: trigger}}); 118 | content.element.dispatchEvent(event); 119 | }; 120 | 121 | function getSign(x) { 122 | if(!Math.sign) { 123 | return ((x > 0) - (x < 0)) || +x; 124 | } else { 125 | return Math.sign(x); 126 | } 127 | }; 128 | 129 | window.SwipeContent = SwipeContent; 130 | 131 | //initialize the SwipeContent objects 132 | var swipe = document.getElementsByClassName('js-swipe-content'); 133 | if( swipe.length > 0 ) { 134 | for( var i = 0; i < swipe.length; i++) { 135 | (function(i){new SwipeContent(swipe[i]);})(i); 136 | } 137 | } 138 | }()); 139 | -------------------------------------------------------------------------------- /resources/js/components/_2_multiple-custom-select.js: -------------------------------------------------------------------------------- 1 | // File#: _2_multiple-custom-select-v2 2 | // Usage: codyhouse.co/license 3 | //This component is overwritten 4 | (function() { 5 | var MultiCustomSelectTwo = function(element) { 6 | this.element = element; 7 | this.checkboxes = this.element.getElementsByClassName('js-multi-select-v2__input'); 8 | this.counter = this.element.getElementsByClassName('js-multi-select-v2__selected-items-counter'); 9 | this.resetBtn = this.element.getElementsByClassName('js-multi-select-v2__reset'); 10 | this.checkedClass = 'multi-select-v2__label--checked'; 11 | this.listWrapper = this.element.getElementsByClassName('js-multi-select-v2__wrapper'); 12 | this.searchInput = this.element.getElementsByClassName('js-multi-select-v2__search'); 13 | initMultiCustomSelectTwo(this); 14 | this.toggleListDisplay(); 15 | }; 16 | 17 | function initMultiCustomSelectTwo(element) { 18 | // init number of checked inputs 19 | resetCounter(element); 20 | // init checked classes 21 | initCheckedClass(element); 22 | //Init the checked labels 23 | element.updateCheckedLabelsDisplay(); 24 | 25 | // detect input checked/unchecked 26 | element.element.addEventListener('change', function(event){ 27 | var label = event.target.closest('label'); 28 | if(label) label.classList.toggle(element.checkedClass, event.target.checked); 29 | resetCounter(element); 30 | // Update the display of checked labels 31 | element.updateCheckedLabelsDisplay(); 32 | }); 33 | 34 | // reset checked inputs 35 | if(element.resetBtn.length > 0) { 36 | element.resetBtn[0].addEventListener('click', function(event) { 37 | for(var i = 0; i < element.checkboxes.length; i++) element.checkboxes[i].checked = false; 38 | resetCounter(element, 0); 39 | resetCheckedClasses(element); 40 | // Clear the display of checked labels 41 | element.updateCheckedLabelsDisplay(); 42 | }); 43 | } 44 | }; 45 | 46 | MultiCustomSelectTwo.prototype.toggleListDisplay = function() { 47 | var _this = this; 48 | 49 | if (this.searchInput.length > 0) { 50 | this.searchInput[0].addEventListener('focus', function() { 51 | if (_this.listWrapper.length > 0) _this.listWrapper[0].style.removeProperty('display'); 52 | }); 53 | 54 | document.addEventListener('click', function(e) { 55 | if (!_this.element.contains(e.target)) { 56 | if (_this.listWrapper.length > 0) _this.listWrapper[0].style.display = 'none'; 57 | } 58 | }); 59 | 60 | if (_this.listWrapper.length > 0) { 61 | _this.listWrapper[0].addEventListener('click', function(e) { 62 | e.stopPropagation(); // Prevent hiding when clicking within the list wrapper 63 | }); 64 | } 65 | } 66 | }; 67 | 68 | MultiCustomSelectTwo.prototype.updateCheckedLabelsDisplay = function() { 69 | var selectedItemsContainer = this.element.querySelector('.js-multi-select-v2__selected-items'); 70 | if (!selectedItemsContainer) return; // Exit if the container does not exist 71 | 72 | // Clear existing content 73 | selectedItemsContainer.innerHTML = ''; 74 | 75 | // Iterate through checkboxes to find checked ones and display their labels 76 | Array.from(this.checkboxes).forEach(function(checkbox) { 77 | if (checkbox.checked) { 78 | var label = checkbox.closest('label').textContent.trim(); // Adjust based on your actual DOM structure 79 | // Create tag element for each checked label 80 | var tagElement = document.createElement('span'); 81 | tagElement.className = 'inline-flex items-center min-h-2 bg-neutral-200 rounded-md py-1 px-2'; 82 | tagElement.innerHTML = `${label}`; 83 | 84 | // Append the tag element to the container 85 | selectedItemsContainer.appendChild(tagElement); 86 | } 87 | }); 88 | }; 89 | 90 | function resetCounter(element, value) { 91 | // update number of selected checkboxes 92 | if(element.counter.length < 1) return; 93 | if(value !== undefined) { 94 | element.counter[0].textContent = value; 95 | return; 96 | } 97 | 98 | var count = 0; 99 | for(var i = 0; i < element.checkboxes.length; i++) { 100 | if(element.checkboxes[i].checked) count = count + 1; 101 | } 102 | element.counter[0].textContent = count; 103 | }; 104 | 105 | function resetCheckedClasses(element) { 106 | var checkedLabels = element.element.getElementsByClassName(element.checkedClass); 107 | while(checkedLabels[0]) { 108 | checkedLabels[0].classList.remove(element.checkedClass); 109 | } 110 | }; 111 | 112 | function initCheckedClass(element) { 113 | for(var i = 0; i < element.checkboxes.length; i++) { 114 | if(element.checkboxes[i].checked) { 115 | var label = element.checkboxes[i].closest('label'); 116 | if(label) label.classList.add(element.checkedClass); 117 | } 118 | } 119 | }; 120 | 121 | //initialize the CustomSelect objects 122 | var customSelect = document.getElementsByClassName('js-multi-select-v2'); 123 | if( customSelect.length > 0 ) { 124 | for( var i = 0; i < customSelect.length; i++) { 125 | (function(i){new MultiCustomSelectTwo(customSelect[i]);})(i); 126 | } 127 | } 128 | }()); 129 | -------------------------------------------------------------------------------- /resources/js/_util.js: -------------------------------------------------------------------------------- 1 | export class Util { 2 | /* class manipulation functions */ 3 | static hasClass = function(el, className) { 4 | return el.classList.contains(className); 5 | }; 6 | 7 | static addClass = function(el, className) { 8 | var classList = className.split(' '); 9 | el.classList.add(classList[0]); 10 | if (classList.length > 1) Util.addClass(el, classList.slice(1).join(' ')); 11 | }; 12 | 13 | static removeClass = function(el, className) { 14 | var classList = className.split(' '); 15 | el.classList.remove(classList[0]); 16 | if (classList.length > 1) Util.removeClass(el, classList.slice(1).join(' ')); 17 | }; 18 | 19 | static toggleClass = function(el, className, bool) { 20 | if(bool) Util.addClass(el, className); 21 | else Util.removeClass(el, className); 22 | }; 23 | 24 | static setAttributes = function(el, attrs) { 25 | for(var key in attrs) { 26 | el.setAttribute(key, attrs[key]); 27 | } 28 | }; 29 | 30 | /* DOM manipulation */ 31 | static getChildrenByClassName = function(el, className) { 32 | var children = el.children, 33 | childrenByClass = []; 34 | for (var i = 0; i < children.length; i++) { 35 | if (Util.hasClass(children[i], className)) childrenByClass.push(children[i]); 36 | } 37 | return childrenByClass; 38 | }; 39 | 40 | static is = function(elem, selector) { 41 | if(selector.nodeType){ 42 | return elem === selector; 43 | } 44 | 45 | var qa = (typeof(selector) === 'string' ? document.querySelectorAll(selector) : selector), 46 | length = qa.length; 47 | 48 | while(length--){ 49 | if(qa[length] === elem){ 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | }; 56 | 57 | /* Animate height of an element */ 58 | static setHeight = function(start, to, element, duration, cb, timeFunction) { 59 | var change = to - start, 60 | currentTime = null; 61 | 62 | var animateHeight = function(timestamp){ 63 | if (!currentTime) currentTime = timestamp; 64 | var progress = timestamp - currentTime; 65 | if(progress > duration) progress = duration; 66 | var val = parseInt((progress/duration)*change + start); 67 | if(timeFunction) { 68 | val = Math[timeFunction](progress, start, to - start, duration); 69 | } 70 | element.style.height = val+"px"; 71 | if(progress < duration) { 72 | window.requestAnimationFrame(animateHeight); 73 | } else { 74 | if(cb) cb(); 75 | } 76 | }; 77 | 78 | //set the height of the element before starting animation -> fix bug on Safari 79 | element.style.height = start+"px"; 80 | window.requestAnimationFrame(animateHeight); 81 | }; 82 | 83 | /* Smooth Scroll */ 84 | static scrollTo = function(final, duration, cb, scrollEl) { 85 | var element = scrollEl || window; 86 | var start = element.scrollTop || document.documentElement.scrollTop, 87 | currentTime = null; 88 | 89 | if(!scrollEl) start = window.scrollY || document.documentElement.scrollTop; 90 | 91 | var animateScroll = function(timestamp){ 92 | if (!currentTime) currentTime = timestamp; 93 | var progress = timestamp - currentTime; 94 | if(progress > duration) progress = duration; 95 | var val = Math.easeInOutQuad(progress, start, final-start, duration); 96 | element.scrollTo(0, val); 97 | if(progress < duration) { 98 | window.requestAnimationFrame(animateScroll); 99 | } else { 100 | cb && cb(); 101 | } 102 | }; 103 | 104 | window.requestAnimationFrame(animateScroll); 105 | }; 106 | 107 | /* Move Focus */ 108 | static moveFocus = function (element) { 109 | if( !element ) element = document.getElementsByTagName("body")[0]; 110 | element.focus(); 111 | if (document.activeElement !== element) { 112 | element.setAttribute('tabindex','-1'); 113 | element.focus(); 114 | } 115 | }; 116 | 117 | /* Misc */ 118 | 119 | static getIndexInArray = function(array, el) { 120 | return Array.prototype.indexOf.call(array, el); 121 | }; 122 | 123 | static cssSupports = function(property, value) { 124 | return CSS.supports(property, value); 125 | }; 126 | 127 | // merge a set of user options into plugin defaults 128 | // https://gomakethings.com/vanilla-javascript-version-of-jquery-extend/ 129 | static extend = function() { 130 | // Variables 131 | var extended = {}; 132 | var deep = false; 133 | var i = 0; 134 | var length = arguments.length; 135 | 136 | // Check if a deep merge 137 | if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { 138 | deep = arguments[0]; 139 | i++; 140 | } 141 | 142 | // Merge the object into the extended object 143 | var merge = function (obj) { 144 | for ( var prop in obj ) { 145 | if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { 146 | // If deep merge and property is an object, merge properties 147 | if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { 148 | extended[prop] = extend( true, extended[prop], obj[prop] ); 149 | } else { 150 | extended[prop] = obj[prop]; 151 | } 152 | } 153 | } 154 | }; 155 | 156 | // Loop through each object and conduct a merge 157 | for ( ; i < length; i++ ) { 158 | var obj = arguments[i]; 159 | merge(obj); 160 | } 161 | 162 | return extended; 163 | }; 164 | 165 | // Check if Reduced Motion is enabled 166 | static osHasReducedMotion = function() { 167 | if(!window.matchMedia) return false; 168 | var matchMediaObj = window.matchMedia('(prefers-reduced-motion: reduce)'); 169 | if(matchMediaObj) return matchMediaObj.matches; 170 | return false; // return false if not supported 171 | }; 172 | } 173 | -------------------------------------------------------------------------------- /resources/js/components/_1_tabs.js: -------------------------------------------------------------------------------- 1 | import { Util } from '../_util.js'; 2 | 3 | // File#: _1_tabs 4 | // Usage: codyhouse.co/license 5 | (function() { 6 | var Tab = function(element) { 7 | this.element = element; 8 | this.tabList = this.element.getElementsByClassName('js-tabs__controls')[0]; 9 | this.listItems = this.tabList.getElementsByTagName('li'); 10 | this.triggers = this.tabList.getElementsByTagName('a'); 11 | this.panelsList = this.element.getElementsByClassName('js-tabs__panels')[0]; 12 | this.panels = Util.getChildrenByClassName(this.panelsList, 'js-tabs__panel'); 13 | this.hideClass = this.element.getAttribute('data-hide-panel-class') ? this.element.getAttribute('data-hide-panel-class') : 'hidden'; 14 | this.customShowClass = this.element.getAttribute('data-show-panel-class') ? this.element.getAttribute('data-show-panel-class') : false; 15 | this.layout = this.element.getAttribute('data-tabs-layout') ? this.element.getAttribute('data-tabs-layout') : 'horizontal'; 16 | // deep linking options 17 | this.deepLinkOn = this.element.getAttribute('data-deep-link') == 'on'; 18 | // init tabs 19 | this.initTab(); 20 | }; 21 | 22 | Tab.prototype.initTab = function() { 23 | //set initial aria attributes 24 | this.tabList.setAttribute('role', 'tablist'); 25 | Util.addClass(this.element, 'tabs--no-interaction'); 26 | 27 | for( var i = 0; i < this.triggers.length; i++) { 28 | var bool = (i == 0), 29 | panelId = this.panels[i].getAttribute('id'); 30 | this.listItems[i].setAttribute('role', 'presentation'); 31 | Util.setAttributes(this.triggers[i], {'role': 'tab', 'aria-selected': bool, 'aria-controls': panelId, 'id': 'tab-'+panelId}); 32 | Util.addClass(this.triggers[i], 'js-tabs__trigger'); 33 | Util.setAttributes(this.panels[i], {'role': 'tabpanel', 'aria-labelledby': 'tab-'+panelId}); 34 | Util.toggleClass(this.panels[i], this.hideClass, !bool); 35 | if(bool && this.customShowClass) Util.addClass(this.panels[i], this.customShowClass); 36 | 37 | if(!bool) this.triggers[i].setAttribute('tabindex', '-1'); 38 | } 39 | 40 | //listen for Tab events 41 | this.initTabEvents(); 42 | 43 | // check deep linking option 44 | this.initDeepLink(); 45 | }; 46 | 47 | Tab.prototype.initTabEvents = function() { 48 | var self = this; 49 | //click on a new tab -> select content 50 | this.tabList.addEventListener('click', function(event) { 51 | if( event.target.closest('.js-tabs__trigger') ) self.triggerTab(event.target.closest('.js-tabs__trigger'), event); 52 | }); 53 | //arrow keys to navigate through tabs 54 | this.tabList.addEventListener('keydown', function(event) { 55 | ; 56 | if( !event.target.closest('.js-tabs__trigger') ) return; 57 | if( tabNavigateNext(event, self.layout) ) { 58 | event.preventDefault(); 59 | self.selectNewTab('next'); 60 | } else if( tabNavigatePrev(event, self.layout) ) { 61 | event.preventDefault(); 62 | self.selectNewTab('prev'); 63 | } 64 | }); 65 | }; 66 | 67 | Tab.prototype.selectNewTab = function(direction) { 68 | var selectedTab = this.tabList.querySelector('[aria-selected="true"]'), 69 | index = Util.getIndexInArray(this.triggers, selectedTab); 70 | index = (direction == 'next') ? index + 1 : index - 1; 71 | //make sure index is in the correct interval 72 | //-> from last element go to first using the right arrow, from first element go to last using the left arrow 73 | if(index < 0) index = this.listItems.length - 1; 74 | if(index >= this.listItems.length) index = 0; 75 | this.triggerTab(this.triggers[index]); 76 | this.triggers[index].focus(); 77 | }; 78 | 79 | Tab.prototype.triggerTab = function(tabTrigger, event) { 80 | var self = this; 81 | event && event.preventDefault(); 82 | var index = Util.getIndexInArray(this.triggers, tabTrigger); 83 | //no need to do anything if tab was already selected 84 | if(this.triggers[index].getAttribute('aria-selected') == 'true') return; 85 | 86 | Util.removeClass(this.element, 'tabs--no-interaction'); 87 | 88 | for( var i = 0; i < this.triggers.length; i++) { 89 | var bool = (i == index); 90 | Util.toggleClass(this.panels[i], this.hideClass, !bool); 91 | if(this.customShowClass) Util.toggleClass(this.panels[i], this.customShowClass, bool); 92 | this.triggers[i].setAttribute('aria-selected', bool); 93 | bool ? this.triggers[i].setAttribute('tabindex', '0') : this.triggers[i].setAttribute('tabindex', '-1'); 94 | } 95 | 96 | // update url if deepLink is on 97 | if(this.deepLinkOn) { 98 | history.replaceState(null, '', '#'+tabTrigger.getAttribute('aria-controls')); 99 | } 100 | }; 101 | 102 | Tab.prototype.initDeepLink = function() { 103 | if(!this.deepLinkOn) return; 104 | var hash = window.location.hash.substr(1); 105 | var self = this; 106 | if(!hash || hash == '') return; 107 | for(var i = 0; i < this.panels.length; i++) { 108 | if(this.panels[i].getAttribute('id') == hash) { 109 | this.triggerTab(this.triggers[i], false); 110 | setTimeout(function(){self.panels[i].scrollIntoView(true);}); 111 | break; 112 | } 113 | }; 114 | }; 115 | 116 | function tabNavigateNext(event, layout) { 117 | if(layout == 'horizontal' && (event.keyCode && event.keyCode == 39 || event.key && event.key == 'ArrowRight')) {return true;} 118 | else if(layout == 'vertical' && (event.keyCode && event.keyCode == 40 || event.key && event.key == 'ArrowDown')) {return true;} 119 | else {return false;} 120 | }; 121 | 122 | function tabNavigatePrev(event, layout) { 123 | if(layout == 'horizontal' && (event.keyCode && event.keyCode == 37 || event.key && event.key == 'ArrowLeft')) {return true;} 124 | else if(layout == 'vertical' && (event.keyCode && event.keyCode == 38 || event.key && event.key == 'ArrowUp')) {return true;} 125 | else {return false;} 126 | }; 127 | 128 | window.Tab = Tab; 129 | 130 | //initialize the Tab objects 131 | var tabs = document.getElementsByClassName('js-tabs'); 132 | if( tabs.length > 0 ) { 133 | for( var i = 0; i < tabs.length; i++) { 134 | (function(i){new Tab(tabs[i]);})(i); 135 | } 136 | } 137 | }()); 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI components library for Laravel Blade, crafted with TailwindCSS and Javascript for simplicity and elegance. 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/combindma/dash-ui.svg?style=flat-square)](https://packagist.org/packages/combindma/dash-ui) 4 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/combindma/dash-ui/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/combindma/dash-ui/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/combindma/dash-ui.svg?style=flat-square)](https://packagist.org/packages/combindma/dash-ui) 6 | 7 | 8 | If you ever dreamed of having a Shopify admin, DashUI offers a suite of UI components, all inspired by [Shopify Polaris](https://polaris.shopify.com/components), exclusively crafted with TailwindCSS, Laravel Blade and Javascript. These components are designed for effortless integration and offer various customization options. 9 | 10 | ## About Combind Agency 11 | 12 | [Combine Agency](https://combind.ma?utm_source=github&utm_medium=banner&utm_campaign=package_name) is a leading web development agency specializing in building innovative and high-performance web applications using modern technologies. Our experienced team of developers, designers, and project managers is dedicated to providing top-notch services tailored to the unique needs of our clients. 13 | 14 | If you need assistance with your next project or would like to discuss a custom solution, please feel free to [contact us](mailto:hello@combind.ma) or visit our [website](https://combind.ma?utm_source=github&utm_medium=banner&utm_campaign=package_name) for more information about our services. Let's build something amazing together! 15 | 16 | ## Demo 17 | Experience DashUI in action by visiting the [Demo Project](https://github.com/combindma/demo-dashui). The demo provides a practical showcase of the DashUI components, allowing you to see how they can be integrated and customized in a real Laravel application. 18 | 19 | ## Installation 20 | 21 | You can install the package via composer: 22 | 23 | ```bash 24 | composer require combindma/dash-ui 25 | ``` 26 | 27 | Optionally, if you intend to use [Blade Google Material Design Icons](https://github.com/codeat3/blade-google-material-design-icons) as it is the case in the demo, run this command: 28 | 29 | ```bash 30 | composer require codeat3/blade-google-material-design-icons 31 | ``` 32 | 33 | We recommend you to enable icon caching using: 34 | ```bash 35 | php artisan icons:cache 36 | ``` 37 | 38 | Optionally, you can publish the views using: 39 | 40 | ```bash 41 | php artisan vendor:publish --tag="dash-ui-views" 42 | ``` 43 | 44 | ## Setup 45 | 46 | #### 1. Installing Tailwind CSS 47 | Install tailwindcss and its peer dependencies via npm. 48 | ```bash 49 | npm install -D tailwindcss postcss @tailwindcss/postcss @tailwindcss/aspect-ratio @tailwindcss/forms @tailwindcss/typography 50 | ``` 51 | 52 | #### 2. Add Tailwind to your PostCSS configuration 53 | Add @tailwindcss/postcss to your postcss.config.mjs file, or wherever PostCSS is configured in your project. 54 | ```javascript 55 | export default { 56 | plugins: { 57 | "@tailwindcss/postcss": {}, 58 | } 59 | } 60 | ``` 61 | 62 | #### 3. Import Dashui CSS 63 | Import the css files and add the @tailwind and source directives to your ./resources/css/tailwind.css file. 64 | 65 | TIP: You can specify your primary color by editing primary colors. 66 | ```css 67 | @import 'tailwindcss'; 68 | @import '../../vendor/combindma/dash-ui/resources/css/dashui.css'; 69 | 70 | @plugin '@tailwindcss/forms'; 71 | @plugin '@tailwindcss/aspect-ratio'; 72 | @plugin '@tailwindcss/typography'; 73 | 74 | @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; 75 | @source '../../vendor/combindma/dash-ui/resources/views/**/*.blade.php'; 76 | @source '../../storage/framework/views/*.php'; 77 | @source '../**/*.blade.php'; 78 | 79 | 80 | @custom-variant dark (&:is(.dark *)); 81 | 82 | @theme { 83 | --font-sans: Inter, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 84 | 85 | --text-*: initial; 86 | --text-xs: 0.75rem; 87 | --text-sm: 0.8125rem; 88 | --text-base: 0.875rem; 89 | --text-lg: 1.25rem; 90 | --text-xl: 1.5rem; 91 | --text-2xl: 1.875rem; 92 | --text-3xl: 2.25rem; 93 | --text-4xl: 3.052rem; 94 | 95 | --color-primary-50: #fafaf9; 96 | --color-primary-100: #f5f5f4; 97 | --color-primary-200: #e7e5e4; 98 | --color-primary-300: #d6d3d1; 99 | --color-primary-400: #a8a29e; 100 | --color-primary-500: #78716c; 101 | --color-primary-600: #57534e; 102 | --color-primary-700: #44403c; 103 | --color-primary-800: #292524; 104 | --color-primary-900: #1c1917; 105 | --color-primary-950: #0c0a09; 106 | } 107 | ``` 108 | 109 | #### 4. Import javascript components to your js file 110 | Import the js file to your ./resources/js/app.js file. 111 | ```javascript 112 | import '../../vendor/combindma/dash-ui/resources/js/dashui.js'; 113 | ``` 114 | 115 | #### 5. Update vite config file 116 | Add this to your file vite.config.js 117 | ```javascript 118 | import { defineConfig } from 'vite'; 119 | import laravel from 'laravel-vite-plugin'; 120 | 121 | export default defineConfig({ 122 | plugins: [ 123 | laravel({ 124 | input: ['resources/css/tailwind.css', 'resources/js/app.js'], 125 | refresh: true, 126 | }), 127 | ], 128 | }); 129 | ``` 130 | 131 | #### 6. Start your build process 132 | Run your build process with 133 | ```bash 134 | npm run build 135 | ``` 136 | 137 | #### 7.Start using Dash UI in your project 138 | Make sure your compiled CSS and Javascript are included in your main layout. 139 | ```html 140 | 141 | 142 | 143 | 144 | 145 | Laravel 146 | 147 | 148 | @vite(['resources/css/tailwind.css']) 149 | 150 | 151 | 152 | @vite(['resources/js/app.js']) 153 | 154 | 155 | ``` 156 | 157 | ## Usage 158 | See the full [documentation](https://combind.notion.site/Dash-UI-288a0eaa11854c69acae5da7842ee788?pvs=4) for all components and how to use them. 159 | 160 | ## Security Vulnerabilities 161 | 162 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 163 | 164 | ## Credits 165 | 166 | - [Combind](https://github.com/Combind) 167 | - [All Contributors](../../contributors) 168 | 169 | ## License 170 | 171 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 172 | -------------------------------------------------------------------------------- /resources/views/components/banner.blade.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | @if($title) 5 |
    ($tone == 'info'), 8 | 'bg-emerald-600 text-white' => ($tone == 'success'), 9 | 'bg-yellow-500' => ($tone == 'warning'), 10 | 'bg-red-600 text-white' => ($tone == 'critical'), 11 | ])> 12 |
    13 | @if($tone == 'success') 14 | 15 | @elseif($tone == 'warning') 16 | 17 | @elseif($tone == 'critical') 18 | 19 | @else 20 | 21 | @endif 22 | {{ $title }} 23 |
    24 |
    25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
    34 |
    35 | @endif 36 |
    37 |
    38 | @if(!$title) 39 |
    ($tone == 'info'), 42 | 'bg-emerald-600 text-white' => ($tone == 'success'), 43 | 'bg-yellow-500' => ($tone == 'warning'), 44 | 'bg-red-600 text-white' => ($tone == 'critical'), 45 | ])> 46 | @if($tone == 'success') 47 | 48 | @elseif($tone == 'warning') 49 | 50 | @elseif($tone == 'critical') 51 | 52 | @else 53 | 54 | @endif 55 |
    56 | @endif 57 |
    class(['text-sm']) }}>{{ $slot }}
    58 |
    59 | @if(!$title) 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | @endif 69 |
    70 |
    71 |
    72 |
    73 | -------------------------------------------------------------------------------- /resources/css/components/_1_modal-window.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------- 2 | 3 | File#: _1_modal-window 4 | Title: Modal Window 5 | Descr: A modal dialog used to display critical information 6 | Usage: codyhouse.co/license 7 | 8 | -------------------------------- */ 9 | .modal { 10 | position: fixed; 11 | z-index: 15; 12 | width: 100%; 13 | height: 100%; 14 | left: 0; 15 | top: 0; 16 | opacity: 0; 17 | visibility: hidden; 18 | } 19 | .modal:not(.modal--is-visible) { 20 | pointer-events: none; 21 | background-color: transparent; 22 | } 23 | 24 | .modal--is-visible { 25 | opacity: 1; 26 | visibility: visible; 27 | } 28 | 29 | /* close button */ 30 | .modal__close-btn { 31 | display: flex; 32 | flex-shrink: 0; 33 | border-radius: 50%; 34 | transition: 0.2s; 35 | } 36 | .modal__close-btn .icon { 37 | display: block; 38 | margin: auto; 39 | } 40 | 41 | .modal__close-btn--outer { 42 | /* close button - outside the modal__content */ 43 | width: 48px; 44 | height: 48px; 45 | position: fixed; 46 | @apply top-3 lg:top-5; 47 | @apply right-3 lg:right-5; 48 | @apply z-10; 49 | @apply bg-gray-900/90; 50 | transition: 0.2s; 51 | } 52 | .modal__close-btn--outer .icon { 53 | @apply text-white; 54 | /* icon color */ 55 | transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); 56 | } 57 | .modal__close-btn--outer:hover { 58 | @apply bg-gray-900/100; 59 | } 60 | .modal__close-btn--outer:hover .icon { 61 | transform: scale(1.1); 62 | } 63 | 64 | .modal__close-btn--inner { 65 | /* close button - inside the modal__content */ 66 | --size: 32px; 67 | width: var(--size); 68 | height: var(--size); 69 | @apply bg-white; 70 | @apply shadow-md; 71 | transition: 0.2s; 72 | } 73 | .modal__close-btn--inner .icon { 74 | color: inherit; 75 | /* icon color */ 76 | } 77 | .modal__close-btn--inner:hover { 78 | @apply bg-white; 79 | @apply shadow-lg; 80 | } 81 | 82 | /* animations */ 83 | :root { 84 | --modal-transition-duration: 0.2s; 85 | /* fallback (i.e., unless specified differently in the variations 👇) */ 86 | } 87 | 88 | @media (prefers-reduced-motion: no-preference) { 89 | .modal--animate-fade { 90 | --modal-transition-duration: 0.2s; 91 | transition: opacity var(--modal-transition-duration), background-color var(--modal-transition-duration), visibility 0s var(--modal-transition-duration); 92 | } 93 | .modal--animate-fade.modal--is-visible { 94 | transition: opacity var(--modal-transition-duration), background-color var(--modal-transition-duration), visibility 0s; 95 | } 96 | 97 | .modal--animate-scale, 98 | .modal--animate-translate-up, 99 | .modal--animate-translate-down, 100 | .modal--animate-translate-right, 101 | .modal--animate-translate-left { 102 | --modal-transition-duration: 0.2s; 103 | transition: opacity var(--modal-transition-duration), background-color var(--modal-transition-duration), visibility 0s var(--modal-transition-duration); 104 | } 105 | .modal--animate-scale .modal__content, 106 | .modal--animate-translate-up .modal__content, 107 | .modal--animate-translate-down .modal__content, 108 | .modal--animate-translate-right .modal__content, 109 | .modal--animate-translate-left .modal__content { 110 | will-change: transform; 111 | transition: -webkit-transform var(--modal-transition-duration) cubic-bezier(0.215, 0.61, 0.355, 1); 112 | transition: transform var(--modal-transition-duration) cubic-bezier(0.215, 0.61, 0.355, 1); 113 | transition: transform var(--modal-transition-duration) cubic-bezier(0.215, 0.61, 0.355, 1), -webkit-transform var(--modal-transition-duration) cubic-bezier(0.215, 0.61, 0.355, 1); 114 | } 115 | .modal--animate-scale.modal--is-visible, 116 | .modal--animate-translate-up.modal--is-visible, 117 | .modal--animate-translate-down.modal--is-visible, 118 | .modal--animate-translate-right.modal--is-visible, 119 | .modal--animate-translate-left.modal--is-visible { 120 | transition: opacity var(--modal-transition-duration), background-color var(--modal-transition-duration), visibility 0s; 121 | } 122 | .modal--animate-scale.modal--is-visible .modal__content, 123 | .modal--animate-translate-up.modal--is-visible .modal__content, 124 | .modal--animate-translate-down.modal--is-visible .modal__content, 125 | .modal--animate-translate-right.modal--is-visible .modal__content, 126 | .modal--animate-translate-left.modal--is-visible .modal__content { 127 | transform: scale(1); 128 | /* reset all transformations */ 129 | } 130 | 131 | .modal--animate-slide-up, 132 | .modal--animate-slide-down, 133 | .modal--animate-slide-right, 134 | .modal--animate-slide-left { 135 | --modal-transition-duration: 0.3s; 136 | transition: opacity 0s var(--modal-transition-duration), background-color var(--modal-transition-duration), visibility 0s var(--modal-transition-duration); 137 | } 138 | .modal--animate-slide-up .modal__content, 139 | .modal--animate-slide-down .modal__content, 140 | .modal--animate-slide-right .modal__content, 141 | .modal--animate-slide-left .modal__content { 142 | will-change: transform; 143 | transition: -webkit-transform var(--modal-transition-duration) cubic-bezier(0.215, 0.61, 0.355, 1); 144 | transition: transform var(--modal-transition-duration) cubic-bezier(0.215, 0.61, 0.355, 1); 145 | transition: transform var(--modal-transition-duration) cubic-bezier(0.215, 0.61, 0.355, 1), -webkit-transform var(--modal-transition-duration) cubic-bezier(0.215, 0.61, 0.355, 1); 146 | } 147 | .modal--animate-slide-up.modal--is-visible, 148 | .modal--animate-slide-down.modal--is-visible, 149 | .modal--animate-slide-right.modal--is-visible, 150 | .modal--animate-slide-left.modal--is-visible { 151 | transition: background-color var(--modal-transition-duration), visibility 0s; 152 | } 153 | .modal--animate-slide-up.modal--is-visible .modal__content, 154 | .modal--animate-slide-down.modal--is-visible .modal__content, 155 | .modal--animate-slide-right.modal--is-visible .modal__content, 156 | .modal--animate-slide-left.modal--is-visible .modal__content { 157 | transform: scale(1); 158 | /* reset all transformations */ 159 | } 160 | 161 | /* scale */ 162 | .modal--animate-scale .modal__content { 163 | -webkit-transform: scale(0.95); transform: scale(0.95); 164 | } 165 | 166 | /* translate */ 167 | .modal--animate-translate-up .modal__content { 168 | -webkit-transform: translateY(40px); transform: translateY(40px); 169 | } 170 | 171 | .modal--animate-translate-down .modal__content { 172 | -webkit-transform: translateY(-40px); transform: translateY(-40px); 173 | } 174 | 175 | .modal--animate-translate-right .modal__content { 176 | -webkit-transform: translateX(-40px); transform: translateX(-40px); 177 | } 178 | 179 | .modal--animate-translate-left .modal__content { 180 | -webkit-transform: translateX(40px); transform: translateX(40px); 181 | } 182 | 183 | /* slide */ 184 | .modal--animate-slide-up .modal__content { 185 | transform: translateY(100%); 186 | } 187 | 188 | .modal--animate-slide-down .modal__content { 189 | transform: translateY(-100%); 190 | } 191 | 192 | .modal--animate-slide-right .modal__content { 193 | -webkit-transform: translateX(-100%); transform: translateX(-100%); 194 | } 195 | 196 | .modal--animate-slide-left .modal__content { 197 | -webkit-transform: translateX(100%); transform: translateX(100%); 198 | } 199 | } 200 | /* load content - optional */ 201 | .modal--is-loading .modal__content { 202 | visibility: hidden; 203 | } 204 | .modal--is-loading .modal__loader { 205 | display: flex; 206 | } 207 | 208 | .modal__loader { 209 | /* loader icon */ 210 | position: fixed; 211 | top: 0; 212 | left: 0; 213 | width: 100%; 214 | height: 100%; 215 | justify-content: center; 216 | align-items: center; 217 | display: none; 218 | pointer-events: none; 219 | } 220 | 221 | /* --image */ 222 | .modal-img-btn { 223 | position: relative; 224 | cursor: pointer; 225 | } 226 | .modal-img-btn::after { 227 | content: ""; 228 | position: absolute; 229 | z-index: 1; 230 | top: 0; 231 | left: 0; 232 | width: 100%; 233 | height: 100%; 234 | @apply bg-gray-900/0; 235 | transition: background 0.2s; 236 | } 237 | .modal-img-btn:hover::after { 238 | @apply bg-gray-900/70; 239 | } 240 | .modal-img-btn:hover .modal-img-btn__icon-wrapper { 241 | opacity: 1; 242 | } 243 | 244 | .modal-img-btn__icon-wrapper { 245 | position: absolute; 246 | z-index: 2; 247 | top: calc(50% - 24px); 248 | left: calc(50% - 24px); 249 | width: 48px; 250 | height: 48px; 251 | display: inline-flex; 252 | align-items: center; 253 | justify-content: center; 254 | border-radius: 50%; 255 | @apply bg-gray-900/70; 256 | opacity: 0; 257 | transition: opacity 0.2s; 258 | } 259 | .modal-img-btn__icon-wrapper .icon { 260 | @apply text-white; 261 | } 262 | 263 | .modal .momentum-scrolling { 264 | -webkit-overflow-scrolling: touch; 265 | } 266 | 267 | /* icon loading animation */ 268 | .icon--is-spinning { 269 | animation: icon-spin 1s infinite linear; 270 | } 271 | 272 | @keyframes icon-spin { 273 | 0% { 274 | transform: rotate(0deg); 275 | } 276 | 100% { 277 | transform: rotate(360deg); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /resources/js/components/_1_modal-window.js: -------------------------------------------------------------------------------- 1 | // File#: _1_modal-window 2 | // Usage: codyhouse.co/license 3 | (function() { 4 | var Modal = function(element) { 5 | this.element = element; 6 | this.triggers = document.querySelectorAll('[aria-controls="'+this.element.getAttribute('id')+'"]'); 7 | this.firstFocusable = null; 8 | this.lastFocusable = null; 9 | this.moveFocusEl = null; // focus will be moved to this element when modal is open 10 | this.modalFocus = this.element.getAttribute('data-modal-first-focus') ? this.element.querySelector(this.element.getAttribute('data-modal-first-focus')) : null; 11 | this.selectedTrigger = null; 12 | this.preventScrollEl = this.getPreventScrollEl(); 13 | this.showClass = "modal--is-visible"; 14 | this.initModal(); 15 | }; 16 | 17 | Modal.prototype.getPreventScrollEl = function() { 18 | var scrollEl = false; 19 | var querySelector = this.element.getAttribute('data-modal-prevent-scroll'); 20 | if(querySelector) scrollEl = document.querySelector(querySelector); 21 | return scrollEl; 22 | }; 23 | 24 | Modal.prototype.initModal = function() { 25 | var self = this; 26 | //open modal when clicking on trigger buttons 27 | if ( this.triggers ) { 28 | for(var i = 0; i < this.triggers.length; i++) { 29 | this.triggers[i].addEventListener('click', function(event) { 30 | event.preventDefault(); 31 | if(self.element.classList.contains(self.showClass)) { 32 | self.closeModal(); 33 | return; 34 | } 35 | self.selectedTrigger = event.currentTarget; 36 | self.showModal(); 37 | self.initModalEvents(); 38 | }); 39 | } 40 | } 41 | 42 | // listen to the openModal event -> open modal without a trigger button 43 | this.element.addEventListener('openModal', function(event){ 44 | if(event.detail) self.selectedTrigger = event.detail; 45 | self.showModal(); 46 | self.initModalEvents(); 47 | }); 48 | 49 | // listen to the closeModal event -> close modal without a trigger button 50 | this.element.addEventListener('closeModal', function(event){ 51 | if(event.detail) self.selectedTrigger = event.detail; 52 | self.closeModal(); 53 | }); 54 | 55 | // if modal is open by default -> initialise modal events 56 | if(this.element.classList.contains(this.showClass)) this.initModalEvents(); 57 | }; 58 | 59 | Modal.prototype.showModal = function() { 60 | var self = this; 61 | this.element.classList.add(this.showClass); 62 | this.getFocusableElements(); 63 | if(this.moveFocusEl) { 64 | this.moveFocusEl.focus(); 65 | // wait for the end of transitions before moving focus 66 | this.element.addEventListener("transitionend", function cb(event) { 67 | self.moveFocusEl.focus(); 68 | self.element.removeEventListener("transitionend", cb); 69 | }); 70 | } 71 | this.emitModalEvents('modalIsOpen'); 72 | // change the overflow of the preventScrollEl 73 | if(this.preventScrollEl) this.preventScrollEl.style.overflow = 'hidden'; 74 | }; 75 | 76 | Modal.prototype.closeModal = function() { 77 | if(!this.element.classList.contains(this.showClass)) return; 78 | this.element.classList.remove(this.showClass); 79 | this.firstFocusable = null; 80 | this.lastFocusable = null; 81 | this.moveFocusEl = null; 82 | if(this.selectedTrigger) this.selectedTrigger.focus(); 83 | //remove listeners 84 | this.cancelModalEvents(); 85 | this.emitModalEvents('modalIsClose'); 86 | // change the overflow of the preventScrollEl 87 | if(this.preventScrollEl) this.preventScrollEl.style.overflow = ''; 88 | }; 89 | 90 | Modal.prototype.initModalEvents = function() { 91 | //add event listeners 92 | this.element.addEventListener('keydown', this); 93 | this.element.addEventListener('click', this); 94 | }; 95 | 96 | Modal.prototype.cancelModalEvents = function() { 97 | //remove event listeners 98 | this.element.removeEventListener('keydown', this); 99 | this.element.removeEventListener('click', this); 100 | }; 101 | 102 | Modal.prototype.handleEvent = function (event) { 103 | switch(event.type) { 104 | case 'click': { 105 | this.initClick(event); 106 | } 107 | case 'keydown': { 108 | this.initKeyDown(event); 109 | } 110 | } 111 | }; 112 | 113 | Modal.prototype.initKeyDown = function(event) { 114 | if( event.keyCode && event.keyCode == 9 || event.key && event.key == 'Tab' ) { 115 | //trap focus inside modal 116 | this.trapFocus(event); 117 | } else if( (event.keyCode && event.keyCode == 13 || event.key && event.key == 'Enter') && event.target.closest('.js-modal__close')) { 118 | event.preventDefault(); 119 | this.closeModal(); // close modal when pressing Enter on close button 120 | } 121 | }; 122 | 123 | Modal.prototype.initClick = function(event) { 124 | //close modal when clicking on close button or modal bg layer 125 | if( !event.target.closest('.js-modal__close') && !event.target.classList.contains('js-modal') ) return; 126 | event.preventDefault(); 127 | this.closeModal(); 128 | }; 129 | 130 | Modal.prototype.trapFocus = function(event) { 131 | if( this.firstFocusable == document.activeElement && event.shiftKey) { 132 | //on Shift+Tab -> focus last focusable element when focus moves out of modal 133 | event.preventDefault(); 134 | this.lastFocusable.focus(); 135 | } 136 | if( this.lastFocusable == document.activeElement && !event.shiftKey) { 137 | //on Tab -> focus first focusable element when focus moves out of modal 138 | event.preventDefault(); 139 | this.firstFocusable.focus(); 140 | } 141 | } 142 | 143 | Modal.prototype.getFocusableElements = function() { 144 | //get all focusable elements inside the modal 145 | var allFocusable = this.element.querySelectorAll(focusableElString); 146 | this.getFirstVisible(allFocusable); 147 | this.getLastVisible(allFocusable); 148 | this.getFirstFocusable(); 149 | }; 150 | 151 | Modal.prototype.getFirstVisible = function(elements) { 152 | //get first visible focusable element inside the modal 153 | for(var i = 0; i < elements.length; i++) { 154 | if( isVisible(elements[i]) ) { 155 | this.firstFocusable = elements[i]; 156 | break; 157 | } 158 | } 159 | }; 160 | 161 | Modal.prototype.getLastVisible = function(elements) { 162 | //get last visible focusable element inside the modal 163 | for(var i = elements.length - 1; i >= 0; i--) { 164 | if( isVisible(elements[i]) ) { 165 | this.lastFocusable = elements[i]; 166 | break; 167 | } 168 | } 169 | }; 170 | 171 | Modal.prototype.getFirstFocusable = function() { 172 | if(!this.modalFocus || !Element.prototype.matches) { 173 | this.moveFocusEl = this.firstFocusable; 174 | return; 175 | } 176 | var containerIsFocusable = this.modalFocus.matches(focusableElString); 177 | if(containerIsFocusable) { 178 | this.moveFocusEl = this.modalFocus; 179 | } else { 180 | this.moveFocusEl = false; 181 | var elements = this.modalFocus.querySelectorAll(focusableElString); 182 | for(var i = 0; i < elements.length; i++) { 183 | if( isVisible(elements[i]) ) { 184 | this.moveFocusEl = elements[i]; 185 | break; 186 | } 187 | } 188 | if(!this.moveFocusEl) this.moveFocusEl = this.firstFocusable; 189 | } 190 | }; 191 | 192 | Modal.prototype.emitModalEvents = function(eventName) { 193 | var event = new CustomEvent(eventName, {detail: this.selectedTrigger}); 194 | this.element.dispatchEvent(event); 195 | }; 196 | 197 | function isVisible(element) { 198 | return element.offsetWidth || element.offsetHeight || element.getClientRects().length; 199 | }; 200 | 201 | window.Modal = Modal; 202 | 203 | //initialize the Modal objects 204 | var modals = document.getElementsByClassName('js-modal'); 205 | // generic focusable elements string selector 206 | var focusableElString = '[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable], audio[controls], video[controls], summary'; 207 | if( modals.length > 0 ) { 208 | var modalArrays = []; 209 | for( var i = 0; i < modals.length; i++) { 210 | (function(i){modalArrays.push(new Modal(modals[i]));})(i); 211 | } 212 | 213 | window.addEventListener('keydown', function(event){ //close modal window on esc 214 | if(event.keyCode && event.keyCode == 27 || event.key && event.key.toLowerCase() == 'escape') { 215 | for( var i = 0; i < modalArrays.length; i++) { 216 | (function(i){modalArrays[i].closeModal();})(i); 217 | }; 218 | } 219 | }); 220 | } 221 | }()); 222 | -------------------------------------------------------------------------------- /resources/js/components/_1_responsive-sidebar.js: -------------------------------------------------------------------------------- 1 | import { Util } from '../_util.js'; 2 | 3 | // File#: _1_responsive-sidebar 4 | // Usage: codyhouse.co/license 5 | (function() { 6 | var Sidebar = function(element) { 7 | this.element = element; 8 | this.triggers = document.querySelectorAll('[aria-controls="'+this.element.getAttribute('id')+'"]'); 9 | this.firstFocusable = null; 10 | this.lastFocusable = null; 11 | this.selectedTrigger = null; 12 | this.showClass = "sidebar--is-visible"; 13 | this.staticClass = "sidebar--static"; 14 | this.customStaticClass = ""; 15 | this.readyClass = "sidebar--loaded"; 16 | this.contentReadyClass = "sidebar-loaded:show"; 17 | this.layout = false; // this will be static or mobile 18 | this.preventScrollEl = getPreventScrollEl(this); 19 | getCustomStaticClass(this); // custom classes for static version 20 | initSidebar(this); 21 | }; 22 | 23 | function getPreventScrollEl(element) { 24 | var scrollEl = false; 25 | var querySelector = element.element.getAttribute('data-sidebar-prevent-scroll'); 26 | if(querySelector) scrollEl = document.querySelector(querySelector); 27 | return scrollEl; 28 | }; 29 | 30 | function getCustomStaticClass(element) { 31 | var customClasses = element.element.getAttribute('data-static-class'); 32 | if(customClasses) element.customStaticClass = ' '+customClasses; 33 | }; 34 | 35 | function initSidebar(sidebar) { 36 | initSidebarResize(sidebar); // handle changes in layout -> mobile to static and viceversa 37 | 38 | if ( sidebar.triggers ) { // open sidebar when clicking on trigger buttons - mobile layout only 39 | for(var i = 0; i < sidebar.triggers.length; i++) { 40 | sidebar.triggers[i].addEventListener('click', function(event) { 41 | event.preventDefault(); 42 | toggleSidebar(sidebar, event.target); 43 | }); 44 | } 45 | } 46 | 47 | // use the 'openSidebar' event to trigger the sidebar 48 | sidebar.element.addEventListener('openSidebar', function(event) { 49 | toggleSidebar(sidebar, event.detail); 50 | }); 51 | }; 52 | 53 | function toggleSidebar(sidebar, target) { 54 | if(Util.hasClass(sidebar.element, sidebar.showClass)) { 55 | sidebar.selectedTrigger = target; 56 | closeSidebar(sidebar); 57 | return; 58 | } 59 | sidebar.selectedTrigger = target; 60 | showSidebar(sidebar); 61 | initSidebarEvents(sidebar); 62 | }; 63 | 64 | function showSidebar(sidebar) { // mobile layout only 65 | Util.addClass(sidebar.element, sidebar.showClass); 66 | getFocusableElements(sidebar); 67 | Util.moveFocus(sidebar.element); 68 | // change the overflow of the preventScrollEl 69 | if(sidebar.preventScrollEl) sidebar.preventScrollEl.style.overflow = 'hidden'; 70 | }; 71 | 72 | function closeSidebar(sidebar) { // mobile layout only 73 | Util.removeClass(sidebar.element, sidebar.showClass); 74 | sidebar.firstFocusable = null; 75 | sidebar.lastFocusable = null; 76 | if(sidebar.selectedTrigger) sidebar.selectedTrigger.focus(); 77 | sidebar.element.removeAttribute('tabindex'); 78 | //remove listeners 79 | cancelSidebarEvents(sidebar); 80 | // change the overflow of the preventScrollEl 81 | if(sidebar.preventScrollEl) sidebar.preventScrollEl.style.overflow = ''; 82 | }; 83 | 84 | function initSidebarEvents(sidebar) { // mobile layout only 85 | //add event listeners 86 | sidebar.element.addEventListener('keydown', handleEvent.bind(sidebar)); 87 | sidebar.element.addEventListener('click', handleEvent.bind(sidebar)); 88 | }; 89 | 90 | function cancelSidebarEvents(sidebar) { // mobile layout only 91 | //remove event listeners 92 | sidebar.element.removeEventListener('keydown', handleEvent.bind(sidebar)); 93 | sidebar.element.removeEventListener('click', handleEvent.bind(sidebar)); 94 | }; 95 | 96 | function handleEvent(event) { // mobile layout only 97 | switch(event.type) { 98 | case 'click': { 99 | initClick(this, event); 100 | } 101 | case 'keydown': { 102 | initKeyDown(this, event); 103 | } 104 | } 105 | }; 106 | 107 | function initKeyDown(sidebar, event) { // mobile layout only 108 | if( event.keyCode && event.keyCode == 27 || event.key && event.key == 'Escape' ) { 109 | //close sidebar window on esc 110 | closeSidebar(sidebar); 111 | } else if( event.keyCode && event.keyCode == 9 || event.key && event.key == 'Tab' ) { 112 | //trap focus inside sidebar 113 | trapFocus(sidebar, event); 114 | } 115 | }; 116 | 117 | function initClick(sidebar, event) { // mobile layout only 118 | //close sidebar when clicking on close button or sidebar bg layer 119 | if( !event.target.closest('.js-sidebar__close-btn') && !Util.hasClass(event.target, 'js-sidebar') ) return; 120 | event.preventDefault(); 121 | closeSidebar(sidebar); 122 | }; 123 | 124 | function trapFocus(sidebar, event) { // mobile layout only 125 | if( sidebar.firstFocusable == document.activeElement && event.shiftKey) { 126 | //on Shift+Tab -> focus last focusable element when focus moves out of sidebar 127 | event.preventDefault(); 128 | sidebar.lastFocusable.focus(); 129 | } 130 | if( sidebar.lastFocusable == document.activeElement && !event.shiftKey) { 131 | //on Tab -> focus first focusable element when focus moves out of sidebar 132 | event.preventDefault(); 133 | sidebar.firstFocusable.focus(); 134 | } 135 | }; 136 | 137 | function initSidebarResize(sidebar) { 138 | // custom event emitted when window is resized - detect only if the sidebar--static@{breakpoint} class was added 139 | var beforeContent = getComputedStyle(sidebar.element, ':before').getPropertyValue('content'); 140 | if(beforeContent && beforeContent !='' && beforeContent !='none') { 141 | checkSidebarLayout(sidebar); 142 | 143 | sidebar.element.addEventListener('update-sidebar', function(event){ 144 | checkSidebarLayout(sidebar); 145 | }); 146 | } 147 | // check if there a main element to show 148 | var mainContent = document.getElementsByClassName(sidebar.contentReadyClass); 149 | if(mainContent.length > 0) Util.removeClass(mainContent[0], sidebar.contentReadyClass); 150 | Util.addClass(sidebar.element, sidebar.readyClass); 151 | }; 152 | 153 | function checkSidebarLayout(sidebar) { 154 | var layout = getComputedStyle(sidebar.element, ':before').getPropertyValue('content').replace(/\'|"/g, ''); 155 | if(layout == sidebar.layout) return; 156 | sidebar.layout = layout; 157 | if(layout != 'static') Util.addClass(sidebar.element, 'hidden'); 158 | Util.toggleClass(sidebar.element, sidebar.staticClass + sidebar.customStaticClass, layout == 'static'); 159 | if(layout != 'static') setTimeout(function(){Util.removeClass(sidebar.element, 'hidden')}); 160 | // reset element role 161 | (layout == 'static') ? sidebar.element.removeAttribute('role', 'alertdialog') : sidebar.element.setAttribute('role', 'alertdialog'); 162 | // reset mobile behaviour 163 | if(layout == 'static' && Util.hasClass(sidebar.element, sidebar.showClass)) closeSidebar(sidebar); 164 | }; 165 | 166 | function getFocusableElements(sidebar) { 167 | //get all focusable elements inside the drawer 168 | var allFocusable = sidebar.element.querySelectorAll('[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable], audio[controls], video[controls], summary'); 169 | getFirstVisible(sidebar, allFocusable); 170 | getLastVisible(sidebar, allFocusable); 171 | }; 172 | 173 | function getFirstVisible(sidebar, elements) { 174 | //get first visible focusable element inside the sidebar 175 | for(var i = 0; i < elements.length; i++) { 176 | if( elements[i].offsetWidth || elements[i].offsetHeight || elements[i].getClientRects().length ) { 177 | sidebar.firstFocusable = elements[i]; 178 | return true; 179 | } 180 | } 181 | }; 182 | 183 | function getLastVisible(sidebar, elements) { 184 | //get last visible focusable element inside the sidebar 185 | for(var i = elements.length - 1; i >= 0; i--) { 186 | if( elements[i].offsetWidth || elements[i].offsetHeight || elements[i].getClientRects().length ) { 187 | sidebar.lastFocusable = elements[i]; 188 | return true; 189 | } 190 | } 191 | }; 192 | 193 | window.Sidebar = Sidebar; 194 | 195 | //initialize the Sidebar objects 196 | var sidebar = document.getElementsByClassName('js-sidebar'); 197 | if( sidebar.length > 0 ) { 198 | for( var i = 0; i < sidebar.length; i++) { 199 | (function(i){new Sidebar(sidebar[i]);})(i); 200 | } 201 | // switch from mobile to static layout 202 | var customEvent = new CustomEvent('update-sidebar'); 203 | window.addEventListener('resize', function(event){ 204 | (!window.requestAnimationFrame) ? setTimeout(function(){resetLayout();}, 250) : window.requestAnimationFrame(resetLayout); 205 | }); 206 | 207 | (window.requestAnimationFrame) // init sidebar layout 208 | ? window.requestAnimationFrame(resetLayout) 209 | : resetLayout(); 210 | 211 | function resetLayout() { 212 | for( var i = 0; i < sidebar.length; i++) { 213 | (function(i){sidebar[i].dispatchEvent(customEvent)})(i); 214 | }; 215 | }; 216 | } 217 | }()); 218 | -------------------------------------------------------------------------------- /resources/js/components/_3_select-autocomplete.js: -------------------------------------------------------------------------------- 1 | import { Util } from '../_util.js'; 2 | 3 | // File#: _3_select-autocomplete 4 | // Usage: codyhouse.co/license 5 | (function() { 6 | var SelectAuto = function(element) { 7 | this.element = element; 8 | this.input = this.element.getElementsByClassName('js-autocomplete__input'); 9 | this.resetBtn = this.element.getElementsByClassName('js-select-auto__input-btn'); 10 | this.select = this.element.getElementsByClassName('js-select-auto__select'); 11 | this.selectedValue = false; // value of the