diff --git a/blog/index.html b/blog/index.html index f08457c3..123443e2 100644 --- a/blog/index.html +++ b/blog/index.html @@ -1,4 +1,4 @@ -Crank.js | Blog \ No newline at end of file diff --git a/blog/introducing-crank/index.html b/blog/introducing-crank/index.html index ec9b15ed..d8184dc9 100644 --- a/blog/introducing-crank/index.html +++ b/blog/introducing-crank/index.html @@ -1,4 +1,4 @@ -Crank.js | Introducing Crank \ No newline at end of file diff --git a/blog/writing-crank-from-scratch/index.html b/blog/writing-crank-from-scratch/index.html index 97b38ea3..f70be487 100644 --- a/blog/writing-crank-from-scratch/index.html +++ b/blog/writing-crank-from-scratch/index.html @@ -1,4 +1,4 @@ -Crank.js | Writing Crank from Scratch \ No newline at end of file diff --git a/guides/async-components/index.html b/guides/async-components/index.html index d43c1284..a8a328cb 100644 --- a/guides/async-components/index.html +++ b/guides/async-components/index.html @@ -1,4 +1,4 @@ -Crank.js | Async Components

Async Function Components

So far, every component we’ve seen has worked synchronously, and Crank will respect this as an intentional decision by the developer by keeping the entire process of rendering synchronous from start to finish. However, modern JavaScript includes promises and async/await, which allow you to write concurrently executing code as if it were synchronous. To facilitate these features, Crank allows components to be asynchronous functions as well, and we call these components, async function components.

async function IPAddress () {
console.log(document.body.innerHTML); // <div>Your IP Address: 127.0.0.1</div>
})();

When Crank renders an async component anywhere in the tree, the entire process becomes asynchronous. Concretely, this means that renderer.render or this.refresh calls return a promise which fulfills when rendering has finished. It also means that no actual DOM updates will be triggered until this moment.

Concurrent Updates

Because async function components can be rerendered while they are still pending, Crank implements a couple rules to make concurrent updates predictable and performant:

  1. There can only be one pending run of an async function component at the same time for an element in the tree. If the same async component is rerendered concurrently while it is still pending, another call is enqueued with the latest props.
  console.log(document.body.innerHTML); // "<div>Run 4</div>"
})();

In the preceding example, at no point is there more than one simultaneous call to the Delay component, despite the fact that it is rerendered concurrently for its second through fourth renders. And because these renderings are enqueued, only the second and fourth renderings have any effect. This is because the element is busy with the second render by the time the third and fourth renderings are requested, and then, only the fourth rendering is actually executed because third rendering’s props are obsolete by the time the component is ready to update again. This behavior allows async components to always be kept up-to-date without producing excess calls to the function.

  1. If two different async components are rendered in the same position, the components are raced. If the earlier component fulfills first, it shows until the later component fulfills. If the later component fulfills first, the earlier component is never rendered.
  console.log(document.body.innerHTML); // "<div><span>Fast</span></div>"
})();

As we’ll see later, this ratcheting effect becomes useful for rendering fallback states for async components.

Async Generator Components

Just as you can write stateful components with sync generator functions, you can also write stateful async components with async generator functions.

  console.log(document.body.innerHTML); //<div>The count is now: 2</div>
})();

AsyncLabeledCounter is an async version of the LabeledCounter example introduced in the section on props updates. This example demonstrates several key differences between sync and async generator components. Firstly, rather than using while or for…of loops as with sync generator components, we now use a for await…of loop. This is possible because contexts are not just an iterable of props, but also an async iterable of props as well.

Secondly, you’ll notice that the async generator yields multiple times per iteration over this, once to show a loading message and once to show the actual count. While it is possible for sync generators components to yield multiple times per iteration over this, it wouldn’t necessarily make sense to do so because generators suspend at each yield, and upon resuming a second time within the same loop, the props would be stale. In contrast, async generator components are continuously resumed. Rather than suspending at each yield, we rely on the for await…of loop, which suspends at its end until the next update.

Loading Indicators

The async components we’ve seen so far have been all or nothing, in the sense that Crank can’t show anything until all promises in the tree have fulfilled. This can be a problem when you have an async call which takes longer than expected. It would be nice if parts of the element tree could be shown without waiting, to create responsive user experiences.

However, because loading indicators which show immediately can paradoxically make your app seem less responsive, we use the async rules described previously along with async generator functions to show loading indicators which appear only when certain components take too long.


renderer.render(<RandomDogApp />, document.body);

In this example, the RandomDogLoader component is an async generator component which races the LoadingIndicator component with the RandomDog component. Because the async generator component resumes continuously, both components are rendered, and according to the second rule of async components, the loading indicator only shows if the RandomDog component takes longer than the LoadingIndicator component, which fulfills at a fixed interval of one second.

The preceding example hints at how we could abstract this pattern to implement the Suspense component from React.

  );
})();

No special tags are needed for async loading states, and the functionality to write this logic is implemented using the same element diffing algorithm that governs synchronous components. Additionally, this approach is more flexible in the sense that you can extend it; for instance, you can add another yield to the for await…of loop to show a second fallback state which waits ten seconds, to inform the user that something went wrong or that servers are slow to respond.

No special tags are needed for async loading states, and the functionality to write this logic is implemented using the same element diffing algorithm that governs synchronous components. Additionally, this approach is more flexible in the sense that you can extend it; for instance, you can add another yield to the for await…of loop to show a second fallback state which waits ten seconds, to inform the user that something went wrong or that servers are slow to respond.

\ No newline at end of file diff --git a/guides/components/index.html b/guides/components/index.html index b6fc7203..38b9fec0 100644 --- a/guides/components/index.html +++ b/guides/components/index.html @@ -1,4 +1,4 @@ -Crank.js | Components \ No newline at end of file diff --git a/guides/custom-renderers/index.html b/guides/custom-renderers/index.html index a27a5ce6..10fd3348 100644 --- a/guides/custom-renderers/index.html +++ b/guides/custom-renderers/index.html @@ -1,4 +1,4 @@ -Crank.js | Custom Renderers

TNode

TNode is the most important type: it is the type of the node associated with each host element. For instance, for the basic DOM renderer, TNode is the DOM Node interface.

TScope

TScope is the type of the scope, a renderer-specific concept for arbitrary data which is passed down the tree between host elements. Scopes are useful for passing contextual information down the tree to be used when nodes are created; for instance, the DOM renderer uses the scope to pass down information about whether we’re currently rendering in an SVG element.

TRoot

TRoot is the type of the root node. This is the type of the second parameter passed to the Renderer.render method, and the root prop passed to Portal elements. It is usually the same type as TNode but can vary according to renderer requirements.

TResult

TResult describes the type of values made visible to renderer consumers. Any time Crank exposes an internal node, for instance, via the crank-ref callback, or as the result of yield expressions in generator components, the renderer can intercept this access and provide something other than the internal nodes, allowing renderers to hide implementation details and provide results which make more sense for a specific environment.

For example, the HTML string renderer has an internal node representation, but converts these nodes to strings before they’re exposed to consumers. This is because the internal nodes must be a referentially unique object which is mutated during rendering, while JavaScript strings are referentially transparent and immutable. Therefore, the TResult type of the HTML renderer is string.

Methods

The following is a description of the signatures of internal renderer methods and when they’re executed. When creating a custom renderer, you are expected to override these methods via inheritance.

Renderer.prototype.create

  el: Element<string | symbol>, scope: TScope
): TNode;

The create method is called for each host element the first time the element is committed. This method is passed the current host element and scope, and should return the node which will be associated with the host element. This node will remain constant for an element for as long as the element is rendered.

By default, this method will throw a Not Implemented error, so custom renderers should always implement this method.

Renderer.prototype.read

read(value: Array<TNode | string> | TNode | string | undefined): TResult;

The renderer exposes rendered values in the following places:

When an element or elements are read in this way, we call the read method to give renderers a final chance to manipulate what is exposed, so as to hide internal implementation details and return something which makes sense for the target environment. The parameter passed to the read method can be a node, a string, an array of nodes and strings, or undefined. The return value is what is actually exposed.

This method is optional. By default, read is an identity function which returns the value passed in.

Renderer.prototype.patch

patch(el: Element<string | symbol>, node: TNode): unknown;

The patch method is called for each host element whenever it is committed. This method is passed the current host element and its related node, and its return value is ignored. This method is usually where you would mutate the internal node according to the props of the host element.

