Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-157776 Prompt Card Block #805

Merged
merged 8 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions acrobat/blocks/prompt-card/prompt-card.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
:root {
--common-font: "Adobe Clean", adobe-clean, "Trebuchet MS", sans-serif;
}

#prompt {
display: none
}

.prompt-card.hidden {
display: none;
}

.view-all {
grid-column: 1 / -1;
display: flex;
justify-content: center;
}

.prompt-toast {
font-family: var(--common-font);
align-items: flex-start;
background: #05834e;
border-radius: 10px;
color: #fff;
display: none;
gap: 10px;
left: 50%;
padding: 20px 16px;
position: fixed;
top: 110px;
transform: translate(-50%, -50%);
z-index: 9999
}

.prompt-toast--show {
display: inline-flex
}

.prompt-toast:before {

Check failure on line 39 in acrobat/blocks/prompt-card/prompt-card.css

View workflow job for this annotation

GitHub Actions / runner / stylelint (18.x)

[stylelint] reported by reviewdog 🐶 Expected double colon pseudo-element notation Raw Output: message:"Expected double colon pseudo-element notation" location:{path:"/home/runner/work/dc/dc/acrobat/blocks/prompt-card/prompt-card.css" range:{start:{line:39 column:14}}} severity:ERROR source:{name:"stylelint" url:"https://stylelint.io/"} code:{value:"selector-pseudo-element-colon-notation"}
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTEwIDE4Ljc1Yy00LjgyNSAwLTguNzUtMy45MjUtOC43NS04Ljc1UzUuMTc1IDEuMjUgMTAgMS4yNXM4Ljc1IDMuOTI1IDguNzUgOC43NS0zLjkyNSA4Ljc1LTguNzUgOC43NVptMC0xNkE3LjI1OCA3LjI1OCAwIDAgMCAyLjc1IDEwYzAgMy45OTggMy4yNTIgNy4yNSA3LjI1IDcuMjVzNy4yNS0zLjI1MiA3LjI1LTcuMjVTMTMuOTk4IDIuNzUgMTAgMi43NVoiLz48cGF0aCBkPSJNOS4xODMgMTMuOTQzYS43NTEuNzUxIDAgMCAxLS41NTgtLjI0OGwtMi42NDMtMi45NGEuNzUuNzUgMCAwIDEgMS4xMTUtMS4wMDNsMi4wMjYgMi4yNTQgMy43LTUuMDQ2YS43NS43NSAwIDEgMSAxLjIwOS44ODdsLTQuMjQ1IDUuNzlhLjc0NS43NDUgMCAwIDEtLjU2Ny4zMDVsLS4wMzcuMDAxWiIvPjwvZz48L3N2Zz4=");
background-repeat: no-repeat;
background-size: contain;
content: "";
height: 20px;
margin-top: 3px;
min-width: 20px;
width: 20px
}

.prompt-close {
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgY2xpcC1wYXRoPSJ1cmwoI2EpIj48cGF0aCBkPSJtNy4zNDIgNiAzLjM5Ni0zLjM5NmEuOTUuOTUgMCAwIDAtMS4zNDMtMS4zNDJMNiA0LjY1OCAyLjYwNCAxLjI2MmEuOTUuOTUgMCAxIDAtMS4zNDMgMS4zNDJMNC42NTcgNiAxLjI2MSA5LjM5NmEuOTUuOTUgMCAxIDAgMS4zNDMgMS4zNDJMNiA3LjM0MmwzLjM5NSAzLjM5NmEuOTQ2Ljk0NiAwIDAgMCAxLjM0My4wMDEuOTUuOTUgMCAwIDAgMC0xLjM0M0w3LjM0MiA2WiIgZmlsbD0iI2ZmZiIvPjwvZz48ZGVmcz48Y2xpcFBhdGggaWQ9ImEiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0wIDBoMTJ2MTJIMHoiLz48L2NsaXBQYXRoPjwvZGVmcz48L3N2Zz4=");
background-repeat: no-repeat;
background-size: contain;
content: "";
height: 12px;
margin-inline-start: 10px;
margin-top: 7px;
width: 12px
}

