project-bifrost-platform/design/ui_kits/product/components.jsx
2026-04-18 16:09:49 +02:00

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,
});