From e33ab71fac0fde1c31b6b2adc5d898e0c3f3bddf Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 24 Oct 2025 14:07:09 +0200 Subject: [PATCH 01/17] move to sidebar --- frontend/src/index.jsx | 284 ++++++++++++++------------ frontend/src/sidebar/Sidebar.jsx | 88 +++++++- frontend/src/styles.css | 337 ++++++++++++++----------------- frontend/src/tags/TagsPanel.jsx | 84 +++++++- 4 files changed, 473 insertions(+), 320 deletions(-) diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index 8f878da..9f407b1 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -345,6 +345,8 @@ const AppLayout = () => { ); const [loading, setLoading] = useState(false); const [isCreateFolderModalOpen, setCreateFolderModalOpen] = useState(false); + const [isTagsModalOpen, setTagsModalOpen] = useState(false); + const [isCorrespondentsModalOpen, setCorrespondentsModalOpen] = useState(false); const [newFolderName, setNewFolderName] = useState(''); const [createFolderError, setCreateFolderError] = useState(''); const [creatingFolder, setCreatingFolder] = useState(false); @@ -423,13 +425,9 @@ const AppLayout = () => { const documentsRouteMatch = useMatch('/documents'); const documentsFolderRouteMatch = useMatch('/documents/folder/:folderId'); const documentsDetailRouteMatch = useMatch('/documents/:documentId'); - const tagsRouteMatch = useMatch('/tags'); - const correspondentsRouteMatch = useMatch('/correspondents'); const isDocumentsRoute = Boolean( documentsRouteMatch || documentsFolderRouteMatch || documentsDetailRouteMatch, ); - const isTagsRoute = Boolean(tagsRouteMatch); - const isCorrespondentsRoute = Boolean(correspondentsRouteMatch); const toggleTagFilter = useCallback((tagId) => { if (!tagId) return; setActiveTagFilters((previous) => @@ -455,6 +453,10 @@ const AppLayout = () => { setSearchLoading(false); }, []); + const handleSearchChange = useCallback((value) => { + setSearchQuery(value); + }, []); + const handleSearchSubmit = useCallback(() => { if (!navigate) return; const targetFolder = selectedFolder && selectedFolder !== 'root' ? selectedFolder : 'root'; @@ -3682,6 +3684,24 @@ const AppLayout = () => { setCreateFolderError(''); }, [creatingFolder]); + const openTagsModal = useCallback(() => { + setCorrespondentsModalOpen(false); + setTagsModalOpen(true); + }, []); + + const closeTagsModal = useCallback(() => { + setTagsModalOpen(false); + }, []); + + const openCorrespondentsModal = useCallback(() => { + setTagsModalOpen(false); + setCorrespondentsModalOpen(true); + }, []); + + const closeCorrespondentsModal = useCallback(() => { + setCorrespondentsModalOpen(false); + }, []); + const handleCreateFolderSubmit = useCallback( async (event) => { event.preventDefault(); @@ -3742,6 +3762,31 @@ const AppLayout = () => { }; }, [isCreateFolderModalOpen, closeCreateFolderModal]); + useEffect(() => { + setTagsModalOpen(false); + setCorrespondentsModalOpen(false); + }, [location.pathname]); + + useEffect(() => { + if (!isTagsModalOpen && !isCorrespondentsModalOpen) { + return; + } + const handleKeyDown = (event) => { + if (event.key === 'Escape') { + event.preventDefault(); + if (isTagsModalOpen) { + closeTagsModal(); + } else if (isCorrespondentsModalOpen) { + closeCorrespondentsModal(); + } + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [isTagsModalOpen, isCorrespondentsModalOpen, closeTagsModal, closeCorrespondentsModal]); + useEffect(() => { if (!token) return undefined; @@ -4442,6 +4487,16 @@ const AppLayout = () => { correspondents, activeCorrespondentIds: activeCorrespondentFilters, onToggleCorrespondentFilter: toggleCorrespondentFilter, + appStatus, + loading, + previewActive, + searchQuery, + onSearchChange: handleSearchChange, + onSearchSubmit: handleSearchSubmit, + onSearchClear: clearFilters, + isFilterActive, + onLogout: handleLogout, + status, }; const resolveThumbnailUrlForDoc = useCallback( @@ -4585,7 +4640,6 @@ const AppLayout = () => { tags, refreshTags, handleTagUpdate, - handleTagCreate, handleTagDelete, handleDocumentTagAttach, correspondents, @@ -4610,6 +4664,8 @@ const AppLayout = () => { ensurePreviewData, notifyApiError, resolveApiPath, + openTagsModal, + openCorrespondentsModal, }), [ token, @@ -4622,7 +4678,6 @@ const AppLayout = () => { tags, refreshTags, handleTagUpdate, - handleTagCreate, handleTagDelete, handleDocumentTagAttach, correspondents, @@ -4647,6 +4702,8 @@ const AppLayout = () => { ensurePreviewData, notifyApiError, resolveApiPath, + openTagsModal, + openCorrespondentsModal, ], ); @@ -4667,81 +4724,6 @@ const AppLayout = () => { active={dropOverlayState.active} folderName={dropOverlayState.folderName} /> -
-
-
-

Papercrate

- - {appStatus === 'bootstrapping' && loading - ? 'Loading your library…' - : previewActive - ? 'Viewing document preview. Press ← Back to return to the library.' - : 'Drag files here to upload.'} - -
-
- setSearchQuery(event.target.value)} - placeholder="Search documents" - aria-label="Search documents" - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); - handleSearchSubmit(); - } - }} - /> - {isFilterActive && ( - - )} -
-
-
- - - -
- {status && ( -
- -
- )} -
- -
-
-
-
{isCreateFolderModalOpen && (
{
)} + {isTagsModalOpen && ( +
+
event.stopPropagation()} + > +
+

Manage Tags

+ +
+
+ +
+
+
+ )} + {isCorrespondentsModalOpen && ( +
+
event.stopPropagation()} + > +
+

Manage Correspondents

+ +
+
+ +
+
+
+ )} ); @@ -4803,20 +4853,35 @@ const DocumentsRoute = () => { detailPanelProps, workspaceMode, skeuoWorkspaceProps, + openTagsModal, + openCorrespondentsModal, } = useAppShell(); + const sidebarPropsWithActions = useMemo( + () => ({ + ...sidebarProps, + onManageTags: openTagsModal, + onManageCorrespondents: openCorrespondentsModal, + }), + [sidebarProps, openTagsModal, openCorrespondentsModal], + ); + if (workspaceMode === 'skeuo') { return ( - - + +
+ +
); } return ( - - - + +
+ + +
); }; @@ -4951,51 +5016,6 @@ const LoginRoute = () => { ); }; -function TagsRoute() { - const { - tags, - refreshTags, - handleTagUpdate, - handleTagDelete, - setStatusMessage, - } = useAppShell(); - return ( -
- -
- ); -} - -function CorrespondentsRoute() { - const { - correspondents, - refreshCorrespondents, - handleCorrespondentCreate, - handleCorrespondentUpdate, - handleCorrespondentDelete, - setStatusMessage, - } = useAppShell(); - - return ( -
- -
- ); -} - const AppRouter = () => ( } /> @@ -5004,8 +5024,6 @@ const AppRouter = () => ( } /> } /> } /> - } /> - } /> } /> diff --git a/frontend/src/sidebar/Sidebar.jsx b/frontend/src/sidebar/Sidebar.jsx index 690b191..a5c408a 100644 --- a/frontend/src/sidebar/Sidebar.jsx +++ b/frontend/src/sidebar/Sidebar.jsx @@ -136,6 +136,18 @@ const Sidebar = ({ correspondents = [], activeCorrespondentIds = [], onToggleCorrespondentFilter, + onManageTags, + onManageCorrespondents, + searchQuery = '', + onSearchChange, + onSearchSubmit, + onSearchClear, + isFilterActive, + appStatus, + loading, + previewActive, + onLogout, + status, }) => { const sortedCorrespondents = useMemo( () => @@ -150,6 +162,27 @@ const Sidebar = ({ ); const handleToggleTag = onToggleTagFilter || (() => {}); const activeTagSet = new Set(activeTagIds); + const handleManageTags = onManageTags || (() => {}); + const handleManageCorrespondents = onManageCorrespondents || (() => {}); + const handleSearchInputChange = useCallback( + (event) => { + onSearchChange?.(event.target.value); + }, + [onSearchChange], + ); + const handleSearchFormSubmit = useCallback( + (event) => { + event.preventDefault(); + onSearchSubmit?.(); + }, + [onSearchSubmit], + ); + const handleSearchClear = useCallback(() => { + onSearchClear?.(); + }, [onSearchClear]); + const handleLogoutClick = useCallback(() => { + onLogout?.(); + }, [onLogout]); const renderNodes = useCallback( (ids, depth) => @@ -193,9 +226,39 @@ const Sidebar = ({ ); const rootNode = folderNodes.get('root'); + const hintText = appStatus === 'bootstrapping' && loading + ? 'Loading your library…' + : previewActive + ? 'Viewing document preview. Press ← Back to return to the library.' + : 'Drag files here to upload.'; return ( ); }; diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 9492ae5..52ca293 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -4,7 +4,7 @@ --bg: #f6f6f6; --surface: #fbfbfb; --surface-subtle: #f3f3f3; - --fg: #1e2127; + --fg: #2b2b2b; --muted: #646a75; --sidebar-fg: #5f6672; --border: #dde1e6; @@ -14,11 +14,9 @@ --overlay-backdrop: rgba(30, 33, 39, 0.45); --overlay-shadow: rgba(30, 33, 39, 0.18); - --app-bar-background: var(--surface-subtle); - /* Accent (primary action, selection, focus) */ --accent: #3f6ad8; - --accent-hover: #365cb9; + --accent-hover: #9bb6ff; --on-accent: #ffffff; /* Soft tints & outlines (for chips, row hovers, subtle fills) */ @@ -205,7 +203,6 @@ button.icon-button.ghost:hover:not([disabled]) { .view-toggle { display: inline-flex; align-items: center; - border: 1px solid var(--border); border-radius: 999px; background: var(--surface-subtle); margin-right: 0.5rem; @@ -251,151 +248,32 @@ button.icon-button.ghost:hover:not([disabled]) { background: var(--bg); } -.app-bar { - display: flex; - flex-direction: column; - gap: 0.4rem; - padding: 0.6rem 1.5rem 0.4rem; - background: var(--app-bar-background); - border-bottom: 1px solid var(--border); - position: sticky; - top: 0; - z-index: 10; -} - -.app-bar__main { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 1rem; -} - -.app-bar__meta { - display: flex; - flex-direction: column; - gap: 0.2rem; - flex: 0 0 auto; -} - -.app-bar h1 { - margin: 0; - font-size: 1.1rem; - font-weight: 600; -} - -.app-bar__hint { - color: var(--muted); - font-size: 0.9rem; -} - -.app-bar__actions { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.app-bar__status { - margin: 0 1rem; - min-width: 220px; -} - -.app-bar__status .status-banner { - margin: 0; -} - -.app-bar__search { - display: flex; - align-items: center; - gap: 0.5rem; - flex: 1 1 320px; - max-width: 420px; - min-width: 240px; - margin: 0 auto; -} - -.app-bar__search input[type='search'] { - flex: 1; - min-width: 180px; - padding: 0.4rem 0.6rem; - border-radius: 4px; - border: 1px solid var(--border); - background: var(--surface-subtle); - color: var(--fg); -} - -.app-bar__search input[type='search']:focus { - outline: 2px solid var(--accent); - outline-offset: 1px; -} - -.app-bar__search-clear { - border: none; - background: transparent; - color: var(--accent); - font-weight: 600; - font-size: 0.85rem; - cursor: pointer; - padding: 0.2rem 0.3rem; -} - -.app-bar__search-clear:hover { - text-decoration: underline; -} - -.app-bar__right { - display: flex; - align-items: center; - gap: 0.75rem; - margin-left: auto; -} - -.app-bar__links { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.app-bar__link { - border: none; - background: transparent; - color: var(--muted); - font: inherit; - padding: 0.3rem 0.65rem; - border-radius: 999px; - cursor: pointer; - display: inline-flex; - align-items: center; - gap: 0.35rem; - transition: background 0.15s ease, color 0.15s ease; -} - -.app-bar__link:hover { - background: var(--sidebar-hover-bg); - color: var(--fg); -} - -.app-bar__link:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; -} - -.app-bar__link.active { - background: var(--sidebar-active-bg); - color: var(--fg); - font-weight: 600; -} - -.app-bar__link-count { - font-size: 0.75rem; - color: var(--muted); -} .documents-main { flex: 1; display: grid; - grid-template-columns: 20rem minmax(0, 1fr) 30rem; + grid-template-columns: 20rem minmax(0, 1fr); + min-height: 0; +} + +.main-content { + display: flex; + flex-direction: column; + min-height: 0; + margin: 0; + border-radius: 0; + background: var(--surface); +} + +.documents-main .main-content--documents { + display: grid; + grid-template-columns: minmax(0, 1fr) 30rem; + min-height: 0; +} + +.documents-main .main-content--documents > * { + min-width: 0; min-height: 0; - overflow: hidden; } .panels-main { @@ -558,7 +436,6 @@ button.icon-button.ghost:hover:not([disabled]) { .preview-workspace__object { width: 100%; height: 100%; - border: none; } .tags-panel__body { @@ -580,7 +457,6 @@ button.icon-button.ghost:hover:not([disabled]) { .tags-table td { padding: 0.45rem 0.6rem; text-align: left; - border-bottom: 1px solid var(--divider); font-size: 0.85rem; } @@ -638,7 +514,6 @@ button.icon-button.ghost:hover:not([disabled]) { width: 2.25rem; height: 2.25rem; padding: 0; - border: none; background: none; cursor: pointer; } @@ -659,6 +534,24 @@ button.icon-button.ghost:hover:not([disabled]) { gap: 0.5rem; } +.tags-actions { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + align-items: center; +} + +.tags-actions__form { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.4rem; +} + +.tags-actions__form input[type='text'] { + min-width: 14rem; +} + .correspondents-actions__form { display: flex; gap: 0.4rem; @@ -680,7 +573,6 @@ button.icon-button.ghost:hover:not([disabled]) { align-items: center; gap: 0.35rem; background: var(--surface-subtle); - border: 1px solid var(--border-subtle); border-radius: 999px; padding: 0.15rem 0.5rem; font-size: 0.9rem; @@ -729,7 +621,6 @@ button.icon-button.ghost:hover:not([disabled]) { } .preview-workspace__metadata { - border: 1px solid var(--border); border-radius: 2px; padding: 0.75rem; background: var(--surface); @@ -744,7 +635,6 @@ button.icon-button.ghost:hover:not([disabled]) { .column { background: transparent; - border: none; display: flex; flex-direction: column; min-height: 0; @@ -760,7 +650,6 @@ button.icon-button.ghost:hover:not([disabled]) { justify-content: space-between; gap: 0.5rem; padding: 0 0 0.5rem; - border-bottom: 1px solid var(--border); } .column-header__titles { @@ -940,7 +829,69 @@ button.icon-button.ghost:hover:not([disabled]) { overflow-y: auto; padding: 1.25rem; color: var(--sidebar-fg); - border-right: 1px solid var(--border); + box-shadow: inset -12px 0 24px -12px var(--shadow-faint); +} + +.sidebar__meta { + display: flex; + flex-direction: column; + gap: 0.2rem; + margin-bottom: 1rem; +} + +.sidebar__title { + margin: 0; + font-size: 1.2rem; + font-weight: 600; + color: var(--fg); +} + +.sidebar__hint { + color: var(--muted); + font-size: 0.9rem; +} + +.sidebar__search { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.6rem; +} + +.sidebar__search input[type='search'] { + flex: 1; + padding: 0.4rem 0.6rem; + border-radius: 4px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--fg); +} + +.sidebar__search button { + padding: 0.35rem 0.75rem; + border: 1px solid transparent; + border-radius: 4px; + background: transparent; + color: var(--accent); + font-size: 0.85rem; + cursor: pointer; +} + +.sidebar__search button:hover:not([disabled]) { + background: var(--sidebar-hover-bg); +} + +.sidebar__status { + margin-top: 0.75rem; +} + +.sidebar__footer { + margin-top: auto; + padding-top: 1rem; +} + +.sidebar__footer button { + width: 100%; } .sidebar-section:first-of-type, @@ -957,21 +908,45 @@ button.icon-button.ghost:hover:not([disabled]) { display: flex; align-items: center; justify-content: space-between; - font-size: 0.75rem; color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.04em; +} + +.sidebar-section__title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.4rem; + width: 100%; + background: transparent; + border: none; + border-radius: 6px; + padding: 0.25rem 0.35rem; + margin: -0.25rem -0.35rem; + color: inherit; + font: inherit; + cursor: pointer; + text-align: left; +} + +.sidebar-section__title:hover:not([disabled]), +.sidebar-section__title:focus-visible { + background: var(--sidebar-hover-bg); + color: var(--fg); +} + +.sidebar-section__title:hover:not([disabled]) h3, +.sidebar-section__title:focus-visible h3 { + color: var(--fg); } .sidebar-section__header h3 { margin: 0; - font-size: 0.75rem; + font-size: 0.9rem; font-weight: 600; color: inherit; } .sidebar-item { - border: none; background: transparent; padding: 0.16rem 0.26rem; text-align: left; @@ -1020,7 +995,6 @@ button.icon-button.ghost:hover:not([disabled]) { } .sidebar-tag-pill { - border: 1px solid var(--sidebar-pill-border, rgba(15, 23, 42, 0.08)); border-radius: 999px; padding: 0.25rem 0.6rem; font-size: 0.75rem; @@ -1096,7 +1070,6 @@ button.icon-button.ghost:hover:not([disabled]) { .documents-panel { padding: 1.25rem 1.25rem 0 1.25rem; - background-color: var(--surface); } .tags-panel, @@ -1130,12 +1103,10 @@ button.icon-button.ghost:hover:not([disabled]) { .documents-panel .column-toolbar { flex-shrink: 0; padding-bottom: 0.4rem; - border-bottom: 1px solid var(--border); } .documents-panel .documents-scroll { overflow-y: auto; - border: none; background: transparent; min-height: 0; padding-right: 0.3rem; @@ -1149,15 +1120,11 @@ button.icon-button.ghost:hover:not([disabled]) { } .documents-panel thead th { - border-bottom: 1px solid var(--border); background-color: var(--surface); position: sticky; top: 0; z-index: 1; padding: 0.45rem 0.6rem; - font-size: 0.75rem; - letter-spacing: 0.04em; - text-transform: uppercase; color: var(--muted); } @@ -1268,14 +1235,9 @@ button.icon-button.ghost:hover:not([disabled]) { .documents-panel tbody tr { background: transparent; - border-bottom: 1px solid var(--border); transition: background 0.15s ease; } -.documents-panel tbody tr:last-child { - border-bottom: none; -} - .documents-panel tbody tr:hover { background: var(--surface-subtle); } @@ -1476,7 +1438,6 @@ button.icon-button.ghost:hover:not([disabled]) { background: var(--surface-subtle); color: var(--fg); font-size: 0.74rem; - border: 1px solid transparent; } .tag-chip { @@ -1509,7 +1470,7 @@ button.icon-button.ghost:hover:not([disabled]) { position: relative; padding: 1.25rem; overflow-y: auto; - border-left: 1px solid var(--border); + box-shadow: 0 0 24px var(--shadow-faint); } .detail-panel .column-body { @@ -1535,7 +1496,6 @@ button.icon-button.ghost:hover:not([disabled]) { overflow: auto; padding: 0.75rem; background: var(--surface-soft); - border: 1px solid var(--border); border-radius: 6px; box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.05); } @@ -1553,7 +1513,6 @@ button.icon-button.ghost:hover:not([disabled]) { width: 100%; min-height: 360px; height: 60vh; - border: none; background: var(--surface-subtle); border-radius: 4px; box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.05); @@ -1582,7 +1541,6 @@ button.icon-button.ghost:hover:not([disabled]) { .preview-pane { margin-top: 0.4rem; - border: none; border-radius: 0; background: transparent; min-height: 220px; @@ -1745,7 +1703,6 @@ button.icon-button.ghost:hover:not([disabled]) { height: 2.2em; padding: 0.45em; border-radius: 999px; - border: none; background: rgba(0, 0, 0, 0.55); color: #fff; cursor: pointer; @@ -1930,7 +1887,6 @@ form.inline { text-align: center; font-size: 0.95rem; box-shadow: none; - border: 1px solid var(--border); } .modal-backdrop { position: fixed; @@ -1945,7 +1901,6 @@ form.inline { .modal { background: var(--surface); - border: 1px solid var(--border); border-radius: 4px; box-shadow: 0 18px 42px var(--overlay-shadow); width: min(360px, 100%); @@ -1955,6 +1910,29 @@ form.inline { gap: 1rem; } +.modal--panel { + width: min(80vw, 960px); + max-height: 90vh; + padding: 0; + border-radius: 12px; + overflow: hidden; + gap: 0; +} + +.panel-modal__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border); +} + +.panel-modal__body { + flex: 1; + overflow: auto; + padding: 0; +} + .modal h3 { margin: 0; font-size: 1.15rem; @@ -2014,7 +1992,6 @@ form.inline { .login-card { width: min(340px, 100%); background: var(--surface); - border: 1px solid var(--border); border-radius: 2px; padding: 1.75rem; box-shadow: none; diff --git a/frontend/src/tags/TagsPanel.jsx b/frontend/src/tags/TagsPanel.jsx index 0c2c70e..fd9d9e0 100644 --- a/frontend/src/tags/TagsPanel.jsx +++ b/frontend/src/tags/TagsPanel.jsx @@ -1,12 +1,22 @@ import React, { useCallback, useMemo, useState } from 'react'; import { getTagColorStyle, HEX_COLOR_PATTERN } from '../utils/colors'; -function TagsPanel({ tags, onRefresh, onUpdateTag, onDeleteTag, onNotify }) { +function TagsPanel({ + tags, + onRefresh, + onCreateTag, + onUpdateTag, + onDeleteTag, + onNotify, +}) { const [editingId, setEditingId] = useState(null); const [draftLabel, setDraftLabel] = useState(''); const [draftColor, setDraftColor] = useState(''); const [saving, setSaving] = useState(false); const [deletingId, setDeletingId] = useState(null); + const [createLabel, setCreateLabel] = useState(''); + const [createColor, setCreateColor] = useState(''); + const [creating, setCreating] = useState(false); const startEdit = useCallback((tag) => { setEditingId(tag.id); @@ -98,6 +108,44 @@ function TagsPanel({ tags, onRefresh, onUpdateTag, onDeleteTag, onNotify }) { [onDeleteTag, editingId, cancelEdit, onNotify], ); + const handleCreate = useCallback( + async (event) => { + event.preventDefault(); + if (typeof onCreateTag !== 'function') { + return; + } + + const trimmedLabel = createLabel.trim(); + if (!trimmedLabel) { + onNotify?.('Tag label cannot be empty.', 'error'); + return; + } + + const trimmedColor = createColor.trim(); + const colorPattern = /^#([0-9a-fA-F]{6})$/; + if (trimmedColor && !colorPattern.test(trimmedColor)) { + onNotify?.('Colors must use the #RRGGBB format.', 'error'); + return; + } + + setCreating(true); + try { + await onCreateTag({ + label: trimmedLabel, + color: trimmedColor ? trimmedColor : null, + }); + setCreateLabel(''); + setCreateColor(''); + } catch (createError) { + const message = createError?.message || 'Failed to create tag.'; + onNotify?.(message, 'error'); + } finally { + setCreating(false); + } + }, + [createLabel, createColor, onCreateTag, onNotify], + ); + return (
@@ -105,12 +153,42 @@ function TagsPanel({ tags, onRefresh, onUpdateTag, onDeleteTag, onNotify }) {

Tags

{tags.length} total
-
+
+
+ setCreateLabel(event.target.value)} + disabled={creating} + /> + setCreateColor(event.target.value)} + disabled={creating} + aria-label="Tag color (optional)" + /> + {createColor && ( + + )} + +
From dc633783e02c7bc4f98bb17fe9310805df9e7e83 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 24 Oct 2025 15:13:37 +0200 Subject: [PATCH 02/17] frontend --- frontend/package-lock.json | 10 + frontend/package.json | 5 +- .../correspondents/CorrespondentsPanel.jsx | 2 +- frontend/src/detail/DetailPanel.jsx | 2 +- frontend/src/documents/DocumentsTable.jsx | 126 +++++----- frontend/src/index.jsx | 21 +- frontend/src/sidebar/Sidebar.jsx | 130 +++++------ frontend/src/skeuomorphic_ws.css | 2 - frontend/src/styles.css | 220 +++++++++++------- frontend/src/tags/TagsPanel.jsx | 2 +- 10 files changed, 283 insertions(+), 237 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aeb6da3..5e35cb5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "papercrate-frontend", "version": "0.1.0", "dependencies": { + "@fontsource/inter": "^5.2.8", "@tabler/icons-react": "3.11.0", "axios": "1.7.7", "react": "18.3.1", @@ -1852,6 +1853,15 @@ "node": ">=10.0.0" } }, + "node_modules/@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5de499f..a3f0da4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "lint": "echo \"No linting configured\"" }, "dependencies": { + "@fontsource/inter": "^5.2.8", "@tabler/icons-react": "3.11.0", "axios": "1.7.7", "react": "18.3.1", @@ -19,6 +20,7 @@ "@babel/core": "7.26.0", "@babel/preset-env": "7.26.0", "@babel/preset-react": "7.26.3", + "@svgr/webpack": "8.1.0", "babel-loader": "9.2.1", "css-loader": "7.1.2", "dotenv": "16.4.5", @@ -26,7 +28,6 @@ "style-loader": "4.0.0", "webpack": "5.95.0", "webpack-cli": "5.1.4", - "webpack-dev-server": "5.1.0", - "@svgr/webpack": "8.1.0" + "webpack-dev-server": "5.1.0" } } diff --git a/frontend/src/correspondents/CorrespondentsPanel.jsx b/frontend/src/correspondents/CorrespondentsPanel.jsx index 25f156a..46ba941 100644 --- a/frontend/src/correspondents/CorrespondentsPanel.jsx +++ b/frontend/src/correspondents/CorrespondentsPanel.jsx @@ -118,7 +118,7 @@ function CorrespondentsPanel({ }, []); return ( -
+

Correspondents

diff --git a/frontend/src/detail/DetailPanel.jsx b/frontend/src/detail/DetailPanel.jsx index c6c5589..7b5c7ca 100644 --- a/frontend/src/detail/DetailPanel.jsx +++ b/frontend/src/detail/DetailPanel.jsx @@ -1086,7 +1086,7 @@ const DetailPanel = ({ }; return ( -