Implementation of this method is optional for renderers.

Renderer.prototype.arrange

  children: Array<TNode | string>,
): unknown;

The arrange method is called whenever an element’s children have changed. It is called with the current host element, the host element’s related node, and the rendered values of all the element’s descendants as an array. In addition to when a host element commits, the arrange method may also be called when a child refreshes or otherwise causes a host element’s rendered children to change. Because the arrange method is called for every root/portal element, the parent can be of type TRoot as well as TNode.

This method is where the magic happens, and is where you connect the nodes of your target environment into an internal tree.

Renderer.prototype.scope

scope(el: Element<string | symbol>, scope: TScope | undefined): TScope;

The scope method is called for each host or portal element as elements are mounted or updated. Unlike the other custom renderer methods, the scope method is called during the pre-order traversal of the tree, much as components are. The scope method is passed the current host element and scope as parameters, and the return value becomes the scope argument passed to the create and scope method calls for descendant host elements.

By default, the scope method returns undefined, meaning the scope will be undefined throughout your application.

Renderer.prototype.escape

escape(text: string, scope: TScope): string;

The escape method is called whenever a string is encountered in the element tree. It is mainly useful when creating string-based renderers like an HTML or XML renderer, because most rendering targets like the DOM provide text node interfaces which sanitize inputs by default.

One important detail is that escape should not return text nodes or anything besides a string. We defer this step to the arrange method because this allows the renderer to normalize a host element’s children by concatenating adjacent strings.

By default, the escape method returns the string which was passed in.

Renderer.prototype.parse

parse(text: string, scope: TScope): TNode | string;

When the renderer encounters a Raw element whose value prop is a string, it calls the parse method with that string and the current scope. The return value should be the parsed node, or it can be a string as well, in which case parse will be handled like a string child by parents.

By default, the parse method returns the string which was passed in.

Renderer.prototype.dispose

dispose(el: Element<string | symbol>, node: TNode): unknown

The dispose method is called whenever a host element is unmounted. It is called with the host element and its related node. You can use this method to manually release a node or clean up event listeners for garbage collection purposes.

This method is optional and its return value is ignored.

Renderer.prototype.complete

complete(root: TRoot): unknown;

The complete method is called at the end of every render execution, when all elements have been committed and all other renderer methods have been called. It is useful, for instance, if your rendering target needs some final code to execute before any mutations take effect.

This method is optional and its return value is ignored.

The complete method is called at the end of every render execution, when all elements have been committed and all other renderer methods have been called. It is useful, for instance, if your rendering target needs some final code to execute before any mutations take effect.

This method is optional and its return value is ignored.

\ No newline at end of file diff --git a/guides/elements/index.html b/guides/elements/index.html index 91e232f0..7be78027 100644 --- a/guides/elements/index.html +++ b/guides/elements/index.html @@ -1,4 +1,4 @@ -Crank.js | Elements and Renderers \ No newline at end of file diff --git a/guides/getting-started/index.html b/guides/getting-started/index.html index bbbe0b41..90950adf 100644 --- a/guides/getting-started/index.html +++ b/guides/getting-started/index.html @@ -1,4 +1,4 @@ -Crank.js | Getting Started \ No newline at end of file diff --git a/guides/handling-events/index.html b/guides/handling-events/index.html index 68cb8fba..8b6a2ef5 100644 --- a/guides/handling-events/index.html +++ b/guides/handling-events/index.html @@ -1,4 +1,4 @@ -Crank.js | Handling Events

When using this method, you do not have to call the removeEventListener() method if you merely want to remove event listeners when the component is unmounted. This is done automatically.

The context’s addEventListener() method attaches to the top-level node or nodes which each component renders, so if you want to listen to events on a nested node, you must use event delegation.

addEventListener() method attaches to the top-level node or nodes which each component renders, so if you want to listen to events on a nested node, you must use event delegation.

While the removeEventListener() method is implemented, you do not have to call the removeEventListener() method if you merely want to remove event listeners when the component is unmounted.

Because the event listener is attached to the outer div, we have to filter events by ev.target.tagName in the listener to make sure we’re not incrementing count based on clicks which don’t target the button element.

Because the event listener is attached to the outer div, we have to filter events by ev.target.tagName in the listener to make sure we’re not incrementing count based on clicks which don’t target the button element.

Event props vs EventTarget

The props-based event API and the context-based EventTarget API both have their advantages. On the one hand, using event props means you can listen to exactly the element you’d like to listen to.

On the other hand, using the addEventListener method allows you to take full advantage of the EventTarget API, which includes registering passive event listeners, or listeners which are dispatched during the capture phase. Additionally, the EventTarget API can be used without referencing or accessing the child elements which a component renders, meaning you can use it to listen to elements nested in other components.

Crank supports both API styles for convenience and flexibility.

Dispatching Events

Crank contexts implement the full EventTarget interface, meaning you can use the dispatchEvent method and the CustomEvent class to dispatch custom events to ancestor components:

\ No newline at end of file diff --git a/guides/lifecycles/index.html b/guides/lifecycles/index.html index 330b2e44..053593f3 100644 --- a/guides/lifecycles/index.html +++ b/guides/lifecycles/index.html @@ -1,4 +1,4 @@ -Crank.js | Lifecycles

You should be careful when writing generator components to make sure that you always place your yield operators in a for or while loop. If you forget and implicitly return from the generator, it will stop updating and nothing will be rendered ever again.

renderer.render(<Numbers />, document.body);
console.log(document.body.innerHTML); // ""

Cleaning Up

When a generator component is removed from the tree, Crank calls the return method on the component’s generator object. You can think of it as whatever yield expression your component was suspended on being replaced by a return statement. This means any loops your component was in when the generator suspended are broken out of, and code after the yield does not execute.

You can take advantage of this behavior by wrapping your yield loops in a try/finally block to release any resources that your component may have used.

// "finally block executed"
console.log(document.body); // ""

The same best practices which apply to try/finally statements in regular functions apply to generator components. In short, you should not yield or return anything in the finally block. Crank will not use the yielded or returned values and doing so might cause your components to inadvertently swallow errors or suspend in unexpected locations.

Catching Errors

We all make mistakes, and it can be useful to catch errors thrown by our components so that we can show the user something or notify error-logging services. To facilitate this, Crank will catch errors thrown when rendering child elements and throw them back into parent generator components using the throw method on the component’s generator object. You can think of it as whatever yield expression your component was suspended on being replaced with a throw statement with the error set to whatever was thrown by the component’s children.

You can take advantage of this behavior by wrapping your yield operations in a try/catch block to catch errors caused by children.

renderer.render(<Catcher />, document.body);
console.log(document.body.innerHTML); // "<div>Error: Hmmm</div>"

As explained previously, this component “sticks” because it uses a return statement, so that the same error message is shown until the component is unmounted. However, you may also want to recover from errors as well, and you can do this by ignoring or handling the error.

console.log(document.body.innerHTML);
// "<div>I’ll be back</div>"

Accessing Rendered Values

Sometimes, declarative rendering is not enough, and you’ll want to access the actual DOM nodes you’ve rendered, to make measurements or call imperative methods like the focus method for form elements, or the play method for media elements. To facilitate this, Crank will pass rendered DOM nodes back into the generator using the next method. This means that as a developer, you can read yield expressions to access the actual rendered DOM nodes.

  }
}

The FocusingInput component focuses every time it is rerendered. We use an async generator component here because async generators continuously resume, so the input.focus call happens directly after the component is rendered. While we also pass rendered nodes into sync generator components as well, attempting to access them directly after the yield may lead to surprising results.

  }
}

The problem is that sync generator components suspend at the point of yield expressions and only resume when updated by the parent or by a call to the refresh method. This means that if you were to try to access the rendered value via a yield expression, your code would not execute until the moment the component rerenders. You can imagine this as the generator function above suspended exactly before the yield expression is assigned to the input variable.

To solve this problem, Crank provides an additional method on the context called schedule, which takes a callback and calls it with the rendered value after the component executes.

  }
}

The schedule method fires the passed in callback synchronously when component finally renders. However, one unfortunate consequence of using a callback is that we lose the sequential execution of code which makes generator components so elegant and easy to understand. We can recover some of this linearity by using the schedule method with the refresh method.

  }
}

The focusing input now focuses before the children are yielded, but because the same input is yielded, the result is mostly the same. The schedule method is designed to work with the refresh method so that sync generator components can schedule multiple rendering passes which work synchronously.

