diff options
| -rw-r--r-- | src/index.js | 52 | ||||
| -rw-r--r-- | src/ui/Discussion.js | 24 | ||||
| -rw-r--r-- | src/ui/Note.js | 148 | ||||
| -rw-r--r-- | src/ui/Selection.js | 90 | ||||
| -rw-r--r-- | src/ui/index.js | 2 | ||||
| -rw-r--r-- | src/ui/theme.js | 21 |
6 files changed, 274 insertions, 63 deletions
diff --git a/src/index.js b/src/index.js index ba50149..b32054e 100644 --- a/src/index.js +++ b/src/index.js @@ -2,8 +2,8 @@ import 'preact/debug'; import 'core-js/stable'; import 'regenerator-runtime/runtime'; import * as jsonld from 'jsonld'; -import { h, Component, render } from 'preact'; -import { Discussion } from './ui'; +import { h, Fragment, Component, render } from 'preact'; +import { Discussion, Selection } from './ui'; const context = [ 'https://www.w3.org/ns/activitystreams', @@ -154,6 +154,27 @@ class GraphContainer extends Component { } } +class SelectionContainer extends Component { + state = { + selection: [], + }; + + toggle = (id) => { + const { selection } = this.state; + if (selection.indexOf(id) < 0) + this.setState({ selection: [ ...selection, id ] }); + else + this.setState({ selection: selection.filter(i => i !== id) }); + } + + render() { + return this.props.render({ + ...this.state, + toggleSelected: this.toggle, + }); + } +} + class CollapseContainer extends Component { state = {}; @@ -193,17 +214,26 @@ const search = new URLSearchParams(window.location.search); const graph = search.has('graph') ? search.get('graph') : 'lib/graph.json'; const app = ( <GraphContainer graph={graph} render={({ name, items }) => ( - <CollapseContainer - items={items} - render={({ collapsed, toggleCollapsed }) => ( - <Discussion - name={name} + <SelectionContainer render={({ selection, toggleSelected }) => ( + <> + <CollapseContainer items={items} - collapsed={collapsed} - toggleCollapsed={toggleCollapsed} + render={({ collapsed, toggleCollapsed }) => ( + <Discussion + name={name} + items={items} + collapsed={collapsed} + toggleCollapsed={toggleCollapsed} + toggleSelected={toggleSelected} + /> + )} + /> + <Selection + items={selection.map(id => items[id])} + toggleSelected={toggleSelected} /> - )} - /> + </> + )} /> )} /> ); render(app, document.body); diff --git a/src/ui/Discussion.js b/src/ui/Discussion.js index 2cea1c1..3176c03 100644 --- a/src/ui/Discussion.js +++ b/src/ui/Discussion.js @@ -6,20 +6,23 @@ import { Links } from './Links'; import { Note } from './Note'; css` -article > div { - position: relative; +article { + display: flex; + flex-direction: column; + + overflow: auto; + flex: 1 1 auto; } -article > div > canvas { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; +article > div { + position: relative; } `; -export const Discussion = ({ name, items, collapsed, toggleCollapsed }) => { +export const Discussion = ({ + name, items, collapsed, + toggleCollapsed, toggleSelected, +}) => { const [{ width, height, positions, links }, setState] = useState({ width: 0, height: 0, @@ -67,7 +70,8 @@ export const Discussion = ({ name, items, collapsed, toggleCollapsed }) => { } collapsed={isCollapsed} position={positions[item.id]} - onHide={toggleCollapsed} + onCollapse={toggleCollapsed} + onSelect={toggleSelected} onSize={setNodeSize} /> ); diff --git a/src/ui/Note.js b/src/ui/Note.js index 6bdec04..404979c 100644 --- a/src/ui/Note.js +++ b/src/ui/Note.js @@ -17,44 +17,59 @@ css` margin: 0; } `; -const Space = () => <div class="flex-space" />; +const Space = (props) => <div class="flex-space" {...props} />; css` section > header { display: flex; flex-direction: row; - height: 1.75rem; + height: 2.5rem; 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; + height: 0.75rem; } section > header > * { - margin: 0 0.5em; + margin: 0 0.5rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - line-height: 1.75rem; + line-height: 2.5rem; } -section > header > img { - height: 1.5rem; - width: 1.5rem; - flex: 0 0 1.5rem; - margin: 0.125rem; - border-radius: 0.2rem; +section > header > .avatar { + height: 2rem; + width: 2rem; + flex: 0 0 auto; + margin: 0.25rem; + border-radius: 0.3rem; +} +section > header > .avatar img { + width: inherit; + height: inherit; +} +section > header > .collapse { + height: 1rem; + width: 1rem; + flex: 0 0 auto; + margin: 0.6rem 0.5rem; + border-radius: 0.3rem; + border: 0.05rem solid var(--theme-header-fg); + + line-height: 0.8rem; + text-align: center; + text-decoration: none; + text-weight: bold; + + transition: background 0.3s; +} +section > header > .collapse:hover { + background: var(--theme-note-bg); } section > header > .user { @@ -67,30 +82,45 @@ section > header > .time { color: #363636; } `; -const Header = ({ user, published, onClick }) => { +const Header = ({ id, user, published, collapsed, onCollapse }) => { const username = user.name || user.preferredUsername; + const onClick = onCollapse && ((e) => { + e.preventDefault(); + onCollapse(id); + }); + return ( - <header onClick={onClick}> - <img src={new URL(user.icon.url, user.id)} /> - <span className="user">{username}</span> + <header> + <a class="avatar" href={user.id}> + <img src={new URL(user.icon.url, user.id)} /> + </a> + <span class="user">{username}</span> <Space /> - <span className="time"><Time time={published} /></span> + <a class="time" href={id}> + <Time time={published} /> + </a> + <a class="collapse" href="#" onClick={onClick}> + {collapsed ? '+' : '-'} + </a> </header> ); }; css` section { - position: absolute; - overflow: hidden; + display: flex; + flex-direction: column; + justify-content: space-between; - width: 22em; + width: 22rem; + + overflow: hidden; 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; + border-radius: 0.4rem; } section.type-Tombstone { @@ -99,7 +129,9 @@ section.type-Tombstone { } section > main { - padding: 1em; + position: relative; + padding: 0.75rem; + overflow: hidden; font-family: serif; } @@ -124,43 +156,77 @@ section.collapsed { section.collapsed > main { display: none; } + +section.ellipsis > main { + max-height: 7em; +} +section.ellipsis > main::after { + position: absolute; + display: block; + content: ''; + + pointer-events: none; + + left: 0; + right: 0; + bottom: 0; + height: 1.25em; + background: linear-gradient(0deg, var(--theme-note-bg) 5%, var(--theme-note-bg-trans) 100%); +} +section.type-Tombstone.ellipsis > main::after { + background: linear-gradient(0deg, var(--theme-note-fg) 5%, var(--theme-note-fg-trans) 100%); +} `; const colors = new ColorHash({ lightness: 0.8 }); export const Note = ({ id, type, attributedTo, published, content, - smallHeader = false, collapsed = false, position, - onHide, onSize, + smallHeader = false, collapsed = false, ellipsis = false, position, + onCollapse, onSelect, onSize, }) => { const backgroundColor = attributedTo.color || colors.hex(attributedTo.id); + const onClick = onSelect && ((e) => { + e.preventDefault(); + onSelect(id); + }); + const root = useRef(null); - useLayoutEffect(() => { + onSize && useLayoutEffect(() => { onSize(id, root.current.offsetWidth, root.current.offsetHeight); }); - const onClickHeader = (e) => { - e.preventDefault(); - onHide(id); - }; - return ( <section style={{ '--theme-header-bg': backgroundColor, + 'position': position && 'absolute', 'left': position && `${position[0] - root.current.offsetWidth / 2}px`, 'top': position && `${position[1] - root.current.offsetHeight / 2}px`, - 'visibility': position ? 'visible' : 'hidden', + 'visibility': onSize && !position ? 'hidden' : 'visible', }} - class={cn(`type-${type}`, collapsed && 'collapsed')} + class={cn( + `type-${type}`, + collapsed && 'collapsed', + ellipsis && 'ellipsis', + )} data-id={id} ref={root} > {smallHeader - ? <header className="small" onClick={onClickHeader} /> - : <Header onClick={onClickHeader} user={attributedTo} published={published} /> + ? <header className="small" onClick={onCollapse} /> + : <Header + id={id} + user={attributedTo} + published={published} + collapsed={collapsed} + onCollapse={onCollapse} + /> } - <main innerHTML={content} /> + <main + innerHTML={content} + onClick={onClick} + /> </section> ); }; diff --git a/src/ui/Selection.js b/src/ui/Selection.js new file mode 100644 index 0000000..d6e62ab --- /dev/null +++ b/src/ui/Selection.js @@ -0,0 +1,90 @@ +import { h } from 'preact'; +import { useState, useEffect } from 'preact/hooks'; +import cn from 'classnames'; +import css from './css'; +import { Note } from './Note'; + +css` +.drawer { + background: var(--theme-note-fg); + + flex: 0 0 auto; + + /* wtf webkit */ + -webkit-backface-visibility: hidden; +} + +.drawer .handle { + height: 2rem; + margin-bottom: 1rem; +} + +.drawer .handle button { + display: block; + + height: 2rem; + line-height: 2rem; + margin: -1rem auto 0; + padding: 0.25rem; + background: var(--theme-note-bg); + border-radius: 0.4rem; +} + +.drawer .scroller { + display: flex; + overflow: auto; + width: 100%; + + transition: max-height 0.3s; +} + +.drawer .contents { + display: flex; + + align-items: flex-start; + + gap: 1rem; + padding: 0 2rem; +} + +.drawer .contents > * { + flex: 0 0 auto; +} +`; + +export const Drawer = ({ height, children }) => { + const [ hidden, setHidden ] = useState(true); + + return ( + <div class="drawer"> + <div class="handle"> + <button onClick={() => setHidden(!hidden)}> + ### + </button> + </div> + <div + class="scroller" + style={{ + height, + 'max-height': hidden ? '0' : height, + }} + > + <div class="contents"> + {children} + </div> + </div> + </div> + ); +}; + +export const Selection = ({ items, toggleSelected }) => ( + <Drawer height="12rem" style={{ }} > + {items.map((item) => ( + <Note + {...item} + ellipsis + onSelect={toggleSelected} + /> + ))} + </Drawer> +); diff --git a/src/ui/index.js b/src/ui/index.js index 38b5b1b..fa831e2 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -1,6 +1,8 @@ import './theme'; import { Discussion } from './Discussion'; +import { Selection } from './Selection'; export { Discussion, + Selection, }; diff --git a/src/ui/theme.js b/src/ui/theme.js index 372cd46..58c8397 100644 --- a/src/ui/theme.js +++ b/src/ui/theme.js @@ -1,11 +1,19 @@ import css from './css'; css` +html { + font-size: 11px; + margin: 0; + padding: 0; +} + body { --theme-backdrop: #1e1e1e; --theme-fg: #eeeeee; --theme-note-bg: #eeeeee; --theme-note-fg: #363636; + --theme-note-bg-trans: rgba(238,238,238,0); + --theme-note-fg-trans: rgba(54,54,54,0); --theme-header-fg: #121212; } @@ -13,6 +21,8 @@ body.darktheme { --theme-backdrop: #1e1e1e; --theme-note-bg: #363636; --theme-note-fg: #eeeeee; + --theme-note-bg-trans: rgba(54,54,54,0); + --theme-note-fg-trans: rgba(238,238,238,0); --theme-header-fg: #121212; } @@ -20,7 +30,16 @@ body { color: var(--theme-fg); background: var(--theme-backdrop); font-family: sans-serif; - font-size: 11px; + + margin: 0; + padding: 0; + width: 100vw; + height: 100vh; + overflow: hidden; + + display: flex; + flex-direction: column; + justify-content: space-around; } `; |