.prompt-blade {
align-items: flex-start;
align-self: stretch;
background-color: #fff;
border: 1px solid #e8e8e8;
border-radius: 10px;
cursor: pointer;
display: flow;
flex: 1 0 0;
flex-direction: column;
justify-content: space-between;
padding: 20px 20px 24px;
transition-delay: 3s;
transition-property: border;
max-width: 276px;
}

.prompt-blade:hover {
box-shadow: 3px 6px 6px 0 rgba(0, 0, 0, .16)

Check failure on line 79 in acrobat/blocks/prompt-card/prompt-card.css

View workflow job for this annotation

GitHub Actions / runner / stylelint (18.x)

[stylelint] reported by reviewdog 🐶 Expected ".16" to be "16%" Raw Output: message:"Expected \".16\" to be \"16%\"" location:{path:"/home/runner/work/dc/dc/acrobat/blocks/prompt-card/prompt-card.css" range:{start:{line:79 column:43}}} severity:ERROR source:{name:"stylelint" url:"https://stylelint.io/"} code:{value:"alpha-value-notation"}

Check failure on line 79 in acrobat/blocks/prompt-card/prompt-card.css

View workflow job for this annotation

GitHub Actions / runner / stylelint (18.x)

[stylelint] reported by reviewdog 🐶 Expected modern color-function notation Raw Output: message:"Expected modern color-function notation" location:{path:"/home/runner/work/dc/dc/acrobat/blocks/prompt-card/prompt-card.css" range:{start:{line:79 column:29}}} severity:ERROR source:{name:"stylelint" url:"https://stylelint.io/"} code:{value:"color-function-notation"}
}

.prompt-blade:active {
border: 1px solid #095aba;
transition-delay: 0s
}

.prompt-icon {
margin-inline-end: 5px;
position: relative;
}

.prompt-prefix {
color: #6d6d6d;
font-size: 12px;
padding: 0 0 16px;
text-transform: uppercase;
display: flex;
align-items: center;
}

.prompt-prefix,
.prompt-title {
font-family: "Adobe Clean", adobe-clean, "Trebuchet MS", sans-serif;
TsayAdobe marked this conversation as resolved.
Show resolved Hide resolved
font-style: normal;
font-weight: 700;
line-height: 1.25
}

.prompt-title {
align-self: stretch;
color: #2c2c2c;
font-size: 18px
}

.prompt-copy {
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
background: #f8f8f8;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
color: #696969;
display: -webkit-box;

Check failure on line 122 in acrobat/blocks/prompt-card/prompt-card.css

View workflow job for this annotation

GitHub Actions / runner / stylelint (18.x)

[stylelint] reported by reviewdog 🐶 Unexpected vendor-prefix "-webkit-box" Raw Output: message:"Unexpected vendor-prefix \"-webkit-box\"" location:{path:"/home/runner/work/dc/dc/acrobat/blocks/prompt-card/prompt-card.css" range:{start:{line:122 column:12}}} severity:ERROR source:{name:"stylelint" url:"https://stylelint.io/"} code:{value:"value-no-vendor-prefix"}
flex: 1 0 0;
font-family: "Adobe Clean", adobe-clean, "Trebuchet MS", sans-serif;
font-size: 20px;
font-style: normal;
font-weight: 400;
height: 155px;
line-height: 150%;
margin-top: 16px;
overflow: hidden;
padding: 8px 16px 0
}

.prompt-copy-btn-wrapper {
align-items: center;
align-self: stretch;
background: #f8f8f8;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
display: flex;
gap: 8px;
justify-content: flex-end;
padding-bottom: 16px;
padding-top: 5px
}

