Add header bar to private view (#142)

* Add header bar basic

* Fix alignment of breadcrumb

* Fix icon size in breadcrumb

* Add slots / stories for header bar

* Fix typo

* Add disabled color overrides to button

* Fix box icon

* Add header actions section for collapsable buttons

* Tweak css of drawer responsively

* Cover viewport (for notched use)

* Hide gray boxes on iOS taps

* Only show hover effect for devices that support hover

* Finish collapsable header buttons

* Remove wrong reference

* Tweak spacing of nav toggle

* Update storybook entry

* Add storybook entry for header actions

* Update structure of private-view and subcomponents

* Add provide support to storybook

* Update storybook / readme's for private view components

* Use defineComponent instead of createComponetn

* Fix broken import

* Fix tests, update readmes, etc

* Add storybook entries for header actions and module bar

* Remove unused utils

* Use defineComponent instead of createComponent

* Update structure of stories

* Fix story of private view
This commit is contained in:
Rijk van Zanten
2020-03-04 13:37:41 -05:00
committed by GitHub
parent dba5329d31
commit 5509d79756
54 changed files with 1153 additions and 229 deletions

View File

@@ -0,0 +1,5 @@
export default function withBackground() {
return {
template: `<div class="alt-colors" style="height: 100%; background-color: var(--background-color);"><story /></div>`
}
}

View File

@@ -0,0 +1,5 @@
export default function withBackground() {
return {
template: `<div style="background-color: #dde3e6; height: 100%;"><story /></div>`
}
}

View File

@@ -1,5 +1,5 @@
export default function withPadding() {
return {
template: `<div style="padding: 24px;"><story /></div>`
template: `<div style="padding: 24px; height: 100%;"><story /></div>`
}
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<link rel="shortcut icon" href="favicon.ico">
<link rel="manifest" href="manifest.webmanifest" />
<title>Directus</title>

View File

@@ -37,6 +37,6 @@ Vue.component('v-slider', VSlider);
Vue.component('v-switch', VSwitch);
Vue.component('v-table', VTable);
import DrawerDetail from '@/views/private/drawer-detail/';
import DrawerDetail from '@/views/private-view/drawer-detail/';
Vue.component('drawer-detail', DrawerDetail);

View File

@@ -8,7 +8,7 @@
>
<v-icon v-if="index > 0" name="chevron_right" small />
<router-link v-if="!item.disabled" :to="item.to" class="section-link">
<v-icon v-if="item.icon" :name="item.icon" />
<v-icon v-if="item.icon" :name="item.icon" small />
{{ item.name }}
</router-link>
<span v-else class="section-link">
@@ -25,7 +25,6 @@ import { defineComponent, PropType } from '@vue/composition-api';
interface Breadcrumb {
to: string;
name: string;
icon?: string;
disabled?: boolean;
}
@@ -49,11 +48,11 @@ export default defineComponent({
--v-breadcrumb-color-disabled: var(--foreground-color-tertiary);
--v-breadcrumb-divider-color: var(--foreground-color-tertiary);
display: inline-block;
display: flex;
align-items: center;
.section {
display: inline-flex;
align-items: center;
display: contents;
.v-icon {
--v-icon-color: var(--v-breadcrumb-divider-color);

View File

@@ -80,7 +80,9 @@ The loading slot is rendered _on top_ of the content that was there before. Make
| `--v-button-color` | `var(--button-primary-foreground-color)` |
| `--v-button-color-hover` | `var(--button-primary-foreground-color-hover)` |
| `--v-button-color-activated` | `var(--button-primary-foreground-color-activated)` |
| `--v-button-color-disabled` | `var(--button-primary-foreground-color-disabled)` |
| `--v-button-background-color` | `var(--button-primary-background-color)` |
| `--v-button-background-color-hover` | `var(--button-primary-background-color-hover)` |
| `--v-button-background-color-activated` | `var(--button-primary-background-color-activated)` |
| `--v-button-background-color-disabled` | `var(--button-primary-background-color-disabled)` |
| `--v-button-font-size` | `16px` |

View File

@@ -3,7 +3,7 @@
:is="component"
active-class="activated"
class="v-button"
:class="[sizeClass, { block, rounded, icon, outlined, loading }]"
:class="[sizeClass, { block, rounded, icon, outlined, loading, secondary }]"
:type="type"
:disabled="disabled"
:to="to"
@@ -57,6 +57,10 @@ export default defineComponent({
type: [String, Object] as PropType<string | Location>,
default: null
},
secondary: {
type: Boolean,
default: false
},
...sizeProps
},
setup(props, { emit }) {
@@ -81,9 +85,11 @@ export default defineComponent({
--v-button-color: var(--button-primary-foreground-color);
--v-button-color-hover: var(--button-primary-foreground-color-hover);
--v-button-color-activated: var(--button-primary-foreground-color-activated);
--v-button-color-disabled: var(--button-primary-foreground-color-disabled);
--v-button-background-color: var(--button-primary-background-color);
--v-button-background-color-hover: var(--button-primary-background-color-hover);
--v-button-background-color-activated: var(--button-primary-background-color-activated);
--v-button-background-color-disabled: var(--button-primary-background-color-disabled);
--v-button-font-size: 16px;
position: relative;
@@ -105,6 +111,16 @@ export default defineComponent({
transition: var(--fast) var(--transition);
transition-property: background-color border;
&.secondary {
--v-button-color: var(--button-secondary-foreground-color);
--v-button-color-hover: var(--button-secondary-foreground-color-hover);
--v-button-color-activated: var(--button-secondary-foreground-color-activated);
--v-button-background-color: var(--button-secondary-background-color);
--v-button-background-color-hover: var(--button-secondary-background-color-hover);
--v-button-background-color-activated: var(--button-secondary-background-color-activated);
--v-button-background-color-disabled: var(--button-secondary-background-color-disabled);
}
&:active {
transform: scale(0.96);
}
@@ -114,22 +130,16 @@ export default defineComponent({
}
&:disabled {
color: var(--button-primary-text-color-disabled);
background-color: var(--button-primary-background-color-disabled);
border: var(--input-border-width) solid var(--button-primary-background-color-disabled);
color: var(--v-button-color-disabled);
background-color: var(--v-button-background-color-disabled);
border: var(--input-border-width) solid var(--v-button-background-color-disabled);
cursor: not-allowed;
&:active {
transform: unset;
transform: scale(1);
}
}
&:not(.loading):not(:disabled):not(.activated):hover {
color: var(--v-button-color-hover);
background-color: var(--v-button-background-color-hover);
border: var(--button-border-width) solid var(--v-button-background-color-hover);
}
&.block {
display: block;
min-width: 100%;
@@ -140,6 +150,8 @@ export default defineComponent({
}
&.outlined {
--v-button-color: var(--v-button-background-color);
background-color: transparent;
}
@@ -178,10 +190,6 @@ export default defineComponent({
width: var(--v-button-height);
min-width: 0;
padding: 0;
&:active {
transform: scale(0.94);
}
}
.content,
@@ -216,5 +224,13 @@ export default defineComponent({
--v-button-color: var(--v-button-color-activated) !important;
--v-button-background-color: var(--v-button-background-color-activated) !important;
}
@media (hover: hover) {
&:not(.loading):not(:disabled):not(.activated):hover {
color: var(--v-button-color-hover);
background-color: var(--v-button-background-color-hover);
border: var(--button-border-width) solid var(--v-button-background-color-hover);
}
}
}
</style>

View File

@@ -1,14 +1,14 @@
<template functional>
<svg
viewBox="0 0 96 100"
viewBox="0 0 22 22"
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
stroke-linejoin="round"
stroke-miterlimit="1.414"
stroke-miterlimit="2"
>
<path
d="M3.153 79.825l42.917 19.73c1.287.593 2.573.593 3.86 0l42.906-19.73c1.787-.821 2.683-2.216 2.685-4.183V24.358a4.632 4.632 0 0 0-.081-.83v-.242c-.048-.2-.11-.396-.184-.587l-.069-.196a4.73 4.73 0 0 0-.369-.692l-.104-.149a4.668 4.668 0 0 0-.403-.485l-.219-.149a4.476 4.476 0 0 0-.507-.415l-.127-.092a4.558 4.558 0 0 0-.622-.346L49.919.445c-1.287-.593-2.574-.593-3.861 0L3.153 20.175a4.51 4.51 0 0 0-.623.346l-.126.092a4.476 4.476 0 0 0-.507.415l-.15.161c-.146.152-.28.313-.404.484l-.103.15c-.143.22-.266.45-.369.691l-.127.185a4.592 4.592 0 0 0-.184.587v.242a4.632 4.632 0 0 0-.081.83v51.284c0 1.964.891 3.358 2.674 4.183zm6.534-48.264l33.697 15.523v41.142L9.687 72.692V31.561zm42.917 56.654V47.084l33.697-15.523v41.142L52.604 88.215zm-4.61-78.55l31.9 14.693-31.9 14.694-31.899-14.694L47.994 9.665z"
d="M2.036 16.961l8.578 3.944a.886.886 0 00.772 0l8.576-3.944c.357-.164.536-.443.536-.836V5.875a.924.924 0 00-.016-.166v-.048a.95.95 0 00-.037-.118l-.014-.039a.91.91 0 00-.073-.138l-.021-.03a1.067 1.067 0 00-.081-.097l-.043-.03a.901.901 0 00-.102-.083l-.025-.018a1.003 1.003 0 00-.124-.069l-8.578-3.944a.886.886 0 00-.772 0L2.036 5.039a.935.935 0 00-.124.069l-.025.018a.901.901 0 00-.102.083l-.03.032a.915.915 0 00-.08.097l-.021.03a.916.916 0 00-.074.138l-.025.037a1.006 1.006 0 00-.037.118v.048a.924.924 0 00-.016.166v10.25c0 .393.178.671.534.836zm1.306-9.646l6.735 3.102v8.223l-6.735-3.104V7.315zm8.578 11.323v-8.221l6.735-3.102v8.223l-6.735 3.1zm-.921-15.7l6.376 2.937-6.376 2.937-6.376-2.937 6.376-2.937z"
/>
</svg>
</template>

View File

@@ -105,11 +105,7 @@ export default defineComponent({
}
svg {
position: relative;
top: 2px;
left: 2px;
width: calc(100% - 4px); // the material icons all have a slight padding
height: calc(100% - 4px);
display: block;
color: inherit;
fill: currentColor;
}

View File

@@ -47,7 +47,7 @@ describe('Hydration', () => {
expect(hydrated).toBe(false);
});
it('Does not hydrate when already hydrated', async () => {
it('Does not dehydrate when already dehydrated', async () => {
await dehydrate();
const collectionsStore = useCollectionsStore({});

View File

@@ -3,7 +3,7 @@ import VueCompositionAPI from '@vue/composition-api';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import useNavigation from '../compositions/use-navigation';
import VTable from '@/components/v-table';
import PrivateView from '@/views/private/';
import PrivateView from '@/views/private-view/';
import router from '@/router';
jest.mock('../compositions/use-navigation');
@@ -22,12 +22,22 @@ describe('Modules / Collections / Routes / CollectionsOverview', () => {
});
it('Uses useNavigation to get navigation links', () => {
shallowMount(CollectionsOverview, { localVue });
shallowMount(CollectionsOverview, {
localVue,
mocks: {
$tc: () => 'title'
}
});
expect(useNavigation).toHaveBeenCalled();
});
it('Calls router.push on navigation', () => {
const component = shallowMount(CollectionsOverview, { localVue });
const component = shallowMount(CollectionsOverview, {
localVue,
mocks: {
$tc: () => 'title'
}
});
(component.vm as any).navigateToCollection({
collection: 'test',
name: 'Test',

View File

@@ -1,5 +1,21 @@
<template>
<private-view class="collections-overview">
<private-view class="collections-overview" :title="$tc('collection', 2)">
<template #title-outer:prepend>
<v-button rounded disabled icon secondary><v-icon name="box" /></v-button>
</template>
<template #actions>
<v-button rounded icon style="--v-button-background-color: var(--success);">
<v-icon name="add" />
</v-button>
<v-button rounded icon style="--v-button-background-color: var(--warning);">
<v-icon name="delete" />
</v-button>
<v-button rounded icon style="--v-button-background-color: var(--danger);">
<v-icon name="favorite" />
</v-button>
</template>
<template #navigation>
<collections-navigation />
</template>

View File

@@ -11,6 +11,7 @@
html {
font-size: 15px;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
body {

View File

@@ -77,10 +77,10 @@ body,
--button-secondary-background-color: var(--blue-grey-100);
--button-secondary-background-color-hover: var(--blue-grey-200);
--button-secondary-background-color-disabled: var(--blue-grey-400);
--button-secondary-background-color-disabled: var(--blue-grey-50);
--button-secondary-foreground-color: var(--white);
--button-secondary-foreground-color-hover: var(--blue-grey-600);
--button-secondary-foreground-color-disabled: var(--blue-grey-600);
--button-secondary-foreground-color-disabled: var(--blue-grey-400);
/* Misc.
Please try to use one of the variables above if applicable.

View File

@@ -1,15 +0,0 @@
import parseCSSVar from './parse-css-var';
describe('Utils / parseCSSVar', () => {
it('Wraps CSS variables in var()', () => {
const result = parseCSSVar('--red');
expect(result).toBe('var(--red)');
});
it('Passes through regular CSS', () => {
const result = parseCSSVar('#abcabc');
expect(result).toBe('#abcabc');
});
});

View File

@@ -1,6 +0,0 @@
// If the passed color string starts with --, it will be returned wrapped in `var()`, so it can be
// used in CSS.
export default function parseCSSVar(color: string): string {
if (color.startsWith('--')) return `var(${color})`;
return color;
}

View File

@@ -0,0 +1,41 @@
import { withKnobs } from '@storybook/addon-knobs';
import markdown from './readme.md';
import withPadding from '../../../../.storybook/decorators/with-padding';
import withAltColors from '../../../../.storybook/decorators/with-alt-colors';
import { defineComponent, provide } from '@vue/composition-api';
import DrawerDetailGroup from './drawer-detail-group.vue';
import DrawerDetail from '../drawer-detail/';
export default {
title: 'Views / Private / Drawer Detail Group',
decorators: [withKnobs, withAltColors, withPadding],
parameters: {
notes: markdown
}
};
export const basic = () =>
defineComponent({
components: { DrawerDetailGroup, DrawerDetail },
setup() {
provide('drawer-open', true);
},
template: `
<drawer-detail-group>
<drawer-detail icon="person" title="People">
Hi there!
</drawer-detail>
<drawer-detail icon="settings" title="Settings">
These sections can be whatever you want them to be
</drawer-detail>
<drawer-detail icon="sentiment_satisfied_alt" title="Fun times">
This is a third section in the sidebar
</drawer-detail>
<drawer-detail icon="forum" title="Comments">
These sections can hold any markup:
<v-input full-width placeholder="I'm an input" />
</drawer-detail>
</drawer-detail-group>`
});

View File

@@ -1,6 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import DrawerDetailGroup from './_drawer-detail-group.vue';
import DrawerDetailGroup from './drawer-detail-group.vue';
import VItemGroup from '@/components/v-item-group';
const localVue = createLocalVue();

View File

@@ -0,0 +1,4 @@
import DrawerDetailGroup from './drawer-detail-group.vue';
export { DrawerDetailGroup };
export default DrawerDetailGroup;

View File

@@ -0,0 +1,39 @@
# Drawer Detail Group
Used in the private view to manage the active state of [the nested `drawer-detail` components](../drawer-detail/).
## Usage
```html
<drawer-detail-group :drawer-open="drawerOpen">
<drawer-detail icon="person" title="Users" >
<!-- section content -->
</drawer-detail>
<drawer-detail icon="settings" title="Settings" >
<!-- section content -->
</drawer-detail>
<drawer-detail icon="map" title="Routes" >
<!-- section content -->
</drawer-detail>
</drawer-detail-group>
```
## Drawer open state
Once the drawer closes, all open drawer details should be closed. By watching the `drawer-open` prop, we can dynamically close all details.
## Props
| Prop | Description | Default |
|---------------|----------------------------------------------------------|---------|
| `drawer-open` | If the drawer sidebar in the private view is open or not | `false` |
## Slots
| Slot | Description |
|-----------|-------------|
| _default_ | |
## Events
n/a
## CSS Variables
n/a

View File

@@ -0,0 +1,48 @@
import { withKnobs, text, boolean } from '@storybook/addon-knobs';
import markdown from './readme.md';
import { defineComponent, provide, ref, watch } from '@vue/composition-api';
import withPadding from '../../../../.storybook/decorators/with-padding';
import withAltColors from '../../../../.storybook/decorators/with-alt-colors';
export default {
title: 'Views / Private / Drawer Detail',
decorators: [withKnobs, withAltColors, withPadding],
parameters: {
notes: markdown
}
};
export const basic = () =>
defineComponent({
props: {
drawerOpen: {
default: boolean('Drawer open', true)
},
icon: {
default: text('Icon', 'person')
},
title: {
default: text('Title', 'People')
}
},
setup(props) {
const open = ref(false);
provide('drawer-open', open);
watch(
() => props.drawerOpen,
newOpen => (open.value = newOpen)
);
provide('item-group', {
register: () => {},
unregister: () => {},
toggle: () => {}
});
},
template: `
<drawer-detail :title="title" :icon="icon">
Content
</drawer-detail>
`
});

View File

@@ -0,0 +1,32 @@
import markdown from './readme.md';
import HeaderBarActions from './header-bar-actions.vue';
import { defineComponent } from '@vue/composition-api';
import withPadding from '../../../../.storybook/decorators/with-padding';
export default {
title: 'Views / Private / Header Bar Actions',
decorators: [withPadding],
parameters: {
notes: markdown
}
};
export const basic = () =>
defineComponent({
components: { HeaderBarActions },
template: `
<div style="display: flex; justify-content: flex-end; position: relative; height: 44px;">
<header-bar-actions>
<v-button rounded icon style="--v-button-background-color: var(--success);">
<v-icon name="add" />
</v-button>
<v-button rounded icon style="--v-button-background-color: var(--warning);">
<v-icon name="delete" />
</v-button>
<v-button rounded icon style="--v-button-background-color: var(--danger);">
<v-icon name="favorite" />
</v-button>
</header-bar-actions>
</div>
`
});

View File

@@ -0,0 +1,21 @@
import { mount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import HeaderBarActions from './header-bar-actions.vue';
import VButton from '@/components/v-button';
import VIcon from '@/components/v-icon';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-button', VButton);
localVue.component('v-icon', VIcon);
describe('Views / Private / Header Bar Actions', () => {
it('Renders', () => {
const component = mount(HeaderBarActions, {
localVue
});
expect(component.isVueInstance()).toBeTruthy();
});
});

View File

@@ -0,0 +1,106 @@
<template>
<div class="actions" :class="{ active }">
<v-button class="expand" icon rounded secondary outlined @click="active = !active">
<v-icon name="arrow_left" />
</v-button>
<div class="action-buttons">
<slot />
</div>
<v-button
class="drawer-toggle"
icon
rounded
secondary
outlined
@click="$emit('toggle:drawer')"
>
<v-icon name="info" />
</v-button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
export default defineComponent({
props: {},
setup() {
const active = ref(false);
return { active };
}
});
</script>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
.actions {
position: relative;
display: flex;
background-color: transparent;
.gradient-wrapper {
display: contents;
}
.expand {
flex-shrink: 0;
margin-right: 8px;
@include breakpoint(medium) {
display: none;
}
}
.action-buttons {
display: flex;
flex-shrink: 0;
> *:not(:last-child) {
display: none;
margin-right: 8px;
}
}
.drawer-toggle {
flex-shrink: 0;
margin-left: 8px;
@include breakpoint(medium) {
display: none;
}
}
&.active {
position: absolute;
top: 0;
right: 0;
align-items: center;
justify-content: flex-end;
height: 100%;
padding: inherit;
padding-left: 8px;
background-color: var(--background-color);
.expand {
transform: rotate(180deg);
}
.action-buttons {
> * {
display: block;
}
}
}
@include breakpoint(medium) {
.action-buttons {
> * {
display: block !important;
}
}
}
}
</style>

View File

@@ -0,0 +1,4 @@
import HeaderBarActions from './header-bar-actions.vue';
export { HeaderBarActions };
export default HeaderBarActions;

View File

@@ -0,0 +1,39 @@
# Header Bar Actions
Wrapper component that will hold all the actions in the right of the header bar. This component is
made to work well with nested `v-button`s, but also works with any other markup.
On mobile, it will only render the last element in the default slot and render a toggle button to
expand / collapse the actions.
## Usage
```html
<header-bar-actions>
<v-button rounded icon style="--v-button-background-color: var(--success);">
<v-icon name="add" />
</v-button>
<v-button rounded icon style="--v-button-background-color: var(--warning);">
<v-icon name="delete" />
</v-button>
<v-button rounded icon style="--v-button-background-color: var(--danger);">
<v-icon name="favorite" />
</v-button>
</header-bar-actions>
```
## Props
n/a
## Slots
| Slot | Description |
|-----------|-------------|
| _default_ | |
## Events
| Event | Description | Value |
|-----------------|-------------------------------------------------------|-------|
| `toggle:drawer` | Emitted when the user clicks the toggle drawer button | -- |
## CSS Variables
n/a

View File

@@ -0,0 +1,244 @@
import { withKnobs, text, boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import markdown from './readme.md';
import withBackground from '../../../../.storybook/decorators/with-background';
import { defineComponent } from '@vue/composition-api';
import HeaderBar from './header-bar.vue';
import VueRouter from 'vue-router';
export default {
title: 'Views / Private / Header Bar',
decorators: [withKnobs, withBackground],
parameters: {
notes: markdown
}
};
export const basic = () =>
defineComponent({
components: { HeaderBar },
props: {
title: {
default: text('Title', 'Hello World')
},
showDrawerToggle: {
default: boolean('Show Drawer Toggle', false)
},
dense: {
default: boolean('Dense', false)
}
},
setup() {
const navToggle = action('update:nav-open');
const drawerToggle = action('update:drawer-open');
return { navToggle, drawerToggle };
},
template: `
<header-bar
:title="title"
:nav-open="false"
:drawer-open="false"
:show-drawer-toggle="showDrawerToggle"
:dense="dense"
@update:nav-open="navToggle"
@update:drawer-open="drawerToggle"
/>
`
});
export const withBreadcrumb = () =>
defineComponent({
router: new VueRouter(),
components: { HeaderBar },
props: {
title: {
default: text('Title', 'Hello World')
},
showDrawerToggle: {
default: boolean('Show Drawer Toggle', false)
},
dense: {
default: boolean('Dense', false)
}
},
setup() {
const navToggle = action('update:nav-open');
const drawerToggle = action('update:drawer-open');
return { navToggle, drawerToggle };
},
template: `
<header-bar
:title="title"
:nav-open="false"
:drawer-open="false"
:show-drawer-toggle="showDrawerToggle"
:dense="dense"
@update:nav-open="navToggle"
@update:drawer-open="drawerToggle"
>
<template #headline>
<v-breadcrumb :items="[
{
name: 'Settings',
to: '/settings'
},
{
name: 'Collection & Fields',
to: '/settings/fields'
}
]" />
</template>
</header-bar>
`
});
export const withBackButton = () =>
defineComponent({
router: new VueRouter(),
components: { HeaderBar },
props: {
title: {
default: text('Title', 'Hello World')
},
showDrawerToggle: {
default: boolean('Show Drawer Toggle', false)
},
dense: {
default: boolean('Dense', false)
}
},
setup() {
const navToggle = action('update:nav-open');
const drawerToggle = action('update:drawer-open');
return { navToggle, drawerToggle };
},
template: `
<header-bar
:title="title"
:nav-open="false"
:drawer-open="false"
:show-drawer-toggle="showDrawerToggle"
:dense="dense"
@update:nav-open="navToggle"
@update:drawer-open="drawerToggle"
>
<template #title-outer:prepend>
<v-button icon rounded secondary>
<v-icon name="arrow_back" />
</v-button>
</template>
</header-bar>
`
});
export const slots = () => {
const SlotLabel = defineComponent({
template: `<span style="display: block; font-size: 10px; padding: 2px; border-radius: 2px; font-family: monospace; color: red; background-color: rgba(255, 0, 0, 0.2)"><slot /></span>`
});
return defineComponent({
components: { HeaderBar, SlotLabel },
props: {
title: {
default: text('Title', 'Hello World')
},
showDrawerToggle: {
default: boolean('Show Drawer Toggle', false)
},
dense: {
default: boolean('Dense', false)
}
},
setup() {
const navToggle = action('update:nav-open');
const drawerToggle = action('update:drawer-open');
return { navToggle, drawerToggle };
},
template: `
<header-bar
:title="title"
:nav-open="false"
:drawer-open="false"
:show-drawer-toggle="showDrawerToggle"
:dense="dense"
@update:nav-open="navToggle"
@update:drawer-open="drawerToggle"
>
<template #title-outer:prepend>
<slot-label>title-outer:prepend</slot-label>
</template>
<template #headline>
<slot-label>breadcrumb</slot-label>
</template>
<template #title:prepend>
<slot-label>title:prepend</slot-label>
</template>
<template #title:append>
<slot-label>title:append</slot-label>
</template>
<template #title-outer:append>
<slot-label>title-outer:append</slot-label>
</template>
<template #actions:prepend>
<slot-label>actions:prepend</slot-label>
</template>
<template #actions>
<slot-label>actions</slot-label>
</template>
<template #actions:append>
<slot-label>actions:append</slot-label>
</template>
</header-bar>
`
});
};
export const withActions = () =>
defineComponent({
components: { HeaderBar },
props: {
title: {
default: text('Title', 'Hello World')
},
showDrawerToggle: {
default: boolean('Show Drawer Toggle', false)
},
dense: {
default: boolean('Dense', false)
}
},
setup() {
const navToggle = action('update:nav-open');
const drawerToggle = action('update:drawer-open');
return { navToggle, drawerToggle };
},
template: `
<header-bar
:title="title"
:nav-open="false"
:drawer-open="false"
:show-drawer-toggle="showDrawerToggle"
:dense="dense"
@update:nav-open="navToggle"
@update:drawer-open="drawerToggle"
>
<template #actions>
<v-button rounded icon style="--v-button-background-color: var(--success);">
<v-icon name="add" />
</v-button>
<v-button rounded icon style="--v-button-background-color: var(--warning);">
<v-icon name="delete" />
</v-button>
<v-button rounded icon style="--v-button-background-color: var(--danger);">
<v-icon name="favorite" />
</v-button>
</template>
</header-bar>
`
});

View File

@@ -0,0 +1,32 @@
import { mount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import HeaderBar from './header-bar.vue';
import VButton from '@/components/v-button';
import VIcon from '@/components/v-icon';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-button', VButton);
localVue.component('v-icon', VIcon);
describe('Views / Private / Header Bar', () => {
it('Emits toggle event when toggle buttons are clicked', () => {
const component = mount(HeaderBar, {
localVue,
propsData: {
title: 'Title'
}
});
const navToggle = component.find('.nav-toggle');
navToggle.trigger('click');
expect(component.emitted('toggle:nav')[0]).toBeTruthy();
const drawerToggle = component.find('.drawer-toggle');
drawerToggle.trigger('click');
expect(component.emitted('toggle:drawer')[0]).toBeTruthy();
});
});

View File

@@ -0,0 +1,120 @@
<template>
<header class="header-bar" :class="{ dense }">
<v-button secondary class="nav-toggle" icon rounded @click="$emit('toggle:nav')">
<v-icon name="menu" />
</v-button>
<div class="title-outer-prepend" v-if="$scopedSlots['title-outer:prepend']">
<slot name="title-outer:prepend" />
</div>
<div class="title-container">
<slot name="headline" />
<div class="title">
<slot name="title:prepend" />
<h1>{{ title }}</h1>
<slot name="title:append" />
</div>
</div>
<slot name="title-outer:append" />
<div class="spacer" />
<slot name="actions:prepend" />
<header-bar-actions @toggle:drawer="$emit('toggle:drawer')">
<slot name="actions" />
</header-bar-actions>
<slot name="actions:append" />
</header>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import HeaderBarActions from '../header-bar-actions';
export default defineComponent({
components: { HeaderBarActions },
props: {
title: {
type: String,
required: true
},
dense: {
type: Boolean,
default: false
}
},
setup() {
return {};
}
});
</script>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
@import '@/styles/mixins/type-styles';
.header-bar {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
height: 64px;
padding: 0 12px;
background-color: var(--background-color);
transition: height var(--medium) var(--transition);
&.dense {
height: 64px;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2);
}
.nav-toggle {
@include breakpoint(medium) {
display: none;
}
}
.title-outer-prepend {
display: none;
@include breakpoint(medium) {
display: block;
}
}
.title-container {
margin-left: 12px;
.title {
display: flex;
align-items: center;
}
h1 {
flex-grow: 1;
@include type-title;
}
}
.spacer {
flex-grow: 1;
}
.drawer-toggle {
flex-shrink: 0;
margin-left: 8px;
@include breakpoint(medium) {
display: none;
}
}
@include breakpoint(small) {
height: 112px;
padding: 0 32px;
}
}
</style>

View File

@@ -0,0 +1,4 @@
import HeaderBar from './header-bar.vue';
export { HeaderBar };
export default HeaderBar;

View File

@@ -0,0 +1,49 @@
# Header Bar
The header bar is the header bar displayed above the content of every component that relies on
`private-view`. It needs to have a lot of slots in order to allow the different routes and modules
to control what things are shown, while also providing enough consistency between views.
## Usage
```html
<header-bar title="Global Settings">
<template #actions>
<v-button to="/collections/settings/+">
<v-icon name="add" />
</v-button>
</template>
</header-bar>
```
## Navigation toggle / `title-outer:prepend` slot
On mobile views, the `title-outer:prepend` slot is replaced with the navigation toggle button. Never
put any view critical actions in this slot.
## Props
| Prop | Description | Default |
|---------------|-------------------------------------------------------------------------------------|---------|
| `title`* | The title of the current page | -- |
| `dense` | Render the header in a smaller total height, leaving more room for the view content | -- |
## Slots
| Slot | Description |
|-----------------------|----------------------------------------------------------------------------------------------------------------------|
| `title-outer:prepend` | Before the title box. This is hidden on mobile. |
| `headline` | Line above the title. Used for breadcrumbs and other secondary information |
| `title:prepend` | Before the actual title |
| `title:append` | After the actual title. Used for status indicator / bookmark toggle |
| `title-outer:append` | After the title box. |
| `actions:prepend` | Before the action buttons |
| `actions` | The actual action buttons. This slot is rendered inside [the `header-bar-actions` component](../header-bar-actions/) |
| `actions:append` | After the actions section. |
## Events
| Event | Description | Value |
|-----------------|------------------------------------------|-------|
| `toggle:nav` | When the nav toggle button is clicked | -- |
| `toggle:drawer` | When the drawer toggle button is clicked | -- |
## CSS Variables
n/a

View File

@@ -0,0 +1,4 @@
import ModuleBarLogo from './module-bar-logo.vue';
export { ModuleBarLogo };
export default ModuleBarLogo;

View File

@@ -0,0 +1,94 @@
import markdown from './readme.md';
import ModuleBarLogo from './module-bar-logo.vue';
import withPadding from '../../../../.storybook/decorators/with-padding';
import useRequestsStore from '@/stores/requests';
import useProjectsStore from '@/stores/projects';
import { defineComponent } from '@vue/composition-api';
export default {
title: 'Views / Private / Module Bar Logo',
decorators: [withPadding],
parameters: {
notes: markdown
}
};
export const basic = () =>
defineComponent({
components: { ModuleBarLogo },
setup() {
useProjectsStore({});
useRequestsStore({});
},
template: `
<module-bar-logo />
`
});
export const withQueue = () =>
defineComponent({
components: { ModuleBarLogo },
setup() {
useProjectsStore({});
const requestsStore = useRequestsStore({});
requestsStore.state.queue = ['abc'];
},
template: `
<module-bar-logo />
`
});
export const withCustomLogo = () =>
defineComponent({
components: { ModuleBarLogo },
setup() {
useRequestsStore({});
const projectsStore = useProjectsStore({});
projectsStore.state.projects = [
{
key: 'my-project',
api: {
version: '8.5.5',
requires2FA: false,
database: 'mysql',
project_name: 'Thumper',
project_logo: {
full_url:
'https://demo.directus.io/uploads/thumper/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg',
url:
'/uploads/thumper/originals/19acff06-4969-5c75-9cd5-dc3f27506de2.svg'
},
project_color: '#4CAF50',
project_foreground: null,
project_background: null,
telemetry: true,
default_locale: 'en-US',
project_public_note: null
},
server: {
max_upload_size: 104857600,
general: {
php_version: '7.2.22',
php_api: 'fpm-fcgi'
}
}
}
];
projectsStore.state.currentProjectKey = 'my-project';
},
template: `
<module-bar-logo /> `
});
export const withCustomColor = () =>
defineComponent({
components: { ModuleBarLogo },
setup() {
useProjectsStore({});
useRequestsStore({});
},
template: `
<module-bar-logo style="--brand: red;" />
`
});

View File

@@ -1,6 +1,6 @@
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import ModuleBarLogo from './_module-bar-logo.vue';
import ModuleBarLogo from './module-bar-logo.vue';
import { useProjectsStore } from '@/stores/projects';
import { useRequestsStore } from '@/stores/requests';

View File

@@ -72,7 +72,7 @@ export default defineComponent({
width: 40px;
height: 32px;
margin: 0 auto;
background-image: url('../../assets/sprite.svg');
background-image: url('../../../assets/sprite.svg');
background-position: 0% 0%;
background-size: 600px 32px;
}

View File

@@ -0,0 +1,24 @@
# Module Bar Logo
Logo at the top of the module bar. Renders either the rabbit (default), or the custom project logo
if it exists.
It listens to the requestsStore, and makes the bunny run whenever a request is made.
## Usage
```html
<module-bar-logo />
```
## Props
n/a
## Slots
n/a
## Events
n/a
## CSS Variables
n/a

View File

@@ -0,0 +1,4 @@
import ModuleBar from './module-bar.vue';
export { ModuleBar };
export default ModuleBar;

View File

@@ -0,0 +1,26 @@
import markdown from './readme.md';
import ModuleBar from './module-bar.vue';
import { defineComponent } from '@vue/composition-api';
import VueRouter from 'vue-router';
import useProjectsStore from '@/stores/projects';
import useRequestsStore from '@/stores/requests';
export default {
title: 'Views / Private / Module Bar',
parameters: {
notes: markdown
}
};
export const basic = () =>
defineComponent({
router: new VueRouter(),
components: { ModuleBar },
setup() {
useProjectsStore({});
useRequestsStore({});
},
template: `
<module-bar />
`
});

View File

@@ -9,7 +9,7 @@
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import ModuleBarLogo from './_module-bar-logo.vue';
import ModuleBarLogo from '../module-bar-logo/';
import { useProjectsStore } from '@/stores/projects';
import { modules } from '@/modules/';

View File

@@ -0,0 +1,21 @@
# Module bar
The left most sidebar that holds the module navigation.
## Usage
```html
<module-bar />
```
## Props
n/a
## Slots
n/a
## Events
n/a
## CSS Variables
n/a

View File

@@ -1,24 +1,23 @@
import Vue from 'vue';
import PrivateView from './private-view.vue';
import markdown from './private-view.readme.md';
import markdown from './readme.md';
import VueRouter from 'vue-router';
import { defineComponent } from '@vue/composition-api';
Vue.component('private-view', PrivateView);
Vue.use(VueRouter);
const router = new VueRouter();
export default {
title: 'Views / Private',
component: PrivateView,
parameters: {
notes: markdown
}
};
export const basic = () => ({
router: router,
template: `
<private-view />
`
});
export const basic = () =>
defineComponent({
router: new VueRouter(),
template: `
<private-view />
`
});

View File

@@ -0,0 +1,44 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import PrivateView from './private-view.vue';
import VOverlay from '@/components/v-overlay';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-overlay', VOverlay);
describe('Views / Private', () => {
it('Adds the is-open class to the nav', async () => {
const component = shallowMount(PrivateView, {
localVue,
propsData: {
title: 'Title'
}
});
expect(component.find('.navigation').classes()).toEqual(['navigation']);
(component.vm as any).navOpen = true;
await component.vm.$nextTick();
expect(component.find('.navigation').classes()).toEqual(['navigation', 'is-open']);
});
it('Adds the is-open class to the drawer', async () => {
const component = shallowMount(PrivateView, {
localVue,
propsData: {
title: 'Title'
}
});
expect(component.find('.drawer').classes()).toEqual(['drawer', 'alt-colors']);
(component.vm as any).drawerOpen = true;
await component.vm.$nextTick();
expect(component.find('.drawer').classes()).toEqual(['drawer', 'alt-colors', 'is-open']);
});
});

View File

@@ -14,18 +14,23 @@
</div>
</aside>
<div class="content">
<header>
<button @click="navOpen = true">Toggle nav</button>
<button v-if="drawerHasContent" @click="drawerOpen = !drawerOpen">
Toggle drawer
</button>
</header>
<header-bar
:title="title"
@toggle:drawer="drawerOpen = !drawerOpen"
@toggle:nav="navOpen = !navOpen"
>
<template
v-for="(_, scopedSlotName) in $scopedSlots"
v-slot:[scopedSlotName]="slotData"
>
<slot :name="scopedSlotName" v-bind="slotData" />
</template>
</header-bar>
<main>
<slot />
</main>
</div>
<aside
v-if="drawerHasContent"
class="drawer alt-colors"
:class="{ 'is-open': drawerOpen }"
@click="drawerOpen = true"
@@ -35,56 +40,38 @@
</drawer-detail-group>
</aside>
<v-overlay
v-if="navWithOverlay"
class="nav-overlay"
:active="navOpen"
@click="navOpen = false"
/>
<v-overlay
v-if="drawerWithOverlay"
class="drawer-overlay"
:active="drawerOpen"
@click="drawerOpen = false"
/>
<v-overlay class="nav-overlay" :active="navOpen" @click="navOpen = false" />
<v-overlay class="drawer-overlay" :active="drawerOpen" @click="drawerOpen = false" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, provide } from '@vue/composition-api';
import useWindowSize from '@/compositions/window-size';
import ModuleBar from './_module-bar.vue';
import DrawerDetailGroup from './_drawer-detail-group.vue';
// Breakpoints:
// 600, 960, 1260, 1900
import { defineComponent, ref, provide } from '@vue/composition-api';
import ModuleBar from './module-bar/';
import DrawerDetailGroup from './drawer-detail-group/';
import HeaderBar from './header-bar';
export default defineComponent({
components: {
ModuleBar,
DrawerDetailGroup
DrawerDetailGroup,
HeaderBar
},
props: {},
setup(props, { slots }) {
props: {
title: {
type: String,
required: true
}
},
setup() {
const navOpen = ref(false);
const drawerOpen = ref(false);
const { width } = useWindowSize();
const navWithOverlay = computed(() => width.value < 960);
const drawerWithOverlay = computed(() => width.value < 1260);
const drawerHasContent = computed(() => !!slots.drawer);
provide('drawer-open', drawerOpen);
return {
navOpen,
drawerOpen,
navWithOverlay,
drawerWithOverlay,
width,
drawerHasContent
drawerOpen
};
}
});
@@ -94,7 +81,7 @@ export default defineComponent({
@import '@/styles/mixins/breakpoint';
.private-view {
--private-view-content-padding: 32px;
--private-view-content-padding: 12px;
display: flex;
width: 100%;
@@ -102,10 +89,18 @@ export default defineComponent({
.nav-overlay {
--v-overlay-z-index: 49;
@include breakpoint(medium) {
display: none;
}
}
.drawer-overlay {
--v-overlay-z-index: 29;
@include breakpoint(large) {
display: none;
}
}
.navigation {
@@ -149,6 +144,15 @@ export default defineComponent({
main {
padding: var(--private-view-content-padding);
}
// Offset for partially visible drawer
@include breakpoint(medium) {
padding-right: 64px;
}
@include breakpoint(large) {
padding-right: 0;
}
}
.drawer {
@@ -173,6 +177,7 @@ export default defineComponent({
@include breakpoint(large) {
position: relative;
flex-basis: 64px;
flex-shrink: 0;
transform: none;
transition: flex-basis var(--slow) var(--transition);
@@ -182,5 +187,9 @@ export default defineComponent({
}
}
}
@include breakpoint(small) {
--private-view-content-padding: 32px;
}
}
</style>

View File

@@ -1,46 +0,0 @@
import Vue from 'vue';
import markdown from './drawer-detail.readme.md';
import DrawerDetail from './drawer-detail.vue';
import PrivateView from '@/views/private';
import VButton from '@/components/v-button';
import VueRouter from 'vue-router';
Vue.component('v-button', VButton);
Vue.component('drawer-detail', DrawerDetail);
Vue.component('private-view', PrivateView);
Vue.use(VueRouter);
const router = new VueRouter();
export default {
title: 'Views / Private / Drawer Detail',
component: DrawerDetail,
parameters: {
notes: markdown
}
};
export const basic = () => ({
router: router,
template: `
<private-view>
<template #drawer>
<drawer-detail icon="person" title="Users">
<p>Users:</p>
<ul>
<li>Admin</li>
</ul>
</drawer-detail>
<drawer-detail icon="settings" title="Settings">
Hello!
</drawer-detail>
<drawer-detail icon="shopping_cart" title="Extra">
These details hold any other markup:
<v-button style="margin-top: 12px;">I'm a button!</v-button>
</drawer-detail>
</template>
</private-view>
`
});

View File

@@ -1,67 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
import PrivateView from './private-view.vue';
import VOverlay from '@/components/v-overlay';
import * as windowSize from '@/compositions/window-size';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-overlay', VOverlay);
describe('Views / Private', () => {
it('Shows nav with overlay if screen is < 960px', async () => {
jest.spyOn(windowSize, 'default').mockImplementation(() => ({
width: { value: 600 },
height: { value: 600 }
}));
const component = shallowMount(PrivateView, { localVue });
expect((component.vm as any).navWithOverlay).toBe(true);
});
it('Does not render overlay for nav if screen is >= 960px', async () => {
jest.spyOn(windowSize, 'default').mockImplementation(() => ({
width: { value: 960 },
height: { value: 960 }
}));
let component = shallowMount(PrivateView, { localVue });
expect((component.vm as any).navWithOverlay).toBe(false);
(windowSize.default as jest.Mock).mockImplementation(() => ({
width: { value: 1000 },
height: { value: 1000 }
}));
component = shallowMount(PrivateView, { localVue });
expect((component.vm as any).navWithOverlay).toBe(false);
});
it('Shows drawer with overlay if screen is < 1260px', async () => {
jest.spyOn(windowSize, 'default').mockImplementation(() => ({
width: { value: 600 },
height: { value: 600 }
}));
const component = shallowMount(PrivateView, { localVue });
expect((component.vm as any).drawerWithOverlay).toBe(true);
});
it('Does not render overlay for drawer if screen is >= 1260px', async () => {
jest.spyOn(windowSize, 'default').mockImplementation(() => ({
width: { value: 1260 },
height: { value: 1260 }
}));
let component = shallowMount(PrivateView, { localVue });
expect((component.vm as any).drawerWithOverlay).toBe(false);
(windowSize.default as jest.Mock).mockImplementation(() => ({
width: { value: 1300 },
height: { value: 1300 }
}));
component = shallowMount(PrivateView, { localVue });
expect((component.vm as any).drawerWithOverlay).toBe(false);
});
});

View File

@@ -1,7 +1,7 @@
import Vue from 'vue';
import PublicView from './public/';
const PrivateView = () => import(/* webpackChunkName: "private-view" */ './private/');
const PrivateView = () => import(/* webpackChunkName: "private-view" */ './private-view/');
Vue.component('public-view', PublicView);
Vue.component('private-view', PrivateView);