mirror of https://github.com/Budibase/budibase.git
Browse Source
Responsive layouts, responsive autoscreens, sections and navigation customisationpull/1774/head
committed by
GitHub
52 changed files with 968 additions and 2563 deletions
@ -0,0 +1,114 @@ |
|||
<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 |
|||
|
|||
$: links.forEach(link => { |
|||
if (!link.id) { |
|||
link.id = generate() |
|||
} |
|||
}) |
|||
|
|||
$: urlOptions = $store.screens |
|||
.map(screen => screen.routing?.route) |
|||
.filter(x => x != null) |
|||
|
|||
const addLink = () => { |
|||
links = [...links, {}] |
|||
} |
|||
|
|||
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> |
|||
@ -0,0 +1,23 @@ |
|||
<script> |
|||
import { Button, ActionButton, 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> |
|||
|
|||
<ActionButton on:click={drawer.show}>Configure Links</ActionButton> |
|||
<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> |
|||
@ -0,0 +1,75 @@ |
|||
<script> |
|||
import { createEventDispatcher } from "svelte" |
|||
import { ActionButton, Body, Icon, Modal, ModalContent } from "@budibase/bbui" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value = "" |
|||
let selected = value |
|||
|
|||
let modal |
|||
let layoutMap = { |
|||
mainSidebar: { |
|||
name: "Main with Sidebar", |
|||
icon: "ColumnTwoB", |
|||
}, |
|||
sidebarMain: { |
|||
name: "Sidebar with Main", |
|||
icon: "ColumnTwoC", |
|||
}, |
|||
twoColumns: { |
|||
name: "Two columns", |
|||
icon: "ColumnTwoA", |
|||
}, |
|||
threeColumns: { |
|||
name: "Three columns", |
|||
icon: "ViewColumn", |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<ActionButton on:click={modal.show}>{layoutMap[value].name}</ActionButton> |
|||
<Modal bind:this={modal}> |
|||
<ModalContent |
|||
onConfirm={() => dispatch("change", selected)} |
|||
size="L" |
|||
title="Select layout" |
|||
> |
|||
<div class="container"> |
|||
{#each Object.entries(layoutMap) as [key, value]} |
|||
<button |
|||
class:selected={selected === key} |
|||
on:click={() => (selected = key)} |
|||
class="layout" |
|||
> |
|||
<Icon color="white" size="L" name={value.icon} /> |
|||
<Body size="XS">{value.name}</Body> |
|||
</button> |
|||
{/each} |
|||
</div> |
|||
</ModalContent> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
grid-gap: 20px; |
|||
grid-template-columns: 1fr 1fr 1fr; |
|||
} |
|||
|
|||
.layout { |
|||
color: var(--spectrum-body-m-text-color, var(--spectrum-alias-text-color)); |
|||
border: none; |
|||
box-sizing: border-box; |
|||
display: grid; |
|||
place-items: center; |
|||
background: var(--background-alt); |
|||
grid-gap: var(--spectrum-alias-grid-margin-xsmall); |
|||
padding: var(--spectrum-alias-item-padding-s); |
|||
transition: 0.3s all; |
|||
border-radius: var(--spacing-s); |
|||
} |
|||
.selected { |
|||
background: var(--spectrum-alias-background-color-tertiary); |
|||
} |
|||
</style> |
|||
@ -1,54 +0,0 @@ |
|||
<script> |
|||
import { Button } from "@budibase/bbui" |
|||
export let remove = false |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="content"> |
|||
<div class="img"><img src="https://picsum.photos/60/60" alt="zoom" /></div> |
|||
<div class="body"> |
|||
<div class="title">Zoom</div> |
|||
<div class="description"> |
|||
Lorem, ipsum dolor sit amet consectetur adipisicing elit |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="footer"> |
|||
<Button wide error={remove} secondary={!remove} on:click> |
|||
<span>{remove ? "Remove" : "Add"}</span> |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
padding: 12px; |
|||
background: var(--light-grey); |
|||
grid-gap: 20px; |
|||
} |
|||
span { |
|||
font-size: 12px; |
|||
font-weight: bold; |
|||
} |
|||
.content { |
|||
display: grid; |
|||
grid-gap: 12px; |
|||
grid-template-columns: 60px auto; |
|||
} |
|||
.title { |
|||
font-size: 14px; |
|||
font-weight: bold; |
|||
} |
|||
.description { |
|||
font-size: 12px; |
|||
} |
|||
.img { |
|||
border-radius: 3px; |
|||
overflow: hidden; |
|||
} |
|||
img { |
|||
height: 60px; |
|||
width: 60px; |
|||
} |
|||
</style> |
|||
@ -1,33 +0,0 @@ |
|||
<script> |
|||
import SettingsModal from "./SettingsModal.svelte" |
|||
import { Modal, Icon } from "@budibase/bbui" |
|||
|
|||
let modal |
|||
</script> |
|||
|
|||
<div |
|||
class="topnavitemright settings" |
|||
data-cy="settings-icon" |
|||
on:click={modal.show} |
|||
> |
|||
<Icon hoverable name="Settings" /> |
|||
</div> |
|||
<Modal bind:this={modal} width="600px"> |
|||
<SettingsModal /> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.topnavitemright { |
|||
cursor: pointer; |
|||
color: var(--grey-7); |
|||
margin: 0 12px 0 0; |
|||
font-weight: 600; |
|||
font-size: 1rem; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 24px; |
|||
width: 24px; |
|||
} |
|||
</style> |
|||
@ -1,34 +0,0 @@ |
|||
<script> |
|||
import { General, DangerZone } from "./tabs" |
|||
import { ModalContent, Tab, Tabs } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Settings" |
|||
showConfirmButton={false} |
|||
showCancelButton={false} |
|||
> |
|||
<div class="container"> |
|||
<Tabs selected="General"> |
|||
<Tab title="General"> |
|||
<General /> |
|||
</Tab> |
|||
<!-- <Tab title="API Keys"> |
|||
<APIKeys /> |
|||
</Tab> --> |
|||
<Tab title="Danger Zone"> |
|||
<DangerZone /> |
|||
</Tab> |
|||
</Tabs> |
|||
</div> |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.container :global(section > header) { |
|||
/* Fix margin defined in BBUI as L rather than XL */ |
|||
margin-bottom: var(--spacing-xl); |
|||
} |
|||
.container :global(textarea) { |
|||
min-height: 60px; |
|||
} |
|||
</style> |
|||
@ -1,62 +0,0 @@ |
|||
<script> |
|||
import { Input, Label, Link } from "@budibase/bbui" |
|||
import api from "builderStore/api" |
|||
import { notifications } from "@budibase/bbui" |
|||
import { database } from "stores/backend" |
|||
import analytics from "analytics" |
|||
|
|||
let keys = { budibase: "" } |
|||
|
|||
async function updateKey([key, value]) { |
|||
if (key === "budibase") { |
|||
const isValid = await analytics.identifyByApiKey(value) |
|||
if (!isValid) { |
|||
notifications.error("Your API Key is invalid.") |
|||
keys = { ...keys } |
|||
return |
|||
} |
|||
} |
|||
const response = await api.put(`/api/keys/${key}`, { value }) |
|||
const res = await response.json() |
|||
keys = { ...keys, ...res } |
|||
notifications.success("API Key saved.") |
|||
} |
|||
|
|||
// Get Keys |
|||
async function fetchKeys() { |
|||
const response = await api.get(`/api/keys/`) |
|||
const res = await response.json() |
|||
// dont want this to ever be editable, as its fetched based on Api Key |
|||
if (res.userId) delete res.userId |
|||
keys = res |
|||
} |
|||
|
|||
fetchKeys() |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<Input |
|||
on:change={e => updateKey(["budibase", e.detail])} |
|||
value={keys.budibase} |
|||
label="Budibase Cloud API Key" |
|||
/> |
|||
<Link primary href="https://portal.budi.live"> |
|||
Log in to the Budibase Hosting Portal to get your API Key. → |
|||
</Link> |
|||
<div> |
|||
<Label extraSmall grey>Instance ID (Webhooks)</Label> |
|||
<span>{$database._id}</span> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
grid-gap: var(--spacing-xl); |
|||
} |
|||
|
|||
span { |
|||
font-size: var(--font-size-xs); |
|||
font-weight: 600; |
|||
} |
|||
</style> |
|||
@ -1,56 +0,0 @@ |
|||
<script> |
|||
import { params, goto } from "@roxi/routify" |
|||
import { Input, Button, Body } from "@budibase/bbui" |
|||
import { del } from "builderStore/api" |
|||
|
|||
let value = "" |
|||
let loading = false |
|||
|
|||
async function deleteApp() { |
|||
loading = true |
|||
const id = $params.application |
|||
await del(`/api/applications/${id}`) |
|||
loading = false |
|||
$goto("/builder") |
|||
} |
|||
</script> |
|||
|
|||
<div class="background"> |
|||
<Body> |
|||
Type |
|||
<b>DELETE</b> |
|||
into the textbox, then click the following button to delete your entire web app. |
|||
</Body> |
|||
<Input |
|||
on:change={e => (value = e.detail)} |
|||
disabled={loading} |
|||
placeholder="" |
|||
/> |
|||
<div class="buttons"> |
|||
<Button |
|||
warning |
|||
disabled={value !== "DELETE" || loading} |
|||
on:click={deleteApp} |
|||
> |
|||
Delete Entire App |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.background { |
|||
display: grid; |
|||
grid-gap: var(--spacing-xl); |
|||
} |
|||
.background :global(p) { |
|||
line-height: 1.2; |
|||
margin: 0; |
|||
} |
|||
|
|||
.buttons { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-end; |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
@ -1,97 +0,0 @@ |
|||
<script> |
|||
import { Input, TextArea } from "@budibase/bbui" |
|||
import { store, hostingStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import { object, string } from "yup" |
|||
import { onMount } from "svelte" |
|||
import { get } from "svelte/store" |
|||
|
|||
let nameValidation, nameError |
|||
let urlValidation, urlError |
|||
|
|||
$: checkName($store.name) |
|||
$: checkUrl($store.url) |
|||
|
|||
async function updateApplication(data) { |
|||
const response = await api.put(`/api/applications/${$store.appId}`, data) |
|||
await response.json() |
|||
store.update(state => { |
|||
state = { |
|||
...state, |
|||
...data, |
|||
} |
|||
return state |
|||
}) |
|||
} |
|||
|
|||
async function checkValidation(input, validation) { |
|||
if (!input || !validation) { |
|||
return |
|||
} |
|||
try { |
|||
await object(validation).validate(input, { abortEarly: false }) |
|||
} catch (error) { |
|||
if (!error || !error.inner) return "" |
|||
return error.inner.reduce((acc, err) => { |
|||
return acc + err.message |
|||
}, "") |
|||
} |
|||
} |
|||
|
|||
async function checkName(name) { |
|||
nameError = await checkValidation({ name }, nameValidation) |
|||
} |
|||
|
|||
async function checkUrl(url) { |
|||
urlError = await checkValidation({ url: url.toLowerCase() }, urlValidation) |
|||
} |
|||
|
|||
onMount(async () => { |
|||
const nameError = "Your application must have a name.", |
|||
urlError = "Your application must have a URL." |
|||
await hostingStore.actions.fetchDeployedApps() |
|||
const existingAppNames = get(hostingStore).deployedAppNames |
|||
const existingAppUrls = get(hostingStore).deployedAppUrls |
|||
const nameIdx = existingAppNames.indexOf(get(store).name) |
|||
const urlIdx = existingAppUrls.indexOf(get(store).url) |
|||
if (nameIdx !== -1) { |
|||
existingAppNames.splice(nameIdx, 1) |
|||
} |
|||
if (urlIdx !== -1) { |
|||
existingAppUrls.splice(urlIdx, 1) |
|||
} |
|||
nameValidation = { |
|||
name: string().required(nameError).notOneOf(existingAppNames), |
|||
} |
|||
urlValidation = { |
|||
url: string().required(urlError).notOneOf(existingAppUrls), |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<Input |
|||
on:change={e => updateApplication({ name: e.detail })} |
|||
value={$store.name} |
|||
error={nameError} |
|||
label="App Name" |
|||
/> |
|||
<Input |
|||
on:change={e => updateApplication({ url: e.detail })} |
|||
value={$store.url} |
|||
error={urlError} |
|||
label="App URL" |
|||
/> |
|||
<TextArea |
|||
on:change={e => updateApplication({ description: e.detail })} |
|||
value={$store.description} |
|||
label="App Description" |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
grid-gap: var(--spacing-xl); |
|||
} |
|||
</style> |
|||
@ -1,40 +0,0 @@ |
|||
<script> |
|||
import Integration from "../Integration.svelte" |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="title">Your Integrations</div> |
|||
<div class="integrations"> |
|||
<Integration remove /> |
|||
<Integration remove /> |
|||
<Integration remove /> |
|||
<Integration remove /> |
|||
</div> |
|||
<div class="apps">Recommended apps</div> |
|||
<div class="integrations"> |
|||
<Integration /> |
|||
<Integration /> |
|||
<Integration /> |
|||
<Integration /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: grid; |
|||
grid-gap: 16px; |
|||
} |
|||
.integrations { |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr; |
|||
grid-gap: 20px; |
|||
} |
|||
.title { |
|||
font-size: 18px; |
|||
font-weight: bold; |
|||
} |
|||
.apps { |
|||
font-size: 14px; |
|||
font-weight: bold; |
|||
} |
|||
</style> |
|||
@ -1 +0,0 @@ |
|||
Permissions |
|||
@ -1,5 +0,0 @@ |
|||
export { default as General } from "./General.svelte" |
|||
export { default as Integrations } from "./Integrations.svelte" |
|||
export { default as Permissions } from "./Permissions.svelte" |
|||
export { default as APIKeys } from "./APIKeys.svelte" |
|||
export { default as DangerZone } from "./DangerZone.svelte" |
|||
File diff suppressed because it is too large
@ -1,16 +0,0 @@ |
|||
import { fade, blur, scale, fly } from "svelte/transition" |
|||
|
|||
// Default options
|
|||
const transitions = new Map([ |
|||
["fade", { tn: fade, opt: {} }], |
|||
["blur", { tn: blur, opt: {} }], |
|||
// This one seems to not result in any effect
|
|||
// ["slide", { tn: slide, opt: {} }],
|
|||
["scale", { tn: scale, opt: {} }], |
|||
["fly", { tn: fly, opt: { y: 80 } }], |
|||
]) |
|||
|
|||
export default function transition(node, { type, options = {} }) { |
|||
const { tn, opt } = transitions.get(type) || { tn: () => {}, opt: {} } |
|||
return tn(node, { ...opt, ...options }) |
|||
} |
|||
@ -0,0 +1,337 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { Heading, Icon } from "@budibase/bbui" |
|||
|
|||
const { styleable, linkable } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
export let title |
|||
export let hideTitle = false |
|||
export let logoUrl |
|||
export let hideLogo = false |
|||
export let navigation = "Top" |
|||
export let sticky = false |
|||
export let links |
|||
export let width = "Large" |
|||
|
|||
const navigationClasses = { |
|||
Top: "top", |
|||
Left: "left", |
|||
None: "none", |
|||
} |
|||
const widthClasses = { |
|||
Large: "l", |
|||
Medium: "m", |
|||
Small: "s", |
|||
} |
|||
|
|||
$: validLinks = links?.filter(link => link.text && link.url) || [] |
|||
$: typeClass = navigationClasses[navigation] || "none" |
|||
$: widthClass = widthClasses[width] || "l" |
|||
let mobileOpen = false |
|||
|
|||
const isInternal = url => { |
|||
return url.startsWith("/") |
|||
} |
|||
|
|||
const ensureExternal = url => { |
|||
return !url.startsWith("http") ? `http://${url}` : url |
|||
} |
|||
|
|||
const close = () => { |
|||
mobileOpen = false |
|||
} |
|||
</script> |
|||
|
|||
<div class="layout layout--{typeClass}" use:styleable={$component.styles}> |
|||
{#if typeClass !== "none"} |
|||
<div class="nav-wrapper" class:sticky> |
|||
<div class="nav nav--{typeClass} size--{widthClass}"> |
|||
<div class="nav-header"> |
|||
{#if validLinks?.length} |
|||
<div class="burger"> |
|||
<Icon |
|||
hoverable |
|||
name="ShowMenu" |
|||
on:click={() => (mobileOpen = !mobileOpen)} |
|||
/> |
|||
</div> |
|||
{/if} |
|||
<div class="logo"> |
|||
{#if !hideLogo} |
|||
<img |
|||
src={logoUrl || "https://i.imgur.com/Xhdt1YP.png"} |
|||
alt={title} |
|||
/> |
|||
{/if} |
|||
{#if !hideTitle && title} |
|||
<Heading size="S">{title}</Heading> |
|||
{/if} |
|||
</div> |
|||
<div class="portal"> |
|||
<Icon |
|||
hoverable |
|||
name="Apps" |
|||
on:click={() => (window.location.href = "/builder/apps")} |
|||
/> |
|||
</div> |
|||
</div> |
|||
<div |
|||
class="mobile-click-handler" |
|||
class:visible={mobileOpen} |
|||
on:click={() => (mobileOpen = false)} |
|||
/> |
|||
{#if validLinks?.length} |
|||
<div class="links" class:visible={mobileOpen}> |
|||
{#each validLinks as { text, url }} |
|||
{#if isInternal(url)} |
|||
<a class="link" href={url} use:linkable on:click={close}> |
|||
{text} |
|||
</a> |
|||
{:else} |
|||
<a class="link" href={ensureExternal(url)} on:click={close}> |
|||
{text} |
|||
</a> |
|||
{/if} |
|||
{/each} |
|||
<div class="close"> |
|||
<Icon |
|||
hoverable |
|||
name="Close" |
|||
on:click={() => (mobileOpen = false)} |
|||
/> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
<div class="main-wrapper"> |
|||
<div class="main size--{widthClass}"> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
/* Main components */ |
|||
.layout { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
height: 100%; |
|||
overflow: auto; |
|||
} |
|||
|
|||
.nav-wrapper { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: center; |
|||
align-items: stretch; |
|||
background: white; |
|||
z-index: 1; |
|||
box-shadow: 0 0 8px -1px rgba(0, 0, 0, 0.075); |
|||
} |
|||
.layout--top .nav-wrapper.sticky { |
|||
position: sticky; |
|||
top: 0; |
|||
left: 0; |
|||
} |
|||
|
|||
.nav { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
padding: var(--spacing-xl) 32px; |
|||
max-width: 100%; |
|||
gap: var(--spacing-xl); |
|||
} |
|||
.nav-header { |
|||
flex: 0 0 auto; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
gap: var(--spacing-xl); |
|||
} |
|||
.main-wrapper { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: center; |
|||
align-items: stretch; |
|||
flex: 1 1 auto; |
|||
} |
|||
.main { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
max-width: 100%; |
|||
position: relative; |
|||
padding: 32px; |
|||
} |
|||
.size--s { |
|||
width: 800px; |
|||
} |
|||
.size--m { |
|||
width: 1100px; |
|||
} |
|||
.size--l { |
|||
width: 1400px; |
|||
} |
|||
|
|||
/* Nav components */ |
|||
.burger { |
|||
display: none; |
|||
} |
|||
.logo { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
gap: var(--spacing-m); |
|||
flex: 1 1 auto; |
|||
} |
|||
.logo img { |
|||
height: 36px; |
|||
} |
|||
.logo :global(h1) { |
|||
font-weight: 600; |
|||
flex: 1 1 auto; |
|||
width: 0; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
.portal { |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
.links { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
gap: var(--spacing-xl); |
|||
margin-top: var(--spacing-xl); |
|||
} |
|||
.link { |
|||
color: var(--spectrum-alias-text-color); |
|||
font-size: var(--spectrum-global-dimension-font-size-200); |
|||
font-weight: 600; |
|||
transition: color 130ms ease-out; |
|||
} |
|||
.link:hover { |
|||
color: var(--spectrum-global-color-blue-600); |
|||
} |
|||
.close { |
|||
display: none; |
|||
position: absolute; |
|||
top: var(--spacing-xl); |
|||
right: var(--spacing-xl); |
|||
} |
|||
.mobile-click-handler { |
|||
display: none; |
|||
} |
|||
|
|||
/* Desktop nav overrides */ |
|||
@media (min-width: 600px) { |
|||
.layout--left { |
|||
flex-direction: row; |
|||
overflow: hidden; |
|||
} |
|||
.layout--left .main-wrapper { |
|||
height: 100%; |
|||
overflow: auto; |
|||
} |
|||
|
|||
.nav--left { |
|||
width: 250px; |
|||
padding: var(--spacing-xl); |
|||
} |
|||
|
|||
.nav--left .links { |
|||
margin-top: var(--spacing-m); |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
} |
|||
.nav--left .link { |
|||
font-size: var(--spectrum-global-dimension-font-size-150); |
|||
} |
|||
} |
|||
|
|||
/* Mobile nav overrides */ |
|||
@media (max-width: 600px) { |
|||
.nav-wrapper { |
|||
position: sticky; |
|||
top: 0; |
|||
left: 0; |
|||
box-shadow: 0 0 8px -1px rgba(0, 0, 0, 0.075); |
|||
} |
|||
|
|||
/* Show close button in drawer */ |
|||
.close { |
|||
display: block; |
|||
} |
|||
|
|||
/* Force standard top bar */ |
|||
.nav { |
|||
padding: var(--spacing-m) 16px; |
|||
} |
|||
.burger { |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
.logo { |
|||
flex: 0 0 auto; |
|||
} |
|||
.logo :global(h1) { |
|||
display: none; |
|||
} |
|||
|
|||
/* Reduce padding */ |
|||
.main { |
|||
padding: 16px; |
|||
} |
|||
|
|||
/* Transform links into drawer */ |
|||
.links { |
|||
margin-top: 0; |
|||
position: fixed; |
|||
top: 0; |
|||
left: -250px; |
|||
transform: translateX(0); |
|||
width: 250px; |
|||
transition: transform 0.26s ease-in-out, opacity 0.26s ease-in-out; |
|||
height: 100vh; |
|||
opacity: 0; |
|||
background: white; |
|||
z-index: 999; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
padding: var(--spacing-xl); |
|||
} |
|||
.link { |
|||
width: calc(100% - 30px); |
|||
font-size: 120%; |
|||
} |
|||
.links.visible { |
|||
opacity: 1; |
|||
transform: translateX(250px); |
|||
box-shadow: 0 0 40px 20px rgba(0, 0, 0, 0.1); |
|||
} |
|||
.mobile-click-handler.visible { |
|||
position: fixed; |
|||
display: block; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
z-index: 998; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,73 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import Placeholder from "./Placeholder.svelte" |
|||
|
|||
const { styleable, builderStore } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
export let type = "mainSidebar" |
|||
export let minSize = 250 |
|||
|
|||
let layoutMap = { |
|||
mainSidebar: 2, |
|||
sidebarMain: 2, |
|||
twoColumns: 2, |
|||
threeColumns: 3, |
|||
} |
|||
|
|||
let containerWidth |
|||
$: columnsDependingOnSize = calculateColumns(containerWidth) |
|||
|
|||
function calculateColumns(parentWidth) { |
|||
const numberOfAllowedColumns = Math.floor(parentWidth / minSize) || 100 |
|||
if (layoutMap[type] <= numberOfAllowedColumns) { |
|||
return false |
|||
} else if (layoutMap[type] > numberOfAllowedColumns) { |
|||
return numberOfAllowedColumns |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div |
|||
bind:clientWidth={containerWidth} |
|||
class="{type} columns-{columnsDependingOnSize}" |
|||
use:styleable={$component.styles} |
|||
> |
|||
<slot /> |
|||
{#if layoutMap[type] - $component.children > 0} |
|||
{#each new Array(layoutMap[type] - $component.children) as _} |
|||
<div class:placeholder={$builderStore.inBuilder}> |
|||
<Placeholder /> |
|||
</div> |
|||
{/each} |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
display: grid; |
|||
grid-gap: 16px; |
|||
} |
|||
.mainSidebar { |
|||
grid-template-columns: 3fr 1fr; |
|||
} |
|||
.sidebarMain { |
|||
grid-template-columns: 1fr 3fr; |
|||
} |
|||
.twoColumns { |
|||
grid-template-columns: 1fr 1fr; |
|||
} |
|||
.threeColumns { |
|||
grid-template-columns: 1fr 1fr 1fr; |
|||
} |
|||
.columns-1 { |
|||
grid-template-columns: 1fr; |
|||
} |
|||
.columns-2 { |
|||
grid-template-columns: 1fr 1fr; |
|||
} |
|||
.placeholder { |
|||
border: 2px dashed var(--grey-5); |
|||
padding: var(--spacing-l); |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue