Compare commits

...

No commits in common. "main" and "page" have entirely different histories.
main ... page

15 changed files with 678 additions and 1327 deletions

View file

@ -1,2 +0,0 @@
[*]
indent_style = tab

View file

@ -1,247 +0,0 @@
# Rendering DOM nodes using `render.js`
```js
import {html} from "nyooom/render.js"
```
A functional-friendly helper library for procedural DOM generation and templating, with support for reactive state objects.
## Summary
```js
html.button(
"Click Me!",
{
class: "primary",
click({target}) {
console.log("User clicked", target)
}
},
button => { console.log("Created", button) }
)
```
* elements as factory functions `content -> element`
* content as arguments or in arrays
* attributes in object arguments
* style, dataset, shadow root, etc. as magic attributes
* events as function attributes
* initialisers as `any -> void` functions
## Interface / Examples
### Basic DOM generation
Accessing the `html` proxy with any string key returns a new node generator function.
When called this function will generate a DOM node (HTML Tag).
The name of the function becomes the tag name of the node.
```js
html.div()
```
Content and attributes can be set via the function arguments:
Strings and DOM nodes are inserted as children, while other objects (except for some special cases) have their key-value pairs turned into attribute-value pairs on the
```js
html.div("Big Text", {style: "font-size: 1.4em"})
```
Arrays are iterated and their values treated as arguments.
This works both for inserting children and setting attributes.
```js
const content = [" ps: hi", {class: "title"}]
html.h1({id: "main-heading"}, "Heading", content)
```
Function arguments are treated differently depending on their length:]
Functions with **no** named parameters are called, and their return value is then evaluated just like an argument to the generator.
All other functions are (immediately) called with the newly created node as their first and only argument.
These can be used to initialise the new node in a point-free style.
```js
const hello = () => html.bold("Hello, World!")
const init = node => console.log("Initialising", node)
html.div(hello, init)
```
Nested tags can be generated with nested function calls.
When properly formatted, this means simpler templates will have the same structure as if written in HTML (sans those pesky closing tags).
```js
html.div(
html.p(
html.b("Bold Text")
)
)
```
### Attribute Processing
For convenience, arrays assigned as attributes will be joined with spaces:
```js
html.a({class: ["button", "important"]})
```
Assigning a function as an attribute will instead attach it as an event listener:
```js
html.button("Click me!", {click: event => {
alert("You clicked the button.")
}})
```
The special `style` property can be set to an object and its key/value pairs will be inserted as CSS properties on the element's `style` object.
```js
const style = { color: "salmon" }
html.span("Salmon", { style })
```
The special property `shadowRoot` will attach a shadow-DOM to the element if none is present and append its content to the shadow root.
Arrays are iterated over and their elements appended individually.
```js
html.div({
shadowRoot = ["Hello, ", html.b("World"), "!"]
})
```
The `dataset` property will add its key/value pairs to the new node's `dataset`,
as a more convenient alternative to setting individual `data-` attributes.
```js
const dataset = { foo: "bar" }
const div = html.div({dataset})
console.log(dataset.foo === div.dataset.foo)
console.log(div.getAttribute("data-foo") === "bar")
```
### Reactivity
Nyooom supports reactivity through a simple protocol:
Observable objects identify themselves with the `observable` attribute,
which must return a truthy value.
Observables are expected to expose a `value` attribute that is both readable and writeable,
and to emit a "change" event whenever its vale has changed.
Observables can be passed to nyooom's node functions as both
attribute values (values in an object) or
child elements (direct arguments or in an array).
#### Reactive Children
Passing an observable as a child element will attempt to insert its current
value into the new node as if it was passed in directly, but will also hook into
the observable to replace the value when the state changes.
```js
const state = new Observable.value(0)
const button = html.button(state, {
click(event) { state.value++ }
})
```
Note that to keep the replacement logic simple, it is not currently possible to
insert use document fragments, as these could insert several top-level children
into a component that would then all have to be replaced. When an observable
contains or changes to a document fragment, nyooom will raise an error.
Before replacing an element, a `"replace"` event is emitted from the old
element. This event bubbles and is cancelable, and can thus be used both to
completely prevent the replacement according to custom logic, to alter or
initialise the new element before it is inserted, or even to modify the old
object instead of replacing it.
#### Reactive Attributes
Passing an observable as an object value will, likewise, treat its value as the
attribute value, and update it whenever the state's value changes.
```js
const state = new Observable.value(0)
const input_1 = html.input({ type: "number", value: state })
const input_2 = html.input({ type: "number", value: state })
```
## Helpers
### Empty Values
Nyooom will produce a warning when it encounters `undefined` as an argument to a
node factory, and skip it.
When completely ommitting the argument would make code more complicated, the
`nothing` export can be used instead, and will be completely ignored by nyooom:
```js
import {html, nothing} from "nyooom/render"
const value = undefined
html.div(value ? html.p(value) : nothing) // <div></div>
```
### Event Handlers
Event handlers often need to call `preventDefault()` and `stopPropagation()` on
the event. To save a bit of typing, the wrapper functions `noDefault` and
`noPropagation` take a function `event => void` and wrap it in a function that
first calls the corresponding method on the event:
```js
html.form(
html.button("Click me!", click: noDefault(() => console.log("Clicked!")))
)
```
### DOM-Fragments
The `fragment` helper function collects a list of DOM nodes from its arguments
into a new document fragment, so they can more easily be processed as a group
and inserted elsewhere.
```js
import {fragment} from "nyooom/render"
const f = fragment(html.p("paragraph 1"), html.p("paragraph 1"))
document.body.append(f.cloneNode(true))
```
### Text
The `text` helper function operates in two modes: When a string is provided, it
outputs a `TextNode`. This acts the same as `document.createTextNode`, except it
won't error for `undefined` and instead simply return an empty text node.
When used as a template literal, it will return a document template containing a
list of text nodes. HTML-Elements can be interpolated into these fragments and
will be inserted into the template.
```js
import {text} from "nyooom/render"
const t = text`Hello, ${html.b("World")}!`
```
## Extending
The Nyooom renderer is built in a modular way that allows for easy extending:
The most generic functionality like the `proxy()` method is implemented in the
`Renderer` class.
This is extended by the `DomRenderer` class which defines how
elements are created inside the browser or a compatible DOM implementation.
The DomHtmlRenderer defines a `camelCase` to `snake-case` conversion for node
names and a list of magic node names to be handled differently.
The `DomSvgRenderer` creates DOM nodes in the SVG namespace.

