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

Blueprints sidebar section for single-click Playground presets #1759

Draft
wants to merge 111 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
cddec5e
Bring in a router and ensure site-slug is always present in the URL
adamziel Sep 5, 2024
dad4104
Move Playground boot to an isolated component that responds to URL pa…
adamziel Sep 5, 2024
1549065
Temporarily rename "browser" storage to "opfs". TODO: Rename it back
adamziel Sep 5, 2024
60a7465
Fix sidebar navigation
adamziel Sep 5, 2024
d25b5e6
Store Playground client in the redux store
adamziel Sep 5, 2024
6bb40a4
Get initial Playground boot to work
adamziel Sep 5, 2024
9158c0b
Use the correct OPFS mount handle when booting
adamziel Sep 5, 2024
51d5abf
Remove siteViewRef
adamziel Sep 5, 2024
7d4726c
Display the initial selected site info in the site manager
adamziel Sep 5, 2024
f211db9
Rip out PlaygroundContext
adamziel Sep 5, 2024
9e5ad4a
Remove unnecessary useCallback usage
brandonpayton Sep 6, 2024
1dcb88e
Get rid of useNavigatorParams
brandonpayton Sep 6, 2024
6c21093
Stop using the Navigator component
brandonpayton Sep 6, 2024
126bcdf
Use usePlaygroundClient where reasonable
brandonpayton Sep 6, 2024
c836577
Fix setting initial client info
brandonpayton Sep 6, 2024
4fa44dd
Add a TODO to fix logs being shared across sites
brandonpayton Sep 6, 2024
50e4bd6
Avoid re-rendering JustViewport when the site manager is expanded
adamziel Sep 6, 2024
b054876
store opfs mount descriptor with the current client info, not as a
adamziel Sep 6, 2024
b833a3e
Rename addSite to createSite and removeSite to deleteSite
adamziel Sep 6, 2024
1aec606
Adjust the TODO comment
adamziel Sep 6, 2024
c406b66
Only execute the Blueprint on the first site boot. Never execute a Bl…
adamziel Sep 6, 2024
51aac0b
Load the most recently created site when no slug is present (instead …
adamziel Sep 6, 2024
6251c89
Add another todo to JustViewport
adamziel Sep 6, 2024
8dcdfdc
Fix the Add Site button
adamziel Sep 6, 2024
1ad41dd
Remove the `?storage` query arg. Use ?site-manager as a feature flag.
adamziel Sep 6, 2024
588900f
Replace all query args when navigating between sites.
adamziel Sep 6, 2024
488f7a9
Reduce type proliferation: use the partial Blueprint type to store th…
adamziel Sep 6, 2024
4f00aa0
Add a TODO
adamziel Sep 6, 2024
1a69968
Make SiteMetadata a field on SiteInfo instead of using inheritance
adamziel Sep 6, 2024
746dc23
Implement the "Save this site" button
adamziel Sep 6, 2024
e554fad
Inline onAddSite
adamziel Sep 6, 2024
45c9007
Decouple saving site to OPFS
adamziel Sep 7, 2024
e656467
Explore a local-fs save button
adamziel Sep 7, 2024
6160e60
Stop gating site-manager
brandonpayton Sep 7, 2024
f61a7b6
Explain why we use a worker to write to OPFS
brandonpayton Sep 7, 2024
1b73682
Store Local FS site metadata in OPFS
adamziel Sep 7, 2024
7f0e84e
Display local directory name in site info panel
adamziel Sep 7, 2024
b42eb63
Add a rudimentary AddSiteForm for the new modal
adamziel Sep 7, 2024
1e10f85
Actually create new sites via the Add Site Form
adamziel Sep 7, 2024
1af4424
Navigating to temporary sites restores the original URL used to creat…
adamziel Sep 7, 2024
b4b70bb
Make the temporary site notice non dismissible
adamziel Sep 7, 2024
49d18d2
Remove the old site settings modal
adamziel Sep 7, 2024
e85b14a
Add "language" select when creating a site
adamziel Sep 7, 2024
5a46d08
Edit site settings form
adamziel Sep 7, 2024
6f86ea1
Avoid redux serialization warning for updateClientInfo action
brandonpayton Sep 8, 2024
10c0f40
Tweak local-fs site config-saving after conflicting changes
brandonpayton Sep 8, 2024
ce42039
Handle site update operation
adamziel Sep 9, 2024
99f8e6b
Add Multisite checkbox to the site creation modal
adamziel Sep 9, 2024
d6771f7
Move hamburger menu inside the SiteView
adamziel Sep 9, 2024
795bf8e
Restore the delete site icon
adamziel Sep 9, 2024
a4b0092
"Duplicate" -> "Duplicate Site"
adamziel Sep 9, 2024
202676e
Inline the SiteView component
adamziel Sep 9, 2024
c8c43b8
Remove the old Playground Configuration Group component
adamziel Sep 9, 2024
7c95df2
Extract the temporary site notice into a separate component, open the…
adamziel Sep 9, 2024
337355c
Fix property merging in createNewSiteInfo()
adamziel Sep 9, 2024
b68c984
Design adjustments
adamziel Sep 9, 2024
0a5d313
Improve design of the site settings modal, use correct language codes
adamziel Sep 9, 2024
19e0346
Responsive interactions
adamziel Sep 10, 2024
66ff550
Responsiveness
adamziel Sep 10, 2024
ff083af
Navigation buttons in mobile sidebar
adamziel Sep 10, 2024
00f2ff7
Another batch of responsive design changes
adamziel Sep 10, 2024
a0646f1
Responsive navigation buttons and animations
adamziel Sep 10, 2024
e0c9690
Design updates: Updated labels, margins, placements
adamziel Sep 10, 2024
3ecbb64
Fix react warnings
adamziel Sep 10, 2024
4d63af7
Use dropdown V2 for site saving options
adamziel Sep 10, 2024
2400ba3
Cleanup OPFS storage API
adamziel Sep 10, 2024
6961270
Remove OPFSHelper class
adamziel Sep 10, 2024
c14e586
Reorganize state management files
adamziel Sep 10, 2024
5fa8b31
Split redux store into separate slices
adamziel Sep 10, 2024
e7c1839
Extract bootSiteClient to a separate thunk
adamziel Sep 10, 2024
1088758
Persist site updates to OPFS at redux level
adamziel Sep 10, 2024
b96a09e
Update local-fs sites metadata on redux store update
adamziel Sep 10, 2024
caba900
Centralized router
adamziel Sep 10, 2024
d8f3f46
Communicate lack of local FS support in the UI
adamziel Sep 11, 2024
3ae1070
Communicate site boot error messages
adamziel Sep 11, 2024
2fe3a29
Fix TS Types
adamziel Sep 11, 2024
de3ba17
Cleanup all TODOs
adamziel Sep 11, 2024
b7f6a7b
Cleanup one last todo and handle more errors
adamziel Sep 11, 2024
96d5514
Update docs
adamziel Sep 11, 2024
f01ba81
Correctly populate the site edit form for temp sites
adamziel Sep 11, 2024
5e03e90
Axe devtools: Fix the reported problems
adamziel Sep 11, 2024
89e5cdc
Consolidate makeBlueprint and resolveBlueprint
adamziel Sep 13, 2024
09e1205
Preserve or remove the hash when navigating between sites
adamziel Sep 13, 2024
59b1685
Retain temporary sites state even if the user navigates away from them
adamziel Sep 13, 2024
9eb59fa
Handle edge cases: no site selected, no sites available
adamziel Sep 13, 2024
f301903
Simplify EnsurePlaygroundSiteIsSelected to fix boot bugs
adamziel Sep 13, 2024
cce879e
Disable options requiring a site client until the site is booted
adamziel Sep 13, 2024
4255bda
Change language from Site to Playground
adamziel Sep 14, 2024
9f0e676
Merge branch 'trunk' into routing
bgrgicak Sep 16, 2024
ce5ef1d
Fix URL blueprint literal parsing to decode Base64 string
brandonpayton Sep 17, 2024
da9384e
Use structuredClone() to deep clone blueprint
brandonpayton Sep 17, 2024
fe2ce73
Avoid top-level declarations that are only used to initialize other t…
brandonpayton Sep 17, 2024
519e50f
Fix single char typo
brandonpayton Sep 17, 2024
1c1ee48
Remove stale comment
brandonpayton Sep 17, 2024
a4f90f2
Note possibilities for edit/fork existing site
brandonpayton Sep 18, 2024
a8b62c8
Clarify a bit about local FS availability detection
brandonpayton Sep 18, 2024
9df2f79
Add locale list TODO
brandonpayton Sep 18, 2024
0d9af8a
Add sync-local-files-button TODO
brandonpayton Sep 18, 2024
180f6e0
Fix restore-from-zip ARIA label
brandonpayton Sep 18, 2024
20647bd
Remove unused generateUniqueSiteName function
brandonpayton Sep 18, 2024
43d17cc
Allow deletion of broken sites with loading errors
brandonpayton Sep 18, 2024
f45d1c4
Fix saving temporary sites in Safari
brandonpayton Sep 18, 2024
d8d7225
Allow a limited number of concurrent writes in Safari
brandonpayton Sep 18, 2024
6197165
Continue writing files as soon as possible in Safari
brandonpayton Sep 18, 2024
f9831a2
Redirect to the updated site instead of passing a stub
brandonpayton Sep 19, 2024
b16eb95
Restore bug-related TODO until we confirm it is resolved
brandonpayton Sep 19, 2024
387337a
Remove unused siteSlug prop from StartPlaygroundOptions
brandonpayton Sep 19, 2024
fce5346
Rebase, add offline fetch
adamziel Sep 20, 2024
757c2d5
Add a new Playground site to the sidebar immediately, but only start
adamziel Sep 20, 2024
23ea019
Use correct vite imports
adamziel Sep 20, 2024
8bbb06c
Remember the fetch remote Blueprint index when offline
adamziel Sep 20, 2024
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
759 changes: 529 additions & 230 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@types/ini": "4.1.0",
"@types/react-transition-group": "4.4.11",
"@types/wicg-file-system-access": "2023.10.5",
"@wordpress/dataviews": "4.4.0",
"ajv": "8.12.0",
"axios": "1.6.1",
"classnames": "^2.3.2",
Expand All @@ -72,11 +73,13 @@
"octokit-plugin-create-pull-request": "5.1.1",
"react": "^18.2.25",
"react-dom": "^18.2.25",
"react-hook-form": "7.53.0",
"react-modal": "^3.16.1",
"react-redux": "8.1.3",
"react-transition-group": "4.4.5",
"unzipper": "0.10.11",
"vite-plugin-api": "1.0.4",
"wouter": "3.3.5",
"xterm": "5.3.0",
"xterm-addon-fit": "0.8.0",
"yargs": "17.7.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ You can go ahead and try it out. The Playground will automatically install the t
| `lazy` | | Defer loading the Playground assets until someone clicks on the "Run" button. Does not accept any values. If `lazy` is added as a URL parameter, loading will be deferred. |
| `login` | `yes` | Log the user in as an admin. Accepts `yes` or `no`. |
| `multisite` | `no` | Enables the WordPress multisite mode. Accepts `yes` or `no`. |
| `storage` | `none` | Selects the storage for Playground: `none` gets erased on page refresh, `browser` is stored in the browser, and `device` is stored in the selected directory on a device. The last two protect the user from accidentally losing their work upon page refresh. |
| `import-site` | | Imports site files and database from a ZIP file specified by a URL. |
| `import-wxr` | | Imports site content from a WXR file specified by a URL. It uses the WordPress Importer plugin, so the default admin user must be logged in. |
| `site-slug` | | Selects which site to load from browser storage. This must be used in combination with `storage=browser`. |
| `site-slug` | | Selects which site to load from browser storage. |
| `language` | `en_US` | Sets the locale for the WordPress instance. This must be used in combination with `networking=yes` otherwise WordPress won't be able to download translations. |

For example, the following code embeds a Playground with a preinstalled Gutenberg plugin and opens the post editor:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Playground [disables network connections](../../blueprints/03-data-format.md#fea

### Temporary by design

As Playground [streams rather than serves](../../main/about/index.md#streamed-not-served) WordPress, all database changes and uploads will be gone when you refresh the page. To avoid losing your work, either [export your work](../../main/quick-start-guide.md#save-your-site) before or enable storage in the browser/device via the [Query API](/developers/apis/query-api#available-options) or the UI.
As Playground [streams rather than serves](../../main/about/index.md#streamed-not-served) WordPress, all database changes and uploads will be gone when you refresh the page. To avoid losing your work, either [export your work](../../main/quick-start-guide.md#save-your-site) before or enable storage in the browser/device via the "Save" button in the UI.

## When developing with Playground

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion packages/docs/site/docs/main/web-instance.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ From the Playground website there are also available some toolbars to customize

The options available from the "Customize Playground" window correpond to the following [Query API options](/developers/apis/query-api#available-options):

- `storage`
- `php`
- `php-extension-bundle`
- `networking`
Expand Down
39 changes: 34 additions & 5 deletions packages/php-wasm/web/src/lib/directory-handle-mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,15 +188,44 @@ export async function copyMemfsToOpfs(
}
await mirrorMemfsDirectoryinOpfs(memfsRoot, opfsRoot);

// @TODO: Understand why Safari is failing when writing many files concurrently
// with createSyncAccessHandle.
// Safari doesn't support createWritable as of 2024-09-18,
// and we are getting errors in Safari while using createSyncAccessHandle() in parallel.
// But in testing, Safari's createSyncAccessHandle() only begins to when
// writing ~125 files concurrently. So for now, we are limiting Safari to 100 concurrent writes
// to hopefully give us a safe margin.
// @ts-ignore -- Safari doesn't support createWritable as of 2024-09-18.
const maxConcurrentWrites = FileSystemFileHandle.prototype.createWritable
? Infinity
: 100;

// Now let's create all the required files in OPFS. This is quite slow
// so we report progress.
let i = 0;
const filesCreated = filesToCreate.map(([opfsDir, memfsPath, entryName]) =>
overwriteOpfsFile(opfsDir, entryName, FS, memfsPath).then(() => {
const outstandingWrites = new Set<Promise<void>>();
for (const [opfsDir, memfsPath, entryName] of filesToCreate) {
const promiseToCreateFile = overwriteOpfsFile(
opfsDir,
entryName,
FS,
memfsPath
).then(() => {
outstandingWrites.delete(promiseToCreateFile);
onProgress?.({ files: ++i, total: filesToCreate.length });
})
);
await Promise.all(filesCreated);
});

outstandingWrites.add(promiseToCreateFile);

if (outstandingWrites.size >= maxConcurrentWrites) {
// We should be under max concurrency when any write completes.
await Promise.race(outstandingWrites);
}
}

if (outstandingWrites.size > 0) {
await Promise.all(outstandingWrites);
}
}

function isMemfsDir(FS: Emscripten.RootFS, path: string) {
Expand Down
8 changes: 7 additions & 1 deletion packages/playground/blueprints/src/lib/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export function compileBlueprint(
onStepCompleted = () => {},
}: CompileBlueprintOptions = {}
): CompiledBlueprint {
// Deep clone the blueprint to avoid mutating the input
blueprint = structuredClone(blueprint);

blueprint = {
...blueprint,
steps: (blueprint.steps || [])
Expand Down Expand Up @@ -158,7 +161,10 @@ export function compileBlueprint(
// Default to the "kitchen sink" PHP extensions bundle if no
// other bundles are specified.
if (blueprint.phpExtensionBundles.length === 0) {
blueprint.phpExtensionBundles.push('kitchen-sink');
blueprint.phpExtensionBundles = [
...blueprint.phpExtensionBundles,
'kitchen-sink',
];
}

/**
Expand Down
1 change: 0 additions & 1 deletion packages/playground/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export interface StartPlaygroundOptions {
* @returns
*/
onBeforeBlueprint?: () => Promise<void>;
siteSlug?: string;
mounts?: Array<MountDescriptor>;
shouldInstallWordPress?: boolean;
}
Expand Down
37 changes: 36 additions & 1 deletion packages/playground/remote/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,47 @@ self.addEventListener('fetch', (event) => {
return;
}

let requestPromise = Promise.resolve(event.request);
if (url.pathname.startsWith('/proxy/')) {
const segments = url.pathname.split('/');
const command = segments[2];
switch (command) {
case 'load-cached-when-offline': {
const proxiedUrl =
url.pathname.substring(
'/proxy/'.length + command.length + 1
) +
(url?.search ? '?' + url.search : '') +
(url?.hash ? '#' + url.hash : '');
requestPromise = requestPromise.then((request) => {
return cloneRequest(request, {
url: proxiedUrl,
});
});
const isOffline = !navigator.onLine;
event.respondWith(
Promise.all([cachePromise, requestPromise]).then(
([cache, request]) =>
// Always save the response to cache, but only load
// it from the cache when we're offline.
cache.cachedFetch(request, {
loadFromCache: isOffline,
})
)
);
return;
}
}
}

/**
* Respond with cached assets if available.
* If the asset is not cached, fetch it from the network and cache it.
*/
event.respondWith(
cachePromise.then((cache) => cache.cachedFetch(event.request))
Promise.all([cachePromise, requestPromise]).then(([cache, request]) =>
cache.cachedFetch(request)
)
);
});

Expand Down
14 changes: 12 additions & 2 deletions packages/playground/remote/src/lib/offline-mode-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,22 @@ export class OfflineModeCache {
return Promise.all(oldKeys.map((key) => caches.delete(key)));
}

async cachedFetch(request: Request): Promise<Response> {
async cachedFetch(
request: Request,
{
loadFromCache = true,
}: {
loadFromCache?: boolean;
} = {}
): Promise<Response> {
if (!this.shouldCacheUrl(new URL(request.url))) {
return await fetch(request);
}

let response = await this.cache.match(request, { ignoreSearch: true });
let response = undefined;
if (loadFromCache) {
response = await this.cache.match(request, { ignoreSearch: true });
}
if (!response) {
response = await fetch(request);
if (response.ok) {
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/storage/src/lib/browser-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function opfsPathToDirectoryHandle(
const parts = opfsPath.split('/').filter((p) => p.length > 0);
let handle = await navigator.storage.getDirectory();
for (const part of parts) {
handle = await handle.getDirectoryHandle(part);
handle = await handle.getDirectoryHandle(part, { create: true });
}
return handle;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/playground/website/cypress/e2e/remote-assets.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ describe('Remote Assets', () => {
const testedStorageOptions = [
'none',
// TODO: Re-enable this option once the tests are more stable
//'browser'
//'opfs'
];

testedStorageOptions.forEach((storage) => {
Expand All @@ -15,7 +15,7 @@ describe('Remote Assets', () => {
cy.visit(`/?storage=${storage}#${blueprint}`);
runAssertions();

if (storage === 'browser') {
if (storage === 'opfs') {
// Reload and re-assert to test when loading from browser storage
cy.reload();
runAssertions();
Expand Down
4 changes: 2 additions & 2 deletions packages/playground/website/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
</script>
</head>
<body>
<div id="root">
<main id="root">
<script>
const query = new URLSearchParams(window.location.search);
const shouldLazyLoadPlayground =
Expand All @@ -68,7 +68,7 @@
root.appendChild(initiator);
}
</script>
</div>
</main>
<script type="module">
if (!shouldLazyLoadPlayground) {
import('./src/main');
Expand Down
90 changes: 27 additions & 63 deletions packages/playground/website/src/components/browser-chrome/index.tsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,68 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import css from './style.module.css';
import AddressBar from '../address-bar';
import { close } from '@wordpress/icons';
import classNames from 'classnames';
import { OpenSiteManagerButton } from '../open-site-manager-button';
import {
useAppSelector,
getActiveClientInfo,
useActiveSite,
} from '../../lib/state/redux/store';
import { SyncLocalFilesButton } from '../sync-local-files-button';

interface BrowserChromeProps {
children?: React.ReactNode;
toolbarButtons?: Array<React.ReactElement | false | null>;
url?: string;
showAddressBar?: boolean;
onUrlChange?: (url: string) => void;
hideToolbar?: boolean;
className?: string;
}

export default function BrowserChrome({
children,
url,
onUrlChange,
showAddressBar = true,
toolbarButtons,
hideToolbar,
className,
}: BrowserChromeProps) {
const clientInfo = useAppSelector(getActiveClientInfo);
const activeSite = useActiveSite();
const showAddressBar = !!clientInfo;
const url = clientInfo?.url;
const addressBarClass = classNames(css.addressBarSlot, {
[css.isHidden]: !showAddressBar,
});
const wrapperClass = classNames(css.wrapper, css.hasFullSizeWindow);

const [noticeHidden, setNoticeHidden] = useState(
document.cookie.includes('hideExperimentalNotice=true')
);

const hideNotice = () => {
document.cookie = 'hideExperimentalNotice=true';
setNoticeHidden(true);
};
useEffect(() => {
const hideNoticeTimeout = setTimeout(hideNotice, 20000);
return () => {
clearTimeout(hideNoticeTimeout);
};
}, []);

const experimentalNoticeClass = classNames(css.experimentalNotice, {
[css.isHidden]: noticeHidden,
});

/**
* Temporary feature flag to enable the site manager
* while using browser storage.
*
* TODO: Remove this once the site manager supports all storage options.
*/
const query = new URLSearchParams(window.location.search);
const showSiteManager = query.get('storage') === 'browser';

return (
<div
className={`${wrapperClass} ${className}`}
data-cy="simulated-browser"
>
<div className={`${css.window} browser-chrome-window`}>
<header
className={`${css.toolbar} ${
showSiteManager ? css.hasSiteManager : ''
} ${hideToolbar ? css.toolbarHidden : ''}`}
className={`
${css.toolbar}
${hideToolbar ? css.toolbarHidden : ''}
`}
aria-label="Playground toolbar"
>
<div className={css.windowControls}>
{showSiteManager && <OpenSiteManagerButton />}
{!showSiteManager && (
<>
<div
className={`${css.windowControl} ${css.isNeutral}`}
></div>
<div
className={`${css.windowControl} ${css.isNeutral}`}
></div>
<div
className={`${css.windowControl} ${css.isNeutral}`}
></div>
</>
)}
<OpenSiteManagerButton />
</div>

<div className={addressBarClass}>
<AddressBar url={url} onUpdate={onUrlChange} />
<AddressBar
url={url}
onUpdate={(newUrl) =>
clientInfo?.client.goTo(newUrl)
}
/>
</div>

<div className={css.toolbarButtons}>{toolbarButtons}</div>
<div className={css.toolbarButtons}>
{activeSite?.metadata?.storage === 'local-fs' ? (
<SyncLocalFilesButton />
) : null}
</div>
</header>
<div className={css.content}>{children}</div>
<div className={experimentalNoticeClass} onClick={hideNotice}>
{close}
This is a cool fun experimental WordPress running in your
browser :) All your changes are private and gone after a
page refresh.
</div>
</div>
</div>
);
Expand Down
Loading
Loading