Embed improvements (#839)

* Smoother resizing of the iframe

* Use bodyBackground with iframe-resizer
This commit is contained in:
Uku Taht 2021-03-15 15:56:12 +02:00 committed by GitHub
parent c29bcc89b5
commit fef3eba0a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 116 additions and 79 deletions

View File

@ -59,26 +59,30 @@ blockquote {
@tailwind utilities;
.main-graph {
height: 440px;
position: relative;
height: 0;
/* Formula is: (top row height + (graph height / graph width * 100%)) */
padding-top: calc(148px + (451 / 1088 * 100%));
}
@screen md {
.main-graph {
height: 480px;
}
padding-top: calc(128px + (451 / 1088 * 100%));
}
@screen lg {
.main-graph {
height: 440px;
padding-top: calc(451 / 1088 * 100%);
}
}
@screen xl {
.main-graph {
height: 460px;
}
.graph-inner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: auto;
}
.light-text {
@ -273,7 +277,7 @@ blockquote {
}
.fullwidth-shadow::before {
@apply absolute top-0 w-screen h-full;
@apply absolute top-0 w-screen h-full bg-gray-50 dark:bg-gray-850;
box-shadow: 0 4px 2px -2px rgba(0, 0, 0, 0.06);
content: "";
z-index: -1;

View File

@ -95,6 +95,7 @@ const embedButton = document.getElementById('generate-embed')
if (embedButton) {
embedButton.addEventListener('click', function(e) {
const baseUrl = document.getElementById('base-url').value
const embedCode = document.getElementById('embed-code')
const theme = document.getElementById('theme').value.toLowerCase()
const background = document.getElementById('background').value
@ -103,11 +104,14 @@ if (embedButton) {
const embedLink = new URL(document.getElementById('embed-link').value)
embedLink.searchParams.set('embed', 'true')
embedLink.searchParams.set('theme', theme)
let backgroundAttr = ''
if (background) {
embedLink.searchParams.set('background', background)
backgroundAttr = `background="${background}"`
}
embedCode.value = `<iframe id="plausible-embed" src="${embedLink.toString()}" width="100%" height="1600px" scrolling="no" frameborder="0" loading="lazy"></iframe>`
embedCode.value = `<iframe plausible-embed ${backgroundAttr} src="${embedLink.toString()}" scrolling="no" frameborder="0" loading="lazy" style="width: 1px; min-width: 100%; height: 1600px;"></iframe>
<div style="font-size: 14px; padding-bottom: 14px;">Stats powered by <a target="_blank" style="color: #4F46E5; text-decoration: underline;" href="https://plausible.io">Plausible Analytics</a></div>
<script async src="${baseUrl}/js/embed.host.js"></script>`
} catch (e) {
embedCode.value = 'ERROR: Please enter a valid URL in the shared link field'
}

View File

@ -24,11 +24,10 @@ class Historical extends React.Component {
}
render() {
const extraStyle = this.props.site.background ? {backgroundColor: this.props.site.background} : {}
return (
<div className="mb-12">
<div id="stats-container-top"></div>
<div className={`sticky top-0 bg-gray-50 dark:bg-gray-850 sm:py-3 py-1 z-9 ${this.props.stuck ? 'z-10 fullwidth-shadow' : ''}`} style={extraStyle}>
<div className={`sticky top-0 sm:py-3 py-1 z-9 ${this.props.stuck && !this.props.site.embedded ? 'z-10 fullwidth-shadow bg-gray-50 dark:bg-gray-850' : ''}`}>
<div className="items-center w-full sm:flex">
<div className="flex items-center w-full mb-2 sm:mb-0">
<SiteSwitcher site={this.props.site} loggedIn={this.props.loggedIn} />

View File

@ -14,7 +14,7 @@ if (container) {
offset: container.dataset.offset,
hasGoals: container.dataset.hasGoals === 'true',
insertedAt: container.dataset.insertedAt,
background: container.dataset.background
embedded: container.dataset.embedded
}
const loggedIn = container.dataset.loggedIn === 'true'

View File

@ -264,9 +264,9 @@ class LineGraph extends React.Component {
return (
<div className={`px-8 w-1/2 my-4 lg:w-auto ${border}`} key={stat.name}>
<div className="text-gray-500 dark:text-gray-400 text-xs font-bold tracking-wide uppercase">{stat.name}</div>
<div className="my-1 flex justify-between items-center">
<b className="text-2xl mr-4 dark:text-gray-100">{ this.renderTopStatNumber(stat) }</b>
<div className="text-xs font-bold tracking-wide text-gray-500 uppercase dark:text-gray-400">{stat.name}</div>
<div className="flex items-center justify-between my-1">
<b className="mr-4 text-2xl dark:text-gray-100">{ this.renderTopStatNumber(stat) }</b>
{this.renderComparison(stat.name, stat.change)}
</div>
</div>
@ -285,7 +285,7 @@ class LineGraph extends React.Component {
return (
<a href={endpoint} download>
<svg className="feather w-4 h-5 absolute text-gray-700 dark:text-gray-300" style={{right: '2rem', top: '-2rem'}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
<svg className="absolute w-4 h-5 text-gray-700 feather dark:text-gray-300" style={{right: '2rem', top: '-2rem'}} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
</a>
)
}
@ -294,15 +294,15 @@ class LineGraph extends React.Component {
const extraClass = this.props.graphData.interval === 'hour' ? '' : 'cursor-pointer'
return (
<React.Fragment>
<div className="graph-inner">
<div className="flex flex-wrap">
{ this.renderTopStats() }
</div>
<div className="px-2 relative">
<div className="relative px-2">
{ this.downloadLink() }
<canvas id="main-graph-canvas" className={'mt-4 ' + extraClass} width="1054" height="342"></canvas>
</div>
</React.Fragment>
</div>
)
}
}
@ -349,8 +349,8 @@ export default class VisitorGraph extends React.Component {
render() {
return (
<div className="w-full relative bg-white dark:bg-gray-825 shadow-xl rounded mt-6 main-graph">
{ this.state.loading && <div className="loading pt-24 sm:pt-32 md:pt-48 mx-auto"><div></div></div> }
<div className="relative w-full mt-6 bg-white rounded shadow-xl dark:bg-gray-825 main-graph">
{ this.state.loading && <div className="graph-inner"><div className="pt-24 mx-auto loading sm:pt-32 md:pt-48"><div></div></div></div> }
{ this.renderInner() }
</div>
)

View File

@ -0,0 +1 @@
import 'iframe-resizer/js/iframeResizer.contentWindow'

11
assets/js/embed.host.js Normal file
View File

@ -0,0 +1,11 @@
import iframeResize from 'iframe-resizer/js/iframeResizer'
const iframe = document.querySelector('[plausible-embed]')
const options = {
heightCalculationMethod: 'taggedElement'
}
if (iframe.getAttribute('background')) {
options.bodyBackground = iframe.getAttribute('background')
}
iframeResize(options, '[plausible-embed]')

View File

@ -20,6 +20,7 @@
"copy-webpack-plugin": "^6.0.3",
"css-loader": "^3.6.0",
"datamaps": "^0.5.9",
"iframe-resizer": "^4.3.1",
"mini-css-extract-plugin": "^0.8.2",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"phoenix": "file:../deps/phoenix",
@ -53,11 +54,11 @@
}
},
"../deps/phoenix": {
"version": "1.4.17",
"version": "1.5.7",
"license": "MIT"
},
"../deps/phoenix_html": {
"version": "2.14.2"
"version": "2.14.3"
},
"node_modules/@babel/code-frame": {
"version": "7.10.4",
@ -2370,9 +2371,9 @@
"integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w=="
},
"node_modules/bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"node_modules/boolbase": {
"version": "1.0.0",
@ -4475,19 +4476,24 @@
"integrity": "sha512-YqAL+NXOzjBnpY+dcOKDlZybJDCOzgsq4koW3fvyty/ldTmsb4QazZpOWmVvZ2m0t5jbBf7L0lIGU3BUipwG+A=="
},
"node_modules/elliptic": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"dependencies": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/elliptic/node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
@ -6541,6 +6547,18 @@
"resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE="
},
"node_modules/iframe-resizer": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.3.1.tgz",
"integrity": "sha512-PkoTPNF6EYhTbDjogdKu7JVgKqRwwNBXMeywZaQyzEYM3BNltA8O9fIIrtUkmj+8VZGckXpwtXsWsaQ5lrhd0w==",
"engines": {
"node": ">=0.8.0"
},
"funding": {
"type": "individual",
"url": "https://github.com/davidjbradshaw/iframe-resizer/blob/master/FUNDING.md"
}
},
"node_modules/ignore": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
@ -17158,9 +17176,9 @@
"integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w=="
},
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"boolbase": {
"version": "1.0.0",
@ -18895,17 +18913,24 @@
"integrity": "sha512-YqAL+NXOzjBnpY+dcOKDlZybJDCOzgsq4koW3fvyty/ldTmsb4QazZpOWmVvZ2m0t5jbBf7L0lIGU3BUipwG+A=="
},
"elliptic": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
},
"dependencies": {
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}
}
},
"emoji-regex": {
@ -20521,6 +20546,11 @@
"resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE="
},
"iframe-resizer": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.3.1.tgz",
"integrity": "sha512-PkoTPNF6EYhTbDjogdKu7JVgKqRwwNBXMeywZaQyzEYM3BNltA8O9fIIrtUkmj+8VZGckXpwtXsWsaQ5lrhd0w=="
},
"ignore": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",

View File

@ -20,6 +20,7 @@
"copy-webpack-plugin": "^6.0.3",
"css-loader": "^3.6.0",
"datamaps": "^0.5.9",
"iframe-resizer": "^4.3.1",
"mini-css-extract-plugin": "^0.8.2",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"phoenix": "file:../deps/phoenix",
@ -50,5 +51,6 @@
"stylelint": "^13.8.0",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^20.0.0"
}
},
"name": "assets"
}

