Add initial version of side panel component
This commit is contained in:
parent
a80821cd73
commit
09360cea55
|
@ -16,7 +16,8 @@
|
|||
"children": [
|
||||
"container",
|
||||
"section",
|
||||
"grid"
|
||||
"grid",
|
||||
"sidepanel"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -5225,5 +5225,13 @@
|
|||
"suffix": "repeater"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sidepanel": {
|
||||
"name": "Side Panel",
|
||||
"icon": "AdDisplay",
|
||||
"hasChildren": true,
|
||||
"illegalChildren": ["section"],
|
||||
"showEmptyState": false,
|
||||
"static": true
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
|
@ -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)"
|
||||
|
|
Loading…
Reference in New Issue