refactor: extract various constants

This commit is contained in:
2025-11-25 10:10:42 +01:00
parent 4d30e47ee8
commit ccc22beda2
40 changed files with 326 additions and 216 deletions
+12 -32
View File
@@ -10,13 +10,16 @@ import React, {
} from 'react';
import type { CSSProperties, PointerEvent as ReactPointerEvent, RefObject } from 'react';
import { useSidebarContext } from '../sidebar/SidebarContext';
type PanelKey = 'sidebar' | 'detail';
interface PanelLimits {
maxRatio: number;
minPx: number;
}
import {
DEFAULT_DETAIL_WIDTH,
DEFAULT_SIDEBAR_WIDTH,
MINIMAL_FREE_RATIO,
MINIMUM_MAIN_CONTENT_WIDTH,
PANEL_LIMITS,
PANEL_STORAGE_KEYS,
SIDEBAR_SOLO_THRESHOLD,
type PanelKey,
} from '../constants/layout';
interface SetPanelWidthOptions {
commit?: boolean;
@@ -50,29 +53,6 @@ type PanelResizeBindings = {
const PanelManagerContext = createContext<PanelManagerContextValue | null>(null);
const PANEL_LIMITS: Record<PanelKey, PanelLimits> = {
sidebar: {
maxRatio: 1 / 3,
minPx: 280,
},
detail: {
maxRatio: 2 / 3,
minPx: 320,
},
};
const STORAGE_KEYS: Record<PanelKey, string> = {
sidebar: 'papercrate_sidebar_width',
detail: 'papercrate_detail_width',
};
const DEFAULT_SIDEBAR_WIDTH = 320;
const DEFAULT_DETAIL_WIDTH = 420;
const MINIMAL_FREE_RATIO = 1 / 3;
const SIDEBAR_SOLO_THRESHOLD = 1 / 2;
const MINIMUM_MAIN_CONTENT_WIDTH = 160;
const clampPanelWidth = (panel: PanelKey, value: number): number => {
const numeric = Number(value);
const limits = PANEL_LIMITS[panel];
@@ -86,7 +66,7 @@ const clampPanelWidth = (panel: PanelKey, value: number): number => {
};
const readStoredWidth = (panel: PanelKey, fallback: number): number => {
const raw = window.localStorage.getItem(STORAGE_KEYS[panel]);
const raw = window.localStorage.getItem(PANEL_STORAGE_KEYS[panel]);
if (!raw) {
return fallback;
}
@@ -95,7 +75,7 @@ const readStoredWidth = (panel: PanelKey, fallback: number): number => {
};
const persistWidth = (panel: PanelKey, value: number): void => {
window.localStorage.setItem(STORAGE_KEYS[panel], String(Math.round(value)));
window.localStorage.setItem(PANEL_STORAGE_KEYS[panel], String(Math.round(value)));
};
const applyPanelWidthToRoot = (panel: PanelKey, width: number, active: boolean): void => {
+6 -5
View File
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useMemo, useReducer } from 'react';
import { clearAuthToken, setAuthToken, setAuthRefreshHandlers } from '../lib/apiClient';
import { ApiProvider } from './ApiContext';
import { listTenants } from '../lib/apiClient';
import { STORED_TOKEN_KEY } from '../constants/app';
type Tenant = Record<string, unknown> | null;
@@ -47,7 +48,7 @@ type AppAction =
const storage = window.sessionStorage;
const STORED_TOKEN = storage?.getItem('papercrate_token') ?? '';
const storedToken = storage?.getItem(STORED_TOKEN_KEY) ?? '';
let STORED_TENANT: Tenant = null;
if (storage) {
@@ -61,13 +62,13 @@ if (storage) {
}
}
if (STORED_TOKEN) {
setAuthToken(STORED_TOKEN);
if (storedToken) {
setAuthToken(storedToken);
}
const initialAppState: AppState = {
status: STORED_TOKEN ? 'authenticated' : 'logged-out',
token: STORED_TOKEN,
status: storedToken ? 'authenticated' : 'logged-out',
token: storedToken,
error: null,
isRefreshing: false,
tenantSelection: null,
+1 -2
View File
@@ -1,10 +1,9 @@
import type { DocumentId, FolderId } from '../types/identifiers';
import { ENTRY_KEY_SEPARATOR } from '../constants/app';
// Entry key utilities for workspace selection
// Entry keys are strings in the format "document:id" or "folder:id"
const ENTRY_KEY_SEPARATOR = ':';
// Create entry key strings
export const createDocumentEntryKey = (documentId: DocumentId): string =>
`document${ENTRY_KEY_SEPARATOR}${documentId}`;
+6 -5
View File
@@ -4,11 +4,12 @@ import {
DEFAULT_SORT_FIELD,
SORT_FIELD_VALUES,
} from './workspaceUtils';
const VIEW_MODE_STORAGE_KEY = 'papercrate_view_mode';
const SORT_FIELD_STORAGE_KEY = 'papercrate_sort_field';
const SORT_DIRECTION_STORAGE_KEY = 'papercrate_sort_direction';
const INCLUDE_DESCENDANTS_STORAGE_KEY = 'papercrate_include_descendants';
import {
INCLUDE_DESCENDANTS_STORAGE_KEY,
SORT_DIRECTION_STORAGE_KEY,
SORT_FIELD_STORAGE_KEY,
VIEW_MODE_STORAGE_KEY,
} from '../constants/workspace';
const readSessionStorage = (key: string): string | null => {
try {
+1 -3
View File
@@ -4,9 +4,7 @@ import TagsPanel from '../tags/TagsPanel';
import CorrespondentsPanel, { CorrespondentsPanelProps } from '../correspondents/CorrespondentsPanel';
import PanelHeader from '../ui/PanelHeader';
import { CloseIcon } from '../ui/icons';
const TAGS_MODAL = 'tags';
const CORRESPONDENTS_MODAL = 'correspondents';
import { CORRESPONDENTS_MODAL, TAGS_MODAL } from '../constants/app';
interface TagRecord {
id?: string;
+15 -5
View File
@@ -1,8 +1,18 @@
export const DEFAULT_FOLDER_NAME = 'Documents';
export const DEFAULT_SORT_FIELD = 'title';
export const DEFAULT_SORT_DIRECTION = 'asc';
export const SORT_FIELD_VALUES = ['title', 'issued_at', 'created_at', 'updated_at'];
export const TAG_FILTER_UNTAGGED = '__UNTAGGED__';
import {
DEFAULT_FOLDER_NAME,
DEFAULT_SORT_DIRECTION,
DEFAULT_SORT_FIELD,
SORT_FIELD_VALUES,
TAG_FILTER_UNTAGGED,
} from '../constants/workspace';
export {
DEFAULT_FOLDER_NAME,
DEFAULT_SORT_DIRECTION,
DEFAULT_SORT_FIELD,
SORT_FIELD_VALUES,
TAG_FILTER_UNTAGGED,
};
export const hasFiles = (event) =>
Array.from(event.dataTransfer?.types || []).includes('Files');
+4
View File
@@ -0,0 +1,4 @@
export const ENTRY_KEY_SEPARATOR = ':';
export const TAGS_MODAL = 'tags';
export const CORRESPONDENTS_MODAL = 'correspondents';
export const STORED_TOKEN_KEY = 'papercrate_token';
+1
View File
@@ -0,0 +1 @@
export const HEX_COLOR_PATTERN = /^#?([0-9a-fA-F]{6})$/;
+48
View File
@@ -0,0 +1,48 @@
export const DB_NAME = 'papercrate_desk';
export const DB_VERSION = 1;
export const LAYOUT_STORE = 'layouts';
export const CLICK_ACTIONS = {
selectSingle: 'selectSingle',
openDetail: 'openDetail',
addCard: 'addCard',
addStack: 'addStack',
none: 'none',
} as const;
export const DRAG_ACTIONS = {
dragSelectSingle: 'dragSelectSingle',
dragSelection: 'dragSelection',
dragSelectStack: 'dragSelectStack',
none: 'none',
} as const;
export const STACK_HIT_EPSILON = 4;
export const POINTER_DRAG_THRESHOLD_SQUARED = 16;
export const LONG_PRESS_DURATION_MS = 450;
export const DRAG_HYSTERESIS_PX = 4;
export const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX;
export const EDGE_COLLISION_THRESHOLD = 0.5;
export const TAG_REMOVE_DISTANCE = 160;
export const DEBUG_DRAG = false;
export const DEBUG_FOCUS = false;
export const DESK_CANVAS_PADDING = 24;
export const DESK_ROTATION_RANGE = 7;
export const DESK_DEFAULT_CANVAS_WIDTH = 1024;
export const DESK_DEFAULT_CANVAS_HEIGHT = 680;
export const DESK_CARD_MIN = 240;
export const DESK_CARD_MAX = 340;
export const CARD_PAGE_WEIGHT_GRAMS = 5;
export const CARD_BASE_WEIGHT_GRAMS = 30;
export const DEFAULT_Z_START = 10;
export const MIN_TIMESTEP = 1 / 120;
export const MAX_TIMESTEP = 1 / 20;
export const MAX_DYNAMIC_ROTATION = 7;
export const MAX_ANGULAR_VELOCITY = 210;
export const ANGULAR_DAMPING = 10;
export const TORQUE_TO_ACCELERATION = 0.007;
export const SETTLE_ANGULAR_VELOCITY = 0.45;
+18
View File
@@ -0,0 +1,18 @@
export const DEFAULT_THUMBNAIL_SIZE = 48;
export const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'] as const;
export const TAG_TEXT_MIME_TYPE = 'text/plain';
export const DEFAULT_GRID_ICON_SIZE = 144;
export const SORT_OPTIONS = [
{ value: 'title', label: 'Title' },
{ value: 'issued_at', label: 'Issued date' },
{ value: 'created_at', label: 'Added' },
{ value: 'updated_at', label: 'Updated date' },
] as const;
export const SORT_LABEL_LOOKUP = SORT_OPTIONS.reduce<Record<string, string>>((acc, option) => {
acc[option.value] = option.label;
return acc;
}, {});
+23
View File
@@ -0,0 +1,23 @@
export const DEFAULT_SIDEBAR_WIDTH = 320;
export const DEFAULT_DETAIL_WIDTH = 420;
export const MINIMAL_FREE_RATIO = 1 / 3;
export const SIDEBAR_SOLO_THRESHOLD = 1 / 2;
export const MINIMUM_MAIN_CONTENT_WIDTH = 160;
export type PanelKey = 'sidebar' | 'detail';
export const PANEL_LIMITS: Record<PanelKey, { maxRatio: number; minPx: number }> = {
sidebar: {
maxRatio: 1 / 3,
minPx: 280,
},
detail: {
maxRatio: 2 / 3,
minPx: 320,
},
};
export const PANEL_STORAGE_KEYS: Record<PanelKey, string> = {
sidebar: 'papercrate_sidebar_width',
detail: 'papercrate_detail_width',
};
+32
View File
@@ -0,0 +1,32 @@
export const RERENDER_DELTA = 48;
export const MAX_PIXEL_RATIO = 2;
export const FAST_SCROLL_VELOCITY_THRESHOLD = 0.8;
export const FAST_SCROLL_DWELL_THRESHOLD_MS = 120;
export const SCROLL_VELOCITY_MIN_DELTA = 0.05;
export const AUDIO_EXTENSIONS = new Set([
'aac',
'aiff',
'flac',
'm4a',
'mp3',
'ogg',
'oga',
'opus',
'wav',
'weba',
]);
export const VIDEO_EXTENSIONS = new Set([
'avi',
'mkv',
'mov',
'mp4',
'm4v',
'webm',
'wmv',
]);
export const DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO = 0.4;
export const PORTRAIT_RATIO_STYLE_ID = 'document-viewer-portrait-ratio-style';
export const MIN_STACKED_BREAKPOINT = 480;
+28
View File
@@ -0,0 +1,28 @@
import type { SettingsSectionConfig } from '../settings/SettingsModal';
import ApiTokensSection from '../settings/sections/ApiTokensSection';
import CapabilitySetsSection from '../settings/sections/CapabilitySetsSection';
import PasskeysSection from '../settings/sections/PasskeysSection';
export const PASSKEYS_SECTION: SettingsSectionConfig = {
id: 'passkeys',
label: 'Passkeys',
component: PasskeysSection,
};
export const API_TOKENS_SECTION: SettingsSectionConfig = {
id: 'apiTokens',
label: 'API tokens',
component: ApiTokensSection,
};
export const CAPABILITY_SETS_SECTION: SettingsSectionConfig = {
id: 'capabilitySets',
label: 'Capability sets',
component: CapabilitySetsSection,
};
export const DEFAULT_SETTINGS_SECTIONS = [
PASSKEYS_SECTION,
API_TOKENS_SECTION,
CAPABILITY_SETS_SECTION,
];
+13
View File
@@ -0,0 +1,13 @@
export const SIDEBAR_COLLAPSE_STORAGE_KEY = 'papercrate_sidebar_collapsed';
export const THEME_STORAGE_KEY = 'papercrate_theme_settings';
export const DEFAULT_NEUTRAL_HUE = 29;
export const DEFAULT_NEUTRAL_CHROMA = 0.44;
export const DEFAULT_NEUTRAL_CONTRAST = 0.56;
export const DEFAULT_THEME_MODE = 'system';
export const THEME_MODES = ['system', 'light', 'dark'];
export const THEME_MODE_LABELS = {
system: 'System default',
light: 'Light',
dark: 'Dark',
};
export const DARK_MODE_MEDIA_QUERY = '(prefers-color-scheme: dark)';
+7
View File
@@ -0,0 +1,7 @@
export const NBSP = String.fromCharCode(160);
export const DEFAULT_VIEWPORT_MARGIN = 8;
export const ELLIPSIS = { id: '__breadcrumbs_ellipsis__', label: '…', onClick: null, raw: null } as const;
export const WIDTH_TOLERANCE = 1;
export const WIDTH_BUFFER_RATIO = 0.95;
export const WIDTH_CHANGE_TOLERANCE = 0.25;
+12
View File
@@ -0,0 +1,12 @@
export const DEFAULT_FOLDER_NAME = 'Documents';
export const DEFAULT_SORT_FIELD = 'title';
export const DEFAULT_SORT_DIRECTION = 'asc';
export const SORT_FIELD_VALUES = ['title', 'issued_at', 'created_at', 'updated_at'];
export const TAG_FILTER_UNTAGGED = '__UNTAGGED__';
export const VIEW_MODE_STORAGE_KEY = 'papercrate_view_mode';
export const SORT_FIELD_STORAGE_KEY = 'papercrate_sort_field';
export const SORT_DIRECTION_STORAGE_KEY = 'papercrate_sort_direction';
export const INCLUDE_DESCENDANTS_STORAGE_KEY = 'papercrate_include_descendants';
export const ROOT_FOLDER_LABEL = DEFAULT_FOLDER_NAME;
+1 -2
View File
@@ -25,6 +25,7 @@ import useDeskPointer from './pointer/useDeskPointer';
import useDeskTagInteractions from './tags/useDeskTagInteractions';
import DesktopDocumentCard from './DesktopDocumentCard';
import usePreviewMetadata from './hooks/usePreviewMetadata';
import { DEBUG_DRAG, DEBUG_FOCUS } from '../constants/desktop';
import '../styles/workspace/workspace-layout.css';
import '../styles/workspace/workspace-items.css';
import '../styles/workspace/workspace-cards.css';
@@ -186,8 +187,6 @@ interface DesktopWorkspaceViewProps {
markLayoutDirty: () => void;
}
const DEBUG_DRAG = false;
const DEBUG_FOCUS = false;
const defaultGetDocumentAsset: GetAsset = () => null;
const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
+1 -4
View File
@@ -1,10 +1,7 @@
import type { DocumentId } from '../types/identifiers';
import { DB_NAME, DB_VERSION, LAYOUT_STORE } from '../constants/desktop';
type TenantId = import('../types/identifiers').TenantId;
const DB_NAME = 'papercrate_desk';
const DB_VERSION = 1;
const LAYOUT_STORE = 'layouts';
const currentDbPromise: { value: Promise<IDBDatabase | null> | null } = { value: null };
const openDatabase = (): Promise<IDBDatabase> => {
+8 -18
View File
@@ -1,28 +1,18 @@
import { safeInvoke } from '../events';
import type { DocumentId } from '../../types/identifiers';
import {
CLICK_ACTIONS,
DRAG_ACTIONS,
LONG_PRESS_DURATION_MS,
POINTER_DRAG_THRESHOLD_SQUARED,
STACK_HIT_EPSILON,
} from '../../constants/desktop';
export const CLICK_ACTIONS = {
selectSingle: 'selectSingle',
openDetail: 'openDetail',
addCard: 'addCard',
addStack: 'addStack',
none: 'none',
} as const;
export const DRAG_ACTIONS = {
dragSelectSingle: 'dragSelectSingle',
dragSelection: 'dragSelection',
dragSelectStack: 'dragSelectStack',
none: 'none',
} as const;
export { CLICK_ACTIONS, DRAG_ACTIONS, STACK_HIT_EPSILON, POINTER_DRAG_THRESHOLD_SQUARED, LONG_PRESS_DURATION_MS };
export type ClickAction = (typeof CLICK_ACTIONS)[keyof typeof CLICK_ACTIONS];
export type DragAction = (typeof DRAG_ACTIONS)[keyof typeof DRAG_ACTIONS];
export const STACK_HIT_EPSILON = 4;
export const POINTER_DRAG_THRESHOLD_SQUARED = 16;
export const LONG_PRESS_DURATION_MS = 450;
export const withinThreshold = (dx: number, dy: number, thresholdSquared: number): boolean => (dx * dx + dy * dy) <= thresholdSquared;
interface PointerIntentArgs {
@@ -9,8 +9,7 @@ import {
parseTagTransferPayload,
writeTagTransferData,
} from '../../documents/tagTransfer';
const TAG_REMOVE_DISTANCE = 160;
import { TAG_REMOVE_DISTANCE } from '../../constants/desktop';
const createDragPreview = (node, clientX, clientY) => {
if (!(node instanceof HTMLElement)) {
+1 -4
View File
@@ -19,6 +19,7 @@ import {
applyDomTransform,
type WorkspaceEngine,
} from './workspaceEngine';
import { DRAG_HYSTERESIS_SQUARED, EDGE_COLLISION_THRESHOLD } from '../constants/desktop';
import type { DocumentId, Identifier } from '../types/identifiers';
interface DocumentLike {
@@ -168,10 +169,6 @@ interface DragTapMetadata {
docTitle: string;
}
const DRAG_HYSTERESIS_PX = 4;
const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX;
const EDGE_COLLISION_THRESHOLD = 0.5;
const getDocumentPageCount = (doc?: DocumentLike | null): number | null => {
const raw = doc?.current_version?.metadata?.page_count ?? doc?.metadata?.page_count;
if (raw == null) {
+36 -18
View File
@@ -1,8 +1,44 @@
import { clamp, formatTransform } from '../utils/math';
import { fetchLayoutRecords, upsertLayoutRecords } from './db';
import {
ANGULAR_DAMPING,
CARD_BASE_WEIGHT_GRAMS,
CARD_PAGE_WEIGHT_GRAMS,
DEFAULT_Z_START,
DESK_CANVAS_PADDING,
DESK_CARD_MAX,
DESK_CARD_MIN,
DESK_DEFAULT_CANVAS_HEIGHT,
DESK_DEFAULT_CANVAS_WIDTH,
DESK_ROTATION_RANGE,
MAX_ANGULAR_VELOCITY,
MAX_DYNAMIC_ROTATION,
MAX_TIMESTEP,
MIN_TIMESTEP,
SETTLE_ANGULAR_VELOCITY,
TORQUE_TO_ACCELERATION,
} from '../constants/desktop';
import type { DocumentId } from '../types/identifiers';
type TenantId = import('../types/identifiers').TenantId;
export {
ANGULAR_DAMPING,
CARD_BASE_WEIGHT_GRAMS,
CARD_PAGE_WEIGHT_GRAMS,
DESK_CANVAS_PADDING,
DESK_CARD_MAX,
DESK_CARD_MIN,
DESK_DEFAULT_CANVAS_HEIGHT,
DESK_DEFAULT_CANVAS_WIDTH,
DESK_ROTATION_RANGE,
MAX_ANGULAR_VELOCITY,
MAX_DYNAMIC_ROTATION,
MAX_TIMESTEP,
MIN_TIMESTEP,
SETTLE_ANGULAR_VELOCITY,
TORQUE_TO_ACCELERATION,
};
interface Point {
x: number;
y: number;
@@ -128,24 +164,6 @@ type UseSyncExternalStoreHook = <State>(
getServerSnapshot: () => State,
) => State;
export const DESK_CANVAS_PADDING = 24;
export const DESK_ROTATION_RANGE = 7;
export const DESK_DEFAULT_CANVAS_WIDTH = 1024;
export const DESK_DEFAULT_CANVAS_HEIGHT = 680;
export const DESK_CARD_MIN = 240;
export const DESK_CARD_MAX = 340;
export const CARD_PAGE_WEIGHT_GRAMS = 5;
export const CARD_BASE_WEIGHT_GRAMS = 30;
const DEFAULT_Z_START = 10;
export const MIN_TIMESTEP = 1 / 120;
export const MAX_TIMESTEP = 1 / 20;
export const MAX_DYNAMIC_ROTATION = 7;
export const MAX_ANGULAR_VELOCITY = 210;
export const ANGULAR_DAMPING = 10;
export const TORQUE_TO_ACCELERATION = 0.007;
export const SETTLE_ANGULAR_VELOCITY = 0.45;
const getMassScale = (massGrams?: number): number => {
if (!Number.isFinite(massGrams) || Number(massGrams) <= 0) {
return 1;
@@ -1,6 +1,5 @@
import React from 'react';
const NBSP = String.fromCharCode(160);
import { NBSP } from '../constants/ui';
export interface CorrespondentLinkEntry {
id?: string | null;
@@ -5,6 +5,7 @@ import {
resolveDocumentAssetUrl,
resolveAssetUrl,
} from '../asset_manager';
import { DEFAULT_THUMBNAIL_SIZE } from '../constants/documents';
import type {
DocumentLike as AssetManagerDocumentLike,
AssetLike as AssetManagerAssetLike,
@@ -12,8 +13,6 @@ import type {
GetAsset as AssetManagerGetAsset,
} from '../asset_manager';
const DEFAULT_THUMBNAIL_SIZE = 48;
// Detect when an element becomes visible within a scroll container so we can delay loading.
const useLazyVisibility = (
rootRef: MutableRefObject<Element | null> | null,
@@ -12,10 +12,9 @@ import SelectionAssignmentMenu, { SelectionAssignmentMenuItem } from './Selectio
import SelectionSummary from './SelectionSummary';
import { useAppState } from '../app/appState';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
import { DEFAULT_FOLDER_NAME } from '../constants/workspace';
import type { DocumentId } from '../types/identifiers';
const ROOT_FOLDER_LABEL = 'Documents';
type NullableDocumentId = DocumentId | null;
type SelectedIdList = NullableDocumentId[] | null;
@@ -128,11 +127,11 @@ const buildFolderTreeOptions = (tree?: FolderTreeNode[] | null): SelectionAssign
});
};
traverse(Array.isArray(tree) ? tree : [], [ROOT_FOLDER_LABEL]);
traverse(Array.isArray(tree) ? tree : [], [DEFAULT_FOLDER_NAME]);
entries.sort((a, b) => (a.label || '').localeCompare(b.label || '', undefined, { sensitivity: 'base' }));
return [{ id: 'root', label: ROOT_FOLDER_LABEL, state: 'none', payload: { id: 'root' } }, ...entries];
return [{ id: 'root', label: DEFAULT_FOLDER_NAME, state: 'none', payload: { id: 'root' } }, ...entries];
};
const buildTagAssignments = (
@@ -18,10 +18,9 @@ import DocumentsPanelHeader, {
import { SelectionFloatingPanel } from '../SelectionFloatingActions';
import { createDocumentsTableHeaderActions } from './DocumentsToolbar';
import { useDocumentsFilter } from '../context/DocumentsFilterContext';
import { DEFAULT_GRID_ICON_SIZE } from '../../constants/documents';
import type { Identifier } from '../../types/identifiers';
const DEFAULT_GRID_ICON_SIZE = 144;
const EntryType = {
folder: 'folder',
document: 'document',
@@ -1,19 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import { SORT_LABEL_LOOKUP, SORT_OPTIONS } from '../../constants/documents';
import QuickAddMenu from '../../ui/QuickAddMenu';
const SORT_OPTIONS = [
{ value: 'title', label: 'Title' },
{ value: 'issued_at', label: 'Issued date' },
{ value: 'created_at', label: 'Added' },
{ value: 'updated_at', label: 'Updated date' },
];
const SORT_LABEL_LOOKUP = SORT_OPTIONS.reduce((acc, option) => {
const next = acc;
next[option.value] = option.label;
return next;
}, {});
interface SortFieldQuickMenuProps {
sortField: string;
onChange?: (value: string) => void;
+1 -3
View File
@@ -1,7 +1,5 @@
import type { DocumentId, TagId } from '../types/identifiers';
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
const TAG_TEXT_MIME_TYPE = 'text/plain';
import { TAG_MIME_TYPES, TAG_TEXT_MIME_TYPE } from '../constants/documents';
interface TagPayload {
id: TagId;
+1 -23
View File
@@ -3,6 +3,7 @@ import type { JSX } from 'react';
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
import { DownloadIcon } from '../ui/icons';
import PdfViewer from './PdfViewer';
import { AUDIO_EXTENSIONS, VIDEO_EXTENSIONS } from '../constants/preview';
interface DocumentLike {
id?: string;
@@ -48,29 +49,6 @@ interface DocumentViewerLayoutProps {
layoutMode?: LayoutMode;
}
const AUDIO_EXTENSIONS = new Set([
'aac',
'aiff',
'flac',
'm4a',
'mp3',
'ogg',
'oga',
'opus',
'wav',
'weba',
]);
const VIDEO_EXTENSIONS = new Set([
'avi',
'mkv',
'mov',
'mp4',
'm4v',
'webm',
'wmv',
]);
const getFileExtension = (filename?: string | null) => {
if (!filename) {
return '';
+7 -6
View File
@@ -17,6 +17,13 @@ import type {
PDFDocumentProxy,
RenderTask,
} from 'pdfjs-dist/types/src/display/api';
import {
FAST_SCROLL_DWELL_THRESHOLD_MS,
FAST_SCROLL_VELOCITY_THRESHOLD,
MAX_PIXEL_RATIO,
RERENDER_DELTA,
SCROLL_VELOCITY_MIN_DELTA,
} from '../constants/preview';
GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
@@ -45,12 +52,6 @@ interface RenderQueueRequest {
cancel: () => void;
}
const RERENDER_DELTA = 48;
const MAX_PIXEL_RATIO = 2;
const FAST_SCROLL_VELOCITY_THRESHOLD = 0.8; // px/ms ~ 800px/s
const FAST_SCROLL_DWELL_THRESHOLD_MS = 120;
const SCROLL_VELOCITY_MIN_DELTA = 0.05;
const getAvailableViewportSize = (viewportNode: Element, stackNode: Element) => {
const viewportStyle = window.getComputedStyle(viewportNode);
const viewportPaddingX = parseFloat(viewportStyle.paddingLeft || '0')
+6 -4
View File
@@ -1,7 +1,11 @@
import { MutableRefObject, useLayoutEffect, useState } from 'react';
import {
DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO,
MIN_STACKED_BREAKPOINT,
PORTRAIT_RATIO_STYLE_ID,
} from '../constants/preview';
export const DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO = 0.4;
const PORTRAIT_RATIO_STYLE_ID = 'document-viewer-portrait-ratio-style';
export { DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO };
const ensurePortraitRatioStyle = () => {
const cssValue = String(DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO);
@@ -19,8 +23,6 @@ const ensurePortraitRatioStyle = () => {
}
};
const MIN_STACKED_BREAKPOINT = 480;
const computeStackedLayoutBreakpoint = () => Math.max(window.innerWidth / 2, MIN_STACKED_BREAKPOINT);
export const useViewerLayoutMode = (
@@ -489,10 +489,4 @@ const ApiTokensSection = ({
);
};
export const API_TOKENS_SECTION = {
id: 'apiTokens',
label: 'API tokens',
component: ApiTokensSection,
};
export default ApiTokensSection;
@@ -5,7 +5,6 @@ import React, {
useMemo,
useState,
} from 'react';
import type { SettingsSectionConfig } from '../SettingsModal';
import { IconX } from '../../ui/icons';
import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown';
import type { CapabilitySetId, CapabilityValue } from '../../types/identifiers';
@@ -641,10 +640,4 @@ const CapabilitySetsSection: React.FC<CapabilitySetsSectionProps> = ({
);
};
export const CAPABILITY_SETS_SECTION: SettingsSectionConfig = {
id: 'capabilitySets',
label: 'Capability sets',
component: CapabilitySetsSection,
};
export default CapabilitySetsSection;
@@ -172,10 +172,4 @@ const PasskeysSection = ({
);
};
export const PASSKEYS_SECTION = {
id: 'passkeys',
label: 'Passkeys',
component: PasskeysSection,
};
export default PasskeysSection;
+9 -12
View File
@@ -1,15 +1,12 @@
import PasskeysSection, { PASSKEYS_SECTION } from './PasskeysSection';
import ApiTokensSection, { API_TOKENS_SECTION } from './ApiTokensSection';
import CapabilitySetsSection, { CAPABILITY_SETS_SECTION } from './CapabilitySetsSection';
export const DEFAULT_SETTINGS_SECTIONS = [
PASSKEYS_SECTION,
import PasskeysSection from './PasskeysSection';
import ApiTokensSection from './ApiTokensSection';
import CapabilitySetsSection from './CapabilitySetsSection';
import {
API_TOKENS_SECTION,
CAPABILITY_SETS_SECTION,
];
DEFAULT_SETTINGS_SECTIONS,
PASSKEYS_SECTION,
} from '../../constants/settings';
export {
PasskeysSection,
ApiTokensSection,
CapabilitySetsSection,
};
export { PasskeysSection, ApiTokensSection, CapabilitySetsSection };
export { PASSKEYS_SECTION, API_TOKENS_SECTION, CAPABILITY_SETS_SECTION, DEFAULT_SETTINGS_SECTIONS };
+1 -7
View File
@@ -32,6 +32,7 @@ import { useSidebarContext } from './SidebarContext';
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
import type { Identifier } from '../types/identifiers';
import { THEME_MODE_LABELS, THEME_MODES } from '../constants/sidebar';
interface CommunityLink {
label: string;
@@ -260,13 +261,6 @@ const FolderNode: React.FC<FolderNodeProps> = ({
);
};
const THEME_MODES = ['system', 'light', 'dark'];
const THEME_MODE_LABELS = {
system: 'System',
light: 'Light',
dark: 'Dark',
};
interface FloatingMenuControls {
isOpen: boolean;
toggle: () => void;
+11 -11
View File
@@ -6,6 +6,16 @@ import React, {
useCallback,
useEffect,
} from 'react';
import {
DARK_MODE_MEDIA_QUERY,
DEFAULT_NEUTRAL_CHROMA,
DEFAULT_NEUTRAL_CONTRAST,
DEFAULT_NEUTRAL_HUE,
DEFAULT_THEME_MODE,
SIDEBAR_COLLAPSE_STORAGE_KEY,
THEME_MODES,
THEME_STORAGE_KEY,
} from '../constants/sidebar';
interface SidebarContextValue {
collapsed: boolean;
@@ -25,20 +35,10 @@ interface SidebarContextValue {
themeModes: ThemeMode[];
}
type ThemeMode = 'system' | 'light' | 'dark';
const SIDEBAR_COLLAPSE_STORAGE_KEY = 'papercrate_sidebar_collapsed';
type ThemeMode = (typeof THEME_MODES)[number];
const SidebarContext = createContext<SidebarContextValue | null>(null);
const THEME_STORAGE_KEY = 'papercrate_theme_settings';
const DEFAULT_NEUTRAL_HUE = 29;
const DEFAULT_NEUTRAL_CHROMA = 0.44;
const DEFAULT_NEUTRAL_CONTRAST = 0.56;
const DEFAULT_THEME_MODE = 'system';
const THEME_MODES = ['system', 'light', 'dark'];
const DARK_MODE_MEDIA_QUERY = '(prefers-color-scheme: dark)';
const loadInitialThemeSettings = () => {
const defaults = {
neutralHue: DEFAULT_NEUTRAL_HUE,
+6 -5
View File
@@ -8,6 +8,12 @@ import React, {
import { createPortal } from 'react-dom';
import PropTypes from 'prop-types';
import useFloatingMenu from './useFloatingMenu';
import {
ELLIPSIS,
WIDTH_BUFFER_RATIO,
WIDTH_CHANGE_TOLERANCE,
WIDTH_TOLERANCE,
} from '../constants/ui';
const normalizeEntries = (entries) =>
(Array.isArray(entries) ? entries : [])
@@ -22,11 +28,6 @@ const normalizeEntries = (entries) =>
})
.filter(Boolean);
const ELLIPSIS = { id: '__breadcrumbs_ellipsis__', label: '…', onClick: null, raw: null };
const WIDTH_TOLERANCE = 1;
const WIDTH_BUFFER_RATIO = 0.95;
const WIDTH_CHANGE_TOLERANCE = 0.25;
const BreadcrumbTrail = ({
entries = [],
className = '',
+1 -2
View File
@@ -1,8 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { MutableRefObject, CSSProperties } from 'react';
import { clamp } from '../utils/math';
const DEFAULT_VIEWPORT_MARGIN = 8;
import { DEFAULT_VIEWPORT_MARGIN } from '../constants/ui';
type PositionStrategy = 'fixed' | 'absolute' | (string & {});
+1 -1
View File
@@ -1,4 +1,4 @@
const HEX_COLOR_PATTERN = /^#?([0-9a-fA-F]{6})$/;
import { HEX_COLOR_PATTERN } from '../constants/colors';
const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));