From d714cd2f6ab5bf5cb8975c1b2d54ca71d167e835 Mon Sep 17 00:00:00 2001 From: Hatem Hosny Date: Thu, 15 Oct 2026 23:04:09 +0300 Subject: [PATCH 1/4] feat(App): add console mode --- src/livecodes/config/validate-config.ts | 1 + src/livecodes/core.ts | 22 ++++++++++++++++++++-- src/livecodes/styles/app.scss | 25 ++++++++++++++++++++++++- src/livecodes/toolspane/tools.ts | 11 +++++++++-- src/sdk/models.ts | 2 +- 5 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/livecodes/config/validate-config.ts b/src/livecodes/config/validate-config.ts index 44032cb65e..3355ee5f43 100644 --- a/src/livecodes/config/validate-config.ts +++ b/src/livecodes/config/validate-config.ts @@ -30,6 +30,7 @@ export const validateConfig = (config: Partial): Partial => { 'focus', 'lite', 'simple', + 'console', 'editor', 'codeblock', 'result', diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index c6ce47fd77..3b0cfdd39b 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -628,6 +628,7 @@ const showMode = (mode?: Config['mode'], view?: Config['view']) => { focus: '111', simple: '111', lite: '111', + console: '111', editor: '110', codeblock: '010', result: '001', @@ -693,9 +694,10 @@ const showMode = (mode?: Config['mode'], view?: Config['view']) => { document.body.classList.toggle('simple-mode', mode === 'simple'); document.body.classList.toggle('focus-mode', mode === 'focus'); document.body.classList.toggle('lite-mode', mode === 'lite'); + document.body.classList.toggle('console-mode', mode === 'console'); document.body.classList.toggle('result', mode === 'result'); document.body.classList.toggle('no-result', mode === 'editor' || mode === 'codeblock'); - if ((mode === 'full' || mode === 'simple') && !split) { + if ((mode === 'full' || mode === 'simple' || mode === 'console') && !split) { split = createSplitPanes(); } if (mode === 'focus') { @@ -5105,6 +5107,17 @@ const configureSimpleMode = (config: Config) => { }); }; +const configureConsoleMode = (config: Config) => { + setConfig({ + ...config, + tools: { + enabled: Array.isArray(config.tools?.enabled) ? config.tools.enabled : ['console'], + active: config.tools?.active || 'console', + status: config.tools?.status || 'full', + }, + }); +}; + const configureModes = ({ config, isEmbed, @@ -5129,6 +5142,9 @@ const configureModes = ({ if (config.mode === 'simple') { configureSimpleMode(config); } + if (config.mode === 'console') { + configureConsoleMode(config); + } }; const importExternalContent = async (options: { @@ -5331,7 +5347,9 @@ const initializePlayground = async ( isLite || (options?.isEmbed ?? false) || initialConfig.mode === 'simple' || - params.mode === 'simple'; + params.mode === 'simple' || + initialConfig.mode === 'console' || + params.mode === 'console'; window.history.replaceState(null, '', './'); // fix URL from "/app" to "/" await initializeStores(stores, isEmbed); diff --git a/src/livecodes/styles/app.scss b/src/livecodes/styles/app.scss index 3e706dbb73..b49c354c06 100644 --- a/src/livecodes/styles/app.scss +++ b/src/livecodes/styles/app.scss @@ -1793,7 +1793,8 @@ a.editor-title { } } -.simple-mode { +.simple-mode, +.console-mode { .editor-title:not(.active), &.embed #toolbar .toolbar-render #result-button, &.embed #toolbar .toolbar-render #fullscreen-button, @@ -1842,6 +1843,28 @@ a.editor-title { } } +.console-mode { + #toolbar { + margin-bottom: 0; + transform: none; + width: 100%; + } + + #zoom-button, + .console-close-button, + #size-label { + display: none !important; + } + + a.tools-pane-title { + cursor: default; + } + + .gutter.gutter-vertical { + cursor: default; + } +} + .focus-mode { #app-menu-button-project, #app-menu-button-settings, diff --git a/src/livecodes/toolspane/tools.ts b/src/livecodes/toolspane/tools.ts index fdda7d78b3..639ede98af 100644 --- a/src/livecodes/toolspane/tools.ts +++ b/src/livecodes/toolspane/tools.ts @@ -224,7 +224,7 @@ export const createToolsPane = ( toolTitle.dataset.id = String(index); toolTitle.classList.add('tools-pane-title', tool.name); toolTitle.innerHTML = tool.title; - if (tool.name === 'console') { + if (tool.name === 'console' && config.mode !== 'console') { toolTitle.title = '(Ctrl/⌘ + Alt + C)'; } toolsPaneTitles.appendChild(toolTitle); @@ -235,6 +235,7 @@ export const createToolsPane = ( 'click', (event: any) => { event.preventDefault(); + if (config.mode === 'console') return; if (event.detail === 0 || event.detail === 1) { timer = setTimeout(() => { if (toolsSplit.getSizes()[0] > 90) { @@ -254,6 +255,7 @@ export const createToolsPane = ( 'dblclick', (event) => { event.preventDefault(); + if (config.mode === 'console') return; clearTimeout(timer); if (toolsSplit.getSizes()[0] < 10) { close(); @@ -268,6 +270,7 @@ export const createToolsPane = ( 'touchstart', (event) => { event.preventDefault(); + if (config.mode === 'console') return; if (toolsSplit.getSizes()[0] > 90) { open(index); } else if (!toolTitle.classList.contains('active')) { @@ -288,7 +291,7 @@ export const createToolsPane = ( 'resize', () => { const toolsPane = document.querySelector('#tools-pane') as HTMLElement; - if (!toolsPane) return; + if (!toolsPane || config.mode === 'console') return; if (toolsSplit.getSizes()[0] < 10) { result.style.height = '0'; toolsPane.style.height = `calc(100% - ${gutterSize}px)`; @@ -375,6 +378,10 @@ export const createToolsPane = ( }); } setActiveTool(activeToolId); + + if (config.mode === 'console') { + toolsSplit.destroy(/* preserveStyles= */ false, /* preserveGutters= */ true); + } }; const observeStyleChange = () => { diff --git a/src/sdk/models.ts b/src/sdk/models.ts index a9f49980a6..6d56b17a02 100644 --- a/src/sdk/models.ts +++ b/src/sdk/models.ts @@ -1226,7 +1226,7 @@ export interface AppConfig { * Sets the [display mode](https://livecodes.io/docs/features/display-modes). * @default "full" */ - mode: 'full' | 'focus' | 'lite' | 'simple' | 'editor' | 'codeblock' | 'result'; + mode: 'full' | 'focus' | 'lite' | 'simple' | 'console' | 'editor' | 'codeblock' | 'result'; /** * Sets enabled and active tools and status of [tools pane](https://livecodes.io/docs/features/tools-pane). From 6cb5e6d60e9ab50224a975d77edfe035a35f3c6e Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:28:00 +0000 Subject: [PATCH 2/4] fix(App): address console mode review feedback --- docs/docs/features/display-modes.mdx | 13 +++++++++++++ src/livecodes/core.ts | 6 ++++++ src/livecodes/styles/app.scss | 14 ++++++++++---- src/livecodes/toolspane/tools.ts | 19 ++++++++++--------- 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/docs/docs/features/display-modes.mdx b/docs/docs/features/display-modes.mdx index 9f9c581bb1..31c838607a 100644 --- a/docs/docs/features/display-modes.mdx +++ b/docs/docs/features/display-modes.mdx @@ -62,6 +62,19 @@ export const simpleConfig = { +## `console` + +This mode is mainly useful for embedded playgrounds. +It shows a single editor together with the [console](./console.mdx) maximized (filling the output area), with the result page collapsed. +The [tools pane](./tools-pane.mdx) is fixed to show only the console; it cannot be resized or closed. +The console can still be used as a REPL, and the code can be run from the editor. + +Demo: JS with console + + + ## `lite` Loads a light-weight, minimal code editor, with limited playground features. diff --git a/src/livecodes/core.ts b/src/livecodes/core.ts index 3b0cfdd39b..a682ef2d9c 100644 --- a/src/livecodes/core.ts +++ b/src/livecodes/core.ts @@ -703,6 +703,12 @@ const showMode = (mode?: Config['mode'], view?: Config['view']) => { if (mode === 'focus') { toolsPane?.setActiveTool('console'); } + if (mode === 'console') { + toolsPane?.setActiveTool('console'); + if (!['open', 'full'].includes(toolsPane?.getStatus() || '')) { + toolsPane?.maximize(); + } + } window.dispatchEvent(new Event(customEvents.resizeEditor)); }; diff --git a/src/livecodes/styles/app.scss b/src/livecodes/styles/app.scss index b49c354c06..0a410e6f76 100644 --- a/src/livecodes/styles/app.scss +++ b/src/livecodes/styles/app.scss @@ -1802,7 +1802,6 @@ a.editor-title { &.embed #toolbar .toolbar-render #light-theme-button, &.embed #toolbar .toolbar-render #dark-theme-button, #editors #editor-tools, - #console-input, #result-popup-btn, #broadcast-status-btn { display: none !important; @@ -1843,6 +1842,12 @@ a.editor-title { } } +.simple-mode { + #console-input { + display: none !important; + } +} + .console-mode { #toolbar { margin-bottom: 0; @@ -1856,12 +1861,13 @@ a.editor-title { display: none !important; } - a.tools-pane-title { + #tools-pane-bar { cursor: default; + pointer-events: none; } - .gutter.gutter-vertical { - cursor: default; + #tools-pane-buttons { + pointer-events: auto; } } diff --git a/src/livecodes/toolspane/tools.ts b/src/livecodes/toolspane/tools.ts index 639ede98af..47704d8944 100644 --- a/src/livecodes/toolspane/tools.ts +++ b/src/livecodes/toolspane/tools.ts @@ -224,7 +224,7 @@ export const createToolsPane = ( toolTitle.dataset.id = String(index); toolTitle.classList.add('tools-pane-title', tool.name); toolTitle.innerHTML = tool.title; - if (tool.name === 'console' && config.mode !== 'console') { + if (tool.name === 'console') { toolTitle.title = '(Ctrl/⌘ + Alt + C)'; } toolsPaneTitles.appendChild(toolTitle); @@ -235,7 +235,6 @@ export const createToolsPane = ( 'click', (event: any) => { event.preventDefault(); - if (config.mode === 'console') return; if (event.detail === 0 || event.detail === 1) { timer = setTimeout(() => { if (toolsSplit.getSizes()[0] > 90) { @@ -255,7 +254,6 @@ export const createToolsPane = ( 'dblclick', (event) => { event.preventDefault(); - if (config.mode === 'console') return; clearTimeout(timer); if (toolsSplit.getSizes()[0] < 10) { close(); @@ -270,7 +268,6 @@ export const createToolsPane = ( 'touchstart', (event) => { event.preventDefault(); - if (config.mode === 'console') return; if (toolsSplit.getSizes()[0] > 90) { open(index); } else if (!toolTitle.classList.contains('active')) { @@ -291,7 +288,7 @@ export const createToolsPane = ( 'resize', () => { const toolsPane = document.querySelector('#tools-pane') as HTMLElement; - if (!toolsPane || config.mode === 'console') return; + if (!toolsPane) return; if (toolsSplit.getSizes()[0] < 10) { result.style.height = '0'; toolsPane.style.height = `calc(100% - ${gutterSize}px)`; @@ -313,6 +310,14 @@ export const createToolsPane = ( buttons.id = 'tools-pane-buttons'; buttons.classList.add('tool-buttons'); toolsPaneBar.appendChild(buttons); + // prevent starting a drag when clicking the buttons + eventsManager.addEventListener(buttons, 'mousedown', (event) => event.stopPropagation(), false); + eventsManager.addEventListener( + buttons, + 'touchstart', + (event) => event.stopPropagation(), + false, + ); const closeButton = document.createElement('button'); closeButton.classList.add('console-close-button'); @@ -378,10 +383,6 @@ export const createToolsPane = ( }); } setActiveTool(activeToolId); - - if (config.mode === 'console') { - toolsSplit.destroy(/* preserveStyles= */ false, /* preserveGutters= */ true); - } }; const observeStyleChange = () => { From 7b8c66282cc141d5007fa0aadcccafdc6078afb2 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:52:38 +0000 Subject: [PATCH 3/4] feat(App): hide console input in console mode and document WASM use --- docs/docs/features/display-modes.mdx | 7 ++++--- src/livecodes/styles/app.scss | 7 +------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/docs/features/display-modes.mdx b/docs/docs/features/display-modes.mdx index 31c838607a..a2d52e8827 100644 --- a/docs/docs/features/display-modes.mdx +++ b/docs/docs/features/display-modes.mdx @@ -67,12 +67,13 @@ export const simpleConfig = { This mode is mainly useful for embedded playgrounds. It shows a single editor together with the [console](./console.mdx) maximized (filling the output area), with the result page collapsed. The [tools pane](./tools-pane.mdx) is fixed to show only the console; it cannot be resized or closed. -The console can still be used as a REPL, and the code can be run from the editor. +The console input is hidden, and the code can be run from the editor. +This mode is particularly useful for [WASM-based languages](../languages/index.mdx) (e.g. Python, Ruby, PHP, C, ...) whose output is mainly text-based (sent to the console). -Demo: JS with console +Demo: Python with console ## `lite` diff --git a/src/livecodes/styles/app.scss b/src/livecodes/styles/app.scss index 0a410e6f76..b898ad2b20 100644 --- a/src/livecodes/styles/app.scss +++ b/src/livecodes/styles/app.scss @@ -1802,6 +1802,7 @@ a.editor-title { &.embed #toolbar .toolbar-render #light-theme-button, &.embed #toolbar .toolbar-render #dark-theme-button, #editors #editor-tools, + #console-input, #result-popup-btn, #broadcast-status-btn { display: none !important; @@ -1842,12 +1843,6 @@ a.editor-title { } } -.simple-mode { - #console-input { - display: none !important; - } -} - .console-mode { #toolbar { margin-bottom: 0; From fc7a8e95f1c08529623853663aba35cffb1133a5 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:19:49 +0000 Subject: [PATCH 4/4] fix(languages): warn about deprecated languages only when actually used The OCaml spec is registered under its canonical name `ocaml`, which its own deprecation predicate matches. Because `getLanguageByAlias` logged the warning on every resolution, internal enumeration (language menus, compiler list, starter templates) triggered the OCaml deprecation warning at startup even when OCaml was not used. Add an opt-out to `getLanguageByAlias` and use it in the enumerating helpers (`languageIsEnabled`, `getStarterTemplates`). Genuine use (config, URL params, language selection) still warns. --- src/livecodes/config/build-config.ts | 2 +- .../languages/__tests__/languages.spec.ts | 34 +++++++++++- src/livecodes/languages/utils.ts | 53 ++++++++++++------- .../templates/get-starter-templates.ts | 6 ++- 4 files changed, 73 insertions(+), 22 deletions(-) diff --git a/src/livecodes/config/build-config.ts b/src/livecodes/config/build-config.ts index 77eb0fa0b6..71dbcd7038 100644 --- a/src/livecodes/config/build-config.ts +++ b/src/livecodes/config/build-config.ts @@ -224,7 +224,7 @@ export const loadParamConfig = (config: Config, params: UrlQueryParams): Partial paramsConfig.languages = params.languages .split(',') .map((lang) => lang.trim()) - .map(getLanguageByAlias) + .map((lang) => getLanguageByAlias(lang)) .filter(Boolean) as Language[]; } diff --git a/src/livecodes/languages/__tests__/languages.spec.ts b/src/livecodes/languages/__tests__/languages.spec.ts index 4896696f0c..99113f0023 100644 --- a/src/livecodes/languages/__tests__/languages.spec.ts +++ b/src/livecodes/languages/__tests__/languages.spec.ts @@ -1,4 +1,10 @@ -import { getLanguageByAlias, getLanguageEditorId, getLanguageExtension, mapLanguage } from '..'; +import { + getLanguageByAlias, + getLanguageEditorId, + getLanguageExtension, + languageIsEnabled, + mapLanguage, +} from '..'; describe('languages', () => { test('getLanguageByAlias', () => { @@ -35,4 +41,30 @@ describe('languages', () => { expect(mapLanguage('ruby')).toBe('ruby'); expect(mapLanguage('html')).toBe('html'); }); + + describe('deprecation warnings', () => { + let warn: jest.SpyInstance; + + beforeEach(() => { + (window as any).deps.loggedDeprecations = []; + warn = jest.spyOn(console, 'warn').mockImplementation(() => { + // + }); + }); + + afterEach(() => { + warn.mockRestore(); + }); + + test('warns when a deprecated language name is used', () => { + expect(getLanguageByAlias('ocaml')).toBe('ocaml'); + expect(warn).toHaveBeenCalled(); + }); + + test('does not warn while enumerating languages', () => { + languageIsEnabled('ocaml', { languages: undefined } as any); + languageIsEnabled('ocaml', { languages: ['html', 'css', 'javascript'] } as any); + expect(warn).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/livecodes/languages/utils.ts b/src/livecodes/languages/utils.ts index 684ebea1c2..1b830c6fe2 100644 --- a/src/livecodes/languages/utils.ts +++ b/src/livecodes/languages/utils.ts @@ -1,8 +1,32 @@ -import type { Compiler, Config, CustomSettings, Language, Processor } from '../models'; +import type { + Compiler, + Config, + CustomSettings, + Language, + LanguageSpecs, + Processor, +} from '../models'; import { getDocsUrl, getLanguageCustomSettings } from '../utils/utils'; import { highlightjsUrl } from '../vendors'; -export const getLanguageByAlias = (alias: string = ''): Language | undefined => { +const logDeprecation = (alias: string, language?: LanguageSpecs) => { + if (!globalThis.window?.deps) return; + window.deps.loggedDeprecations ??= []; + const deprecation = language?.deprecation?.(alias.toLowerCase() as Language); + if (!deprecation || window.deps.loggedDeprecations.includes(deprecation.old)) return; + // eslint-disable-next-line no-console + console.warn( + `Deprecation warning: The language name "${alias}" will use the compiler "${deprecation.new}" in the future.`, + `If you want to continue using the current compiler, use the language name "${deprecation.old}" instead.`, + language?.name ? `Docs: ${getDocsUrl()}languages/${language.name}` : '', + ); + window.deps.loggedDeprecations.push(deprecation.old); +}; + +export const getLanguageByAlias = ( + alias: string = '', + options: { warn?: boolean } = {}, +): Language | undefined => { if (!alias) return; const aliasLowerCase = alias?.toLowerCase(); const language = window.deps.languages.find( @@ -11,20 +35,10 @@ export const getLanguageByAlias = (alias: string = ''): Language | undefined => lang.title.toLowerCase() === aliasLowerCase || lang.extensions.map((ext) => ext.toLowerCase()).includes(aliasLowerCase), ); - if (globalThis.window?.deps) window.deps.loggedDeprecations ??= []; - const deprecation = language?.deprecation?.(aliasLowerCase as Language); - if ( - deprecation && - window.deps.loggedDeprecations && - !window.deps.loggedDeprecations.includes(deprecation.old) - ) { - // eslint-disable-next-line no-console - console.warn( - `Deprecation warning: The language name "${alias}" will use the compiler "${deprecation.new}" in the future.`, - `If you want to continue using the current compiler, use the language name "${deprecation.old}" instead.`, - language?.name ? `Docs: ${getDocsUrl()}languages/${language.name}` : '', - ); - window.deps.loggedDeprecations?.push(deprecation.old); + // only warn when the alias is actually used (e.g. from config or user selection), + // not when enumerating languages (menus, compiler list, starter templates) + if (options.warn !== false) { + logDeprecation(alias, language); } return language?.name; }; @@ -71,7 +85,7 @@ export const mapLanguage = ( 'html'; export const languageIsEnabled = (language: Language, config: Config) => { - const lang = getLanguageByAlias(language); + const lang = getLanguageByAlias(language, { warn: false }); if (!lang) return false; if (!config.languages) return true; if ( @@ -80,7 +94,10 @@ export const languageIsEnabled = (language: Language, config: Config) => { ) { return true; } - return config.languages?.map(getLanguageByAlias).filter(Boolean).includes(lang); + return config.languages + ?.map((alias) => getLanguageByAlias(alias, { warn: false })) + .filter(Boolean) + .includes(lang); }; export const processorIsEnabled = (processor: Processor, config: Config) => { diff --git a/src/livecodes/templates/get-starter-templates.ts b/src/livecodes/templates/get-starter-templates.ts index d1d1487548..8ee8fc79f7 100644 --- a/src/livecodes/templates/get-starter-templates.ts +++ b/src/livecodes/templates/get-starter-templates.ts @@ -28,7 +28,9 @@ const loadTemplates = async (baseUrl: string): Promise => export const getStarterTemplates = async (config: Config, baseUrl: string): Promise => (await loadTemplates(baseUrl)) .filter((template) => { - const enabledLanguages = config.languages?.map(getLanguageByAlias).filter(Boolean); + const enabledLanguages = config.languages + ?.map((language) => getLanguageByAlias(language, { warn: false })) + .filter(Boolean); if (!enabledLanguages) return true; if (template.name === 'blank') return true; @@ -38,7 +40,7 @@ export const getStarterTemplates = async (config: Config, baseUrl: string): Prom template.script?.language, ]; for (const language of templateLanguages) { - const lang = getLanguageByAlias(language); + const lang = getLanguageByAlias(language, { warn: false }); if (!lang || !enabledLanguages.includes(lang)) return false; } return true;