.prompt-copy-btn {
color: #686868;
cursor: pointer;
font-family: var(--common-font);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 150%;
padding: 2px 8px
}

.prompt-copy-btn:after {

Check failure on line 159 in acrobat/blocks/prompt-card/prompt-card.css

View workflow job for this annotation

GitHub Actions / runner / stylelint (18.x)

[stylelint] reported by reviewdog 🐶 Expected double colon pseudo-element notation Raw Output: message:"Expected double colon pseudo-element notation" location:{path:"/home/runner/work/dc/dc/acrobat/blocks/prompt-card/prompt-card.css" range:{start:{line:159 column:17}}} severity:ERROR source:{name:"stylelint" url:"https://stylelint.io/"} code:{value:"selector-pseudo-element-colon-notation"}
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTkiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0iIzI0MjQyNCI+PHBhdGggZD0iTTE2Ljc1IDExLjVoLS41YS4yNS4yNSAwIDAgMC0uMjUuMjV2LjVjMCAuMTM4LjExMi4yNS4yNS4yNWguNWEuMjUuMjUgMCAwIDAgLjI1LS4yNXYtLjVhLjI1LjI1IDAgMCAwLS4yNS0uMjVaTTE2Ljc1IDkuNWgtLjVhLjI1LjI1IDAgMCAwLS4yNS4yNXYuNWMwIC4xMzguMTEyLjI1LjI1LjI1aC41YS4yNS4yNSAwIDAgMCAuMjUtLjI1di0uNWEuMjUuMjUgMCAwIDAtLjI1LS4yNVpNMTYuNzUgNy41aC0uNWEuMjUuMjUgMCAwIDAtLjI1LjI1di41YzAgLjEzOC4xMTIuMjUuMjUuMjVoLjVhLjI1LjI1IDAgMCAwIC4yNS0uMjV2LS41YS4yNS4yNSAwIDAgMC0uMjUtLjI1Wk0xNi43NSA1LjVoLS41YS4yNS4yNSAwIDAgMC0uMjUuMjV2LjVjMCAuMTM4LjExMi4yNS4yNS4yNWguNWEuMjUuMjUgMCAwIDAgLjI1LS4yNXYtLjVhLjI1LjI1IDAgMCAwLS4yNS0uMjVaTTE2Ljc1IDMuNWgtLjVhLjI1LjI1IDAgMCAwLS4yNS4yNXYuNWMwIC4xMzguMTEyLjI1LjI1LjI1aC41YS4yNS4yNSAwIDAgMCAuMjUtLjI1di0uNWEuMjUuMjUgMCAwIDAtLjI1LS4yNVpNMTYuNzUgMS41aC0uNWEuMjUuMjUgMCAwIDAtLjI1LjI1di41YzAgLjEzOC4xMTIuMjUuMjUuMjVoLjVhLjI1LjI1IDAgMCAwIC4yNS0uMjV2LS41YS4yNS4yNSAwIDAgMC0uMjUtLjI1Wk0xNC43NSAxLjVoLS41YS4yNS4yNSAwIDAgMC0uMjUuMjV2LjVjMCAuMTM4LjExMi4yNS4yNS4yNWguNWEuMjUuMjUgMCAwIDAgLjI1LS4yNXYtLjVhLjI1LjI1IDAgMCAwLS4yNS0uMjVaTTEyLjc1IDEuNWgtLjVhLjI1LjI1IDAgMCAwLS4yNS4yNXYuNWMwIC4xMzguMTEyLjI1LjI1LjI1aC41YS4yNS4yNSAwIDAgMCAuMjUtLjI1di0uNWEuMjUuMjUgMCAwIDAtLjI1LS4yNVpNMTAuNzUgMS41aC0uNWEuMjUuMjUgMCAwIDAtLjI1LjI1di41YzAgLjEzOC4xMTIuMjUuMjUuMjVoLjVhLjI1LjI1IDAgMCAwIC4yNS0uMjV2LS41YS4yNS4yNSAwIDAgMC0uMjUtLjI1Wk04Ljc1IDEuNWgtLjVhLjI1LjI1IDAgMCAwLS4yNS4yNXYuNWMwIC4xMzguMTEyLjI1LjI1LjI1aC41QS4yNS4yNSAwIDAgMCA5IDIuMjV2LS41YS4yNS4yNSAwIDAgMC0uMjUtLjI1Wk02Ljc1IDEuNWgtLjVhLjI1LjI1IDAgMCAwLS4yNS4yNXYuNWMwIC4xMzguMTEyLjI1LjI1LjI1aC41QS4yNS4yNSAwIDAgMCA3IDIuMjV2LS41YS4yNS4yNSAwIDAgMC0uMjUtLjI1Wk02Ljc1IDMuNWgtLjVhLjI1LjI1IDAgMCAwLS4yNS4yNXYuNWMwIC4xMzguMTEyLjI1LjI1LjI1aC41QS4yNS4yNSAwIDAgMCA3IDQuMjV2LS41YS4yNS4yNSAwIDAgMC0uMjUtLjI1Wk02Ljc1IDUuNWgtLjVhLjI1LjI1IDAgMCAwLS4yNS4yNXYuNWMwIC4xMzguMTEyLjI1LjI1LjI1aC41QS4yNS4yNSAwIDAgMCA3IDYuMjV2LS41YS4yNS4yNSAwIDAgMC0uMjUtLjI1Wk02Ljc1IDcuNWgtLjVhLjI1LjI1IDAgMCAwLS4yNS4yNXYuNWMwIC4xMzguMTEyLjI1LjI1LjI1aC41QS4yNS4yNSAwIDAgMCA3IDguMjV2LS41YS4yNS4yNSAwIDAgMC0uMjUtLjI1Wk02Ljc1IDkuNWgtLjVhLjI1LjI1IDAgMCAwLS4yNS4yNXYuNWMwIC4xMzguMTEyLjI1LjI1LjI1aC41YS4yNS4yNSAwIDAgMCAuMjUtLjI1di0uNWEuMjUuMjUgMCAwIDAtLjI1LS4yNVpNNi43NSAxMS41aC0uNWEuMjUuMjUgMCAwIDAtLjI1LjI1di41YzAgLjEzOC4xMTIuMjUuMjUuMjVoLjVhLjI1LjI1IDAgMCAwIC4yNS0uMjV2LS41YS4yNS4yNSAwIDAgMC0uMjUtLjI1Wk04Ljc1IDExLjVoLS41YS4yNS4yNSAwIDAgMC0uMjUuMjV2LjVjMCAuMTM4LjExMi4yNS4yNS4yNWguNWEuMjUuMjUgMCAwIDAgLjI1LS4yNXYtLjVhLjI1LjI1IDAgMCAwLS4yNS0uMjVaTTEwLjc1IDExLjVoLS41YS4yNS4yNSAwIDAgMC0uMjUuMjV2LjVjMCAuMTM4LjExMi4yNS4yNS4yNWguNWEuMjUuMjUgMCAwIDAgLjI1LS4yNXYtLjVhLjI1LjI1IDAgMCAwLS4yNS0uMjVaTTEyLjc1IDExLjVoLS41YS4yNS4yNSAwIDAgMC0uMjUuMjV2LjVjMCAuMTM4LjExMi4yNS4yNS4yNWguNWEuMjUuMjUgMCAwIDAgLjI1LS4yNXYtLjVhLjI1LjI1IDAgMCAwLS4yNS0uMjVaTTE0Ljc1IDExLjVoLS41YS4yNS4yNSAwIDAgMC0uMjUuMjV2LjVjMCAuMTM4LjExMi4yNS4yNS4yNWguNWEuMjUuMjUgMCAwIDAgLjI1LS4yNXYtLjVhLjI1LjI1IDAgMCAwLS4yNS0uMjVaIi8+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik01IDYuNUgxLjVBLjUuNSAwIDAgMCAxIDd2MTBhLjUuNSAwIDAgMCAuNS41aDEwYS41LjUgMCAwIDAgLjUtLjV2LTMuNUg1LjVBLjUuNSAwIDAgMSA1IDEzVjYuNVoiLz48L2c+PC9zdmc+");
background-repeat: no-repeat;
background-size: contain;
content: "";
display: inline-block;
height: 18px;
min-width: 18px;
position: relative;
top: 5px;
TsayAdobe marked this conversation as resolved.
Show resolved Hide resolved
width: 18px;
margin-inline-start: 8px
}
173 changes: 173 additions & 0 deletions acrobat/blocks/prompt-card/prompt-card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/* eslint-disable compat/compat */
import { setLibs } from '../../scripts/utils.js';

