diff --git a/README.md b/README.md index 840911805f..af7b0fa384 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ You can connect to the Playground using the JavaScript client. Here's an example + + + + + +
+
+
+
+ + + + + + + + + + + + + + ⌘ ctrl + space: show autocomplete + + ⌘ ctrl + enter: + + + +
+
+ +
+ + + +
+
+

+					
+ +
+
+
+
+
+ + +
+
+
+ + + + + + + + + + + + + +
+ +
+
+
+
+ + diff --git a/packages/playground/website/builder/builder.js b/packages/playground/website/builder/builder.js new file mode 100644 index 0000000000..231e86b3e3 --- /dev/null +++ b/packages/playground/website/builder/builder.js @@ -0,0 +1,917 @@ +const importStartPlaygroundWeb = import( + 'https://playground.wordpress.net/client/index.js' +); +const fetchBlueprintSchema = fetch( + 'https://playground.wordpress.net/blueprint-schema.json' +).then((r) => r.json()); + +const FALLBACK_TIMEOUT = 30 * 1000; + +const deref = (obj, root) => { + if (!obj || typeof obj !== 'object' || !('$ref' in obj)) { + return obj; + } + + const path = obj['$ref'].substr(2).split('/'); + let node = root; + + for (const p of path) { + if (!(p in node)) { + throw new Error(`Invalid reference: "${obj['$ref']}"`); + } + node = node[p]; + } + + return { ...obj, ...node }; +}; + +const reader = Symbol('reader'); + +const getSchemaReader = (schema, root = null) => { + if (schema[reader]) { + return schema[reader]; + } + + if (!root) { + root = schema; + } + + const proxy = new Proxy(schema, { + get: (target, key, receiver) => { + const val = Reflect.get(target, key, receiver); + if (val && typeof val === 'object') { + return getSchemaReader(deref(val, root), root); + } + return val; + }, + }); + + schema[reader] = proxy; + + return proxy; +}; + +const getPrevKeys = (editor, { column, row }) => { + const content = editor.getValue(); + const lines = content.split('\n'); + const line = String(lines[row]); + const colon = line.indexOf(':'); + + const path = []; + + if (colon > -1 && column > colon) { + const openQuote = line.indexOf('"'); + const closeQuote = line.indexOf('"', 1 + openQuote); + path.push(line.substring(1 + openQuote, closeQuote)); + } + + let indent = 0; + + while (line[indent] === ' ' || line[indent] === '\t') { + indent++; + } + + let checkRow = -1 + row; + + while (checkRow >= 0) { + const openQuote = lines[checkRow].indexOf('"'); + const closeQuote = lines[checkRow].indexOf('"', 1 + openQuote); + if (openQuote > -1 && openQuote < indent) { + path.push(lines[checkRow].substring(1 + openQuote, closeQuote)); + indent = openQuote; + } + checkRow--; + } + + return path; +}; + +const getLastOfType = (editor, type, { column, row }, skip = 0) => { + const content = editor.getValue(); + const lines = content.split('\n'); + + let checkRow = -1 + row; + + while (checkRow >= 0) { + const openBracket = lines[checkRow].indexOf('{'); + + if (openBracket > -1) { + if (--skip < 0) { + return null; + } + checkRow--; + continue; + } + + let indent = 0; + + while ( + lines[checkRow][indent] === ' ' || + lines[checkRow][indent] === '\t' + ) { + indent++; + } + + const openQuote = lines[checkRow].indexOf('"'); + const closeQuote = lines[checkRow].indexOf('"', 1 + openQuote); + const openVQuote = lines[checkRow].indexOf('"', 1 + closeQuote); + const closeVQuote = lines[checkRow].indexOf('"', 1 + openVQuote); + + if (openQuote > -1 && openQuote === indent) { + const checkType = lines[checkRow].substring( + 1 + openQuote, + closeQuote + ); + if (type === checkType) { + return lines[checkRow].substring(1 + openVQuote, closeVQuote); + } + } + + checkRow--; + } + + return null; +}; + +const getPrevSiblings = (editor, { column, row }) => { + const content = editor.getValue(); + const lines = content.split('\n'); + + let checkRow = -1 + row; + let indent = 0; + + while (lines[row][indent] === ' ' || lines[row][indent] === '\t') { + indent++; + } + + const siblings = []; + + while (checkRow >= 0) { + const openBracket = lines[checkRow].indexOf('{'); + + if (openBracket > -1 && openBracket < indent) { + break; + } + + const openQuote = lines[checkRow].indexOf('"'); + const closeQuote = lines[checkRow].indexOf('"', 1 + openQuote); + + if (openQuote > -1 && openQuote === indent) { + siblings.push(lines[checkRow].substring(1 + openQuote, closeQuote)); + } + + checkRow--; + } + + return siblings; +}; + +const getStepProperties = async (stepType) => { + const schema = await fetchBlueprintSchema; + const reader = getSchemaReader(schema); + return reader.definitions.StepDefinition.oneOf + .filter((s) => s.properties.step['const'] === stepType) + .map((s) => s.properties) + .flat() + .pop(); +}; + +const completeStepProperty = async (stepType, prefix) => { + const schema = await fetchBlueprintSchema; + return schema.definitions.StepDefinition.oneOf + .filter((s) => s.properties.step['const'] === stepType) + .map((s) => Object.keys(s.properties)) + .flat() + .filter((s) => s.substr(0, prefix.length) === prefix) + .filter((s) => !['step', 'progress'].includes(s)); +}; + +const getStepSubProperties = async (stepType, resType, property) => { + const schema = await fetchBlueprintSchema; + const reader = getSchemaReader(schema); + return reader.definitions.StepDefinition.oneOf + .filter((s) => s.properties.step['const'] === stepType) + .map((s) => { + return s.properties[property].anyOf; + }) + .flat() + .filter((s) => !resType || s.properties.resource.const === resType) + .map((s) => s.properties) + .flat() + .pop(); +}; + +const completeStepSubProperty = async ( + stepType, + resType, + property, + subKey, + prefix +) => { + if (!resType && !subKey) { + return ['resource']; + } + const schema = await fetchBlueprintSchema; + const reader = getSchemaReader(schema); + return reader.definitions.StepDefinition.oneOf + .filter((s) => s.properties.step['const'] === stepType) + .map((s) => { + return s.properties[property].anyOf; + }) + .flat() + .filter((s) => !resType || s.properties.resource.const === resType) + .map((s) => { + if (subKey === null) { + return Object.keys(s.properties); + } + return s.properties.resource.const; + }) + .flat() + .filter((s) => !['resource'].includes(s)); +}; + +const completeStep = async (prefix) => { + const schema = await fetchBlueprintSchema; + const reader = getSchemaReader(schema); + return reader.definitions.StepDefinition.oneOf + .map((s) => s.properties.step['const']) + .filter((s) => s.substr(0, prefix.length) === prefix); +}; + +const completePhpVersion = async (prefix) => { + const schema = await fetchBlueprintSchema; + const reader = getSchemaReader(schema); + return reader.definitions.SupportedPHPVersion.enum.filter( + (s) => s.substr(0, prefix.length) === prefix + ); +}; + +const completeRootKey = async (prefix) => { + const schema = await fetchBlueprintSchema; + const reader = getSchemaReader(schema); + return Object.keys(reader.definitions.Blueprint.properties).filter( + (s) => s[0] !== '$' && s.substr(0, prefix.length) === prefix + ); +}; + +const completeFeature = async (prefix) => { + const schema = await fetchBlueprintSchema; + const reader = getSchemaReader(schema); + return Object.keys( + reader.definitions.Blueprint.properties.features.properties + ).filter((s) => s[0] !== '$' && s.substr(0, prefix.length) === prefix); +}; + +let debounce = null; +let starting = null; + +const getCompletions = async (editor, session, pos, prefix, callback) => { + const list = []; + const prevKey = getPrevKeys(editor, pos); + + const content = editor.getValue(); + const lines = content.split('\n'); + const line = String(lines[pos.row]); + const colon = line.indexOf(':'); + + const { row, column } = pos; + + const qA = + !lines[row][-1 + column] || lines[row][-1 + column] === ' ' ? '"' : ''; + const qB = !lines[row][column] || lines[row][column] === ' ' ? '"' : ''; + + if ( + prevKey.length === 3 && + prefix.length >= 3 && + prevKey.join('<') === 'slug { + try { + const res = await fetch( + `https://playground.wordpress.net/plugin-proxy.php?${proxyParams}` + ); + const json = await res.json(); + json?.plugins.forEach((p) => { + var doc = new DOMParser().parseFromString( + p.name, + 'text/html' + ); + const meta = doc.documentElement.textContent; + callback(null, [ + { + name: p.slug, + value: qA + p.slug + qB, + score: 1, + meta, + }, + ]); + }); + } finally { + document.body.setAttribute('data-loading', false); + } + }, 250); + } + + if ( + prevKey.length === 3 && + prefix.length >= 3 && + prevKey.join('<') === 'slug { + try { + const res = await fetch( + `https://playground.wordpress.net/plugin-proxy.php?${proxyParams}` + ); + const json = await res.json(); + json?.themes.forEach((p) => { + var doc = new DOMParser().parseFromString( + p.name, + 'text/html' + ); + const meta = doc.documentElement.textContent; + callback(null, [ + { + name: p.slug, + value: qA + p.slug + qB, + score: 1, + meta, + }, + ]); + }); + } finally { + document.body.setAttribute('data-loading', false); + } + }, 250); + } + + switch (prevKey[0]) { + case 'preferredVersions': { + const used = await getPrevSiblings(editor, pos); + list.push(...['wp', 'php'].filter((s) => !used.includes(s))); + break; + } + + case 'wp': + list.push('latest'); + break; + + case 'php': + list.push(...(await completePhpVersion(prefix))); + break; + + case 'steps': + { + const used = await getPrevSiblings(editor, pos); + const stepType = getLastOfType(editor, 'step', pos); + if (stepType) { + const suggestions = await completeStepProperty( + stepType, + prefix + ); + list.push(...suggestions.filter((s) => !used.includes(s))); + } else { + list.push('step'); + } + } + break; + + case 'step': + list.push(...(await completeStep(prefix))); + break; + + case 'features': + list.push(...(await completeFeature(prefix))); + break; + + case undefined: + list.push(...(await completeRootKey(prefix))); + break; + + default: + switch (prevKey[-1 + prevKey.length]) { + case 'steps': + { + const stepType = getLastOfType(editor, 'step', pos, 1); + const resType = getLastOfType( + editor, + 'resource', + pos, + 1 + ); + if (prevKey.length === 2) { + if (colon === -1) { + const used = await getPrevSiblings(editor, pos); + const suggestions = + await completeStepSubProperty( + stepType, + resType, + prevKey[-2 + prevKey.length], + null, + prefix + ); + list.push( + ...suggestions.filter( + (s) => !used.includes(s) + ) + ); + } + } else if ( + prevKey.length === 3 && + prevKey[0] === 'resource' + ) { + list.push( + ...(await completeStepSubProperty( + stepType, + resType, + prevKey[-2 + prevKey.length], + prevKey[0], + prefix + )) + ); + } + } + break; + } + + break; + } + + for (const fill of list) { + callback(null, [ + { + name: fill, + value: qA + fill + qB, + score: 1, + meta: 'Blueprint Schema', + }, + ]); + } +}; + +let errorTag; +const showError = (error) => { + console.error(error); + if (!errorTag) errorTag = document.getElementById('error-output'); + const errDoc = `${error}`; + errorTag.setAttribute('srcdoc', errDoc); +}; +const clearError = (error) => { + if (!errorTag) errorTag = document.getElementById('error-output'); + errorTag.setAttribute('srcdoc', ''); +}; + +const formatJson = (editor, jsonObject = {}) => { + const existing = editor.getSession().getValue(); + const formatted = JSON.stringify(jsonObject, null, 2) + '\n'; + if (formatted !== existing) { + editor.getSession().setValue(formatted); + if (formatted !== existing) { + editor.getSession().setValue(formatted); + } + } +}; + +function getCurrentBlueprint(editor) { + const blueprint = JSON.parse(editor.getValue()); + if (blueprint.features && blueprint.features.networking === false) { + blueprint.features.networking = true; + } + return blueprint; +} + +const runBlueprint = async (editor) => { + const fallback = setTimeout(() => { + document.body.setAttribute('data-starting', false); + starting = false; + }, FALLBACK_TIMEOUT); + try { + window.location.hash = JSON.stringify(JSON.parse(editor.getValue())); + if (starting) { + return; + } + starting = true; + document.body.setAttribute('data-starting', true); + clearError(); + const blueprintJsonObject = getCurrentBlueprint(editor); + window.location.hash = JSON.stringify(getCurrentBlueprint(editor)); + formatJson(editor, blueprintJsonObject); + const blueprintCopy = JSON.parse(JSON.stringify(blueprintJsonObject)); + delete blueprintCopy.features; // I am getting error otherwise + const startPlaygroundWeb = (await importStartPlaygroundWeb) + .startPlaygroundWeb; + await startPlaygroundWeb({ + iframe: document.getElementById('wp-playground'), + remoteUrl: `https://playground.wordpress.net/remote.html`, + blueprint: blueprintCopy, + }); + document.body.setAttribute('data-starting', false); + clearTimeout(fallback); + starting = false; + } catch (error) { + starting = false; + document.body.setAttribute('data-starting', false); + showError(error); + clearTimeout(fallback); + } finally { + document.body.setAttribute('data-starting', false); + } +}; + +const loadFromHash = (editor) => { + const hash = decodeURI(window.location.hash.substr(1)); + try { + formatJson(editor, JSON.parse(hash)); + } catch (error) { + console.error(error); + } +}; + +document.addEventListener('DOMContentLoaded', () => { + const iframeSrc = 'https://playground.wordpress.net/'; + const iframe = document.querySelector('iframe#wp-playground'); + const textarea = document.querySelector('#jsontext'); + const button = document.querySelector('button#run'); + const newTab = document.querySelector('button#new-tab'); + + // eslint-disable-next-line no-undef + const editor = ace.edit('jsontext'); + editor.setTheme('ace/theme/github_dark'); + editor.session.setMode('ace/mode/json'); + + // eslint-disable-next-line no-undef + const langTools = ace.require('ace/ext/language_tools'); + + langTools.setCompleters([]); + + langTools.addCompleter({ triggerCharacters: ['"'], getCompletions }); + + editor.setOptions({ + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, + enableSnippets: true, + useSoftTabs: true, + tabSize: 2, + }); + + editor.getSession().on('change', async (event) => { + if (event.action !== 'insert') { + return; + } + + const content = editor.getValue(); + const lines = content.split('\n'); + + if ( + event.start.row === event.end.row && + 1 < Math.abs(event.start.column - event.end.column) + ) { + if (lines[event.end.row][event.end.column] === '"') { + editor.moveCursorTo(event.end.row, event.end.column + 1); + return; + } + } + + if (lines[event.end.row][event.end.column]) { + return; + } + + const indent = (lines[event.start.row].match(/^(\s+)/g) || [''])[0]; + + const inserted = event.lines.join('\n'); + const prevKey = getPrevKeys(editor, event.end); + + if (inserted.length > 1) { + return; + } + + if (inserted === ':') { + const colon = lines[event.start.row].indexOf(':'); + if (colon > -1 && colon < event.start.column) { + return; + } + + if (prevKey.length === 1 && prevKey[0] === 'landingPage') { + editor + .getSession() + .insert( + { row: event.end.row, column: event.end.column }, + ' ""' + ); + editor.moveCursorTo(event.end.row, 1 + event.end.column); + } + + if (prevKey.length === 1 && prevKey[0] === 'preferredVersions') { + editor + .getSession() + .insert( + { row: event.end.row, column: event.end.column }, + ' {}' + ); + editor.moveCursorTo(event.end.row, 1 + event.end.column); + } + + if (prevKey.length === 2 && prevKey[1] === 'preferredVersions') { + editor + .getSession() + .insert( + { row: event.end.row, column: event.end.column }, + ' ""' + ); + editor.moveCursorTo(event.end.row, 1 + event.end.column); + setTimeout(() => editor.execCommand('startAutocomplete'), 0); + } + + if ( + prevKey.length === 1 && + (prevKey[0] === 'steps' || prevKey[0] === 'features') + ) { + editor + .getSession() + .insert( + { row: event.end.row, column: event.end.column }, + ' []' + ); + editor.moveCursorTo(event.end.row, 1 + event.end.column); + } + + if (prevKey.length === 3 && prevKey[2] === 'steps') { + const stepType = getLastOfType(editor, 'step', event.end, 1); + const resType = getLastOfType(editor, 'resource', event.end); + const subProps = await getStepSubProperties( + stepType, + resType, + prevKey[1] + ); + const subProp = subProps[prevKey[0]]; + + if (subProp?.type === 'string') { + editor + .getSession() + .insert( + { row: event.end.row, column: event.end.column }, + ' ""' + ); + editor.moveCursorTo(event.end.row, 2 + event.end.column); + editor.execCommand('startAutocomplete'); + } else if (subProp?.type === 'object') { + editor + .getSession() + .insert( + { row: event.end.row, column: event.end.column }, + ' {}' + ); + editor.moveCursorTo(event.end.row, 2 + event.end.column); + } + } + + if ( + prevKey.length === 2 && + prevKey[0] === 'step' && + prevKey[1] === 'steps' + ) { + editor.getSession().insert( + { + row: event.start.row, + column: event.start.column + 1, + }, + ' ""' + ); + editor.moveCursorTo(event.end.row, 1 + event.end.column); + editor.execCommand('startAutocomplete'); + } else if (prevKey.length === 2 && prevKey[1] === 'steps') { + const stepType = await getLastOfType(editor, 'step', event.end); + const properties = await getStepProperties(stepType); + const property = properties[prevKey[0]] ?? null; + const propType = property.type ?? null; + const propRef = property['$ref']; + + if (propType === 'string') { + editor + .getSession() + .insert( + { row: event.end.row, column: event.end.column }, + ' ""' + ); + editor.moveCursorTo(event.end.row, 2 + event.end.column); + } else if (propRef === '#/definitions/FileReference') { + editor + .getSession() + .insert( + { row: event.end.row, column: event.end.column }, + ' {}' + ); + editor.moveCursorTo(event.end.row, 2 + event.end.column); + } + } + } else if (inserted === '[') { + editor + .getSession() + .insert( + { row: event.end.row, column: event.start.column + 1 }, + ']' + ); + return; + } else if (inserted === '{') { + editor + .getSession() + .insert( + { row: event.end.row, column: event.start.column + 1 }, + '}' + ); + return; + } else if (inserted === ',') { + const nextIndent = (lines[1 + event.start.row].match(/^(\s+)/g) || [ + '', + ])[0]; + if (nextIndent.length >= indent.length) { + return; + } + if (lines[event.start.row][-1 + event.start.column] !== '"') { + editor + .getSession() + .insert( + { row: event.end.row, column: event.end.column }, + '\n' + indent + ); + editor.moveCursorTo(1 + event.end.row, 1 + indent.length); + return; + } + editor + .getSession() + .insert( + { row: event.end.row, column: event.end.column }, + '\n' + indent + '""' + ); + editor.moveCursorTo(1 + event.end.row, 1 + indent.length); + editor.execCommand('startAutocomplete'); + } + }); + + window.test = { + iframeSrc, + iframe, + textarea, + button, + }; + + button.addEventListener('click', () => { + try { + clearError(); + runBlueprint(editor); + } catch (error) { + showError(error); + } + }); + + let prevWin; + + newTab.addEventListener('click', () => { + runBlueprint(editor); + const query = new URLSearchParams(); + + query.set('mode', 'seamless'); + const url = + `https://playground.wordpress.net/?${query}#` + + JSON.stringify(getCurrentBlueprint(editor)); + if (prevWin) { + prevWin.close(); + } + prevWin = window.open(url, '_blank'); + }); + + if (window.location.hash) { + loadFromHash(editor); + } else { + formatJson(editor, { + landingPage: '/wp-admin/', + phpExtensionBundles: ['kitchen-sink'], + preferredVersions: { + php: '7.4', + wp: '5.9', + }, + steps: [ + { + step: 'login', + username: 'admin', + password: 'password', + }, + ], + }); + } + + runBlueprint(editor); + + window.addEventListener('hashchange', () => { + loadFromHash(editor); + runBlueprint(editor); + }); + + editor.commands.addCommand({ + name: 'Run Blueprint', + bindKey: { + win: 'Ctrl-Enter|Ctrl-S', + mac: 'Command-Enter|Command-S', + }, + exec: (editor) => runBlueprint(editor), + readOnly: false, + }); + + const save = document.querySelector('button#save'); + const open = document.querySelector('button#open'); + + const saveMethod = () => { + const dataUri = `data:application/json;base64,${btoa( + JSON.stringify(JSON.parse(editor.getValue()), null, 2) + )}`; + const link = document.createElement('a'); + link.setAttribute('href', dataUri); + link.setAttribute( + 'download', + `blueprint-${new Date().toISOString()}.json` + ); + link.click(); + }; + + const openMethod = () => { + const input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.addEventListener('change', (event) => { + [...input.files].forEach((f) => { + const reader = new FileReader(); + reader.addEventListener('load', () => { + editor.setValue( + JSON.stringify(JSON.parse(reader.result), null, 2) + ); + editor.moveCursorTo(0, 0); + runBlueprint(editor); + }); + reader.readAsText(f); + }); + }); + input.click(); + }; + + save.addEventListener('click', saveMethod); + open.addEventListener('click', openMethod); + + editor.commands.addCommand({ + name: 'Save Blueprint', + bindKey: { + win: 'Ctrl-S', + mac: 'Command-S', + }, + exec: (editor) => saveMethod(), + readOnly: false, + }); + + editor.commands.addCommand({ + name: 'Open Blueprint', + bindKey: { + win: 'Ctrl-O', + mac: 'Command-O', + }, + exec: (editor) => openMethod(), + readOnly: false, + }); +}); diff --git a/packages/playground/website/builder/download.svg b/packages/playground/website/builder/download.svg new file mode 100644 index 0000000000..6f71b2b0e5 --- /dev/null +++ b/packages/playground/website/builder/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/playground/website/builder/edit.svg b/packages/playground/website/builder/edit.svg new file mode 100644 index 0000000000..3d563c709a --- /dev/null +++ b/packages/playground/website/builder/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/playground/website/builder/new-tab-button.svg b/packages/playground/website/builder/new-tab-button.svg new file mode 100644 index 0000000000..0d7031a707 --- /dev/null +++ b/packages/playground/website/builder/new-tab-button.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/playground/website/builder/open.svg b/packages/playground/website/builder/open.svg new file mode 100644 index 0000000000..eae4d9dfbc --- /dev/null +++ b/packages/playground/website/builder/open.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/packages/playground/website/builder/play-button.svg b/packages/playground/website/builder/play-button.svg new file mode 100644 index 0000000000..9e7577819a --- /dev/null +++ b/packages/playground/website/builder/play-button.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/playground/website/builder/save.svg b/packages/playground/website/builder/save.svg new file mode 100644 index 0000000000..b68bd97293 --- /dev/null +++ b/packages/playground/website/builder/save.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/playground/website/builder/style.css b/packages/playground/website/builder/style.css new file mode 100644 index 0000000000..fb2240778b --- /dev/null +++ b/packages/playground/website/builder/style.css @@ -0,0 +1,428 @@ +html { + background-color: #aaa; + font-family: sans-serif; +} + +html, +body { + overflow: hidden; +} + +html, +body, +.playground-wrapper { + height: 100%; +} + +.playground-wrapper { + display: flex; + flex-direction: column; + padding: 10px; + box-sizing: border-box; + background-color: #1e2327; + box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.75); +} + +body { + padding: 1em; + padding: 0; + box-sizing: border-box; + margin: 0; +} + +main { + display: flex; + flex-direction: row; + justify-content: stretch; + align-items: center; + flex: 1; +} + +.column { + display: flex; + flex: 1; + flex-direction: column; + justify-content: space-between; + align-items: center; + height: 100%; + width: 100%; + justify-content: stretch; + align-items: stretch; + flex: 2; + max-width: 50%; + background-color: #24292e; + border: 1px solid #40464d; + box-sizing: border-box; + position: relative; +} + +.column.editor { + flex: 1; +} + +.separator { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + height: 100%; + width: 10px; +} + +.frame-wrapper { + position: relative; +} + +.frame-center { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +svg { + filter: brightness(0.333) contrast(2) brightness(3); + mix-blend-mode: lighten; +} + +iframe { + position: absolute; + top: 0; + left: 0; + height: calc(100% / var(--zoom, 1)); + width: calc(100% / var(--zoom, 1)); + transform-origin: top left; + transform: scale(var(--zoom, 1)); + box-sizing: border-box; + border: 0; + background-color: rgba(0, 0, 0, 0.25); + transition: transform 0.5s linear, width 0.5s linear, height 0.5s linear; +} + +.column.viewer { + overflow: hidden; +} + +button { + font-weight: bold; + background: none; + border: none; + cursor: pointer; + background-color: #ccc; + padding: 8px; + margin: 7px; + box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.25); + user-select: none; + border-radius: 2px; +} + +button:active { + position: relative; + top: 1px; + left: 1px; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.125); + filter: brightness(0.94); +} + +button:hover:not(:active) { + filter: brightness(1.06); +} + +button.cta { + background-color: #eaaa00; +} + +button.run { + background-image: url('./play-button.svg'); + background-size: auto 55%; + background-position: 0.325em center; + background-repeat: no-repeat; + padding-left: 1.85em; +} + +button#open-ai { + background-image: url('./brain.svg'); + background-size: auto 75%; + background-position: 0.325em center; + background-repeat: no-repeat; + padding-left: 2.85em; +} + +button#save { + background-image: url('./save.svg'); + background-size: auto 60%; + background-position: center; + background-repeat: no-repeat; + width: 2.25rem; +} +button#open { + background-image: url('./open.svg'); + background-size: auto 50%; + background-position: center; + background-repeat: no-repeat; + width: 2.25rem; +} + +button#open-ai { + background-image: url('./brain.svg'); + background-size: auto 75%; + background-position: 0.325em center; + background-repeat: no-repeat; + padding-left: 2.85em; +} + +button#save { + background-image: url('./save.svg'); + background-size: auto 60%; + background-position: center; + background-repeat: no-repeat; + width: 2.25rem; +} +button#open { + background-image: url('./open.svg'); + background-size: auto 50%; + background-position: center; + background-repeat: no-repeat; + width: 2.25rem; +} + +button#new-tab { + background-image: url('./new-tab-button.svg'); + background-size: auto 55%; + background-position: calc(100% + -0.5em) center; + background-repeat: no-repeat; + padding-right: 2.1em; +} + +.jsontext, +.frame-wrapper { + width: 100%; + height: 100%; + transition: height 0.25s ease-out, padding 0.25s ease-out, + background-color 0.25s ease-in; +} + +.frame-wrapper:has(#error-output) { + background-color: rgba(0, 0, 0, 0.25); + margin: 0; + padding: 1rem; + box-sizing: border-box; +} + +.frame-wrapper:has(#error-output[srcdoc='']) { + background-color: rgba(255, 255, 255, 0.75); + height: 0%; + padding: 0rem; +} + +.json-container { + word-break: break-all; + white-space: break-spaces; + padding-left: 30px; + height: 100%; +} + +.toolbar { + position: relative; + z-index: 1; + background-color: #40464d; + text-align: right; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; +} + +.toolbar span { + color: white; + font-size: 0.8rem; + font-style: italic; + white-space: pre; +} + +.toolbar svg, +.toolbar span { + margin-left: 0.5rem; +} + +.toolbar svg { + display: none; +} + +body[data-loading='true'] .toolbar svg { + display: initial; +} + +.toolbar span.spacer { + flex: 1; + display: inline-block; +} + +pre { + margin-bottom: 0; + margin-top: 10px; +} + +button { + transition: opacity 0.5s ease-in-out; + height: 2rem; +} + +button.zoom { + color: transparent; + background-repeat: no-repeat; + background-position: center; + background-size: 63%; + width: 2rem; +} + +#zoom { + font-style: normal; + text-align: center; + width: 2.25rem; + margin: 0; +} + +button#zoom-in { + background-image: url(./zoom-in.svg); +} + +button#zoom-out { + background-image: url(./zoom-out.svg); +} + +[data-starting='true'] button.run { + pointer-events: none; + filter: saturate(0.25); + opacity: 0.5; +} + +[data-starting='true'] button.run { + animation: button-pulse infinite alternate 0.5s; +} + +@keyframes button-pulse { + from { + opacity: 1; + } + to { + opacity: 0.5; + } +} + +section#ai { + display: flex; + flex-direction: column; + height: 0rem; + overflow: hidden; + transition: height 0.25s ease-in; +} + +body[data-show-ai='true'] section#ai { + height: 35rem; +} + +#prompt { + font-family: sans-serif; + font-size: 1.1rem; + background-color: #ccc; + width: 100%; + resize: none; + box-sizing: border-box; + margin: 0; + flex: 1; + padding: 1rem; + border-radius: 0; + border: 0; +} + +button.close { + background-image: url(./x.svg); + background-size: auto 55%; + background-position: center; + background-repeat: no-repeat; + width: 2.2rem; +} + +section.fly-in { + position: absolute; + left: 0; + top: 0; + height: 100%; + min-width: 20rem; + background-color: #1e2327; + border-right: 1px solid rgba(255, 255, 255, 0.25); + z-index: 10; + color: white; + padding: 20px; + box-sizing: border-box; + transition: transform 0.125s ease-out; +} + +section.fly-in p { + margin: 0; +} + +section.fly-in ul { + padding: 0; + display: flex; + flex-direction: column; +} + +section.fly-in ul li { + list-style: none; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + height: 2rem; + white-space: pre; +} + +section.fly-in ul li span { + flex: 1; +} + +section.fly-in ul li button { + opacity: 0; + pointer-events: none; +} + +section.fly-in ul li:hover button { + opacity: 0.5; + visibility: initial; + pointer-events: initial; +} + +section.fly-in ul li:hover button:hover { + opacity: 1; + filter: invert(1); +} + +section.fly-in ul li button { + border: 0; + background: transparent; + box-shadow: none; + filter: invert(1); + + background-size: auto 55%; + background-position: center; + background-repeat: no-repeat; + width: 1.25rem; +} + +section.fly-in ul li:hover button.rename { + background-image: url(./edit.svg); +} +section.fly-in ul li:hover button.download { + background-image: url(./download.svg); +} +section.fly-in ul li:hover button.delete { + background-image: url(./x.svg); +} + +body[data-menu='false'] section.fly-in { + transform: translateX(-100%); +} diff --git a/packages/playground/website/builder/x.svg b/packages/playground/website/builder/x.svg new file mode 100644 index 0000000000..f7fb5dbb77 --- /dev/null +++ b/packages/playground/website/builder/x.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/playground/website/builder/zoom-in.svg b/packages/playground/website/builder/zoom-in.svg new file mode 100644 index 0000000000..9b8ea26541 --- /dev/null +++ b/packages/playground/website/builder/zoom-in.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/playground/website/builder/zoom-out.svg b/packages/playground/website/builder/zoom-out.svg new file mode 100644 index 0000000000..9759fcb7c1 --- /dev/null +++ b/packages/playground/website/builder/zoom-out.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/playground/website/project.json b/packages/playground/website/project.json index d51d503cb5..5a7f4d3635 100644 --- a/packages/playground/website/project.json +++ b/packages/playground/website/project.json @@ -16,6 +16,7 @@ "rimraf --no-preserve-root ./wasm-wordpress-net", "mkdir ./wasm-wordpress-net", "cp -r ./blueprints/blueprint-schema.json ./wasm-wordpress-net/", + "cp -r ./client ./wasm-wordpress-net/", "cp -r ./remote/* ./wasm-wordpress-net/", "cp -r ./website/* ./wasm-wordpress-net/", "cat ./remote/.htaccess > ./wasm-wordpress-net/.htaccess" diff --git a/packages/playground/website/vite.config.ts b/packages/playground/website/vite.config.ts index 51df9601de..5e441dcbb8 100644 --- a/packages/playground/website/vite.config.ts +++ b/packages/playground/website/vite.config.ts @@ -112,6 +112,9 @@ export default defineConfig(({ command, mode }) => { 'time-traveling.html': fileURLToPath( new URL('./demos/time-traveling.html', import.meta.url) ), + 'builder/builder.html': fileURLToPath( + new URL('./builder/builder.html', import.meta.url) + ), }, // output: { // entryFileNames: (assetInfo) => {