The focusing input now focuses before the children are yielded, but because the same input is yielded, the result is mostly the same. The schedule method is designed to work with the refresh method so that sync generator components can schedule multiple rendering passes which work synchronously.

\ No newline at end of file diff --git a/guides/reference-for-react-developers/index.html b/guides/reference-for-react-developers/index.html index 5d333909..9b34adc3 100644 --- a/guides/reference-for-react-developers/index.html +++ b/guides/reference-for-react-developers/index.html @@ -1,4 +1,4 @@ -Crank.js | Reference for React Developers

This example is pseudocode which demonstrates where React’s class methods would be called relative to an async generator component. Refer to the guide on lifecycles for more information on using generator functions.

The following are specific equivalents for React methods.

setState and forceUpdate

Crank uses generator functions and local variables for local state. Refer to the section on stateful components.

Crank is not “reactive” in the same sense as React, in that it does not track your component’s local state and rerender when it detects a change. You can either use the context’s refresh to manually refresh the component, similar to React’s forceUpdate method, or you can use async generator components, which refresh automatically whenever the returned async generator yields.

defaultProps

Crank doesn’t have a defaultProps implementation. Instead, you can provide default values when destructuring props. See the guide on default props.

componentWillMount and componentDidMount

Setup code for components can be written at the top of generator components. It will not execute until the component is mounted in the tree.

shouldComponentUpdate

As an alternative to React’s shouldComponentUpdate method, you can use Copy elements to prevent the rerendering of a specific subtree. Refer to the description of Copy elements for more information.

getDerivedStateFromProps, componentWillUpdate and getSnapshotBeforeUpdate

Code which compares old and new props or state can be written directly in your components. See the section on prop updates for an example of a component which compares old and new props.

componentDidUpdate

To execute code after rendering, you can use async generator components or the schedule method. See the guide on accessing rendered values for more information.

componentWillUnmount

You can use a try/finally block to run code when a component is unmounted. You can also use the cleanup method if you’re writing extensions which don’t run in the main execution of the component.

componentDidCatch

To catch errors which occur in child components, you can use generator components and wrap yield operations in a try/catch block. Refer to the relevant guide on catching errors.

Hooks

Crank does not implement any APIs similar to React Hooks. The main appeal of hooks for library authors is that you can encapsulate entire APIs in one or two hooks. Refer to the guide on reusable logic for a description of strategies you can use to reuse logic and write library wrappers in Crank.

The following are alternatives to specific hooks.

useState and useReducer

Crank uses generator functions and local variables for local state. Refer to the section on stateful components.

useEffect and useLayoutEffect

Crank does not have any requirements that rendering should be “pure.” In other words, you can trigger side-effects directly while rendering because Crank does not execute components more times than you might expect. Refer to the guide on accessing rendered values for more information on code which executes after rendering.

useMemo and useCallback

Because the execution of generator components is preserved, there is no need to “memoize” or “cache” callbacks or other values. You can simply assign them to a constant variable.

useImperativeHandle

Crank does not have a way to access component instances, and parent components should not access child components directly. A web component wrapper for defining custom elements with imperative APIs is planned.

Suspense and Concurrent Mode

Crank uses async functions and promises for scheduling and coordinating async processes. See the guide on async components for an introduction to async components, as well as a demonstration of how you can implement the Suspense component directly in user space.

PropTypes

Crank is written in TypeScript, and you can add type checking to components by typing the props parameter of the component function. See the guide on TypeScript for detailed instructions on how to type components.

Array Children

Crank does not restrict children in JSX elements to just arrays. You can interpolate ES6 maps, sets or any other iterable into your Crank elements. Additionally, Crank does not warn you if elements in the iterable are unkeyed.

Fragments

The Fragment element works almost exactly as it does in React, except that in Crank you can also use a callback ref to access its contents.

React.cloneElement

You can clone elements using the cloneElement function.

ReactDOM.createPortal

The createPortal function is replaced by the special Portal element, whose behavior and expected props varies according to the target rendering environment. Refer to the guide on the Portal element for more information.

React.memo

See the guide on Copy tags for a demonstration of how you can use Copy elements to implement React.memo in user space.

DOM element props

The following are a list of the differences in props APIs for DOM elements.

className and htmlFor

Crank prefers attribute names rather than the JS property DOM equivalents when these names are mismatched.

<label class="my-label" for="my-id">Label</label>

In short, Crank is optimized for easy copy-pasting, which using props like className and htmlFor does not encourage. See the section on prop naming conventions for more information.

style

The style prop can be an object of CSS declarations. However, unlike React, CSS property names match the case of their CSS equivalents, and we do not add units to numbers. Additionally, Crank allows the style prop to be a CSS string as well.

<div style="color: red"><span style={{"font-size": "16px"}}>Hello</span></div>

Refer to the guide on the style prop for more information.

Event props

Host elements can be listened to using onevent props, but the prop name will be all lowercase. Crank also provides an EventTarget API for components to add and remove event listeners from the top-level node or nodes of each component. In both cases, Crank does not use a synthetic event system or polyfill events in any way. Refer to the guide on event handling for a longer explanation of event handling in Crank.

Controlled and Uncontrolled Props

Crank does not have a concept of controlled or uncontrolled props, and does not provide defaultValue-style props for DOM elements. See the section on form elements for a detailed description of how Crank handles stateful form elements.

dangerouslySetInnerHTML

Host DOM elements accept an innerHTML prop; Crank does not provide the dangerouslySetInnerHTML={{__html}} API like React. Alternatively, you can use the special Raw tag to insert HTML strings or even DOM nodes directly into an element tree without a parent. Refer to the sections on the innerHTML prop and on the Raw tag for more information.

Keys

Crank provides keyed rendering via the special crank-key prop. The prop was renamed because “key” is a common word and because the prop is not erased from the props object passed into components.

Keys work similarly to the way they do in React. The main difference is that Crank does not warn about unkeyed elements which appear in arrays or iterables.

Refs

Crank provides the callback-style ref API from React via the crank-ref prop. Unlike React, all elements can be read using the crank-ref prop, including Fragment elements. See the guide on the crank-ref prop.

You can also access rendered values in many other ways. Refer to this section for more information.

React Contexts

Because we refer to the this keyword of components as “the component’s context” (“controller” would have been three more characters), we refer to the equivalent concept of React’s Context API as “provisions” instead. We use the context methods provide and consume to define provisions between ancestor and descendant components. See the guide on provisions for more information.

Refer to the guide on the style prop for more information.

Event props

Host elements can be listened to using onevent props, but the prop name will be all lowercase. Crank also provides an EventTarget API for components to add and remove event listeners from the top-level node or nodes of each component. In both cases, Crank does not use a synthetic event system or polyfill events in any way. Refer to the guide on event handling for a longer explanation of event handling in Crank.

Controlled and Uncontrolled Props

Crank does not have a concept of controlled or uncontrolled props, and does not provide defaultValue-style props for DOM elements. See the section on form elements for a detailed description of how Crank handles stateful form elements.

dangerouslySetInnerHTML

Host DOM elements accept an innerHTML prop; Crank does not provide the dangerouslySetInnerHTML={{__html}} API like React. Alternatively, you can use the special Raw tag to insert HTML strings or even DOM nodes directly into an element tree without a parent. Refer to the sections on the innerHTML prop and on the Raw tag for more information.

Keys

Crank provides keyed rendering via the special crank-key prop. The prop was renamed because “key” is a common word and because the prop is not erased from the props object passed into components.

Keys work similarly to the way they do in React. The main difference is that Crank does not warn about unkeyed elements which appear in arrays or iterables.

Refs

Crank provides the callback-style ref API from React via the crank-ref prop. Unlike React, all elements can be read using the crank-ref prop, including Fragment elements. See the guide on the crank-ref prop.

You can also access rendered values in many other ways. Refer to this section for more information.

React Contexts

Because we refer to the this keyword of components as “the component’s context” (“controller” would have been three more characters), we refer to the equivalent concept of React’s Context API as “provisions” instead. We use the context methods provide and consume to define provisions between ancestor and descendant components. See the guide on provisions for more information.

\ No newline at end of file diff --git a/guides/reusable-logic/index.html b/guides/reusable-logic/index.html index aed6f5d4..07e93d05 100644 --- a/guides/reusable-logic/index.html +++ b/guides/reusable-logic/index.html @@ -1,4 +1,4 @@ -Crank.js | Reusable Logic