View file

@ -1,74 +0,0 @@
export const domArray = (methods, extra) => {
if (extra) return domArray(extra)(methods)
const traps = {
get(target, prop) {
if (prop === "length") {
return target.children.length
} else if (prop === Symbol.iterator) {
return function*() {
for (const child of target.children) {
yield methods.get.call(child)
}
}
} else if (prop.match?.call(prop, /^[0-9]+$/)) {
const child = target.children[prop]
if (child) return methods.get.call(child)
return child
} else {
return Array.prototype[prop]
}
},
set(target, prop, value) {
if (prop.match?.call(prop, /^[0-9]+$/)) {
const child = target.children[prop]
if (child) {
methods.set.call(child, value)
return true
} else {
for (let i = target.children.length; i < Number(prop); i++) {
target.appendChild(methods.new(undefined))
}
const element = methods.new(value)
target.appendChild(element)
if (methods.get.call(element) !== value)
methods.set.call(element, value)
return true
}
} else if (prop == "length") {
if (value == target.children.length)
return true
else
return false
}
},
deleteProperty(target, prop) {
if (prop.match?.call(prop, /^[0-9]+$/)) {
const child = target.children[prop]
if (child) child.remove()
return true
}
},
has(target, prop) {
return (prop === Symbol.iterator) || (prop in target.children) || (prop in Array.prototype)
}
}
return element => {
if (!(element instanceof Element)) throw(new Error("Creating domArray on non-element"))
return new Proxy(element, traps)
}
}
export const meta = (element=document.head) => new Proxy(element, {
get: (target, name) => target.querySelector(`meta[name="${name}"]`)?.content,
set: (target, name, value) => {
let meta = target.querySelector(`meta[name="${name}"]`)
if (!meta) {
meta = document.createElement("meta")
meta.name = name
target.append(meta)
}
meta.content = value
}
})

View file

@ -1,8 +0,0 @@
<script type="importmap">
{
"imports": {
"nyooom/render": "./render.js",
"nyooom/observable": "./observable.js"
}
}
</script>

501
index.html Normal file
View file

