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
// Canvas setup
const canvas = document.createElement('canvas')
const canvas = document.createElement("canvas")
canvas.width = 700
canvas.height = 700
document.body.appendChild(canvas)
const ctx = canvas.getContext('2d')
const ctx = canvas.getContext("2d")
ctx.translate(canvas.width / 2, canvas.height)
// Draw a tree
await fractalTreeBasic({totalIterations: 10, basicLength: 10, rotate: 25})
await fractalTreeBasic({ totalIterations: 10, basicLength: 10, rotate: 25 })
function sleep() {
return new Promise(resolve => setTimeout(resolve, 3))
}
async function fractalTreeBasic({totalIterations, basicLength, rotate}) {
async function fractalTreeBasic({ totalIterations, basicLength, rotate }) {
// Draw the tree trunk
const trunkLength = basicLength * 2 * Math.pow(1.2, totalIterations + 1)
const width = Math.pow(totalIterations, 0.6)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(0, - trunkLength)
ctx.lineTo(0, -trunkLength)
ctx.lineWidth = width
ctx.strokeStyle = 'black'
ctx.strokeStyle = "black"
ctx.stroke()
await drawBranch(90, [0, - trunkLength], totalIterations + 1)
await drawBranch(90, [0, -trunkLength], totalIterations + 1)
async function drawBranch(angle, startPoint, 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 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 x1 = startPoint[0]
@@ -47,7 +46,7 @@ async function fractalTreeBasic({totalIterations, basicLength, rotate}) {
const y2 = y1 - len * Math.sin((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.moveTo(x1, y1)

View File

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

View File

@@ -1,34 +1,40 @@
window.addEventListener('load', () => {
const text = document.createElement('input')
window.addEventListener("load", () => {
const text = document.createElement("input")
const checkbox = document.createElement('input')
checkbox.setAttribute('type', 'checkbox')
const checkbox = document.createElement("input")
checkbox.setAttribute("type", "checkbox")
const radio = document.createElement('input')
radio.setAttribute('type', 'radio')
const radio = document.createElement("input")
radio.setAttribute("type", "radio")
const range = document.createElement('input')
range.setAttribute('type', 'range')
const range = document.createElement("input")
range.setAttribute("type", "range")
const select = document.createElement('select')
Array.from({length: 5}, (_, i) => i).forEach(i => {
const option = document.createElement('option')
option.setAttribute('value', i)
const select = document.createElement("select")
Array.from({ length: 5 }, (_, i) => i).forEach(i => {
const option = document.createElement("option")
option.setAttribute("value", i)
option.innerText = i
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]) => {
document.body.appendChild(el);
['click', 'input', 'change'].forEach(type => {
document.body.appendChild(el)
;["click", "input", "change"].forEach(type => {
el.addEventListener(type, e => {
const row = document.createElement('div')
const row = document.createElement("div")
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 latest = await provider.getBlock('latest')
const latest = await provider.getBlock("latest")
/*
Find ethereum block by timestamp using binary search
*/
async function getBlockNumberByTimestamp(timestamp, low = 0, high = latest.number) {
if(low + 1 == high) {
async function getBlockNumberByTimestamp(
timestamp,
low = 0,
high = latest.number,
) {
if (low + 1 == high) {
return low
} else {
const mid = Math.floor((low + high) / 2)
const midBlock = await provider.getBlock(mid)
if(midBlock.timestamp > timestamp) {
if (midBlock.timestamp > timestamp) {
return getBlockNumberByTimestamp(timestamp, low, mid)
} else {
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 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 latest = await p.getBlock()
const txs = await Promise.all(latest.transactions.map(t =>
p.getTransactionReceipt(t)
))
const txs = await Promise.all(
latest.transactions.map(t => p.getTransactionReceipt(t)),
)
const totalGas = txs
.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
function fib(n) {
if(n == 0 || n == 1) {
if (n == 0 || n == 1) {
return n
} else {
return fib(n - 1) + fib(n - 2)

View File

@@ -2,30 +2,29 @@
// Author: Sarah Bricault
// Canvas setup
const canvas = document.createElement('canvas')
const canvas = document.createElement("canvas")
canvas.width = 700
canvas.height = 700
document.body.appendChild(canvas)
const ctx = canvas.getContext('2d')
const ctx = canvas.getContext("2d")
ctx.translate(canvas.width / 2, canvas.height)
// Draw a tree
fractalTreeBasic({totalIterations: 10, basicLength: 10, rotate: 25})
function fractalTreeBasic({totalIterations, basicLength, rotate}) {
fractalTreeBasic({ totalIterations: 10, basicLength: 10, rotate: 25 })
function fractalTreeBasic({ totalIterations, basicLength, rotate }) {
// Draw the tree trunk
const trunkLength = basicLength * 2 * Math.pow(1.2, totalIterations + 1)
const width = Math.pow(totalIterations, 0.6)
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(0, - trunkLength)
ctx.lineTo(0, -trunkLength)
ctx.lineWidth = width
ctx.strokeStyle = 'black'
ctx.strokeStyle = "black"
ctx.stroke()
drawBranch(90, [0, - trunkLength], totalIterations + 1)
drawBranch(90, [0, -trunkLength], totalIterations + 1)
function drawBranch(angle, startPoint, 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 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 x1 = startPoint[0]
@@ -43,7 +42,7 @@ function fractalTreeBasic({totalIterations, basicLength, rotate}) {
const y2 = y1 - len * Math.sin((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.moveTo(x1, y1)

View File

@@ -1,7 +1,8 @@
import _ from 'https://unpkg.com/lodash-es'
import _ from "lodash-es"
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 repos = await resp.json()
return _(repos.items)
@@ -9,7 +10,7 @@ async function getPopularLanguages() {
.filter(l => l != null)
.countBy()
.toPairs()
.orderBy(([lang, useCount]) => useCount, 'desc')
.orderBy(([lang, useCount]) => useCount, "desc")
.map(([lang]) => lang)
.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 repos = await resp.json()
const langs = _(repos.items)
@@ -8,43 +8,18 @@ const langs = _(repos.items)
.filter(l => l != null)
.countBy()
.toPairs()
.map(([language, count]) => ({language, count}))
.map(([language, count]) => ({ language, count }))
.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
*/
barY(langs, {x: "language", y: "count", sort: {x: "y", reverse: true}, fill: 'purple'})
.plot()
barY(langs, {
x: "language",
y: "count",
sort: { x: "y", reverse: true },
fill: "purple",
}).plot()

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
Example of a TODO app built using the Preact library
*/
import React from 'https://esm.sh/preact/compat'
import React from "preact/compat"
// Core
@@ -13,7 +13,7 @@ let state
if (globalThis.leporello) {
// Retrieve initial state from Leporello storage
// 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
and can be used as an event handler.
*/
const handler = fn => (...args) => {
state = fn(state, ...args)
if (globalThis.leporello) {
// Persist state to Leporello storage to restore it after page reloads
globalThis.leporello.storage.set('state', state)
const handler =
fn =>
(...args) => {
state = fn(state, ...args)
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
const connect = comp => props => comp(props, state)
@@ -39,12 +41,12 @@ const render = () => React.render(<App />, document.body)
if (state == null) {
state = {
todos: [],
text: '',
filter: 'ALL',
text: "",
filter: "ALL",
}
}
window.addEventListener('load', render)
window.addEventListener("load", render)
// Components
@@ -71,7 +73,7 @@ const FilterLink = connect(({ filter, children }, state) => {
<button
onClick={handler(changeFilter.bind(null, filter))}
disabled={disabled}
style={{ marginLeft: '4px' }}
style={{ marginLeft: "4px" }}
>
{children}
</button>
@@ -89,7 +91,7 @@ const TodoList = connect((_, state) => (
const Todo = ({ todo }) => (
<li
onClick={handler(toggleTodo.bind(null, todo))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
style={{ textDecoration: todo.completed ? "line-through" : "none" }}
>
{todo.text}
</li>
@@ -99,11 +101,7 @@ const AddTodo = connect((_, state) => {
return (
<div>
<form onSubmit={handler(createTodo)}>
<input
value={state.text}
onChange={handler(changeText)}
autoFocus
/>
<input value={state.text} onChange={handler(changeText)} autoFocus />
<button type="submit">Add Todo</button>
</form>
</div>
@@ -114,14 +112,14 @@ const AddTodo = connect((_, state) => {
// Returns a filtered list of TODOs based on the current filter state
function visibleTodos(state) {
if (state.filter == 'ALL') {
if (state.filter == "ALL") {
return state.todos
} else if (state.filter == 'ACTIVE') {
} else if (state.filter == "ACTIVE") {
return state.todos.filter(t => !t.completed)
} else if (state.filter == 'COMPLETED') {
} else if (state.filter == "COMPLETED") {
return state.todos.filter(t => t.completed)
} else {
throw new Error('Unknown filter')
throw new Error("Unknown filter")
}
}
@@ -148,7 +146,7 @@ function createTodo(state, e) {
return {
...state,
todos: [...state.todos, { text: state.text }],
text: '',
text: "",
}
}
@@ -157,7 +155,7 @@ function toggleTodo(todo, state) {
return {
...state,
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>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Redux Todos Example</title>
<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/redux'></script>
<script src='https://unpkg.com/react-redux'></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/redux"></script>
<script src="https://unpkg.com/react-redux"></script>
<script type='module'>
if(new URLSearchParams(window.location.search).get('leporello') == null) {
await import('../src/index.js');
<script type="module">
if (
new URLSearchParams(window.location.search).get("leporello") == null
) {
await import("../src/index.js")
}
</script>
</head>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,18 @@
import Todo from './Todo.js'
import Todo from "./Todo.js"
const h = React.createElement
const TodoList = ({ todos, toggleTodo }) => (
h('ul', null,
const TodoList = ({ todos, toggleTodo }) =>
h(
"ul",
null,
todos.map(todo =>
h(Todo, {
key: todo.id,
...todo,
onClick: () => toggleTodo(todo.id),
})
)
}),
),
)
)
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 AddTodo = ({ dispatch }) => {
const inputref = {}
return (
h('div', null,
h('form', {
return h(
"div",
null,
h(
"form",
{
onSubmit: e => {
e.preventDefault()
if (inputref.input.value.trim()) {
dispatch(addTodo(inputref.input.value))
Object.assign(inputref.input, {value: ''})
Object.assign(inputref.input, { value: "" })
}
}
},
},
h('input', {ref: input => Object.assign(inputref, {input})}),
h('button', {type: 'submit'}, 'Add Todo')
)
)
h("input", { ref: input => Object.assign(inputref, { input }) }),
h("button", { type: "submit" }, "Add Todo"),
),
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,8 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Leporello.js</title>
<script type='module' src='./src/ts_libs.js' async></script>
<script src='ace/ace.js'></script>
<script src='ace/keybinding-vim.js'></script>
<script src='ace/ext-language_tools.js'></script>
@@ -20,526 +22,7 @@
<script type='module' src="./src/launch.js"></script>
<style>
: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>
<link rel=stylesheet href='./styles.css'></link>
</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);
}
}