Provisions allow libraries to define components which interact with their descendants without rigidly defined component hierarchies or requiring data to be passed manually between components via props. This makes them useful, for instance, when writing multiple components which communicate with each other, like custom select and option form elements, or drag-and-drop components.

Anything can be passed as a key to the provide and consume methods, so you can use a symbol to ensure that the provision you pass between your components are private and do not collide with provisions set by others.

Note: Crank does not link “providers” and “consumers” in any way, and doesn’t automatically refresh consumer components when the provide method is called. It’s up to you to ensure consumers update when providers update.

context.schedule

You can pass a callback to the schedule method to listen for when the component renders. Callbacks passed to schedule fire synchronously after the component commits, with the rendered value of the component as its only argument. Scheduled callbacks fire once per call and callback function (think requestAnimationFrame, not setInterval). This means you have to continuously call the schedule method for each update if you want to execute some code every time your component commits.

context.cleanup

Similarly, you can pass a callback to the cleanup method to listen for when the component unmounts. Callbacks passed to cleanup fire synchronously when the component is unmounted. Each registered callback fires only once. The callback is called with the last rendered value of the component as its only argument.

Strategies for Reusing Logic

The following are various patterns you can use to write and reuse logic between components, as well as a description of their tradeoffs. We will be wrapping the global window.setInterval function in examples to demonstrate each design pattern.

Global Context Extensions

You can import and extend the Context class’s prototype to globally extend all contexts in your application.

  }
}

In this example, we define the methods setInterval and clearInterval directly the Context prototype. The setInterval and clearInterval methods will now be available to all components.

Pros:

Cons:

Global context extensions are useful for creating Crank-specific wrappers around already global or otherwise well-known APIs like setInterval, requestAnimationFrame or fetch.

Context Helper Utilities

As an alternative to global context extensions, you can write utility functions which are passed contexts to scope reusable logic per component. In the following example, instead of defining the setInterval method globally, we define it locally by passing the context of the component into the createSetInterval function.


}

Pros:

Cons:

Context helper utilities are useful when you want to write locally-scoped utilities, and are especially well-suited for use-cases which requires setup logic. Possible use-cases include wrappers around stateful APIs like mutation observers or HTML drag and drop.

Higher-order Components

Because Crank components are just functions, we can write functions which both take components as parameters and return wrapped component functions.


const Counter = interval((props) => <div>Seconds: {props.seconds}</div>);

The interval function takes a component function and returns a component which passes the number of seconds as a prop. Additionally, it refreshes the returned component whenever the interval is fired.

Pros:

Cons:

The main advantage of higher-order components is that you can respond to props in your utilities just like you would with a component. Higher-order components are most useful when you need reusable logic which responds to prop updates or sets only well-known props. Possible use-cases include styled component or animation libraries.

Async Iterators

Because components can be written as async generator functions, you can integrate utility functions which return async iterators seamlessly with Crank. Async iterators are a great way to model resources because they define a standard way for releasing resources when they’re no longer needed by returning the iterator.

  }
}

Pros:

Cons:

If you use async iterators/generators already, Crank is the perfect framework for your application.

Pros:

Cons:

If you use async iterators/generators already, Crank is the perfect framework for your application.

\ No newline at end of file diff --git a/guides/special-props-and-tags/index.html b/guides/special-props-and-tags/index.html index 14b8e26d..a6cb89de 100644 --- a/guides/special-props-and-tags/index.html +++ b/guides/special-props-and-tags/index.html @@ -1,4 +1,4 @@ -Crank.js | Special Props and Tags

Keys are scoped to an element’s children, and can be any JavaScript value. When rendering iterables, it’s useful to key elements of the iterable, because it’s common for the values of rendered iterables to added, moved or removed.

console.log(document.firstChild.firstChild === span); // true
Loading...

All elements in the element tree can be keyed. If the element is a component element, the crank-key prop is erased from the props object passed to the component.

crank-ref

Sometimes, you may want to access the rendered value of a specific element in the element tree. To do this, you can pass a callback as the crank-ref prop. This callback is called with the rendered value of the element when the element has committed.

renderer.render(<MyPlayer />, document.body);
Loading...

Refs can be attached to any element in the element tree, and the value passed to the callback will vary according the type of the element and the specific renderer.

children

The children prop passed to components is special because it is not usually set with JSX’s key="value" prop syntax, but by the contents between the opening and closing tags. Crank places no limitations on the types of values that can be passed into components as children, but patterns like render props from the React community, where a callback is passed as the child of a component, should be avoided.

The actual type of the children prop will vary according to the number of children passed in. If a component element has no children (<Component/>), the children prop will be undefined, if it has one child (<Component><Child/></Component>), the children prop will be set to that child, and if it has multiple children (<Component><Child/><Child/></Component>), the children prop will be set to an array of those children. We do this to reduce runtime memory costs. All props have to be retained between renders, and most elements contain only zero or one child, so avoiding the allocation of an extra array for every element in the tree can noticeably reduce memory requirements.

Therefore, the children prop should be treated as a black box, only to be rendered somewhere within a component’s returned or yielded children. Attempting to iterate over or manipulate the passed in children of a component is an anti-pattern, and you should use event dispatch or provisions to coordinate ancestor and descendant components.

Special DOM Props

The following props are specific to host elements for the HTML and DOM renderers.

style

The style prop can be used to add inline styles to an element. It can either be a CSS string, in which case it works exactly as it does in HTML, or it can also be an object, in which case CSS properties can be set individually.

<div style="color: red"><span style={{"font-size": "16px"}}>Hello</span></div>

Note: Unlike other JSX frameworks, Crank does not camel-case style names or add pixel units to numbers.

innerHTML

The innerHTML prop can be used to set the element’s children with an HTML string.

Be careful when using the innerHTML prop, as passing unsanitized text inputs can lead to security vulnerabilities.

As an alternative, you can also use the special Raw element tag, which allows to inject raw HTML or even actual DOM nodes into the element tree, without requiring a parent host element.

Prop Naming Conventions

Crank strives to make copying and pasting HTML into your components as easy as possible, and to this extent it allows you to use class and for as props in your elements instead of className and htmlFor.

<label class="my-label" for="my-id">Label</label>

You can still use the className and htmlFor props as well, but using the former names is preferred. This philosophy also extends to SVG elements, and you can use props like clip-path and stroke-width without having to camel case them.

Special Tags

Crank provides four element tags which have special meaning to the renderer, and affect element diffing and rendering output in various ways.

Fragment

Crank provides a Fragment tag, which allows you to render multiple children into a parent without wrapping them in another DOM node. Under the hood, iterables which appear in the element tree are also implicitly wrapped in a Fragment element by the renderer.

console.log(document.body.innerHTML);
// "<div>Sibling 1</div><div>Sibling 2</div>"

Copy

It‘s often fine to rerender Crank components, because elements are diffed, persistent between renders, and unnecessary mutations usually avoided. However, you might want to prevent a child from updating when the parent rerenders, perhaps because a certain prop hasn’t changed, because you want to batch updates from the parent, or as a performance optimization. To do this, you can use the Copy tag to indicate to Crank that you don’t want to update a previously rendered element in that same position.

  };
}

In this example, memo is a higher-order component, a function which takes a component and returns a component. This wrapper component compares old and new props and yields a Copy element if every prop is shallowly equal. A Copy element can appear anywhere in an element tree to prevent rerenderings, and the only props Copy elements take are the crank-key and crank-ref props, which work as expected.

Portal

Sometimes you may want to render into a DOM node which isn’t the current parent element, or even a part of the currently rendered DOM tree. You can do this with the Portal tag, passing in a DOM node as its root prop. The Portal’s children will be rendered into the specified root element, just as if Renderer.render was called with the root value as its second argument.

console.log(root2.innerHTML);
// "<div>This div is rendered into root2</div>"

This tag is useful for creating modals or tooltips, which usually need to be rendered into separate DOM elements at the bottom of the page for visibility reasons. Events dispatched from a Portal element‘s child components via the dispatchEvent method will still bubble into parent components.

Raw

Sometimes, you may want to insert raw HTML or actual DOM nodes directly into the element tree. Crank allows you to do this with the Raw element. The Raw element takes a value prop which is interpreted by the renderer. For the DOM renderer, if value is an HTML string, the renderer will parse and insert the resulting DOM nodes. If the value is already a DOM node, Crank will insert them in place.

  );
}

Be careful when using Raw elements, as passing unsanitized text inputs can lead to security vulnerabilities.