@ -0,0 +1,501 @@
<html theme=dark>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<script type="importmap">
{
"imports": {
"nyooom/render": "https://cdn.jsdelivr.net/gh/darkwiiplayer/nyooom@d457c38/render.js",
"nyooom/observable": "https://cdn.jsdelivr.net/gh/darkwiiplayer/nyooom@d457c38/observable.js",
"nyooom/showcase": "./js/nyooom-showcase.js"
}
}
</script>
<script type="module" src="https://cdn.jsdelivr.net/gh/darkwiiplayer/components@master/TypeWriter.js"></script>
<style>
@import
/* url('https://cdn.jsdelivr.net/gh/darkwiiplayer/css@main/all.css') */
url('https://darkwiiplayer.github.io/css/all.css') layer(framework);
/* url('https://cdn.jsdelivr.net/gh/darkwiiplayer/css@main/schemes/talia.css') */
@import url('styles.css') layer(site);
.jsdelivr-badge {
filter: saturate(.4) hue-rotate(250deg);
border-radius: .2em;
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
<script>
const scrollHook = () => {
document.body.setAttribute("scroll", window.scrollY)
}
window.addEventListener("scroll", scrollHook)
document.addEventListener("readystatechange", scrollHook)
const observer = new IntersectionObserver(events => {}, {
rootMargin: '-1px 0px 0px 0px',
threshold: [1]
})
</script>
<script type="module">
import hljs from 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/highlight.min.js';
import lang_html from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/xml.min.js"
hljs.registerLanguage("html", lang_html)
document.querySelectorAll('code[lang="js"]').forEach(code => {
code.innerHTML = hljs.highlight("javascript", code.innerText).value
})
</script>
<script type="module">
import "nyooom/showcase"
</script>
<a name=top></a>
<header role="navigation" class="fixed">
<nav class="bar">
<ul>
<li><a href="#top">Top</a></li>
<li><a href="https://git.but.gay/darkwiiplayer/nyooom">Git</a></li>
<li><a href="https://github.com/darkwiiplayer/nyooom">Github</a></li>
<li><a href="https://www.npmjs.com/package/nyooom">npm</a></li>
</ul>
</nav>
<img alt="" class="jsdelivr-badge" src="https://data.jsdelivr.com/v1/package/npm/nyooom/badge">
</header>
<page-hero role="banner" cover=60>
<!-- <img src="https://picsum.photos/1920/1080"> -->
<hgroup>
<h1 style="font-size: 6vw">Nyooom</span></h1>
<code lang="js">import { html } from "nyooom/render"</code>
<p>
A new way of building
<type-writer loop>
<span>HTML</span>
<template>SVG</template>
<template>templates</template>
<template>components</template>
<template>applications</template>
</type-writer>
in vanilla JavaScript
</p>
</hgroup>
<flex-row gap=1>
<!--<a class="button" href="#elevator-pitch">Elevator Pitch</a>-->
<a class="button" href="#getting-started">Get Started</a>
</flex-row>
</page-hero>
<main style="margin-top: 0">
<section id="elevator-pitch" class="box">
<div class="content-width" style="padding: 2em 0">
<h2>Elevator Pitch</h2>
<p class="big">
Nyooom combines <em>easy</em> and <em>powerful</em> HTML generation with optional <em>reactivity</em>, all in plain JavaScript.
</p>
<nyooom-showcase preview=false>
<code><div contenteditable="false">return html.p(
"Text Node",
html.br(),
html.b("HTML Node"),
{style:{color:"salmon"}},
)</div></code>
</nyooom-showcase>
<vertical-spacer></vertical-spacer>
<p>
Nyooom is <em>small</em> enough that you can use it even if you only need it in a single function,
<em>obvious</em> enough to figure out what's going on even if you've never used it,
<em>powerful</em> enough that you won't <i>need</i> another framework and
<em>flexible</em> enough to let you use one regardless.
</p>
<p>
Sounds good? Continue reading to find out how to try it yourself!
</p>
</div>
</section>
<section id="getting-started" class="content-width">
<h2>Getting Started</h2>
<section>
<p>
Trying out nyooom is super easy:
</p>
<pre class="box"><code lang=js>import {html} from "https://cdn.jsdelivr.net/npm/nyooom/nyooom.js"</code></pre>
</section>
<section>
<p>
Calling any method on the <code>html</code> export will generate an HTML node.<br>
Passing in arguments is how you add children, set attributes and even do more advanced things.
</p>
<nyooom-showcase preview="false">
<code>
<div contenteditable="false">return html.div("content")</div>
</code>
</nyooom-showcase>
<p>
Using the <code>component</code> helper from the <code>nyooom/observable</code> module
makes it easier to see results on your page:
Just pass it a function that returns some DOM nodes, and it'll register a custom element for you.
</p>
<pre class="box"><code lang=js>import {component} from "https://cdn.jsdelivr.net/npm/nyooom/observable.js"
const myComponent = () =&gt; html.div("Rendered Component")
component(myComponent) //Registers it as &lt;my-component&gt;</code></pre>
<p>
Of course, you can also just call <code lang=js>document.body.append(html.span("My Text"))</code> in your script.
</p>
</section>
</section>
<section class="content-width">
<h2>Basic DOM generation</h2>
<section>
<p>
Content can be added to a node by simply passing it as arguments to the function.
This works for strings, HTML elements, functions and even observable state containers.
</p>
<nyooom-showcase preview="false">
<code>
<div contenteditable="false">return html.span(
"Nyooom ",
html.u(
"is ",
html.b("cool")
)
)</div>
</code>
</nyooom-showcase>
</section>
<section>
<p>
Adding attributes is just as easy: Just pass an object into the function.
If the value is an array, it'll get joined with spaces.
</p>
<nyooom-showcase preview="false">
<code>
<div contenteditable="false">return html.span({
id: "warning",
class: ["danger", "bold"],
})</div>
</code>
</nyooom-showcase>
</section>
<section>
<p>
Event listeners work just like attribute, except the value is a function:
</p>
<nyooom-showcase code="false">
<code>
<div contenteditable="false">return html.button("Click Me!", {
click: event => {
alert("Button clicked :3")
}
})</div>
</code>
</nyooom-showcase>
<section>
<p class="big">
Some "attributes" are handled in special ways to make things easier:
</p>
<p>
<code>style</code> sets the inline styles of the node.
</p>
<p>
<code>camelCase</code> names will be converted to <code>kebab-case</code> and values
will be converted to strings.
</p>
<nyooom-showcase>
<code>
<div contenteditable="false">return html.div(
{ style: {
color: "salmon"
}},
html.span("Big Salmon",
{ style: {
fontSize: "1.4em"
}}
)
)</div>
</code>
</nyooom-showcase>
<p>
<code>dataset</code> converts its key-value pairs into <code>data-</code> attributes.
</p>
<nyooom-showcase preview=false>
<code>
<div contenteditable="false">return html.span({
dataset: {
from: "Fire",
to: "Heat",
snakeCase: "converted"
}
}, "Heat from Fire")</div>
</code>
</nyooom-showcase>
<p>
<code>shadowRoot</code> will attach a shadow DOM to the newly created node.
</p>
<nyooom-showcase code=false>
<code>
<div contenteditable="false">return html.div({
shadowRoot: [
html.p("Shadow DOM text"),
"Arrays work too :)"
]
}, "Light DOM text")</div>
</code>
</nyooom-showcase>
</section>
</section>
</section>
<section class="content-width">
<h2>It's just JavaScript</h2>
<p>Nyooom is entirely implemented as a light-weight JavaScript library.</p>
<p>
This means that there is no new syntax for things you already know how to do.
Functions like <code>filter</code> or <code>map</code> can be applied directly to your data,
nodes can be assigned to variables right as you create them,
common structures can be extracted into function and even passed around as arguments,
and the syntax is just the boring old javascript that most tools already understand.
</p>
<nyooom-showcase code="false">
<code>
<div contenteditable="false">return html.ul(
["u", "b", "i"].map(
type =&gt; html.li(html[type](
`&lt;${type}&gt; element`
))
)
)</div>
</code>
</nyooom-showcase>
</section>
<section class="content-width">
<h2>State changes in Nyooom</h2>
<pre class="box"><code lang=js>import {ObservableObject} from 'nyooom/observable'</code></pre>
<p>
Nyooom offers a series of state management classes it calls "observables".
Their purpose is to store one or more values and emit events when they are changed.
</p>
<p>
The <code>render.js</code> library handles observables by setting DOM nodes up to
get updated when the observable emits a change event. Neihter module depends on the other
and they use a common protocol to work together.
</p>
<nyooom-showcase code="false">
<code>
<div contenteditable="false">const counter = new State(0)
return html.buttonGroup(
html.button(
"Click Me! ",
html.span(
counter.compose(val =&gt; val ? val : "")
),
{
click() {
counter.value += 1
}
}
),
html.button(
"Clear", {
click() {
counter.value = 0
}
}
)
)</div>
</code>
</nyooom-showcase>
</section>
<section class="content-width">
<h2>Obligatory Todo list</h2>
<p>
This simple ToDo component uses nothing more than Nyooom and
the stylesheets already on this page to save some boilerplate.
</p>
<nyooom-showcase code="false">
<code>
<div contenteditable="false">const task = value =>
html.flexRow(
{ class: "todo", gap: 1 },
value,
html.span("[x]", {
style: { cursor: "pointer" },
click(event) {
event
.target
.closest(".todo")
.remove()
}
})
)
let todo, input
return todo = html.flexColumn(
{gap: 1},
input = html.input({
type: "text",
placeholder:
"Do some stuff",
}),
html.button("Add Task",
{
click() {
todo.append(task(
input.value ||
input.placeholder
))
}
})
)</div>
</code>
</nyooom-showcase>
</section>
<section id="nyooom-explained" class="content-width">
<h2>Nyooom Explained</h2>
<h3>A bit of boring history</h3>
<p>
The <code>nyooom/render.js</code> module traces its basic idea back to a <a href="https://github.com/darkwiiplayer/skooma">Lua module</a>.
The basic mechanism is the exact same, as the languages are very similar;
with the noteable difference that the Lua version outputs text and uses a very
lightweight DOM-like data-structure as an intermediary representation.
</p>
<p>
Porting the library code to JavaScript was an obvious choice, given how similar both
languages are in how they handle functions and metaprogramming.
The concept worked well to generate HTML to send to the client,
so it would work equally well to generate DOM nodes directly in the browser.
And the obvious advantages over a server-side implementation is that things like event listeners could be attached directly.
</p>
<p>
One design goal while further expanding the capabilities of nyooom has been to preserve this original use-case.
The library is <i>small enough</i> that pulling it into a project just to generate a few nested DOM structures
here and there isn't an unjustifiable waste of application size.
This is also why the <code>render</code> module does not depend on the <code>observable</code>
module, and has as little code as it can get away with to enable interoperability.
</p>
<p>
The later addition of the <code>Obervable</code> module, and the ability of nyooom to detect state objects and
handle them specially to enable reactivity is what truly enables the library to be used on its own to build
simple and even moderately complex interactive components.
<br>
And with most of this extra complexity existing in a separate file,
the size impact on the core nyooom library is very minimal, when
reactivity is not wanted, or an alternative mechanism for reactivity
is preferred.
</p>
<h3>Implementation</h3>
<p>
Nyooom is based on a convenient wrapper around <code>document.createElement</code>
made even more convenient by a <code>Proxy</code> to generate & populate DOM nodes.
</p>
<h3>Rationale & design principles</h3>
<p>
The design of nyooom is based on the following assumption about web applications:
</p>
<blockquote>
Most web application code can be modeled with simple abstractions.<br>
The few cases that can't tend to be too complex for any general-purpose abstraction anyway.
</blockquote>
<p>
Leading to a framework that focuses on the former group,
while giving developers the tools to handle the tricky bits without having to hack themselves out of the framework first.
</p>
<p>
One consideration that sets nyooom apart from other alternatives,
is the emphasis on being both easy to adopt, integrate, and even abandon.
</p>
<p>
While nyooom provides some level of abstraction over native browser APIs, everything done via nyooom is
still easy to mentally map onto the underlying APIs. This reduces the mental load of translating what the
code says, and what is happening to the website, creating a common mental model with vanilla javascript,
as well as other non-magic libraries and micro-frameworks.
</p>
<p>
This, in practice, means that adding some code that uses nyooom to an existing project, integrating it with
other technologies, and even removing it in parts or in whole from a project that has outgrown it, or from performance-critical
parts of an application that need even the last drop of performance squeezed out of the browser are all very
simple to achieve.
</p>
<p>
Even taking out certain aspects like reactivity, while still using nyooom to generate DOM nodes that get manipulated
manually in response to complex state-changes is as easy as replacing the state objects with primitive values
and writing the custom code to take their place.
</p>
<p>
Where other frameworks suck in their stomach and do their best to fit every use-case you could have,
nyooom is happy to just step aside and make space for more specialised tools.
<!-- nyooom: the framework that promotes body-acceptance -->
</p>
</section>
</main>
<footer class="inset box">
<flex-row gap=3>
<table>
<caption style="padding: 0">Feedback</caption>
<tr>
<th scope="col">Fediverse</th>
<td><a href="https://tech.lgbt/@darkwiiplayer">@darkwiiplayer@tech.lgbt</a></td></tr>
</table>
</flex-row>
</footer>
</htmlt>

108
js/nyooom-showcase.js Normal file
View file

@ -0,0 +1,108 @@
import {html,nothing} from "nyooom/render"
import {ObservableValue,ObservableObject} from "nyooom/observable"
import element from "https://darkwiiplayer.github.io/easier-elements-js/easier-elements.js"
import hljs from 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/highlight.min.js';
import lang_html from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/xml.min.js"
hljs.registerLanguage("html", lang_html)
const capture = event => event.stopPropagation()
const css = `
:host {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
gap: var(--padding, 1em);
padding: var(--padding, 1em);
position: relative;
}
@media (max-width: 60em) {
:host {
display: flex;
flex-flow: column;
}
}
.error {
font-family: "Courier New", sans-serif;
grid-column: span 2;
}
.error:empty { display: none; }
.error:not(:empty)~* { display: none; }
.hidden { display: none; }
html-preview {
contain: inline-size layout style paint;
}
.edit {
left: -1.4em;
z-index: 10;
position: absolute;
display: block;
content: '🖉';
line-height: 100%;
opacity: .2;
font-size: 2em;
cursor: pointer;
}
.edit.editing {
opacity: .6;
}
`
const theme = html.link({
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css"
})
element(class NyooomShowcase extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: "open"})
this.addEventListener("input", _ => {
this.render()
})
}
get editable() {
return this.querySelector('[contenteditable="true"]')
}
format() {
if (!this.editable) {
const code = this.querySelector('[contenteditable]')
code.innerHTML = hljs.highlight("javascript", code.innerText).value
}
}
connectedCallback() {
this.classList.add("box")
this.shadowRoot.replaceChildren(
html.slot(),
html.style(css),
...Array.from(document.styleSheets).map(sheet => sheet.ownerNode.cloneNode(true)),
theme.cloneNode(true),
this.error = html.div({class: ["error"]}),
this.output = html.code(),
this.preview = html.htmlPreview({input: capture}),
)
this.format()
this.render()
}
render() {
const code = this.querySelector("code").innerText
try {
const fn = new Function("html", "empty", "State", code)
const result = fn(html, nothing, ObservableValue)
this.error.replaceChildren()
this.output.innerHTML = hljs.highlight("html", result.outerHTML).value
this.preview.classList.toggle("hidden", this.getAttribute("preview") === "false")
this.output.classList.toggle("hidden", this.getAttribute("code") === "false")
this.preview.replaceChildren(result)
} catch (error) {
console.error(error.stack)
this.error.innerText = error.stack
}
}
})

