Add initial version of side panel component

This commit is contained in:
Andrew Kingston 2022-10-27 08:08:35 +01:00
parent a80821cd73
commit 09360cea55
10 changed files with 241 additions and 90 deletions

View File

@ -16,7 +16,8 @@
"children": [
"container",
"section",
"grid"
"grid",
"sidepanel"
]
},
{

View File

@ -5225,5 +5225,13 @@
"suffix": "repeater"
}
]
},
"sidepanel": {
"name": "Side Panel",
"icon": "AdDisplay",
"hasChildren": true,
"illegalChildren": ["section"],
"showEmptyState": false,
"static": true
}
}

View File

@ -16,6 +16,7 @@
builderStore,
currentRole,
environmentStore,
sidePanelStore,
} = sdk
const component = getContext("component")
const context = getContext("context")
@ -150,113 +151,130 @@
class:desktop={!mobile}
class:mobile={!!mobile}
>
{#if typeClass !== "none"}
<div
class="interactive component navigation"
data-id="navigation"
data-name="Navigation"
data-icon="Link"
>
<div class="layout-body">
{#if typeClass !== "none"}
<div
class="nav-wrapper"
class:sticky
class:hidden={$routeStore.queryParams?.peek}
class:clickable={$builderStore.inBuilder}
on:click={$builderStore.inBuilder
? builderStore.actions.clickNav
: null}
style={navStyle}
class="interactive component navigation"
data-id="navigation"
data-name="Navigation"
data-icon="Link"
>
<div class="nav nav--{typeClass} size--{navWidthClass}">
<div class="nav-header">
<div
class="nav-wrapper"
class:sticky
class:hidden={$routeStore.queryParams?.peek}
class:clickable={$builderStore.inBuilder}
on:click={$builderStore.inBuilder
? builderStore.actions.clickNav
: null}
style={navStyle}
>
<div class="nav nav--{typeClass} size--{navWidthClass}">
<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={navigateToPortal} />
</div>
</div>
<div
class="mobile-click-handler"
class:visible={mobileOpen}
on:click={() => (mobileOpen = false)}
/>
{#if validLinks?.length}
<div class="burger">
<Icon
hoverable
name="ShowMenu"
on:click={() => (mobileOpen = !mobileOpen)}
/>
<div class="links" class:visible={mobileOpen}>
{#each validLinks as { text, url }}
{#if isInternal(url)}
<a
class={FieldTypes.LINK}
href={url}
use:linkable
on:click={close}
use:active={url}
>
{text}
</a>
{:else}
<a
class={FieldTypes.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 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={navigateToPortal} />
</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={FieldTypes.LINK}
href={url}
use:linkable
on:click={close}
use:active={url}
>
{text}
</a>
{:else}
<a
class={FieldTypes.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}
{#if !$builderStore.inBuilder && licensing.logoEnabled() && $environmentStore.cloud}
<FreeLogo />
{/if}
<div class="main-wrapper">
<div class="main size--{pageWidthClass}">
<slot />
</div>
</div>
{/if}
{#if !$builderStore.inBuilder && licensing.logoEnabled() && $environmentStore.cloud}
<FreeLogo />
{/if}
<div class="main-wrapper">
<div class="main size--{pageWidthClass}">
<slot />
</div>
<div id="side-panel-container" class:open={$sidePanelStore.open}>
<div class="side-panel-header">
<Icon name="Close" hoverable on:click={sidePanelStore.actions.close} />
</div>
</div>
</div>
<style>
/* Main components */
.layout {
height: 100%;
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-content: center;
align-items: stretch;
z-index: 1;
border-top: 1px solid var(--spectrum-global-color-gray-300);
overflow: hidden;
}
.component {
display: contents;
}
.layout {
.layout-body {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
height: 100%;
flex: 1 1 auto;
overflow: auto;
overflow-x: hidden;
@ -316,6 +334,28 @@
align-items: center;
gap: var(--spacing-xl);
}
#side-panel-container {
flex: 0 0 360px;
background: var(--background);
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
transition: margin-right 130ms ease-out;
z-index: 3;
padding: var(--spacing-xl);
margin-right: -370px;
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
}
#side-panel-container.open {
margin-right: 0;
}
.side-panel-header {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.main-wrapper {
display: flex;
flex-direction: row;

View File

@ -0,0 +1,57 @@
<script>
import { getContext } from "svelte"
const component = getContext("component")
const { styleable, sidePanelStore } = getContext("sdk")
$: open = $sidePanelStore.contentId === $component.id
const showInSidePanel = (el, visible) => {
const target = document.getElementById("side-panel-container")
const destroy = () => {
el.parentNode?.removeChild(el)
}
const update = visible => {
if (visible) {
target.appendChild(el)
el.hidden = false
} else {
destroy()
el.hidden = true
}
}
// Apply initial visibility
update(visible)
return {
update,
destroy,
}
}
</script>
<div
use:styleable={$component.styles}
use:showInSidePanel={open}
hidden
class="side-panel"
>
<slot />
</div>
<div>
<button on:click={() => sidePanelStore.actions.open($component.id)}
>open</button
>
<button on:click={sidePanelStore.actions.close}>close</button>
</div>
<style>
.side-panel {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xl);
}
</style>

View File

@ -35,10 +35,10 @@ export { default as tag } from "./Tag.svelte"
export { default as markdownviewer } from "./MarkdownViewer.svelte"
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
export { default as grid } from "./Grid.svelte"
export { default as sidepanel } from "./SidePanel.svelte"
export * from "./charts"
export * from "./forms"
export * from "./table"
export * from "./blocks"
export * from "./dynamic-filter"

View File

@ -43,7 +43,8 @@
if (callbackCount >= observers.length) {
return
}
nextIndicators[idx].visible = entries[0].isIntersecting
nextIndicators[idx].visible =
nextIndicators[idx].isSidePanel || entries[0].isIntersecting
if (++callbackCount === observers.length) {
indicators = nextIndicators
updating = false
@ -91,8 +92,9 @@
// Extract valid children
// Sanity limit of 100 active indicators
const children = Array.from(parents)
.map(parent => parent?.children?.[0])
const children = Array.from(
document.getElementsByClassName(`${componentId}-dom`)
)
.filter(x => x != null)
.slice(0, 100)
@ -121,6 +123,7 @@
width: elBounds.width + 4,
height: elBounds.height + 4,
visible: false,
isSidePanel: child.classList.contains("side-panel"),
})
})
}

View File

@ -10,6 +10,7 @@ import {
componentStore,
currentRole,
environmentStore,
sidePanelStore,
} from "stores"
import { styleable } from "utils/styleable"
import { linkable } from "utils/linkable"
@ -30,6 +31,7 @@ export default {
uploadStore,
componentStore,
environmentStore,
sidePanelStore,
currentRole,
styleable,
linkable,

View File

@ -24,6 +24,7 @@ export {
dndIsNewComponent,
dndIsDragging,
} from "./dnd"
export { sidePanelStore } from "./sidePanel.js"
// Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context"

View File

@ -0,0 +1,37 @@
import { writable, derived } from "svelte/store"
export const createSidePanelStore = () => {
const initialState = {
contentId: null,
}
const store = writable(initialState)
const derivedStore = derived(store, $store => {
return {
...$store,
open: $store.contentId != null,
}
})
const open = id => {
store.update(state => {
state.contentId = id
return state
})
}
const close = () => {
store.update(state => {
state.contentId = null
return state
})
}
return {
subscribe: derivedStore.subscribe,
actions: {
open,
close,
},
}
}
export const sidePanelStore = createSidePanelStore()

View File

@ -25,6 +25,8 @@ export const styleable = (node, styles = {}) => {
// Creates event listeners and applies initial styles
const setupStyles = (newStyles = {}) => {
node.classList.add(`${newStyles.id}-dom`)
let baseStyles = {}
if (newStyles.empty) {
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)"