Be careful when using Raw elements, as passing unsanitized text inputs can lead to security vulnerabilities.

\ No newline at end of file diff --git a/guides/working-with-typescript/index.html b/guides/working-with-typescript/index.html index 237c2eb4..cf3535f9 100644 --- a/guides/working-with-typescript/index.html +++ b/guides/working-with-typescript/index.html @@ -1,4 +1,4 @@ -Crank.js | Working with TypeScript

Typing Component Return Values

You’ll often want to add a return type to your components. Crank exports custom types to help you type the return types of components:

  }
}

Element is just the type returned by JSX expressions/createElement. As you can see, you still have to modify the return type of functions based on whether the function is async or a generator. You can also use the type Child which represents any valid value in an element tree.

  yield <div>Hello world</div>;
}

Anything assignable to Child can be part of the element tree, and almost anything can be assigned to Child.

Typing Props

You can type the props object passed to components. This allows JSX elements which use your component as a tag to be type-checked.

const el = <Greeting name="Brian" />; // compiles
const el1 = <Greeting name={1} />; // throws a type error

The children prop can be typed using the Children type provided by Crank. The Children type is a broad type which can be Child or arbitrarily nested iterables of Child. TypeScript doesn’t really provide a way to prevent functions from being used as the children prop, but such patterns are strongly discouraged. You should typically treat children as an opaque value only to be interpolated into JSX because its value can be almost anything.

  );
}

Typing Event Listeners

If you dispatch custom events, you may want parent event listeners to be typed with the event you bubbled automatically. To do so, you can use module augmentation to extend the EventMap interface from the global Crank module.

  );
}

When importing the DOM or HTML renderers, the EventMap will be extended with the GlobalEventHandlersMap interface.

Typing Provisions

By default, calls to the context’s provide and consume methods will be loosely typed. If you want stricter typings of these methods, you can use module augmentation to extend the ProvisionMap interface from the global Crank module.

  return <p>{greeting}, {name}</p>;
}
\ No newline at end of file diff --git a/guides/xx-performance/index.html b/guides/xx-performance/index.html index da7755cb..2e582bc5 100644 --- a/guides/xx-performance/index.html +++ b/guides/xx-performance/index.html @@ -1,4 +1,4 @@ -Crank.js | \ No newline at end of file diff --git a/index.html b/index.html index 4698748b..8d2607c8 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ -Crank.js

Async generator functions let you write components that are both async and stateful. Crank uses promises wherever it makes sense, and has a rich async execution model which allows you to do things like race components to display loading states.

Async generator functions let you write components that are both async and stateful. Crank uses promises wherever it makes sense, and has a rich async execution model which allows you to do things like racing components to display loading states.


