diff options
Diffstat (limited to 'src/ui')
| -rw-r--r-- | src/ui/Links.js | 50 | ||||
| -rw-r--r-- | src/ui/Note.js | 155 | ||||
| -rw-r--r-- | src/ui/css.js | 13 | ||||
| -rw-r--r-- | src/ui/index.js | 10 | ||||
| -rw-r--r-- | src/ui/theme.js | 26 |
5 files changed, 254 insertions, 0 deletions
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/ui/css.js b/src/ui/css.js new file mode 100644 index 0000000..e793c4f --- /dev/null +++ b/src/ui/css.js @@ -0,0 +1,13 @@ +export default ([ code] ) => { + const style = document.createElement('style'); + document.head.appendChild(style); + style.appendChild(document.createTextNode('')); + + let i = 0; + for (const v of code.split('}')) { + if (v.indexOf('{') < 0) continue; + style.sheet.insertRule(v + '}', i++); + } + + return style; +}; 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; +} +`; + |
