aboutsummaryrefslogtreecommitdiff
path: root/preact/demo
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-08-23 16:46:06 -0300
committerSebastian <sebasjm@gmail.com>2021-08-23 16:48:30 -0300
commit38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch)
tree453dbf70000cc5e338b06201af1eaca8343f8f73 /preact/demo
parentf26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff)
downloadnode-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.tar.gz
node-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.tar.bz2
node-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.zip
added web vendorsHEADmaster
Diffstat (limited to 'preact/demo')
-rw-r--r--preact/demo/contenteditable.js25
-rw-r--r--preact/demo/context.js69
-rw-r--r--preact/demo/devtools.js42
-rw-r--r--preact/demo/fragments.js26
-rw-r--r--preact/demo/index.js187
-rw-r--r--preact/demo/key_bug.js32
-rw-r--r--preact/demo/list.js63
-rw-r--r--preact/demo/logger.js170
-rw-r--r--preact/demo/mobx.js75
-rw-r--r--preact/demo/nested-suspense/addnewcomponent.js5
-rw-r--r--preact/demo/nested-suspense/component-container.js17
-rw-r--r--preact/demo/nested-suspense/dropzone.js5
-rw-r--r--preact/demo/nested-suspense/editor.js5
-rw-r--r--preact/demo/nested-suspense/index.js69
-rw-r--r--preact/demo/nested-suspense/subcomponent.js5
-rw-r--r--preact/demo/old.js.bak103
-rw-r--r--preact/demo/package.json44
-rw-r--r--preact/demo/people/Readme.md3
-rw-r--r--preact/demo/people/index.tsx59
-rw-r--r--preact/demo/people/profile.tsx59
-rw-r--r--preact/demo/people/router.tsx153
-rw-r--r--preact/demo/people/store.ts83
-rw-r--r--preact/demo/people/styles/animations.scss34
-rw-r--r--preact/demo/people/styles/app.scss100
-rw-r--r--preact/demo/people/styles/avatar.scss16
-rw-r--r--preact/demo/people/styles/button.scss115
-rw-r--r--preact/demo/people/styles/index.scss168
-rw-r--r--preact/demo/people/styles/profile.scss26
-rw-r--r--preact/demo/preact.js39
-rw-r--r--preact/demo/profiler.js88
-rw-r--r--preact/demo/pythagoras/index.js86
-rw-r--r--preact/demo/pythagoras/pythagoras.js97
-rw-r--r--preact/demo/redux.js48
-rw-r--r--preact/demo/reduxUpdate.js61
-rw-r--r--preact/demo/reorder.js104
-rw-r--r--preact/demo/spiral.js140
-rw-r--r--preact/demo/stateOrderBug.js80
-rw-r--r--preact/demo/style.css24
-rw-r--r--preact/demo/style.scss149
-rw-r--r--preact/demo/styled-components.js31
-rw-r--r--preact/demo/suspense-router/bye.js12
-rw-r--r--preact/demo/suspense-router/hello.js12
-rw-r--r--preact/demo/suspense-router/index.js30
-rw-r--r--preact/demo/suspense-router/simple-router.js87
-rw-r--r--preact/demo/suspense.js97
-rw-r--r--preact/demo/textFields.js37
-rw-r--r--preact/demo/todo.js47
-rw-r--r--preact/demo/tsconfig.json15
-rw-r--r--preact/demo/webpack.config.js112
49 files changed, 3154 insertions, 0 deletions
diff --git a/preact/demo/contenteditable.js b/preact/demo/contenteditable.js
new file mode 100644
index 0000000..4e913ba
--- /dev/null
+++ b/preact/demo/contenteditable.js
@@ -0,0 +1,25 @@
+import { createElement } from 'preact';
+import { useState } from 'preact/hooks';
+
+export default function Contenteditable() {
+ const [value, setValue] = useState("Hey there<br />I'm editable!");
+
+ return (
+ <div>
+ <div>
+ <button onClick={() => setValue('')}>Clear!</button>
+ </div>
+ <div
+ style={{
+ border: '1px solid gray',
+ padding: '8px',
+ margin: '8px 0',
+ background: 'white'
+ }}
+ contentEditable
+ onInput={e => setValue(e.currentTarget.innerHTML)}
+ dangerouslySetInnerHTML={{ __html: value }}
+ />
+ </div>
+ );
+}
diff --git a/preact/demo/context.js b/preact/demo/context.js
new file mode 100644
index 0000000..3fe3bc0
--- /dev/null
+++ b/preact/demo/context.js
@@ -0,0 +1,69 @@
+// eslint-disable-next-line no-unused-vars
+import { createElement, Component, createContext, Fragment } from 'preact';
+const { Provider, Consumer } = createContext();
+
+class ThemeProvider extends Component {
+ state = {
+ value: this.props.value
+ };
+
+ onClick = () => {
+ this.setState(prev => ({
+ value:
+ prev.value === this.props.value ? this.props.next : this.props.value
+ }));
+ };
+
+ render() {
+ return (
+ <div>
+ <button onClick={this.onClick}>Toggle</button>
+ <Provider value={this.state.value}>{this.props.children}</Provider>
+ </div>
+ );
+ }
+}
+
+class Child extends Component {
+ shouldComponentUpdate() {
+ return false;
+ }
+
+ render() {
+ return (
+ <>
+ <p>(blocked update)</p>
+ {this.props.children}
+ </>
+ );
+ }
+}
+
+export default class ContextDemo extends Component {
+ render() {
+ return (
+ <ThemeProvider value="blue" next="red">
+ <Child>
+ <Consumer>
+ {data => (
+ <div>
+ <p>
+ current theme: <b>{data}</b>
+ </p>
+ <ThemeProvider value="black" next="white">
+ <Consumer>
+ {data => (
+ <p>
+ current sub theme: <b>{data}</b>
+ </p>
+ )}
+ </Consumer>
+ </ThemeProvider>
+ </div>
+ )}
+ </Consumer>
+ </Child>
+ </ThemeProvider>
+ );
+ }
+}
diff --git a/preact/demo/devtools.js b/preact/demo/devtools.js
new file mode 100644
index 0000000..0484582
--- /dev/null
+++ b/preact/demo/devtools.js
@@ -0,0 +1,42 @@
+// eslint-disable-next-line no-unused-vars
+import {
+ createElement,
+ Component,
+ memo,
+ Fragment,
+ Suspense,
+ lazy
+} from 'react';
+
+function Foo() {
+ return <div>I'm memoed</div>;
+}
+
+function LazyComp() {
+ return <div>I'm (fake) lazy loaded</div>;
+}
+
+const Lazy = lazy(() => Promise.resolve({ default: LazyComp }));
+
+const Memoed = memo(Foo);
+
+export default class DevtoolsDemo extends Component {
+ render() {
+ return (
+ <div>
+ <h1>memo()</h1>
+ <p>
+ <b>functional component:</b>
+ </p>
+ <Memoed />
+ <h1>lazy()</h1>
+ <p>
+ <b>functional component:</b>
+ </p>
+ <Suspense fallback={<div>Loading (fake) lazy loaded component...</div>}>
+ <Lazy />
+ </Suspense>
+ </div>
+ );
+ }
+}
diff --git a/preact/demo/fragments.js b/preact/demo/fragments.js
new file mode 100644
index 0000000..b713860
--- /dev/null
+++ b/preact/demo/fragments.js
@@ -0,0 +1,26 @@
+import { createElement, Component, Fragment } from 'preact';
+
+export default class extends Component {
+ state = { number: 0 };
+
+ componentDidMount() {
+ setInterval(_ => this.updateChildren(), 1000);
+ }
+
+ updateChildren() {
+ this.setState(state => ({ number: state.number + 1 }));
+ }
+
+ render(props, state) {
+ return (
+ <div>
+ <div>{state.number}</div>
+ <>
+ <div>one</div>
+ <div>{state.number}</div>
+ <div>three</div>
+ </>
+ </div>
+ );
+ }
+}
diff --git a/preact/demo/index.js b/preact/demo/index.js
new file mode 100644
index 0000000..72c58ee
--- /dev/null
+++ b/preact/demo/index.js
@@ -0,0 +1,187 @@
+import { createElement, render, Component, Fragment } from 'preact';
+// import renderToString from 'preact-render-to-string';
+import './style.scss';
+import { Router, Link } from 'preact-router';
+import Pythagoras from './pythagoras';
+import Spiral from './spiral';
+import Reorder from './reorder';
+import Todo from './todo';
+import Fragments from './fragments';
+import Context from './context';
+import installLogger from './logger';
+import ProfilerDemo from './profiler';
+import KeyBug from './key_bug';
+import StateOrderBug from './stateOrderBug';
+import PeopleBrowser from './people';
+import StyledComp from './styled-components';
+import { initDevTools } from 'preact/devtools/src/devtools';
+import { initDebug } from 'preact/debug/src/debug';
+import DevtoolsDemo from './devtools';
+import SuspenseDemo from './suspense';
+import Redux from './redux';
+import TextFields from './textFields';
+import ReduxBug from './reduxUpdate';
+import SuspenseRouterBug from './suspense-router';
+import NestedSuspenseBug from './nested-suspense';
+import Contenteditable from './contenteditable';
+import { MobXDemo } from './mobx';
+
+let isBenchmark = /(\/spiral|\/pythagoras|[#&]bench)/g.test(
+ window.location.href
+);
+if (!isBenchmark) {
+ // eslint-disable-next-line no-console
+ console.log('Enabling devtools and debug');
+ initDevTools();
+ initDebug();
+}
+
+// mobx-state-tree fix
+window.setImmediate = setTimeout;
+
+class Home extends Component {
+ render() {
+ return (
+ <div>
+ <h1>Hello</h1>
+ </div>
+ );
+ }
+}
+
+class DevtoolsWarning extends Component {
+ onClick = () => {
+ window.location.reload();
+ };
+
+ render() {
+ return (
+ <button onClick={this.onClick}>
+ Start Benchmark (disables devtools)
+ </button>
+ );
+ }
+}
+
+class App extends Component {
+ render({ url }) {
+ return (
+ <div class="app">
+ <header>
+ <nav>
+ <Link href="/" activeClassName="active">
+ Home
+ </Link>
+ <Link href="/reorder" activeClassName="active">
+ Reorder
+ </Link>
+ <Link href="/spiral" activeClassName="active">
+ Spiral
+ </Link>
+ <Link href="/pythagoras" activeClassName="active">
+ Pythagoras
+ </Link>
+ <Link href="/todo" activeClassName="active">
+ ToDo
+ </Link>
+ <Link href="/fragments" activeClassName="active">
+ Fragments
+ </Link>
+ <Link href="/key_bug" activeClassName="active">
+ Key Bug
+ </Link>
+ <Link href="/profiler" activeClassName="active">
+ Profiler
+ </Link>
+ <Link href="/context" activeClassName="active">
+ Context
+ </Link>
+ <Link href="/devtools" activeClassName="active">
+ Devtools
+ </Link>
+ <Link href="/empty-fragment" activeClassName="active">
+ Empty Fragment
+ </Link>
+ <Link href="/people" activeClassName="active">
+ People Browser
+ </Link>
+ <Link href="/state-order" activeClassName="active">
+ State Order
+ </Link>
+ <Link href="/styled-components" activeClassName="active">
+ Styled Components
+ </Link>
+ <Link href="/redux" activeClassName="active">
+ Redux
+ </Link>
+ <Link href="/mobx" activeClassName="active">
+ MobX
+ </Link>
+ <Link href="/suspense" activeClassName="active">
+ Suspense / lazy
+ </Link>
+ <Link href="/textfields" activeClassName="active">
+ Textfields
+ </Link>
+ <Link href="/reduxBug/1" activeClassName="active">
+ Redux Bug
+ </Link>
+ <Link href="/suspense-router" activeClassName="active">
+ Suspense Router Bug
+ </Link>
+ <Link href="/nested-suspense" activeClassName="active">
+ Nested Suspense Bug
+ </Link>
+ <Link href="/contenteditable" activeClassName="active">
+ contenteditable
+ </Link>
+ </nav>
+ </header>
+ <main>
+ <Router url={url}>
+ <Home path="/" />
+ <StateOrderBug path="/state-order" />
+ <Reorder path="/reorder" />
+ <div path="/spiral">
+ {!isBenchmark ? <DevtoolsWarning /> : <Spiral />}
+ </div>
+ <div path="/pythagoras">
+ {!isBenchmark ? <DevtoolsWarning /> : <Pythagoras />}
+ </div>
+ <Todo path="/todo" />
+ <Fragments path="/fragments" />
+ <ProfilerDemo path="/profiler" />
+ <KeyBug path="/key_bug" />
+ <Context path="/context" />
+ <DevtoolsDemo path="/devtools" />
+ <SuspenseDemo path="/suspense" />
+ <EmptyFragment path="/empty-fragment" />
+ <PeopleBrowser path="/people/:user?" />
+ <StyledComp path="/styled-components" />
+ <Redux path="/redux" />
+ <MobXDemo path="/mobx" />
+ <TextFields path="/textfields" />
+ <ReduxBug path="/reduxBug/:start" />
+ <SuspenseRouterBug path="/suspense-router" />
+ <NestedSuspenseBug path="/nested-suspense" />
+ <Contenteditable path="/contenteditable" />
+ </Router>
+ </main>
+ </div>
+ );
+ }
+}
+
+function EmptyFragment() {
+ return <Fragment />;
+}
+
+// document.body.innerHTML = renderToString(<App url={location.href.match(/[#&]ssr/) ? undefined : '/'} />);
+// document.body.firstChild.setAttribute('is-ssr', 'true');
+
+installLogger(
+ String(localStorage.LOG) === 'true' || location.href.match(/logger/),
+ String(localStorage.CONSOLE) === 'true' || location.href.match(/console/)
+);
+
+render(<App />, document.body);
diff --git a/preact/demo/key_bug.js b/preact/demo/key_bug.js
new file mode 100644
index 0000000..0ccb1a6
--- /dev/null
+++ b/preact/demo/key_bug.js
@@ -0,0 +1,32 @@
+import { createElement, Component } from 'preact';
+
+function Foo(props) {
+ return <div>This is: {props.children}</div>;
+}
+
+export default class KeyBug extends Component {
+ constructor() {
+ super();
+ this.onClick = this.onClick.bind(this);
+ this.state = { active: false };
+ }
+
+ onClick() {
+ this.setState(prev => ({ active: !prev.active }));
+ }
+
+ render() {
+ return (
+ <div>
+ {this.state.active && <Foo>foo</Foo>}
+ <h1>Hello World</h1>
+ <br />
+ <Foo>
+ bar <Foo>bar</Foo>
+ </Foo>
+ <br />
+ <button onClick={this.onClick}>Toggle</button>
+ </div>
+ );
+ }
+}
diff --git a/preact/demo/list.js b/preact/demo/list.js
new file mode 100644
index 0000000..2ef968c
--- /dev/null
+++ b/preact/demo/list.js
@@ -0,0 +1,63 @@
+import { h, render } from 'preact';
+import htm from 'htm';
+import './style.css';
+
+const html = htm.bind(h);
+const createRoot = parent => ({
+ render: v => render(v, parent)
+});
+
+function List({ items, renders, useKeys, useCounts, update }) {
+ const toggleKeys = () => update({ useKeys: !useKeys });
+ const toggleCounts = () => update({ useCounts: !useCounts });
+ const swap = () => {
+ const u = { items: items.slice() };
+ u.items[1] = items[8];
+ u.items[8] = items[1];
+ update(u);
+ };
+ return html`
+ <div>
+ <button onClick=${update}>Re-render</button>
+ <button onClick=${swap}>Swap 2 & 8</button>
+ <label>
+ <input type="checkbox" checked=${useKeys} onClick=${toggleKeys} />
+ Use Keys
+ </label>
+ <label>
+ <input type="checkbox" checked=${useCounts} onClick=${toggleCounts} />
+ Counts
+ </label>
+ <ul class="list">
+ ${items.map(
+ (item, i) => html`
+ <li
+ class=${i % 2 ? 'odd' : 'even'}
+ key=${useKeys ? item.name : undefined}
+ >
+ ${item.name} ${useCounts ? ` (${renders} renders)` : ''}
+ </li>
+ `
+ )}
+ </ul>
+ </div>
+ `;
+}
+
+const root = createRoot(document.body);
+
+let data = {
+ items: new Array(1000).fill(null).map((x, i) => ({ name: `Item ${i + 1}` })),
+ renders: 0,
+ useKeys: false,
+ useCounts: false
+};
+
+function update(partial) {
+ if (partial) Object.assign(data, partial);
+ data.renders++;
+ data.update = update;
+ root.render(List(data));
+}
+
+update();
diff --git a/preact/demo/logger.js b/preact/demo/logger.js
new file mode 100644
index 0000000..59df9b9
--- /dev/null
+++ b/preact/demo/logger.js
@@ -0,0 +1,170 @@
+export default function logger(logStats, logConsole) {
+ if (!logStats && !logConsole) {
+ return;
+ }
+
+ const consoleBuffer = new ConsoleBuffer();
+
+ let calls = {};
+ let lock = true;
+
+ function serialize(obj) {
+ if (obj instanceof Text) return '#text';
+ if (obj instanceof Element) return `<${obj.localName}>`;
+ if (obj === document) return 'document';
+ return Object.prototype.toString.call(obj).replace(/(^\[object |\]$)/g, '');
+ }
+
+ function count(key) {
+ if (lock === true) return;
+ calls[key] = (calls[key] || 0) + 1;
+
+ if (logConsole) {
+ consoleBuffer.log(key);
+ }
+ }
+
+ function logCall(obj, method, name) {
+ let old = obj[method];
+ obj[method] = function() {
+ let c = '';
+ for (let i = 0; i < arguments.length; i++) {
+ if (c) c += ', ';
+ c += serialize(arguments[i]);
+ }
+ count(`${serialize(this)}.${method}(${c})`);
+ return old.apply(this, arguments);
+ };
+ }
+
+ logCall(document, 'createElement');
+ logCall(document, 'createElementNS');
+ logCall(Element.prototype, 'remove');
+ logCall(Element.prototype, 'appendChild');
+ logCall(Element.prototype, 'removeChild');
+ logCall(Element.prototype, 'insertBefore');
+ logCall(Element.prototype, 'replaceChild');
+ logCall(Element.prototype, 'setAttribute');
+ logCall(Element.prototype, 'setAttributeNS');
+ logCall(Element.prototype, 'removeAttribute');
+ logCall(Element.prototype, 'removeAttributeNS');
+ let d =
+ Object.getOwnPropertyDescriptor(CharacterData.prototype, 'data') ||
+ Object.getOwnPropertyDescriptor(Node.prototype, 'data');
+ Object.defineProperty(Text.prototype, 'data', {
+ get() {
+ let value = d.get.call(this);
+ count(`get #text.data`);
+ return value;
+ },
+ set(v) {
+ count(`set #text.data`);
+ return d.set.call(this, v);
+ }
+ });
+
+ let root;
+ function setup() {
+ if (!logStats) return;
+
+ lock = true;
+ root = document.createElement('table');
+ root.style.cssText =
+ 'position: fixed; right: 0; top: 0; z-index:999; background: #000; font-size: 12px; color: #FFF; opacity: 0.9; white-space: nowrap;';
+ let header = document.createElement('thead');
+ header.innerHTML =
+ '<tr><td colspan="2">Stats <button id="clear-logs">clear</button></td></tr>';
+ root.tableBody = document.createElement('tbody');
+ root.appendChild(root.tableBody);
+ root.appendChild(header);
+ document.documentElement.appendChild(root);
+ let btn = document.getElementById('clear-logs');
+ btn.addEventListener('click', () => {
+ for (let key in calls) {
+ calls[key] = 0;
+ }
+ });
+ lock = false;
+ }
+
+ let rows = {};
+ function createRow(id) {
+ let row = document.createElement('tr');
+ row.key = document.createElement('td');
+ row.key.textContent = id;
+ row.appendChild(row.key);
+ row.value = document.createElement('td');
+ row.value.textContent = ' ';
+ row.appendChild(row.value);
+ root.tableBody.appendChild(row);
+ return (rows[id] = row);
+ }
+
+ function insertInto(parent) {
+ parent.appendChild(root);
+ }
+
+ function remove() {
+ clearInterval(updateTimer);
+ }
+
+ function update() {
+ if (!logStats) return;
+
+ lock = true;
+ for (let i in calls) {
+ if (calls.hasOwnProperty(i)) {
+ let row = rows[i] || createRow(i);
+ row.value.firstChild.nodeValue = calls[i];
+ }
+ }
+ lock = false;
+ }
+
+ let updateTimer = setInterval(update, 50);
+
+ setup();
+ lock = false;
+ return { insertInto, update, remove };
+}
+
+/**
+ * Logging to the console significantly affects performance.
+ * Buffer calls to console and replay them at the end of the
+ * current stack
+ * @extends {Console}
+ */
+class ConsoleBuffer {
+ constructor() {
+ /** @type {Array<[string, any[]]>} */
+ this.buffer = [];
+ this.deferred = null;
+
+ for (let methodName of Object.keys(console)) {
+ this[methodName] = this.proxy(methodName);
+ }
+ }
+
+ proxy(methodName) {
+ return (...args) => {
+ this.buffer.push([methodName, args]);
+ this.deferFlush();
+ };
+ }
+
+ deferFlush() {
+ if (this.deferred == null) {
+ this.deferred = Promise.resolve()
+ .then(() => this.flush())
+ .then(() => (this.deferred = null));
+ }
+ }
+
+ flush() {
+ let method;
+ while ((method = this.buffer.shift())) {
+ let [name, args] = method;
+ console[name](...args);
+ }
+ }
+}
diff --git a/preact/demo/mobx.js b/preact/demo/mobx.js
new file mode 100644
index 0000000..8e4159f
--- /dev/null
+++ b/preact/demo/mobx.js
@@ -0,0 +1,75 @@
+import React, { createElement, forwardRef, useRef, useState } from 'react';
+import { decorate, observable } from 'mobx';
+import { observer, useObserver } from 'mobx-react';
+import 'mobx-react-lite/batchingForReactDom';
+
+class Todo {
+ constructor() {
+ this.id = Math.random();
+ this.title = 'initial';
+ this.finished = false;
+ }
+}
+decorate(Todo, {
+ title: observable,
+ finished: observable
+});
+
+const Forward = observer(
+ // eslint-disable-next-line react/display-name
+ forwardRef(({ todo }, ref) => {
+ return (
+ <p ref={ref}>
+ Forward: "{todo.title}" {'' + todo.finished}
+ </p>
+ );
+ })
+);
+
+const todo = new Todo();
+
+const TodoView = observer(({ todo }) => {
+ return (
+ <p>
+ Todo View: "{todo.title}" {'' + todo.finished}
+ </p>
+ );
+});
+
+const HookView = ({ todo }) => {
+ return useObserver(() => {
+ return (
+ <p>
+ Todo View: "{todo.title}" {'' + todo.finished}
+ </p>
+ );
+ });
+};
+
+export function MobXDemo() {
+ const ref = useRef(null);
+ let [v, set] = useState(0);
+
+ const success = ref.current && ref.current.nodeName === 'P';
+
+ return (
+ <div>
+ <input
+ type="text"
+ placeholder="type here..."
+ onInput={e => {
+ todo.title = e.target.value;
+ set(v + 1);
+ }}
+ />
+ <p>
+ <b style={`color: ${success ? 'green' : 'red'}`}>
+ {success ? 'SUCCESS' : 'FAIL'}
+ </b>
+ </p>
+ <TodoView todo={todo} />
+ <Forward todo={todo} ref={ref} />
+ <HookView todo={todo} />
+ </div>
+ );
+}
diff --git a/preact/demo/nested-suspense/addnewcomponent.js b/preact/demo/nested-suspense/addnewcomponent.js
new file mode 100644
index 0000000..e3e4695
--- /dev/null
+++ b/preact/demo/nested-suspense/addnewcomponent.js
@@ -0,0 +1,5 @@
+import { createElement } from 'react';
+
+export default function AddNewComponent({ appearance }) {
+ return <div>AddNewComponent (component #{appearance})</div>;
+}
diff --git a/preact/demo/nested-suspense/component-container.js b/preact/demo/nested-suspense/component-container.js
new file mode 100644
index 0000000..b1da87b
--- /dev/null
+++ b/preact/demo/nested-suspense/component-container.js
@@ -0,0 +1,17 @@
+import { createElement, lazy } from 'react';
+
+const pause = timeout =>
+ new Promise(d => setTimeout(d, timeout), console.log(timeout));
+
+const SubComponent = lazy(() =>
+ pause(Math.random() * 1000).then(() => import('./subcomponent.js'))
+);
+
+export default function ComponentContainer({ appearance }) {
+ return (
+ <div>
+ GenerateComponents (component #{appearance})
+ <SubComponent />
+ </div>
+ );
+}
diff --git a/preact/demo/nested-suspense/dropzone.js b/preact/demo/nested-suspense/dropzone.js
new file mode 100644
index 0000000..c4a28b4
--- /dev/null
+++ b/preact/demo/nested-suspense/dropzone.js
@@ -0,0 +1,5 @@
+import { createElement } from 'react';
+
+export default function DropZone({ appearance }) {
+ return <div>DropZone (component #{appearance})</div>;
+}
diff --git a/preact/demo/nested-suspense/editor.js b/preact/demo/nested-suspense/editor.js
new file mode 100644
index 0000000..253d130
--- /dev/null
+++ b/preact/demo/nested-suspense/editor.js
@@ -0,0 +1,5 @@
+import { createElement } from 'react';
+
+export default function Editor({ children }) {
+ return <div className="Editor">{children}</div>;
+}
diff --git a/preact/demo/nested-suspense/index.js b/preact/demo/nested-suspense/index.js
new file mode 100644
index 0000000..6c525b3
--- /dev/null
+++ b/preact/demo/nested-suspense/index.js
@@ -0,0 +1,69 @@
+import { createElement, Suspense, lazy, Component } from 'react';
+
+const Loading = function() {
+ return <div>Loading...</div>;
+};
+const Error = function({ resetState }) {
+ return (
+ <div>
+ Error!&nbsp;
+ <a onClick={resetState} href="#">
+ Reset app
+ </a>
+ </div>
+ );
+};
+
+const pause = timeout =>
+ new Promise(d => setTimeout(d, timeout), console.log(timeout));
+
+const DropZone = lazy(() =>
+ pause(Math.random() * 1000).then(() => import('./dropzone.js'))
+);
+const Editor = lazy(() =>
+ pause(Math.random() * 1000).then(() => import('./editor.js'))
+);
+const AddNewComponent = lazy(() =>
+ pause(Math.random() * 1000).then(() => import('./addnewcomponent.js'))
+);
+const GenerateComponents = lazy(() =>
+ pause(Math.random() * 1000).then(() => import('./component-container.js'))
+);
+
+export default class App extends Component {
+ state = { hasError: false };
+
+ static getDerivedStateFromError(error) {
+ // Update state so the next render will show the fallback UI.
+ console.warn(error);
+ return { hasError: true };
+ }
+
+ render() {
+ return this.state.hasError ? (
+ <Error resetState={() => this.setState({ hasError: false })} />
+ ) : (
+ <Suspense fallback={<Loading />}>
+ <DropZone appearance={0} />
+ <Editor title="APP_TITLE">
+ <main>
+ <Suspense fallback={<Loading />}>
+ <GenerateComponents appearance={1} />
+ </Suspense>
+ <AddNewComponent appearance={2} />
+ </main>
+ <aside>
+ <section>
+ <Suspense fallback={<Loading />}>
+ <GenerateComponents appearance={3} />
+ </Suspense>
+ <AddNewComponent appearance={4} />
+ </section>
+ </aside>
+ </Editor>
+
+ <footer>Footer here</footer>
+ </Suspense>
+ );
+ }
+}
diff --git a/preact/demo/nested-suspense/subcomponent.js b/preact/demo/nested-suspense/subcomponent.js
new file mode 100644
index 0000000..74d6d1d
--- /dev/null
+++ b/preact/demo/nested-suspense/subcomponent.js
@@ -0,0 +1,5 @@
+import { createElement } from 'react';
+
+export default function SubComponent({ onClick }) {
+ return <div>Lazy loaded sub component</div>;
+}
diff --git a/preact/demo/old.js.bak b/preact/demo/old.js.bak
new file mode 100644
index 0000000..7f2b7c0
--- /dev/null
+++ b/preact/demo/old.js.bak
@@ -0,0 +1,103 @@
+
+// function createRoot(title) {
+// let div = document.createElement('div');
+// let h2 = document.createElement('h2');
+// h2.textContent = title;
+// div.appendChild(h2);
+// document.body.appendChild(div);
+// return div;
+// }
+
+
+/*
+function logCall(obj, method, name) {
+ let old = obj[method];
+ obj[method] = function(...args) {
+ console.log(`<${this.localName}>.`+(name || `${method}(${args})`));
+ return old.apply(this, args);
+ };
+}
+
+logCall(HTMLElement.prototype, 'appendChild');
+logCall(HTMLElement.prototype, 'removeChild');
+logCall(HTMLElement.prototype, 'insertBefore');
+logCall(HTMLElement.prototype, 'replaceChild');
+logCall(HTMLElement.prototype, 'setAttribute');
+logCall(HTMLElement.prototype, 'removeAttribute');
+let d = Object.getOwnPropertyDescriptor(Node.prototype, 'nodeValue');
+Object.defineProperty(Text.prototype, 'nodeValue', {
+ get() {
+ let value = d.get.call(this);
+ console.log('get Text#nodeValue: ', value);
+ return value;
+ },
+ set(v) {
+ console.log('set Text#nodeValue', v);
+ return d.set.call(this, v);
+ }
+});
+
+
+render((
+ <div>
+ <h4>This is a test.</h4>
+ <Foo />
+ <time>...</time>
+ </div>
+), createRoot('Stateful component update demo:'));
+
+
+class Foo extends Component {
+ componentDidMount() {
+ console.log('mounted');
+ this.timer = setInterval( () => {
+ this.setState({ time: Date.now() });
+ }, 5000);
+ }
+ componentWillUnmount() {
+ clearInterval(this.timer);
+ }
+ render(props, state, context) {
+ // console.log('rendering', props, state, context);
+ return <time>test: {state.time}</time>
+ }
+}
+
+
+render((
+ <div>
+ <h4>This is a test.</h4>
+ <Foo />
+ <time>...</time>
+ </div>
+), createRoot('Stateful component update demo:'));
+
+
+let items = [];
+let count = 0;
+let three = createRoot('Top-level render demo:');
+
+setInterval( () => {
+ if (count++ %20 < 10 ) {
+ items.push(<li key={count} style={{
+ position: 'relative',
+ transition: 'all 200ms ease',
+ paddingLeft: items.length*20 +'px'
+ }}>item #{items.length}</li>);
+ }
+ else {
+ items.shift();
+ }
+
+ render((
+ <div>
+ <h4>This is a test.</h4>
+ <time>{Date.now()}</time>
+ <ul>{items}</ul>
+ </div>
+ ), three);
+}, 5000);
+
+// Mount the top-level component to the DOM:
+render(<Main />, document.body);
+*/
diff --git a/preact/demo/package.json b/preact/demo/package.json
new file mode 100644
index 0000000..21cfb36
--- /dev/null
+++ b/preact/demo/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "demo",
+ "main": "index.js",
+ "scripts": {
+ "start": "webpack-dev-server --inline"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.0.0-beta.55",
+ "@babel/plugin-proposal-class-properties": "^7.0.0-beta.55",
+ "@babel/plugin-proposal-decorators": "^7.4.0",
+ "@babel/plugin-syntax-dynamic-import": "^7.2.0",
+ "@babel/plugin-transform-react-constant-elements": "^7.0.0-beta.55",
+ "@babel/plugin-transform-react-jsx": "^7.0.0-beta.55",
+ "@babel/plugin-transform-runtime": "^7.4.0",
+ "@babel/preset-env": "^7.0.0-beta.55",
+ "@babel/preset-react": "^7.0.0-beta.55",
+ "@babel/preset-typescript": "^7.3.3",
+ "babel-loader": "^8.0.0-beta.0",
+ "css-loader": "2.1.1",
+ "html-webpack-plugin": "3.2.0",
+ "node-sass": "^4.12.0",
+ "sass-loader": "7.1.0",
+ "style-loader": "0.23.1",
+ "webpack": "4.33.0",
+ "webpack-cli": "^3.3.4",
+ "webpack-dev-server": "^3.7.1"
+ },
+ "dependencies": {
+ "@material-ui/core": "4.9.5",
+ "d3-scale": "^1.0.7",
+ "d3-selection": "^1.2.0",
+ "htm": "2.1.1",
+ "mobx": "^5.15.4",
+ "mobx-react": "^6.2.2",
+ "mobx-state-tree": "^3.16.0",
+ "preact-render-to-string": "^5.0.2",
+ "preact-router": "^3.0.0",
+ "react-redux": "^7.1.0",
+ "react-router": "^5.0.1",
+ "react-router-dom": "^5.0.1",
+ "redux": "^4.0.1",
+ "styled-components": "^4.2.0"
+ }
+}
diff --git a/preact/demo/people/Readme.md b/preact/demo/people/Readme.md
new file mode 100644
index 0000000..c18a1cb
--- /dev/null
+++ b/preact/demo/people/Readme.md
@@ -0,0 +1,3 @@
+# People demo page
+
+This section of our demo was originally made by [phaux](https://github.com/phaux) in the [web-app-boilerplate](https://github.com/phaux/web-app-boilerplate) repo. It has been slightly modified from it's original to better work inside of our demo app
diff --git a/preact/demo/people/index.tsx b/preact/demo/people/index.tsx
new file mode 100644
index 0000000..0e728ab
--- /dev/null
+++ b/preact/demo/people/index.tsx
@@ -0,0 +1,59 @@
+import { observer } from 'mobx-react';
+import { Component, h } from 'preact';
+import { Profile } from './profile';
+import { Link, Route, Router } from './router';
+import { store } from './store';
+
+import './styles/index.scss';
+
+@observer
+export default class App extends Component {
+ componentDidMount() {
+ store.loadUsers().catch(console.error);
+ }
+
+ render() {
+ return (
+ <Router>
+ <div id="people-app">
+ <nav>
+ <div style={{ margin: 16, textAlign: 'center' }}>
+ Sort by{' '}
+ <select
+ value={store.usersOrder}
+ onChange={(ev: any) => {
+ store.setUsersOrder(ev.target.value);
+ }}
+ >
+ <option value="name">Name</option>
+ <option value="id">ID</option>
+ </select>
+ </div>
+ <ul>
+ {store.getSortedUsers().map((user, i) => (
+ <li
+ key={user.id}
+ style={{
+ animationDelay: `${i * 20}ms`,
+ top: `calc(var(--menu-item-height) * ${i})`,
+ transitionDelay: `${i * 20}ms`
+ }}
+ >
+ <Link href={`people/${user.id}`} active>
+ <img class="avatar" src={user.picture.large} />
+ {user.name.first} {user.name.last}
+ </Link>
+ </li>
+ ))}
+ </ul>
+ </nav>
+ <section id="people-main">
+ <Route match="people">
+ <Route match="*" component={Profile} />
+ </Route>
+ </section>
+ </div>
+ </Router>
+ );
+ }
+}
diff --git a/preact/demo/people/profile.tsx b/preact/demo/people/profile.tsx
new file mode 100644
index 0000000..e1f44ea
--- /dev/null
+++ b/preact/demo/people/profile.tsx
@@ -0,0 +1,59 @@
+import { computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { Component, h } from 'preact';
+import { RouteChildProps } from './router';
+import { store } from './store';
+
+export type ProfileProps = RouteChildProps;
+@observer
+export class Profile extends Component<ProfileProps> {
+ @observable id = '';
+ @observable busy = false;
+
+ componentDidMount() {
+ this.id = this.props.route;
+ }
+
+ componentWillReceiveProps(props: ProfileProps) {
+ this.id = props.route;
+ }
+
+ render() {
+ const user = this.user;
+ if (user == null) return null;
+ return (
+ <div class="profile">
+ <img class="avatar" src={user.picture.large} />
+ <h2>
+ {user.name.first} {user.name.last}
+ </h2>
+ <div class="details">
+ <p>
+ {user.gender === 'female' ? '👩' : '👨'} {user.id}
+ </p>
+ <p>🖂 {user.email}</p>
+ </div>
+ <p>
+ <button
+ class={this.busy ? 'secondary busy' : 'secondary'}
+ disabled={this.busy}
+ onClick={this.remove}
+ >
+ Remove contact
+ </button>
+ </p>
+ </div>
+ );
+ }
+
+ @computed get user() {
+ return store.users.find(u => u.id === this.id);
+ }
+
+ remove = async () => {
+ this.busy = true;
+ await new Promise<void>(cb => setTimeout(cb, 1500));
+ store.deleteUser(this.id);
+ this.busy = false;
+ };
+}
diff --git a/preact/demo/people/router.tsx b/preact/demo/people/router.tsx
new file mode 100644
index 0000000..56fc420
--- /dev/null
+++ b/preact/demo/people/router.tsx
@@ -0,0 +1,153 @@
+import {
+ ComponentChild,
+ ComponentFactory,
+ createContext,
+ FunctionalComponent,
+ h,
+ JSX
+} from 'preact';
+import {
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState
+} from 'preact/hooks';
+
+export type RouterData = {
+ match: string[];
+ path: string[];
+ navigate(path: string): void;
+};
+
+const RouterContext = createContext<RouterData>({
+ match: [],
+ path: [],
+ navigate() {}
+});
+export const useRouter = () => useContext(RouterContext);
+
+const useLocation = (cb: () => void) => {
+ useEffect(() => {
+ window.addEventListener('popstate', cb);
+ return () => {
+ window.removeEventListener('popstate', cb);
+ };
+ }, [cb]);
+};
+
+export const Router: FunctionalComponent = props => {
+ const [path, setPath] = useState(location.pathname);
+
+ const update = useCallback(() => {
+ setPath(location.pathname);
+ }, [setPath]);
+
+ useLocation(update);
+
+ const navigate = useCallback(
+ (path: string) => {
+ history.pushState(null, '', path);
+ update();
+ },
+ [update]
+ );
+
+ const router = useMemo<RouterData>(
+ () => ({
+ match: [],
+ navigate,
+ path: path.split('/').filter(Boolean)
+ }),
+ [navigate, path]
+ );
+
+ return <RouterContext.Provider children={props.children} value={router} />;
+};
+
+export type RouteChildProps = { route: string };
+export type RouteProps = {
+ component?: ComponentFactory<RouteChildProps>;
+ match: string;
+ render?(route: string): ComponentChild;
+};
+export const Route: FunctionalComponent<RouteProps> = props => {
+ const router = useRouter();
+ const [dir, ...subpath] = router.path;
+
+ if (dir == null) return null;
+ if (props.match !== '*' && dir !== props.match) return null;
+
+ const children = useMemo(() => {
+ if (props.component) return <props.component key={dir} route={dir} />;
+ if (props.render) return props.render(dir);
+ return props.children;
+ }, [props.component, props.render, props.children, dir]);
+
+ const innerRouter = useMemo<RouterData>(
+ () => ({
+ ...router,
+ match: [...router.match, dir],
+ path: subpath
+ }),
+ [router.match, dir, subpath.join('/')]
+ );
+
+ return <RouterContext.Provider children={children} value={innerRouter} />;
+};
+
+export type LinkProps = JSX.HTMLAttributes & {
+ active?: boolean | string;
+};
+export const Link: FunctionalComponent<LinkProps> = props => {
+ const router = useRouter();
+
+ const classProps = [props.class, props.className];
+ const originalClasses = useMemo(() => {
+ const classes = [];
+ for (const prop of classProps) if (prop) classes.push(...prop.split(/\s+/));
+ return classes;
+ }, classProps);
+
+ const activeClass = useMemo(() => {
+ if (!props.active || props.href == null) return undefined;
+ const href = props.href.split('/').filter(Boolean);
+ const path =
+ props.href[0] === '/' ? [...router.match, ...router.path] : router.path;
+ const isMatch = href.every((dir, i) => dir === path[i]);
+ if (isMatch) return props.active === true ? 'active' : props.active;
+ }, [originalClasses, props.active, props.href, router.match, router.path]);
+
+ const classes =
+ activeClass == null ? originalClasses : [...originalClasses, activeClass];
+
+ const getHref = useCallback(() => {
+ if (props.href == null || props.href[0] === '/') return props.href;
+ const path = props.href.split('/').filter(Boolean);
+ return '/' + [...router.match, ...path].join('/');
+ }, [router.match, props.href]);
+
+ const handleClick = useCallback(
+ (ev: MouseEvent) => {
+ const href = getHref();
+ if (props.onClick != null) props.onClick(ev);
+ if (ev.defaultPrevented) return;
+ if (href == null) return;
+ if (ev.button !== 0) return;
+ if (props.target != null && props.target !== '_self') return;
+ if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) return;
+ ev.preventDefault();
+ router.navigate(href);
+ },
+ [getHref, router.navigate, props.onClick, props.target]
+ );
+
+ return (
+ <a
+ {...props}
+ class={classes.join(' ')}
+ href={getHref()}
+ onClick={handleClick}
+ />
+ );
+};
diff --git a/preact/demo/people/store.ts b/preact/demo/people/store.ts
new file mode 100644
index 0000000..1967afd
--- /dev/null
+++ b/preact/demo/people/store.ts
@@ -0,0 +1,83 @@
+import { flow, Instance, types } from 'mobx-state-tree';
+
+const cmp = <T, U>(fn: (x: T) => U) => (a: T, b: T): number =>
+ fn(a) > fn(b) ? 1 : -1;
+
+const User = types.model({
+ email: types.string,
+ gender: types.enumeration(['male', 'female']),
+ id: types.identifier,
+ name: types.model({
+ first: types.string,
+ last: types.string
+ }),
+ picture: types.model({
+ large: types.string
+ })
+});
+
+const Store = types
+ .model({
+ users: types.array(User),
+ usersOrder: types.enumeration(['name', 'id'])
+ })
+ .views(self => ({
+ getSortedUsers() {
+ if (self.usersOrder === 'name')
+ return self.users.slice().sort(cmp(x => x.name.first));
+ if (self.usersOrder === 'id')
+ return self.users.slice().sort(cmp(x => x.id));
+ throw Error(`Unknown ordering ${self.usersOrder}`);
+ }
+ }))
+ .actions(self => ({
+ addUser: flow<unknown, []>(function*() {
+ const data = yield fetch('https://randomuser.me/api?results=1')
+ .then(res => res.json())
+ .then(data =>
+ data.results.map((user: any) => ({
+ ...user,
+ id: user.login.username
+ }))
+ );
+ self.users.push(...data);
+ }),
+ loadUsers: flow<unknown, []>(function*() {
+ const data = yield fetch(
+ `https://randomuser.me/api?seed=${12321}&results=12`
+ )
+ .then(res => res.json())
+ .then(data =>
+ data.results.map((user: any) => ({
+ ...user,
+ id: user.login.username
+ }))
+ );
+ self.users.replace(data);
+ }),
+ deleteUser(id: string) {
+ const user = self.users.find(u => u.id === id);
+ if (user != null) self.users.remove(user);
+ },
+ setUsersOrder(order: 'name' | 'id') {
+ self.usersOrder = order;
+ }
+ }));
+
+export type StoreType = Instance<typeof Store>;
+export const store = Store.create({
+ usersOrder: 'name',
+ users: []
+});
+
+// const { Provider, Consumer } = createContext<StoreType>(undefined as any)
+
+// export const StoreProvider: FunctionalComponent = props => {
+// const store = Store.create({})
+// return <Provider value={store} children={props.children} />
+// }
+
+// export type StoreProps = {store: StoreType}
+// export function injectStore<T>(Child: AnyComponent<T & StoreProps>): FunctionalComponent<T> {
+// return props => <Consumer render={store => <Child {...props} store={store}/>}/>
+// }
diff --git a/preact/demo/people/styles/animations.scss b/preact/demo/people/styles/animations.scss
new file mode 100644
index 0000000..c7ce8cb
--- /dev/null
+++ b/preact/demo/people/styles/animations.scss
@@ -0,0 +1,34 @@
+@keyframes popup {
+ from {
+ box-shadow: 0 0 0 black;
+ opacity: 0;
+ transform: scale(0.9);
+ }
+ to {
+ box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5);
+ opacity: 1;
+ transform: none;
+ }
+}
+
+@keyframes zoom {
+ from {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+@keyframes appear-from-left {
+ from {
+ opacity: 0;
+ transform: translateX(-25px);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
diff --git a/preact/demo/people/styles/app.scss b/preact/demo/people/styles/app.scss
new file mode 100644
index 0000000..4473f9e
--- /dev/null
+++ b/preact/demo/people/styles/app.scss
@@ -0,0 +1,100 @@
+#people-app {
+ position: relative;
+ overflow: hidden;
+ min-height: 100vh;
+ animation: popup 300ms cubic-bezier(0.3, 0.7, 0.3, 1) forwards;
+ background: var(--app-background);
+ --menu-width: 260px;
+ --menu-item-height: 50px;
+
+ @media (min-width: 1280px) {
+ max-width: 1280px;
+ min-height: calc(100vh - 64px);
+ margin: 32px auto;
+ border-radius: 10px;
+ }
+
+ > nav {
+ position: absolute;
+ display: flow-root;
+ width: var(--menu-width);
+ height: 100%;
+ background-color: var(--app-background-secondary);
+ overflow-x: hidden;
+ overflow-y: auto;
+ }
+
+ > nav h4 {
+ padding-left: 16px;
+ font-weight: normal;
+ text-transform: uppercase;
+ }
+
+ > nav ul {
+ position: relative;
+ }
+
+ > nav li {
+ position: absolute;
+ width: 100%;
+ animation: zoom 200ms forwards;
+ opacity: 0;
+ transition: top 200ms;
+ }
+
+ > nav li > a {
+ position: relative;
+ display: flex;
+ overflow: hidden;
+ flex-flow: row;
+ align-items: center;
+ margin-left: 16px;
+ border-right: 2px solid transparent;
+ border-bottom-left-radius: 48px;
+ border-top-left-radius: 48px;
+ text-transform: capitalize;
+ transition: border 500ms;
+ }
+
+ > nav li > a:hover {
+ background-color: var(--app-highlight);
+ }
+
+ > nav li > a::after {
+ position: absolute;
+ top: 0;
+ right: -2px;
+ bottom: 0;
+ left: 0;
+ background-image: radial-gradient(
+ circle,
+ var(--app-ripple) 1%,
+ transparent 1%
+ );
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 10000%;
+ content: '';
+ opacity: 0;
+ transition: opacity 700ms, background 300ms;
+ }
+
+ > nav li > a:active::after {
+ background-size: 100%;
+ opacity: 0.5;
+ transition: none;
+ }
+
+ > nav li > a.active {
+ border-color: var(--app-primary);
+ background-color: var(--app-highlight);
+ }
+
+ > nav li > a > * {
+ margin: 8px;
+ }
+
+ #people-main {
+ padding-left: var(--menu-width);
+ }
+}
diff --git a/preact/demo/people/styles/avatar.scss b/preact/demo/people/styles/avatar.scss
new file mode 100644
index 0000000..437c6c4
--- /dev/null
+++ b/preact/demo/people/styles/avatar.scss
@@ -0,0 +1,16 @@
+#people-app {
+ .avatar {
+ display: inline-block;
+ overflow: hidden;
+ width: var(--avatar-size, 32px);
+ height: var(--avatar-size, 32px);
+ background-color: var(--avatar-color, var(--app-primary));
+ border-radius: 50%;
+ font-size: calc(var(--avatar-size, 32px) * 0.5);
+ line-height: var(--avatar-size, 32px);
+ object-fit: cover;
+ text-align: center;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+}
diff --git a/preact/demo/people/styles/button.scss b/preact/demo/people/styles/button.scss
new file mode 100644
index 0000000..ce43138
--- /dev/null
+++ b/preact/demo/people/styles/button.scss
@@ -0,0 +1,115 @@
+#people-app {
+ button {
+ position: relative;
+ overflow: hidden;
+ min-width: 36px;
+ height: 36px;
+ padding: 0 16px;
+ border: none;
+ background-color: transparent;
+ border-radius: 4px;
+ color: var(--app-text);
+ font-family: 'Montserrat', sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ transition: background 300ms, color 200ms;
+ white-space: nowrap;
+ }
+
+ button::before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: var(--app-ripple);
+ content: '';
+ opacity: 0;
+ transition: opacity 200ms;
+ }
+
+ button:hover:not(:disabled)::before {
+ opacity: 0.3;
+ transition: opacity 100ms;
+ }
+
+ button:active:not(:disabled)::before {
+ opacity: 0.7;
+ transition: none;
+ }
+
+ button::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-image: radial-gradient(
+ circle,
+ var(--app-ripple) 1%,
+ transparent 1%
+ );
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 20000%;
+ content: '';
+ opacity: 0;
+ transition: opacity 700ms, background 400ms;
+ }
+
+ button:active:not(:disabled)::after {
+ background-size: 100%;
+ opacity: 1;
+ transition: none;
+ }
+
+ button.primary {
+ background-color: var(--app-primary);
+ box-shadow: 0 2px 6px var(--app-shadow);
+ }
+
+ button.secondary {
+ background-color: var(--app-secondary);
+ box-shadow: 0 2px 6px var(--app-shadow);
+ }
+
+ button:disabled {
+ color: var(--app-text-secondary);
+ }
+
+ button.busy {
+ animation: stripes 500ms linear infinite;
+ background-image: repeating-linear-gradient(
+ 45deg,
+ var(--app-shadow) 0%,
+ var(--app-shadow) 25%,
+ transparent 25%,
+ transparent 50%,
+ var(--app-shadow) 50%,
+ var(--app-shadow) 75%,
+ transparent 75%,
+ transparent 100%
+ );
+ color: var(--app-text);
+ /* letter-spacing: -.7em; */
+ }
+
+ button:disabled:not(.primary):not(.secondary).busy,
+ button:disabled.primary:not(.busy),
+ button:disabled.secondary:not(.busy) {
+ background-color: var(--app-background-disabled);
+ }
+
+ @keyframes stripes {
+ from {
+ background-position-x: 0;
+ background-size: 16px 16px;
+ }
+ to {
+ background-position-x: 16px;
+ background-size: 16px 16px;
+ }
+ }
+}
diff --git a/preact/demo/people/styles/index.scss b/preact/demo/people/styles/index.scss
new file mode 100644
index 0000000..80b342e
--- /dev/null
+++ b/preact/demo/people/styles/index.scss
@@ -0,0 +1,168 @@
+@import 'app.scss';
+@import 'animations.scss';
+@import 'avatar.scss';
+@import 'profile.scss';
+@import 'button.scss';
+
+// :root {
+#people-app {
+ --app-background: #2f2b43;
+ --app-background-secondary: #353249;
+ --app-background-disabled: #555366;
+ --app-highlight: rgba(255, 255, 255, 0.1);
+ --app-ripple: rgba(255, 255, 255, 0.5);
+ --app-shadow: rgba(0, 0, 0, 0.15);
+ --app-text: #fff;
+ --app-text-secondary: #807e97;
+ --app-primary: #ff0087;
+ --app-secondary: #4d7cfe;
+ --app-tertiary: #00ec97;
+ --app-danger: #f3c835;
+ --spinner-size: 200px;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+// body {
+#people-app {
+ // display: flow-root;
+ // overflow: auto;
+ // min-height: 100vh;
+ // margin: 0;
+ // animation: background-light 5s ease-out forwards;
+ // /* very fancy background */
+ // background: radial-gradient(
+ // circle 15px at 150px 90vh,
+ // rgba(255, 255, 255, 0.35),
+ // rgba(255, 255, 255, 0.35) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 9px at 60px 50vh,
+ // rgba(255, 255, 255, 0.55),
+ // rgba(255, 255, 255, 0.55) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 19px at 40vw 70px,
+ // rgba(255, 255, 255, 0.3),
+ // rgba(255, 255, 255, 0.3) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 12px at 80vw 80px,
+ // rgba(255, 255, 255, 0.4),
+ // rgba(255, 255, 255, 0.4) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 7px at 55vw calc(100vh - 95px),
+ // rgba(255, 255, 255, 0.6),
+ // rgba(255, 255, 255, 0.6) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 14px at 25vw calc(100vh - 35px),
+ // rgba(255, 255, 255, 0.4),
+ // rgba(255, 255, 255, 0.4) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 11px at calc(100vw - 95px) 55vh,
+ // rgba(255, 255, 255, 0.45),
+ // rgba(255, 255, 255, 0.45) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 13px at calc(100vw - 35px) 85vh,
+ // rgba(255, 255, 255, 0.4),
+ // rgba(255, 255, 255, 0.4) 90%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 50vw at 0 -25%,
+ // rgba(255, 255, 255, 0.07),
+ // rgba(255, 255, 255, 0.07) 100%,
+ // transparent
+ // ),
+ // radial-gradient(
+ // circle 80vw at top left,
+ // rgba(255, 255, 255, 0.07),
+ // rgba(255, 255, 255, 0.07) 100%,
+ // transparent
+ // ),
+ // radial-gradient(circle at bottom right, #ef2fb8, transparent),
+ // radial-gradient(circle at top right, #c45af3, transparent),
+ // linear-gradient(#ee66ca, #ff47a6);
+ color: var(--app-text);
+ font-family: 'Montserrat', sans-serif;
+}
+
+#people-app {
+ .spinner {
+ position: absolute;
+ top: 200px;
+ left: calc(50% - var(--spinner-size) / 2);
+ width: var(--spinner-size);
+ height: var(--spinner-size);
+ animation: zoom 250ms 500ms forwards ease-out;
+ opacity: 0;
+ transition: opacity 200ms, transform 200ms ease-in;
+ }
+
+ .spinner.exit {
+ opacity: 0;
+ transform: scale(0.5);
+ }
+
+ .spinner::before,
+ .spinner::after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: calc(var(--spinner-size) / 3);
+ height: calc(var(--spinner-size) / 3);
+ animation: spinner 2s infinite ease-in-out;
+ background-color: rgba(255, 255, 255, 0.6);
+ content: '';
+ }
+
+ .spinner::after {
+ animation-delay: -1s;
+ }
+
+ @keyframes spinner {
+ 25% {
+ transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px))
+ rotate(-90deg) scale(0.5);
+ }
+ 50% {
+ transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px))
+ translateY(calc(var(--spinner-size) / 3 * 2 - 1px)) rotate(-179deg);
+ }
+ 50.1% {
+ transform: translateX(calc(var(--spinner-size) / 3 * 2 - 1px))
+ translateY(calc(var(--spinner-size) / 3 * 2 - 1px)) rotate(-180deg);
+ }
+ 75% {
+ transform: translateX(0) translateY(calc(var(--spinner-size) / 3 * 2 - 1px))
+ rotate(-270deg) scale(0.5);
+ }
+ 100% {
+ transform: rotate(-360deg);
+ }
+ }
+
+ ul,
+ ol {
+ padding-left: 0;
+ list-style: none;
+ }
+
+ a {
+ color: inherit;
+ text-decoration: none;
+ }
+}
diff --git a/preact/demo/people/styles/profile.scss b/preact/demo/people/styles/profile.scss
new file mode 100644
index 0000000..f74b760
--- /dev/null
+++ b/preact/demo/people/styles/profile.scss
@@ -0,0 +1,26 @@
+#people-app {
+ .profile {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ margin: 32px 0;
+ animation: appear-from-left 0.5s forwards;
+ --avatar-size: 80px;
+ }
+
+ .profile h2 {
+ text-transform: capitalize;
+ }
+
+ .profile .details {
+ display: flex;
+ flex-flow: column;
+ align-items: stretch;
+ margin: 16px auto;
+ }
+
+ .profile .details p {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
+}
diff --git a/preact/demo/preact.js b/preact/demo/preact.js
new file mode 100644
index 0000000..9b053e0
--- /dev/null
+++ b/preact/demo/preact.js
@@ -0,0 +1,39 @@
+import {
+ options,
+ createElement,
+ cloneElement,
+ Component as CevicheComponent,
+ render
+} from 'preact';
+
+options.vnode = vnode => {
+ vnode.nodeName = vnode.type;
+ vnode.attributes = vnode.props;
+ vnode.children = vnode._children || [].concat(vnode.props.children || []);
+};
+
+function asArray(arr) {
+ return Array.isArray(arr) ? arr : [arr];
+}
+
+function normalize(obj) {
+ if (Array.isArray(obj)) {
+ return obj.map(normalize);
+ }
+ if ('type' in obj && !('attributes' in obj)) {
+ obj.attributes = obj.props;
+ }
+ return obj;
+}
+
+export function Component(props, context) {
+ CevicheComponent.call(this, props, context);
+ const render = this.render;
+ this.render = function(props, state, context) {
+ if (props.children) props.children = asArray(normalize(props.children));
+ return render.call(this, props, state, context);
+ };
+}
+Component.prototype = new CevicheComponent();
+
+export { createElement, createElement as h, cloneElement, render };
diff --git a/preact/demo/profiler.js b/preact/demo/profiler.js
new file mode 100644
index 0000000..bb44269
--- /dev/null
+++ b/preact/demo/profiler.js
@@ -0,0 +1,88 @@
+import { createElement, Component, options } from 'preact';
+
+function getPrimes(max) {
+ let sieve = [],
+ i,
+ j,
+ primes = [];
+ for (i = 2; i <= max; ++i) {
+ if (!sieve[i]) {
+ // i has not been marked -- it is prime
+ primes.push(i);
+ for (j = i << 1; j <= max; j += i) {
+ sieve[j] = true;
+ }
+ }
+ }
+ return primes.join('');
+}
+
+function Foo(props) {
+ return <div>{props.children}</div>;
+}
+
+function Bar() {
+ getPrimes(10000);
+ return (
+ <div>
+ <span>...yet another component</span>
+ </div>
+ );
+}
+
+function PrimeNumber(props) {
+ // Slow down rendering of this component
+ getPrimes(10);
+
+ return (
+ <div>
+ <span>I'm a slow component</span>
+ <br />
+ {props.children}
+ </div>
+ );
+}
+
+export default class ProfilerDemo extends Component {
+ constructor() {
+ super();
+ this.onClick = this.onClick.bind(this);
+ this.state = { counter: 0 };
+ }
+
+ componentDidMount() {
+ options._diff = vnode => (vnode.startTime = performance.now());
+ options.diffed = vnode => (vnode.endTime = performance.now());
+ }
+
+ componentWillUnmount() {
+ delete options._diff;
+ delete options.diffed;
+ }
+
+ onClick() {
+ this.setState(prev => ({ counter: ++prev.counter }));
+ }
+
+ render() {
+ return (
+ <div class="foo">
+ <h1>⚛ Preact</h1>
+ <p>
+ <b>Devtools Profiler integration 🕒</b>
+ </p>
+ <Foo>
+ <PrimeNumber>
+ <Foo>I'm a fast component</Foo>
+ <Bar />
+ </PrimeNumber>
+ </Foo>
+ <Foo>I'm the fastest component 🎉</Foo>
+ <span>Counter: {this.state.counter}</span>
+ <br />
+ <br />
+ <button onClick={this.onClick}>Force re-render</button>
+ </div>
+ );
+ }
+}
diff --git a/preact/demo/pythagoras/index.js b/preact/demo/pythagoras/index.js
new file mode 100644
index 0000000..470e304
--- /dev/null
+++ b/preact/demo/pythagoras/index.js
@@ -0,0 +1,86 @@
+import { createElement, Component } from 'preact';
+import { select as d3select, mouse as d3mouse } from 'd3-selection';
+import { scaleLinear } from 'd3-scale';
+import Pythagoras from './pythagoras';
+
+export default class PythagorasDemo extends Component {
+ svg = {
+ width: 1280,
+ height: 600
+ };
+
+ state = {
+ currentMax: 0,
+ baseW: 80,
+ heightFactor: 0,
+ lean: 0
+ };
+
+ realMax = 11;
+
+ svgRef = c => {
+ this.svgElement = c;
+ };
+
+ scaleFactor = scaleLinear()
+ .domain([this.svg.height, 0])
+ .range([0, 0.8]);
+
+ scaleLean = scaleLinear()
+ .domain([0, this.svg.width / 2, this.svg.width])
+ .range([0.5, 0, -0.5]);
+
+ onMouseMove = event => {
+ let [x, y] = d3mouse(this.svgElement);
+
+ this.setState({
+ heightFactor: this.scaleFactor(y),
+ lean: this.scaleLean(x)
+ });
+ };
+
+ restart = () => {
+ this.setState({ currentMax: 0 });
+ this.next();
+ };
+
+ next = () => {
+ let { currentMax } = this.state;
+
+ if (currentMax < this.realMax) {
+ this.setState({ currentMax: currentMax + 1 });
+ this.timer = setTimeout(this.next, 500);
+ }
+ };
+
+ componentDidMount() {
+ this.selected = d3select(this.svgElement).on('mousemove', this.onMouseMove);
+ this.next();
+ }
+
+ componentWillUnmount() {
+ this.selected.on('mousemove', null);
+ clearTimeout(this.timer);
+ }
+
+ render({}, { currentMax, baseW, heightFactor, lean }) {
+ let { width, height } = this.svg;
+
+ return (
+ <div class="App">
+ <svg width={width} height={height} ref={this.svgRef}>
+ <Pythagoras
+ w={baseW}
+ h={baseW}
+ heightFactor={heightFactor}
+ lean={lean}
+ x={width / 2 - 40}
+ y={height - baseW}
+ lvl={0}
+ maxlvl={currentMax}
+ />
+ </svg>
+ </div>
+ );
+ }
+}
diff --git a/preact/demo/pythagoras/pythagoras.js b/preact/demo/pythagoras/pythagoras.js
new file mode 100644
index 0000000..bbe3c65
--- /dev/null
+++ b/preact/demo/pythagoras/pythagoras.js
@@ -0,0 +1,97 @@
+import { interpolateViridis } from 'd3-scale';
+import { createElement } from 'preact';
+
+Math.deg = function(radians) {
+ return radians * (180 / Math.PI);
+};
+
+const memoizedCalc = (function() {
+ const memo = {};
+
+ const key = ({ w, heightFactor, lean }) => `${w}-${heightFactor}-${lean}`;
+
+ return args => {
+ let memoKey = key(args);
+
+ if (memo[memoKey]) {
+ return memo[memoKey];
+ }
+
+ let { w, heightFactor, lean } = args;
+ let trigH = heightFactor * w;
+
+ let result = {
+ nextRight: Math.sqrt(trigH ** 2 + (w * (0.5 + lean)) ** 2),
+ nextLeft: Math.sqrt(trigH ** 2 + (w * (0.5 - lean)) ** 2),
+ A: Math.deg(Math.atan(trigH / ((0.5 - lean) * w))),
+ B: Math.deg(Math.atan(trigH / ((0.5 + lean) * w)))
+ };
+
+ memo[memoKey] = result;
+ return result;
+ };
+})();
+
+export default function Pythagoras({
+ w,
+ x,
+ y,
+ heightFactor,
+ lean,
+ left,
+ right,
+ lvl,
+ maxlvl
+}) {
+ if (lvl >= maxlvl || w < 1) {
+ return null;
+ }
+
+ const { nextRight, nextLeft, A, B } = memoizedCalc({
+ w,
+ heightFactor,
+ lean
+ });
+
+ let rotate = '';
+
+ if (left) {
+ rotate = `rotate(${-A} 0 ${w})`;
+ } else if (right) {
+ rotate = `rotate(${B} ${w} ${w})`;
+ }
+
+ return (
+ <g transform={`translate(${x} ${y}) ${rotate}`}>
+ <rect
+ width={w}
+ height={w}
+ x={0}
+ y={0}
+ style={{ fill: interpolateViridis(lvl / maxlvl) }}
+ />
+
+ <Pythagoras
+ w={nextLeft}
+ x={0}
+ y={-nextLeft}
+ lvl={lvl + 1}
+ maxlvl={maxlvl}
+ heightFactor={heightFactor}
+ lean={lean}
+ left
+ />
+
+ <Pythagoras
+ w={nextRight}
+ x={w - nextRight}
+ y={-nextRight}
+ lvl={lvl + 1}
+ maxlvl={maxlvl}
+ heightFactor={heightFactor}
+ lean={lean}
+ right
+ />
+ </g>
+ );
+}
diff --git a/preact/demo/redux.js b/preact/demo/redux.js
new file mode 100644
index 0000000..d63dfb7
--- /dev/null
+++ b/preact/demo/redux.js
@@ -0,0 +1,48 @@
+import { createElement } from 'preact';
+import React from 'react';
+import { createStore } from 'redux';
+import { connect, Provider } from 'react-redux';
+
+const store = createStore((state = { value: 0 }, action) => {
+ switch (action.type) {
+ case 'increment':
+ return { value: state.value + 1 };
+ case 'decrement':
+ return { value: state.value - 1 };
+ default:
+ return state;
+ }
+});
+
+class Child extends React.Component {
+ render() {
+ return (
+ <div>
+ <div>Child #1: {this.props.foo}</div>
+ <ConnectedChild2 />
+ </div>
+ );
+ }
+}
+const ConnectedChild = connect(store => ({ foo: store.value }))(Child);
+
+class Child2 extends React.Component {
+ render() {
+ return <div>Child #2: {this.props.foo}</div>;
+ }
+}
+const ConnectedChild2 = connect(store => ({ foo: store.value }))(Child2);
+
+export default function Redux() {
+ return (
+ <div>
+ <h1>Counter</h1>
+ <Provider store={store}>
+ <ConnectedChild />
+ </Provider>
+ <br />
+ <button onClick={() => store.dispatch({ type: 'increment' })}>+</button>
+ <button onClick={() => store.dispatch({ type: 'decrement' })}>-</button>
+ </div>
+ );
+}
diff --git a/preact/demo/reduxUpdate.js b/preact/demo/reduxUpdate.js
new file mode 100644
index 0000000..37e1f9c
--- /dev/null
+++ b/preact/demo/reduxUpdate.js
@@ -0,0 +1,61 @@
+import { createElement, Component } from 'preact';
+import { connect, Provider } from 'react-redux';
+import { createStore } from 'redux';
+import { HashRouter, Route, Link } from 'react-router-dom';
+
+const store = createStore(
+ (state, action) => ({ ...state, display: action.display }),
+ { display: false }
+);
+
+function _Redux({ showMe, counter }) {
+ if (!showMe) return null;
+ return <div>showMe {counter}</div>;
+}
+const Redux = connect(
+ state => console.log('injecting', state.display) || { showMe: state.display }
+)(_Redux);
+
+let display = false;
+class Test extends Component {
+ componentDidUpdate(prevProps) {
+ if (this.props.start != prevProps.start) {
+ this.setState({ f: (this.props.start || 0) + 1 });
+ setTimeout(() => this.setState({ i: (this.state.i || 0) + 1 }));
+ }
+ }
+
+ render() {
+ const { f } = this.state;
+ return (
+ <div>
+ <button
+ onClick={() => {
+ display = !display;
+ store.dispatch({ type: 1, display });
+ }}
+ >
+ Toggle visibility
+ </button>
+ <Link to={`/${(parseInt(this.props.start) || 0) + 1}`}>Click me</Link>
+
+ <Redux counter={f} />
+ </div>
+ );
+ }
+}
+
+function App() {
+ return (
+ <Provider store={store}>
+ <HashRouter>
+ <Route
+ path="/:start?"
+ render={({ match }) => <Test start={match.params.start || 0} />}
+ />
+ </HashRouter>
+ </Provider>
+ );
+}
+
+export default App;
diff --git a/preact/demo/reorder.js b/preact/demo/reorder.js
new file mode 100644
index 0000000..35acc5e
--- /dev/null
+++ b/preact/demo/reorder.js
@@ -0,0 +1,104 @@
+import { createElement, Component } from 'preact';
+
+function createItems(count = 10) {
+ let items = [];
+ for (let i = 0; i < count; i++) {
+ items.push({
+ label: `Item #${i + 1}`,
+ key: i + 1
+ });
+ }
+ return items;
+}
+
+function random() {
+ return Math.random() < 0.5 ? 1 : -1;
+}
+
+export default class Reorder extends Component {
+ state = {
+ items: createItems(),
+ count: 1,
+ useKeys: false
+ };
+
+ shuffle = () => {
+ this.setState({ items: this.state.items.slice().sort(random) });
+ };
+
+ swapTwo = () => {
+ let items = this.state.items.slice(),
+ first = Math.floor(Math.random() * items.length),
+ second;
+ do {
+ second = Math.floor(Math.random() * items.length);
+ } while (second === first);
+ let other = items[first];
+ items[first] = items[second];
+ items[second] = other;
+ this.setState({ items });
+ };
+
+ reverse = () => {
+ this.setState({ items: this.state.items.slice().reverse() });
+ };
+
+ setCount = e => {
+ this.setState({ count: Math.round(e.target.value) });
+ };
+
+ rotate = () => {
+ let { items, count } = this.state;
+ items = items.slice(count).concat(items.slice(0, count));
+ this.setState({ items });
+ };
+
+ rotateBackward = () => {
+ let { items, count } = this.state,
+ len = items.length;
+ items = items.slice(len - count, len).concat(items.slice(0, len - count));
+ this.setState({ items });
+ };
+
+ toggleKeys = () => {
+ this.setState({ useKeys: !this.state.useKeys });
+ };
+
+ renderItem = item => (
+ <li key={this.state.useKeys ? item.key : null}>{item.label}</li>
+ );
+
+ render({}, { items, count, useKeys }) {
+ return (
+ <div class="reorder-demo">
+ <header>
+ <button onClick={this.shuffle}>Shuffle</button>
+ <button onClick={this.swapTwo}>Swap Two</button>
+ <button onClick={this.reverse}>Reverse</button>
+ <button onClick={this.rotate}>Rotate</button>
+ <button onClick={this.rotateBackward}>Rotate Backward</button>
+ <label>
+ <input
+ type="checkbox"
+ onClick={this.toggleKeys}
+ checked={useKeys}
+ />{' '}
+ use keys?
+ </label>
+ <label>
+ <input
+ type="number"
+ step="1"
+ min="1"
+ style={{ width: '3em' }}
+ onInput={this.setCount}
+ value={count}
+ />{' '}
+ count
+ </label>
+ </header>
+ <ul>{items.map(this.renderItem)}</ul>
+ </div>
+ );
+ }
+}
diff --git a/preact/demo/spiral.js b/preact/demo/spiral.js
new file mode 100644
index 0000000..baaa052
--- /dev/null
+++ b/preact/demo/spiral.js
@@ -0,0 +1,140 @@
+import { createElement, Component } from 'preact';
+
+const COUNT = 500;
+const LOOPS = 6;
+
+// Component.debounce = requestAnimationFrame;
+
+export default class Spiral extends Component {
+ state = { x: 0, y: 0, big: false, counter: 0 };
+
+ handleClick = e => {
+ console.log('click');
+ };
+
+ increment = () => {
+ if (this.stop) return;
+ // this.setState({ counter: this.state.counter + 1 }, this.increment);
+ this.setState({ counter: this.state.counter + 1 });
+ // this.forceUpdate();
+ requestAnimationFrame(this.increment);
+ };
+
+ setMouse({ pageX: x, pageY: y }) {
+ this.setState({ x, y });
+ return false;
+ }
+
+ setBig(big) {
+ this.setState({ big });
+ }
+
+ componentDidMount() {
+ console.log('mount');
+
+ // let touch = navigator.maxTouchPoints > 1;
+ let touch = false;
+
+ // set mouse position state on move:
+ addEventListener(touch ? 'touchmove' : 'mousemove', e => {
+ this.setMouse(e.touches ? e.touches[0] : e);
+ });
+
+ // holding the mouse down enables big mode:
+ addEventListener(touch ? 'touchstart' : 'mousedown', e => {
+ this.setBig(true);
+ e.preventDefault();
+ });
+ addEventListener(touch ? 'touchend' : 'mouseup', e => this.setBig(false));
+
+ requestAnimationFrame(this.increment);
+ }
+
+ componentWillUnmount() {
+ console.log('unmount');
+ this.stop = true;
+ }
+
+ // componentDidUpdate() {
+ // // invoking setState() in componentDidUpdate() creates an animation loop:
+ // this.increment();
+ // }
+
+ // builds and returns a brand new DOM (every time)
+ render(props, { x, y, big, counter }) {
+ let max =
+ COUNT +
+ Math.round(Math.sin((counter / 90) * 2 * Math.PI) * COUNT * 0.5),
+ cursors = [];
+
+ // the advantage of JSX is that you can use the entirety of JS to "template":
+ for (let i = max; i--; ) {
+ let f = (i / max) * LOOPS,
+ θ = f * 2 * Math.PI,
+ m = 20 + i * 2,
+ hue = (f * 255 + counter * 10) % 255;
+ cursors[i] = (
+ <Cursor
+ big={big}
+ color={'hsl(' + hue + ',100%,50%)'}
+ x={(x + Math.sin(θ) * m) | 0}
+ y={(y + Math.cos(θ) * m) | 0}
+ />
+ );
+ }
+
+ return (
+ <div id="main" onClick={this.handleClick}>
+ <Cursor label x={x} y={y} big={big} />
+ {cursors}
+ </div>
+ );
+ }
+}
+
+/** Represents a single coloured dot. */
+class Cursor extends Component {
+ // get shared/pooled class object
+ getClass(big, label) {
+ let cl = 'cursor';
+ if (big) cl += ' big';
+ if (label) cl += ' label';
+ return cl;
+ }
+
+ // skip any pointless re-renders
+ shouldComponentUpdate(props) {
+ for (let i in props)
+ if (i !== 'children' && props[i] !== this.props[i]) return true;
+ return false;
+ }
+
+ // first argument is "props", the attributes passed to <Cursor ...>
+ render({ x, y, label, color, big }) {
+ let inner = null;
+ if (label)
+ inner = (
+ <span class="label">
+ {x},{y}
+ </span>
+ );
+ return (
+ <div
+ class={this.getClass(big, label)}
+ style={{
+ transform: `translate(${x || 0}px, ${y || 0}px) scale(${
+ big ? 2 : 1
+ })`,
+ // transform: `translate3d(${x || 0}px, ${y || 0}px, 0) scale(${big?2:1})`,
+ borderColor: color
+ }}
+ // style={{ left: x || 0, top: y || 0, borderColor: color }}
+ >
+ {inner}
+ </div>
+ );
+ }
+}
+
+// Addendum: disable dragging on mobile
+addEventListener('touchstart', e => (e.preventDefault(), false));
diff --git a/preact/demo/stateOrderBug.js b/preact/demo/stateOrderBug.js
new file mode 100644
index 0000000..b9f333d
--- /dev/null
+++ b/preact/demo/stateOrderBug.js
@@ -0,0 +1,80 @@
+import htm from 'htm';
+import { h } from 'preact';
+import { useState, useCallback } from 'preact/hooks';
+
+const html = htm.bind(h);
+
+// configuration used to show behavior vs. workaround
+let childFirst = true;
+const Config = () => html`
+ <label>
+ <input
+ type="checkbox"
+ checked=${childFirst}
+ onchange=${evt => {
+ childFirst = evt.target.checked;
+ }}
+ />
+ Set child state before parent state.
+ </label>
+`;
+
+const Child = ({ items, setItems }) => {
+ let [pendingId, setPendingId] = useState(null);
+ if (!pendingId) {
+ setPendingId(
+ (pendingId = Math.random()
+ .toFixed(20)
+ .slice(2))
+ );
+ }
+
+ const onInput = useCallback(
+ evt => {
+ let val = evt.target.value,
+ _items = [...items, { _id: pendingId, val }];
+ if (childFirst) {
+ setPendingId(null);
+ setItems(_items);
+ } else {
+ setItems(_items);
+ setPendingId(null);
+ }
+ },
+ [childFirst, setPendingId, setItems, items, pendingId]
+ );
+
+ return html`
+ <div class="item-editor">
+ ${items.map(
+ (item, idx) => html`
+ <input
+ key=${item._id}
+ value=${item.val}
+ oninput=${evt => {
+ let val = evt.target.value,
+ _items = [...items];
+ _items.splice(idx, 1, { ...item, val });
+ setItems(_items);
+ }}
+ />
+ `
+ )}
+
+ <input
+ key=${pendingId}
+ placeholder="type to add an item"
+ oninput=${onInput}
+ />
+ </div>
+ `;
+};
+
+const Parent = () => {
+ let [items, setItems] = useState([]);
+ return html`
+ <div><${Config} /><${Child} items=${items} setItems=${setItems} /></div>
+ `;
+};
+
+export default Parent;
diff --git a/preact/demo/style.css b/preact/demo/style.css
new file mode 100644
index 0000000..52f0b34
--- /dev/null
+++ b/preact/demo/style.css
@@ -0,0 +1,24 @@
+html, body {
+ font: 14px system-ui, sans-serif;
+}
+.list {
+ list-style: none;
+ padding: 0;
+}
+.list > li {
+ position: relative;
+ padding: 5px 10px;
+ animation: fadeIn 1s ease;
+}
+@keyframes fadeIn {
+ 0% {
+ box-shadow: inset 0 0 2px 2px red,
+ 0 0 2px 2px red;
+ }
+}
+.list > .odd {
+ background-color: #def;
+}
+.list > .even {
+ background-color: #fed;
+}
diff --git a/preact/demo/style.scss b/preact/demo/style.scss
new file mode 100644
index 0000000..8466bbd
--- /dev/null
+++ b/preact/demo/style.scss
@@ -0,0 +1,149 @@
+html, body {
+ height: 100%;
+ margin: 0;
+ background: #eee;
+ font: 400 16px/1.3 'Helvetica Neue',helvetica,sans-serif;
+ text-rendering: optimizeSpeed;
+ color: #444;
+}
+
+.app {
+ display: block;
+ flex-direction: column;
+ height: 100%;
+
+ > header {
+ flex: 0;
+ background: #f9f9f9;
+ box-shadow: inset 0 -.5px 0 0 rgba(0,0,0,0.2), 0 .5px 0 0 rgba(255,255,255,0.6);
+
+ nav {
+ display: inline-block;
+ padding: 4px 7px;
+
+ a {
+ display: inline-block;
+ margin: 2px;
+ padding: 4px 10px;
+ background-color: rgba(255,255,255,0);
+ border-radius: 1em;
+ color: #6b1d8f;
+ text-decoration: none;
+ // transition: all 250ms ease;
+ transition: all 250ms cubic-bezier(.2,0,.4,2);
+ &:hover {
+ background-color: rgba(255,255,255,1);
+ box-shadow: 0 0 0 2px #6b1d8f;
+ }
+ &.active {
+ background-color: #6b1d8f;
+ color: white;
+ }
+ }
+ }
+ }
+
+ > main {
+ flex: 1;
+ padding: 10px;
+ }
+}
+
+
+h1 {
+ margin: 0;
+ color: #6b1d8f;
+ font-weight: 300;
+ font-size: 250%;
+}
+
+
+input, textarea {
+ box-sizing: border-box;
+ margin: 1px;
+ padding: .25em .5em;
+ background: #fff;
+ border: 1px solid #999;
+ border-radius: 3px;
+ font: inherit;
+ color: #000;
+ outline: none;
+
+ &:focus {
+ border-color: #6b1d8f;
+ }
+}
+
+
+button, input[type="submit"], input[type="reset"], input[type="button"] {
+ box-sizing: border-box;
+ margin: 1px;
+ padding: .25em .8em;
+ background: #6b1d8f;
+ border: 1px solid #6b1d8f;
+ // border: none;
+ border-radius: 1.5em;
+ font: inherit;
+ color: white;
+ outline: none;
+ cursor: pointer;
+}
+
+
+.cursor {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 8px;
+ height: 8px;
+ margin: -5px 0 0 -5px;
+ border: 2px solid #F00;
+ border-radius: 50%;
+ transform-origin: 50% 50%;
+ pointer-events: none;
+ overflow: hidden;
+ font-size: 9px;
+ line-height: 25px;
+ text-indent: 15px;
+ white-space: nowrap;
+
+ &:not(.label) {
+ contain: strict;
+ }
+
+ &.label {
+ overflow: visible;
+ }
+
+ // &.big {
+ // transform: scale(2);
+ // // width: 24px;
+ // // height: 24px;
+ // // margin: -13px 0 0 -13px;
+ // }
+
+ .label {
+ position: absolute;
+ left: 0;
+ top: 0;
+ //transform: translateZ(0);
+ // z-index: 10;
+ }
+}
+
+
+.animation-picker {
+ position: fixed;
+ display: inline-block;
+ right: 0;
+ top: 0;
+ padding: 10px;
+ background: #000;
+ color: #BBB;
+ z-index: 1000;
+
+ select {
+ font-size: 100%;
+ margin-left: 5px;
+ }
+}
diff --git a/preact/demo/styled-components.js b/preact/demo/styled-components.js
new file mode 100644
index 0000000..f8433ba
--- /dev/null
+++ b/preact/demo/styled-components.js
@@ -0,0 +1,31 @@
+import { createElement } from 'preact';
+import styled, { css } from 'styled-components';
+
+const Button = styled.button`
+ background: transparent;
+ border-radius: 3px;
+ border: 2px solid palevioletred;
+ color: palevioletred;
+ margin: 0.5em 1em;
+ padding: 0.25em 1em;
+
+ ${props =>
+ props.primary &&
+ css`
+ background: palevioletred;
+ color: white;
+ `}
+`;
+
+const Container = styled.div`
+ text-align: center;
+`;
+
+export default function StyledComp() {
+ return (
+ <Container>
+ <Button>Normal Button</Button>
+ <Button primary>Primary Button</Button>
+ </Container>
+ );
+}
diff --git a/preact/demo/suspense-router/bye.js b/preact/demo/suspense-router/bye.js
new file mode 100644
index 0000000..fe2a49c
--- /dev/null
+++ b/preact/demo/suspense-router/bye.js
@@ -0,0 +1,12 @@
+import { createElement } from 'react';
+import { Link } from './simple-router';
+
+/** @jsx createElement */
+
+export default function Bye() {
+ return (
+ <div>
+ Bye! <Link to="/">Go to Hello!</Link>
+ </div>
+ );
+}
diff --git a/preact/demo/suspense-router/hello.js b/preact/demo/suspense-router/hello.js
new file mode 100644
index 0000000..2566dd3
--- /dev/null
+++ b/preact/demo/suspense-router/hello.js
@@ -0,0 +1,12 @@
+import { createElement } from 'react';
+import { Link } from './simple-router';
+
+/** @jsx createElement */
+
+export default function Hello() {
+ return (
+ <div>
+ Hello! <Link to="/bye">Go to Bye!</Link>
+ </div>
+ );
+}
diff --git a/preact/demo/suspense-router/index.js b/preact/demo/suspense-router/index.js
new file mode 100644
index 0000000..2572209
--- /dev/null
+++ b/preact/demo/suspense-router/index.js
@@ -0,0 +1,30 @@
+import { createElement, Suspense, lazy } from 'react';
+
+import { Router, Route, Switch } from './simple-router';
+
+/** @jsx createElement */
+
+let Hello = lazy(() => import('./hello.js'));
+let Bye = lazy(() => import('./bye.js'));
+
+function Loading() {
+ return <div>Hey! This is a fallback because we're loading things! :D</div>;
+}
+
+export default function SuspenseRouterBug() {
+ return (
+ <Router>
+ <h1>Suspense Router bug</h1>
+ <Suspense fallback={<Loading />}>
+ <Switch>
+ <Route path="/" exact>
+ <Hello />
+ </Route>
+ <Route path="/bye">
+ <Bye />
+ </Route>
+ </Switch>
+ </Suspense>
+ </Router>
+ );
+}
diff --git a/preact/demo/suspense-router/simple-router.js b/preact/demo/suspense-router/simple-router.js
new file mode 100644
index 0000000..ed48ea6
--- /dev/null
+++ b/preact/demo/suspense-router/simple-router.js
@@ -0,0 +1,87 @@
+import {
+ createElement,
+ cloneElement,
+ createContext,
+ useState,
+ useContext,
+ Children,
+ useLayoutEffect
+} from 'react';
+
+/** @jsx createElement */
+
+const memoryHistory = {
+ /**
+ * @typedef {{ pathname: string }} Location
+ * @typedef {(location: Location) => void} HistoryListener
+ * @type {HistoryListener[]}
+ */
+ listeners: [],
+
+ /**
+ * @param {HistoryListener} listener
+ */
+ listen(listener) {
+ const newLength = this.listeners.push(listener);
+ return () => this.listeners.splice(newLength - 1, 1);
+ },
+
+ /**
+ * @param {Location} to
+ */
+ navigate(to) {
+ this.listeners.forEach(listener => listener(to));
+ }
+};
+
+/** @type {import('react').Context<{ history: typeof memoryHistory; location: Location }>} */
+const RouterContext = createContext(null);
+
+export function Router({ history = memoryHistory, children }) {
+ const [location, setLocation] = useState({ pathname: '/' });
+
+ useLayoutEffect(() => {
+ return history.listen(newLocation => setLocation(newLocation));
+ }, []);
+
+ return (
+ <RouterContext.Provider value={{ history, location }}>
+ {children}
+ </RouterContext.Provider>
+ );
+}
+
+export function Switch(props) {
+ const { location } = useContext(RouterContext);
+
+ let element = null;
+ Children.forEach(props.children, child => {
+ if (element == null && child.props.path == location.pathname) {
+ element = child;
+ }
+ });
+
+ return element;
+}
+
+/**
+ * @param {{ children: any; path: string; exact?: boolean; }} props
+ */
+export function Route({ children, path, exact }) {
+ return children;
+}
+
+export function Link({ to, children }) {
+ const { history } = useContext(RouterContext);
+ const onClick = event => {
+ event.preventDefault();
+ event.stopPropagation();
+ history.navigate({ pathname: to });
+ };
+
+ return (
+ <a href={to} onClick={onClick}>
+ {children}
+ </a>
+ );
+}
diff --git a/preact/demo/suspense.js b/preact/demo/suspense.js
new file mode 100644
index 0000000..9b620fa
--- /dev/null
+++ b/preact/demo/suspense.js
@@ -0,0 +1,97 @@
+// eslint-disable-next-line no-unused-vars
+import {
+ createElement,
+ Component,
+ memo,
+ Fragment,
+ Suspense,
+ lazy
+} from 'react';
+
+function LazyComp() {
+ return <div>I'm (fake) lazy loaded</div>;
+}
+
+const Lazy = lazy(() => Promise.resolve({ default: LazyComp }));
+
+function createSuspension(name, timeout, error) {
+ let done = false;
+ let prom;
+
+ return {
+ name,
+ timeout,
+ start: () => {
+ if (!prom) {
+ prom = new Promise((res, rej) => {
+ setTimeout(() => {
+ done = true;
+ if (error) {
+ rej(error);
+ } else {
+ res();
+ }
+ }, timeout);
+ });
+ }
+
+ return prom;
+ },
+ getPromise: () => prom,
+ isDone: () => done
+ };
+}
+
+function CustomSuspense({ isDone, start, timeout, name }) {
+ if (!isDone()) {
+ throw start();
+ }
+
+ return (
+ <div>
+ Hello from CustomSuspense {name}, loaded after {timeout / 1000}s
+ </div>
+ );
+}
+
+function init() {
+ return {
+ s1: createSuspension('1', 1000, null),
+ s2: createSuspension('2', 2000, null),
+ s3: createSuspension('3', 3000, null)
+ };
+}
+
+export default class DevtoolsDemo extends Component {
+ constructor(props) {
+ super(props);
+ this.state = init();
+ this.onRerun = this.onRerun.bind(this);
+ }
+
+ onRerun() {
+ this.setState(init());
+ }
+
+ render(props, state) {
+ return (
+ <div>
+ <h1>lazy()</h1>
+ <Suspense fallback={<div>Loading (fake) lazy loaded component...</div>}>
+ <Lazy />
+ </Suspense>
+ <h1>Suspense</h1>
+ <div>
+ <button onClick={this.onRerun}>Rerun</button>
+ </div>
+ <Suspense fallback={<div>Fallback 1</div>}>
+ <CustomSuspense {...state.s1} />
+ <Suspense fallback={<div>Fallback 2</div>}>
+ <CustomSuspense {...state.s2} />
+ <CustomSuspense {...state.s3} />
+ </Suspense>
+ </Suspense>
+ </div>
+ );
+ }
+}
diff --git a/preact/demo/textFields.js b/preact/demo/textFields.js
new file mode 100644
index 0000000..58f69f1
--- /dev/null
+++ b/preact/demo/textFields.js
@@ -0,0 +1,37 @@
+import React, { useState } from 'react';
+import TextField from '@material-ui/core/TextField';
+
+/** @jsx React.createElement */
+
+const PatchedTextField = props => {
+ const [value, set] = useState(props.value);
+ return (
+ <TextField {...props} value={value} onChange={e => set(e.target.value)} />
+ );
+};
+
+const TextFields = () => (
+ <div>
+ <TextField
+ variant="outlined"
+ margin="normal"
+ fullWidth
+ label="Cannot type in"
+ />
+ <PatchedTextField
+ variant="outlined"
+ margin="normal"
+ fullWidth
+ label="I can"
+ />
+ <TextField
+ defaultValue="Reset after blur or empty"
+ variant="outlined"
+ margin="normal"
+ fullWidth
+ label="default value"
+ />
+ </div>
+);
+
+export default TextFields;
diff --git a/preact/demo/todo.js b/preact/demo/todo.js
new file mode 100644
index 0000000..189b4e8
--- /dev/null
+++ b/preact/demo/todo.js
@@ -0,0 +1,47 @@
+import { createElement, Component } from 'preact';
+
+let counter = 0;
+
+export default class TodoList extends Component {
+ state = { todos: [], text: '' };
+
+ setText = e => {
+ this.setState({ text: e.target.value });
+ };
+
+ addTodo = () => {
+ let { todos, text } = this.state;
+ todos = todos.concat({ text, id: ++counter });
+ this.setState({ todos, text: '' });
+ };
+
+ removeTodo = e => {
+ let id = e.target.getAttribute('data-id');
+ this.setState({ todos: this.state.todos.filter(t => t.id != id) });
+ };
+
+ render({}, { todos, text }) {
+ return (
+ <form onSubmit={this.addTodo} action="javascript:">
+ <input value={text} onInput={this.setText} />
+ <button type="submit">Add</button>
+ <ul>
+ <TodoItems todos={todos} removeTodo={this.removeTodo} />
+ </ul>
+ </form>
+ );
+ }
+}
+
+class TodoItems extends Component {
+ render({ todos, removeTodo }) {
+ return todos.map(todo => (
+ <li key={todo.id}>
+ <button type="button" onClick={removeTodo} data-id={todo.id}>
+ &times;
+ </button>{' '}
+ {todo.text}
+ </li>
+ ));
+ }
+}
diff --git a/preact/demo/tsconfig.json b/preact/demo/tsconfig.json
new file mode 100644
index 0000000..b252efc
--- /dev/null
+++ b/preact/demo/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "experimentalDecorators": true,
+ "jsx": "react",
+ "jsxFactory": "h",
+ "baseUrl": ".",
+ "target": "es2018",
+ "module": "es2015",
+ "moduleResolution": "node",
+ "paths": {
+ "preact/hooks": ["../hooks/src/index.js"],
+ "preact": ["../src/index.js"],
+ }
+ }
+}
diff --git a/preact/demo/webpack.config.js b/preact/demo/webpack.config.js
new file mode 100644
index 0000000..063f507
--- /dev/null
+++ b/preact/demo/webpack.config.js
@@ -0,0 +1,112 @@
+/* eslint-disable */
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+const preact = path.join(__dirname, '..', 'src');
+const compat = path.join(__dirname, '..', 'compat', 'src');
+
+module.exports = {
+ context: __dirname,
+ entry: './index',
+ output: {
+ publicPath: '/'
+ },
+ resolve: {
+ alias: {
+ ['preact/debug']: path.join(__dirname, '..', 'debug'),
+ ['preact/devtools']: path.join(__dirname, '..', 'devtools'),
+ ['preact/hooks']: path.join(__dirname, '..', 'hooks', 'src'),
+ preact: preact,
+ react: compat,
+ 'react-dom': compat
+ },
+ extensions: ['.tsx', '.ts', '.js']
+ },
+ module: {
+ rules: [
+ {
+ test: /\.tsx?$/,
+ loader: 'babel-loader',
+ options: {
+ sourceMap: true,
+ presets: [
+ [require.resolve('@babel/preset-typescript'), { jsxPragma: 'h' }],
+ [
+ require.resolve('@babel/preset-env'),
+ {
+ targets: {
+ browsers: ['last 2 versions', 'IE >= 9']
+ },
+ modules: false,
+ loose: true
+ }
+ ],
+ [require.resolve('@babel/preset-react')]
+ ],
+ plugins: [
+ [require.resolve('@babel/plugin-transform-runtime')],
+ [require.resolve('@babel/plugin-transform-react-jsx-source')],
+ [
+ require.resolve('@babel/plugin-transform-react-jsx'),
+ { pragma: 'h', pragmaFrag: 'Fragment' }
+ ],
+ [
+ require.resolve('@babel/plugin-proposal-decorators'),
+ { legacy: true }
+ ],
+ [
+ require.resolve('@babel/plugin-proposal-class-properties'),
+ { loose: true }
+ ]
+ ]
+ }
+ },
+ {
+ test: /\.js$/,
+ loader: 'babel-loader',
+ options: {
+ sourceMap: true,
+ presets: [
+ [
+ require.resolve('@babel/preset-env'),
+ {
+ targets: {
+ browsers: ['last 2 versions', 'IE >= 9']
+ },
+ modules: false,
+ loose: true
+ }
+ ],
+ [require.resolve('@babel/preset-react')]
+ ],
+ plugins: [
+ [require.resolve('@babel/plugin-transform-react-jsx-source')],
+ [
+ require.resolve('@babel/plugin-transform-react-jsx'),
+ { pragma: 'createElement', pragmaFrag: 'Fragment' }
+ ],
+ [require.resolve('@babel/plugin-proposal-class-properties')],
+ [
+ require.resolve('@babel/plugin-transform-react-constant-elements')
+ ],
+ [require.resolve('@babel/plugin-syntax-dynamic-import')]
+ ]
+ }
+ },
+ {
+ test: /\.s?css$/,
+ use: ['style-loader', 'css-loader', 'sass-loader']
+ }
+ ]
+ },
+ devtool: 'inline-source-map',
+ node: {
+ process: 'mock',
+ Buffer: false,
+ setImmediate: false
+ },
+ devServer: {
+ historyApiFallback: true
+ },
+ plugins: [new HtmlWebpackPlugin()]
+};