const miloLibs = setLibs('/libs');
const { createTag } = await import(`${miloLibs}/utils/utils.js`);

const classToastShow = 'prompt-toast--show';
const getPlaceHolder = (x) => (window.mph?.[x] || x);

function copyPrompt(cfg) {
navigator.clipboard.writeText(cfg.prompt);

let toast = document.querySelector('.prompt-toast');
if (!toast) {
toast = createTag('div', { class: 'prompt-toast' }, cfg.toast);
const toastClose = createTag('i', { class: 'prompt-close' });
toast.appendChild(toastClose);
document.body.appendChild(toast);

toastClose.addEventListener('click', () => {
toast.classList.remove(classToastShow);
});
}
toast.childNodes[0].textContent = cfg.toast;
toast.classList.add(classToastShow);

setTimeout(() => toast.classList.remove(classToastShow), 5000);
}

async function createBlock(element, cfg) {
cfg.icon = cfg.icon || '/acrobat/img/icons/aichat.svg';
cfg.button = cfg.button || getPlaceHolder('Copy');
cfg.toast = cfg.toast || getPlaceHolder('Copied to clipboard');
const blade = createTag('div', {
class: 'prompt-blade',
title: cfg.prompt,
'data-toast': cfg.toast,
'daa-im': true,
'daa-lh': 'Featured prompts | Executive summary',
});
const prefix = createTag('div', { class: 'prompt-prefix' });
const icon = createTag('img', {
class: 'prompt-icon',
alt: 'AI Assistant Icon',
src: cfg.icon,
width: 18,
height: 18,
});
const title = createTag('div', { class: 'prompt-title' }, cfg.title);
const copy = createTag('div', { class: 'prompt-copy' }, cfg.prompt);
const prompt = createTag('input', { id: 'prompt', value: cfg.prompt });
const wrapper = createTag('div', { class: 'prompt-copy-btn-wrapper' });
const copyBtn = createTag('span', { class: 'prompt-copy-btn', role: 'button', tabindex: 0, 'aria-label': 'Copy button' }, cfg.button);
wrapper.append(copyBtn);
prefix.appendChild(icon);
prefix.appendChild(createTag('span', null, cfg.prefix));
blade.append(prefix, title, copy, prompt, wrapper);
element.replaceChildren(blade);

blade.addEventListener('click', () => {
copyPrompt(cfg);
});

copyBtn.addEventListener('keypress', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
copyPrompt(cfg);
}
});
}

