aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2021-02-05 18:32:12 +0000
committers-ol <s-ol@users.noreply.github.com>2021-02-05 18:42:02 +0000
commit464174b7e5e4e5c389c896173e8c7ab21badda8a (patch)
tree0f88ffc3db86026f86cb2941e4a05bd8a2753903
parentlinks leave shadow (diff)
downloadfedidag-464174b7e5e4e5c389c896173e8c7ab21badda8a.tar.gz
fedidag-464174b7e5e4e5c389c896173e8c7ab21badda8a.zip
clean up UI code
-rw-r--r--src/index.js66
-rw-r--r--src/layout.js62
-rw-r--r--src/ui.js171
-rw-r--r--src/ui/Links.js50
-rw-r--r--src/ui/Note.js155
-rw-r--r--src/ui/css.js (renamed from src/dom.js)2
-rw-r--r--src/ui/index.js10
-rw-r--r--src/ui/theme.js26
8 files changed, 282 insertions, 260 deletions
diff --git a/src/index.js b/src/index.js
index 5240fbc..090b67a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,13 +1,12 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import * as jsonld from 'jsonld';
-import * as dom from './dom';
import Layout from './layout';
-import { Note } from './ui';
+import { css, Links, Note } from './ui';
import { render, h, Fragment, Component } from 'preact';
import { useRef, useState, useLayoutEffect, useEffect } from 'preact/hooks';
-dom.css`
+css`
article > div {
position: relative;
}
@@ -29,53 +28,6 @@ const context = [
},
];
-const LinksCanvas = ({ links, ...props }) => {
- const canvas = useRef(null);
-
- useLayoutEffect(() => {
- const ctx = canvas.current.getContext('2d');
- ctx.lineWidth = 4;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- ctx.strokeStyle = '#696969';
- ctx.fillStyle = '#696969';
- ctx.shadowBlur = 5;
- ctx.shadowOffsetX = 3;
- ctx.shadowOffsetY = -2;
-
- ctx.clearRect(0, 0, canvas.current.width, canvas.current.height);
-
- for (const points of links) {
- ctx.shadowColor = '#000000';
- ctx.beginPath();
- ctx.moveTo(...points[1]);
- for (let i = 4; i < points.length; i += 3) {
- const [ax, ay] = points[i - 2];
- const [bx, by] = points[i - 1];
- const [x, y] = points[i];
- ctx.bezierCurveTo(ax, ay, bx, by, x, y);
- }
- ctx.stroke();
-
- const [lx, ly] = points[points.length - 1];
- const [tx, ty] = points[0];
- const [dx, dy] = [tx - lx, ty - ly];
- const [hx, hy] = [dy*0.7, dx*-0.7];
-
- ctx.shadowColor = 'transparent';
- ctx.beginPath();
- ctx.moveTo(lx + dx*0.8, ly + dy*0.8);
- ctx.lineTo(lx - dx*0.5 + hx, ly - dy*0.5 + hy);
- ctx.lineTo(lx - dx*0.5 - hx, ly - dy*0.5 - hy);
- ctx.fill();
- }
- });
-
- return (
- <canvas {...props} ref={canvas} />
- );
-};
-
class GraphContainer extends Component {
state = {
name: "loading…",
@@ -139,31 +91,31 @@ class GraphContainer extends Component {
}
render() {
- return this.props.render(this.state);
+ return this.props.render(this.state);
}
}
const App = ({ discussion }) => {
const { name, items } = discussion;
const [{ width, height, positions, links }, setState] = useState({
- width: 0,
- height: 0,
- positions: {},
- links: [],
+ width: 0,
+ height: 0,
+ positions: {},
+ links: [],
});
const layout = new Layout();
const setNodeSize = layout.addNode.bind(layout);
useEffect(() => {
- layout.render().then(setState);
+ layout.render().then(setState);
}, [ items ]);
return (
<article>
<h1>{name}</h1>
<div>
- <LinksCanvas width={width} height={height} links={links} />
+ <Links width={width} height={height} links={links} />
<div>
{Object.values(items).map((item) => {
const onlyParent = item.inReplyTo.length === 1 && items[item.inReplyTo[0].id];
diff --git a/src/layout.js b/src/layout.js
index b774c31..000d22c 100644
--- a/src/layout.js
+++ b/src/layout.js
@@ -6,41 +6,41 @@ const gv = new Viz({ workerURL: 'lib/lite.render.js' });
export default class Layout {
constructor() {
this.src = 'digraph { node [shape="rectangle"];\n';
- }
+ }
- addNode(id, width, height) {
- this.src += ` "${id}" [fixedsize=true; width=${width / 72}; height=${height / 72}];\n`;
- }
+ addNode(id, width, height) {
+ this.src += ` "${id}" [fixedsize=true; width=${width / 72}; height=${height / 72}];\n`;
+ }
- addLink(from, to) {
- this.src += ` "${from}" -> "${to}";\n`;
- }
+ addLink(from, to) {
+ this.src += ` "${from}" -> "${to}";\n`;
+ }
- async render() {
- this.src += '}';
+ async render() {
+ this.src += '}';
- const info = await gv.renderJSONObject(this.src, { format: 'json0' });
- const [width, height] = info.bb.split(',').map(toNumber).slice(2);
+ const info = await gv.renderJSONObject(this.src, { format: 'json0' });
+ const [width, height] = info.bb.split(',').map(toNumber).slice(2);
const positions = {};
- for (const object of info.objects) {
- const [x, y] = object.pos.split(',').map(toNumber);
- positions[object.name] = [ x, height - y ];
- }
-
- const links = info.edges.map((edge) => {
- const pos = edge.pos.slice(2);
- return pos.split(' ').map(p => {
- const [x, y] = p.split(',');
- return [toNumber(x), height - toNumber(y)];
- });
-
- });
-
- return {
- width, height,
- positions,
- links,
- };
- }
+ for (const object of info.objects) {
+ const [x, y] = object.pos.split(',').map(toNumber);
+ positions[object.name] = [ x, height - y ];
+ }
+
+ const links = info.edges.map((edge) => {
+ const pos = edge.pos.slice(2);
+ return pos.split(' ').map(p => {
+ const [x, y] = p.split(',');
+ return [toNumber(x), height - toNumber(y)];
+ });
+
+ });
+
+ return {
+ width, height,
+ positions,
+ links,
+ };
+ }
}
diff --git a/src/ui.js b/src/ui.js
deleted file mode 100644
index ce707b7..0000000
--- a/src/ui.js
+++ /dev/null
@@ -1,171 +0,0 @@
-import { render, h, Fragment } from 'preact';
-import { useRef, useLayoutEffect } from 'preact/hooks';
-import ColorHash from 'color-hash';
-import { formatRelative } from 'date-fns';
-import * as dom from './dom';
-
-dom.css`
-body {
- --theme-backdrop: #1e1e1e;
- --theme-fg: #eeeeee;
- --theme-note-bg: #eeeeee;
- --theme-note-fg: #363636;
- --theme-header-fg: #121212;
-}
-
-body.darktheme {
- --theme-backdrop: #1e1e1e;
- --theme-note-bg: #363636;
- --theme-note-fg: #eeeeee;
- --theme-header-fg: #121212;
-}
-
-body {
- color: var(--theme-fg);
- background: var(--theme-backdrop);
- font-family: sans-serif;
- font-size: 11px;
-}
-`;
-
-const now = new Date();
-const Time = ({ time }) => {
- const dt = new Date(time);
- return formatRelative(dt, now);
-};
-
-dom.css`
-.flex-space {
- flex: 1;
- margin: 0;
-}
-`;
-
-const Space = () => <div class="flex-space" />;
-
-dom.css`
-section {
- position: absolute;
- overflow: hidden;
-
- width: 22em;
- color: var(--theme-note-fg);
- background: var(--theme-note-bg);
-
- border-radius: 0.4em;
-}
-
-section.type-Tombstone {
- color: var(--theme-note-bg);
- background: var(--theme-note-fg);
-}
-
-section > header {
- display: flex;
- flex-direction: row;
-
- height: 1.75rem;
-
- color: var(--theme-header-fg);
- background: var(--theme-header-bg);
-
- cursor: pointer;
- transition: opacity 0.3s;
-}
-
-section > header:hover {
- opacity: 0.5;
-}
-
-section > header.small {
- height: 0.5em;
-}
-
-section > header > * {
- margin: 0 0.5em;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- line-height: 1.75rem;
-}
-
-section > header > img {
- height: 2em;
- width: 2em;
- flex: 0 0 2em;
- margin: 0.25em;
- border-radius: 0.2em;
-}
-
-section > header > .user {
- flex: 0 0.5 auto;
-}
-
-section > header > .time {
- flex: 0 1 auto;
- font-size: 0.8em;
- color: #363636;
-}
-
-section > main {
- padding: 1em;
- font-family: serif;
-}
-
-section.tombstone > main {
- font-family: inherit;
-}
-
-section.hidden {
- display: none;
-}
-
-section.collapsed {
- width: auto;
-}
-section.collapsed > main {
- display: none;
-}
-`;
-
-const colors = new ColorHash({ lightness: 0.8 });
-export const Note = ({
- id, type, attributedTo, published, content,
- smallHeader = false, position,
- onHide, onSize,
-}) => {
- const backgroundColor = attributedTo.color || colors.hex(attributedTo.id);
- const username = attributedTo.name || attributedTo.preferredUsername;
-
- const root = useRef(null);
- useLayoutEffect(() => {
- onSize(id, root.current.offsetWidth, root.current.offsetHeight);
- });
-
- return (
- <section
- style={{
- '--theme-header-bg': backgroundColor,
- 'left': position && `${position[0] - root.current.offsetWidth / 2}px`,
- 'top': position && `${position[1] - root.current.offsetHeight / 2}px`,
- 'visibility': position ? 'visible' : 'hidden',
- }}
- className={`type-${type}`}
- data-id={id}
- ref={root}
- >
- {smallHeader
- ? <header className="small" onClick={onHide} />
- : <header onClick={onHide}>
- <img src={new URL(attributedTo.icon.url, attributedTo.id)} />
- <span className="user">{username}</span>
- <Space />
- <span className="time"><Time time={published} /></span>
- </header>
- }
- <main>
- {content}
- </main>
- </section>
- );
-};
diff --git a/src/ui/Links.js b/src/ui/Links.js
new file mode 100644
index 0000000..dfd981f
--- /dev/null
+++ b/src/ui/Links.js
@@ -0,0 +1,50 @@
+import { render, h, Fragment } from 'preact';
+import { useRef, useLayoutEffect } from 'preact/hooks';
+
+export const Links = ({ links, ...props }) => {
+ const canvas = useRef(null);
+
+ useLayoutEffect(() => {
+ const ctx = canvas.current.getContext('2d');
+ ctx.lineWidth = 4;
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ ctx.strokeStyle = '#696969';
+ ctx.fillStyle = '#696969';
+ ctx.shadowBlur = 2;
+ ctx.shadowOffsetX = 2;
+ ctx.shadowOffsetY = 4;
+
+ ctx.clearRect(0, 0, canvas.current.width, canvas.current.height);
+
+ for (const points of links) {
+ ctx.shadowColor = 'rgba(0,0,0, 0.7)';
+ ctx.beginPath();
+ ctx.moveTo(...points[1]);
+ for (let i = 4; i < points.length; i += 3) {
+ const [ax, ay] = points[i - 2];
+ const [bx, by] = points[i - 1];
+ const [x, y] = points[i];
+ ctx.bezierCurveTo(ax, ay, bx, by, x, y);
+ }
+ ctx.stroke();
+
+ const [lx, ly] = points[points.length - 1];
+ const [tx, ty] = points[0];
+ const [dx, dy] = [tx - lx, ty - ly];
+ const [hx, hy] = [dy*0.7, dx*-0.7];
+
+ ctx.shadowColor = 'transparent';
+ ctx.beginPath();
+ ctx.moveTo(lx + dx*0.8, ly + dy*0.8);
+ ctx.lineTo(lx - dx*0.5 + hx, ly - dy*0.5 + hy);
+ ctx.lineTo(lx - dx*0.5 - hx, ly - dy*0.5 - hy);
+ ctx.fill();
+ }
+ });
+
+ return (
+ <canvas {...props} ref={canvas} />
+ );
+};
+
diff --git a/src/ui/Note.js b/src/ui/Note.js
new file mode 100644
index 0000000..b7387a8
--- /dev/null
+++ b/src/ui/Note.js
@@ -0,0 +1,155 @@
+import { render, h, Fragment } from 'preact';
+import { useRef, useLayoutEffect } from 'preact/hooks';
+import ColorHash from 'color-hash';
+import { formatRelative } from 'date-fns';
+import css from './css';
+
+const now = new Date();
+const Time = ({ time }) => {
+ const dt = new Date(time);
+ return formatRelative(dt, now);
+};
+
+css`
+.flex-space {
+ flex: 1;
+ margin: 0;
+}
+`;
+const Space = () => <div class="flex-space" />;
+
+css`
+section > header {
+ display: flex;
+ flex-direction: row;
+
+ height: 1.75rem;
+
+ color: var(--theme-header-fg);
+ background: var(--theme-header-bg);
+
+ cursor: pointer;
+ transition: opacity 0.3s;
+}
+
+section > header:hover {
+ opacity: 0.5;
+}
+
+section > header.small {
+ height: 0.5em;
+}
+
+section > header > * {
+ margin: 0 0.5em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 1.75rem;
+}
+
+section > header > img {
+ height: 1.5rem;
+ width: 1.5rem;
+ flex: 0 0 1.5rem;
+ margin: 0.125rem;
+ border-radius: 0.2rem;
+}
+
+section > header > .user {
+ flex: 0 0.5 auto;
+}
+
+section > header > .time {
+ flex: 0 1 auto;
+ font-size: 0.8em;
+ color: #363636;
+}
+`;
+const Header = ({ user, published, onClick }) => {
+ const username = user.name || user.preferredUsername;
+
+ return (
+ <header onClick={onClick}>
+ <img src={new URL(user.icon.url, user.id)} />
+ <span className="user">{username}</span>
+ <Space />
+ <span className="time"><Time time={published} /></span>
+ </header>
+ );
+};
+
+css`
+section {
+ position: absolute;
+ overflow: hidden;
+
+ width: 22em;
+ color: var(--theme-note-fg);
+ background: var(--theme-note-bg);
+ box-shadow: rgba(0,0,0, 0.7) 2px 4px 5px;
+
+ border-radius: 0.4em;
+}
+
+section.type-Tombstone {
+ color: var(--theme-note-bg);
+ background: var(--theme-note-fg);
+}
+
+section > main {
+ padding: 1em;
+ font-family: serif;
+}
+
+section.tombstone > main {
+ font-family: inherit;
+}
+
+section.hidden {
+ display: none;
+}
+
+section.collapsed {
+ width: auto;
+}
+section.collapsed > main {
+ display: none;
+}
+`;
+
+const colors = new ColorHash({ lightness: 0.8 });
+export const Note = ({
+ id, type, attributedTo, published, content,
+ smallHeader = false, position,
+ onHide, onSize,
+}) => {
+ const backgroundColor = attributedTo.color || colors.hex(attributedTo.id);
+
+ const root = useRef(null);
+ useLayoutEffect(() => {
+ onSize(id, root.current.offsetWidth, root.current.offsetHeight);
+ });
+
+ return (
+ <section
+ style={{
+ '--theme-header-bg': backgroundColor,
+ 'left': position && `${position[0] - root.current.offsetWidth / 2}px`,
+ 'top': position && `${position[1] - root.current.offsetHeight / 2}px`,
+ 'visibility': position ? 'visible' : 'hidden',
+ }}
+ className={`type-${type}`}
+ data-id={id}
+ ref={root}
+ >
+ {smallHeader
+ ? <header className="small" onClick={onHide} />
+ : <Header onClick={onHide} user={attributedTo} published={published} />
+ }
+ <main>
+ {content}
+ </main>
+ </section>
+ );
+};
diff --git a/src/dom.js b/src/ui/css.js
index 45f8a7a..e793c4f 100644
--- a/src/dom.js
+++ b/src/ui/css.js
@@ -1,4 +1,4 @@
-export const css = ([ code] ) => {
+export default ([ code] ) => {
const style = document.createElement('style');
document.head.appendChild(style);
style.appendChild(document.createTextNode(''));
diff --git a/src/ui/index.js b/src/ui/index.js
new file mode 100644
index 0000000..3224b71
--- /dev/null
+++ b/src/ui/index.js
@@ -0,0 +1,10 @@
+import './theme';
+import { Links } from './Links';
+import { Note } from './Note';
+import css from './css';
+
+export {
+ Links,
+ Note,
+ css,
+};
diff --git a/src/ui/theme.js b/src/ui/theme.js
new file mode 100644
index 0000000..372cd46
--- /dev/null
+++ b/src/ui/theme.js
@@ -0,0 +1,26 @@
+import css from './css';
+
+css`
+body {
+ --theme-backdrop: #1e1e1e;
+ --theme-fg: #eeeeee;
+ --theme-note-bg: #eeeeee;
+ --theme-note-fg: #363636;
+ --theme-header-fg: #121212;
+}
+
+body.darktheme {
+ --theme-backdrop: #1e1e1e;
+ --theme-note-bg: #363636;
+ --theme-note-fg: #eeeeee;
+ --theme-header-fg: #121212;
+}
+
+body {
+ color: var(--theme-fg);
+ background: var(--theme-backdrop);
+ font-family: sans-serif;
+ font-size: 11px;
+}
+`;
+