mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
feat(collections): frontend — CollectionsView rename, delete, add/remove entries, per-entry visibility
This commit is contained in:
parent
c4ce2129bb
commit
80dce80033
2 changed files with 551 additions and 62 deletions
|
|
@ -1,15 +1,25 @@
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { listCollections, createCollection, getCollection } from '../api.js'
|
import {
|
||||||
|
listCollections, createCollection, getCollection,
|
||||||
|
addEntryToCollection, removeEntryFromCollection,
|
||||||
|
updateEntryVisibility, updateCollection, deleteCollection,
|
||||||
|
} from '../api.js'
|
||||||
|
|
||||||
const VIS_LABELS = { 0: 'Private', 1: 'Public', 2: 'Users only', 3: 'Public' }
|
const VIS_OPTIONS = [
|
||||||
|
{ value: 0, label: 'Private' },
|
||||||
|
{ value: 2, label: 'Users only' },
|
||||||
|
{ value: 3, label: 'Public' },
|
||||||
|
]
|
||||||
|
const VIS_LABEL = v => VIS_OPTIONS.find(o => o.value === v)?.label ?? String(v)
|
||||||
|
|
||||||
export default function CollectionsView({ archiveId }) {
|
export default function CollectionsView({ archiveId }) {
|
||||||
const [collections, setCollections] = useState([])
|
const [collections, setCollections] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [selectedColl, setSelectedColl] = useState(null)
|
const [selectedUid, setSelectedUid] = useState(null)
|
||||||
const [collDetail, setCollDetail] = useState(null)
|
const [collDetail, setCollDetail] = useState(null)
|
||||||
const [detailLoading, setDetailLoading] = useState(false)
|
const [detailLoading, setDetailLoading] = useState(false)
|
||||||
|
const [detailError, setDetailError] = useState(null)
|
||||||
|
|
||||||
// Create form
|
// Create form
|
||||||
const [newName, setNewName] = useState('')
|
const [newName, setNewName] = useState('')
|
||||||
|
|
@ -18,7 +28,21 @@ export default function CollectionsView({ archiveId }) {
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
const [createError, setCreateError] = useState(null)
|
const [createError, setCreateError] = useState(null)
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
// Add-entry form
|
||||||
|
const [addUid, setAddUid] = useState('')
|
||||||
|
const [addVis, setAddVis] = useState(2)
|
||||||
|
const [adding, setAdding] = useState(false)
|
||||||
|
const [addError, setAddError] = useState(null)
|
||||||
|
|
||||||
|
// Inline rename state
|
||||||
|
const [renaming, setRenaming] = useState(false)
|
||||||
|
const [renameVal, setRenameVal] = useState('')
|
||||||
|
const renameRef = useRef(null)
|
||||||
|
|
||||||
|
const selected = collections.find(c => c.collection_uid === selectedUid) ?? null
|
||||||
|
const isDefault = selected?.slug === '_default_'
|
||||||
|
|
||||||
|
const refreshList = useCallback(async () => {
|
||||||
if (!archiveId) return
|
if (!archiveId) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
@ -32,16 +56,25 @@ export default function CollectionsView({ archiveId }) {
|
||||||
}
|
}
|
||||||
}, [archiveId])
|
}, [archiveId])
|
||||||
|
|
||||||
useEffect(() => { refresh() }, [refresh])
|
const refreshDetail = useCallback(async (uid) => {
|
||||||
|
if (!uid) { setCollDetail(null); return }
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedColl) { setCollDetail(null); return }
|
|
||||||
setDetailLoading(true)
|
setDetailLoading(true)
|
||||||
getCollection(archiveId, selectedColl.collection_uid)
|
setDetailError(null)
|
||||||
.then(d => setCollDetail(d))
|
try {
|
||||||
.catch(e => setError(e.message))
|
const d = await getCollection(archiveId, uid)
|
||||||
.finally(() => setDetailLoading(false))
|
setCollDetail(d)
|
||||||
}, [selectedColl, archiveId])
|
} catch (e) {
|
||||||
|
setDetailError(e.message)
|
||||||
|
} finally {
|
||||||
|
setDetailLoading(false)
|
||||||
|
}
|
||||||
|
}, [archiveId])
|
||||||
|
|
||||||
|
useEffect(() => { refreshList() }, [refreshList])
|
||||||
|
useEffect(() => { refreshDetail(selectedUid) }, [selectedUid, refreshDetail])
|
||||||
|
|
||||||
|
// Auto-focus rename input
|
||||||
|
useEffect(() => { if (renaming && renameRef.current) renameRef.current.focus() }, [renaming])
|
||||||
|
|
||||||
async function handleCreate(e) {
|
async function handleCreate(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
@ -51,11 +84,12 @@ export default function CollectionsView({ archiveId }) {
|
||||||
setCreating(true)
|
setCreating(true)
|
||||||
setCreateError(null)
|
setCreateError(null)
|
||||||
try {
|
try {
|
||||||
await createCollection(archiveId, name, slug, newVis)
|
const coll = await createCollection(archiveId, name, slug, newVis)
|
||||||
setNewName('')
|
setNewName('')
|
||||||
setNewSlug('')
|
setNewSlug('')
|
||||||
setNewVis(2)
|
setNewVis(2)
|
||||||
await refresh()
|
await refreshList()
|
||||||
|
setSelectedUid(coll.collection_uid)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setCreateError(err.message)
|
setCreateError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -63,71 +97,240 @@ export default function CollectionsView({ archiveId }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRenameCommit() {
|
||||||
|
const name = renameVal.trim()
|
||||||
|
if (!name || !selected) { setRenaming(false); return }
|
||||||
|
try {
|
||||||
|
await updateCollection(archiveId, selected.collection_uid, { name })
|
||||||
|
await refreshList()
|
||||||
|
setCollDetail(d => d ? { ...d, name } : d)
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
} finally {
|
||||||
|
setRenaming(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleVisChange(newVisVal) {
|
||||||
|
if (!selected) return
|
||||||
|
try {
|
||||||
|
await updateCollection(archiveId, selected.collection_uid, { default_visibility_bits: newVisVal })
|
||||||
|
await refreshList()
|
||||||
|
setCollDetail(d => d ? { ...d, default_visibility_bits: newVisVal } : d)
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
if (!selected) return
|
||||||
|
if (!window.confirm(`Delete collection "${selected.name}"? Entries will not be deleted.`)) return
|
||||||
|
try {
|
||||||
|
await deleteCollection(archiveId, selected.collection_uid)
|
||||||
|
setSelectedUid(null)
|
||||||
|
setCollDetail(null)
|
||||||
|
await refreshList()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddEntry(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const uid = addUid.trim()
|
||||||
|
if (!uid || !selected) return
|
||||||
|
setAdding(true)
|
||||||
|
setAddError(null)
|
||||||
|
try {
|
||||||
|
await addEntryToCollection(archiveId, selected.collection_uid, uid, addVis)
|
||||||
|
setAddUid('')
|
||||||
|
await refreshDetail(selected.collection_uid)
|
||||||
|
} catch (err) {
|
||||||
|
setAddError(err.message)
|
||||||
|
} finally {
|
||||||
|
setAdding(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRemoveEntry(entryUid) {
|
||||||
|
if (!selected) return
|
||||||
|
try {
|
||||||
|
await removeEntryFromCollection(archiveId, selected.collection_uid, entryUid)
|
||||||
|
await refreshDetail(selected.collection_uid)
|
||||||
|
} catch (e) {
|
||||||
|
setDetailError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEntryVisChange(entryUid, vis) {
|
||||||
|
if (!selected) return
|
||||||
|
try {
|
||||||
|
await updateEntryVisibility(archiveId, selected.collection_uid, entryUid, vis)
|
||||||
|
setCollDetail(d => d ? {
|
||||||
|
...d,
|
||||||
|
entries: d.entries.map(en =>
|
||||||
|
en.entry_uid === entryUid ? { ...en, collection_visibility_bits: vis } : en
|
||||||
|
),
|
||||||
|
} : d)
|
||||||
|
} catch (e) {
|
||||||
|
setDetailError(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!archiveId) return <div className="view-placeholder">Select an archive.</div>
|
if (!archiveId) return <div className="view-placeholder">Select an archive.</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="collections-view">
|
<div className="collections-view">
|
||||||
<h2 className="view-heading">Collections</h2>
|
<h2 className="collections-heading">Collections</h2>
|
||||||
|
|
||||||
{loading && <div className="muted">Loading…</div>}
|
{loading && <div className="muted">Loading…</div>}
|
||||||
{error && <div className="error-text">{error}</div>}
|
{error && <div className="collections-error">{error} <button onClick={() => setError(null)} className="coll-dismiss">×</button></div>}
|
||||||
|
|
||||||
<div className="collections-layout">
|
<div className="collections-layout">
|
||||||
<div className="collections-list">
|
{/* Sidebar */}
|
||||||
|
<div className="collections-sidebar">
|
||||||
{collections.map(c => (
|
{collections.map(c => (
|
||||||
<div
|
<button
|
||||||
key={c.collection_uid}
|
key={c.collection_uid}
|
||||||
className={`collection-row${selectedColl?.collection_uid === c.collection_uid ? ' is-selected' : ''}`}
|
className={`coll-row${selectedUid === c.collection_uid ? ' is-active' : ''}`}
|
||||||
onClick={() => setSelectedColl(c)}
|
onClick={() => setSelectedUid(c.collection_uid)}
|
||||||
>
|
>
|
||||||
<span className="collection-name">{c.name}</span>
|
<span className="coll-row-name">{c.name}</span>
|
||||||
<span className="muted" style={{ fontSize: '0.8em' }}>
|
<span className="coll-row-meta">{VIS_LABEL(c.default_visibility_bits)}</span>
|
||||||
{VIS_LABELS[c.default_visibility_bits] ?? c.default_visibility_bits}
|
</button>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
{collections.length === 0 && !loading && (
|
{collections.length === 0 && !loading && (
|
||||||
<div className="muted">No collections yet.</div>
|
<div className="muted" style={{ padding: '8px 12px' }}>No collections yet.</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedColl && (
|
{/* Detail pane */}
|
||||||
<div className="collection-detail">
|
{selected ? (
|
||||||
<h3 className="collection-detail-name">{selectedColl.name}</h3>
|
<div className="coll-detail">
|
||||||
<div className="muted" style={{ marginBottom: '0.75rem' }}>
|
{/* Header */}
|
||||||
Default visibility: {VIS_LABELS[selectedColl.default_visibility_bits] ?? selectedColl.default_visibility_bits}
|
<div className="coll-detail-header">
|
||||||
</div>
|
{renaming ? (
|
||||||
{detailLoading ? (
|
<input
|
||||||
<div className="muted">Loading entries…</div>
|
ref={renameRef}
|
||||||
) : collDetail ? (
|
className="coll-rename-input"
|
||||||
collDetail.entries.length === 0 ? (
|
value={renameVal}
|
||||||
<div className="muted">No entries visible to you in this collection.</div>
|
onChange={e => setRenameVal(e.target.value)}
|
||||||
|
onBlur={handleRenameCommit}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') handleRenameCommit()
|
||||||
|
if (e.key === 'Escape') setRenaming(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ul className="collection-entries-list">
|
<h3
|
||||||
{collDetail.entries.map(entry => (
|
className={`coll-detail-name${isDefault ? '' : ' coll-detail-name--editable'}`}
|
||||||
<li key={entry.entry_uid} className="collection-entry-item">
|
title={isDefault ? undefined : 'Click to rename'}
|
||||||
<span className="entry-title">
|
onClick={() => { if (!isDefault) { setRenameVal(selected.name); setRenaming(true) } }}
|
||||||
{entry.title || entry.entry_uid}
|
>
|
||||||
</span>
|
{collDetail?.name ?? selected.name}
|
||||||
<span className="muted" style={{ fontSize: '0.8em' }}>
|
{!isDefault && <span className="coll-edit-hint"> ✎</span>}
|
||||||
{entry.source_kind}
|
</h3>
|
||||||
</span>
|
)}
|
||||||
</li>
|
{!isDefault && (
|
||||||
))}
|
<button className="coll-delete-btn" onClick={handleDelete} title="Delete collection">Delete</button>
|
||||||
</ul>
|
)}
|
||||||
)
|
</div>
|
||||||
) : null}
|
|
||||||
|
{/* Visibility */}
|
||||||
|
<div className="coll-detail-vis">
|
||||||
|
<span className="coll-vis-label">Default visibility</span>
|
||||||
|
<select
|
||||||
|
className="coll-vis-select"
|
||||||
|
value={collDetail?.default_visibility_bits ?? selected.default_visibility_bits}
|
||||||
|
onChange={e => handleVisChange(Number(e.target.value))}
|
||||||
|
disabled={isDefault}
|
||||||
|
>
|
||||||
|
{VIS_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Entries */}
|
||||||
|
<div className="coll-entries-section">
|
||||||
|
<div className="coll-section-heading">Entries</div>
|
||||||
|
{detailLoading && <div className="muted">Loading…</div>}
|
||||||
|
{detailError && <div className="collections-error">{detailError}</div>}
|
||||||
|
{!detailLoading && collDetail && (
|
||||||
|
collDetail.entries.length === 0 ? (
|
||||||
|
<div className="muted">No entries in this collection.</div>
|
||||||
|
) : (
|
||||||
|
<ul className="coll-entries-list">
|
||||||
|
{collDetail.entries.map(entry => (
|
||||||
|
<li key={entry.entry_uid} className="coll-entry-row">
|
||||||
|
<div className="coll-entry-info">
|
||||||
|
<span className="coll-entry-title">{entry.title || entry.entry_uid}</span>
|
||||||
|
<span className="coll-entry-kind muted">{entry.source_kind}</span>
|
||||||
|
</div>
|
||||||
|
<div className="coll-entry-actions">
|
||||||
|
<select
|
||||||
|
className="coll-entry-vis-select"
|
||||||
|
value={entry.collection_visibility_bits}
|
||||||
|
onChange={e => handleEntryVisChange(entry.entry_uid, Number(e.target.value))}
|
||||||
|
>
|
||||||
|
{VIS_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
{!isDefault && (
|
||||||
|
<button
|
||||||
|
className="coll-entry-remove"
|
||||||
|
onClick={() => handleRemoveEntry(entry.entry_uid)}
|
||||||
|
title="Remove from collection"
|
||||||
|
>×</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add entry (non-default only) */}
|
||||||
|
{!isDefault && (
|
||||||
|
<form className="coll-add-entry-form" onSubmit={handleAddEntry}>
|
||||||
|
<div className="coll-section-heading">Add entry</div>
|
||||||
|
<div className="coll-add-entry-row">
|
||||||
|
<input
|
||||||
|
className="coll-add-entry-input"
|
||||||
|
type="text"
|
||||||
|
value={addUid}
|
||||||
|
onChange={e => setAddUid(e.target.value)}
|
||||||
|
placeholder="entry_uid"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className="coll-vis-select"
|
||||||
|
value={addVis}
|
||||||
|
onChange={e => setAddVis(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
{VIS_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
<button className="coll-add-btn" type="submit" disabled={adding}>
|
||||||
|
{adding ? '…' : 'Add'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{addError && <div className="collections-error" style={{ marginTop: 4 }}>{addError}</div>}
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="coll-detail coll-detail--empty">
|
||||||
|
<div className="muted">Select a collection to view details.</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details className="create-collection-form" style={{ marginTop: '1.5rem' }}>
|
{/* Create form */}
|
||||||
|
<details className="coll-create-details" style={{ marginTop: '1.5rem' }}>
|
||||||
<summary style={{ cursor: 'pointer', fontWeight: 600 }}>Create collection</summary>
|
<summary style={{ cursor: 'pointer', fontWeight: 600 }}>Create collection</summary>
|
||||||
<form onSubmit={handleCreate} style={{ marginTop: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', maxWidth: 400 }}>
|
<form onSubmit={handleCreate} style={{ marginTop: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', maxWidth: 400 }}>
|
||||||
<label>
|
<label>
|
||||||
Name
|
Name
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="capture-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={newName}
|
value={newName}
|
||||||
onChange={e => { setNewName(e.target.value); if (!newSlug) setNewSlug(e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) }}
|
onChange={e => { setNewName(e.target.value); if (!newSlug) setNewSlug(e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) }}
|
||||||
|
|
@ -138,7 +341,7 @@ export default function CollectionsView({ archiveId }) {
|
||||||
<label>
|
<label>
|
||||||
Slug
|
Slug
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="capture-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={newSlug}
|
value={newSlug}
|
||||||
onChange={e => setNewSlug(e.target.value)}
|
onChange={e => setNewSlug(e.target.value)}
|
||||||
|
|
@ -148,14 +351,12 @@ export default function CollectionsView({ archiveId }) {
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Default visibility
|
Default visibility
|
||||||
<select className="form-input" value={newVis} onChange={e => setNewVis(Number(e.target.value))}>
|
<select className="capture-input" style={{ height: 42 }} value={newVis} onChange={e => setNewVis(Number(e.target.value))}>
|
||||||
<option value={0}>Private (admin/owner only)</option>
|
{VIS_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
<option value={2}>Users only (logged in)</option>
|
|
||||||
<option value={3}>Public</option>
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{createError && <div className="muted" style={{ color: 'var(--accent)' }}>{createError}</div>}
|
{createError && <div className="collections-error">{createError}</div>}
|
||||||
<button className="btn" type="submit" disabled={creating}>
|
<button className="capture-submit" type="submit" disabled={creating} style={{ alignSelf: 'flex-start', padding: '8px 20px' }}>
|
||||||
{creating ? 'Creating…' : 'Create'}
|
{creating ? 'Creating…' : 'Create'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -640,3 +640,291 @@ select {
|
||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Collections view ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.collections-view {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-heading {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-family: Georgia, "Times New Roman", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-error {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-dismiss {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 220px 1fr;
|
||||||
|
gap: 0;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
min-height: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-sidebar {
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--line-soft);
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-row:hover {
|
||||||
|
background: var(--paper-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-row.is-active {
|
||||||
|
background: var(--paper-2);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-row-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-row-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-detail {
|
||||||
|
padding: 20px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-detail--empty {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-detail-name {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-family: Georgia, "Times New Roman", serif;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-detail-name--editable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-detail-name--editable:hover {
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-edit-hint {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-rename-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: Georgia, "Times New Roman", serif;
|
||||||
|
border: 2px solid var(--ink);
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--paper-3);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-delete-btn {
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
background: none;
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 5px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-delete-btn:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--paper);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-detail-vis {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-vis-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-vis-select {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--paper-3);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-section-heading {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-entries-section {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-entries-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-entry-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 7px 0;
|
||||||
|
border-bottom: 1px solid var(--line-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-entry-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-entry-title {
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-entry-kind {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-entry-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-entry-vis-select {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--paper-3);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-entry-remove {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-entry-remove:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-add-entry-form {
|
||||||
|
border-top: 1px solid var(--line-soft);
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-add-entry-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-add-entry-input {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--paper-3);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-add-btn {
|
||||||
|
border: none;
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--paper);
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-add-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-add-btn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coll-create-details summary {
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue