Add component for customising navigation links, improve layouts and responsiveness

This commit is contained in:
Andrew Kingston 2021-06-17 12:18:48 +01:00
parent bd52745a90
commit a522a87ee8
12 changed files with 231 additions and 1224 deletions

View File

@ -78,7 +78,7 @@
"posthog-js": "1.4.5", "posthog-js": "1.4.5",
"remixicon": "2.5.0", "remixicon": "2.5.0",
"shortid": "2.2.15", "shortid": "2.2.15",
"svelte-dnd-action": "^0.8.9", "svelte-dnd-action": "^0.9.8",
"svelte-loading-spinners": "^0.1.1", "svelte-loading-spinners": "^0.1.1",
"svelte-portal": "0.1.0", "svelte-portal": "0.1.0",
"uuid": "8.3.1", "uuid": "8.3.1",

View File

@ -8,7 +8,7 @@
import { setWith } from "lodash" import { setWith } from "lodash"
$: definition = store.actions.components.getDefinition( $: definition = store.actions.components.getDefinition(
$selectedComponent._component $selectedComponent?._component
) )
$: isComponentOrScreen = $: isComponentOrScreen =
$store.currentView === "component" || $store.currentView === "component" ||

View File

@ -0,0 +1,108 @@
<script>
import {
Button,
Icon,
DrawerContent,
Layout,
Input,
Combobox,
} from "@budibase/bbui"
import { flip } from "svelte/animate"
import { dndzone } from "svelte-dnd-action"
import { generate } from "shortid"
import { store } from "builderStore"
export let links = []
const flipDurationMs = 150
$: urlOptions = $store.screens
.map(screen => screen.routing?.route)
.filter(x => x != null)
const addLink = () => {
links = [...links, { id: generate() }]
}
const removeLink = id => {
links = links.filter(link => link.id !== id)
}
const updateLinks = e => {
links = e.detail.items
}
</script>
<DrawerContent>
<div class="container">
<Layout>
{#if links?.length}
<div
class="links"
use:dndzone={{
items: links,
flipDurationMs,
dropTargetStyle: { outline: "none" },
}}
on:finalize={updateLinks}
on:consider={updateLinks}
>
{#each links as link (link.id)}
<div class="link" animate:flip={{ duration: flipDurationMs }}>
<Icon name="DragHandle" size="XL" />
<Input bind:value={link.text} placeholder="Text" />
<Combobox
bind:value={link.url}
placeholder="URL"
options={urlOptions}
/>
<Icon
name="Close"
hoverable
size="S"
on:click={() => removeLink(link.id)}
/>
</div>
{/each}
</div>
{/if}
<div class="button-container">
<Button secondary icon="Add" on:click={addLink}>Add Link</Button>
</div>
</Layout>
</div>
</DrawerContent>
<style>
.container {
width: 100%;
max-width: 600px;
margin: var(--spacing-m) auto;
}
.links {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.link {
padding: 4px 8px;
gap: var(--spacing-l);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-radius: var(--border-radius-s);
transition: background-color ease-in-out 130ms;
}
.link:hover {
background-color: var(--spectrum-global-color-gray-100);
}
.link > :global(.spectrum-Form-item) {
flex: 1 1 auto;
width: 0;
}
.button-container {
margin-left: var(--spacing-l);
}
</style>

View File

@ -0,0 +1,23 @@
<script>
import { Button, Drawer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import NavigationDrawer from "./NavigationDrawer.svelte"
export let value = []
let drawer
const dispatch = createEventDispatcher()
const save = () => {
dispatch("change", value)
drawer.hide()
}
</script>
<Button secondary on:click={drawer.show}>Configure Links</Button>
<Drawer bind:this={drawer} title={"Navigation Links"}>
<svelte:fragment slot="description">
Configure the links in your navigation bar.
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<NavigationDrawer slot="body" bind:links={value} />
</Drawer>

View File

@ -1,40 +0,0 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { flip } from "svelte/animate"
import { dndzone } from "svelte-dnd-action"
let flipDurationMs = 150
// This should be the screens and any external links the user has added
let items = [
{ text: "Test", id: 0 },
{ text: "First", id: 1 },
{ text: "Second", id: 2 },
]
</script>
<div class="container">
<ul
use:dndzone={{
items,
flipDurationMs,
dropTargetStyle: { outline: "none" },
}}
>
{#each items as item (item)}
<li animate:flip={{ duration: flipDurationMs }}>{item}</li>
{/each}
</ul>
<ActionButton icon="Add">Add External Link</ActionButton>
</div>
<style>
.container {
display: grid;
}
ul {
display: grid;
grid-template-columns: 1fr;
list-style-type: none;
}
</style>

View File

@ -16,7 +16,7 @@
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte" import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte" import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
import SectionSelect from "./PropertyControls/SectionSelect.svelte" import SectionSelect from "./PropertyControls/SectionSelect.svelte"
import NavigationSelect from "./PropertyControls/NavigationSelect.svelte" import NavigationEditor from "./PropertyControls/NavigationEditor/NavigationEditor.svelte"
import EventsEditor from "./PropertyControls/EventsEditor" import EventsEditor from "./PropertyControls/EventsEditor"
import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte" import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte"
import { IconSelect } from "./PropertyControls/IconSelect" import { IconSelect } from "./PropertyControls/IconSelect"
@ -65,7 +65,7 @@
multifield: MultiFieldSelect, multifield: MultiFieldSelect,
schema: SchemaSelect, schema: SchemaSelect,
section: SectionSelect, section: SectionSelect,
navigationSelect: NavigationSelect, navigation: NavigationEditor,
filter: FilterEditor, filter: FilterEditor,
"field/string": StringFieldSelect, "field/string": StringFieldSelect,
"field/number": NumberFieldSelect, "field/number": NumberFieldSelect,

File diff suppressed because it is too large Load Diff

View File

@ -38,9 +38,9 @@
"defaultValue": "Top" "defaultValue": "Top"
}, },
{ {
"type": "navigationSelect", "type": "navigation",
"label": "Links", "label": "Links",
"key": "type" "key": "links"
} }
] ]
}, },
@ -58,7 +58,6 @@
"type": "select", "type": "select",
"label": "Direction", "label": "Direction",
"key": "direction", "key": "direction",
"key": "direction",
"showInBar": true, "showInBar": true,
"options": [ "options": [
{ {

View File

@ -1,6 +1,6 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { ActionButton, Heading } from "@budibase/bbui" import { ActionButton, Heading, Icon } from "@budibase/bbui"
const { styleable, linkable } = getContext("sdk") const { styleable, linkable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
@ -11,11 +11,11 @@
export let hideLogo = false export let hideLogo = false
export let navigation = "Top" export let navigation = "Top"
export let sticky = true export let sticky = true
export let links
export let links = [ $: validLinks = links?.filter(link => link.text && link.url) || []
{ text: "Some Text", url: "/" }, $: type = navigationClasses[navigation] || "none"
{ text: "Some Text", url: "/" }, let mobileOpen = false
]
const navigationClasses = { const navigationClasses = {
Top: "top", Top: "top",
@ -23,56 +23,76 @@
None: "none", None: "none",
} }
$: type = navigationClasses[navigation] || "none" const isInternal = url => {
let mobileOpen = false return url.startsWith("/")
}
const ensureExternal = url => {
return !url.startsWith("http") ? `http://${url}` : url
}
const close = () => {
mobileOpen = false
}
</script> </script>
<div class="layout layout--{type}" use:styleable={$component.styles}> <div class="layout layout--{type}" use:styleable={$component.styles}>
{#if type !== "none"} {#if type !== "none"}
<div class="nav-wrapper" class:sticky> <div class="nav-wrapper" class:sticky>
<div class="nav nav--{type}"> <div class="nav nav--{type}">
<div class="burger"> <div class="nav-header">
<ActionButton {#if validLinks?.length}
quiet <div class="burger">
icon="ShowMenu" <Icon
on:click={() => (mobileOpen = !mobileOpen)} hoverable
/> name="ShowMenu"
</div> on:click={() => (mobileOpen = !mobileOpen)}
/>
<div class="logo"> </div>
{#if !hideLogo}
<img src="https://i.imgur.com/Xhdt1YP.png" alt={title} />
{/if}
{#if !hideTitle}
<Heading>{title}</Heading>
{/if} {/if}
<div class="logo">
{#if !hideLogo}
<img src={logoUrl} alt={title} />
{/if}
{#if !hideTitle}
<Heading>{title}</Heading>
{/if}
</div>
<div class="portal">
<Icon
hoverable
name="Apps"
on:click={() => (window.location.href = "/builder/apps")}
/>
</div>
</div> </div>
<div class="portal">
<ActionButton quiet icon="Apps" on:click />
</div>
<div <div
class="mobile-click-handler" class="mobile-click-handler"
class:visible={mobileOpen} class:visible={mobileOpen}
on:click={() => (mobileOpen = false)} on:click={() => (mobileOpen = false)}
/> />
<div class="links" class:visible={mobileOpen}> {#if validLinks?.length}
{#each links as { text, url, external }} <div class="links" class:visible={mobileOpen}>
{#if external} {#each validLinks as { text, url }}
<a class="link" href={url}>{text}</a> {#if isInternal(url)}
{:else} <a class="link" href={url} use:linkable on:click={close}>
<a class="link" href={url} use:linkable>{text}</a> {text}
{/if} </a>
{/each} {:else}
<div class="close"> <a class="link" href={ensureExternal(url)} on:click={close}>
<ActionButton {text}
quiet </a>
icon="Close" {/if}
on:click={() => (mobileOpen = false)} {/each}
/> <div class="close">
<Icon
hoverable
name="Close"
on:click={() => (mobileOpen = false)}
/>
</div>
</div> </div>
</div> {/if}
</div> </div>
</div> </div>
{/if} {/if}
@ -110,11 +130,20 @@
} }
.nav { .nav {
flex: 1 1 auto; display: flex;
display: grid; flex-direction: column;
justify-content: flex-start;
align-items: stretch;
padding: var(--spacing-xl); padding: var(--spacing-xl);
max-width: 1400px; width: 1400px;
grid-template-columns: 1fr auto; max-width: 100%;
}
.nav-header {
flex: 0 0 auto;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
} }
.main-wrapper { .main-wrapper {
display: flex; display: flex;
@ -124,12 +153,12 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.main { .main {
flex: 1 1 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
max-width: 1400px; width: 1400px;
max-width: 100%;
position: relative; position: relative;
} }
@ -143,16 +172,13 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
gap: var(--spacing-xl); gap: var(--spacing-xl);
grid-column: 1;
} }
.logo img { .logo img {
height: 48px; height: 48px;
} }
.portal { .portal {
display: grid; display: grid;
justify-items: center; place-items: center;
align-items: center;
grid-column: 2;
} }
.links { .links {
display: flex; display: flex;
@ -160,8 +186,6 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
gap: var(--spacing-l); gap: var(--spacing-l);
grid-column: 1 / 3;
grid-row: 2;
} }
.link { .link {
color: var(--spectrum-alias-text-color); color: var(--spectrum-alias-text-color);
@ -175,8 +199,8 @@
.close { .close {
display: none; display: none;
position: absolute; position: absolute;
top: var(--spacing-m); top: var(--spacing-xl);
right: var(--spacing-m); right: var(--spacing-xl);
} }
.mobile-click-handler { .mobile-click-handler {
display: none; display: none;
@ -194,12 +218,9 @@
} }
.nav--top { .nav--top {
grid-template-rows: auto auto;
justify-content: space-between;
gap: var(--spacing-xl); gap: var(--spacing-xl);
} }
.nav--left { .nav--left {
grid-template-rows: auto 1fr;
width: 250px; width: 250px;
gap: var(--spacing-m); gap: var(--spacing-m);
} }
@ -246,19 +267,11 @@
/* Force standard top bar */ /* Force standard top bar */
.nav { .nav {
justify-content: space-between; padding: var(--spacing-m) var(--spacing-xl);
gap: var(--spacing-xl);
grid-template-columns: auto auto auto;
grid-template-rows: auto;
padding: var(--spacing-m);
} }
.burger { .burger {
display: grid; display: grid;
place-items: center; place-items: center;
grid-column: 1;
}
.logo {
grid-column: 2;
} }
.logo img { .logo img {
height: 36px; height: 36px;
@ -266,9 +279,6 @@
.logo :global(h1) { .logo :global(h1) {
display: none; display: none;
} }
.portal {
grid-column: 3;
}
/* Transform links into drawer */ /* Transform links into drawer */
.links { .links {
@ -289,6 +299,7 @@
} }
.link { .link {
width: calc(100% - 30px); width: calc(100% - 30px);
font-size: 120%;
} }
.links.visible { .links.visible {
opacity: 1; opacity: 1;

View File

@ -24,7 +24,7 @@ export { default as stackedlist } from "./StackedList.svelte"
export { default as card } from "./Card.svelte" export { default as card } from "./Card.svelte"
export { default as text } from "./Text.svelte" export { default as text } from "./Text.svelte"
export { default as navigation } from "./Navigation.svelte" export { default as navigation } from "./Navigation.svelte"
export { default as layout } from "./layout/Layout.svelte" export { default as layout } from "./Layout.svelte"
export { default as link } from "./Link.svelte" export { default as link } from "./Link.svelte"
export { default as heading } from "./Heading.svelte" export { default as heading } from "./Heading.svelte"
export { default as image } from "./Image.svelte" export { default as image } from "./Image.svelte"

View File

@ -1,33 +0,0 @@
<script>
import {
ActionButton,
SideNavigation,
SideNavigationItem as Item,
} from "@budibase/bbui"
export let links
</script>
<div class="overlay">
<SideNavigation>
{#each links as { text, url }}
<!-- Needs logic to select current route -->
<Item selected={false} href={url} on:click>{text}</Item>
{/each}
</SideNavigation>
<div class="close">
<ActionButton quiet icon="Close" on:click />
</div>
</div>
<style>
.overlay {
background: white;
position: absolute;
inset: 0;
}
.close {
position: absolute;
top: var(--spacing-m);
right: var(--spacing-m);
}
</style>

View File

@ -1,71 +0,0 @@
<script>
import Mobile from "./Mobile.svelte"
import {
ActionButton,
SideNavigation,
SideNavigationItem as Item,
} from "@budibase/bbui"
export let navigation
export let links
export let mobileOpen = false
</script>
<div class="container">
{#if navigation === "Top"}
<ul>
{#each links as { text, url }}
<li><a href={url}>{text}</a></li>
{/each}
</ul>
{:else}
<SideNavigation>
{#each links as { text, url }}
<!-- Needs logic to select current route -->
<Item selected={false} href="/">{text}</Item>
{/each}
</SideNavigation>
{/if}
</div>
<div class="mobile">
<ActionButton
quiet
selected
icon="ShowMenu"
on:click={() => (mobileOpen = !mobileOpen)}
/>
{#if mobileOpen}
<Mobile {links} on:click={() => (mobileOpen = !mobileOpen)} />
{/if}
</div>
<style>
.container {
display: none;
}
ul {
list-style-type: none;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
ul > * {
margin-right: 16px;
}
:global(ul > a) {
font-size: 1.5em;
text-decoration: none;
margin-right: 16px;
}
@media (min-width: 600px) {
.mobile {
display: none;
}
.container {
display: initial;
}
}
</style>