various
This commit is contained in:
Generated
+17
@@ -1959,6 +1959,7 @@ checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder-lite",
|
"byteorder-lite",
|
||||||
|
"image-webp",
|
||||||
"moxcms",
|
"moxcms",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"png",
|
"png",
|
||||||
@@ -1966,6 +1967,16 @@ dependencies = [
|
|||||||
"zune-jpeg",
|
"zune-jpeg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "image-webp"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder-lite",
|
||||||
|
"quick-error",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.11.4"
|
version = "2.11.4"
|
||||||
@@ -2695,6 +2706,12 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-error"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.32.0"
|
version = "0.32.0"
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ sha2 = "0.10"
|
|||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
bytes = "1.5"
|
bytes = "1.5"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
|
||||||
pdfium-render = "0.8"
|
pdfium-render = "0.8"
|
||||||
mime_guess = "2.0"
|
mime_guess = "2.0"
|
||||||
tempfile = "3.10"
|
tempfile = "3.10"
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ impl JobHandler for GenerateThumbnailsJob {
|
|||||||
.put_object(
|
.put_object(
|
||||||
&s3_key,
|
&s3_key,
|
||||||
image.image_bytes.clone(),
|
image.image_bytes.clone(),
|
||||||
Some("image/png".into()),
|
Some("image/webp".into()),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -315,7 +315,7 @@ impl JobHandler for GenerateThumbnailsJob {
|
|||||||
.put_object(
|
.put_object(
|
||||||
&s3_key,
|
&s3_key,
|
||||||
image.image_bytes.clone(),
|
image.image_bytes.clone(),
|
||||||
Some("image/png".into()),
|
Some("image/webp".into()),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -609,7 +609,7 @@ fn encode_dynamic_image(image: image::DynamicImage) -> Result<GeneratedImage, St
|
|||||||
let (width, height) = image.dimensions();
|
let (width, height) = image.dimensions();
|
||||||
let mut cursor = Cursor::new(Vec::new());
|
let mut cursor = Cursor::new(Vec::new());
|
||||||
image
|
image
|
||||||
.write_to(&mut cursor, ImageFormat::Png)
|
.write_to(&mut cursor, ImageFormat::WebP)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
Ok(GeneratedImage {
|
Ok(GeneratedImage {
|
||||||
image_bytes: cursor.into_inner(),
|
image_bytes: cursor.into_inner(),
|
||||||
@@ -660,7 +660,7 @@ fn persist_assets_metadata(
|
|||||||
id: asset.asset_id,
|
id: asset.asset_id,
|
||||||
document_version_id: context.version.id,
|
document_version_id: context.version.id,
|
||||||
asset_type: asset.asset_type.to_string(),
|
asset_type: asset.asset_type.to_string(),
|
||||||
mime_type: "image/png".to_string(),
|
mime_type: "image/webp".to_string(),
|
||||||
metadata: json!({
|
metadata: json!({
|
||||||
"generated_at": Utc::now().to_rfc3339(),
|
"generated_at": Utc::now().to_rfc3339(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -120,6 +120,100 @@
|
|||||||
box-shadow: 2px 2px 4px var(--shadow-medium);
|
box-shadow: 2px 2px 4px var(--shadow-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1400;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: clamp(1.5rem, 4vw, 3rem);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--surface-overlay);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__content {
|
||||||
|
position: relative;
|
||||||
|
width: min(640px, 92vw);
|
||||||
|
max-height: min(80vh, 640px);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 24px 64px var(--shadow-strong);
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
pointer-events: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__header,
|
||||||
|
.desk-help-overlay__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__header {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__body {
|
||||||
|
padding: 1rem 1.25rem 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__body p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__list li {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__body kbd {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desk-help-overlay__footer {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.tag-chip--draggable:active {
|
.tag-chip--draggable:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { resolveDocumentAssetUrl, createAssetView } from './asset_manager';
|
import { resolveDocumentAssetUrl, createAssetView } from './asset_manager';
|
||||||
import { useAssetNavigator } from './hooks/useAssetNavigator';
|
import { useAssetNavigator } from './hooks/useAssetNavigator';
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from './ui/icons';
|
import { ArrowLeftIcon, ArrowRightIcon, CloseIcon } from './ui/icons';
|
||||||
import { createDocumentsTableHeaderActions } from './documents/DocumentsPanel';
|
import { createDocumentsTableHeaderActions } from './documents/DocumentsPanel';
|
||||||
import createWorkspaceSurfaceConfig from './documents/workspaceHeader';
|
import createWorkspaceSurfaceConfig from './documents/workspaceHeader';
|
||||||
import DetailPanel from './detail/DetailPanel';
|
import DetailPanel from './detail/DetailPanel';
|
||||||
@@ -33,6 +34,10 @@ const DEFAULT_CANVAS_HEIGHT = 680;
|
|||||||
const CARD_MIN = 240;
|
const CARD_MIN = 240;
|
||||||
const CARD_MAX = 340;
|
const CARD_MAX = 340;
|
||||||
const TAG_REMOVE_DISTANCE = 160;
|
const TAG_REMOVE_DISTANCE = 160;
|
||||||
|
const STACK_HIT_EPSILON = 4;
|
||||||
|
const STACK_CENTER_TOLERANCE = 0.35;
|
||||||
|
const STACK_CENTER_MIN = 32;
|
||||||
|
const STACK_ROTATION_TOLERANCE = 15;
|
||||||
|
|
||||||
const DEBUG_DRAG = false;
|
const DEBUG_DRAG = false;
|
||||||
const DEBUG_FOCUS = true;
|
const DEBUG_FOCUS = true;
|
||||||
@@ -568,6 +573,7 @@ const DesktopWorkspace = ({
|
|||||||
onDocumentOpen,
|
onDocumentOpen,
|
||||||
onInspectDocument = null,
|
onInspectDocument = null,
|
||||||
onDocumentPointerSelect = null,
|
onDocumentPointerSelect = null,
|
||||||
|
onDocumentStackSelect = null,
|
||||||
onAssignTagToDocument = null,
|
onAssignTagToDocument = null,
|
||||||
onRemoveTagFromDocument = null,
|
onRemoveTagFromDocument = null,
|
||||||
ensureAssetUrl = null,
|
ensureAssetUrl = null,
|
||||||
@@ -575,6 +581,8 @@ const DesktopWorkspace = ({
|
|||||||
activeTagIds = [],
|
activeTagIds = [],
|
||||||
selectedDocumentIds = [],
|
selectedDocumentIds = [],
|
||||||
onClearSelection = null,
|
onClearSelection = null,
|
||||||
|
helpOpen = false,
|
||||||
|
onHelpClose = null,
|
||||||
}) => {
|
}) => {
|
||||||
const items = useMemo(() => (searchResults ? searchResults : documents), [documents, searchResults]);
|
const items = useMemo(() => (searchResults ? searchResults : documents), [documents, searchResults]);
|
||||||
|
|
||||||
@@ -1715,6 +1723,7 @@ const syncLayoutSnapshot = useCallback(() => {
|
|||||||
onDocumentOpen,
|
onDocumentOpen,
|
||||||
onInspectDocument,
|
onInspectDocument,
|
||||||
onDocumentPointerSelect,
|
onDocumentPointerSelect,
|
||||||
|
onDocumentStackSelect,
|
||||||
ensureAssetUrl,
|
ensureAssetUrl,
|
||||||
getDocumentAsset,
|
getDocumentAsset,
|
||||||
handleNavigatorSnapshot,
|
handleNavigatorSnapshot,
|
||||||
@@ -1779,12 +1788,14 @@ const syncLayoutSnapshot = useCallback(() => {
|
|||||||
documentLookup,
|
documentLookup,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
onClearSelection,
|
onClearSelection,
|
||||||
|
onDocumentStackSelect,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DesktopProvider value={contextValue}>
|
<DesktopProvider value={contextValue}>
|
||||||
<DesktopWorkspaceView />
|
<DesktopWorkspaceView />
|
||||||
|
<DesktopHelpOverlay open={helpOpen} onClose={onHelpClose} />
|
||||||
</DesktopProvider>
|
</DesktopProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1823,15 +1834,245 @@ const DesktopWorkspaceView = () => {
|
|||||||
overlayOriginRect,
|
overlayOriginRect,
|
||||||
overlayOriginTransform,
|
overlayOriginTransform,
|
||||||
onDocumentPointerSelect,
|
onDocumentPointerSelect,
|
||||||
|
onDocumentStackSelect,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
onClearSelection,
|
onClearSelection,
|
||||||
|
documentLookup,
|
||||||
} = useDesktopContext();
|
} = useDesktopContext();
|
||||||
|
|
||||||
const { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel } =
|
const { handlePointerDown, handlePointerMove, handlePointerUp, handlePointerCancel } =
|
||||||
useDocumentDrag();
|
useDocumentDrag();
|
||||||
|
|
||||||
|
const resolveStackDocIds = useCallback(
|
||||||
|
(event, targetDocId = null) => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container || !event) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const pointerCanvasX = event.clientX - rect.left;
|
||||||
|
const pointerCanvasY = event.clientY - rect.top;
|
||||||
|
|
||||||
|
if (!Number.isFinite(pointerCanvasX) || !Number.isFinite(pointerCanvasY)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const hits = [];
|
||||||
|
items.forEach((doc) => {
|
||||||
|
if (!doc?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layout = layoutSnapshot.get(doc.id) ?? layoutRef.current.get(doc.id);
|
||||||
|
if (!layout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sizeInfo = ensureDocumentSize(doc);
|
||||||
|
if (!sizeInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { width, height } = sizeInfo;
|
||||||
|
if (!width || !height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTagSet.size) {
|
||||||
|
const docTagKeys = Array.isArray(doc.tags)
|
||||||
|
? doc.tags.map((tag) => (tag ? tag.id : null)).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
if (!docTagKeys.some((key) => activeTagSet.has(key))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerX = Number(layout.centerX);
|
||||||
|
const centerY = Number(layout.centerY);
|
||||||
|
if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotationDeg = Number(layout.rotation) || 0;
|
||||||
|
const rotationRad = (rotationDeg * Math.PI) / 180;
|
||||||
|
const dx = pointerCanvasX - centerX;
|
||||||
|
const dy = pointerCanvasY - centerY;
|
||||||
|
const cosRotation = Math.cos(-rotationRad);
|
||||||
|
const sinRotation = Math.sin(-rotationRad);
|
||||||
|
const localX = dx * cosRotation - dy * sinRotation;
|
||||||
|
const localY = dx * sinRotation + dy * cosRotation;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const halfHeight = height / 2;
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(localX) <= halfWidth + STACK_HIT_EPSILON
|
||||||
|
&& Math.abs(localY) <= halfHeight + STACK_HIT_EPSILON
|
||||||
|
) {
|
||||||
|
const docKey = String(doc.id);
|
||||||
|
if (!hits.some((entry) => entry.id === docKey)) {
|
||||||
|
hits.push({
|
||||||
|
id: docKey,
|
||||||
|
z: Number.isFinite(layout.z) ? layout.z : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hits.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
hits.sort((a, b) => (b.z ?? 0) - (a.z ?? 0));
|
||||||
|
|
||||||
|
const targetKey = targetDocId != null ? String(targetDocId) : hits[0].id;
|
||||||
|
const orderedIds = hits.map((entry) => entry.id);
|
||||||
|
|
||||||
|
if (targetKey) {
|
||||||
|
const targetIndex = orderedIds.indexOf(targetKey);
|
||||||
|
if (targetIndex > 0) {
|
||||||
|
const [targetEntry] = orderedIds.splice(targetIndex, 1);
|
||||||
|
orderedIds.unshift(targetEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryKey = orderedIds[0];
|
||||||
|
if (!primaryKey) {
|
||||||
|
return orderedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryDoc = documentLookup.get(primaryKey) || null;
|
||||||
|
const primaryLayout = primaryDoc
|
||||||
|
? layoutSnapshot.get(primaryDoc.id) ?? layoutRef.current.get(primaryKey)
|
||||||
|
: null;
|
||||||
|
const primarySize = primaryDoc ? ensureDocumentSize(primaryDoc) : null;
|
||||||
|
|
||||||
|
if (!primaryLayout || !primarySize) {
|
||||||
|
return orderedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryCenterX = Number(primaryLayout.centerX);
|
||||||
|
const primaryCenterY = Number(primaryLayout.centerY);
|
||||||
|
const primaryRotation = Number(primaryLayout.rotation) || 0;
|
||||||
|
if (!Number.isFinite(primaryCenterX) || !Number.isFinite(primaryCenterY)) {
|
||||||
|
return orderedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerTolX = Math.max(primarySize.width * STACK_CENTER_TOLERANCE, STACK_CENTER_MIN);
|
||||||
|
const centerTolY = Math.max(primarySize.height * STACK_CENTER_TOLERANCE, STACK_CENTER_MIN);
|
||||||
|
|
||||||
|
const filteredIds = [];
|
||||||
|
|
||||||
|
orderedIds.forEach((docKey, index) => {
|
||||||
|
if (!docKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index === 0 || docKey === targetKey) {
|
||||||
|
filteredIds.push(docKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateDoc = documentLookup.get(docKey) || null;
|
||||||
|
if (!candidateDoc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateLayout = layoutSnapshot.get(candidateDoc.id) ?? layoutRef.current.get(docKey);
|
||||||
|
if (!candidateLayout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateSize = ensureDocumentSize(candidateDoc);
|
||||||
|
if (!candidateSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateCenterX = Number(candidateLayout.centerX);
|
||||||
|
const candidateCenterY = Number(candidateLayout.centerY);
|
||||||
|
if (!Number.isFinite(candidateCenterX) || !Number.isFinite(candidateCenterY)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dx = Math.abs(candidateCenterX - primaryCenterX);
|
||||||
|
const dy = Math.abs(candidateCenterY - primaryCenterY);
|
||||||
|
if (dx > centerTolX || dy > centerTolY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateRotation = Number(candidateLayout.rotation) || 0;
|
||||||
|
const rotationDiffRaw = Math.abs(candidateRotation - primaryRotation) % 360;
|
||||||
|
const rotationDiff = rotationDiffRaw > 180 ? 360 - rotationDiffRaw : rotationDiffRaw;
|
||||||
|
if (rotationDiff > STACK_ROTATION_TOLERANCE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeRatio = candidateSize.width && primarySize.width
|
||||||
|
? Math.min(candidateSize.width, primarySize.width) / Math.max(candidateSize.width, primarySize.width)
|
||||||
|
: 1;
|
||||||
|
const heightRatio = candidateSize.height && primarySize.height
|
||||||
|
? Math.min(candidateSize.height, primarySize.height) / Math.max(candidateSize.height, primarySize.height)
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
if (sizeRatio < 0.55 || heightRatio < 0.55) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredIds.push(docKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredIds;
|
||||||
|
},
|
||||||
|
[
|
||||||
|
activeTagSet,
|
||||||
|
ensureDocumentSize,
|
||||||
|
items,
|
||||||
|
layoutRef,
|
||||||
|
layoutSnapshot,
|
||||||
|
containerRef,
|
||||||
|
documentLookup,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const allSizesReady = items.every((doc) => ensureDocumentSize(doc));
|
const allSizesReady = items.every((doc) => ensureDocumentSize(doc));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined' || typeof onClearSelection !== 'function') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if (!event || event.defaultPrevented) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = event.key;
|
||||||
|
if (!(key === ' ' || key === 'Space' || key === 'Spacebar')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedDocumentIds || selectedDocumentIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.target;
|
||||||
|
if (target instanceof HTMLElement) {
|
||||||
|
const tag = target.tagName ? target.tagName.toLowerCase() : '';
|
||||||
|
if (
|
||||||
|
target.isContentEditable
|
||||||
|
|| tag === 'input'
|
||||||
|
|| tag === 'textarea'
|
||||||
|
|| tag === 'select'
|
||||||
|
|| tag === 'button'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
onClearSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown, true);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown, true);
|
||||||
|
}, [onClearSelection, selectedDocumentIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -1931,8 +2172,27 @@ const DesktopWorkspaceView = () => {
|
|||||||
}}
|
}}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
const alreadySelected = selectedDocumentIds.includes(doc.id);
|
const alreadySelected = selectedDocumentIds.includes(doc.id);
|
||||||
|
const metaOrCtrlOnly =
|
||||||
|
(event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey;
|
||||||
|
|
||||||
|
let stackDocIds = null;
|
||||||
if (
|
if (
|
||||||
typeof onDocumentPointerSelect === 'function'
|
metaOrCtrlOnly
|
||||||
|
&& selectedDocumentIds.length === 0
|
||||||
|
&& typeof onDocumentStackSelect === 'function'
|
||||||
|
) {
|
||||||
|
const hits = resolveStackDocIds(event, doc.id);
|
||||||
|
if (Array.isArray(hits) && hits.length > 0) {
|
||||||
|
stackDocIds = hits;
|
||||||
|
onDocumentStackSelect(hits, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stackHandled = Array.isArray(stackDocIds) && stackDocIds.length > 0;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!stackHandled
|
||||||
|
&& typeof onDocumentPointerSelect === 'function'
|
||||||
&& (!alreadySelected
|
&& (!alreadySelected
|
||||||
|| event.metaKey
|
|| event.metaKey
|
||||||
|| event.ctrlKey
|
|| event.ctrlKey
|
||||||
@@ -1941,7 +2201,7 @@ const DesktopWorkspaceView = () => {
|
|||||||
) {
|
) {
|
||||||
onDocumentPointerSelect(doc.id, event);
|
onDocumentPointerSelect(doc.id, event);
|
||||||
}
|
}
|
||||||
handlePointerDown(event, doc.id);
|
handlePointerDown(event, doc.id, { stackDocIds });
|
||||||
}}
|
}}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
@@ -2021,6 +2281,110 @@ const DesktopWorkspaceView = () => {
|
|||||||
|
|
||||||
export default DesktopWorkspace;
|
export default DesktopWorkspace;
|
||||||
|
|
||||||
|
const DesktopHelpOverlay = ({ open = false, onClose = null }) => {
|
||||||
|
const portalTarget = typeof document !== 'undefined' ? document.body : null;
|
||||||
|
const closeButtonRef = useRef(null);
|
||||||
|
const previousFocusRef = useRef(null);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
if (typeof onClose === 'function') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || typeof window === 'undefined') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if (!event) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown, true);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown, true);
|
||||||
|
}, [open, handleClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
const previous = previousFocusRef.current;
|
||||||
|
if (previous && typeof previous.focus === 'function') {
|
||||||
|
previous.focus();
|
||||||
|
}
|
||||||
|
previousFocusRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
previousFocusRef.current = document.activeElement instanceof HTMLElement
|
||||||
|
? document.activeElement
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeButtonRef.current && typeof closeButtonRef.current.focus === 'function') {
|
||||||
|
closeButtonRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
if (!open || !portalTarget) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="desk-help-overlay" role="dialog" aria-modal="true" aria-labelledby="desk-help-title">
|
||||||
|
<div className="desk-help-overlay__backdrop" onClick={handleClose} />
|
||||||
|
<div className="desk-help-overlay__content">
|
||||||
|
<div className="desk-help-overlay__header">
|
||||||
|
<h2 id="desk-help-title">Desk view tips</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-button"
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label="Close desk view tips"
|
||||||
|
ref={closeButtonRef}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="desk-help-overlay__body">
|
||||||
|
<p>Use the desk as a freeform workspace for triage and quick comparisons.</p>
|
||||||
|
<ul className="desk-help-overlay__list">
|
||||||
|
<li><strong>Single-click</strong> a document to open it in the detail panel.</li>
|
||||||
|
<li><strong>Double-click</strong> to open the zoomed preview.</li>
|
||||||
|
<li>
|
||||||
|
<strong>Drag</strong> selected cards to reposition them; build a selection with
|
||||||
|
{' '}
|
||||||
|
<kbd>Cmd</kbd>/<kbd>Ctrl</kbd>
|
||||||
|
{' '}+ click or Shift-click.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Cmd/Ctrl + click</strong> with an empty selection scoops up the stack under
|
||||||
|
{' '}the pointer.
|
||||||
|
</li>
|
||||||
|
<li><strong>Space</strong> clears the current selection.</li>
|
||||||
|
<li>
|
||||||
|
<strong>Drag tags</strong> from the sidebar onto a card to assign them, or fling a
|
||||||
|
{' '}tag away to remove it.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="desk-help-overlay__footer">
|
||||||
|
<button type="button" className="button primary" onClick={handleClose}>
|
||||||
|
Got it
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
portalTarget,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const createDesktopSurface = ({
|
export const createDesktopSurface = ({
|
||||||
workspaceProps,
|
workspaceProps,
|
||||||
renderSidebarToggle,
|
renderSidebarToggle,
|
||||||
@@ -2049,6 +2413,7 @@ export const createDesktopSurface = ({
|
|||||||
viewMode: viewMode || 'desk',
|
viewMode: viewMode || 'desk',
|
||||||
onViewModeChange,
|
onViewModeChange,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
onShowDeskHelp: viewMode === 'desk' ? workspaceProps?.onOpenHelp : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
|
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ const AppLayout = () => {
|
|||||||
const stored = window.localStorage.getItem('papercrate_view_mode');
|
const stored = window.localStorage.getItem('papercrate_view_mode');
|
||||||
return stored === 'grid' || stored === 'desk' ? stored : 'list';
|
return stored === 'grid' || stored === 'desk' ? stored : 'list';
|
||||||
});
|
});
|
||||||
|
const [deskHelpOpen, setDeskHelpOpen] = useState(false);
|
||||||
const lastNonDeskViewRef = useRef(documentsViewMode === 'desk' ? 'list' : documentsViewMode);
|
const lastNonDeskViewRef = useRef(documentsViewMode === 'desk' ? 'list' : documentsViewMode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -205,6 +206,12 @@ const AppLayout = () => {
|
|||||||
lastNonDeskViewRef.current = documentsViewMode;
|
lastNonDeskViewRef.current = documentsViewMode;
|
||||||
}
|
}
|
||||||
}, [documentsViewMode]);
|
}, [documentsViewMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (documentsViewMode !== 'desk' && deskHelpOpen) {
|
||||||
|
setDeskHelpOpen(false);
|
||||||
|
}
|
||||||
|
}, [documentsViewMode, deskHelpOpen]);
|
||||||
const initialRowSelection = [];
|
const initialRowSelection = [];
|
||||||
const tokenRef = useRef(token);
|
const tokenRef = useRef(token);
|
||||||
const refreshPromiseRef = useRef(null);
|
const refreshPromiseRef = useRef(null);
|
||||||
@@ -4819,6 +4826,36 @@ const AppLayout = () => {
|
|||||||
[handleRowSelection],
|
[handleRowSelection],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDeskDocumentStackSelect = useCallback(
|
||||||
|
(docIds) => {
|
||||||
|
if (!Array.isArray(docIds) || docIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowKeys = docIds
|
||||||
|
.map((id) => resolveDocumentRowKey(id))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (!rowKeys.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applySelection(rowKeys, {
|
||||||
|
anchor: rowKeys[0],
|
||||||
|
interactedKeys: rowKeys,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[applySelection],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeskHelpOpen = useCallback(() => {
|
||||||
|
setDeskHelpOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeskHelpClose = useCallback(() => {
|
||||||
|
setDeskHelpOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const deskWorkspaceProps = useMemo(
|
const deskWorkspaceProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
documents,
|
documents,
|
||||||
@@ -4832,6 +4869,10 @@ const AppLayout = () => {
|
|||||||
onDocumentOpen: openDocumentPreview,
|
onDocumentOpen: openDocumentPreview,
|
||||||
onInspectDocument: handleDeskInspectDocument,
|
onInspectDocument: handleDeskInspectDocument,
|
||||||
onDocumentPointerSelect: handleDeskDocumentPointerSelect,
|
onDocumentPointerSelect: handleDeskDocumentPointerSelect,
|
||||||
|
onDocumentStackSelect: handleDeskDocumentStackSelect,
|
||||||
|
onOpenHelp: handleDeskHelpOpen,
|
||||||
|
helpOpen: deskHelpOpen,
|
||||||
|
onHelpClose: handleDeskHelpClose,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
onClearSelection: clearDocumentSelection,
|
onClearSelection: clearDocumentSelection,
|
||||||
resolveThumbnailUrl: resolveThumbnailUrlForDoc,
|
resolveThumbnailUrl: resolveThumbnailUrlForDoc,
|
||||||
@@ -4853,6 +4894,10 @@ const AppLayout = () => {
|
|||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
handleDeskInspectDocument,
|
handleDeskInspectDocument,
|
||||||
handleDeskDocumentPointerSelect,
|
handleDeskDocumentPointerSelect,
|
||||||
|
handleDeskDocumentStackSelect,
|
||||||
|
handleDeskHelpOpen,
|
||||||
|
handleDeskHelpClose,
|
||||||
|
deskHelpOpen,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
clearDocumentSelection,
|
clearDocumentSelection,
|
||||||
resolveThumbnailUrlForDoc,
|
resolveThumbnailUrlForDoc,
|
||||||
|
|||||||
@@ -97,18 +97,21 @@ const useDocumentDrag = () => {
|
|||||||
if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
|
if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openOverlayForDoc(data.docId, data.originInfo);
|
|
||||||
},
|
|
||||||
onDouble: ({ data }) => {
|
|
||||||
if (!data || !data.docId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof onInspectDocument === 'function') {
|
if (typeof onInspectDocument === 'function') {
|
||||||
onInspectDocument(data.docId);
|
onInspectDocument(data.docId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onDocumentOpen?.(data.docId);
|
onDocumentOpen?.(data.docId);
|
||||||
},
|
},
|
||||||
|
onDouble: ({ data, event }) => {
|
||||||
|
if (!data || !data.docId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openOverlayForDoc(data.docId, data.originInfo);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dragStateRef = useRef(null);
|
const dragStateRef = useRef(null);
|
||||||
@@ -247,7 +250,7 @@ const useDocumentDrag = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(event, docIdInput) => {
|
(event, docIdInput, options = {}) => {
|
||||||
if (debugDrag) {
|
if (debugDrag) {
|
||||||
console.log(
|
console.log(
|
||||||
'[desk] handlePointerDown fired for doc',
|
'[desk] handlePointerDown fired for doc',
|
||||||
@@ -275,15 +278,31 @@ const useDocumentDrag = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stackDocIdsOption = Array.isArray(options?.stackDocIds)
|
||||||
|
? options.stackDocIds
|
||||||
|
.map((value) => (value != null ? String(value) : null))
|
||||||
|
.filter(Boolean)
|
||||||
|
: null;
|
||||||
|
|
||||||
let selectionIds = Array.isArray(selectedDocumentIds)
|
let selectionIds = Array.isArray(selectedDocumentIds)
|
||||||
? selectedDocumentIds.map((id) => String(id))
|
? selectedDocumentIds.map((id) => String(id))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
if (stackDocIdsOption && stackDocIdsOption.length) {
|
||||||
|
selectionIds = stackDocIdsOption;
|
||||||
|
}
|
||||||
|
|
||||||
const metaOrCtrl = event.metaKey || event.ctrlKey;
|
const metaOrCtrl = event.metaKey || event.ctrlKey;
|
||||||
if (metaOrCtrl && !selectionIds.includes(docKey)) {
|
if (!stackDocIdsOption && metaOrCtrl && !selectionIds.includes(docKey)) {
|
||||||
selectionIds = [...selectionIds, docKey];
|
selectionIds = [...selectionIds, docKey];
|
||||||
}
|
}
|
||||||
let groupDocIds = [];
|
let groupDocIds = [];
|
||||||
if (selectionIds.includes(docKey) && selectionIds.length > 1) {
|
if (stackDocIdsOption && stackDocIdsOption.length) {
|
||||||
|
groupDocIds = stackDocIdsOption.filter((id, index, array) => {
|
||||||
|
const unique = array.indexOf(id) === index;
|
||||||
|
return unique && documentLookup.has(id);
|
||||||
|
});
|
||||||
|
} else if (selectionIds.includes(docKey) && selectionIds.length > 1) {
|
||||||
groupDocIds = selectionIds
|
groupDocIds = selectionIds
|
||||||
.map((id) => String(id))
|
.map((id) => String(id))
|
||||||
.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id));
|
.filter((id, index, array) => array.indexOf(id) === index && documentLookup.has(id));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
IconFileStack,
|
IconFileStack,
|
||||||
RefreshIcon,
|
RefreshIcon,
|
||||||
MinusVerticalIcon,
|
MinusVerticalIcon,
|
||||||
|
InfoIcon,
|
||||||
} from '../ui/icons';
|
} from '../ui/icons';
|
||||||
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
||||||
import createWorkspaceSurfaceConfig from './workspaceHeader';
|
import createWorkspaceSurfaceConfig from './workspaceHeader';
|
||||||
@@ -526,6 +527,7 @@ export const createDocumentsTableHeaderActions = ({
|
|||||||
viewMode,
|
viewMode,
|
||||||
onViewModeChange,
|
onViewModeChange,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
onShowDeskHelp = null,
|
||||||
}) => {
|
}) => {
|
||||||
const isListView = viewMode === 'list';
|
const isListView = viewMode === 'list';
|
||||||
const isGridView = viewMode === 'grid';
|
const isGridView = viewMode === 'grid';
|
||||||
@@ -574,6 +576,17 @@ export const createDocumentsTableHeaderActions = ({
|
|||||||
>
|
>
|
||||||
<RefreshIcon />
|
<RefreshIcon />
|
||||||
</button>
|
</button>
|
||||||
|
{isDeskView && typeof onShowDeskHelp === 'function' ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-button"
|
||||||
|
onClick={onShowDeskHelp}
|
||||||
|
aria-label="Show desk view tips"
|
||||||
|
title="Show desk view tips"
|
||||||
|
>
|
||||||
|
<InfoIcon />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
const SIDEBAR_COLLAPSE_STORAGE_KEY = 'papercrate_sidebar_collapsed';
|
||||||
|
|
||||||
const SidebarContext = createContext(null);
|
const SidebarContext = createContext(null);
|
||||||
|
|
||||||
const THEME_STORAGE_KEY = 'papercrate_theme_settings';
|
const THEME_STORAGE_KEY = 'papercrate_theme_settings';
|
||||||
@@ -71,8 +73,26 @@ const loadInitialThemeSettings = () => {
|
|||||||
return { neutralHue: neutralHueValue, mode: modeValue };
|
return { neutralHue: neutralHueValue, mode: modeValue };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadInitialCollapsedState = (defaultValue) => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return Boolean(defaultValue);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const stored = window.sessionStorage.getItem(SIDEBAR_COLLAPSE_STORAGE_KEY);
|
||||||
|
if (stored === '1' || stored === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stored === '0' || stored === 'false') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[sidebar] failed to read collapse state', error);
|
||||||
|
}
|
||||||
|
return Boolean(defaultValue);
|
||||||
|
};
|
||||||
|
|
||||||
export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
||||||
const [collapsed, setCollapsed] = useState(initialCollapsed);
|
const [collapsed, setCollapsedState] = useState(() => loadInitialCollapsedState(initialCollapsed));
|
||||||
const initialTheme = useMemo(() => loadInitialThemeSettings(), []);
|
const initialTheme = useMemo(() => loadInitialThemeSettings(), []);
|
||||||
const [neutralHue, setNeutralHueState] = useState(initialTheme.neutralHue);
|
const [neutralHue, setNeutralHueState] = useState(initialTheme.neutralHue);
|
||||||
const [themeMode, setThemeModeState] = useState(initialTheme.mode);
|
const [themeMode, setThemeModeState] = useState(initialTheme.mode);
|
||||||
@@ -141,6 +161,25 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
|||||||
setThemeModeState(THEME_MODES[nextIndex]);
|
setThemeModeState(THEME_MODES[nextIndex]);
|
||||||
}, [themeMode]);
|
}, [themeMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(SIDEBAR_COLLAPSE_STORAGE_KEY, collapsed ? '1' : '0');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[sidebar] failed to persist collapse state', error);
|
||||||
|
}
|
||||||
|
}, [collapsed]);
|
||||||
|
|
||||||
|
const setCollapsed = useCallback((value) => {
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
setCollapsedState((prev) => Boolean(value(prev)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCollapsedState(Boolean(value));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const contextValue = useMemo(
|
const contextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
collapsed,
|
collapsed,
|
||||||
@@ -154,7 +193,16 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
|||||||
themeModes: THEME_MODES,
|
themeModes: THEME_MODES,
|
||||||
defaultNeutralHue: DEFAULT_NEUTRAL_HUE,
|
defaultNeutralHue: DEFAULT_NEUTRAL_HUE,
|
||||||
}),
|
}),
|
||||||
[collapsed, neutralHue, setNeutralHue, resetNeutralHue, themeMode, setThemeMode, cycleThemeMode],
|
[
|
||||||
|
collapsed,
|
||||||
|
setCollapsed,
|
||||||
|
neutralHue,
|
||||||
|
setNeutralHue,
|
||||||
|
resetNeutralHue,
|
||||||
|
themeMode,
|
||||||
|
setThemeMode,
|
||||||
|
cycleThemeMode,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <SidebarContext.Provider value={contextValue}>{children}</SidebarContext.Provider>;
|
return <SidebarContext.Provider value={contextValue}>{children}</SidebarContext.Provider>;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
IconLayoutSidebarLeftCollapse,
|
IconLayoutSidebarLeftCollapse,
|
||||||
IconLayoutSidebarLeftExpand,
|
IconLayoutSidebarLeftExpand,
|
||||||
IconLayoutSidebarRightCollapse,
|
IconLayoutSidebarRightCollapse,
|
||||||
|
IconInfoCircle,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import FolderSvg from '../assets/folder.svg';
|
import FolderSvg from '../assets/folder.svg';
|
||||||
|
|
||||||
@@ -158,6 +159,15 @@ export const SidebarExpandIcon = ({ className, size = '1em', stroke = 1.6, ...re
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const InfoIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||||
|
<IconInfoCircle
|
||||||
|
className={composeClassName('icon', className)}
|
||||||
|
size={size}
|
||||||
|
stroke={stroke}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
export const DetailPanelCollapseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
export const DetailPanelCollapseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||||
<IconLayoutSidebarRightCollapse
|
<IconLayoutSidebarRightCollapse
|
||||||
className={composeClassName('icon', className)}
|
className={composeClassName('icon', className)}
|
||||||
|
|||||||
Reference in New Issue
Block a user