async function createBlocks(element, blockArray, templateCfg) {
const { parentNode } = element;
for (const [i, cfg] of blockArray.entries()) {
const blockEl = createTag('div', { class: 'prompt-card' });
if (templateCfg.rows && i > 0) blockEl.classList.add('hidden');
await createBlock(blockEl, { ...templateCfg, ...cfg });
parentNode.insertBefore(blockEl, element.previousSibling);
}
element.remove();

if (templateCfg.rows && parentNode.classList.contains('section')) {
const resizeObserver = new ResizeObserver(() => {
const computedStyle = window.getComputedStyle(parentNode);
if (/^(\d+(\.\d+)?(px|fr|em|rem|%))( (\d+(\.\d+)?(px|fr|em|rem|%)))*$/.test(computedStyle.gridTemplateColumns)) {
const visibleCnt = computedStyle.gridTemplateColumns.split(' ').length * templateCfg.rows;
const promptcards = [...parentNode.querySelectorAll('.prompt-card')];
if (promptcards.length <= visibleCnt) {
parentNode.querySelector('.view-all')?.remove();
resizeObserver.disconnect();
}
promptcards.forEach(
(x, i) => (i < visibleCnt ? x.classList.remove('hidden') : x.classList.add('hidden')),
);
}
});
resizeObserver.observe(parentNode);

const viewMore = createTag('div', { class: 'view-all' });
const moreBtn = createTag('div', { class: 'con-button outline' }, getPlaceHolder('View all'));
moreBtn.addEventListener('click', (e) => {
resizeObserver.disconnect();
[...parentNode.querySelectorAll('.prompt-card')].forEach((x) => x.classList.remove('hidden'));
e.target.parentNode.remove();
});
viewMore.appendChild(moreBtn);
parentNode.appendChild(viewMore);
}
}