View file

@ -1,6 +0,0 @@
{
"compilerOptions": {
"checkJs": true,
"target": "es2021"
}
}

View file

@ -1,22 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

23
mini.js
View file

@ -1,23 +0,0 @@
/**
* @param {String} name
* @return {String}
*/
const snakeToHTML = name => name.replace(/([A-Z])/g, "-$1").replace(/^-/, "").toLowerCase()
// @ts-ignore
export default new Proxy(/** @type {Object<string,(...args: any)=>HTMLElement>} */(document), {
/** @param {string} tag */
get: (_, tag) => /** @param {any[]} args */ (...args) => {
let node = document.createElement(snakeToHTML(tag))
for (const arg of args) {
if (arg instanceof HTMLElement) {
node.append(arg)
} else if (arg instanceof Object) {
for (let key in arg) {
node[key] = arg[key]
}
}
}
return node
}
})

View file

@ -1,398 +0,0 @@
/** @type FinalizationRegistry<AbortController> */
const abortRegistry = new FinalizationRegistry(controller => controller.abort())
/** @param {String} string */
const camelToKebab = string => string.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`)
/** @param {String} string */
const kebabToCamel = string => string.replace(/([a-z])-([a-z])/g, (_, a, b) => a+b.toUpperCase())
const identity = object=>object
const target = Symbol("Proxy Target")
/* Custom Event Classes */
/**
* @typedef {Object} Change
* @property {string} property
* @property {any} from
* @property {any} to
* @property {boolean} mutation - The change happened inside the value without a new assignment
*/
/** Event fired for every change before the internal state has been updated that can be canceled. */
export class ChangeEvent extends Event {
/** @param {Change} change */
constructor(change) {
super('change', {cancelable: true})
this.change = Object.freeze(change)
}
}
/** Event fired for one or more changed values after the internal state has been updated. */
export class ChangedEvent extends Event {
/** @type {any} */
#final
/** @type {any} */
#values
/** @param {Change[]} changes */
constructor(...changes) {
super('changed')
this.changes = changes
}
get values() {
if (!this.#values) {
const values = new Map()
for (const {property, from, to} of this.changes) {
let list = values.get(property)
if (!list) {
list = [from]
values.set(property, list)
}
list.push(to)
}
this.#values = values
}
return this.#values
}
get final() {
if (!this.#final) {
this.#final = new Map()
for (const [property, list] of this.values) {
if (list[0] !== list[list.length-1]) {
this.#final.set(property, list[list.length-1])
}
}
}
return this.#final
}
}
export class Observable extends EventTarget {
#synchronous
/** @type Change[]> */
#queue
#abortController = new AbortController
#ref = new WeakRef(this)
get ref() { return this.#ref }
constructor({synchronous=false}={}) {
super()
Object.defineProperty(this, "observable", {value: true, configurable: false, writable: false})
if (this.constructor === Observable) {
throw new TypeError("Cannot instantiate abstract class")
}
this.#synchronous = !!synchronous
abortRegistry.register(this, this.#abortController)
this.proxy = new Proxy(this.constructor.prototype.proxy, {
get: (target, prop) => target.call(this, prop)
})
}
/** @param {Change[]} changes */
emit(...changes) {
this.dispatchEvent(new ChangedEvent(...changes))
}
/**
* @param {string} prop
*/
proxy(prop, {get=undefined, set=undefined, ...options}={}) {
const proxy = new ProxiedObservableValue(this, prop, options)
if (get) proxy.get = get
if (set) proxy.set = set
return proxy
}
/**
* @param {string} prop
* @param {function(any):void} callback
*/
subscribe(prop, callback) {
const controller = new AbortController()
// @ts-ignore
this.addEventListener("change", ({final}) => {
if (final.has(prop)) return callback(final.get(prop))
}, {signal: controller.signal})
callback(this[prop])
return () => controller.abort()
}
/** Queues up a change event
* @param {string} property - Name of the changed property
* @param {any} from
* @param {any} to
* @param {boolean} mutation - whether a change was an assignment or a mutation (nested change)
*/
enqueue(property, from, to, mutation=false) {
const change = {property, from, to, mutation}
if (!this.dispatchEvent(new ChangeEvent(change))) return false
if (!this.synchronous) {
if (!this.#queue) {
this.#queue = []
queueMicrotask(() => {
this.emit(...this.#queue)
this.#queue = undefined
})
}
this.#queue.push(change)
} else {
this.emit(change)
}
return true
}
get signal() { return this.#abortController.signal }
get synchronous() { return this.#synchronous }
get changesQueued() { return Boolean(this.#queue) }
}
export class ObservableObject extends Observable {
/**
* @param {Object} target
* @param {Object} options
*/
constructor(target={}, {shallow=true, ...options}={}) {
super(options)
Object.defineProperty(this, "target", target)
this.values = new Proxy(target, {
/**
* @param {Object} target
* @param {String} prop
* @param {any} value
*/
set: (target, prop, value) => {
const old = target[prop]
if (old === value) {
return true
} else {
if (this.enqueue(prop, old, value)) {
if (!shallow) {
if (old instanceof Observable) this.disown(prop, old)
if (value instanceof Observable) this.adopt(prop, value)
}
target[prop] = value
return true
} else {
return false
}
}
},
/**
* @param {Object} target
* @param {String} prop
*/
get: (target, prop) => target[prop],
})
}
/**
* @param {string} prop
* @param {Object} options
*/
proxy(prop, {get=undefined, set=undefined, ...options}={}) {
const proxy = new ProxiedObservableValue(this, prop, {values: this.values, ...options})
if (get) proxy.get = get
if (set) proxy.set = set
return proxy
}
/** @type Map<Observable, Map<String, Function>> */
#nested = new Map()
/** Adopts an observable to be notified of its changes
* @param {string} prop
* @param {Observable} observable
*/
adopt(prop, observable) {
let handlers = this.#nested.get(observable)
if (!handlers) {
// Actual adoption
handlers = new Map()
this.#nested.set(observable, handlers)
}
const ref = this.ref
const handler = () => ref.deref()?.emit(prop, observable, observable, {observable: true})
handlers.set(prop, handler)
observable.addEventListener("changed", handler, {signal: this.signal})
}
/** Undoes the adoption of a nested observable, cancelling the associated event hook
* @param {string} prop
* @param {Observable} observable
*/
disown(prop, observable) {
const handlers = this.#nested.get(observable)
const handler = handlers.get(prop)
observable.removeEventListener("changed", handler)
handlers.delete(prop)
if (handlers.size == 0) {
this.#nested.delete(observable)
}
}
}
export class ObservableValue extends Observable {
#value
/**
* @param {any} value
* @param {Object} options
*/
constructor(value, options) {
super(options)
this.#value = value
}
get value() { return this.#value }
set value(value) {
if (this.enqueue("value", this.#value, value)) {
this.#value = value
}
}
/**
* @param {(value: any) => any} func
*/
transform(func) {
return new Composition(func, {}, this)
}
proxy(methods) {
}
}
class ProxiedObservableValue extends ObservableValue {
#values
#prop
/**
* @param {Observable} backend
* @param {string} prop
*/
constructor(backend, prop, {values=backend, ...options}={}) {
super(options)
this.#values = values
this.#prop = prop
const ref = this.ref
backend.addEventListener("change", event => {
const {property, from, to, ...rest} = event.change
if (property == this.#prop) {
ref.deref()?.enqueue({
property,
from: this.get(from),
to: this.get(to),
...rest
})
}
}, { signal: this.signal })
}
get = identity
set = identity
get value() { return this.get(this.#values[this.#prop]) }
set value(value) { this.#values[this.#prop] = this.set(value) }
}
const attributeObserver = new MutationObserver(mutations => {
for (const {type, target, attributeName: name} of mutations) {
if (type == "attributes" && target instanceof HTMLElement) {
const next = target.getAttribute(name)
const camelName = kebabToCamel(name)
if (String(target.state.values[camelName]) !== next)
target.state.values[camelName] = next
}
}
})
export const component = (name, generator, methods) => {
if (typeof name === "function") {
methods = generator
generator = name
name = camelToKebab(generator.name)
}
const jsName = kebabToCamel(name)
component[jsName] = class extends HTMLElement{
/** @type {ObservableObject} */
state
constructor() {
super()
const target = Object.fromEntries([...this.attributes].map(attribute => [kebabToCamel(attribute.name), attribute.value]))
this.state = new ObservableObject(target)
this.state.addEventListener("changed", event => {
if (event instanceof ChangedEvent)
for (const {property, to: value} of event.changes) {
const kebabName = camelToKebab(property)
if (this.getAttribute(kebabName) !== String(value))
this.setAttribute(kebabName, value)
}
})
attributeObserver.observe(this, {attributes: true})
const content = generator.call(this, this.state)
if (content) this.replaceChildren(content)
}
}
const element = component[jsName]
if (methods) {
Object.defineProperties(element.prototype, Object.getOwnPropertyDescriptors(methods))
}
customElements.define(name, element)
return element;
}
class Composition extends ObservableValue {
#func
#states
/**
* @param {(...values: any[]) => any} func
* @param {Object} options
* @param {Observable[]} states
*/
constructor(func, options, ...obesrvables) {
super(options)
this.#func = func
this.#states = obesrvables
const abortController = new AbortController()
abortRegistry.register(this, abortController)
const ref = new WeakRef(this)
obesrvables.forEach(state => {
state.addEventListener("changed", () => {
ref.deref()?.update()
}, {signal: abortController.signal})
})
this.update()
}
update() {
const value = this.#func(...this.#states.map(state => state.value))
this.value = value
}
}
/**
* @param {Function} func
*/
export const compose = func =>
/**
* @param {Observable[]} observables
*/
(...observables) =>
new Composition(func, {}, ...observables)

View file

@ -1,16 +0,0 @@
{
"name": "nyooom",
"version": "0.0.2",
"author": "darkwiiplayer",
"license": "Unlicense",
"exports": {
"./render": "./render.js"
},
"type": "module",
"repository": {
"type": "git",
"url": "https://git.but.gay/darkwiiplayer/nyooom"
},
"homepage": "https://darkwiiplayer.github.io/nyooom",
"keywords": []
}

View file

@ -1,84 +0,0 @@
# Nyooom
```js
import {html} from "nyooom/render"
import {ObservableValue} from "nyooom/observable"
const text = new ObservableValue("Nyooom is cool")
setTimeout(() => {text.value = "Nyooom is awesome!"}, 1e5)
document.body.append(html.div(
html.h1("Hello, World!"),
html.p(text, {class: "amazing"}),
html.button("Show Proof", {click: event => { alert("It's true!") }})
))
```
## Goals
> Arrakis teaches the attitude of the knife - chopping off what's incomplete and
> saying: "Now, it's complete because it's ended here."
>
> — Frank Herbert, Dune
Nyooom aims to offer as much convenienve as possible within the following
constraints:
1. Small, independent modules that can also work on their own
1. Code should be easy to figure out by someone who doesn't actiely use nyooom
1. Easy to gradually introduce and remove rather than forcing big re-writes
1. Flexible, hackable and easy to audit
## Importmaps
The included file `importmaps.html` can be used as a starting point for
importing `nyooom` via importmaps in a minimal environment. Search-and-replace
`./` to wherever the library should be loaded from if necessary.
## A few more examples:
Create a Button that deletes itself:
```js
document.body.append(
html.button("Delete Me", {click: event => event.target.remove()})
)
```
Turn a two-dimensional array into an HTML table:
```js
const table = rows =>
html.table(html.tbody(rows.map(
row => html.tr(row.map(
cell => html.rd(cell, {dataset: {
content: cell.toLowerCase(),
}})
))
)))
```
A list that you can add items to
```js
let list, input = ""
document.body.append(html.div([
list=html.ul(),
html.input({type: 'text', input: e => input = e.target.value}),
html.button({click: event => list.append(html.li(input))}, "Add"),
]))
```
A list that you can also delete items from
```js
const listItem = content => html.li(
html.span(content), " ", html.a("[remove]", {
click: event => event.target.closest("li").remove(),
style: { cursor: 'pointer', color: 'red' },
})
)
let list, input = ""
document.body.append(html.div([
list=html.ul(),
html.input({type: 'text', input: e => input = e.target.value}),
html.button({click: event => list.append(listItem(input))}, "Add"),
]))
```

18
ref.js
View file

@ -1,18 +0,0 @@
/** A reference to a reactive element that follows it around through changes */
export class Ref {
#current
/** @param {Element|Text} target A reactive element to follow */
constructor(target) {
this.#current = target
this.follow(target)
}
follow(target) {
target.addEventListener("replaced", ({next}) => {
this.#current = next
this.follow(next)
})
}
deref() { return this.#current }
}

429
render.js
View file

@ -1,429 +0,0 @@
// Keep a referee alive until a referrer is collected
const weakReferences = new WeakMap()
/** Keeps the referenced value alive until the referrer is collected
* @param {Object} referrer
* @param {Object} reference
*/
const untilDeathDoThemPart = (referrer, reference) => {
if (!weakReferences.has(referrer)) weakReferences.set(referrer, new Set())
weakReferences.get(referrer).add(reference)
}
/** Like AbortController, but resets after each abort */
class MultiAbortController {
#controller = new AbortController()
/** @return {AbortSignal} */
get signal() { return this.#controller.signal }
abort() { this.#controller.abort(); this.#controller = new AbortController() }
}
/** A symbol representing nothing to be appended to an element */
export const nothing = Symbol("Explicit non-argument for Nyooom")
/** Converts a snake-case string to a CSS property name
* @param {string} key
* @return {string}
*/
const snakeToCSS = key => key.replace(/([A-Z])/g, "-$1").replace(/^-/, "--").toLowerCase()
const snakeToHTML = key => key.replace(/([A-Z])/g, "-$1").replace(/^-/, "").toLowerCase()
/** @typedef SpecialAttributeDescriptor
* @type {object}
* @property {function(Node):void} [get]
* @property {function(Node,any):void} [set]
* @property {function(Node,function(any):void):void} [subscribe]
* @property {function(Node):boolean} [filter]
*/
/** @typedef {EventTarget & {value: any}} Observable */
/** Cancelable event triggered when a reactive element gets replaced with something else */
export class BeforeReplaceEvent extends Event {
/** @param {Element|Text} next */
constructor(next) {
super("nyooom:beforereplace", { cancelable: true })
this.next = next
}
}
/** Event triggered when a reactive element was replaced */
export class AfterReplaceEvent extends Event {
/** @param {Element|Text} next */
constructor(next) {
super("nyooom:afterreplace")
this.next = next
}
}
/** Event triggered when a new element replaces an old one */
export class ReplacedEvent extends Event {
/** @param {Element|Text} old */
constructor(old) {
super("nyooom:replaced", { bubbles: true })
this.old = old
}
}
/** Event triggered when a reactive attribute changes on an element */
export class AttributeEvent extends Event {
/**
* @param {String} attribute
* @param {any} from
* @param {any} to
*/
constructor(attribute, from, to) {
super("nyooom:attribute", { cancelable: true })
this.attribute = attribute
this.from = from
this.to = to
}
}
// Other utility exports
/** Wraps an event handler in a function that calls preventDefault on the event
* @param {function(event) : event} fn
* @return {function(event)}
*/
export const noDefault = fn => event => { event.preventDefault(); return fn(event) }
/** Wraps an event handler in a function that calls preventDefault on the event
* @param {function(event) : event} fn
* @return {function(event)}
*/
export const noPropagate = fn => event => { event.stopPropagation(); return fn(event) }
/** Main class doing all the rendering */
export class Renderer {
static proxy() {
return new Proxy(new this(), {
/** @param {string} prop */
get: (renderer, prop) => /** @param {any[]} args */ (...args) => renderer.node(prop, args),
has: (renderer, prop) => renderer.nodeSupported(prop),
})
}
/** @param {string} name */
node(name, ...args) {
throw "Attempting to use an abstract Renderer"
}
/** @param {string|symbol} name */
nodeSupported(name) {
if (typeof(name) != "string") return false
return true
}
/** Turns an attribute value into a string */
/** @param {any} value */
static serialiseAttributeValue(value) {
if (typeof value == "string" || typeof value == "number")
return value
else if (value && "join" in value)
return value.join(" ")
else if (Object.getPrototypeOf(value) == Object.prototype)
return JSON.stringify(value)
else
return value.toString()
}
}
export class DomRenderer extends Renderer {
/** @type {Object<string,SpecialAttributeDescriptor>} */
static specialAttributes = Object.freeze({})
/** Processes a list of arguments for an HTML Node
* @param {Element|ShadowRoot} element
* @param {Array} args
*/
static apply(element, ...args) {
for (const arg of args) if (arg !== nothing) {
if (Array.isArray(arg)) {
this.apply(element, ...arg)
} else {
const child = this.toElement(arg)
if (child)
element.append(child)
else if (typeof arg == "function")
this.apply(element, arg(element) || nothing)
else if (arg instanceof DocumentFragment)
element.append(arg)
else if (arg && typeof(arg)=="object")
for (const key in arg)
if (element instanceof Element)
this.setAttribute(element, key, arg[key])
else
throw `Attempting to set attributes on a non-element (${element.constructor.name})`
else
console.warn(`An argument of type ${typeof arg} has been ignored`, element)
}
}
}
/** Creates a new node
* @param {String} name
* @param {Array} args
*/
node(name, args) {
const element = this.createElement(name)
this.constructor.apply(element, args)
return element
}
/**
* @protected
* @param {String} name
* @param {Object} options
* @return {Node}
*/
createElement(name, options={}) {
return document.createElement(name, options)
}
/** Turns an argument into something that can be inserted as a child into a DOM node
* @protected
* @param {any} value
* @return {Element|Text}
*/
static toElement(value) {
if (typeof value == "string" || typeof value == "number")
return document.createTextNode(value.toString())
else if (value instanceof Element)
return value
else if (this.isObservable(value))
return this.toReactiveElement(value)
}
/**
* @protected
* @param {Observable} observable
* @return {Element|Text}
*/
static toReactiveElement(observable) {
if (observable.value instanceof DocumentFragment) {
throw "Failed to create reactive element: Document fragments cannot be replaced dynamically"
}
const element = this.toElement(observable.value) || document.createComment("Reactive element Placeholder")
untilDeathDoThemPart(element, observable)
let ref = new WeakRef(element)
const handleChange = () => {
const element = ref.deref()
if (!element) return
const next = this.toElement(observable.value)
if (element?.dispatchEvent(new BeforeReplaceEvent(next))) {
element.replaceWith(next)
next.dispatchEvent(new ReplacedEvent(element))
untilDeathDoThemPart(next, observable)
element.dispatchEvent(new AfterReplaceEvent(next))
ref = new WeakRef(next)
}
observable.addEventListener("changed", handleChange, {once: true})
}
observable.addEventListener("changed", handleChange, {once: true})
return element
}
/** Set an attribute on an element
* @protected
* @param {Element} element
* @param {string} attribute
* @param {any} value
* @param {AbortSignal} [cleanupSignal]
*/
static setAttribute(element, attribute, value, cleanupSignal) {
const special = this.getSpecialAttribute(element, attribute)
if (this.isObservable(value))
{ this.setReactiveAttribute(element, attribute, value) }
else if (typeof value === "function")
element.addEventListener(attribute, value, { signal: cleanupSignal })
else if (special?.set)
special.set(element, value)
else if (value === true)
{ if (!element.hasAttribute(attribute)) element.setAttribute(attribute, '') }
else if (value === false)
element.removeAttribute(attribute)
else {
element.setAttribute(attribute, this.serialiseAttributeValue(value))
}
}
/** Set up a binding between an attribute and an observable
* @protected
* @param {Element} element
* @param {string} attribute
* @param {Observable} observable
*/
static setReactiveAttribute(element, attribute, observable) {
const multiAbort = new MultiAbortController()
observable.addEventListener("changed", () => {
multiAbort.abort()
if (element.dispatchEvent(new AttributeEvent(attribute, element.getAttribute(attribute), observable.value)))
this.setAttribute(element, attribute, observable.value, multiAbort.signal)
})
this.setAttribute(element, attribute, observable.value, multiAbort.signal)
const special = this.getSpecialAttribute(element, attribute)
if (special?.subscribe) {
untilDeathDoThemPart(element, observable)
special.subscribe(element, value => {
if (value != observable.value) observable.value = value
})
}
}
/**
* @param {CSSStyleDeclaration} style The style property of a node
* @param {object} rules A map of snake case property names to css values
*/
static insertStyles(style, rules) {
for (const [key, value] of Object.entries(rules))
if (typeof value == "undefined")
style.removeProperty(snakeToCSS(key))
else
style.setProperty(snakeToCSS(key), value.toString())
}
/** Returns whether an object is an observable according to nyooom's contract
* @param {any} object
* @return {object is Observable}
*/
static isObservable(object) {
return object && object.observable
}
/** Wraps a list of elements in a document fragment
* @param {Array<Element|String>} elements
*/
static documentFragment(...elements) {
const fragment = new DocumentFragment()
for (const element of elements)
fragment.append(this.toElement(element))
return fragment
}
/**
* @protected
* @param {Element} element
* @param {String} attribute
*/
static getSpecialAttribute(element, attribute) {
const special = this.specialAttributes[attribute]
if (special?.filter == undefined)
return special
if (special.filter(element))
return special
return undefined
}
/**
* @param {String|Array<String>} data
* @param {Array<String|Element>} items
*/
static createTextOrFragment(data = "", ...items) {
return Array.isArray(data)
? this.textFromTemplate(data, items)
: document.createTextNode(data)
}
/** Turns a template literal into document fragment.
* Strings are returned as text nodes.
* Elements are inserted in between.
* @param {Array<String>} literals
* @param {Array<any>} items
* @return {DocumentFragment}
*/
static textFromTemplate(literals, items) {
const fragment = new DocumentFragment()
for (const key in items) {
fragment.append(document.createTextNode(literals[key]))
fragment.append(this.toElement(items[key]))
}
fragment.append(document.createTextNode(literals[literals.length - 1]))
fragment.normalize()
return fragment
}
}
/** Renderer for normal HTML nodes targetting a browser's DOM */
export class DomHtmlRenderer extends DomRenderer {
/**
* @param {String} name
* @param {Object} options
* @return {Node}
*/
createElement(name, options) {
return document.createElement(snakeToHTML(name), options)
}
/** @type {Object<string,SpecialAttributeDescriptor>} */
static specialAttributes = {
value: {
/** @param {HTMLInputElement} element */
get(element) { return element.value },
/** @param {HTMLInputElement} element */
set(element, value) {
element.setAttribute("value", value)
element.value = value
},
/** @param {HTMLInputElement} element */
subscribe(element, callback) {
element.addEventListener("input", () => {
callback(this.get(element))
})
},
/** @param {HTMLElement} element */
filter(element) {
return element.nodeName.toLowerCase() == "input"
}
},
style: {
/** @param {HTMLElement} element */
set(element, value) { DomRenderer.insertStyles(element.style, value) }
},
dataset: {
/** @param {HTMLElement} element */
set(element, value) {
for (const [attribute2, value2] of Object.entries(value)) {
element.dataset[attribute2] = DomRenderer.serialiseAttributeValue(value2)
}
}
},
shadowRoot: {
/** @param {HTMLElement} element */
set(element, value) {
DomRenderer.apply(
(element.shadowRoot || element.attachShadow({ mode: "open" })),
value
)
}
}
}
}
/** Renderer for normal SVG nodes targetting a browser's DOM */
export class DomSvgRenderer extends DomRenderer {
/**
* @param {String} name
* @param {Object} options
* @return {Node}
*/
createElement(name, options) {
return document.createElementNS("http://www.w3.org/2000/svg", name, options)
}
}
export const html = DomHtmlRenderer.proxy()
export const svg = DomSvgRenderer.proxy()
export const fragment = DomRenderer.documentFragment.bind(DomRenderer)
export const text = DomRenderer.createTextOrFragment.bind(DomRenderer)
export default html

69
styles.css Normal file
View file

@ -0,0 +1,69 @@
:root {
--corner-radius: 2px;
}
[contenteditable] { white-space: pre }
#elevator-pitch {
& .big {
font-size: 1.4em;
}
& em {
font-style: unset;
color: var(--secondary-4);
@container style(--color-scheme: dark) {
color: var(--secondary-7);
}
}
}
p.important {
font-size: 1.2em;
}
page-hero {
background-image:
radial-gradient(ellipse at 100% -40%, #fff0, #ccaaff06 70%, #fff0 70%),
radial-gradient(ellipse at -40% -20%, #fff0, #bb88ff02 60%, #fff0 60%),
radial-gradient(ellipse at 180% 180%, #fff0, #bb88ff02 60%, #fff0 60%),
linear-gradient(to top left, #a2f1, transparent),
linear-gradient(to top right, #c2d1, transparent);
&::after {
content: '';
display: block;
position: absolute;
bottom: 0;
height: .2rem;
left: 0;
right: 0;
background-image: linear-gradient(to right, #0660, #0f6, #0660);
background-image: linear-gradient(to right in hsl, #6060, #f0f, #6060);
filter: saturate(.4) opacity(.2);
}
}
skooma-showcase {
--padding: .6em;
border-radius: var(--corner-radius);
padding: var(--padding);
background-color: #fff1;
--hr-color: var(--primary-4);
tab-size: 3;
}
type-writer {
font-weight: bold;
&:defined::after {
content: "|";
opacity: 0.5;
}
}
main {
line-height: 1.2em;
p {
line-height: 1.6em;
}
}