This commit is contained in:
dmitry-vsl
2025-05-30 19:59:02 +00:00
parent 1a30311de4
commit 75b1c5e942
30 changed files with 959 additions and 832 deletions

View File

@@ -2,34 +2,33 @@
// Author: Sarah Bricault // Author: Sarah Bricault
// Canvas setup // Canvas setup
const canvas = document.createElement('canvas') const canvas = document.createElement("canvas")
canvas.width = 700 canvas.width = 700
canvas.height = 700 canvas.height = 700
document.body.appendChild(canvas) document.body.appendChild(canvas)
const ctx = canvas.getContext('2d') const ctx = canvas.getContext("2d")
ctx.translate(canvas.width / 2, canvas.height) ctx.translate(canvas.width / 2, canvas.height)
// Draw a tree // Draw a tree
await fractalTreeBasic({totalIterations: 10, basicLength: 10, rotate: 25}) await fractalTreeBasic({ totalIterations: 10, basicLength: 10, rotate: 25 })
function sleep() { function sleep() {
return new Promise(resolve => setTimeout(resolve, 3)) return new Promise(resolve => setTimeout(resolve, 3))
} }
async function fractalTreeBasic({totalIterations, basicLength, rotate}) { async function fractalTreeBasic({ totalIterations, basicLength, rotate }) {
// Draw the tree trunk // Draw the tree trunk
const trunkLength = basicLength * 2 * Math.pow(1.2, totalIterations + 1) const trunkLength = basicLength * 2 * Math.pow(1.2, totalIterations + 1)
const width = Math.pow(totalIterations, 0.6) const width = Math.pow(totalIterations, 0.6)
ctx.beginPath() ctx.beginPath()
ctx.moveTo(0, 0) ctx.moveTo(0, 0)
ctx.lineTo(0, - trunkLength) ctx.lineTo(0, -trunkLength)
ctx.lineWidth = width ctx.lineWidth = width
ctx.strokeStyle = 'black' ctx.strokeStyle = "black"
ctx.stroke() ctx.stroke()
await drawBranch(90, [0, - trunkLength], totalIterations + 1) await drawBranch(90, [0, -trunkLength], totalIterations + 1)
async function drawBranch(angle, startPoint, iterations) { async function drawBranch(angle, startPoint, iterations) {
const len = basicLength * Math.pow(1.2, iterations) const len = basicLength * Math.pow(1.2, iterations)
@@ -38,7 +37,7 @@ async function fractalTreeBasic({totalIterations, basicLength, rotate}) {
const red = Math.floor(255 - (iterations / totalIterations) * 255) const red = Math.floor(255 - (iterations / totalIterations) * 255)
const green = 0 const green = 0
const blue = Math.floor( 255 - (iterations / totalIterations) * 255) const blue = Math.floor(255 - (iterations / totalIterations) * 255)
const color = `rgb(${red}, ${green}, ${blue})` const color = `rgb(${red}, ${green}, ${blue})`
const x1 = startPoint[0] const x1 = startPoint[0]
@@ -47,7 +46,7 @@ async function fractalTreeBasic({totalIterations, basicLength, rotate}) {
const y2 = y1 - len * Math.sin((angle * Math.PI) / 180) const y2 = y1 - len * Math.sin((angle * Math.PI) / 180)
const x2 = x1 + len * Math.cos((angle * Math.PI) / 180) const x2 = x1 + len * Math.cos((angle * Math.PI) / 180)
console.log('draw branch', x1, y1, x2, y2) console.log("draw branch", x1, y1, x2, y2)
ctx.beginPath() ctx.beginPath()
ctx.moveTo(x1, y1) ctx.moveTo(x1, y1)

View File

@@ -1,8 +1,8 @@
// Original source: // Original source:
// https://www.freecodecamp.org/news/how-to-create-animated-bubbles-with-html5-canvas-and-javascript/ // https://www.freecodecamp.org/news/how-to-create-animated-bubbles-with-html5-canvas-and-javascript/
const canvas = document.createElement('canvas') const canvas = document.createElement("canvas")
canvas.style.backgroundColor = '#00b4ff' canvas.style.backgroundColor = "#00b4ff"
document.body.appendChild(canvas) document.body.appendChild(canvas)
canvas.width = window.innerWidth canvas.width = window.innerWidth
canvas.height = window.innerHeight canvas.height = window.innerHeight
@@ -10,9 +10,9 @@ canvas.height = window.innerHeight
const context = canvas.getContext("2d") const context = canvas.getContext("2d")
context.font = "30px Arial" context.font = "30px Arial"
context.textAlign = 'center' context.textAlign = "center"
context.fillStyle = 'white' context.fillStyle = "white"
context.fillText('Click to spawn bubbles', canvas.width/2, canvas.height/2) context.fillText("Click to spawn bubbles", canvas.width / 2, canvas.height / 2)
let circles = [] let circles = []
@@ -28,7 +28,7 @@ function draw(circle) {
1, 1,
circle.x + 0.5, circle.x + 0.5,
circle.y + 0.5, circle.y + 0.5,
circle.radius circle.radius,
) )
gradient.addColorStop(0.3, "rgba(255, 255, 255, 0.3)") gradient.addColorStop(0.3, "rgba(255, 255, 255, 0.3)")
@@ -39,20 +39,20 @@ function draw(circle) {
} }
function move(circle, timeDelta) { function move(circle, timeDelta) {
circle.x = circle.x + timeDelta*circle.dx circle.x = circle.x + timeDelta * circle.dx
circle.y = circle.y - timeDelta*circle.dy circle.y = circle.y - timeDelta * circle.dy
} }
let intervalId let intervalId
function startAnimation() { function startAnimation() {
if(intervalId == null) { if (intervalId == null) {
intervalId = setInterval(animate, 20) intervalId = setInterval(animate, 20)
} }
} }
function stopAnimation() { function stopAnimation() {
if(intervalId != null) { if (intervalId != null) {
clearInterval(intervalId) clearInterval(intervalId)
intervalId = null intervalId = null
} }
@@ -65,31 +65,31 @@ const animate = () => {
const timeDelta = prevFrameTime == null ? 0 : now - prevFrameTime const timeDelta = prevFrameTime == null ? 0 : now - prevFrameTime
prevFrameTime = now prevFrameTime = now
if(circles.length == 0) { if (circles.length == 0) {
return return
} }
context.clearRect(0, 0, canvas.width, canvas.height) context.clearRect(0, 0, canvas.width, canvas.height)
circles.forEach(circle => { circles.forEach(circle => {
move(circle, timeDelta) move(circle, timeDelta)
draw(circle) draw(circle)
}) })
} }
const createCircles = (event) => { const createCircles = event => {
startAnimation() startAnimation()
circles = circles.concat(Array.from({length: 50}, () => ( circles = circles.concat(
{ Array.from({ length: 50 }, () => ({
x: event.pageX, x: event.pageX,
y: event.pageY, y: event.pageY,
radius: Math.random() * 50, radius: Math.random() * 50,
dx: Math.random() * 0.3, dx: Math.random() * 0.3,
dy: Math.random() * 0.7, dy: Math.random() * 0.7,
hue: 200, hue: 200,
} })),
))) )
} }
canvas.addEventListener("click", createCircles) canvas.addEventListener("click", createCircles)

View File

@@ -1,34 +1,40 @@
window.addEventListener('load', () => { window.addEventListener("load", () => {
const text = document.createElement('input') const text = document.createElement("input")
const checkbox = document.createElement('input') const checkbox = document.createElement("input")
checkbox.setAttribute('type', 'checkbox') checkbox.setAttribute("type", "checkbox")
const radio = document.createElement('input') const radio = document.createElement("input")
radio.setAttribute('type', 'radio') radio.setAttribute("type", "radio")
const range = document.createElement('input') const range = document.createElement("input")
range.setAttribute('type', 'range') range.setAttribute("type", "range")
const select = document.createElement('select') const select = document.createElement("select")
Array.from({length: 5}, (_, i) => i).forEach(i => { Array.from({ length: 5 }, (_, i) => i).forEach(i => {
const option = document.createElement('option') const option = document.createElement("option")
option.setAttribute('value', i) option.setAttribute("value", i)
option.innerText = i option.innerText = i
select.appendChild(option) select.appendChild(option)
}) })
const div = document.createElement('div') const div = document.createElement("div")
const elements = { text, checkbox, range, select, radio, div }
const elements = { text, checkbox, range, select, radio, div}
Object.entries(elements).forEach(([name, el]) => { Object.entries(elements).forEach(([name, el]) => {
document.body.appendChild(el); document.body.appendChild(el)
['click', 'input', 'change'].forEach(type => { ;["click", "input", "change"].forEach(type => {
el.addEventListener(type, e => { el.addEventListener(type, e => {
const row = document.createElement('div') const row = document.createElement("div")
div.appendChild(row) div.appendChild(row)
row.innerText = [name, type, e.target.value, e.target.checked, e.target.selectedIndex].join(', ') row.innerText = [
name,
type,
e.target.value,
e.target.checked,
e.target.selectedIndex,
].join(", ")
}) })
}) })
}) })

View File

@@ -1,21 +1,25 @@
import {ethers} from 'https://unpkg.com/ethers/dist/ethers.js' import { ethers } from "ethers"
const URL = 'https://rpc.ankr.com/eth' const URL = "https://eth-mainnet.public.blastapi.io"
const provider = await ethers.getDefaultProvider(URL) const provider = await ethers.getDefaultProvider(URL)
const latest = await provider.getBlock('latest') const latest = await provider.getBlock("latest")
/* /*
Find ethereum block by timestamp using binary search Find ethereum block by timestamp using binary search
*/ */
async function getBlockNumberByTimestamp(timestamp, low = 0, high = latest.number) { async function getBlockNumberByTimestamp(
if(low + 1 == high) { timestamp,
low = 0,
high = latest.number,
) {
if (low + 1 == high) {
return low return low
} else { } else {
const mid = Math.floor((low + high) / 2) const mid = Math.floor((low + high) / 2)
const midBlock = await provider.getBlock(mid) const midBlock = await provider.getBlock(mid)
if(midBlock.timestamp > timestamp) { if (midBlock.timestamp > timestamp) {
return getBlockNumberByTimestamp(timestamp, low, mid) return getBlockNumberByTimestamp(timestamp, low, mid)
} else { } else {
return getBlockNumberByTimestamp(timestamp, mid, high) return getBlockNumberByTimestamp(timestamp, mid, high)
@@ -23,6 +27,6 @@ async function getBlockNumberByTimestamp(timestamp, low = 0, high = latest.numbe
} }
} }
const timestamp = new Date('2019-06-01').getTime()/1000 const timestamp = new Date("2019-06-01").getTime() / 1000
const blockNumber = await getBlockNumberByTimestamp(timestamp) const blockNumber = await getBlockNumberByTimestamp(timestamp)
const block = await provider.getBlock(blockNumber) const block = await provider.getBlock(blockNumber)

View File

@@ -1,15 +1,15 @@
import {ethers} from 'https://unpkg.com/ethers@5.7.2/dist/ethers.esm.js' import { ethers } from "ethers"
const URL = 'https://rpc.ankr.com/eth_goerli' const URL = "https://eth-mainnet.public.blastapi.io"
const p = ethers.getDefaultProvider(URL) const p = ethers.getDefaultProvider(URL)
const latest = await p.getBlock() const latest = await p.getBlock()
const txs = await Promise.all(latest.transactions.map(t => const txs = await Promise.all(
p.getTransactionReceipt(t) latest.transactions.map(t => p.getTransactionReceipt(t)),
)) )
const totalGas = txs const totalGas = txs
.filter(tx => tx != null) .filter(tx => tx != null)
.reduce((gas,tx) => gas.add(tx.gasUsed), ethers.BigNumber.from(0)) .reduce((gas, tx) => gas.add(tx.gasUsed), ethers.BigNumber.from(0))

View File

@@ -1,7 +1,7 @@
// Fibonacci numbers // Fibonacci numbers
function fib(n) { function fib(n) {
if(n == 0 || n == 1) { if (n == 0 || n == 1) {
return n return n
} else { } else {
return fib(n - 1) + fib(n - 2) return fib(n - 1) + fib(n - 2)

View File

@@ -2,30 +2,29 @@
// Author: Sarah Bricault // Author: Sarah Bricault
// Canvas setup // Canvas setup
const canvas = document.createElement('canvas') const canvas = document.createElement("canvas")
canvas.width = 700 canvas.width = 700
canvas.height = 700 canvas.height = 700
document.body.appendChild(canvas) document.body.appendChild(canvas)
const ctx = canvas.getContext('2d') const ctx = canvas.getContext("2d")
ctx.translate(canvas.width / 2, canvas.height) ctx.translate(canvas.width / 2, canvas.height)
// Draw a tree // Draw a tree
fractalTreeBasic({totalIterations: 10, basicLength: 10, rotate: 25}) fractalTreeBasic({ totalIterations: 10, basicLength: 10, rotate: 25 })
function fractalTreeBasic({totalIterations, basicLength, rotate}) {
function fractalTreeBasic({ totalIterations, basicLength, rotate }) {
// Draw the tree trunk // Draw the tree trunk
const trunkLength = basicLength * 2 * Math.pow(1.2, totalIterations + 1) const trunkLength = basicLength * 2 * Math.pow(1.2, totalIterations + 1)
const width = Math.pow(totalIterations, 0.6) const width = Math.pow(totalIterations, 0.6)
ctx.beginPath() ctx.beginPath()
ctx.moveTo(0, 0) ctx.moveTo(0, 0)
ctx.lineTo(0, - trunkLength) ctx.lineTo(0, -trunkLength)
ctx.lineWidth = width ctx.lineWidth = width
ctx.strokeStyle = 'black' ctx.strokeStyle = "black"
ctx.stroke() ctx.stroke()
drawBranch(90, [0, - trunkLength], totalIterations + 1) drawBranch(90, [0, -trunkLength], totalIterations + 1)
function drawBranch(angle, startPoint, iterations) { function drawBranch(angle, startPoint, iterations) {
const len = basicLength * Math.pow(1.2, iterations) const len = basicLength * Math.pow(1.2, iterations)
@@ -34,7 +33,7 @@ function fractalTreeBasic({totalIterations, basicLength, rotate}) {
const red = Math.floor(255 - (iterations / totalIterations) * 255) const red = Math.floor(255 - (iterations / totalIterations) * 255)
const green = 0 const green = 0
const blue = Math.floor( 255 - (iterations / totalIterations) * 255) const blue = Math.floor(255 - (iterations / totalIterations) * 255)
const color = `rgb(${red}, ${green}, ${blue})` const color = `rgb(${red}, ${green}, ${blue})`
const x1 = startPoint[0] const x1 = startPoint[0]
@@ -43,7 +42,7 @@ function fractalTreeBasic({totalIterations, basicLength, rotate}) {
const y2 = y1 - len * Math.sin((angle * Math.PI) / 180) const y2 = y1 - len * Math.sin((angle * Math.PI) / 180)
const x2 = x1 + len * Math.cos((angle * Math.PI) / 180) const x2 = x1 + len * Math.cos((angle * Math.PI) / 180)
console.log('draw branch', x1, y1, x2, y2) console.log("draw branch", x1, y1, x2, y2)
ctx.beginPath() ctx.beginPath()
ctx.moveTo(x1, y1) ctx.moveTo(x1, y1)

View File

@@ -1,7 +1,8 @@
import _ from 'https://unpkg.com/lodash-es' import _ from "lodash-es"
async function getPopularLanguages() { async function getPopularLanguages() {
const url = 'https://api.github.com/search/repositories?q=stars:%3E1&sort=stars' const url =
"https://api.github.com/search/repositories?q=stars:%3E1&sort=stars"
const resp = await fetch(url) const resp = await fetch(url)
const repos = await resp.json() const repos = await resp.json()
return _(repos.items) return _(repos.items)
@@ -9,7 +10,7 @@ async function getPopularLanguages() {
.filter(l => l != null) .filter(l => l != null)
.countBy() .countBy()
.toPairs() .toPairs()
.orderBy(([lang, useCount]) => useCount, 'desc') .orderBy(([lang, useCount]) => useCount, "desc")
.map(([lang]) => lang) .map(([lang]) => lang)
.value() .value()
} }

View File

@@ -1,6 +1,6 @@
import _ from 'https://unpkg.com/lodash-es' import _ from "lodash-es"
const url = 'https://api.github.com/search/repositories?q=stars:%3E1&sort=stars' const url = "https://api.github.com/search/repositories?q=stars:%3E1&sort=stars"
const resp = await fetch(url) const resp = await fetch(url)
const repos = await resp.json() const repos = await resp.json()
const langs = _(repos.items) const langs = _(repos.items)
@@ -8,43 +8,18 @@ const langs = _(repos.items)
.filter(l => l != null) .filter(l => l != null)
.countBy() .countBy()
.toPairs() .toPairs()
.map(([language, count]) => ({language, count})) .map(([language, count]) => ({ language, count }))
.value() .value()
import {barY} from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm"; import { barY } from "@observablehq/plot"
/* /*
Move the cursor to the following line and see the plot displayed alongside the code Move the cursor to the following line and see the plot displayed alongside the code
*/ */
barY(langs, {x: "language", y: "count", sort: {x: "y", reverse: true}, fill: 'purple'}) barY(langs, {
.plot() x: "language",
y: "count",
sort: { x: "y", reverse: true },
fill: "purple",
}).plot()

View File

@@ -1,38 +1,44 @@
const fib = n => { const fib = n => {
if(n == 0) { if (n == 0) {
return 0 return 0
} }
if(n == 1) { if (n == 1) {
return 1 return 1
} }
return fib(n - 1) + fib(n - 2) return fib(n - 1) + fib(n - 2)
} }
/* external */
import {h, render} from 'https://unpkg.com/preact?module';
/* external */ /* external */
import {Stateful} from './stateful.js' import { h, render } from "preact"
/* external */
import { Stateful } from "./stateful.js"
const Fibonacci = Stateful({ const Fibonacci = Stateful({
getInitialState: () => ({index: 0}), getInitialState: () => ({ index: 0 }),
handlers: { handlers: {
prev: ({index}, event) => ({index: index - 1}), prev: ({ index }, event) => ({ index: index - 1 }),
next: ({index}, event) => ({index: index + 1}), next: ({ index }, event) => ({ index: index + 1 }),
}, },
render: (props, state, handlers) => render: (props, state, handlers) =>
h('div', null, h(
h('h1', null, "div",
'nth Fibonacci number is ', null,
h(
"h1",
null,
"nth Fibonacci number is ",
fib(state.index), fib(state.index),
' for n = ', " for n = ",
state.index state.index,
), ),
h('button', {onClick: handlers.prev}, 'Previous'), ' ', h("button", { onClick: handlers.prev }, "Previous"),
h('button', {onClick: handlers.next}, 'Next'), ' ', " ",
) h("button", { onClick: handlers.next }, "Next"),
" ",
),
}) })
render(h(Fibonacci), globalThis.document.body) render(h(Fibonacci), globalThis.document.body)

View File

@@ -1,18 +1,15 @@
import {Component} from 'https://unpkg.com/preact?module'; import { Component } from "preact"
export const Stateful = ({getInitialState, handlers, render}) => {
export const Stateful = ({ getInitialState, handlers, render }) => {
return class extends Component { return class extends Component {
constructor() { constructor() {
super() super()
this.compState = getInitialState() this.compState = getInitialState()
this.handlers = Object.fromEntries( this.handlers = Object.fromEntries(
Object Object.entries(handlers).map(([name, h]) => [
.entries(handlers) name,
.map(([name, h]) => this.makeHandler(h),
[name, this.makeHandler(h)] ]),
)
) )
} }
@@ -27,5 +24,4 @@ export const Stateful = ({getInitialState, handlers, render}) => {
return render(this.props, this.compState, this.handlers) return render(this.props, this.compState, this.handlers)
} }
} }
} }

View File

@@ -2,7 +2,7 @@
Example of a TODO app built using the Preact library Example of a TODO app built using the Preact library
*/ */
import React from 'https://esm.sh/preact/compat' import React from "preact/compat"
// Core // Core
@@ -13,7 +13,7 @@ let state
if (globalThis.leporello) { if (globalThis.leporello) {
// Retrieve initial state from Leporello storage // Retrieve initial state from Leporello storage
// See: https://github.com/leporello-js/leporello-js?tab=readme-ov-file#saving-state-between-page-reloads // See: https://github.com/leporello-js/leporello-js?tab=readme-ov-file#saving-state-between-page-reloads
state = globalThis.leporello.storage.get('state') state = globalThis.leporello.storage.get("state")
} }
/* /*
@@ -21,14 +21,16 @@ if (globalThis.leporello) {
This helper function wraps such a function so that its result updates the global state This helper function wraps such a function so that its result updates the global state
and can be used as an event handler. and can be used as an event handler.
*/ */
const handler = fn => (...args) => { const handler =
state = fn(state, ...args) fn =>
if (globalThis.leporello) { (...args) => {
// Persist state to Leporello storage to restore it after page reloads state = fn(state, ...args)
globalThis.leporello.storage.set('state', state) if (globalThis.leporello) {
// Persist state to Leporello storage to restore it after page reloads
globalThis.leporello.storage.set("state", state)
}
render()
} }
render()
}
// Higher-order function that injects the current state into a component // Higher-order function that injects the current state into a component
const connect = comp => props => comp(props, state) const connect = comp => props => comp(props, state)
@@ -39,12 +41,12 @@ const render = () => React.render(<App />, document.body)
if (state == null) { if (state == null) {
state = { state = {
todos: [], todos: [],
text: '', text: "",
filter: 'ALL', filter: "ALL",
} }
} }
window.addEventListener('load', render) window.addEventListener("load", render)
// Components // Components
@@ -68,10 +70,10 @@ const Footer = () => (
const FilterLink = connect(({ filter, children }, state) => { const FilterLink = connect(({ filter, children }, state) => {
const disabled = state.filter == filter const disabled = state.filter == filter
return ( return (
<button <button
onClick={handler(changeFilter.bind(null, filter))} onClick={handler(changeFilter.bind(null, filter))}
disabled={disabled} disabled={disabled}
style={{ marginLeft: '4px' }} style={{ marginLeft: "4px" }}
> >
{children} {children}
</button> </button>
@@ -87,9 +89,9 @@ const TodoList = connect((_, state) => (
)) ))
const Todo = ({ todo }) => ( const Todo = ({ todo }) => (
<li <li
onClick={handler(toggleTodo.bind(null, todo))} onClick={handler(toggleTodo.bind(null, todo))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} style={{ textDecoration: todo.completed ? "line-through" : "none" }}
> >
{todo.text} {todo.text}
</li> </li>
@@ -99,11 +101,7 @@ const AddTodo = connect((_, state) => {
return ( return (
<div> <div>
<form onSubmit={handler(createTodo)}> <form onSubmit={handler(createTodo)}>
<input <input value={state.text} onChange={handler(changeText)} autoFocus />
value={state.text}
onChange={handler(changeText)}
autoFocus
/>
<button type="submit">Add Todo</button> <button type="submit">Add Todo</button>
</form> </form>
</div> </div>
@@ -114,14 +112,14 @@ const AddTodo = connect((_, state) => {
// Returns a filtered list of TODOs based on the current filter state // Returns a filtered list of TODOs based on the current filter state
function visibleTodos(state) { function visibleTodos(state) {
if (state.filter == 'ALL') { if (state.filter == "ALL") {
return state.todos return state.todos
} else if (state.filter == 'ACTIVE') { } else if (state.filter == "ACTIVE") {
return state.todos.filter(t => !t.completed) return state.todos.filter(t => !t.completed)
} else if (state.filter == 'COMPLETED') { } else if (state.filter == "COMPLETED") {
return state.todos.filter(t => t.completed) return state.todos.filter(t => t.completed)
} else { } else {
throw new Error('Unknown filter') throw new Error("Unknown filter")
} }
} }
@@ -146,18 +144,18 @@ function createTodo(state, e) {
} }
return { return {
...state, ...state,
todos: [...state.todos, { text: state.text }], todos: [...state.todos, { text: state.text }],
text: '', text: "",
} }
} }
// Toggles the completion state of a TODO item // Toggles the completion state of a TODO item
function toggleTodo(todo, state) { function toggleTodo(todo, state) {
return { return {
...state, ...state,
todos: state.todos.map(t => todos: state.todos.map(t =>
t == todo ? { ...todo, completed: !todo.completed } : t t == todo ? { ...todo, completed: !todo.completed } : t,
) ),
} }
} }

View File

@@ -1,18 +1,20 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Redux Todos Example</title> <title>Redux Todos Example</title>
<script src='https://unpkg.com/react/umd/react.development.js'></script> <script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src='https://unpkg.com/react-dom/umd/react-dom.development.js'></script> <script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src='https://unpkg.com/redux'></script> <script src="https://unpkg.com/redux"></script>
<script src='https://unpkg.com/react-redux'></script> <script src="https://unpkg.com/react-redux"></script>
<script type='module'> <script type="module">
if(new URLSearchParams(window.location.search).get('leporello') == null) { if (
await import('../src/index.js'); new URLSearchParams(window.location.search).get("leporello") == null
) {
await import("../src/index.js")
} }
</script> </script>
</head> </head>

View File

@@ -6,23 +6,23 @@ const nextTodoId = new Function(`
`)() `)()
export const addTodo = text => ({ export const addTodo = text => ({
type: 'ADD_TODO', type: "ADD_TODO",
id: nextTodoId(), id: nextTodoId(),
text text,
}) })
export const setVisibilityFilter = filter => ({ export const setVisibilityFilter = filter => ({
type: 'SET_VISIBILITY_FILTER', type: "SET_VISIBILITY_FILTER",
filter filter,
}) })
export const toggleTodo = id => ({ export const toggleTodo = id => ({
type: 'TOGGLE_TODO', type: "TOGGLE_TODO",
id id,
}) })
export const VisibilityFilters = { export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL', SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: 'SHOW_COMPLETED', SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: 'SHOW_ACTIVE' SHOW_ACTIVE: "SHOW_ACTIVE",
} }

View File

@@ -1,15 +1,9 @@
import Footer from './Footer.js' import Footer from "./Footer.js"
import AddTodo from '../containers/AddTodo.js' import AddTodo from "../containers/AddTodo.js"
import VisibleTodoList from '../containers/VisibleTodoList.js' import VisibleTodoList from "../containers/VisibleTodoList.js"
const h = React.createElement const h = React.createElement
const App = () => ( const App = () => h("div", null, h(AddTodo), h(VisibleTodoList), h(Footer))
h('div', null,
h(AddTodo),
h(VisibleTodoList),
h(Footer),
)
)
export default App export default App

View File

@@ -1,15 +1,16 @@
import FilterLink from '../containers/FilterLink.js' import FilterLink from "../containers/FilterLink.js"
import { VisibilityFilters } from '../actions/index.js' import { VisibilityFilters } from "../actions/index.js"
const h = React.createElement const h = React.createElement
const Footer = () => ( const Footer = () =>
h('div', null, h(
h('span', null, 'Show: '), "div",
h(FilterLink, {filter: VisibilityFilters.SHOW_ALL}, 'All'), null,
h(FilterLink, {filter: VisibilityFilters.SHOW_ACTIVE}, 'Active'), h("span", null, "Show: "),
h(FilterLink, {filter: VisibilityFilters.SHOW_COMPLETED}, 'Completed'), h(FilterLink, { filter: VisibilityFilters.SHOW_ALL }, "All"),
h(FilterLink, { filter: VisibilityFilters.SHOW_ACTIVE }, "Active"),
h(FilterLink, { filter: VisibilityFilters.SHOW_COMPLETED }, "Completed"),
) )
)
export default Footer export default Footer

View File

@@ -1,13 +1,16 @@
const h = React.createElement const h = React.createElement
const Link = ({ active, children, onClick }) => ( const Link = ({ active, children, onClick }) =>
h('button', { h(
onClick, "button",
disabled: active, {
style:{ onClick,
marginLeft: '4px', disabled: active,
} style: {
}, children) marginLeft: "4px",
) },
},
children,
)
export default Link export default Link

View File

@@ -1,12 +1,15 @@
const h = React.createElement const h = React.createElement
const Todo = ({ onClick, completed, text }) => ( const Todo = ({ onClick, completed, text }) =>
h('li', { h(
onClick, "li",
style: { {
textDecoration: completed ? 'line-through' : 'none' onClick,
style: {
textDecoration: completed ? "line-through" : "none",
},
}, },
}, text) text,
) )
export default Todo export default Todo

View File

@@ -1,17 +1,18 @@
import Todo from './Todo.js' import Todo from "./Todo.js"
const h = React.createElement const h = React.createElement
const TodoList = ({ todos, toggleTodo }) => ( const TodoList = ({ todos, toggleTodo }) =>
h('ul', null, h(
"ul",
null,
todos.map(todo => todos.map(todo =>
h(Todo, { h(Todo, {
key: todo.id, key: todo.id,
...todo, ...todo,
onClick: () => toggleTodo(todo.id), onClick: () => toggleTodo(todo.id),
}) }),
) ),
) )
)
export default TodoList export default TodoList

View File

@@ -1,25 +1,27 @@
import { addTodo } from '../actions/index.js' import { addTodo } from "../actions/index.js"
const h = React.createElement const h = React.createElement
const AddTodo = ({ dispatch }) => { const AddTodo = ({ dispatch }) => {
const inputref = {} const inputref = {}
return ( return h(
h('div', null, "div",
h('form', { null,
h(
"form",
{
onSubmit: e => { onSubmit: e => {
e.preventDefault() e.preventDefault()
if (inputref.input.value.trim()) { if (inputref.input.value.trim()) {
dispatch(addTodo(inputref.input.value)) dispatch(addTodo(inputref.input.value))
Object.assign(inputref.input, {value: ''}) Object.assign(inputref.input, { value: "" })
} }
} },
}, },
h('input', {ref: input => Object.assign(inputref, {input})}), h("input", { ref: input => Object.assign(inputref, { input }) }),
h('button', {type: 'submit'}, 'Add Todo') h("button", { type: "submit" }, "Add Todo"),
) ),
)
) )
} }

View File

@@ -1,15 +1,12 @@
import { setVisibilityFilter } from '../actions/index.js' import { setVisibilityFilter } from "../actions/index.js"
import Link from '../components/Link.js' import Link from "../components/Link.js"
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.visibilityFilter active: ownProps.filter === state.visibilityFilter,
}) })
const mapDispatchToProps = (dispatch, ownProps) => ({ const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => dispatch(setVisibilityFilter(ownProps.filter)) onClick: () => dispatch(setVisibilityFilter(ownProps.filter)),
}) })
export default ReactRedux.connect( export default ReactRedux.connect(mapStateToProps, mapDispatchToProps)(Link)
mapStateToProps,
mapDispatchToProps
)(Link)

View File

@@ -1,28 +1,25 @@
import { toggleTodo } from '../actions/index.js' import { toggleTodo } from "../actions/index.js"
import TodoList from '../components/TodoList.js' import TodoList from "../components/TodoList.js"
import { VisibilityFilters } from '../actions/index.js' import { VisibilityFilters } from "../actions/index.js"
const getVisibleTodos = (todos, filter) => { const getVisibleTodos = (todos, filter) => {
if(filter == VisibilityFilters.SHOW_ALL) { if (filter == VisibilityFilters.SHOW_ALL) {
return todos return todos
} else if(filter == VisibilityFilters.SHOW_COMPLETED) { } else if (filter == VisibilityFilters.SHOW_COMPLETED) {
return todos.filter(t => t.completed) return todos.filter(t => t.completed)
} else if(filter == VisibilityFilters.SHOW_ACTIVE) { } else if (filter == VisibilityFilters.SHOW_ACTIVE) {
return todos.filter(t => !t.completed) return todos.filter(t => !t.completed)
} else { } else {
throw new Error('Unknown filter: ' + filter) throw new Error("Unknown filter: " + filter)
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter) todos: getVisibleTodos(state.todos, state.visibilityFilter),
}) })
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
toggleTodo: id => dispatch(toggleTodo(id)) toggleTodo: id => dispatch(toggleTodo(id)),
}) })
export default ReactRedux.connect( export default ReactRedux.connect(mapStateToProps, mapDispatchToProps)(TodoList)
mapStateToProps,
mapDispatchToProps
)(TodoList)

View File

@@ -1,11 +1,11 @@
import App from './components/App.js' import App from "./components/App.js"
import rootReducer from './reducers/index.js' import rootReducer from "./reducers/index.js"
const h = React.createElement const h = React.createElement
const store = Redux.createStore(rootReducer) const store = Redux.createStore(rootReducer)
ReactDOM.render( ReactDOM.render(
h(ReactRedux.Provider, {store}, h(App)), h(ReactRedux.Provider, { store }, h(App)),
document.getElementById('root') document.getElementById("root"),
) )

View File

@@ -1,7 +1,7 @@
import todos from './todos.js' import todos from "./todos.js"
import visibilityFilter from './visibilityFilter.js' import visibilityFilter from "./visibilityFilter.js"
export default Redux.combineReducers({ export default Redux.combineReducers({
todos, todos,
visibilityFilter visibilityFilter,
}) })

View File

@@ -1,18 +1,16 @@
const todos = (state = [], action) => { const todos = (state = [], action) => {
if(action.type == 'ADD_TODO') { if (action.type == "ADD_TODO") {
return [ return [
...state, ...state,
{ {
id: action.id, id: action.id,
text: action.text, text: action.text,
completed: false completed: false,
} },
] ]
} else if(action.type == 'TOGGLE_TODO') { } else if (action.type == "TOGGLE_TODO") {
return state.map(todo => return state.map(todo =>
(todo.id === action.id) todo.id === action.id ? { ...todo, completed: !todo.completed } : todo,
? {...todo, completed: !todo.completed}
: todo
) )
} else { } else {
return state return state

View File

@@ -1,7 +1,7 @@
import { VisibilityFilters } from '../actions/index.js' import { VisibilityFilters } from "../actions/index.js"
const visibilityFilter = (state = VisibilityFilters.SHOW_ALL, action) => { const visibilityFilter = (state = VisibilityFilters.SHOW_ALL, action) => {
if(action.type == 'SET_VISIBILITY_FILTER') { if (action.type == "SET_VISIBILITY_FILTER") {
return action.filter return action.filter
} else { } else {
return state return state

View File

@@ -5,6 +5,8 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Leporello.js</title> <title>Leporello.js</title>
<script type='module' src='./src/ts_libs.js' async></script>
<script src='ace/ace.js'></script> <script src='ace/ace.js'></script>
<script src='ace/keybinding-vim.js'></script> <script src='ace/keybinding-vim.js'></script>
<script src='ace/ext-language_tools.js'></script> <script src='ace/ext-language_tools.js'></script>
@@ -20,526 +22,7 @@
<script type='module' src="./src/launch.js"></script> <script type='module' src="./src/launch.js"></script>
<style> <link rel=stylesheet href='./styles.css'></link>
:root {
--shadow_color: rgb(171 200 214);
--active_color: rgb(173, 228, 253);
--error-color: #ff000024;
--warn-color: #fff6d5;
}
html, body, .app {
height: 100%;
}
body {
margin: 0px;
}
.spinner {
display: inline-block;
height: 0.8em;
width: 0.8em;
min-width: 0.8em;
border-radius: 50%;
border-top: none !important;
border: 2px solid;
animation: rotate 0.6s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.app {
/* same as ace editor */
font-family: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
}
.app::backdrop {
background-color: white;
}
.root {
height: 100%;
display: grid;
grid-template-areas:
"code code"
"bottom files"
"statusbar statusbar";
grid-template-columns: 70% 30%;
grid-template-rows: auto 1fr 2.5em;
}
.editor_container, .bottom, .files_container, .statusbar {
box-shadow: 1px 1px 3px 0px var(--shadow_color);
}
.editor_container, .bottom, .statusbar, .files_container {
margin: 8px;
}
.editor_container:focus-within,
.bottom:focus-within,
.files_container:focus-within,
dialog {
outline: none;
box-shadow: 1px 1px 6px 3px var(--shadow_color);
}
.tab_content:focus-within, .problems_container:focus-within {
outline: none;
}
.editor_container {
height: 55vh;
resize: vertical;
position: relative;
grid-area: code;
font-size: 16px;
}
/* ace markers */
.selection {
position: absolute;
background-color: #ff00ff;
z-index: 1; /* make it on top of evaluated_ok and evaluated_error */
}
.evaluated_ok {
position: absolute;
background-color: rgb(225, 244, 253);
}
.evaluated_error {
position: absolute;
background-color: var(--error-color);
}
.error-code {
/*
TODO: make underline like in all editors
*/
position: absolute;
border-bottom: 4px solid red;
}
/* end of ace markers */
.eval_error {
padding: 0em 1em;
color: red;
}
/* Tabs */
.tabs {
display: flex;
padding-bottom: 0.5em;
}
.tabs > .tab {
margin-right: 1em;
padding: 0.3em 1em;
display: flex;
align-items: center;
}
.tabs > .tab.active {
background-color: rgb(225, 244, 253);
}
/* debugger */
.bottom {
grid-area: bottom;
overflow: auto;
display: grid;
}
.debugger {
display: flex;
flex-direction: column;
}
.debugger_wrapper {
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
.debugger, .problems_container {
padding: 5px;
overflow: auto;
}
.logs, .io_trace {
padding-left: 1em;
}
.logs .log {
cursor: pointer;
}
.logs .log.active {
background-color: var(--active_color) !important;
}
.logs .log.error {
background-color: var(--error-color);
color: black !important; /* override red color that is set for calltree */
&.native {
color: grey !important;
}
}
.logs .log.warn {
background-color: var(--warn-color);
}
.tab_content {
flex: 1;
overflow: auto;
}
.callnode {
/* This makes every callnode be the size of the the longest one, so
* every callnode is clickable anywhere in the calltree view, and
* background for active callnodes is as wide as the entire container.
* Useful when scrolling very wide call trees */
min-width: fit-content;
margin-left: 1em;
}
.callnode .active {
background-color: var(--active_color);
}
.call_el {
/*
Make active callnode background start from the left of the calltree
view
*/
margin-left: -1000vw;
padding-left: 1000vw;
width: 100%;
cursor: pointer;
display: inline-block;
}
.call_el .expand_icon, .call_el .expand_icon_placeholder {
padding-left: 5px;
padding-right: 2px;
}
.call_header {
white-space: nowrap;
}
.call_header.error {
color: red;
}
.call_header.error.native {
color: red;
}
.call_header.native {
font-style: italic;
color: grey;
}
.call_header .loop_step {
color: grey;
border-radius: 5px;
padding: 1px 5px;
font-size: 0.9em;
margin-right: 0.3em;
}
/* problems view */
.problem a {
color: red;
}
/* files */
.files_container {
overflow: auto;
grid-area: files;
display: flex;
flex-direction: column;
}
.allow_file_access {
display: flex;
flex-direction: column;
}
.allow_file_access .subtitle {
font-size: 0.8em;
}
.files {
overflow: auto;
padding: 5px;
}
.files .file {
margin-left: 1em;
}
.files > .file {
margin-left: 0em !important;
}
.files .file_title {
display: flex;
}
.files .file_title.active {
background-color: var(--active_color);
}
.files .file_title .select_entrypoint {
margin-left: auto;
width: 40px;
text-align: center;
}
.files .file_title .icon {
display: inline-block;
margin-right: 5px;
width: 1em;
}
.file_actions {
position: relative;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 5px;
background-color: rgb(225 244 253 / 80%);
}
.file_actions .file_action {
margin-right: 2em;
}
.file_actions .select_entrypoint_title {
width: 40px;
position: absolute;
right: 7px;
font-size: 0.8em;
text-align: center;
}
/* value_explorer */
.embed_value_explorer_container.is_not_dom_el {
height: 0px;
}
.embed_value_explorer_container.is_dom_el {
padding: 1em;
}
.embed_value_explorer_wrapper {
/* preserve wrapper from getting clicks for code line left to it */
pointer-events: none;
}
.embed_value_explorer_container.is_not_dom_el .embed_value_explorer_wrapper {
margin-left: 1em;
}
.embed_value_explorer_content {
pointer-events: initial;
white-space: pre;
max-width: fit-content;
background-color: white;
box-shadow: 1px 2px 9px -1px var(--shadow_color);
}
.embed_value_explorer_content:focus {
outline: none;
box-shadow: 1px 2px 11px 1px var(--shadow_color);
}
.embed_value_explorer_content > .value_explorer_node {
margin-left: 0 !important;
}
.embed_value_explorer_control {
display: block;
margin-bottom: 1em;
font-size: 0.9em;
}
.value_explorer_node {
margin-left: 1em;
}
.value_explorer_header {
display: inline-block;
padding-right: 1em;
cursor: pointer;
}
.value_explorer_header .expand_icon {
padding: 5px;
}
.value_explorer_header.active {
background-color: rgb(148, 227, 191);
}
.value_explorer_key {
color: rgb(150, 0, 128);
font-weight: bold;
}
/* status */
.statusbar {
margin-bottom: 0px;
grid-area: statusbar;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.statusbar .spinner {
margin-right: 0.5em;
}
.status, .current_file {
font-size: 1.5em;
}
.status {
color: red;
}
.statusbar_action {
margin-right: 2em;
}
.statusbar_action.first {
margin-left: auto;
}
.open_app_window_button {
position: relative;
}
.open_app_window_tooltip {
padding: 1em;
position: absolute;
margin-bottom: 20px;
bottom: 100%;
border: none;
font-size: 1.7em;
background-color: rgb(120 206 247);
border-radius: 21px;
transform: scale(0);
transition: transform 0.3s;
}
.open_app_window_tooltip.on {
transform: scale(1);
}
.open_app_window_tooltip:after {
content: '';
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-top: 20px solid rgb(120 206 247);
position: absolute;
bottom: -20px;
left: 50%;
transform: translate(-50%);
}
.options {
padding: 5px;
}
.options > * {
margin: 5px;
}
.show_help, .github {
margin: 0em 0.5em;
}
.share_button, .upload_button {
border: none;
color: white;
background: rgb(23 166 236);
}
.share_button {
font-size: 1.2em;
margin-left: 1em;
margin-right: 0.5em;
}
.share_button[disabled] {
background: grey;
}
.share_dialog input, .share_dialog button {
font-size: 1.2em;
}
.share_dialog button {
padding: 5px;
height: 2em;
}
dialog {
border: none;
}
dialog::backdrop {
background-color: rgb(225 244 253 / 80%);
}
.help_dialog[open] {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0;
min-width: 70%;
min-height: 70%;
background-color: white;
}
.help {
padding: 2em;
border-spacing: 5px;
}
.help th {
padding: 0.5em;
}
.help th.key {
width: 5em;
}
.help td.key {
background-color: rgb(225, 244, 253, 0.5);
border-radius: 10px;
text-align: center;
}
.help_dialog form {
margin-bottom: 1em;
}
</style>
</head> </head>

File diff suppressed because one or more lines are too long

114
src/ts_libs.js Normal file
View File

@@ -0,0 +1,114 @@
const lib_names = [
'lib.d.ts',
'lib.decorators.d.ts',
'lib.decorators.legacy.d.ts',
'lib.dom.asynciterable.d.ts',
'lib.dom.d.ts',
'lib.dom.iterable.d.ts',
'lib.es2015.collection.d.ts',
'lib.es2015.core.d.ts',
'lib.es2015.d.ts',
'lib.es2015.generator.d.ts',
'lib.es2015.iterable.d.ts',
'lib.es2015.promise.d.ts',
'lib.es2015.proxy.d.ts',
'lib.es2015.reflect.d.ts',
'lib.es2015.symbol.d.ts',
'lib.es2015.symbol.wellknown.d.ts',
'lib.es2016.array.include.d.ts',
'lib.es2016.d.ts',
'lib.es2016.full.d.ts',
'lib.es2016.intl.d.ts',
'lib.es2017.arraybuffer.d.ts',
'lib.es2017.d.ts',
'lib.es2017.date.d.ts',
'lib.es2017.full.d.ts',
'lib.es2017.intl.d.ts',
'lib.es2017.object.d.ts',
'lib.es2017.sharedmemory.d.ts',
'lib.es2017.string.d.ts',
'lib.es2017.typedarrays.d.ts',
'lib.es2018.asyncgenerator.d.ts',
'lib.es2018.asynciterable.d.ts',
'lib.es2018.d.ts',
'lib.es2018.full.d.ts',
'lib.es2018.intl.d.ts',
'lib.es2018.promise.d.ts',
'lib.es2018.regexp.d.ts',
'lib.es2019.array.d.ts',
'lib.es2019.d.ts',
'lib.es2019.full.d.ts',
'lib.es2019.intl.d.ts',
'lib.es2019.object.d.ts',
'lib.es2019.string.d.ts',
'lib.es2019.symbol.d.ts',
'lib.es2020.bigint.d.ts',
'lib.es2020.d.ts',
'lib.es2020.date.d.ts',
'lib.es2020.full.d.ts',
'lib.es2020.intl.d.ts',
'lib.es2020.number.d.ts',
'lib.es2020.promise.d.ts',
'lib.es2020.sharedmemory.d.ts',
'lib.es2020.string.d.ts',
'lib.es2020.symbol.wellknown.d.ts',
'lib.es2021.d.ts',
'lib.es2021.full.d.ts',
'lib.es2021.intl.d.ts',
'lib.es2021.promise.d.ts',
'lib.es2021.string.d.ts',
'lib.es2021.weakref.d.ts',
'lib.es2022.array.d.ts',
'lib.es2022.d.ts',
'lib.es2022.error.d.ts',
'lib.es2022.full.d.ts',
'lib.es2022.intl.d.ts',
'lib.es2022.object.d.ts',
'lib.es2022.regexp.d.ts',
'lib.es2022.string.d.ts',
'lib.es2023.array.d.ts',
'lib.es2023.collection.d.ts',
'lib.es2023.d.ts',
'lib.es2023.full.d.ts',
'lib.es2023.intl.d.ts',
'lib.es2024.arraybuffer.d.ts',
'lib.es2024.collection.d.ts',
'lib.es2024.d.ts',
'lib.es2024.full.d.ts',
'lib.es2024.object.d.ts',
'lib.es2024.promise.d.ts',
'lib.es2024.regexp.d.ts',
'lib.es2024.sharedmemory.d.ts',
'lib.es2024.string.d.ts',
'lib.es5.d.ts',
'lib.es6.d.ts',
'lib.esnext.array.d.ts',
'lib.esnext.collection.d.ts',
'lib.esnext.d.ts',
'lib.esnext.decorators.d.ts',
'lib.esnext.disposable.d.ts',
'lib.esnext.full.d.ts',
'lib.esnext.intl.d.ts',
'lib.esnext.iterator.d.ts',
]
export let ts_libs_promise = load_ts_libs()
async function load_ts_libs() {
if(globalThis.process == null) {
return Object.fromEntries(await Promise.all(
lib_names.map(name =>
fetch('typescript/' + name).then(r => r.text()).then(text => ([name, text]))
)
))
} else {
const fs = await import('fs')
return Object.fromEntries(
lib_names.map(name =>
[name, fs.readFileSync('typescript/' + name, 'ascii')]
)
)
}
}

548
styles.css Normal file
View File

@@ -0,0 +1,548 @@
:root {
--shadow_color: rgb(171 200 214);
--active_color: rgb(173, 228, 253);
--error-color: #ff000024;
--warn-color: #fff6d5;
}
html, body, .app {
height: 100%;
background-color: #f4f4f4;
}
body {
margin: 0px;
}
.spinner {
display: inline-block;
height: 0.8em;
width: 0.8em;
min-width: 0.8em;
border-radius: 50%;
border-top: none !important;
border: 2px solid;
animation: rotate 0.6s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.app {
/* same as ace editor */
font-family: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
}
.app::backdrop {
background-color: white;
}
.root {
height: 100%;
display: grid;
grid-template-areas:
"code"
"bottom"
"statusbar";
grid-template-rows: auto 1fr 2.5em;
}
.editor_container, .bottom, .statusbar {
box-shadow: 1px 1px 3px 0px var(--shadow_color);
background-color: white;
}
.editor_container, .bottom, .statusbar {
margin: 8px;
}
.editor_container:focus-within,
.bottom:focus-within,
dialog {
outline: none;
box-shadow: 1px 1px 6px 3px var(--shadow_color);
}
.tab_content:focus-within, .problems_container:focus-within {
outline: none;
}
.editor_container {
height: 55vh;
resize: vertical;
position: relative;
grid-area: code;
font-size: 16px;
}
/* ace markers */
.selection {
position: absolute;
background-color: #ff00ff;
z-index: 1; /* make it on top of evaluated_ok and evaluated_error */
}
.evaluated_ok {
position: absolute;
background-color: rgb(225, 244, 253);
}
.evaluated_error {
position: absolute;
background-color: var(--error-color);
}
.error-code {
/*
TODO: make underline like in all editors
*/
position: absolute;
border-bottom: 4px solid red;
}
/* end of ace markers */
.eval_error {
padding: 0em 1em;
color: red;
}
/* Tabs */
.tabs {
font-family: system-ui;
display: flex;
padding-bottom: 0.5em;
}
.tabs > .tab {
margin-right: 1em;
padding: 0.3em 1em;
display: flex;
align-items: center;
}
.tabs > .tab.active {
background-color: rgb(225, 244, 253);
}
/* debugger */
.bottom {
grid-area: bottom;
overflow: auto;
display: grid;
}
.debugger {
display: flex;
flex-direction: column;
}
.debugger_wrapper {
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
.debugger, .problems_container {
padding: 5px;
overflow: auto;
}
.logs, .io_trace {
padding-left: 1em;
}
.logs .log {
cursor: pointer;
}
.logs .log.active {
background-color: var(--active_color) !important;
}
.logs .log.error {
background-color: var(--error-color);
color: black !important; /* override red color that is set for calltree */
&.native {
color: grey !important;
}
}
.logs .log.warn {
background-color: var(--warn-color);
}
.tab_content {
flex: 1;
overflow: auto;
}
.callnode {
/* This makes every callnode be the size of the the longest one, so
* every callnode is clickable anywhere in the calltree view, and
* background for active callnodes is as wide as the entire container.
* Useful when scrolling very wide call trees */
min-width: fit-content;
margin-left: 1em;
}
.callnode .active {
background-color: var(--active_color);
}
.call_el {
/*
Make active callnode background start from the left of the calltree
view
*/
margin-left: -1000vw;
padding-left: 1000vw;
width: 100%;
cursor: pointer;
display: inline-block;
}
.call_el .expand_icon, .call_el .expand_icon_placeholder {
padding-left: 5px;
padding-right: 2px;
}
.call_header {
white-space: nowrap;
}
.call_header.error {
color: red;
}
.call_header.error.native {
color: red;
}
.call_header.native {
font-style: italic;
color: grey;
}
.call_header .loop_step {
color: grey;
font-size: 0.9em;
margin-right: 0.3em;
}
/* io trace */
.io_trace .event {
border-radius: 1em;
line-height: 2em;
padding: 0.1em 0.5em;
background-color: var(--active_color);
}
/* problems view */
.problem a {
color: red;
}
/* files */
.files_container {
overflow: auto;
display: flex;
flex-direction: column;
}
.allow_file_access {
display: flex;
flex-direction: column;
}
.allow_file_access .subtitle {
font-size: 0.8em;
}
.files {
overflow: auto;
padding: 5px;
}
.files .file {
margin-left: 1em;
}
.files > .file {
margin-left: 0em !important;
}
.files .file_title {
display: flex;
margin-left: -100vw;
padding-left: 100vw;
}
.files .file_title.active {
background-color: var(--active_color);
}
.files .file_title .select_entrypoint {
margin-left: auto;
width: 3em;
margin-right: 0.7em;
text-align: center;
}
.files .file_title .icon {
display: inline-block;
margin-right: 5px;
width: 1em;
}
.file_actions {
font-family: system-ui;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 1em;
background-color: rgb(225 244 253 / 80%);
}
.file_actions .file_action {
margin-right: 2em;
}
.file_actions .select_entrypoint_title {
width: 3em;
text-align: center;
}
/* value_explorer */
.embed_value_explorer_container.is_not_dom_el {
height: 0px;
}
.embed_value_explorer_container.is_dom_el {
padding: 1em;
}
.embed_value_explorer_wrapper {
/* preserve wrapper from getting clicks for code line left to it */
pointer-events: none;
}
.embed_value_explorer_container.is_not_dom_el .embed_value_explorer_wrapper {
margin-left: 1em;
}
.embed_value_explorer_content {
pointer-events: initial;
white-space: pre;
max-width: fit-content;
background-color: white;
box-shadow: 1px 2px 9px -1px var(--shadow_color);
}
.embed_value_explorer_content:focus {
outline: none;
box-shadow: 1px 2px 11px 1px var(--shadow_color);
}
.embed_value_explorer_content > .value_explorer_node {
margin-left: 0 !important;
}
.embed_value_explorer_control {
display: block;
margin-bottom: 1em;
font-size: 0.9em;
}
.value_explorer_node {
margin-left: 1em;
}
.value_explorer_header {
display: inline-block;
padding-right: 1em;
cursor: pointer;
}
.value_explorer_header .expand_icon {
padding: 5px;
}
.value_explorer_header.active {
background-color: rgb(148, 227, 191);
}
.value_explorer_key {
color: rgb(150, 0, 128);
font-weight: bold;
}
/* status */
.statusbar {
font-family: system-ui;
margin-bottom: 0px;
grid-area: statusbar;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.statusbar .spinner {
margin-right: 0.5em;
}
.status, .current_file {
font-size: 1.5em;
}
.status {
color: red;
}
.statusbar_action {
margin-right: 2em;
}
.statusbar_action.first {
margin-left: auto;
}
.open_app_window_button {
position: relative;
}
.open_app_window_tooltip {
padding: 1em;
position: absolute;
margin-bottom: 20px;
bottom: 100%;
border: none;
font-size: 1.7em;
background-color: rgb(120 206 247);
border-radius: 21px;
transform: scale(0);
transition: transform 0.3s;
}
.open_app_window_tooltip.on {
transform: scale(1);
}
.open_app_window_tooltip:after {
content: '';
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-top: 20px solid rgb(120 206 247);
position: absolute;
bottom: -20px;
left: 50%;
transform: translate(-50%);
}
.options {
padding: 5px;
}
.options > * {
margin: 5px;
}
.show_help, .github {
margin: 0em 0.5em;
}
.statusbar_button, .upload_button {
border: none;
color: white;
background: rgb(23 166 236);
}
.statusbar_button {
font-size: 1.2em;
margin-left: 1em;
&:last-of-type {
margin: 0em 0.5em;
}
}
.statusbar_button[disabled] {
background: grey;
}
.share_dialog input, .share_dialog button {
font-size: 1.2em;
}
.share_dialog button {
padding: 5px;
height: 2em;
}
dialog {
border: none;
}
dialog::backdrop {
background-color: rgb(225 244 253 / 80%);
}
.help_dialog[open] {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0;
min-width: 70%;
min-height: 70%;
background-color: white;
}
.help {
padding: 2em;
border-spacing: 5px;
}
.help th {
padding: 0.5em;
}
.help th.key {
width: 5em;
}
.help td.key {
background-color: rgb(225, 244, 253, 0.5);
border-radius: 10px;
text-align: center;
}
.help_dialog form {
margin-bottom: 1em;
}
.panel:not([open]) {
display: none;
}
.panel[open] {
padding: 0px;
margin: 0px 0px 0px auto;
height: 100%;
max-height: 100%;
animation: slide-in 0.2s ease-in forwards;
&::backdrop {
background-color: rgb(225 244 253 / 60%);
}
}
@keyframes slide-in{
0% {
transform: translateX(100%);
}
100% {
transform: translateX(0);
}
}