async function processGroup(element, cfg, startIndex) {
let blockArray;
if (startIndex > -1) {
blockArray = [];
const keys = [...element.children[startIndex].children].map((x) => x.textContent.toLowerCase());
[...element.children].slice(startIndex + 1).forEach((x) => {
const values = [...x.children].map((y) => y.textContent);
const block = keys.reduce((obj, key, index) => ({ ...obj, [key]: values[index] }), {});
blockArray.push(block);
});
} else {
const resp = await fetch(cfg.json);
if (!resp.ok) {
element.remove();
return;
}
const json = await resp.json();
const keys = Object.keys(cfg).filter((k) => !['json', 'rows'].includes(k));
blockArray = json.data.filter(
(x) => keys.reduce((a, k) => a && cfg[k] === x[k], true),
);
}
await createBlocks(element, blockArray, cfg);
}

function readKeyValueSet(element) {
const cfg = {};
for (const x of [...element.children]) {
if (x.children.length < 2) break;
cfg[x.children[0].textContent.toLowerCase()] = x.children[1].textContent;
}
return cfg;
}

export default async function init(element) {
if (element.classList.contains('template') && element.classList.contains('group')) {
const cfg = readKeyValueSet(element);
await processGroup(element, cfg, Object.keys(cfg).length + 1);
return;
}

if (element.classList.contains('group')) {
await processGroup(element, window.promptCardTemplate, 0);
return;
}

let cfg = readKeyValueSet(element);

if (element.classList.contains('template')) {
window.promptCardTemplate = cfg;
element.remove();
return;
}

if (element.classList.contains('json')) {
await processGroup(element, cfg, -1);
return;
}

cfg = { ...window.promptCardTemplate, ...cfg };

await createBlock(element, cfg);
}
21 changes: 21 additions & 0 deletions acrobat/img/icons/aichat.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions test/blocks/prompt-card/mocks/body-block-icon.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<main>
<div class="prompt-card">
<div>
<div>Icon</div>
<div>https://main--dc--adobecom.hlx.live/dc-shared/assets/images/frictionless/verb-footer-images/word-to-pdf.svg</div>
</div>
<div>
<div>Prefix</div>
<div>Ask</div>
</div>
<div>
<div>Title</div>
<div>Sum it up</div>
</div>
<div>
<div>Prompt</div>
<div>Summarize this document in 3 sentences.</div>
</div>
<div>
<div>Button</div>
<div>Copy</div>
</div>
<div>
<div>Toast</div>
<div>Copied to clipboard</div>
</div>
</div>
</main>
Loading
Loading