Merge pull request #2576 from Budibase/responsive-preview

Responsive preview
This commit is contained in:
Andrew Kingston 2021-09-10 12:13:46 +01:00 committed by GitHub
commit 10182e19c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 272 additions and 136 deletions

View File

@ -43,6 +43,7 @@ const INITIAL_FRONTEND_STATE = {
deviceAwareness: false,
state: false,
customThemes: false,
devicePreview: false,
},
currentFrontEndType: "none",
selectedScreenId: "",
@ -56,6 +57,7 @@ const INITIAL_FRONTEND_STATE = {
clientLibPath: "",
theme: "",
customTheme: {},
previewDevice: "desktop",
}
export const getFrontendStore = () => {
@ -230,6 +232,12 @@ export const getFrontendStore = () => {
await store.actions.layouts.save(selectedAsset)
}
},
setDevice: device => {
store.update(state => {
state.previewDevice = device
return state
})
},
},
layouts: {
select: layoutId => {

View File

@ -83,10 +83,11 @@
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
margin-top: -10px;
}
.components :global(> *) {
margin-top: 10px;
height: 32px;
display: grid;
place-items: center;
}
.buttonContent {

View File

@ -50,6 +50,7 @@
previewType: $store.currentFrontEndType,
theme: $store.theme,
customTheme: $store.customTheme,
previewDevice: $store.previewDevice,
}
// Saving pages and screens to the DB causes them to have _revs.
@ -140,11 +141,12 @@
</div>
{/if}
<iframe
style="height: 100%; width: 100%"
title="componentPreview"
bind:this={iframe}
srcdoc={template}
class:hidden={loading || error}
class:tablet={$store.previewDevice === "tablet"}
class:mobile={$store.previewDevice === "mobile"}
/>
</div>
<ConfirmDialog
@ -160,6 +162,8 @@
.component-container {
grid-row-start: middle;
grid-column-start: middle;
display: grid;
place-items: center;
position: relative;
overflow: hidden;
margin: auto;
@ -200,4 +204,9 @@
font-weight: 400;
margin: 0;
}
iframe {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,22 @@
<script>
import { ActionGroup, ActionButton } from "@budibase/bbui"
import { store } from "builderStore"
</script>
<ActionGroup compact>
<ActionButton
icon="DeviceDesktop"
selected={$store.previewDevice === "desktop"}
on:click={() => store.actions.preview.setDevice("desktop")}
/>
<ActionButton
icon="DeviceTablet"
selected={$store.previewDevice === "tablet"}
on:click={() => store.actions.preview.setDevice("tablet")}
/>
<ActionButton
icon="DevicePhone"
selected={$store.previewDevice === "mobile"}
on:click={() => store.actions.preview.setDevice("mobile")}
/>
</ActionGroup>

View File

@ -17,18 +17,14 @@ export default `
margin: 0;
}
html {
height: calc(100% - 16px);
width: calc(100% - 16px);
height: 100%;
width: 100%;
overflow: hidden;
margin: 8px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
html.loaded {
box-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.1);
}
body {
flex: 1 1 auto;
overflow: hidden;
@ -67,7 +63,8 @@ export default `
previewType,
appId,
theme,
customTheme
customTheme,
previewDevice
} = parsed
// Set some flags so the app knows we're in the builder
@ -80,6 +77,7 @@ export default `
window["##BUDIBASE_PREVIEW_TYPE##"] = previewType
window["##BUDIBASE_PREVIEW_THEME##"] = theme
window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"] = customTheme
window["##BUDIBASE_PREVIEW_DEVICE##"] = previewDevice
// Initialise app
try {

View File

@ -15,6 +15,7 @@
import { get } from "svelte/store"
import AppThemeSelect from "components/design/AppPreview/AppThemeSelect.svelte"
import ThemeEditor from "components/design/AppPreview/ThemeEditor.svelte"
import DevicePreviewSelect from "components/design/AppPreview/DevicePreviewSelect.svelte"
// Cache previous values so we don't update the URL more than necessary
let previousType
@ -151,6 +152,9 @@
{#if $currentAsset}
<div class="preview-header">
<ComponentSelectionList />
{#if $store.clientFeatures.devicePreview}
<DevicePreviewSelect />
{/if}
{#if $store.clientFeatures.customThemes}
<ThemeEditor />
{:else if $store.clientFeatures.spectrumThemes}
@ -208,7 +212,8 @@
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
align-items: flex-start;
gap: 1rem;
}
.preview-header > :global(*) {
flex: 0 0 auto;

View File

@ -4,7 +4,8 @@
"intelligentLoading": true,
"deviceAwareness": true,
"state": true,
"customThemes": true
"customThemes": true,
"devicePreview": true
},
"layout": {
"name": "Layout",

View File

@ -85,30 +85,45 @@
<UserBindingsProvider>
<DeviceBindingsProvider>
<StateBindingsProvider>
<CustomThemeWrapper>
<div id="app-root" class:preview={$builderStore.inBuilder}>
{#key $screenStore.activeLayout._id}
<Component instance={$screenStore.activeLayout.props} />
{/key}
</div>
</CustomThemeWrapper>
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
<!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
<!--
<!-- Clip boundary for selection indicators -->
<div
id="clip-root"
class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice === "tablet"}
class:mobile-preview={$builderStore.previewDevice === "mobile"}
>
<!-- Actual app -->
<div id="app-root">
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component instance={$screenStore.activeLayout.props} />
{/key}
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
</div>
<!-- Selection indicators should be bounded by device -->
<!--
We don't want to key these by componentID as they control their own
re-mounting to avoid flashes.
-->
{#if $builderStore.inBuilder}
<SelectionIndicator />
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<SelectionIndicator />
<HoverIndicator />
{/if}
</div>
</StateBindingsProvider>
</DeviceBindingsProvider>
</UserBindingsProvider>
@ -117,20 +132,33 @@
{/if}
<style>
#spectrum-root,
#app-root {
height: 100%;
width: 100%;
#spectrum-root {
padding: 0;
margin: 0;
overflow: hidden;
height: 100%;
width: 100%;
background: transparent;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
#clip-root {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background-color: transparent;
}
#app-root {
position: relative;
}
#app-root.preview {
border: 1px solid var(--spectrum-global-color-gray-300);
overflow: hidden;
height: 100%;
width: 100%;
}
.error {
position: absolute;
width: 100%;
@ -157,4 +185,22 @@
.error :global(h1) {
font-weight: 400;
}
/* Preview styles */
/* The additional 6px of size is to account for 4px padding and 2px border */
#clip-root.preview {
padding: 2px;
}
#clip-root.tablet-preview {
width: calc(1024px + 6px);
height: calc(768px + 6px);
}
#clip-root.mobile-preview {
width: calc(390px + 6px);
height: calc(844px + 6px);
}
.preview #app-root {
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
}
</style>

View File

@ -5,6 +5,7 @@
const { routeStore, styleable, linkable, builderStore } = getContext("sdk")
const component = getContext("component")
const context = getContext("context")
export let title
export let hideTitle = false
@ -58,9 +59,19 @@
}
</script>
<div class="layout layout--{typeClass}" use:styleable={$component.styles}>
<div
class="layout layout--{typeClass}"
use:styleable={$component.styles}
class:desktop={!$context.device.mobile && !$context.device.tablet}
class:mobile={!!$context.device.mobile}
>
{#if typeClass !== "none"}
<div class="nav-wrapper" class:sticky class:hidden={isPeeking}>
<div
class="nav-wrapper"
class:sticky
class:hidden={isPeeking}
style={`--height:${$context.device.height}px; --width:${$context.device.width}px;`}
>
<div class="nav nav--{typeClass} size--{widthClass}">
<div class="nav-header">
{#if validLinks?.length}
@ -286,101 +297,97 @@
}
/* Desktop nav overrides */
@media (min-width: 600px) {
.layout--left {
flex-direction: row;
overflow: hidden;
}
.layout--left .main-wrapper {
height: 100%;
overflow: auto;
}
.desktop.layout--left {
flex-direction: row;
overflow: hidden;
}
.desktop.layout--left .main-wrapper {
height: 100%;
overflow: auto;
}
.nav--left {
width: 250px;
padding: var(--spacing-xl);
}
.desktop .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);
}
.desktop .nav--left .links {
margin-top: var(--spacing-m);
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.desktop .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);
}
.mobile .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;
}
/* Show close button in drawer */
.mobile .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;
}
/* Force standard top bar */
.mobile .nav {
padding: var(--spacing-m) 16px;
}
.mobile .burger {
display: grid;
place-items: center;
}
.mobile .logo {
flex: 0 0 auto;
}
.mobile .logo :global(h1) {
display: none;
}
/* Reduce padding */
.main {
padding: 16px;
}
/* Reduce padding */
.mobile .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: var(--navBackground);
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 80px 20px rgba(0, 0, 0, 0.3);
}
.mobile-click-handler.visible {
position: fixed;
display: block;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 998;
}
/* Transform links into drawer */
.mobile .links {
margin-top: 0;
position: absolute;
top: 0;
left: -250px;
transform: translateX(0);
width: 250px;
transition: transform 0.26s ease-in-out, opacity 0.26s ease-in-out;
height: var(--height);
opacity: 0;
background: var(--navBackground);
z-index: 999;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
padding: var(--spacing-xl);
}
.mobile .link {
width: calc(100% - 30px);
font-size: 120%;
}
.mobile .links.visible {
opacity: 1;
transform: translateX(250px);
box-shadow: 0 0 80px 20px rgba(0, 0, 0, 0.3);
}
.mobile .mobile-click-handler.visible {
position: absolute;
display: block;
top: 0;
left: 0;
width: var(--width);
height: var(--height);
z-index: 998;
}
</style>

View File

@ -3,21 +3,25 @@
import { onMount } from "svelte"
let width = window.innerWidth
let height = window.innerHeight
const tabletBreakpoint = 768
const desktopBreakpoint = 1280
const resizeObserver = new ResizeObserver(entries => {
if (entries?.[0]) {
width = entries[0].contentRect?.width
height = entries[0].contentRect?.height
}
})
$: data = {
mobile: width && width < tabletBreakpoint,
tablet: width && width >= tabletBreakpoint && width < desktopBreakpoint,
width,
height,
}
onMount(() => {
const doc = document.documentElement
const doc = document.getElementById("app-root")
resizeObserver.observe(doc)
return () => {

View File

@ -18,10 +18,11 @@
}}
out:fade={{ duration: transition ? 130 : 0 }}
class="indicator"
class:flipped={top < 20}
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
>
{#if text}
<div class="text" class:flipped={top < 22}>
<div class="text" class:flipped={top < 20}>
{text}
</div>
{/if}
@ -29,11 +30,17 @@
<style>
.indicator {
position: fixed;
position: absolute;
z-index: var(--zIndex);
border: 2px solid var(--color);
pointer-events: none;
border-radius: 4px;
border-top-right-radius: 4px;
border-top-left-radius: 0;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.indicator.flipped {
border-top-left-radius: 4px;
}
.text {
background-color: var(--color);
@ -45,16 +52,17 @@
padding: 0 8px 2px 8px;
transform: translateY(-100%);
font-size: 11px;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
white-space: nowrap;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.flipped {
.text.flipped {
border-top-left-radius: 4px;
transform: translateY(0%);
top: -2px;
}

View File

@ -70,17 +70,22 @@
updating = false
}
const device = document.getElementById("app-root")
const deviceBounds = device.getBoundingClientRect()
children.forEach((child, idx) => {
const callback = createIntersectionCallback(idx)
const threshold = children.length > 1 ? 1 : 0
const observer = new IntersectionObserver(callback, { threshold })
const observer = new IntersectionObserver(callback, {
threshold,
root: device,
})
observer.observe(child)
observers.push(observer)
const elBounds = child.getBoundingClientRect()
nextIndicators.push({
top: elBounds.top + scrollY - 2,
left: elBounds.left + scrollX - 2,
top: elBounds.top + scrollY - deviceBounds.top,
left: elBounds.left + scrollX - deviceBounds.left,
width: elBounds.width + 4,
height: elBounds.height + 4,
visible: false,

View File

@ -26,8 +26,16 @@
const id = $builderStore.selectedComponentId
const parent = document.getElementsByClassName(id)?.[0]
const element = parent?.childNodes?.[0]
// The settings bar is higher in the dom tree than the selection indicators
// as we want to be able to render the settings bar wider than the screen,
// or outside the screen.
// Therefore we use the clip root rather than the app root to determine
// its position.
const device = document.getElementById("clip-root")
if (element && self) {
// Batch reads to minimize reflow
const deviceBounds = device.getBoundingClientRect()
const elBounds = element.getBoundingClientRect()
const width = self.offsetWidth
const height = self.offsetHeight
@ -35,9 +43,16 @@
// Vertically, always render above unless no room, then render inside
let newTop = elBounds.top + scrollY - verticalOffset - height
if (newTop < deviceBounds.top - 50) {
newTop = deviceBounds.top - 50
}
if (newTop < 0) {
newTop = 0
}
const deviceBottom = deviceBounds.top + deviceBounds.height
if (newTop > deviceBottom - 44) {
newTop = deviceBottom - 44
}
// Horizontally, try to center first.
// Failing that, render to left edge of component.

View File

@ -18,6 +18,7 @@ const loadBudibase = () => {
previewType: window["##BUDIBASE_PREVIEW_TYPE##"],
theme: window["##BUDIBASE_PREVIEW_THEME##"],
customTheme: window["##BUDIBASE_PREVIEW_CUSTOM_THEME##"],
previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"],
})
// Set app ID - this window flag is set by both the preview and the real

View File

@ -20,6 +20,9 @@ const createBuilderStore = () => {
previewId: null,
previewType: null,
selectedPath: [],
theme: null,
customTheme: null,
previewDevice: "desktop",
}
const writableStore = writable(initialState)
const derivedStore = derived(writableStore, $state => {

View File

@ -7,7 +7,10 @@
<svelte:head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<title>{title}</title>
<link rel="icon" type="image/png" href={favicon} />