diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2021-02-05 18:32:12 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2021-02-05 18:42:02 +0000 |
| commit | 464174b7e5e4e5c389c896173e8c7ab21badda8a (patch) | |
| tree | 0f88ffc3db86026f86cb2941e4a05bd8a2753903 | |
| parent | links leave shadow (diff) | |
| download | fedidag-464174b7e5e4e5c389c896173e8c7ab21badda8a.tar.gz fedidag-464174b7e5e4e5c389c896173e8c7ab21badda8a.zip | |
clean up UI code
| -rw-r--r-- | src/index.js | 66 | ||||
| -rw-r--r-- | src/layout.js | 62 | ||||
| -rw-r--r-- | src/ui.js | 171 | ||||
| -rw-r--r-- | src/ui/Links.js | 50 | ||||
| -rw-r--r-- | src/ui/Note.js | 155 | ||||
| -rw-r--r-- | src/ui/css.js (renamed from src/dom.js) | 2 | ||||
| -rw-r--r-- | src/ui/index.js | 10 | ||||
| -rw-r--r-- | src/ui/theme.js | 26 |
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; +} +`; + |
