Add initial version of side panel component
This commit is contained in:
parent
240c014ae3
commit
058547fd67
|
@ -16,7 +16,8 @@
|
||||||
"children": [
|
"children": [
|
||||||
"container",
|
"container",
|
||||||
"section",
|
"section",
|
||||||
"grid"
|
"grid",
|
||||||
|
"sidepanel"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -5225,5 +5225,13 @@
|
||||||
"suffix": "repeater"
|
"suffix": "repeater"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"sidepanel": {
|
||||||
|
"name": "Side Panel",
|
||||||
|
"icon": "AdDisplay",
|
||||||
|
"hasChildren": true,
|
||||||
|
"illegalChildren": ["section"],
|
||||||
|
"showEmptyState": false,
|
||||||
|
"static": true
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
builderStore,
|
builderStore,
|
||||||
currentRole,
|
currentRole,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
|
sidePanelStore,
|
||||||
} = sdk
|
} = sdk
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
@ -150,113 +151,130 @@
|
||||||
class:desktop={!mobile}
|
class:desktop={!mobile}
|
||||||
class:mobile={!!mobile}
|
class:mobile={!!mobile}
|
||||||
>
|
>
|
||||||
{#if typeClass !== "none"}
|
<div class="layout-body">
|
||||||
<div
|
{#if typeClass !== "none"}
|
||||||
class="interactive component navigation"
|
|
||||||
data-id="navigation"
|
|
||||||
data-name="Navigation"
|
|
||||||
data-icon="Link"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="nav-wrapper"
|
class="interactive component navigation"
|
||||||
class:sticky
|
data-id="navigation"
|
||||||
class:hidden={$routeStore.queryParams?.peek}
|
data-name="Navigation"
|
||||||
class:clickable={$builderStore.inBuilder}
|
data-icon="Link"
|
||||||
on:click={$builderStore.inBuilder
|
|
||||||
? builderStore.actions.clickNav
|
|
||||||
: null}
|
|
||||||
style={navStyle}
|
|
||||||
>
|
>
|
||||||
<div class="nav nav--{typeClass} size--{navWidthClass}">
|
<div
|
||||||
<div class="nav-header">
|
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}
|
{#if validLinks?.length}
|
||||||
<div class="burger">
|
<div class="links" class:visible={mobileOpen}>
|
||||||
<Icon
|
{#each validLinks as { text, url }}
|
||||||
hoverable
|
{#if isInternal(url)}
|
||||||
name="ShowMenu"
|
<a
|
||||||
on:click={() => (mobileOpen = !mobileOpen)}
|
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>
|
</div>
|
||||||
{/if}
|
{/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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !$builderStore.inBuilder && licensing.logoEnabled() && $environmentStore.cloud}
|
||||||
|
<FreeLogo />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<div class="main size--{pageWidthClass}">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
<div id="side-panel-container" class:open={$sidePanelStore.open}>
|
||||||
{#if !$builderStore.inBuilder && licensing.logoEnabled() && $environmentStore.cloud}
|
<div class="side-panel-header">
|
||||||
<FreeLogo />
|
<Icon name="Close" hoverable on:click={sidePanelStore.actions.close} />
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<div class="main size--{pageWidthClass}">
|
|
||||||
<slot />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Main components */
|
/* 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 {
|
.component {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
.layout {
|
.layout-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
height: 100%;
|
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
@ -316,6 +334,28 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xl);
|
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 {
|
.main-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
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 markdownviewer } from "./MarkdownViewer.svelte"
|
||||||
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
||||||
export { default as grid } from "./Grid.svelte"
|
export { default as grid } from "./Grid.svelte"
|
||||||
|
export { default as sidepanel } from "./SidePanel.svelte"
|
||||||
export * from "./charts"
|
export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
export * from "./table"
|
export * from "./table"
|
||||||
|
|
||||||
export * from "./blocks"
|
export * from "./blocks"
|
||||||
export * from "./dynamic-filter"
|
export * from "./dynamic-filter"
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,8 @@
|
||||||
if (callbackCount >= observers.length) {
|
if (callbackCount >= observers.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nextIndicators[idx].visible = entries[0].isIntersecting
|
nextIndicators[idx].visible =
|
||||||
|
nextIndicators[idx].isSidePanel || entries[0].isIntersecting
|
||||||
if (++callbackCount === observers.length) {
|
if (++callbackCount === observers.length) {
|
||||||
indicators = nextIndicators
|
indicators = nextIndicators
|
||||||
updating = false
|
updating = false
|
||||||
|
@ -91,8 +92,9 @@
|
||||||
|
|
||||||
// Extract valid children
|
// Extract valid children
|
||||||
// Sanity limit of 100 active indicators
|
// Sanity limit of 100 active indicators
|
||||||
const children = Array.from(parents)
|
const children = Array.from(
|
||||||
.map(parent => parent?.children?.[0])
|
document.getElementsByClassName(`${componentId}-dom`)
|
||||||
|
)
|
||||||
.filter(x => x != null)
|
.filter(x => x != null)
|
||||||
.slice(0, 100)
|
.slice(0, 100)
|
||||||
|
|
||||||
|
@ -121,6 +123,7 @@
|
||||||
width: elBounds.width + 4,
|
width: elBounds.width + 4,
|
||||||
height: elBounds.height + 4,
|
height: elBounds.height + 4,
|
||||||
visible: false,
|
visible: false,
|
||||||
|
isSidePanel: child.classList.contains("side-panel"),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
componentStore,
|
componentStore,
|
||||||
currentRole,
|
currentRole,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
|
sidePanelStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { styleable } from "utils/styleable"
|
import { styleable } from "utils/styleable"
|
||||||
import { linkable } from "utils/linkable"
|
import { linkable } from "utils/linkable"
|
||||||
|
@ -30,6 +31,7 @@ export default {
|
||||||
uploadStore,
|
uploadStore,
|
||||||
componentStore,
|
componentStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
|
sidePanelStore,
|
||||||
currentRole,
|
currentRole,
|
||||||
styleable,
|
styleable,
|
||||||
linkable,
|
linkable,
|
||||||
|
|
|
@ -24,6 +24,7 @@ export {
|
||||||
dndIsNewComponent,
|
dndIsNewComponent,
|
||||||
dndIsDragging,
|
dndIsDragging,
|
||||||
} from "./dnd"
|
} from "./dnd"
|
||||||
|
export { sidePanelStore } from "./sidePanel.js"
|
||||||
|
|
||||||
// Context stores are layered and duplicated, so it is not a singleton
|
// Context stores are layered and duplicated, so it is not a singleton
|
||||||
export { createContextStore } from "./context"
|
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
|
// Creates event listeners and applies initial styles
|
||||||
const setupStyles = (newStyles = {}) => {
|
const setupStyles = (newStyles = {}) => {
|
||||||
|
node.classList.add(`${newStyles.id}-dom`)
|
||||||
|
|
||||||
let baseStyles = {}
|
let baseStyles = {}
|
||||||
if (newStyles.empty) {
|
if (newStyles.empty) {
|
||||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)"
|
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)"
|
||||||
|
|
Loading…
Reference in New Issue