\ No newline at end of file
diff --git a/playground/index.html b/playground/index.html
index 4e25bae3..9dd95af0 100644
--- a/playground/index.html
+++ b/playground/index.html
@@ -1,4 +1,4 @@
-Crank.js
\ No newline at end of file
diff --git a/static/client-Q5HR47TJ.css b/static/client-Q5HR47TJ.css
new file mode 100644
index 00000000..7e6cd1bd
--- /dev/null
+++ b/static/client-Q5HR47TJ.css
@@ -0,0 +1,552 @@
+/* node_modules/normalize.css/normalize.css */
+html {
+  line-height: 1.15;
+  -webkit-text-size-adjust: 100%;
+}
+body {
+  margin: 0;
+}
+main {
+  display: block;
+}
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+hr {
+  box-sizing: content-box;
+  height: 0;
+  overflow: visible;
+}
+pre {
+  font-family: monospace, monospace;
+  font-size: 1em;
+}
+a {
+  background-color: transparent;
+}
+abbr[title] {
+  border-bottom: none;
+  -webkit-text-decoration: underline;
+  text-decoration: underline;
+  text-decoration: underline;
+  -webkit-text-decoration: underline dotted;
+  text-decoration: underline dotted;
+}
+b,
+strong {
+  font-weight: bolder;
+}
+code,
+kbd,
+samp {
+  font-family: monospace, monospace;
+  font-size: 1em;
+}
+small {
+  font-size: 80%;
+}
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+sub {
+  bottom: -0.25em;
+}
+sup {
+  top: -0.5em;
+}
+img {
+  border-style: none;
+}
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit;
+  font-size: 100%;
+  line-height: 1.15;
+  margin: 0;
+}
+button,
+input {
+  overflow: visible;
+}
+button,
+select {
+  text-transform: none;
+}
+button,
+[type=button],
+[type=reset],
+[type=submit] {
+  -webkit-appearance: button;
+}
+button::-moz-focus-inner,
+[type=button]::-moz-focus-inner,
+[type=reset]::-moz-focus-inner,
+[type=submit]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+button:-moz-focusring,
+[type=button]:-moz-focusring,
+[type=reset]:-moz-focusring,
+[type=submit]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+fieldset {
+  padding: 0.35em 0.75em 0.625em;
+}
+legend {
+  box-sizing: border-box;
+  color: inherit;
+  display: table;
+  max-width: 100%;
+  padding: 0;
+  white-space: normal;
+}
+progress {
+  vertical-align: baseline;
+}
+textarea {
+  overflow: auto;
+}
+[type=checkbox],
+[type=radio] {
+  box-sizing: border-box;
+  padding: 0;
+}
+[type=number]::-webkit-inner-spin-button,
+[type=number]::-webkit-outer-spin-button {
+  height: auto;
+}
+[type=search] {
+  -webkit-appearance: textfield;
+  outline-offset: -2px;
+}
+[type=search]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+::-webkit-file-upload-button {
+  -webkit-appearance: button;
+  font: inherit;
+}
+details {
+  display: block;
+}
+summary {
+  display: list-item;
+}
+template {
+  display: none;
+}
+[hidden] {
+  display: none;
+}
+
+/* src/styles/prism-theme.css */
+:root {
+  --coldark00: #111b27;
+  --coldark01: #213043;
+  --coldark02: #3c526d;
+  --coldark03: #8da1b9;
+  --coldark04: #d0dae7;
+  --coldark05: #e3eaf2;
+  --coldark06: #f0f4f8;
+  --coldark07: #0b121b;
+  --coldark08: #66cccc;
+  --coldark09: #e6d37a;
+  --coldark10: #6cb8e6;
+  --coldark11: #91d076;
+  --coldark12: #f4adf4;
+  --coldark13: #c699e3;
+  --coldark14: #e9ae7e;
+  --coldark15: #cd6660;
+}
+.color-scheme-light {
+  --coldark00: #e3eaf2;
+  --coldark01: #d0dae7;
+  --coldark02: #8da1b9;
+  --coldark03: #3c526d;
+  --coldark04: #213043;
+  --coldark05: #111b27;
+  --coldark06: #0b121b;
+  --coldark07: #f0f4f8;
+  --coldark08: #006d6d;
+  --coldark09: #755f00;
+  --coldark10: #005a8e;
+  --coldark11: #116b00;
+  --coldark12: #af00af;
+  --coldark13: #7c00aa;
+  --coldark14: #a04900;
+  --coldark15: #c22f2e;
+}
+code[class*=language-],
+pre[class*=language-] {
+  color: #e3eaf2;
+  color: var(--coldark05);
+  background: none;
+  font-family:
+    Consolas,
+    Monaco,
+    "Andale Mono",
+    "Ubuntu Mono",
+    monospace;
+  text-align: left;
+  white-space: pre;
+  -moz-tab-size: 2;
+  -o-tab-size: 2;
+  tab-size: 2;
+  -webkit-hyphens: none;
+  hyphens: none;
+}
+pre[class*=language-]::-moz-selection,
+pre[class*=language-] ::-moz-selection,
+code[class*=language-]::-moz-selection,
+code[class*=language-] ::-moz-selection {
+  background: #3c526d;
+  background: var(--coldark02);
+}
+pre[class*=language-]::-moz-selection,
+pre[class*=language-] ::-moz-selection,
+code[class*=language-]::-moz-selection,
+code[class*=language-] ::-moz-selection {
+  background: #3c526d;
+  background: var(--coldark02);
+}
+pre[class*=language-]::selection,
+pre[class*=language-] ::selection,
+code[class*=language-]::selection,
+code[class*=language-] ::selection {
+  background: #3c526d;
+  background: var(--coldark02);
+}
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+  color: #8da1b9;
+  color: var(--coldark03);
+}
+.token.punctuation {
+  color: #e3eaf2;
+  color: var(--coldark05);
+}
+.token.delimiter.important,
+.token.selector .parent,
+.token.tag,
+.token.tag .token.punctuation {
+  color: #66cccc;
+  color: var(--coldark08);
+}
+.token.attr-name,
+.token.boolean,
+.token.boolean.important,
+.token.number,
+.token.constant,
+.token.selector .token.attribute {
+  color: #e6d37a;
+  color: var(--coldark09);
+}
+.token.class-name,
+.token.key,
+.token.parameter,
+.token.property,
+.token.property-access,
+.token.variable {
+  color: #6cb8e6;
+  color: var(--coldark10);
+}
+.token.attr-value,
+.token.inserted,
+.token.color,
+.token.selector .token.value,
+.token.string,
+.token.string .token.url-link {
+  color: #91d076;
+  color: var(--coldark11);
+}
+.token.builtin,
+.token.keyword-array,
+.token.package,
+.token.regex {
+  color: #f4adf4;
+  color: var(--coldark12);
+}
+.token.function,
+.token.selector .token.class,
+.token.selector .token.id {
+  color: #c699e3;
+  color: var(--coldark13);
+}
+.token.atrule .token.rule,
+.token.combinator,
+.token.keyword,
+.token.operator,
+.token.pseudo-class,
+.token.pseudo-element,
+.token.selector,
+.token.unit {
+  color: #e9ae7e;
+  color: var(--coldark14);
+}
+.token.deleted,
+.token.important {
+  color: #cd6660;
+  color: var(--coldark15);
+}
+.token.keyword-this,
+.token.this {
+  color: #6cb8e6;
+  color: var(--coldark10);
+}
+.token.important,
+.token.keyword-this,
+.token.this,
+.token.bold {
+  font-weight: bold;
+}
+.token.delimiter.important {
+  font-weight: inherit;
+}
+.token.italic {
+  font-style: italic;
+}
+.token.entity {
+  cursor: help;
+}
+.language-markdown .token.title,
+.language-markdown .token.title .token.punctuation {
+  color: #6cb8e6;
+  color: var(--coldark10);
+  font-weight: bold;
+}
+.language-markdown .token.blockquote.punctuation {
+  color: #f4adf4;
+  color: var(--coldark12);
+}
+.language-markdown .token.code {
+  color: #66cccc;
+  color: var(--coldark08);
+}
+.language-markdown .token.hr.punctuation {
+  color: #6cb8e6;
+  color: var(--coldark10);
+}
+.language-markdown .token.url .token.content {
+  color: #91d076;
+  color: var(--coldark11);
+}
+.language-markdown .token.url-link {
+  color: #e6d37a;
+  color: var(--coldark09);
+}
+.language-markdown .token.list.punctuation {
+  color: #f4adf4;
+  color: var(--coldark12);
+}
+.language-markdown .token.table-header {
+  color: #e3eaf2;
+  color: var(--coldark05);
+}
+.language-json .token.operator {
+  color: #e3eaf2;
+  color: var(--coldark05);
+}
+.language-scss .token.variable {
+  color: #66cccc;
+  color: var(--coldark08);
+}
+
+/* src/styles/client.css */
+:root {
+  --bg-color: #0a0e1f;
+  --text-color: #f5f9ff;
+  --highlight-color: #dbb368;
+}
+.color-scheme-light {
+  --bg-color: #e7f4f5;
+  --text-color: #0a0e1f;
+}
+@supports ((-webkit-backdrop-filter: blur(4px)) or (backdrop-filter: blur(4px))) {
+  .blur-background {
+    background-color: rgba(10, 14, 31, 0.5) !important;
+  }
+  .blur-background {
+    -webkit-backdrop-filter: blur(4px);
+    backdrop-filter: blur(4px);
+  }
+}
+@supports ((-webkit-backdrop-filter: blur(4px)) or (backdrop-filter: blur(4px))) {
+  .color-scheme-light .blur-background {
+    background-color: rgba(231, 244, 245, 0.5) !important;
+  }
+  .color-scheme-light .blur-background {
+    -webkit-backdrop-filter: blur(4px);
+    backdrop-filter: blur(4px);
+  }
+}
+@supports ((-webkit-backdrop-filter: blur(8px)) or (backdrop-filter: blur(8px))) {
+  .blur-background-2 {
+    background-color: rgba(10, 14, 31, 0.5) !important;
+  }
+  .blur-background-2 {
+    -webkit-backdrop-filter: blur(8px);
+    backdrop-filter: blur(8px);
+  }
+}
+@supports ((-webkit-backdrop-filter: blur(8px)) or (backdrop-filter: blur(8px))) {
+  .color-scheme-light .blur-background-2 {
+    background-color: rgba(231, 244, 245, 0.5) !important;
+  }
+  .color-scheme-light .blur-background-2 {
+    -webkit-backdrop-filter: blur(8px);
+    backdrop-filter: blur(8px);
+  }
+}
+* {
+  color: #f5f9ff;
+  color: var(--text-color);
+  box-sizing: border-box;
+}
+::-moz-selection {
+  background: dodgerblue;
+}
+::selection {
+  background: dodgerblue;
+}
+html {
+  scroll-behavior: smooth;
+}
+body {
+  background-color: #0a0e1f;
+  background-color: var(--bg-color);
+  color: #f5f9ff;
+  color: var(--text-color);
+  font-family: sans-serif;
+}
+p {
+  line-height: 1.4;
+  margin: 1.5em 0;
+}
+img {
+  display: block;
+  margin: 0 auto;
+  background: #e1e4ea;
+  max-width: 100%;
+}
+@media screen and (min-width: 1100px) {
+  img {
+    max-width: 900px;
+  }
+}
+button {
+  background-color: transparent;
+  border-radius: 0;
+  border: 1px solid currentcolor;
+  padding: 0.3em;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  padding: 0;
+  margin: 0 0 1.4em;
+}
+h1 a,
+h2 a,
+h3 a,
+h4 a,
+h5 a,
+h6 a {
+  color: inherit;
+  -webkit-text-decoration: none;
+  text-decoration: none;
+}
+h1,
+h2,
+h3 {
+  color: #dbb368;
+  color: var(--highlight-color);
+}
+blockquote {
+  border-left: 2px solid #dbb368;
+  border-left: 2px solid var(--highlight-color);
+  margin: 1.5em 0;
+  padding: 0 0 0 .8em;
+}
+blockquote p {
+  margin: 0;
+}
+a {
+  color: inherit;
+}
+a[aria-current] {
+  color: #dbb368;
+  color: var(--highlight-color);
+}
+li {
+  margin: 0 0 1em;
+}
+pre {
+  margin: 0;
+  padding: 5px;
+}
+@media screen and (min-width: 800px) {
+  pre {
+    padding: 1em;
+  }
+}
+pre {
+  font-size: 14px;
+  color: #f5f9ff;
+  color: var(--text-color);
+  line-height: 1.4;
+  -moz-tab-size: 2;
+  -o-tab-size: 2;
+  tab-size: 2;
+  outline: none;
+}
+code {
+  padding: .1em .2em;
+  margin: 0;
+  font-size: .9em;
+  background: var(--coldark01);
+}
+pre code {
+  padding: 0;
+  margin: 0;
+  font-size: 14px;
+  background: none;
+}
+select,
+input,
+textarea {
+  background-color: #0a0e1f;
+  background-color: var(--bg-color);
+  color: #f5f9ff;
+  color: var(--text-color);
+  border: 1px solid #f5f9ff;
+  border: 1px solid var(--text-color);
+}
+/**
+ * Adapted from Coldark Theme for Prism.js to support light/dark mode
+ *
+ * Tested with HTML, CSS, JS, JSON, PHP, YAML, Bash script
+ * @author Armand Philippot 
+ * @homepage https://github.com/ArmandPhilippot/coldark-prism
+ * @license MIT
+ */
+/*! Bundled license information:
+
+normalize.css/normalize.css:
+  (*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css *)
+*/
+/*# sourceMappingURL=client-Q5HR47TJ.css.map */
diff --git a/static/client-Q5HR47TJ.css.map b/static/client-Q5HR47TJ.css.map
new file mode 100644
index 00000000..2b53d47a
--- /dev/null
+++ b/static/client-Q5HR47TJ.css.map
@@ -0,0 +1,7 @@
+{
+  "version": 3,
+  "sources": ["../node_modules/normalize.css/node_modules/normalize.css/normalize.css", "styles/src/styles/prism-theme.css", "styles/src/styles/client.css", "styles/"],
+  "sourcesContent": ["/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */\n\n/* Document\n   ========================================================================== */\n\n/**\n * 1. Correct the line height in all browsers.\n * 2. Prevent adjustments of font size after orientation changes in iOS.\n */\n\nhtml {\n  line-height: 1.15; /* 1 */\n  -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/* Sections\n   ========================================================================== */\n\n/**\n * Remove the margin in all browsers.\n */\n\nbody {\n  margin: 0;\n}\n\n/**\n * Render the `main` element consistently in IE.\n */\n\nmain {\n  display: block;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\n\n/* Grouping content\n   ========================================================================== */\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n  box-sizing: content-box; /* 1 */\n  height: 0; /* 1 */\n  overflow: visible; /* 2 */\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\npre {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/* Text-level semantics\n   ========================================================================== */\n\n/**\n * Remove the gray background on active links in IE 10.\n */\n\na {\n  background-color: transparent;\n}\n\n/**\n * 1. Remove the bottom border in Chrome 57-\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n  border-bottom: none; /* 1 */\n  text-decoration: underline; /* 2 */\n  text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n  font-weight: bolder;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\nsamp {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n  font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nsup {\n  top: -0.5em;\n}\n\n/* Embedded content\n   ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10.\n */\n\nimg {\n  border-style: none;\n}\n\n/* Forms\n   ========================================================================== */\n\n/**\n * 1. Change the font styles in all browsers.\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  font-family: inherit; /* 1 */\n  font-size: 100%; /* 1 */\n  line-height: 1.15; /* 1 */\n  margin: 0; /* 2 */\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n  overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n  text-transform: none;\n}\n\n/**\n * Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n  -webkit-appearance: button;\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n  border-style: none;\n  padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n  outline: 1px dotted ButtonText;\n}\n\n/**\n * Correct the padding in Firefox.\n */\n\nfieldset {\n  padding: 0.35em 0.75em 0.625em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n *    `fieldset` elements in all browsers.\n */\n\nlegend {\n  box-sizing: border-box; /* 1 */\n  color: inherit; /* 2 */\n  display: table; /* 1 */\n  max-width: 100%; /* 1 */\n  padding: 0; /* 3 */\n  white-space: normal; /* 1 */\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n  vertical-align: baseline;\n}\n\n/**\n * Remove the default vertical scrollbar in IE 10+.\n */\n\ntextarea {\n  overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10.\n * 2. Remove the padding in IE 10.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n  box-sizing: border-box; /* 1 */\n  padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n  -webkit-appearance: textfield; /* 1 */\n  outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding in Chrome and Safari on macOS.\n */\n\n[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n  -webkit-appearance: button; /* 1 */\n  font: inherit; /* 2 */\n}\n\n/* Interactive\n   ========================================================================== */\n\n/*\n * Add the correct display in Edge, IE 10+, and Firefox.\n */\n\ndetails {\n  display: block;\n}\n\n/*\n * Add the correct display in all browsers.\n */\n\nsummary {\n  display: list-item;\n}\n\n/* Misc\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 10+.\n */\n\ntemplate {\n  display: none;\n}\n\n/**\n * Add the correct display in IE 10.\n */\n\n[hidden] {\n  display: none;\n}\n", "/**\n * Adapted from Coldark Theme for Prism.js to support light/dark mode\n *\n * Tested with HTML, CSS, JS, JSON, PHP, YAML, Bash script\n * @author Armand Philippot \n * @homepage https://github.com/ArmandPhilippot/coldark-prism\n * @license MIT\n */\n:root {\n\t--coldark00: #111b27;\n\t--coldark01: #213043;\n\t--coldark02: #3c526d;\n\t--coldark03: #8da1b9;\n\t--coldark04: #d0dae7;\n\t--coldark05: #e3eaf2;\n\t--coldark06: #f0f4f8;\n\t--coldark07: #0b121b;\n\t--coldark08: #66cccc;\n\t--coldark09: #e6d37a;\n\t--coldark10: #6cb8e6;\n\t--coldark11: #91d076;\n\t--coldark12: #f4adf4;\n\t--coldark13: #c699e3;\n\t--coldark14: #e9ae7e;\n\t--coldark15: #cd6660;\n}\n\n.color-scheme-light {\n\t--coldark00: #e3eaf2;\n\t--coldark01: #d0dae7;\n\t--coldark02: #8da1b9;\n\t--coldark03: #3c526d;\n\t--coldark04: #213043;\n\t--coldark05: #111b27;\n\t--coldark06: #0b121b;\n\t--coldark07: #f0f4f8;\n\t--coldark08: #006d6d;\n\t--coldark09: #755f00;\n\t--coldark10: #005a8e;\n\t--coldark11: #116b00;\n\t--coldark12: #af00af;\n\t--coldark13: #7c00aa;\n\t--coldark14: #a04900;\n\t--coldark15: #c22f2e;\n}\n\ncode[class*=\"language-\"],\npre[class*=\"language-\"] {\n\tcolor: var(--coldark05);\n\tbackground: none;\n\tfont-family: Consolas, Monaco, \"Andale Mono\", \"Ubuntu Mono\", monospace;\n\ttext-align: left;\n\twhite-space: pre;\n\t-moz-tab-size: 2;\n\t-o-tab-size: 2;\n\ttab-size: 2;\n\t-webkit-hyphens: none;\n\t-moz-hyphens: none;\n\t-ms-hyphens: none;\n\thyphens: none;\n}\n\npre[class*=\"language-\"]::-moz-selection,\npre[class*=\"language-\"] ::-moz-selection,\ncode[class*=\"language-\"]::-moz-selection,\ncode[class*=\"language-\"] ::-moz-selection {\n\tbackground: var(--coldark02);\n}\n\npre[class*=\"language-\"]::selection,\npre[class*=\"language-\"] ::selection,\ncode[class*=\"language-\"]::selection,\ncode[class*=\"language-\"] ::selection {\n\tbackground: var(--coldark02);\n}\n\n/*\n:not(pre) > code[class*=\"language-\"],\npre[class*=\"language-\"] {\n\tbackground: var(--coldark00);\n}\n*/\n\n.token.comment,\n.token.prolog,\n.token.doctype,\n.token.cdata {\n\tcolor: var(--coldark03);\n}\n\n.token.punctuation {\n\tcolor: var(--coldark05);\n}\n\n.token.delimiter.important,\n.token.selector .parent,\n.token.tag,\n.token.tag .token.punctuation {\n\tcolor: var(--coldark08);\n}\n\n.token.attr-name,\n.token.boolean,\n.token.boolean.important,\n.token.number,\n.token.constant,\n.token.selector .token.attribute {\n\tcolor: var(--coldark09);\n}\n\n.token.class-name,\n.token.key,\n.token.parameter,\n.token.property,\n.token.property-access,\n.token.variable {\n\tcolor: var(--coldark10);\n}\n\n.token.attr-value,\n.token.inserted,\n.token.color,\n.token.selector .token.value,\n.token.string,\n.token.string .token.url-link {\n\tcolor: var(--coldark11);\n}\n\n.token.builtin,\n.token.keyword-array,\n.token.package,\n.token.regex {\n\tcolor: var(--coldark12);\n}\n\n.token.function,\n.token.selector .token.class,\n.token.selector .token.id {\n\tcolor: var(--coldark13);\n}\n\n.token.atrule .token.rule,\n.token.combinator,\n.token.keyword,\n.token.operator,\n.token.pseudo-class,\n.token.pseudo-element,\n.token.selector,\n.token.unit {\n\tcolor: var(--coldark14);\n}\n\n.token.deleted,\n.token.important {\n\tcolor: var(--coldark15);\n}\n\n.token.keyword-this,\n.token.this {\n\tcolor: var(--coldark10);\n}\n\n.token.important,\n.token.keyword-this,\n.token.this,\n.token.bold {\n\tfont-weight: bold;\n}\n\n.token.delimiter.important {\n\tfont-weight: inherit;\n}\n\n.token.italic {\n\tfont-style: italic;\n}\n\n.token.entity {\n\tcursor: help;\n}\n\n.language-markdown .token.title,\n.language-markdown .token.title .token.punctuation {\n\tcolor: var(--coldark10);\n\tfont-weight: bold;\n}\n\n.language-markdown .token.blockquote.punctuation {\n\tcolor: var(--coldark12);\n}\n\n.language-markdown .token.code {\n\tcolor: var(--coldark08);\n}\n\n.language-markdown .token.hr.punctuation {\n\tcolor: var(--coldark10);\n}\n\n.language-markdown .token.url .token.content {\n\tcolor: var(--coldark11);\n}\n\n.language-markdown .token.url-link {\n\tcolor: var(--coldark09);\n}\n\n.language-markdown .token.list.punctuation {\n\tcolor: var(--coldark12);\n}\n\n.language-markdown .token.table-header {\n\tcolor: var(--coldark05);\n}\n\n.language-json .token.operator {\n\tcolor: var(--coldark05);\n}\n\n.language-scss .token.variable {\n\tcolor: var(--coldark08);\n}\n", "@import \"normalize.css/normalize.css\";\n@import \"./prism-theme.css\";\n\n:root {\n\t--bg-color: #0a0e1f;\n\t--text-color: #f5f9ff;\n\t--highlight-color: #dbb368;\n}\n\n.color-scheme-light {\n\t--bg-color: #e7f4f5;\n\t--text-color: #0a0e1f;\n}\n\n.blur-background {\n\t@supports (backdrop-filter: blur(4px)) {\n\t\tbackdrop-filter: blur(4px);\n\t\tbackground-color: rgba(10, 14, 31, 0.5) !important;\n\t}\n}\n\n/* colors have to match background or it looks bad */\n.color-scheme-light {\n\t.blur-background {\n\t\t@supports (backdrop-filter: blur(4px)) {\n\t\t\tbackdrop-filter: blur(4px);\n\t\t\tbackground-color: rgba(231, 244, 245, 0.5) !important;\n\t\t}\n\t}\n}\n\n.blur-background-2 {\n\t@supports (backdrop-filter: blur(8px)) {\n\t\tbackdrop-filter: blur(8px);\n\t\tbackground-color: rgba(10, 14, 31, 0.5) !important;\n\t}\n}\n\n.color-scheme-light {\n\t.blur-background-2 {\n\t\t@supports (backdrop-filter: blur(8px)) {\n\t\t\tbackdrop-filter: blur(8px);\n\t\t\tbackground-color: rgba(231, 244, 245, 0.5) !important;\n\t\t}\n\t}\n}\n\n* {\n\tcolor: var(--text-color);\n\tbox-sizing: border-box;\n}\n\n::selection {\n\tbackground: dodgerblue;\n}\n\nhtml {\n\tscroll-behavior: smooth;\n}\n\nbody {\n\tbackground-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tfont-family: sans-serif;\n}\n\np {\n\tline-height: 1.4;\n\tmargin: 1.5em 0;\n}\n\nimg {\n\tdisplay: block;\n\tmargin: 0 auto;\n\tbackground: #e1e4ea;\n\tmax-width: 100%;\n\t@media screen and (min-width: 1100px) {\n\t\tmax-width: 900px;\n\t}\n}\n\nbutton {\n\tbackground-color: transparent;\n\tborder-radius: 0;\n\tborder: 1px solid currentcolor;\n\tpadding: 0.3em;\n}\n\nh1, h2, h3, h4, h5, h6 {\n\tpadding: 0;\n\tmargin: 0 0 1.4em;\n\ta {\n\t\tcolor: inherit;\n\t\ttext-decoration: none;\n\t}\n}\n\nh1, h2, h3 {\n\tcolor: var(--highlight-color);\n}\n\nblockquote {\n\tborder-left: 2px solid var(--highlight-color);\n\tmargin: 1.5em 0;\n\tpadding: 0 0 0 .8em;\n\n\tp {\n\t\tmargin: 0;\n\t}\n}\n\na {\n\tcolor: inherit;\n\n\t&[aria-current] {\n\t\tcolor: var(--highlight-color);\n\t}\n}\n\nli {\n\tmargin: 0 0 1em;\n}\n\npre {\n\tmargin: 0;\n\tpadding: 5px;\n\t@media screen and (min-width: 800px) {\n\t\tpadding: 1em;\n\t}\n\n\tfont-size: 14px;\n\tcolor: var(--text-color);\n\tline-height: 1.4;\n\ttab-size: 2;\n\toutline: none;\n}\n\ncode {\n\tpadding: .1em .2em;\n\tmargin: 0;\n\tfont-size: .9em;\n\tbackground: var(--coldark01);\n}\n\npre code {\n\tpadding: 0;\n\tmargin: 0;\n\tfont-size: 14px;\n\tbackground: none;\n}\n\nselect, input, textarea {\n  background-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tborder: 1px solid var(--text-color);\n}\n", null],
+  "mappings": ";AAUA;AACE;AACA;;AAUF;AACE;;AAOF;AACE;;AAQF;AACE;AACA;;AAWF;AACE;AACA;AACA;;AAQF;AACE;AACA;;AAUF;AACE;;AAQF;AACE;AACA;AAAA;AACA;AAAA;AAAA;;AAOF;;AAEE;;AAQF;;;AAGE;AACA;;AAOF;AACE;;AAQF;;AAEE;AACA;AACA;AACA;;AAGF;AACE;;AAGF;AACE;;AAUF;AACE;;AAWF;;;;;AAKE;AACA;AACA;AACA;;AAQF;;AAEE;;AAQF;;AAEE;;AAOF;;;;AAIE;;AAOF;;;;AAIE;AACA;;AAOF;;;;AAIE;;AAOF;AACE;;AAUF;AACE;AACA;AACA;AACA;AACA;AACA;;AAOF;AACE;;AAOF;AACE;;AAQF;;AAEE;AACA;;AAOF;;AAEE;;AAQF;AACE;AACA;;AAOF;AACE;;AAQF;AACE;AACA;;AAUF;AACE;;AAOF;AACE;;AAUF;AACE;;AAOF;AACE;;;;ACnVF;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGD;AACC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGD;;AAEC;AAAA;AACA;AACA;;;;;;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;;AAGD;;;;AAIC;AAAA;;AAGD;;;;AAIC;AAAA;;AAJD;;;;AAIC;AAAA;;AAUD;;;;AAIC;AAAA;;AAGD;AACC;AAAA;;AAGD;;;;AAIC;AAAA;;AAGD;;;;;;AAMC;AAAA;;AAGD;;;;;;AAMC;AAAA;;AAGD;;;;;;AAMC;AAAA;;AAGD;;;;AAIC;AAAA;;AAGD;;;AAGC;AAAA;;AAGD;;;;;;;;AAQC;AAAA;;AAGD;;AAEC;AAAA;;AAGD;;AAEC;AAAA;;AAGD;;;;AAIC;;AAGD;AACC;;AAGD;AACC;;AAGD;AACC;;AAGD;;AAEC;AAAA;AACA;;AAGD;AACC;AAAA;;AAGD;AACC;AAAA;;AAGD;AACC;AAAA;;AAGD;AACC;AAAA;;AAGD;AACC;AAAA;;AAGD;AACC;AAAA;;AAGD;AACC;AAAA;;AAGD;AACC;AAAA;;AAGD;AACC;AAAA;;;;ACzND;AACC;AACA;AACA;;AAGD;AACC;AACA;;AAIA;AADD;AAGE;;AAHF;AAEE;AAAA;;;AAQA;AADD;AAGE;;AAHF;AAEE;AAAA;;;AAOF;AADD;AAGE;;AAHF;AAEE;AAAA;;;AAOA;AADD;AAGE;;AAHF;AAEE;AAAA;;;AAMH;AACC;AAAA;AACA;;AAGD;AACC;;AADD;AACC;;AAGD;AACC;;AAGD;AACC;AAAA;AACA;AAAA;AACA;;AAGD;AACC;AACA;;AAGD;AACC;AACA;AACA;AACA;;AACA;AALD;AAME;;;AAIF;AACC;AACA;AACA;AACA;;AAGD;;;;;;AACC;AACA;;AACA;;;;;;AACC;AACA;AAAA;;AAIF;;;AACC;AAAA;;AAGD;AACC;AAAA;AACA;AACA;;AAEA;AACC;;AAIF;AACC;;AAEA;AACC;AAAA;;AAIF;AACC;;AAGD;AACC;AACA;;AACA;AAHD;AAIE;;;AC/HF;ADkIC;AACA;AAAA;AACA;AACA;AAAA;AAAA;AACA;;AAGD;AACC;AACA;AACA;AACA;;AAGD;AACC;AACA;AACA;AACA;;AAGD;;;AACE;AAAA;AACD;AAAA;AACA;AAAA;;",
+  "names": []
+}