From 5622a5ac31f1ce65c352d35e86d2077cc14a68f4 Mon Sep 17 00:00:00 2001 From: PENEKhun Date: Thu, 9 Oct 2025 13:21:41 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=A6=AC?= =?UTF-8?q?=EB=89=B4=EC=96=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- itdoc-doc/package.json | 5 +- itdoc-doc/pnpm-lock.yaml | 246 ++------- itdoc-doc/src/components/Playground/index.tsx | 466 +++++++++++++----- .../components/Playground/styles.module.css | 424 ++++++++++------ pnpm-lock.yaml | 246 ++------- 5 files changed, 704 insertions(+), 683 deletions(-) diff --git a/itdoc-doc/package.json b/itdoc-doc/package.json index 9c8aa0a..1de2d50 100644 --- a/itdoc-doc/package.json +++ b/itdoc-doc/package.json @@ -20,18 +20,17 @@ ] }, "dependencies": { - "@codemirror/lang-javascript": "^6.2.4", - "@codemirror/theme-one-dark": "^6.1.3", "@docusaurus/core": "3.7.0", "@docusaurus/plugin-google-gtag": "^3.7.0", "@docusaurus/preset-classic": "3.7.0", "@mdx-js/react": "^3.0.0", - "@uiw/react-codemirror": "^4.25.2", + "@monaco-editor/react": "^4.6.0", "@webcontainer/api": "^1.6.1", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "clsx": "^2.0.0", "docusaurus": "^1.14.7", + "monaco-editor": "^0.52.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/itdoc-doc/pnpm-lock.yaml b/itdoc-doc/pnpm-lock.yaml index ee06865..db320d8 100644 --- a/itdoc-doc/pnpm-lock.yaml +++ b/itdoc-doc/pnpm-lock.yaml @@ -8,12 +8,6 @@ importers: .: dependencies: - '@codemirror/lang-javascript': - specifier: ^6.2.4 - version: 6.2.4 - '@codemirror/theme-one-dark': - specifier: ^6.1.3 - version: 6.1.3 '@docusaurus/core': specifier: 3.7.0 version: 3.7.0(@mdx-js/react@3.1.0(@types/react@19.1.0)(react@19.1.0))(acorn@8.14.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.6.3) @@ -26,9 +20,9 @@ importers: '@mdx-js/react': specifier: ^3.0.0 version: 3.1.0(@types/react@19.1.0)(react@19.1.0) - '@uiw/react-codemirror': - specifier: ^4.25.2 - version: 4.25.2(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.19.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.4)(codemirror@6.0.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@monaco-editor/react': + specifier: ^4.6.0 + version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@webcontainer/api': specifier: ^1.6.1 version: 1.6.1 @@ -44,6 +38,9 @@ importers: docusaurus: specifier: ^1.14.7 version: 1.14.7(typescript@5.6.3)(webpack@5.98.0) + monaco-editor: + specifier: ^0.52.0 + version: 0.52.2 prism-react-renderer: specifier: ^2.3.0 version: 2.4.1(react@19.1.0) @@ -744,33 +741,6 @@ packages: resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} engines: {node: '>=6.9.0'} - '@codemirror/autocomplete@6.19.0': - resolution: {integrity: sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==} - - '@codemirror/commands@6.9.0': - resolution: {integrity: sha512-454TVgjhO6cMufsyyGN70rGIfJxJEjcqjBG2x2Y03Y/+Fm99d3O/Kv1QDYWuG6hvxsgmjXmBuATikIIYvERX+w==} - - '@codemirror/lang-javascript@6.2.4': - resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} - - '@codemirror/language@6.11.3': - resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} - - '@codemirror/lint@6.9.0': - resolution: {integrity: sha512-wZxW+9XDytH3SKvS8cQzMyQCaaazH8XL1EMHleHe00wVzsv7NBQKVW2yzEHrRhmM7ZOhVdItPbvlRBvMp9ej7A==} - - '@codemirror/search@6.5.11': - resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} - - '@codemirror/state@6.5.2': - resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} - - '@codemirror/theme-one-dark@6.1.3': - resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} - - '@codemirror/view@6.38.4': - resolution: {integrity: sha512-hduz0suCcUSC/kM8Fq3A9iLwInJDl8fD1xLpTIk+5xkNm8z/FT7UsIa9sOXrkpChh+XXc18RzswE8QqELsVl+g==} - '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -1255,21 +1225,6 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} - '@lezer/common@1.2.3': - resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} - - '@lezer/highlight@1.2.1': - resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} - - '@lezer/javascript@1.5.4': - resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} - - '@lezer/lr@1.4.2': - resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} - - '@marijn/find-cluster-break@1.0.2': - resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} - '@mdx-js/mdx@3.1.0': resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} @@ -1279,6 +1234,16 @@ packages: '@types/react': '>=16' react: '>=16' + '@monaco-editor/loader@1.5.0': + resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@mrmlnc/readdir-enhanced@2.2.1': resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} engines: {node: '>=4'} @@ -1589,28 +1554,6 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@uiw/codemirror-extensions-basic-setup@4.25.2': - resolution: {integrity: sha512-s2fbpdXrSMWEc86moll/d007ZFhu6jzwNu5cWv/2o7egymvLeZO52LWkewgbr+BUCGWGPsoJVWeaejbsb/hLcw==} - peerDependencies: - '@codemirror/autocomplete': '>=6.0.0' - '@codemirror/commands': '>=6.0.0' - '@codemirror/language': '>=6.0.0' - '@codemirror/lint': '>=6.0.0' - '@codemirror/search': '>=6.0.0' - '@codemirror/state': '>=6.0.0' - '@codemirror/view': '>=6.0.0' - - '@uiw/react-codemirror@4.25.2': - resolution: {integrity: sha512-XP3R1xyE0CP6Q0iR0xf3ed+cJzJnfmbLelgJR6osVVtMStGGZP3pGQjjwDRYptmjGHfEELUyyBLdY25h0BQg7w==} - peerDependencies: - '@babel/runtime': '>=7.11.0' - '@codemirror/state': '>=6.0.0' - '@codemirror/theme-one-dark': '>=6.0.0' - '@codemirror/view': '>=6.0.0' - codemirror: '>=6.0.0' - react: '>=17.0.0' - react-dom: '>=17.0.0' - '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -2288,9 +2231,6 @@ packages: resolution: {integrity: sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==} engines: {node: '>= 4.0'} - codemirror@6.0.2: - resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} - coffee-script@1.12.7: resolution: {integrity: sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==} engines: {node: '>=0.8.0'} @@ -2483,9 +2423,6 @@ packages: typescript: optional: true - crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -4946,6 +4883,9 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + moo@0.5.2: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} @@ -6735,6 +6675,9 @@ packages: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + static-extend@0.1.2: resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} engines: {node: '>=0.10.0'} @@ -6852,9 +6795,6 @@ packages: strnum@1.1.2: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} - style-mod@4.1.2: - resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} - style-to-js@1.1.16: resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} @@ -7272,9 +7212,6 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - w3c-keyname@2.2.8: - resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - watchpack@2.4.2: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} @@ -8383,69 +8320,6 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@codemirror/autocomplete@6.19.0': - dependencies: - '@codemirror/language': 6.11.3 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - '@lezer/common': 1.2.3 - - '@codemirror/commands@6.9.0': - dependencies: - '@codemirror/language': 6.11.3 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - '@lezer/common': 1.2.3 - - '@codemirror/lang-javascript@6.2.4': - dependencies: - '@codemirror/autocomplete': 6.19.0 - '@codemirror/language': 6.11.3 - '@codemirror/lint': 6.9.0 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - '@lezer/common': 1.2.3 - '@lezer/javascript': 1.5.4 - - '@codemirror/language@6.11.3': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 - style-mod: 4.1.2 - - '@codemirror/lint@6.9.0': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - crelt: 1.0.6 - - '@codemirror/search@6.5.11': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - crelt: 1.0.6 - - '@codemirror/state@6.5.2': - dependencies: - '@marijn/find-cluster-break': 1.0.2 - - '@codemirror/theme-one-dark@6.1.3': - dependencies: - '@codemirror/language': 6.11.3 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - '@lezer/highlight': 1.2.1 - - '@codemirror/view@6.38.4': - dependencies: - '@codemirror/state': 6.5.2 - crelt: 1.0.6 - style-mod: 4.1.2 - w3c-keyname: 2.2.8 - '@colors/colors@1.5.0': optional: true @@ -9534,24 +9408,6 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} - '@lezer/common@1.2.3': {} - - '@lezer/highlight@1.2.1': - dependencies: - '@lezer/common': 1.2.3 - - '@lezer/javascript@1.5.4': - dependencies: - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 - - '@lezer/lr@1.4.2': - dependencies: - '@lezer/common': 1.2.3 - - '@marijn/find-cluster-break@1.0.2': {} - '@mdx-js/mdx@3.1.0(acorn@8.14.1)': dependencies: '@types/estree': 1.0.7 @@ -9588,6 +9444,17 @@ snapshots: '@types/react': 19.1.0 react: 19.1.0 + '@monaco-editor/loader@1.5.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@monaco-editor/loader': 1.5.0 + monaco-editor: 0.52.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + '@mrmlnc/readdir-enhanced@2.2.1': dependencies: call-me-maybe: 1.0.2 @@ -9939,33 +9806,6 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@uiw/codemirror-extensions-basic-setup@4.25.2(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.4)': - dependencies: - '@codemirror/autocomplete': 6.19.0 - '@codemirror/commands': 6.9.0 - '@codemirror/language': 6.11.3 - '@codemirror/lint': 6.9.0 - '@codemirror/search': 6.5.11 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - - '@uiw/react-codemirror@4.25.2(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.19.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.4)(codemirror@6.0.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@babel/runtime': 7.27.0 - '@codemirror/commands': 6.9.0 - '@codemirror/state': 6.5.2 - '@codemirror/theme-one-dark': 6.1.3 - '@codemirror/view': 6.38.4 - '@uiw/codemirror-extensions-basic-setup': 4.25.2(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.4) - codemirror: 6.0.2 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - transitivePeerDependencies: - - '@codemirror/autocomplete' - - '@codemirror/language' - - '@codemirror/lint' - - '@codemirror/search' - '@ungap/structured-clone@1.3.0': {} '@webassemblyjs/ast@1.14.1': @@ -10784,16 +10624,6 @@ snapshots: chalk: 2.4.2 q: 1.5.1 - codemirror@6.0.2: - dependencies: - '@codemirror/autocomplete': 6.19.0 - '@codemirror/commands': 6.9.0 - '@codemirror/language': 6.11.3 - '@codemirror/lint': 6.9.0 - '@codemirror/search': 6.5.11 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - coffee-script@1.12.7: {} collapse-white-space@2.1.0: {} @@ -10971,8 +10801,6 @@ snapshots: optionalDependencies: typescript: 5.6.3 - crelt@1.0.6: {} - cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -14152,6 +13980,8 @@ snapshots: dependencies: minimist: 1.2.8 + monaco-editor@0.52.2: {} + moo@0.5.2: {} mrmime@2.0.1: {} @@ -16267,6 +16097,8 @@ snapshots: stable@0.1.8: {} + state-local@1.0.7: {} + static-extend@0.1.2: dependencies: define-property: 0.2.5 @@ -16384,8 +16216,6 @@ snapshots: strnum@1.1.2: {} - style-mod@4.1.2: {} - style-to-js@1.1.16: dependencies: style-to-object: 1.0.8 @@ -16849,8 +16679,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - w3c-keyname@2.2.8: {} - watchpack@2.4.2: dependencies: glob-to-regexp: 0.4.1 diff --git a/itdoc-doc/src/components/Playground/index.tsx b/itdoc-doc/src/components/Playground/index.tsx index 5621f9b..09c1932 100644 --- a/itdoc-doc/src/components/Playground/index.tsx +++ b/itdoc-doc/src/components/Playground/index.tsx @@ -17,9 +17,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment" import useBaseUrl from "@docusaurus/useBaseUrl" -import CodeMirror from "@uiw/react-codemirror" -import { javascript } from "@codemirror/lang-javascript" -import { oneDark } from "@codemirror/theme-one-dark" +import Editor, { OnMount } from "@monaco-editor/react" import { Terminal } from "@xterm/xterm" import { FitAddon } from "@xterm/addon-fit" import "@xterm/xterm/css/xterm.css" @@ -177,6 +175,75 @@ const waitingTips = [ }, ] +type PlaygroundFileId = "app" | "test" | "package" + +interface PlaygroundFileDefinition { + id: PlaygroundFileId + label: string + path: string + description: string + language: "javascript" | "json" + monacoUri: string + editable: boolean +} + +interface ExplorerFileNode { + type: "file" + fileId: PlaygroundFileId + label: string + depth: number +} + +interface ExplorerFolderNode { + type: "folder" + label: string + depth: number + children: ExplorerFileNode[] +} + +type ExplorerNode = ExplorerFileNode | ExplorerFolderNode + +const PLAYGROUND_FILES: Record = { + app: { + id: "app", + label: "app.js", + path: "app.js", + description: "Express entry point", + language: "javascript", + monacoUri: "file:///app.js", + editable: true, + }, + test: { + id: "test", + label: "app.test.js", + path: "__tests__/app.test.js", + description: "Test code with itdoc", + language: "javascript", + monacoUri: "file:///__tests__/app.test.js", + editable: true, + }, + package: { + id: "package", + label: "package.json", + path: "package.json", + description: "Playground dependencies and scripts", + language: "json", + monacoUri: "file:///package.json", + editable: false, + }, +} + +const EXPLORER_NODES: ExplorerNode[] = [ + { type: "file", fileId: "app", label: "app.js", depth: 0 }, + { type: "file", fileId: "package", label: "package.json", depth: 0 }, + { + type: "folder", + label: "__tests__", + depth: 0, + children: [{ type: "file", fileId: "test", label: "app.test.js", depth: 1 }], + }, +] + function formatDuration(milliseconds: number): string { const bounded = Math.max(0, milliseconds) const totalSeconds = Math.floor(bounded / 1000) @@ -217,11 +284,11 @@ function createPackageJson(): string { return JSON.stringify(packageJson, null, 4) } -function createProjectFiles(code: string, tests: string): FileSystemTree { +function createProjectFiles(code: string, tests: string, packageJson: string): FileSystemTree { return { "package.json": { file: { - contents: createPackageJson(), + contents: packageJson, }, }, "app.js": { @@ -347,9 +414,12 @@ const Playground: React.FC = ({ onRequestHelp }) => { const [elapsedInstallMs, setElapsedInstallMs] = useState(0) const [showSwaggerPreview, setShowSwaggerPreview] = useState(false) const [showRunModal, setShowRunModal] = useState(false) + const [openFiles, setOpenFiles] = useState(["app"]) + const [activeFileId, setActiveFileId] = useState("app") const initialCodeRef = useRef(initialExpressCode) const initialTestCodeRef = useRef(initialTestCode) + const packageJsonRef = useRef(createPackageJson()) const webcontainerRef = useRef(null) const runningProcessRef = useRef(null) const terminalHostRef = useRef(null) @@ -358,10 +428,9 @@ const Playground: React.FC = ({ onRequestHelp }) => { const pendingTerminalChunksRef = useRef([]) const redocContainerRef = useRef(null) const redocScriptReadyRef = useRef | null>(null) + const didAutofocusEditorRef = useRef(false) const textDecoder = useMemo(() => new TextDecoder(), []) - const codeMirrorExtensions = useMemo(() => [javascript({ typescript: false, jsx: false })], []) - const jsonCodeMirrorExtensions = useMemo(() => [javascript({ json: true })], []) const appendTerminalOutput = useCallback((chunk: string) => { if (terminalRef.current) { terminalRef.current.write(chunk) @@ -384,6 +453,72 @@ const Playground: React.FC = ({ onRequestHelp }) => { } }, []) + const activeFile = PLAYGROUND_FILES[activeFileId] + const activeFileValue = useMemo(() => { + switch (activeFileId) { + case "app": + return expressCode + case "test": + return testCode + case "package": + default: + return packageJsonRef.current + } + }, [activeFileId, expressCode, testCode]) + + const handleSelectFile = useCallback((fileId: PlaygroundFileId) => { + setActiveFileId(fileId) + setOpenFiles((files) => (files.includes(fileId) ? files : [...files, fileId])) + }, []) + + const handleCloseTab = useCallback( + (fileId: PlaygroundFileId) => { + setOpenFiles((files) => { + if (files.length <= 1 || !files.includes(fileId)) { + return files + } + + const closingIndex = files.indexOf(fileId) + const nextFiles = files.filter((id) => id !== fileId) + + if (fileId === activeFileId) { + const fallbackIndex = closingIndex > 0 ? closingIndex - 1 : 0 + const fallbackFile = nextFiles[fallbackIndex] ?? nextFiles[0] + if (fallbackFile) { + setActiveFileId(fallbackFile) + } + } + + return nextFiles + }) + }, + [activeFileId], + ) + + const handleEditorChange = useCallback( + (value: string | undefined) => { + if (!activeFile || !activeFile.editable) { + return + } + + const nextValue = value ?? "" + + if (activeFile.id === "app") { + setExpressCode(nextValue) + } else if (activeFile.id === "test") { + setTestCode(nextValue) + } + }, + [activeFile], + ) + + const handleEditorMount = useCallback((editor) => { + if (!didAutofocusEditorRef.current) { + editor.focus() + didAutofocusEditorRef.current = true + } + }, []) + const ensureRedocScript = useCallback((): Promise => { if (!ExecutionEnvironment.canUseDOM) { return Promise.resolve() @@ -529,7 +664,11 @@ const Playground: React.FC = ({ onRequestHelp }) => { webcontainerRef.current = instance await instance.mount( - createProjectFiles(initialCodeRef.current, initialTestCodeRef.current), + createProjectFiles( + initialCodeRef.current, + initialTestCodeRef.current, + packageJsonRef.current, + ), ) if (cancelled) { return @@ -862,15 +1001,20 @@ const Playground: React.FC = ({ onRequestHelp }) => { return (
-
-
- -
-

itdoc playground

-

Express · Mocha · WebContainer

-
+
+
+

itdoc playground

+ {activeFile ? ( +
+ Editing + {activeFile.path} + + {activeFile.description} + +
+ ) : null}
-
+
{statusLabel} @@ -899,101 +1043,191 @@ const Playground: React.FC = ({ onRequestHelp }) => { {errorMessage}
) : null} -
-
- {showWorkspace ? ( - <> -
-
-
- app.js - - Express entry point - -
+
+ {showWorkspace ? ( +
+
-
-
-
- - __tests__/app.test.js - - - Mocha + itdoc suite - -
-
-
-
- setTestCode(value)} - className={styles.codeEditor} - /> -
-

- Tip: Write an Itdoc DSL to test the implemented Express API - before running the tests. -

-
-
- - ) : ( -
-

Booting your sandbox…

-

- WebContainer is preparing the environment. Dependencies install - automatically the first time the playground loads. -

- {waitingTip ? ( - - ) : null} -
- )} -
+ ) + })} + + +
+
+ {openFiles.map((fileId) => { + const file = PLAYGROUND_FILES[fileId] + const isActive = activeFileId === fileId + const tabId = `editor-tab-${file.id}` + const panelId = `editor-panel-${file.id}` + return ( +
+ + +
+ ) + })} +
+
+ {activeFile ? ( + + ) : null} +
+
+

+ Tip: Keep the Express app and itdoc tests aligned before you run + the suite. +

+
+
+
+ ) : ( +
+

Booting your sandbox…

+

+ WebContainer is preparing the environment. Dependencies install + automatically the first time the playground loads. +

+ {waitingTip ? ( + + ) : null} +
+ )} {showRunModal ? (
= ({ onRequestHelp }) => {
oas.json
{oasOutput ? ( - ) : (

diff --git a/itdoc-doc/src/components/Playground/styles.module.css b/itdoc-doc/src/components/Playground/styles.module.css index e484cbb..01088ed 100644 --- a/itdoc-doc/src/components/Playground/styles.module.css +++ b/itdoc-doc/src/components/Playground/styles.module.css @@ -19,10 +19,10 @@ display: flex; flex-direction: column; min-height: calc(100vh - 6rem); - background-color: var(--ifm-color-emphasis-0, #f8fafc); - border: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(15, 23, 42, 0.92); + border: 1px solid rgba(148, 163, 184, 0.18); border-radius: 1rem; - box-shadow: 0 28px 60px rgba(15, 23, 42, 0.12); + box-shadow: 0 28px 60px rgba(15, 23, 42, 0.45); overflow: hidden; } @@ -32,53 +32,69 @@ color: var(--ifm-color-content-secondary, #64748b); } -.topBar { +.commandBar { display: flex; align-items: center; justify-content: space-between; gap: var(--ifm-spacing-lg, 1.5rem); - padding: 1.1rem clamp(1.2rem, 4vw, 2rem); - background: linear-gradient(135deg, #0f172a, #1e293b); + padding: 0.85rem clamp(1.2rem, 4vw, 2rem); + background: linear-gradient(135deg, #0b1220, #111827); color: #e2e8f0; - border-bottom: 1px solid rgba(148, 163, 184, 0.25); -} - -.brandGroup { - display: flex; - align-items: center; - gap: var(--ifm-spacing-md, 1rem); -} - -.brandMark { - width: 0.75rem; - height: 0.75rem; - border-radius: 50%; - background: var(--ifm-color-primary, #6366f1); - box-shadow: 0 0 18px rgba(99, 102, 241, 0.65); + border-bottom: 1px solid rgba(148, 163, 184, 0.18); } -.brandText { +.commandContext { display: flex; flex-direction: column; gap: 0.2rem; + min-width: 0; } -.brandTitle { +.commandTitle { margin: 0; font-size: 1.05rem; font-weight: 600; - letter-spacing: 0.02em; + letter-spacing: 0.015em; } -.brandSubtitle { - margin: 0; - font-size: 0.78rem; - color: rgba(226, 232, 240, 0.7); - letter-spacing: 0.08em; +.commandMeta { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.35rem 0.6rem; + color: rgba(226, 232, 240, 0.78); + font-size: 0.85rem; + overflow: hidden; +} + +.commandMetaLabel { text-transform: uppercase; + font-size: 0.72rem; + letter-spacing: 0.12em; + color: rgba(148, 163, 184, 0.85); +} + +.commandPath { + font-family: var(--ifm-font-family-monospace, "Fira Code", Menlo, monospace); + padding: 0.2rem 0.5rem; + border-radius: 0.35rem; + background: rgba(30, 64, 175, 0.16); + color: #bfdbfe; + max-width: clamp(140px, 28vw, 320px); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.topActions { +.commandDescription { + color: rgba(226, 232, 240, 0.75); + max-width: clamp(180px, 36vw, 340px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.commandActions { display: flex; align-items: center; gap: var(--ifm-spacing-md, 1rem); @@ -111,9 +127,9 @@ justify-content: center; padding: 0.45rem 1.15rem; border-radius: 999px; - border: 1px solid rgba(50, 122, 224, 0.55); - background: rgba(247, 248, 250, 0.71); - color: rgba(58, 53, 53, 0.92); + border: 1px solid rgba(34, 197, 94, 0.9); + background: linear-gradient(135deg, rgba(34, 197, 94, 0.95), rgba(22, 163, 74, 0.95)); + color: #052e16; font-weight: 600; font-size: 0.75rem; letter-spacing: 0.08em; @@ -126,8 +142,8 @@ } .runButton:hover { - background: rgba(226, 232, 240, 0.24); - color: #ffffff; + background: linear-gradient(135deg, rgba(34, 197, 94, 1), rgba(21, 128, 61, 1)); + color: #ecfdf5; transform: translateY(-1px); } @@ -211,10 +227,10 @@ } .inlineAlert { - margin: 1rem clamp(1.2rem, 4vw, 2rem) 0; + margin: 1rem clamp(1.2rem, 4vw, 2rem); padding: 0.85rem 1.1rem; border-radius: 0.75rem; - background: rgba(254, 202, 202, 0.35); + background: rgb(254, 202, 202); border: 1px solid rgba(248, 113, 113, 0.45); color: #b91c1c; font-size: 0.9rem; @@ -224,28 +240,24 @@ margin-right: 0.35rem; } -.mainArea { +.workspaceMain { flex: 1; display: flex; flex-direction: column; - background: linear-gradient( - 180deg, - rgba(248, 250, 252, 0.94) 0%, - rgba(226, 232, 240, 0.92) 100% - ); + background: linear-gradient(180deg, rgba(10, 16, 32, 0.92), rgba(15, 23, 42, 0.94)); } -.workspace { +.workspaceInner { flex: 1; min-height: 0; - display: grid; + display: flex; gap: clamp(1rem, 3vw, 1.75rem); - grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); padding: clamp(1rem, 4vw, 2rem); } .workspacePlaceholder { - grid-column: span 12; + flex: 1; + min-height: 0; display: flex; flex-direction: column; align-items: center; @@ -253,14 +265,14 @@ gap: 0.75rem; padding: clamp(2.5rem, 6vw, 4rem); text-align: center; - color: var(--ifm-color-content-secondary, #64748b); + color: rgba(226, 232, 240, 0.75); } .placeholderTitle { margin: 0; font-size: 1.25rem; font-weight: 600; - color: var(--ifm-color-emphasis-800, #1f2937); + color: #f1f5f9; } .placeholderCopy { @@ -268,52 +280,221 @@ max-width: 28rem; } -.column { - grid-column: span 12; +.explorer { + width: clamp(200px, 23vw, 260px); + background: rgba(15, 23, 42, 0.8); + border: 1px solid rgba(148, 163, 184, 0.15); + border-radius: 0.85rem; display: flex; flex-direction: column; - background: var(--ifm-color-white, #ffffff); - border-radius: 0.9rem; - border: 1px solid rgba(15, 23, 42, 0.08); - box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08); - min-height: 0; + padding: 0.75rem; + gap: 0.5rem; + color: rgba(226, 232, 240, 0.86); +} + +.explorerHeader { + font-size: 0.75rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: rgba(148, 163, 184, 0.72); + padding: 0 0.5rem; } -.columnHeader { +.tree { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.treeRoot { + font-family: var(--ifm-font-family-monospace, Menlo, Monaco, "Courier New", monospace); + font-weight: 600; + color: rgba(148, 163, 184, 0.8); + padding: 0.25rem 0.5rem; +} + +.treeFolder { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.treeFolderLabel { display: flex; align-items: center; - justify-content: space-between; - gap: var(--ifm-spacing-md, 1rem); - padding: 1rem 1.25rem; - border-bottom: 1px solid rgba(15, 23, 42, 0.06); - background: rgba(248, 250, 252, 0.9); + gap: 0.4rem; + font-weight: 600; + color: rgba(191, 219, 254, 0.9); + padding: 0.25rem 0.5rem; } -.columnHeading { +.treeChildren { display: flex; flex-direction: column; gap: 0.2rem; + padding-left: 1.5rem; } -.columnTitle { - margin: 0; - font-weight: 600; - font-size: 0.95rem; - color: var(--ifm-color-emphasis-800, #1f2937); +.treeItem { + display: flex; + align-items: center; + gap: 0.45rem; + padding: 0.25rem 0.5rem; + border-radius: 0.5rem; + background: transparent; + border: none; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; + transition: + background-color 0.2s ease, + color 0.2s ease; } -.columnMeta { - margin: 0; - font-size: 0.75rem; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--ifm-color-content-secondary, #64748b); +.treeItem:hover { + background: rgba(59, 130, 246, 0.14); +} + +.treeItemActive { + background: rgba(59, 130, 246, 0.2); + color: #93c5fd; +} + +.treeItemOpen .treeLabel::after { + content: ""; + display: inline-block; + width: 6px; + height: 6px; + margin-left: 0.45rem; + border-radius: 50%; + background: rgba(59, 130, 246, 0.9); +} + +.treeIcon { + width: 0.75rem; + height: 0.75rem; + display: inline-flex; + align-items: center; + justify-content: center; + color: rgba(148, 163, 184, 0.9); +} + +.treeIconFolder::before { + content: ""; + display: inline-block; + width: 0.85rem; + height: 0.55rem; + border-radius: 0.1rem; + border: 1px solid rgba(96, 165, 250, 0.75); + background: rgba(59, 130, 246, 0.2); } -.columnActions { +.treeIconFile::before { + content: ""; + display: inline-block; + width: 0.7rem; + height: 0.6rem; + border-radius: 0.08rem; + border: 1px solid rgba(148, 163, 184, 0.8); + background: rgba(148, 163, 184, 0.2); +} + +.treeLabel { + white-space: nowrap; +} + +.editorPane { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + background: rgba(15, 23, 42, 0.78); + border: 1px solid rgba(148, 163, 184, 0.18); + border-radius: 0.9rem; + overflow: hidden; +} + +.tabBar { + display: flex; + align-items: stretch; + gap: 0.1rem; + background: rgba(17, 24, 39, 0.92); + padding: 0.25rem 0.35rem 0.2rem; +} + +.tabItem { + display: inline-flex; + align-items: stretch; + border-radius: 0.55rem 0.55rem 0 0; + overflow: hidden; + background: transparent; + border: 1px solid transparent; + transition: + background-color 0.2s ease, + border-color 0.2s ease; +} + +.tabItemActive { + background: rgba(30, 64, 175, 0.24); + border-color: rgba(96, 165, 250, 0.35); +} + +.tabButton { + padding: 0.45rem 0.85rem; + background: transparent; + border: none; + color: rgba(226, 232, 240, 0.85); + font: inherit; + cursor: pointer; +} + +.tabButton:hover { + color: #bfdbfe; +} + +.tabClose { + padding: 0 0.55rem; + border: none; + background: transparent; + color: rgba(226, 232, 240, 0.55); + cursor: pointer; + font-size: 0.9rem; +} + +.tabClose:hover:not(:disabled) { + color: #fca5a5; +} + +.tabClose:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.editorSurface { + flex: 1; + min-height: 0; + display: flex; + background: #0f172a; +} + +.editorSurface :global(.monaco-editor) { + width: 100%; + height: 100%; +} + +.editorSurface :global(.monaco-editor .overflow-guard) { + height: 100%; +} + +.editorFooter { display: flex; align-items: center; - gap: 0.75rem; + justify-content: space-between; + gap: 1rem; + padding: 0.65rem 0.9rem; + background: rgba(15, 23, 42, 0.9); + border-top: 1px solid rgba(148, 163, 184, 0.18); } .previewLaunchButton { @@ -487,48 +668,10 @@ gap: 1rem; } -.columnBody { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - gap: var(--ifm-spacing-md, 1rem); - padding: 1.25rem 1.25rem 1.5rem; -} - -.editorWrapper { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - gap: var(--ifm-spacing-sm, 0.5rem); -} - -.codeEditor :global(.cm-editor) { - border-radius: 0.75rem; - border: 1px solid var(--ifm-color-emphasis-300, #d1d5db); - background-color: #1e1e1e; - box-shadow: 0 4px 18px rgba(8, 15, 45, 0.25); - height: 100%; - min-height: 0; -} - -.codeEditor :global(.cm-scroller) { - font-family: var(--ifm-font-family-monospace, Menlo, Monaco, 'Courier New', monospace); - font-size: 0.95rem; - line-height: 1.5; - min-height: 0; -} - -.codeEditor :global(.cm-gutters) { - background-color: transparent; - border-right: 1px solid rgba(255, 255, 255, 0.08); -} - .hint { margin: 0; font-size: 0.9rem; - color: var(--ifm-color-content-secondary, #64748b); + color: rgba(203, 213, 225, 0.72); } .oasWorkspace { @@ -547,7 +690,7 @@ display: flex; flex-direction: column; padding: var(--ifm-spacing-md, 1rem); - background-color: rgba(255, 255, 255, 0.9); + background: rgba(15, 23, 42, 0.92); border-radius: 0 0 0.75rem 0.75rem; gap: var(--ifm-spacing-md, 1rem); min-height: 0; @@ -555,14 +698,13 @@ overflow: auto; } -.oasEditorSurface :global(.cm-editor) { - height: 100%; - max-height: 100%; +.oasEditorSurface :global(.monaco-editor) { border-radius: 0.65rem; + height: 100%; } -.oasEditorSurface :global(.cm-scroller) { - min-height: 0; +.oasEditorSurface :global(.monaco-editor .overflow-guard) { + height: 100%; } .oasPreviewContainer { @@ -570,7 +712,7 @@ display: flex; flex-direction: column; padding: var(--ifm-spacing-md, 1rem); - background-color: rgba(255, 255, 255, 0.9); + background: rgba(15, 23, 42, 0.92); border-radius: 0 0 0.75rem 0.75rem; min-height: 0; max-height: 420px; @@ -583,8 +725,8 @@ } .codeSurface { - background: linear-gradient(135deg, rgba(241, 245, 249, 0.9), rgba(226, 232, 240, 0.9)); - border: 1px solid rgba(148, 163, 184, 0.35); + background: linear-gradient(135deg, rgba(30, 41, 59, 0.92), rgba(15, 23, 42, 0.92)); + border: 1px solid rgba(148, 163, 184, 0.28); border-radius: 0.75rem; display: flex; flex-direction: column; @@ -593,19 +735,19 @@ } .codeChrome { - font-family: var(--ifm-font-family-monospace, Menlo, Monaco, 'Courier New', monospace); + font-family: var(--ifm-font-family-monospace, Menlo, Monaco, "Courier New", monospace); font-size: 0.85rem; letter-spacing: 0.02em; padding: 0.6rem var(--ifm-spacing-md, 1rem); - color: var(--ifm-color-emphasis-700, #334155); - background: rgba(148, 163, 184, 0.25); - border-bottom: 1px solid rgba(148, 163, 184, 0.4); + color: #bfdbfe; + background: rgba(30, 41, 59, 0.85); + border-bottom: 1px solid rgba(96, 165, 250, 0.35); } .oasEmpty { margin: auto; text-align: center; - color: var(--ifm-color-content-secondary, #64748b); + color: rgba(148, 163, 184, 0.84); max-width: 18rem; line-height: 1.5; } @@ -1113,27 +1255,15 @@ padding: 0 1.5rem 1.5rem; } - .workspace { - grid-template-columns: repeat(1, minmax(0, 1fr)); - } -} - -@media (min-width: 900px) { - .codeColumn { - grid-column: span 6; + .workspaceInner { + flex-direction: column; } -} -@media (min-width: 996px) { - .oasEditorCard, - .oasPreviewCard { - grid-column: span 6; + .explorer { + width: 100%; } -} -@media (min-width: 1200px) { - .codeColumn, - .previewColumn { - grid-column: span 4; + .editorPane { + min-height: 360px; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b68c62c..4b857a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,12 +343,6 @@ importers: itdoc-doc: dependencies: - '@codemirror/lang-javascript': - specifier: ^6.2.4 - version: 6.2.4 - '@codemirror/theme-one-dark': - specifier: ^6.1.3 - version: 6.1.3 '@docusaurus/core': specifier: 3.7.0 version: 3.7.0(@mdx-js/react@3.1.0(@types/react@19.1.0)(react@19.0.0))(@swc/core@1.11.29)(acorn@8.14.1)(esbuild@0.25.0)(eslint@9.17.0(jiti@1.21.7)(supports-color@10.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(supports-color@10.0.0)(typescript@5.6.3) @@ -361,9 +355,9 @@ importers: '@mdx-js/react': specifier: ^3.0.0 version: 3.1.0(@types/react@19.1.0)(react@19.0.0) - '@uiw/react-codemirror': - specifier: ^4.25.2 - version: 4.25.2(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.19.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.4)(codemirror@6.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@monaco-editor/react': + specifier: ^4.6.0 + version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@webcontainer/api': specifier: ^1.6.1 version: 1.6.1 @@ -379,6 +373,9 @@ importers: docusaurus: specifier: ^1.14.7 version: 1.14.7(eslint@9.17.0(jiti@1.21.7)(supports-color@10.0.0))(supports-color@10.0.0)(typescript@5.6.3)(webpack@5.99.6(@swc/core@1.11.29)(esbuild@0.25.0)) + monaco-editor: + specifier: ^0.52.0 + version: 0.52.2 prism-react-renderer: specifier: ^2.3.0 version: 2.4.1(react@19.0.0) @@ -1285,33 +1282,6 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@codemirror/autocomplete@6.19.0': - resolution: {integrity: sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==} - - '@codemirror/commands@6.9.0': - resolution: {integrity: sha512-454TVgjhO6cMufsyyGN70rGIfJxJEjcqjBG2x2Y03Y/+Fm99d3O/Kv1QDYWuG6hvxsgmjXmBuATikIIYvERX+w==} - - '@codemirror/lang-javascript@6.2.4': - resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} - - '@codemirror/language@6.11.3': - resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==} - - '@codemirror/lint@6.9.0': - resolution: {integrity: sha512-wZxW+9XDytH3SKvS8cQzMyQCaaazH8XL1EMHleHe00wVzsv7NBQKVW2yzEHrRhmM7ZOhVdItPbvlRBvMp9ej7A==} - - '@codemirror/search@6.5.11': - resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} - - '@codemirror/state@6.5.2': - resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} - - '@codemirror/theme-one-dark@6.1.3': - resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} - - '@codemirror/view@6.38.4': - resolution: {integrity: sha512-hduz0suCcUSC/kM8Fq3A9iLwInJDl8fD1xLpTIk+5xkNm8z/FT7UsIa9sOXrkpChh+XXc18RzswE8QqELsVl+g==} - '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -2379,25 +2349,10 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} - '@lezer/common@1.2.3': - resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} - - '@lezer/highlight@1.2.1': - resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} - - '@lezer/javascript@1.5.4': - resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} - - '@lezer/lr@1.4.2': - resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} - '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} - '@marijn/find-cluster-break@1.0.2': - resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} - '@mdx-js/mdx@3.1.0': resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} @@ -2407,6 +2362,16 @@ packages: '@types/react': '>=16' react: '>=16' + '@monaco-editor/loader@1.5.0': + resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@mrmlnc/readdir-enhanced@2.2.1': resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} engines: {node: '>=4'} @@ -3453,28 +3418,6 @@ packages: resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@uiw/codemirror-extensions-basic-setup@4.25.2': - resolution: {integrity: sha512-s2fbpdXrSMWEc86moll/d007ZFhu6jzwNu5cWv/2o7egymvLeZO52LWkewgbr+BUCGWGPsoJVWeaejbsb/hLcw==} - peerDependencies: - '@codemirror/autocomplete': '>=6.0.0' - '@codemirror/commands': '>=6.0.0' - '@codemirror/language': '>=6.0.0' - '@codemirror/lint': '>=6.0.0' - '@codemirror/search': '>=6.0.0' - '@codemirror/state': '>=6.0.0' - '@codemirror/view': '>=6.0.0' - - '@uiw/react-codemirror@4.25.2': - resolution: {integrity: sha512-XP3R1xyE0CP6Q0iR0xf3ed+cJzJnfmbLelgJR6osVVtMStGGZP3pGQjjwDRYptmjGHfEELUyyBLdY25h0BQg7w==} - peerDependencies: - '@babel/runtime': '>=7.11.0' - '@codemirror/state': '>=6.0.0' - '@codemirror/theme-one-dark': '>=6.0.0' - '@codemirror/view': '>=6.0.0' - codemirror: '>=6.0.0' - react: '>=17.0.0' - react-dom: '>=17.0.0' - '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -4587,9 +4530,6 @@ packages: resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} engines: {node: '>=0.10.0'} - codemirror@6.0.2: - resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} - coffee-script@1.12.7: resolution: {integrity: sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==} engines: {node: '>=0.8.0'} @@ -4846,9 +4786,6 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -8516,6 +8453,9 @@ packages: engines: {node: '>=18'} hasBin: true + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + moo@0.5.2: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} @@ -10861,6 +10801,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + static-extend@0.1.2: resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} engines: {node: '>=0.10.0'} @@ -11038,9 +10981,6 @@ packages: resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==} engines: {node: '>=16'} - style-mod@4.1.2: - resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} - style-to-js@1.1.16: resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} @@ -11785,9 +11725,6 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - w3c-keyname@2.2.8: - resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -13374,69 +13311,6 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@codemirror/autocomplete@6.19.0': - dependencies: - '@codemirror/language': 6.11.3 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - '@lezer/common': 1.2.3 - - '@codemirror/commands@6.9.0': - dependencies: - '@codemirror/language': 6.11.3 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - '@lezer/common': 1.2.3 - - '@codemirror/lang-javascript@6.2.4': - dependencies: - '@codemirror/autocomplete': 6.19.0 - '@codemirror/language': 6.11.3 - '@codemirror/lint': 6.9.0 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - '@lezer/common': 1.2.3 - '@lezer/javascript': 1.5.4 - - '@codemirror/language@6.11.3': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 - style-mod: 4.1.2 - - '@codemirror/lint@6.9.0': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - crelt: 1.0.6 - - '@codemirror/search@6.5.11': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - crelt: 1.0.6 - - '@codemirror/state@6.5.2': - dependencies: - '@marijn/find-cluster-break': 1.0.2 - - '@codemirror/theme-one-dark@6.1.3': - dependencies: - '@codemirror/language': 6.11.3 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - '@lezer/highlight': 1.2.1 - - '@codemirror/view@6.38.4': - dependencies: - '@codemirror/state': 6.5.2 - crelt: 1.0.6 - style-mod: 4.1.2 - w3c-keyname: 2.2.8 - '@colors/colors@1.5.0': optional: true @@ -15272,26 +15146,8 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} - '@lezer/common@1.2.3': {} - - '@lezer/highlight@1.2.1': - dependencies: - '@lezer/common': 1.2.3 - - '@lezer/javascript@1.5.4': - dependencies: - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 - - '@lezer/lr@1.4.2': - dependencies: - '@lezer/common': 1.2.3 - '@lukeed/csprng@1.1.0': {} - '@marijn/find-cluster-break@1.0.2': {} - '@mdx-js/mdx@3.1.0(acorn@8.14.1)(supports-color@10.0.0)': dependencies: '@types/estree': 1.0.6 @@ -15328,6 +15184,17 @@ snapshots: '@types/react': 19.1.0 react: 19.0.0 + '@monaco-editor/loader@1.5.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@monaco-editor/loader': 1.5.0 + monaco-editor: 0.52.2 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@mrmlnc/readdir-enhanced@2.2.1': dependencies: call-me-maybe: 1.0.2 @@ -16522,33 +16389,6 @@ snapshots: '@typescript-eslint/types': 8.32.1 eslint-visitor-keys: 4.2.0 - '@uiw/codemirror-extensions-basic-setup@4.25.2(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.4)': - dependencies: - '@codemirror/autocomplete': 6.19.0 - '@codemirror/commands': 6.9.0 - '@codemirror/language': 6.11.3 - '@codemirror/lint': 6.9.0 - '@codemirror/search': 6.5.11 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - - '@uiw/react-codemirror@4.25.2(@babel/runtime@7.27.0)(@codemirror/autocomplete@6.19.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.4)(codemirror@6.0.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': - dependencies: - '@babel/runtime': 7.27.0 - '@codemirror/commands': 6.9.0 - '@codemirror/state': 6.5.2 - '@codemirror/theme-one-dark': 6.1.3 - '@codemirror/view': 6.38.4 - '@uiw/codemirror-extensions-basic-setup': 4.25.2(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.4) - codemirror: 6.0.2 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - transitivePeerDependencies: - - '@codemirror/autocomplete' - - '@codemirror/language' - - '@codemirror/lint' - - '@codemirror/search' - '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -17898,16 +17738,6 @@ snapshots: code-point-at@1.1.0: {} - codemirror@6.0.2: - dependencies: - '@codemirror/autocomplete': 6.19.0 - '@codemirror/commands': 6.9.0 - '@codemirror/language': 6.11.3 - '@codemirror/lint': 6.9.0 - '@codemirror/search': 6.5.11 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.4 - coffee-script@1.12.7: {} collapse-white-space@2.1.0: {} @@ -18172,8 +18002,6 @@ snapshots: create-require@1.1.1: {} - crelt@1.0.6: {} - cross-env@7.0.3: dependencies: cross-spawn: 7.0.6 @@ -23237,6 +23065,8 @@ snapshots: requirejs: 2.3.7 requirejs-config-file: 4.0.0 + monaco-editor@0.52.2: {} + moo@0.5.2: {} mrmime@2.0.1: {} @@ -26018,6 +25848,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + state-local@1.0.7: {} + static-extend@0.1.2: dependencies: define-property: 0.2.5 @@ -26199,8 +26031,6 @@ snapshots: '@tokenizer/token': 0.3.0 peek-readable: 5.4.2 - style-mod@4.1.2: {} - style-to-js@1.1.16: dependencies: style-to-object: 1.0.8 @@ -27099,8 +26929,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - w3c-keyname@2.2.8: {} - walker@1.0.8: dependencies: makeerror: 1.0.12 From e458b1c3d60dc948032abc3f2085d81c17ca7a9c Mon Sep 17 00:00:00 2001 From: PENEKhun Date: Thu, 9 Oct 2025 14:02:12 +0900 Subject: [PATCH 2/9] =?UTF-8?q?refactor:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Playground/EditorTabs.tsx | 128 +++ .../components/Playground/FileExplorer.tsx | 107 +++ .../components/Playground/InstallOverlay.tsx | 156 ++++ .../src/components/Playground/RunModal.tsx | 135 ++++ .../components/Playground/SwaggerPreview.tsx | 70 ++ .../src/components/Playground/TopBar.tsx | 79 ++ .../Playground/WorkspacePlaceholder.tsx | 61 ++ .../src/components/Playground/constants.ts | 198 +++++ itdoc-doc/src/components/Playground/index.tsx | 755 ++---------------- itdoc-doc/src/components/Playground/types.ts | 45 ++ itdoc-doc/src/pages/playground.tsx | 120 ++- 11 files changed, 1119 insertions(+), 735 deletions(-) create mode 100644 itdoc-doc/src/components/Playground/EditorTabs.tsx create mode 100644 itdoc-doc/src/components/Playground/FileExplorer.tsx create mode 100644 itdoc-doc/src/components/Playground/InstallOverlay.tsx create mode 100644 itdoc-doc/src/components/Playground/RunModal.tsx create mode 100644 itdoc-doc/src/components/Playground/SwaggerPreview.tsx create mode 100644 itdoc-doc/src/components/Playground/TopBar.tsx create mode 100644 itdoc-doc/src/components/Playground/WorkspacePlaceholder.tsx create mode 100644 itdoc-doc/src/components/Playground/constants.ts create mode 100644 itdoc-doc/src/components/Playground/types.ts diff --git a/itdoc-doc/src/components/Playground/EditorTabs.tsx b/itdoc-doc/src/components/Playground/EditorTabs.tsx new file mode 100644 index 0000000..f620f58 --- /dev/null +++ b/itdoc-doc/src/components/Playground/EditorTabs.tsx @@ -0,0 +1,128 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useMemo } from "react" +import Editor, { OnMount } from "@monaco-editor/react" + +import styles from "./styles.module.css" +import type { PlaygroundFileId, PlaygroundFileMap } from "./types" + +interface EditorTabsProps { + files: PlaygroundFileMap + openFiles: PlaygroundFileId[] + activeFileId: PlaygroundFileId + onSelectFile: (fileId: PlaygroundFileId) => void + onCloseTab: (fileId: PlaygroundFileId) => void + onEditorChange: (value: string | undefined) => void + onEditorMount: OnMount + activeFileValue: string + oasOutput: string + onOpenPreview: () => void + canCloseTabs: boolean +} + +const EditorTabs: React.FC = ({ + files, + openFiles, + activeFileId, + onSelectFile, + onCloseTab, + onEditorChange, + onEditorMount, + activeFileValue, + oasOutput, + onOpenPreview, + canCloseTabs, +}) => { + const activeFile = useMemo(() => files[activeFileId] ?? null, [files, activeFileId]) + + return ( +

+
+ {openFiles.map((fileId) => { + const file = files[fileId] + const isActive = activeFileId === fileId + const tabId = `editor-tab-${file.id}` + const panelId = `editor-panel-${file.id}` + + return ( +
+ + +
+ ) + })} +
+
+ {activeFile ? ( + + ) : null} +
+
+

+ Tip: Keep the Express app and itdoc tests aligned before you run the suite. +

+
+
+ ) +} + +export default EditorTabs diff --git a/itdoc-doc/src/components/Playground/FileExplorer.tsx b/itdoc-doc/src/components/Playground/FileExplorer.tsx new file mode 100644 index 0000000..70f7763 --- /dev/null +++ b/itdoc-doc/src/components/Playground/FileExplorer.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" + +import styles from "./styles.module.css" +import type { ExplorerNode, PlaygroundFileId, PlaygroundFileMap } from "./types" + +interface FileExplorerProps { + nodes: ExplorerNode[] + files: PlaygroundFileMap + activeFileId: PlaygroundFileId + openFiles: PlaygroundFileId[] + onSelectFile: (fileId: PlaygroundFileId) => void +} + +const FileExplorer: React.FC = ({ + nodes, + files, + activeFileId, + openFiles, + onSelectFile, +}) => { + return ( + + ) +} + +export default FileExplorer diff --git a/itdoc-doc/src/components/Playground/InstallOverlay.tsx b/itdoc-doc/src/components/Playground/InstallOverlay.tsx new file mode 100644 index 0000000..aec9329 --- /dev/null +++ b/itdoc-doc/src/components/Playground/InstallOverlay.tsx @@ -0,0 +1,156 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" + +import styles from "./styles.module.css" +import type { InstallStatus } from "./types" + +interface InstallOverlayProps { + isVisible: boolean + isInstalling: boolean + installStatus: InstallStatus + currentMilestone: { title: string; description: string } | null + milestones: { title: string; description: string }[] + activeMilestoneIndex: number + progressPercent: number + formattedElapsed: string + showFinalizingHint: boolean + waitingTipTitle?: string + waitingTipBody?: string + onNextTip?: () => void + errorMessage: string | null +} + +const InstallOverlay: React.FC = ({ + isVisible, + isInstalling, + installStatus, + currentMilestone, + milestones, + activeMilestoneIndex, + progressPercent, + formattedElapsed, + showFinalizingHint, + waitingTipTitle, + waitingTipBody, + onNextTip, + errorMessage, +}) => { + if (!isVisible) { + return null + } + + const renderTipCard = () => { + if (!waitingTipTitle || !waitingTipBody) { + return null + } + + return ( + + ) + } + + return ( +
+ {isInstalling && currentMilestone ? ( +
+
+
+
+ {progressPercent}% +
+
+
+

{currentMilestone.description}

+
+ +
+
    + {milestones.map((milestone, index) => { + const stateClass = + index < activeMilestoneIndex + ? styles.milestoneItemComplete + : index === activeMilestoneIndex + ? styles.milestoneItemActive + : styles.milestoneItemUpcoming + + return ( +
  • +
  • + ) + })} +
+
Elapsed: {formattedElapsed}
+ {showFinalizingHint ? ( +

+ Almost there—WebContainer is preparing the editors and terminal. This + final step can take up to a minute the first time the sandbox boots. +

+ ) : null} +
+ {renderTipCard()} +
+
+ ) : installStatus === "error" ? ( +
+

Environment failed to start

+

{errorMessage ?? "An unexpected error occurred during setup."}

+

+ Refresh the page to try again or verify your browser supports WebContainer. +

+
+ ) : null} +
+ ) +} + +export default InstallOverlay diff --git a/itdoc-doc/src/components/Playground/RunModal.tsx b/itdoc-doc/src/components/Playground/RunModal.tsx new file mode 100644 index 0000000..19443b2 --- /dev/null +++ b/itdoc-doc/src/components/Playground/RunModal.tsx @@ -0,0 +1,135 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" +import Editor from "@monaco-editor/react" + +import styles from "./styles.module.css" + +interface RunModalProps { + isOpen: boolean + titleId: string + dockStatusMessage: string + onClose: () => void + terminalHostRef: React.RefObject + oasOutput: string + onOpenPreview: () => void +} + +const RunModal: React.FC = ({ + isOpen, + titleId, + dockStatusMessage, + onClose, + terminalHostRef, + oasOutput, + onOpenPreview, +}) => { + if (!isOpen) { + return null + } + + return ( +
+
event.stopPropagation()}> +
+
+

Run output

+ {dockStatusMessage} +
+ +
+
+
+
+
+

Terminal

+ {dockStatusMessage} +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+

Generated docs by itdoc

+ +
+
+
+
+
oas.json
+
+ {oasOutput ? ( + + ) : ( +

+ Run the tests to generate the OpenAPI document. +

+ )} +
+
+
+
+
+
+
+
+
+ ) +} + +export default RunModal diff --git a/itdoc-doc/src/components/Playground/SwaggerPreview.tsx b/itdoc-doc/src/components/Playground/SwaggerPreview.tsx new file mode 100644 index 0000000..1b6e0c0 --- /dev/null +++ b/itdoc-doc/src/components/Playground/SwaggerPreview.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" + +import styles from "./styles.module.css" + +interface SwaggerPreviewProps { + isOpen: boolean + titleId: string + onClose: () => void + redocContainerRef: React.RefObject + hasDocument: boolean +} + +const SwaggerPreview: React.FC = ({ + isOpen, + titleId, + onClose, + redocContainerRef, + hasDocument, +}) => { + if (!isOpen) { + return null + } + + return ( +
+
event.stopPropagation()}> +
+

Swagger Preview

+ +
+
+ {hasDocument ? ( +
+ ) : ( +

+ Run the tests to generate the OpenAPI document before opening the + preview. +

+ )} +
+
+
+ ) +} + +export default SwaggerPreview diff --git a/itdoc-doc/src/components/Playground/TopBar.tsx b/itdoc-doc/src/components/Playground/TopBar.tsx new file mode 100644 index 0000000..8b9d87d --- /dev/null +++ b/itdoc-doc/src/components/Playground/TopBar.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" + +import styles from "./styles.module.css" +import type { InstallStatus, PlaygroundFileDefinition } from "./types" + +interface TopBarProps { + installStatus: InstallStatus + statusLabel: string + isRunning: boolean + runDisabled: boolean + onRun: () => void + onRequestHelp?: () => void + activeFile: PlaygroundFileDefinition | null +} + +const TopBar: React.FC = ({ + installStatus, + statusLabel, + isRunning, + runDisabled, + onRun, + onRequestHelp, + activeFile, +}) => { + return ( +
+
+

itdoc playground

+ {activeFile ? ( +
+ Editing + {activeFile.path} + {activeFile.description} +
+ ) : null} +
+
+ + {statusLabel} + + {onRequestHelp ? ( + + ) : null} + +
+
+ ) +} + +export default TopBar diff --git a/itdoc-doc/src/components/Playground/WorkspacePlaceholder.tsx b/itdoc-doc/src/components/Playground/WorkspacePlaceholder.tsx new file mode 100644 index 0000000..0e84df8 --- /dev/null +++ b/itdoc-doc/src/components/Playground/WorkspacePlaceholder.tsx @@ -0,0 +1,61 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react" + +import styles from "./styles.module.css" + +interface WorkspacePlaceholderProps { + tipTitle?: string + tipBody?: string + onNextTip?: () => void +} + +const WorkspacePlaceholder: React.FC = ({ + tipTitle, + tipBody, + onNextTip, +}) => { + return ( +
+

Booting your sandbox…

+

+ WebContainer is preparing the environment. Dependencies install automatically the + first time the playground loads. +

+ {tipTitle && tipBody ? ( + + ) : null} +
+ ) +} + +export default WorkspacePlaceholder diff --git a/itdoc-doc/src/components/Playground/constants.ts b/itdoc-doc/src/components/Playground/constants.ts new file mode 100644 index 0000000..1f69d15 --- /dev/null +++ b/itdoc-doc/src/components/Playground/constants.ts @@ -0,0 +1,198 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { + ExplorerNode, + PlaygroundFileDefinition, + PlaygroundFileId, + PlaygroundFileMap, +} from "./types" + +export const ITDOC_TARBALL_ASSET = "/playground/itdoc.tgz" +export const FALLBACK_ITDOC_VERSION = "^0.4.1" + +export const initialExpressCode = `const express = require("express") + +const app = express() + +app.use(express.json()) + +app.get("/greeting", (req, res) => { + res.status(200).json({ + message: "Hello from the itdoc playground!", + }) +}) + +app.post("/users", (req, res) => { + const { name, email } = req.body + + if (!name || !email) { + return res.status(400).json({ + error: "Both name and email are required.", + }) + } + + const user = { + id: "user_123", + name, + email, + } + + return res.status(201).json(user) +}) + +module.exports = app +` + +export const initialTestCode = `const { describeAPI, itDoc, HttpMethod, HttpStatus, field } = require("itdoc") +const app = require("../app") + +describeAPI( + HttpMethod.GET, + "/greeting", + { + summary: "Retrieve greeting message", + tag: "Greetings", + description: "Returns a friendly greeting from the Express application.", + }, + app, + (apiDoc) => { + itDoc("returns greeting payload", async () => { + await apiDoc + .test() + .prettyPrint() + .req() + .res() + .status(HttpStatus.OK) + .body({ + message: field("Greeting message", "Hello from the itdoc playground!"), + }) + }) + }, +) + +describeAPI( + HttpMethod.POST, + "/users", + { + summary: "Create a user", + tag: "Users", + description: "Creates a new user and returns the created resource.", + }, + app, + (apiDoc) => { + itDoc("creates a user and returns details", async () => { + await apiDoc + .test() + .req() + .body({ + name: field("User name", "Ada Lovelace"), + email: field("User email", "ada@example.com"), + }) + .res() + .status(HttpStatus.CREATED) + .body({ + id: field("Generated identifier", "user_123"), + name: field("User name", "Ada Lovelace"), + email: field("User email", "ada@example.com"), + }) + }) + }, +) +` + +export const installMilestones = [ + { + title: "Booting WebContainer runtime", + description: + "Spinning up the in-browser Node.js environment so the playground can run without leaving this tab.", + }, + { + title: "Fetching itdoc", + description: "Fetching the latest version of the itdoc library.", + }, + { + title: "Installing npm dependencies", + description: + "Downloading the dependencies required for using itdoc, such as express and mocha.", + }, + { + title: "Finalizing workspace", + description: + "Wiring up editors, terminal, and previews so you can start tweaking the Express app and tests.", + }, +] + +export const waitingTips = [ + { + title: "Origin of the name itdoc", + body: "The name 'itdoc' comes from the typical testing pattern describe()... it()... meaning 'documentation (doc) generated from test cases (it)'.", + }, + { + title: "Did you know?", + body: "The itdoc mascot logo was actually created using generative AI. :grin:", + }, + { + title: "How to run itdoc tests", + body: "You can execute itdoc tests with Mocha or Jest from the CLI. API documentation is generated automatically based on the test results—no extra configuration needed.", + }, +] + +const fileDefinitions: PlaygroundFileMap = { + app: { + id: "app", + label: "app.js", + path: "app.js", + description: "Express entry point", + language: "javascript", + monacoUri: "file:///app.js", + editable: true, + }, + test: { + id: "test", + label: "app.test.js", + path: "__tests__/app.test.js", + description: "Mocha scenario powered by itdoc", + language: "javascript", + monacoUri: "file:///__tests__/app.test.js", + editable: true, + }, + package: { + id: "package", + label: "package.json", + path: "package.json", + description: "Playground dependencies and scripts", + language: "json", + monacoUri: "file:///package.json", + editable: false, + }, +} + +export const PLAYGROUND_FILES: PlaygroundFileMap = fileDefinitions + +export const EXPLORER_NODES: ExplorerNode[] = [ + { type: "file", fileId: "app", label: "app.js" }, + { type: "file", fileId: "package", label: "package.json" }, + { + type: "folder", + label: "__tests__", + children: [{ type: "file", fileId: "test", label: "app.test.js" }], + }, +] + +export function resolveFileDefinition(fileId: PlaygroundFileId): PlaygroundFileDefinition { + return PLAYGROUND_FILES[fileId] +} diff --git a/itdoc-doc/src/components/Playground/index.tsx b/itdoc-doc/src/components/Playground/index.tsx index 09c1932..b3e4d9d 100644 --- a/itdoc-doc/src/components/Playground/index.tsx +++ b/itdoc-doc/src/components/Playground/index.tsx @@ -17,21 +17,35 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment" import useBaseUrl from "@docusaurus/useBaseUrl" -import Editor, { OnMount } from "@monaco-editor/react" +import type { OnMount } from "@monaco-editor/react" import { Terminal } from "@xterm/xterm" import { FitAddon } from "@xterm/addon-fit" import "@xterm/xterm/css/xterm.css" -import styles from "./styles.module.css" -const itdocTarballAsset = "/playground/itdoc.tgz" -const FALLBACK_ITDOC_VERSION = "^0.4.1" +import FileExplorer from "./FileExplorer" +import InstallOverlay from "./InstallOverlay" +import EditorTabs from "./EditorTabs" +import RunModal from "./RunModal" +import SwaggerPreview from "./SwaggerPreview" +import TopBar from "./TopBar" +import WorkspacePlaceholder from "./WorkspacePlaceholder" +import { + EXPLORER_NODES, + FALLBACK_ITDOC_VERSION, + ITDOC_TARBALL_ASSET, + PLAYGROUND_FILES, + initialExpressCode, + initialTestCode, + installMilestones, + waitingTips, +} from "./constants" +import styles from "./styles.module.css" +import type { InstallStatus, PlaygroundFileId } from "./types" type FileSystemTree = import("@webcontainer/api").FileSystemTree type WebContainerInstance = import("@webcontainer/api").WebContainer type WebContainerProcess = import("@webcontainer/api").Process -type InstallStatus = "idle" | "installing" | "ready" | "error" - declare global { interface Window { Redoc?: { @@ -48,202 +62,6 @@ interface PlaygroundProps { onRequestHelp?: () => void } -const initialExpressCode = `const express = require("express") - -const app = express() - -app.use(express.json()) - -app.get("/greeting", (req, res) => { - res.status(200).json({ - message: "Hello from the itdoc playground!", - }) -}) - -app.post("/users", (req, res) => { - const { name, email } = req.body - - if (!name || !email) { - return res.status(400).json({ - error: "Both name and email are required.", - }) - } - - const user = { - id: "user_123", - name, - email, - } - - return res.status(201).json(user) -}) - -module.exports = app -` - -const initialTestCode = `const { describeAPI, itDoc, HttpMethod, HttpStatus, field } = require("itdoc") -const app = require("../app") - -describeAPI( - HttpMethod.GET, - "/greeting", - { - summary: "Retrieve greeting message", - tag: "Greetings", - description: "Returns a friendly greeting from the Express application.", - }, - app, - (apiDoc) => { - itDoc("returns greeting payload", async () => { - await apiDoc - .test() - .prettyPrint() - .req() - .res() - .status(HttpStatus.OK) - .body({ - message: field("Greeting message", "Hello from the itdoc playground!"), - }) - }) - }, -) - -describeAPI( - HttpMethod.POST, - "/users", - { - summary: "Create a user", - tag: "Users", - description: "Creates a new user and returns the created resource.", - }, - app, - (apiDoc) => { - itDoc("creates a user and returns details", async () => { - await apiDoc - .test() - .req() - .body({ - name: field("User name", "Ada Lovelace"), - email: field("User email", "ada@example.com"), - }) - .res() - .status(HttpStatus.CREATED) - .body({ - id: field("Generated identifier", "user_123"), - name: field("User name", "Ada Lovelace"), - email: field("User email", "ada@example.com"), - }) - }) - }, -) -` - -const installMilestones = [ - { - title: "Booting WebContainer runtime", - description: - "Spinning up the in-browser Node.js environment so the playground can run without leaving this tab.", - }, - { - title: "Fetching itdoc", - description: "Fetching the latest version of the itdoc library.", - }, - { - title: "Installing npm dependencies", - description: - "Downloading the dependencies required for using itdoc, such as express and mocha.", - }, - { - title: "Finalizing workspace", - description: - "Wiring up editors, terminal, and previews so you can start tweaking the Express app and tests.", - }, -] - -const waitingTips = [ - { - title: "Origin of the name itdoc", - body: "The name 'itdoc' comes from the typical testing pattern describe()... it()... meaning 'documentation (doc) generated from test cases (it)'.", - }, - { - title: "Did you know?", - body: "The itdoc mascot logo was actually created using generative AI. :grin:", - }, - { - title: "How to run itdoc tests", - body: "You can execute itdoc tests with Mocha or Jest from the CLI. API documentation is generated automatically based on the test results—no extra configuration needed.", - }, -] - -type PlaygroundFileId = "app" | "test" | "package" - -interface PlaygroundFileDefinition { - id: PlaygroundFileId - label: string - path: string - description: string - language: "javascript" | "json" - monacoUri: string - editable: boolean -} - -interface ExplorerFileNode { - type: "file" - fileId: PlaygroundFileId - label: string - depth: number -} - -interface ExplorerFolderNode { - type: "folder" - label: string - depth: number - children: ExplorerFileNode[] -} - -type ExplorerNode = ExplorerFileNode | ExplorerFolderNode - -const PLAYGROUND_FILES: Record = { - app: { - id: "app", - label: "app.js", - path: "app.js", - description: "Express entry point", - language: "javascript", - monacoUri: "file:///app.js", - editable: true, - }, - test: { - id: "test", - label: "app.test.js", - path: "__tests__/app.test.js", - description: "Test code with itdoc", - language: "javascript", - monacoUri: "file:///__tests__/app.test.js", - editable: true, - }, - package: { - id: "package", - label: "package.json", - path: "package.json", - description: "Playground dependencies and scripts", - language: "json", - monacoUri: "file:///package.json", - editable: false, - }, -} - -const EXPLORER_NODES: ExplorerNode[] = [ - { type: "file", fileId: "app", label: "app.js", depth: 0 }, - { type: "file", fileId: "package", label: "package.json", depth: 0 }, - { - type: "folder", - label: "__tests__", - depth: 0, - children: [{ type: "file", fileId: "test", label: "app.test.js", depth: 1 }], - }, -] - function formatDuration(milliseconds: number): string { const bounded = Math.max(0, milliseconds) const totalSeconds = Math.floor(bounded / 1000) @@ -563,7 +381,7 @@ const Playground: React.FC = ({ onRequestHelp }) => { ) const canUseDom = ExecutionEnvironment.canUseDOM - const itdocTarballUrl = useBaseUrl(itdocTarballAsset) + const itdocTarballUrl = useBaseUrl(ITDOC_TARBALL_ASSET) useEffect(() => { if (!canUseDom || !showRunModal) { @@ -1001,42 +819,15 @@ const Playground: React.FC = ({ onRequestHelp }) => { return (
-
-
-

itdoc playground

- {activeFile ? ( -
- Editing - {activeFile.path} - - {activeFile.description} - -
- ) : null} -
-
- - {statusLabel} - - {onRequestHelp ? ( - - ) : null} - -
-
+ {showWorkspace && errorMessage ? (
Test run failed. @@ -1046,440 +837,66 @@ const Playground: React.FC = ({ onRequestHelp }) => {
{showWorkspace ? (
- -
-
- {openFiles.map((fileId) => { - const file = PLAYGROUND_FILES[fileId] - const isActive = activeFileId === fileId - const tabId = `editor-tab-${file.id}` - const panelId = `editor-panel-${file.id}` - return ( -
- - -
- ) - })} -
-
- {activeFile ? ( - - ) : null} -
-
-

- Tip: Keep the Express app and itdoc tests aligned before you run - the suite. -

-
-
+ + setShowSwaggerPreview(true)} + canCloseTabs={openFiles.length > 1} + />
) : ( -
-

Booting your sandbox…

-

- WebContainer is preparing the environment. Dependencies install - automatically the first time the playground loads. -

- {waitingTip ? ( - - ) : null} -
+ )}
- {showRunModal ? ( -
setShowRunModal(false)} - > -
event.stopPropagation()}> -
-
-

Run output

- {dockStatusMessage} -
- -
-
-
-
-
-

Terminal

- - {dockStatusMessage} - -
-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-

- Generated docs by itdoc -

- -
-
-
-
-
oas.json
-
- {oasOutput ? ( - - ) : ( -

- Run the tests to generate the OpenAPI - document. -

- )} -
-
-
-
-
-
-
-
-
- ) : null} - {showOverlay ? ( -
- {isInstalling && currentMilestone ? ( -
-
-
-
- {progressPercent}% -
-
-
-

- {currentMilestone.description} -

-
- -
-
    - {installMilestones.map((milestone, index) => { - const stateClass = - index < activeMilestoneIndex - ? styles.milestoneItemComplete - : index === activeMilestoneIndex - ? styles.milestoneItemActive - : styles.milestoneItemUpcoming - - return ( -
  • -
  • - ) - })} -
-
- Elapsed: {formattedElapsed} -
- {showFinalizingHint ? ( -

- Almost there—WebContainer is preparing the editors and - terminal. This final step can take up to a minute the - first time the sandbox boots. -

- ) : null} -
- {waitingTip ? ( - - ) : null} -
-
- ) : installStatus === "error" ? ( -
-

Environment failed to start

-

{errorMessage ?? "An unexpected error occurred during setup."}

-

- Refresh the page to try again or verify your browser supports - WebContainer. -

-
- ) : null} -
- ) : null} - {showSwaggerPreview ? ( -
setShowSwaggerPreview(false)} - > -
event.stopPropagation()} - > -
-

Swagger Preview

- -
-
- {oasOutput ? ( -
- Loading preview… -
- ) : ( -

- Run the tests to generate the OpenAPI document before opening - the preview. -

- )} -
-
-
- ) : null} + setShowRunModal(false)} + terminalHostRef={terminalHostRef} + oasOutput={oasOutput} + onOpenPreview={() => setShowSwaggerPreview(true)} + /> + setShowSwaggerPreview(false)} + redocContainerRef={redocContainerRef} + hasDocument={Boolean(oasOutput)} + /> +
) } diff --git a/itdoc-doc/src/components/Playground/types.ts b/itdoc-doc/src/components/Playground/types.ts new file mode 100644 index 0000000..f74c230 --- /dev/null +++ b/itdoc-doc/src/components/Playground/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type InstallStatus = "idle" | "installing" | "ready" | "error" + +export type PlaygroundFileId = "app" | "test" | "package" + +export interface PlaygroundFileDefinition { + id: PlaygroundFileId + label: string + path: string + description: string + language: "javascript" | "json" + monacoUri: string + editable: boolean +} + +export interface ExplorerFileNode { + type: "file" + fileId: PlaygroundFileId + label: string +} + +export interface ExplorerFolderNode { + type: "folder" + label: string + children: ExplorerFileNode[] +} + +export type ExplorerNode = ExplorerFileNode | ExplorerFolderNode + +export type PlaygroundFileMap = Record diff --git a/itdoc-doc/src/pages/playground.tsx b/itdoc-doc/src/pages/playground.tsx index 1ed2a22..4dcb293 100644 --- a/itdoc-doc/src/pages/playground.tsx +++ b/itdoc-doc/src/pages/playground.tsx @@ -22,27 +22,66 @@ import styles from "./playground.module.css" const instructionsTitleId = "playground-help-title" -const PlaygroundPage: React.FC = () => { - const [showHelp, setShowHelp] = useState(false) - - useEffect(() => { - if (!showHelp || typeof document === "undefined") { - return - } +interface PlaygroundHelpModalProps { + onClose: () => void +} - const originalOverflow = document.body.style.overflow - document.body.style.overflow = "hidden" +const PlaygroundHelpModal: React.FC = ({ onClose }) => ( +
+
event.stopPropagation()} + > +
+

How to use the playground

+ +
+
+
+

Get started quickly

+
    +
  1. + Let the installer finish booting the WebContainer. The status pill flips + to Ready once the in-browser npm install{" "} + wraps up. +
  2. +
  3. + Write a simple API using Express and create an itdoc test for it. The + default scenario wires up /greeting and /users{" "} + endpoints. +
  4. +
  5. + Hit Run to execute npm test. Watch the + terminal for install logs, assertions, and any failures. +
  6. +
  7. + Green tests regenerate the OpenAPI output in oas.json. Red + output points to the exact assertion or handler that needs an update. +
  8. +
+
+
+
+
+) - return () => { - document.body.style.overflow = originalOverflow - } - }, [showHelp]) +const PlaygroundPage: React.FC = () => { + const [showHelp, setShowHelp] = useState(false) useEffect(() => { if (!showHelp || typeof window === "undefined") { return } + const { body } = document + const originalOverflow = body.style.overflow + body.style.overflow = "hidden" + const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { setShowHelp(false) @@ -52,6 +91,7 @@ const PlaygroundPage: React.FC = () => { window.addEventListener("keydown", handleKeyDown) return () => { + body.style.overflow = originalOverflow window.removeEventListener("keydown", handleKeyDown) } }, [showHelp]) @@ -80,59 +120,7 @@ const PlaygroundPage: React.FC = () => {
- {showHelp ? ( -
setShowHelp(false)} - > -
event.stopPropagation()} - > -
-

How to use the playground

- -
-
-
-

Get started quickly

-
    -
  1. - Let the installer finish booting the WebContainer. The - status pill flips to Ready - once the in-browser npm install wraps up. -
  2. -
  3. - Write a simple API using Express and create an itdoc test - for it. The default scenario wires up /greeting{" "} - and /users endpoints. -
  4. -
  5. - Hit Run to execute npm test. - Watch the terminal for install logs, assertions, and any - failures. -
  6. -
  7. - Green tests regenerate the OpenAPI output in{" "} - oas.json. Red output points to the exact - assertion or handler that needs an update. -
  8. -
-
-
-
-
- ) : null} + {showHelp ? setShowHelp(false)} /> : null} ) } From 96313659a4aeb048a9986daf41139aaf173de11f Mon Sep 17 00:00:00 2001 From: PENEKhun Date: Thu, 9 Oct 2025 14:08:33 +0900 Subject: [PATCH 3/9] =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=A6=AC?= =?UTF-8?q?=EB=89=B4=EC=96=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- itdoc-doc/src/components/Playground/constants.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/itdoc-doc/src/components/Playground/constants.ts b/itdoc-doc/src/components/Playground/constants.ts index 1f69d15..2e31a45 100644 --- a/itdoc-doc/src/components/Playground/constants.ts +++ b/itdoc-doc/src/components/Playground/constants.ts @@ -14,12 +14,7 @@ * limitations under the License. */ -import type { - ExplorerNode, - PlaygroundFileDefinition, - PlaygroundFileId, - PlaygroundFileMap, -} from "./types" +import type { ExplorerNode, PlaygroundFileMap } from "./types" export const ITDOC_TARBALL_ASSET = "/playground/itdoc.tgz" export const FALLBACK_ITDOC_VERSION = "^0.4.1" @@ -165,7 +160,7 @@ const fileDefinitions: PlaygroundFileMap = { id: "test", label: "app.test.js", path: "__tests__/app.test.js", - description: "Mocha scenario powered by itdoc", + description: "Test code using itdoc", language: "javascript", monacoUri: "file:///__tests__/app.test.js", editable: true, @@ -192,7 +187,3 @@ export const EXPLORER_NODES: ExplorerNode[] = [ children: [{ type: "file", fileId: "test", label: "app.test.js" }], }, ] - -export function resolveFileDefinition(fileId: PlaygroundFileId): PlaygroundFileDefinition { - return PLAYGROUND_FILES[fileId] -} From 7d517f1c45f619cdade613ad341c0268e3d8156c Mon Sep 17 00:00:00 2001 From: PENEKhun Date: Thu, 9 Oct 2025 14:09:57 +0900 Subject: [PATCH 4/9] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Playground/constants.ts | 1 - itdoc-doc/src/components/Playground/index.tsx | 38 +------------------ 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/itdoc-doc/src/components/Playground/constants.ts b/itdoc-doc/src/components/Playground/constants.ts index 2e31a45..bd22f26 100644 --- a/itdoc-doc/src/components/Playground/constants.ts +++ b/itdoc-doc/src/components/Playground/constants.ts @@ -17,7 +17,6 @@ import type { ExplorerNode, PlaygroundFileMap } from "./types" export const ITDOC_TARBALL_ASSET = "/playground/itdoc.tgz" -export const FALLBACK_ITDOC_VERSION = "^0.4.1" export const initialExpressCode = `const express = require("express") diff --git a/itdoc-doc/src/components/Playground/index.tsx b/itdoc-doc/src/components/Playground/index.tsx index b3e4d9d..511ea86 100644 --- a/itdoc-doc/src/components/Playground/index.tsx +++ b/itdoc-doc/src/components/Playground/index.tsx @@ -31,7 +31,6 @@ import TopBar from "./TopBar" import WorkspacePlaceholder from "./WorkspacePlaceholder" import { EXPLORER_NODES, - FALLBACK_ITDOC_VERSION, ITDOC_TARBALL_ASSET, PLAYGROUND_FILES, initialExpressCode, @@ -200,24 +199,6 @@ async function provisionLocalItDocPackage( } } -async function switchItDocDependencyToRegistry( - instance: WebContainerInstance, - log: (chunk: string) => void, -): Promise { - try { - const packageJsonRaw = await instance.fs.readFile("package.json", "utf-8") - const packageJson = JSON.parse(packageJsonRaw.toString()) - packageJson.dependencies.itdoc = FALLBACK_ITDOC_VERSION - await instance.fs.writeFile("package.json", JSON.stringify(packageJson, null, 4)) - log(`Updated package.json to use itdoc@${FALLBACK_ITDOC_VERSION} from npm registry.\n`) - return true - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - log(`\nUnable to reconfigure package.json for registry install (${message}).\n`) - return false - } -} - const Playground: React.FC = ({ onRequestHelp }) => { const [installStatus, setInstallStatus] = useState("idle") const [isRunning, setIsRunning] = useState(false) @@ -492,24 +473,7 @@ const Playground: React.FC = ({ onRequestHelp }) => { return } - const tarballReady = await provisionLocalItDocPackage( - instance, - itdocTarballUrl, - appendTerminalOutput, - ) - if (!tarballReady) { - const switched = await switchItDocDependencyToRegistry( - instance, - appendTerminalOutput, - ) - if (!switched) { - setInstallStatus("error") - setErrorMessage( - "Failed to provision the itdoc dependency. Check the terminal output for details.", - ) - return - } - } + await provisionLocalItDocPackage(instance, itdocTarballUrl, appendTerminalOutput) const installProcess = await instance.spawn("npm", ["install"]) runningProcessRef.current = installProcess From 0d963d8411ff434a2b62ba2113d9309483f27ad5 Mon Sep 17 00:00:00 2001 From: PENEKhun Date: Sun, 19 Oct 2025 21:02:59 +0900 Subject: [PATCH 5/9] change oas editor surface background color --- itdoc-doc/src/components/Playground/styles.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/itdoc-doc/src/components/Playground/styles.module.css b/itdoc-doc/src/components/Playground/styles.module.css index 01088ed..1e632f4 100644 --- a/itdoc-doc/src/components/Playground/styles.module.css +++ b/itdoc-doc/src/components/Playground/styles.module.css @@ -690,7 +690,7 @@ display: flex; flex-direction: column; padding: var(--ifm-spacing-md, 1rem); - background: rgba(15, 23, 42, 0.92); + background: #1e1e1e; border-radius: 0 0 0.75rem 0.75rem; gap: var(--ifm-spacing-md, 1rem); min-height: 0; From 64940482a5c42921fa0fde1b2768daa38f7edb21 Mon Sep 17 00:00:00 2001 From: PENEKhun Date: Sun, 19 Oct 2025 21:13:47 +0900 Subject: [PATCH 6/9] fix swagger preview was not displaying well --- .../components/Playground/SwaggerPreview.tsx | 18 ++- itdoc-doc/src/components/Playground/index.tsx | 138 +++++++----------- .../components/Playground/styles.module.css | 12 +- 3 files changed, 72 insertions(+), 96 deletions(-) diff --git a/itdoc-doc/src/components/Playground/SwaggerPreview.tsx b/itdoc-doc/src/components/Playground/SwaggerPreview.tsx index 1b6e0c0..bcbafb0 100644 --- a/itdoc-doc/src/components/Playground/SwaggerPreview.tsx +++ b/itdoc-doc/src/components/Playground/SwaggerPreview.tsx @@ -22,7 +22,7 @@ interface SwaggerPreviewProps { isOpen: boolean titleId: string onClose: () => void - redocContainerRef: React.RefObject + previewHtml: string | null hasDocument: boolean } @@ -30,7 +30,7 @@ const SwaggerPreview: React.FC = ({ isOpen, titleId, onClose, - redocContainerRef, + previewHtml, hasDocument, }) => { if (!isOpen) { @@ -54,7 +54,19 @@ const SwaggerPreview: React.FC = ({
{hasDocument ? ( -
+ previewHtml ? ( +