diff options
| author | ivarlovlie <git@ivarlovlie.no> | 2022-08-21 16:56:11 +0200 |
|---|---|---|
| committer | ivarlovlie <git@ivarlovlie.no> | 2022-08-21 16:56:11 +0200 |
| commit | 740f43e2bac7b1eee919351694905fecf9291943 (patch) | |
| tree | 70186389391541528e0463b1d8e3e244a07a555c | |
| parent | c97994d1e7321726eec888c5cca5a4750a5b7dec (diff) | |
| download | greatoffice-740f43e2bac7b1eee919351694905fecf9291943.tar.xz greatoffice-740f43e2bac7b1eee919351694905fecf9291943.zip | |
feat: Before tailwind
19 files changed, 1750 insertions, 271 deletions
diff --git a/apps/projects/src/app/index.scss b/apps/projects/src/app/index.scss index b47151f..f83b1a1 100644 --- a/apps/projects/src/app/index.scss +++ b/apps/projects/src/app/index.scss @@ -37,4 +37,4 @@ @use '../../web-shared/src/styles/components/menu'; @use '../../web-shared/src/styles/components/user-menu'; @use '../../web-shared/src/styles/components/light-dark-switch'; -@use '../../web-shared/src/styles/components/side-navigation'; +@use '../../web-shared/src/styles/components/side-navigation-v4'; diff --git a/apps/projects/src/app/pages/_layout.svelte b/apps/projects/src/app/pages/_layout.svelte index 594fe9e..07a4a25 100644 --- a/apps/projects/src/app/pages/_layout.svelte +++ b/apps/projects/src/app/pages/_layout.svelte @@ -1,182 +1,66 @@ <script> - import { onMount } from "svelte"; - import { location, link } from "svelte-spa-router"; - import { logout_user } from "$app/lib/services/user-service"; - import { random_string } from "$shared/lib/helpers"; - import { get_session_data } from "$shared/lib/session"; - import ProfileModal from "$app/pages/views/profile-modal.svelte"; - import { Menu, MenuItem, MenuItemSeparator } from "$shared/components/menu"; - import Button from "$shared/components/button.svelte"; - import { IconNames } from "$shared/lib/configuration"; - import LL from "$app/lib/i18n/i18n-svelte"; - import BlowoutToolbelt from "$shared/components/blowout-toolbelt.svelte"; + import {onMount} from "svelte"; + import {location, link} from "svelte-spa-router"; + import {logout_user} from "$app/lib/services/user-service"; + import {random_string} from "$shared/lib/helpers"; + import {get_session_data} from "$shared/lib/session"; + import ProfileModal from "$app/pages/views/profile-modal.svelte"; + import {Menu, MenuItem, MenuItemSeparator} from "$shared/components/menu"; + import Button from "$shared/components/button.svelte"; + import {IconNames} from "$shared/lib/configuration"; + import LL from "$app/lib/i18n/i18n-svelte"; + import BlowoutToolbelt from "$shared/components/blowout-toolbelt.svelte"; + import {NavWrapper, NavItem} from "./nav"; - let ProfileModalFunctions = {}; - let showUserMenu = false; - let userMenuTriggerNode; - const userMenuId = "__menu_" + random_string(3); - const username = get_session_data()?.profile.username; + let ProfileModalFunctions = {}; + let showUserMenu = false; + let userMenuTriggerNode; + const userMenuId = "__menu_" + random_string(3); + const username = get_session_data()?.profile.username; - onMount(() => { - userMenuTriggerNode = document.getElementById("open-user-menu"); - }); + onMount(() => { + userMenuTriggerNode = document.getElementById("open-user-menu"); + }); </script> <ProfileModal bind:functions={ProfileModalFunctions}/> <BlowoutToolbelt/> -<header class="position-sticky top-0 z-index-header bg shadow-xs padding-x-component padding-y-sm flex items-center justify-between hide@md"> - <a class="block size-32 hover:reduce-opacity" - href="#"> - <svg class="block" - viewBox="0 0 40 40"> - <circle fill="var(--color-contrast-higher)" - cx="20" - cy="20" - r="20"/> - <path d="M12.64,20.564a6.437,6.437,0,0,0-4.4,1.388S10,24.2,12.133,24.475a6.486,6.486,0,0,0,3.625-.846S14.455,20.8,12.64,20.564Z" - fill="var(--color-bg)"/> - <path d="M22.036,13.407a7.041,7.041,0,0,0-1.111-3.88s-3,1.562-3.152,3.54a6.978,6.978,0,0,0,1.739,4.688S21.851,15.73,22.036,13.407Z" - fill="var(--color-bg)"/> - <path d="M29.048,26.433a7.624,7.624,0,0,0-.321-4.122c-1.052-2.448-4.326-3.784-4.326-3.784a7.973,7.973,0,0,0-.164,5.713A3.294,3.294,0,0,0,25.451,25.6,16.016,16.016,0,0,1,14.758,10.527v-1h-2v1A17.988,17.988,0,0,0,21.19,25.746a5.859,5.859,0,0,0-2.433-.151,8.093,8.093,0,0,0-4,2.352s2.6,2.883,4.846,2.49a7.889,7.889,0,0,0,4.627-3.153,17.885,17.885,0,0,0,6.527,1.243h1v-2h-1A16.094,16.094,0,0,1,29.048,26.433Z" - fill="var(--color-bg)"/> - </svg> - </a> - - <button class="btn btn--subtle">Menu</button> -</header> - -<div class="flex@md"> - <aside class="sidebar sidebar--static@md sidebar--is-visible" - data-static-class="position-relative z-index-2 bg-dark flex flex-column"> - <div class="sidebar__panel flex-grow flex flex-column"> - <header class="sidebar__header bg padding-y-sm padding-left-md padding-right-sm border-bottom z-index-2"> - <h1 class="text-md text-truncate">Menu</h1> - - <button class="reset sidebar__close-btn"> - <svg class="icon icon--xs" - viewBox="0 0 16 16"><title>Close panel</title> - <g stroke-width="2" - stroke="currentColor" - fill="none" - stroke-linecap="round" - stroke-linejoin="round" - stroke-miterlimit="10"> - <line x1="13.5" - y1="2.5" - x2="2.5" - y2="13.5"></line> - <line x1="2.5" - y1="2.5" - x2="13.5" - y2="13.5"></line> - </g> - </svg> - </button> - </header> - - <header class="display@md padding-x-xs margin-bottom-lg"> - <div class="flex items-center justify-between padding-right-xs padding-left-xxxs"> - <a class="block hover:reduce-opacity width-xl" - href="#"> - <img src="/_assets/projects.png" - alt="Projects from Greatoffice"> - </a> +<NavWrapper> + <ul slot="navigation-items"> + <NavItem to="/home" text="{$LL.nav.home()}"/> + <NavItem to="/data" text="{$LL.nav.data()}"/> + <NavItem to="/settings" text="{$LL.nav.settings()}"/> + </ul> + <div slot="navigation-footer" class="tabs-nav-v2 justify-between"> + <div class="tab-v2 padding-x-sm"> + <Button class="user-menu-control" + variant="reset" + id="open-user-menu" + on:click={() => showUserMenu = !showUserMenu} + text={username} + icon={IconNames.chevronDown} + icon_width="2rem" + icon_height="2rem" + icon_right_aligned="true" + title="{$LL.nav.usermenu.toggleTitle()}" + aria-controls="{userMenuId}" + /> + <Menu bind:show="{showUserMenu}" + trigger={userMenuTriggerNode} + id="{userMenuId}"> + <div slot="options"> + <MenuItem on:click={() => ProfileModalFunctions.open()}> + <span title="{$LL.nav.usermenu.profileTitle()}">{$LL.nav.usermenu.profile()}</span> + </MenuItem> + <MenuItemSeparator/> + <MenuItem danger="true" + on:click={() => logout_user()}> + <span title="{$LL.nav.usermenu.logoutTitle()}">{$LL.nav.usermenu.logout()}</span> + </MenuItem> </div> - </header> - - <div class="position-relative z-index-1"> - <nav class="sidenav-v4 padding-x-xs padding-bottom-xs"> - <ul> - <li class="sidenav-v4__item"> - <a href="/home" - use:link - class="sidenav-v4__link {($location === '/' || $location.startsWith('/home')) ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.home()}</a> - </li> - <li class="sidenav-v4__item"> - <a href="/data" - use:link - class="sidenav-v4__link {$location.startsWith('/data') ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.data()}</a> - </li> - <li class="sidenav-v4__item"> - <a href="/settings" - use:link - class="sidenav-v4__link {$location.startsWith('/settings') ? 'tabs-nav-v2__item--selected' : ''}">{$LL.nav.settings()}</a> - </li> - </ul> - </nav> - - <div class="padding-x-xs padding-bottom-sm hide@md"> - <div class="padding-x-sm"> - <div class="search-input search-input--icon-left text-md"> - <input class="search-input__input form-control" - type="search" - name="search-input" - id="search-input" - placeholder="Search..." - aria-label="Search"> - <button class="search-input__btn"> - <svg class="icon" - viewBox="0 0 20 20"><title>Submit</title> - <g fill="none" - stroke="currentColor" - stroke-linecap="round" - stroke-linejoin="round" - stroke-width="2"> - <circle cx="8" - cy="8" - r="6"/> - <line x1="12.242" - y1="12.242" - x2="18" - y2="18"/> - </g> - </svg> - </button> - </div> - </div> - </div> - </div> - - <footer class="sidebar__footer border-top border-alpha padding-xs margin-top-auto position-sticky bottom-0 z-index-1"> - </footer> + </Menu> </div> - </aside> - - <nav class="container max-width-xl@md width-fit-content@md width-100% max-width-none margin-y-xs@md margin-bottom-xs block@md position-relative@md position-fixed z-index-fixed-element bottom-unset@md bottom-0 is-hidden"> - <div class="tabs-nav-v2 justify-between"> - <div class="tab-v2 padding-x-sm"> - <Button class="user-menu-control" - variant="reset" - id="open-user-menu" - on:click={() => showUserMenu = !showUserMenu} - text={username} - icon={IconNames.chevronDown} - icon_width="2rem" - icon_height="2rem" - icon_right_aligned="true" - title="{$LL.nav.usermenu.toggleTitle()}" - aria-controls="{userMenuId}" - /> - <Menu bind:show="{showUserMenu}" - trigger={userMenuTriggerNode} - id="{userMenuId}"> - <div slot="options"> - <MenuItem on:click={() => ProfileModalFunctions.open()}> - <span title="{$LL.nav.usermenu.profileTitle()}">{$LL.nav.usermenu.profile()}</span> - </MenuItem> - <MenuItemSeparator/> - <MenuItem danger="true" - on:click={() => logout_user()}> - <span title="{$LL.nav.usermenu.logoutTitle()}">{$LL.nav.usermenu.logout()}</span> - </MenuItem> - </div> - </Menu> - </div> - </div> - </nav> - - <main class="container max-width-xl position-relative z-index-1 flex-grow min-height-100vh position-sticky@md top-0@md height-100vh@md overflow-auto@md"> - <slot/> - </main> -</div> + </div> + <slot slot="main-content"/> +</NavWrapper>
\ No newline at end of file diff --git a/apps/projects/src/app/pages/nav/css/1_responsive-sidebar.css b/apps/projects/src/app/pages/nav/css/1_responsive-sidebar.css new file mode 100644 index 0000000..515a9f2 --- /dev/null +++ b/apps/projects/src/app/pages/nav/css/1_responsive-sidebar.css @@ -0,0 +1,179 @@ +/* -------------------------------- + +File#: _1_responsive-sidebar +Title: Responsive Sidebar +Descr: Responsive sidebar container +Usage: codyhouse.co/license + +-------------------------------- */ +/* mobile version only (--default) 👇 */ +.sidebar:not(.sidebar--static) { + position: fixed; + top: 0; + left: 0; + z-index: var(--z-index-fixed-element, 10); + width: 100%; + height: 100%; + visibility: hidden; + transition: visibility 0s 0.3s; +} +.sidebar:not(.sidebar--static)::after { + /* overlay layer */ + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0); + transition: background-color 0.3s; + z-index: 1; +} +.sidebar:not(.sidebar--static) .sidebar__panel { + /* content */ + position: absolute; + top: 0; + left: 0; + z-index: 2; + width: 100%; + max-width: 380px; + height: 100%; + overflow: auto; + -webkit-overflow-scrolling: touch; + background-color: var(--color-bg); + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + transition: box-shadow 0.3s, -webkit-transform 0.3s; + transition: box-shadow 0.3s, transform 0.3s; + transition: box-shadow 0.3s, transform 0.3s, -webkit-transform 0.3s; +} +.sidebar:not(.sidebar--static).sidebar--right-on-mobile .sidebar__panel { + left: auto; + right: 0; + -webkit-transform: translateX(100%); + transform: translateX(100%); +} +.sidebar:not(.sidebar--static).sidebar--is-visible { + visibility: visible; + transition: none; +} +.sidebar:not(.sidebar--static).sidebar--is-visible::after { + background-color: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.85); +} +.sidebar:not(.sidebar--static).sidebar--is-visible .sidebar__panel { + -webkit-transform: translateX(0); + transform: translateX(0); + box-shadow: var(--shadow-md); +} + +/* end mobile version */ +.sidebar__header { + display: flex; + align-items: center; + justify-content: space-between; + position: -webkit-sticky; + position: sticky; + top: 0; +} + +.sidebar__close-btn { + --size: 32px; + width: var(--size); + height: var(--size); + display: flex; + border-radius: 50%; + background-color: var(--color-bg-light); + box-shadow: var(--inner-glow), var(--shadow-sm); + transition: 0.2s; + flex-shrink: 0; +} +.sidebar__close-btn .icon { + display: block; + margin: auto; +} +.sidebar__close-btn:hover { + background-color: var(--color-bg-lighter); + box-shadow: var(--inner-glow), var(--shadow-md); +} + +/* desktop version only (--static) 👇 */ +.sidebar--static { + flex-shrink: 0; + flex-grow: 1; +} +.sidebar--static .sidebar__header { + display: none; +} + +.sidebar--sticky-on-desktop { + position: -webkit-sticky; + position: sticky; + top: var(--space-sm); + max-height: calc(100vh - var(--space-sm)); + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +/* end desktop version */ +.sidebar, .sidebar-loaded\:show { + opacity: 0; + /* hide sidebar - or other elements using the .sidebar-loaded:show class - while it is initialized in JS */ +} + +.sidebar--loaded { + opacity: 1; +} + +/* detect when the sidebar needs to switch from the mobile layout to a static one - used in JS */ +[class*=sidebar--static]::before { + display: none; +} + +.sidebar--static::before { + content: "static"; +} + +.sidebar--static\@xs::before { + content: "mobile"; +} +@media (min-width: 32rem) { + .sidebar--static\@xs::before { + content: "static"; + } +} + +.sidebar--static\@sm::before { + content: "mobile"; +} +@media (min-width: 48rem) { + .sidebar--static\@sm::before { + content: "static"; + } +} + +.sidebar--static\@md::before { + content: "mobile"; +} +@media (min-width: 64rem) { + .sidebar--static\@md::before { + content: "static"; + } +} + +.sidebar--static\@lg::before { + content: "mobile"; +} +@media (min-width: 80rem) { + .sidebar--static\@lg::before { + content: "static"; + } +} + +.sidebar--static\@xl::before { + content: "mobile"; +} +@media (min-width: 90rem) { + .sidebar--static\@xl::before { + content: "static"; + } +}
\ No newline at end of file diff --git a/apps/projects/src/app/pages/nav/css/2_side-navigation-v4.css b/apps/projects/src/app/pages/nav/css/2_side-navigation-v4.css new file mode 100644 index 0000000..ec5fcdf --- /dev/null +++ b/apps/projects/src/app/pages/nav/css/2_side-navigation-v4.css @@ -0,0 +1,213 @@ +/* -------------------------------- + +File#: _2_side-navigation-v4 +Title: Side Navigation v4 +Descr: Main, side navigation +Usage: codyhouse.co/license + +-------------------------------- */ +.sidenav-v4 { + --sidenav-v4-icon-size: 20px; + --sidenav-v4-icon-margin-right: var(--space-xxs); +} + +.sidenav-v4__item { + position: relative; +} + +.sidenav-v4__link, +.sidenav-v4__sub-link, +.sidenav-v4__separator { + padding: var(--space-sm); +} + +.sidenav-v4__link, .sidenav-v4__sub-link { + display: flex; + align-items: center; + width: 100%; + border-radius: var(--radius-md); + text-decoration: none; + color: inherit; + line-height: 1; + font-size: var(--text-md); + transition: 0.2s; +} +.sidenav-v4__link:hover, .sidenav-v4__sub-link:hover { + color: var(--color-primary); + background-color: hsla(var(--color-contrast-higher-h), var(--color-contrast-higher-s), var(--color-contrast-higher-l), 0.075); +} +.sidenav-v4__link[aria-current=page], .sidenav-v4__sub-link[aria-current=page] { + color: var(--color-primary); +} + +.sidenav-v4__sub-link { + position: relative; + color: var(--color-contrast-medium); + /* dot indicator */ +} +.sidenav-v4__sub-link::before { + content: ""; + display: block; + --size: 6px; + width: var(--size); + height: var(--size); + background: currentColor; + border-radius: 50%; + margin-left: calc(var(--sidenav-v4-icon-size)/2 - var(--size)/2); + margin-right: calc(var(--sidenav-v4-icon-size)/2 - var(--size)/2 + var(--sidenav-v4-icon-margin-right)); + opacity: 0; + /* visible only if current */ +} +.sidenav-v4__sub-link[aria-current=page]::before { + /* show dot indicator */ + opacity: 1; +} + +.sidenav-v4__notification-marker { + margin-left: auto; + background-color: var(--color-accent); + border-radius: var(--radius-md); + height: 16px; + line-height: 16px; + padding: 0 4px; + color: var(--color-white); + font-size: 12px; + /* hide - visible only on desktop */ + display: none; +} + +/* label icon */ +.sidenav-v4__icon { + --size: var(--sidenav-v4-icon-size); + margin-right: var(--sidenav-v4-icon-margin-right); +} + +/* arrow icon - visible on mobile if item is expandable */ +.sidenav-v4__arrow-icon { + --size: 20px; + /* hide icon for links - show only for buttons created in JS */ +} +.sidenav-v4__arrow-icon .icon__group { + will-change: transform; + -webkit-transform-origin: 50% 50%; + transform-origin: 50% 50%; + -webkit-transform: rotate(-90deg); + transform: rotate(-90deg); + transition: -webkit-transform 0.3s var(--ease-out); + transition: transform 0.3s var(--ease-out); + transition: transform 0.3s var(--ease-out), -webkit-transform 0.3s var(--ease-out); +} +.sidenav-v4__arrow-icon .icon__group > * { + -webkit-transform-origin: 50% 50%; + transform-origin: 50% 50%; + stroke-dasharray: 20; + stroke-dashoffset: 0; + -webkit-transform: translateY(0px); + transform: translateY(0px); + transition: stroke-dashoffset 0.3s, -webkit-transform 0.3s; + transition: transform 0.3s, stroke-dashoffset 0.3s; + transition: transform 0.3s, stroke-dashoffset 0.3s, -webkit-transform 0.3s; + transition-timing-function: var(--ease-out); +} +.sidenav-v4__item--collapsed .sidenav-v4__arrow-icon .icon__group { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); +} +.sidenav-v4__item--collapsed .sidenav-v4__arrow-icon .icon__group > * { + -webkit-transform: translateY(4px); + transform: translateY(4px); +} +.sidenav-v4__item--collapsed .sidenav-v4__arrow-icon .icon__group > *:first-child { + stroke-dashoffset: 10.15; +} +.sidenav-v4__item--collapsed .sidenav-v4__arrow-icon .icon__group > *:last-child { + stroke-dashoffset: 10.15; +} +.sidenav-v4__link--href .sidenav-v4__arrow-icon { + display: none; +} + +/* current item */ +.sidenav-v4__item--current .sidenav-v4__sub-list { + display: block; + /* show sublist */ +} + +/* separator */ +.sidenav-v4__separator span { + display: block; + width: var(--sidenav-v4-icon-size); + height: 1px; + background-color: var(--color-contrast-lower); +} + +/* mobile only */ +@media not all and (min-width: 64rem) { + .sidenav-v4__item--collapsed .sidenav-v4__sub-list { + display: none; + } + + .sidenav-v4__link--href { + display: none; + /* hide link -> show button */ + } +} +/* desktop */ +@media (min-width: 64rem) { + .sidenav-v4__sub-list { + display: none; + } + + .sidenav-v4__link, +.sidenav-v4__sub-link, +.sidenav-v4__separator { + padding: var(--space-xs); + } + + .sidenav-v4__link, +.sidenav-v4__sub-link { + font-size: var(--text-sm); + } + + .sidenav-v4__link--btn { + display: none; + /* hide button -> show link */ + } + + /* tooltip */ + .sidenav-v4__item:not(.sidenav-v4__item--current) .sidenav-v4__sub-list { + width: 220px; + position: absolute; + z-index: var(--z-index-overlay); + left: 100%; + top: 0; + background-color: var(--color-bg-light); + box-shadow: var(--inner-glow), var(--shadow-md); + border-radius: var(--radius-md); + overflow: hidden; + } + .sidenav-v4__item:not(.sidenav-v4__item--current) .sidenav-v4__sub-link { + border-radius: 0; + color: var(--color-contrast-high); + } + .sidenav-v4__item:not(.sidenav-v4__item--current) .sidenav-v4__sub-link::before { + display: none; + /* remove dot indicator */ + } + .sidenav-v4__item:not(.sidenav-v4__item--current) .sidenav-v4__sub-link:hover { + color: var(--color-primary); + } + .sidenav-v4__item:not(.sidenav-v4__item--current).sidenav-v4__item--hover .sidenav-v4__sub-list, .sidenav-v4__item:not(.sidenav-v4__item--current):focus-within .sidenav-v4__sub-list { + display: block; + } + .sidenav-v4__item:not(.sidenav-v4__item--current):hover .sidenav-v4__link { + /* highlight main link if tooltip is visible */ + color: var(--color-primary); + background-color: hsla(var(--color-contrast-higher-h), var(--color-contrast-higher-s), var(--color-contrast-higher-l), 0.075); + } + + /* notification marker */ + .sidenav-v4__notification-marker { + display: block; + } +}
\ No newline at end of file diff --git a/apps/projects/src/app/pages/nav/html/side-navigation-v4.html b/apps/projects/src/app/pages/nav/html/side-navigation-v4.html new file mode 100644 index 0000000..1131b4d --- /dev/null +++ b/apps/projects/src/app/pages/nav/html/side-navigation-v4.html @@ -0,0 +1,211 @@ +<div class="padding-component hide@md no-js:is-hidden"> + <button class="btn btn--primary" aria-controls="sidenav-v4">Show sidebar</button> +</div> + +<div class="flex@md"> + <aside id="sidenav-v4" class="sidebar sidebar--static@md js-sidebar" data-static-class="position-relative z-index-2 bg width-100% max-width-xxxxs shadow-sm"> + <div class="sidebar__panel"> + <!-- 👇 header visible only on mobile --> + <header class="sidebar__header bg padding-y-sm padding-left-md padding-right-sm border-bottom z-index-2"> + <h1 class="text-md text-truncate" id="sidebar-title">Menu</h1> + + <button class="reset sidebar__close-btn js-sidebar__close-btn js-tab-focus"> + <svg class="icon icon--xs" viewBox="0 0 16 16"><title>Close panel</title><g stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"><line x1="13.5" y1="2.5" x2="2.5" y2="13.5"></line><line x1="2.5" y1="2.5" x2="13.5" y2="13.5"></line></g></svg> + </button> + </header> + + <div class="position-relative z-index-1"> + <nav class="sidenav-v4 padding-xs js-sidenav-v4"> + <ul> + <li class="sidenav-v4__item"> + <a class="sidenav-v4__link js-sidenav-v4__link" href="#0"> + <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20"> + <g fill="currentColor"> + <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12v7H4v-7"></path> + <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 10l9-9 9 9"></path> + <path d="M10 14a2 2 0 0 1 2 2v2H8v-2a2 2 0 0 1 2-2z"></path> + </g> + </svg> + + <span>Overview</span> + + <svg class="sidenav-v4__arrow-icon icon margin-left-auto" viewBox="0 0 20 20"> + <g class="icon__group" fill="none" stroke="currentColor" stroke-width="2px" stroke-linecap="round" stroke-linejoin="round"> + <line x1="3" y1="3" x2="17" y2="17" /> + <line x1="17" y1="3" x2="3" y2="17" /> + </g> + </svg> + </a> + + <ul class="sidenav-v4__sub-list"> + <li> + <a class="sidenav-v4__sub-link" href="#0">All Data</a> + </li> + + <li> + <a class="sidenav-v4__sub-link" href="#0">Category 1</a> + </li> + + <li> + <a class="sidenav-v4__sub-link" href="#0">Category 2</a> + </li> + </ul> + </li> + + <li class="sidenav-v4__item sidenav-v4__item--current"> + <a class="sidenav-v4__link js-sidenav-v4__link" href="#0"> + <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20"> + <g fill="currentColor"> + <path d="M10 20a2 2 0 0 1-2-2h4a2 2 0 0 1-2 2z"></path> + <path d="M19 15a3 3 0 0 1-3-3V7a6 6 0 0 0-6-6 6 6 0 0 0-6 6v5a3 3 0 0 1-3 3h18z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path> + </g> + </svg> + + <span>Notifications</span> + + <span class="sidenav-v4__notification-marker">8 <i class="sr-only">notifications</i></span> + + <svg class="sidenav-v4__arrow-icon icon margin-left-auto" viewBox="0 0 20 20"> + <g class="icon__group" fill="none" stroke="currentColor" stroke-width="2px" stroke-linecap="round" stroke-linejoin="round"> + <line x1="3" y1="3" x2="17" y2="17" /> + <line x1="17" y1="3" x2="3" y2="17" /> + </g> + </svg> + </a> + + <ul class="sidenav-v4__sub-list"> + <li> + <a class="sidenav-v4__sub-link" href="#0">All Notifications</a> + </li> + + <li> + <a class="sidenav-v4__sub-link" href="#0" aria-current="page">Friends</a> + </li> + + <li> + <a class="sidenav-v4__sub-link" href="#0">Other</a> + </li> + </ul> + </li> + + <li class="sidenav-v4__item"> + <a class="sidenav-v4__link js-sidenav-v4__link" href="#0"> + <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20"> + <g fill="currentColor"> + <path d="M17 2H3a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h4l3 4 3-4h4a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path> + </g> + </svg> + + <span>Comments</span> + + <svg class="sidenav-v4__arrow-icon icon margin-left-auto" viewBox="0 0 20 20"> + <g class="icon__group" fill="none" stroke="currentColor" stroke-width="2px" stroke-linecap="round" stroke-linejoin="round"> + <line x1="3" y1="3" x2="17" y2="17" /> + <line x1="17" y1="3" x2="3" y2="17" /> + </g> + </svg> + </a> + + <ul class="sidenav-v4__sub-list"> + <li> + <a class="sidenav-v4__sub-link" href="#0">All Comments</a> + </li> + + <li> + <a class="sidenav-v4__sub-link" href="#0">+ New Comment</a> + </li> + + <li> + <a class="sidenav-v4__sub-link" href="#0">Spam</a> + </li> + </ul> + </li> + + <li class="sidenav-v4__separator" role="presentation"><span></span></li> + + <li class="sidenav-v4__item"> + <a class="sidenav-v4__link js-sidenav-v4__link" href="#0"> + <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20"> + <g fill="currentColor"> + <rect x="2" y="2" width="16" height="16" rx="2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></rect> + <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 14l6-6 2 6H6z"></path><circle cx="6.5" cy="6.5" r="1.5"></circle> + </g> + </svg> + + <span>Assets</span> + + <svg class="sidenav-v4__arrow-icon icon margin-left-auto" viewBox="0 0 20 20"> + <g class="icon__group" fill="none" stroke="currentColor" stroke-width="2px" stroke-linecap="round" stroke-linejoin="round"> + <line x1="3" y1="3" x2="17" y2="17" /> + <line x1="17" y1="3" x2="3" y2="17" /> + </g> + </svg> + </a> + + <ul class="sidenav-v4__sub-list"> + <li> + <a class="sidenav-v4__sub-link" href="#0">All Assets</a> + </li> + + <li> + <a class="sidenav-v4__sub-link" href="#0">Upload</a> + </li> + </ul> + </li> + + <li class="sidenav-v4__item"> + <a class="sidenav-v4__link js-sidenav-v4__link" href="#0"> + <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20"> + <g fill="currentColor"> + <circle cx="10" cy="4" r="3" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></circle> + <path d="M10 11a8 8 0 0 0-7.562 5.383A2 2 0 0 0 4.347 19h11.306a2 2 0 0 0 1.909-2.617A8 8 0 0 0 10 11z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path> + </g> + </svg> + + <span>Users</span> + + <svg class="sidenav-v4__arrow-icon icon margin-left-auto" viewBox="0 0 20 20"> + <g class="icon__group" fill="none" stroke="currentColor" stroke-width="2px" stroke-linecap="round" stroke-linejoin="round"> + <line x1="3" y1="3" x2="17" y2="17" /> + <line x1="17" y1="3" x2="3" y2="17" /> + </g> + </svg> + </a> + + <ul class="sidenav-v4__sub-list"> + <li> + <a class="sidenav-v4__sub-link" href="#0">All Users</a> + </li> + + <li> + <a class="sidenav-v4__sub-link" href="#0">+ New User</a> + </li> + </ul> + </li> + + <li class="sidenav-v4__item"> + <a class="sidenav-v4__link js-sidenav-v4__link" href="#0"> + <svg class="sidenav-v4__icon icon" viewBox="0 0 20 20"> + <g fill="currentColor"> + <path d="M11 16l-1.55 1.55a4.95 4.95 0 0 1-7 0 4.95 4.95 0 0 1 0-7l2.192-2.192a4.95 4.95 0 0 1 7 0A4.907 4.907 0 0 1 12.731 10" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path> + <path d="M9 4l1.55-1.55a4.95 4.95 0 0 1 7 0 4.95 4.95 0 0 1 0 7l-2.192 2.192a4.95 4.95 0 0 1-7 0A4.907 4.907 0 0 1 7.269 10" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path> + </g> + </svg> + + <span>Link</span> + </a> + </li> + </ul> + </nav> + </div> + </div> + </aside> + + <main class="position-relative z-index-1 flex-grow height-100vh sidebar-loaded:show"> + <!-- start main content --> + <div class="text-component padding-md"> + <p>Main content.</p> + </div> + <!-- end main content --> + </main> +</div>
\ No newline at end of file diff --git a/apps/projects/src/app/pages/nav/index.ts b/apps/projects/src/app/pages/nav/index.ts new file mode 100644 index 0000000..ca91c20 --- /dev/null +++ b/apps/projects/src/app/pages/nav/index.ts @@ -0,0 +1,6 @@ +import NavWrapper from "./nav-wrapper.svelte"; +import NavItem from "./nav-item.svelte"; +export { + NavWrapper, + NavItem +}
\ No newline at end of file diff --git a/apps/projects/src/app/pages/nav/js/_1_diagonal-movement.js b/apps/projects/src/app/pages/nav/js/_1_diagonal-movement.js new file mode 100644 index 0000000..ed4a47d --- /dev/null +++ b/apps/projects/src/app/pages/nav/js/_1_diagonal-movement.js @@ -0,0 +1,296 @@ +// File#: _1_diagonal-movement +// Usage: codyhouse.co/license +/* + Modified version of the jQuery-menu-aim plugin + https://github.com/kamens/jQuery-menu-aim + - Replaced jQuery with Vanilla JS + - Minor changes +*/ +(function() { + var menuAim = function(opts) { + init(opts); + }; + + window.menuAim = menuAim; + + function init(opts) { + var activeRow = null, + mouseLocs = [], + lastDelayLoc = null, + timeoutId = null, + options = Util.extend({ + menu: '', + rows: false, //if false, get direct children - otherwise pass nodes list + submenuSelector: "*", + submenuDirection: "right", + tolerance: 75, // bigger = more forgivey when entering submenu + enter: function(){}, + exit: function(){}, + activate: function(){}, + deactivate: function(){}, + exitMenu: function(){} + }, opts), + menu = options.menu; + + var MOUSE_LOCS_TRACKED = 3, // number of past mouse locations to track + DELAY = 300; // ms delay when user appears to be entering submenu + + /** + * Keep track of the last few locations of the mouse. + */ + var mouseMoveFallback = function(event) { + (!window.requestAnimationFrame) ? mousemoveDocument(event) : window.requestAnimationFrame(function(){mousemoveDocument(event);}); + }; + + var mousemoveDocument = function(e) { + mouseLocs.push({x: e.pageX, y: e.pageY}); + + if (mouseLocs.length > MOUSE_LOCS_TRACKED) { + mouseLocs.shift(); + } + }; + + /** + * Cancel possible row activations when leaving the menu entirely + */ + var mouseleaveMenu = function() { + if (timeoutId) { + clearTimeout(timeoutId); + } + + // If exitMenu is supplied and returns true, deactivate the + // currently active row on menu exit. + if (options.exitMenu(this)) { + if (activeRow) { + options.deactivate(activeRow); + } + + activeRow = null; + } + }; + + /** + * Trigger a possible row activation whenever entering a new row. + */ + var mouseenterRow = function() { + if (timeoutId) { + // Cancel any previous activation delays + clearTimeout(timeoutId); + } + + options.enter(this); + possiblyActivate(this); + }, + mouseleaveRow = function() { + options.exit(this); + }; + + /* + * Immediately activate a row if the user clicks on it. + */ + var clickRow = function() { + activate(this); + }; + + /** + * Activate a menu row. + */ + var activate = function(row) { + if (row == activeRow) { + return; + } + + if (activeRow) { + options.deactivate(activeRow); + } + + options.activate(row); + activeRow = row; + }; + + /** + * Possibly activate a menu row. If mouse movement indicates that we + * shouldn't activate yet because user may be trying to enter + * a submenu's content, then delay and check again later. + */ + var possiblyActivate = function(row) { + var delay = activationDelay(); + + if (delay) { + timeoutId = setTimeout(function() { + possiblyActivate(row); + }, delay); + } else { + activate(row); + } + }; + + /** + * Return the amount of time that should be used as a delay before the + * currently hovered row is activated. + * + * Returns 0 if the activation should happen immediately. Otherwise, + * returns the number of milliseconds that should be delayed before + * checking again to see if the row should be activated. + */ + var activationDelay = function() { + if (!activeRow || !Util.is(activeRow, options.submenuSelector)) { + // If there is no other submenu row already active, then + // go ahead and activate immediately. + return 0; + } + + function getOffset(element) { + var rect = element.getBoundingClientRect(); + return { top: rect.top + window.pageYOffset, left: rect.left + window.pageXOffset }; + }; + + var offset = getOffset(menu), + upperLeft = { + x: offset.left, + y: offset.top - options.tolerance + }, + upperRight = { + x: offset.left + menu.offsetWidth, + y: upperLeft.y + }, + lowerLeft = { + x: offset.left, + y: offset.top + menu.offsetHeight + options.tolerance + }, + lowerRight = { + x: offset.left + menu.offsetWidth, + y: lowerLeft.y + }, + loc = mouseLocs[mouseLocs.length - 1], + prevLoc = mouseLocs[0]; + + if (!loc) { + return 0; + } + + if (!prevLoc) { + prevLoc = loc; + } + + if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || prevLoc.y < offset.top || prevLoc.y > lowerRight.y) { + // If the previous mouse location was outside of the entire + // menu's bounds, immediately activate. + return 0; + } + + if (lastDelayLoc && loc.x == lastDelayLoc.x && loc.y == lastDelayLoc.y) { + // If the mouse hasn't moved since the last time we checked + // for activation status, immediately activate. + return 0; + } + + // Detect if the user is moving towards the currently activated + // submenu. + // + // If the mouse is heading relatively clearly towards + // the submenu's content, we should wait and give the user more + // time before activating a new row. If the mouse is heading + // elsewhere, we can immediately activate a new row. + // + // We detect this by calculating the slope formed between the + // current mouse location and the upper/lower right points of + // the menu. We do the same for the previous mouse location. + // If the current mouse location's slopes are + // increasing/decreasing appropriately compared to the + // previous's, we know the user is moving toward the submenu. + // + // Note that since the y-axis increases as the cursor moves + // down the screen, we are looking for the slope between the + // cursor and the upper right corner to decrease over time, not + // increase (somewhat counterintuitively). + function slope(a, b) { + return (b.y - a.y) / (b.x - a.x); + }; + + var decreasingCorner = upperRight, + increasingCorner = lowerRight; + + // Our expectations for decreasing or increasing slope values + // depends on which direction the submenu opens relative to the + // main menu. By default, if the menu opens on the right, we + // expect the slope between the cursor and the upper right + // corner to decrease over time, as explained above. If the + // submenu opens in a different direction, we change our slope + // expectations. + if (options.submenuDirection == "left") { + decreasingCorner = lowerLeft; + increasingCorner = upperLeft; + } else if (options.submenuDirection == "below") { + decreasingCorner = lowerRight; + increasingCorner = lowerLeft; + } else if (options.submenuDirection == "above") { + decreasingCorner = upperLeft; + increasingCorner = upperRight; + } + + var decreasingSlope = slope(loc, decreasingCorner), + increasingSlope = slope(loc, increasingCorner), + prevDecreasingSlope = slope(prevLoc, decreasingCorner), + prevIncreasingSlope = slope(prevLoc, increasingCorner); + + if (decreasingSlope < prevDecreasingSlope && increasingSlope > prevIncreasingSlope) { + // Mouse is moving from previous location towards the + // currently activated submenu. Delay before activating a + // new menu row, because user may be moving into submenu. + lastDelayLoc = loc; + return DELAY; + } + + lastDelayLoc = null; + return 0; + }; + + var reset = function(triggerDeactivate) { + if (timeoutId) { + clearTimeout(timeoutId); + } + + if (activeRow && triggerDeactivate) { + options.deactivate(activeRow); + } + + activeRow = null; + }; + + var destroyInstance = function() { + menu.removeEventListener('mouseleave', mouseleaveMenu); + document.removeEventListener('mousemove', mouseMoveFallback); + if(rows.length > 0) { + for(var i = 0; i < rows.length; i++) { + rows[i].removeEventListener('mouseenter', mouseenterRow); + rows[i].removeEventListener('mouseleave', mouseleaveRow); + rows[i].removeEventListener('click', clickRow); + } + } + + }; + + /** + * Hook up initial menu events + */ + menu.addEventListener('mouseleave', mouseleaveMenu); + var rows = (options.rows) ? options.rows : menu.children; + if(rows.length > 0) { + for(var i = 0; i < rows.length; i++) {(function(i){ + rows[i].addEventListener('mouseenter', mouseenterRow); + rows[i].addEventListener('mouseleave', mouseleaveRow); + rows[i].addEventListener('click', clickRow); + })(i);} + } + + document.addEventListener('mousemove', mouseMoveFallback); + + /* Reset/destroy menu */ + menu.addEventListener('reset', function(event){ + reset(event.detail); + }); + menu.addEventListener('destroy', destroyInstance); + }; +}()); + diff --git a/apps/projects/src/app/pages/nav/js/_1_responsive-sidebar.js b/apps/projects/src/app/pages/nav/js/_1_responsive-sidebar.js new file mode 100644 index 0000000..f9599d8 --- /dev/null +++ b/apps/projects/src/app/pages/nav/js/_1_responsive-sidebar.js @@ -0,0 +1,215 @@ +// File#: _1_responsive-sidebar +// Usage: codyhouse.co/license +(function() { + var Sidebar = function(element) { + this.element = element; + this.triggers = document.querySelectorAll('[aria-controls="'+this.element.getAttribute('id')+'"]'); + this.firstFocusable = null; + this.lastFocusable = null; + this.selectedTrigger = null; + this.showClass = "sidebar--is-visible"; + this.staticClass = "sidebar--static"; + this.customStaticClass = ""; + this.readyClass = "sidebar--loaded"; + this.contentReadyClass = "sidebar-loaded:show"; + this.layout = false; // this will be static or mobile + this.preventScrollEl = getPreventScrollEl(this); + getCustomStaticClass(this); // custom classes for static version + initSidebar(this); + }; + + function getPreventScrollEl(element) { + var scrollEl = false; + var querySelector = element.element.getAttribute('data-sidebar-prevent-scroll'); + if(querySelector) scrollEl = document.querySelector(querySelector); + return scrollEl; + }; + + function getCustomStaticClass(element) { + var customClasses = element.element.getAttribute('data-static-class'); + if(customClasses) element.customStaticClass = ' '+customClasses; + }; + + function initSidebar(sidebar) { + initSidebarResize(sidebar); // handle changes in layout -> mobile to static and viceversa + + if ( sidebar.triggers ) { // open sidebar when clicking on trigger buttons - mobile layout only + for(var i = 0; i < sidebar.triggers.length; i++) { + sidebar.triggers[i].addEventListener('click', function(event) { + event.preventDefault(); + toggleSidebar(sidebar, event.target); + }); + } + } + + // use the 'openSidebar' event to trigger the sidebar + sidebar.element.addEventListener('openSidebar', function(event) { + toggleSidebar(sidebar, event.detail); + }); + }; + + function toggleSidebar(sidebar, target) { + if(Util.hasClass(sidebar.element, sidebar.showClass)) { + sidebar.selectedTrigger = target; + closeSidebar(sidebar); + return; + } + sidebar.selectedTrigger = target; + showSidebar(sidebar); + initSidebarEvents(sidebar); + }; + + function showSidebar(sidebar) { // mobile layout only + Util.addClass(sidebar.element, sidebar.showClass); + getFocusableElements(sidebar); + Util.moveFocus(sidebar.element); + // change the overflow of the preventScrollEl + if(sidebar.preventScrollEl) sidebar.preventScrollEl.style.overflow = 'hidden'; + }; + + function closeSidebar(sidebar) { // mobile layout only + Util.removeClass(sidebar.element, sidebar.showClass); + sidebar.firstFocusable = null; + sidebar.lastFocusable = null; + if(sidebar.selectedTrigger) sidebar.selectedTrigger.focus(); + sidebar.element.removeAttribute('tabindex'); + //remove listeners + cancelSidebarEvents(sidebar); + // change the overflow of the preventScrollEl + if(sidebar.preventScrollEl) sidebar.preventScrollEl.style.overflow = ''; + }; + + function initSidebarEvents(sidebar) { // mobile layout only + //add event listeners + sidebar.element.addEventListener('keydown', handleEvent.bind(sidebar)); + sidebar.element.addEventListener('click', handleEvent.bind(sidebar)); + }; + + function cancelSidebarEvents(sidebar) { // mobile layout only + //remove event listeners + sidebar.element.removeEventListener('keydown', handleEvent.bind(sidebar)); + sidebar.element.removeEventListener('click', handleEvent.bind(sidebar)); + }; + + function handleEvent(event) { // mobile layout only + switch(event.type) { + case 'click': { + initClick(this, event); + } + case 'keydown': { + initKeyDown(this, event); + } + } + }; + + function initKeyDown(sidebar, event) { // mobile layout only + if( event.keyCode && event.keyCode == 27 || event.key && event.key == 'Escape' ) { + //close sidebar window on esc + closeSidebar(sidebar); + } else if( event.keyCode && event.keyCode == 9 || event.key && event.key == 'Tab' ) { + //trap focus inside sidebar + trapFocus(sidebar, event); + } + }; + + function initClick(sidebar, event) { // mobile layout only + //close sidebar when clicking on close button or sidebar bg layer + if( !event.target.closest('.js-sidebar__close-btn') && !Util.hasClass(event.target, 'js-sidebar') ) return; + event.preventDefault(); + closeSidebar(sidebar); + }; + + function trapFocus(sidebar, event) { // mobile layout only + if( sidebar.firstFocusable == document.activeElement && event.shiftKey) { + //on Shift+Tab -> focus last focusable element when focus moves out of sidebar + event.preventDefault(); + sidebar.lastFocusable.focus(); + } + if( sidebar.lastFocusable == document.activeElement && !event.shiftKey) { + //on Tab -> focus first focusable element when focus moves out of sidebar + event.preventDefault(); + sidebar.firstFocusable.focus(); + } + }; + + function initSidebarResize(sidebar) { + // custom event emitted when window is resized - detect only if the sidebar--static@{breakpoint} class was added + var beforeContent = getComputedStyle(sidebar.element, ':before').getPropertyValue('content'); + if(beforeContent && beforeContent !='' && beforeContent !='none') { + checkSidebarLayout(sidebar); + + sidebar.element.addEventListener('update-sidebar', function(event){ + checkSidebarLayout(sidebar); + }); + } + // check if there a main element to show + var mainContent = document.getElementsByClassName(sidebar.contentReadyClass); + if(mainContent.length > 0) Util.removeClass(mainContent[0], sidebar.contentReadyClass); + Util.addClass(sidebar.element, sidebar.readyClass); + }; + + function checkSidebarLayout(sidebar) { + var layout = getComputedStyle(sidebar.element, ':before').getPropertyValue('content').replace(/\'|"/g, ''); + if(layout == sidebar.layout) return; + sidebar.layout = layout; + if(layout != 'static') Util.addClass(sidebar.element, 'is-hidden'); + Util.toggleClass(sidebar.element, sidebar.staticClass + sidebar.customStaticClass, layout == 'static'); + if(layout != 'static') setTimeout(function(){Util.removeClass(sidebar.element, 'is-hidden')}); + // reset element role + (layout == 'static') ? sidebar.element.removeAttribute('role', 'alertdialog') : sidebar.element.setAttribute('role', 'alertdialog'); + // reset mobile behaviour + if(layout == 'static' && Util.hasClass(sidebar.element, sidebar.showClass)) closeSidebar(sidebar); + }; + + function getFocusableElements(sidebar) { + //get all focusable elements inside the drawer + 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'); + getFirstVisible(sidebar, allFocusable); + getLastVisible(sidebar, allFocusable); + }; + + function getFirstVisible(sidebar, elements) { + //get first visible focusable element inside the sidebar + for(var i = 0; i < elements.length; i++) { + if( elements[i].offsetWidth || elements[i].offsetHeight || elements[i].getClientRects().length ) { + sidebar.firstFocusable = elements[i]; + return true; + } + } + }; + + function getLastVisible(sidebar, elements) { + //get last visible focusable element inside the sidebar + for(var i = elements.length - 1; i >= 0; i--) { + if( elements[i].offsetWidth || elements[i].offsetHeight || elements[i].getClientRects().length ) { + sidebar.lastFocusable = elements[i]; + return true; + } + } + }; + + window.Sidebar = Sidebar; + + //initialize the Sidebar objects + var sidebar = document.getElementsByClassName('js-sidebar'); + if( sidebar.length > 0 ) { + for( var i = 0; i < sidebar.length; i++) { + (function(i){new Sidebar(sidebar[i]);})(i); + } + // switch from mobile to static layout + var customEvent = new CustomEvent('update-sidebar'); + window.addEventListener('resize', function(event){ + (!window.requestAnimationFrame) ? setTimeout(function(){resetLayout();}, 250) : window.requestAnimationFrame(resetLayout); + }); + + (window.requestAnimationFrame) // init sidebar layout + ? window.requestAnimationFrame(resetLayout) + : resetLayout(); + + function resetLayout() { + for( var i = 0; i < sidebar.length; i++) { + (function(i){sidebar[i].dispatchEvent(customEvent)})(i); + }; + }; + } +}());
\ No newline at end of file diff --git a/apps/projects/src/app/pages/nav/js/_2_side-navigation-v4.js b/apps/projects/src/app/pages/nav/js/_2_side-navigation-v4.js new file mode 100644 index 0000000..63ef9c4 --- /dev/null +++ b/apps/projects/src/app/pages/nav/js/_2_side-navigation-v4.js @@ -0,0 +1,73 @@ +// File#: _2_side-navigation-v4 +// Usage: codyhouse.co/license +(function() { + function initSideNav(nav) { + // create btns - visible on mobile only + createBtns(nav); + // toggle sublists on mobile when clicking on buttons + toggleSubLists(nav); + // init diagonal movement + initDiagonalMove(nav); + }; + + function createBtns(nav) { + // on mobile -> create a <button> element for each link with a submenu + var expandableLinks = nav.getElementsByClassName('js-sidenav-v4__link'); + for(var i = 0; i < expandableLinks.length; i++) { + createSingleBtn(expandableLinks[i]); + } + }; + + function createSingleBtn(link) { + if(!hasSubList(link)) return; + // create btn and insert it into the DOM + var btnClasses = link.getAttribute('class').replace('js-sidenav-v4__link', 'js-sidenav-v4__btn'); + btnClasses = btnClasses +' sidenav-v4__link--btn'; + var btnHtml = '<button class="reset '+btnClasses+'">'+link.innerHTML+'</button>'; + link.insertAdjacentHTML('afterend', btnHtml); + // add class to link element + Util.addClass(link, 'sidenav-v4__link--href'); + // check if we need to add the collpsed class to the <li> element + var listItem = link.parentElement; + if(!Util.hasClass(listItem, 'sidenav-v4__item--current')) Util.addClass(listItem, 'sidenav-v4__item--collapsed'); + }; + + function hasSubList(link) { + // check if link has submenu + var sublist = link.nextElementSibling; + if(!sublist) return false; + return Util.hasClass(sublist, 'sidenav-v4__sub-list'); + }; + + function toggleSubLists(nav) { + // open/close sublist on mobile + nav.addEventListener('click', function(event){ + var btn = event.target.closest('.js-sidenav-v4__btn'); + if(!btn) return; + Util.toggleClass(btn.parentElement, 'sidenav-v4__item--collapsed', !Util.hasClass(btn.parentElement, 'sidenav-v4__item--collapsed')); + }); + }; + + function initDiagonalMove(nav) { + // improve dropdown navigation + new menuAim({ + menu: nav.querySelector('ul'), + activate: function(row) { + Util.addClass(row, 'sidenav-v4__item--hover'); + }, + deactivate: function(row) { + Util.removeClass(row, 'sidenav-v4__item--hover'); + }, + exitMenu: function() { + return true; + }, + }); + }; + + var sideNavs = document.getElementsByClassName('js-sidenav-v4'); + if( sideNavs.length > 0 ) { + for( var i = 0; i < sideNavs.length; i++) { + (function(i){initSideNav(sideNavs[i]);})(i); + } + } +}());
\ No newline at end of file diff --git a/apps/projects/src/app/pages/nav/nav-item.svelte b/apps/projects/src/app/pages/nav/nav-item.svelte new file mode 100644 index 0000000..335cbbb --- /dev/null +++ b/apps/projects/src/app/pages/nav/nav-item.svelte @@ -0,0 +1,18 @@ +<script lang="ts"> + import {link} from "svelte-spa-router"; + import Icon from "$shared/components/icon.svelte"; + + export let external = ""; + export let to = ""; + export let text; + export let icon; +</script> + +<li class="sidenav-v4__item"> + <a class="sidenav-v4__link" href={to ?? external} use:link={external === ""}> + {#if icon} + <Icon class="sidenav-v4__icon icon" name="{icon}" /> + {/if} + <span>{text}</span> + </a> +</li> diff --git a/apps/projects/src/app/pages/nav/nav-wrapper.svelte b/apps/projects/src/app/pages/nav/nav-wrapper.svelte new file mode 100644 index 0000000..8321544 --- /dev/null +++ b/apps/projects/src/app/pages/nav/nav-wrapper.svelte @@ -0,0 +1,20 @@ +<script lang="ts"> + import {random_string} from "$shared/lib/helpers"; + + export let id = "nav__" + random_string(4); + const staticClasses = "position-relative z-index-2 bg width-100% max-width-xxxxs shadow-sm" +</script> +<div class="flex@md"> + <aside id="{id}" class="sidebar sidebar--static@md {staticClasses}"> + <div class="sidebar__panel"> + <div class="position-relative z-index-1"> + <nav class="sidenav-v4 padding-xs"> + <slot name="navigation-items"></slot> + </nav> + </div> + </div> + </aside> + <main class="container max-width-xl position-relative z-index-1 flex-grow min-height-100vh position-sticky@md top-0@md height-100vh@md overflow-auto@m"> + <slot name="main-content"></slot> + </main> +</div>
\ No newline at end of file diff --git a/apps/projects/src/app/pages/nav/scss/_1_responsive-sidebar.scss b/apps/projects/src/app/pages/nav/scss/_1_responsive-sidebar.scss new file mode 100644 index 0000000..e4304f1 --- /dev/null +++ b/apps/projects/src/app/pages/nav/scss/_1_responsive-sidebar.scss @@ -0,0 +1,147 @@ +@use '../base' as *; + +/* -------------------------------- + +File#: _1_responsive-sidebar +Title: Responsive Sidebar +Descr: Responsive sidebar container +Usage: codyhouse.co/license + +-------------------------------- */ + +/* mobile version only (--default) 👇 */ +.sidebar:not(.sidebar--static) { + position: fixed; + top: 0; + left: 0; + z-index: var(--z-index-fixed-element, 10); + width: 100%; + height: 100%; + visibility: hidden; + transition: visibility 0s 0.3s; + + &::after { /* overlay layer */ + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: alpha(var(--color-black), 0); + transition: background-color .3s; + z-index: 1; + } + + .sidebar__panel { /* content */ + position: absolute; + top: 0; + left: 0; + z-index: 2; + width: 100%; + max-width: 380px; + height: 100%; + overflow: auto; + -webkit-overflow-scrolling: touch; + background-color: var(--color-bg); + transform: translateX(-100%); + transition: box-shadow 0.3s,transform 0.3s; + } + + &.sidebar--right-on-mobile { + .sidebar__panel { + left: auto; + right: 0; + transform: translateX(100%); + } + } + + &.sidebar--is-visible { + visibility: visible; + transition: none; + + &::after { + background-color: alpha(var(--color-black), 0.85); + } + + .sidebar__panel { + transform: translateX(0); + box-shadow: var(--shadow-md); + } + } +} +/* end mobile version */ + +.sidebar__header { + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; +} + +.sidebar__close-btn { + --size: 32px; + width: var(--size); + height: var(--size); + display: flex; + border-radius: 50%; + background-color: var(--color-bg-light); + box-shadow: var(--inner-glow), var(--shadow-sm); + transition: .2s; + flex-shrink: 0; + + .icon { + display: block; + margin: auto; + } + + &:hover { + background-color: var(--color-bg-lighter); + box-shadow: var(--inner-glow), var(--shadow-md); + } +} + +/* desktop version only (--static) 👇 */ +.sidebar--static { + flex-shrink: 0; + flex-grow: 1; + + .sidebar__header { + display: none; + } +} + +.sidebar--sticky-on-desktop { + position: sticky; + top: var(--space-sm); + max-height: calc(100vh - var(--space-sm)); + overflow: auto; + -webkit-overflow-scrolling: touch; +} +/* end desktop version */ + +.sidebar, .sidebar-loaded\:show { + opacity: 0; /* hide sidebar - or other elements using the .sidebar-loaded:show class - while it is initialized in JS */ +} + +.sidebar--loaded { + opacity: 1; +} + +/* detect when the sidebar needs to switch from the mobile layout to a static one - used in JS */ +[class*="sidebar--static"]::before { + display: none; +} + +.sidebar--static::before { + content: 'static'; +} + +@each $breakpoint, $value in $breakpoints { + .sidebar--static\@#{$breakpoint}::before { + content: 'mobile'; + @include breakpoint(#{$breakpoint}) { + content: 'static'; + } + } +}
\ No newline at end of file diff --git a/apps/projects/src/app/pages/nav/scss/_2_side-navigation-v4.scss b/apps/projects/src/app/pages/nav/scss/_2_side-navigation-v4.scss new file mode 100644 index 0000000..2b421df --- /dev/null +++ b/apps/projects/src/app/pages/nav/scss/_2_side-navigation-v4.scss @@ -0,0 +1,237 @@ +@use '../base' as *; +@use '_1_responsive-sidebar.scss' as *; + +/* -------------------------------- + +File#: _2_side-navigation-v4 +Title: Side Navigation v4 +Descr: Main, side navigation +Usage: codyhouse.co/license + +-------------------------------- */ + +.sidenav-v4 { + --sidenav-v4-icon-size: 20px; + --sidenav-v4-icon-margin-right: var(--space-xxs); +} + +.sidenav-v4__item { + position: relative; +} + +.sidenav-v4__link, +.sidenav-v4__sub-link, +.sidenav-v4__separator { + padding: var(--space-sm); +} + +.sidenav-v4__link, .sidenav-v4__sub-link { + display: flex; + align-items: center; + + width: 100%; + border-radius: var(--radius-md); + + text-decoration: none; + color: inherit; + line-height: 1; + font-size: var(--text-md); + + transition: .2s; + + &:hover { + color: var(--color-primary); + background-color: alpha(var(--color-contrast-higher), 0.075); + } + + &[aria-current="page"] { + color: var(--color-primary); + } +} + +.sidenav-v4__sub-link { + position: relative; + color: var(--color-contrast-medium); + + /* dot indicator */ + &::before { + content: ''; + display: block; + --size: 6px; + width: var(--size); + height: var(--size); + background: currentColor; + border-radius: 50%; + margin-left: calc(var(--sidenav-v4-icon-size)/2 - var(--size)/2); + margin-right: calc(var(--sidenav-v4-icon-size)/2 - var(--size)/2 + var(--sidenav-v4-icon-margin-right)); + + opacity: 0; /* visible only if current */ + } + + &[aria-current="page"] { + &::before { /* show dot indicator */ + opacity: 1; + } + } +} + +.sidenav-v4__notification-marker { + margin-left: auto; + background-color: var(--color-accent); + border-radius: var(--radius-md); + + height: 16px; + line-height: 16px; + padding: 0 4px; + color: var(--color-white); + font-size: 12px; + + /* hide - visible only on desktop */ + display: none; +} + +/* label icon */ +.sidenav-v4__icon { + --size: var(--sidenav-v4-icon-size); + margin-right: var(--sidenav-v4-icon-margin-right); +} + +/* arrow icon - visible on mobile if item is expandable */ +.sidenav-v4__arrow-icon { + --size: 20px; + + .icon__group { + will-change: transform; + transform-origin: 50% 50%; + transform: rotate(-90deg); + transition: transform .3s var(--ease-out); + + > * { + transform-origin: 50% 50%; + stroke-dasharray: 20; + stroke-dashoffset: 0; + transform: translateY(0px); + transition: transform .3s, stroke-dashoffset .3s; + transition-timing-function: var(--ease-out); + } + + .sidenav-v4__item--collapsed & { + transform: rotate(0deg); + + > * { + transform: translateY(4px); + } + + > *:first-child { + stroke-dashoffset: 10.15; + } + + > *:last-child { + stroke-dashoffset: 10.15; + } + } + } + + /* hide icon for links - show only for buttons created in JS */ + .sidenav-v4__link--href & { + display: none; + } +} + +/* current item */ +.sidenav-v4__item--current { + .sidenav-v4__sub-list { + display: block; /* show sublist */ + } +} + +/* separator */ +.sidenav-v4__separator { + span { + display: block; + width: var(--sidenav-v4-icon-size); + height: 1px; + background-color: var(--color-contrast-lower); + } +} + +/* mobile only */ +@include breakpoint(md, "not all") { + .sidenav-v4__item--collapsed { + .sidenav-v4__sub-list { + display: none; + } + } + + .sidenav-v4__link--href { + display: none; /* hide link -> show button */ + } +} + +/* desktop */ +@include breakpoint(md) { + .sidenav-v4__sub-list { + display: none; + } + + .sidenav-v4__link, + .sidenav-v4__sub-link, + .sidenav-v4__separator { + padding: var(--space-xs); + } + + .sidenav-v4__link, + .sidenav-v4__sub-link { + font-size: var(--text-sm); + } + + .sidenav-v4__link--btn { + display: none; /* hide button -> show link */ + } + + /* tooltip */ + .sidenav-v4__item:not(.sidenav-v4__item--current) { + .sidenav-v4__sub-list { + width: 220px; + position: absolute; + z-index: var(--z-index-overlay); + left: 100%; + top: 0; + + background-color: var(--color-bg-light); + box-shadow: var(--inner-glow), var(--shadow-md); + border-radius: var(--radius-md); + + overflow: hidden; + } + + .sidenav-v4__sub-link { + border-radius: 0; + color: var(--color-contrast-high); + + &::before { + display: none; /* remove dot indicator */ + } + + &:hover { + color: var(--color-primary); + } + } + + &.sidenav-v4__item--hover, &:focus-within { + .sidenav-v4__sub-list { + display: block; + } + } + + &:hover .sidenav-v4__link { /* highlight main link if tooltip is visible */ + color: var(--color-primary); + background-color: alpha(var(--color-contrast-higher), 0.075); + } + } + + /* notification marker */ + .sidenav-v4__notification-marker { + display: block; + } +}
\ No newline at end of file diff --git a/apps/projects/src/app/pages/nav/side-navigation-v4.zip b/apps/projects/src/app/pages/nav/side-navigation-v4.zip Binary files differnew file mode 100644 index 0000000..d034eaf --- /dev/null +++ b/apps/projects/src/app/pages/nav/side-navigation-v4.zip diff --git a/apps/projects/src/app/pages/ui-workbench.svelte b/apps/projects/src/app/pages/ui-workbench.svelte index af1bf4e..ff2b058 100644 --- a/apps/projects/src/app/pages/ui-workbench.svelte +++ b/apps/projects/src/app/pages/ui-workbench.svelte @@ -1,47 +1,7 @@ <script> - import Dropdown from "$shared/components/dropdown.svelte"; - import {generate_random_hex_color} from "$shared/lib/colors"; - - let entries = []; - - let dropdown; - - for (let i = 1; i < 20; i++) { - entries.push({ - id: crypto.randomUUID(), - name: "Option " + i, - selected: false, - color: generate_random_hex_color(true) - }); - } - - function on_create({detail}) { - const copy = entries; - const entry = {id: crypto.randomUUID(), name: detail.name}; - copy.push(entry); - entries = copy; - console.log("Created", entry); - dropdown.select_entry(entry.id); - } - - function on_select({detail}) { - console.log(detail); - } + import {NavWrapper} from "./nav/index"; </script> -<main class="grid gap-y-lg padding-md"> - <div class="row"> - <label for="dropdown">Choose an entry</label> - <Dropdown id="dropdown" - name="dropdown" - placeholder="Search or create" - maxlength="50" - creatable="true" - multiple="false" - {entries} - bind:this={dropdown} - on:create={on_create} - on:select={on_select} - /> - </div> -</main> +<NavWrapper> + +</NavWrapper>
\ No newline at end of file diff --git a/apps/web-shared/src/styles/components/responsive-sidebar.scss b/apps/web-shared/src/styles/components/responsive-sidebar.scss index 29a45f4..71c86da 100644 --- a/apps/web-shared/src/styles/components/responsive-sidebar.scss +++ b/apps/web-shared/src/styles/components/responsive-sidebar.scss @@ -1,15 +1,16 @@ @use '../base' as *; /* -------------------------------- + File#: _1_responsive-sidebar Title: Responsive Sidebar Descr: Responsive sidebar container Usage: codyhouse.co/license --------------------------------- */ -$maxwidth: 250px; +-------------------------------- */ -.sidebar { +/* mobile version only (--default) 👇 */ +.sidebar:not(.sidebar--static) { position: fixed; top: 0; left: 0; @@ -17,6 +18,7 @@ $maxwidth: 250px; width: 100%; height: 100%; visibility: hidden; + transition: visibility 0s 0.3s; &::after { /* overlay layer */ content: ''; @@ -26,36 +28,23 @@ $maxwidth: 250px; width: 100%; height: 100%; background-color: alpha(var(--color-black), 0); + transition: background-color .3s; z-index: 1; } - @include breakpoint(sm) { - visibility: visible; - position: relative; - z-index: 1; - width: 100%; - max-width: $maxwidth; - border-right: var(--border-width, 1px) var(--border-style, solid) hsla(var(--color-contrast-lower-h), var(--color-contrast-lower-s), var(--color-contrast-lower-l), var(--border-o, 1)); - - .sidebar__header { - display: none; - } - - .sidebar__panel { - position: relative !important; - } - } - - .sidebar__panel { + .sidebar__panel { /* content */ position: absolute; top: 0; left: 0; z-index: 2; width: 100%; + max-width: 380px; height: 100%; overflow: auto; -webkit-overflow-scrolling: touch; background-color: var(--color-bg); + transform: translateX(-100%); + transition: box-shadow 0.3s,transform 0.3s; } &.sidebar--right-on-mobile { @@ -68,9 +57,10 @@ $maxwidth: 250px; &.sidebar--is-visible { visibility: visible; - + transition: none; + &::after { - background-color: alpha(var(--color-black), 0.55); + background-color: alpha(var(--color-black), 0.85); } .sidebar__panel { @@ -79,6 +69,7 @@ $maxwidth: 250px; } } } +/* end mobile version */ .sidebar__header { display: flex; @@ -96,8 +87,9 @@ $maxwidth: 250px; border-radius: 50%; background-color: var(--color-bg-light); box-shadow: var(--inner-glow), var(--shadow-sm); + transition: .2s; flex-shrink: 0; - + .icon { display: block; margin: auto; @@ -108,3 +100,40 @@ $maxwidth: 250px; box-shadow: var(--inner-glow), var(--shadow-md); } } + +/* desktop version only (--static) 👇 */ +.sidebar--static { + flex-shrink: 0; + flex-grow: 1; + + .sidebar__header { + display: none; + } +} + +.sidebar--sticky-on-desktop { + position: sticky; + top: var(--space-sm); + max-height: calc(100vh - var(--space-sm)); + overflow: auto; + -webkit-overflow-scrolling: touch; +} +/* end desktop version */ + +.sidebar--loaded { + opacity: 1; +} + + +.sidebar--static::before { + content: 'static'; +} + +@each $breakpoint, $value in $breakpoints { + .sidebar--static\@#{$breakpoint}::before { + content: 'mobile'; + @include breakpoint(#{$breakpoint}) { + content: 'static'; + } + } +}
\ No newline at end of file diff --git a/apps/web-shared/src/styles/components/side-navigation.scss b/apps/web-shared/src/styles/components/side-navigation-v4.scss index 2e6597b..c2c13d2 100644 --- a/apps/web-shared/src/styles/components/side-navigation.scss +++ b/apps/web-shared/src/styles/components/side-navigation-v4.scss @@ -1,5 +1,5 @@ @use '../base' as *; -@use 'vanilla-responsive-sidebar' as *; +@use 'responsive-sidebar.scss' as *; /* -------------------------------- @@ -19,7 +19,7 @@ Usage: codyhouse.co/license position: relative; } -.sidenav-v4__link, +.sidenav-v4__link, .sidenav-v4__sub-link, .sidenav-v4__separator { padding: var(--space-sm); @@ -28,10 +28,10 @@ Usage: codyhouse.co/license .sidenav-v4__link, .sidenav-v4__sub-link { display: flex; align-items: center; - + width: 100%; border-radius: var(--radius-md); - + text-decoration: none; color: inherit; line-height: 1; @@ -52,7 +52,7 @@ Usage: codyhouse.co/license .sidenav-v4__sub-link { position: relative; color: var(--color-contrast-medium); - + /* dot indicator */ &::before { content: ''; @@ -79,7 +79,7 @@ Usage: codyhouse.co/license margin-left: auto; background-color: var(--color-accent); border-radius: var(--radius-md); - + height: 16px; line-height: 16px; padding: 0 4px; @@ -104,12 +104,14 @@ Usage: codyhouse.co/license will-change: transform; transform-origin: 50% 50%; transform: rotate(-90deg); + transition: transform .3s var(--ease-out); > * { transform-origin: 50% 50%; stroke-dasharray: 20; stroke-dashoffset: 0; transform: translateY(0px); + transition: transform .3s, stroke-dashoffset .3s; transition-timing-function: var(--ease-out); } @@ -123,13 +125,17 @@ Usage: codyhouse.co/license > *:first-child { stroke-dashoffset: 10.15; } - + > *:last-child { stroke-dashoffset: 10.15; } } } - + + /* hide icon for links - show only for buttons created in JS */ + .sidenav-v4__link--href & { + display: none; + } } /* current item */ @@ -168,15 +174,15 @@ Usage: codyhouse.co/license display: none; } - .sidenav-v4__link, + .sidenav-v4__link, .sidenav-v4__sub-link, .sidenav-v4__separator { padding: var(--space-xs); } - .sidenav-v4__link, + .sidenav-v4__link, .sidenav-v4__sub-link { - font-size: var(--text-base); + font-size: var(--text-sm); } .sidenav-v4__link--btn { @@ -191,11 +197,11 @@ Usage: codyhouse.co/license z-index: var(--z-index-overlay); left: 100%; top: 0; - + background-color: var(--color-bg-light); box-shadow: var(--inner-glow), var(--shadow-md); border-radius: var(--radius-md); - + overflow: hidden; } @@ -228,4 +234,4 @@ Usage: codyhouse.co/license .sidenav-v4__notification-marker { display: block; } -} +}
\ No newline at end of file diff --git a/apps/web-shared/src/styles/custom-style/_typography.scss b/apps/web-shared/src/styles/custom-style/_typography.scss index 3bf56a6..7fa98c5 100644 --- a/apps/web-shared/src/styles/custom-style/_typography.scss +++ b/apps/web-shared/src/styles/custom-style/_typography.scss @@ -15,7 +15,7 @@ --text-scale-ratio: 1.2; // multiplier used to generate the type scale values 👇 // line-height - --body-line-height: 1.4; + --body-line-height: 1; --heading-line-height: 1; // capital letters - used in combo with the lhCrop mixin @@ -37,14 +37,6 @@ --text-xxxxl: calc(var(--text-xxxl) * var(--text-scale-ratio)); } - -@include breakpoint(md) { - :root { - --text-base-size: 1.25rem; - --text-scale-ratio: 1.25; - } -} - body { font-family: var(--font-primary); } diff --git a/apps/web-shared/src/styles/custom-style/_util.scss b/apps/web-shared/src/styles/custom-style/_util.scss index 5d976a4..4b86e73 100644 --- a/apps/web-shared/src/styles/custom-style/_util.scss +++ b/apps/web-shared/src/styles/custom-style/_util.scss @@ -1,15 +1,8 @@ @use '../base' as *; -// -------------------------------- - -// How to create custom utility classes 👇 - -// -------------------------------- - .border-none { border: none !important; } -// } // 👇 (optional) create responsive variations - edit only [my-util-class, property, value] // @each $breakpoint, $value in $breakpoints { |
