├── 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 |
2 |
Content is loading...
3 |
6 |
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 | class([
2 | 'block w-full box-border rounded-lg text-sm border-0 py-1 pl-3 pr-10 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 | ]) }}>
6 | {{ $slot }}
7 |
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 |
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 |
3 | {{ $label }}
4 | @if($helpText)
5 | {{ $helpText }}
6 | @endif
7 |
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 | class(['copy-to-clip js-copy-to-clip js-tooltip-trigger']) }}>
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/resources/views/components/navigation.blade.php:
--------------------------------------------------------------------------------
1 |
2 | {{ $slot }}
3 |
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 |
2 | Search
3 | class(['expandable-search__input appearance-none box-border border-0 pl-2 lg:pl-3 text-sm text-transparent border-0 overflow-hidden rounded-lg hover:cursor-pointer focus:ring-2 focus:ring-primary-700 js-expandable-search__input']) }}>
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 | !$iconLeft,
12 | 'left-0 ml-1' => $iconLeft
13 | ])>
14 |
15 |
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 |
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 |
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 | !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'] }}
18 | @endforeach
19 |
20 |
21 |
22 | {{ $slot }}
23 |
24 |
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 | class([
23 | 'text-left block px-2 py-1.5 rounded-sm lg:rounded-md hover:cursor-pointer',
24 | 'hover:bg-neutral-100 active:bg-neutral-200/70' => !$active && !$destructive,
25 | 'bg-neutral-200/70' => $active,
26 | 'text-red-800 hover:bg-red-100/80 active:bg-red-200/60' => $destructive,
27 | ]) }}>
28 |
29 | @if(isset($icon))
30 | {{ $icon }}
31 | @endif
32 | {{ $label }}
33 | @if(isset($suffix))
34 | {{ $suffix }}
35 | @endif
36 |
37 | @if(isset($helpText))
38 | {{ $helpText }}
39 | @endif
40 |
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 | class([
22 | 'btn',
23 | 'group' => (!$pressed && !$disabled && ($variant !== 'plain') && ($variant !== 'subtle')),
24 | 'btn--primary' => ($variant === 'primary'),
25 | 'btn--primary-critical' => ($variant === 'primary' && $tone === 'critical'),
26 | 'btn--primary-success' => ($variant === 'primary' && $tone === 'success'),
27 | 'btn--secondary' => ($variant === 'secondary'),
28 | 'btn--subtle' => ($variant === 'subtle'),
29 | 'btn--plain' => ($variant === 'plain'),
30 | 'btn--plain-critical' => ($variant === 'plain' && $tone === 'critical'),
31 | 'btn--pressed' => $pressed,
32 | 'py-3 text-sm' => ($size === 'large'),
33 | 'w-full justify-center' => $fullWidth,
34 | ]) }}>
35 | !$disabled])>{{ $slot }}
36 |
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 |
8 |
13 |
14 | {{ $searchField }}
15 |
16 |
17 |
18 | {{ $userName }}
19 | @if($avatar)
20 |
21 | @else
22 |
23 | @endif
24 |
25 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/resources/views/components/autocomplete.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
class(['js-autocomplete__input']) }} />
4 |
5 |
10 |
11 |
12 |
13 |
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 |
47 | @endif
48 |
49 |
50 |
--------------------------------------------------------------------------------
/resources/views/components/combobox.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
38 |
0 {{ $selectedText }}
39 |
{{ $resetText }}
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/resources/views/components/select-auto.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
class(['js-select-auto__select hidden']) }}>
4 | {{ $slot }}
5 |
6 |
7 |
8 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | @if(isset($template))
43 |
44 | {{ $template }}
45 |
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 | [](https://packagist.org/packages/combindma/dash-ui)
4 | [](https://github.com/combindma/dash-ui/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain)
5 | [](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 the user selected
12 | this.selectOptions = []; // autocomplete list extracted from the element
13 | this.focusOutId = false; // keep track of focus status
14 | this.autocompleteResults = this.element.getElementsByClassName('js-autocomplete__results');
15 | initSelectAuto(this);
16 | };
17 |
18 | function initSelectAuto(element) {
19 | if(element.select.length == 0) return;
20 | initDataResults(element); // populate autocomplete list
21 | Util.addClass(element.select[0], 'hidden'); // hide native element
22 | setInitialSelection(element);
23 | initAutocomplete(element);
24 | initSelectAutoEvents(element);
25 | };
26 |
27 | function initDataResults(element) {
28 | // create the list of possible results based on the input
29 | var optgroups = element.select[0].getElementsByTagName('optgroup');
30 | if(optgroups.length > 0) {
31 | var directChildren = element.select[0].children;
32 | for(var i = 0; i < directChildren.length; i++) {
33 | var childType = directChildren[i].tagName.toLowerCase();
34 | if(childType == 'option') pushOptions(element, [directChildren[i]]);
35 | else if(childType == 'optgroup') pushOptgroup(element, directChildren[i]);
36 | }
37 | } else {
38 | // no s -> loop through
39 | pushOptions(element, element.select[0].getElementsByTagName('option'));
40 | }
41 | };
42 |
43 | function pushOptgroup(element, optgroup) {
44 | // push item
45 | var item = {};
46 | item.label = optgroup.getAttribute('label');
47 | item.template = 'optgroup';
48 | item = setCustomData(item, optgroup);
49 | element.selectOptions.push(item);
50 | // now push s
51 | pushOptions(element, optgroup.getElementsByTagName('option'));
52 | };
53 |
54 | function pushOptions(element, options) {
55 | for(var i = 0; i < options.length; i++) {
56 | pushSingleOption(element, options[i]);
57 | };
58 | };
59 |
60 | function pushSingleOption(element, option) {
61 | // do not push s without a value
62 | if(!option.getAttribute('value')) return;
63 | var item = {};
64 | item.label = option.text;
65 | item.template = 'option';
66 | item.value = option.value;
67 | item = setCustomData(item, option);
68 | element.selectOptions.push(item);
69 | };
70 |
71 | function setCustomData(obj, element) {
72 | // get custom data-attributes added to s/ s and add them to the autocomplete list
73 | var dataset = element.dataset;
74 | for (var prop in dataset) {
75 | if (Object.prototype.hasOwnProperty.call(dataset, prop)) {
76 | obj[prop] = dataset[prop];
77 | }
78 | }
79 | return obj;
80 | };
81 |
82 | function initAutocomplete(element) {
83 | // CodyHouse Autocomplete component
84 | // more info: https://codyhouse.co/ds/components/info/autocomplete
85 | new Autocomplete({
86 | element: element.element,
87 | characters: 0,
88 | searchData: function(value, cb, eventType) {
89 | selectAutoSearch(element, value, cb, eventType);
90 | },
91 | onClick: function(option, obj, event, cb) {
92 | selectAutoClick(element, option, obj, event, cb);
93 | }
94 | });
95 | };
96 |
97 | function selectAutoSearch(element, query, cb, eventType) {
98 | // get search results
99 | // more info: https://codyhouse.co/ds/components/info/autocomplete#search-data
100 |
101 | if(eventType == 'focus') {
102 | // show all results when input is first in focus
103 | var data = JSON.parse(JSON.stringify(element.selectOptions));
104 | } else {
105 | // filter results
106 | var data = element.selectOptions.filter(function(item){
107 | // return item if item['label'] contains 'query' or if it is an
108 | return (query == '' || item['template'] == 'optgroup') ? true : item['label'].toLowerCase().indexOf(query.toLowerCase()) > -1;
109 | });
110 |
111 | // remove empty s
112 | var i = data.length;
113 | while (i--) {
114 | if (data[i].template == 'optgroup' && ( i == data.length - 1 || data[i+1].template == 'optgroup') ) {
115 | data.splice(i, 1);
116 | }
117 | }
118 | }
119 | // add a custom class to the selected in the autocomplete list
120 | for(var i = 0; i < data.length; i++) {
121 | if(element.selectedValue && data[i].value && data[i].value == element.selectedValue && data[i].template != 'optgroup') {
122 | data[i].class = 'select-auto__option--selected';
123 | } else if(data[i].class) {
124 | delete data[i].class;
125 | }
126 | }
127 |
128 | if(data.length == 0) { // fallback for no results found
129 | data = [{
130 | label: 'No results',
131 | template: 'no-results'
132 | }];
133 | }
134 | // required by the Autocomplete component
135 | cb(data);
136 | };
137 |
138 | function selectAutoClick(element, option, obj, event, cb) {
139 | // an option in the autocomplete list has been selected
140 | if(option.getAttribute('data-autocomplete-template') != 'option') return;
141 | // get selected value + selected label
142 | var value = option.querySelector('[data-autocomplete-value]').innerText;
143 | var label = option.querySelector('[data-autocomplete-label]').innerText;
144 | resetSelectAuto(element, value, label);
145 | cb(); // this closes the autocomplete
146 | };
147 |
148 | function initSelectAutoEvents(element) {
149 | // on focus out -> reset input to initial value or to '' if the option was not selected
150 | element.input[0].addEventListener('focusout', function(event) {
151 | if(element.focusOutId) clearTimeout(element.focusOutId);
152 | element.focusOutId = setTimeout(function(){
153 | if(!element.element.contains(document.activeElement) || element.resetBtn[0].contains(document.activeElement)) {
154 | checkSelectAuto(element);
155 | }
156 | }, 100);
157 | });
158 |
159 | // when clicking on x -> reset selection to false
160 | if(element.resetBtn.length > 0) {
161 | element.resetBtn[0].addEventListener('click', function(event) {
162 | event.preventDefault();
163 | resetSelectAuto(element, false, '');
164 | element.input[0].focus();
165 | });
166 | }
167 | };
168 |
169 | function checkSelectAuto(element) {
170 | // check if we need to reset the value of the autocomplete input -> used when input loses focus
171 | var selectedLabel = !element.selectedValue ? '' : element.select[0].options[element.select[0].selectedIndex].text;
172 | if(element.input[0].value == selectedLabel) return;
173 |
174 | // user typed one of the possible options
175 | var optionInList = optionSelectedInList(element);
176 | if(optionInList[0]) {
177 | // update element and return
178 | resetSelectAuto(element, optionInList[2], optionInList[1]);
179 | return;
180 | }
181 |
182 | (element.input[0].value == '')
183 | ? resetSelectAuto(element, false, '')
184 | : resetSelectAuto(element, element.selectedValue, selectedLabel);
185 | };
186 |
187 | function optionSelectedInList(element) {
188 | var inList = false,
189 | label = '',
190 | value = false;
191 | for(var i = 0; i < element.selectOptions.length; i++) {
192 | if(element.selectOptions[i].template == 'option' && element.selectOptions[i].label.toLowerCase() == element.input[0].value.toLowerCase()) {
193 | inList = true;
194 | label = element.selectOptions[i].label;
195 | value = element.selectOptions[i].value;
196 | break;
197 | }
198 | }
199 | return [inList, label, value];
200 | };
201 |
202 | function resetSelectAuto(element, value, label) {
203 | // a new has been selected
204 | element.input[0].value = label;
205 | element.selectedValue = value;
206 | Util.toggleClass(element.element, 'select-auto--selection-done', value);
207 | if(value === false) { // no value set
208 | element.select[0].selectedIndex = -1;
209 | } else {
210 | element.select[0].value = value;
211 | }
212 | element.select[0].dispatchEvent(new Event('change'));
213 | };
214 |
215 | function setInitialSelection(element) {
216 | // if an option has the 'selected' attribute -> fill the input and add the selected class in the custome dropdown
217 | var selectedOption = element.select[0].querySelector('option[selected]');
218 | if(selectedOption) {
219 | // there's an option that is already selected
220 | var label = selectedOption.label;
221 | var value = selectedOption.value;
222 | element.input[0].value = label;
223 | element.selectedValue = value;
224 | Util.addClass(element.element, 'select-auto--selection-done');
225 | }
226 | };
227 |
228 | window.SelectAuto = SelectAuto;
229 |
230 | // init the SelectAuto object
231 | var selectAuto = document.getElementsByClassName('js-select-auto');
232 | if( selectAuto.length > 0 ) {
233 | for( var i = 0; i < selectAuto.length; i++) {
234 | (function(i){new SelectAuto(selectAuto[i]);})(i);
235 | }
236 | }
237 | }());
238 |
--------------------------------------------------------------------------------
/resources/js/components/_1_tooltip.js:
--------------------------------------------------------------------------------
1 | import { Util } from '../_util.js';
2 |
3 | // File#: _1_tooltip
4 | // Usage: codyhouse.co/license
5 | (function() {
6 | var Tooltip = function(element) {
7 | this.element = element;
8 | this.tooltip = false;
9 | this.tooltipIntervalId = false;
10 | this.tooltipContent = this.element.getAttribute('title');
11 | this.tooltipPosition = (this.element.getAttribute('data-tooltip-position')) ? this.element.getAttribute('data-tooltip-position') : 'top';
12 | this.tooltipClasses = (this.element.getAttribute('data-tooltip-class')) ? this.element.getAttribute('data-tooltip-class') : false;
13 | this.tooltipId = 'js-tooltip-element'; // id of the tooltip element -> trigger will have the same aria-describedby attr
14 | // there are cases where you only need the aria-label -> SR do not need to read the tooltip content (e.g., footnotes)
15 | this.tooltipDescription = (this.element.getAttribute('data-tooltip-describedby') && this.element.getAttribute('data-tooltip-describedby') == 'false') ? false : true;
16 |
17 | this.tooltipDelay = this.element.getAttribute('data-tooltip-delay'); // show tooltip after a delay (in ms)
18 | if(!this.tooltipDelay) this.tooltipDelay = 300;
19 | this.tooltipDelta = parseInt(this.element.getAttribute('data-tooltip-gap')); // distance beetwen tooltip and trigger element (in px)
20 | if(isNaN(this.tooltipDelta)) this.tooltipDelta = 10;
21 | this.tooltipTriggerHover = false;
22 | // tooltp sticky option
23 | this.tooltipSticky = (this.tooltipClasses && this.tooltipClasses.indexOf('tooltip--sticky') > -1);
24 | this.tooltipHover = false;
25 | if(this.tooltipSticky) {
26 | this.tooltipHoverInterval = false;
27 | }
28 | // tooltip triangle - css variable to control its position
29 | this.tooltipTriangleVar = '--tooltip-triangle-translate';
30 | resetTooltipContent(this);
31 | initTooltip(this);
32 | };
33 |
34 | function resetTooltipContent(tooltip) {
35 | var htmlContent = tooltip.element.getAttribute('data-tooltip-title');
36 | if(htmlContent) {
37 | tooltip.tooltipContent = htmlContent;
38 | }
39 | };
40 |
41 | function initTooltip(tooltipObj) {
42 | // reset trigger element
43 | tooltipObj.element.removeAttribute('title');
44 | tooltipObj.element.setAttribute('tabindex', '0');
45 | // add event listeners
46 | tooltipObj.element.addEventListener('mouseenter', handleEvent.bind(tooltipObj));
47 | tooltipObj.element.addEventListener('focus', handleEvent.bind(tooltipObj));
48 | };
49 |
50 | function removeTooltipEvents(tooltipObj) {
51 | // remove event listeners
52 | tooltipObj.element.removeEventListener('mouseleave', handleEvent.bind(tooltipObj));
53 | tooltipObj.element.removeEventListener('blur', handleEvent.bind(tooltipObj));
54 | };
55 |
56 | function handleEvent(event) {
57 | // handle events
58 | switch(event.type) {
59 | case 'mouseenter':
60 | case 'focus':
61 | showTooltip(this, event);
62 | break;
63 | case 'mouseleave':
64 | case 'blur':
65 | checkTooltip(this);
66 | break;
67 | case 'newContent':
68 | changeTooltipContent(this, event);
69 | break;
70 | }
71 | };
72 |
73 | function showTooltip(tooltipObj, event) {
74 | // tooltip has already been triggered
75 | if(tooltipObj.tooltipIntervalId) return;
76 | tooltipObj.tooltipTriggerHover = true;
77 | // listen to close events
78 | tooltipObj.element.addEventListener('mouseleave', handleEvent.bind(tooltipObj));
79 | tooltipObj.element.addEventListener('blur', handleEvent.bind(tooltipObj));
80 | // custom event to reset tooltip content
81 | tooltipObj.element.addEventListener('newContent', handleEvent.bind(tooltipObj));
82 |
83 | // show tooltip with a delay
84 | tooltipObj.tooltipIntervalId = setTimeout(function(){
85 | createTooltip(tooltipObj);
86 | }, tooltipObj.tooltipDelay);
87 | };
88 |
89 | function createTooltip(tooltipObj) {
90 | tooltipObj.tooltip = document.getElementById(tooltipObj.tooltipId);
91 |
92 | if( !tooltipObj.tooltip ) { // tooltip element does not yet exist
93 | tooltipObj.tooltip = document.createElement('div');
94 | document.body.appendChild(tooltipObj.tooltip);
95 | }
96 |
97 | // remove data-reset attribute that is used when updating tooltip content (newContent custom event)
98 | tooltipObj.tooltip.removeAttribute('data-reset');
99 |
100 | // reset tooltip content/position
101 | Util.setAttributes(tooltipObj.tooltip, {'id': tooltipObj.tooltipId, 'class': 'tooltip tooltip--is-hidden js-tooltip', 'role': 'tooltip'});
102 | tooltipObj.tooltip.innerHTML = tooltipObj.tooltipContent;
103 | if(tooltipObj.tooltipDescription) tooltipObj.element.setAttribute('aria-describedby', tooltipObj.tooltipId);
104 | if(tooltipObj.tooltipClasses) Util.addClass(tooltipObj.tooltip, tooltipObj.tooltipClasses);
105 | if(tooltipObj.tooltipSticky) Util.addClass(tooltipObj.tooltip, 'tooltip--sticky');
106 | placeTooltip(tooltipObj);
107 | Util.removeClass(tooltipObj.tooltip, 'tooltip--is-hidden');
108 |
109 | // if tooltip is sticky, listen to mouse events
110 | if(!tooltipObj.tooltipSticky) return;
111 | tooltipObj.tooltip.addEventListener('mouseenter', function cb(){
112 | tooltipObj.tooltipHover = true;
113 | if(tooltipObj.tooltipHoverInterval) {
114 | clearInterval(tooltipObj.tooltipHoverInterval);
115 | tooltipObj.tooltipHoverInterval = false;
116 | }
117 | tooltipObj.tooltip.removeEventListener('mouseenter', cb);
118 | tooltipLeaveEvent(tooltipObj);
119 | });
120 | };
121 |
122 | function tooltipLeaveEvent(tooltipObj) {
123 | tooltipObj.tooltip.addEventListener('mouseleave', function cb(){
124 | tooltipObj.tooltipHover = false;
125 | tooltipObj.tooltip.removeEventListener('mouseleave', cb);
126 | hideTooltip(tooltipObj);
127 | });
128 | };
129 |
130 | function placeTooltip(tooltipObj) {
131 | // set top and left position of the tooltip according to the data-tooltip-position attr of the trigger
132 | var dimention = [tooltipObj.tooltip.offsetHeight, tooltipObj.tooltip.offsetWidth],
133 | positionTrigger = tooltipObj.element.getBoundingClientRect(),
134 | position = [],
135 | scrollY = window.scrollY || window.pageYOffset;
136 |
137 | position['top'] = [ (positionTrigger.top - dimention[0] - tooltipObj.tooltipDelta + scrollY), (positionTrigger.right/2 + positionTrigger.left/2 - dimention[1]/2)];
138 | position['bottom'] = [ (positionTrigger.bottom + tooltipObj.tooltipDelta + scrollY), (positionTrigger.right/2 + positionTrigger.left/2 - dimention[1]/2)];
139 | position['left'] = [(positionTrigger.top/2 + positionTrigger.bottom/2 - dimention[0]/2 + scrollY), positionTrigger.left - dimention[1] - tooltipObj.tooltipDelta];
140 | position['right'] = [(positionTrigger.top/2 + positionTrigger.bottom/2 - dimention[0]/2 + scrollY), positionTrigger.right + tooltipObj.tooltipDelta];
141 |
142 | var direction = tooltipObj.tooltipPosition;
143 | if( direction == 'top' && position['top'][0] < scrollY) direction = 'bottom';
144 | else if( direction == 'bottom' && position['bottom'][0] + tooltipObj.tooltipDelta + dimention[0] > scrollY + window.innerHeight) direction = 'top';
145 | else if( direction == 'left' && position['left'][1] < 0 ) direction = 'right';
146 | else if( direction == 'right' && position['right'][1] + dimention[1] > window.innerWidth ) direction = 'left';
147 |
148 | // reset tooltip triangle translate value
149 | tooltipObj.tooltip.style.setProperty(tooltipObj.tooltipTriangleVar, '0px');
150 |
151 | if(direction == 'top' || direction == 'bottom') {
152 | var deltaMarg = 5;
153 | if(position[direction][1] < 0 ) {
154 | position[direction][1] = deltaMarg;
155 | // make sure triangle is at the center of the tooltip trigger
156 | tooltipObj.tooltip.style.setProperty(tooltipObj.tooltipTriangleVar, (positionTrigger.left + 0.5*positionTrigger.width - 0.5*dimention[1] - deltaMarg)+'px');
157 | }
158 | if(position[direction][1] + dimention[1] > window.innerWidth ) {
159 | position[direction][1] = window.innerWidth - dimention[1] - deltaMarg;
160 | // make sure triangle is at the center of the tooltip trigger
161 | tooltipObj.tooltip.style.setProperty(tooltipObj.tooltipTriangleVar, (0.5*dimention[1] - (window.innerWidth - positionTrigger.right) - 0.5*positionTrigger.width + deltaMarg)+'px');
162 | }
163 | }
164 | tooltipObj.tooltip.style.top = position[direction][0]+'px';
165 | tooltipObj.tooltip.style.left = position[direction][1]+'px';
166 | Util.addClass(tooltipObj.tooltip, 'tooltip--'+direction);
167 | };
168 |
169 | function checkTooltip(tooltipObj) {
170 | tooltipObj.tooltipTriggerHover = false;
171 | if(!tooltipObj.tooltipSticky) hideTooltip(tooltipObj);
172 | else {
173 | if(tooltipObj.tooltipHover) return;
174 | if(tooltipObj.tooltipHoverInterval) return;
175 | tooltipObj.tooltipHoverInterval = setTimeout(function(){
176 | hideTooltip(tooltipObj);
177 | tooltipObj.tooltipHoverInterval = false;
178 | }, 300);
179 | }
180 | };
181 |
182 | function hideTooltip(tooltipObj) {
183 | if(tooltipObj.tooltipHover || tooltipObj.tooltipTriggerHover) return;
184 | clearInterval(tooltipObj.tooltipIntervalId);
185 | if(tooltipObj.tooltipHoverInterval) {
186 | clearInterval(tooltipObj.tooltipHoverInterval);
187 | tooltipObj.tooltipHoverInterval = false;
188 | }
189 | tooltipObj.tooltipIntervalId = false;
190 | if(!tooltipObj.tooltip) return;
191 | // hide tooltip
192 | removeTooltip(tooltipObj);
193 | // remove events
194 | removeTooltipEvents(tooltipObj);
195 | };
196 |
197 | function removeTooltip(tooltipObj) {
198 | if(tooltipObj.tooltipContent == tooltipObj.tooltip.innerHTML || tooltipObj.tooltip.getAttribute('data-reset') == 'on') {
199 | Util.addClass(tooltipObj.tooltip, 'tooltip--is-hidden');
200 | tooltipObj.tooltip.removeAttribute('data-reset');
201 | }
202 | if(tooltipObj.tooltipDescription) tooltipObj.element.removeAttribute('aria-describedby');
203 | };
204 |
205 | function changeTooltipContent(tooltipObj, event) {
206 | if(tooltipObj.tooltip && tooltipObj.tooltipTriggerHover && event.detail) {
207 | tooltipObj.tooltip.innerHTML = event.detail;
208 | tooltipObj.tooltip.setAttribute('data-reset', 'on');
209 | placeTooltip(tooltipObj);
210 | }
211 | };
212 |
213 | window.Tooltip = Tooltip;
214 |
215 | //initialize the Tooltip objects
216 | var tooltips = document.getElementsByClassName('js-tooltip-trigger');
217 | if( tooltips.length > 0 ) {
218 | for( var i = 0; i < tooltips.length; i++) {
219 | (function(i){new Tooltip(tooltips[i]);})(i);
220 | }
221 | }
222 | }());
223 |
--------------------------------------------------------------------------------