├── .eslintignore
├── src-tauri
├── build.rs
├── icons
│ ├── 32x32.png
│ ├── icon.icns
│ ├── icon.ico
│ ├── icon.png
│ ├── logo.png
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── StoreLogo.png
│ ├── Square30x30Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ └── Square310x310Logo.png
├── .gitignore
├── src
│ ├── main.rs
│ └── lib.rs
├── capabilities
│ └── default.json
├── Cargo.toml
└── tauri.conf.json
├── assets
├── screen.png
├── favicon.ico
├── vertical.png
├── horizontal.png
├── fonts
│ ├── CascadiaCodePL.woff2
│ └── CascadiaCodePLItalic.woff2
├── html5.svg
├── typescript.svg
├── favicon.svg
├── js.svg
└── css.svg
├── eslint.config.js
├── .prettierrc
├── vercel.json
├── src
├── components
│ ├── codi-editor
│ │ ├── codi-editor.js
│ │ ├── extensions
│ │ │ ├── register-themes.js
│ │ │ ├── editor-hotkeys.js
│ │ │ └── autocomplete-html-tag.js
│ │ ├── CodiEditor.styles.js
│ │ ├── CodiEditor.js
│ │ └── themes.json
│ └── layout-preview
│ │ ├── layout-preview.jsx
│ │ ├── LayoutPreview.js
│ │ └── layout-preview.css
├── utils
│ ├── index.js
│ ├── debounce.js
│ ├── string.js
│ ├── createHtml.js
│ ├── translator.js
│ ├── dom.js
│ ├── run-js.js
│ ├── WindowPreviewer.js
│ ├── js-execution-worker.js
│ └── notification.js
├── theme.js
├── language.js
├── css
│ ├── fonts.css
│ ├── animations.css
│ ├── drag-drop-area.css
│ ├── notifications.css
│ ├── settings.css
│ ├── skypack.css
│ ├── history.css
│ ├── aside.css
│ ├── console.css
│ ├── base.css
│ └── editors-layout.css
├── constants
│ ├── button-actions.js
│ ├── editor-grid-template.js
│ ├── initial-settings.js
│ ├── console-icons.js
│ └── grid-templates.js
├── state.js
├── monaco-prettier
│ └── configurePrettier.js
├── editor.js
├── scroll.js
├── settings.js
├── drag-file.js
├── download.js
├── language
│ ├── en.js
│ ├── pt.js
│ └── es.js
├── aside.js
├── events-controller.js
├── style.css
├── grid.js
├── console-script.js
├── main.js
├── skypack.js
├── history.js
└── console.js
├── vite.config.js
├── .gitignore
├── .github
└── workflows
│ ├── conflicts.yml
│ ├── release.yml
│ └── pipeline.yml
├── git-hooks
└── pre-commit
├── package.json
├── CONTRIBUTING.md
├── README.md
├── LICENSE
└── index.html
/.eslintignore:
--------------------------------------------------------------------------------
1 | ./src-tauri/target/**
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/assets/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/assets/screen.png
--------------------------------------------------------------------------------
/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/assets/favicon.ico
--------------------------------------------------------------------------------
/assets/vertical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/assets/vertical.png
--------------------------------------------------------------------------------
/assets/horizontal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/assets/horizontal.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/icons/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/assets/fonts/CascadiaCodePL.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/assets/fonts/CascadiaCodePL.woff2
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import neostandard from 'neostandard'
2 |
3 | export default neostandard({
4 | noStyle: true
5 | })
6 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 | /gen/schemas
5 |
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "arrowParens": "avoid"
6 | }
7 |
--------------------------------------------------------------------------------
/assets/fonts/CascadiaCodePLItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/midudev/codi.link/main/assets/fonts/CascadiaCodePLItalic.woff2
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "routes": [
3 | { "handle": "filesystem" },
4 | { "src": "/(.*)", "dest": "/index.html" }
5 | ]
6 | }
--------------------------------------------------------------------------------
/src/components/codi-editor/codi-editor.js:
--------------------------------------------------------------------------------
1 | import { CodiEditor } from './CodiEditor.js'
2 |
3 | window.customElements.define('codi-editor', CodiEditor)
4 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | fn main() {
5 | app_lib::run();
6 | }
7 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import preact from '@preact/preset-vite'
3 | import tailwindcss from '@tailwindcss/vite'
4 |
5 | export default defineConfig({
6 | plugins: [preact(), tailwindcss()]
7 | })
8 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export { $$, $, setFormControlValue } from './dom'
2 | export { capitalize, searchByLine } from './string'
3 | export { default as debounce } from './debounce'
4 | export { default as WindowPreviewer } from './WindowPreviewer'
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 | package-lock.json
7 | yarn.lock
8 | pnpm-lock.yaml
9 |
10 | # cypress
11 | cypress/plugins/
12 | cypress/support/
13 | cypress/screenshots/
14 | cypress/videos/
15 |
16 | # tauri
17 | src-tauri/target/
18 |
--------------------------------------------------------------------------------
/src/components/codi-editor/extensions/register-themes.js:
--------------------------------------------------------------------------------
1 | import themesJson from '../themes.json'
2 |
3 | export function registerThemes (monaco) {
4 | Object.entries(themesJson).forEach(([themeName, themeDef]) => {
5 | monaco.editor.defineTheme(themeName, themeDef)
6 | })
7 | }
8 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "enables the default permissions",
5 | "windows": [
6 | "main"
7 | ],
8 | "permissions": [
9 | "core:default"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Change the color theme used in the workbench.
3 | * @param {'vs-dark' | 'vs' | 'hc-black' | 'hc-light'} theme
4 | */
5 |
6 | const setTheme = theme => {
7 | document.documentElement.setAttribute('data-theme', theme)
8 | }
9 |
10 | export default setTheme
11 |
--------------------------------------------------------------------------------
/src/language.js:
--------------------------------------------------------------------------------
1 | import { translate } from './utils/translator.js'
2 |
3 | /**
4 | * Change the language used in the workbench.
5 | * @param {'en' | 'es' | 'pt'} language
6 | */
7 | const setLanguage = language => {
8 | document.documentElement.setAttribute('data-language', language)
9 | translate(language)
10 | }
11 |
12 | export default setLanguage
13 |
--------------------------------------------------------------------------------
/src/css/fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Cascadia Code PL";
3 | src: url("/assets/fonts/CascadiaCodePL.woff2") format("woff2");
4 | font-weight: normal;
5 | font-style: normal;
6 | }
7 |
8 | @font-face {
9 | font-family: "Cascadia Code PL";
10 | src: url("/assets/fonts/CascadiaCodePLItalic.woff2") format("woff2");
11 | font-weight: normal;
12 | font-style: italic;
13 | }
14 |
--------------------------------------------------------------------------------
/assets/html5.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/assets/typescript.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/workflows/conflicts.yml:
--------------------------------------------------------------------------------
1 | name: Label Pull Requests with Conflicts
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | triage:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: mschilde/auto-label-merge-conflicts@master
12 | with:
13 | CONFLICT_LABEL_NAME: "has conflicts"
14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
15 | MAX_RETRIES: 5
16 | WAIT_MS: 5000
--------------------------------------------------------------------------------
/src/css/animations.css:
--------------------------------------------------------------------------------
1 | @keyframes spin {
2 | 0% {
3 | transform: rotate(-360deg);
4 | }
5 | }
6 |
7 | @keyframes fadeInSlideUp {
8 | 0% {
9 | opacity: 0;
10 | transform: translateY(20px);
11 | }
12 | 100% {
13 | opacity: 1;
14 | transform: translateY(0);
15 | }
16 | }
17 |
18 | @keyframes fadeOutSlideDown {
19 | 0% {
20 | opacity: 1;
21 | }
22 |
23 | 100% {
24 | opacity: 0;
25 | transform: translateY(20px);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/constants/button-actions.js:
--------------------------------------------------------------------------------
1 | export const BUTTON_ACTIONS = {
2 | downloadUserCode: 'download-user-code',
3 | openIframeTab: 'open-iframe-tab',
4 | copyToClipboard: 'copy-to-clipboard',
5 | closeAsideBar: 'close-aside-bar',
6 | showSkypackBar: 'show-skypack-bar',
7 | showSettingsBar: 'show-settings-bar',
8 | showConsoleBar: 'show-console-bar',
9 | showHistoryBar: 'show-history-bar',
10 | clearHistory: 'clear-history',
11 | openNewInstance: 'open-new-instance'
12 | }
13 |
--------------------------------------------------------------------------------
/src-tauri/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[cfg_attr(mobile, tauri::mobile_entry_point)]
2 | pub fn run() {
3 | tauri::Builder::default()
4 | .setup(|app| {
5 | if cfg!(debug_assertions) {
6 | app.handle().plugin(
7 | tauri_plugin_log::Builder::default()
8 | .level(log::LevelFilter::Info)
9 | .build(),
10 | )?;
11 | }
12 | Ok(())
13 | })
14 | .run(tauri::generate_context!())
15 | .expect("error while running tauri application");
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/layout-preview/layout-preview.jsx:
--------------------------------------------------------------------------------
1 | import register from 'preact-custom-element'
2 | import './layout-preview.css'
3 |
4 | function LayoutPreview ({ active, layout }) {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 | >
12 | )
13 | }
14 |
15 | // Register the Preact component as a custom element
16 | register(LayoutPreview, 'layout-preview', ['active', 'layout'], { shadow: false })
17 |
--------------------------------------------------------------------------------
/src/state.js:
--------------------------------------------------------------------------------
1 | import { persist } from 'zustand/middleware'
2 | import { createStore } from 'zustand/vanilla'
3 |
4 | import { DEFAULT_INITIAL_SETTINGS } from './constants/initial-settings'
5 |
6 | const useStore = createStore(
7 | persist(
8 | (set, get) => ({
9 | ...DEFAULT_INITIAL_SETTINGS,
10 | updateSettings: ({ key, value }) => {
11 | set({ [key]: value })
12 | }
13 | }),
14 | { name: 'appInitialState' }
15 | )
16 | )
17 |
18 | export const { getState, setState, subscribe } = useStore
19 |
--------------------------------------------------------------------------------
/src/utils/debounce.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /**
4 | * Function to create a debounce function
5 | * @param {function} func Function to debounce
6 | * @param {number} msWait Number of milliseconds to wait before calling function
7 | * @returns {function} Debounce function
8 | */
9 | export default function debounce (func, msWait) {
10 | let timeout
11 | return function (...args) {
12 | const context = this
13 | clearTimeout(timeout)
14 | timeout = setTimeout(() => func.apply(context, args), msWait)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/constants/editor-grid-template.js:
--------------------------------------------------------------------------------
1 | export const EDITOR_GRID_TEMPLATE = {
2 | vertical: 'grid-template-columns: 1fr 8px 1fr 8px 1fr 8px 1fr; grid-template-rows: 1fr',
3 | horizontal: 'grid-template-columns: 1fr; grid-template-rows: 1fr 8px 1fr 8px 1fr 8px 1fr',
4 | bottom: 'grid-template-columns: 1fr 8px 1fr 8px 1fr; grid-template-rows: 1fr 8px 1fr',
5 | tabs: 'grid-template-columns: 5fr 8px 3fr, grid-template-rows: 40px 1fr'
6 | }
7 |
8 | export const DEFAULT_GRID_TEMPLATE = 'grid-template-columns: 1fr 8px 1fr; grid-template-rows: 1fr 8px 1fr'
9 |
--------------------------------------------------------------------------------
/git-hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | RED="\033[1;31m"
4 | GREEN="\033[1;32m"
5 | NC="\033[0m"
6 |
7 | echo "Executing pre-commit"
8 |
9 | linter_exit_code=1
10 | staged_js_files=$(git diff --cached --diff-filter=d --name-only | grep -E '\.(js|jsx)$')
11 | bunx standard $staged_js_files --quiet --fix
12 | linter_exit_code=$?
13 | git add -f $staged_js_files
14 |
15 | if [ $linter_exit_code -ne 0 ]
16 | then
17 | echo "${RED} ❌ Linter errors have occurred ${NC}"
18 | exit 1
19 | else
20 | echo "${GREEN} ✔ Linter did not find any errors ${NC}"
21 | exit 0
22 | fi
--------------------------------------------------------------------------------
/assets/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/monaco-prettier/configurePrettier.js:
--------------------------------------------------------------------------------
1 | export const configurePrettierHotkeys = editors => {
2 | window.onresize = function () {
3 | editors.forEach(e => e.layout())
4 | }
5 |
6 | const alt = e =>
7 | navigator.userAgent.toLowerCase().includes('mac')
8 | ? e.metaKey
9 | : e.ctrlKey
10 |
11 | const hotKeys = (e) => {
12 | editors.forEach(editor => {
13 | // Control/Command + P
14 | if (alt(e) && e.keyCode === 80) {
15 | e.preventDefault()
16 | editor.trigger('anyString', 'editor.action.quickCommand')
17 | }
18 | })
19 | }
20 |
21 | window.addEventListener('keydown', hotKeys)
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/layout-preview/LayoutPreview.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html } from 'lit'
2 | import { LayoutPreviewStyles } from './LayoutPreview.styles.js'
3 |
4 | export class LayoutPreview extends LitElement {
5 | static get styles () {
6 | return LayoutPreviewStyles
7 | }
8 |
9 | static get properties () {
10 | return {
11 | active: {
12 | type: Boolean
13 | },
14 | layout: {
15 | type: String
16 | }
17 | }
18 | }
19 |
20 | render () {
21 | return html`
22 |
23 |
24 |
25 |
26 | `
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/assets/js.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "app"
3 | version = "0.1.0"
4 | description = "A Tauri App"
5 | authors = ["you"]
6 | license = ""
7 | repository = ""
8 | edition = "2021"
9 | rust-version = "1.77.2"
10 |
11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12 |
13 | [lib]
14 | name = "app_lib"
15 | crate-type = ["staticlib", "cdylib", "rlib"]
16 |
17 | [build-dependencies]
18 | tauri-build = { version = "2.0.2", features = [] }
19 |
20 | [dependencies]
21 | serde_json = "1.0"
22 | serde = { version = "1.0", features = ["derive"] }
23 | log = "0.4"
24 | tauri = { version = "2.1.0", features = [] }
25 | tauri-plugin-log = "2.0.0-rc"
26 |
--------------------------------------------------------------------------------
/src/components/codi-editor/CodiEditor.styles.js:
--------------------------------------------------------------------------------
1 | import { css } from 'lit'
2 |
3 | export const CodiEditorStyles = css`
4 | :host {
5 | position: relative;
6 | overflow: hidden;
7 | }
8 |
9 | img {
10 | position: absolute;
11 | right: 16px;
12 | bottom: 16px;
13 | z-index: 9;
14 | width: 32px;
15 | height: 32px;
16 | object-fit: contain;
17 | object-position: center;
18 | pointer-events: none;
19 | }
20 |
21 | slot:hover + img {
22 | opacity: 0.2;
23 | }
24 |
25 | slot:focus-within + img {
26 | opacity: 0.1;
27 | }
28 |
29 |
30 | @media (max-width: 650px) {
31 | :host::after {
32 | left: 16px;
33 | right: unset;
34 | }
35 |
36 | img {
37 | left: 16px;
38 | right: unset;
39 | }
40 | }
41 | `
42 |
--------------------------------------------------------------------------------
/src/constants/initial-settings.js:
--------------------------------------------------------------------------------
1 | import { DEFAULT_GRID_TEMPLATE } from './editor-grid-template'
2 | import { DEFAULT_LAYOUT } from './grid-templates'
3 |
4 | export const DEFAULT_INITIAL_SETTINGS = {
5 | fontFamily: "'Cascadia Code PL', 'Menlo', 'Monaco', 'Courier New', 'monospace'",
6 | fontLigatures: 'on',
7 | fontSize: 18,
8 | lineNumbers: 'off',
9 | tabSize: 2,
10 | maxExecutionTime: 200,
11 | minimap: false,
12 | preserveGrid: true,
13 | theme: 'vs-dark',
14 | language: 'es',
15 | sidebar: 'default',
16 | wordWrap: 'on',
17 | zipFileName: 'codi.link',
18 | zipInSingleFile: false,
19 | saveLocalstorage: true,
20 | layout: {
21 | gutters: DEFAULT_LAYOUT,
22 | style: DEFAULT_GRID_TEMPLATE,
23 | type: 'default'
24 | },
25 | cursorBlinking: 'blink',
26 | cursorSmoothCaretAnimation: 'off'
27 | }
28 |
--------------------------------------------------------------------------------
/src/editor.js:
--------------------------------------------------------------------------------
1 | import { getState } from './state.js'
2 |
3 | const {
4 | fontSize,
5 | lineNumbers,
6 | minimap,
7 | theme,
8 | wordWrap,
9 | fontLigatures,
10 | fontFamily,
11 | tabSize,
12 | cursorBlinking,
13 | cursorSmoothCaretAnimation
14 | } = getState()
15 |
16 | const COMMON_EDITOR_OPTIONS = {
17 | fontSize,
18 | lineNumbers,
19 | tabSize,
20 | minimap: {
21 | enabled: minimap
22 | },
23 | wordWrap,
24 | theme,
25 | fontLigatures,
26 | fontFamily,
27 | cursorBlinking,
28 | cursorSmoothCaretAnimation,
29 |
30 | automaticLayout: true,
31 | fixedOverflowWidgets: true,
32 | scrollBeyondLastLine: false,
33 | roundedSelection: false,
34 | padding: {
35 | top: 16
36 | }
37 | }
38 |
39 | export const createEditor = (domElement) => domElement.createEditor({ ...COMMON_EDITOR_OPTIONS })
40 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
3 | "productName": "codi.link",
4 | "version": "0.1.0",
5 | "identifier": "midudev.codi.link",
6 | "build": {
7 | "frontendDist": "../dist",
8 | "devUrl": "http://localhost:5173",
9 | "beforeDevCommand": "bun dev",
10 | "beforeBuildCommand": "bun run build"
11 | },
12 | "app": {
13 | "windows": [
14 | {
15 | "title": "codi.link",
16 | "width": 800,
17 | "height": 600,
18 | "resizable": true,
19 | "fullscreen": false
20 | }
21 | ],
22 | "security": {
23 | "csp": null
24 | }
25 | },
26 | "bundle": {
27 | "active": true,
28 | "targets": "all",
29 | "icon": [
30 | "icons/32x32.png",
31 | "icons/128x128.png",
32 | "icons/128x128@2x.png",
33 | "icons/icon.icns",
34 | "icons/icon.ico"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/utils/string.js:
--------------------------------------------------------------------------------
1 | import Notification from './notification.js'
2 |
3 | export const capitalize = (str) => {
4 | return str
5 | .split('-')
6 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
7 | .join('')
8 | }
9 |
10 | /**
11 | * Search for a string line in a string
12 | * @param {string} str Search text
13 | * @param {string} input Where to search
14 | * @param {number} lines In how many lines do we want to search
15 | * @returns {string} Line finded
16 | */
17 | export const searchByLine = (str, input, lines = 10) => {
18 | const linesArr = str.split('\n')
19 | const parsedInput = input.endsWith(';') ? input.slice(0, -1) : input
20 | return linesArr.slice(0, lines).find((line) => line.includes(parsedInput))
21 | }
22 |
23 | export const copyToClipboard = (str) => {
24 | Notification.show({ type: 'info', message: 'Shareable URL has been copied to clipboard.' })
25 | return navigator.clipboard.writeText(str)
26 | }
27 |
--------------------------------------------------------------------------------
/src/constants/console-icons.js:
--------------------------------------------------------------------------------
1 | export const CONSOLE_BADGES = {
2 | 'log:log': { label: 'LOG', color: '#9cdcfe' },
3 | 'log:info': { label: 'INFO', color: '#4fc3f7' },
4 | 'log:warn': { label: 'WARN', color: '#ffa726' },
5 | 'log:error': { label: 'ERROR', color: '#ef5350' },
6 | 'log:debug': { label: 'DEBUG', color: '#ab47bc' },
7 | 'log:table': { label: 'TABLE', color: '#66bb6a' },
8 | 'log:count': { label: 'COUNT', color: '#26c6da' },
9 | 'log:trace': { label: 'TRACE', color: '#8d6e63' },
10 | 'log:dir': { label: 'DIR', color: '#78909c' },
11 | 'log:dirxml': { label: 'DIRXML', color: '#78909c' },
12 | 'log:time': { label: 'TIME', color: '#ffd54f' },
13 | 'log:assert': { label: 'ASSERT', color: '#ff7043' },
14 | error: null
15 | }
16 |
17 | export const createConsoleBadge = (type) => {
18 | const badge = CONSOLE_BADGES[type]
19 | if (!badge) return ''
20 | return `${badge.label} `
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/createHtml.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { generateConsoleScript } from '../console-script'
4 |
5 | /**
6 | * Create an index.html content from provided data
7 | * @param {object} params - The parameters
8 | * @param {string} params.css - CSS
9 | * @param {string} params.html - HTML content
10 | * @param {string} params.js - JavaScript
11 | * @param {boolean} isEditor - Whether the code is being run in the editor or preview
12 | * @returns {string}
13 | */
14 | export const createHtml = ({ css, html, js }, isEditor = false) => {
15 | return `
16 |
17 |
18 |
19 |
20 |
23 | ${isEditor ? generateConsoleScript({ html, css }) : ''}
24 |
25 |
26 | ${html}
27 |
30 |
31 | `
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/translator.js:
--------------------------------------------------------------------------------
1 | import en from '../language/en'
2 | import es from '../language/es'
3 | import pt from '../language/pt'
4 |
5 | const translations = {
6 | en,
7 | es,
8 | pt
9 | }
10 |
11 | function makeTranslation (key, language) {
12 | return translations[language][key] || key
13 | }
14 |
15 | function updateContent (language = 'en') {
16 | const elements = document.querySelectorAll('[data-translate]')
17 | elements.forEach(element => {
18 | const key = element.getAttribute('data-translate')
19 | element.innerText = makeTranslation(key, language)
20 | })
21 | }
22 |
23 | function updatePlaceholders (language = 'en') {
24 | const elements = document.querySelectorAll('[data-translate-placeholder]')
25 | elements.forEach(element => {
26 | const key = element.getAttribute('data-translate-placeholder')
27 | element.placeholder = makeTranslation(key, language)
28 | })
29 | }
30 |
31 | function translate (language) {
32 | updateContent(language)
33 | updatePlaceholders(language)
34 | }
35 |
36 | export { translate }
37 |
--------------------------------------------------------------------------------
/src/css/drag-drop-area.css:
--------------------------------------------------------------------------------
1 | .open-file-dragging {
2 | width: 100%;
3 | & .zone-drag-drop {
4 | width: 100%;
5 | height: 100vh;
6 | background: #00000065;
7 | color: #fff;
8 | opacity: 0.5;
9 | display: flex;
10 | flex-direction: column;
11 | align-items: center;
12 | justify-content: center;
13 | position: relative;
14 | transition: 0.3s opacity ease;
15 | &.focus {
16 | opacity: 1;
17 | }
18 | & strong {
19 | position: absolute;
20 | margin-bottom: 48px;
21 | font-size: 1.2rem;
22 | }
23 | & input {
24 | width: 100%;
25 | height: 100%;
26 | opacity: 0;
27 | }
28 | & svg {
29 | height: 64px;
30 | width: 64px;
31 | position: absolute;
32 | margin-top: 52px;
33 | }
34 | }
35 | }
36 |
37 | .overlay-drag {
38 | width: 100%;
39 | height: 100vh;
40 | display: flex;
41 | align-items: center;
42 | justify-content: center;
43 | background: #0000007a;
44 | position: absolute;
45 | top: 0;
46 | z-index: 99999;
47 |
48 | &.hidden {
49 | display: none;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/utils/dom.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @param {string} selector
3 | * @param {ParentNode} context
4 | */
5 | export const $ = (selector, context = document) =>
6 | context.querySelector(selector)
7 |
8 | export const $$ = (selector, context = document) =>
9 | context.querySelectorAll(selector)
10 |
11 | export const isNodeSelect = el => el.nodeName === 'SELECT'
12 | export const isNodeCheckbox = el => el.nodeName === 'INPUT' && el.type === 'checkbox'
13 | export const isNodeRadio = el => el.nodeName === 'INPUT' && el.type === 'radio'
14 |
15 | const updateSelectValue = (el, value) => {
16 | const optionToSelect = el.querySelector(`option[value="${value}"]`)
17 | if (!optionToSelect) return console.warn('Option to initialized not found')
18 | optionToSelect.setAttribute('selected', '')
19 | }
20 |
21 | export const setFormControlValue = (el, value) => {
22 | const isSelect = isNodeSelect(el)
23 | const isCheckbox = isNodeCheckbox(el)
24 | const isRadio = isNodeRadio(el)
25 |
26 | if (isSelect) updateSelectValue(el, value)
27 | else if (isCheckbox || isRadio) el.checked = value
28 | else el.value = value
29 | }
30 |
--------------------------------------------------------------------------------
/src/utils/run-js.js:
--------------------------------------------------------------------------------
1 | export default function runJs (code, timeout = 200) {
2 | const startTime = Date.now()
3 |
4 | return new Promise((resolve, reject) => {
5 | const worker = new window.Worker(new URL('./js-execution-worker.js', import.meta.url))
6 |
7 | const logError = (message) => {
8 | window.parent.postMessage({
9 | console: {
10 | type: 'loop',
11 | payload: { message }
12 | }
13 | }, document.location.origin)
14 | }
15 |
16 | const timeoutId = setTimeout(() => {
17 | worker.terminate()
18 | logError('Process terminated to avoid infinite loop')
19 |
20 | const executionTime = Date.now() - startTime
21 | reject(new Error(`Execution timed out after ${executionTime}ms`))
22 | }, timeout)
23 |
24 | worker.onmessage = (e) => {
25 | clearTimeout(timeoutId)
26 |
27 | const data = e.data
28 | worker.terminate()
29 |
30 | if (data.error) {
31 | logError(data.error)
32 | // reject(data.error)
33 | }
34 |
35 | resolve(data.result)
36 | }
37 |
38 | worker.postMessage({ code })
39 | })
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/codi-editor/extensions/editor-hotkeys.js:
--------------------------------------------------------------------------------
1 | import * as monaco from 'monaco-editor'
2 | import { $ } from '../../../utils/dom.js'
3 | import { copyToClipboard } from '../../../utils/string'
4 |
5 | export const initEditorHotKeys = (editor) => {
6 | // Shortcut: Open/Close Settings
7 | editor.addAction({
8 | id: 'toggle-settings',
9 | label: 'Toggle Settings',
10 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Comma],
11 | contextMenuGroupId: 'navigation',
12 | contextMenuOrder: 1.5,
13 | // Method that will be executed when the action is triggered.
14 | // @param editor The editor instance is passed in as a convenience
15 | run: () => {
16 | const $settingsButton = $("button[data-action='show-settings-bar']")
17 | $settingsButton && $settingsButton.click()
18 | }
19 | })
20 |
21 | // Shortcut: Copy URL
22 | editor.addCommand(
23 | monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyC,
24 | () => {
25 | const url = new URL(window.location.href)
26 | const urlToCopy = `https://codi.link${url.pathname}`
27 | copyToClipboard(urlToCopy)
28 | }
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/src/scroll.js:
--------------------------------------------------------------------------------
1 | import { $ } from './utils/dom.js'
2 |
3 | const $buttonUp = $('.button-up')
4 | const $buttonDown = $('.button-down')
5 |
6 | const previewersId = ['editor-preview', 'markup', 'script', 'style']
7 | let curretPreviewer = 0
8 |
9 | const updateButtonsStatus = (curretPreviewer) => {
10 | $buttonUp.disabled = curretPreviewer === 0
11 | $buttonDown.disabled = curretPreviewer === previewersId.length - 1
12 | }
13 |
14 | const updatePreviewer = (curretPreviewer) => {
15 | previewersId.forEach((previewer, index) => {
16 | const element = $(`#${previewer}`)
17 |
18 | if (curretPreviewer === index) {
19 | element.classList.remove('previewer-hide')
20 | element.classList.add('previewer-active')
21 | } else {
22 | element.classList.add('previewer-hide')
23 | element.classList.remove('previewer-active')
24 | }
25 | })
26 | }
27 |
28 | $buttonUp.addEventListener('click', (ev) => {
29 | curretPreviewer -= 1
30 | updateButtonsStatus(curretPreviewer)
31 | updatePreviewer(curretPreviewer)
32 | })
33 |
34 | $buttonDown.addEventListener('click', () => {
35 | curretPreviewer += 1
36 | updateButtonsStatus(curretPreviewer)
37 | updatePreviewer(curretPreviewer)
38 | })
39 |
--------------------------------------------------------------------------------
/src/utils/WindowPreviewer.js:
--------------------------------------------------------------------------------
1 | import { createHtml } from './createHtml'
2 |
3 | let previewUrl = null
4 | let previewWindowRef = null
5 |
6 | export function getPreviewUrl () {
7 | return previewUrl
8 | }
9 |
10 | export function updatePreview ({ html, css, js }) {
11 | if (previewUrl) {
12 | URL.revokeObjectURL(previewUrl)
13 | }
14 |
15 | const htmlForPreview = createHtml({ html, css, js }, true)
16 |
17 | const blob = new window.Blob([htmlForPreview], { type: 'text/html' })
18 |
19 | previewUrl = URL.createObjectURL(blob)
20 |
21 | if (previewWindowRef?.deref()) {
22 | previewWindowRef.deref().location = previewUrl
23 | }
24 | }
25 |
26 | export function clearPreview () {
27 | URL.revokeObjectURL(previewUrl)
28 | previewUrl = null
29 | }
30 |
31 | export function showPreviewerWindow () {
32 | const previewWindow = window.open(previewUrl, '_blank')
33 |
34 | // Use a WeafRef so when the user closes the window it could be garbage collected.
35 | // We need to hold a reference so we can update the location of the window when
36 | // the pewview changes.
37 | previewWindowRef = new window.WeakRef(previewWindow)
38 | const title = `${document.title} | Preview`
39 | previewWindow.document.title = title
40 | }
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "codi.link",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "private": true,
6 | "scripts": {
7 | "build": "vite build",
8 | "dev": "vite",
9 | "lint": "bunx standard",
10 | "tauri": "tauri",
11 | "phoenix": "rm -rf ./node_modules && bun install",
12 | "postinstall": "git config core.hooksPath ./git-hooks",
13 | "serve": "vite preview"
14 | },
15 | "devDependencies": {
16 | "@preact/preset-vite": "2.10.2",
17 | "@tauri-apps/cli": "2.9.4",
18 | "autoprefixer": "10.4.22",
19 | "eslint": "9.39.1",
20 | "neostandard": "0.12.2",
21 | "postcss": "8.5.6",
22 | "postcss-nesting": "13.0.2",
23 | "prettier": "3.6.2",
24 | "tailwindcss": "4.1.17",
25 | "vite": "7.2.2"
26 | },
27 | "dependencies": {
28 | "@tailwindcss/vite": "4.1.17",
29 | "client-zip": "2.5.0",
30 | "emmet-monaco-es": "5.6.1",
31 | "escape-html": "1.0.3",
32 | "js-base64": "3.7.8",
33 | "lit": "3.3.1",
34 | "monaco-editor": "0.54.0",
35 | "monacopilot": "1.2.7",
36 | "preact": "10.27.2",
37 | "preact-custom-element": "4.6.0",
38 | "split-grid": "1.0.11",
39 | "zustand": "5.0.8"
40 | },
41 | "postcss": {
42 | "plugins": {
43 | "postcss-nesting": {}
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/constants/grid-templates.js:
--------------------------------------------------------------------------------
1 | export const DEFAULT_LAYOUT = {
2 | columnGutters: [{
3 | track: 1,
4 | element: '.first-gutter'
5 | }, {
6 | track: 1,
7 | element: '.second-gutter'
8 | }],
9 | rowGutters: [{
10 | track: 1,
11 | element: '.last-gutter'
12 | }, {
13 | track: 1,
14 | element: '.second-gutter'
15 | }]
16 | }
17 |
18 | export const VERTICAL_LAYOUT = {
19 | columnGutters: [{
20 | track: 1,
21 | element: '.first-gutter'
22 | }, {
23 | track: 3,
24 | element: '.second-gutter'
25 | }, {
26 | track: 5,
27 | element: '.last-gutter'
28 | }]
29 | }
30 |
31 | export const HORIZONTAL_LAYOUT = {
32 | rowGutters: [{
33 | track: 1,
34 | element: '.first-gutter'
35 | }, {
36 | track: 3,
37 | element: '.second-gutter'
38 | }, {
39 | track: 5,
40 | element: '.last-gutter'
41 | }]
42 | }
43 |
44 | export const BOTTOM_LAYOUT = {
45 | columnGutters: [{
46 | track: 1,
47 | element: '.first-gutter'
48 | }, {
49 | track: 3,
50 | element: '.second-gutter'
51 | }],
52 | rowGutters: [{
53 | track: 1,
54 | element: '.last-gutter'
55 | }]
56 | }
57 |
58 | export const TABS_LAYOUT = {
59 | columnGutters: [{
60 | track: 1,
61 | element: '.first-gutter'
62 | }]
63 | }
64 |
--------------------------------------------------------------------------------
/src/css/notifications.css:
--------------------------------------------------------------------------------
1 | #notifications {
2 | position: relative;
3 | z-index: 999999;
4 | display: flex;
5 | flex-direction: column;
6 | }
7 |
8 | #notifications-wrapper {
9 | position: fixed;
10 | width: fit-content;
11 | height: fit-content;
12 | bottom: 10px;
13 | right: 10px;
14 | }
15 |
16 | .notification {
17 | display: flex;
18 | align-items: center;
19 | column-gap: 0.33rem;
20 | padding: 0.75rem;
21 | margin-bottom: 1rem;
22 | box-shadow: 0 0 15px 2px rgb(0 0 0 / 30%);
23 | background-color: #2f363d;
24 | color: #e1e4e8;
25 | transition: opacity cubic-bezier(0.215, 0.61, 0.455, 1);
26 | user-select: none;
27 | cursor: pointer;
28 | border-radius: 9999px;
29 | }
30 |
31 | .notification.animation-in {
32 | animation: fadeInSlideUp 0.4s ease;
33 | }
34 |
35 | .notification.animation-out {
36 | animation: fadeOutSlideDown 0.4s ease;
37 | }
38 |
39 | .icon-close {
40 | color: rgba(225, 228, 232, 0.822);
41 | margin-left: 0.5rem;
42 | }
43 |
44 | .icon-close:hover {
45 | color: rgb(225, 228, 232);
46 | }
47 |
48 | .notification__icon,
49 | .notification__message,
50 | .icon-close {
51 | display: flex;
52 | align-self: center;
53 | }
54 |
55 | .notification--info .notification__icon {
56 | color: #6199cb;
57 | }
58 |
59 | .notification--warning .notification__icon {
60 | color: #c3a001;
61 | }
62 |
63 | .notification--danger .notification__icon {
64 | color: #a34535;
65 | }
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | import { getState } from './state.js'
2 | import { $, setFormControlValue } from './utils/dom.js'
3 |
4 | const ELEMENT_TYPES = {
5 | INPUT: 'input',
6 | SELECT: 'select',
7 | CHECKBOX: 'checkbox',
8 | RADIO: 'radio'
9 | }
10 |
11 | /**
12 | * @type {HTMLFormElement}
13 | */
14 | const $settingsForm = $('#settings')
15 |
16 | const {
17 | updateSettings,
18 | ...settings
19 | } = getState()
20 |
21 | $settingsForm.addEventListener('submit', e => e.preventDefault())
22 | $settingsForm.addEventListener('input', updateSettingValue)
23 | $settingsForm.addEventListener('change', updateSettingValue)
24 |
25 | Array.from($settingsForm.elements).forEach((el) => {
26 | const { name: settingKey, value } = el
27 |
28 | if (!settingKey) return
29 |
30 | let actualSettingValue = settings[settingKey]
31 |
32 | if (settingKey === 'layout') {
33 | if (value === actualSettingValue.type) {
34 | actualSettingValue = true
35 | } else { return }
36 | }
37 |
38 | // Reflect the initial configuration in the settings section.
39 | setFormControlValue(el, actualSettingValue)
40 | })
41 |
42 | function updateSettingValue ({ target }) {
43 | const { value, checked, name: settingKey } = target
44 |
45 | const isCheckbox = target.type === ELEMENT_TYPES.CHECKBOX
46 | const isRadio = target.type === ELEMENT_TYPES.RADIO
47 |
48 | const settingValue = isCheckbox ? checked : value
49 |
50 | if (isRadio) {
51 | if (!checked) { return }
52 | }
53 |
54 | updateSettings({ key: settingKey, value: settingValue })
55 | }
56 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to codi.link
2 |
3 | First and foremost, thank you! We appreciate that you want to contribute to codi.link, your time is valuable, and your contributions mean a lot to us.
4 |
5 | ## Important!
6 | By contributing to this project, you:
7 |
8 | * Agree that you have authored 100% of the content
9 | * Agree that you have the necessary rights to the content
10 | * Agree that you have received the necessary permissions from your employer to make the contributions (if applicable)
11 | * Agree that the content you contribute may be provided under the Project license(s)
12 | * Agree that, if you did not author 100% of the content, the appropriate licenses and copyrights have been added along with any other necessary attribution.
13 |
14 | ## Getting Started
15 |
16 | **What does "contributing" mean?**
17 |
18 | Creating an issue is the simplest form of contributing to a project. But there are many ways to contribute, including the following:
19 |
20 | - Feature requests
21 | - Bug reports
22 |
23 | If you'd like to learn more about contributing in general, the [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing) has a lot of useful information.
24 |
25 | **Showing support for codi.link**
26 |
27 | Please keep in mind that open source software is built by people like you, who spend their free time creating things the rest the community can use.
28 |
29 | Don't have time to contribute? Don't worry, here are some other ways to show your support to codi.link:
30 |
31 | - ⭐ Star the [project](https://github.com/midudev/codi.link)
32 | - 🐦 Tweet your support for codi.link
33 |
34 |
--------------------------------------------------------------------------------
/assets/css.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/drag-file.js:
--------------------------------------------------------------------------------
1 | import { $ } from './utils/dom'
2 | import { eventBus, EVENTS } from './events-controller'
3 |
4 | const $inputFileDrop = $('#input-file-drop')
5 | const $overlayDrag = $('.overlay-drag')
6 | const $dragAndDropZone = $('.zone-drag-drop')
7 |
8 | $inputFileDrop.addEventListener('drop', (e) => {
9 | readFiles(e)
10 | $overlayDrag.classList.add('hidden')
11 | })
12 |
13 | $inputFileDrop.addEventListener('dragenter', () => {
14 | $dragAndDropZone.classList.add('focus')
15 | })
16 |
17 | $inputFileDrop.addEventListener('dragleave', () => {
18 | $dragAndDropZone.classList.remove('focus')
19 | })
20 |
21 | window.addEventListener('dragenter', (e) => {
22 | if (e.clientY >= 0 || e.clientX >= 0) {
23 | $overlayDrag.classList.remove('hidden')
24 | }
25 | })
26 |
27 | window.addEventListener('dragleave', ({ clientX, clientY }) => {
28 | if (clientY <= 0 || clientX <= 0 ||
29 | (clientX >= window.innerWidth || clientY >= window.innerHeight)) {
30 | $overlayDrag.classList.add('hidden')
31 | }
32 | })
33 |
34 | window.addEventListener('visibilitychange', () => {
35 | if (document.hidden) {
36 | $overlayDrag.classList.add('hidden')
37 | }
38 | })
39 |
40 | function readFiles (e) {
41 | const { files } = e.dataTransfer
42 | Object.values(files).forEach(file => {
43 | const { type: typeFile } = file
44 | const reader = new window.FileReader()
45 | reader.onload = ({ target }) => {
46 | printContent(target.result, typeFile)
47 | }
48 | reader.readAsBinaryString(file)
49 | })
50 | }
51 |
52 | function printContent (content, typeFile) {
53 | eventBus.emit(EVENTS.DRAG_FILE, { content, typeFile })
54 | }
55 |
--------------------------------------------------------------------------------
/src/utils/js-execution-worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 |
3 | function evalUserCode (code) {
4 | const consolePattern = /console\.\w+\(([^)]|\n)*\);?/g
5 | const whileTruePattern = /(? {
30 | return `/* ${match.trim()} */\n`
31 | })
32 |
33 | try {
34 | // eslint-disable-next-line no-new-func
35 | const func = new Function(parsedCode)
36 | const result = func()
37 | return { success: true, result }
38 | } catch (error) {
39 | return { success: false, error: error.message }
40 | }
41 | }
42 |
43 | self.onmessage = function (e) {
44 | const userCode = e.data.code
45 | const { success, result, error } = evalUserCode(userCode)
46 |
47 | if (success) {
48 | self.postMessage({ result })
49 | } else {
50 | self.postMessage({ error })
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/download.js:
--------------------------------------------------------------------------------
1 | import { createHtml } from './utils/createHtml.js'
2 |
3 | const getZip = () =>
4 | import('client-zip').then(({ downloadZip }) => downloadZip)
5 |
6 | const DEFAULT_ZIP_FILE_NAME = 'codi.link'
7 |
8 | export async function downloadUserCode ({
9 | htmlContent,
10 | cssContent,
11 | jsContent,
12 | zipFileName = DEFAULT_ZIP_FILE_NAME,
13 | zipInSingleFile = false
14 | }) {
15 | zipFileName = zipFileName === '' ? DEFAULT_ZIP_FILE_NAME : zipFileName
16 |
17 | const createZip = zipInSingleFile
18 | ? createZipWithSingleFile
19 | : createZipWithMultipleFiles
20 |
21 | const zipBlob = await createZip({ htmlContent, cssContent, jsContent })
22 | return generateZip({ zipBlob, zipFileName })
23 | }
24 |
25 | async function createZipWithSingleFile ({ htmlContent, cssContent, jsContent }) {
26 | const zip = await getZip()
27 | const indexHTML = createHtml({ css: cssContent, html: htmlContent, js: jsContent })
28 | return await zip({ name: 'index.html', input: indexHTML }).blob()
29 | }
30 |
31 | async function createZipWithMultipleFiles ({ htmlContent, cssContent, jsContent }) {
32 | const zip = await getZip()
33 |
34 | const indexHtml = `
35 |
36 |
37 |
38 |
39 |
40 | ${htmlContent}
41 |
42 |
43 | `
44 |
45 | return await zip([
46 | { name: 'style.css', input: cssContent },
47 | { name: 'script.js', input: jsContent },
48 | { name: 'index.html', input: indexHtml }
49 | ]).blob()
50 | }
51 |
52 | function generateZip ({ zipBlob, zipFileName }) {
53 | console.log({ zipBlob, zipFileName })
54 | const element = window.document.createElement('a')
55 | element.href = window.URL.createObjectURL(zipBlob)
56 | element.download = `${zipFileName}.zip`
57 | element.click()
58 | element.remove()
59 | }
60 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: 'Publish new executables'
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | publish-tauri:
9 | permissions:
10 | contents: write
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | include:
15 | - platform: 'macos-latest' # for Arm based macs (M1 and above).
16 | args: '--target aarch64-apple-darwin'
17 | - platform: 'macos-latest' # for Intel based macs.
18 | args: '--target x86_64-apple-darwin'
19 | - platform: 'ubuntu-22.04'
20 | args: ''
21 | - platform: 'windows-latest'
22 | args: ''
23 |
24 | runs-on: ${{ matrix.platform }}
25 | steps:
26 | - uses: actions/checkout@v4
27 |
28 | - name: Setup Node
29 | uses: actions/setup-node@v4
30 | with:
31 | node-version: lts/*
32 |
33 | - name: Install Bun
34 | uses: oven-sh/setup-bun@v1
35 |
36 | - name: Install Rust stable
37 | uses: dtolnay/rust-toolchain@stable
38 | with:
39 | # These targets are only used on macOS runners, so it's conditional to speed up other builds
40 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
41 |
42 | - name: Install dependencies (Ubuntu only)
43 | if: matrix.platform == 'ubuntu-22.04'
44 | run: |
45 | sudo apt-get update
46 | sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
47 |
48 | - name: Install frontend dependencies
49 | run: bun install
50 |
51 | - uses: tauri-apps/tauri-action@v0
52 | env:
53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54 | with:
55 | tagName: app-v__VERSION__ # the action automatically replaces __VERSION__ with the app version
56 | releaseName: 'App v__VERSION__'
57 | releaseBody: 'See the assets to download this version and install.'
58 | releaseDraft: true
59 | prerelease: false
60 | args: ${{ matrix.args }}
61 |
--------------------------------------------------------------------------------
/src/components/codi-editor/extensions/autocomplete-html-tag.js:
--------------------------------------------------------------------------------
1 | const HTML_LANGUAGE_ID = 'html'
2 |
3 | // https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
4 | const EMPTY_HTML_TAGS = [
5 | 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
6 | ]
7 |
8 | export const registerAutoCompleteHTMLTag = (monaco) => {
9 | monaco.languages.registerCompletionItemProvider(HTML_LANGUAGE_ID, completionProvider(monaco))
10 | }
11 |
12 | const completionProvider = (monaco) => {
13 | return {
14 | triggerCharacters: ['>'],
15 | provideCompletionItems: (model, position) => {
16 | const tagName = getMatchingTagName({ model, position })
17 | const isEmptyTag = checkIsEmptyTag(tagName)
18 |
19 | if (!tagName || isEmptyTag) {
20 | return
21 | }
22 |
23 | return buildCompletionList({ tagName, position }, monaco)
24 | }
25 | }
26 | }
27 |
28 | const getMatchingTagName = ({ model, position: { lineNumber, column } }) => {
29 | const textFromCurrentLineUntilPosition = model.getValueInRange({
30 | startLineNumber: lineNumber,
31 | endLineNumber: lineNumber,
32 | startColumn: 1,
33 | endColumn: column
34 | })
35 |
36 | return textFromCurrentLineUntilPosition.match(/.*<(\w+).*>$/)?.[1]
37 | }
38 |
39 | const checkIsEmptyTag = (tagName) => EMPTY_HTML_TAGS.includes(tagName)
40 |
41 | const buildCompletionList = ({ tagName, position: { lineNumber, column } }, monaco) => {
42 | const closingTag = `${tagName}>`
43 | const insertTextSnippet = `$0${closingTag}`
44 | const rangeInCurrentPosition = {
45 | startLineNumber: lineNumber,
46 | endLineNumber: lineNumber,
47 | startColumn: column,
48 | endColumn: column
49 | }
50 |
51 | return {
52 | suggestions: [
53 | {
54 | label: closingTag,
55 | kind: monaco.languages.CompletionItemKind.EnumMember,
56 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
57 | insertText: insertTextSnippet,
58 | range: rangeInCurrentPosition
59 | }
60 | ]
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/.github/workflows/pipeline.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | on:
4 | pull_request:
5 | branches: [main]
6 | types: [opened, synchronize]
7 | push:
8 | branches: [main]
9 |
10 | jobs:
11 | cancel_previous:
12 | name: Cancel previous redundant builds
13 | runs-on: ubuntu-22.04
14 | steps:
15 | - uses: styfle/cancel-workflow-action@0.9.1
16 | with:
17 | access_token: ${{ github.token }}
18 |
19 | build:
20 | needs: cancel_previous
21 | name: Build app
22 | runs-on: ubuntu-22.04
23 | steps:
24 | - name: Checkout git repository
25 | uses: actions/checkout@v4
26 | with:
27 | fetch-depth: 0
28 |
29 | - name: Install Bun
30 | uses: oven-sh/setup-bun@v1
31 |
32 | - name: Cache Bun dependencies
33 | uses: actions/cache@v4
34 | with:
35 | path: ~/.bun/install/cache
36 | key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
37 |
38 | - name: Install dependencies and build
39 | run: |
40 | bun install
41 | bun run build
42 |
43 | tauri:
44 | if: github.event_name == 'pull_request'
45 | needs: build
46 | name: Test Tauri build (compile only)
47 | runs-on: ubuntu-22.04
48 | steps:
49 | - name: Checkout repository
50 | uses: actions/checkout@v4
51 |
52 | - name: Install Bun
53 | uses: oven-sh/setup-bun@v1
54 |
55 | - name: Cache Cargo registry and target
56 | uses: actions/cache@v4
57 | with:
58 | path: |
59 | ~/.cargo/registry
60 | ~/.cargo/git
61 | src-tauri/target
62 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
63 |
64 | - name: Install Rust stable
65 | uses: dtolnay/rust-toolchain@stable
66 |
67 | - name: Install minimal system deps (Ubuntu only)
68 | run: |
69 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
70 |
71 | - name: Install frontend deps
72 | run: bun install
73 |
74 | - name: Compile Tauri crate (no bundling)
75 | run: cargo build --manifest-path src-tauri/Cargo.toml
--------------------------------------------------------------------------------
/src/language/en.js:
--------------------------------------------------------------------------------
1 | const en = {
2 | language: 'Language',
3 | english: 'English',
4 | spanish: 'Spanish',
5 | portuguese: 'Portuguese',
6 | editor: 'Editor',
7 | addDependency: 'Add dependency',
8 | addDependencyDescription:
9 | 'An import statement will be added to the top of the JavaScript editor for the package.',
10 | lineNumbers: 'Line numbers',
11 | on: 'On',
12 | off: 'Off',
13 | relative: 'Relative',
14 | interval: 'Interval',
15 | layout: 'Layout',
16 | console: 'Console',
17 | consoleDescription: 'Shows the result of the code execution.',
18 | dependencies: 'Dependencies',
19 | download: 'Download',
20 | history: 'History',
21 | preview: 'Preview',
22 | copyClipboard: 'Copy to clipboard',
23 | settings: 'Settings',
24 | historyDescription: 'Manages the sandboxes history.',
25 | clear: 'Clear',
26 | new: 'New',
27 | wordWrap: 'Word wrap',
28 | wordWrapColumn: 'wordWrapColumn',
29 | bounded: 'Bounded',
30 | fontSize: 'Font size',
31 | tabSize: 'Tab size',
32 | editorMinimap: 'Editor',
33 | showMinimap: 'Show minimap',
34 | fontLigatures: 'Font ligatures',
35 | enableFontLigatures: 'Enable font ligatures',
36 | maxExecutionTime: 'Max. execution time',
37 | maxExecutionTimeDesc:
38 | 'The maximum execution time in milliseconds to prevent the editor from freezing.',
39 | cursorBlinking: 'Cursor blinking',
40 | blink: 'Blink',
41 | smooth: 'Smooth',
42 | phase: 'Phase',
43 | expand: 'Expand',
44 | solid: 'Solid',
45 | cursorSmoothCaretAnimation: 'Cursor Smooth Caret Animation',
46 | explicit: 'Explicit',
47 | colorTheme: 'Color theme',
48 | dark: 'Dark',
49 | light: 'Light',
50 | highContrastDark: 'High Contrast Dark',
51 | highContrastLight: 'High Contrast Light',
52 | default: 'Default',
53 | workbench: 'Workbench',
54 | preserveGrid: 'Preserve Grid',
55 | preserveGridLayout: 'Preserve Grid Layout',
56 | featuresDownload: 'Features › Download',
57 | fileName: 'File name',
58 | Content: 'Content',
59 | exportOneZip: 'Export one single zipped file',
60 | featuresAutosave: 'Features › Autosave',
61 | localStorage: 'Local storage',
62 | automaticallySaveUrl:
63 | 'Automatically save URL to local storage for fast content loading',
64 | searchDependency: 'Search and add a package...'
65 | }
66 |
67 | export default en
68 |
--------------------------------------------------------------------------------
/src/components/layout-preview/layout-preview.css:
--------------------------------------------------------------------------------
1 | layout-preview {
2 | display: grid;
3 | height: var(--layout-preview-size, 40px);
4 | width: var(--layout-preview-size, 40px);
5 | padding: var(--layout-preview-padding, 5px);
6 | cursor: pointer;
7 | grid-template-columns: 1fr 1fr;
8 | grid-template-rows: 1fr 1fr;
9 | grid-template-areas: 'html js' 'css result';
10 | gap: var(--layout-preview-gap, 2px);
11 | }
12 |
13 | layout-preview[active] {
14 | background-color: var(--layout-preview-background-color, #797b80);
15 | border-radius: var(--layout-preview-border-radius, 5px);
16 | }
17 |
18 | layout-preview[layout=layout-2] {
19 | grid-template-areas: 'html css' 'js result';
20 | }
21 |
22 | layout-preview[layout=vertical] {
23 | grid-template-columns: repeat(4, 1fr);
24 | grid-template-rows: 1fr;
25 | grid-template-areas: 'html css js result';
26 | }
27 |
28 | layout-preview[layout=horizontal] {
29 | grid-template-rows: repeat(4, 1fr);
30 | grid-template-columns: 1fr;
31 | grid-template-areas: 'html' 'css' 'js' 'result';
32 | }
33 |
34 | layout-preview[layout=bottom] {
35 | grid-template-columns: repeat(3, 1fr);
36 | grid-template-rows: repeat(2, 1fr);
37 | grid-template-areas: 'result result result' 'html js css';
38 | }
39 |
40 | layout-preview[layout=tabs]{
41 | grid-template-columns: repeat(3, 1fr);
42 | grid-template-rows: repeat(2, 1fr);
43 | grid-template-areas: 'html html result' 'html html result';
44 | }
45 |
46 | layout-preview[layout=tabs] .css,
47 | layout-preview[layout=tabs] .js {
48 | position: absolute;
49 | width: 5px;
50 | height: 5px;
51 | top: 5px;
52 | border-left: 1px solid black;
53 | border-bottom: 1px solid black;
54 | }
55 |
56 | layout-preview[layout=tabs] .css {
57 | left: calc(33.33% - 3px);
58 | }
59 |
60 | layout-preview[layout=tabs] .js {
61 | left: calc(33.33% + 4px);
62 | }
63 |
64 | layout-preview .html {
65 | grid-area: html;
66 | background-color: var(--layout-preview-background-color-html, #e34f26);
67 | }
68 |
69 | layout-preview .css {
70 | grid-area: css;
71 | background-color: var(--layout-preview-background-color-css, #30a9dc);
72 | }
73 |
74 | layout-preview .js {
75 | grid-area: js;
76 | background-color: var(--layout-preview-background-color-js, #f7df1e);
77 | }
78 |
79 | layout-preview .result {
80 | grid-area: result;
81 | background-color: var(--layout-preview-background-color-result, #ffffff);
82 | }
83 |
--------------------------------------------------------------------------------
/src/language/pt.js:
--------------------------------------------------------------------------------
1 | const pt = {
2 | language: 'Idioma',
3 | english: 'Inglês',
4 | spanish: 'Espanhol',
5 | portuguese: 'Português',
6 | editor: 'Editor',
7 | addDependency: 'Adicionar dependência',
8 | addDependencyDescription:
9 | 'Uma declaração de importação será adicionada ao topo do editor JavaScript para o pacote.',
10 | lineNumbers: 'Números de linha',
11 | on: 'Ligado',
12 | off: 'Desligado',
13 | relative: 'Relativo',
14 | interval: 'Intervalo',
15 | layout: 'Layout',
16 | console: 'Console',
17 | consoleDescription: 'Mostra o resultado da execução do código.',
18 | dependencies: 'Dependências',
19 | download: 'Baixar',
20 | history: 'Histórico',
21 | preview: 'Pré-visualização',
22 | copyClipboard: 'Copiar para a área de transferência',
23 | settings: 'Configurações',
24 | historyDescription: 'Gerencia o histórico das sandboxes.',
25 | clear: 'Limpar',
26 | new: 'Novo',
27 | wordWrap: 'Quebra de linha',
28 | wordWrapColumn: 'Coluna de quebra de linha',
29 | bounded: 'Limitado',
30 | fontSize: 'Tamanho da fonte',
31 | tabSize: 'Tamanho da tabulação',
32 | editorMinimap: 'Editor',
33 | showMinimap: 'Mostrar minimapa',
34 | fontLigatures: 'Ligações de fonte',
35 | enableFontLigatures: 'Habilitar ligações de fonte',
36 | maxExecutionTime: 'Tempo máximo de execução',
37 | maxExecutionTimeDesc:
38 | 'O tempo máximo de execução em milissegundos para evitar que o editor trave.',
39 | cursorBlinking: 'Piscamento do cursor',
40 | blink: 'Piscada',
41 | smooth: 'Suave',
42 | phase: 'Fase',
43 | expand: 'Expandir',
44 | solid: 'Sólido',
45 | cursorSmoothCaretAnimation: 'Animação suave do cursor',
46 | explicit: 'Explícito',
47 | colorTheme: 'Tema de cor',
48 | dark: 'Escuro',
49 | light: 'Claro',
50 | highContrastDark: 'Alto contraste escuro',
51 | highContrastLight: 'Alto contraste claro',
52 | default: 'Padrão',
53 | workbench: 'Área de trabalho',
54 | preserveGrid: 'Preservar grade',
55 | preserveGridLayout: 'Preservar layout da grade',
56 | featuresDownload: 'Recursos › Baixar',
57 | fileName: 'Nome do arquivo',
58 | Content: 'Conteúdo',
59 | exportOneZip: 'Exportar um único arquivo zipado',
60 | featuresAutosave: 'Recursos › Autossalvar',
61 | localStorage: 'Armazenamento local',
62 | automaticallySaveUrl:
63 | 'Salvar automaticamente a URL no armazenamento local para carregamento rápido de conteúdo',
64 | searchDependency: 'Pesquisar e adicionar um pacote...'
65 | }
66 |
67 | export default pt
68 |
--------------------------------------------------------------------------------
/src/components/codi-editor/CodiEditor.js:
--------------------------------------------------------------------------------
1 | import { LitElement, html } from 'lit'
2 | import * as monaco from 'monaco-editor'
3 | import { emmetHTML } from 'emmet-monaco-es'
4 | import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
5 | import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
6 | import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
7 | import JsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
8 | import { registerAutoCompleteHTMLTag } from './extensions/autocomplete-html-tag.js'
9 | import { initEditorHotKeys } from './extensions/editor-hotkeys.js'
10 | import { CodiEditorStyles } from './CodiEditor.styles.js'
11 | import { registerThemes } from './extensions/register-themes.js'
12 |
13 | const iconUrls = {
14 | css: new URL('../../../assets/css.svg', import.meta.url),
15 | html: new URL('../../../assets/html5.svg', import.meta.url),
16 | javascript: new URL('../../../assets/js.svg', import.meta.url)
17 | }
18 |
19 | export class CodiEditor extends LitElement {
20 | static get styles () {
21 | return CodiEditorStyles
22 | }
23 |
24 | static get properties () {
25 | return {
26 | language: {
27 | type: String,
28 | reflects: true
29 | },
30 | value: {
31 | type: String
32 | },
33 | class: {
34 | type: String
35 | }
36 | }
37 | }
38 |
39 | render () {
40 | const iconUrl = iconUrls[this.language]
41 | return html` `
42 | }
43 |
44 | constructor () {
45 | super()
46 | this.constructor.initEditor()
47 | }
48 |
49 | createEditor (options) {
50 | this.editor = monaco.editor.create(this, {
51 | value: this.value,
52 | language: this.language,
53 | ...options
54 | })
55 | initEditorHotKeys(this.editor)
56 | return this.editor
57 | }
58 |
59 | static initEditor () {
60 | if (!this.editorInitialized) {
61 | window.MonacoEnvironment = {
62 | getWorker (_, label) {
63 | switch (label) {
64 | case 'html': return new HtmlWorker()
65 | case 'javascript': return new JsWorker()
66 | case 'css': return new CssWorker()
67 | default: return new EditorWorker()
68 | }
69 | }
70 | }
71 |
72 | registerThemes(monaco)
73 | emmetHTML(monaco)
74 | registerAutoCompleteHTMLTag(monaco)
75 | this.editorInitialized = true
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/css/settings.css:
--------------------------------------------------------------------------------
1 | .settings {
2 | & .settings-content {
3 | display: grid;
4 | place-content: center;
5 | height: 100%;
6 | }
7 |
8 | & .settings-type {
9 | opacity: 0.6;
10 | }
11 |
12 | & .settings-item {
13 | display: flex;
14 | flex-direction: column;
15 | width: 100%;
16 | max-width: 500px;
17 | padding: 1em;
18 |
19 | & .layout-preview-container {
20 | display: flex;
21 | justify-content: space-between;
22 | padding: 10px 0;
23 |
24 | & label {
25 | border-radius: 5px;
26 | position: relative;
27 | transition: 0.3s ease opacity, 0.3s ease background-color;
28 | }
29 |
30 | & label:hover {
31 | background-color: #000;
32 | opacity: 0.9;
33 | }
34 |
35 | & input[type='radio'] {
36 | cursor: pointer;
37 | height: 100%;
38 | left: 0;
39 | margin: 0;
40 | opacity: 0;
41 | padding: 0;
42 | position: absolute;
43 | top: 0;
44 | width: 100%;
45 | }
46 | }
47 | }
48 |
49 | & .checkbox {
50 | display: flex;
51 | align-items: center;
52 | }
53 |
54 | & .checkbox input {
55 | border: 0;
56 | clip: rect(0, 0, 0, 0);
57 | height: 1px;
58 | width: 1px;
59 | margin: -1px;
60 | padding: 0;
61 | position: absolute;
62 | overflow: hidden;
63 | white-space: nowrap;
64 | }
65 |
66 | & .checkbox span {
67 | display: flex;
68 | align-items: center;
69 | }
70 |
71 | & .checkbox input:focus + span::before {
72 | content: '';
73 | outline: 1px solid #fff;
74 | }
75 |
76 | & .checkbox input:checked + span::before {
77 | content: '✓';
78 | }
79 |
80 | & .checkbox span::before {
81 | align-items: center;
82 | background: var(--input-background);
83 | border-radius: 3px;
84 | border: 1px solid var(--input-border);
85 | color: var(--input-foreground);
86 | content: '';
87 | display: flex;
88 | height: 18px;
89 | justify-content: center;
90 | margin-right: 9px;
91 | min-width: 18px;
92 | width: 18px;
93 | }
94 |
95 | & .input {
96 | background-color: var(--input-background);
97 | color: var(--input-foreground);
98 | border: 1px solid var(--input-border);
99 | padding: 0.3em;
100 | }
101 |
102 | & .setting-description {
103 | opacity: 0.6;
104 | margin: 0;
105 | padding-top: 0.5em;
106 | font-size: 0.9em;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/language/es.js:
--------------------------------------------------------------------------------
1 | const es = {
2 | language: 'Idioma',
3 | english: 'Inglés',
4 | spanish: 'Español',
5 | portuguese: 'Portugués',
6 | editor: 'Editor',
7 | addDependency: 'Añadir dependencia',
8 | addDependencyDescription:
9 | 'Se agregará una declaración de importación en la parte superior del editor de JavaScript para el paquete.',
10 | lineNumbers: 'Números de línea',
11 | on: 'Activado',
12 | off: 'Desactivado',
13 | relative: 'Relativo',
14 | interval: 'Intervalo',
15 | layout: 'Diseño',
16 | console: 'Consola',
17 | consoleDescription: 'Muestra el resultado de la ejecución del código.',
18 | dependencies: 'Dependencias',
19 | download: 'Descargar',
20 | history: 'Historial',
21 | preview: 'Vista previa',
22 | copyClipboard: 'Copiar al portapapeles',
23 | settings: 'Ajustes',
24 | historyDescription: 'Administra el historial de los entornos de pruebas.',
25 | clear: 'Limpiar',
26 | new: 'Nuevo',
27 | wordWrap: 'Ajuste de línea',
28 | wordWrapColumn: 'wordWrapColumn',
29 | bounded: 'Limitado',
30 | fontSize: 'Tamaño de fuente',
31 | tabSize: 'Tamaño de tabulación',
32 | editorMinimap: 'Editor',
33 | showMinimap: 'Mostrar minimapa',
34 | fontLigatures: 'Ligaduras de fuente',
35 | enableFontLigatures: 'Habilitar ligaduras de fuente',
36 | maxExecutionTime: 'Tiempo máx. de ejecución',
37 | maxExecutionTimeDesc:
38 | 'El tiempo máximo de ejecución en milisegundos para evitar que el editor se congele.',
39 | cursorBlinking: 'Parpadeo del cursor',
40 | blink: 'Parpadeo',
41 | smooth: 'Suave',
42 | phase: 'Fase',
43 | expand: 'Expandir',
44 | solid: 'Sólido',
45 | cursorSmoothCaretAnimation: 'Animación suave del cursor',
46 | explicit: 'Explícito',
47 | colorTheme: 'Tema de color',
48 | dark: 'Oscuro',
49 | light: 'Claro',
50 | highContrastDark: 'Alto contraste oscuro',
51 | highContrastLight: 'Alto contraste claro',
52 | default: 'Predeterminado',
53 | workbench: 'Área de trabajo',
54 | preserveGrid: 'Preservar cuadrícula',
55 | preserveGridLayout: 'Preservar diseño de cuadrícula',
56 | featuresDownload: 'Características › Descargar',
57 | fileName: 'Nombre del archivo',
58 | Content: 'Contenido',
59 | exportOneZip: 'Exportar un solo archivo comprimido',
60 | featuresAutosave: 'Características › Guardado automático',
61 | localStorage: 'Almacenamiento local',
62 | automaticallySaveUrl:
63 | 'Guardar automáticamente la URL en el almacenamiento local para una carga rápida del contenido',
64 | searchDependency: 'Buscar y agregar un paquete...'
65 | }
66 |
67 | export default es
68 |
--------------------------------------------------------------------------------
/src/aside.js:
--------------------------------------------------------------------------------
1 | import { eventBus, EVENTS } from './events-controller.js'
2 | import { $, $$ } from './utils/dom.js'
3 | import * as Preview from './utils/WindowPreviewer'
4 | import { BUTTON_ACTIONS } from './constants/button-actions.js'
5 | import { copyToClipboard } from './utils/string.js'
6 | import { resetConsoleBadge } from './console.js'
7 |
8 | const $aside = $('aside')
9 | const $asideBar = $('.aside-bar')
10 | const $buttons = $$('button', $aside)
11 | const $editorAsideButton = $('#editor-aside-button')
12 |
13 | const toggleAsideBar = isHidden => {
14 | $asideBar.toggleAttribute('hidden', isHidden)
15 | }
16 |
17 | const SIMPLE_CLICK_ACTIONS = {
18 | [BUTTON_ACTIONS.downloadUserCode]: () => {
19 | eventBus.emit(EVENTS.DOWNLOAD_USER_CODE)
20 | },
21 |
22 | [BUTTON_ACTIONS.openIframeTab]: () => {
23 | Preview.showPreviewerWindow()
24 | },
25 |
26 | [BUTTON_ACTIONS.copyToClipboard]: async () => {
27 | const url = new URL(window.location.href)
28 | const urlToCopy = `https://codi.link${url.pathname}`
29 | copyToClipboard(urlToCopy)
30 | },
31 |
32 | [BUTTON_ACTIONS.clearHistory]: () => {
33 | eventBus.emit(EVENTS.CLEAR_HISTORY)
34 | },
35 |
36 | [BUTTON_ACTIONS.openNewInstance]: () => {
37 | eventBus.emit(EVENTS.OPEN_NEW_INSTANCE)
38 | }
39 | }
40 |
41 | const NON_SIMPLE_CLICK_ACTIONS = {
42 | [BUTTON_ACTIONS.closeAsideBar]: () => {
43 | toggleAsideBar(true)
44 | $('.scroll-buttons-container').removeAttribute('hidden')
45 | },
46 |
47 | [BUTTON_ACTIONS.showSkypackBar]: () => {
48 | showAsideBar('#skypack')
49 | $('#skypack-search-input').focus()
50 | $('.scroll-buttons-container').setAttribute('hidden', '')
51 | },
52 |
53 | [BUTTON_ACTIONS.showSettingsBar]: () => {
54 | showAsideBar('#settings')
55 | $('.scroll-buttons-container').setAttribute('hidden', '')
56 | },
57 | [BUTTON_ACTIONS.showConsoleBar]: () => {
58 | showAsideBar('#console')
59 | $('.scroll-buttons-container').setAttribute('hidden', '')
60 | resetConsoleBadge()
61 | },
62 |
63 | [BUTTON_ACTIONS.showHistoryBar]: () => {
64 | showAsideBar('#history')
65 | $('.scroll-buttons-container').setAttribute('hidden', '')
66 | }
67 | }
68 |
69 | const showAsideBar = selector => {
70 | $asideBar.removeAttribute('hidden')
71 | $$('.bar-content').forEach(el => el.setAttribute('hidden', ''))
72 | $(selector).removeAttribute('hidden')
73 | }
74 |
75 | const ACTIONS = {
76 | ...SIMPLE_CLICK_ACTIONS,
77 | ...NON_SIMPLE_CLICK_ACTIONS
78 | }
79 |
80 | $buttons.forEach(button => {
81 | button.addEventListener('click', ({ currentTarget }) => {
82 | let action = button.getAttribute('data-action')
83 | const isSimpleClickAction =
84 | button.getAttribute('data-is-simple-click-action') === 'true'
85 |
86 | if (isSimpleClickAction) return ACTIONS[action]()
87 |
88 | const alreadyActive = currentTarget.classList.contains('is-active')
89 | $('.is-active').classList.remove('is-active')
90 |
91 | const buttonToActive = alreadyActive ? $editorAsideButton : currentTarget
92 | buttonToActive.classList.add('is-active')
93 |
94 | action = alreadyActive ? 'close-aside-bar' : action
95 |
96 | ACTIONS[action]()
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/src/utils/notification.js:
--------------------------------------------------------------------------------
1 | import { $ } from './dom'
2 |
3 | const STATE_ICONS = {
4 | info: ' ',
5 | warning: ' ',
6 | danger: ' '
7 | }
8 |
9 | const TRANSITION_DURATION = 400 // ms
10 | const NOTIFICATION_DURATION = 300000 // ms
11 |
12 | export default {
13 | /**
14 | * Display a notification
15 | * @param {Object} options - The options object
16 | * @param {string} options.type - Notification type: info, warning, danger
17 | * @param {string} options.message - Message to display
18 | */
19 | show: ({ type, message }) => {
20 | const notifications = $('#notifications')
21 | const notification = document.createElement('div')
22 |
23 | notification.className = `notification notification--${type}`
24 | notification.innerHTML = `
25 |
26 | ${STATE_ICONS[type]}
27 |
28 |
29 | ${message}
30 |
31 |
32 |
33 |
34 | `
35 |
36 | let wrapper = $('#notifications-wrapper')
37 |
38 | if (!wrapper) {
39 | wrapper = document.createElement('div')
40 | wrapper.setAttribute('id', 'notifications-wrapper')
41 | notifications.appendChild(wrapper)
42 | }
43 |
44 | notification.classList.add('animation-in')
45 |
46 | // Accesibility attributes
47 | notification.setAttribute('role', 'alert')
48 | notification.setAttribute('aria-live', 'assertive')
49 | notification.setAttribute('aria-atomic', 'true')
50 |
51 | setTimeout(() => {
52 | notification.classList.remove('animation-in')
53 | notification.classList.add('animation-out')
54 | }, NOTIFICATION_DURATION - TRANSITION_DURATION / 2)
55 |
56 | // Remove notification after NOTIFICATION_DURATION
57 | setTimeout(() => {
58 | notification.remove()
59 | }, NOTIFICATION_DURATION)
60 |
61 | notification.querySelector('.icon-close').addEventListener('click', () => {
62 | notification.classList.add('bounce-leave')
63 | setTimeout(() => {
64 | notification.remove()
65 | }, TRANSITION_DURATION / 2)
66 | })
67 |
68 | wrapper.insertAdjacentElement('beforeend', notification)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/css/skypack.css:
--------------------------------------------------------------------------------
1 | .skypack {
2 | & .skypack-header {
3 | position: sticky;
4 | top: 0;
5 | z-index: 1;
6 | padding-top: 1em;
7 | }
8 |
9 | & .skypack-type {
10 | padding-bottom: 8px;
11 | opacity: 0.6;
12 | }
13 |
14 | & .skypack-item {
15 | color: #fff;
16 | display: flex;
17 | flex-direction: column;
18 | width: 100%;
19 | padding: 0 1em 1em;
20 | }
21 |
22 | & .skypack-item strong {
23 | display: block;
24 | overflow: hidden;
25 | text-overflow: ellipsis;
26 | }
27 |
28 | & .input {
29 | display: flex;
30 | background-color: var(--input-background);
31 | border: 1px solid var(--input-border);
32 | color: var(--input-foreground);
33 | padding: 0.5em;
34 | margin-top: 16px;
35 | font-size: 1rem;
36 | }
37 |
38 | & .input .input-icon {
39 | fill: rgba(128, 128, 128, 0.5);
40 | }
41 |
42 | & .input input {
43 | width: 100%;
44 | padding: 0.05em;
45 | padding-left: 8px;
46 | border: none;
47 | background-color: transparent;
48 | outline: none;
49 | color: var(--input-foreground);
50 | }
51 |
52 | & .search-results-message {
53 | font-weight: 500;
54 | font-size: 14px;
55 | margin: 4px 0 0;
56 | padding-bottom: 8px;
57 | }
58 |
59 | & .search-results.hidden {
60 | display: none;
61 | }
62 |
63 | & .search-results .extensions ul {
64 | list-style: none;
65 | margin: 0.5em 0;
66 | padding: 0;
67 | }
68 |
69 | & .search-results .extensions ul li {
70 | margin-bottom: 0.5em;
71 | padding: 0.5em;
72 | cursor: pointer;
73 | text-overflow: ellipsis;
74 | overflow: hidden;
75 | white-space: nowrap;
76 | display: flex;
77 | flex-direction: column;
78 |
79 | & header {
80 | display: flex;
81 | align-items: center;
82 |
83 | & .skypack-result-badges {
84 | display: flex;
85 | align-items: center;
86 | padding-left: 0.25rem;
87 |
88 | & .skypack-badge {
89 | margin-left: 0.25rem;
90 | margin-right: 0.25rem;
91 | border: 0px;
92 | border-radius: 0.3em;
93 | font-size: 0.7em;
94 | padding: 0.125em 0.375em 0.12em;
95 |
96 | &.popular {
97 | background-color: #80b918;
98 | color: #000;
99 | }
100 |
101 | &.deprecated {
102 | background-color: #c81d25;
103 | color: #fff;
104 | }
105 |
106 | &.exact-match {
107 | background-color: #ffc300;
108 | color: #000;
109 | }
110 |
111 | &.typescript {
112 | width: 18px;
113 | height: 18px;
114 | background: url(/assets/typescript.svg) no-repeat 50% 50%;
115 | }
116 | }
117 | }
118 | }
119 |
120 | & .skypack-description {
121 | font-size: 0.9em;
122 | text-overflow: ellipsis;
123 | overflow: hidden;
124 | }
125 |
126 | & .skypack-updated {
127 | font-size: 0.8em;
128 | opacity: 0.5;
129 | }
130 |
131 | & footer {
132 | display: flex;
133 | justify-content: space-between;
134 | align-items: flex-end;
135 | }
136 |
137 | & .skypack-open {
138 | font-size: 0.7em;
139 | opacity: 0;
140 | text-decoration: none;
141 |
142 | &:hover {
143 | text-decoration: underline;
144 | }
145 | }
146 | }
147 |
148 | & .search-results ul li:focus,
149 | & .search-results ul li:hover {
150 | background-color: rgba(128, 128, 128, 0.14);
151 |
152 | & .skypack-open {
153 | opacity: 1;
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/css/history.css:
--------------------------------------------------------------------------------
1 | .history {
2 | & .history-header {
3 | position: sticky;
4 | top: 0;
5 | z-index: 1;
6 | padding-top: 1em;
7 | padding-left: 0.5em;
8 | }
9 |
10 | & .history-item {
11 | color: #fff;
12 | display: flex;
13 | flex-direction: column;
14 | width: 100%;
15 | padding: 0 0 1em 0.5em;
16 | }
17 |
18 | & .history-item strong {
19 | display: block;
20 | overflow: hidden;
21 | text-overflow: ellipsis;
22 | }
23 |
24 | & .history-actions {
25 | display: flex;
26 | align-items: center;
27 | justify-content: flex-end;
28 | gap: 6px;
29 | margin-top: 1em;
30 |
31 | & button {
32 | background: rgba(255, 255, 255, 0.05);
33 | display: flex;
34 | align-items: center;
35 | gap: 6px;
36 | border: 1px solid rgba(255, 255, 255, 0.05);
37 | color: #fff;
38 | cursor: pointer;
39 | padding: 6px 8px 6px 6px;
40 | transition: 0.2s ease background-color, 0.2s ease scale;
41 | border-radius: 4px;
42 |
43 | &:hover {
44 | background-color: rgba(255, 255, 255, 0.1);
45 | }
46 |
47 | &:active {
48 | background-color: rgba(255, 255, 255, 0.2);
49 | scale: 0.95;
50 | }
51 |
52 | & svg {
53 | width: 16px;
54 | height: 16px;
55 | }
56 | }
57 | }
58 |
59 | & .history-list {
60 | list-style: none;
61 | padding: 0;
62 | display: grid;
63 |
64 | & .group {
65 | margin-bottom: 8px;
66 | }
67 |
68 | & .group h4 {
69 | margin: 0;
70 | font-size: 12px;
71 | opacity: 0.5;
72 | font-weight: 600;
73 | margin: 0;
74 | padding: 0.5em 0 0.5em 0.5em;
75 | }
76 |
77 | & li {
78 | transition: 0.2s ease background-color;
79 | display: flex;
80 | align-items: center;
81 | width: 100%;
82 | gap: 6px;
83 | cursor: pointer;
84 | border-radius: 4px;
85 |
86 | &:hover {
87 | background-color: rgba(255, 255, 255, 0.05);
88 |
89 | & .actions {
90 | opacity: 1;
91 | }
92 | }
93 |
94 | &.is-active {
95 | background-color: rgba(255, 255, 255, 0.1);
96 | }
97 |
98 | & button {
99 | color: #fff;
100 | border: none;
101 | background: none;
102 | text-decoration: none;
103 | word-break: break-all;
104 | flex-grow: 1;
105 | padding: 0.8em 0 0.8em 0.5em;
106 | white-space: nowrap;
107 | overflow: hidden;
108 | text-overflow: ellipsis;
109 | text-align: start;
110 | cursor: pointer;
111 | }
112 |
113 | & input {
114 | background: transparent;
115 | border: none;
116 | color: #fff;
117 | flex-grow: 1;
118 | padding: 0.8em 0 0.8em 0.5em;
119 | outline: none;
120 | }
121 |
122 | & .actions {
123 | transition: 0.1s ease opacity;
124 | display: inline-flex;
125 | align-items: center;
126 | flex-shrink: 0;
127 | padding-right: 0.5em;
128 | opacity: 0;
129 | gap: 6px;
130 |
131 | & button {
132 | transition: 0.2s ease opacity, 0.2s ease scale;
133 | border: none;
134 | background: none;
135 | color: #fff;
136 | cursor: pointer;
137 | line-height: 0;
138 | padding: 3px;
139 |
140 | &:hover {
141 | opacity: 0.7;
142 | }
143 |
144 | &:active {
145 | scale: 0.9;
146 | }
147 |
148 | & svg {
149 | width: 18px;
150 | height: 18px;
151 | }
152 |
153 | }
154 | }
155 | }
156 |
157 | }
158 | }
--------------------------------------------------------------------------------
/src/events-controller.js:
--------------------------------------------------------------------------------
1 | import { decode } from 'js-base64'
2 | import { capitalize, searchByLine } from './utils/string.js'
3 | import { downloadUserCode } from './download.js'
4 | import { getState } from './state.js'
5 | import { getHistoryState } from './history.js'
6 |
7 | class EventBus extends window.EventTarget {
8 | on (type, listener) {
9 | this.addEventListener(type, listener)
10 | }
11 |
12 | off (type, listener) {
13 | this.removeEventListener(type, listener)
14 | }
15 |
16 | emit (type, detail) {
17 | const event = new window.CustomEvent(type, { detail, cancelable: true })
18 |
19 | this.dispatchEvent(event)
20 | }
21 | }
22 |
23 | export const eventBus = new EventBus()
24 |
25 | let jsEditor
26 | let htmlEditor
27 | let cssEditor
28 |
29 | export const initializeEventsController = ({
30 | jsEditor: _jsEditor,
31 | htmlEditor: _htmlEditor,
32 | cssEditor: _cssEditor
33 | }) => {
34 | jsEditor = _jsEditor
35 | htmlEditor = _htmlEditor
36 | cssEditor = _cssEditor
37 | }
38 |
39 | export const EVENTS = {
40 | ADD_SKYPACK_PACKAGE: 'ADD_SKYPACK_PACKAGE',
41 | DOWNLOAD_USER_CODE: 'DOWNLOAD_USER_CODE',
42 | DRAG_FILE: 'DRAG_FILE',
43 | OPEN_EXISTING_INSTANCE: 'OPEN_EXISTING_INSTANCE',
44 | OPEN_NEW_INSTANCE: 'OPEN_NEW_INSTANCE',
45 | CLEAR_HISTORY: 'CLEAR_HISTORY'
46 | }
47 |
48 | eventBus.on(
49 | EVENTS.ADD_SKYPACK_PACKAGE,
50 | ({ detail: { skypackPackage, url } }) => {
51 | const importStatement = `import ${capitalize(skypackPackage).replaceAll('.', '_')} from '${url}';`
52 | const existPackage = searchByLine(jsEditor.getValue(), url)
53 | if (!existPackage) {
54 | jsEditor.setValue(`${importStatement}\n${jsEditor.getValue()}`)
55 | }
56 | }
57 | )
58 |
59 | eventBus.on(EVENTS.DOWNLOAD_USER_CODE, () => {
60 | const { zipInSingleFile, zipFileName } = getState()
61 |
62 | downloadUserCode({
63 | zipFileName,
64 | zipInSingleFile,
65 | htmlContent: htmlEditor.getValue(),
66 | cssContent: cssEditor.getValue(),
67 | jsContent: jsEditor.getValue()
68 | })
69 | })
70 |
71 | eventBus.on(EVENTS.DRAG_FILE, ({ detail: { content, typeFile } }) => {
72 | const file = typeFile
73 |
74 | switch (file) {
75 | case 'text/javascript':
76 | jsEditor.setValue(content)
77 | break
78 | case 'text/css':
79 | cssEditor.setValue(content)
80 | break
81 | case 'text/html':
82 | htmlEditor.setValue(content)
83 | break
84 | default:
85 | break
86 | }
87 | })
88 |
89 | eventBus.on(EVENTS.OPEN_NEW_INSTANCE, () => {
90 | const htmlContent = htmlEditor.getValue()
91 | const cssContent = cssEditor.getValue()
92 | const jsContent = jsEditor.getValue()
93 |
94 | const isEmpty = !htmlContent && !cssContent && !jsContent
95 | if (isEmpty) return
96 |
97 | const { updateHistory } = getHistoryState()
98 | updateHistory({ key: 'current', value: null })
99 | })
100 |
101 | eventBus.on(EVENTS.OPEN_EXISTING_INSTANCE, ({ detail: { id, value } }) => {
102 | const { updateHistory } = getHistoryState()
103 | let { pathname } = window.location
104 | window.history.replaceState(null, null, `/${value}`)
105 | pathname = window.location.pathname
106 |
107 | const [rawHtml, rawCss, rawJs] = pathname.slice(1).split('%7C')
108 |
109 | const VALUES = {
110 | html: rawHtml ? decode(rawHtml) : '',
111 | css: rawCss ? decode(rawCss) : '',
112 | javascript: rawJs ? decode(rawJs) : ''
113 | }
114 |
115 | htmlEditor.setValue(VALUES.html)
116 | cssEditor.setValue(VALUES.css)
117 | jsEditor.setValue(VALUES.javascript)
118 | updateHistory({ key: 'current', value: id })
119 | })
120 |
121 | eventBus.on(EVENTS.CLEAR_HISTORY, () => {
122 | const { clearHistory } = getHistoryState()
123 | clearHistory()
124 | })
125 |
--------------------------------------------------------------------------------
/src/css/aside.css:
--------------------------------------------------------------------------------
1 | aside {
2 | display: flex;
3 | height: 100dvh;
4 | }
5 |
6 | aside button {
7 | position: relative;
8 | }
9 |
10 | .console-badge-count {
11 | position: absolute;
12 | top: 6px;
13 | right: 6px;
14 | background: #ef5350;
15 | color: #fff;
16 | border-radius: 50%;
17 | min-width: 16px;
18 | height: 16px;
19 | display: flex;
20 | align-items: center;
21 | justify-content: center;
22 | font-size: 9px;
23 | font-weight: 700;
24 | padding: 0 4px;
25 | pointer-events: none;
26 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
27 | }
28 |
29 | .console-badge-count[hidden] {
30 | display: none;
31 | }
32 |
33 | aside button .button-title {
34 | background: var(--button-title-background);
35 | border-radius: 3px;
36 | border: 1px solid var(--button-title-border);
37 | color: var(--button-title-foreground);
38 | display: none;
39 | font-size: 1.1em;
40 | padding: 0.5em 0.75em;
41 | position: absolute;
42 | right: -10px;
43 | transform: translateX(100%);
44 | z-index: 1000;
45 |
46 | &:after {
47 | content: "";
48 | position: absolute;
49 | transform: translate(-100%, -50%);
50 | top: 50%;
51 | left: 1px;
52 | right: 0;
53 | width: 0px;
54 | height: 0px;
55 | border-top: 0.5em solid transparent;
56 | border-bottom: 0.5em solid transparent;
57 | border-right: 0.5em solid var(--button-title-background);
58 | }
59 |
60 | &:before {
61 | content: "";
62 | position: absolute;
63 | transform: translate(-100%, -50%);
64 | top: 50%;
65 | left: 0px;
66 | right: 0;
67 | width: 0px;
68 | height: 0px;
69 | border-top: 0.5em solid transparent;
70 | border-bottom: 0.5em solid transparent;
71 | border-right: 0.5em solid var(--button-title-border);
72 | }
73 | }
74 |
75 | aside button.is-active {
76 | opacity: 1;
77 | &::after {
78 | content: '';
79 | inset: 0;
80 | margin: auto;
81 | background: #000;
82 | border-radius: 4px;
83 | }
84 | }
85 |
86 | @media (hover: hover) and (pointer: fine) {
87 | aside button:hover {
88 | opacity: 1;
89 | & .button-title {
90 | display: block;
91 | }
92 | }
93 | }
94 |
95 | .aside-bar {
96 | background: var(--aside-bar-background);
97 | color: var(--aside-bar-foreground);
98 | border-right: 1px solid var(--aside-bar-border);
99 | width: 310px;
100 | height: 100dvh;
101 | overflow-y: auto;
102 | scrollbar-gutter: stable;
103 |
104 | &::-webkit-scrollbar {
105 | width: 0.85em;
106 | }
107 |
108 | &::-webkit-scrollbar-thumb {
109 | background-color: hsla(0, 0%, 50%, 0);
110 | }
111 |
112 | &:hover::-webkit-scrollbar-thumb {
113 | background-color: hsla(0, 0%, 50%, 0.4);
114 | }
115 |
116 | &::-webkit-scrollbar-thumb:hover {
117 | background-color: hsla(0, 0%, 50%, 0.6);
118 | }
119 |
120 | &::-webkit-scrollbar-thumb:active {
121 | background-color: hsla(0, 0%, 50%, 1);
122 | }
123 | }
124 |
125 | .aside-sections {
126 | background: var(--aside-sections-background);
127 | border-right: 1px solid var(--aside-sections-border);
128 | display: flex;
129 | flex-direction: column;
130 | height: 100%;
131 | justify-content: space-between;
132 | width: 52px;
133 | }
134 |
135 | .aside-sections button {
136 | background: transparent;
137 | border: 0;
138 | cursor: pointer;
139 | width: 100%;
140 | color: var(--button-foreground);
141 | transition: opacity 0.1s ease;
142 | padding: 10px 0;
143 | }
144 |
145 | .aside-sections button.is-active {
146 | border-radius: 4px;
147 | color: var(--button-foreground-active);
148 | }
149 |
150 | .aside-sections button:not(:disabled):hover {
151 | color: var(--button-foreground-active);
152 | }
153 |
154 | .aside-sections svg {
155 | width: 25px;
156 | height: 25px;
157 | }
158 |
159 | .aside-sections header,
160 | .aside-sections footer {
161 | display: flex;
162 | flex-direction: column;
163 | justify-content: center;
164 | }
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | @import url('./css/base.css');
2 | @import url('./css/fonts.css');
3 | @import url('./css/animations.css');
4 |
5 | @import url('./css/editors-layout.css');
6 | @import url('./css/aside.css');
7 | @import url('./css/notifications.css');
8 | @import url('./css/skypack.css');
9 | @import url('./css/settings.css');
10 | @import url('./css/drag-drop-area.css');
11 | @import url('./css/console.css');
12 | @import url('./css/history.css');
13 |
14 |
15 | .scroll-buttons-container {
16 | display: none;
17 | }
18 |
19 | /* Magic Menu Styles */
20 | .preview {
21 | position: relative;
22 | }
23 |
24 | .iframe-container{
25 | min-height: 0;
26 | }
27 | body{
28 | height: 100dvh;
29 | }
30 |
31 | @media (max-width: 650px) {
32 | #app {
33 | grid-template-columns: 1fr;
34 | }
35 |
36 | aside {
37 | display: flex;
38 | flex-direction: column;
39 | width: 100%;
40 | position: absolute;
41 | top: 0;
42 | z-index: 10;
43 | transition: top 0.2s;
44 | }
45 |
46 | .aside-bar {
47 | width: 100%;
48 | max-height: calc(100dvh - 49px);
49 | }
50 |
51 | .aside-sections {
52 | flex-direction: row;
53 | width: 100%;
54 | padding-inline-start: 10px;
55 | padding-inline-end: 10px;
56 | height: fit-content;
57 | }
58 |
59 | .aside-sections > header,
60 | .aside-sections > footer {
61 | flex-direction: row;
62 | gap: 0 5px;
63 | }
64 |
65 | .bar-button {
66 | transform: scale(0.8);
67 | }
68 |
69 | .aside-sections button.is-active,
70 | aside button.is-active {
71 | border-left: none;
72 | }
73 |
74 | aside button:hover .button-title {
75 | display: none;
76 | }
77 |
78 | .search-results {
79 | max-width: 90vw;
80 | }
81 |
82 | #editor {
83 | scroll-snap-type: y mandatory;
84 | overflow-y: scroll;
85 |
86 | display: flex;
87 | flex-direction: column;
88 | margin: 75px 0 0;
89 | }
90 |
91 | .editor{
92 | min-height: calc(100vh - 75px);
93 | scroll-snap-align: start;
94 | }
95 |
96 | .iframe-container {
97 | scroll-snap-align: start;
98 | min-height: 100dvh;
99 | }
100 |
101 | iframe body {
102 | padding: 0 0 5em;
103 | }
104 |
105 | .vertical-gutter,
106 | .horizontal-gutter {
107 | display: none;
108 | }
109 |
110 | .margin {
111 | max-width: 10px;
112 | }
113 |
114 | #html .margin {
115 | background: #F33E15;
116 | }
117 | #css .margin {
118 | background: #158BF3;
119 | }
120 | #js .margin {
121 | background: #E99F1E;
122 | }
123 |
124 | #html::after,
125 | #css::after,
126 | #js::after {
127 | display: none;
128 | }
129 |
130 | .iPadShowKeyboard {
131 | display: none;
132 | }
133 |
134 | .scroll-buttons-container {
135 | display: block;
136 | position: absolute;
137 | bottom: 16px;
138 | right: 16px;
139 |
140 | display: flex;
141 | justify-content: center;
142 | align-items: center;
143 | gap: 0 1em;
144 | z-index: 10;
145 | }
146 |
147 | .scroll-button {
148 | width: 4em;
149 | aspect-ratio: 1;
150 | border: none;
151 | border-radius: 50%;
152 | display: flex;
153 | justify-content: center;
154 | align-items: center;
155 | transition: 0.2s background-color ease;
156 |
157 | background: #158BF3;
158 | color: #fff;
159 | cursor: pointer;
160 | -webkit-tap-highlight-color: transparent;
161 | }
162 |
163 | .scroll-button:hover {
164 | background: #3ba1fa;
165 | }
166 |
167 | .scroll-button > svg {
168 | width: 24px;
169 | }
170 |
171 | .previewer-active {
172 | height: calc(100dvh - 75px);
173 | display: block;
174 | }
175 | .previewer-hide {
176 | display: none;
177 | }
178 | }
179 |
180 |
181 | button {
182 | &:disabled, &:hover:disabled {
183 | opacity: 0.2;
184 | cursor: not-allowed;
185 | }
186 | }
187 |
188 | .spinner {
189 | border: 3px solid rgba(0, 0, 0, 0.1);
190 | width: 26px;
191 | height: 26px;
192 | border-radius: 50%;
193 | border-left-color: #c5c5c5;
194 | margin: auto;
195 |
196 | animation: spin 1s linear infinite;
197 | }
198 |
--------------------------------------------------------------------------------
/src/css/console.css:
--------------------------------------------------------------------------------
1 | .console {
2 | & .console-header {
3 | position: sticky;
4 | top: 0;
5 | z-index: 1;
6 | padding-top: 1em;
7 | }
8 |
9 | & .console-type {
10 | padding-bottom: 8px;
11 | opacity: 0.6;
12 | }
13 |
14 | & .console-item {
15 | color: #fff;
16 | display: flex;
17 | flex-direction: column;
18 | width: 100%;
19 | padding: 0 0 0 1em;
20 | }
21 |
22 | & .console-item strong {
23 | display: block;
24 | overflow: hidden;
25 | text-overflow: ellipsis;
26 | }
27 |
28 | & .console-list {
29 | list-style: none;
30 | padding: 0;
31 | display: grid;
32 | gap: 0.5em;
33 |
34 | & li {
35 | display: flex;
36 | align-items: start;
37 | gap: 0.75em;
38 |
39 | &:not(:last-child) {
40 | border-bottom: 1px solid rgba(255, 255, 255, 0.1);
41 | }
42 | }
43 |
44 | & .error {
45 | background: rgb(235, 189, 189);
46 | border: 2px solid rgb(204, 0, 0);
47 | color: black;
48 | padding-top: 0.5em
49 | }
50 |
51 | & .error::before {
52 | content: '❗';
53 | margin-right: 4px;
54 | width: 1em;
55 | }
56 |
57 | & .log-log {
58 | color: var(--log-log)
59 | }
60 |
61 | & .log-info {
62 | color: var(--log-info)
63 | }
64 |
65 | & .log-error {
66 | color: var(--log-error)
67 | }
68 |
69 | & .log-warn {
70 | color: var(--log-warn)
71 | }
72 |
73 | & .log-debug {
74 | color: var(--log-debug, #ab47bc)
75 | }
76 |
77 | & .log-table {
78 | color: var(--log-table, #66bb6a)
79 | }
80 |
81 | & .log-count {
82 | color: var(--log-count, #26c6da)
83 | }
84 |
85 | & .log-trace {
86 | color: var(--log-trace, #8d6e63)
87 | }
88 |
89 | & .log-dir {
90 | color: var(--log-dir, #78909c)
91 | }
92 |
93 | & .log-dirxml {
94 | color: var(--log-dirxml, #78909c)
95 | }
96 |
97 | & .log-time {
98 | color: var(--log-time, #ffd54f)
99 | }
100 |
101 | & .log-assert {
102 | color: var(--log-assert, #ff7043)
103 | }
104 | }
105 |
106 | /* aca tocaria ajustar los colores dependiendo del tema que este puesto pero por defecto es el mismo que el resto de la pagina */
107 | .console-string {
108 | color: #ce9178;
109 | }
110 |
111 | .console-number {
112 | color: #b5cea8;
113 | }
114 |
115 | .console-boolean {
116 | color: #569cd6;
117 | }
118 |
119 | .console-null {
120 | color: #d16969;
121 | }
122 |
123 | .console-undefined {
124 | color: #d16969;
125 | }
126 |
127 | .console-key {
128 | color: #9cdcfe;
129 | }
130 |
131 | .console-regexp {
132 | color: #b46695;
133 | }
134 |
135 | .console-date {
136 | color: #9cdcfe;
137 | }
138 |
139 | .console-fn::before {
140 | content: "ƒ ";
141 | font-style: italic;
142 | color: #ce9178;
143 | }
144 |
145 | .console-async-fn::before {
146 | content: "async ƒ ";
147 | }
148 |
149 | .console-generator-fn::before {
150 | content: "ƒ* ";
151 | }
152 |
153 | .console-badge {
154 | display: inline-block;
155 | padding: 2px 6px;
156 | border-radius: 3px;
157 | font-size: 8px;
158 | font-weight: 600;
159 | letter-spacing: 0.5px;
160 | color: #000;
161 | flex-shrink: 0;
162 | margin-top: 2px;
163 | }
164 |
165 | .console-table {
166 | margin-top: 0.5em;
167 | overflow-x: auto;
168 |
169 | & table {
170 | border-collapse: collapse;
171 | font-size: 12px;
172 | min-width: 100%;
173 | background: rgba(255, 255, 255, 0.05);
174 | }
175 |
176 | & th {
177 | background: rgba(255, 255, 255, 0.1);
178 | padding: 6px 12px;
179 | text-align: left;
180 | font-weight: 600;
181 | border: 1px solid rgba(255, 255, 255, 0.2);
182 | color: #9cdcfe;
183 | }
184 |
185 | & td {
186 | padding: 6px 12px;
187 | border: 1px solid rgba(255, 255, 255, 0.1);
188 | vertical-align: top;
189 | }
190 |
191 | & tr:hover {
192 | background: rgba(255, 255, 255, 0.05);
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ###
[Codi.link](https://codi.link)
4 |
5 | ***Your HTML5/CSS3/JavaScript Playground Editor***
6 |
7 |
8 |
9 |
10 | 
11 | 
12 |
13 |
14 |
15 |
16 |
17 |
18 | Table of Contents
19 |
20 |
21 | About The Project
22 | Getting Started
23 |
26 |
27 | Contributing
28 | License
29 | Contact
30 | Acknowledgments
31 |
32 |
33 |
34 | ## About The Project
35 |
36 | 
37 |
38 | codi.link is a live editor for HTML, CSS and JS. It allows you to edit your code in real-time, and see the result instantly. [Check a demo](https://codi.link/PGRpdj4KICA8YnV0dG9uPvCfpbMgQ2xpY2sgbWUgWUFZITwvYnV0dG9uPgo8L2Rpdj4=%7CYnV0dG9uIHsKICBmb250LXNpemU6IDQ4cHg7CiAgYm9yZGVyOiAxcHggc29saWQgIzA5ZjsKICBiYWNrZ3JvdW5kOiAjZmZmOwogIGNvbG9yOiAjMzMzOwogIHBhZGRpbmc6IDRweCAxNnB4OwogIGN1cnNvcjogcG9pbnRlcjsKICBib3JkZXItcmFkaXVzOiA5OTk5cHg7Cn0KCmJvZHkgewogIGRpc3BsYXk6IGdyaWQ7CiAgcGxhY2UtY29udGVudDogY2VudGVyOwogIGhlaWdodDogMTAwdmg7Cn0=%7CaW1wb3J0IENhbnZhc0NvbmZldHRpIGZyb20gJ2h0dHBzOi8vY2RuLnNreXBhY2suZGV2L2NhbnZhcy1jb25mZXR0aSc7Cgpkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdidXR0b24nKS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHsKICBDYW52YXNDb25mZXR0aSgpCn0p)
39 |
40 | Back to top 🔼
41 |
42 | ## Getting Started
43 |
44 | Install the dependencies:
45 |
46 | ```sh
47 | $ npm install
48 | // or
49 | $ yarn
50 | ```
51 |
52 | Run in dev mode:
53 |
54 | ```sh
55 | $ npm run dev
56 | // or
57 | $ yarn dev
58 | ```
59 |
60 | Run using Docker (by the Community):
61 |
62 | ```sh
63 | docker run -d --rm -p 5173:5173 --name codilink ferning98/codi.link
64 | ```
65 | You can get more details and examples on how to run this on Docker [here](https://hub.docker.com/r/ferning98/codi.link)
66 |
67 | ### Built With
68 |
69 | - JavaScript
70 | - [Lit](https://lit.dev)
71 | - [Vite](https://vitejs.dev)
72 | - [Zustand](https://zustand.surge.sh)
73 |
74 | Back to top 🔼
75 |
76 | ## Contributing
77 |
78 |
79 |
80 | 
81 |
82 |
83 |
84 | Contributions are what make the Open Source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
85 |
86 | If you have a suggestion that would make this better, please fork the repo and create a Pull Request. You can also simply [open an issue](https://github.com/midudev/codi.link/issues) with the tag *enhancement*.
87 |
88 | Don't forget to **give the project a star ⭐!** Thanks again!
89 |
90 | 1. Fork the project
91 |
92 | 2. Clone the repository
93 |
94 | ```bash
95 | git clone https://github.com/@username/codi.link
96 | ```
97 |
98 | 3. Create your Feature Branch
99 |
100 | ```bash
101 | git checkout -b feature/AmazingFeature
102 | ```
103 |
104 | 4. Push to the Branch
105 |
106 | ```bash
107 | git push origin feature/AmazingFeature
108 | ```
109 |
110 | 5. Open a Pull Request
111 |
112 | Back to top 🔼
113 |
114 | ## License
115 |
116 | Distributed under the MIT License. See `LICENSE` for more information.
117 |
118 | Back to top 🔼
119 |
120 | ## Contact 📭
121 |
122 | **Miguel Ángel Durán @midudev**
123 | [@midudev](https://twitter.com/midudev) - miduga@gmail.com
124 |
125 | Back to top 🔼
126 |
--------------------------------------------------------------------------------
/src/grid.js:
--------------------------------------------------------------------------------
1 | import Split from 'split-grid'
2 | import {
3 | DEFAULT_GRID_TEMPLATE,
4 | EDITOR_GRID_TEMPLATE
5 | } from './constants/editor-grid-template'
6 | import {
7 | BOTTOM_LAYOUT,
8 | DEFAULT_LAYOUT,
9 | HORIZONTAL_LAYOUT,
10 | TABS_LAYOUT,
11 | VERTICAL_LAYOUT
12 | } from './constants/grid-templates'
13 | import { getState } from './state'
14 | import { $, $$ } from './utils/dom'
15 |
16 | const $editor = $('#editor')
17 | const rootElement = document.documentElement
18 | const $$layoutSelector = $$('layout-preview')
19 | const $$editors = $$('#editor codi-editor')
20 | const $tabsContainer = $('#tabs')
21 | const $$tabs = $$('#tabs label')
22 | let splitInstance
23 |
24 | const selectTab = event => {
25 | $$editors.forEach($editor => ($editor.style.display = 'none'))
26 | const $targetEditor = $(`#${event.target.getAttribute('for')}`)
27 | $targetEditor.style.display = 'block'
28 | $$tabs.forEach($t => $t.classList.remove('active'))
29 | event.target.classList.add('active')
30 | }
31 |
32 | $$tabs.forEach($tab => {
33 | $tab.addEventListener('click', selectTab)
34 | })
35 |
36 | const formatGutters = gutter => ({
37 | ...gutter,
38 | element: $(gutter.element)
39 | })
40 |
41 | // Metodo de preservasión de grid
42 | const saveGridTemplate = () => {
43 | const { preserveGrid } = getState()
44 | if (!preserveGrid) return
45 |
46 | const gridStyles = $('.grid').style
47 |
48 | const gridTemplate = JSON.stringify({
49 | 'grid-template-columns': gridStyles['grid-template-columns'],
50 | 'grid-template-rows': gridStyles['grid-template-rows']
51 | })
52 |
53 | window.localStorage.setItem('gridTemplate', gridTemplate)
54 | }
55 |
56 | const getInitialGridStyle = () => {
57 | const { preserveGrid } = getState()
58 | if (!preserveGrid) return window.localStorage.removeItem('gridTemplate')
59 |
60 | const gridTemplate = JSON.parse(window.localStorage.getItem('gridTemplate'))
61 |
62 | return (
63 | gridTemplate &&
64 | `grid-template-columns: ${gridTemplate['grid-template-columns']}; grid-template-rows: ${gridTemplate['grid-template-rows']}`
65 | )
66 | }
67 |
68 | const configLayoutTabsElements = type => {
69 | if (type === 'tabs') {
70 | $tabsContainer.removeAttribute('hidden')
71 | $tabsContainer.style.display = 'grid'
72 | $tabsContainer.querySelector('label').classList.add('active')
73 | $('.second-gutter').style.display = 'none'
74 | $('.last-gutter').style.display = 'none'
75 | $$editors.forEach(($editor, index) => {
76 | $editor.style.display = 'none'
77 | $editor.style.gridArea = 'editors'
78 |
79 | if (index === 0) {
80 | $editor.style.display = 'block'
81 | }
82 | })
83 | } else {
84 | $tabsContainer.setAttribute('hidden', 'hidde')
85 | $tabsContainer.style.display = 'none'
86 | $('.second-gutter').style.display = 'block'
87 | $('.last-gutter').style.display = 'block'
88 | $$editors.forEach(($editor, i) => {
89 | $editor.style.display = 'block'
90 | $editor.style.gridArea = $editor.getAttribute('data-grid-area')
91 | })
92 | }
93 | }
94 |
95 | const setGridLayout = (type = '') => {
96 | const style = EDITOR_GRID_TEMPLATE[type] || DEFAULT_GRID_TEMPLATE
97 |
98 | const gutters =
99 | {
100 | vertical: VERTICAL_LAYOUT,
101 | horizontal: HORIZONTAL_LAYOUT,
102 | bottom: BOTTOM_LAYOUT,
103 | tabs: TABS_LAYOUT
104 | }[type] ?? DEFAULT_LAYOUT
105 |
106 | configLayoutTabsElements(type)
107 |
108 | const initialStyle = !splitInstance && getInitialGridStyle()
109 |
110 | rootElement.setAttribute('data-layout', type)
111 | $editor.setAttribute('style', initialStyle || style)
112 |
113 | $$layoutSelector.forEach(layoutEl => {
114 | if (type === layoutEl.getAttribute('layout')) {
115 | layoutEl.setAttribute('active', 'true')
116 | } else {
117 | layoutEl.removeAttribute('active')
118 | }
119 | })
120 |
121 | saveGridTemplate()
122 |
123 | const splitConfig = {
124 | ...gutters,
125 | ...(gutters.columnGutters && {
126 | columnGutters: gutters.columnGutters.map(formatGutters)
127 | }),
128 | ...(gutters.rowGutters && {
129 | rowGutters: gutters.rowGutters.map(formatGutters)
130 | }),
131 | ...(type === 'tabs' && { columnMinSizes: { 0: 300 } }),
132 | minSize: 1,
133 | onDragEnd: saveGridTemplate
134 | }
135 |
136 | if (splitInstance) {
137 | splitInstance.destroy(true)
138 | }
139 |
140 | splitInstance = Split(splitConfig)
141 | }
142 |
143 | export default setGridLayout
144 |
--------------------------------------------------------------------------------
/src/components/codi-editor/themes.json:
--------------------------------------------------------------------------------
1 | {
2 | "one-dark-pro": {
3 | "label": "One Dark Pro",
4 | "base": "vs-dark",
5 | "inherit": true,
6 | "semanticHighlighting": true,
7 | "rules": [
8 | {
9 | "token": "comment",
10 | "foreground": "#7f8590",
11 | "fontStyle": "italic"
12 | },
13 | {
14 | "token": "keyword",
15 | "foreground": "#c779dc"
16 | },
17 | {
18 | "token": "number",
19 | "foreground": "#d19a66"
20 | },
21 | {
22 | "token": "string",
23 | "foreground": "#99c27a"
24 | },
25 | {
26 | "token": "storage",
27 | "foreground": "#ffad66"
28 | },
29 | {
30 | "token": "identifier",
31 | "foreground": "#62afef"
32 | },
33 | {
34 | "token": "tag",
35 | "foreground": "#e06c75"
36 | },
37 | {
38 | "token": "attribute.value",
39 | "foreground": "#99c27a"
40 | },
41 | {
42 | "token": "attribute.name",
43 | "foreground": "#d19a66"
44 | },
45 | {
46 | "token": "selector.class",
47 | "foreground": "#d19a66"
48 | }
49 | ],
50 | "colors": {
51 | "editor.background": "#282b34",
52 | "editor.lineHighlightBackground": "#2e313d"
53 | }
54 | },
55 | "dracula": {
56 | "base": "vs-dark",
57 | "inherit": true,
58 | "label": "Dracula",
59 | "rules": [
60 | {
61 | "token": "comment",
62 | "foreground": "#5b72a4",
63 | "fontStyle": "italic"
64 | },
65 | {
66 | "token": "keyword",
67 | "foreground": "#fe7bc8"
68 | },
69 | {
70 | "token": "number",
71 | "foreground": "#bf95f9"
72 | },
73 | {
74 | "token": "string",
75 | "foreground": "#e5fb8e"
76 | },
77 | {
78 | "token": "storage",
79 | "foreground": "#ffad66"
80 | },
81 | {
82 | "token": "identifier",
83 | "foreground": "#bf95f9"
84 | },
85 | {
86 | "token": "tag",
87 | "foreground": "#fe7bc8"
88 | },
89 | {
90 | "token": "attribute.value",
91 | "foreground": "#e5fb8e"
92 | },
93 | {
94 | "token": "attribute.name",
95 | "foreground": "#50fb7b"
96 | },
97 | {
98 | "token": "selector.class",
99 | "foreground": "#d19a66"
100 | }
101 | ],
102 | "colors": {
103 | "editor.background": "#2a2c37",
104 | "editor.lineHighlightBackground": "#2a2c37"
105 | }
106 | },
107 | "mosqueta-dark": {
108 | "base": "vs-dark",
109 | "inherit": true,
110 | "label": "Mosqueta Dark",
111 | "rules": [
112 | {
113 | "token": "comment",
114 | "foreground": "#6272a4",
115 | "fontStyle": "italic"
116 | },
117 | {
118 | "token": "keyword",
119 | "foreground": "#e67cbe"
120 | },
121 | {
122 | "token": "number",
123 | "foreground": "#ffec80"
124 | },
125 | {
126 | "token": "string",
127 | "foreground": "#f8ff97"
128 | },
129 | {
130 | "token": "storage",
131 | "foreground": "#ffad66"
132 | },
133 | {
134 | "token": "identifier",
135 | "foreground": "#deff8a"
136 | },
137 | {
138 | "token": "tag",
139 | "foreground": "#ffb3d9"
140 | },
141 | {
142 | "token": "attribute.value",
143 | "foreground": "#f9fe95"
144 | },
145 | {
146 | "token": "attribute.name",
147 | "foreground": "#fdd686"
148 | },
149 | {
150 | "token": "selector.class",
151 | "foreground": "#d19a66"
152 | }
153 | ],
154 | "colors": {
155 | "editor.background": "#232935",
156 | "editor.lineHighlightBackground": "#1a1d28"
157 | }
158 | }
159 | }
--------------------------------------------------------------------------------
/src/console-script.js:
--------------------------------------------------------------------------------
1 | export const generateConsoleScript = ({ html, css }) => {
2 | return ``
163 | }
164 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { encode, decode } from 'js-base64'
2 | import { $, $$ } from './utils/dom.js'
3 | import { createEditor } from './editor.js'
4 | import debounce from './utils/debounce.js'
5 | import runJs from './utils/run-js.js'
6 | import { initializeEventsController, eventBus, EVENTS } from './events-controller.js'
7 | import { getState, subscribe } from './state.js'
8 | import * as Preview from './utils/WindowPreviewer.js'
9 | import setGridLayout from './grid.js'
10 | import setTheme from './theme.js'
11 | import setLanguage from './language.js'
12 | import { configurePrettierHotkeys } from './monaco-prettier/configurePrettier'
13 | import { getHistoryState, subscribeHistory, setHistory } from './history.js'
14 |
15 | import './aside.js'
16 | import './skypack.js'
17 | import './settings.js'
18 | import './scroll.js'
19 | import './drag-file.js'
20 | import './console.js'
21 |
22 | import { BUTTON_ACTIONS } from './constants/button-actions.js'
23 |
24 | import './components/layout-preview/layout-preview.jsx'
25 | import './components/codi-editor/codi-editor.js'
26 |
27 | const { layout: currentLayout, theme, language, saveLocalstorage } = getState()
28 | const { history, updateHistoryItem } = getHistoryState()
29 |
30 | setGridLayout(currentLayout)
31 | setTheme(theme)
32 | setLanguage(language)
33 |
34 | const iframe = $('iframe')
35 |
36 | const editorElements = $$('codi-editor')
37 |
38 | let { pathname } = window.location
39 |
40 | if (pathname === '/' && saveLocalstorage === true && history.current) {
41 | const hashedCode = history.items.find(item => item.id === history.current).value
42 | window.history.replaceState(null, null, `/${hashedCode}`)
43 | pathname = window.location.pathname
44 | }
45 |
46 | const [rawHtml, rawCss, rawJs] = pathname.slice(1).split(pathname.includes('%7C') ? '%7C' : '|')
47 |
48 | const VALUES = {
49 | html: rawHtml ? decode(rawHtml) : '',
50 | css: rawCss ? decode(rawCss) : '',
51 | javascript: rawJs ? decode(rawJs) : ''
52 | }
53 |
54 | const EDITORS = Array.from(editorElements).reduce((acc, domElement) => {
55 | const { language } = domElement
56 | domElement.value = VALUES[language]
57 | acc[language] = createEditor(domElement)
58 | return acc
59 | }, {})
60 |
61 | subscribe(state => {
62 | const newOptions = { ...state, minimap: { enabled: state.minimap } }
63 |
64 | Object.values(EDITORS).forEach(editor => {
65 | editor.updateOptions({
66 | ...editor.getRawOptions(),
67 | ...newOptions
68 | })
69 | })
70 | setGridLayout(state.layout)
71 | setTheme(state.theme)
72 | setLanguage(state.language)
73 | })
74 |
75 | const MS_UPDATE_DEBOUNCED_TIME = 200
76 | const MS_UPDATE_HASH_DEBOUNCED_TIME = 1000
77 | const debouncedUpdate = debounce(update, MS_UPDATE_DEBOUNCED_TIME)
78 | const debouncedUpdateHash = debounce(
79 | updateHashedCode,
80 | MS_UPDATE_HASH_DEBOUNCED_TIME
81 | )
82 |
83 | const { html: htmlEditor, css: cssEditor, javascript: jsEditor } = EDITORS
84 |
85 | if (saveLocalstorage) {
86 | setHistory(history)
87 |
88 | subscribeHistory(store => {
89 | if (!store.history.current) {
90 | jsEditor.setValue('')
91 | cssEditor.setValue('')
92 | htmlEditor.setValue('')
93 | }
94 | setHistory(store.history)
95 | })
96 | }
97 |
98 | htmlEditor.focus()
99 | Object.values(EDITORS).forEach(editor => {
100 | editor.onDidChangeModelContent(() =>
101 | debouncedUpdate({ notReload: editor === cssEditor })
102 | )
103 | })
104 | initializeEventsController({ htmlEditor, cssEditor, jsEditor })
105 |
106 | configurePrettierHotkeys([htmlEditor, cssEditor, jsEditor])
107 |
108 | update()
109 |
110 | function update ({ notReload } = {}) {
111 | const values = {
112 | html: htmlEditor.getValue(),
113 | css: cssEditor.getValue(),
114 | js: jsEditor.getValue()
115 | }
116 |
117 | Preview.updatePreview(values)
118 |
119 | if (!notReload) {
120 | const { maxExecutionTime } = getState()
121 | runJs(values.js, parseInt(maxExecutionTime))
122 | .then(() => {
123 | iframe.setAttribute('src', Preview.getPreviewUrl())
124 | })
125 | .catch(error => {
126 | console.error('Execution error:', error)
127 | })
128 | }
129 |
130 | updateCss()
131 |
132 | debouncedUpdateHash(values)
133 | if (saveLocalstorage) {
134 | updateHistory(values)
135 | }
136 | updateButtonAvailabilityIfContent(values)
137 | }
138 |
139 | function updateCss () {
140 | const iframeStyleEl = iframe.contentDocument.querySelector('#preview-style')
141 |
142 | if (iframeStyleEl) {
143 | iframeStyleEl.textContent = cssEditor.getValue()
144 | }
145 | }
146 |
147 | function updateHashedCode ({ html, css, js }) {
148 | const hashedCode = `${encode(html)}|${encode(css)}|${encode(js)}`
149 | window.history.replaceState(null, null, `/${hashedCode}`)
150 | }
151 |
152 | function updateHistory ({ html, css, js }) {
153 | const { history } = getHistoryState()
154 | const hashedCode = `${encode(html)}|${encode(css)}|${encode(js)}`
155 | const isEmpty = !html.replace(/\n/g, '').trim() && !css.replace(/\n/g, '').trim() && !js.replace(/\n/g, '').trim()
156 |
157 | if (isEmpty && !history.current) {
158 | return
159 | }
160 |
161 | updateHistoryItem({ value: hashedCode })
162 | }
163 |
164 | function updateButtonAvailabilityIfContent ({ html, css, js }) {
165 | const buttonActions = [
166 | BUTTON_ACTIONS.downloadUserCode,
167 | BUTTON_ACTIONS.openIframeTab,
168 | BUTTON_ACTIONS.copyToClipboard
169 | ]
170 |
171 | const hasContent = html || css || js
172 | buttonActions.forEach(action => {
173 | const button = $(`button[data-action='${action}']`)
174 | button.disabled = !hasContent
175 | })
176 | }
177 |
178 | // Keyboard shortcut: Cmd+S (Mac) or Ctrl+S (Windows/Linux) to download
179 | window.addEventListener('keydown', (event) => {
180 | const isSaveShortcut = (event.metaKey || event.ctrlKey) && event.key === 's'
181 |
182 | if (isSaveShortcut) {
183 | event.preventDefault()
184 |
185 | const downloadButton = $(`button[data-action='${BUTTON_ACTIONS.downloadUserCode}']`)
186 |
187 | // Only trigger download if button is not disabled (has content)
188 | if (!downloadButton.disabled) {
189 | eventBus.emit(EVENTS.DOWNLOAD_USER_CODE)
190 | }
191 | }
192 | })
193 |
--------------------------------------------------------------------------------
/src/css/base.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --app-background: rgb(30, 30, 30);
3 | --app-foreground: rgb(204, 204, 204);
4 |
5 | --aside-sections-background: rgb(51, 51, 51);
6 | --aside-sections-border: transparent;
7 |
8 | --aside-bar-background: rgb(37, 37, 38);
9 | --aside-bar-foreground: rgb(204, 204, 204);
10 | --aside-bar-border: transparent;
11 |
12 | --grid-background: rgb(37, 37, 38);
13 |
14 | --button-foreground: rgba(255, 255, 255, 0.4);
15 | --button-foreground-active: rgb(255, 255, 255);
16 |
17 | --button-title-background: rgb(51, 51, 51);
18 | --button-title-foreground: rgb(225, 228, 232);
19 | --button-title-border: rgb(51, 51, 51);
20 |
21 | --input-background: rgb(51, 51, 51);
22 | --input-foreground: rgb(225, 228, 232);
23 | --input-border: rgb(51, 51, 51);
24 |
25 | --log-log: rgb(205, 238, 181);
26 | --log-info: rgb(181, 181, 238);
27 | --log-error: rgb(227, 189, 189);
28 | --log-warn: rgb(247, 231, 161);
29 | }
30 |
31 | [data-theme='vs'] {
32 | --app-background: rgb(255, 255, 254);
33 | --app-foreground: rgb(0, 0, 0);
34 |
35 | --aside-sections-background: rgb(44, 44, 44);
36 |
37 | --aside-bar-background: rgb(243, 243, 243);
38 | --aside-bar-foreground: rgb(97, 97, 97);
39 |
40 | --grid-background: rgb(243, 243, 243);
41 |
42 | --gutter-background: rgb(243, 243, 243);
43 |
44 | --input-background: rgb(243, 243, 243);
45 | --input-foreground: rgb(97, 97, 97);
46 | --input-border: rgb(97, 97, 97);
47 |
48 | --log-log: rgb(14, 197, 8);
49 | --log-info: rgb(31, 76, 226);
50 | --log-error: rgb(219, 14, 14);
51 | --log-warn: rgb(203, 168, 8);
52 | }
53 |
54 | [data-theme='hc-black'] {
55 | --app-background: rgb(0, 0, 0);
56 | --app-foreground: rgb(255, 255, 255);
57 |
58 | --aside-sections-background: rgb(0, 0, 0);
59 | --aside-sections-border: rgb(255, 255, 255);
60 |
61 | --aside-bar-background: rgb(0, 0, 0);
62 | --aside-bar-foreground: rgb(255, 255, 255);
63 | --aside-bar-border: rgb(255, 255, 255);
64 |
65 | --grid-background: rgb(255, 255, 255);
66 |
67 | --button-foreground: rgb(255, 255, 255);
68 |
69 | --button-title-background: rgb(255, 255, 255);
70 | --button-title-foreground: rgb(0, 0, 0);
71 | --button-title-border: rgb(0, 0, 0);
72 |
73 | --input-background: rgb(0, 0, 0);
74 | --input-foreground: rgb(255, 255, 255);
75 | --input-border: rgb(255, 255, 255);
76 | }
77 |
78 | [data-theme='hc-light'] {
79 | --app-background: rgb(255, 255, 255);
80 | --app-foreground: rgb(0, 0, 0);
81 |
82 | --aside-sections-background: rgb(255, 255, 255);
83 | --aside-sections-border: rgb(15, 74, 133);
84 |
85 | --aside-bar-background: rgb(255, 255, 255);
86 | --aside-bar-foreground: rgb(0, 0, 0);
87 | --aside-bar-border: rgb(15, 74, 133);
88 |
89 | --grid-background: rgb(15, 74, 133);
90 |
91 | --gutter-background: rgb(255, 255, 255);
92 |
93 | --button-foreground: rgb(0, 0, 0);
94 | --button-foreground-active: rgb(0, 0, 0);
95 |
96 | --button-title-background: rgb(0, 0, 0);
97 | --button-title-foreground: rgb(255, 255, 255);
98 | --button-title-border: rgb(255, 255, 255);
99 |
100 | --input-background: rgb(255, 255, 255);
101 | --input-foreground: rgb(0, 0, 0);
102 | --input-border: rgb(0, 0, 0);
103 |
104 | --log-log: rgb(7, 154, 2);
105 | --log-info: rgb(6, 53, 210);
106 | --log-error: rgb(223, 0, 0);
107 | --log-warn: rgb(178, 145, 0);
108 | }
109 |
110 | [data-theme='mosqueta-dark'] {
111 | --app-background: #171a25;
112 | --app-foreground: rgb(51, 51, 51);
113 |
114 | --aside-sections-background: #171a25;
115 | --aside-sections-border: transparent;
116 |
117 | --aside-bar-background: #1e2530;
118 | --aside-bar-foreground: rgb(204, 204, 204);
119 | --aside-bar-border: transparent;
120 |
121 | --grid-background: #171a25;
122 |
123 | --button-foreground: rgba(255, 255, 255, 0.4);
124 | --button-foreground-active: #ff89ef;
125 | --button-border-active: #40142c;
126 | --button-background-active: #40142c;
127 |
128 | --button-title-background: #171a25;
129 | --button-title-foreground: rgb(225, 228, 232);
130 | --button-title-border: rgb(51, 51, 51);
131 |
132 | --input-background: #171a25;
133 | --input-foreground: #ff89ef;
134 | --input-border: #171a25;
135 | }
136 |
137 | [data-theme='dracula'] {
138 | --app-background: #171a25;
139 | --app-foreground: rgb(51, 51, 51);
140 |
141 | --aside-sections-background: #353646;
142 | --aside-sections-border: transparent;
143 |
144 | --aside-bar-background: #20232c;
145 | --aside-bar-foreground: #ffffff;
146 | --aside-bar-border: transparent;
147 |
148 | --grid-background: #171a25;
149 |
150 | --button-foreground: #6473a6;
151 | --button-foreground-active: #ffffff;
152 | --button-border-active: #40142c;
153 | --button-background-active: #40142c;
154 |
155 | --button-title-background: #171a25;
156 | --button-title-foreground: rgb(225, 228, 232);
157 | --button-title-border: rgb(51, 51, 51);
158 |
159 | --input-background: #454659;
160 | --input-foreground: #ffffff;
161 | --input-border: #171a25;
162 | }
163 |
164 | [data-theme='one-dark-pro'] {
165 | --app-background: #171a25;
166 | --app-foreground: rgb(51, 51, 51);
167 |
168 | --aside-sections-background: #282b34;
169 | --aside-sections-border: transparent;
170 |
171 | --aside-bar-background: #21262c;
172 | --aside-bar-foreground: #ffffff;
173 | --aside-bar-border: transparent;
174 |
175 | --grid-background: #171a25;
176 |
177 | --button-foreground: #6e7077;
178 | --button-foreground-active: #ffffff;
179 | --button-border-active: #40142c;
180 | --button-background-active: #40142c;
181 |
182 | --button-title-background: #171a25;
183 | --button-title-foreground: rgb(225, 228, 232);
184 | --button-title-border: rgb(51, 51, 51);
185 |
186 | --input-background: #404554;
187 | --input-foreground: #ffffff;
188 | --input-border: #171a25;
189 | }
190 |
191 | body {
192 | background-color: var(--app-background);
193 | color: var(--app-foreground);
194 | font-size: 16px;
195 | height: 100dvh;
196 | line-height: 1.42857143;
197 | margin: 0;
198 | overflow: hidden;
199 | padding: 0;
200 | position: fixed;
201 | width: 100vw;
202 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
203 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
204 | -webkit-font-smoothing: antialiased;
205 | -moz-osx-font-smoothing: grayscale;
206 | }
207 |
208 | * {
209 | box-sizing: border-box;
210 | }
211 |
212 | *::before,
213 | *::after {
214 | box-sizing: inherit;
215 | }
216 |
217 | a {
218 | color: #41a0ff;
219 | text-decoration: none;
220 | }
221 |
222 | a:hover {
223 | text-decoration: underline;
224 | }
225 |
226 | fieldset {
227 | margin: 0;
228 | padding: 0;
229 | border: 0;
230 | }
231 |
232 | select {
233 | background-color: var(--input-background);
234 | color: var(--input-foreground);
235 | border: 1px solid var(--input-border);
236 | height: 26px;
237 | margin-top: 8px;
238 | padding: 2px 8px;
239 | width: 100%;
240 | }
241 |
242 | [hidden] {
243 | display: none !important;
244 | }
245 |
246 | #app {
247 | display: grid;
248 | grid-template-columns: auto 1fr;
249 | height: 100dvh;
250 |
251 | #editor {
252 | height: 100dvh;
253 | }
254 | }
255 |
256 | iframe {
257 | background: #fff;
258 | border: 0;
259 | height: 100%;
260 | width: 100%;
261 | }
--------------------------------------------------------------------------------
/src/css/editors-layout.css:
--------------------------------------------------------------------------------
1 | .grid {
2 | display: grid;
3 | grid-template-areas:
4 | 'editor-html . editor-js'
5 | '. . .'
6 | 'editor-css . editor-preview';
7 | grid-template-columns: 1fr 8px 1fr;
8 | grid-template-rows: 1fr 8px 1fr;
9 | height: 100vh;
10 |
11 | [data-layout='default'] & {
12 | & .first-gutter {
13 | position: relative;
14 | box-shadow: -1px 0 0 0 var(--grid-background),
15 | 1px 0 0 0 var(--grid-background);
16 | }
17 | & .second-gutter {
18 | background: transparent;
19 | }
20 | & .last-gutter {
21 | position: relative;
22 | box-shadow: 0 -1px 0 0 var(--grid-background),
23 | 0 1px 0 0 var(--grid-background);
24 | }
25 | }
26 | [data-layout='layout-2'] & {
27 | grid-template-areas:
28 | 'editor-html . editor-css'
29 | '. . .'
30 | 'editor-js . editor-preview';
31 |
32 | & .first-gutter {
33 | position: relative;
34 | box-shadow: -1px 0 0 0 var(--grid-background),
35 | 1px 0 0 0 var(--grid-background);
36 | }
37 | & .second-gutter {
38 | background: transparent;
39 | }
40 | & .last-gutter {
41 | position: relative;
42 | box-shadow: 0 -1px 0 0 var(--grid-background),
43 | 0 1px 0 0 var(--grid-background);
44 | }
45 | }
46 | [data-layout='vertical'] & {
47 | grid-template-areas: 'editor-html . editor-css . editor-js . editor-preview';
48 | & .first-gutter {
49 | cursor: col-resize;
50 | grid-area: 1 / 2 / 1 / 2;
51 | position: relative;
52 | box-shadow: -1px 0 0 0 var(--grid-background),
53 | 1px 0 0 0 var(--grid-background);
54 | }
55 | & .second-gutter {
56 | cursor: col-resize;
57 | grid-area: 1 / 4 / 1 / 4;
58 | background-image: url(/assets/vertical.png);
59 | position: relative;
60 | box-shadow: -1px 0 0 0 var(--grid-background),
61 | 1px 0 0 0 var(--grid-background);
62 | }
63 | & .last-gutter {
64 | cursor: col-resize;
65 | grid-area: 1 / 6 / 1 / 6;
66 | background-image: url(/assets/vertical.png);
67 | position: relative;
68 | box-shadow: -1px 0 0 0 var(--grid-background),
69 | 1px 0 0 0 var(--grid-background);
70 | }
71 | }
72 | [data-layout='horizontal'] & {
73 | grid-template-areas:
74 | 'editor-html'
75 | '.'
76 | 'editor-css'
77 | '.'
78 | 'editor-js'
79 | '.'
80 | 'editor-preview';
81 | & .first-gutter {
82 | grid-area: 2 / 1 / 2 / 1;
83 | cursor: row-resize;
84 | background-image: url(/assets/horizontal.png);
85 | position: relative;
86 | box-shadow: 0 -1px 0 0 var(--grid-background),
87 | 0 1px 0 0 var(--grid-background);
88 | }
89 | & .second-gutter {
90 | grid-area: 4 / 1 / 4 / 1;
91 | cursor: row-resize;
92 | background-image: url(/assets/horizontal.png);
93 | position: relative;
94 | box-shadow: 0 -1px 0 0 var(--grid-background),
95 | 0 1px 0 0 var(--grid-background);
96 | }
97 | & .last-gutter {
98 | grid-area: 6 / 1 / 6 / 1;
99 | cursor: row-resize;
100 | position: relative;
101 | box-shadow: 0 -1px 0 0 var(--grid-background),
102 | 0 1px 0 0 var(--grid-background);
103 | }
104 | }
105 | [data-layout='bottom'] & {
106 | grid-template-columns: 1fr 8px 1fr 8px 1fr;
107 | grid-template-rows: 1fr 8px 1fr;
108 | grid-template-areas:
109 | 'editor-preview editor-preview editor-preview editor-preview editor-preview'
110 | '. . . . .'
111 | 'editor-html . editor-js . editor-css';
112 | & .first-gutter {
113 | cursor: col-resize;
114 | grid-area: 2 / 2 / 4 / 3;
115 | position: relative;
116 | box-shadow: -1px 0 0 0 var(--grid-background),
117 | 1px 0 0 0 var(--grid-background);
118 | }
119 | & .second-gutter {
120 | cursor: col-resize;
121 | grid-area: 2 / 4 / 4 / 5;
122 | background-image: url(/assets/vertical.png);
123 | position: relative;
124 | box-shadow: -1px 0 0 0 var(--grid-background),
125 | 1px 0 0 0 var(--grid-background);
126 | }
127 | & .last-gutter {
128 | cursor: row-resize;
129 | grid-area: 2 / 1 / 2 / 6;
130 | position: relative;
131 | box-shadow: 0 -1px 0 0 var(--grid-background),
132 | 0 1px 0 0 var(--grid-background);
133 | }
134 | }
135 | [data-layout='tabs'] & {
136 | grid-template-columns: 5fr 8px 3fr;
137 | grid-template-rows: 40px 1fr;
138 | grid-template-areas: 'tabs . editor-preview' 'editors . editor-preview';
139 |
140 | & #tabs {
141 | display: grid;
142 | }
143 |
144 | & .first-gutter {
145 | cursor: col-resize;
146 | background-image: url(/assets/vertical.png);
147 | grid-area: 1 / 2 / 3 / 2;
148 | position: relative;
149 | box-shadow: -1px 0 0 0 var(--grid-background),
150 | 1px 0 0 0 var(--grid-background);
151 | }
152 | & .second-gutter,
153 | & .last-gutter {
154 | display: none;
155 | }
156 | }
157 |
158 | & #markup {
159 | grid-area: editor-html;
160 | }
161 |
162 | & #style {
163 | grid-area: editor-css;
164 | }
165 |
166 | & #script {
167 | grid-area: editor-js;
168 | }
169 |
170 | & #editor-preview {
171 | grid-area: editor-preview;
172 | }
173 | }
174 |
175 | #tabs {
176 | grid-area: tabs;
177 | grid-template-columns: repeat(3, 1fr);
178 |
179 | & label {
180 | display: flex !important;
181 | align-items: center;
182 | padding-left: 8px;
183 | border-bottom: 2px solid rgb(72, 72, 72);
184 | opacity: .2;
185 | transition: opacity 0.2s ease;
186 |
187 | &.active {
188 | border-bottom: 2px solid;
189 | opacity: 1 !important;
190 |
191 | &[for="markup"] {
192 | border-bottom-color: #e34f26;
193 | }
194 | &[for="style"] {
195 | border-bottom-color: rebeccapurple;
196 | }
197 | &[for="script"] {
198 | border-bottom-color: rgb(255, 193, 7);
199 | }
200 | }
201 |
202 | &:hover {
203 | cursor: pointer;
204 | opacity: .7;
205 | }
206 |
207 | & + label {
208 | border-left: 1px solid rgb(41, 41, 41);
209 | }
210 |
211 | &::before {
212 | content: '';
213 | display: inline-block;
214 | width: 16px;
215 | height: 16px;
216 | margin-right: 0.5em;
217 | background-repeat: no-repeat;
218 | background-position: center;
219 | background-size: contain;
220 | }
221 |
222 | &[for="markup"]::before {
223 | background-image: url(/assets/html5.svg);
224 | }
225 | &[for="style"]::before {
226 | background-image: url(/assets/css.svg);
227 | }
228 | &[for="script"]::before {
229 | background-image: url(/assets/js.svg);
230 | }
231 | }
232 | }
233 |
234 | .first-gutter {
235 | cursor: col-resize;
236 | grid-area: 1 / 2 / 4 / 2;
237 | background: var(--grid-background);
238 | background-repeat: no-repeat;
239 | background-position: 50% center;
240 | background-image: url(/assets/vertical.png);
241 | }
242 |
243 | .second-gutter {
244 | cursor: all-scroll;
245 | z-index: 1;
246 | grid-area: 2 / 2 / 2 / 2;
247 | background: var(--grid-background);
248 | background-repeat: no-repeat;
249 | background-position: 50% center;
250 | }
251 |
252 | .last-gutter {
253 | cursor: row-resize;
254 | grid-area: 2 / 1 / 2 / 4;
255 | background: var(--grid-background);
256 | background-repeat: no-repeat;
257 | background-position: 50% center;
258 | background-image: url(/assets/horizontal.png);
259 | }
260 |
--------------------------------------------------------------------------------
/src/skypack.js:
--------------------------------------------------------------------------------
1 | import { eventBus, EVENTS } from './events-controller.js'
2 | import debounce from './utils/debounce.js'
3 | import { $ } from './utils/dom.js'
4 | import escapeHTML from 'escape-html'
5 |
6 | const CDN_URL = 'https://cdn.skypack.dev'
7 | const PACKAGE_VIEW_URL = 'https://www.skypack.dev/view'
8 |
9 | const $asideBar = $('.aside-bar')
10 | const $searchResults = $('#skypack .search-results')
11 | const $searchResultsList = $searchResults.querySelector('ul')
12 | const $searchResultsMessage = $('#skypack .search-results-message')
13 | const $skypackSearch = $('#skypack input[type="search"]')
14 | const $spinner = $searchResults.querySelector('.spinner')
15 |
16 | $skypackSearch.addEventListener('input', debounce(handleSearchInput, 200))
17 |
18 | let lastSearchInput = ''
19 | let currentPage = 1
20 | let totalPages = 1
21 | let lastFetchAbortController
22 |
23 | async function handleSearchInput () {
24 | const $searchInput = $skypackSearch
25 |
26 | const searchTerm = $searchInput.value.toLowerCase().trim()
27 |
28 | if (searchTerm === lastSearchInput) return
29 |
30 | lastSearchInput = searchTerm
31 |
32 | if (!searchTerm) return clearSearch()
33 |
34 | await startSearch()
35 | }
36 |
37 | function clearSearch () {
38 | lastFetchAbortController?.abort()
39 | hideSpinner()
40 | $searchResultsMessage.innerHTML = ''
41 | $searchResults.classList.add('hidden')
42 | }
43 |
44 | async function startSearch () {
45 | $searchResults.classList.remove('hidden')
46 | $searchResultsList.innerHTML = ''
47 |
48 | $searchResultsMessage.innerHTML = 'Searching...'
49 |
50 | showSpinner()
51 | await fetchPackagesAndDisplayResults({ page: 1 })
52 |
53 | $searchResults.classList.remove('hidden')
54 | }
55 |
56 | function finishSearch () {
57 | hideSpinner()
58 | }
59 |
60 | async function fetchPackagesAndDisplayResults ({ page = 1 }) {
61 | lastFetchAbortController?.abort()
62 | lastFetchAbortController = new window.AbortController()
63 |
64 | const [error, fetchResult] = await fetchPackages({
65 | page,
66 | abortController: lastFetchAbortController,
67 | packageName: lastSearchInput
68 | })
69 |
70 | // the last aborted fetch enters here
71 | if (error) return
72 |
73 | const {
74 | page: resultPage,
75 | hits: results,
76 | nbPages,
77 | nbHits: totalCount
78 | } = fetchResult
79 | currentPage = resultPage + 1
80 | totalPages = nbPages
81 |
82 | // microbenchmark: only displays total count at first time
83 | currentPage === 1 && displayTotalCount(totalCount)
84 |
85 | if (results.length === 0) return finishSearch()
86 |
87 | displayResults({ results, lastSearchInput })
88 |
89 | const $loadMoreResultsSentinel =
90 | $searchResultsList.querySelector('li:last-child')
91 | createLoadMoreResultsSentinelObserver($loadMoreResultsSentinel)
92 | }
93 |
94 | function displayTotalCount (totalCount) {
95 | const moreResultsSymbol = totalCount === 10_000 ? '+' : ''
96 | const formattedTotalCount =
97 | Intl.NumberFormat('es').format(totalCount) + moreResultsSymbol
98 | $searchResultsMessage.innerHTML = `${formattedTotalCount} results for "${escapeHTML(
99 | lastSearchInput
100 | )}"`
101 | }
102 |
103 | async function fetchNextPagePackagesAndDisplayResults () {
104 | const nextPage = currentPage + 1
105 | if (nextPage > totalPages) return finishSearch()
106 |
107 | await fetchPackagesAndDisplayResults({ page: nextPage })
108 | }
109 |
110 | async function fetchPackages ({ abortController, packageName, page = 1 }) {
111 | try {
112 | // Restamos 1 porque la API de Algolia usa índice 0 para la primera página
113 | const pageIndex = page - 1
114 | const resultFetch = await window.fetch(
115 | 'https://ofcncog2cu-dsn.algolia.net/1/indexes/npm-search/query?x-algolia-agent=Algolia%20for%20JavaScript%20(3.35.1)%3B%20Browser%20(lite)&x-algolia-application-id=OFCNCOG2CU&x-algolia-api-key=f54e21fa3a2a0160595bb058179bfb1e',
116 | {
117 | headers: {
118 | accept: 'application/json',
119 | 'accept-language': 'es-ES,es;q=0.6',
120 | 'content-type': 'application/x-www-form-urlencoded',
121 | 'sec-fetch-dest': 'empty',
122 | 'sec-fetch-mode': 'cors',
123 | 'sec-fetch-site': 'cross-site',
124 | 'sec-gpc': '1'
125 | },
126 | referrer: 'https://www.jsdelivr.com/',
127 | referrerPolicy: 'strict-origin-when-cross-origin',
128 | body: `{"params": "query=${packageName}&page=${pageIndex}&hitsPerPage=10&attributesToRetrieve=%5B%22deprecated%22%2C%22description%22%2C%22githubRepo%22%2C%22homepage%22%2C%22keywords%22%2C%22license%22%2C%22name%22%2C%22owner%22%2C%22version%22%2C%22popular%22%2C%22moduleTypes%22%2C%22styleTypes%22%2C%22jsDelivrHits%22%5D&analyticsTags=%5B%22jsdelivr%22%5D&facetFilters=moduleTypes%3Aesm"}`,
129 | method: 'POST',
130 | mode: 'cors',
131 | credentials: 'omit',
132 | signal: abortController.signal
133 | }
134 | )
135 |
136 | const resultFetchJson = await resultFetch.json()
137 | return [null, resultFetchJson]
138 | } catch (error) {
139 | return [error, {}]
140 | }
141 | }
142 |
143 | function displayResults ({ results, searchTerm }) {
144 | results.forEach((result) => {
145 | // console.log(result)
146 | const $li = document.createElement('li')
147 | $li.title = result.description
148 | $li.innerHTML = getResultHTML({ result, searchTerm })
149 | $li.tabIndex = 0
150 |
151 | $li.addEventListener('click', async (e) => {
152 | const url = `https://cdn.jsdelivr.net/npm/${result.name}@${result.version}/+esm`
153 |
154 | if (e.target.className === 'skypack-open') {
155 | if (e.target.hasAttribute('data-copy')) {
156 | e.preventDefault()
157 | navigator.clipboard.writeText(url)
158 | }
159 | return
160 | }
161 |
162 | handlePackageSelected(result.name, url)
163 | })
164 |
165 | $li.addEventListener('keydown', (e) => {
166 | if (e.keyCode === 13) handlePackageSelected(result.name)
167 | })
168 |
169 | $searchResultsList.appendChild($li)
170 | })
171 | }
172 |
173 | function getResultHTML ({ result, searchTerm }) {
174 | const resultBadgesHTML = getResultBadgesHTML({ result, searchTerm })
175 |
176 | return `
177 |
178 | ${escapeHTML(result.name)}
179 | ${resultBadgesHTML}
180 |
181 | ${escapeHTML(
182 | result.description
183 | )}
184 | `
195 | }
196 |
197 | function getResultBadgesHTML ({ result, searchTerm }) {
198 | const isPopular = result.popularityScore >= 0.8
199 | const popularHtml = isPopular
200 | ? 'popular
'
201 | : ''
202 | const typescriptHtml = result.hasTypes
203 | ? '
'
204 | : ''
205 | const deprecatedHtml = result.isDeprecated
206 | ? 'deprecated
'
207 | : ''
208 | const exactMatchHtml =
209 | result.name === searchTerm
210 | ? 'exact match
'
211 | : ''
212 | return `
213 |
214 | ${typescriptHtml}
215 | ${popularHtml}
216 | ${deprecatedHtml}
217 | ${exactMatchHtml}
218 |
`
219 | }
220 |
221 | function handlePackageSelected (packageName, packageUrl) {
222 | let parsedName = packageName.split('/').join('-')
223 | if (parsedName.startsWith('@')) parsedName = parsedName.substr(1)
224 | eventBus.emit(EVENTS.ADD_SKYPACK_PACKAGE, {
225 | skypackPackage: parsedName,
226 | url: packageUrl
227 | })
228 | }
229 |
230 | function createLoadMoreResultsSentinelObserver ($sentinelEl) {
231 | const handleIntersect = async (entries, observer) => {
232 | const [entry] = entries
233 | if (!entry.isIntersecting) return
234 |
235 | observer.disconnect()
236 | await fetchNextPagePackagesAndDisplayResults()
237 | }
238 |
239 | const observerOptions = {
240 | root: $asideBar,
241 | rootMargin: '100%'
242 | }
243 |
244 | const observer = new window.IntersectionObserver(
245 | handleIntersect,
246 | observerOptions
247 | )
248 | observer.observe($sentinelEl)
249 | }
250 |
251 | function showSpinner () {
252 | $spinner.removeAttribute('hidden')
253 | }
254 |
255 | function hideSpinner () {
256 | $spinner.setAttribute('hidden', '')
257 | }
258 |
--------------------------------------------------------------------------------
/src/history.js:
--------------------------------------------------------------------------------
1 | import { persist } from 'zustand/middleware'
2 | import { createStore } from 'zustand/vanilla'
3 | import { $ } from './utils/dom.js'
4 | import { EVENTS, eventBus } from './events-controller.js'
5 |
6 | const genId = () => {
7 | return Date.now()
8 | }
9 |
10 | const useHistoryStore = createStore(
11 | persist(
12 | (set, get) => ({
13 | history: {
14 | current: null,
15 | items: []
16 | },
17 | updateHistory: ({ key, value }) => {
18 | set({ history: { ...get().history, [key]: value } })
19 | },
20 | updateHistoryItem: ({ value }) => {
21 | const id = get().history.current || genId()
22 | const currentHistory = get().history.items
23 | const item = currentHistory.find(item => item.id === id)
24 | const alreadyExists = !!item
25 | const timestamp = new Date().getTime() / 1000
26 |
27 | if (alreadyExists) {
28 | if (value === item.value) return
29 |
30 | set({
31 | history: {
32 | current: id,
33 | items: currentHistory.map(item =>
34 | item.id === id
35 | ? { ...item, value, timestamp }
36 | : item
37 | )
38 | }
39 | })
40 | } else {
41 | const instanceName = 'Untitled'
42 | const regex = new RegExp(`^${instanceName}(\\(\\d+\\))?$`)
43 | const newNameItems = currentHistory.filter(item => regex.test(item.name))
44 | const itemName = newNameItems.length > 0 ? `${instanceName}(${newNameItems.length})` : instanceName
45 | set({ history: { current: id, items: [...currentHistory, { id, name: itemName, value, timestamp }] } })
46 | }
47 | },
48 | removeHistoryItem: ({ id }) => {
49 | const { current, items } = get().history
50 | set({
51 | history:
52 | {
53 | current: current === id
54 | ? null
55 | : current,
56 | items: items.filter(item => item.id !== id)
57 | }
58 | })
59 | },
60 | updateHistoryItemName: ({ id, prevName, newName }) => {
61 | const prevNameLower = prevName.toLocaleLowerCase()
62 | const newNameLower = newName.toLocaleLowerCase()
63 |
64 | if (!newName || prevNameLower === newNameLower) return
65 |
66 | const { items, ...history } = get().history
67 | const regex = new RegExp(`^${newNameLower}(\\(\\d+\\))?$`)
68 | const newNameItems = items.filter(item => item.id !== id && regex.test(item.name.toLocaleLowerCase()))
69 | const alreadyExists = newNameItems.length > 0
70 |
71 | let name
72 | if (alreadyExists) {
73 | const existingNumbers = newNameItems
74 | .map(item => {
75 | const match = item.name.toLocaleLowerCase().match(/\((\d+)\)$/)
76 | return match ? parseInt(match[1], 10) : 0
77 | })
78 | .sort((a, b) => a - b)
79 | const highestNumber = existingNumbers.length > 0 ? existingNumbers[existingNumbers.length - 1] : 0
80 | name = `${newName}(${highestNumber + 1})`
81 | } else {
82 | name = newName
83 | }
84 |
85 | set({
86 | history: {
87 | ...history,
88 | items: items.map(item =>
89 | item.id === id
90 | ? { ...item, name }
91 | : item)
92 | }
93 | })
94 | },
95 | clearHistory: () => set({ history: { current: null, items: [] } })
96 | }),
97 | { name: 'history', getHistory: () => window.localStorage.getItem('history') }
98 | )
99 | )
100 |
101 | export const {
102 | getState: getHistoryState,
103 | setState: setHistoryState,
104 | subscribe: subscribeHistory
105 | } = useHistoryStore
106 |
107 | const $historyList = $('#history .history-list')
108 |
109 | const HISTORY_ICONS = {
110 | remove: `
111 |
112 | `,
113 | edit: `
114 |
115 | `
116 | }
117 |
118 | const { updateHistoryItemName, removeHistoryItem } = getHistoryState()
119 |
120 | const removeButton = ({ id, name, isActive }) => {
121 | const $removeButton = document.createElement('button')
122 | $removeButton.innerHTML = HISTORY_ICONS.remove
123 | $removeButton.ariaLabel = `Remove ${name}`
124 |
125 | $removeButton.addEventListener('click', (e) => {
126 | e.preventDefault()
127 |
128 | removeHistoryItem({ id })
129 | if (isActive) {
130 | eventBus.emit(EVENTS.OPEN_NEW_INSTANCE)
131 | }
132 | })
133 |
134 | return $removeButton
135 | }
136 |
137 | const editButton = ({ id, name }) => {
138 | const $editButton = document.createElement('button')
139 | $editButton.innerHTML = HISTORY_ICONS.edit
140 | $editButton.ariaLabel = `Edit ${name}`
141 |
142 | $editButton.addEventListener('click', (e) => {
143 | e.preventDefault()
144 |
145 | const $button = $historyList.querySelector(`#history-item-${id} button`)
146 | const $input = document.createElement('input')
147 | $input.value = $button.textContent
148 | $button.replaceWith($input)
149 | $input.focus()
150 | $input.select()
151 |
152 | const updateName = () => {
153 | const value = $input.value
154 | $button.textContent = value
155 | updateHistoryItemName({ id, prevName: name, newName: value })
156 | $input.replaceWith($button)
157 | }
158 |
159 | $input.addEventListener('keypress', (e) => {
160 | if (e.key === 'Enter') {
161 | $input.blur()
162 | }
163 | })
164 | $input.addEventListener('blur', () => updateName())
165 | })
166 |
167 | return $editButton
168 | }
169 |
170 | const openItemButton = ({ id, name, value }) => {
171 | const $button = document.createElement('button')
172 | $button.textContent = name
173 | $button.ariaLabel = `Open ${name}`
174 |
175 | $button.addEventListener('click', (e) => {
176 | e.preventDefault()
177 | eventBus.emit(EVENTS.OPEN_EXISTING_INSTANCE, { value, id })
178 | })
179 |
180 | return $button
181 | }
182 |
183 | const createListItem = ({ id, name, value, isActive }) => {
184 | const $li = document.createElement('li')
185 | $li.id = `history-item-${id}`
186 |
187 | if (isActive) {
188 | $li.classList.add('is-active')
189 | }
190 |
191 | const $openButton = openItemButton({ id, name, value })
192 | const $removeButton = removeButton({ id, name, isActive })
193 | const $editButton = editButton({ id, name })
194 | const $actions = document.createElement('div')
195 |
196 | $actions.classList.add('actions')
197 |
198 | $li.appendChild($openButton)
199 | $li.appendChild($actions)
200 |
201 | $actions.appendChild($editButton)
202 | $actions.appendChild($removeButton)
203 |
204 | $li.appendChild($actions)
205 |
206 | return $li
207 | }
208 |
209 | const compareTimestamps = (timestamp) => {
210 | const currentDate = new Date(new Date().getTime())
211 | const givenDate = new Date(timestamp * 1000)
212 |
213 | const differenceInTime = currentDate - givenDate
214 | const differenceInDays = Math.floor(differenceInTime / (1000 * 60 * 60 * 24))
215 |
216 | return differenceInDays
217 | }
218 |
219 | export const setHistory = (history) => {
220 | $historyList.innerHTML = ''
221 | const sortedItems = history.items.sort((a, b) => b.timestamp - a.timestamp)
222 | const groupedItems = {}
223 |
224 | for (let i = 0; i < sortedItems.length; i++) {
225 | const itemTimestamp = sortedItems[i].timestamp
226 | const diff = compareTimestamps(itemTimestamp)
227 | let key = ''
228 |
229 | key = `${diff} days ago`
230 |
231 | if (diff === 0) {
232 | key = 'Today'
233 | }
234 |
235 | if (diff === 1) {
236 | key = 'Yesterday'
237 | }
238 |
239 | if (diff > 30) {
240 | const months = Math.floor(diff / 30)
241 | key = `${months} ${months > 1 ? 'months' : 'month'} ago`
242 | }
243 |
244 | if (diff > 365) {
245 | const years = Math.floor(diff / 365)
246 | key = `${years} ${years > 1 ? 'years' : 'year'} ago`
247 | }
248 |
249 | groupedItems[key] = groupedItems[key] || []
250 | groupedItems[key].push(sortedItems[i])
251 | }
252 |
253 | for (const [key, value] of Object.entries(groupedItems)) {
254 | const $group = document.createElement('div')
255 | $group.classList.add('group')
256 | const $title = document.createElement('h4')
257 | $title.textContent = key
258 |
259 | $group.appendChild($title)
260 | value.forEach(({ id, name, value }) => {
261 | const $li = createListItem({ id, name, value, isActive: history.current === id })
262 | $group.appendChild($li)
263 | })
264 |
265 | $historyList.appendChild($group)
266 | }
267 | }
268 |
--------------------------------------------------------------------------------
/src/console.js:
--------------------------------------------------------------------------------
1 | import { createConsoleBadge } from './constants/console-icons'
2 | import { $ } from './utils/dom'
3 |
4 | const $iframe = $('iframe')
5 | const $consoleList = $('#console .console-list')
6 | const $consoleBadge = $('.console-badge-count')
7 |
8 | let consoleLogCount = 0
9 |
10 | const updateConsoleBadge = () => {
11 | if (consoleLogCount === 0) {
12 | $consoleBadge.setAttribute('hidden', '')
13 | } else {
14 | $consoleBadge.removeAttribute('hidden')
15 | $consoleBadge.textContent = consoleLogCount > 99 ? '+99' : consoleLogCount
16 | }
17 | }
18 |
19 | const clearConsole = () => {
20 | $consoleList.innerHTML = ''
21 | consoleLogCount = 0
22 | updateConsoleBadge()
23 | }
24 |
25 | export const resetConsoleBadge = () => {
26 | consoleLogCount = 0
27 | updateConsoleBadge()
28 | }
29 |
30 | const isValidIdentifier = (key) => /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)
31 |
32 | const escapeHtml = (unsafe) => {
33 | return unsafe
34 | .replace(/&/g, '&')
35 | .replace(//g, '>')
37 | .replace(/"/g, '"')
38 | .replace(/'/g, ''')
39 | }
40 |
41 | const formatTable = (data) => {
42 | if (!data || typeof data !== 'object') {
43 | return formatValue(data)
44 | }
45 |
46 | if (Array.isArray(data)) {
47 | if (data.length === 0) return '[]'
48 |
49 | // Check if array contains objects with similar keys
50 | const firstItem = data[0]
51 | if (typeof firstItem === 'object' && firstItem !== null && !Array.isArray(firstItem)) {
52 | const keys = Object.keys(firstItem)
53 | let table = ''
54 | table += '
(index) '
55 | keys.forEach(key => {
56 | table += `${escapeHtml(key)} `
57 | })
58 | table += ''
59 |
60 | data.forEach((item, index) => {
61 | table += `${index} `
62 | keys.forEach(key => {
63 | const value = item[key]
64 | table += `${formatValue(value)} `
65 | })
66 | table += ' '
67 | })
68 |
69 | table += '
'
70 | return table
71 | }
72 |
73 | // Simple array
74 | let table = '(index) Value '
75 | data.forEach((item, index) => {
76 | table += `${index} ${formatValue(item)} `
77 | })
78 | table += '
'
79 | return table
80 | }
81 |
82 | // Object
83 | const keys = Object.keys(data)
84 | if (keys.length === 0) return '{}'
85 |
86 | let table = '(index) Value '
87 | keys.forEach(key => {
88 | table += `${escapeHtml(key)} ${formatValue(data[key])} `
89 | })
90 | table += '
'
91 | return table
92 | }
93 |
94 | const formatValue = (value, indentLevel = 0) => {
95 | const indent = ' '.repeat(indentLevel)
96 |
97 | if (value === null) {
98 | return 'null '
99 | }
100 |
101 | if (value === undefined) {
102 | return 'undefined '
103 | }
104 |
105 | if (typeof value === 'string') {
106 | return `"${escapeHtml(value)}" `
107 | }
108 |
109 | if (typeof value === 'number') {
110 | return `${value} `
111 | }
112 |
113 | if (typeof value === 'bigint') {
114 | return `${value} `
115 | }
116 |
117 | if (typeof value === 'boolean') {
118 | return `${value} `
119 | }
120 |
121 | if (Array.isArray(value)) {
122 | if (value.length === 0) return '[]'
123 |
124 | let result = '[\n'
125 |
126 | value.forEach((item, index) => {
127 | result += `${indent} ${formatValue(item, indentLevel + 1)}`
128 | if (index < value.length - 1) result += ','
129 | result += '\n'
130 | })
131 |
132 | result += `${indent}]`
133 | return result
134 | }
135 |
136 | if (typeof value === 'object') {
137 | if (value && value.type === 'function') {
138 | const fnContent = escapeHtml(value.content)
139 | const startOfBody = fnContent.indexOf('{')
140 |
141 | const isAsync = value.async
142 | const isGenerator = value.generator
143 |
144 | let className = 'console-fn'
145 | if (isAsync) {
146 | className += ' console-async-fn'
147 | }
148 | if (isGenerator) {
149 | className += ' console-generator-fn'
150 | }
151 |
152 | // Function signature logic
153 | let signature
154 | if (startOfBody === -1) {
155 | signature = fnContent.trim()
156 | } else {
157 | signature = fnContent.substring(0, startOfBody).trim()
158 | }
159 |
160 | signature = signature
161 | .replace(/^async\s+/, '')
162 | .replace(/^function\s*\*\s*/, '')
163 | .replace(/^function\s*/, '')
164 | .trim()
165 |
166 | // Function body logic
167 | let functionBody
168 | if (startOfBody === -1) {
169 | functionBody = ''
170 | } else {
171 | const bodyContent = fnContent.substring(startOfBody + 1, fnContent.lastIndexOf('}'))
172 | const compressedBody = bodyContent.trim().length > 0 ? '...' : ''
173 | functionBody = ` {${compressedBody}}`
174 | }
175 |
176 | return `${signature}${functionBody} `
177 | }
178 |
179 | if (value && value.type === 'circular') {
180 | return '[Circular] '
181 | }
182 |
183 | if (value && value.type === 'regexp') {
184 | return `${escapeHtml(value.value)} `
185 | }
186 |
187 | if (value && value.type === 'unknown') {
188 | return `${escapeHtml(String(value.value))} `
189 | }
190 |
191 | if (value && value.type === 'date') {
192 | const isoString = value.value
193 | const cleanedString = isoString
194 | .replace('T', ' ')
195 | .replace(/\.\d{3}Z$/, '')
196 |
197 | return `${escapeHtml(cleanedString)} `
198 | }
199 |
200 | if (value && value.type === 'set') {
201 | const short = `Set(${value.size})`
202 |
203 | if (value.size === 0) return `${short} {} `
204 |
205 | let result = `${short} {\n`
206 |
207 | ;(value.values || []).forEach((v, index) => {
208 | result += `${indent} ${formatValue(v, indentLevel + 1)}`
209 | if (index < value.values.length - 1) result += ','
210 | result += '\n'
211 | })
212 |
213 | result += `${indent}} `
214 | return result
215 | }
216 |
217 | const isSymbolKey = (k) => typeof k === 'string' && k.startsWith('Symbol(') && k.endsWith(')')
218 |
219 | const formatKey = (key) => {
220 | return (isValidIdentifier(key) || isSymbolKey(key))
221 | ? `${escapeHtml(key)} `
222 | : `"${escapeHtml(key)}" `
223 | }
224 |
225 | if (value && value.type === 'map') {
226 | const short = `Map(${value.size})`
227 | if (value.size === 0) return `${short} {} `
228 |
229 | let result = `${short} {\n`
230 |
231 | ;(value.entries || []).forEach(([k, v], index) => {
232 | const keyFormatted = formatKey(k)
233 | const valueFormatted = formatValue(v, indentLevel + 1)
234 |
235 | result += `${indent} ${keyFormatted} => ${valueFormatted}`
236 |
237 | if (index < value.entries.length - 1) result += ','
238 | result += '\n'
239 | })
240 |
241 | result += `${indent}} `
242 | return result
243 | }
244 |
245 | const keys = Object.keys(value)
246 | if (keys.length === 0) return '{}'
247 |
248 | let result = '{\n'
249 |
250 | keys.forEach((key, index) => {
251 | const renderedKey = formatKey(key)
252 |
253 | result += `${indent} ${renderedKey}: ${formatValue(value[key], indentLevel + 1)}`
254 |
255 | if (index < keys.length - 1) result += ','
256 | result += '\n'
257 | })
258 |
259 | result += `${indent}}`
260 | return result
261 | }
262 |
263 | return String(value)
264 | }
265 |
266 | const createListItem = (content, type) => {
267 | const $li = document.createElement('li')
268 | $li.classList.add(`log-${type.split(':')[1]}`)
269 |
270 | const badge = createConsoleBadge(type)
271 | if (badge) {
272 | $li.innerHTML = badge
273 | }
274 |
275 | const $pre = document.createElement('pre')
276 | $pre.style.whiteSpace = 'pre-wrap'
277 | $pre.style.margin = '0'
278 |
279 | $pre.innerHTML = content
280 |
281 | $li.appendChild($pre)
282 |
283 | return $li
284 | }
285 |
286 | const handlers = {
287 | system: (payload) => {
288 | if (payload === 'clear') {
289 | clearConsole()
290 | }
291 | },
292 | error: (payload) => {
293 | const { line, column, message } = payload
294 | const errorItem = createListItem(`${line}:${column} ${message}`, 'error')
295 | errorItem.classList.add('error')
296 | $consoleList.appendChild(errorItem)
297 | consoleLogCount++
298 | updateConsoleBadge()
299 | },
300 | default: (payload, type) => {
301 | let content
302 | if (type === 'log:table') {
303 | content = payload.map(item => formatTable(item)).join(' ')
304 | } else {
305 | content = payload.map(item => formatValue(item)).join(' ')
306 | }
307 | const listItem = createListItem(content, type)
308 | $consoleList.appendChild(listItem)
309 | consoleLogCount++
310 | updateConsoleBadge()
311 | },
312 | loop: (payload) => {
313 | clearConsole()
314 | const { message } = payload
315 | const errorItem = createListItem(`${message}`, 'error')
316 | errorItem.classList.add('error')
317 | $consoleList.appendChild(errorItem)
318 | consoleLogCount++
319 | updateConsoleBadge()
320 | }
321 | }
322 |
323 | window.addEventListener('message', (ev) => {
324 | const { console: consoleData = {} } = ev.data
325 | const { payload, type } = consoleData
326 |
327 | if (ev.source === $iframe.contentWindow) {
328 | const handler = handlers[type] || handlers.default
329 | handler(payload, type)
330 | } else if (type === 'loop') {
331 | handlers.loop(payload)
332 | }
333 | })
334 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Attribution-NonCommercial-NoDerivatives 4.0 International
2 |
3 | =======================================================================
4 |
5 | Creative Commons Corporation ("Creative Commons") is not a law firm and
6 | does not provide legal services or legal advice. Distribution of
7 | Creative Commons public licenses does not create a lawyer-client or
8 | other relationship. Creative Commons makes its licenses and related
9 | information available on an "as-is" basis. Creative Commons gives no
10 | warranties regarding its licenses, any material licensed under their
11 | terms and conditions, or any related information. Creative Commons
12 | disclaims all liability for damages resulting from their use to the
13 | fullest extent possible.
14 |
15 | Using Creative Commons Public Licenses
16 |
17 | Creative Commons public licenses provide a standard set of terms and
18 | conditions that creators and other rights holders may use to share
19 | original works of authorship and other material subject to copyright
20 | and certain other rights specified in the public license below. The
21 | following considerations are for informational purposes only, are not
22 | exhaustive, and do not form part of our licenses.
23 |
24 | Considerations for licensors: Our public licenses are
25 | intended for use by those authorized to give the public
26 | permission to use material in ways otherwise restricted by
27 | copyright and certain other rights. Our licenses are
28 | irrevocable. Licensors should read and understand the terms
29 | and conditions of the license they choose before applying it.
30 | Licensors should also secure all rights necessary before
31 | applying our licenses so that the public can reuse the
32 | material as expected. Licensors should clearly mark any
33 | material not subject to the license. This includes other CC-
34 | licensed material, or material used under an exception or
35 | limitation to copyright. More considerations for licensors:
36 | wiki.creativecommons.org/Considerations_for_licensors
37 |
38 | Considerations for the public: By using one of our public
39 | licenses, a licensor grants the public permission to use the
40 | licensed material under specified terms and conditions. If
41 | the licensor's permission is not necessary for any reason--for
42 | example, because of any applicable exception or limitation to
43 | copyright--then that use is not regulated by the license. Our
44 | licenses grant only permissions under copyright and certain
45 | other rights that a licensor has authority to grant. Use of
46 | the licensed material may still be restricted for other
47 | reasons, including because others have copyright or other
48 | rights in the material. A licensor may make special requests,
49 | such as asking that all changes be marked or described.
50 | Although not required by our licenses, you are encouraged to
51 | respect those requests where reasonable. More considerations
52 | for the public:
53 | wiki.creativecommons.org/Considerations_for_licensees
54 |
55 | =======================================================================
56 |
57 | Creative Commons Attribution-NonCommercial-NoDerivatives 4.0
58 | International Public License
59 |
60 | By exercising the Licensed Rights (defined below), You accept and agree
61 | to be bound by the terms and conditions of this Creative Commons
62 | Attribution-NonCommercial-NoDerivatives 4.0 International Public
63 | License ("Public License"). To the extent this Public License may be
64 | interpreted as a contract, You are granted the Licensed Rights in
65 | consideration of Your acceptance of these terms and conditions, and the
66 | Licensor grants You such rights in consideration of benefits the
67 | Licensor receives from making the Licensed Material available under
68 | these terms and conditions.
69 |
70 |
71 | Section 1 -- Definitions.
72 |
73 | a. Adapted Material means material subject to Copyright and Similar
74 | Rights that is derived from or based upon the Licensed Material
75 | and in which the Licensed Material is translated, altered,
76 | arranged, transformed, or otherwise modified in a manner requiring
77 | permission under the Copyright and Similar Rights held by the
78 | Licensor. For purposes of this Public License, where the Licensed
79 | Material is a musical work, performance, or sound recording,
80 | Adapted Material is always produced where the Licensed Material is
81 | synched in timed relation with a moving image.
82 |
83 | b. Copyright and Similar Rights means copyright and/or similar rights
84 | closely related to copyright including, without limitation,
85 | performance, broadcast, sound recording, and Sui Generis Database
86 | Rights, without regard to how the rights are labeled or
87 | categorized. For purposes of this Public License, the rights
88 | specified in Section 2(b)(1)-(2) are not Copyright and Similar
89 | Rights.
90 |
91 | c. Effective Technological Measures means those measures that, in the
92 | absence of proper authority, may not be circumvented under laws
93 | fulfilling obligations under Article 11 of the WIPO Copyright
94 | Treaty adopted on December 20, 1996, and/or similar international
95 | agreements.
96 |
97 | d. Exceptions and Limitations means fair use, fair dealing, and/or
98 | any other exception or limitation to Copyright and Similar Rights
99 | that applies to Your use of the Licensed Material.
100 |
101 | e. Licensed Material means the artistic or literary work, database,
102 | or other material to which the Licensor applied this Public
103 | License.
104 |
105 | f. Licensed Rights means the rights granted to You subject to the
106 | terms and conditions of this Public License, which are limited to
107 | all Copyright and Similar Rights that apply to Your use of the
108 | Licensed Material and that the Licensor has authority to license.
109 |
110 | g. Licensor means the individual(s) or entity(ies) granting rights
111 | under this Public License.
112 |
113 | h. NonCommercial means not primarily intended for or directed towards
114 | commercial advantage or monetary compensation. For purposes of
115 | this Public License, the exchange of the Licensed Material for
116 | other material subject to Copyright and Similar Rights by digital
117 | file-sharing or similar means is NonCommercial provided there is
118 | no payment of monetary compensation in connection with the
119 | exchange.
120 |
121 | i. Share means to provide material to the public by any means or
122 | process that requires permission under the Licensed Rights, such
123 | as reproduction, public display, public performance, distribution,
124 | dissemination, communication, or importation, and to make material
125 | available to the public including in ways that members of the
126 | public may access the material from a place and at a time
127 | individually chosen by them.
128 |
129 | j. Sui Generis Database Rights means rights other than copyright
130 | resulting from Directive 96/9/EC of the European Parliament and of
131 | the Council of 11 March 1996 on the legal protection of databases,
132 | as amended and/or succeeded, as well as other essentially
133 | equivalent rights anywhere in the world.
134 |
135 | k. You means the individual or entity exercising the Licensed Rights
136 | under this Public License. Your has a corresponding meaning.
137 |
138 |
139 | Section 2 -- Scope.
140 |
141 | a. License grant.
142 |
143 | 1. Subject to the terms and conditions of this Public License,
144 | the Licensor hereby grants You a worldwide, royalty-free,
145 | non-sublicensable, non-exclusive, irrevocable license to
146 | exercise the Licensed Rights in the Licensed Material to:
147 |
148 | a. reproduce and Share the Licensed Material, in whole or
149 | in part, for NonCommercial purposes only; and
150 |
151 | b. produce and reproduce, but not Share, Adapted Material
152 | for NonCommercial purposes only.
153 |
154 | 2. Exceptions and Limitations. For the avoidance of doubt, where
155 | Exceptions and Limitations apply to Your use, this Public
156 | License does not apply, and You do not need to comply with
157 | its terms and conditions.
158 |
159 | 3. Term. The term of this Public License is specified in Section
160 | 6(a).
161 |
162 | 4. Media and formats; technical modifications allowed. The
163 | Licensor authorizes You to exercise the Licensed Rights in
164 | all media and formats whether now known or hereafter created,
165 | and to make technical modifications necessary to do so. The
166 | Licensor waives and/or agrees not to assert any right or
167 | authority to forbid You from making technical modifications
168 | necessary to exercise the Licensed Rights, including
169 | technical modifications necessary to circumvent Effective
170 | Technological Measures. For purposes of this Public License,
171 | simply making modifications authorized by this Section 2(a)
172 | (4) never produces Adapted Material.
173 |
174 | 5. Downstream recipients.
175 |
176 | a. Offer from the Licensor -- Licensed Material. Every
177 | recipient of the Licensed Material automatically
178 | receives an offer from the Licensor to exercise the
179 | Licensed Rights under the terms and conditions of this
180 | Public License.
181 |
182 | b. No downstream restrictions. You may not offer or impose
183 | any additional or different terms or conditions on, or
184 | apply any Effective Technological Measures to, the
185 | Licensed Material if doing so restricts exercise of the
186 | Licensed Rights by any recipient of the Licensed
187 | Material.
188 |
189 | 6. No endorsement. Nothing in this Public License constitutes or
190 | may be construed as permission to assert or imply that You
191 | are, or that Your use of the Licensed Material is, connected
192 | with, or sponsored, endorsed, or granted official status by,
193 | the Licensor or others designated to receive attribution as
194 | provided in Section 3(a)(1)(A)(i).
195 |
196 | b. Other rights.
197 |
198 | 1. Moral rights, such as the right of integrity, are not
199 | licensed under this Public License, nor are publicity,
200 | privacy, and/or other similar personality rights; however, to
201 | the extent possible, the Licensor waives and/or agrees not to
202 | assert any such rights held by the Licensor to the limited
203 | extent necessary to allow You to exercise the Licensed
204 | Rights, but not otherwise.
205 |
206 | 2. Patent and trademark rights are not licensed under this
207 | Public License.
208 |
209 | 3. To the extent possible, the Licensor waives any right to
210 | collect royalties from You for the exercise of the Licensed
211 | Rights, whether directly or through a collecting society
212 | under any voluntary or waivable statutory or compulsory
213 | licensing scheme. In all other cases the Licensor expressly
214 | reserves any right to collect such royalties, including when
215 | the Licensed Material is used other than for NonCommercial
216 | purposes.
217 |
218 |
219 | Section 3 -- License Conditions.
220 |
221 | Your exercise of the Licensed Rights is expressly made subject to the
222 | following conditions.
223 |
224 | a. Attribution.
225 |
226 | 1. If You Share the Licensed Material, You must:
227 |
228 | a. retain the following if it is supplied by the Licensor
229 | with the Licensed Material:
230 |
231 | i. identification of the creator(s) of the Licensed
232 | Material and any others designated to receive
233 | attribution, in any reasonable manner requested by
234 | the Licensor (including by pseudonym if
235 | designated);
236 |
237 | ii. a copyright notice;
238 |
239 | iii. a notice that refers to this Public License;
240 |
241 | iv. a notice that refers to the disclaimer of
242 | warranties;
243 |
244 | v. a URI or hyperlink to the Licensed Material to the
245 | extent reasonably practicable;
246 |
247 | b. indicate if You modified the Licensed Material and
248 | retain an indication of any previous modifications; and
249 |
250 | c. indicate the Licensed Material is licensed under this
251 | Public License, and include the text of, or the URI or
252 | hyperlink to, this Public License.
253 |
254 | For the avoidance of doubt, You do not have permission under
255 | this Public License to Share Adapted Material.
256 |
257 | 2. You may satisfy the conditions in Section 3(a)(1) in any
258 | reasonable manner based on the medium, means, and context in
259 | which You Share the Licensed Material. For example, it may be
260 | reasonable to satisfy the conditions by providing a URI or
261 | hyperlink to a resource that includes the required
262 | information.
263 |
264 | 3. If requested by the Licensor, You must remove any of the
265 | information required by Section 3(a)(1)(A) to the extent
266 | reasonably practicable.
267 |
268 |
269 | Section 4 -- Sui Generis Database Rights.
270 |
271 | Where the Licensed Rights include Sui Generis Database Rights that
272 | apply to Your use of the Licensed Material:
273 |
274 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right
275 | to extract, reuse, reproduce, and Share all or a substantial
276 | portion of the contents of the database for NonCommercial purposes
277 | only and provided You do not Share Adapted Material;
278 |
279 | b. if You include all or a substantial portion of the database
280 | contents in a database in which You have Sui Generis Database
281 | Rights, then the database in which You have Sui Generis Database
282 | Rights (but not its individual contents) is Adapted Material; and
283 |
284 | c. You must comply with the conditions in Section 3(a) if You Share
285 | all or a substantial portion of the contents of the database.
286 |
287 | For the avoidance of doubt, this Section 4 supplements and does not
288 | replace Your obligations under this Public License where the Licensed
289 | Rights include other Copyright and Similar Rights.
290 |
291 |
292 | Section 5 -- Disclaimer of Warranties and Limitation of Liability.
293 |
294 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
295 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
296 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
297 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
298 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
299 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
300 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
301 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
302 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
303 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
304 |
305 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
306 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
307 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
308 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
309 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
310 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
311 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
312 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
313 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
314 |
315 | c. The disclaimer of warranties and limitation of liability provided
316 | above shall be interpreted in a manner that, to the extent
317 | possible, most closely approximates an absolute disclaimer and
318 | waiver of all liability.
319 |
320 |
321 | Section 6 -- Term and Termination.
322 |
323 | a. This Public License applies for the term of the Copyright and
324 | Similar Rights licensed here. However, if You fail to comply with
325 | this Public License, then Your rights under this Public License
326 | terminate automatically.
327 |
328 | b. Where Your right to use the Licensed Material has terminated under
329 | Section 6(a), it reinstates:
330 |
331 | 1. automatically as of the date the violation is cured, provided
332 | it is cured within 30 days of Your discovery of the
333 | violation; or
334 |
335 | 2. upon express reinstatement by the Licensor.
336 |
337 | For the avoidance of doubt, this Section 6(b) does not affect any
338 | right the Licensor may have to seek remedies for Your violations
339 | of this Public License.
340 |
341 | c. For the avoidance of doubt, the Licensor may also offer the
342 | Licensed Material under separate terms or conditions or stop
343 | distributing the Licensed Material at any time; however, doing so
344 | will not terminate this Public License.
345 |
346 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
347 | License.
348 |
349 |
350 | Section 7 -- Other Terms and Conditions.
351 |
352 | a. The Licensor shall not be bound by any additional or different
353 | terms or conditions communicated by You unless expressly agreed.
354 |
355 | b. Any arrangements, understandings, or agreements regarding the
356 | Licensed Material not stated herein are separate from and
357 | independent of the terms and conditions of this Public License.
358 |
359 |
360 | Section 8 -- Interpretation.
361 |
362 | a. For the avoidance of doubt, this Public License does not, and
363 | shall not be interpreted to, reduce, limit, restrict, or impose
364 | conditions on any use of the Licensed Material that could lawfully
365 | be made without permission under this Public License.
366 |
367 | b. To the extent possible, if any provision of this Public License is
368 | deemed unenforceable, it shall be automatically reformed to the
369 | minimum extent necessary to make it enforceable. If the provision
370 | cannot be reformed, it shall be severed from this Public License
371 | without affecting the enforceability of the remaining terms and
372 | conditions.
373 |
374 | c. No term or condition of this Public License will be waived and no
375 | failure to comply consented to unless expressly agreed to by the
376 | Licensor.
377 |
378 | d. Nothing in this Public License constitutes or may be interpreted
379 | as a limitation upon, or waiver of, any privileges and immunities
380 | that apply to the Licensor or You, including from the legal
381 | processes of any jurisdiction or authority.
382 |
383 | =======================================================================
384 |
385 | Creative Commons is not a party to its public
386 | licenses. Notwithstanding, Creative Commons may elect to apply one of
387 | its public licenses to material it publishes and in those instances
388 | will be considered the “Licensor.” The text of the Creative Commons
389 | public licenses is dedicated to the public domain under the CC0 Public
390 | Domain Dedication. Except for the limited purpose of indicating that
391 | material is shared under a Creative Commons public license or as
392 | otherwise permitted by the Creative Commons policies published at
393 | creativecommons.org/policies, Creative Commons does not authorize the
394 | use of the trademark "Creative Commons" or any other trademark or logo
395 | of Creative Commons without its prior written consent including,
396 | without limitation, in connection with any unauthorized modifications
397 | to any of its public licenses or any other arrangements,
398 | understandings, or agreements concerning use of licensed material. For
399 | the avoidance of doubt, this paragraph does not form part of the
400 | public licenses.
401 |
402 | Creative Commons may be contacted at creativecommons.org.
403 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | codi.link | HTML, CSS, JavaScript Live Editor Playground
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
Drop File to Open
44 |
45 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
59 | Editor
60 |
61 |
64 |
65 |
66 |
67 |
68 | Dependencies
69 |
70 |
73 |
74 |
75 |
76 |
77 | Console
78 |
79 |
82 |
83 | 0
84 |
85 |
86 |
88 | Download
89 |
90 |
93 |
94 |
95 |
96 |
97 | History
98 |
99 |
102 |
103 |
104 |
105 |
106 |
107 |
109 | Preview
110 |
111 |
114 |
115 |
116 |
117 |
119 | Copy to clipboard
120 |
121 |
124 |
125 |
126 |
127 | Settings
128 |
129 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
171 |
172 |
188 |
189 |
220 |
221 |
448 |
449 |
450 |
451 |
452 |
453 | HTML
454 | JavaScript
455 | CSS
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
--------------------------------------------------------------------------------