View File

@ -4,7 +4,6 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const DefinePlugin = require('webpack').DefinePlugin;
module.exports = (env, options) => ({
optimization: {
@ -15,7 +14,9 @@ module.exports = (env, options) => ({
},
entry: {
'app': ['./js/app.js'],
'dashboard': ['./js/dashboard/mount.js']
'dashboard': ['./js/dashboard/mount.js'],
'embed.host': ['./js/embed.host.js'],
'embed.content': ['./js/embed.content.js']
},
output: {
filename: '[name].js',
@ -40,8 +41,5 @@ module.exports = (env, options) => ({
plugins: [
new MiniCssExtractPlugin({filename: '../css/[name].css'}),
new CopyWebpackPlugin({patterns: [{from: 'static/', to: '../' }]}),
new DefinePlugin({
"BASE_URL": JSON.stringify(process.env.BASE_URL || "http://localtest.me:8000")
})
]
});

View File

@ -136,10 +136,10 @@ defmodule PlausibleWeb.StatsController do
title: "Plausible · " <> shared_link.site.domain,
offer_email_report: false,
demo: false,
skip_plausible_tracking: true,
shared_link_auth: shared_link.slug,
embedded: conn.params["embed"] == "true",
theme: conn.params["theme"],
background: conn.params["background"]
theme: conn.params["theme"]
)
end

View File

@ -1,13 +0,0 @@
<div class="mt-4 bg-gray-800 dark:bg-gray-800">
<div class="container px-4 py-2 sm:px-6 lg:px-8">
<div class="xl:grid xl:grid-cols-3 xl:gap-8">
<div class="col-start-2 xl:my-0">
<a href="https://plausible.io" class="text-center">
<span class="text-xs text-gray-100">Powered by</span>
<img src="/images/icon/plausible_logo_sm.png" class="inline-block w-4 ml-1" />
<span class="text-xs font-semibold tracking-wider text-gray-300 leading-5">Plausible Analytics</span>
</a>
</div>
</div>
</div>
</div>

View File

@ -12,18 +12,19 @@
<%= render("_tracking.html", assigns) %>
<script type="text/javascript" data-pref="<%= @conn.assigns[:theme] || (@conn.assigns[:current_user] && @conn.assigns[:current_user].theme) %>" src="<%= Routes.static_path(@conn, "/js/applyTheme.js") %>"></script>
</head>
<body class="flex flex-col h-full bg-gray-50 dark:bg-gray-850" style="<%= if @conn.assigns[:background], do: "background-color: " <> @conn.assigns[:background] %>">
<body class="flex flex-col bg-gray-50 dark:bg-gray-850" style="<%= if !@conn.assigns[:embedded], do: "height: 100%" %>">
<%= if !@conn.assigns[:embedded] do %>
<%= render("_header.html", assigns) %>
<%= render("_notice.html", assigns) %>
<% end %>
<main class="flex-1">
<%= Map.get(assigns, :inner_layout) || @inner_content %>
<%= Map.get(assigns, :inner_layout) || @inner_content %>
</main>
<%= if @conn.assigns[:embedded] do %>
<%= render("_embedded_footer.html", assigns) %>
<div data-iframe-height></div>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/embed.content.js") %>"></script>
<% else %>
<%= render("_footer.html", assigns) %>
<% end %>

View File

@ -54,7 +54,7 @@
<header class="relative">
<h2 class="text-lg font-medium text-gray-900 leading-6 dark:text-gray-100">Embed dashboard</h2>
<p class="mt-1 text-sm text-gray-500 leading-5 dark:text-gray-200">You can use shared links to embed your stats in any other webpage using an <code>iframe</code>. Copy & paste a shared link into the form below to generate the embed code.</p>
<%= link(to: "https://docs.plausible.io/visibility", target: "_blank") do %>
<%= link(to: "https://plausible.io/docs/embed-dashboard", target: "_blank") do %>
<svg class="absolute top-0 right-0 w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
<% end %>
</header>
@ -81,9 +81,11 @@
<div class="mt-1">
<input type="text" name="background" id="background" class="block w-full border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md" placeholder="#F9FAFB">
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-200">Hint: try using `transparent` background to blend the dashboard with your site background</p>
</div>
</div>
<input type="hidden" id="base-url" value="<%= plausible_url() %>" />
<button id="generate-embed" class="my-4 button">Generate embed code 👇</button>
<div class="mt-2">
@ -95,8 +97,6 @@
<a onclick="var textarea = document.getElementById('embed-code'); textarea.focus(); textarea.select(); document.execCommand('copy');" href="javascript:void(0)" class="text-sm text-indigo-500 no-underline hover:underline">
<svg class="absolute text-indigo-800" style="top: 12px; right: 12px;" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</a>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-200">If you're using custom goals, you need to increase the height of the window to match the height of your dashboard.</p>
</div>
</div>
</div>

View File

@ -1,11 +1,11 @@
<div class="container" data-site-domain="<%= @site.domain %>">
<div class="<%= if !@conn.assigns[:embedded], do: "container" %>" data-site-domain="<%= @site.domain %>">
<%= if @offer_email_report do %>
<div class="w-full px-4 text-sm font-bold text-center text-blue-900 bg-blue-200 rounded transition" style="top: 91px" role="alert">
<%= link("Click here to enable weekly email reports →", to: "/#{URI.encode_www_form(@site.domain)}/settings/email-reports", class: "py-2 block") %>
</div>
<% end %>
<div class="pt-6"></div>
<div id="stats-react-container" data-domain="<%= @site.domain %>" data-offset="<%= Timex.Timezone.total_offset(Timex.Timezone.get(@site.timezone)) %>" data-has-goals="<%= @has_goals %>" data-logged-in="<%= !!@conn.assigns[:current_user] %>" data-inserted-at="<%= @site.inserted_at %>" data-shared-link-auth="<%= assigns[:shared_link_auth] %>" data-background="<%= @conn.assigns[:background] %>"></div>
<div id="stats-react-container" data-domain="<%= @site.domain %>" data-offset="<%= Timex.Timezone.total_offset(Timex.Timezone.get(@site.timezone)) %>" data-has-goals="<%= @has_goals %>" data-logged-in="<%= !!@conn.assigns[:current_user] %>" data-inserted-at="<%= @site.inserted_at %>" data-shared-link-auth="<%= assigns[:shared_link_auth] %>" data-embedded="<%= @conn.assigns[:embedded] %>"></div>
<div id="modal_root"></div>
<%= if !@conn.assigns[:current_user] && @conn.assigns[:demo] do %>
<div class="bg-gray-50 dark:bg-gray-850">