128 lines
6.6 KiB
JavaScript
128 lines
6.6 KiB
JavaScript
// Fenja UI Kit — Components
|
|
// Shared icons
|
|
const Ic = {
|
|
feather: (p) => (<svg viewBox="0 0 24 24" {...p}><path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"/><line x1="16" y1="8" x2="2" y2="22"/><line x1="17.5" y1="15" x2="9" y2="15"/></svg>),
|
|
search: (p) => (<svg viewBox="0 0 24 24" {...p}><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>),
|
|
book: (p) => (<svg viewBox="0 0 24 24" {...p}><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>),
|
|
bookmark: (p) => (<svg viewBox="0 0 24 24" {...p}><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>),
|
|
note: (p) => (<svg viewBox="0 0 24 24" {...p}><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>),
|
|
plus: (p) => (<svg viewBox="0 0 24 24" {...p}><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>),
|
|
settings: (p) => (<svg viewBox="0 0 24 24" {...p}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>),
|
|
send: (p) => (<svg viewBox="0 0 24 24" {...p}><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>),
|
|
star: (p) => (<svg viewBox="0 0 24 24" {...p}><polygon points="12 2 15 8.5 22 9.3 17 14 18.2 21 12 17.8 5.8 21 7 14 2 9.3 9 8.5"/></svg>),
|
|
archive: (p) => (<svg viewBox="0 0 24 24" {...p}><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>),
|
|
quote: (p) => (<svg viewBox="0 0 24 24" {...p}><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.76-2-2-2H4c-1 0-2 1-2 2v7c0 1 1 2 2 2h3c0 2-1 4-4 4z"/><path d="M14 21c3 0 7-1 7-8V5c0-1.25-.76-2-2-2h-4c-1 0-2 1-2 2v7c0 1 1 2 2 2h3c0 2-1 4-4 4z"/></svg>),
|
|
};
|
|
|
|
const Icon = ({ name, size = 18, ...rest }) => {
|
|
const C = Ic[name];
|
|
return C ? <C width={size} height={size} {...rest} /> : null;
|
|
};
|
|
|
|
const RuneSpinner = () => (
|
|
<span className="rune-spinner" aria-label="Reading">
|
|
<svg viewBox="0 0 24 24">
|
|
<polygon points="12,2 22,8 22,16 12,22 2,16 2,8"/>
|
|
<line x1="12" y1="2" x2="12" y2="22"/>
|
|
<line className="accent" x1="2" y1="8" x2="22" y2="16"/>
|
|
</svg>
|
|
</span>
|
|
);
|
|
|
|
const Button = ({ kind = "primary", children, onClick }) => (
|
|
<button className={`btn btn-${kind}`} onClick={onClick}>{children}</button>
|
|
);
|
|
|
|
const IconButton = ({ name, label, onClick }) => (
|
|
<button className="btn-icon" aria-label={label} onClick={onClick}><Icon name={name} /></button>
|
|
);
|
|
|
|
const Tag = ({ children }) => <span className="tag">{children}</span>;
|
|
|
|
const Sidebar = ({ view, setView }) => (
|
|
<aside className="sidebar">
|
|
<div className="brand">
|
|
<img src="../../assets/fenja-wordmark-black.svg" alt="Fenja AI" className="wordmark"/>
|
|
</div>
|
|
|
|
<div className="nav-section">
|
|
<div className="heading">Archive</div>
|
|
<div className={`nav-item ${view==='archive'?'active':''}`} onClick={() => setView('archive')}>
|
|
<Icon name="archive" size={16}/> All documents <span className="count">412</span>
|
|
</div>
|
|
<div className={`nav-item ${view==='starred'?'active':''}`} onClick={() => setView('starred')}>
|
|
<Icon name="star" size={16}/> Starred <span className="count">23</span>
|
|
</div>
|
|
<div className={`nav-item ${view==='notes'?'active':''}`} onClick={() => setView('notes')}>
|
|
<Icon name="note" size={16}/> Marginalia <span className="count">84</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="nav-section">
|
|
<div className="heading">Collections</div>
|
|
<div className="nav-item"><span className="pigment-dot" style={{width:8,height:8,background:'#6d8c7c',borderRadius:'50%'}}/> Essays <span className="count">34</span></div>
|
|
<div className="nav-item"><span className="pigment-dot" style={{width:8,height:8,background:'#5a6d83',borderRadius:'50%'}}/> Research <span className="count">112</span></div>
|
|
<div className="nav-item"><span className="pigment-dot" style={{width:8,height:8,background:'#c29d59',borderRadius:'50%'}}/> Correspondence <span className="count">58</span></div>
|
|
<div className="nav-item"><span className="pigment-dot" style={{width:8,height:8,background:'#8d7a85',borderRadius:'50%'}}/> Unfiled <span className="count">9</span></div>
|
|
</div>
|
|
|
|
<div className="sidebar-footer">
|
|
<div className="avatar">E</div>
|
|
<div>
|
|
<div className="name">Elin Holmqvist</div>
|
|
<div className="email">elin@fenja.ai</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
|
|
const Topbar = ({ crumbs, children }) => (
|
|
<div className="topbar">
|
|
<div className="crumb">
|
|
{crumbs.map((c, i) => (
|
|
<React.Fragment key={i}>
|
|
{i > 0 && <span className="sep">—</span>}
|
|
<span>{c}</span>
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
<div className="topbar-actions">{children}</div>
|
|
</div>
|
|
);
|
|
|
|
const FloatingSearch = ({ onFocus }) => (
|
|
<div className="floating-search" onClick={onFocus}>
|
|
<Icon name="search" size={18} />
|
|
<input placeholder="Ask the archive, or search by title…" />
|
|
<span className="shortcut">⌘K</span>
|
|
</div>
|
|
);
|
|
|
|
const ArchiveItem = ({ item, onOpen }) => (
|
|
<div className="archive-item" onClick={() => onOpen(item)}>
|
|
<div>
|
|
<div className="title">{item.title}</div>
|
|
<div className="excerpt">{item.excerpt}</div>
|
|
</div>
|
|
<div className="pigment"><span className="dot" style={{background: item.color}} />{item.collection}</div>
|
|
<div className="meta">{item.meta}</div>
|
|
</div>
|
|
);
|
|
|
|
const Composer = ({ value, onChange, onSend, loading }) => (
|
|
<div className="composer">
|
|
{loading ? <RuneSpinner/> : <Icon name="quote" size={16} />}
|
|
<input
|
|
value={value}
|
|
placeholder="Ask a question of this document…"
|
|
onChange={(e) => onChange(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && onSend()}
|
|
/>
|
|
<button className="btn-icon" onClick={onSend} aria-label="Send"><Icon name="send" size={15}/></button>
|
|
</div>
|
|
);
|
|
|
|
Object.assign(window, {
|
|
Icon, Button, IconButton, Tag, Sidebar, Topbar, FloatingSearch,
|
|
ArchiveItem, Composer, RuneSpinner,
|
|
});
|