[WIP] Start working on website
This commit is contained in:
commit
d7b89c2747
3 changed files with 603 additions and 0 deletions
432
index.html
Normal file
432
index.html
Normal file
|
@ -0,0 +1,432 @@
|
|||
<html theme=dark>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<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);
|
||||
@import
|
||||
/* url('https://cdn.jsdelivr.net/gh/darkwiiplayer/css@main/schemes/talia.css') */
|
||||
url('https://darkwiiplayer.github.io/css/schemes/talia.css') layer(theme);
|
||||
@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 "./js/SkoomaShowcase.js"
|
||||
</script>
|
||||
|
||||
<a name=top></a>
|
||||
|
||||
<header class="fixed">
|
||||
<nav class="bar">
|
||||
<ul>
|
||||
<li><a href="#top">Top</a></li>
|
||||
<li><a href="#getting-started">Getting Started</a></li>
|
||||
<li><a href="https://github.com/darkwiiplayer/skooma-js">GitHub</a></li>
|
||||
<li><a href="https://www.npmjs.com/package/skooma">npm</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<img class="jsdelivr-badge" src="https://data.jsdelivr.com/v1/package/npm/skooma/badge">
|
||||
</header>
|
||||
|
||||
<page-hero cover=60>
|
||||
<!-- <img src="https://picsum.photos/1920/1080"> -->
|
||||
<hgroup>
|
||||
<h1 style="font-size: 6vw">Skooma</span></h1>
|
||||
<code lang="js">import { html } from "skooma.js"</code>
|
||||
<p>
|
||||
A new way of building
|
||||
<type-writer loop>
|
||||
<span>HTML</span>,
|
||||
<span>components</span> and
|
||||
<span>applications</span>
|
||||
</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>
|
||||
|
||||
<vertical-spacer style="height: 6rem"></vertical-spacer>
|
||||
|
||||
<main>
|
||||
<section id="elevator-pitch" class="content-width">
|
||||
<h2>Elevator Pitch</h2>
|
||||
|
||||
<p>
|
||||
Skooma lets you <u>elegantly</u> express nested DOM structures in <u>plain JavaScript</u>.
|
||||
<br>
|
||||
It makes your code more <u>declarative</u> without re-inventing language features like loops and conditions.
|
||||
</p>
|
||||
|
||||
<skooma-showcase>
|
||||
<code><div contenteditable="false">return html.p(
|
||||
"Text Node",
|
||||
html.br(),
|
||||
html.b("HTML Node")
|
||||
)</div></code>
|
||||
</skooma-showcase>
|
||||
|
||||
<p>
|
||||
Skooma aims to be what <code>document.createElement</code> should have been.
|
||||
It creates a node, sets classes and attributes, inserts content and attaches event listeners.
|
||||
</p>
|
||||
|
||||
<p>Some key characteristics:</p>
|
||||
<ul>
|
||||
<li> <b>No build steps required</b>
|
||||
<li> <b>Plays nice with web components</b>
|
||||
<li> <b>Just vanilla JavaScript</b>
|
||||
<li> Code mirrors output in structure
|
||||
<li> Single Responsibility: Generating HTML
|
||||
<li> No closing tags
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="getting-started" class="content-width">
|
||||
<h2>Getting Started</h2>
|
||||
|
||||
<section>
|
||||
<p class="important">
|
||||
Trying out skooma is super easy!<br>
|
||||
Just import the <code>html</code> export into your script and start generating DOM nodes.
|
||||
</p>
|
||||
|
||||
<pre><code lang=js>import {html} from "https://cdn.jsdelivr.net/npm/skooma@1.3.1/skooma.min.js"</code></pre>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p>
|
||||
To create elements, skooma exposes the <code>html</code> proxy.
|
||||
Accessing this proxy with any value will return a node factory function.
|
||||
<br>
|
||||
In its most basic form, these functions will simply generate a new DOM node
|
||||
of the respective type.
|
||||
</p>
|
||||
|
||||
<skooma-showcase preview="false">
|
||||
<code>
|
||||
<div contenteditable="false">return html.div()</div>
|
||||
</code>
|
||||
</skooma-showcase>
|
||||
|
||||
<p>
|
||||
If you quickly want to render your HTML to a website to see the result,
|
||||
the component helper found in <code>sckooma/state.js</code> makes this a lot easier.<br>
|
||||
This wrapper registers a new custom element to insert the results of a function in your document.
|
||||
</p>
|
||||
|
||||
<pre><code lang=js>import {component} from "https://cdn.jsdelivr.net/npm/skooma@1.3.1/state.min.js"
|
||||
const myComponent = () => html.div("Rendered Component")
|
||||
component(myComponent) //Registers it as <my-component></code></pre>
|
||||
|
||||
<p>
|
||||
This wrapper also provides some state-management features that will be described in a separate section.
|
||||
</p>
|
||||
|
||||
<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">
|
||||
<h3>Basic DOM generation</h3>
|
||||
|
||||
<section>
|
||||
<p>
|
||||
Content can be added to a node by simply passing it as arguments to the function.
|
||||
String arguments get inserted as text nodes, and DOM nodes are simply appended to the child list.
|
||||
<br>
|
||||
This results in an easy way to compose nested DOM structures.
|
||||
</p>
|
||||
|
||||
<skooma-showcase preview="false">
|
||||
<code>
|
||||
<div contenteditable="false">return html.span(
|
||||
"Testing ",
|
||||
html.u("stuff")
|
||||
)</div>
|
||||
</code>
|
||||
</skooma-showcase>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p>
|
||||
Attributes are added by passing an object to the function,
|
||||
where each key-value pair gets turned into an attribute on
|
||||
the generated node.
|
||||
</p>
|
||||
|
||||
<skooma-showcase preview="false">
|
||||
<code>
|
||||
<div contenteditable="false">return html.span({
|
||||
id: "warning",
|
||||
class: ["danger", "bold"],
|
||||
// Arrays get joined with spaces
|
||||
})</div>
|
||||
</code>
|
||||
</skooma-showcase>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p>
|
||||
When trying to assign a function as the value of an attribute,
|
||||
skooma will instead register that function as an event handler
|
||||
with the attribute name being used the event name instead.
|
||||
</p>
|
||||
|
||||
<skooma-showcase code="false">
|
||||
<code>
|
||||
<div contenteditable="false">return html.button(
|
||||
"Click Me!",
|
||||
{ click: event => {
|
||||
alert("Button clicked :3")
|
||||
} }
|
||||
)</div>
|
||||
</code>
|
||||
</skooma-showcase>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="content-width">
|
||||
|
||||
<h2>Leveraging JS features</h2>
|
||||
|
||||
<p>
|
||||
When generating HTML with skooma, you never stop writing JavaScript.
|
||||
This means that all JavaScript features are available anywhere
|
||||
inside your code. 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, and the syntax is always 100%
|
||||
pure javascript as most browsers and dev tools understand it.
|
||||
</p>
|
||||
|
||||
<skooma-showcase code="false">
|
||||
<code>
|
||||
<div contenteditable="false">return html.ul(
|
||||
["u", "b", "i"].map(
|
||||
type => html.li(html[type](
|
||||
`<${type}> element`
|
||||
))
|
||||
)
|
||||
)</div>
|
||||
</code>
|
||||
</skooma-showcase>
|
||||
|
||||
<p>
|
||||
A more complex example that assembles an array of objects into a table:
|
||||
</p>
|
||||
|
||||
<skooma-showcase code="false">
|
||||
<code>
|
||||
<div contenteditable="false">const table = (...rows) => {
|
||||
const keys = new Set()
|
||||
for (const row of rows) {
|
||||
Object.keys(row).forEach(
|
||||
key => keys.add(key)
|
||||
)
|
||||
}
|
||||
const header = Array.from(keys)
|
||||
return html.table(
|
||||
{class: "full-width"},
|
||||
html.thead(
|
||||
html.tr(header.map(
|
||||
key => html.th(key)
|
||||
))
|
||||
),
|
||||
html.tbody (
|
||||
rows.map(row => html.tr(
|
||||
header.map(
|
||||
key => html.td(
|
||||
row[key] ?? empty
|
||||
)
|
||||
)
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
return table(
|
||||
{name: "Alice", hobby: "fishing"},
|
||||
{name: "Bob", hobby: "cooking", age: 22}
|
||||
)</div>
|
||||
</code>
|
||||
</skooma-showcase>
|
||||
</section>
|
||||
|
||||
<section class="content-width">
|
||||
<h2>Managing state in Skooma</h2>
|
||||
<pre><code lang=js>import {State} from 'skooma/state.js'</code></pre>
|
||||
|
||||
<p>
|
||||
Reactivity and state management in skooma are split between the core module and the <code>state</code> module.
|
||||
</p>
|
||||
|
||||
<h3>Generator Functions</h3>
|
||||
<p>
|
||||
The core <code>skooma.js</code> module understands a state object to be anything that is
|
||||
</p>
|
||||
<ol>
|
||||
<li> Not a DOM node
|
||||
<li> Has a <code>value</code> property
|
||||
<li> Has an <code>addEventListener</code> method
|
||||
</ol>
|
||||
<p>
|
||||
When such an object is passed into a generator function as either a child element or an attribute,
|
||||
skooma will use the <code>value</code> of the state object and register a <code>"change"</code> event
|
||||
that replaces the attribute or child element with the current <code>value</code>.
|
||||
</p>
|
||||
<p>
|
||||
One special case worth mentioning is when an attribute is assigned a state object with a function as its value.
|
||||
In these cases, the function gets registered as an event handler just like if it was assigned directly,
|
||||
but updating the state will remove the event handler and register a new event handler with the current <code>value</code>.
|
||||
</p>
|
||||
|
||||
<h3>The <code>state</code> module</h3>
|
||||
<p>
|
||||
This module primarily exposts the <code>State</code> class,
|
||||
which extends the <code>EventTarget</code> class.
|
||||
</p>
|
||||
<p>
|
||||
Every state object exposes a proxy object via the <code>proxy</code> property.
|
||||
This proxy can be used like any normal object, but setting any property on it will dispatch a <code>"change"</code> event on the corresponding state object.
|
||||
The proxy object itself also has a getter and setter for the <code>value</code> property, which gets forwarded directly to <code>proxy.value</code> for easier access.
|
||||
</p>
|
||||
<p>
|
||||
This means that any <code>State</code> object satisfies the API of a state object from the previous section,
|
||||
meaning they can be used to build reactive components.
|
||||
</p>
|
||||
|
||||
<skooma-showcase code="false">
|
||||
<code>
|
||||
<div contenteditable="false">const counter = new State({value: 0})
|
||||
|
||||
counter.valueChanged = newValue =>
|
||||
console.log(`Value: ${newValue}`)
|
||||
|
||||
return html.flexColumn(
|
||||
{gap: 1},
|
||||
html.button(
|
||||
"Click Me! ",
|
||||
html.span(counter),
|
||||
{ click: () => {
|
||||
counter.value += 1
|
||||
}}
|
||||
),
|
||||
html.button(
|
||||
"Clear",
|
||||
{ click: () => {
|
||||
counter.value = 0
|
||||
}}
|
||||
)
|
||||
)</div>
|
||||
</code>
|
||||
</skooma-showcase>
|
||||
|
||||
<!-- TODO: Describe constructor options -->
|
||||
<p>
|
||||
The basic <code>State</code> object is backed by a plain JS object, so their attributes are unique and do not persist page reload.<br>
|
||||
By contrast, the <code>StoredState</code> class, which extends the core <code>State</code> class, is backed by a <code>Storage</code> object like
|
||||
<code>window.localStorage</code> or <code>window.sessionStorage</code>, meaning that they persist page reloads.<br>
|
||||
Additionally, they detect changes both from the current as well as from other browser tabs/windows, so any updates of the state
|
||||
get propagated automatically to all states backed by the same storage.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="content-width">
|
||||
|
||||
<h2>A simple Todo list</h2>
|
||||
|
||||
<p>
|
||||
A simple, but not completely bare-bones todo list application,
|
||||
using nothing more than Skooma and the CSS already present
|
||||
on this page to save some code.
|
||||
</p>
|
||||
|
||||
<skooma-showcase code="false">
|
||||
<code>
|
||||
<div contenteditable="false">let todo, input
|
||||
const task = value =>
|
||||
html.flexRow (
|
||||
{class: ["todo"], gap: 1},
|
||||
value,
|
||||
html.span("[x]", {
|
||||
style: {
|
||||
color: "var(--primary-6)",
|
||||
cursor: "pointer"
|
||||
},
|
||||
click: event => {
|
||||
event
|
||||
.target
|
||||
.closest(".todo")
|
||||
.remove()
|
||||
}
|
||||
})
|
||||
)
|
||||
return todo =
|
||||
html.flexColumn(
|
||||
{gap: 1},
|
||||
input=html.input({
|
||||
type: "text",
|
||||
placeholder:
|
||||
"Do some stuff",
|
||||
}),
|
||||
html.button("Add Task",
|
||||
{
|
||||
click: event => {
|
||||
todo.append(task(
|
||||
input.value ||
|
||||
input.placeholder
|
||||
))
|
||||
}
|
||||
})
|
||||
)</div>
|
||||
</code>
|
||||
</skooma-showcase>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="inset box">
|
||||
<flex-row gap=1>
|
||||
<div>
|
||||
Skooma is great!
|
||||
</div>
|
||||
</flex-row>
|
||||
</footer>
|
||||
</html>
|
124
js/SkoomaShowcase.js
Normal file
124
js/SkoomaShowcase.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
import {html,empty} from "https://cdn.jsdelivr.net/gh/darkwiiplayer/skooma-js@f79e7a9/skooma.js"
|
||||
import {State} from "https://cdn.jsdelivr.net/gh/darkwiiplayer/skooma-js@f79e7a9/state.js"
|
||||
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 SkoomaShowcase 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.shadowRoot.replaceChildren(
|
||||
html.slot(),
|
||||
html.span("🖉", {
|
||||
class: "edit",
|
||||
click: ({target: button}) => {
|
||||
this.querySelectorAll("[contenteditable]")
|
||||
.forEach(item => {
|
||||
if (item.contentEditable == "true") {
|
||||
item.contentEditable = "false"
|
||||
button.classList.remove("editing")
|
||||
this.format()
|
||||
} else {
|
||||
item.contentEditable = "true"
|
||||
button.classList.add("editing")
|
||||
item.innerText = item.innerText
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
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, empty, State)
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
47
styles.css
Normal file
47
styles.css
Normal file
|
@ -0,0 +1,47 @@
|
|||
:root {
|
||||
--corner-radius: 2px;
|
||||
}
|
||||
|
||||
[contenteditable] { white-space: pre }
|
||||
|
||||
#elevator-pitch p, 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;
|
||||
&::after {
|
||||
content: "|";
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue